Skip to main content

actr_cli/commands/
install.rs

1//! Install Command Implementation
2//!
3//! Implement install flow based on reuse architecture with check-first principle
4
5use crate::core::{
6    ActrCliError, Command, CommandContext, CommandResult, ComponentType, DependencySpec,
7    ErrorReporter, InstallResult,
8};
9use actr_config::LockFile;
10use actr_protocol::{ActrType, ActrTypeExt};
11use actr_version::{CompatibilityLevel, Fingerprint, ProtoFile, ServiceCompatibility};
12use anyhow::Result;
13use async_trait::async_trait;
14use clap::Args;
15
16/// Install command
17#[derive(Args, Debug)]
18#[command(
19    about = "Install service dependencies",
20    long_about = "Install service dependencies. You can install specific service packages, or install all dependencies configured in Actr.toml.\n\nExamples:\n  actr install                          # Install all dependencies from Actr.toml\n  actr install user-service             # Install a service by name\n  actr install my-alias --actr-type acme+EchoService  # Install with alias and explicit actr_type"
21)]
22pub struct InstallCommand {
23    /// Package name or alias (when used with --actr-type, this becomes the alias)
24    #[arg(value_name = "PACKAGE")]
25    pub packages: Vec<String>,
26
27    /// Actor type for the dependency (format: manufacturer+name, e.g., acme+EchoService).
28    /// When specified, the PACKAGE argument is treated as an alias.
29    #[arg(long, value_name = "TYPE")]
30    pub actr_type: Option<String>,
31
32    /// Service fingerprint for version pinning
33    #[arg(long, value_name = "FINGERPRINT")]
34    pub fingerprint: Option<String>,
35
36    /// Force reinstallation
37    #[arg(long)]
38    pub force: bool,
39
40    /// Force update of all dependencies
41    #[arg(long)]
42    pub force_update: bool,
43
44    /// Skip fingerprint verification
45    #[arg(long)]
46    pub skip_verification: bool,
47}
48
49/// Installation mode
50#[derive(Debug, Clone)]
51pub enum InstallMode {
52    /// Mode 1: Add new dependency (npm install <package>)
53    /// - Pull remote proto to protos/ folder
54    /// - Modify Actr.toml (add dependency)
55    /// - Update Actr.lock.toml
56    AddNewPackage { packages: Vec<String> },
57
58    /// Mode 1b: Add dependency with explicit alias and actr_type (actr install <alias> --actr-type <type>)
59    /// - Discover service by actr_type
60    /// - Use first argument as alias
61    /// - Modify Actr.toml (add dependency with alias)
62    /// - Update Actr.lock.toml
63    AddWithAlias {
64        alias: String,
65        actr_type: ActrType,
66        fingerprint: Option<String>,
67    },
68
69    /// Mode 2: Install dependencies in config (npm install)
70    /// - Do NOT modify Actr.toml
71    /// - Use lock file versions if available
72    /// - Only update Actr.lock.toml
73    InstallFromConfig { force_update: bool },
74}
75
76#[async_trait]
77impl Command for InstallCommand {
78    async fn execute(&self, context: &CommandContext) -> Result<CommandResult> {
79        // Check-First principle: validate project state first
80        if !self.is_actr_project() {
81            return Err(ActrCliError::InvalidProject {
82                message: "Not an Actor-RTC project. Run 'actr init' to initialize.".to_string(),
83            }
84            .into());
85        }
86
87        // Determine installation mode
88        let mode = if let Some(actr_type_str) = &self.actr_type {
89            // Mode 1b: Install with explicit alias and actr_type
90            if self.packages.is_empty() {
91                return Err(ActrCliError::InvalidArgument {
92                    message:
93                        "When using --actr-type, you must provide an alias as the first argument"
94                            .to_string(),
95                }
96                .into());
97            }
98            let alias = self.packages[0].clone();
99            let actr_type = ActrType::from_string_repr(actr_type_str).map_err(|_| {
100                ActrCliError::InvalidArgument {
101                    message: format!(
102                        "Invalid actr_type format '{}'. Expected format: manufacturer+name (e.g., acme+EchoService)",
103                        actr_type_str
104                    ),
105                }
106            })?;
107            InstallMode::AddWithAlias {
108                alias,
109                actr_type,
110                fingerprint: self.fingerprint.clone(),
111            }
112        } else if !self.packages.is_empty() {
113            if self.fingerprint.is_some() {
114                return Err(ActrCliError::InvalidArgument {
115                    message: "Using --fingerprint requires specifying --actr-type explicitly.
116Use: actr install <ALIAS> --actr-type <TYPE> --fingerprint <FINGERPRINT>"
117                        .to_string(),
118                }
119                .into());
120            }
121
122            InstallMode::AddNewPackage {
123                packages: self.packages.clone(),
124            }
125        } else {
126            if self.fingerprint.is_some() {
127                return Err(ActrCliError::InvalidArgument {
128                    message: "Using --fingerprint requires specifying an alias and --actr-type.
129Use: actr install <ALIAS> --actr-type <TYPE> --fingerprint <FINGERPRINT>"
130                        .to_string(),
131                }
132                .into());
133            }
134
135            InstallMode::InstallFromConfig {
136                force_update: self.force_update,
137            }
138        };
139
140        // Execute based on mode
141        match mode {
142            InstallMode::AddNewPackage { ref packages } => {
143                self.execute_add_package(context, packages).await
144            }
145            InstallMode::AddWithAlias {
146                ref alias,
147                ref actr_type,
148                ref fingerprint,
149            } => {
150                self.execute_add_with_alias(context, alias, actr_type, fingerprint.as_deref())
151                    .await
152            }
153            InstallMode::InstallFromConfig { force_update } => {
154                self.execute_install_from_config(context, force_update)
155                    .await
156            }
157        }
158    }
159
160    fn required_components(&self) -> Vec<ComponentType> {
161        // Install command needs complete install pipeline components
162        vec![
163            ComponentType::ConfigManager,
164            ComponentType::DependencyResolver,
165            ComponentType::ServiceDiscovery,
166            ComponentType::NetworkValidator,
167            ComponentType::FingerprintValidator,
168            ComponentType::ProtoProcessor,
169            ComponentType::CacheManager,
170        ]
171    }
172
173    fn name(&self) -> &str {
174        "install"
175    }
176
177    fn description(&self) -> &str {
178        "npm-style service-level dependency management (check-first architecture)"
179    }
180}
181
182impl InstallCommand {
183    pub fn new(
184        packages: Vec<String>,
185        actr_type: Option<String>,
186        fingerprint: Option<String>,
187        force: bool,
188        force_update: bool,
189        skip_verification: bool,
190    ) -> Self {
191        Self {
192            packages,
193            actr_type,
194            fingerprint,
195            force,
196            force_update,
197            skip_verification,
198        }
199    }
200
201    // Create from clap Args
202    pub fn from_args(args: &InstallCommand) -> Self {
203        InstallCommand {
204            packages: args.packages.clone(),
205            actr_type: args.actr_type.clone(),
206            fingerprint: args.fingerprint.clone(),
207            force: args.force,
208            force_update: args.force_update,
209            skip_verification: args.skip_verification,
210        }
211    }
212
213    /// Check if in Actor-RTC project
214    fn is_actr_project(&self) -> bool {
215        std::path::Path::new("Actr.toml").exists()
216    }
217
218    /// Execute Mode 1: Add new package (actr install <package>)
219    /// - Pull remote proto to protos/ folder
220    /// - Modify Actr.toml (add dependency)
221    /// - Update Actr.lock.toml
222    async fn execute_add_package(
223        &self,
224        context: &CommandContext,
225        packages: &[String],
226    ) -> Result<CommandResult> {
227        println!("actr install {}", packages.join(" "));
228
229        let install_pipeline = {
230            let mut container = context.container.lock().unwrap();
231            container.get_install_pipeline()?
232        };
233
234        let mut resolved_specs = Vec::new();
235
236        println!("🔍 Phase 1: Complete Validation");
237        for package in packages {
238            // Phase 1: Check-First validation
239            println!("  ├─ 📋 Parsing dependency spec: {}", package);
240
241            // Discover service details
242            // The service_details in install_pipeline is designed to fetch specific service details directly
243            // However, we want to support interactive selection if multiple services match (or same service with multiple versions)
244            // But get_service_details currently only returns one service or error
245            // To support interactive selection, we need to use discover_services first
246
247            let service_discovery = install_pipeline.validation_pipeline().service_discovery();
248            let ui = context.container.lock().unwrap().get_user_interface()?;
249
250            // First, try to discover services matching the name
251            // We create a filter for the name
252            let filter = crate::core::ServiceFilter {
253                name_pattern: Some(package.clone()),
254                version_range: None,
255                tags: None,
256            };
257
258            let services = service_discovery.discover_services(Some(&filter)).await?;
259
260            let selected_service = if services.is_empty() {
261                // If no services found by discovery, fall back to get_service_details which might have different lookup logic
262                // or just error out with the nice message we added earlier
263                match service_discovery.get_service_details(package).await {
264                    Ok(details) => details.info,
265                    Err(_) => {
266                        println!("  └─ âš ī¸  Service not found: {}", package);
267                        println!();
268                        println!(
269                            "💡 Tip: If you want to specify a fingerprint, use the full command:"
270                        );
271                        println!(
272                            "      actr install {} --actr-type <TYPE> --fingerprint <FINGERPRINT>",
273                            package
274                        );
275                        println!();
276                        return Err(anyhow::anyhow!("Service not found"));
277                    }
278                }
279            } else if services.len() == 1 {
280                // Only one service found, auto-select
281                let service = services[0].clone();
282                println!("  ├─ 🔍 Automatically selected service: {}", service.name);
283                service
284            } else {
285                // Multiple services found, ask user to select
286                println!(
287                    "  ├─ 🔍 Found {} services matching '{}'",
288                    services.len(),
289                    package
290                );
291
292                // Format items for selection
293                let items: Vec<String> = services
294                    .iter()
295                    .map(|s| {
296                        format!(
297                            "{} ({}) - {}",
298                            s.name,
299                            s.fingerprint.chars().take(8).collect::<String>(),
300                            s.actr_type.to_string_repr()
301                        )
302                    })
303                    .collect();
304
305                let selection_index = ui
306                    .select_from_list(&items, "Please select a service to install")
307                    .await?;
308
309                services[selection_index].clone()
310            };
311
312            let service_details = service_discovery
313                .get_service_details(&selected_service.name)
314                .await?;
315
316            println!(
317                "  ├─ 🔍 Service discovery: fingerprint {}",
318                service_details.info.fingerprint
319            );
320
321            // Connectivity check - Skipped for install as we only need metadata
322            // let connectivity = install_pipeline
323            //     .validation_pipeline()
324            //     .network_validator()
325            //     .check_connectivity(package, &NetworkCheckOptions::default())
326            //     .await?;
327
328            println!("  ├─ 🌐 Network connectivity test (Skipped) ✅");
329
330            // Fingerprint check
331            println!("  ├─ 🔐 Fingerprint integrity verification ✅");
332
333            // Create dependency spec with resolved info
334            let resolved_spec = DependencySpec {
335                alias: package.clone(),
336                actr_type: Some(service_details.info.actr_type.clone()),
337                name: package.clone(),
338                fingerprint: Some(service_details.info.fingerprint.clone()),
339            };
340            resolved_specs.push(resolved_spec);
341            println!("  └─ ✅ Added to installation plan");
342            println!();
343        }
344
345        if resolved_specs.is_empty() {
346            return Ok(CommandResult::Success("No packages to install".to_string()));
347        }
348
349        // Phase 2: Atomic installation
350        println!("📝 Phase 2: Atomic Installation");
351
352        // Execute installation for all packages
353        match install_pipeline.install_dependencies(&resolved_specs).await {
354            Ok(result) => {
355                println!("  ├─ 💾 Backing up current configuration");
356                println!("  ├─ 📝 Updating Actr.toml configuration ✅");
357                println!("  ├─ đŸ“Ļ Caching proto files ✅");
358                println!("  ├─ 🔒 Updating Actr.lock.toml ✅");
359                println!("  └─ ✅ Installation completed");
360                println!();
361                self.display_install_success(&result);
362                Ok(CommandResult::Install(result))
363            }
364            Err(e) => {
365                println!("  └─ 🔄 Restoring backup (due to installation failure)");
366                let cli_error = ActrCliError::InstallFailed {
367                    reason: e.to_string(),
368                };
369                eprintln!("{}", ErrorReporter::format_error(&cli_error));
370                Err(e)
371            }
372        }
373    }
374
375    /// Execute Mode 1b: Add dependency with explicit alias and actr_type
376    /// - Discover service by actr_type
377    /// - Use provided alias
378    /// - Modify Actr.toml (add dependency with alias)
379    /// - Update Actr.lock.toml
380    async fn execute_add_with_alias(
381        &self,
382        context: &CommandContext,
383        alias: &str,
384        actr_type: &ActrType,
385        fingerprint: Option<&str>,
386    ) -> Result<CommandResult> {
387        use actr_protocol::ActrTypeExt;
388
389        println!(
390            "actr install {} --actr-type {}",
391            alias,
392            actr_type.to_string_repr()
393        );
394
395        let install_pipeline = {
396            let mut container = context.container.lock().unwrap();
397            container.get_install_pipeline()?
398        };
399
400        println!("🔍 Phase 1: Complete Validation");
401        println!("  ├─ 📋 Alias: {}", alias);
402        println!("  ├─ đŸˇī¸  Actor Type: {}", actr_type.to_string_repr());
403
404        // Discover service by actr_type
405        let service_discovery = install_pipeline.validation_pipeline().service_discovery();
406
407        // Find service matching the actr_type
408        let services = service_discovery.discover_services(None).await?;
409        let matching_service = services
410            .iter()
411            .find(|s| s.actr_type == *actr_type)
412            .ok_or_else(|| ActrCliError::ServiceNotFound {
413                name: actr_type.to_string_repr(),
414            })?;
415
416        let service_name = matching_service.name.clone();
417        println!("  ├─ 🔍 Service discovered: {}", service_name);
418
419        // Get full service details
420        let service_details = service_discovery.get_service_details(&service_name).await?;
421
422        println!(
423            "  ├─ 🔍 Service fingerprint: {}",
424            service_details.info.fingerprint
425        );
426
427        // Verify fingerprint if provided
428        if let Some(expected_fp) = fingerprint {
429            if service_details.info.fingerprint != expected_fp {
430                println!("  └─ ❌ Fingerprint mismatch");
431                return Err(ActrCliError::FingerprintMismatch {
432                    expected: expected_fp.to_string(),
433                    actual: service_details.info.fingerprint.clone(),
434                }
435                .into());
436            }
437            println!("  ├─ 🔐 Fingerprint verification ✅");
438        }
439
440        // Connectivity check - Skipped for install as we only need metadata
441        // let connectivity = install_pipeline
442        //     .validation_pipeline()
443        //     .network_validator()
444        //     .check_connectivity(&service_name, &NetworkCheckOptions::default())
445        //     .await?;
446
447        println!("  ├─ 🌐 Network connectivity test (Skipped) ✅");
448
449        // Create dependency spec with alias
450        let resolved_spec = DependencySpec {
451            alias: alias.to_string(),
452            actr_type: Some(service_details.info.actr_type.clone()),
453            name: service_name.clone(),
454            fingerprint: Some(
455                fingerprint
456                    .map(|s| s.to_string())
457                    .unwrap_or_else(|| service_details.info.fingerprint.clone()),
458            ),
459        };
460
461        println!("  └─ ✅ Added to installation plan");
462        println!();
463
464        // Phase 2: Atomic installation
465        println!("📝 Phase 2: Atomic Installation");
466
467        // Execute installation
468        match install_pipeline
469            .install_dependencies(&[resolved_spec])
470            .await
471        {
472            Ok(result) => {
473                println!("  ├─ 💾 Backing up current configuration");
474                println!("  ├─ 📝 Updating Actr.toml configuration ✅");
475                println!("  ├─ đŸ“Ļ Caching proto files ✅");
476                println!("  ├─ 🔒 Updating Actr.lock.toml ✅");
477                println!("  └─ ✅ Installation completed");
478                println!();
479                self.display_install_success(&result);
480                Ok(CommandResult::Install(result))
481            }
482            Err(e) => {
483                println!("  └─ 🔄 Restoring backup (due to installation failure)");
484                let cli_error = ActrCliError::InstallFailed {
485                    reason: e.to_string(),
486                };
487                eprintln!("{}", ErrorReporter::format_error(&cli_error));
488                Err(e)
489            }
490        }
491    }
492
493    /// Execute Mode 2: Install from config (actr install)
494    /// - Do NOT modify Actr.toml
495    /// - Use lock file versions if available
496    /// - Check for compatibility conflicts when lock file exists
497    /// - Only update Actr.lock.toml
498    async fn execute_install_from_config(
499        &self,
500        context: &CommandContext,
501        force_update: bool,
502    ) -> Result<CommandResult> {
503        if force_update || self.force {
504            println!("đŸ“Ļ Force updating all service dependencies");
505        } else {
506            println!("đŸ“Ļ Installing service dependencies from config");
507        }
508        println!();
509
510        // Load dependencies from Actr.toml
511        let dependency_specs = self.load_dependencies_from_config(context).await?;
512
513        if dependency_specs.is_empty() {
514            println!("â„šī¸ No dependencies configured, generating empty lock file");
515
516            // Generate empty lock file with metadata
517            let install_pipeline = {
518                let mut container = context.container.lock().unwrap();
519                container.get_install_pipeline()?
520            };
521            let project_root = install_pipeline.config_manager().get_project_root();
522            let lock_file_path = project_root.join("Actr.lock.toml");
523
524            let mut lock_file = LockFile::new();
525            lock_file.update_timestamp();
526            lock_file
527                .save_to_file(&lock_file_path)
528                .map_err(|e| ActrCliError::InstallFailed {
529                    reason: format!("Failed to save lock file: {}", e),
530                })?;
531
532            println!("  └─ 🔒 Generated Actr.lock.toml");
533            return Ok(CommandResult::Success(
534                "Generated empty lock file".to_string(),
535            ));
536        }
537
538        // Check for duplicate actr_type conflicts
539        let conflicts = self.check_actr_type_conflicts(&dependency_specs);
540        if !conflicts.is_empty() {
541            println!("❌ Dependency conflict detected:");
542            for conflict in &conflicts {
543                println!("   â€ĸ {}", conflict);
544            }
545            println!();
546            println!(
547                "💡 Tip: Each actr_type can only be used once. Please use different aliases for different services or remove duplicate dependencies."
548            );
549            return Err(ActrCliError::DependencyConflict {
550                message: format!(
551                    "{} dependency conflict(s) detected. Each actr_type must be unique.",
552                    conflicts.len()
553                ),
554            }
555            .into());
556        }
557
558        println!("🔍 Phase 1: Full Validation");
559        for spec in &dependency_specs {
560            println!("  ├─ 📋 Parsing dependency: {}", spec.alias);
561        }
562
563        // Get install pipeline
564        let install_pipeline = {
565            let mut container = context.container.lock().unwrap();
566            container.get_install_pipeline()?
567        };
568
569        // Check for compatibility conflicts when lock file exists (unless force_update)
570        if !force_update && !self.force {
571            let project_root = install_pipeline.config_manager().get_project_root();
572            let lock_file_path = project_root.join("Actr.lock.toml");
573            if lock_file_path.exists() {
574                println!("  ├─ 🔒 Lock file found, checking compatibility...");
575
576                // Perform compatibility check
577                let conflicts = self
578                    .check_lock_file_compatibility(
579                        &lock_file_path,
580                        &dependency_specs,
581                        &install_pipeline,
582                    )
583                    .await?;
584
585                if !conflicts.is_empty() {
586                    println!("  └─ ❌ Compatibility conflicts detected");
587                    println!();
588                    println!("âš ī¸  Breaking changes detected:");
589                    for conflict in &conflicts {
590                        println!("   â€ĸ {}", conflict);
591                    }
592                    println!();
593                    println!(
594                        "💡 Tip: Use --force-update to override and update to the latest versions"
595                    );
596                    return Err(ActrCliError::CompatibilityConflict {
597                        message: format!(
598                            "{} breaking change(s) detected. Use --force-update to override.",
599                            conflicts.len()
600                        ),
601                    }
602                    .into());
603                }
604                println!("  ├─ ✅ Compatibility check passed");
605            }
606        }
607
608        // Verify fingerprints match registered services (unless --force is used)
609        println!("  ├─ ✅ Verifying fingerprints...");
610        let fingerprint_mismatches = self
611            .verify_fingerprints(&dependency_specs, &install_pipeline)
612            .await?;
613
614        if !fingerprint_mismatches.is_empty() && !self.force {
615            println!("  └─ ❌ Fingerprint mismatch detected");
616            println!();
617            println!("âš ī¸  Fingerprint mismatch:");
618            for mismatch in &fingerprint_mismatches {
619                println!("   â€ĸ {}", mismatch);
620            }
621            println!();
622            println!(
623                "💡 Tip: Use --force to update Actr.toml with the current service fingerprints"
624            );
625            return Err(ActrCliError::FingerprintValidation {
626                message: format!(
627                    "{} fingerprint mismatch(es) detected. Use --force to update.",
628                    fingerprint_mismatches.len()
629                ),
630            }
631            .into());
632        }
633
634        // If --force is used and there are mismatches, update Actr.toml
635        if !fingerprint_mismatches.is_empty() && self.force {
636            println!("  ├─ âš ī¸  Fingerprint mismatch detected, updating Actr.toml...");
637            self.update_config_fingerprints(context, &dependency_specs, &install_pipeline)
638                .await?;
639            println!("  ├─ ✅ Actr.toml updated with current fingerprints");
640
641            // Reload dependency specs with updated fingerprints
642            let dependency_specs = self.load_dependencies_from_config(context).await?;
643
644            println!("  ├─ 🔍 Service discovery (DiscoveryRequest)");
645            println!("  ├─ 🌐 Network connectivity test");
646            println!("  └─ ✅ Installation plan generated");
647            println!();
648
649            // Execute installation with updated specs
650            println!("📝 Phase 2: Atomic Installation");
651            return match install_pipeline
652                .install_dependencies(&dependency_specs)
653                .await
654            {
655                Ok(install_result) => {
656                    println!("  ├─ 📚 Caching proto files ✅");
657                    println!("  ├─ 🔒 Updating Actr.lock.toml ✅");
658                    println!("  └─ ✅ Installation completed");
659                    println!();
660                    println!(
661                        "📝 Note: Actr.toml fingerprints were updated to match current services"
662                    );
663                    self.display_install_success(&install_result);
664                    Ok(CommandResult::Install(install_result))
665                }
666                Err(e) => {
667                    println!("  └─ ❌ Installation failed");
668                    let cli_error = ActrCliError::InstallFailed {
669                        reason: e.to_string(),
670                    };
671                    eprintln!("{}", ErrorReporter::format_error(&cli_error));
672                    Err(e)
673                }
674            };
675        }
676
677        println!("  ├─ ✅ Fingerprint verification passed");
678        println!("  ├─ 🔍 Service discovery (DiscoveryRequest)");
679        println!("  ├─ 🌐 Network connectivity test");
680        println!("  └─ ✅ Installation plan generated");
681        println!();
682
683        // Execute check-first install flow (Mode 2: no config update)
684        println!("📝 Phase 2: Atomic Installation");
685        match install_pipeline
686            .install_dependencies(&dependency_specs)
687            .await
688        {
689            Ok(install_result) => {
690                println!("  ├─ đŸ“Ļ Caching proto files ✅");
691                println!("  ├─ 🔒 Updating Actr.lock.toml ✅");
692                println!("  └─ ✅ Installation completed");
693                println!();
694                self.display_install_success(&install_result);
695                Ok(CommandResult::Install(install_result))
696            }
697            Err(e) => {
698                println!("  └─ ❌ Installation failed");
699                let cli_error = ActrCliError::InstallFailed {
700                    reason: e.to_string(),
701                };
702                eprintln!("{}", ErrorReporter::format_error(&cli_error));
703                Err(e)
704            }
705        }
706    }
707
708    /// Load dependencies from config file
709    async fn load_dependencies_from_config(
710        &self,
711        context: &CommandContext,
712    ) -> Result<Vec<DependencySpec>> {
713        let config_manager = {
714            let container = context.container.lock().unwrap();
715            container.get_config_manager()?
716        };
717        let config = config_manager
718            .load_config(
719                config_manager
720                    .get_project_root()
721                    .join("Actr.toml")
722                    .as_path(),
723            )
724            .await?;
725
726        let specs: Vec<DependencySpec> = config
727            .dependencies
728            .into_iter()
729            .map(|dependency| DependencySpec {
730                alias: dependency.alias,
731                actr_type: dependency.actr_type,
732                name: dependency.name,
733                fingerprint: dependency.fingerprint,
734            })
735            .collect();
736
737        Ok(specs)
738    }
739
740    /// Check for duplicate actr_type conflicts in dependencies
741    fn check_actr_type_conflicts(&self, specs: &[DependencySpec]) -> Vec<String> {
742        use std::collections::HashMap;
743
744        let mut actr_type_map: HashMap<String, Vec<&str>> = HashMap::new();
745        let mut conflicts = Vec::new();
746
747        for spec in specs {
748            if let Some(ref actr_type) = spec.actr_type {
749                let type_str = actr_type.to_string_repr();
750                actr_type_map.entry(type_str).or_default().push(&spec.alias);
751            }
752        }
753
754        for (actr_type, aliases) in actr_type_map {
755            if aliases.len() > 1 {
756                conflicts.push(format!(
757                    "actr_type '{}' is used by multiple dependencies: {}",
758                    actr_type,
759                    aliases.join(", ")
760                ));
761            }
762        }
763
764        conflicts
765    }
766
767    /// Verify that fingerprints in Actr.toml match the currently registered services
768    async fn verify_fingerprints(
769        &self,
770        specs: &[DependencySpec],
771        install_pipeline: &std::sync::Arc<crate::core::InstallPipeline>,
772    ) -> Result<Vec<String>> {
773        let mut mismatches = Vec::new();
774        let service_discovery = install_pipeline.validation_pipeline().service_discovery();
775
776        for spec in specs {
777            // Only check if fingerprint is specified in Actr.toml
778            let expected_fingerprint = match &spec.fingerprint {
779                Some(fp) => fp,
780                None => continue,
781            };
782
783            // Get current service details
784            let current_service = match service_discovery.get_service_details(&spec.name).await {
785                Ok(s) => s,
786                Err(e) => {
787                    mismatches.push(format!(
788                        "{}: Service not found or unavailable ({})",
789                        spec.alias, e
790                    ));
791                    continue;
792                }
793            };
794
795            let current_fingerprint = &current_service.info.fingerprint;
796
797            // Compare fingerprints
798            if expected_fingerprint != current_fingerprint {
799                mismatches.push(format!(
800                    "{}: Expected fingerprint '{}', but service has '{}'",
801                    spec.alias, expected_fingerprint, current_fingerprint
802                ));
803            }
804        }
805
806        Ok(mismatches)
807    }
808
809    /// Update Actr.toml with current service fingerprints
810    async fn update_config_fingerprints(
811        &self,
812        _context: &CommandContext,
813        specs: &[DependencySpec],
814        install_pipeline: &std::sync::Arc<crate::core::InstallPipeline>,
815    ) -> Result<()> {
816        let service_discovery = install_pipeline.validation_pipeline().service_discovery();
817        let config_manager = install_pipeline.config_manager();
818
819        // Update fingerprints for each dependency that has one specified
820        for spec in specs {
821            if spec.fingerprint.is_none() {
822                continue;
823            }
824
825            // Get current service fingerprint
826            let current_service = match service_discovery.get_service_details(&spec.name).await {
827                Ok(s) => s,
828                Err(_) => continue,
829            };
830
831            let old_fingerprint = spec
832                .fingerprint
833                .clone()
834                .unwrap_or_else(|| "none".to_string());
835            let new_fingerprint = current_service.info.fingerprint.clone();
836
837            // Create updated spec with new fingerprint
838            let updated_spec = DependencySpec {
839                alias: spec.alias.clone(),
840                name: spec.name.clone(),
841                actr_type: spec.actr_type.clone(),
842                fingerprint: Some(new_fingerprint.clone()),
843            };
844
845            // Use update_dependency to modify Actr.toml directly
846            config_manager.update_dependency(&updated_spec).await?;
847
848            println!(
849                "   📝 Updated '{}' fingerprint: {} → {}",
850                spec.alias, old_fingerprint, new_fingerprint
851            );
852        }
853
854        Ok(())
855    }
856
857    /// Check compatibility between locked dependencies and currently registered services
858    ///
859    /// This method compares the fingerprints stored in the lock file with the fingerprints
860    /// of the services currently registered on the signaling server. If a service's proto
861    /// definition has breaking changes compared to the locked version, a conflict is reported.
862    async fn check_lock_file_compatibility(
863        &self,
864        lock_file_path: &std::path::Path,
865        dependency_specs: &[DependencySpec],
866        install_pipeline: &std::sync::Arc<crate::core::InstallPipeline>,
867    ) -> Result<Vec<String>> {
868        use actr_protocol::ServiceSpec;
869
870        let mut conflicts = Vec::new();
871
872        // Load lock file
873        let lock_file = match LockFile::from_file(lock_file_path) {
874            Ok(lf) => lf,
875            Err(e) => {
876                tracing::warn!("Failed to parse lock file: {}", e);
877                return Ok(conflicts); // If we can't parse lock file, skip compatibility check
878            }
879        };
880
881        // For each dependency, check if the currently registered service is compatible
882        for spec in dependency_specs {
883            // Find the locked dependency by name
884            let locked_dep = lock_file.dependencies.iter().find(|d| d.name == spec.name);
885
886            let locked_dep = match locked_dep {
887                Some(d) => d,
888                None => {
889                    // Dependency not in lock file, skip (will be newly installed)
890                    tracing::debug!("Dependency '{}' not in lock file, skipping", spec.name);
891                    continue;
892                }
893            };
894
895            let locked_fingerprint = &locked_dep.fingerprint;
896
897            // Get current service details from the registry
898            let service_discovery = install_pipeline.validation_pipeline().service_discovery();
899            let current_service = match service_discovery.get_service_details(&spec.name).await {
900                Ok(s) => s,
901                Err(e) => {
902                    tracing::warn!("Failed to get service details for '{}': {}", spec.name, e);
903                    continue;
904                }
905            };
906
907            let current_fingerprint = &current_service.info.fingerprint;
908
909            // If fingerprints match, no need for deep analysis
910            if locked_fingerprint == current_fingerprint {
911                tracing::debug!(
912                    "Fingerprint match for '{}', no compatibility check needed",
913                    spec.name
914                );
915                continue;
916            }
917
918            // Fingerprints differ - perform deep compatibility analysis using actr-version
919            tracing::info!(
920                "Fingerprint mismatch for '{}': locked={}, current={}",
921                spec.name,
922                locked_fingerprint,
923                current_fingerprint
924            );
925
926            // Build ServiceSpec from locked proto content for comparison
927            // Note: Since lock file only stores metadata (not full proto content),
928            // we need to use semantic fingerprint comparison for compatibility check
929
930            // Convert current service proto files to actr-version ProtoFile format
931            let current_proto_files: Vec<ProtoFile> = current_service
932                .proto_files
933                .iter()
934                .map(|pf| ProtoFile {
935                    name: pf.name.clone(),
936                    content: pf.content.clone(),
937                    path: Some(pf.path.to_string_lossy().to_string()),
938                })
939                .collect();
940
941            // Calculate current service's semantic fingerprint
942            let current_semantic_fp =
943                match Fingerprint::calculate_service_semantic_fingerprint(&current_proto_files) {
944                    Ok(fp) => fp,
945                    Err(e) => {
946                        tracing::warn!(
947                            "Failed to calculate semantic fingerprint for '{}': {}",
948                            spec.name,
949                            e
950                        );
951                        // If we can't calculate fingerprint, report as potential conflict
952                        conflicts.push(format!(
953                            "{}: Unable to verify compatibility (fingerprint calculation failed)",
954                            spec.name
955                        ));
956                        continue;
957                    }
958                };
959
960            // Compare fingerprints using semantic analysis
961            // The locked fingerprint should be a service_semantic fingerprint
962            let locked_semantic = if locked_fingerprint.starts_with("service_semantic:") {
963                locked_fingerprint
964                    .strip_prefix("service_semantic:")
965                    .unwrap_or(locked_fingerprint)
966            } else {
967                locked_fingerprint.as_str()
968            };
969
970            if current_semantic_fp != locked_semantic {
971                // Semantic fingerprints differ - this indicates breaking changes
972                // Build ServiceSpec structures for detailed comparison
973                let locked_spec = ServiceSpec {
974                    name: spec.name.clone(),
975                    description: locked_dep.description.clone(),
976                    fingerprint: locked_fingerprint.clone(),
977                    protobufs: locked_dep
978                        .files
979                        .iter()
980                        .map(|pf| actr_protocol::service_spec::Protobuf {
981                            package: pf.path.clone(),
982                            content: String::new(), // Lock file doesn't store content
983                            fingerprint: pf.fingerprint.clone(),
984                        })
985                        .collect(),
986                    published_at: locked_dep.published_at,
987                    tags: locked_dep.tags.clone(),
988                };
989
990                let current_spec = ServiceSpec {
991                    name: spec.name.clone(),
992                    description: Some(current_service.info.description.clone().unwrap_or_default()),
993                    fingerprint: format!("service_semantic:{}", current_semantic_fp),
994                    protobufs: current_proto_files
995                        .iter()
996                        .map(|pf| actr_protocol::service_spec::Protobuf {
997                            package: pf.name.clone(),
998                            content: pf.content.clone(),
999                            fingerprint: String::new(),
1000                        })
1001                        .collect(),
1002                    published_at: current_service.info.published_at,
1003                    tags: current_service.info.tags.clone(),
1004                };
1005
1006                // Attempt to analyze compatibility
1007                match ServiceCompatibility::analyze_compatibility(&locked_spec, &current_spec) {
1008                    Ok(analysis) => {
1009                        match analysis.level {
1010                            CompatibilityLevel::BreakingChanges => {
1011                                let change_summary = analysis
1012                                    .breaking_changes
1013                                    .iter()
1014                                    .map(|c| c.message.clone())
1015                                    .collect::<Vec<_>>()
1016                                    .join("; ");
1017
1018                                conflicts.push(format!(
1019                                    "{}: Breaking changes detected - {}",
1020                                    spec.name, change_summary
1021                                ));
1022                            }
1023                            CompatibilityLevel::BackwardCompatible => {
1024                                tracing::info!(
1025                                    "Service '{}' has backward compatible changes",
1026                                    spec.name
1027                                );
1028                                // Backward compatible is allowed, no conflict
1029                            }
1030                            CompatibilityLevel::FullyCompatible => {
1031                                // This shouldn't happen if fingerprints differ, but handle it
1032                                tracing::debug!(
1033                                    "Service '{}' is fully compatible despite fingerprint difference",
1034                                    spec.name
1035                                );
1036                            }
1037                        }
1038                    }
1039                    Err(e) => {
1040                        // If detailed analysis fails, report based on fingerprint difference
1041                        tracing::warn!("Compatibility analysis failed for '{}': {}", spec.name, e);
1042                        conflicts.push(format!(
1043                            "{}: Service definition changed (locked: {}, current: {})",
1044                            spec.name, locked_fingerprint, current_fingerprint
1045                        ));
1046                    }
1047                }
1048            }
1049        }
1050
1051        Ok(conflicts)
1052    }
1053
1054    /// Display install success information
1055    fn display_install_success(&self, result: &InstallResult) {
1056        println!();
1057        println!("✅ Installation successful!");
1058        println!(
1059            "   đŸ“Ļ Installed dependencies: {}",
1060            result.installed_dependencies.len()
1061        );
1062        println!("   đŸ—‚ī¸  Cache updates: {}", result.cache_updates);
1063
1064        if result.updated_config {
1065            println!("   📝 Configuration file updated");
1066        }
1067
1068        if result.updated_lock_file {
1069            println!("   🔒 Lock file updated");
1070        }
1071
1072        if !result.warnings.is_empty() {
1073            println!();
1074            println!("âš ī¸  Warnings:");
1075            for warning in &result.warnings {
1076                println!("   â€ĸ {warning}");
1077            }
1078        }
1079
1080        println!();
1081        println!("💡 Tip: Run 'actr gen' to generate the latest code");
1082    }
1083}
1084
1085impl Default for InstallCommand {
1086    fn default() -> Self {
1087        Self::new(Vec::new(), None, None, false, false, false)
1088    }
1089}