codex-plugin-doctor 1.16.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.16.0
361
+ - uses: Esquetta/CodexPluginDoctor@v1.17.0
362
362
  with:
363
- version: "1.16.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
+ }
@@ -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>] [--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 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",
@@ -1219,6 +1222,50 @@ export async function runCli(args, io = defaultIo, options = {}) {
1219
1222
  ].join("\n"));
1220
1223
  return 0;
1221
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
+ }
1222
1269
  if (command === "fix") {
1223
1270
  if (!maybePath || maybePath.startsWith("--")) {
1224
1271
  io.writeStderr("Missing target path. Usage: codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)");
@@ -1324,10 +1371,32 @@ export async function runCli(args, io = defaultIo, options = {}) {
1324
1371
  return report.exitCode;
1325
1372
  }
1326
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
+ }
1327
1396
  const auditFlags = maybePath ? [maybePath, ...remainingArgs] : remainingArgs;
1328
1397
  const installed = auditFlags.includes("--installed");
1329
1398
  if (!installed) {
1330
- 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>]");
1331
1400
  return 2;
1332
1401
  }
1333
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.16.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",