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
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
232 .check_connectivity(&service.name, &NetworkCheckOptions::default())
233 .await
234 {
235 Ok(connectivity) => {
236 if connectivity.is_reachable {
237 println!(" โโ โ
Network connectivity");
238 } else {
239 println!(" โโ โ Network connectivity");
240 let detail = connectivity.error.as_deref().unwrap_or("unknown error");
241 failures.push(format!(
242 "Network connectivity failed for '{}': {}",
243 service.name, detail
244 ));
245 }
246 }
247 Err(e) => {
248 println!(" โโ โ Network connectivity");
249 failures.push(format!("Network connectivity check failed: {e}"));
250 }
251 }
252
253 if let Some(expected_fingerprint) = expected_fingerprint.filter(|fp| !fp.is_empty()) {
254 match fingerprint_validator
255 .compute_service_fingerprint(service)
256 .await
257 {
258 Ok(actual) => {
259 let expected = Fingerprint {
260 algorithm: actual.algorithm.clone(),
261 value: expected_fingerprint.to_string(),
262 };
263 let is_valid = fingerprint_validator
264 .verify_fingerprint(&expected, &actual)
265 .await
266 .unwrap_or(false);
267 if is_valid {
268 println!(" โโ โ
Fingerprint match");
269 } else {
270 println!(" โโ โ Fingerprint match");
271 failures.push(format!("Fingerprint mismatch for '{}'", service.name));
272 }
273 }
274 Err(e) => {
275 println!(" โโ โ Fingerprint check");
276 failures.push(format!("Fingerprint check failed: {e}"));
277 }
278 }
279 } else {
280 println!(" โโ โ ๏ธ Fingerprint missing; skipping check");
281 }
282
283 if check_conflicts {
284 let mut resolved = Vec::with_capacity(existing_specs.len() + 1);
285 for spec in existing_specs {
286 resolved.push(ResolvedDependency {
287 spec: spec.clone(),
288 fingerprint: spec.fingerprint.clone().unwrap_or_default(),
289 proto_files: Vec::new(),
290 });
291 }
292 resolved.push(ResolvedDependency {
293 spec: dependency_spec.clone(),
294 fingerprint: dependency_spec.fingerprint.clone().unwrap_or_default(),
295 proto_files: Vec::new(),
296 });
297
298 match dependency_resolver.check_conflicts(&resolved).await {
299 Ok(conflicts) => {
300 if conflicts.is_empty() {
301 println!(" โโ โ
Dependency conflicts");
302 } else {
303 println!(" โโ โ Dependency conflicts");
304 let details = conflicts
305 .iter()
306 .map(|conflict| conflict.description.clone())
307 .collect::<Vec<_>>()
308 .join(", ");
309 failures.push(format!("Dependency conflicts: {details}"));
310 }
311 }
312 Err(e) => {
313 println!(" โโ โ Dependency conflicts");
314 failures.push(format!("Dependency conflict check failed: {e}"));
315 }
316 }
317 } else {
318 println!(" โโ โ ๏ธ Dependency conflict check skipped (already configured)");
319 }
320
321 if failures.is_empty() {
322 println!(" โโ โ
Validation passed");
323 Ok(())
324 } else {
325 println!(" โโ โ Validation failed");
326 Err(ActrCliError::ValidationFailed {
327 details: failures.join("; "),
328 }
329 .into())
330 }
331 }
332
333 fn display_services_table(&self, services: &[ServiceInfo]) {
335 println!();
336 const TOTAL_MAX_WIDTH: usize = 160;
338 const BORDER_OVERHEAD: usize = 7;
340
341 let name_width = services
343 .iter()
344 .map(|s| s.name.chars().count())
345 .max()
346 .unwrap_or(0)
347 .max("Service Name".len());
348
349 let tags_width = services
350 .iter()
351 .map(|s| s.tags.join(", ").chars().count())
352 .max()
353 .unwrap_or(0)
354 .max("Tags".len());
355
356 let desc_width = services
357 .iter()
358 .map(|s| {
359 s.description
360 .as_deref()
361 .unwrap_or("No description")
362 .chars()
363 .count()
364 })
365 .max()
366 .unwrap_or(0)
367 .max("Description".len());
368
369 let name_w = name_width;
370 let tags_w = tags_width;
371 let mut desc_w = desc_width;
372
373 if name_w + tags_w + desc_w + BORDER_OVERHEAD > TOTAL_MAX_WIDTH {
375 let available = TOTAL_MAX_WIDTH - BORDER_OVERHEAD;
376 let used = name_w + tags_w;
377 desc_w = available.saturating_sub(used).max(10); }
379
380 let top_border = format!(
382 "โโ{}โโฌโ{}โโฌโ{}โโ",
383 "โ".repeat(name_w),
384 "โ".repeat(tags_w),
385 "โ".repeat(desc_w)
386 );
387 let header = format!(
388 "โ {:width$} โ {:tags_w$} โ {:desc_w$} โ",
389 "Service Name",
390 "Tags",
391 "Description",
392 width = name_w,
393 tags_w = tags_w,
394 desc_w = desc_w
395 );
396 let separator = format!(
397 "โโ{}โโผโ{}โโผโ{}โโค",
398 "โ".repeat(name_w),
399 "โ".repeat(tags_w),
400 "โ".repeat(desc_w)
401 );
402 let bottom_border = format!(
403 "โโ{}โโดโ{}โโดโ{}โโ",
404 "โ".repeat(name_w),
405 "โ".repeat(tags_w),
406 "โ".repeat(desc_w)
407 );
408
409 println!("{top_border}");
410 println!("{header}");
411 println!("{separator}");
412
413 for service in services {
414 let tags_str = service.tags.join(", ");
415 let description = service
416 .description
417 .as_deref()
418 .unwrap_or("No description")
419 .chars()
420 .take(desc_w)
421 .collect::<String>();
422
423 println!(
424 "โ {:name_w$} โ {:tags_w$} โ {:desc_w$} โ",
425 service.name,
426 tags_str.chars().take(tags_w).collect::<String>(),
427 description,
428 name_w = name_w,
429 tags_w = tags_w,
430 desc_w = desc_w
431 );
432 }
433
434 println!("{bottom_border}");
435 println!();
436 }
437
438 fn display_service_info(&self, service: &ServiceInfo) {
440 println!("๐ Selected service: {}", service.name);
441 if let Some(desc) = &service.description {
442 println!("๐ Description: {desc}");
443 }
444 println!("๐ Fingerprint: {}", service.fingerprint);
445 let time = service
446 .published_at
447 .and_then(|published_at| chrono::DateTime::from_timestamp(published_at, 0))
448 .map(|dt| {
449 dt.with_timezone(&chrono::Local)
450 .format("%Y-%m-%d %H:%M:%S")
451 .to_string()
452 })
453 .unwrap_or_else(|| "Unknown".to_string());
454 println!("๐
Publication Time: {}", time);
455 println!(
456 "๐ท๏ธ Tags: {}",
457 if service.tags.is_empty() {
458 "(none)".to_string()
459 } else {
460 service.tags.join(", ")
461 }
462 );
463 println!("๐ Methods count: {}", service.methods.len());
464 println!();
465 }
466
467 #[allow(unused)]
468 fn display_service_details(&self, details: &ServiceDetails) {
470 println!("๐ {} Detailed Information:", details.info.name);
471 println!("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ");
472 self.display_service_info(&details.info);
473 println!("๐ Available Methods:");
474 if details.info.methods.is_empty() {
475 println!(" (None)");
476 } else {
477 for method in &details.info.methods {
478 println!(
479 " โข {}: {} โ {}",
480 method.name, method.input_type, method.output_type
481 );
482 }
483 }
484
485 if !details.dependencies.is_empty() {
486 println!();
487 println!("๐ Dependent Services:");
488 for dep in &details.dependencies {
489 println!(" โข {dep}");
490 }
491 }
492
493 println!();
494 println!("๐ Proto Files:");
495 if details.proto_files.is_empty() {
496 println!(" (None)");
497 } else {
498 for proto in &details.proto_files {
499 println!(" โข {} ({} services)", proto.name, proto.services.len());
500 }
501 }
502
503 println!();
504 }
505
506 async fn export_proto_files(
508 &self,
509 service: &ServiceInfo,
510 service_discovery: &std::sync::Arc<dyn ServiceDiscovery>,
511 config_manager: &std::sync::Arc<dyn ConfigManager>,
512 ) -> Result<()> {
513 println!("๐ค Exporting proto files for {}...", service.name);
514
515 let proto_files = service_discovery.get_service_proto(&service.name).await?;
516
517 let output_dir = config_manager
518 .get_project_root()
519 .join("exports")
520 .join("remote")
521 .join(&service.name);
522 std::fs::create_dir_all(&output_dir)?;
523
524 for proto in &proto_files {
525 let file_path = output_dir.join(&proto.name);
526 if let Some(parent) = file_path.parent() {
527 std::fs::create_dir_all(parent)?;
528 }
529 std::fs::write(&file_path, &proto.content)?;
530 println!("โ
Exported: {}", file_path.display());
531 }
532
533 println!("๐ Export completed, total {} files", proto_files.len());
534 Ok(())
535 }
536
537 async fn add_to_config_with_validation(
539 &self,
540 service: &ServiceInfo,
541 context: &CommandContext,
542 ) -> Result<CommandResult> {
543 let (
544 config_manager,
545 user_interface,
546 dependency_resolver,
547 service_discovery,
548 network_validator,
549 fingerprint_validator,
550 ) = {
551 let container = context.container.lock().unwrap();
552 (
553 container.get_config_manager()?,
554 container.get_user_interface()?,
555 container.get_dependency_resolver()?,
556 container.get_service_discovery()?,
557 container.get_network_validator()?,
558 container.get_fingerprint_validator()?,
559 )
560 };
561
562 let dependency_spec = DependencySpec {
564 alias: service.name.clone(),
565 actr_type: Some(service.actr_type.clone()),
566 name: service.name.clone(),
567 fingerprint: Some(service.fingerprint.clone()),
568 };
569
570 let config = config_manager
572 .load_config(
573 config_manager
574 .get_project_root()
575 .join("Actr.toml")
576 .as_path(),
577 )
578 .await?;
579
580 let existing_by_name = config
581 .dependencies
582 .iter()
583 .find(|dep| dep.name == service.name);
584 let existing_by_alias = config
585 .dependencies
586 .iter()
587 .find(|dep| dep.alias == dependency_spec.alias);
588
589 if let Some(existing) = existing_by_alias
590 && existing.name != service.name
591 {
592 return Err(ActrCliError::Dependency {
593 message: format!(
594 "Dependency alias '{}' already exists for '{}'",
595 existing.alias, existing.name
596 ),
597 }
598 .into());
599 }
600
601 let should_update_config = existing_by_name.is_none();
602 if let Some(existing) = existing_by_name {
603 println!(
604 "โน๏ธ Dependency with name '{}' already exists (alias: '{}')",
605 service.name, existing.alias
606 );
607 if let (Some(existing_fp), Some(discovered_fp)) = (
608 existing.fingerprint.as_deref(),
609 dependency_spec.fingerprint.as_deref(),
610 ) && existing_fp != discovered_fp
611 {
612 println!(
613 "โ ๏ธ Fingerprint mismatch: config '{}' vs discovery '{}'",
614 existing_fp, discovered_fp
615 );
616 }
617 println!(" Skipping configuration update");
618 }
619
620 let expected_fingerprint = existing_by_name
621 .and_then(|dep| dep.fingerprint.clone())
622 .or_else(|| dependency_spec.fingerprint.clone());
623 let existing_specs = dependency_resolver.resolve_spec(&config).await?;
624 self.validate_dependency(
625 service,
626 &dependency_spec,
627 expected_fingerprint.as_deref(),
628 should_update_config,
629 &existing_specs,
630 &dependency_resolver,
631 &service_discovery,
632 &network_validator,
633 &fingerprint_validator,
634 )
635 .await?;
636
637 if should_update_config {
638 println!("๐ Adding {} to configuration file...", service.name);
639 let backup = config_manager.backup_config().await?;
640 match config_manager.update_dependency(&dependency_spec).await {
641 Ok(_) => {
642 config_manager.remove_backup(backup).await?;
643 println!("โ
Added {} to configuration file", service.name);
644 }
645 Err(e) => {
646 config_manager.restore_backup(backup).await?;
647 return Err(ActrCliError::Config {
648 message: format!("Configuration update failed: {e}"),
649 }
650 .into());
651 }
652 }
653 }
654
655 println!();
657 let should_install = if self.auto_install {
658 true
659 } else {
660 user_interface
661 .confirm("๐ค Install this dependency now?")
662 .await?
663 };
664
665 if should_install {
666 println!();
668 println!("๐ฆ Installing {}...", service.name);
669
670 let install_pipeline = {
671 let mut container = context.container.lock().unwrap();
672 match container.get_install_pipeline() {
673 Ok(pipeline) => pipeline,
674 Err(_) => {
675 println!("โน๏ธ Install pipeline is not implemented yet; skipping.");
676 return Ok(CommandResult::Success(
677 "Dependency added; install pending".to_string(),
678 ));
679 }
680 }
681 };
682
683 match install_pipeline
684 .install_dependencies(&[dependency_spec])
685 .await
686 {
687 Ok(install_result) => {
688 println!(" โโ ๐ฆ Cache proto files โ
");
689 println!(" โโ ๐ Update lock file โ
");
690 println!(" โโ โ
Installation complete");
691 println!();
692 println!("๐ก Tip: Run 'actr gen' to generate the latest code");
693
694 Ok(CommandResult::Install(install_result))
695 }
696 Err(e) => {
697 eprintln!("โ Installation failed: {e}");
698 Ok(CommandResult::Success(
699 "Dependency added but installation failed".to_string(),
700 ))
701 }
702 }
703 } else {
704 println!("โ
Dependency added to configuration file");
705 println!("๐ก Tip: Run 'actr install' to install dependencies");
706 Ok(CommandResult::Success(
707 "Dependency added to configuration".to_string(),
708 ))
709 }
710 }
711}
712
713impl Default for DiscoveryCommand {
714 fn default() -> Self {
715 Self::new(None, false, false)
716 }
717}
718
719#[cfg(test)]
720mod tests {
721 use super::*;
722
723 #[test]
724 fn test_create_service_filter() {
725 let cmd = DiscoveryCommand::new(Some("user-*".to_string()), false, false);
726 let filter = cmd.create_service_filter();
727
728 assert!(filter.is_some());
729 let filter = filter.unwrap();
730 assert_eq!(filter.name_pattern, Some("user-*".to_string()));
731 }
732
733 #[test]
734 fn test_create_service_filter_none() {
735 let cmd = DiscoveryCommand::new(None, false, false);
736 let filter = cmd.create_service_filter();
737
738 assert!(filter.is_none());
739 }
740
741 #[test]
742 fn test_required_components() {
743 let cmd = DiscoveryCommand::default();
744 let components = cmd.required_components();
745
746 assert!(components.contains(&ComponentType::ServiceDiscovery));
748 assert!(components.contains(&ComponentType::UserInterface));
749 assert!(components.contains(&ComponentType::ConfigManager));
750 assert!(components.contains(&ComponentType::DependencyResolver));
751 assert!(components.contains(&ComponentType::NetworkValidator));
752 assert!(components.contains(&ComponentType::FingerprintValidator));
753 assert!(!components.contains(&ComponentType::CacheManager));
754 assert!(!components.contains(&ComponentType::ProtoProcessor));
755 }
756}