actr_cli/commands/
install.rs

1//! Install Command Implementation
2//!
3//! Implement install flow based on reuse architecture with check-first principle
4
5use anyhow::Result;
6use async_trait::async_trait;
7use clap::Args;
8
9use crate::core::{
10    ActrCliError, Command, CommandContext, CommandResult, ComponentType, DependencySpec,
11    ErrorReporter, InstallResult,
12};
13
14/// Install command
15#[derive(Args, Debug)]
16#[command(
17    about = "Install service dependencies",
18    long_about = "Install service dependencies. You can install specific service packages, or install all dependencies configured in Actr.toml"
19)]
20pub struct InstallCommand {
21    /// List of service packages to install (e.g., actr://user-service@1.0.0/)
22    #[arg(value_name = "PACKAGE")]
23    pub packages: Vec<String>,
24
25    /// Force reinstallation
26    #[arg(long)]
27    pub force: bool,
28
29    /// Force update of all dependencies
30    #[arg(long)]
31    pub force_update: bool,
32
33    /// Skip fingerprint verification
34    #[arg(long)]
35    pub skip_verification: bool,
36}
37
38#[async_trait]
39impl Command for InstallCommand {
40    async fn execute(&self, context: &CommandContext) -> Result<CommandResult> {
41        // Check-First principle: validate project state first
42        if !self.is_actr_project() {
43            return Err(ActrCliError::InvalidProject {
44                message: "Not an Actor-RTC project. Run 'actr init' to initialize.".to_string(),
45            }
46            .into());
47        }
48
49        // Determine installation mode
50        let dependency_specs = if !self.packages.is_empty() {
51            // Mode 1: Add new dependency (npm install <package>)
52            println!("đŸ“Ļ Adding {} new service dependencies", self.packages.len());
53            self.parse_new_packages()?
54        } else {
55            // Mode 2: Install dependencies in config (npm install)
56            if self.force_update {
57                println!("đŸ“Ļ Force updating all service dependencies in configuration");
58            } else {
59                println!("đŸ“Ļ Installing service dependencies in configuration");
60            }
61            self.load_dependencies_from_config(context).await?
62        };
63
64        if dependency_specs.is_empty() {
65            println!("â„šī¸ No dependencies to install");
66            return Ok(CommandResult::Success(
67                "No dependencies to install".to_string(),
68            ));
69        }
70
71        // Get install pipeline (automatically includes ValidationPipeline)
72        let install_pipeline = {
73            let mut container = context.container.lock().unwrap();
74            container.get_install_pipeline()?
75        };
76
77        // Execute check-first install flow
78        match install_pipeline
79            .install_dependencies(&dependency_specs)
80            .await
81        {
82            Ok(install_result) => {
83                self.display_install_success(&install_result);
84                Ok(CommandResult::Install(install_result))
85            }
86            Err(e) => {
87                // User-friendly error display
88                let cli_error = ActrCliError::InstallFailed {
89                    reason: e.to_string(),
90                };
91                eprintln!("{}", ErrorReporter::format_error(&cli_error));
92                Err(e)
93            }
94        }
95    }
96
97    fn required_components(&self) -> Vec<ComponentType> {
98        // Install command needs complete install pipeline components
99        vec![
100            ComponentType::ConfigManager,
101            ComponentType::DependencyResolver,
102            ComponentType::ServiceDiscovery,
103            ComponentType::NetworkValidator,
104            ComponentType::FingerprintValidator,
105            ComponentType::ProtoProcessor,
106            ComponentType::CacheManager,
107        ]
108    }
109
110    fn name(&self) -> &str {
111        "install"
112    }
113
114    fn description(&self) -> &str {
115        "npm-style service-level dependency management (check-first architecture)"
116    }
117}
118
119impl InstallCommand {
120    pub fn new(
121        packages: Vec<String>,
122        force: bool,
123        force_update: bool,
124        skip_verification: bool,
125    ) -> Self {
126        Self {
127            packages,
128            force,
129            force_update,
130            skip_verification,
131        }
132    }
133
134    // Create from clap Args
135    pub fn from_args(args: &InstallCommand) -> Self {
136        InstallCommand {
137            packages: args.packages.clone(),
138            force: args.force,
139            force_update: args.force_update,
140            skip_verification: args.skip_verification,
141        }
142    }
143
144    /// Check if in Actor-RTC project
145    fn is_actr_project(&self) -> bool {
146        std::path::Path::new("Actr.toml").exists()
147    }
148
149    /// Parse new package specs
150    fn parse_new_packages(&self) -> Result<Vec<DependencySpec>> {
151        let mut specs = Vec::new();
152
153        for package_spec in &self.packages {
154            let spec = self.parse_package_spec(package_spec)?;
155            specs.push(spec);
156        }
157
158        Ok(specs)
159    }
160
161    /// Parse single package spec
162    fn parse_package_spec(&self, package_spec: &str) -> Result<DependencySpec> {
163        if package_spec.starts_with("actr://") {
164            // Direct actr:// URI
165            self.parse_actr_uri(package_spec)
166        } else if package_spec.contains('@') {
167            // service-name@version format
168            self.parse_versioned_spec(package_spec)
169        } else {
170            // Simple service name
171            self.parse_simple_spec(package_spec)
172        }
173    }
174
175    /// Parse actr:// URI
176    fn parse_actr_uri(&self, uri: &str) -> Result<DependencySpec> {
177        // Simplified URI parsing, actual implementation should be more strict
178        if !uri.starts_with("actr://") {
179            return Err(anyhow::anyhow!("Invalid actr:// URI: {uri}"));
180        }
181
182        let uri_part = &uri[7..]; // Remove "actr://"
183        let service_name = if let Some(pos) = uri_part.find('/') {
184            uri_part[..pos].to_string()
185        } else {
186            uri_part.to_string()
187        };
188
189        // Extract query parameters (simplified version)
190        let (version, fingerprint) = if uri.contains('?') {
191            self.parse_query_params(uri)?
192        } else {
193            (None, None)
194        };
195
196        Ok(DependencySpec {
197            name: service_name,
198            uri: uri.to_string(),
199            version,
200            fingerprint,
201        })
202    }
203
204    /// Parse query parameters
205    fn parse_query_params(&self, uri: &str) -> Result<(Option<String>, Option<String>)> {
206        if let Some(query_start) = uri.find('?') {
207            let query = &uri[query_start + 1..];
208            let mut version = None;
209            let mut fingerprint = None;
210
211            for param in query.split('&') {
212                if let Some((key, value)) = param.split_once('=') {
213                    match key {
214                        "version" => version = Some(value.to_string()),
215                        "fingerprint" => fingerprint = Some(value.to_string()),
216                        _ => {} // Ignore unknown parameters
217                    }
218                }
219            }
220
221            Ok((version, fingerprint))
222        } else {
223            Ok((None, None))
224        }
225    }
226
227    /// Parse versioned spec (service@version)
228    fn parse_versioned_spec(&self, spec: &str) -> Result<DependencySpec> {
229        let parts: Vec<&str> = spec.split('@').collect();
230        if parts.len() != 2 {
231            return Err(anyhow::anyhow!(
232                "Invalid package specification: {spec}. Use 'service-name@version'"
233            ));
234        }
235
236        let service_name = parts[0].to_string();
237        let version = parts[1].to_string();
238        let uri = format!("actr://{service_name}/?version={version}");
239
240        Ok(DependencySpec {
241            name: service_name,
242            uri,
243            version: Some(version),
244            fingerprint: None,
245        })
246    }
247
248    /// Parse simple spec (service-name)
249    fn parse_simple_spec(&self, spec: &str) -> Result<DependencySpec> {
250        let service_name = spec.to_string();
251        let uri = format!("actr://{service_name}/");
252
253        Ok(DependencySpec {
254            name: service_name,
255            uri,
256            version: None,
257            fingerprint: None,
258        })
259    }
260
261    /// Load dependencies from config file
262    async fn load_dependencies_from_config(
263        &self,
264        context: &CommandContext,
265    ) -> Result<Vec<DependencySpec>> {
266        let config_manager = {
267            let container = context.container.lock().unwrap();
268            container.get_config_manager()?
269        };
270        let config = config_manager
271            .load_config(
272                config_manager
273                    .get_project_root()
274                    .join("Actr.toml")
275                    .as_path(),
276            )
277            .await?;
278
279        let mut specs = Vec::new();
280
281        for dependency in &config.dependencies {
282            let uri = format!(
283                "actr://{}:{}+{}@v1/",
284                dependency.realm.realm_id,
285                dependency.actr_type.manufacturer,
286                dependency.actr_type.name
287            );
288            specs.push(DependencySpec {
289                name: dependency.alias.clone(),
290                uri,
291                version: None,
292                fingerprint: dependency.fingerprint.clone(),
293            });
294        }
295
296        Ok(specs)
297    }
298
299    /// Display install success information
300    fn display_install_success(&self, result: &InstallResult) {
301        println!();
302        println!("✅ Installation successful!");
303        println!(
304            "   đŸ“Ļ Installed dependencies: {}",
305            result.installed_dependencies.len()
306        );
307        println!("   đŸ—‚ī¸  Cache updates: {}", result.cache_updates);
308
309        if result.updated_config {
310            println!("   📝 Configuration file updated");
311        }
312
313        if result.updated_lock_file {
314            println!("   🔒 Lock file updated");
315        }
316
317        if !result.warnings.is_empty() {
318            println!();
319            println!("âš ī¸  Warnings:");
320            for warning in &result.warnings {
321                println!("   â€ĸ {warning}");
322            }
323        }
324
325        println!();
326        println!("💡 Tip: Run 'actr gen' to generate the latest code");
327    }
328}
329
330impl Default for InstallCommand {
331    fn default() -> Self {
332        Self::new(Vec::new(), false, false, false)
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    #[test]
341    fn test_parse_simple_spec() {
342        let cmd = InstallCommand::default();
343        let spec = cmd.parse_simple_spec("user-service").unwrap();
344
345        assert_eq!(spec.name, "user-service");
346        assert_eq!(spec.uri, "actr://user-service/");
347        assert_eq!(spec.version, None);
348        assert_eq!(spec.fingerprint, None);
349    }
350
351    #[test]
352    fn test_parse_versioned_spec() {
353        let cmd = InstallCommand::default();
354        let spec = cmd.parse_versioned_spec("user-service@1.2.0").unwrap();
355
356        assert_eq!(spec.name, "user-service");
357        assert_eq!(spec.uri, "actr://user-service/?version=1.2.0");
358        assert_eq!(spec.version, Some("1.2.0".to_string()));
359        assert_eq!(spec.fingerprint, None);
360    }
361
362    #[test]
363    fn test_parse_actr_uri_simple() {
364        let cmd = InstallCommand::default();
365        let spec = cmd.parse_actr_uri("actr://user-service/").unwrap();
366
367        assert_eq!(spec.name, "user-service");
368        assert_eq!(spec.uri, "actr://user-service/");
369        assert_eq!(spec.version, None);
370        assert_eq!(spec.fingerprint, None);
371    }
372
373    #[test]
374    fn test_parse_actr_uri_with_params() {
375        let cmd = InstallCommand::default();
376        let spec = cmd
377            .parse_actr_uri("actr://user-service/?version=1.2.0&fingerprint=sha256:abc123")
378            .unwrap();
379
380        assert_eq!(spec.name, "user-service");
381        assert_eq!(
382            spec.uri,
383            "actr://user-service/?version=1.2.0&fingerprint=sha256:abc123"
384        );
385        assert_eq!(spec.version, Some("1.2.0".to_string()));
386        assert_eq!(spec.fingerprint, Some("sha256:abc123".to_string()));
387    }
388}