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 InstallMode::AddNewPackage {
114 packages: self.packages.clone(),
115 }
116 } else {
117 InstallMode::InstallFromConfig {
118 force_update: self.force_update,
119 }
120 };
121
122 match mode {
124 InstallMode::AddNewPackage { ref packages } => {
125 self.execute_add_package(context, packages).await
126 }
127 InstallMode::AddWithAlias {
128 ref alias,
129 ref actr_type,
130 ref fingerprint,
131 } => {
132 self.execute_add_with_alias(context, alias, actr_type, fingerprint.as_deref())
133 .await
134 }
135 InstallMode::InstallFromConfig { force_update } => {
136 self.execute_install_from_config(context, force_update)
137 .await
138 }
139 }
140 }
141
142 fn required_components(&self) -> Vec<ComponentType> {
143 vec![
145 ComponentType::ConfigManager,
146 ComponentType::DependencyResolver,
147 ComponentType::ServiceDiscovery,
148 ComponentType::NetworkValidator,
149 ComponentType::FingerprintValidator,
150 ComponentType::ProtoProcessor,
151 ComponentType::CacheManager,
152 ]
153 }
154
155 fn name(&self) -> &str {
156 "install"
157 }
158
159 fn description(&self) -> &str {
160 "npm-style service-level dependency management (check-first architecture)"
161 }
162}
163
164impl InstallCommand {
165 pub fn new(
166 packages: Vec<String>,
167 actr_type: Option<String>,
168 fingerprint: Option<String>,
169 force: bool,
170 force_update: bool,
171 skip_verification: bool,
172 ) -> Self {
173 Self {
174 packages,
175 actr_type,
176 fingerprint,
177 force,
178 force_update,
179 skip_verification,
180 }
181 }
182
183 pub fn from_args(args: &InstallCommand) -> Self {
185 InstallCommand {
186 packages: args.packages.clone(),
187 actr_type: args.actr_type.clone(),
188 fingerprint: args.fingerprint.clone(),
189 force: args.force,
190 force_update: args.force_update,
191 skip_verification: args.skip_verification,
192 }
193 }
194
195 fn is_actr_project(&self) -> bool {
197 std::path::Path::new("Actr.toml").exists()
198 }
199
200 async fn execute_add_package(
205 &self,
206 context: &CommandContext,
207 packages: &[String],
208 ) -> Result<CommandResult> {
209 println!("actr install {}", packages.join(" "));
210
211 let install_pipeline = {
212 let mut container = context.container.lock().unwrap();
213 container.get_install_pipeline()?
214 };
215
216 let mut resolved_specs = Vec::new();
217
218 println!("đ Phase 1: Complete Validation");
219 for package in packages {
220 println!(" ââ đ Parsing dependency spec: {}", package);
222
223 let service_details = install_pipeline
225 .validation_pipeline()
226 .service_discovery()
227 .get_service_details(package)
228 .await?;
229
230 println!(
231 " ââ đ Service discovery: fingerprint {}",
232 service_details.info.fingerprint
233 );
234
235 let connectivity = install_pipeline
237 .validation_pipeline()
238 .network_validator()
239 .check_connectivity(package)
240 .await?;
241
242 if connectivity.is_reachable {
243 println!(" ââ đ Network connectivity test â
");
244 } else {
245 println!(" ââ â Network connection failed");
246 return Err(anyhow::anyhow!(
247 "Network connectivity test failed for {}",
248 package,
249 ));
250 }
251
252 println!(" ââ đ Fingerprint integrity verification â
");
254
255 let resolved_spec = DependencySpec {
257 alias: package.clone(),
258 actr_type: Some(service_details.info.actr_type.clone()),
259 name: package.clone(),
260 fingerprint: Some(service_details.info.fingerprint.clone()),
261 };
262 resolved_specs.push(resolved_spec);
263 println!(" ââ â
Added to installation plan");
264 println!();
265 }
266
267 if resolved_specs.is_empty() {
268 return Ok(CommandResult::Success("No packages to install".to_string()));
269 }
270
271 println!("đ Phase 2: Atomic Installation");
273
274 match install_pipeline.install_dependencies(&resolved_specs).await {
276 Ok(result) => {
277 println!(" ââ đž Backing up current configuration");
278 println!(" ââ đ Updating Actr.toml configuration â
");
279 println!(" ââ đĻ Caching proto files â
");
280 println!(" ââ đ Updating Actr.lock.toml â
");
281 println!(" ââ â
Installation completed");
282 println!();
283 self.display_install_success(&result);
284 Ok(CommandResult::Install(result))
285 }
286 Err(e) => {
287 println!(" ââ đ Restoring backup (due to installation failure)");
288 let cli_error = ActrCliError::InstallFailed {
289 reason: e.to_string(),
290 };
291 eprintln!("{}", ErrorReporter::format_error(&cli_error));
292 Err(e)
293 }
294 }
295 }
296
297 async fn execute_add_with_alias(
303 &self,
304 context: &CommandContext,
305 alias: &str,
306 actr_type: &ActrType,
307 fingerprint: Option<&str>,
308 ) -> Result<CommandResult> {
309 use actr_protocol::ActrTypeExt;
310
311 println!(
312 "actr install {} --actr-type {}",
313 alias,
314 actr_type.to_string_repr()
315 );
316
317 let install_pipeline = {
318 let mut container = context.container.lock().unwrap();
319 container.get_install_pipeline()?
320 };
321
322 println!("đ Phase 1: Complete Validation");
323 println!(" ââ đ Alias: {}", alias);
324 println!(" ââ đˇī¸ Actor Type: {}", actr_type.to_string_repr());
325
326 let service_discovery = install_pipeline.validation_pipeline().service_discovery();
328
329 let services = service_discovery.discover_services(None).await?;
331 let matching_service = services
332 .iter()
333 .find(|s| s.actr_type == *actr_type)
334 .ok_or_else(|| ActrCliError::ServiceNotFound {
335 name: actr_type.to_string_repr(),
336 })?;
337
338 let service_name = matching_service.name.clone();
339 println!(" ââ đ Service discovered: {}", service_name);
340
341 let service_details = service_discovery.get_service_details(&service_name).await?;
343
344 println!(
345 " ââ đ Service fingerprint: {}",
346 service_details.info.fingerprint
347 );
348
349 if let Some(expected_fp) = fingerprint {
351 if service_details.info.fingerprint != expected_fp {
352 println!(" ââ â Fingerprint mismatch");
353 return Err(ActrCliError::FingerprintMismatch {
354 expected: expected_fp.to_string(),
355 actual: service_details.info.fingerprint.clone(),
356 }
357 .into());
358 }
359 println!(" ââ đ Fingerprint verification â
");
360 }
361
362 let connectivity = install_pipeline
364 .validation_pipeline()
365 .network_validator()
366 .check_connectivity(&service_name)
367 .await?;
368
369 if connectivity.is_reachable {
370 println!(" ââ đ Network connectivity test â
");
371 } else {
372 println!(" ââ â Network connection failed");
373 return Err(anyhow::anyhow!(
374 "Network connectivity test failed for {}",
375 service_name,
376 ));
377 }
378
379 let resolved_spec = DependencySpec {
381 alias: alias.to_string(),
382 actr_type: Some(service_details.info.actr_type.clone()),
383 name: service_name.clone(),
384 fingerprint: Some(
385 fingerprint
386 .map(|s| s.to_string())
387 .unwrap_or_else(|| service_details.info.fingerprint.clone()),
388 ),
389 };
390
391 println!(" ââ â
Added to installation plan");
392 println!();
393
394 println!("đ Phase 2: Atomic Installation");
396
397 match install_pipeline
399 .install_dependencies(&[resolved_spec])
400 .await
401 {
402 Ok(result) => {
403 println!(" ââ đž Backing up current configuration");
404 println!(" ââ đ Updating Actr.toml configuration â
");
405 println!(" ââ đĻ Caching proto files â
");
406 println!(" ââ đ Updating Actr.lock.toml â
");
407 println!(" ââ â
Installation completed");
408 println!();
409 self.display_install_success(&result);
410 Ok(CommandResult::Install(result))
411 }
412 Err(e) => {
413 println!(" ââ đ Restoring backup (due to installation failure)");
414 let cli_error = ActrCliError::InstallFailed {
415 reason: e.to_string(),
416 };
417 eprintln!("{}", ErrorReporter::format_error(&cli_error));
418 Err(e)
419 }
420 }
421 }
422
423 async fn execute_install_from_config(
429 &self,
430 context: &CommandContext,
431 force_update: bool,
432 ) -> Result<CommandResult> {
433 if force_update || self.force {
434 println!("đĻ Force updating all service dependencies");
435 } else {
436 println!("đĻ Installing service dependencies from config");
437 }
438 println!();
439
440 let dependency_specs = self.load_dependencies_from_config(context).await?;
442
443 if dependency_specs.is_empty() {
444 println!("âšī¸ No dependencies configured, generating empty lock file");
445
446 let install_pipeline = {
448 let mut container = context.container.lock().unwrap();
449 container.get_install_pipeline()?
450 };
451 let project_root = install_pipeline.config_manager().get_project_root();
452 let lock_file_path = project_root.join("Actr.lock.toml");
453
454 let mut lock_file = LockFile::new();
455 lock_file.update_timestamp();
456 lock_file
457 .save_to_file(&lock_file_path)
458 .map_err(|e| ActrCliError::InstallFailed {
459 reason: format!("Failed to save lock file: {}", e),
460 })?;
461
462 println!(" ââ đ Generated Actr.lock.toml");
463 return Ok(CommandResult::Success(
464 "Generated empty lock file".to_string(),
465 ));
466 }
467
468 let conflicts = self.check_actr_type_conflicts(&dependency_specs);
470 if !conflicts.is_empty() {
471 println!("â Dependency conflict detected:");
472 for conflict in &conflicts {
473 println!(" âĸ {}", conflict);
474 }
475 println!();
476 println!(
477 "đĄ Tip: Each actr_type can only be used once. Please use different aliases for different services or remove duplicate dependencies."
478 );
479 return Err(ActrCliError::DependencyConflict {
480 message: format!(
481 "{} dependency conflict(s) detected. Each actr_type must be unique.",
482 conflicts.len()
483 ),
484 }
485 .into());
486 }
487
488 println!("đ Phase 1: Full Validation");
489 for spec in &dependency_specs {
490 println!(" ââ đ Parsing dependency: {}", spec.alias);
491 }
492
493 let install_pipeline = {
495 let mut container = context.container.lock().unwrap();
496 container.get_install_pipeline()?
497 };
498
499 if !force_update && !self.force {
501 let project_root = install_pipeline.config_manager().get_project_root();
502 let lock_file_path = project_root.join("Actr.lock.toml");
503 if lock_file_path.exists() {
504 println!(" ââ đ Lock file found, checking compatibility...");
505
506 let conflicts = self
508 .check_lock_file_compatibility(
509 &lock_file_path,
510 &dependency_specs,
511 &install_pipeline,
512 )
513 .await?;
514
515 if !conflicts.is_empty() {
516 println!(" ââ â Compatibility conflicts detected");
517 println!();
518 println!("â ī¸ Breaking changes detected:");
519 for conflict in &conflicts {
520 println!(" âĸ {}", conflict);
521 }
522 println!();
523 println!(
524 "đĄ Tip: Use --force-update to override and update to the latest versions"
525 );
526 return Err(ActrCliError::CompatibilityConflict {
527 message: format!(
528 "{} breaking change(s) detected. Use --force-update to override.",
529 conflicts.len()
530 ),
531 }
532 .into());
533 }
534 println!(" ââ â
Compatibility check passed");
535 }
536 }
537
538 println!(" ââ â
Verifying fingerprints...");
540 let fingerprint_mismatches = self
541 .verify_fingerprints(&dependency_specs, &install_pipeline)
542 .await?;
543
544 if !fingerprint_mismatches.is_empty() && !self.force {
545 println!(" ââ â Fingerprint mismatch detected");
546 println!();
547 println!("â ī¸ Fingerprint mismatch:");
548 for mismatch in &fingerprint_mismatches {
549 println!(" âĸ {}", mismatch);
550 }
551 println!();
552 println!(
553 "đĄ Tip: Use --force to update Actr.toml with the current service fingerprints"
554 );
555 return Err(ActrCliError::FingerprintValidation {
556 message: format!(
557 "{} fingerprint mismatch(es) detected. Use --force to update.",
558 fingerprint_mismatches.len()
559 ),
560 }
561 .into());
562 }
563
564 if !fingerprint_mismatches.is_empty() && self.force {
566 println!(" ââ â ī¸ Fingerprint mismatch detected, updating Actr.toml...");
567 self.update_config_fingerprints(context, &dependency_specs, &install_pipeline)
568 .await?;
569 println!(" ââ â
Actr.toml updated with current fingerprints");
570
571 let dependency_specs = self.load_dependencies_from_config(context).await?;
573
574 println!(" ââ đ Service discovery (DiscoveryRequest)");
575 println!(" ââ đ Network connectivity test");
576 println!(" ââ â
Installation plan generated");
577 println!();
578
579 println!("đ Phase 2: Atomic Installation");
581 return match install_pipeline
582 .install_dependencies(&dependency_specs)
583 .await
584 {
585 Ok(install_result) => {
586 println!(" ââ đ Caching proto files â
");
587 println!(" ââ đ Updating Actr.lock.toml â
");
588 println!(" ââ â
Installation completed");
589 println!();
590 println!(
591 "đ Note: Actr.toml fingerprints were updated to match current services"
592 );
593 self.display_install_success(&install_result);
594 Ok(CommandResult::Install(install_result))
595 }
596 Err(e) => {
597 println!(" ââ â Installation failed");
598 let cli_error = ActrCliError::InstallFailed {
599 reason: e.to_string(),
600 };
601 eprintln!("{}", ErrorReporter::format_error(&cli_error));
602 Err(e)
603 }
604 };
605 }
606
607 println!(" ââ â
Fingerprint verification passed");
608 println!(" ââ đ Service discovery (DiscoveryRequest)");
609 println!(" ââ đ Network connectivity test");
610 println!(" ââ â
Installation plan generated");
611 println!();
612
613 println!("đ Phase 2: Atomic Installation");
615 match install_pipeline
616 .install_dependencies(&dependency_specs)
617 .await
618 {
619 Ok(install_result) => {
620 println!(" ââ đĻ Caching proto files â
");
621 println!(" ââ đ Updating Actr.lock.toml â
");
622 println!(" ââ â
Installation completed");
623 println!();
624 self.display_install_success(&install_result);
625 Ok(CommandResult::Install(install_result))
626 }
627 Err(e) => {
628 println!(" ââ â Installation failed");
629 let cli_error = ActrCliError::InstallFailed {
630 reason: e.to_string(),
631 };
632 eprintln!("{}", ErrorReporter::format_error(&cli_error));
633 Err(e)
634 }
635 }
636 }
637
638 async fn load_dependencies_from_config(
640 &self,
641 context: &CommandContext,
642 ) -> Result<Vec<DependencySpec>> {
643 let config_manager = {
644 let container = context.container.lock().unwrap();
645 container.get_config_manager()?
646 };
647 let config = config_manager
648 .load_config(
649 config_manager
650 .get_project_root()
651 .join("Actr.toml")
652 .as_path(),
653 )
654 .await?;
655
656 let specs: Vec<DependencySpec> = config
657 .dependencies
658 .into_iter()
659 .map(|dependency| DependencySpec {
660 alias: dependency.alias,
661 actr_type: dependency.actr_type,
662 name: dependency.name,
663 fingerprint: dependency.fingerprint,
664 })
665 .collect();
666
667 Ok(specs)
668 }
669
670 fn check_actr_type_conflicts(&self, specs: &[DependencySpec]) -> Vec<String> {
672 use std::collections::HashMap;
673
674 let mut actr_type_map: HashMap<String, Vec<&str>> = HashMap::new();
675 let mut conflicts = Vec::new();
676
677 for spec in specs {
678 if let Some(ref actr_type) = spec.actr_type {
679 let type_str = actr_type.to_string_repr();
680 actr_type_map.entry(type_str).or_default().push(&spec.alias);
681 }
682 }
683
684 for (actr_type, aliases) in actr_type_map {
685 if aliases.len() > 1 {
686 conflicts.push(format!(
687 "actr_type '{}' is used by multiple dependencies: {}",
688 actr_type,
689 aliases.join(", ")
690 ));
691 }
692 }
693
694 conflicts
695 }
696
697 async fn verify_fingerprints(
699 &self,
700 specs: &[DependencySpec],
701 install_pipeline: &std::sync::Arc<crate::core::InstallPipeline>,
702 ) -> Result<Vec<String>> {
703 let mut mismatches = Vec::new();
704 let service_discovery = install_pipeline.validation_pipeline().service_discovery();
705
706 for spec in specs {
707 let expected_fingerprint = match &spec.fingerprint {
709 Some(fp) => fp,
710 None => continue,
711 };
712
713 let current_service = match service_discovery.get_service_details(&spec.name).await {
715 Ok(s) => s,
716 Err(e) => {
717 mismatches.push(format!(
718 "{}: Service not found or unavailable ({})",
719 spec.alias, e
720 ));
721 continue;
722 }
723 };
724
725 let current_fingerprint = ¤t_service.info.fingerprint;
726
727 if expected_fingerprint != current_fingerprint {
729 mismatches.push(format!(
730 "{}: Expected fingerprint '{}', but service has '{}'",
731 spec.alias, expected_fingerprint, current_fingerprint
732 ));
733 }
734 }
735
736 Ok(mismatches)
737 }
738
739 async fn update_config_fingerprints(
741 &self,
742 _context: &CommandContext,
743 specs: &[DependencySpec],
744 install_pipeline: &std::sync::Arc<crate::core::InstallPipeline>,
745 ) -> Result<()> {
746 let service_discovery = install_pipeline.validation_pipeline().service_discovery();
747 let config_manager = install_pipeline.config_manager();
748
749 for spec in specs {
751 if spec.fingerprint.is_none() {
752 continue;
753 }
754
755 let current_service = match service_discovery.get_service_details(&spec.name).await {
757 Ok(s) => s,
758 Err(_) => continue,
759 };
760
761 let old_fingerprint = spec
762 .fingerprint
763 .clone()
764 .unwrap_or_else(|| "none".to_string());
765 let new_fingerprint = current_service.info.fingerprint.clone();
766
767 let updated_spec = DependencySpec {
769 alias: spec.alias.clone(),
770 name: spec.name.clone(),
771 actr_type: spec.actr_type.clone(),
772 fingerprint: Some(new_fingerprint.clone()),
773 };
774
775 config_manager.update_dependency(&updated_spec).await?;
777
778 println!(
779 " đ Updated '{}' fingerprint: {} â {}",
780 spec.alias, old_fingerprint, new_fingerprint
781 );
782 }
783
784 Ok(())
785 }
786
787 async fn check_lock_file_compatibility(
793 &self,
794 lock_file_path: &std::path::Path,
795 dependency_specs: &[DependencySpec],
796 install_pipeline: &std::sync::Arc<crate::core::InstallPipeline>,
797 ) -> Result<Vec<String>> {
798 use actr_protocol::ServiceSpec;
799
800 let mut conflicts = Vec::new();
801
802 let lock_file = match LockFile::from_file(lock_file_path) {
804 Ok(lf) => lf,
805 Err(e) => {
806 tracing::warn!("Failed to parse lock file: {}", e);
807 return Ok(conflicts); }
809 };
810
811 for spec in dependency_specs {
813 let locked_dep = lock_file.dependencies.iter().find(|d| d.name == spec.name);
815
816 let locked_dep = match locked_dep {
817 Some(d) => d,
818 None => {
819 tracing::debug!("Dependency '{}' not in lock file, skipping", spec.name);
821 continue;
822 }
823 };
824
825 let locked_fingerprint = &locked_dep.fingerprint;
826
827 let service_discovery = install_pipeline.validation_pipeline().service_discovery();
829 let current_service = match service_discovery.get_service_details(&spec.name).await {
830 Ok(s) => s,
831 Err(e) => {
832 tracing::warn!("Failed to get service details for '{}': {}", spec.name, e);
833 continue;
834 }
835 };
836
837 let current_fingerprint = ¤t_service.info.fingerprint;
838
839 if locked_fingerprint == current_fingerprint {
841 tracing::debug!(
842 "Fingerprint match for '{}', no compatibility check needed",
843 spec.name
844 );
845 continue;
846 }
847
848 tracing::info!(
850 "Fingerprint mismatch for '{}': locked={}, current={}",
851 spec.name,
852 locked_fingerprint,
853 current_fingerprint
854 );
855
856 let current_proto_files: Vec<ProtoFile> = current_service
862 .proto_files
863 .iter()
864 .map(|pf| ProtoFile {
865 name: pf.name.clone(),
866 content: pf.content.clone(),
867 path: Some(pf.path.to_string_lossy().to_string()),
868 })
869 .collect();
870
871 let current_semantic_fp =
873 match Fingerprint::calculate_service_semantic_fingerprint(¤t_proto_files) {
874 Ok(fp) => fp,
875 Err(e) => {
876 tracing::warn!(
877 "Failed to calculate semantic fingerprint for '{}': {}",
878 spec.name,
879 e
880 );
881 conflicts.push(format!(
883 "{}: Unable to verify compatibility (fingerprint calculation failed)",
884 spec.name
885 ));
886 continue;
887 }
888 };
889
890 let locked_semantic = if locked_fingerprint.starts_with("service_semantic:") {
893 locked_fingerprint
894 .strip_prefix("service_semantic:")
895 .unwrap_or(locked_fingerprint)
896 } else {
897 locked_fingerprint.as_str()
898 };
899
900 if current_semantic_fp != locked_semantic {
901 let locked_spec = ServiceSpec {
904 name: spec.name.clone(),
905 description: locked_dep.description.clone(),
906 fingerprint: locked_fingerprint.clone(),
907 protobufs: locked_dep
908 .files
909 .iter()
910 .map(|pf| actr_protocol::service_spec::Protobuf {
911 package: pf.path.clone(),
912 content: String::new(), fingerprint: pf.fingerprint.clone(),
914 })
915 .collect(),
916 published_at: locked_dep.published_at,
917 tags: locked_dep.tags.clone(),
918 };
919
920 let current_spec = ServiceSpec {
921 name: spec.name.clone(),
922 description: Some(current_service.info.description.clone().unwrap_or_default()),
923 fingerprint: format!("service_semantic:{}", current_semantic_fp),
924 protobufs: current_proto_files
925 .iter()
926 .map(|pf| actr_protocol::service_spec::Protobuf {
927 package: pf.name.clone(),
928 content: pf.content.clone(),
929 fingerprint: String::new(),
930 })
931 .collect(),
932 published_at: current_service.info.published_at,
933 tags: current_service.info.tags.clone(),
934 };
935
936 match ServiceCompatibility::analyze_compatibility(&locked_spec, ¤t_spec) {
938 Ok(analysis) => {
939 match analysis.level {
940 CompatibilityLevel::BreakingChanges => {
941 let change_summary = analysis
942 .breaking_changes
943 .iter()
944 .map(|c| c.message.clone())
945 .collect::<Vec<_>>()
946 .join("; ");
947
948 conflicts.push(format!(
949 "{}: Breaking changes detected - {}",
950 spec.name, change_summary
951 ));
952 }
953 CompatibilityLevel::BackwardCompatible => {
954 tracing::info!(
955 "Service '{}' has backward compatible changes",
956 spec.name
957 );
958 }
960 CompatibilityLevel::FullyCompatible => {
961 tracing::debug!(
963 "Service '{}' is fully compatible despite fingerprint difference",
964 spec.name
965 );
966 }
967 }
968 }
969 Err(e) => {
970 tracing::warn!("Compatibility analysis failed for '{}': {}", spec.name, e);
972 conflicts.push(format!(
973 "{}: Service definition changed (locked: {}, current: {})",
974 spec.name, locked_fingerprint, current_fingerprint
975 ));
976 }
977 }
978 }
979 }
980
981 Ok(conflicts)
982 }
983
984 fn display_install_success(&self, result: &InstallResult) {
986 println!();
987 println!("â
Installation successful!");
988 println!(
989 " đĻ Installed dependencies: {}",
990 result.installed_dependencies.len()
991 );
992 println!(" đī¸ Cache updates: {}", result.cache_updates);
993
994 if result.updated_config {
995 println!(" đ Configuration file updated");
996 }
997
998 if result.updated_lock_file {
999 println!(" đ Lock file updated");
1000 }
1001
1002 if !result.warnings.is_empty() {
1003 println!();
1004 println!("â ī¸ Warnings:");
1005 for warning in &result.warnings {
1006 println!(" âĸ {warning}");
1007 }
1008 }
1009
1010 println!();
1011 println!("đĄ Tip: Run 'actr gen' to generate the latest code");
1012 }
1013}
1014
1015impl Default for InstallCommand {
1016 fn default() -> Self {
1017 Self::new(Vec::new(), None, None, false, false, false)
1018 }
1019}