`KNOWN_OPEN` in cli-unknown-flag-rejection.test.mjs named two CLIs as still carrying the argument-swallow defect. That number was the previous session's field of view, not a measurement. Measuring all fourteen found **7** open on the unknown-flag arm and **10** on a second arm the deferral note never described. Arm 1 — unknown flag: with no `else` branch, `--zzz` leaves no trace. exit 0, full payload, a confident answer to a question the caller did not ask. Arm 2 — the sharper one: `a === '--output-file' && args[i + 1]` asks only whether a next token EXISTS, never whether it is a value. `manifest`, `campaign-cli` and `knowledge-refresh-cli` each wrote a file literally named `--json` into the caller's working directory when handed `--output-file --json`, exit 0, with `--json` mode silently dropped. A wrong answer is bad; an unintended file on disk is worse. Two of the CLIs this catches were already in GUARDED and green on arm 1 while arm 2 stood open a few lines away — the guard asserted one relation instead of the invariant. Fixed with a shared gate (`lib/cli-args.mjs`) that runs BEFORE each CLI's own parse loop rather than replacing it: valid argv reaches the existing parser byte-for-byte unchanged, so the byte-stability argument is structural rather than empirical. `drift-cli`, `fix-cli` and `plugin-health-scanner` were already correct on both arms and were moved into GUARDED instead of rewritten. The three CLIs with a bespoke unknown-flag branch had it removed once the gate made it unreachable. Suite 1488 → 1531. Frozen v5.0.0 snapshots untouched; `self-audit --check-readme` passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P6Rh59Mtj4uYrdYMCYZJE
77 lines
2.5 KiB
JavaScript
77 lines
2.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* whats-active CLI — produce a read-only inventory of everything Claude Code
|
|
* loads for a given repo path. Thin shim over scanners/lib/active-config-reader.mjs.
|
|
*
|
|
* Usage:
|
|
* node whats-active.mjs [path] [--json] [--output-file <path>]
|
|
* [--verbose] [--suggest-disables]
|
|
*
|
|
* Exit codes: 0=ok, 3=unrecoverable error.
|
|
* Zero external dependencies.
|
|
*/
|
|
|
|
import { resolve } from 'node:path';
|
|
import { stat } from 'node:fs/promises';
|
|
import { writeOutputFile } from './lib/write-output.mjs';
|
|
import { readActiveConfig } from './lib/active-config-reader.mjs';
|
|
import { requireValidArgs } from './lib/cli-args.mjs';
|
|
|
|
/** Flag surface, measured 2026-08-09. Anything else is exit 3. */
|
|
const ARG_SPEC = { boolean: ['--json', '--raw', '--verbose', '--suggest-disables'], value: ['--output-file'] };
|
|
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
if (!requireValidArgs(args, ARG_SPEC)) return;
|
|
let targetPath = '.';
|
|
let outputFile = null;
|
|
let jsonMode = false;
|
|
// --raw is accepted for CLI surface consistency but is a no-op here:
|
|
// whats-active produces an inventory snapshot, not findings.
|
|
let rawMode = false;
|
|
let verbose = false;
|
|
let suggestDisables = false;
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
if (args[i] === '--json') jsonMode = true;
|
|
else if (args[i] === '--raw') rawMode = true;
|
|
else if (args[i] === '--verbose') verbose = true;
|
|
else if (args[i] === '--suggest-disables') suggestDisables = true;
|
|
else if (args[i] === '--output-file' && args[i + 1]) outputFile = args[++i];
|
|
else if (!args[i].startsWith('-')) targetPath = args[i];
|
|
}
|
|
|
|
const absPath = resolve(targetPath);
|
|
try {
|
|
const s = await stat(absPath);
|
|
if (!s.isDirectory()) {
|
|
process.stderr.write(`Error: ${absPath} is not a directory\n`);
|
|
process.exitCode = 3;
|
|
return;
|
|
}
|
|
} catch {
|
|
process.stderr.write(`Error: path does not exist: ${absPath}\n`);
|
|
process.exitCode = 3;
|
|
return;
|
|
}
|
|
|
|
const result = await readActiveConfig(absPath, { verbose, suggestDisables });
|
|
const json = JSON.stringify(result, null, 2);
|
|
|
|
if (outputFile) {
|
|
await writeOutputFile(outputFile, json, 'utf-8');
|
|
}
|
|
|
|
if (jsonMode || rawMode || !outputFile) {
|
|
process.stdout.write(json + '\n');
|
|
}
|
|
}
|
|
|
|
const isDirectRun = process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname);
|
|
if (isDirectRun) {
|
|
main().catch(err => {
|
|
process.stderr.write(`Fatal: ${err.message}\n`);
|
|
process.exitCode = 3;
|
|
});
|
|
}
|