audit-tools 0.32.51 → 0.32.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "audit-tools",
3
- "version": "0.32.51",
3
+ "version": "0.32.53",
4
4
  "private": false,
5
5
  "license": "ISC",
6
6
  "description": "Portable hybrid code auditing + remediation orchestrators for arbitrary repositories.",
@@ -36,44 +36,66 @@ function newestMtimeMs(path) {
36
36
  return newest;
37
37
  }
38
38
 
39
- function shouldBuildDist() {
39
+ export function shouldBuildDist() {
40
40
  if (!existsSync(sourceRoot)) {
41
41
  return false;
42
42
  }
43
- if (!existsSync(tsconfigPath)) {
44
- return !existsSync(distEntry);
45
- }
46
43
  if (!existsSync(distEntry)) {
47
44
  return true;
48
45
  }
49
- return statSync(distEntry).mtimeMs < Math.max(
50
- newestMtimeMs(sourceRoot),
51
- statSync(tsconfigPath).mtimeMs,
52
- );
46
+ // Compare dist freshness against src/ (and tsconfig.json when present). A
47
+ // missing tsconfig.json must NOT collapse this to an existence-only check: a
48
+ // dist older than src/ is still stale and must rebuild (CE-003). Previously
49
+ // the tsconfig-absent branch returned `!existsSync(distEntry)`, so a present
50
+ // but stale dist was silently used.
51
+ const newestSourceMs = existsSync(tsconfigPath)
52
+ ? Math.max(newestMtimeMs(sourceRoot), statSync(tsconfigPath).mtimeMs)
53
+ : newestMtimeMs(sourceRoot);
54
+ return statSync(distEntry).mtimeMs < newestSourceMs;
55
+ }
56
+
57
+ // Default build runner (platform-branched npm run build). Injectable in
58
+ // ensureBuilt so the build-failure control flow is unit-testable without
59
+ // spawning a real build.
60
+ function runNpmBuild() {
61
+ return process.platform === "win32"
62
+ ? spawnSync(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", "npm run build"], {
63
+ cwd: __dirname,
64
+ encoding: "utf8",
65
+ stdio: ["ignore", "pipe", "pipe"],
66
+ })
67
+ : spawnSync("npm", ["run", "build"], {
68
+ cwd: __dirname,
69
+ encoding: "utf8",
70
+ stdio: ["ignore", "pipe", "pipe"],
71
+ });
53
72
  }
54
73
 
55
- function ensureBuilt() {
56
- if (!shouldBuildDist()) return;
57
- const result =
58
- process.platform === "win32"
59
- ? spawnSync(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", "npm run build"], {
60
- cwd: __dirname,
61
- encoding: "utf8",
62
- stdio: ["ignore", "pipe", "pipe"],
63
- })
64
- : spawnSync("npm", ["run", "build"], {
65
- cwd: __dirname,
66
- encoding: "utf8",
67
- stdio: ["ignore", "pipe", "pipe"],
68
- });
74
+ // Returns true when it is safe to proceed to dist forwarding, false when a
75
+ // failed/signaled build has initiated a terminating exit action and the caller
76
+ // MUST stop. The signal branch of applyWrapperExitAction re-raises the signal
77
+ // and schedules a fallback exit but RETURNS control (the fallback can only fire
78
+ // once the event loop is free); without this boolean guard main() would then
79
+ // run its blocking spawnSync and forward the command against a stale/absent
80
+ // dist before the fallback ever fires (CE-002). The exit-code branch terminates
81
+ // synchronously, so it never reaches the `return false`.
82
+ export function ensureBuilt({
83
+ shouldBuild = shouldBuildDist,
84
+ runBuild = runNpmBuild,
85
+ applyExit = applyWrapperExitAction,
86
+ } = {}) {
87
+ if (!shouldBuild()) return true;
88
+ const result = runBuild();
69
89
  if (result.stdout) process.stderr.write(result.stdout);
70
90
  if (result.stderr) process.stderr.write(result.stderr);
71
91
  if (result.error) {
72
92
  console.error(`remediate-code: failed to auto-build dist (${result.error.message})`);
73
93
  }
74
94
  if (result.status !== 0 || result.signal) {
75
- applyWrapperExitAction(getWrapperExitAction(result));
95
+ applyExit(getWrapperExitAction(result));
96
+ return false;
76
97
  }
98
+ return true;
77
99
  }
