codex-plugin-doctor 1.31.0 → 1.32.0

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/README.md CHANGED
@@ -400,9 +400,9 @@ jobs:
400
400
  runs-on: ubuntu-latest
401
401
  steps:
402
402
  - uses: actions/checkout@v5
403
- - uses: Esquetta/CodexPluginDoctor@v1.31.0
403
+ - uses: Esquetta/CodexPluginDoctor@v1.32.0
404
404
  with:
405
- version: "1.31.0"
405
+ version: "1.32.0"
406
406
  path: .
407
407
  runtime: "true"
408
408
  policy: codex-publish
@@ -4,10 +4,14 @@ export interface WatchPluginOptions {
4
4
  runtime?: boolean;
5
5
  jsonOutput?: boolean;
6
6
  outputPath?: string | null;
7
+ maxIterations?: number;
8
+ failFast?: boolean;
9
+ accumulateJsonPath?: string | null;
7
10
  }
8
11
  export interface WatchPluginResult {
9
12
  targetPath: string;
10
13
  validations: number;
11
14
  failures: number;
15
+ iterationsReached?: boolean;
12
16
  }
13
17
  export declare function watchPlugin(options: WatchPluginOptions): Promise<WatchPluginResult>;
@@ -1,6 +1,27 @@
1
1
  import { watch } from "node:fs";
