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
6pub mod commands;
7pub mod types;
8pub mod utils;
9
10use crate::error::Result;
11use clap::Parser;
12use std::path::PathBuf;
13use tracing::error;
14
15// Import utilities
16use crate::cli::utils::setup_logging;
17
18// Remove global config - we'll load it per command as needed
19
20/// Main CLI entry point
21pub async fn run_cli() -> Result<()> {
22    let cli = Cli::parse();
23
24    // Set up logging based on verbosity
25    setup_logging(cli.verbose)?;
26
27    let result = match cli.command {
28        Commands::Run {
29            paths,
30            parallel,
31            jobs,
32            fail_fast,
33            watch,
34            interactive,
35            force,
36            shard,
37        } => {
38            let config = crate::cli::types::CliConfig {
39                parallel,
40                jobs,
41                format: cli.format.clone(),
42                fail_fast,
43                watch,
44                interactive,
45                verbose: cli.verbose,
46                force,
47            };
48
49            // If no paths provided, discover all test files automatically
50            let paths_to_run = if let Some(paths) = paths {
51                paths
52            } else {
53                // Default behavior: discover all test files
54                vec![PathBuf::from(".")]
55            };
56
57            run_tests_with_shard(&paths_to_run, &config, shard).await
58        }
59
60        Commands::Validate { files } => {
61            for file in files {
62                validate_config(&file)?;
63            }
64            Ok(())
65        }
66
67        Commands::Init { force, config } => {
68            init_project(force, config)?;
69            Ok(())
70        }
71
72        Commands::Template { template, name, output } => {
73            // Handle template types that generate TOML files (v0.6.0 Tera templates)
74            let template_result = match template.as_str() {
75                "otel" => Some((generate_otel_template()?, "OTEL validation template")),
76                "matrix" => Some((generate_matrix_template()?, "Matrix testing template")),
77                "macros" | "macro-library" => Some((generate_macro_library()?, "Tera macro library")),
78                "full-validation" | "validation" => Some((generate_full_validation_template()?, "Full validation template")),
79                "deterministic" => Some((generate_deterministic_template()?, "Deterministic testing template")),
80                "lifecycle-matcher" => Some((generate_lifecycle_matcher()?, "Lifecycle matcher template")),
81                _ => None,
82            };
83
84            if let Some((content, description)) = template_result {
85                // Template file generation
86                if let Some(output_path) = output {
87                    std::fs::write(&output_path, &content).map_err(|e| {
88                        crate::error::CleanroomError::io_error(format!(
89                            "Failed to write template to {}: {}",
90                            output_path.display(),
91                            e
92                        ))
93                    })?;
94                    println!("✓ {} generated: {}", description, output_path.display());
95                } else {
96                    println!("{}", content);
97                }
98                Ok(())
99            } else {
100                // Regular project template (default, advanced, minimal, database, api)
101                generate_from_template(&template, name.as_deref())?;
102                Ok(())
103            }
104        }
105
106        Commands::Plugins => {
107            list_plugins()?;
108            Ok(())
109        }
110
111        Commands::Services { command } => match command {
112            ServiceCommands::Status => {
113                show_service_status().await?;
114                Ok(())
115            }
116            ServiceCommands::Logs { service, lines } => {
117                show_service_logs(&service, lines).await?;
118                Ok(())
119            }
120            ServiceCommands::Restart { service } => {
121                restart_service(&service).await?;
122                Ok(())
123            }
124            ServiceCommands::AiManage {
125                auto_scale: _,
126                predict_load: _,
127                optimize_resources: _,
128                horizon_minutes: _,
129                service: _,
130            } => {
131                Err(crate::error::CleanroomError::validation_error(
132                    "AI service management is an experimental feature in the clnrm-ai crate.\n\
133                     To use this feature, enable the 'ai' feature flag or use the clnrm-ai crate directly."
134                ))
135            }
136        },
137
138        Commands::Report {
139            input,
140            output,
141            format,
142        } => {
143            let format_str = match format {
144                ReportFormat::Html => "html",
145                ReportFormat::Markdown => "markdown",
146                ReportFormat::Json => "json",
147                ReportFormat::Pdf => "pdf",
148            };
149            generate_report(input.as_ref(), output.as_ref(), format_str).await?;
150            Ok(())
151        }
152
153        Commands::SelfTest { suite, report, otel_exporter, otel_endpoint } => {
154            run_self_tests(suite, report, otel_exporter, otel_endpoint).await?;
155            Ok(())
156        }
157
158        Commands::AiOrchestrate {
159            paths: _,
160            predict_failures: _,
161            auto_optimize: _,
162            confidence_threshold: _,
163            max_workers: _,
164        } => {
165            Err(crate::error::CleanroomError::validation_error(
166                "AI orchestration is an experimental feature in the clnrm-ai crate.\n\
167                 To use this feature, enable the 'ai' feature flag or use the clnrm-ai crate directly."
168            ))
169        }
170
171        Commands::AiPredict {
172            analyze_history: _,
173            predict_failures: _,
174            recommendations: _,
175            format: _,
176        } => {
177            Err(crate::error::CleanroomError::validation_error(
178                "AI predictive analytics is an experimental feature in the clnrm-ai crate.\n\
179                 To use this feature, enable the 'ai' feature flag or use the clnrm-ai crate directly."
180            ))
181        }
182
183        Commands::AiOptimize {
184            execution_order: _,
185            resource_allocation: _,
186            parallel_execution: _,
187            auto_apply: _,
188        } => {
189            Err(crate::error::CleanroomError::validation_error(
190                "AI test optimization is an experimental feature in the clnrm-ai crate.\n\
191                 To use this feature, enable the 'ai' feature flag or use the clnrm-ai crate directly."
192            ))
193        }
194
195        Commands::AiReal { analyze: _ } => {
196            Err(crate::error::CleanroomError::validation_error(
197                "AI real-time analysis is an experimental feature in the clnrm-ai crate.\n\
198                 To use this feature, enable the 'ai' feature flag or use the clnrm-ai crate directly."
199            ))
200        }
201
202        Commands::Health { verbose } => system_health_check(verbose).await,
203
204        Commands::Fmt { files, check, verify } => {
205            format_files(&files, check, verify)?;
206            Ok(())
207        }
208
209        Commands::DryRun { files, verbose } => {
210            use crate::CleanroomError;
211            let file_refs: Vec<_> = files.iter().map(|p| p.as_path()).collect();
212            let results = dry_run_validate(file_refs, verbose)?;
213
214            // Count failures
215            let failed_count = results.iter().filter(|r| !r.valid).count();
216
217            // Exit with error if any validations failed
218            if failed_count > 0 {
219                return Err(CleanroomError::validation_error(format!(
220                    "{} file(s) failed validation",
221                    failed_count
222                )));
223            }
224
225            Ok(())
226        }
227
228        Commands::Dev { paths, debounce_ms, clear, only, timebox } => {
229            let config = crate::cli::types::CliConfig {
230                format: cli.format.clone(),
231                verbose: cli.verbose,
232                ..Default::default()
233            };
234
235            run_dev_mode_with_filters(paths, debounce_ms, clear, only, timebox, config).await
236        }
237
238        Commands::Lint { files, format, deny_warnings } => {
239            let file_refs: Vec<_> = files.iter().map(|p| p.as_path()).collect();
240
241            // Convert format enum to string
242            let format_str = match format {
243                crate::cli::types::LintFormat::Human => "human",
244                crate::cli::types::LintFormat::Json => "json",
245                crate::cli::types::LintFormat::Github => "github",
246            };
247
248            // This will print diagnostics and return error if needed
249            lint_files(file_refs, format_str, deny_warnings)?;
250
251            Ok(())
252        }
253
254        Commands::Diff { baseline, current, format, only_changes } => {
255            // Convert format enum to string
256            let format_str = match format {
257                crate::cli::types::DiffFormat::Tree => "tree",
258                crate::cli::types::DiffFormat::Json => "json",
259                crate::cli::types::DiffFormat::SideBySide => "side-by-side",
260            };
261
262            let result = diff_traces(&baseline, &current, format_str, only_changes)?;
263
264            // Exit with error code if differences found
265            if result.added_count > 0 || result.removed_count > 0 || result.modified_count > 0 {
266                std::process::exit(1);
267            }
268
269            Ok(())
270        }
271
272        Commands::Record { paths, output } => {
273            run_record(paths, output).await
274        }
275
276        Commands::AiMonitor {
277            interval: _,
278            anomaly_threshold: _,
279            ai_alerts: _,
280            anomaly_detection: _,
281            proactive_healing: _,
282            webhook_url: _,
283        } => {
284            Err(crate::error::CleanroomError::validation_error(
285                "AI monitoring is an experimental feature in the clnrm-ai crate.\n\
286                 To use this feature, enable the 'ai' feature flag or use the clnrm-ai crate directly."
287            ))
288        }
289
290        Commands::Marketplace { command } => {
291            let marketplace = crate::marketplace::Marketplace::default().await?;
292            crate::marketplace::commands::execute_marketplace_command(&marketplace, command).await
293        }
294
295        // PRD v1.0 additional commands
296        Commands::Pull { paths, parallel, jobs } => {
297            pull_images(paths, parallel, jobs).await
298        }
299
300        Commands::Graph { trace, format, highlight_missing, filter } => {
301            visualize_graph(&trace, &format, highlight_missing, filter.as_deref())
302        }
303
304        Commands::Repro { baseline, verify_digest, output } => {
305            reproduce_baseline(&baseline, verify_digest, output.as_ref()).await
306        }
307
308        Commands::RedGreen { paths, expect, verify_red, verify_green } => {
309            // Handle new --expect flag or fall back to deprecated flags
310            let (should_verify_red, should_verify_green) = match expect {
311                Some(crate::cli::types::TddState::Red) => (true, false),
312                Some(crate::cli::types::TddState::Green) => (false, true),
313                None => (verify_red, verify_green),
314            };
315            run_red_green_validation(&paths, should_verify_red, should_verify_green).await
316        }
317
318        Commands::Render { template, map, output, show_vars } => {
319            render_template_with_vars(&template, &map, output.as_ref(), show_vars)
320        }
321
322        Commands::Spans { trace, grep, format, show_attrs, show_events } => {
323            filter_spans(&trace, grep.as_deref(), &format, show_attrs, show_events)
324        }
325
326        Commands::Collector { command } => {
327            match command {
328                crate::cli::types::CollectorCommands::Up { image, http_port, grpc_port, detach } => {
329                    start_collector(&image, http_port, grpc_port, detach).await
330                }
331                crate::cli::types::CollectorCommands::Down { volumes } => {
332                    stop_collector(volumes).await
333                }
334                crate::cli::types::CollectorCommands::Status => {
335                    show_collector_status().await
336                }
337                crate::cli::types::CollectorCommands::Logs { lines, follow } => {
338                    show_collector_logs(lines, follow).await
339                }
340            }
341        }
342
343        Commands::Analyze { test_file, traces } => {
344            use crate::cli::commands::v0_7_0::analyze::analyze_traces;
345
346            match analyze_traces(&test_file, traces.as_deref()) {
347                Ok(report) => {
348                    println!("{}", report.format_report());
349
350                    // Exit with code 1 if any validator failed
351                    if !report.is_success() {
352                        std::process::exit(1);
353                    }
354                    Ok(())
355                }
356                Err(e) => {
357                    eprintln!("Error analyzing traces: {}", e);
358                    std::process::exit(1);
359                }
360            }
361        }
362    };
363
364    if let Err(e) = result {
365        error!("Command failed: {}", e);
366        std::process::exit(1);
367    }
368
369    Ok(())
370}
371
372// Re-export all public types and functions for backward compatibility
373pub use commands::*;
374pub use types::*;