78
100
 
79
101
  export function getWrapperExitAction(result, platform = process.platform) {
@@ -119,7 +141,11 @@ export async function main(argv = process.argv.slice(2)) {
119
141
  return;
120
142
  }
121
143
 
122
- ensureBuilt();
144
+ // A failed/signaled build returns false here; stop rather than forward the
145
+ // command against a stale/absent dist (CE-002). The exit action is already
146
+ // in flight (synchronous exit for a nonzero status, pending fallback exit for
147
+ // a re-raised signal).
148
+ if (!ensureBuilt()) return;
123
149
  if (!existsSync(distEntry)) {
124
150
  console.error("remediate-code: dist/remediate/index.js not found. Run: npm run build");
125
151
  process.exit(1);
@@ -23,12 +23,31 @@ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
23
23
  const distEntry = join(repoRoot, 'dist', 'audit', 'index.js');
24
24
  const packageJsonPath = join(repoRoot, 'package.json');
25
25
  const promptAssetPath = join(repoRoot, 'skills', 'audit-code', 'audit-code.prompt.md');
26
- const packageVersion = JSON.parse(await readFile(packageJsonPath, 'utf8')).version;
26
+
27
+ // Deferred (NOT a top-level await): package.json is only needed by the
28
+ // `--version` branch, and a top-level read would fail EVERY invocation —
29
+ // including `--help` — whenever package.json is unreadable (CE-006).
30
+ async function readPackageVersion() {
31
+ return JSON.parse(await readFile(packageJsonPath, 'utf8')).version;
32
+ }
27
33
 
28
34
  function hasFlag(argv, name) {
29
35
  return argv.includes(name);
30
36
  }
31
37
 
38
+ // Informational flags (--help/--version) short-circuit the wrapper only when
39
+ // they appear BEFORE the first non-flag token (the command). A whole-argv scan
40
+ // hijacks post-command tokens that belong to the dist CLI — e.g.
41
+ // `audit-code explain-task -v` printed the wrapper's version instead of
42
+ // forwarding `-v` to the dist command (CE-007).
43
+ function hasLeadingFlag(argv, name) {
44
+ for (const token of argv) {
45
+ if (token === name) return true;
46
+ if (!token.startsWith('-')) return false;
47
+ }
48
+ return false;
49
+ }
50
+
32
51
  function getFlag(argv, name) {
33
52
  const index = argv.indexOf(name);
34
53
  if (index < 0) return undefined;
@@ -41,6 +60,22 @@ function setDefaultFlag(argv, name, value) {
41
60
  }
42
61
  }
43
62
 
63
+ // Overwrite an existing flag's value (or append when absent). setDefaultFlag
64
+ // only fills a MISSING flag, so a user-supplied RELATIVE --root/--artifacts-dir
65
+ // was forwarded raw and then re-resolved against the child's cwd (repoRoot),
66
+ // not the caller's cwd — e.g. `--root .` pointed at the package dir (CE-001).
67
+ // Normalizing to an absolute path here makes the forwarded value cwd-stable.
68
+ function setFlag(argv, name, value) {
69
+ const index = argv.indexOf(name);
70
+ if (index < 0) {
71
+ argv.push(name, value);
72
+ } else {
73
+ argv[index + 1] = value;
74
+ }
75
+ }
76
+
77
+ export { hasLeadingFlag, setFlag };
78
+
44
79
  function nodeExecutable() {
45
80
  return process.execPath;
46
81
  }
@@ -193,8 +228,10 @@ async function runDistCommand(commandName, argv, { ensureArtifactsDir = false }
193
228
  const rootValue = resolve(getFlag(commandArgs, '--root') ?? '.');
194
229
  const artifactsDir = resolve(getFlag(commandArgs, '--artifacts-dir') ?? join(rootValue, '.audit-tools', 'audit'));
195
230
 
196
- setDefaultFlag(commandArgs, '--root', rootValue);
197
- setDefaultFlag(commandArgs, '--artifacts-dir', artifactsDir);
231
+ // Overwrite (not default) so a user-supplied relative value is normalized to
232
+ // the caller-cwd-resolved absolute path before it reaches the child (CE-001).
233
+ setFlag(commandArgs, '--root', rootValue);
234
+ setFlag(commandArgs, '--artifacts-dir', artifactsDir);
198
235
 
199
236
  if (ensureArtifactsDir) {
200
237
  await mkdir(artifactsDir, { recursive: true });
@@ -206,15 +243,21 @@ async function runDistCommand(commandName, argv, { ensureArtifactsDir = false }
206
243
  });
207
244
  }
208
245
 
209
- async function runDistCommandInline(commandName, argv) {
246
+ async function runDistCommandInline(commandName, argv, { ensureArtifactsDir = false } = {}) {
210
247
  const commandArgs = [...argv];
211
248
  const rootValue = resolve(getFlag(commandArgs, '--root') ?? '.');
212
249
  const artifactsDir = resolve(getFlag(commandArgs, '--artifacts-dir') ?? join(rootValue, '.audit-tools', 'audit'));
213
250
 
214
- setDefaultFlag(commandArgs, '--root', rootValue);
215
- setDefaultFlag(commandArgs, '--artifacts-dir', artifactsDir);
251
+ setFlag(commandArgs, '--root', rootValue);
252
+ setFlag(commandArgs, '--artifacts-dir', artifactsDir);
216
253
 
217
- await mkdir(artifactsDir, { recursive: true });
254
+ // Gate the mkdir behind the same ensureArtifactsDir flag as runDistCommand so
255
+ // "the artifacts directory is created only for designated stateful commands"
256
+ // holds on this path too (CE-001); mcp is a designated stateful command and
257
+ // opts in explicitly at its call site.
258
+ if (ensureArtifactsDir) {
259
+ await mkdir(artifactsDir, { recursive: true });
260
+ }
218
261
  await ensureBuilt();
219
262
 
220
263
  // Propagate the invocation hint into this (long-lived) server process so it
@@ -238,13 +281,13 @@ export async function runAuditCodeWrapper({
238
281
  argv = process.argv.slice(2),
239
282
  preferredEntrypoint
240
283
  }) {
241
- if (hasFlag(argv, '--help') || hasFlag(argv, '-h')) {
284
+ if (hasLeadingFlag(argv, '--help') || hasLeadingFlag(argv, '-h')) {
242
285
  printHelp({ usageName, preferredEntrypoint });
243
286
  return;
244
287
  }
245
288
 
246
- if (hasFlag(argv, '--version') || hasFlag(argv, '-v')) {
247
- console.log(packageVersion);
289
+ if (hasLeadingFlag(argv, '--version') || hasLeadingFlag(argv, '-v')) {
290
+ console.log(await readPackageVersion());
248
291
  return;
249
292
  }
250
293
 
@@ -280,7 +323,7 @@ export async function runAuditCodeWrapper({
280
323
  // because they may be the FIRST call in a fresh repo and must create the
281
324
  // run directory before dist reads it.
282
325
  if (argv[0] === 'mcp') {
283
- await runDistCommandInline('mcp', argv.slice(1));
326
+ await runDistCommandInline('mcp', argv.slice(1), { ensureArtifactsDir: true });
284
327
  return;
285
328
  }
286
329