1use 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#[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 #[arg(value_name = "PACKAGE")]
31 pub packages: Vec<String>,
32
33 #[arg(long, value_name = "TYPE")]
36 pub actr_type: Option<String>,
37
38 #[arg(long, value_name = "FINGERPRINT")]
40 pub fingerprint: Option<String>,
41
42 #[arg(long)]
44 pub force: bool,
45
46 #[arg(long)]
48 pub force_update: bool,
49
50 #[arg(long)]
52 pub skip_verification: bool,
53}
54
55#[derive(Debug, Clone)]
57pub enum InstallMode {
58 AddNewPackage { packages: Vec<String> },
63
64 AddWithAlias {
70 alias: String,
71 actr_type: ActrType,
72 fingerprint: Option<String>,
73 },
74
75 InstallFromConfig { force_update: bool },
80}
81
82#[async_trait]
83impl Command for InstallCommand {
84 async fn execute(&self, context: &CommandContext) -> Result<CommandResult> {
85 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 let mode = if let Some(actr_type_str) = &self.actr_type {
95 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 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 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 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 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 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 println!(" ├─ 📋 Parsing dependency spec: {}", package);
253
254 let service_discovery = install_pipeline.validation_pipeline().service_discovery();
261 let ui = context.container.lock().unwrap().get_user_interface()?;
262
263 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 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 let service = services[0].clone();
295 println!(" ├─ 🔍 Automatically selected service: {}", service.name);
296 service
297 } else {
298 println!(
300 " ├─ 🔍 Found {} services matching '{}'",
301 services.len(),
302 package
303 );
304
305 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 println!(" ├─ 🌐 Network connectivity test (Skipped) ✅");
342
343 println!(" ├─ 🔐 Fingerprint integrity verification ✅");
345
346 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 println!("📝 Phase 2: Atomic Installation");
364
365 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 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 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 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 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 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 println!(" ├─ 🌐 Network connectivity test (Skipped) ✅");
475
476 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 println!("📝 Phase 2: Atomic Installation");
490
491 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 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 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 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 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 let install_pipeline = {
591 let mut container = context.container.lock().unwrap();
592 container.get_install_pipeline()?
593 };
594
595 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 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 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 !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 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 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 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 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 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 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 let expected_fingerprint = match &spec.fingerprint {
814 Some(fp) => fp,
815 None => continue,
816 };
817
818 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 = ¤t_service.info.fingerprint;
832
833 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 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 for spec in specs {
857 if spec.fingerprint.is_none() {
858 continue;
859 }
860
861 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 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 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 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 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); }
916 };
917
918 for spec in dependency_specs {
920 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 tracing::debug!("Dependency '{}' not in lock file, skipping", spec.name);
928 continue;
929 }
930 };
931
932 let locked_fingerprint = &locked_dep.fingerprint;
933
934 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 = ¤t_service.info.fingerprint;
946
947 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 tracing::info!(
958 "Fingerprint mismatch for '{}': locked={}, current={}",
959 spec.name,
960 locked_fingerprint,
961 current_fingerprint
962 );
963
964 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 let current_semantic_fp =
981 match Fingerprint::calculate_service_semantic_fingerprint(¤t_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 conflicts.push(format!(
991 "{}: Unable to verify compatibility (fingerprint calculation failed)",
992 spec.name
993 ));
994 continue;
995 }
996 };
997
998 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 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(), fingerprint: pf.fingerprint.clone(),
1028 })
1029 .collect(),
1030 published_at: locked_dep.published_at,
1031 tags: locked_dep.tags.clone(),
1032 };
1033
1034 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 match ServiceCompatibility::analyze_compatibility(&locked_spec, ¤t_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 }
1086 CompatibilityLevel::FullyCompatible => {
1087 tracing::debug!(
1089 "Service '{}' is fully compatible despite fingerprint difference",
1090 spec.name
1091 );
1092 }
1093 }
1094 }
1095 Err(e) => {
1096 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 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}