#!/usr/bin/env node /** * campaign-cli — read-only reporter for the durable machine-wide campaign ledger * (v5.7 Fase 2, Block 3b). * * Mirrors the knowledge-refresh-cli precedent: it is the DETERMINISTIC, READ-ONLY half * of the hybrid motor. It loads the campaign ledger (the durable file that sits ABOVE * individual config-audit sessions), validates it, and emits the repo list + a * machine-wide roll-up as JSON. It NEVER writes the ledger — initialization and every * status transition belong to the command layer (Block 3c `/config-audit campaign`), * which calls the lib's pure transforms + saveLedger only on explicit, human-approved * action. A missing ledger file is reported gracefully (initialized:false), NEVER created. * * Naming: `-cli` suffix → NOT an orchestrated scanner (the scan-orchestrator only loads * scanner modules), so the scanner count is unchanged and the snapshot suite stays * byte-stable. * * Usage: * node campaign-cli.mjs [--ledger-file ] [--output-file ] * * Exit codes: 0 = initialized & valid, 1 = not initialized yet (advisory), 3 = error. */ import { resolve } from 'node:path'; import { writeFile } from 'node:fs/promises'; import { loadLedger, validateLedger, rollUp, buildBacklog, defaultLedgerPath, } from './lib/campaign-ledger.mjs'; function fail(message) { process.stderr.write(`Error: ${message}\n`); process.exit(3); } async function main() { const args = process.argv.slice(2); let ledgerFile = null; let outputFile = null; for (let i = 0; i < args.length; i++) { const a = args[i]; if (a === '--ledger-file' && args[i + 1]) ledgerFile = args[++i]; else if (a === '--output-file' && args[i + 1]) outputFile = args[++i]; } const ledgerPath = resolve(ledgerFile || defaultLedgerPath()); let ledger; try { // loadLedger returns null on ENOENT (graceful first run) and throws on parse error. ledger = await loadLedger(ledgerPath); } catch (err) { fail(`could not read ledger at ${ledgerPath}: ${err.message}`); } let payload; let exitCode; if (ledger === null) { // Graceful first run — the ledger does not exist yet. We DO NOT create it; that is // the command layer's job (Block 3c), on explicit human-approved action. payload = { status: 'ok', initialized: false, ledgerPath, schemaVersion: null, createdDate: null, updatedDate: null, repos: [], rollUp: rollUp({ repos: [] }), backlog: buildBacklog({ repos: [] }), }; exitCode = 1; // advisory: there is no campaign to report yet } else { const { valid, errors } = validateLedger(ledger); if (!valid) { fail(`ledger at ${ledgerPath} is invalid:\n - ${errors.join('\n - ')}`); } payload = { status: 'ok', initialized: true, ledgerPath, schemaVersion: ledger.schemaVersion, createdDate: ledger.createdDate, updatedDate: ledger.updatedDate, repos: ledger.repos, rollUp: rollUp(ledger), backlog: buildBacklog(ledger), }; exitCode = 0; } const json = JSON.stringify(payload, null, 2); if (outputFile) await writeFile(outputFile, json, 'utf-8'); else process.stdout.write(json + '\n'); process.exit(exitCode); } 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); }); }