#!/usr/bin/env node /** * Config-Audit Drift CLI * Compare current configuration against a saved baseline. * Usage: * node drift-cli.mjs --save [--name my-baseline] * node drift-cli.mjs [--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 --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, humanizedProgress: !jsonMode && !rawMode }); 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 --save\n`); } process.exit(1); } // Run current scan const current = await runAllScanners(targetPath, { includeGlobal, humanizedProgress: !jsonMode && !rawMode, }); // 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); }); }