1use anyhow::Result;
6use async_trait::async_trait;
7use clap::Args;
8
9use crate::core::{
10 ActrCliError, Command, CommandContext, CommandResult, ComponentType, ConfigManager,
11 DependencyResolver, DependencySpec, Fingerprint, FingerprintValidator, NetworkCheckOptions,
12 NetworkValidator, ResolvedDependency, ServiceDetails, ServiceDiscovery, ServiceInfo,
13};
14
15#[derive(Args, Debug)]
17#[command(
18 about = "Discover network services",
19 long_about = "Discover Actor services in the network, view available services and choose to install"
20)]
21pub struct DiscoveryCommand {
22 #[arg(long, value_name = "PATTERN")]
24 pub filter: Option<String>,
25
26 #[arg(long)]
28 pub verbose: bool,
29
30 #[arg(long)]
32 pub auto_install: bool,
33
34 #[arg(long)]
36 pub list_only: bool,
37}
38
39#[async_trait]
40impl Command for DiscoveryCommand {
41 async fn execute(&self, context: &CommandContext) -> Result<CommandResult> {
42 let (service_discovery, user_interface, config_manager) = {
44 let container = context.container.lock().unwrap();
45 (
46 container.get_service_discovery()?,
47 container.get_user_interface()?,
48 container.get_config_manager()?,
49 )
50 };
51
52 let filter = self.create_service_filter();
55 let services = service_discovery.discover_services(filter.as_ref()).await?;
56 tracing::debug!("Discovered services: {:?}", services);
57
58 if services.is_empty() {
59 println!("âšī¸ No available Actor services discovered in the current network");
60 return Ok(CommandResult::Success("No services discovered".to_string()));
61 }
62
63 println!("đ Discovered Actor services:");
64 self.display_services_table(&services);
66
67 if self.list_only {
68 return Ok(CommandResult::Success("Services listed".to_string()));
69 }
70
71 let service_options: Vec<String> = services.iter().map(|s| s.name.clone()).collect();
73
74 let selected_index = match user_interface
75 .select_from_list(&service_options, "Select a service to view (Esc to quit)")
76 .await
77 {
78 Ok(index) => index,
79 Err(err) if Self::is_operation_cancelled(&err) => {
80 return Ok(CommandResult::Success("Operation cancelled".to_string()));
81 }
82 Err(err) => return Err(err),
83 };
84
85 let selected_service = &services[selected_index];
86 let mut selected_details = None;
87
88 if self.verbose {
89 let details = service_discovery
90 .get_service_details(&selected_service.name)
91 .await?;
92 self.display_service_details(&details);
93 selected_details = Some(details);
94 }
95
96 let menu_prompt = format!("Options for {}", selected_service.name);
98
99 let action_menu = vec![
101 "[1] View service details (fingerprint, publication time)".to_string(),
102 "[2] Export proto files".to_string(),
103 "[3] Add to configuration file".to_string(),
104 ];
105
106 let action_choice = match user_interface
107 .select_from_list(&action_menu, &menu_prompt)
108 .await
109 {
110 Ok(choice) => choice,
111 Err(err) if Self::is_operation_cancelled(&err) => {
112 return Ok(CommandResult::Success("Operation cancelled".to_string()));
113 }
114 Err(err) => return Err(err),
115 };
116
117 match action_choice {
118 0 => {
119 if let Some(details) = selected_details.as_ref() {
120 self.display_service_details(details);
121 } else {
122 let details = service_discovery
123 .get_service_details(&selected_service.name)
124 .await?;
125 self.display_service_details(&details);
126 }
127 Ok(CommandResult::Success(
128 "Service details displayed".to_string(),
129 ))
130 }
131 1 => {
132 self.export_proto_files(selected_service, &service_discovery, &config_manager)
134 .await?;
135 Ok(CommandResult::Success("Proto files exported".to_string()))
136 }
137 2 => {
138 self.add_to_config_with_validation(selected_service, context)
140 .await
141 }
142 _ => Ok(CommandResult::Success("Invalid choice".to_string())),
143 }
144 }
145
146 fn required_components(&self) -> Vec<ComponentType> {
147 vec![
149 ComponentType::ServiceDiscovery, ComponentType::UserInterface, ComponentType::ConfigManager, ComponentType::DependencyResolver,
153 ComponentType::NetworkValidator,
154 ComponentType::FingerprintValidator,
155 ]
156 }
157
158 fn name(&self) -> &str {
159 "discovery"
160 }
161
162 fn description(&self) -> &str {
163 "Discover available Actor services in the network (Reuse architecture + check-first)"
164 }
165}
166
167impl DiscoveryCommand {
168 pub fn new(filter: Option<String>, verbose: bool, auto_install: bool) -> Self {
169 Self {
170 filter,
171 verbose,
172 auto_install,
173 list_only: false,
174 }
175 }
176
177 pub fn from_args(args: &DiscoveryCommand) -> Self {
179 DiscoveryCommand {
180 filter: args.filter.clone(),
181 verbose: args.verbose,
182 auto_install: args.auto_install,
183 list_only: args.list_only,
184 }
185 }
186
187 fn create_service_filter(&self) -> Option<crate::core::ServiceFilter> {
189 self.filter
190 .as_ref()
191 .map(|pattern| crate::core::ServiceFilter {
192 name_pattern: Some(pattern.clone()),
193 version_range: None,
194 tags: None,
195 })
196 }
197
198 fn is_operation_cancelled(err: &anyhow::Error) -> bool {
199 matches!(
200 err.downcast_ref::<ActrCliError>(),
201 Some(ActrCliError::OperationCancelled)
202 )
203 }
204
205 #[allow(clippy::too_many_arguments)]
206 async fn validate_dependency(
207 &self,
208 service: &ServiceInfo,
209 dependency_spec: &DependencySpec,
210 expected_fingerprint: Option<&str>,
211 check_conflicts: bool,
212 existing_specs: &[DependencySpec],
213 dependency_resolver: &std::sync::Arc<dyn DependencyResolver>,
214 service_discovery: &std::sync::Arc<dyn ServiceDiscovery>,
215 network_validator: &std::sync::Arc<dyn NetworkValidator>,
216 fingerprint_validator: &std::sync::Arc<dyn FingerprintValidator>,
217 ) -> Result<()> {
218 println!();
219 println!("đ Validating dependency...");
220
221 let mut failures = Vec::new();
222
223 match service_discovery
224 .check_service_availability(&service.name)
225 .await
226 {
227 Ok(status) => {
228 if status.is_available {
229 println!(" ââ â
Service availability");
230 } else {
231 println!(" ââ â Service availability");
232 failures.push(format!("Service '{}' not found in registry", service.name));
233 }
234 }
235 Err(e) => {
236 println!(" ââ â Service availability");
237 failures.push(format!("Service availability check failed: {e}"));
238 }
239 }
240
241 match network_validator
242 .check_connectivity(&service.name, &NetworkCheckOptions::default())
243 .await
244 {
245 Ok(connectivity) => {
246 if connectivity.is_reachable {
247 println!(" ââ â
Network connectivity");
248 } else {
249 println!(" ââ â Network connectivity");
250 let detail = connectivity.error.as_deref().unwrap_or("unknown error");
251 failures.push(format!(
252 "Network connectivity failed for '{}': {}",
253 service.name, detail
254 ));
255 }
256 }
257 Err(e) => {
258 println!(" ââ â Network connectivity");
259 failures.push(format!("Network connectivity check failed: {e}"));
260 }
261 }
262
263 if let Some(expected_fingerprint) = expected_fingerprint.filter(|fp| !fp.is_empty()) {
264 match fingerprint_validator
265 .compute_service_fingerprint(service)
266 .await
267 {
268 Ok(actual) => {
269 let expected = Fingerprint {
270 algorithm: actual.algorithm.clone(),
271 value: expected_fingerprint.to_string(),
272 };
273 let is_valid = fingerprint_validator
274 .verify_fingerprint(&expected, &actual)
275 .await
276 .unwrap_or(false);
277 if is_valid {
278 println!(" ââ â
Fingerprint match");
279 } else {
280 println!(" ââ â Fingerprint match");
281 failures.push(format!("Fingerprint mismatch for '{}'", service.name));
282 }
283 }
284 Err(e) => {
285 println!(" ââ â Fingerprint check");
286 failures.push(format!("Fingerprint check failed: {e}"));
287 }
288 }
289 } else {
290 println!(" ââ â ī¸ Fingerprint missing; skipping check");
291 }
292
293 if check_conflicts {
294 let mut resolved = Vec::with_capacity(existing_specs.len() + 1);
295 for spec in existing_specs {
296 resolved.push(ResolvedDependency {
297 spec: spec.clone(),
298 fingerprint: spec.fingerprint.clone().unwrap_or_default(),
299 proto_files: Vec::new(),
300 });
301 }
302 resolved.push(ResolvedDependency {
303 spec: dependency_spec.clone(),
304 fingerprint: dependency_spec.fingerprint.clone().unwrap_or_default(),
305 proto_files: Vec::new(),
306 });
307
308 match dependency_resolver.check_conflicts(&resolved).await {
309 Ok(conflicts) => {
310 if conflicts.is_empty() {
311 println!(" ââ â
Dependency conflicts");
312 } else {
313 println!(" ââ â Dependency conflicts");
314 let details = conflicts
315 .iter()
316 .map(|conflict| conflict.description.clone())
317 .collect::<Vec<_>>()
318 .join(", ");
319 failures.push(format!("Dependency conflicts: {details}"));
320 }
321 }
322 Err(e) => {
323 println!(" ââ â Dependency conflicts");
324 failures.push(format!("Dependency conflict check failed: {e}"));
325 }
326 }
327 } else {
328 println!(" ââ â ī¸ Dependency conflict check skipped (already configured)");
329 }
330
331 if failures.is_empty() {
332 println!(" ââ â
Validation passed");
333 Ok(())
334 } else {
335 println!(" ââ â Validation failed");
336 Err(ActrCliError::ValidationFailed {
337 details: failures.join("; "),
338 }
339 .into())
340 }
341 }
342
343 fn display_services_table(&self, services: &[ServiceInfo]) {
345 println!();
346 const TOTAL_MAX_WIDTH: usize = 160;
348 const BORDER_OVERHEAD: usize = 7;
350
351 let name_width = services
353 .iter()
354 .map(|s| s.name.chars().count())
355 .max()
356 .unwrap_or(0)
357 .max("Service Name".len());
358
359 let tags_width = services
360 .iter()
361 .map(|s| s.tags.join(", ").chars().count())
362 .max()
363 .unwrap_or(0)
364 .max("Tags".len());
365
366 let desc_width = services
367 .iter()
368 .map(|s| {
369 s.description
370 .as_deref()
371 .unwrap_or("No description")
372 .chars()
373 .count()
374 })
375 .max()
376 .unwrap_or(0)
377 .max("Description".len());
378
379 let name_w = name_width;
380 let tags_w = tags_width;
381 let mut desc_w = desc_width;
382
383 if name_w + tags_w + desc_w + BORDER_OVERHEAD > TOTAL_MAX_WIDTH {
385 let available = TOTAL_MAX_WIDTH - BORDER_OVERHEAD;
386 let used = name_w + tags_w;
387 desc_w = available.saturating_sub(used).max(10); }
389
390 let top_border = format!(
392 "ââ{}ââŦâ{}ââŦâ{}ââ",
393 "â".repeat(name_w),
394 "â".repeat(tags_w),
395 "â".repeat(desc_w)
396 );
397 let header = format!(
398 "â {:width$} â {:tags_w$} â {:desc_w$} â",
399 "Service Name",
400 "Tags",
401 "Description",
402 width = name_w,
403 tags_w = tags_w,
404 desc_w = desc_w
405 );
406 let separator = format!(
407 "ââ{}ââŧâ{}ââŧâ{}ââ¤",
408 "â".repeat(name_w),
409 "â".repeat(tags_w),
410 "â".repeat(desc_w)
411 );
412 let bottom_border = format!(
413 "ââ{}ââ´â{}ââ´â{}ââ",
414 "â".repeat(name_w),
415 "â".repeat(tags_w),
416 "â".repeat(desc_w)
417 );
418
419 println!("{top_border}");
420 println!("{header}");
421 println!("{separator}");
422
423 for service in services {
424 let tags_str = service.tags.join(", ");
425 let description = service
426 .description
427 .as_deref()
428 .unwrap_or("No description")
429 .chars()
430 .take(desc_w)
431 .collect::<String>();
432
433 println!(
434 "â {:name_w$} â {:tags_w$} â {:desc_w$} â",
435 service.name,
436 tags_str.chars().take(tags_w).collect::<String>(),
437 description,
438 name_w = name_w,
439 tags_w = tags_w,
440 desc_w = desc_w
441 );
442 }
443
444 println!("{bottom_border}");
445 println!();
446 }
447
448 fn display_service_info(&self, service: &ServiceInfo) {
450 println!("đ Selected service: {}", service.name);
451 if let Some(desc) = &service.description {
452 println!("đ Description: {desc}");
453 }
454 println!("đ Fingerprint: {}", service.fingerprint);
455 let time = service
456 .published_at
457 .and_then(|published_at| chrono::DateTime::from_timestamp(published_at, 0))
458 .map(|dt| {
459 dt.with_timezone(&chrono::Local)
460 .format("%Y-%m-%d %H:%M:%S")
461 .to_string()
462 })
463 .unwrap_or_else(|| "Unknown".to_string());
464 println!("đ
Publication Time: {}", time);
465 println!(
466 "đˇī¸ Tags: {}",
467 if service.tags.is_empty() {
468 "(none)".to_string()
469 } else {
470 service.tags.join(", ")
471 }
472 );
473 println!("đ Methods count: {}", service.methods.len());
474 println!();
475 }
476
477 #[allow(unused)]
478 fn display_service_details(&self, details: &ServiceDetails) {
480 println!("đ {} Detailed Information:", details.info.name);
481 println!("ââââââââââââââââââââââââââââââââââââââââ");
482 self.display_service_info(&details.info);
483 println!("đ Available Methods:");
484 if details.info.methods.is_empty() {
485 println!(" (None)");
486 } else {
487 for method in &details.info.methods {
488 println!(
489 " âĸ {}: {} â {}",
490 method.name, method.input_type, method.output_type
491 );
492 }
493 }
494
495 if !details.dependencies.is_empty() {
496 println!();
497 println!("đ Dependent Services:");
498 for dep in &details.dependencies {
499 println!(" âĸ {dep}");
500 }
501 }
502
503 println!();
504 println!("đ Proto Files:");
505 if details.proto_files.is_empty() {
506 println!(" (None)");
507 } else {
508 for proto in &details.proto_files {
509 println!(" âĸ {} ({} services)", proto.name, proto.services.len());
510 }
511 }
512
513 println!();
514 }
515
516 async fn export_proto_files(
518 &self,
519 service: &ServiceInfo,
520 service_discovery: &std::sync::Arc<dyn ServiceDiscovery>,
521 config_manager: &std::sync::Arc<dyn ConfigManager>,
522 ) -> Result<()> {
523 println!("đ¤ Exporting proto files for {}...", service.name);
524
525 let proto_files = service_discovery.get_service_proto(&service.name).await?;
526
527 let output_dir = config_manager
528 .get_project_root()
529 .join("exports")
530 .join("remote")
531 .join(&service.name);
532 std::fs::create_dir_all(&output_dir)?;
533
534 for proto in &proto_files {
535 let file_path = output_dir.join(&proto.name);
536 if let Some(parent) = file_path.parent() {
537 std::fs::create_dir_all(parent)?;
538 }
539 std::fs::write(&file_path, &proto.content)?;
540 println!("â
Exported: {}", file_path.display());
541 }
542
543 println!("đ Export completed, total {} files", proto_files.len());
544 Ok(())
545 }
546
547 async fn add_to_config_with_validation(
549 &self,
550 service: &ServiceInfo,
551 context: &CommandContext,
552 ) -> Result<CommandResult> {
553 let (
554 config_manager,
555 user_interface,
556 dependency_resolver,
557 service_discovery,
558 network_validator,
559 fingerprint_validator,
560 ) = {
561 let container = context.container.lock().unwrap();
562 (
563 container.get_config_manager()?,
564 container.get_user_interface()?,
565 container.get_dependency_resolver()?,
566 container.get_service_discovery()?,
567 container.get_network_validator()?,
568 container.get_fingerprint_validator()?,
569 )
570 };
571
572 let dependency_spec = DependencySpec {
574 alias: service.name.clone(),
575 actr_type: Some(service.actr_type.clone()),
576 name: service.name.clone(),
577 fingerprint: Some(service.fingerprint.clone()),
578 };
579
580 let config = config_manager
582 .load_config(
583 config_manager
584 .get_project_root()
585 .join("manifest.toml")
586 .as_path(),
587 )
588 .await?;
589
590 let existing_by_name = config.dependencies.iter().find(|dep| {
591 dep.service
592 .as_ref()
593 .map(|s| s.name.as_str())
594 .or_else(|| dep.actr_type.as_ref().map(|t| t.name.as_str()))
595 .unwrap_or(dep.alias.as_str())
596 == service.name
597 });
598 let existing_by_alias = config
599 .dependencies
600 .iter()
601 .find(|dep| dep.alias == dependency_spec.alias);
602
603 if let Some(existing) = existing_by_alias
604 && existing
605 .service
606 .as_ref()
607 .map(|s| s.name.as_str())
608 .or_else(|| existing.actr_type.as_ref().map(|t| t.name.as_str()))
609 .unwrap_or(existing.alias.as_str())
610 != service.name
611 {
612 return Err(ActrCliError::Dependency {
613 message: format!(
614 "Dependency alias '{}' already exists for '{}'",
615 existing.alias,
616 existing
617 .service
618 .as_ref()
619 .map(|s| s.name.as_str())
620 .or_else(|| existing.actr_type.as_ref().map(|t| t.name.as_str()))
621 .unwrap_or(existing.alias.as_str())
622 ),
623 }
624 .into());
625 }
626
627 let should_update_config = existing_by_name.is_none();
628 if let Some(existing) = existing_by_name {
629 println!(
630 "âšī¸ Dependency with name '{}' already exists (alias: '{}')",
631 service.name, existing.alias
632 );
633 if let (Some(existing_fp), Some(discovered_fp)) = (
634 existing.service.as_ref().map(|s| s.fingerprint.as_str()),
635 dependency_spec.fingerprint.as_deref(),
636 ) && existing_fp != discovered_fp
637 {
638 println!(
639 "â ī¸ Fingerprint mismatch: config '{}' vs discovery '{}'",
640 existing_fp, discovered_fp
641 );
642 }
643 println!(" Skipping configuration update");
644 }
645
646 let expected_fingerprint = existing_by_name
647 .and_then(|dep| dep.service.as_ref().map(|s| s.fingerprint.clone()))
648 .or_else(|| dependency_spec.fingerprint.clone());
649 let existing_specs = dependency_resolver.resolve_spec(&config).await?;
650 self.validate_dependency(
651 service,
652 &dependency_spec,
653 expected_fingerprint.as_deref(),
654 should_update_config,
655 &existing_specs,
656 &dependency_resolver,
657 &service_discovery,
658 &network_validator,
659 &fingerprint_validator,
660 )
661 .await?;
662
663 if should_update_config {
664 println!("đ Adding {} to configuration file...", service.name);
665 let backup = config_manager.backup_config().await?;
666 match config_manager.update_dependency(&dependency_spec).await {
667 Ok(_) => {
668 config_manager.remove_backup(backup).await?;
669 println!("â
Added {} to configuration file", service.name);
670 }
671 Err(e) => {
672 config_manager.restore_backup(backup).await?;
673 return Err(ActrCliError::Config {
674 message: format!("Configuration update failed: {e}"),
675 }
676 .into());
677 }
678 }
679 }
680
681 println!();
683 let should_install = if self.auto_install {
684 true
685 } else {
686 user_interface
687 .confirm("đ¤ Install this dependency now?")
688 .await?
689 };
690
691 if should_install {
692 println!();
694 println!("đĻ Installing {}...", service.name);
695
696 let install_pipeline = {
697 let mut container = context.container.lock().unwrap();
698 match container.get_install_pipeline() {
699 Ok(pipeline) => pipeline,
700 Err(_) => {
701 println!("âšī¸ Install pipeline is not implemented yet; skipping.");
702 return Ok(CommandResult::Success(
703 "Dependency added; install pending".to_string(),
704 ));
705 }
706 }
707 };
708
709 match install_pipeline
710 .install_dependencies(&[dependency_spec])
711 .await
712 {
713 Ok(install_result) => {
714 println!(" ââ đĻ Cache proto files â
");
715 println!(" ââ đ Update lock file â
");
716 println!(" ââ â
Installation complete");
717 println!();
718 println!("đĄ Tip: Run 'actr gen' to generate the latest code");
719
720 Ok(CommandResult::Install(install_result))
721 }
722 Err(e) => {
723 eprintln!("â Installation failed: {e}");
724 Ok(CommandResult::Success(
725 "Dependency added but installation failed".to_string(),
726 ))
727 }
728 }
729 } else {
730 println!("â
Dependency added to configuration file");
731 println!("đĄ Tip: Run 'actr deps install' to install dependencies");
732 Ok(CommandResult::Success(
733 "Dependency added to configuration".to_string(),
734 ))
735 }
736 }
737}
738
739impl Default for DiscoveryCommand {
740 fn default() -> Self {
741 Self::new(None, false, false)
742 }
743}
744
745#[cfg(test)]
746mod tests {
747 use super::*;
748
749 #[test]
750 fn test_create_service_filter() {
751 let cmd = DiscoveryCommand::new(Some("user-*".to_string()), false, false);
752 let filter = cmd.create_service_filter();
753
754 assert!(filter.is_some());
755 let filter = filter.unwrap();
756 assert_eq!(filter.name_pattern, Some("user-*".to_string()));
757 }
758
759 #[test]
760 fn test_create_service_filter_none() {
761 let cmd = DiscoveryCommand::new(None, false, false);
762 let filter = cmd.create_service_filter();
763
764 assert!(filter.is_none());
765 }
766
767 #[test]
768 fn test_required_components() {
769 let cmd = DiscoveryCommand::default();
770 let components = cmd.required_components();
771
772 assert!(components.contains(&ComponentType::ServiceDiscovery));
774 assert!(components.contains(&ComponentType::UserInterface));
775 assert!(components.contains(&ComponentType::ConfigManager));
776 assert!(components.contains(&ComponentType::DependencyResolver));
777 assert!(components.contains(&ComponentType::NetworkValidator));
778 assert!(components.contains(&ComponentType::FingerprintValidator));
779 assert!(!components.contains(&ComponentType::CacheManager));
780 assert!(!components.contains(&ComponentType::ProtoProcessor));
781 }
782}