#!/usr/bin/env node /** * Config-Audit Fix CLI * Standalone entry point for running fixes without the command. * Usage: node fix-cli.mjs [--apply] [--global] [--json] * Dry-run by default — must pass --apply to write changes. * Zero external dependencies. */ import { resolve } from 'node:path'; import { runAllScanners } from './scan-orchestrator.mjs'; import { planFixes, applyFixes, verifyFixes } from './fix-engine.mjs'; import { createBackup } from './lib/backup.mjs'; import { humanizeFinding } from './lib/humanizer.mjs'; async function main() { const args = process.argv.slice(2); let targetPath = '.'; let apply = false; let jsonMode = false; let rawMode = false; let includeGlobal = false; for (let i = 0; i < args.length; i++) { if (args[i] === '--apply') { apply = 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]; } } // Whether to suppress prose stderr (true for both --json and --raw machine paths). const machineMode = jsonMode || rawMode; const resolvedPath = resolve(targetPath); if (!machineMode) { process.stderr.write(`Config-Audit Fix CLI v2.1.0\n`); process.stderr.write(`Target: ${resolvedPath}\n`); process.stderr.write(`Mode: ${apply ? 'APPLY' : 'DRY-RUN'}\n\n`); process.stderr.write(`Scanning...\n`); } // 1. Run all scanners const envelope = await runAllScanners(targetPath, { includeGlobal, humanizedProgress: !machineMode, }); // 2. Plan fixes const { fixes, skipped, manual } = planFixes(envelope); if (!machineMode) { process.stderr.write(`\n`); process.stderr.write(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`); process.stderr.write(` Config-Audit Fix Plan\n`); process.stderr.write(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`); if (fixes.length > 0) { process.stderr.write(` Auto-fixable (${fixes.length}):\n`); for (let i = 0; i < fixes.length; i++) { process.stderr.write(` ${i + 1}. [${fixes[i].findingId}] ${fixes[i].description}\n`); } } else { process.stderr.write(` No auto-fixable issues found.\n`); } if (manual.length > 0) { // Default mode humanizes the manual-finding titles for the prose render. // The JSON `manual` array (later in this function) keeps v5.0.0 verbatim. process.stderr.write(`\n Manual (${manual.length}):\n`); for (let i = 0; i < manual.length; i++) { const m = manual[i]; const title = humanizeFinding({ id: m.findingId, scanner: typeof m.findingId === 'string' ? m.findingId.split('-')[1] || '' : '', severity: m.severity || 'info', title: m.title, description: m.description || '', recommendation: m.recommendation || '', }).title; process.stderr.write(` ${fixes.length + i + 1}. [${m.findingId}] ${title}\n`); } } if (skipped.length > 0) { process.stderr.write(`\n Skipped (${skipped.length}): could not generate fix plan\n`); } process.stderr.write(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`); } // 3. Apply or dry-run let applied = []; let failed = []; let verified = []; let regressions = []; let backupId = null; if (fixes.length === 0) { if (machineMode) { const output = { planned: [], applied: [], failed: [], verified: [], regressions: [], manual, backupId: null }; process.stdout.write(JSON.stringify(output, null, 2) + '\n'); } process.exit(0); } if (apply) { // Create backup first const filesToBackup = [...new Set(fixes.filter(f => f.type !== 'file-rename').map(f => f.file))]; const backup = createBackup(filesToBackup); backupId = backup.backupId; if (!machineMode) { process.stderr.write(`\n Backup created: ${backup.backupPath}\n`); process.stderr.write(` Applying ${fixes.length} fixes...\n\n`); } const result = await applyFixes(fixes, { dryRun: false, backupDir: backup.backupPath }); applied = result.applied; failed = result.failed; if (!machineMode) { process.stderr.write(` Results: ${applied.length} applied, ${failed.length} failed\n`); if (failed.length > 0) { for (const f of failed) { process.stderr.write(` FAILED: [${f.findingId}] ${f.error}\n`); } } } // 4. Verify if (applied.length > 0) { if (!machineMode) { process.stderr.write(`\n Verifying...\n`); } const verification = await verifyFixes(envelope, applied); verified = verification.verified; regressions = verification.regressions; if (!machineMode) { process.stderr.write(` Verified: ${verified.length}/${applied.length}\n`); if (regressions.length > 0) { process.stderr.write(` Regressions: ${regressions.join(', ')}\n`); } process.stderr.write(`\n Rollback: node scanners/rollback-cli.mjs ${backupId}\n`); } } } else { // Dry-run mode const result = await applyFixes(fixes, { dryRun: true }); applied = result.applied; if (!machineMode) { process.stderr.write(`\n Dry-run complete. Pass --apply to execute.\n`); } } // JSON output (both --json and --raw write byte-equal v5.0.0-shape stdout) if (machineMode) { const output = { planned: fixes.map(f => ({ findingId: f.findingId, file: f.file, type: f.type, description: f.description, })), applied: applied.map(a => ({ findingId: a.findingId, file: a.file, status: a.status, })), failed: failed.map(f => ({ findingId: f.findingId, file: f.file, status: f.status, error: f.error, })), verified, regressions, manual: manual.map(m => ({ findingId: m.findingId, title: m.title, recommendation: m.recommendation, })), backupId, }; process.stdout.write(JSON.stringify(output, null, 2) + '\n'); } } // 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); }); }