1use 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#[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 #[arg(value_name = "PACKAGE")]
25 pub packages: Vec<String>,
26
27 #[arg(long, value_name = "TYPE")]
30 pub actr_type: Option<String>,
31
32 #[arg(long, value_name = "FINGERPRINT")]
34 pub fingerprint: Option<String>,
35
36 #[arg(long)]
38 pub force: bool,
39
40 #[arg(long)]
42 pub force_update: bool,
43
44 #[arg(long)]
46 pub skip_verification: bool,
47}
48
49#[derive(Debug, Clone)]
51pub enum InstallMode {
52 AddNewPackage { packages: Vec<String> },
57
58 AddWithAlias {
64 alias: String,
65 actr_type: ActrType,
66 fingerprint: Option<String>,
67 },
68
69 InstallFromConfig { force_update: bool },
74}
75
76#[async_trait]
77impl Command for InstallCommand {
78 async fn execute(&self, context: &CommandContext) -> Result<CommandResult> {
79 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 let mode = if let Some(actr_type_str) = &self.actr_type {
89 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 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 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 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 fn is_actr_project(&self) -> bool {
215 std::path::Path::new("Actr.toml").exists()
216 }
217
218 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 println!(" ââ đ Parsing dependency spec: {}", package);
240
241 let service_discovery = install_pipeline.validation_pipeline().service_discovery();
248 let ui = context.container.lock().unwrap().get_user_interface()?;
249
250 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 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 let service = services[0].clone();
282 println!(" ââ đ Automatically selected service: {}", service.name);
283 service
284 } else {
285 println!(
287 " ââ đ Found {} services matching '{}'",
288 services.len(),
289 package
290 );
291
292 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 println!(" ââ đ Network connectivity test (Skipped) â
");
329
330 println!(" ââ đ Fingerprint integrity verification â
");
332
333 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 println!("đ Phase 2: Atomic Installation");
351
352 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 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 let service_discovery = install_pipeline.validation_pipeline().service_discovery();
406
407 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 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 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 println!(" ââ đ Network connectivity test (Skipped) â
");
448
449 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 println!("đ Phase 2: Atomic Installation");
466
467 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 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 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 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 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 let install_pipeline = {
565 let mut container = context.container.lock().unwrap();
566 container.get_install_pipeline()?
567 };
568
569 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 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 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 !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 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 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 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 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 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 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 let expected_fingerprint = match &spec.fingerprint {
779 Some(fp) => fp,
780 None => continue,
781 };
782
783 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 = ¤t_service.info.fingerprint;
796
797 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 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 for spec in specs {
821 if spec.fingerprint.is_none() {
822 continue;
823 }
824
825 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 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 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 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 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); }
879 };
880
881 for spec in dependency_specs {
883 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 tracing::debug!("Dependency '{}' not in lock file, skipping", spec.name);
891 continue;
892 }
893 };
894
895 let locked_fingerprint = &locked_dep.fingerprint;
896
897 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 = ¤t_service.info.fingerprint;
908
909 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 tracing::info!(
920 "Fingerprint mismatch for '{}': locked={}, current={}",
921 spec.name,
922 locked_fingerprint,
923 current_fingerprint
924 );
925
926 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 let current_semantic_fp =
943 match Fingerprint::calculate_service_semantic_fingerprint(¤t_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 conflicts.push(format!(
953 "{}: Unable to verify compatibility (fingerprint calculation failed)",
954 spec.name
955 ));
956 continue;
957 }
958 };
959
960 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 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(), 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 match ServiceCompatibility::analyze_compatibility(&locked_spec, ¤t_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 }
1030 CompatibilityLevel::FullyCompatible => {
1031 tracing::debug!(
1033 "Service '{}' is fully compatible despite fingerprint difference",
1034 spec.name
1035 );
1036 }
1037 }
1038 }
1039 Err(e) => {
1040 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 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}