From 01d625c93ec21afe355ad3b5ef5db76b02939368 Mon Sep 17 00:00:00 2001 From: CTO Hermes Date: Tue, 2 Jun 2026 23:16:38 +0700 Subject: [PATCH] feat: add cora config validate subcommand (#88) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validates current configuration and reports field status: - Config file location - Provider, model, API key presence - Severity, output format, temperature, cache TTL - Shows which fields are explicitly set vs using defaults Exit 0 if valid, exit 2 if issues found. Also updates AGENT.md: fix test count 157→224, document validate. 224 tests pass, clippy clean, fmt clean. Closes #88 Co-Authored-By: codecoradev --- AGENT.md | 6 +- src/commands/config_cmd.rs | 230 +++++++++++++++++++++++++++++++++++++ src/main.rs | 3 + 3 files changed, 236 insertions(+), 3 deletions(-) diff --git a/AGENT.md b/AGENT.md index 0e5007a..5c46d0e 100644 --- a/AGENT.md +++ b/AGENT.md @@ -15,7 +15,7 @@ no managed API, no cloud service. Runs locally against diffs, scans, or branches ```bash cargo build # Build (debug) cargo build --release # Build (release) -cargo test # Run all 157 tests +cargo test # Run all 224 tests cargo clippy # Lint cargo fmt # Format check (use -- --check for CI) ``` @@ -29,7 +29,7 @@ src/ │ ├── mod.rs │ ├── review.rs # cora review (diff-based review) │ ├── scan.rs # cora scan (full-file scan) -│ ├── config_cmd.rs # cora config (show/set) +│ ├── config_cmd.rs # cora config (show/set/validate) │ ├── auth.rs # cora auth (API key management) │ ├── hook_cmd.rs # cora hook (pre-commit hook install/uninstall) │ ├── init.rs # cora init (project scaffolding) @@ -67,7 +67,7 @@ src/ | File | Purpose | |---|---| -| `commands/config_cmd.rs` | Config subcommand — display, set, path resolution | +| `commands/config_cmd.rs` | Config subcommand — display, set, validate, path resolution | | `config/loader.rs` | Config loading with full priority chain resolution | | `config/schema.rs` | All config structs, defaults, serde annotations | | `commands/init.rs` | Project scaffolding, `.cora.yaml` generation | diff --git a/src/commands/config_cmd.rs b/src/commands/config_cmd.rs index ddbfa40..0164364 100644 --- a/src/commands/config_cmd.rs +++ b/src/commands/config_cmd.rs @@ -191,3 +191,233 @@ pub fn execute_config_set(key: &str, value: &str, global: bool) -> Result<()> { Ok(()) } + +/// Execute `cora config validate` — load config and report validity. +/// +/// Returns exit code: 0 if valid, 2 if issues found. +pub fn execute_config_validate() -> Result { + // 1. Load resolved config (same as other commands) + let config = loader::load_config(None, None, None, None, None, false)?; + + // 2. Find the raw config file to check which fields were explicitly set + let cora_file = loader::find_cora_file(&std::env::current_dir().unwrap_or_default()) + .unwrap_or_else(|e| { + eprintln!( + "{} Warning: could not search for config file: {}", + "⚠️".yellow(), + e + ); + None + }); + + // Also check global config for fields set there + let global_cora = load_global_cora_file(); + + // Track how many fields were explicitly set + let mut explicitly_set = 0u32; + let total_fields = 8u32; + + // ── Check config file ── + match &cora_file { + Some((path, _)) => { + println!("{} Config file: {}", "✅".green().bold(), path.display()); + explicitly_set += 1; + } + None => { + println!( + "{} Config file: not found (using defaults)", + "⚠️".yellow().bold() + ); + } + } + + // ── Check provider ── + let provider_set = is_explicitly_set( + &cora_file, + &global_cora, + |c| c.provider.as_ref(), + |g| g.provider.as_ref(), + ); + if provider_set { + println!( + "{} Provider: {}", + "✅".green().bold(), + config.provider.provider + ); + explicitly_set += 1; + } else { + println!( + "{} Provider: not set (default: {})", + "⚠️".yellow().bold(), + config.provider.provider + ); + } + + // ── Check model ── + let model_set = is_explicitly_set( + &cora_file, + &global_cora, + |c| c.provider.as_ref().and_then(|p| p.model.as_ref()), + |g| g.provider.as_ref().and_then(|p| p.model.as_ref()), + ); + if model_set { + println!("{} Model: {}", "✅".green().bold(), config.provider.model); + explicitly_set += 1; + } else { + println!( + "{} Model: not set (default: {})", + "⚠️".yellow().bold(), + config.provider.model + ); + } + + // ── Check API key ── + let auth = loader::auth_status().context("failed to check auth status")?; + if auth.has_key { + let source = if auth.source.contains("env") { + "env var".to_string() + } else { + format!("auth file ({})", auth.source) + }; + println!("{} API key: configured ({})", "✅".green().bold(), source); + explicitly_set += 1; + } else { + println!("{} API key: not configured", "❌".red().bold()); + } + + // ── Check severity ── + let severity_set = is_explicitly_set( + &cora_file, + &global_cora, + |c| c.hook.as_ref().and_then(|h| h.min_severity.as_ref()), + |g| g.hook.as_ref().and_then(|h| h.min_severity.as_ref()), + ); + if severity_set { + println!( + "{} Severity: {}", + "✅".green().bold(), + config.hook.min_severity + ); + explicitly_set += 1; + } else { + println!( + "{} Severity: not set (default: {})", + "⚠️".yellow().bold(), + config.hook.min_severity + ); + } + + // ── Check output format ── + let format_set = is_explicitly_set( + &cora_file, + &global_cora, + |c| c.output.as_ref().and_then(|o| o.format.as_ref()), + |g| g.output.as_ref().and_then(|o| o.format.as_ref()), + ); + if format_set { + println!( + "{} Output format: {}", + "✅".green().bold(), + config.output.format + ); + explicitly_set += 1; + } else { + println!( + "{} Output format: not set (default: {})", + "⚠️".yellow().bold(), + config.output.format + ); + } + + // ── Check temperature ── + let temp_set = is_explicitly_set( + &cora_file, + &global_cora, + |c| c.llm.as_ref().and_then(|l| l.temperature.as_ref()), + |g| g.llm.as_ref().and_then(|l| l.temperature.as_ref()), + ); + if temp_set { + println!( + "{} Temperature: {}", + "✅".green().bold(), + config.temperature + ); + explicitly_set += 1; + } else { + println!( + "{} Temperature: not set (default: {})", + "⚠️".yellow().bold(), + config.temperature + ); + } + + // ── Check cache TTL ── + let cache_set = is_explicitly_set( + &cora_file, + &global_cora, + |c| c.llm.as_ref().and_then(|l| l.cache_ttl.as_ref()), + |g| g.llm.as_ref().and_then(|l| l.cache_ttl.as_ref()), + ); + if cache_set { + println!("{} Cache TTL: {}", "✅".green().bold(), config.cache_ttl); + explicitly_set += 1; + } else { + println!( + "{} Cache TTL: not set (default: {})", + "⚠️".yellow().bold(), + config.cache_ttl + ); + } + + println!(); + + // Determine overall validity + let has_issues = !auth.has_key; + if has_issues { + println!( + "{}", + "Configuration issues found. Fix the ❌ items above." + .red() + .bold() + ); + println!( + "{}", + format!("{}/{} fields explicitly set.", explicitly_set, total_fields).dimmed() + ); + Ok(2) + } else { + println!("{}", "Configuration valid.".green().bold()); + println!( + "{}", + format!("{}/{} fields explicitly set.", explicitly_set, total_fields).dimmed() + ); + Ok(0) + } +} + +/// Check if a field was explicitly set in either the project config or global config. +/// Uses closure extractors to check both config sources. +fn is_explicitly_set( + project: &Option<(PathBuf, CoraFile)>, + global: &Option, + project_extractor: impl Fn(&CoraFile) -> Option<&T>, + global_extractor: impl Fn(&CoraFile) -> Option<&T>, +) -> bool { + let project_has = project + .as_ref() + .and_then(|(_, c)| project_extractor(c)) + .is_some(); + let global_has = global.as_ref().and_then(global_extractor).is_some(); + project_has || global_has +} + +/// Load the global config file (same logic as loader::load_global_config but returns Option directly). +fn load_global_cora_file() -> Option { + let dir = loader::cora_dir().ok()?; + let path = dir.join("config.yaml"); + if !path.is_file() { + return None; + } + let content = std::fs::read_to_string(&path).ok()?; + CoraFile::from_str(&content).ok() +} diff --git a/src/main.rs b/src/main.rs index b67f74c..b139d3c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -255,6 +255,8 @@ enum ConfigAction { #[clap(long)] global: bool, }, + /// Validate the current configuration and report status + Validate, } #[allow(clippy::too_many_lines)] @@ -398,6 +400,7 @@ async fn main() -> Result<()> { config_cmd::execute_config_set(&key, &value, global)?; 0 } + ConfigAction::Validate => config_cmd::execute_config_validate()?, }, Command::Providers => { providers::execute_providers();