2
+ import { writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { validatePlugin } from "./validate-plugin.js";
5
+ const ignoredDirs = new Set([
6
+ "node_modules",
7
+ ".git",
8
+ "dist",
9
+ "coverage",
10
+ ".cache",
11
+ "build",
12
+ ".turbo",
13
+ ".next",
14
+ ".nuxt",
15
+ "__pycache__"
16
+ ]);
17
+ function isIgnored(filename) {
18
+ for (const dir of ignoredDirs) {
19
+ if (filename.includes(dir) || filename.startsWith(`${dir}/`) || filename.startsWith(`${dir}\\`)) {
20
+ return true;
21
+ }
22
+ }
23
+ return false;
24
+ }
4
25
  function renderWatchValidation(result, iteration, jsonOutput) {
5
26
  if (jsonOutput) {
6
27
  const report = {
@@ -12,7 +33,7 @@ function renderWatchValidation(result, iteration, jsonOutput) {
12
33
  findingsCount: result.findings.length,
13
34
  findings: result.findings
14
35
  };
15
- return JSON.stringify(report, null, 2);
36
+ return JSON.stringify(report);
16
37
  }
17
38
  const icon = result.status === "pass" ? "PASS" : "FAIL";
18
39
  return [
@@ -24,25 +45,40 @@ function renderWatchValidation(result, iteration, jsonOutput) {
24
45
  ].filter(Boolean).join("\n");
25
46
  }
26
47
  export async function watchPlugin(options) {
27
- const { targetPath, debounceMs = 300, runtime = false, jsonOutput = false, outputPath = null } = options;
48
+ const { targetPath, debounceMs = 300, runtime = false, jsonOutput = false, outputPath = null, maxIterations = 0, failFast = false, accumulateJsonPath = null } = options;
28
49
  const resolvedPath = path.resolve(targetPath);
29
50
  let validations = 0;
30
51
  let failures = 0;
31
52
  let timer = null;
32
53
  let pending = false;
33
54
  const runValidation = async (iteration) => {
34
- const result = await validatePlugin(resolvedPath, { runtime });
35
- validations += 1;
36
- if (result.status !== "pass") {
37
- failures += 1;
55
+ try {
56
+ const result = await validatePlugin(resolvedPath, { runtime });
57
+ if (result.status !== "pass") {
58
+ failures += 1;
59
+ }
60
+ const output = renderWatchValidation(result, iteration, jsonOutput);
61
+ if (outputPath) {
62
+ await writeFile(outputPath, output, "utf8");
63
+ }
64
+ if (accumulateJsonPath && jsonOutput) {
65
+ await writeFile(accumulateJsonPath, `${output}\n`, { encoding: "utf8", flag: "a" });
66
+ }
67
+ if (!outputPath && !accumulateJsonPath) {
68
+ if (jsonOutput) {
69
+ process.stdout.write(`${JSON.stringify(JSON.parse(output), null, 2)}\n\n`);
70
+ }
71
+ else {
72
+ process.stdout.write(`${output}\n\n`);
73
+ }
74
+ }
38
75
  }
39
- const output = renderWatchValidation(result, iteration, jsonOutput);
40
- if (outputPath) {
41
- const { writeFile } = await import("node:fs/promises");
42
- await writeFile(outputPath, output, "utf8");
76
+ catch (error) {
77
+ process.stderr.write(`Watch validation error: ${error.message}\n`);
78
+ failures += 1;
43
79
  }
44
- else {
45
- process.stdout.write(`${output}\n\n`);
80
+ finally {
81
+ validations += 1;
46
82
  }
47
83
  };
48
84
  return new Promise((resolve, _reject) => {
@@ -60,10 +96,29 @@ export async function watchPlugin(options) {
60
96
  timer = null;
61
97
  iteration += 1;
62
98
  await runValidation(iteration);
99
+ if (failFast && failures > 0) {
100
+ cleanup();
101
+ resolve({ targetPath: resolvedPath, validations, failures });
102
+ return;
103
+ }
104
+ if (maxIterations > 0 && validations >= maxIterations) {
105
+ cleanup();
106
+ resolve({ targetPath: resolvedPath, validations, failures, iterationsReached: true });
107
+ return;
108
+ }
109
+ if (maxIterations > 0) {
110
+ schedule();
111
+ }
63
112
  }, debounceMs);
64
113
  };
65
114
  const watcher = watch(resolvedPath, { recursive: true }, (_eventType, filename) => {
66
- if (!filename || filename.includes("node_modules") || filename.startsWith(".git")) {
115
+ if (!filename || isIgnored(filename)) {
116
+ return;
117
+ }
118
+ if (outputPath && filename === path.basename(outputPath)) {
119
+ return;
120
+ }
121
+ if (accumulateJsonPath && filename === path.basename(accumulateJsonPath)) {
67
122
  return;
68
123
  }
69
124
  schedule();
@@ -71,17 +126,31 @@ export async function watchPlugin(options) {
71
126
  watcher.on("error", (error) => {
72
127
  process.stderr.write(`Watch error: ${error.message}\n`);
73
128
  });
74
- const handleExit = () => {
129
+ const cleanup = () => {
75
130
  if (timer !== null) {
76
131
  clearTimeout(timer);
77
132
  }
78
133
  watcher.close();
134
+ process.removeListener("SIGINT", handleExit);
135
+ process.removeListener("SIGTERM", handleExit);
136
+ process.removeListener("SIGHUP", handleExit);
137
+ };
138
+ const handleExit = () => {
139
+ cleanup();
79
140
  resolve({ targetPath: resolvedPath, validations, failures });
80
141
  };
81
142
  process.on("SIGINT", handleExit);
82
143
  process.on("SIGTERM", handleExit);
83
144
  process.on("SIGHUP", handleExit);
84
- process.stdout.write(`Watching ${resolvedPath} for changes (debounce: ${debounceMs}ms, runtime: ${runtime ? "enabled" : "disabled"})...\nPress Ctrl+C to stop.\n\n`);
145
+ const flags = [];
146
+ if (maxIterations > 0) {
147
+ flags.push(`max-iterations: ${maxIterations}`);
148
+ }
149
+ if (failFast) {
150
+ flags.push("fail-fast");
151
+ }
152
+ const flagsStr = flags.length > 0 ? `, ${flags.join(", ")}` : "";
153
+ process.stdout.write(`Watching ${resolvedPath} for changes (debounce: ${debounceMs}ms, runtime: ${runtime ? "enabled" : "disabled"}${flagsStr})...\nPress Ctrl+C to stop.\n\n`);
85
154
  schedule();
86
155
  });
87
156
  }
package/dist/run-cli.js CHANGED
@@ -78,7 +78,7 @@ const defaultIo = {
78
78
  }
79
79
  };
80
80
  function printUsage(io) {
81
- io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor audit deps <path> [--json] [--output <path>]\n codex-plugin-doctor mcp <path> [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor suppress add <path> [--fingerprint <sha256> --reason <text> --expires-at YYYY-MM-DD] [--config <path>] [--json]\n codex-plugin-doctor suppress list <path> [--config <path>] [--json]\n codex-plugin-doctor suppress remove <path> [--fingerprint <sha256>|--index <n>] [--config <path>] [--json]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor watch <path> [--runtime] [--json] [--output <path>] [--debounce-ms <ms>]\n codex-plugin-doctor doctor [npm <package>|contract|corpus|runtime-plan <path> [--json|--markdown] [--output <path>]|runtime-policy <path> [--json] [--output <path>]|review-bundle <path> --output <dir> --sign-key-env NAME [--json] [--allow-dirty] [--allow-untagged]|review-bundle verify <bundle-dir> --target <path> --sign-key-env NAME [--json] [--output <path>] [--failures-only]|review-bundle diff --before <dir> --after <dir> [--json]|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|release-evidence <path> --sign-key-env NAME [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>]|release-evidence verify <evidence.json> --target <path> --sign-key-env NAME|release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME [--upload]|mcp <path>|inspector <path>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path> [--max-total-ms <ms>] [--max-stage-ms stage=ms]|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor init-git-hooks [path] [--force] [--json]\n codex-plugin-doctor init-git-hooks [path] --remove [--json]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
81
+ io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor audit deps <path> [--json] [--output <path>]\n codex-plugin-doctor mcp <path> [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor suppress add <path> [--fingerprint <sha256> --reason <text> --expires-at YYYY-MM-DD] [--config <path>] [--json]\n codex-plugin-doctor suppress list <path> [--config <path>] [--json]\n codex-plugin-doctor suppress remove <path> [--fingerprint <sha256>|--index <n>] [--config <path>] [--json]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor watch <path> [--runtime] [--json] [--output <path>] [--debounce-ms <ms>] [--max-iterations <n>] [--fail-fast] [--accumulate-json <path>]\n codex-plugin-doctor doctor [npm <package>|contract|corpus|runtime-plan <path> [--json|--markdown] [--output <path>]|runtime-policy <path> [--json] [--output <path>]|review-bundle <path> --output <dir> --sign-key-env NAME [--json] [--allow-dirty] [--allow-untagged]|review-bundle verify <bundle-dir> --target <path> --sign-key-env NAME [--json] [--output <path>] [--failures-only]|review-bundle diff --before <dir> --after <dir> [--json]|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|release-evidence <path> --sign-key-env NAME [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>]|release-evidence verify <evidence.json> --target <path> --sign-key-env NAME|release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME [--upload]|mcp <path>|inspector <path>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path> [--max-total-ms <ms>] [--max-stage-ms stage=ms]|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor init-git-hooks [path] [--force] [--json]\n codex-plugin-doctor init-git-hooks [path] --remove [--json]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
82
82
  io.writeStderr("Suppression governance: codex-plugin-doctor suppress list <path> [--fail-on-expired] [--fail-on-invalid] [--warn-expiring-within-days <days>]\n codex-plugin-doctor suppress prune <path> [--apply] [--json]");
83
83
  }
84
84
  const suppressUsageText = [
@@ -1815,23 +1815,41 @@ export async function runCli(args, io = defaultIo, options = {}) {
1815
1815
  : remainingArgs;
1816
1816
  const runtime = watchFlags.includes("--runtime");
1817
1817
  const jsonOutput = watchFlags.includes("--json");
1818
+ const failFast = watchFlags.includes("--fail-fast");
1818
1819
  const outputIndex = watchFlags.indexOf("--output");
1819
1820
  const outputPath = outputIndex === -1 ? null : watchFlags[outputIndex + 1];
1820
1821
  const debounceIndex = watchFlags.indexOf("--debounce-ms");
1821
1822
  const debounceRaw = debounceIndex === -1 ? null : watchFlags[debounceIndex + 1];
1822
1823
  const debounceMs = debounceRaw ? Number(debounceRaw) || 300 : 300;
1824
+ const maxIterationsIndex = watchFlags.indexOf("--max-iterations");
1825
+ const maxIterationsRaw = maxIterationsIndex === -1 ? null : watchFlags[maxIterationsIndex + 1];
1826
+ const maxIterations = maxIterationsRaw ? Number(maxIterationsRaw) || 0 : 0;
1827
+ const accumulateIndex = watchFlags.indexOf("--accumulate-json");
1828
+ const accumulatePath = accumulateIndex === -1 ? null : watchFlags[accumulateIndex + 1];
1823
1829
  if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
1824
1830
  io.writeStderr("Missing path after --output.");
1825
1831
  return 2;
1826
1832
  }
1833
+ if (maxIterationsIndex !== -1 && (!maxIterationsRaw || maxIterationsRaw.startsWith("--"))) {
1834
+ io.writeStderr("Missing number after --max-iterations.");
1835
+ return 2;
1836
+ }
1837
+ if (accumulateIndex !== -1 && (!accumulatePath || accumulatePath.startsWith("--"))) {
1838
+ io.writeStderr("Missing path after --accumulate-json.");
1839
+ return 2;
1840
+ }
1827
1841
  const result = await watchPlugin({
1828
1842
  targetPath,
1829
1843
  debounceMs,
1830
1844
  runtime,
1831
1845
  jsonOutput,
1832
- outputPath
1846
+ outputPath,
1847
+ maxIterations,
1848
+ failFast,
1849
+ accumulateJsonPath: accumulatePath
1833
1850
  });
1834
- io.writeStdout(`\nStopped watching ${result.targetPath}: ${result.validations} validations, ${result.failures} failures.`);
1851
+ const reason = result.iterationsReached ? "max iterations reached" : "stopped";
1852
+ io.writeStdout(`\nStopped watching ${result.targetPath}: ${result.validations} validations, ${result.failures} failures (${reason}).`);
1835
1853
  return result.failures > 0 ? 1 : 0;
1836
1854
  }
1837
1855
  if (command === "fix") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "1.31.0",
3
+ "version": "1.32.0",
4
4
  "description": "CLI-first validator for Codex plugins, skills, and MCP package surfaces with runtime MCP protocol validation.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",