fix(scanners): a target path that does not exist is an error, not a grade
Surfaced by the router dogfood: quoting the placeholder stopped the shell from swallowing it, which moved the failure down into the CLIs — and revealed that most of them never check the target at all. Measured: node scanners/posture.mjs /nonexistent/path/xyz --output-file … exit 0 Health: B (86/100) — Good shape — a few items to address Nothing in that output distinguishes it from a real audit: well-formed envelope, all 10 areas present, 16 opportunities reported. A typo'd path did not fail — it flattered. Exit 3 is the right code by the plugin's own contract: 0/1/2 are PASS/WARNING/ FAIL about a configuration that WAS examined, and every command template gates on exactly that distinction, so a bad path flowed through the whole workflow as a clean result. This was a consistency gap, not a design question. Measured across the nine target-taking CLIs, four already did it right with the same message and the same exit code (manifest, token-hotspots-cli, whats-active, optimize-lens-cli); five did not (scan-orchestrator and drift-cli exit 1, posture, plugin-health-scanner and fix-cli exit 0). The five now share lib/require-target-dir.mjs, which carries that exact behaviour. The four with inline copies are left alone — consolidating them is a cleanup, not part of this fix. The guard is asserted over ALL nine CLIs, so a new one cannot join the wrong half, and a third case is covered: a target that exists but is a regular file. A valid target — including an empty directory — is explicitly unaffected. Suite 1483 -> 1486, frozen v5.0.0 snapshots untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDAwy1ZXRpZxht1wyCeSbF
This commit is contained in:
parent
0b763f25c1
commit
c76dc537ce
7 changed files with 219 additions and 0 deletions
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
import { resolve } from 'node:path';
|
||||
import { writeOutputFile } from './lib/write-output.mjs';
|
||||
import { requireTargetDir } from './lib/require-target-dir.mjs';
|
||||
import { runAllScanners } from './scan-orchestrator.mjs';
|
||||
import { diffEnvelopes, formatDiffReport } from './lib/diff-engine.mjs';
|
||||
import { saveBaseline, loadBaseline, listBaselines } from './lib/baseline.mjs';
|
||||
|
|
@ -93,6 +94,11 @@ async function main() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!(await requireTargetDir(resolve(targetPath)))) {
|
||||
process.exitCode = 3;
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Save mode ---
|
||||
if (save) {
|
||||
if (!jsonMode && !rawMode) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
import { resolve } from 'node:path';
|
||||
import { writeOutputFile } from './lib/write-output.mjs';
|
||||
import { requireTargetDir } from './lib/require-target-dir.mjs';
|
||||
import { runAllScanners } from './scan-orchestrator.mjs';
|
||||
import { planFixes, applyFixes, verifyFixes } from './fix-engine.mjs';
|
||||
import { createBackup } from './lib/backup.mjs';
|
||||
|
|
@ -66,6 +67,11 @@ async function main() {
|
|||
|
||||
const resolvedPath = resolve(targetPath);
|
||||
|
||||
if (!(await requireTargetDir(resolvedPath))) {
|
||||
process.exitCode = 3;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!machineMode) {
|
||||
process.stderr.write(`Config-Audit Fix CLI v2.1.0\n`);
|
||||
process.stderr.write(`Target: ${resolvedPath}\n`);
|
||||
|
|
|
|||
49
scanners/lib/require-target-dir.mjs
Normal file
49
scanners/lib/require-target-dir.mjs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* 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<boolean>} 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
import { readdir, stat, readFile } from 'node:fs/promises';
|
||||
import { writeOutputFile } from './lib/write-output.mjs';
|
||||
import { requireTargetDir } from './lib/require-target-dir.mjs';
|
||||
import { join, basename, resolve, sep } from 'node:path';
|
||||
import { finding, scannerResult, resetCounter } from './lib/output.mjs';
|
||||
import { SEVERITY } from './lib/severity.mjs';
|
||||
|
|
@ -772,6 +773,11 @@ async function main() {
|
|||
}
|
||||
}
|
||||
|
||||
if (!(await requireTargetDir(resolve(targetPath)))) {
|
||||
process.exitCode = 3;
|
||||
return;
|
||||
}
|
||||
|
||||
const humanizedProgress = !jsonMode && !rawMode;
|
||||
process.stderr.write(humanizedProgress ? `Plugin Health v2.1.0\n` : `Plugin Health Scanner v2.1.0\n`);
|
||||
process.stderr.write(`Target: ${resolve(targetPath)}\n\n`);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import { resolve } from 'node:path';
|
||||
import { writeOutputFile } from './lib/write-output.mjs';
|
||||
import { requireTargetDir } from './lib/require-target-dir.mjs';
|
||||
import { runAllScanners } from './scan-orchestrator.mjs';
|
||||
import { humanizeEnvelope } from './lib/humanizer.mjs';
|
||||
import {
|
||||
|
|
@ -86,6 +87,11 @@ async function main() {
|
|||
}
|
||||
}
|
||||
|
||||
if (!(await requireTargetDir(resolve(targetPath)))) {
|
||||
process.exitCode = 3;
|
||||
return;
|
||||
}
|
||||
|
||||
const filterFixtures = !args.includes('--include-fixtures');
|
||||
const humanizedProgress = !jsonMode && !rawMode;
|
||||
const result = await runPosture(targetPath, {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
import { resolve, sep } from 'node:path';
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { writeOutputFile } from './lib/write-output.mjs';
|
||||
import { requireTargetDir } from './lib/require-target-dir.mjs';
|
||||
import { resetCounter } from './lib/output.mjs';
|
||||
import { envelope } from './lib/output.mjs';
|
||||
import { discoverConfigFiles, discoverConfigFilesMulti, discoverFullMachinePaths } from './lib/file-discovery.mjs';
|
||||
|
|
@ -258,6 +259,11 @@ async function main() {
|
|||
const jsonMode = args.includes('--json');
|
||||
const rawMode = args.includes('--raw');
|
||||
|
||||
if (!(await requireTargetDir(resolve(targetPath)))) {
|
||||
process.exitCode = 3;
|
||||
return;
|
||||
}
|
||||
|
||||
const humanizedProgress = !jsonMode && !rawMode;
|
||||
process.stderr.write(humanizedProgress ? `Config-Audit v2.2.0\n` : `Config-Audit Scanner v2.2.0\n`);
|
||||
process.stderr.write(`Target: ${resolve(targetPath)}\n`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue