codex-plugin-doctor 1.16.0 → 1.18.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 +23 -11
- package/dist/core/dep-audit.d.ts +17 -0
- package/dist/core/dep-audit.js +169 -0
- package/dist/core/init-git-hooks.d.ts +8 -0
- package/dist/core/init-git-hooks.js +77 -0
- package/dist/core/output-contract.js +16 -0
- package/dist/core/watch-plugin.d.ts +13 -0
- package/dist/core/watch-plugin.js +87 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/run-cli.js +82 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -232,12 +232,18 @@ codex-plugin-doctor doctor clients
|
|
|
232
232
|
codex-plugin-doctor doctor --update-check
|
|
233
233
|
codex-plugin-doctor audit --installed
|
|
234
234
|
codex-plugin-doctor audit --installed --security --compat
|
|
235
|
-
codex-plugin-doctor audit --installed --security --compat --json --output local-audit.json
|
|
236
|
-
codex-plugin-doctor audit --installed --security --compat --cache
|
|
237
|
-
codex-plugin-doctor audit --installed --changed --cache
|
|
238
|
-
codex-plugin-doctor
|
|
239
|
-
codex-plugin-doctor
|
|
240
|
-
codex-plugin-doctor
|
|
235
|
+
codex-plugin-doctor audit --installed --security --compat --json --output local-audit.json
|
|
236
|
+
codex-plugin-doctor audit --installed --security --compat --cache
|
|
237
|
+
codex-plugin-doctor audit --installed --changed --cache
|
|
238
|
+
codex-plugin-doctor audit deps .
|
|
239
|
+
codex-plugin-doctor audit deps . --json --output dependency-audit.json
|
|
240
|
+
codex-plugin-doctor watch .
|
|
241
|
+
codex-plugin-doctor watch . --json --output watch-latest.json --debounce-ms 500
|
|
242
|
+
codex-plugin-doctor init-git-hooks .
|
|
243
|
+
codex-plugin-doctor init-git-hooks . --json
|
|
244
|
+
codex-plugin-doctor mcp .
|
|
245
|
+
codex-plugin-doctor mcp . --json
|
|
246
|
+
codex-plugin-doctor mcp . --json --output mcp-doctor.json
|
|
241
247
|
codex-plugin-doctor init my-plugin
|
|
242
248
|
codex-plugin-doctor init my-mcp --template mcp-stdio
|
|
243
249
|
codex-plugin-doctor init remote-mcp --template mcp-http
|
|
@@ -294,9 +300,15 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
|
|
|
294
300
|
|
|
295
301
|
`doctor` checks the local environment, including package version, platform, Node version, npm global prefix, Codex home, and Codex plugin cache visibility. The text output also includes recommended next commands for self-test, installed plugin discovery, runtime checks, compatibility scoring, and CI setup. `doctor contract` publishes the machine-readable output contract, including public JSON schema surfaces, stable-through-1.0 compatibility metadata, and a frozen rule catalog digest. Add `--json` for automation or `--output output-contract.json` to write the contract to disk. `doctor corpus` runs the bundled validation corpus against healthy runtime, risky security, starter skill, and generic MCP packages, then reports whether each case matched its expected outcome. Add `--json` for automation or `--output validation-corpus.json` to write the corpus report to disk. `doctor npm <package>` runs a preinstall scan by packing the npm package with scripts disabled, extracting the publish tarball, and running validation, security, trust, and recommendation checks against the shipped contents. Use a published Codex plugin package as the target; scanning `codex-plugin-doctor` itself intentionally reports a missing plugin manifest because this CLI package is not a plugin package. Add `--json` for automation or `--output npm-preinstall.json` to write the report to disk. `doctor attest <path>` creates a local attestation with stable package/report digests, validation/security/compatibility/trust summary, and verification metadata. Add `--sign-key-env NAME` to attach a local HMAC-SHA256 signature without printing the secret, or `--json --output attestation.json` to write the artifact to disk. `doctor attest verify <attestation.json> --target <path> --sign-key-env NAME` recomputes the package fingerprint, report digest, and HMAC signature offline; verification intentionally treats `generatedAt`, `targetPath`, `verification`, and `signature.keyHint` as unsigned display metadata. `doctor runtime-plan <path>` creates a non-executing runtime plan that lists MCP server commands, safe probe methods, risk reasons, and a stable approval digest before any local server is started. Add `--markdown --output runtime-plan.md` to preserve a review-ready approval artifact with the execution boundary, checklist, servers, probes, and risk reasons. `doctor runtime-policy <path>` evaluates the same runtime plan and security signals, then recommends `allow`, `review`, `sandbox_recommended`, or `deny` before local MCP execution starts. `doctor review-bundle <path> --output <dir> --sign-key-env NAME` writes a signed review directory with runtime plan, runtime policy, attestation, release evidence, manifest, Markdown summary files, and SHA-256 file integrity digests. `doctor review-bundle verify <bundle-dir> --target <path> --sign-key-env NAME` verifies the bundle manifest, expected files, manifest integrity digests, runtime artifacts, signed attestation, and signed release evidence offline before a reviewer trusts the handoff. `doctor review-bundle diff --before <dir> --after <dir>` compares two review bundles and flags risk-increasing changes in status, runtime policy, release readiness, signatures, release evidence, and runtime plan digest. `check --runtime --require-runtime-approval --runtime-approval-digest <digest>` refuses to run runtime probes unless the current plan digest matches the approved digest. `doctor release-evidence <path> --sign-key-env NAME` creates one redacted release bundle with signed attestation, offline verification, corpus, performance, security, trust, package metadata, git release gates, and runtime approval status. Strict release evidence requires a clean tagged worktree; use `--allow-dirty` or `--allow-untagged` only for local rehearsal. `doctor release-evidence verify <evidence.json> --target <path> --sign-key-env NAME` verifies a shared release evidence artifact offline against an explicit package path; the artifact target path is treated as display metadata, not trusted input. `doctor release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME` writes a signed release evidence file and prints the `gh release upload` command; add `--upload` to run the upload through GitHub CLI with `--clobber`. `doctor inspector <path>` builds a safe MCP Inspector launch command from a packaged `.mcp.json` file without starting the Inspector proxy automatically. Use `--server <name>` when the package contains multiple MCP server entries. `doctor diff --before <path> --after <path>` compares two package roots and reports new findings, resolved findings, trust score delta, and whether risk increased. `doctor recommend <path>` turns validation, security, and compatibility signals into a prioritized action plan with blocker, high, medium, and info actions. Add `--json` for automation or `--output recommendations.json` to write the report to disk. `doctor trust <path>` creates a local trust score from package lifecycle scripts, dependency specs, and MCP security findings. Use it before release when you want supply-chain risks summarized as one score. `doctor perf <path>` profiles the shared package analysis pipeline and reports per-stage durations for validation, config, security, compatibility, trust, recommendations, and total runtime. Add `--max-total-ms <ms>` or repeatable `--max-stage-ms stage=ms` to fail CI when a budget is exceeded. `doctor mcp <path>` exposes the generic MCP static health report under the doctor command family without starting local MCP servers. `doctor export --bundle <path>` creates a redacted operator handoff bundle that includes validation JSON, security scorecard data, compatibility matrix, recommendations, and trust score in one file. `doctor snapshot` creates a redacted diagnostics bundle with environment health, client config readiness, installed plugin metadata, and next commands. Add `--json` for machine-readable output or `--output doctor-snapshot.json` to write the bundle to disk. `doctor clients` reports local Codex, Claude Desktop, Cursor, Cline, and Windsurf config readiness. `doctor --update-check` compares the installed CLI version with the latest npm version and prints the upgrade command when a newer release is available.
|
|
296
302
|
|
|
297
|
-
`audit --installed` runs a local ecosystem audit against every discovered Codex plugin in the installed plugin cache. Add `--security` to include security scorecards, `--compat` to include the all-client compatibility matrix, and `--json --output local-audit.json` when you want a shareable machine-readable report. Add `--cache` to reuse unchanged plugin results between runs; add `--changed` to only report plugins whose fingerprint changed since the last cached audit. Use `--cache-file path/to/audit-cache.json` when CI or scripted runs need an explicit cache location.
|
|
298
|
-
|
|
299
|
-
|
|
303
|
+
`audit --installed` runs a local ecosystem audit against every discovered Codex plugin in the installed plugin cache. Add `--security` to include security scorecards, `--compat` to include the all-client compatibility matrix, and `--json --output local-audit.json` when you want a shareable machine-readable report. Add `--cache` to reuse unchanged plugin results between runs; add `--changed` to only report plugins whose fingerprint changed since the last cached audit. Use `--cache-file path/to/audit-cache.json` when CI or scripted runs need an explicit cache location.
|
|
304
|
+
|
|
305
|
+
`audit deps <path>` runs `npm audit --json` for the target package and normalizes dependency vulnerability results into a stable `schemaVersion: "1.0.0"` report. Use `--json --output dependency-audit.json` when CI needs machine-readable status, vulnerability counts, and the raw npm audit payload.
|
|
306
|
+
|
|
307
|
+
`watch <path>` continuously validates a plugin package after file changes. Use `--debounce-ms <ms>` to tune noisy repositories, `--runtime` when runtime probes are required, and `--json --output watch-latest.json` when another process should consume the latest validation snapshot.
|
|
308
|
+
|
|
309
|
+
`init-git-hooks [path]` installs generated pre-commit and pre-push hooks that run Codex Plugin Doctor validation before local commits and pushes. Add `--force` to overwrite existing hooks and `--json` when setup automation needs the written hook paths.
|
|
310
|
+
|
|
311
|
+
`--policy codex-publish|mcp-strict|security` applies opinionated gates without requiring a local `.codex-doctor.json`. `codex-publish` fails warnings and enables runtime probes for release checks, `mcp-strict` does the same for MCP-heavy packages, and `security` fails warning-level security findings so advisory risks can block a local audit or CI gate.
|
|
300
312
|
|
|
301
313
|
`mcp <path>` diagnoses generic MCP packages that may not have a Codex plugin manifest. It looks for `.mcp.json` or a manifest `mcpServers` reference, validates the top-level `mcpServers` object and server transports, adds MCP command-surface security findings, and includes the all-client compatibility matrix in the same report.
|
|
302
314
|
|
|
@@ -358,9 +370,9 @@ jobs:
|
|
|
358
370
|
runs-on: ubuntu-latest
|
|
359
371
|
steps:
|
|
360
372
|
- uses: actions/checkout@v5
|
|
361
|
-
- uses: Esquetta/CodexPluginDoctor@v1.
|
|
373
|
+
- uses: Esquetta/CodexPluginDoctor@v1.18.0
|
|
362
374
|
with:
|
|
363
|
-
version: "1.
|
|
375
|
+
version: "1.18.0"
|
|
364
376
|
path: .
|
|
365
377
|
runtime: "true"
|
|
366
378
|
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,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
|
+
}
|
|
@@ -34,11 +34,27 @@ const publicSchemaDefinitions = [
|
|
|
34
34
|
command: "codex-plugin-doctor audit --installed --json",
|
|
35
35
|
required: ["schemaVersion", "generatedAt", "summary", "items"]
|
|
36
36
|
},
|
|
37
|
+
{
|
|
38
|
+
id: "doctor.audit.deps.json",
|
|
39
|
+
command: "codex-plugin-doctor audit deps <path> --json",
|
|
40
|
+
required: ["schemaVersion", "targetPath", "status", "totalVulnerabilities", "vulnerabilities", "audit"]
|
|
41
|
+
},
|
|
37
42
|
{
|
|
38
43
|
id: "doctor.fix.plan.json",
|
|
39
44
|
command: "codex-plugin-doctor fix <path> --dry-run --json",
|
|
40
45
|
required: ["schemaVersion", "targetPath", "mode", "actions"]
|
|
41
46
|
},
|
|
47
|
+
{
|
|
48
|
+
id: "doctor.watch.validation.json",
|
|
49
|
+
command: "codex-plugin-doctor watch <path> --json",
|
|
50
|
+
required: ["schemaVersion", "iteration", "timestamp", "targetPath", "status", "findingsCount", "findings"]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "doctor.git.hooks.json",
|
|
54
|
+
command: "codex-plugin-doctor init-git-hooks <path> --json",
|
|
55
|
+
outputKind: "doctor.git.hooks",
|
|
56
|
+
required: ["schemaVersion", "kind", "rootPath", "hookPaths", "preExisting"]
|
|
57
|
+
},
|
|
42
58
|
{
|
|
43
59
|
id: "doctor.history.json",
|
|
44
60
|
command: "codex-plugin-doctor history <history.jsonl> --json",
|
|
@@ -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] [--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");
|
|
77
80
|
}
|
|
78
81
|
const performanceStageNames = new Set([
|
|
79
82
|
"validation",
|
|
@@ -1219,6 +1222,61 @@ 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 jsonOutput = initFlags.includes("--json");
|
|
1232
|
+
const result = await initGitHooks(targetPath, { force });
|
|
1233
|
+
if (jsonOutput) {
|
|
1234
|
+
io.writeStdout(JSON.stringify({
|
|
1235
|
+
schemaVersion: "1.0.0",
|
|
1236
|
+
kind: "doctor.git.hooks",
|
|
1237
|
+
rootPath: result.rootPath,
|
|
1238
|
+
hookPaths: result.hookPaths,
|
|
1239
|
+
preExisting: result.preExisting
|
|
1240
|
+
}, null, 2));
|
|
1241
|
+
return 0;
|
|
1242
|
+
}
|
|
1243
|
+
const overwritten = result.preExisting.length > 0
|
|
1244
|
+
? `\nOverwritten existing hooks: ${result.preExisting.join(", ")}`
|
|
1245
|
+
: "";
|
|
1246
|
+
io.writeStdout([
|
|
1247
|
+
"Initialized Codex Plugin Doctor git hooks",
|
|
1248
|
+
`Root: ${result.rootPath}`,
|
|
1249
|
+
`Hooks: ${result.hookPaths.join(", ")}`,
|
|
1250
|
+
overwritten
|
|
1251
|
+
].filter(Boolean).join("\n"));
|
|
1252
|
+
return 0;
|
|
1253
|
+
}
|
|
1254
|
+
if (command === "watch") {
|
|
1255
|
+
const targetPath = maybePath && !maybePath.startsWith("--") ? maybePath : ".";
|
|
1256
|
+
const watchFlags = maybePath && maybePath.startsWith("--")
|
|
1257
|
+
? [maybePath, ...remainingArgs]
|
|
1258
|
+
: remainingArgs;
|
|
1259
|
+
const runtime = watchFlags.includes("--runtime");
|
|
1260
|
+
const jsonOutput = watchFlags.includes("--json");
|
|
1261
|
+
const outputIndex = watchFlags.indexOf("--output");
|
|
1262
|
+
const outputPath = outputIndex === -1 ? null : watchFlags[outputIndex + 1];
|
|
1263
|
+
const debounceIndex = watchFlags.indexOf("--debounce-ms");
|
|
1264
|
+
const debounceRaw = debounceIndex === -1 ? null : watchFlags[debounceIndex + 1];
|
|
1265
|
+
const debounceMs = debounceRaw ? Number(debounceRaw) || 300 : 300;
|
|
1266
|
+
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
1267
|
+
io.writeStderr("Missing path after --output.");
|
|
1268
|
+
return 2;
|
|
1269
|
+
}
|
|
1270
|
+
const result = await watchPlugin({
|
|
1271
|
+
targetPath,
|
|
1272
|
+
debounceMs,
|
|
1273
|
+
runtime,
|
|
1274
|
+
jsonOutput,
|
|
1275
|
+
outputPath
|
|
1276
|
+
});
|
|
1277
|
+
io.writeStdout(`\nStopped watching ${result.targetPath}: ${result.validations} validations, ${result.failures} failures.`);
|
|
1278
|
+
return result.failures > 0 ? 1 : 0;
|
|
1279
|
+
}
|
|
1222
1280
|
if (command === "fix") {
|
|
1223
1281
|
if (!maybePath || maybePath.startsWith("--")) {
|
|
1224
1282
|
io.writeStderr("Missing target path. Usage: codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)");
|
|
@@ -1324,10 +1382,32 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1324
1382
|
return report.exitCode;
|
|
1325
1383
|
}
|
|
1326
1384
|
if (command === "audit") {
|
|
1385
|
+
if (maybePath === "deps") {
|
|
1386
|
+
const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--") ? remainingArgs[0] : ".";
|
|
1387
|
+
const depsFlags = remainingArgs[0] && remainingArgs[0].startsWith("--")
|
|
1388
|
+
? remainingArgs
|
|
1389
|
+
: remainingArgs.slice(1);
|
|
1390
|
+
const jsonOutput = depsFlags.includes("--json");
|
|
1391
|
+
const outputIndex = depsFlags.indexOf("--output");
|
|
1392
|
+
const outputPath = outputIndex === -1 ? null : depsFlags[outputIndex + 1];
|
|
1393
|
+
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
1394
|
+
io.writeStderr("Missing path after --output.");
|
|
1395
|
+
return 2;
|
|
1396
|
+
}
|
|
1397
|
+
const report = await buildDepAudit(targetPath);
|
|
1398
|
+
const renderedReport = jsonOutput
|
|
1399
|
+
? renderDepAuditJson(report)
|
|
1400
|
+
: renderDepAudit(report);
|
|
1401
|
+
if (outputPath) {
|
|
1402
|
+
await writeFile(outputPath, renderedReport, "utf8");
|
|
1403
|
+
}
|
|
1404
|
+
io.writeStdout(renderedReport);
|
|
1405
|
+
return report.status === "fail" ? 1 : 0;
|
|
1406
|
+
}
|
|
1327
1407
|
const auditFlags = maybePath ? [maybePath, ...remainingArgs] : remainingArgs;
|
|
1328
1408
|
const installed = auditFlags.includes("--installed");
|
|
1329
1409
|
if (!installed) {
|
|
1330
|
-
io.writeStderr("Usage: codex-plugin-doctor audit --installed [filter] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]");
|
|
1410
|
+
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
1411
|
return 2;
|
|
1332
1412
|
}
|
|
1333
1413
|
const installedIndex = auditFlags.indexOf("--installed");
|
package/package.json
CHANGED