Skip to main content

actr_cli/commands/
check.rs

1//! Check command implementation - verify Actor-RTC service availability
2//!
3//! The check command validates that services are available in the registry
4//! and optionally verifies they match the configured dependencies.
5
6use crate::core::{Command, CommandContext, CommandResult, ComponentType, NetworkCheckOptions};
7use actr_config::ConfigParser;
8use anyhow::{Context, Result};
9use async_trait::async_trait;
10use clap::Args;
11use comfy_table::{Attribute, Cell, Color, Table};
12use futures_util::future;
13use owo_colors::OwoColorize;
14use tracing::info;
15
16/// Check command - validates service availability
17#[derive(Args, Debug)]
18#[command(
19    about = "Validate project dependencies",
20    long_about = "Validate that services are available in the registry and match the configured dependencies"
21)]
22pub struct CheckCommand {
23    /// Service names to check (e.g., "user-service", "order-service")
24    /// If not provided, checks all services from the configuration file
25    #[arg(value_name = "SERVICE_NAME")]
26    pub packages: Vec<String>,
27
28    /// Manifest file to load services from (defaults to manifest.toml)
29    #[arg(short = 'm', long = "manifest-path")]
30    pub manifest_path: Option<String>,
31
32    /// Show detailed connection information
33    #[arg(short, long)]
34    pub verbose: bool,
35
36    /// Timeout for each service check in seconds
37    #[arg(long, default_value = "10")]
38    pub timeout: u64,
39
40    /// Also verify services are installed in manifest.lock.toml
41    #[arg(long)]
42    pub lock: bool,
43}
44
45#[async_trait]
46impl Command for CheckCommand {
47    async fn execute(&self, context: &CommandContext) -> Result<CommandResult> {
48        let config_path = self.manifest_path.as_deref().unwrap_or("manifest.toml");
49
50        let pipeline = {
51            let mut container = context.container.lock().unwrap();
52            container.get_validation_pipeline()?
53        };
54        let options = NetworkCheckOptions::with_timeout_secs(self.timeout);
55
56        println!("🔍 Starting dependency validation...");
57        info!("🔍 Starting dependency validation...");
58
59        // 1. Validate Config and Signaling Server
60        let config_validation = pipeline.config_manager().validate_config().await?;
61        if !config_validation.is_valid {
62            let mut msg = format!("{} Configuration validation failed:\n", "❌".red());
63            for err in config_validation.errors {
64                msg.push_str(&format!("  - {}\n", err.red()));
65            }
66            return Ok(CommandResult::Error(msg));
67        }
68
69        let config = ConfigParser::from_manifest_file(config_path)
70            .with_context(|| format!("Failed to load manifest: {}", config_path))?;
71
72        // Resolve signaling URL from default + global CLI config + project CLI config.
73        // `resolve_effective_cli_config` applies built-in defaults, so we always have a URL.
74        let cli_config =
75            crate::config::resolver::resolve_effective_cli_config().unwrap_or_default();
76        let signaling_url = cli_config.network.signaling_url;
77
78        println!("🌐 Checking signaling server: {}...", signaling_url);
79        info!("🌐 Checking signaling server: {}...", signaling_url);
80        let signaling_status = pipeline
81            .network_validator()
82            .check_connectivity(&signaling_url, &options)
83            .await?;
84        if signaling_status.is_reachable {
85            let latency = signaling_status.response_time_ms.unwrap_or(0);
86            println!("  ✔ Signaling server is reachable ({}ms)", latency);
87            info!("  ✔ Signaling server is reachable ({}ms)", latency);
88        } else {
89            let err = signaling_status
90                .error
91                .unwrap_or_else(|| "Unknown error".to_string());
92            return Ok(CommandResult::Error(format!(
93                "{} Signaling server unreachable: {}",
94                "❌".red(),
95                err.red()
96            )));
97        }
98
99        // 2. Resolve Dependencies to check
100
101        let all_specs = pipeline.dependency_resolver().resolve_spec(&config).await?;
102
103        let specs_to_check = if self.packages.is_empty() {
104            all_specs
105        } else {
106            all_specs
107                .into_iter()
108                .filter(|s| self.packages.contains(&s.name) || self.packages.contains(&s.alias))
109                .collect()
110        };
111
112        if specs_to_check.is_empty() {
113            if self.packages.is_empty() {
114                return Ok(CommandResult::Success(
115                    "No dependencies to check".to_string(),
116                ));
117            } else {
118                return Ok(CommandResult::Error(format!(
119                    "None of the specified packages found in {}",
120                    config_path
121                )));
122            }
123        }
124
125        // 3. Perform Validation via Pipeline
126        let dep_validations = pipeline.validate_dependencies(&specs_to_check).await?;
127
128        // 3.1 Lock File Validation (if requested)
129        if self.lock {
130            println!("🔒 Verifying lock file integrity...");
131            info!("🔒 Verifying lock file integrity...");
132            let lock_path = std::path::Path::new("manifest.lock.toml");
133            if !lock_path.exists() {
134                return Ok(CommandResult::Error(
135                    "manifest.lock.toml not found".to_string(),
136                ));
137            }
138
139            let lock_file = actr_config::LockFile::from_file(lock_path)
140                .map_err(|e| anyhow::anyhow!("Failed to read lock file: {}", e))?;
141
142            for spec in &specs_to_check {
143                if let Some(locked) = lock_file.get_dependency(&spec.name) {
144                    // Check if versions/types match if necessary
145                    if let Some(spec_fp) = &spec.fingerprint
146                        && spec_fp != &locked.fingerprint
147                    {
148                        return Ok(CommandResult::Error(format!(
149                            "{} Fingerprint mismatch for '{}' in lock file:\n  Expected: {}\n  Locked:   {}",
150                            "❌".red(),
151                            spec.alias,
152                            spec_fp,
153                            locked.fingerprint
154                        )));
155                    }
156                } else {
157                    return Ok(CommandResult::Error(format!(
158                        "{} Dependency '{}' not found in manifest.lock.toml",
159                        "❌".red(),
160                        spec.alias
161                    )));
162                }
163            }
164            println!("  ✔ Lock file integrity verified");
165            info!("  ✔ Lock file integrity verified");
166        }
167
168        // For network and fingerprint, we need ResolvedDependency
169        // Use parallel fetch for service details to improve performance
170        let fetch_futures = specs_to_check.iter().map(|spec| {
171            let sd = pipeline.service_discovery().clone();
172            let name = spec.name.clone();
173            async move {
174                let details = sd.get_service_details(&name).await;
175                (name, details)
176            }
177        });
178
179        let fetch_results = future::join_all(fetch_futures).await;
180
181        let mut service_details_map = std::collections::HashMap::new();
182        let mut fetch_errors: Vec<(String, anyhow::Error)> = Vec::new();
183
184        for (name, result) in fetch_results {
185            match result {
186                Ok(details) => {
187                    service_details_map.insert(name, details);
188                }
189                Err(e) => {
190                    fetch_errors.push((name, e));
191                }
192            }
193        }
194
195        let resolved_deps: Vec<_> = specs_to_check
196            .iter()
197            .map(|spec| {
198                let details = service_details_map.get(&spec.name);
199                crate::core::ResolvedDependency {
200                    spec: spec.clone(),
201                    fingerprint: details
202                        .map(|d| d.info.fingerprint.clone())
203                        .unwrap_or_default(),
204                    proto_files: details.map(|d| d.proto_files.clone()).unwrap_or_default(),
205                }
206            })
207            .collect();
208
209        let net_validations = pipeline
210            .validate_network_connectivity(&resolved_deps, &options)
211            .await?;
212        let fp_validations = pipeline.validate_fingerprints(&resolved_deps).await?;
213
214        // 4. Report Results
215        let mut table = Table::new();
216        table.set_header(vec![
217            Cell::new("Dependency").add_attribute(Attribute::Bold),
218            Cell::new("Availability").add_attribute(Attribute::Bold),
219            Cell::new("Network").add_attribute(Attribute::Bold),
220            Cell::new("Fingerprint").add_attribute(Attribute::Bold),
221        ]);
222
223        let mut all_ok = true;
224
225        for i in 0..specs_to_check.len() {
226            let spec = &specs_to_check[i];
227            let dep_v = &dep_validations[i];
228            let net_v = &net_validations[i];
229            let fp_v = &fp_validations[i];
230
231            let mut row = vec![Cell::new(&spec.alias)];
232
233            // Availability
234            if dep_v.is_available {
235                row.push(Cell::new("✔ Available").fg(Color::Green));
236            } else {
237                let err_msg = if self.verbose {
238                    dep_v.error.as_deref().unwrap_or("Unknown error")
239                } else {
240                    "Missing"
241                };
242                row.push(Cell::new(format!("✘ {}", err_msg)).fg(Color::Red));
243                all_ok = false;
244            }
245
246            // Network
247            if !net_v.is_applicable {
248                let cell_text = if self.verbose {
249                    net_v.error.clone().unwrap_or_else(|| "N/A".to_string())
250                } else {
251                    "N/A".to_string()
252                };
253                row.push(Cell::new(cell_text).fg(Color::Yellow));
254            } else if net_v.is_reachable {
255                let latency = net_v
256                    .latency_ms
257                    .map(|l| format!(" ({}ms)", l))
258                    .unwrap_or_default();
259                row.push(Cell::new(format!("✔ Reachable{}", latency)).fg(Color::Green));
260            } else {
261                // If service detail fetch failed earlier, network check will likely fail too.
262                // Check if we had a fetch error for this service
263                let fetch_err = fetch_errors
264                    .iter()
265                    .find(|(n, _)| n == &spec.name)
266                    .map(|(_, e)| e.to_string());
267
268                let err_display = if self.verbose {
269                    if let Some(fe) = fetch_err {
270                        format!("Fetch Error: {}", fe)
271                    } else {
272                        net_v
273                            .error
274                            .clone()
275                            .unwrap_or_else(|| "Unreachable".to_string())
276                    }
277                } else {
278                    "✘ Unreachable".to_string()
279                };
280
281                row.push(Cell::new(err_display).fg(Color::Red));
282                all_ok = false;
283            }
284
285            // Fingerprint
286            if fp_v.is_valid {
287                row.push(Cell::new("✔ Match").fg(Color::Green));
288            } else {
289                let mut cell_text = "✘ Mismatch".to_string();
290                if self.verbose {
291                    let expected = &fp_v.expected.value;
292                    let actual_opt = fp_v.actual.as_ref().map(|f| &f.value);
293
294                    if let Some(actual) = actual_opt {
295                        if !expected.is_empty() {
296                            cell_text = format!(
297                                "✘ Mismatch\n  Exp: {:.8}...\n  Act: {:.8}...",
298                                expected, actual
299                            );
300                        }
301                    } else if let Some(err) = &fp_v.error {
302                        cell_text = format!("✘ Error: {}", err);
303                    }
304                }
305                row.push(Cell::new(cell_text).fg(Color::Red));
306                all_ok = false;
307            }
308
309            table.add_row(row);
310        }
311
312        println!("\n{table}");
313
314        if all_ok {
315            Ok(CommandResult::Success(format!(
316                "\n{} All {} services passed validation!",
317                "✨".green(),
318                specs_to_check.len()
319            )))
320        } else {
321            Ok(CommandResult::Error(format!(
322                "\n{} Some services failed validation. Run with --verbose for details.",
323                "⚠️".yellow()
324            )))
325        }
326    }
327
328    fn required_components(&self) -> Vec<ComponentType> {
329        vec![
330            ComponentType::ConfigManager,
331            ComponentType::DependencyResolver,
332            ComponentType::ServiceDiscovery,
333            ComponentType::NetworkValidator,
334            ComponentType::FingerprintValidator,
335        ]
336    }
337
338    fn name(&self) -> &str {
339        "check"
340    }
341
342    fn description(&self) -> &str {
343        "Validate project dependencies and service availability"
344    }
345}