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