codex-plugin-doctor 0.3.0 → 0.5.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 +316 -296
- package/dist/compatibility/apply-install-preview.d.ts +15 -0
- package/dist/compatibility/apply-install-preview.js +71 -0
- package/dist/compatibility/compatibility-matrix.js +7 -6
- package/dist/core/read-json-file.d.ts +2 -0
- package/dist/core/read-json-file.js +8 -0
- package/dist/reporting/render-badge-report.d.ts +10 -0
- package/dist/reporting/render-badge-report.js +32 -0
- package/dist/run-cli.d.ts +1 -0
- package/dist/run-cli.js +76 -16
- package/examples/README.md +73 -0
- package/examples/codex-doctor-risky/.codex-plugin/plugin.json +7 -0
- package/examples/codex-doctor-risky/.mcp.json +12 -0
- package/examples/codex-doctor-risky/mock-server.js +1 -0
- package/examples/codex-doctor-runtime/.codex-plugin/plugin.json +8 -0
- package/examples/codex-doctor-runtime/.mcp.json +9 -0
- package/examples/codex-doctor-runtime/mock-server.js +237 -0
- package/examples/codex-doctor-runtime/skills/context-check/SKILL.md +7 -0
- package/examples/codex-doctor-starter/.codex-plugin/plugin.json +7 -0
- package/examples/codex-doctor-starter/skills/repo-scan/SKILL.md +7 -0
- package/package.json +3 -2
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { copyFile, mkdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readJsonFile } from "../core/read-json-file.js";
|
|
4
|
+
async function fileExists(targetPath) {
|
|
5
|
+
try {
|
|
6
|
+
const details = await stat(targetPath);
|
|
7
|
+
return details.isFile();
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function buildBackupPath(configPath) {
|
|
17
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
18
|
+
return `${configPath}.${timestamp}.bak`;
|
|
19
|
+
}
|
|
20
|
+
export async function applyInstallPreview(client, preview) {
|
|
21
|
+
await mkdir(path.dirname(preview.configPath), { recursive: true });
|
|
22
|
+
const configExists = await fileExists(preview.configPath);
|
|
23
|
+
const currentConfig = configExists
|
|
24
|
+
? await readJsonFile(preview.configPath)
|
|
25
|
+
: {};
|
|
26
|
+
if (!isRecord(currentConfig)) {
|
|
27
|
+
throw new Error(`${client} MCP config must be a JSON object.`);
|
|
28
|
+
}
|
|
29
|
+
const currentServers = currentConfig.mcpServers;
|
|
30
|
+
if (currentServers !== undefined && !isRecord(currentServers)) {
|
|
31
|
+
throw new Error(`${client} MCP config has an invalid \`mcpServers\` shape.`);
|
|
32
|
+
}
|
|
33
|
+
const existingServers = currentServers ?? {};
|
|
34
|
+
const incomingServers = preview.snippet.mcpServers;
|
|
35
|
+
const duplicateServers = Object.keys(incomingServers).filter((serverName) => Object.prototype.hasOwnProperty.call(existingServers, serverName));
|
|
36
|
+
if (duplicateServers.length > 0) {
|
|
37
|
+
throw new Error(`Refusing to overwrite existing MCP server names: ${duplicateServers.join(", ")}`);
|
|
38
|
+
}
|
|
39
|
+
const backupPath = configExists ? buildBackupPath(preview.configPath) : null;
|
|
40
|
+
if (backupPath) {
|
|
41
|
+
await copyFile(preview.configPath, backupPath);
|
|
42
|
+
}
|
|
43
|
+
const nextConfig = {
|
|
44
|
+
...currentConfig,
|
|
45
|
+
mcpServers: {
|
|
46
|
+
...existingServers,
|
|
47
|
+
...incomingServers
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
await writeFile(preview.configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
|
|
51
|
+
return {
|
|
52
|
+
client,
|
|
53
|
+
configPath: preview.configPath,
|
|
54
|
+
backupPath,
|
|
55
|
+
appliedServers: Object.keys(incomingServers)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function renderApplyInstallResult(result) {
|
|
59
|
+
const lines = [
|
|
60
|
+
`Applied ${result.client} MCP config`,
|
|
61
|
+
"==============================",
|
|
62
|
+
`Config: ${result.configPath}`,
|
|
63
|
+
`Backup: ${result.backupPath ?? "No existing config file was present."}`,
|
|
64
|
+
"",
|
|
65
|
+
"Applied servers:"
|
|
66
|
+
];
|
|
67
|
+
for (const serverName of result.appliedServers) {
|
|
68
|
+
lines.push(`- ${serverName}`);
|
|
69
|
+
}
|
|
70
|
+
return lines.join("\n");
|
|
71
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { validatePlugin } from "../core/validate-plugin.js";
|
|
5
|
+
import { readJsonFile } from "../core/read-json-file.js";
|
|
5
6
|
async function fileExists(targetPath) {
|
|
6
7
|
try {
|
|
7
8
|
const details = await stat(targetPath);
|
|
@@ -40,7 +41,7 @@ export async function readMcpConfigPath(targetPath) {
|
|
|
40
41
|
return null;
|
|
41
42
|
}
|
|
42
43
|
try {
|
|
43
|
-
const manifest =
|
|
44
|
+
const manifest = await readJsonFile(manifestPath);
|
|
44
45
|
return typeof manifest.mcpServers === "string"
|
|
45
46
|
? path.resolve(rootPath, manifest.mcpServers)
|
|
46
47
|
: null;
|
|
@@ -63,7 +64,7 @@ async function checkGenericMcp(targetPath) {
|
|
|
63
64
|
};
|
|
64
65
|
}
|
|
65
66
|
try {
|
|
66
|
-
const parsed =
|
|
67
|
+
const parsed = await readJsonFile(mcpConfigPath);
|
|
67
68
|
const servers = parsed.mcpServers;
|
|
68
69
|
if (typeof servers !== "object" ||
|
|
69
70
|
servers === null ||
|
|
@@ -98,7 +99,7 @@ async function readMcpServerNames(targetPath) {
|
|
|
98
99
|
return [];
|
|
99
100
|
}
|
|
100
101
|
try {
|
|
101
|
-
const parsed =
|
|
102
|
+
const parsed = await readJsonFile(mcpConfigPath);
|
|
102
103
|
const servers = parsed.mcpServers;
|
|
103
104
|
return typeof servers === "object" && servers !== null && !Array.isArray(servers)
|
|
104
105
|
? Object.keys(servers)
|
|
@@ -166,7 +167,7 @@ async function checkClaudeDesktop(targetPath, genericMcpResult, environment = {}
|
|
|
166
167
|
};
|
|
167
168
|
}
|
|
168
169
|
try {
|
|
169
|
-
const parsed =
|
|
170
|
+
const parsed = await readJsonFile(configPath);
|
|
170
171
|
const servers = parsed.mcpServers;
|
|
171
172
|
if (servers !== undefined && (typeof servers !== "object" ||
|
|
172
173
|
servers === null ||
|
|
@@ -238,7 +239,7 @@ async function checkCursor(targetPath, genericMcpResult, environment = {}) {
|
|
|
238
239
|
};
|
|
239
240
|
}
|
|
240
241
|
try {
|
|
241
|
-
const parsed =
|
|
242
|
+
const parsed = await readJsonFile(configPath);
|
|
242
243
|
const servers = parsed.mcpServers;
|
|
243
244
|
if (servers !== undefined && (typeof servers !== "object" ||
|
|
244
245
|
servers === null ||
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
export function parseJsonText(text) {
|
|
3
|
+
const normalizedText = text.startsWith("\uFEFF") ? text.slice(1) : text;
|
|
4
|
+
return JSON.parse(normalizedText);
|
|
5
|
+
}
|
|
6
|
+
export async function readJsonFile(filePath) {
|
|
7
|
+
return parseJsonText(await readFile(filePath, "utf8"));
|
|
8
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CheckResult } from "../domain/types.js";
|
|
2
|
+
export interface BadgeReport {
|
|
3
|
+
schemaVersion: 1;
|
|
4
|
+
label: "doctor";
|
|
5
|
+
message: "PASS" | "WARN" | "FAIL";
|
|
6
|
+
color: "brightgreen" | "yellow" | "red";
|
|
7
|
+
}
|
|
8
|
+
export declare function buildBadgeReport(result: CheckResult): BadgeReport;
|
|
9
|
+
export declare function renderBadgeJson(result: CheckResult): string;
|
|
10
|
+
export declare function renderBadgeMarkdown(result: CheckResult): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
function badgeForStatus(status) {
|
|
2
|
+
if (status === "pass") {
|
|
3
|
+
return {
|
|
4
|
+
message: "PASS",
|
|
5
|
+
color: "brightgreen"
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
if (status === "warn") {
|
|
9
|
+
return {
|
|
10
|
+
message: "WARN",
|
|
11
|
+
color: "yellow"
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
message: "FAIL",
|
|
16
|
+
color: "red"
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function buildBadgeReport(result) {
|
|
20
|
+
return {
|
|
21
|
+
schemaVersion: 1,
|
|
22
|
+
label: "doctor",
|
|
23
|
+
...badgeForStatus(result.status)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function renderBadgeJson(result) {
|
|
27
|
+
return JSON.stringify(buildBadgeReport(result), null, 2);
|
|
28
|
+
}
|
|
29
|
+
export function renderBadgeMarkdown(result) {
|
|
30
|
+
const badge = buildBadgeReport(result);
|
|
31
|
+
return ``;
|
|
32
|
+
}
|
package/dist/run-cli.d.ts
CHANGED
package/dist/run-cli.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
2
4
|
import { discoverInstalledPlugins, filterInstalledPlugins } from "./core/discover-installed-plugins.js";
|
|
3
5
|
import { buildCompatibilityMatrix, matrixExitCode } from "./compatibility/compatibility-matrix.js";
|
|
6
|
+
import { applyInstallPreview, renderApplyInstallResult } from "./compatibility/apply-install-preview.js";
|
|
4
7
|
import { buildClaudeDesktopInstallPreview, renderClaudeDesktopInstallPreview } from "./compatibility/claude-desktop-install-preview.js";
|
|
5
8
|
import { buildCursorInstallPreview, renderCursorInstallPreview } from "./compatibility/cursor-install-preview.js";
|
|
6
9
|
import { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
|
|
7
10
|
import { initPluginPackage } from "./core/init-plugin.js";
|
|
8
11
|
import { runCheck } from "./index.js";
|
|
9
12
|
import { renderInstalledSummary } from "./reporting/render-installed-summary.js";
|
|
13
|
+
import { renderBadgeJson, renderBadgeMarkdown } from "./reporting/render-badge-report.js";
|
|
10
14
|
import { renderCompatibilityScorecard } from "./reporting/render-compatibility-scorecard.js";
|
|
11
15
|
import { renderCompatibilityReport } from "./reporting/render-compatibility-report.js";
|
|
12
16
|
import { renderJsonReport } from "./reporting/render-json-report.js";
|
|
@@ -28,7 +32,7 @@ const defaultIo = {
|
|
|
28
32
|
}
|
|
29
33
|
};
|
|
30
34
|
function printUsage(io) {
|
|
31
|
-
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--json|--markdown] [--output <path>] [--runtime] [--verbose-runtime] [--no-animations] [--ascii]\n codex-plugin-doctor compat <path> [--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview]\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version");
|
|
35
|
+
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--runtime] [--verbose-runtime] [--no-animations] [--ascii]\n codex-plugin-doctor compat <path> [--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version");
|
|
32
36
|
}
|
|
33
37
|
function renderInstalledPlugins(plugins) {
|
|
34
38
|
const lines = [
|
|
@@ -66,6 +70,22 @@ function filterCompatibilityMatrix(matrix, clientFilter) {
|
|
|
66
70
|
results: matrix.results.filter((result) => result.client === client)
|
|
67
71
|
};
|
|
68
72
|
}
|
|
73
|
+
function resolveBundledSelfTestTarget() {
|
|
74
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "examples", "codex-doctor-runtime");
|
|
75
|
+
}
|
|
76
|
+
function renderSelfTestReport(targetPath, validationStatus, findingsCount, compatibilityMatrix) {
|
|
77
|
+
return [
|
|
78
|
+
"Codex Plugin Doctor Self-Test",
|
|
79
|
+
"=============================",
|
|
80
|
+
`Version: ${packageVersion}`,
|
|
81
|
+
`Sample: ${targetPath}`,
|
|
82
|
+
`Validation: ${validationStatus.toUpperCase()}`,
|
|
83
|
+
"Runtime probes: enabled",
|
|
84
|
+
`Findings: ${findingsCount}`,
|
|
85
|
+
"",
|
|
86
|
+
renderCompatibilityScorecard(compatibilityMatrix)
|
|
87
|
+
].join("\n");
|
|
88
|
+
}
|
|
69
89
|
export async function runCli(args, io = defaultIo, options = {}) {
|
|
70
90
|
const [command, maybePath, ...remainingArgs] = args;
|
|
71
91
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
@@ -75,7 +95,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
75
95
|
const terminalContext = options.terminalContext ?? {
|
|
76
96
|
stdoutIsTTY: Boolean(process.stdout.isTTY),
|
|
77
97
|
stderrIsTTY: Boolean(process.stderr.isTTY),
|
|
78
|
-
env: process.env
|
|
98
|
+
env: process.env,
|
|
99
|
+
platform: process.platform
|
|
79
100
|
};
|
|
80
101
|
if (command === "list" && maybePath === "--installed") {
|
|
81
102
|
const installedPlugins = await discoverInstalledPlugins({
|
|
@@ -97,6 +118,17 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
97
118
|
io.writeStdout(renderRuleExplanation(rule));
|
|
98
119
|
return 0;
|
|
99
120
|
}
|
|
121
|
+
if (command === "self-test" || command === "demo") {
|
|
122
|
+
const targetPath = resolveBundledSelfTestTarget();
|
|
123
|
+
const runCheckImpl = options.runCheckImpl ?? runCheck;
|
|
124
|
+
const result = applyDoctorConfig(await runCheckImpl(targetPath, { runtime: true }), await loadDoctorConfig(targetPath));
|
|
125
|
+
const compatibilityMatrix = await buildCompatibilityMatrix(targetPath, {
|
|
126
|
+
env: terminalContext.env,
|
|
127
|
+
platform: terminalContext.platform
|
|
128
|
+
});
|
|
129
|
+
io.writeStdout(renderSelfTestReport(targetPath, result.status, result.findings.length, compatibilityMatrix));
|
|
130
|
+
return result.exitCode === 1 || matrixExitCode(compatibilityMatrix) === 1 ? 1 : 0;
|
|
131
|
+
}
|
|
100
132
|
if (command === "init") {
|
|
101
133
|
const targetPath = maybePath && !maybePath.startsWith("--") ? maybePath : ".";
|
|
102
134
|
const result = await initPluginPackage(targetPath);
|
|
@@ -118,6 +150,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
118
150
|
const jsonOutput = compatFlags.includes("--json");
|
|
119
151
|
const scorecardOutput = compatFlags.includes("--scorecard");
|
|
120
152
|
const installPreview = compatFlags.includes("--install-preview");
|
|
153
|
+
const applyInstall = compatFlags.includes("--apply");
|
|
154
|
+
const backupInstall = compatFlags.includes("--backup");
|
|
121
155
|
const clientIndex = compatFlags.indexOf("--client");
|
|
122
156
|
const clientFilter = clientIndex === -1 ? null : compatFlags[clientIndex + 1];
|
|
123
157
|
const outputIndex = compatFlags.indexOf("--output");
|
|
@@ -130,21 +164,36 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
130
164
|
io.writeStderr("Missing path after --output.");
|
|
131
165
|
return 2;
|
|
132
166
|
}
|
|
133
|
-
if (installPreview &&
|
|
167
|
+
if ((installPreview || applyInstall) &&
|
|
134
168
|
clientFilter?.toLowerCase() !== "claude-desktop" &&
|
|
135
169
|
clientFilter?.toLowerCase() !== "cursor") {
|
|
136
|
-
io.writeStderr("--install-preview
|
|
170
|
+
io.writeStderr("--install-preview and --apply require --client claude-desktop or --client cursor.");
|
|
171
|
+
return 2;
|
|
172
|
+
}
|
|
173
|
+
if (installPreview && applyInstall) {
|
|
174
|
+
io.writeStderr("Use either --install-preview or --apply, not both.");
|
|
137
175
|
return 2;
|
|
138
176
|
}
|
|
139
|
-
if (
|
|
177
|
+
if (applyInstall && !backupInstall) {
|
|
178
|
+
io.writeStderr("--apply requires --backup.");
|
|
179
|
+
return 2;
|
|
180
|
+
}
|
|
181
|
+
if (installPreview || applyInstall) {
|
|
140
182
|
try {
|
|
141
|
-
const
|
|
142
|
-
?
|
|
143
|
-
env: terminalContext.env
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
183
|
+
const preview = clientFilter?.toLowerCase() === "cursor"
|
|
184
|
+
? await buildCursorInstallPreview(targetPath, {
|
|
185
|
+
env: terminalContext.env,
|
|
186
|
+
platform: terminalContext.platform
|
|
187
|
+
})
|
|
188
|
+
: await buildClaudeDesktopInstallPreview(targetPath, {
|
|
189
|
+
env: terminalContext.env,
|
|
190
|
+
platform: terminalContext.platform
|
|
191
|
+
});
|
|
192
|
+
const report = applyInstall
|
|
193
|
+
? renderApplyInstallResult(await applyInstallPreview(clientFilter?.toLowerCase() === "cursor" ? "Cursor" : "Claude Desktop", preview))
|
|
194
|
+
: clientFilter?.toLowerCase() === "cursor"
|
|
195
|
+
? renderCursorInstallPreview(preview)
|
|
196
|
+
: renderClaudeDesktopInstallPreview(preview);
|
|
148
197
|
if (outputPath) {
|
|
149
198
|
await writeFile(outputPath, report, "utf8");
|
|
150
199
|
}
|
|
@@ -158,7 +207,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
158
207
|
}
|
|
159
208
|
}
|
|
160
209
|
let matrix = await buildCompatibilityMatrix(targetPath, {
|
|
161
|
-
env: terminalContext.env
|
|
210
|
+
env: terminalContext.env,
|
|
211
|
+
platform: terminalContext.platform
|
|
162
212
|
});
|
|
163
213
|
if (clientFilter) {
|
|
164
214
|
const filteredMatrix = filterCompatibilityMatrix(matrix, clientFilter);
|
|
@@ -198,6 +248,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
198
248
|
: remainingArgs;
|
|
199
249
|
const jsonOutput = normalizedFlags.includes("--json");
|
|
200
250
|
const markdownOutput = normalizedFlags.includes("--markdown");
|
|
251
|
+
const badgeJsonOutput = normalizedFlags.includes("--badge-json");
|
|
252
|
+
const badgeMarkdownOutput = normalizedFlags.includes("--badge-markdown");
|
|
201
253
|
const sarifOutput = normalizedFlags.includes("--sarif");
|
|
202
254
|
const runtimeProbeEnabled = normalizedFlags.includes("--runtime");
|
|
203
255
|
const verboseRuntime = normalizedFlags.includes("--verbose-runtime");
|
|
@@ -216,9 +268,13 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
216
268
|
io.writeStderr("Missing path after --config.");
|
|
217
269
|
return 2;
|
|
218
270
|
}
|
|
271
|
+
if (checkInstalled && (badgeJsonOutput || badgeMarkdownOutput)) {
|
|
272
|
+
io.writeStderr("Badge output requires a single package target.");
|
|
273
|
+
return 2;
|
|
274
|
+
}
|
|
219
275
|
const outputPolicy = determineOutputPolicy({
|
|
220
|
-
jsonOutput,
|
|
221
|
-
markdownOutput,
|
|
276
|
+
jsonOutput: jsonOutput || badgeJsonOutput,
|
|
277
|
+
markdownOutput: markdownOutput || badgeMarkdownOutput,
|
|
222
278
|
outputPath,
|
|
223
279
|
noAnimations,
|
|
224
280
|
asciiMode,
|
|
@@ -290,7 +346,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
290
346
|
? renderSarifReport(result)
|
|
291
347
|
: jsonOutput
|
|
292
348
|
? renderJsonReport(result, { runtimeProbeEnabled })
|
|
293
|
-
:
|
|
349
|
+
: badgeJsonOutput
|
|
350
|
+
? renderBadgeJson(result)
|
|
351
|
+
: badgeMarkdownOutput
|
|
352
|
+
? renderBadgeMarkdown(result)
|
|
353
|
+
: renderTextReport(result, { ascii: outputPolicy.style === "ascii" });
|
|
294
354
|
if (outputPath) {
|
|
295
355
|
await writeFile(outputPath, report, "utf8");
|
|
296
356
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
This folder contains manual example packs for local testing. Unlike `tests/fixtures`, these examples are meant for humans to run directly against the CLI.
|
|
4
|
+
|
|
5
|
+
## Example Packs
|
|
6
|
+
|
|
7
|
+
### `codex-doctor-starter`
|
|
8
|
+
|
|
9
|
+
Minimal valid Codex plugin package with one skill and no runtime MCP server.
|
|
10
|
+
|
|
11
|
+
Expected result:
|
|
12
|
+
|
|
13
|
+
- static validation passes
|
|
14
|
+
- no runtime probing needed
|
|
15
|
+
|
|
16
|
+
Command:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
codex-plugin-doctor check examples/codex-doctor-starter
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### `codex-doctor-runtime`
|
|
23
|
+
|
|
24
|
+
Valid Codex plugin package with:
|
|
25
|
+
|
|
26
|
+
- skill metadata
|
|
27
|
+
- `.mcp.json`
|
|
28
|
+
- mock MCP stdio server
|
|
29
|
+
- `tools/list`
|
|
30
|
+
- `tools/call`
|
|
31
|
+
- `resources/list`
|
|
32
|
+
- `resources/read`
|
|
33
|
+
- `resources/templates/list`
|
|
34
|
+
- `prompts/list`
|
|
35
|
+
- `prompts/get`
|
|
36
|
+
|
|
37
|
+
Expected result:
|
|
38
|
+
|
|
39
|
+
- static validation passes
|
|
40
|
+
- runtime validation passes
|
|
41
|
+
- runtime scorecard shows all supported runtime capabilities as `pass`
|
|
42
|
+
|
|
43
|
+
Command:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
codex-plugin-doctor check examples/codex-doctor-runtime --json --runtime --verbose-runtime
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### `codex-doctor-risky`
|
|
50
|
+
|
|
51
|
+
Intentionally flawed package for showing failure output.
|
|
52
|
+
|
|
53
|
+
Expected result:
|
|
54
|
+
|
|
55
|
+
- security finding for hard-coded secret
|
|
56
|
+
|
|
57
|
+
Command:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
codex-plugin-doctor check examples/codex-doctor-risky --ascii
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Suggested Local Flow
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install
|
|
67
|
+
npm run build
|
|
68
|
+
npm link
|
|
69
|
+
codex-plugin-doctor check examples/codex-doctor-starter
|
|
70
|
+
codex-plugin-doctor check examples/codex-doctor-runtime --json --runtime --verbose-runtime
|
|
71
|
+
codex-plugin-doctor check examples/codex-doctor-risky --ascii
|
|
72
|
+
```
|
|
73
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
process.stdin.resume();
|