/** * Target-path precondition shared by the target-taking CLIs. * * A scan target is a scan ROOT. If it does not exist, or is not a directory, * the scanner cannot do its job — and by the plugin's exit-code contract that * is exit 3, not a verdict. Codes 0/1/2 are PASS/WARNING/FAIL *about a * configuration that was examined*; every command template gates on exactly * that distinction, so returning a verdict for an unreadable target sends a * typo'd path through the whole workflow as a clean result. * * Measured before this guard existed (session #56): * node scanners/posture.mjs /nonexistent/path/xyz → exit 0, * "Health: B (86/100) — Good shape — a few items to address" * * The message and exit code here are not new: `manifest.mjs`, * `token-hotspots-cli.mjs`, `whats-active.mjs` and `optimize-lens-cli.mjs` * already carried this exact block inline. This module is where the CLIs that * lacked it get it from; the four that have their own copies are left alone * (consolidating them is a cleanup, not part of this fix). */ import { stat } from 'node:fs/promises'; /** * Verify that `absPath` is an existing directory. * * Writes the diagnostic to stderr itself, so callers stay a two-line guard: * * if (!(await requireTargetDir(resolvedPath))) { process.exitCode = 3; return; } * * Never throws, and never calls `process.exit()` — an abrupt exit discards * unflushed stdout when the CLI is on a pipe. * * @param {string} absPath - Resolved absolute target path. * @returns {Promise} true when the target is usable as a scan root. */ export async function requireTargetDir(absPath) { try { const s = await stat(absPath); if (!s.isDirectory()) { process.stderr.write(`Error: ${absPath} is not a directory\n`); return false; } return true; } catch { process.stderr.write(`Error: path does not exist: ${absPath}\n`); return false; } }