codex-plugin-doctor 0.15.0 → 0.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 +7 -2
- package/dist/core/attestation.d.ts +62 -0
- package/dist/core/attestation.js +238 -0
- package/dist/core/inspector-bridge.d.ts +23 -0
- package/dist/core/inspector-bridge.js +100 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/run-cli.js +57 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,8 +75,9 @@ Output formats:
|
|
|
75
75
|
- Markdown reports
|
|
76
76
|
- Shields-compatible badge JSON and static badge Markdown
|
|
77
77
|
- validation history JSONL and trend summaries
|
|
78
|
+
- deterministic local attestation artifacts
|
|
78
79
|
- `--output` file writing
|
|
79
|
-
- CI summary and artifact generation
|
|
80
|
+
- CI summary and artifact generation
|
|
80
81
|
|
|
81
82
|
## Quick Start
|
|
82
83
|
|
|
@@ -178,6 +179,10 @@ codex-plugin-doctor self-test
|
|
|
178
179
|
codex-plugin-doctor doctor
|
|
179
180
|
codex-plugin-doctor doctor npm codex-plugin-doctor
|
|
180
181
|
codex-plugin-doctor doctor npm codex-plugin-doctor --json --output npm-preinstall.json
|
|
182
|
+
codex-plugin-doctor doctor attest .
|
|
183
|
+
codex-plugin-doctor doctor attest . --json --output attestation.json
|
|
184
|
+
codex-plugin-doctor doctor inspector .
|
|
185
|
+
codex-plugin-doctor doctor inspector . --server context7 --json --output inspector-command.json
|
|
181
186
|
codex-plugin-doctor doctor diff --before ./old-plugin --after ./new-plugin
|
|
182
187
|
codex-plugin-doctor doctor diff --before ./old-plugin --after ./new-plugin --json --output risk-diff.json
|
|
183
188
|
codex-plugin-doctor doctor recommend .
|
|
@@ -254,7 +259,7 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
|
|
|
254
259
|
|
|
255
260
|
`self-test` runs the bundled runtime-complete sample through static validation, runtime MCP probes, and the compatibility scorecard. It is the fastest post-install check after `npm install -g codex-plugin-doctor`.
|
|
256
261
|
|
|
257
|
-
`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 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. Add `--json` for automation or `--output npm-preinstall.json` to write the report to disk. `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. `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.
|
|
262
|
+
`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 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. Add `--json` for automation or `--output npm-preinstall.json` to write the report to disk. `doctor attest <path>` creates a deterministic local attestation with a package fingerprint, report digest, validation/security/compatibility/trust summary, and unsigned verification metadata. Add `--json` for automation or `--output attestation.json` to write the artifact to disk. `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. `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.
|
|
258
263
|
|
|
259
264
|
`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.
|
|
260
265
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export interface PackageFingerprint {
|
|
2
|
+
algorithm: "sha256";
|
|
3
|
+
digest: string;
|
|
4
|
+
files: {
|
|
5
|
+
total: number;
|
|
6
|
+
bytes: number;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export interface Digest {
|
|
10
|
+
algorithm: "sha256";
|
|
11
|
+
digest: string;
|
|
12
|
+
}
|
|
13
|
+
export interface DoctorAttestation {
|
|
14
|
+
schemaVersion: "1.0.0";
|
|
15
|
+
kind: "doctor.attestation";
|
|
16
|
+
generatedAt: string;
|
|
17
|
+
version: string;
|
|
18
|
+
targetPath: string;
|
|
19
|
+
subject: {
|
|
20
|
+
name: string;
|
|
21
|
+
version: string | null;
|
|
22
|
+
description: string | null;
|
|
23
|
+
};
|
|
24
|
+
packageFingerprint: PackageFingerprint;
|
|
25
|
+
reportDigest: Digest;
|
|
26
|
+
summary: {
|
|
27
|
+
status: "pass" | "warn" | "fail";
|
|
28
|
+
validation: {
|
|
29
|
+
status: "pass" | "warn" | "fail";
|
|
30
|
+
findingCount: number;
|
|
31
|
+
};
|
|
32
|
+
security: {
|
|
33
|
+
status: "pass" | "warn" | "fail";
|
|
34
|
+
score: number;
|
|
35
|
+
findingCount: number;
|
|
36
|
+
};
|
|
37
|
+
compatibility: {
|
|
38
|
+
failedClients: string[];
|
|
39
|
+
};
|
|
40
|
+
trust: {
|
|
41
|
+
status: "pass" | "warn" | "fail";
|
|
42
|
+
score: number;
|
|
43
|
+
findingCount: number;
|
|
44
|
+
};
|
|
45
|
+
recommendations: {
|
|
46
|
+
actionCount: number;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
verification: {
|
|
50
|
+
recomputeCommand: string;
|
|
51
|
+
notes: string[];
|
|
52
|
+
};
|
|
53
|
+
signature: {
|
|
54
|
+
status: "unsigned";
|
|
55
|
+
reason: string;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export declare function buildDoctorAttestation(targetPath: string): Promise<DoctorAttestation>;
|
|
59
|
+
export declare function renderDoctorAttestationJson(attestation: DoctorAttestation): string;
|
|
60
|
+
export declare function renderDoctorAttestation(attestation: DoctorAttestation, options?: {
|
|
61
|
+
outputPath?: string | null;
|
|
62
|
+
}): string;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./package-analysis.js";
|
|
5
|
+
import { discoverPackage } from "./discover-package.js";
|
|
6
|
+
import { readJsonFile } from "./read-json-file.js";
|
|
7
|
+
import { matrixExitCode } from "../compatibility/compatibility-matrix.js";
|
|
8
|
+
import { packageVersion } from "../version.js";
|
|
9
|
+
const excludedDirectoryNames = new Set([
|
|
10
|
+
".git",
|
|
11
|
+
".hg",
|
|
12
|
+
".svn",
|
|
13
|
+
".cache",
|
|
14
|
+
".turbo",
|
|
15
|
+
".codex-doctor",
|
|
16
|
+
"node_modules",
|
|
17
|
+
"coverage"
|
|
18
|
+
]);
|
|
19
|
+
function sha256(value) {
|
|
20
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
21
|
+
}
|
|
22
|
+
function isPlainObject(value) {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
function stableStringify(value) {
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
28
|
+
}
|
|
29
|
+
if (isPlainObject(value)) {
|
|
30
|
+
return `{${Object.keys(value)
|
|
31
|
+
.sort()
|
|
32
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
|
|
33
|
+
.join(",")}}`;
|
|
34
|
+
}
|
|
35
|
+
return JSON.stringify(value);
|
|
36
|
+
}
|
|
37
|
+
async function collectFileEntries(rootPath, currentPath = rootPath) {
|
|
38
|
+
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
39
|
+
const fileEntries = await Promise.all(entries.map(async (entry) => {
|
|
40
|
+
if (entry.isDirectory() && excludedDirectoryNames.has(entry.name)) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
const entryPath = path.join(currentPath, entry.name);
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
return collectFileEntries(rootPath, entryPath);
|
|
46
|
+
}
|
|
47
|
+
if (!entry.isFile()) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
const [details, content] = await Promise.all([
|
|
51
|
+
stat(entryPath),
|
|
52
|
+
readFile(entryPath)
|
|
53
|
+
]);
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
relativePath: path.relative(rootPath, entryPath).split(path.sep).join("/"),
|
|
57
|
+
size: details.size,
|
|
58
|
+
digest: sha256(content)
|
|
59
|
+
}
|
|
60
|
+
];
|
|
61
|
+
}));
|
|
62
|
+
return fileEntries.flat().sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
63
|
+
}
|
|
64
|
+
async function buildPackageFingerprint(rootPath) {
|
|
65
|
+
const files = await collectFileEntries(rootPath);
|
|
66
|
+
const bytes = files.reduce((total, file) => total + file.size, 0);
|
|
67
|
+
const digest = sha256(stableStringify(files));
|
|
68
|
+
return {
|
|
69
|
+
algorithm: "sha256",
|
|
70
|
+
digest,
|
|
71
|
+
files: {
|
|
72
|
+
total: files.length,
|
|
73
|
+
bytes
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async function readPackageSubject(rootPath) {
|
|
78
|
+
try {
|
|
79
|
+
const packageJson = await readJsonFile(path.join(rootPath, "package.json"));
|
|
80
|
+
return isPlainObject(packageJson) ? packageJson : null;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function buildSubject(analysis, packageJson, manifest) {
|
|
87
|
+
const name = typeof manifest?.name === "string"
|
|
88
|
+
? manifest.name
|
|
89
|
+
: typeof packageJson?.name === "string"
|
|
90
|
+
? packageJson.name
|
|
91
|
+
: path.basename(analysis.targetPath);
|
|
92
|
+
const version = typeof manifest?.version === "string"
|
|
93
|
+
? manifest.version
|
|
94
|
+
: typeof packageJson?.version === "string"
|
|
95
|
+
? packageJson.version
|
|
96
|
+
: null;
|
|
97
|
+
const description = typeof manifest?.description === "string"
|
|
98
|
+
? manifest.description
|
|
99
|
+
: typeof packageJson?.description === "string"
|
|
100
|
+
? packageJson.description
|
|
101
|
+
: null;
|
|
102
|
+
return {
|
|
103
|
+
name,
|
|
104
|
+
version,
|
|
105
|
+
description
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async function readManifestSubject(rootPath) {
|
|
109
|
+
const discoveredPackage = await discoverPackage(rootPath);
|
|
110
|
+
if (!discoveredPackage) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
name: discoveredPackage.manifest.name,
|
|
115
|
+
version: discoveredPackage.manifest.version,
|
|
116
|
+
description: discoveredPackage.manifest.description
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function buildReportDigestPayload(analysis, packageFingerprint) {
|
|
120
|
+
return {
|
|
121
|
+
version: packageVersion,
|
|
122
|
+
packageFingerprint,
|
|
123
|
+
validation: {
|
|
124
|
+
status: analysis.validation.status,
|
|
125
|
+
findings: analysis.validation.findings
|
|
126
|
+
},
|
|
127
|
+
security: {
|
|
128
|
+
status: analysis.security.status,
|
|
129
|
+
score: analysis.security.score,
|
|
130
|
+
findings: analysis.security.findings
|
|
131
|
+
},
|
|
132
|
+
compatibility: analysis.compatibility.results.map((result) => ({
|
|
133
|
+
client: result.client,
|
|
134
|
+
status: result.status,
|
|
135
|
+
summary: result.summary
|
|
136
|
+
})),
|
|
137
|
+
trust: {
|
|
138
|
+
status: analysis.trust.status,
|
|
139
|
+
score: analysis.trust.score,
|
|
140
|
+
findings: analysis.trust.findings
|
|
141
|
+
},
|
|
142
|
+
recommendations: buildDoctorRecommendationsFromAnalysis(analysis).actions
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function buildSummary(analysis) {
|
|
146
|
+
const recommendations = buildDoctorRecommendationsFromAnalysis(analysis);
|
|
147
|
+
const failedClients = analysis.compatibility.results
|
|
148
|
+
.filter((result) => result.status === "fail")
|
|
149
|
+
.map((result) => result.client);
|
|
150
|
+
const status = analysis.validation.status === "fail" ||
|
|
151
|
+
analysis.security.status === "fail" ||
|
|
152
|
+
analysis.trust.status === "fail" ||
|
|
153
|
+
matrixExitCode(analysis.compatibility) === 1
|
|
154
|
+
? "fail"
|
|
155
|
+
: analysis.validation.status === "warn" ||
|
|
156
|
+
analysis.security.status === "warn" ||
|
|
157
|
+
analysis.trust.status === "warn"
|
|
158
|
+
? "warn"
|
|
159
|
+
: "pass";
|
|
160
|
+
return {
|
|
161
|
+
status,
|
|
162
|
+
validation: {
|
|
163
|
+
status: analysis.validation.status,
|
|
164
|
+
findingCount: analysis.validation.findings.length
|
|
165
|
+
},
|
|
166
|
+
security: {
|
|
167
|
+
status: analysis.security.status,
|
|
168
|
+
score: analysis.security.score,
|
|
169
|
+
findingCount: analysis.security.findings.length
|
|
170
|
+
},
|
|
171
|
+
compatibility: {
|
|
172
|
+
failedClients
|
|
173
|
+
},
|
|
174
|
+
trust: {
|
|
175
|
+
status: analysis.trust.status,
|
|
176
|
+
score: analysis.trust.score,
|
|
177
|
+
findingCount: analysis.trust.findings.length
|
|
178
|
+
},
|
|
179
|
+
recommendations: {
|
|
180
|
+
actionCount: recommendations.actions.length
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export async function buildDoctorAttestation(targetPath) {
|
|
185
|
+
const analysis = await buildPackageAnalysis(targetPath);
|
|
186
|
+
const [packageFingerprint, packageJson, manifest] = await Promise.all([
|
|
187
|
+
buildPackageFingerprint(analysis.targetPath),
|
|
188
|
+
readPackageSubject(analysis.targetPath),
|
|
189
|
+
readManifestSubject(analysis.targetPath)
|
|
190
|
+
]);
|
|
191
|
+
const reportDigest = sha256(stableStringify(buildReportDigestPayload(analysis, packageFingerprint)));
|
|
192
|
+
return {
|
|
193
|
+
schemaVersion: "1.0.0",
|
|
194
|
+
kind: "doctor.attestation",
|
|
195
|
+
generatedAt: analysis.generatedAt,
|
|
196
|
+
version: packageVersion,
|
|
197
|
+
targetPath: analysis.targetPath,
|
|
198
|
+
subject: buildSubject(analysis, packageJson, manifest),
|
|
199
|
+
packageFingerprint,
|
|
200
|
+
reportDigest: {
|
|
201
|
+
algorithm: "sha256",
|
|
202
|
+
digest: reportDigest
|
|
203
|
+
},
|
|
204
|
+
summary: buildSummary(analysis),
|
|
205
|
+
verification: {
|
|
206
|
+
recomputeCommand: `codex-plugin-doctor doctor attest ${analysis.targetPath} --json`,
|
|
207
|
+
notes: [
|
|
208
|
+
"Compare packageFingerprint.digest to confirm the same local package contents.",
|
|
209
|
+
"Compare reportDigest.digest to confirm the same validation, security, compatibility, trust, and recommendation signals."
|
|
210
|
+
]
|
|
211
|
+
},
|
|
212
|
+
signature: {
|
|
213
|
+
status: "unsigned",
|
|
214
|
+
reason: "v0.17 creates deterministic local attestations without key management or hosted signing."
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
export function renderDoctorAttestationJson(attestation) {
|
|
219
|
+
return JSON.stringify(attestation, null, 2);
|
|
220
|
+
}
|
|
221
|
+
export function renderDoctorAttestation(attestation, options = {}) {
|
|
222
|
+
const lines = [
|
|
223
|
+
"Doctor Attestation",
|
|
224
|
+
"==================",
|
|
225
|
+
`Target: ${attestation.targetPath}`,
|
|
226
|
+
`Subject: ${attestation.subject.name}${attestation.subject.version ? `@${attestation.subject.version}` : ""}`,
|
|
227
|
+
`Status: ${attestation.summary.status.toUpperCase()}`,
|
|
228
|
+
`Package fingerprint: ${attestation.packageFingerprint.digest}`,
|
|
229
|
+
`Report digest: ${attestation.reportDigest.digest}`,
|
|
230
|
+
`Signature: ${attestation.signature.status}`
|
|
231
|
+
];
|
|
232
|
+
if (options.outputPath) {
|
|
233
|
+
lines.push(`Output: ${options.outputPath}`);
|
|
234
|
+
}
|
|
235
|
+
lines.push("", "Verification", "------------");
|
|
236
|
+
lines.push(attestation.verification.recomputeCommand);
|
|
237
|
+
return lines.join("\n");
|
|
238
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface DoctorInspectorReport {
|
|
2
|
+
schemaVersion: "1.0.0";
|
|
3
|
+
generatedAt: string;
|
|
4
|
+
kind: "doctor.inspector";
|
|
5
|
+
targetPath: string;
|
|
6
|
+
status: "pass" | "fail";
|
|
7
|
+
exitCode: 0 | 1;
|
|
8
|
+
mcpConfigPath: string | null;
|
|
9
|
+
serverName: string | null;
|
|
10
|
+
command: {
|
|
11
|
+
executable: string;
|
|
12
|
+
args: string[];
|
|
13
|
+
} | null;
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
export interface BuildDoctorInspectorReportOptions {
|
|
17
|
+
serverName?: string | null;
|
|
18
|
+
}
|
|
19
|
+
export declare function buildDoctorInspectorReport(targetPath: string, options?: BuildDoctorInspectorReportOptions): Promise<DoctorInspectorReport>;
|
|
20
|
+
export declare function renderDoctorInspectorReportJson(report: DoctorInspectorReport): string;
|
|
21
|
+
export declare function renderDoctorInspectorReport(report: DoctorInspectorReport, options?: {
|
|
22
|
+
outputPath?: string | null;
|
|
23
|
+
}): string;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readJsonFile } from "./read-json-file.js";
|
|
3
|
+
import { discoverPackage } from "./discover-package.js";
|
|
4
|
+
function isPlainObject(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function isPathWithinRoot(rootPath, candidatePath) {
|
|
8
|
+
const relativePath = path.relative(rootPath, candidatePath);
|
|
9
|
+
return (relativePath === "" ||
|
|
10
|
+
(!relativePath.startsWith("..") && !path.isAbsolute(relativePath)));
|
|
11
|
+
}
|
|
12
|
+
function buildFailure(targetPath, message, mcpConfigPath = null) {
|
|
13
|
+
return {
|
|
14
|
+
schemaVersion: "1.0.0",
|
|
15
|
+
generatedAt: new Date().toISOString(),
|
|
16
|
+
kind: "doctor.inspector",
|
|
17
|
+
targetPath: path.resolve(targetPath),
|
|
18
|
+
status: "fail",
|
|
19
|
+
exitCode: 1,
|
|
20
|
+
mcpConfigPath,
|
|
21
|
+
serverName: null,
|
|
22
|
+
command: null,
|
|
23
|
+
message
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export async function buildDoctorInspectorReport(targetPath, options = {}) {
|
|
27
|
+
const discoveredPackage = await discoverPackage(targetPath);
|
|
28
|
+
if (!discoveredPackage?.manifest.mcpServers) {
|
|
29
|
+
return buildFailure(targetPath, "The target package does not declare an MCP server config in `.codex-plugin/plugin.json`.");
|
|
30
|
+
}
|
|
31
|
+
const mcpConfigPath = path.resolve(discoveredPackage.rootPath, discoveredPackage.manifest.mcpServers);
|
|
32
|
+
if (!isPathWithinRoot(discoveredPackage.rootPath, mcpConfigPath)) {
|
|
33
|
+
return buildFailure(discoveredPackage.rootPath, "The target package points the MCP server config outside the package root.", mcpConfigPath);
|
|
34
|
+
}
|
|
35
|
+
let parsedConfig;
|
|
36
|
+
try {
|
|
37
|
+
parsedConfig = await readJsonFile(mcpConfigPath);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return buildFailure(discoveredPackage.rootPath, "The MCP server config could not be parsed as JSON.", mcpConfigPath);
|
|
41
|
+
}
|
|
42
|
+
if (!isPlainObject(parsedConfig) || !isPlainObject(parsedConfig.mcpServers)) {
|
|
43
|
+
return buildFailure(discoveredPackage.rootPath, "The MCP server config does not contain a valid `mcpServers` object.", mcpConfigPath);
|
|
44
|
+
}
|
|
45
|
+
const serverNames = Object.keys(parsedConfig.mcpServers).sort();
|
|
46
|
+
const selectedServerName = options.serverName ?? serverNames[0] ?? null;
|
|
47
|
+
if (!selectedServerName || !serverNames.includes(selectedServerName)) {
|
|
48
|
+
return buildFailure(discoveredPackage.rootPath, options.serverName
|
|
49
|
+
? `The MCP server config does not contain server \`${options.serverName}\`.`
|
|
50
|
+
: "The MCP server config does not contain any server entries.", mcpConfigPath);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
schemaVersion: "1.0.0",
|
|
54
|
+
generatedAt: new Date().toISOString(),
|
|
55
|
+
kind: "doctor.inspector",
|
|
56
|
+
targetPath: discoveredPackage.rootPath,
|
|
57
|
+
status: "pass",
|
|
58
|
+
exitCode: 0,
|
|
59
|
+
mcpConfigPath,
|
|
60
|
+
serverName: selectedServerName,
|
|
61
|
+
command: {
|
|
62
|
+
executable: "npx",
|
|
63
|
+
args: [
|
|
64
|
+
"-y",
|
|
65
|
+
"@modelcontextprotocol/inspector",
|
|
66
|
+
"--config",
|
|
67
|
+
mcpConfigPath,
|
|
68
|
+
"--server",
|
|
69
|
+
selectedServerName
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
message: "Run this command to open the MCP Inspector for the selected packaged server."
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function renderDoctorInspectorReportJson(report) {
|
|
76
|
+
return JSON.stringify(report, null, 2);
|
|
77
|
+
}
|
|
78
|
+
export function renderDoctorInspectorReport(report, options = {}) {
|
|
79
|
+
const lines = [
|
|
80
|
+
"Doctor MCP Inspector",
|
|
81
|
+
"====================",
|
|
82
|
+
`Target: ${report.targetPath}`,
|
|
83
|
+
`Status: ${report.status.toUpperCase()}`,
|
|
84
|
+
`Message: ${report.message}`
|
|
85
|
+
];
|
|
86
|
+
if (report.mcpConfigPath) {
|
|
87
|
+
lines.push(`Config: ${report.mcpConfigPath}`);
|
|
88
|
+
}
|
|
89
|
+
if (report.serverName) {
|
|
90
|
+
lines.push(`Server: ${report.serverName}`);
|
|
91
|
+
}
|
|
92
|
+
if (options.outputPath) {
|
|
93
|
+
lines.push(`Output: ${options.outputPath}`);
|
|
94
|
+
}
|
|
95
|
+
if (report.command) {
|
|
96
|
+
lines.push("", "Command", "-------");
|
|
97
|
+
lines.push([report.command.executable, ...report.command.args].join(" "));
|
|
98
|
+
}
|
|
99
|
+
return lines.join("\n");
|
|
100
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,10 +4,12 @@ export { buildTrustScore, renderTrustScore, renderTrustScoreJson, type BuildTrus
|
|
|
4
4
|
export { buildDoctorSnapshot, renderDoctorSnapshot, renderDoctorSnapshotJson, type DoctorSnapshot } from "./core/doctor-snapshot.js";
|
|
5
5
|
export { buildDoctorRecommendations, renderDoctorRecommendations, renderDoctorRecommendationsJson, type DoctorRecommendationAction, type DoctorRecommendationsReport } from "./core/doctor-recommendations.js";
|
|
6
6
|
export { buildDoctorExportBundle, renderDoctorExportBundle, renderDoctorExportBundleJson, type DoctorExportBundle } from "./core/doctor-export-bundle.js";
|
|
7
|
+
export { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestationJson, type DoctorAttestation, type Digest, type PackageFingerprint } from "./core/attestation.js";
|
|
7
8
|
export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis, type PackageAnalysis, type PackageAnalysisOptions, type PackageAnalysisStage, type PackageAnalysisTiming } from "./core/package-analysis.js";
|
|
8
9
|
export { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson, type BuildDoctorPerformanceReportOptions, type DoctorPerformanceReport, type DoctorPerformanceStage, type DoctorPerformanceStageName } from "./core/performance-report.js";
|
|
9
10
|
export { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson, type BuildDoctorNpmPackageReportOptions, type DoctorNpmPackageReport } from "./core/npm-package-doctor.js";
|
|
10
11
|
export { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson, type BuildDoctorRiskDiffReportOptions, type DoctorRiskDiffReport, type RiskDiffFinding, type RiskFindingCategory } from "./core/risk-diff.js";
|
|
12
|
+
export { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorInspectorReportJson, type BuildDoctorInspectorReportOptions, type DoctorInspectorReport } from "./core/inspector-bridge.js";
|
|
11
13
|
export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson, type EcosystemAuditReport } from "./audit/ecosystem-audit.js";
|
|
12
14
|
export { applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames, type PolicyPackName } from "./policy/policy-packs.js";
|
|
13
15
|
export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson, type GenericMcpDoctorReport } from "./mcp/generic-mcp-doctor.js";
|
package/dist/index.js
CHANGED
|
@@ -4,10 +4,12 @@ export { buildTrustScore, renderTrustScore, renderTrustScoreJson } from "./secur
|
|
|
4
4
|
export { buildDoctorSnapshot, renderDoctorSnapshot, renderDoctorSnapshotJson } from "./core/doctor-snapshot.js";
|
|
5
5
|
export { buildDoctorRecommendations, renderDoctorRecommendations, renderDoctorRecommendationsJson } from "./core/doctor-recommendations.js";
|
|
6
6
|
export { buildDoctorExportBundle, renderDoctorExportBundle, renderDoctorExportBundleJson } from "./core/doctor-export-bundle.js";
|
|
7
|
+
export { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestationJson } from "./core/attestation.js";
|
|
7
8
|
export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./core/package-analysis.js";
|
|
8
9
|
export { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson } from "./core/performance-report.js";
|
|
9
10
|
export { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson } from "./core/npm-package-doctor.js";
|
|
10
11
|
export { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson } from "./core/risk-diff.js";
|
|
12
|
+
export { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorInspectorReportJson } from "./core/inspector-bridge.js";
|
|
11
13
|
export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson } from "./audit/ecosystem-audit.js";
|
|
12
14
|
export { applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames } from "./policy/policy-packs.js";
|
|
13
15
|
export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson } from "./mcp/generic-mcp-doctor.js";
|
package/dist/run-cli.js
CHANGED
|
@@ -16,9 +16,11 @@ import { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
|
|
|
16
16
|
import { buildDoctorSnapshot, renderDoctorSnapshot, renderDoctorSnapshotJson } from "./core/doctor-snapshot.js";
|
|
17
17
|
import { buildDoctorRecommendations, renderDoctorRecommendations, renderDoctorRecommendationsJson } from "./core/doctor-recommendations.js";
|
|
18
18
|
import { buildDoctorExportBundle, renderDoctorExportBundle, renderDoctorExportBundleJson } from "./core/doctor-export-bundle.js";
|
|
19
|
+
import { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestationJson } from "./core/attestation.js";
|
|
19
20
|
import { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson } from "./core/performance-report.js";
|
|
20
21
|
import { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson } from "./core/npm-package-doctor.js";
|
|
21
22
|
import { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson } from "./core/risk-diff.js";
|
|
23
|
+
import { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorInspectorReportJson } from "./core/inspector-bridge.js";
|
|
22
24
|
import { applyFixPlan, buildFixPlan, renderApplyFixResult, renderFixPlanJsonReport, renderFixPlan } from "./core/fix-plan.js";
|
|
23
25
|
import { renderClientDoctor, renderEnvironmentDoctor, renderEnvironmentDoctorJson } from "./core/environment-doctor.js";
|
|
24
26
|
import { initCiWorkflow } from "./core/init-ci.js";
|
|
@@ -64,7 +66,7 @@ const defaultIo = {
|
|
|
64
66
|
}
|
|
65
67
|
};
|
|
66
68
|
function printUsage(io) {
|
|
67
|
-
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] [--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>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path>|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");
|
|
69
|
+
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] [--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>|attest <path>|inspector <path>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path>|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");
|
|
68
70
|
}
|
|
69
71
|
function renderInstalledPlugins(plugins) {
|
|
70
72
|
const lines = [
|
|
@@ -229,6 +231,30 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
229
231
|
io.writeStdout(renderedReport);
|
|
230
232
|
return report.exitCode;
|
|
231
233
|
}
|
|
234
|
+
if (maybePath === "attest") {
|
|
235
|
+
const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--")
|
|
236
|
+
? remainingArgs[0]
|
|
237
|
+
: ".";
|
|
238
|
+
const attestFlags = remainingArgs[0] && !remainingArgs[0].startsWith("--")
|
|
239
|
+
? remainingArgs.slice(1)
|
|
240
|
+
: remainingArgs;
|
|
241
|
+
const jsonOutput = attestFlags.includes("--json");
|
|
242
|
+
const outputIndex = attestFlags.indexOf("--output");
|
|
243
|
+
const outputPath = outputIndex === -1 ? null : attestFlags[outputIndex + 1];
|
|
244
|
+
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
245
|
+
io.writeStderr("Missing path after --output.");
|
|
246
|
+
return 2;
|
|
247
|
+
}
|
|
248
|
+
const attestation = await buildDoctorAttestation(targetPath);
|
|
249
|
+
const attestationJson = renderDoctorAttestationJson(attestation);
|
|
250
|
+
if (outputPath) {
|
|
251
|
+
await writeFile(outputPath, attestationJson, "utf8");
|
|
252
|
+
}
|
|
253
|
+
io.writeStdout(jsonOutput
|
|
254
|
+
? attestationJson
|
|
255
|
+
: renderDoctorAttestation(attestation, { outputPath }));
|
|
256
|
+
return attestation.summary.status === "fail" ? 1 : 0;
|
|
257
|
+
}
|
|
232
258
|
if (maybePath === "npm") {
|
|
233
259
|
const packageSpec = remainingArgs[0] && !remainingArgs[0].startsWith("--")
|
|
234
260
|
? remainingArgs[0]
|
|
@@ -291,6 +317,36 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
291
317
|
io.writeStdout(renderedReport);
|
|
292
318
|
return report.exitCode;
|
|
293
319
|
}
|
|
320
|
+
if (maybePath === "inspector") {
|
|
321
|
+
const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--")
|
|
322
|
+
? remainingArgs[0]
|
|
323
|
+
: ".";
|
|
324
|
+
const inspectorFlags = remainingArgs[0] && !remainingArgs[0].startsWith("--")
|
|
325
|
+
? remainingArgs.slice(1)
|
|
326
|
+
: remainingArgs;
|
|
327
|
+
const jsonOutput = inspectorFlags.includes("--json");
|
|
328
|
+
const outputIndex = inspectorFlags.indexOf("--output");
|
|
329
|
+
const outputPath = outputIndex === -1 ? null : inspectorFlags[outputIndex + 1];
|
|
330
|
+
const serverIndex = inspectorFlags.indexOf("--server");
|
|
331
|
+
const serverName = serverIndex === -1 ? null : inspectorFlags[serverIndex + 1];
|
|
332
|
+
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
333
|
+
io.writeStderr("Missing path after --output.");
|
|
334
|
+
return 2;
|
|
335
|
+
}
|
|
336
|
+
if (serverIndex !== -1 && (!serverName || serverName.startsWith("--"))) {
|
|
337
|
+
io.writeStderr("Missing server name after --server.");
|
|
338
|
+
return 2;
|
|
339
|
+
}
|
|
340
|
+
const report = await buildDoctorInspectorReport(targetPath, { serverName });
|
|
341
|
+
const renderedReport = jsonOutput
|
|
342
|
+
? renderDoctorInspectorReportJson(report)
|
|
343
|
+
: renderDoctorInspectorReport(report, { outputPath });
|
|
344
|
+
if (outputPath) {
|
|
345
|
+
await writeFile(outputPath, renderedReport, "utf8");
|
|
346
|
+
}
|
|
347
|
+
io.writeStdout(renderedReport);
|
|
348
|
+
return report.exitCode;
|
|
349
|
+
}
|
|
294
350
|
if (maybePath === "trust") {
|
|
295
351
|
const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--")
|
|
296
352
|
? remainingArgs[0]
|
package/package.json
CHANGED