Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions skills/corgea/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <command> --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]`
Expand Down
45 changes: 30 additions & 15 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<i8>().unwrap_or(0);
}
}

pub fn load() -> io::Result<Self> {
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::<i8>().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)
}
Expand Down
10 changes: 9 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
60 changes: 60 additions & 0 deletions tests/cli_config_errors.rs
Original file line number Diff line number Diff line change
@@ -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}"
);
}
Loading