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 ๅ‘ฝไปค
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
35#[async_trait]
36impl Command for DiscoveryCommand {
37    async fn execute(&self, context: &CommandContext) -> Result<CommandResult> {
38        // Get reusable components
39        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        // Phase 1: Service Discovery
49
50        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        // Display discovered services table
61        self.display_services_table(&services);
62
63        // Selection Phase
64        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        // Action menu prompt
89        let menu_prompt = format!("Options for {}", selected_service.name);
90
91        // Action menu items (as shown in screenshot)
92        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                // Export proto files
125                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                // Add to configuration file - core flow of reuse architecture
131                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        // Components needed for Discovery command
140        vec![
141            ComponentType::ServiceDiscovery, // Core service discovery
142            ComponentType::UserInterface,    // User interface
143            ComponentType::ConfigManager,    // Configuration management
144            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    // Create from clap Args
169    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    /// Create service filter
178    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    /// Display services table
334    fn display_services_table(&self, services: &[ServiceInfo]) {
335        println!();
336        // Total width limit is 160
337        const TOTAL_MAX_WIDTH: usize = 160;
338        // Border and separator overhead
339        const BORDER_OVERHEAD: usize = 7;
340
341        // Calculate the maximum width of each column
342        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 the total width is exceeded, truncate the Description
374        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); // Description ่‡ณๅฐ‘ 10 ๅญ—็ฌฆ
378        }
379
380        // Generate table header
381        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    /// Display service info
439    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    /// Display service details
469    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    /// Export proto files
507    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    /// Add to configuration file - core flow of reuse architecture
538    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        // Convert to dependency spec
563        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        // Check if a dependency with the same name already exists
571        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        // Ask if user wants to install immediately
656        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            // Reuse install flow
667            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        // Discovery command requires validation components for check-first flow.
747        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}