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
14 changes: 14 additions & 0 deletions .codex/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "./harness post-edit codex"
}
]
}
]
}
}
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ http-body-util = "0.1"
url = "2.5"
open = "5.0"
urlencoding = "2.1"
serde_yaml_ng = "0.10"

[target.'cfg(not(target_os = "windows"))'.dependencies]
openssl = { version = "0.10", features = ["vendored"] }
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,29 @@ Once the binary is installed, login with your token from the Corgea app.
corgea login <token>
```

## Dependency Inventory (offline)

`corgea deps` builds a dependency inventory from npm, Python, and Java manifests
and lockfiles, then evaluates a pinning policy (DEP rules). Runs fully offline —
no token or network required.

```bash
corgea deps scan # table report for the current directory
corgea deps scan --format agent # compact TSV for coding agents
corgea deps scan --format json # JSON inventory on stdout
corgea deps scan --format quiet # no stdout; exit code still applies
corgea deps scan --fail-on high # exit 1 if any finding is >= high
corgea deps scan --out-format json # machine-readable (json or sarif)
corgea deps graph --format json # print the resolved dependency graph
corgea deps explain <package> --format agent # show why a package is present
corgea deps diff --base origin/main --format json
corgea deps sbom --format cyclonedx # emit a CycloneDX SBOM
corgea deps policy init --exist-ok # write starter policy, or keep existing file
```

`corgea deps` defaults to `--format agent` when an agent environment is detected (`AI_AGENT`, `CODEX_SANDBOX`, `CLAUDECODE`, and related agent variables). Use `--format human` to force the normal terminal output.

See [Dependency Scanning (CLI)](https://docs.corgea.app/cli/deps) for the full flag and exit-code reference.

## Development Setup

Expand Down
31 changes: 31 additions & 0 deletions examples/deps_skill.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use std::path::Path;
use std::process::ExitCode;

use corgea::deps::skill::{check_skill_file, generated_marked_section, update_skill_file};

const SKILL_PATH: &str = "skills/corgea/SKILL.md";

fn main() -> ExitCode {
let mode = std::env::args()
.nth(1)
.unwrap_or_else(|| "print".to_string());
let skill_path = Path::new(SKILL_PATH);

let result = match mode.as_str() {
"print" => {
println!("{}", generated_marked_section());
Ok(())
}
"check" => check_skill_file(skill_path),
"update" => update_skill_file(skill_path),
_ => Err("usage: cargo run --example deps_skill -- [print|check|update]".to_string()),
};

match result {
Ok(()) => ExitCode::SUCCESS,
Err(message) => {
eprintln!("{message}");
ExitCode::FAILURE
}
}
}
70 changes: 67 additions & 3 deletions harness
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
# Usage: ./harness <command> [--verbose] [--min=N]
#
# Commands: check, fix, lint, test, audit, coverage, pre-commit, ci,
# post-edit, setup-hooks, suppressions
# post-edit, setup-hooks, suppressions, deps-skill
# Use `./harness post-edit codex` for Codex hooks that require JSON-safe stdout.

set -u

Expand Down Expand Up @@ -97,6 +98,20 @@ run_with_summary() {
return 0
}

json_escape() {
local value="$1"
value="${value//\\/\\\\}"
value="${value//\"/\\\"}"
value="${value//$'\n'/\\n}"
value="${value//$'\r'/\\r}"
value="${value//$'\t'/\\t}"
printf "%s" "$value"
}

codex_system_message() {
printf '{"systemMessage":"%s"}\n' "$(json_escape "$1")"
}

# ── Git helpers ─────────────────────────────────────────────────────

staged_rs_files() {
Expand Down Expand Up @@ -160,6 +175,10 @@ cmd_test() {
run_with_summary "Tests" 0 -- cargo test
}

cmd_deps_skill() {
run "Deps skill drift" 0 -- cargo run --example deps_skill -- check
}

cmd_audit() {
_cmd_audit_inner 0
}
Expand Down Expand Up @@ -192,8 +211,49 @@ cmd_coverage() {
}

cmd_post_edit() {
local codex=0
local hook_input=""
local arg
for arg in "$@"; do
case "$arg" in
codex) codex=1 ;;
"")
;;
*)
printf "Unknown post-edit option: %s\n" "$arg" >&2
return 1
;;
esac
done

if [ ! -t 0 ]; then
hook_input="$(cat || true)"
if printf "%s" "$hook_input" | grep -qE '"hook_event_name"[[:space:]]*:'; then
codex=1
fi
fi

local changed; changed="$(changed_rs_files)"
[ -z "$changed" ] && return 0

if [ "$codex" -eq 1 ]; then
local tmp; tmp="$(mktemp)"
cargo fmt >"$tmp" 2>&1
local rc=$?
local output; output="$(tail -40 "$tmp")"
rm -f "$tmp"

[ "$rc" -eq 0 ] && return 0

if [ -n "$output" ]; then
codex_system_message "cargo fmt failed:
$output"
else
codex_system_message "cargo fmt failed without output."
fi
return 0
fi

# Never fail the Stop hook.
run "Format" 1 -- cargo fmt || true
return 0
Expand Down Expand Up @@ -226,6 +286,8 @@ cmd_check() {
[ $? -eq 0 ] && passed=$(( passed + 1 )) || failed=$(( failed + 1 ))
run_with_summary "Tests" 1 -- cargo test
[ $? -eq 0 ] && passed=$(( passed + 1 )) || failed=$(( failed + 1 ))
run "Deps skill drift" 1 -- cargo run --example deps_skill -- check
[ $? -eq 0 ] && passed=$(( passed + 1 )) || failed=$(( failed + 1 ))

cmd_suppressions

Expand Down Expand Up @@ -278,13 +340,15 @@ case "$cmd" in
coverage) cmd_coverage ;;
pre-commit) cmd_pre_commit ;;
ci) cmd_ci ;;
post-edit) cmd_post_edit ;;
post-edit) cmd_post_edit "${@:2}" ;;
setup-hooks) cmd_setup_hooks ;;
suppressions) cmd_suppressions ;;
deps-skill) cmd_deps_skill ;;
-h|--help|help)
printf "Usage: ./harness <command> [--verbose] [--min=N]\n\n"
printf "Commands: check, fix, lint, test, audit, coverage, pre-commit,\n"
printf " ci, post-edit, setup-hooks, suppressions\n"
printf " ci, post-edit, setup-hooks, suppressions, deps-skill\n"
printf "\nCodex hook mode: ./harness post-edit codex\n"
;;
*)
printf "Unknown command: %s\n" "$cmd" >&2
Expand Down
22 changes: 22 additions & 0 deletions skills/corgea/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,28 @@ corgea setup-hooks --default-config # Default: secrets + PII, fail on

Installs a pre-commit hook running `corgea scan blast --only-uncommitted`. Bypass with `git commit --no-verify`.

<!-- BEGIN GENERATED CORGEA DEPS SKILL -->
### Deps — `corgea deps <command>`

Offline dependency inventory and policy checks. No Corgea token or network required.
Agent environments default to compact TSV; force output with `--format human|agent|json|quiet`.

- `corgea deps scan [PATH]` — Scan manifests and lockfiles, build inventory, evaluate policy. Flags: `--fail-on`, `--out-format`, `--out-file`, `--format`
Examples: `corgea deps scan --format agent`; `corgea deps scan --format quiet --fail-on high`
- `corgea deps graph [PATH]` — Print the dependency graph. Flags: `--format`
Examples: `corgea deps graph --format agent`; `corgea deps graph tests/fixtures/node-app --format json`
- `corgea deps explain <PACKAGE> [PATH]` — Explain why a package is present. Flags: `--format`
Examples: `corgea deps explain lodash --format agent`; `corgea deps explain left-pad tests/fixtures/node-app --format json`
- `corgea deps diff --base <BASE> [PATH]` — Compare dependency graph against a git ref. Flags: `--base`, `--fail-on-new`, `--format`
Examples: `corgea deps diff --base origin/main --format json`; `corgea deps diff --base HEAD . --fail-on-new high`
- `corgea deps sbom [PATH]` — Generate an SBOM. Flags: `--format`, `--out`
Examples: `corgea deps sbom --format cyclonedx`; `corgea deps sbom --format cyclonedx --out bom.json`
- `corgea deps policy init [PATH]` — Write a starter `.corgea/deps.yml` policy file. Flags: `--exist-ok`, `--format`
Examples: `corgea deps policy init`; `corgea deps policy init --exist-ok --format quiet`

Notes: `deps scan --out-format table|json|sarif` is the report/export selector; do not combine it with `deps scan --format`.
<!-- END GENERATED CORGEA DEPS SKILL -->

## Common Workflows

### Scan full project
Expand Down
103 changes: 103 additions & 0 deletions src/deps/detect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use std::path::{Path, PathBuf};

use crate::deps::model::Ecosystem;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum DepFileKind {
NpmManifest,
NpmLockfile,
YarnLockfile,
PnpmLockfile,
PipRequirements,
PipConstraints,
PyProject,
PoetryLock,
UvLock,
MavenPom,
GradleBuild,
GradleLockfile,
GoMod,
GoSum,
CargoManifest,
CargoLock,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DetectedFile {
pub path: PathBuf,
pub kind: DepFileKind,
pub ecosystem: Ecosystem,
}

const SKIP_DIRS: &[&str] = &[
"node_modules",
".git",
"vendor",
"target",
".venv",
"venv",
"__pycache__",
"dist",
"build",
];

/// Recursively detect supported dependency files; skip vendored/VCS dirs.
pub fn detect_dependency_files(root: &Path) -> Vec<DetectedFile> {
let mut out = Vec::new();
detect_recursive(root, &mut out);
out.sort_by(|a, b| a.path.cmp(&b.path));
out
}

fn detect_recursive(dir: &Path, out: &mut Vec<DetectedFile>) {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};

for entry in entries.flatten() {
let path = entry.path();
let file_name = entry.file_name();
let name = file_name.to_string_lossy();

if path.is_dir() {
if SKIP_DIRS.iter().any(|s| name == *s) {
continue;
}
detect_recursive(&path, out);
continue;
}

if let Some(detected) = classify_file(&path) {
out.push(detected);
}
}
}

fn classify_file(path: &Path) -> Option<DetectedFile> {
let name = path.file_name()?.to_string_lossy();
let kind_eco = match name.as_ref() {
"package.json" => (DepFileKind::NpmManifest, Ecosystem::Npm),
"package-lock.json" | "npm-shrinkwrap.json" => (DepFileKind::NpmLockfile, Ecosystem::Npm),
"yarn.lock" => (DepFileKind::YarnLockfile, Ecosystem::Npm),
"pnpm-lock.yaml" => (DepFileKind::PnpmLockfile, Ecosystem::Npm),
"requirements.txt" => (DepFileKind::PipRequirements, Ecosystem::PyPI),
"constraints.txt" => (DepFileKind::PipConstraints, Ecosystem::PyPI),
"pyproject.toml" => (DepFileKind::PyProject, Ecosystem::PyPI),
"poetry.lock" => (DepFileKind::PoetryLock, Ecosystem::PyPI),
"uv.lock" => (DepFileKind::UvLock, Ecosystem::PyPI),
"pom.xml" => (DepFileKind::MavenPom, Ecosystem::Maven),
"build.gradle" | "build.gradle.kts" => (DepFileKind::GradleBuild, Ecosystem::Maven),
"gradle.lockfile" => (DepFileKind::GradleLockfile, Ecosystem::Maven),
"go.mod" => (DepFileKind::GoMod, Ecosystem::Go),
"go.sum" => (DepFileKind::GoSum, Ecosystem::Go),
"Cargo.toml" => (DepFileKind::CargoManifest, Ecosystem::Cargo),
"Cargo.lock" => (DepFileKind::CargoLock, Ecosystem::Cargo),
_ => return None,
};
Some(DetectedFile {
path: path.to_path_buf(),
kind: kind_eco.0,
ecosystem: kind_eco.1,
})
}
Loading
Loading