codex-plugin-doctor 1.15.0 → 1.17.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
@@ -358,9 +358,9 @@ jobs:
358
358
  runs-on: ubuntu-latest
359
359
  steps:
360
360
  - uses: actions/checkout@v5
361
- - uses: Esquetta/CodexPluginDoctor@v1.15.0
361
+ - uses: Esquetta/CodexPluginDoctor@v1.17.0
362
362
  with:
363
- version: "1.15.0"
363
+ version: "1.17.0"
364
364
  path: .
365
365
  runtime: "true"
366
366
  policy: codex-publish
@@ -0,0 +1,17 @@
1
+ export interface DepAuditVulnerability {
2
+ name: string;
3
+ severity: "critical" | "high" | "moderate" | "low";
4
+ isDirect: boolean;
5
+ fixAvailable: boolean;
6
+ via: string[];
7
+ }
8
+ export interface DepAuditReport {
9
+ targetPath: string;
10
+ status: "pass" | "warn" | "fail";
11
+ vulnerabilities: DepAuditVulnerability[];
12
+ totalVulnerabilities: number;
13
+ auditJson: unknown;
14
+ }
15
+ export declare function buildDepAudit(targetPath: string): Promise<DepAuditReport>;
16
+ export declare function renderDepAudit(report: DepAuditReport): string;
17
+ export declare function renderDepAuditJson(report: DepAuditReport): string;
@@ -0,0 +1,169 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import path from "node:path";
4
+ function resolvePackageJson(targetPath) {
5
+ return path.resolve(targetPath, "package.json");
6
+ }
7
+ async function fileExists(filePath) {
8
+ try {
9
+ const details = await stat(filePath);
10
+ return details.isFile();
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
16
+ async function runNpmAudit(cwd) {
17
+ return new Promise((resolve, reject) => {
18
+ execFile("npm", ["audit", "--json"], { cwd, shell: process.platform === "win32", timeout: 120_000 }, (error, stdout, stderr) => {
19
+ const parsed = (() => {
20
+ try {
21
+ return JSON.parse(stdout);
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ })();
27
+ if (parsed) {
28
+ resolve(parsed);
29
+ }
30
+ else {
31
+ reject(new Error(stderr.trim() || error?.message || "npm audit failed"));
32
+ }
33
+ });
34
+ });
35
+ }
36
+ function extractVulnerabilities(auditJson) {
37
+ const result = [];
38
+ if (!auditJson || typeof auditJson !== "object") {
39
+ return result;
40
+ }
41
+ const data = auditJson;
42
+ const vulns = data.vulnerabilities;
43
+ if (!vulns) {
44
+ return result;
45
+ }
46
+ for (const [name, entry] of Object.entries(vulns)) {
47
+ if (!entry || typeof entry !== "object") {
48
+ continue;
49
+ }
50
+ const vuln = entry;
51
+ const severity = vuln.severity;
52
+ const isDirect = Boolean(vuln.isDirect);
53
+ const fixAvailable = Boolean(vuln.fixAvailable);
54
+ const via = Array.isArray(vuln.via) ? vuln.via.map((v) => (typeof v === "string" ? v : "unknown")) : [];
55
+ if (severity === "critical" || severity === "high" || severity === "moderate" || severity === "low") {
56
+ result.push({
57
+ name,
58
+ severity,
59
+ isDirect,
60
+ fixAvailable,
61
+ via
62
+ });
63
+ }
64
+ }
65
+ return result;
66
+ }
67
+ function computeStatus(vulnerabilities) {
68
+ if (vulnerabilities.length === 0) {
69
+ return "pass";
70
+ }
71
+ const hasCritical = vulnerabilities.some((v) => v.severity === "critical");
72
+ const hasHigh = vulnerabilities.some((v) => v.severity === "high");
73
+ if (hasCritical) {
74
+ return "fail";
75
+ }
76
+ if (hasHigh) {
77
+ return "fail";
78
+ }
79
+ return "warn";
80
+ }
81
+ export async function buildDepAudit(targetPath) {
82
+ const resolvedPath = path.resolve(targetPath);
83
+ const packageJsonPath = resolvePackageJson(resolvedPath);
84
+ if (!(await fileExists(packageJsonPath))) {
85
+ return {
86
+ targetPath: resolvedPath,
87
+ status: "pass",
88
+ vulnerabilities: [],
89
+ totalVulnerabilities: 0,
90
+ auditJson: null
91
+ };
92
+ }
93
+ let pkg;
94
+ try {
95
+ pkg = JSON.parse(await readFile(packageJsonPath, "utf8"));
96
+ }
97
+ catch {
98
+ return {
99
+ targetPath: resolvedPath,
100
+ status: "warn",
101
+ vulnerabilities: [],
102
+ totalVulnerabilities: 0,
103
+ auditJson: { error: "Failed to parse package.json" }
104
+ };
105
+ }
106
+ const hasDeps = pkg.dependencies || pkg.devDependencies;
107
+ if (!hasDeps) {
108
+ return {
109
+ targetPath: resolvedPath,
110
+ status: "pass",
111
+ vulnerabilities: [],
112
+ totalVulnerabilities: 0,
113
+ auditJson: { message: "No runtime dependencies to audit" }
114
+ };
115
+ }
116
+ let auditJson;
117
+ try {
118
+ auditJson = await runNpmAudit(resolvedPath);
119
+ }
120
+ catch (error) {
121
+ return {
122
+ targetPath: resolvedPath,
123
+ status: "warn",
124
+ vulnerabilities: [],
125
+ totalVulnerabilities: 0,
126
+ auditJson: { error: error.message }
127
+ };
128
+ }
129
+ const vulnerabilities = extractVulnerabilities(auditJson);
130
+ const status = computeStatus(vulnerabilities);
131
+ return {
132
+ targetPath: resolvedPath,
133
+ status,
134
+ vulnerabilities,
135
+ totalVulnerabilities: vulnerabilities.length,
136
+ auditJson
137
+ };
138
+ }
139
+ export function renderDepAudit(report) {
140
+ const lines = [
141
+ "Dependency Vulnerability Audit",
142
+ "=============================",
143
+ `Path: ${report.targetPath}`,
144
+ `Status: ${report.status.toUpperCase()}`,
145
+ `Vulnerabilities: ${report.totalVulnerabilities}`,
146
+ ""
147
+ ];
148
+ if (report.vulnerabilities.length === 0) {
149
+ lines.push("No known vulnerabilities found.");
150
+ return lines.join("\n");
151
+ }
152
+ for (const vuln of report.vulnerabilities) {
153
+ const tag = vuln.severity === "critical" ? "CRITICAL" :
154
+ vuln.severity === "high" ? "HIGH" :
155
+ vuln.severity === "moderate" ? "MODERATE" : "LOW";
156
+ lines.push(`${tag.padEnd(10)} ${vuln.name}`, ` Direct: ${vuln.isDirect ? "yes" : "no"}`, ` Fix available: ${vuln.fixAvailable ? "yes" : "no"}`, vuln.via.length > 0 ? ` Via: ${vuln.via.join(", ")}` : "", "");
157
+ }
158
+ return lines.join("\n");
159
+ }
160
+ export function renderDepAuditJson(report) {
161
+ return JSON.stringify({
162
+ schemaVersion: "1.0.0",
163
+ targetPath: report.targetPath,
164
+ status: report.status,
165
+ totalVulnerabilities: report.totalVulnerabilities,
166
+ vulnerabilities: report.vulnerabilities,
167
+ audit: report.auditJson
168
+ }, null, 2);
169
+ }
@@ -0,0 +1,8 @@
1
+ export interface InitGitHooksResult {
2
+ rootPath: string;
3
+ hookPaths: string[];
4
+ preExisting: string[];
5
+ }
6
+ export declare function initGitHooks(targetPath: string, options?: {
7
+ force?: boolean;
8
+ }): Promise<InitGitHooksResult>;
@@ -0,0 +1,77 @@
1
+ import { chmod, mkdir, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ const preCommitHook = [
4
+ "#!/usr/bin/env sh",
5
+ "# Generated by codex-plugin-doctor init git-hooks",
6
+ "# Validates the plugin package before every commit.",
7
+ "",
8
+ "echo \"Codex Plugin Doctor: running pre-commit validation...\"",
9
+ "",
10
+ "npx --yes codex-plugin-doctor check . --profile ci",
11
+ "",
12
+ "if [ $? -ne 0 ]; then",
13
+ " echo \"\"",
14
+ " echo \"Plugin validation failed. Commit blocked.\"",
15
+ " echo \"Run 'codex-plugin-doctor check .' to see full diagnostics.\"",
16
+ " exit 1",
17
+ "fi",
18
+ ""
19
+ ].join("\n");
20
+ const prePushHook = [
21
+ "#!/usr/bin/env sh",
22
+ "# Generated by codex-plugin-doctor init git-hooks",
23
+ "# Validates the plugin package before every push.",
24
+ "",
25
+ "echo \"Codex Plugin Doctor: running pre-push validation...\"",
26
+ "",
27
+ "npx --yes codex-plugin-doctor check . --profile ci --runtime",
28
+ "",
29
+ "if [ $? -ne 0 ]; then",
30
+ " echo \"\"",
31
+ " echo \"Plugin validation failed. Push blocked.\"",
32
+ " echo \"Run 'codex-plugin-doctor check . --runtime' to see full diagnostics.\"",
33
+ " exit 1",
34
+ "fi",
35
+ ""
36
+ ].join("\n");
37
+ async function fileExists(filePath) {
38
+ try {
39
+ await stat(filePath);
40
+ return true;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ async function writeHook(hookPath, content, force) {
47
+ const exists = await fileExists(hookPath);
48
+ if (exists && !force) {
49
+ return false;
50
+ }
51
+ await writeFile(hookPath, content, "utf8");
52
+ if (process.platform !== "win32") {
53
+ await chmod(hookPath, 0o755);
54
+ }
55
+ return exists;
56
+ }
57
+ export async function initGitHooks(targetPath, options = {}) {
58
+ const rootPath = path.resolve(targetPath);
59
+ const hooksDir = path.join(rootPath, ".git", "hooks");
60
+ const force = options.force ?? false;
61
+ await mkdir(hooksDir, { recursive: true });
62
+ const hookPaths = [];
63
+ const preExisting = [];
64
+ const preCommitPath = path.join(hooksDir, "pre-commit");
65
+ const prePushPath = path.join(hooksDir, "pre-push");
66
+ const wasPreCommitExisting = await writeHook(preCommitPath, preCommitHook, force);
67
+ hookPaths.push(preCommitPath);
68
+ if (wasPreCommitExisting) {
69
+ preExisting.push(preCommitPath);
70
+ }
71
+ const wasPrePushExisting = await writeHook(prePushPath, prePushHook, force);
72
+ hookPaths.push(prePushPath);
73
+ if (wasPrePushExisting) {
74
+ preExisting.push(prePushPath);
75
+ }
76
+ return { rootPath, hookPaths, preExisting };
77
+ }
@@ -125,7 +125,9 @@ export declare function verifyDoctorReviewBundle(bundleDirectory: string, option
125
125
  targetPath: string;
126
126
  }): Promise<DoctorReviewBundleVerificationReport>;
127
127
  export declare function renderDoctorReviewBundleVerificationJson(report: DoctorReviewBundleVerificationReport): string;
128
- export declare function renderDoctorReviewBundleVerification(report: DoctorReviewBundleVerificationReport): string;
128
+ export declare function renderDoctorReviewBundleVerification(report: DoctorReviewBundleVerificationReport, options?: {
129
+ failuresOnly?: boolean;
130
+ }): string;
129
131
  export declare function renderDoctorReviewBundleDiffJson(report: DoctorReviewBundleDiffReport): string;
130
132
  export declare function renderDoctorReviewBundleDiff(report: DoctorReviewBundleDiffReport): string;
131
133
  export declare function renderDoctorReviewBundle(bundle: DoctorReviewBundle): string;
@@ -730,7 +730,7 @@ export async function verifyDoctorReviewBundle(bundleDirectory, options) {
730
730
  export function renderDoctorReviewBundleVerificationJson(report) {
731
731
  return JSON.stringify(report, null, 2);
732
732
  }
733
- export function renderDoctorReviewBundleVerification(report) {
733
+ export function renderDoctorReviewBundleVerification(report, options = {}) {
734
734
  const lines = [
735
735
  "Doctor Review Bundle Verification",
736
736
  "=================================",
@@ -757,6 +757,9 @@ export function renderDoctorReviewBundleVerification(report) {
757
757
  lines.push(` ${check.message}`);
758
758
  }
759
759
  }
760
+ if (options.failuresOnly) {
761
+ return lines.join("\n");
762
+ }
760
763
  lines.push("", "Checks", "------");
761
764
  for (const check of report.checks) {
762
765
  lines.push(`${check.status === "pass" ? "PASS" : "FAIL"} ${check.id}`);
@@ -0,0 +1,13 @@
1
+ export interface WatchPluginOptions {
2
+ targetPath: string;
3
+ debounceMs?: number;
4
+ runtime?: boolean;
5
+ jsonOutput?: boolean;
6
+ outputPath?: string | null;
7
+ }
8
+ export interface WatchPluginResult {
9
+ targetPath: string;
10
+ validations: number;
11
+ failures: number;
12
+ }
13
+ export declare function watchPlugin(options: WatchPluginOptions): Promise<WatchPluginResult>;
@@ -0,0 +1,87 @@
1
+ import { watch } from "node:fs";
2
+ import path from "node:path";
3
+ import { validatePlugin } from "./validate-plugin.js";
4
+ function renderWatchValidation(result, iteration, jsonOutput) {
5
+ if (jsonOutput) {
6
+ const report = {
7
+ schemaVersion: "1.0.0",
8
+ iteration,
9
+ timestamp: new Date().toISOString(),
10
+ targetPath: result.targetPath,
11
+ status: result.status,
12
+ findingsCount: result.findings.length,
13
+ findings: result.findings
14
+ };
15
+ return JSON.stringify(report, null, 2);
16
+ }
17
+ const icon = result.status === "pass" ? "PASS" : "FAIL";
18
+ return [
19
+ `[#${String(iteration).padStart(3, "0")}] ${icon} ${result.targetPath}`,
20
+ ` findings: ${result.findings.length} (${result.status})`,
21
+ result.findings.length > 0
22
+ ? result.findings.map((f) => ` ${f.severity === "fail" ? "FAIL" : "WARN"} ${f.id}: ${f.message}`).join("\n")
23
+ : ""
24
+ ].filter(Boolean).join("\n");
25
+ }
26
+ export async function watchPlugin(options) {
27
+ const { targetPath, debounceMs = 300, runtime = false, jsonOutput = false, outputPath = null } = options;
28
+ const resolvedPath = path.resolve(targetPath);
29
+ let validations = 0;
30
+ let failures = 0;
31
+ let timer = null;
32
+ let pending = false;
33
+ const runValidation = async (iteration) => {
34
+ const result = await validatePlugin(resolvedPath, { runtime });
35
+ validations += 1;
36
+ if (result.status !== "pass") {
37
+ failures += 1;
38
+ }
39
+ const output = renderWatchValidation(result, iteration, jsonOutput);
40
+ if (outputPath) {
41
+ const { writeFile } = await import("node:fs/promises");
42
+ await writeFile(outputPath, output, "utf8");
43
+ }
44
+ else {
45
+ process.stdout.write(`${output}\n\n`);
46
+ }
47
+ };
48
+ return new Promise((resolve, _reject) => {
49
+ let iteration = 0;
50
+ const schedule = () => {
51
+ if (timer !== null) {
52
+ clearTimeout(timer);
53
+ }
54
+ if (pending) {
55
+ return;
56
+ }
57
+ pending = true;
58
+ timer = setTimeout(async () => {
59
+ pending = false;
60
+ timer = null;
61
+ iteration += 1;
62
+ await runValidation(iteration);
63
+ }, debounceMs);
64
+ };
65
+ const watcher = watch(resolvedPath, { recursive: true }, (_eventType, filename) => {
66
+ if (!filename || filename.includes("node_modules") || filename.startsWith(".git")) {
67
+ return;
68
+ }
69
+ schedule();
70
+ });
71
+ watcher.on("error", (error) => {
72
+ process.stderr.write(`Watch error: ${error.message}\n`);
73
+ });
74
+ const handleExit = () => {
75
+ if (timer !== null) {
76
+ clearTimeout(timer);
77
+ }
78
+ watcher.close();
79
+ resolve({ targetPath: resolvedPath, validations, failures });
80
+ };
81
+ process.on("SIGINT", handleExit);
82
+ process.on("SIGTERM", handleExit);
83
+ 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`);
85
+ schedule();
86
+ });
87
+ }
package/dist/index.d.ts CHANGED
@@ -19,4 +19,7 @@ export { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorIn
19
19
  export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson, type EcosystemAuditReport } from "./audit/ecosystem-audit.js";
20
20
  export { applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames, type PolicyPackName } from "./policy/policy-packs.js";
21
21
  export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson, type GenericMcpDoctorReport } from "./mcp/generic-mcp-doctor.js";
22
+ export { watchPlugin, type WatchPluginOptions, type WatchPluginResult } from "./core/watch-plugin.js";
23
+ export { buildDepAudit, renderDepAudit, renderDepAuditJson, type DepAuditReport, type DepAuditVulnerability } from "./core/dep-audit.js";
24
+ export { initGitHooks, type InitGitHooksResult } from "./core/init-git-hooks.js";
22
25
  export declare function runCheck(targetPath: string, options?: CheckOptions): Promise<CheckResult>;
package/dist/index.js CHANGED
@@ -19,6 +19,9 @@ export { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorIn
19
19
  export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson } from "./audit/ecosystem-audit.js";
20
20
  export { applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames } from "./policy/policy-packs.js";
21
21
  export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson } from "./mcp/generic-mcp-doctor.js";
22
+ export { watchPlugin } from "./core/watch-plugin.js";
23
+ export { buildDepAudit, renderDepAudit, renderDepAuditJson } from "./core/dep-audit.js";
24
+ export { initGitHooks } from "./core/init-git-hooks.js";
22
25
  export async function runCheck(targetPath, options = {}) {
23
26
  return validatePlugin(targetPath, options);
24
27
  }
package/dist/run-cli.js CHANGED
@@ -30,6 +30,9 @@ import { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorIn
30
30
  import { applyFixPlan, buildFixPlan, renderApplyFixResult, renderFixPlanJsonReport, renderFixPlan } from "./core/fix-plan.js";
31
31
  import { renderClientDoctor, renderEnvironmentDoctor, renderEnvironmentDoctorJson } from "./core/environment-doctor.js";
32
32
  import { initCiWorkflow } from "./core/init-ci.js";
33
+ import { watchPlugin } from "./core/watch-plugin.js";
34
+ import { buildDepAudit, renderDepAudit, renderDepAuditJson } from "./core/dep-audit.js";
35
+ import { initGitHooks } from "./core/init-git-hooks.js";
33
36
  import { initPluginPackage, initPluginTemplates, isInitPluginTemplate } from "./core/init-plugin.js";
34
37
  import { runCheck } from "./index.js";
35
38
  import { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson } from "./mcp/generic-mcp-doctor.js";
@@ -73,7 +76,7 @@ const defaultIo = {
73
76
  }
74
77
  };
75
78
  function printUsage(io) {
76
- 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 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 fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\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>]|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 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");
79
+ 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 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]\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");
77
80
  }
78
81
  const performanceStageNames = new Set([
79
82
  "validation",
@@ -449,6 +452,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
449
452
  : null;
450
453
  const verifyFlags = bundleDirectory ? remainingArgs.slice(2) : remainingArgs.slice(1);
451
454
  const jsonOutput = verifyFlags.includes("--json");
455
+ const failuresOnly = verifyFlags.includes("--failures-only");
452
456
  const outputIndex = verifyFlags.indexOf("--output");
453
457
  const outputPath = outputIndex === -1 ? null : verifyFlags[outputIndex + 1];
454
458
  const targetIndex = verifyFlags.indexOf("--target");
@@ -490,7 +494,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
490
494
  });
491
495
  const renderedReport = jsonOutput
492
496
  ? renderDoctorReviewBundleVerificationJson(report)
493
- : renderDoctorReviewBundleVerification(report);
497
+ : renderDoctorReviewBundleVerification(report, { failuresOnly });
494
498
  if (outputPath) {
495
499
  await writeFile(outputPath, renderedReport, "utf8");
496
500
  }
@@ -1218,6 +1222,50 @@ export async function runCli(args, io = defaultIo, options = {}) {
1218
1222
  ].join("\n"));
1219
1223
  return 0;
1220
1224
  }
1225
+ if (command === "init-git-hooks") {
1226
+ const targetPath = maybePath && !maybePath.startsWith("--") ? maybePath : ".";
1227
+ const initFlags = maybePath && maybePath.startsWith("--")
1228
+ ? [maybePath, ...remainingArgs]
1229
+ : remainingArgs;
1230
+ const force = initFlags.includes("--force");
1231
+ const result = await initGitHooks(targetPath, { force });
1232
+ const overwritten = result.preExisting.length > 0
1233
+ ? `\nOverwritten existing hooks: ${result.preExisting.join(", ")}`
1234
+ : "";
1235
+ io.writeStdout([
1236
+ "Initialized Codex Plugin Doctor git hooks",
1237
+ `Root: ${result.rootPath}`,
1238
+ `Hooks: ${result.hookPaths.join(", ")}`,
1239
+ overwritten
1240
+ ].filter(Boolean).join("\n"));
1241
+ return 0;
1242
+ }
1243
+ if (command === "watch") {
1244
+ const targetPath = maybePath && !maybePath.startsWith("--") ? maybePath : ".";
1245
+ const watchFlags = maybePath && maybePath.startsWith("--")
1246
+ ? [maybePath, ...remainingArgs]
1247
+ : remainingArgs;
1248
+ const runtime = watchFlags.includes("--runtime");
1249
+ const jsonOutput = watchFlags.includes("--json");
1250
+ const outputIndex = watchFlags.indexOf("--output");
1251
+ const outputPath = outputIndex === -1 ? null : watchFlags[outputIndex + 1];
1252
+ const debounceIndex = watchFlags.indexOf("--debounce-ms");
1253
+ const debounceRaw = debounceIndex === -1 ? null : watchFlags[debounceIndex + 1];
1254
+ const debounceMs = debounceRaw ? Number(debounceRaw) || 300 : 300;
1255
+ if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
1256
+ io.writeStderr("Missing path after --output.");
1257
+ return 2;
1258
+ }
1259
+ const result = await watchPlugin({
1260
+ targetPath,
1261
+ debounceMs,
1262
+ runtime,
1263
+ jsonOutput,
1264
+ outputPath
1265
+ });
1266
+ io.writeStdout(`\nStopped watching ${result.targetPath}: ${result.validations} validations, ${result.failures} failures.`);
1267
+ return result.failures > 0 ? 1 : 0;
1268
+ }
1221
1269
  if (command === "fix") {
1222
1270
  if (!maybePath || maybePath.startsWith("--")) {
1223
1271
  io.writeStderr("Missing target path. Usage: codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)");
@@ -1323,10 +1371,32 @@ export async function runCli(args, io = defaultIo, options = {}) {
1323
1371
  return report.exitCode;
1324
1372
  }
1325
1373
  if (command === "audit") {
1374
+ if (maybePath === "deps") {
1375
+ const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--") ? remainingArgs[0] : ".";
1376
+ const depsFlags = remainingArgs[0] && remainingArgs[0].startsWith("--")
1377
+ ? remainingArgs
1378
+ : remainingArgs.slice(1);
1379
+ const jsonOutput = depsFlags.includes("--json");
1380
+ const outputIndex = depsFlags.indexOf("--output");
1381
+ const outputPath = outputIndex === -1 ? null : depsFlags[outputIndex + 1];
1382
+ if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
1383
+ io.writeStderr("Missing path after --output.");
1384
+ return 2;
1385
+ }
1386
+ const report = await buildDepAudit(targetPath);
1387
+ const renderedReport = jsonOutput
1388
+ ? renderDepAuditJson(report)
1389
+ : renderDepAudit(report);
1390
+ if (outputPath) {
1391
+ await writeFile(outputPath, renderedReport, "utf8");
1392
+ }
1393
+ io.writeStdout(renderedReport);
1394
+ return report.status === "fail" ? 1 : 0;
1395
+ }
1326
1396
  const auditFlags = maybePath ? [maybePath, ...remainingArgs] : remainingArgs;
1327
1397
  const installed = auditFlags.includes("--installed");
1328
1398
  if (!installed) {
1329
- io.writeStderr("Usage: codex-plugin-doctor audit --installed [filter] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]");
1399
+ io.writeStderr("Usage: codex-plugin-doctor audit --installed [filter] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor audit deps <path> [--json] [--output <path>]");
1330
1400
  return 2;
1331
1401
  }
1332
1402
  const installedIndex = auditFlags.indexOf("--installed");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "1.15.0",
3
+ "version": "1.17.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",