Adds --raw flag to all 6 remaining CLIs and wires humanization into the
default rendering path. --json and --raw both bypass humanization for
v5.0.0 byte-equal output; default mode humanizes findings/diff/prose.
token-hotspots-cli: humanizes payload.findings before stdout JSON write.
plugin-health-scanner: humanizes finding titles in stderr brief summary;
--json/--raw write byte-identical v5.0.0-shape result to stdout.
drift-cli: humanizes diff.{newFindings,resolvedFindings,unchangedFindings,
movedFindings} before formatDiffReport; --raw applies to save and list
modes too. Baselines remain raw v5.0.0 on disk.
fix-cli: humanizes manual-finding titles in stderr fix-plan prose; both
--json and --raw produce identical machine-readable JSON to stdout.
manifest, whats-active: --raw is a no-op (no findings, inventory only)
but parsed for CLI surface consistency.
Decision on missing --output-file flag for drift-cli/fix-cli/plugin-health:
deferred. SC-6/SC-7 tests in Wave 4 will use stdout-redirect (the simpler
Alt B path) since these CLIs already write JSON to stdout in machine modes.
Test cli-humanizer.test.mjs covers all 6 CLIs. Three CLIs that read
environment state (plugin-health, manifest, whats-active) verify
mode-equivalence (--json == --raw) instead of frozen-snapshot byte-equal,
because their output reflects current marketplace state which drifts as
plugins are added since the Wave 0 capture.
Wave 3 / Step 7 of v5.1.0 humanizer.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
143 lines
5.2 KiB
JavaScript
143 lines
5.2 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Config-Audit Drift CLI
|
|
* Compare current configuration against a saved baseline.
|
|
* Usage:
|
|
* node drift-cli.mjs <path> --save [--name my-baseline]
|
|
* node drift-cli.mjs <path> [--baseline my-baseline] [--json]
|
|
* node drift-cli.mjs --list
|
|
* Zero external dependencies.
|
|
*/
|
|
|
|
import { resolve } from 'node:path';
|
|
import { runAllScanners } from './scan-orchestrator.mjs';
|
|
import { diffEnvelopes, formatDiffReport } from './lib/diff-engine.mjs';
|
|
import { saveBaseline, loadBaseline, listBaselines } from './lib/baseline.mjs';
|
|
import { humanizeFindings } from './lib/humanizer.mjs';
|
|
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
let targetPath = '.';
|
|
let baselineName = 'default';
|
|
let save = false;
|
|
let list = false;
|
|
let jsonMode = false;
|
|
let rawMode = false;
|
|
let includeGlobal = false;
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
if (args[i] === '--save') {
|
|
save = true;
|
|
} else if (args[i] === '--name' && args[i + 1]) {
|
|
baselineName = args[++i];
|
|
} else if (args[i] === '--baseline' && args[i + 1]) {
|
|
baselineName = args[++i];
|
|
} else if (args[i] === '--list') {
|
|
list = true;
|
|
} else if (args[i] === '--json') {
|
|
jsonMode = true;
|
|
} else if (args[i] === '--raw') {
|
|
rawMode = true;
|
|
} else if (args[i] === '--global') {
|
|
includeGlobal = true;
|
|
} else if (!args[i].startsWith('-')) {
|
|
targetPath = args[i];
|
|
}
|
|
}
|
|
|
|
// --- List mode ---
|
|
if (list) {
|
|
const result = await listBaselines();
|
|
if (jsonMode || rawMode) {
|
|
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
} else {
|
|
if (result.baselines.length === 0) {
|
|
process.stderr.write('No baselines saved.\n');
|
|
process.stderr.write('Save one with: node drift-cli.mjs <path> --save\n');
|
|
} else {
|
|
process.stderr.write('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
process.stderr.write(' Saved Baselines\n');
|
|
process.stderr.write('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n');
|
|
for (const b of result.baselines) {
|
|
process.stderr.write(` ${b.name.padEnd(20)} ${b.findingCount} findings ${b.savedAt}\n`);
|
|
}
|
|
process.stderr.write('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
}
|
|
}
|
|
process.exit(0);
|
|
}
|
|
|
|
// --- Save mode ---
|
|
if (save) {
|
|
if (!jsonMode && !rawMode) {
|
|
process.stderr.write(`Config-Audit Drift CLI v2.1.0\n`);
|
|
process.stderr.write(`Saving baseline "${baselineName}" for ${resolve(targetPath)}\n\n`);
|
|
}
|
|
|
|
const envelope = await runAllScanners(targetPath, { includeGlobal });
|
|
const result = await saveBaseline(envelope, baselineName);
|
|
|
|
if (jsonMode || rawMode) {
|
|
process.stdout.write(JSON.stringify({ saved: true, name: result.name, path: result.path }, null, 2) + '\n');
|
|
} else {
|
|
process.stderr.write(`\nBaseline "${result.name}" saved to ${result.path}\n`);
|
|
process.stderr.write(`Findings: ${envelope.aggregate.total_findings}\n`);
|
|
}
|
|
process.exit(0);
|
|
}
|
|
|
|
// --- Drift mode (default) ---
|
|
if (!jsonMode && !rawMode) {
|
|
process.stderr.write(`Config-Audit Drift CLI v2.1.0\n`);
|
|
process.stderr.write(`Target: ${resolve(targetPath)}\n`);
|
|
process.stderr.write(`Baseline: ${baselineName}\n\n`);
|
|
}
|
|
|
|
// Load baseline
|
|
const baseline = await loadBaseline(baselineName);
|
|
if (!baseline) {
|
|
if (jsonMode || rawMode) {
|
|
process.stdout.write(JSON.stringify({ error: `Baseline "${baselineName}" not found. Save one with --save.` }, null, 2) + '\n');
|
|
} else {
|
|
process.stderr.write(`Baseline "${baselineName}" not found.\n`);
|
|
process.stderr.write(`Save one first: node drift-cli.mjs <path> --save\n`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
// Run current scan
|
|
const current = await runAllScanners(targetPath, { includeGlobal });
|
|
|
|
// Diff
|
|
const diff = diffEnvelopes(baseline, current);
|
|
|
|
if (jsonMode || rawMode) {
|
|
// --json and --raw both write the raw v5.0.0-shape diff (byte-identical).
|
|
process.stdout.write(JSON.stringify(diff, null, 2) + '\n');
|
|
} else {
|
|
// Default mode: humanize finding-bearing diff fields before report rendering.
|
|
const humanizedDiff = {
|
|
...diff,
|
|
newFindings: humanizeFindings(diff.newFindings || []),
|
|
resolvedFindings: humanizeFindings(diff.resolvedFindings || []),
|
|
unchangedFindings: humanizeFindings(diff.unchangedFindings || []),
|
|
movedFindings: humanizeFindings(diff.movedFindings || []),
|
|
};
|
|
const report = formatDiffReport(humanizedDiff);
|
|
process.stderr.write('\n' + report + '\n');
|
|
}
|
|
|
|
// Exit code: 0=stable/improving, 1=degrading
|
|
if (diff.summary.trend === 'degrading') process.exit(1);
|
|
process.exit(0);
|
|
}
|
|
|
|
// Only run CLI if invoked directly
|
|
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.exit(3);
|
|
});
|
|
}
|