/** * write-output — the one place a scanner's `--output-file` payload is written. * * Every command in this plugin follows the same contract (`.claude/rules/ux-rules.md`): * run the scanner with `--output-file 2>/dev/null`, check the exit code, then Read * the file. The path the command chooses is frequently one it has never created — e.g. * `commands/campaign.md` writes its report to * `~/.claude/config-audit/sessions/campaign-report.json`, which on a fresh machine does not * exist yet. That is precisely the FIRST run, the case campaign-cli otherwise handles * gracefully by reporting `initialized: false`. * * Before this helper existed, all 13 payload writers called `writeFile` directly and threw * ENOENT there. The exit code was 3, and the command's own exit-code table reads 3 as "the * input is missing or corrupt" — so the user was told the ledger might be corrupt and * warned off the one action that would have fixed anything. `saveLedger` had always created * its parent directory; the payload write simply never did. The asymmetry was accidental. * * Creating the parent is the honest behaviour: the caller asked for a file at a path, and * nothing about a missing intermediate directory is an error the caller can learn from. */ import { writeFile, mkdir } from 'node:fs/promises'; import { dirname } from 'node:path'; /** * Write a scanner payload, creating the parent directory if needed. * * Signature-compatible with `writeFile(path, contents, encoding)` so call sites are a pure * rename — the encoding argument is kept rather than defaulted away. * * @param {string} path - destination file * @param {string} contents - serialized payload * @param {string} [encoding='utf-8'] * @returns {Promise} */ export async function writeOutputFile(path, contents, encoding = 'utf-8') { await mkdir(dirname(path), { recursive: true }); await writeFile(path, contents, encoding); }