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