diff --git a/skills/corgea/SKILL.md b/skills/corgea/SKILL.md index bbf8d9e..bfd8f02 100644 --- a/skills/corgea/SKILL.md +++ b/skills/corgea/SKILL.md @@ -8,6 +8,25 @@ allowed-tools: Shell, Read, Grep, Glob, StrReplace Find and fix security vulnerabilities using AI-powered scanning (BLAST), third-party scanners, and AI-generated fixes. +## Check the installed version first + +This file describes a CLI version, not the one on the machine. Run `corgea --version` before relying on anything below. + +```bash +corgea --version +``` + +`corgea --help` and `corgea --help` come from the installed binary, so they are authoritative about which commands and flags exist. **Where this file and `--help` disagree, `--help` is right.** Confirm any command from here that you have not seen in `--help` before running it. + +If a command or flag is missing, the CLI is likely older than this reference. Report that to the user with the upgrade for how it was installed, rather than upgrading unprompted — CI runners and self-hosted installs are often pinned deliberately. + +```bash +pip install --upgrade corgea-cli # installed with pip +npm install -g @corgea/cli # installed with npm +``` + +A missing flag is the visible case. A flag whose default or output shape changed will not error, so treat a surprising result on an older CLI as a version difference before treating it as a bug. + ## Commands ### Scan — `corgea scan [scanner]` diff --git a/src/config.rs b/src/config.rs index 5e4986c..42f6365 100644 --- a/src/config.rs +++ b/src/config.rs @@ -54,31 +54,46 @@ impl Config { Ok(file_path) } + /// The settings a fresh install starts from, before anything is persisted. + fn defaults() -> Self { + Self { + url: "https://www.corgea.app".to_string(), + debug: 0, + token: "".to_string(), + default_agent: None, + recency_gate: default_recency_gate(), + recency_threshold_days: default_recency_threshold_days(), + } + } + + fn apply_env_overrides(&mut self) { + if let Ok(corgea_debug) = env::var("CORGEA_DEBUG") { + self.debug = corgea_debug.parse::().unwrap_or(0); + } + } + pub fn load() -> io::Result { let file_path = Self::config_path()?; if !file_path.exists() { - let config = Self { - url: "https://www.corgea.app".to_string(), - debug: 0, - token: "".to_string(), - default_agent: None, - recency_gate: default_recency_gate(), - recency_threshold_days: default_recency_threshold_days(), - }; - - let toml = toml::to_string(&config).expect("Failed to serialize config"); + let toml = toml::to_string(&Self::defaults()).expect("Failed to serialize config"); fs::write(&file_path, toml)?; } let contents = fs::read_to_string(&file_path)?; - let mut config: Self = toml::from_str(&contents).expect("Failed to deserialize config"); - - if let Ok(corgea_debug) = env::var("CORGEA_DEBUG") { - config.debug = corgea_debug.parse::().unwrap_or(0); - } + // An unparseable config is a normal error, not a bug: it is a file the + // user can edit. Returning it rather than panicking lets the caller + // report it as what it is, naming the file to fix. + let mut config: Self = toml::from_str(&contents).map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("Failed to parse {}: {}", file_path.display(), e), + ) + })?; + + config.apply_env_overrides(); Ok(config) } diff --git a/src/main.rs b/src/main.rs index 57cac78..4712f5b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -511,7 +511,15 @@ fn default_log_level(debug_flag: i8) -> &'static str { fn main() { let cli = Cli::parse(); - let mut corgea_config = Config::load().expect("Failed to load config"); + let mut corgea_config = match Config::load() { + Ok(config) => config, + // `config.toml` is a file the user can edit, so a bad one is theirs to + // fix, not a Rust panic with a backtrace note. + Err(e) => { + eprintln!("Failed to load config: {}", e); + std::process::exit(1); + } + }; init_logging(&corgea_config); fn verify_token_and_exit_when_fail(config: &Config) { if config.get_token().is_empty() { diff --git a/tests/cli_config_errors.rs b/tests/cli_config_errors.rs new file mode 100644 index 0000000..00e547b --- /dev/null +++ b/tests/cli_config_errors.rs @@ -0,0 +1,60 @@ +//! `config.toml` is a file the user can edit, so a bad one must read as an +//! error naming the file, not as a Rust panic. + +mod common; + +use std::fs; + +fn write_malformed_config(home: &std::path::Path) { + let dir = home.join(".corgea"); + fs::create_dir_all(&dir).expect("create config dir"); + fs::write(dir.join("config.toml"), "this is not = valid = toml\n").expect("write config"); +} + +#[test] +fn a_malformed_config_fails_cleanly_instead_of_panicking() { + let (mut cmd, home) = common::corgea_isolated(); + write_malformed_config(home.path()); + + let out = cmd.args(["ls"]).output().expect("run corgea"); + let stderr = String::from_utf8_lossy(&out.stderr); + + assert!(!out.status.success(), "a broken config must stop the run"); + assert!( + stderr.contains("config.toml"), + "the error should name the file to fix: {stderr}" + ); + assert!( + !stderr.contains("panicked"), + "an editable file being wrong is not a crash: {stderr}" + ); +} + +/// Uses the same command as the test above, so the two differ only in whether +/// the config parses. `--help` would not do: clap exits during parsing, before +/// `Config::load` is ever reached. +#[test] +fn a_valid_config_is_still_read() { + let (mut cmd, home) = common::corgea_isolated(); + let dir = home.path().join(".corgea"); + fs::create_dir_all(&dir).expect("create config dir"); + fs::write( + dir.join("config.toml"), + "url = \"https://example.invalid\"\ndebug = 0\ntoken = \"\"\n", + ) + .expect("write config"); + + let out = cmd.args(["ls"]).output().expect("run corgea"); + let stderr = String::from_utf8_lossy(&out.stderr); + + assert!( + !stderr.contains("Failed to load config"), + "a parseable config must load: {stderr}" + ); + // Reaching the auth gate is what proves the config was read: the token it + // found was the empty one written above. + assert!( + stderr.contains("No token set"), + "expected the run to get as far as the auth gate: {stderr}" + ); +}