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 +2 -2
- 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/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 +71 -2
- package/package.json +1 -1
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.
|
|
361
|
+
- uses: Esquetta/CodexPluginDoctor@v1.17.0
|
|
362
362
|
with:
|
|
363
|
-
version: "1.
|
|
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,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