actr_cli/commands/
check.rs1use 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#[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 #[arg(value_name = "SERVICE_NAME")]
26 pub packages: Vec<String>,
27
28 #[arg(short = 'm', long = "manifest-path")]
30 pub manifest_path: Option<String>,
31
32 #[arg(short, long)]
34 pub verbose: bool,
35
36 #[arg(long, default_value = "10")]
38 pub timeout: u64,
39
40 #[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 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 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 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 let dep_validations = pipeline.validate_dependencies(&specs_to_check).await?;
127
128 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 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 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 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 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 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 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 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}