clnrm_core/cli/
mod.rs

1//! CLI module for the cleanroom testing framework
2//!
3//! Provides a professional command-line interface using clap for running tests,
4//! managing services, and generating reports.
5
6// Allow shadow warnings - we intentionally import items for internal use
7// while also re-exporting them via glob exports at the end of the module
8#![allow(hidden_glob_reexports)]
9
10pub mod commands;
11pub mod noun_verb_integration;
12pub mod telemetry;
13pub mod types;
14pub mod utils;
15
16use crate::error::Result;
17use clap::Parser;
18use std::path::PathBuf;
19use tracing::error;
20
21// Import utilities - using explicit paths to avoid shadowing pub use exports
22use self::commands::run::run_tests_with_shard_and_report;
23use self::types::{Cli, Commands};
24use self::utils::setup_logging;
25
26// Import all command functions - using self:: to avoid shadowing pub use exports
27use self::commands::health::system_health_check;
28use self::commands::init::init_project;
29use self::commands::report::generate_report;
30use self::commands::validate::validate_config;
31
32// Remove global config - we'll load it per command as needed
33
34/// Main CLI entry point
35pub async fn run_cli() -> Result<()> {
36    let cli = Cli::parse();
37
38    // Set up logging based on verbosity
39    setup_logging(cli.verbose)?;
40
41    let result = match cli.command {
42        Commands::Run {
43            paths,
44            parallel,
45            jobs,
46            fail_fast,
47            watch,
48            force,
49            shard,
50            digest,
51            report_junit,
52            validate,
53            otel_exporter,
54            otel_endpoint,
55            live_check,
56            validation_mode,
57            registry_path,
58            otlp_port,
59            admin_port,
60            diagnostic_format,
61            stop_timeout,
62        } => {
63            // CLI flags take precedence: --live-check or --validate enables validation
64            let should_validate = validate || live_check;
65
66            let config = crate::cli::types::CliConfig {
67                parallel,
68                jobs,
69                format: cli.format.clone(),
70                fail_fast,
71                watch,
72                verbose: cli.verbose,
73                force,
74                digest,
75                validate: should_validate,
76            };
77
78            // If no paths provided, discover all test files automatically
79            let paths_to_run = if let Some(paths) = paths {
80                paths
81            } else {
82                // Default behavior: discover all test files
83                vec![PathBuf::from(".")]
84            };
85
86            // TODO: Pass CLI validation parameters to executor
87            // For now, these are stored but not yet used in the executor
88            // Phase 3 will integrate validation_mode, registry_path, etc.
89            let _ = (
90                validation_mode,
91                registry_path,
92                otlp_port,
93                admin_port,
94                diagnostic_format,
95                stop_timeout,
96            );
97
98            run_tests_with_shard_and_report(
99                &paths_to_run,
100                &config,
101                shard,
102                report_junit.as_deref(),
103                &otel_exporter,
104                otel_endpoint.as_deref(),
105            )
106            .await
107        }
108
109        Commands::Validate { files } => {
110            for file in files {
111                validate_config(&file)?;
112            }
113            Ok(())
114        }
115
116        Commands::Init { force, config } => {
117            init_project(force, config)?;
118            Ok(())
119        }
120
121        Commands::Template {
122            template,
123            name,
124            output,
125        } => {
126            // Handle template types that generate TOML files (v0.6.0 Tera templates)
127            let template_result = match template.as_str() {
128                "otel" => Some((generate_otel_template()?, "OTEL validation template")),
129                "matrix" => Some((generate_matrix_template()?, "Matrix testing template")),
130                "macros" | "macro-library" => {
131                    Some((generate_macro_library()?, "Tera macro library"))
132                }
133                "full-validation" | "validation" => Some((
134                    generate_full_validation_template()?,
135                    "Full validation template",
136                )),
137                "deterministic" => Some((
138                    generate_deterministic_template()?,
139                    "Deterministic testing template",
140                )),
141                "lifecycle-matcher" => {
142                    Some((generate_lifecycle_matcher()?, "Lifecycle matcher template"))
143                }
144                _ => None,
145            };
146
147            if let Some((content, description)) = template_result {
148                // Template file generation
149                if let Some(output_path) = output {
150                    std::fs::write(&output_path, &content).map_err(|e| {
151                        crate::error::CleanroomError::io_error(format!(
152                            "Failed to write template to {}: {}",
153                            output_path.display(),
154                            e
155                        ))
156                    })?;
157                    println!("✓ {} generated: {}", description, output_path.display());
158                } else {
159                    println!("{}", content);
160                }
161                Ok(())
162            } else {
163                // Regular project template (default, advanced, minimal, database, api)
164                generate_from_template(&template, name.as_deref())?;
165                Ok(())
166            }
167        }
168
169        Commands::Plugins => {
170            list_plugins()?;
171            Ok(())
172        }
173
174        Commands::Services { command } => match command {
175            ServiceCommands::Status => {
176                show_service_status().await?;
177                Ok(())
178            }
179            ServiceCommands::Logs { service, lines } => {
180                show_service_logs(&service, lines).await?;
181                Ok(())
182            }
183            ServiceCommands::Restart { service } => {
184                restart_service(&service).await?;
185                Ok(())
186            }
187            #[cfg(feature = "ai")]
188            ServiceCommands::AiManage {
189                auto_scale: _,
190                predict_load: _,
191                optimize_resources: _,
192                horizon_minutes: _,
193                service: _,
194            } => Err(crate::error::CleanroomError::validation_error(
195                "AI service management is not available in this version.",
196            )),
197        },
198
199        Commands::Report {
200            input,
201            output,
202            format,
203        } => {
204            let format_str = match format {
205                ReportFormat::Html => "html",
206                ReportFormat::Markdown => "markdown",
207                ReportFormat::Json => "json",
208                ReportFormat::Pdf => "pdf",
209            };
210            generate_report(input.as_ref(), output.as_ref(), format_str).await?;
211            Ok(())
212        }
213
214        Commands::SelfTest {
215            suite,
216            report,
217            otel_exporter,
218            otel_endpoint,
219        } => {
220            run_self_tests(suite, report, otel_exporter, otel_endpoint).await?;
221            Ok(())
222        }
223
224        #[cfg(feature = "ai")]
225        Commands::AiOrchestrate {
226            paths: _,
227            predict_failures: _,
228            auto_optimize: _,
229            confidence_threshold: _,
230            max_workers: _,
231        } => Err(crate::error::CleanroomError::validation_error(
232            "AI orchestration is not available in this version.",
233        )),
234
235        #[cfg(feature = "ai")]
236        Commands::AiPredict {
237            analyze_history: _,
238            predict_failures: _,
239            recommendations: _,
240            format: _,
241        } => Err(crate::error::CleanroomError::validation_error(
242            "AI predictive analytics is not available in this version.",
243        )),
244
245        #[cfg(feature = "ai")]
246        Commands::AiOptimize {
247            execution_order: _,
248            resource_allocation: _,
249            parallel_execution: _,
250            auto_apply: _,
251        } => Err(crate::error::CleanroomError::validation_error(
252            "AI test optimization is not available in this version.",
253        )),
254
255        #[cfg(feature = "ai")]
256        Commands::AiReal { analyze: _ } => Err(crate::error::CleanroomError::validation_error(
257            "AI real-time analysis is not available in this version.",
258        )),
259
260        Commands::Health { verbose } => system_health_check(verbose).await,
261
262        Commands::Fmt {
263            files,
264            check,
265            verify,
266        } => {
267            format_files(&files, check, verify)?;
268            Ok(())
269        }
270
271        Commands::DryRun { files, verbose } => {
272            use crate::CleanroomError;
273            let file_refs: Vec<_> = files.iter().map(|p| p.as_path()).collect();
274            let results = dry_run_validate(file_refs, verbose)?;
275
276            // Count failures
277            let failed_count = results.iter().filter(|r| !r.valid).count();
278
279            // Exit with error if any validations failed
280            if failed_count > 0 {
281                return Err(CleanroomError::validation_error(format!(
282                    "{} file(s) failed validation",
283                    failed_count
284                )));
285            }
286
287            Ok(())
288        }
289
290        Commands::Dev {
291            paths,
292            debounce_ms,
293            clear,
294            only,
295            timebox,
296        } => {
297            let config = crate::cli::types::CliConfig {
298                format: cli.format.clone(),
299                verbose: cli.verbose,
300                ..Default::default()
301            };
302
303            run_dev_mode_with_filters(paths, debounce_ms, clear, only, timebox, config).await
304        }
305
306        Commands::Lint {
307            files,
308            format,
309            deny_warnings,
310        } => {
311            let file_refs: Vec<_> = files.iter().map(|p| p.as_path()).collect();
312
313            // Convert format enum to string
314            let format_str = match format {
315                crate::cli::types::LintFormat::Human => "human",
316                crate::cli::types::LintFormat::Json => "json",
317                crate::cli::types::LintFormat::Github => "github",
318            };
319
320            // This will print diagnostics and return error if needed
321            lint_files(file_refs, format_str, deny_warnings)?;
322
323            Ok(())
324        }
325
326        Commands::Diff {
327            baseline,
328            current,
329            format,
330            only_changes,
331        } => {
332            // Convert format enum to string
333            let format_str = match format {
334                crate::cli::types::DiffFormat::Tree => "tree",
335                crate::cli::types::DiffFormat::Json => "json",
336                crate::cli::types::DiffFormat::SideBySide => "side-by-side",
337            };
338
339            let result = diff_traces(&baseline, &current, format_str, only_changes)?;
340
341            // Exit with error code if differences found
342            if result.added_count > 0 || result.removed_count > 0 || result.modified_count > 0 {
343                std::process::exit(1);
344            }
345
346            Ok(())
347        }
348
349        Commands::Record { paths, output } => run_record(paths, output).await,
350
351        #[cfg(feature = "ai")]
352        Commands::AiMonitor {
353            interval: _,
354            anomaly_threshold: _,
355            ai_alerts: _,
356            anomaly_detection: _,
357            proactive_healing: _,
358            webhook_url: _,
359        } => Err(crate::error::CleanroomError::validation_error(
360            "AI monitoring is not available in this version.",
361        )),
362
363        // PRD v1.0 additional commands
364        Commands::Pull {
365            paths,
366            parallel,
367            jobs,
368        } => pull_images(paths, parallel, jobs).await,
369
370        Commands::Graph {
371            trace,
372            format,
373            highlight_missing,
374            filter,
375        } => visualize_graph(&trace, &format, highlight_missing, filter.as_deref()),
376
377        Commands::Repro {
378            baseline,
379            verify_digest,
380            output,
381        } => reproduce_baseline(&baseline, verify_digest, output.as_deref()).await,
382
383        Commands::RedGreen {
384            paths,
385            expect,
386            verify_red,
387            verify_green,
388        } => {
389            // Handle new --expect flag or fall back to deprecated flags
390            let (should_verify_red, should_verify_green) = match expect {
391                Some(crate::cli::types::TddState::Red) => (true, false),
392                Some(crate::cli::types::TddState::Green) => (false, true),
393                None => (verify_red, verify_green),
394            };
395            run_red_green_validation(&paths, should_verify_red, should_verify_green).await
396        }
397
398        Commands::Render {
399            template,
400            map,
401            output,
402            show_vars,
403        } => {
404            // Join map Vec<String> into JSON string format
405            let map_str = if map.is_empty() {
406                "{}".to_string()
407            } else {
408                // Convert Vec<String> of "key=value" pairs to JSON object
409                let mut json_map = std::collections::HashMap::new();
410                for pair in &map {
411                    if let Some((key, value)) = pair.split_once('=') {
412                        json_map.insert(
413                            key.to_string(),
414                            serde_json::Value::String(value.to_string()),
415                        );
416                    }
417                }
418                serde_json::to_string(&json_map).map_err(|e| {
419                    crate::error::CleanroomError::serialization_error(format!(
420                        "Failed to serialize map: {}",
421                        e
422                    ))
423                })?
424            };
425            render_template_with_vars(&template, &map_str, output.as_deref(), show_vars)?;
426            Ok(())
427        }
428
429        Commands::Spans {
430            trace,
431            grep,
432            format,
433            show_attrs,
434            show_events,
435        } => filter_spans(&trace, grep.as_deref(), &format, show_attrs, show_events),
436
437        Commands::Collector { command } => match command {
438            crate::cli::types::CollectorCommands::Up {
439                image,
440                http_port,
441                grpc_port,
442                detach,
443            } => start_collector(&image, http_port, grpc_port, detach).await,
444            crate::cli::types::CollectorCommands::Down { volumes } => stop_collector(volumes).await,
445            crate::cli::types::CollectorCommands::Status => show_collector_status().await,
446            crate::cli::types::CollectorCommands::Logs { lines, follow } => {
447                show_collector_logs(lines, follow).await
448            }
449        },
450
451        Commands::Analyze { test_file, traces } => {
452            use crate::cli::commands::analyze::analyze_traces;
453
454            match analyze_traces(&test_file, traces.as_deref()) {
455                Ok(report) => {
456                    println!("{}", report.format_report());
457
458                    // Exit with code 1 if any validator failed
459                    if !report.is_success() {
460                        std::process::exit(1);
461                    }
462                    Ok(())
463                }
464                Err(e) => {
465                    eprintln!("Error analyzing traces: {}", e);
466                    std::process::exit(1);
467                }
468            }
469        }
470
471        Commands::LiveCheck { command } => match command {
472            crate::cli::types::LiveCheckCommands::Status => show_status(),
473            crate::cli::types::LiveCheckCommands::ValidateRegistry { registry } => {
474                validate_registry(&registry)
475            }
476            crate::cli::types::LiveCheckCommands::TestWeaver => test_weaver(),
477            crate::cli::types::LiveCheckCommands::Modes => show_modes(),
478            crate::cli::types::LiveCheckCommands::Version => show_version(),
479        },
480    };
481
482    if let Err(e) = result {
483        error!("Command failed: {}", e);
484        std::process::exit(1);
485    }
486
487    Ok(())
488}
489
490// Re-export all public types and functions for backward compatibility
491pub use commands::*;
492pub use types::*;