Skip to main content

actr_cli/commands/
discovery.rs

1//! Discovery Command Implementation
2//!
3//! Demonstrates multi-level reuse patterns: Service Discovery -> Validation -> Optional Install
4
5use 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/// Discovery command
16#[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    /// Service name filter pattern (e.g., user-*)
23    #[arg(long, value_name = "PATTERN")]
24    pub filter: Option<String>,
25
26    /// Show detailed information
27    #[arg(long)]
28    pub verbose: bool,
29
30    /// Automatically install selected services
31    #[arg(long)]
32    pub auto_install: bool,
33
34    /// List discovered services and exit without interactive selection
35    #[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        // Get reusable components
43        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        // Phase 1: Service Discovery
53
54        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        // Display discovered services table
65        self.display_services_table(&services);
66
67        if self.list_only {
68            return Ok(CommandResult::Success("Services listed".to_string()));
69        }
70
71        // Selection Phase
72        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        // Action menu prompt
97        let menu_prompt = format!("Options for {}", selected_service.name);
98
99        // Action menu items (as shown in screenshot)
100        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                // Export proto files
133                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                // Add to configuration file - core flow of reuse architecture
139                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        // Components needed for Discovery command
148        vec![
149            ComponentType::ServiceDiscovery, // Core service discovery
150            ComponentType::UserInterface,    // User interface
151            ComponentType::ConfigManager,    // Configuration management
152            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    // Create from clap Args
178    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    /// Create service filter
188    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    /// Display services table
344    fn display_services_table(&self, services: &[ServiceInfo]) {
345        println!();
346        // Total width limit is 160
347        const TOTAL_MAX_WIDTH: usize = 160;
348        // Border and separator overhead
349        const BORDER_OVERHEAD: usize = 7;
350
351        // Calculate the maximum width of each column
352        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 the total width is exceeded, truncate the Description
384        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); // Description min 10 chars
388        }
389
390        // Generate table header
391        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    /// Display service info
449    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    /// Display service details
479    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    /// Export proto files
517    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    /// Add to configuration file - core flow of reuse architecture
548    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        // Convert to dependency spec
573        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        // Check if a dependency with the same name already exists
581        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        // Ask if user wants to install immediately
682        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            // Reuse install flow
693            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        // Discovery command requires validation components for check-first flow.
773        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}