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