actr_cli/commands/
check.rs1use 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#[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 #[arg(value_name = "SERVICE_NAME")]
28 pub packages: Vec<String>,
29
30 #[arg(short = 'f', long = "file")]
32 pub config_file: Option<String>,
33
34 #[arg(short, long)]
36 pub verbose: bool,
37
38 #[arg(long, default_value = "10")]
40 pub timeout: u64,
41
42 #[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 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 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 let dep_validations = pipeline.validate_dependencies(&specs_to_check).await?;
138
139 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 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 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 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 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 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 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 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}