codex-plugin-doctor 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -87,9 +87,10 @@ If you already have Codex installed locally and do not know plugin paths, discov
87
87
 
88
88
  ```bash
89
89
  codex-plugin-doctor list --installed
90
- codex-plugin-doctor check --installed
91
- codex-plugin-doctor check --installed --all-summary
92
- codex-plugin-doctor check --installed github
90
+ codex-plugin-doctor check --installed
91
+ codex-plugin-doctor check --installed --all-summary
92
+ codex-plugin-doctor check --installed --compat --all-summary
93
+ codex-plugin-doctor check --installed github
93
94
  codex-plugin-doctor explain plugin.manifest.missing
94
95
  ```
95
96
 
@@ -161,11 +162,17 @@ x plugin.security.hard_coded_secret
161
162
  Run these from a Codex plugin package root:
162
163
 
163
164
  ```bash
164
- codex-plugin-doctor --version
165
+ codex-plugin-doctor --version
165
166
  codex-plugin-doctor self-test
166
167
  codex-plugin-doctor doctor
168
+ codex-plugin-doctor doctor clients
169
+ codex-plugin-doctor doctor --update-check
167
170
  codex-plugin-doctor init my-plugin
171
+ codex-plugin-doctor init my-mcp --template mcp-stdio
172
+ codex-plugin-doctor init remote-mcp --template mcp-http
173
+ codex-plugin-doctor init runtime-demo --template full-runtime
168
174
  codex-plugin-doctor compat .
175
+ codex-plugin-doctor compat . --all --scorecard
169
176
  codex-plugin-doctor compat . --client codex
170
177
  codex-plugin-doctor compat . --client generic-mcp
171
178
  codex-plugin-doctor compat . --client claude-desktop
@@ -184,6 +191,7 @@ codex-plugin-doctor check . --profile ci
184
191
  codex-plugin-doctor check . --profile strict
185
192
  codex-plugin-doctor check . --profile publish
186
193
  codex-plugin-doctor check . --json
194
+ codex-plugin-doctor check . --explain
187
195
  codex-plugin-doctor check . --json --output report.json
188
196
  codex-plugin-doctor check . --markdown --output report.md
189
197
  codex-plugin-doctor check . --badge-json --output doctor-badge.json
@@ -198,13 +206,16 @@ codex-plugin-doctor history validation-history.jsonl
198
206
  codex-plugin-doctor history validation-history.jsonl --json
199
207
  codex-plugin-doctor history validation-history.jsonl --fail-on-regression
200
208
  codex-plugin-doctor fix . --dry-run
209
+ codex-plugin-doctor fix . --interactive --backup
201
210
  codex-plugin-doctor fix . --apply --backup
202
211
  codex-plugin-doctor check . --json --runtime --verbose-runtime
203
212
  ```
204
213
 
205
214
  `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`.
206
215
 
207
- `doctor` checks the local environment, including package version, platform, Node version, npm global prefix, Codex home, and Codex plugin cache visibility.
216
+ `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 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.
217
+
218
+ `init [path] --template ...` creates targeted starter packages. `skill-only` is the default minimal skill package, `mcp-stdio` adds a local stdio MCP config and mock server, `mcp-http` scaffolds a streamable HTTP MCP config, and `full-runtime` generates a stdio sample that passes the runtime protocol probes.
208
219
 
209
220
  `compat --client claude-desktop` checks whether the MCP package can be added to the local Claude Desktop setup. On Windows it looks for `%APPDATA%\Claude\claude_desktop_config.json`; on macOS it looks for `~/Library/Application Support/Claude/claude_desktop_config.json`. A valid existing config returns `PASS`, a missing Claude Desktop install returns `WARN`, and a malformed local config returns `FAIL` so you do not add new servers into a broken config file. If the package server name already exists in Claude Desktop, the command returns `WARN` with the duplicate server name. Add `--install-preview` to print the JSON snippet that should be merged into `claude_desktop_config.json`; it does not modify files. Use `--apply --backup` only when you want the CLI to create a timestamped backup and merge the server config. Apply mode refuses to overwrite duplicate server names.
210
221
 
@@ -212,15 +223,19 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
212
223
 
213
224
  `compat --client cline` checks whether the MCP package can be added to Cline. It uses `CLINE_DIR/data/settings/cline_mcp_settings.json` when `CLINE_DIR` is set, otherwise `~/.cline/data/settings/cline_mcp_settings.json`. Add `--install-preview` to print the JSON snippet that should be merged into `cline_mcp_settings.json`.
214
225
 
215
- `compat --scorecard` turns the compatibility matrix into a compact score summary. `PASS` maps to `100`, `WARN` maps to `70`, and `FAIL` or `SKIPPED` maps to `0`.
226
+ `compat --all` makes the all-client matrix explicit when you want Codex, Generic MCP, Claude Desktop, Cursor, Cline, and Windsurf in one run. `compat --scorecard` turns the compatibility matrix into a compact score summary. `PASS` maps to `100`, `WARN` maps to `70`, and `FAIL` or `SKIPPED` maps to `0`.
227
+
228
+ `check --installed --compat --all-summary` validates every discovered Codex plugin from the local plugin cache and appends a compact compatibility summary for Codex, Generic MCP, Claude Desktop, Cursor, Cline, and Windsurf. This is the fastest repo-free audit when a user does not know individual plugin paths.
216
229
 
217
230
  `check --profile ci|strict|publish` applies named validation policies. `ci` keeps default behavior, `strict` fails on warnings, and `publish` fails on warnings while enabling runtime probing by default.
218
231
 
232
+ `check --explain` adds inline rule catalog context to text reports, including why a finding matters, a more detailed fix path, and a compact example.
233
+
219
234
  `check --badge-json` emits Shields endpoint-compatible JSON such as `{"schemaVersion":1,"label":"doctor","message":"PASS","color":"brightgreen"}`. `check --badge-markdown` emits a static shields.io Markdown badge for README or release notes. Badge output is intentionally limited to single package checks, not `check --installed`.
220
235
 
221
236
  `check --history <path>` appends a compact JSONL validation snapshot after a single package check. `history <path>` reads the JSONL file and compares the latest run to the previous run, including status, finding-count deltas, and whether the latest run regressed. Add `history --json` for automation output or `history --fail-on-regression` when CI should fail after a worse latest run.
222
237
 
223
- `fix --dry-run` renders safe automatic fix plans without changing files. `fix --apply --backup` applies only supported safe fixes, such as manifest defaults and missing skills directories, after creating backups.
238
+ `fix --dry-run` renders safe automatic fix plans without changing files. `fix --interactive --backup` shows the same numbered plan, then applies everything after `yes` or only selected action numbers such as `1,3`. `fix --apply --backup` applies supported safe fixes, such as manifest defaults and missing skills directories, after creating backups.
224
239
 
225
240
  Optional local policy file:
226
241
 
@@ -235,9 +250,10 @@ Run these when you want Codex Plugin Doctor to find plugins from the local Codex
235
250
 
236
251
  ```bash
237
252
  codex-plugin-doctor list --installed
238
- codex-plugin-doctor check --installed
239
- codex-plugin-doctor check --installed --all-summary
240
- codex-plugin-doctor check --installed github
253
+ codex-plugin-doctor check --installed
254
+ codex-plugin-doctor check --installed --all-summary
255
+ codex-plugin-doctor check --installed --compat --all-summary
256
+ codex-plugin-doctor check --installed github
241
257
  codex-plugin-doctor check --installed github --runtime --no-animations
242
258
  codex-plugin-doctor explain plugin.security.hard_coded_secret
243
259
  ```
@@ -255,9 +271,9 @@ jobs:
255
271
  runs-on: ubuntu-latest
256
272
  steps:
257
273
  - uses: actions/checkout@v4
258
- - uses: Esquetta/CodexPluginDoctor@v0.8.0
274
+ - uses: Esquetta/CodexPluginDoctor@v0.9.0
259
275
  with:
260
- version: "0.8.0"
276
+ version: "0.9.0"
261
277
  path: .
262
278
  runtime: "false"
263
279
  ```
@@ -300,11 +316,12 @@ Recent validation waves covered:
300
316
 
301
317
  Release preparation is reproducible from the repository:
302
318
 
303
- ```bash
304
- npm run prepare-release
305
- ```
306
-
307
- This runs tests, builds the TypeScript output, and performs `npm pack --dry-run`.
319
+ ```bash
320
+ npm run prepare-release
321
+ npm run release-check
322
+ ```
323
+
324
+ `prepare-release` runs tests, builds the TypeScript output, and performs `npm pack --dry-run`. `release-check` adds release preflight checks for a clean git tree, existing npm versions, existing version tags, tests, build, and pack dry-run.
308
325
 
309
326
  Related docs:
310
327
 
@@ -14,6 +14,16 @@ export interface EnvironmentDoctorReport {
14
14
  path: string | null;
15
15
  };
16
16
  }
17
+ export interface ClientDoctorResult {
18
+ client: string;
19
+ status: "pass" | "warn";
20
+ configPath: string | null;
21
+ configExists: boolean;
22
+ directoryWritable: boolean;
23
+ summary: string;
24
+ }
17
25
  export declare function renderEnvironmentDoctor(terminalContext: CliTerminalContext): Promise<string>;
26
+ export declare function buildClientDoctorReport(terminalContext: CliTerminalContext): Promise<ClientDoctorResult[]>;
27
+ export declare function renderClientDoctor(terminalContext: CliTerminalContext): Promise<string>;
18
28
  export declare function buildEnvironmentDoctorReport(terminalContext: CliTerminalContext): Promise<EnvironmentDoctorReport>;
19
29
  export declare function renderEnvironmentDoctorJson(terminalContext: CliTerminalContext): Promise<string>;
@@ -1,3 +1,4 @@
1
+ import { constants } from "node:fs";
1
2
  import { access } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { packageVersion } from "../version.js";
@@ -10,6 +11,15 @@ async function pathExists(targetPath) {
10
11
  return false;
11
12
  }
12
13
  }
14
+ async function pathWritable(targetPath) {
15
+ try {
16
+ await access(targetPath, constants.W_OK);
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
13
23
  function resolveCodexHome(env) {
14
24
  if (env.CODEX_HOME) {
15
25
  return path.resolve(env.CODEX_HOME);
@@ -22,6 +32,89 @@ function resolveCodexHome(env) {
22
32
  }
23
33
  return null;
24
34
  }
35
+ function resolveHomeDirectory(env) {
36
+ return env.USERPROFILE ?? env.HOME ?? null;
37
+ }
38
+ function resolveClaudeConfigPath(terminalContext) {
39
+ const platform = terminalContext.platform ?? process.platform;
40
+ const homeDirectory = resolveHomeDirectory(terminalContext.env);
41
+ if (platform === "win32") {
42
+ return terminalContext.env.APPDATA
43
+ ? path.join(terminalContext.env.APPDATA, "Claude", "claude_desktop_config.json")
44
+ : null;
45
+ }
46
+ if (platform === "darwin" && homeDirectory) {
47
+ return path.join(homeDirectory, "Library", "Application Support", "Claude", "claude_desktop_config.json");
48
+ }
49
+ return null;
50
+ }
51
+ function resolveClineConfigPath(env) {
52
+ const homeDirectory = resolveHomeDirectory(env);
53
+ const clineDirectory = env.CLINE_DIR
54
+ ? path.resolve(env.CLINE_DIR)
55
+ : homeDirectory
56
+ ? path.join(homeDirectory, ".cline")
57
+ : null;
58
+ return clineDirectory
59
+ ? path.join(clineDirectory, "data", "settings", "cline_mcp_settings.json")
60
+ : null;
61
+ }
62
+ function resolveClientConfigPaths(terminalContext) {
63
+ const env = terminalContext.env;
64
+ const homeDirectory = resolveHomeDirectory(env);
65
+ return [
66
+ {
67
+ client: "Codex",
68
+ configPath: resolveCodexHome(env)
69
+ },
70
+ {
71
+ client: "Claude Desktop",
72
+ configPath: resolveClaudeConfigPath(terminalContext)
73
+ },
74
+ {
75
+ client: "Cursor",
76
+ configPath: homeDirectory ? path.join(homeDirectory, ".cursor", "mcp.json") : null
77
+ },
78
+ {
79
+ client: "Cline",
80
+ configPath: resolveClineConfigPath(env)
81
+ },
82
+ {
83
+ client: "Windsurf",
84
+ configPath: homeDirectory
85
+ ? path.join(homeDirectory, ".codeium", "windsurf", "mcp_config.json")
86
+ : null
87
+ }
88
+ ];
89
+ }
90
+ async function inspectClientConfig(client, configPath) {
91
+ if (!configPath) {
92
+ return {
93
+ client,
94
+ status: "warn",
95
+ configPath,
96
+ configExists: false,
97
+ directoryWritable: false,
98
+ summary: "Config path could not be resolved."
99
+ };
100
+ }
101
+ const configExists = await pathExists(configPath);
102
+ const directory = client === "Codex" ? configPath : path.dirname(configPath);
103
+ const directoryExists = await pathExists(directory);
104
+ const directoryWritable = directoryExists ? await pathWritable(directory) : false;
105
+ return {
106
+ client,
107
+ status: configExists || directoryWritable ? "pass" : "warn",
108
+ configPath,
109
+ configExists,
110
+ directoryWritable,
111
+ summary: configExists
112
+ ? "Config path exists."
113
+ : directoryWritable
114
+ ? "Config directory exists and is writable."
115
+ : "Config path was not detected on this machine."
116
+ };
117
+ }
25
118
  export async function renderEnvironmentDoctor(terminalContext) {
26
119
  const report = await buildEnvironmentDoctorReport(terminalContext);
27
120
  return [
@@ -32,9 +125,35 @@ export async function renderEnvironmentDoctor(terminalContext) {
32
125
  `Node: ${report.node}`,
33
126
  `npm global prefix: ${report.npmGlobalPrefix}`,
34
127
  `Codex home: ${report.codexHome.status.toUpperCase()}${report.codexHome.path ? ` (${report.codexHome.path})` : ""}`,
35
- `Codex plugin cache: ${report.codexPluginCache.status.toUpperCase()}${report.codexPluginCache.path ? ` (${report.codexPluginCache.path})` : ""}`
128
+ `Codex plugin cache: ${report.codexPluginCache.status.toUpperCase()}${report.codexPluginCache.path ? ` (${report.codexPluginCache.path})` : ""}`,
129
+ "",
130
+ "Recommended next commands",
131
+ "-------------------------",
132
+ "codex-plugin-doctor self-test",
133
+ "codex-plugin-doctor list --installed",
134
+ "codex-plugin-doctor check . --runtime --explain",
135
+ "codex-plugin-doctor compat . --all --scorecard",
136
+ "codex-plugin-doctor init-ci ."
36
137
  ].join("\n");
37
138
  }
139
+ export async function buildClientDoctorReport(terminalContext) {
140
+ return Promise.all(resolveClientConfigPaths(terminalContext).map((item) => inspectClientConfig(item.client, item.configPath)));
141
+ }
142
+ export async function renderClientDoctor(terminalContext) {
143
+ const results = await buildClientDoctorReport(terminalContext);
144
+ const lines = [
145
+ "Codex Plugin Doctor Clients",
146
+ "===========================",
147
+ ""
148
+ ];
149
+ for (const result of results) {
150
+ lines.push(`${result.client}: ${result.status.toUpperCase()} - ${result.summary}`);
151
+ lines.push(` Config: ${result.configPath ?? "(unknown)"}`);
152
+ lines.push(` Exists: ${result.configExists ? "yes" : "no"}`);
153
+ lines.push(` Writable: ${result.directoryWritable ? "yes" : "no"}`);
154
+ }
155
+ return lines.join("\n");
156
+ }
38
157
  export async function buildEnvironmentDoctorReport(terminalContext) {
39
158
  const codexHome = resolveCodexHome(terminalContext.env);
40
159
  const codexHomeExists = codexHome ? await pathExists(codexHome) : false;
@@ -14,6 +14,9 @@ export interface ApplyFixPlanResult {
14
14
  filesChanged: number;
15
15
  backupDirectory: string;
16
16
  }
17
+ export interface ApplyFixPlanOptions {
18
+ actionIndexes?: number[];
19
+ }
17
20
  export interface FixPlanJsonReport {
18
21
  schemaVersion: "1.0.0";
19
22
  mode: "dry-run" | "apply";
@@ -25,8 +28,8 @@ export interface FixPlanJsonReport {
25
28
  }>;
26
29
  }
27
30
  export declare function buildFixPlan(targetPath: string): Promise<FixPlan>;
28
- export declare function renderFixPlan(plan: FixPlan, mode: "dry-run"): string;
29
- export declare function applyFixPlan(targetPath: string): Promise<ApplyFixPlanResult>;
31
+ export declare function renderFixPlan(plan: FixPlan, mode: "dry-run" | "interactive"): string;
32
+ export declare function applyFixPlan(targetPath: string, options?: ApplyFixPlanOptions): Promise<ApplyFixPlanResult>;
30
33
  export declare function renderApplyFixResult(result: ApplyFixPlanResult): string;
31
34
  export declare function renderFixPlanJsonReport(plan: FixPlan, options: {
32
35
  mode: "dry-run" | "apply";
@@ -167,11 +167,19 @@ async function backupFile(rootPath, backupDirectory, filePath) {
167
167
  await mkdir(path.dirname(backupPath), { recursive: true });
168
168
  await copyFile(filePath, backupPath);
169
169
  }
170
- export async function applyFixPlan(targetPath) {
170
+ export async function applyFixPlan(targetPath, options = {}) {
171
171
  const plan = await buildFixPlan(targetPath);
172
+ const selectedIndexes = new Set(options.actionIndexes ?? []);
173
+ const actions = selectedIndexes.size === 0
174
+ ? plan.actions
175
+ : plan.actions.filter((_, index) => selectedIndexes.has(index + 1));
176
+ const appliedPlan = {
177
+ ...plan,
178
+ actions
179
+ };
172
180
  const backupDirectory = path.join(plan.targetPath, ".codex-doctor-backups", timestampForPath());
173
181
  let filesChanged = 0;
174
- for (const action of plan.actions) {
182
+ for (const action of appliedPlan.actions) {
175
183
  if (action.operation === "update-json" && action.id === "manifest.safe_defaults") {
176
184
  await backupFile(plan.targetPath, backupDirectory, action.targetPath);
177
185
  const manifest = JSON.parse(await readFile(action.targetPath, "utf8"));
@@ -210,7 +218,7 @@ export async function applyFixPlan(targetPath) {
210
218
  }
211
219
  }
212
220
  return {
213
- plan,
221
+ plan: appliedPlan,
214
222
  filesChanged,
215
223
  backupDirectory
216
224
  };
@@ -1,6 +1,15 @@
1
+ export declare const initPluginTemplates: readonly ["skill-only", "mcp-stdio", "mcp-http", "full-runtime"];
2
+ export type InitPluginTemplate = (typeof initPluginTemplates)[number];
3
+ export interface InitPluginOptions {
4
+ template?: InitPluginTemplate;
5
+ }
1
6
  export interface InitPluginResult {
2
7
  rootPath: string;
3
8
  manifestPath: string;
4
9
  skillPath: string;
10
+ template: InitPluginTemplate;
11
+ mcpConfigPath?: string;
12
+ serverPath?: string;
5
13
  }
6
- export declare function initPluginPackage(targetPath: string): Promise<InitPluginResult>;
14
+ export declare function isInitPluginTemplate(value: string): value is InitPluginTemplate;
15
+ export declare function initPluginPackage(targetPath: string, options?: InitPluginOptions): Promise<InitPluginResult>;
@@ -1,39 +1,186 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ export const initPluginTemplates = [
4
+ "skill-only",
5
+ "mcp-stdio",
6
+ "mcp-http",
7
+ "full-runtime"
8
+ ];
3
9
  function toPackageName(inputPath) {
4
10
  return path.basename(path.resolve(inputPath))
5
11
  .toLowerCase()
6
12
  .replace(/[^a-z0-9-]+/g, "-")
7
13
  .replace(/^-+|-+$/g, "") || "codex-plugin";
8
14
  }
9
- export async function initPluginPackage(targetPath) {
15
+ export function isInitPluginTemplate(value) {
16
+ return initPluginTemplates.includes(value);
17
+ }
18
+ function buildSkillMarkdown(template) {
19
+ const description = template === "skill-only"
20
+ ? "Use when verifying that this Codex plugin package loads correctly."
21
+ : "Use when testing this Codex plugin package and its bundled MCP server.";
22
+ return [
23
+ "---",
24
+ "name: hello",
25
+ `description: ${description}`,
26
+ "---",
27
+ "",
28
+ "# Hello",
29
+ "",
30
+ template === "skill-only"
31
+ ? "This starter skill confirms the plugin package structure is valid."
32
+ : "This starter skill confirms the plugin package and MCP server wiring are valid.",
33
+ ""
34
+ ].join("\n");
35
+ }
36
+ function buildStdioMcpConfig(packageName) {
37
+ return `${JSON.stringify({
38
+ mcpServers: {
39
+ [packageName]: {
40
+ command: "node",
41
+ args: ["./mock-server.js"]
42
+ }
43
+ }
44
+ }, null, 2)}\n`;
45
+ }
46
+ function buildHttpMcpConfig(packageName) {
47
+ return `${JSON.stringify({
48
+ mcpServers: {
49
+ [packageName]: {
50
+ url: "http://localhost:8787/mcp"
51
+ }
52
+ }
53
+ }, null, 2)}\n`;
54
+ }
55
+ function buildFullRuntimeServer() {
56
+ return [
57
+ "const readline = require(\"node:readline\");",
58
+ "",
59
+ "const rl = readline.createInterface({",
60
+ " input: process.stdin,",
61
+ " crlfDelay: Infinity",
62
+ "});",
63
+ "",
64
+ "function send(id, payload) {",
65
+ " process.stdout.write(`${JSON.stringify({ jsonrpc: \"2.0\", id, ...payload })}\\n`);",
66
+ "}",
67
+ "",
68
+ "rl.on(\"line\", (line) => {",
69
+ " const message = JSON.parse(line);",
70
+ " const cursor = message.params && message.params.cursor;",
71
+ "",
72
+ " if (message.method === \"initialize\") {",
73
+ " send(message.id, {",
74
+ " result: {",
75
+ " protocolVersion: \"2025-11-25\",",
76
+ " capabilities: { tools: {}, resources: {}, prompts: {} },",
77
+ " serverInfo: { name: \"codex-plugin-template\", version: \"0.1.0\" }",
78
+ " }",
79
+ " });",
80
+ " return;",
81
+ " }",
82
+ "",
83
+ " if (message.method === \"tools/list\") {",
84
+ " send(message.id, {",
85
+ " result: cursor === \"tools-page-2\"",
86
+ " ? { tools: [{ name: \"format_status\", description: \"Return a formatted health status.\", inputSchema: { type: \"object\", properties: {}, required: [] } }] }",
87
+ " : { tools: [{ name: \"ping\", description: \"Return a healthcheck response.\", inputSchema: { type: \"object\", properties: {}, required: [] } }], nextCursor: \"tools-page-2\" }",
88
+ " });",
89
+ " return;",
90
+ " }",
91
+ "",
92
+ " if (message.method === \"tools/call\") {",
93
+ " send(message.id, { result: { content: [{ type: \"text\", text: \"codex-plugin-template-ok\" }] } });",
94
+ " return;",
95
+ " }",
96
+ "",
97
+ " if (message.method === \"resources/list\") {",
98
+ " send(message.id, {",
99
+ " result: cursor === \"resources-page-2\"",
100
+ " ? { resources: [{ name: \"workspace-license\", uri: \"file:///workspace/LICENSE\" }] }",
101
+ " : { resources: [{ name: \"workspace-readme\", uri: \"file:///workspace/README.md\" }], nextCursor: \"resources-page-2\" }",
102
+ " });",
103
+ " return;",
104
+ " }",
105
+ "",
106
+ " if (message.method === \"resources/read\") {",
107
+ " send(message.id, { result: { contents: [{ uri: \"file:///workspace/README.md\", text: \"# Workspace README\" }] } });",
108
+ " return;",
109
+ " }",
110
+ "",
111
+ " if (message.method === \"resources/templates/list\") {",
112
+ " send(message.id, {",
113
+ " result: cursor === \"templates-page-2\"",
114
+ " ? { resourceTemplates: [{ name: \"log\", uriTemplate: \"file:///workspace/logs/{name}.log\" }] }",
115
+ " : { resourceTemplates: [{ name: \"doc\", uriTemplate: \"file:///workspace/docs/{name}.md\" }], nextCursor: \"templates-page-2\" }",
116
+ " });",
117
+ " return;",
118
+ " }",
119
+ "",
120
+ " if (message.method === \"prompts/list\") {",
121
+ " send(message.id, {",
122
+ " result: cursor === \"prompts-page-2\"",
123
+ " ? { prompts: [{ name: \"summary\", description: \"Summarize the current change.\" }] }",
124
+ " : { prompts: [{ name: \"code_review\", description: \"Review code for bugs.\", arguments: [{ name: \"diff\", required: true }] }], nextCursor: \"prompts-page-2\" }",
125
+ " });",
126
+ " return;",
127
+ " }",
128
+ "",
129
+ " if (message.method === \"prompts/get\") {",
130
+ " if (!message.params || !message.params.arguments || message.params.arguments.diff !== \"codex-plugin-doctor-probe\") {",
131
+ " send(message.id, { error: { code: -32602, message: \"Missing required diff argument\" } });",
132
+ " return;",
133
+ " }",
134
+ "",
135
+ " send(message.id, {",
136
+ " result: {",
137
+ " description: \"Prompt for code review\",",
138
+ " messages: [{ role: \"user\", content: { type: \"text\", text: \"Review this diff for bugs.\" } }]",
139
+ " }",
140
+ " });",
141
+ " return;",
142
+ " }",
143
+ "",
144
+ " send(message.id, { error: { code: -32601, message: `Unsupported method: ${message.method}` } });",
145
+ "});",
146
+ ""
147
+ ].join("\n");
148
+ }
149
+ export async function initPluginPackage(targetPath, options = {}) {
150
+ const template = options.template ?? "skill-only";
10
151
  const rootPath = path.resolve(targetPath);
11
152
  const manifestDirectory = path.join(rootPath, ".codex-plugin");
12
153
  const skillsDirectory = path.join(rootPath, "skills", "hello");
13
154
  const manifestPath = path.join(manifestDirectory, "plugin.json");
14
155
  const skillPath = path.join(skillsDirectory, "SKILL.md");
156
+ const mcpConfigPath = path.join(rootPath, ".mcp.json");
157
+ const serverPath = path.join(rootPath, "mock-server.js");
158
+ const packageName = toPackageName(rootPath);
15
159
  await mkdir(manifestDirectory, { recursive: true });
16
160
  await mkdir(skillsDirectory, { recursive: true });
17
161
  await writeFile(manifestPath, `${JSON.stringify({
18
- name: toPackageName(rootPath),
162
+ name: packageName,
19
163
  version: "0.1.0",
20
164
  description: "A Codex plugin package scaffolded by Codex Plugin Doctor.",
21
- skills: "skills"
165
+ skills: "skills",
166
+ ...(template === "skill-only" ? {} : { mcpServers: ".mcp.json" })
22
167
  }, null, 2)}\n`, "utf8");
23
- await writeFile(skillPath, [
24
- "---",
25
- "name: hello",
26
- "description: Use when verifying that this Codex plugin package loads correctly.",
27
- "---",
28
- "",
29
- "# Hello",
30
- "",
31
- "This starter skill confirms the plugin package structure is valid.",
32
- ""
33
- ].join("\n"), "utf8");
168
+ await writeFile(skillPath, buildSkillMarkdown(template), "utf8");
169
+ if (template === "mcp-stdio" || template === "full-runtime") {
170
+ await writeFile(mcpConfigPath, buildStdioMcpConfig(packageName), "utf8");
171
+ await writeFile(serverPath, buildFullRuntimeServer(), "utf8");
172
+ }
173
+ if (template === "mcp-http") {
174
+ await writeFile(mcpConfigPath, buildHttpMcpConfig(packageName), "utf8");
175
+ }
34
176
  return {
35
177
  rootPath,
36
178
  manifestPath,
37
- skillPath
179
+ skillPath,
180
+ template,
181
+ mcpConfigPath: template === "skill-only" ? undefined : mcpConfigPath,
182
+ serverPath: template === "mcp-stdio" || template === "full-runtime"
183
+ ? serverPath
184
+ : undefined
38
185
  };
39
186
  }
@@ -1,7 +1,9 @@
1
1
  import type { CheckResult } from "../domain/types.js";
2
2
  import type { InstalledPlugin } from "../core/discover-installed-plugins.js";
3
+ import type { CompatibilityMatrix } from "../compatibility/compatibility-matrix.js";
3
4
  export interface InstalledPluginCheckResult {
4
5
  plugin: InstalledPlugin;
5
6
  result: CheckResult;
7
+ compatibilityMatrix?: CompatibilityMatrix;
6
8
  }
7
9
  export declare function renderInstalledSummary(checkedPlugins: InstalledPluginCheckResult[]): string;
@@ -1,3 +1,36 @@
1
+ function statusLabel(status) {
2
+ return status.toUpperCase();
3
+ }
4
+ function statusCount(matrix, status) {
5
+ return matrix.results.filter((result) => result.status === status).length;
6
+ }
7
+ function renderCompatibilitySummary(checkedPlugins) {
8
+ const pluginsWithCompatibility = checkedPlugins.filter((item) => item.compatibilityMatrix);
9
+ if (pluginsWithCompatibility.length === 0) {
10
+ return [];
11
+ }
12
+ const lines = [
13
+ "",
14
+ "Installed Compatibility Summary",
15
+ "===============================",
16
+ `Checked: ${pluginsWithCompatibility.length}`,
17
+ "",
18
+ "Clients",
19
+ "-------"
20
+ ];
21
+ for (const item of pluginsWithCompatibility) {
22
+ const matrix = item.compatibilityMatrix;
23
+ if (!matrix) {
24
+ continue;
25
+ }
26
+ lines.push("", `${item.plugin.name} - ${item.plugin.relativePath}`);
27
+ lines.push(` Score: ${statusCount(matrix, "pass")} pass, ${statusCount(matrix, "warn")} warn, ${statusCount(matrix, "fail")} fail, ${statusCount(matrix, "skipped")} skipped`);
28
+ for (const result of matrix.results) {
29
+ lines.push(` ${result.client}: ${statusLabel(result.status)} - ${result.summary}`);
30
+ }
31
+ }
32
+ return lines;
33
+ }
1
34
  export function renderInstalledSummary(checkedPlugins) {
2
35
  const passCount = checkedPlugins.filter((item) => item.result.status === "pass").length;
3
36
  const warnCount = checkedPlugins.filter((item) => item.result.status === "warn").length;
@@ -18,5 +51,6 @@ export function renderInstalledSummary(checkedPlugins) {
18
51
  const suffix = findingIds.length > 0 ? ` (${findingIds.join(", ")})` : "";
19
52
  lines.push(`${item.result.status.toUpperCase()} ${item.plugin.name} - ${item.plugin.relativePath}${suffix}`);
20
53
  }
54
+ lines.push(...renderCompatibilitySummary(checkedPlugins));
21
55
  return lines.join("\n");
22
56
  }
@@ -1,4 +1,5 @@
1
1
  import type { CheckResult } from "../domain/types.js";
2
2
  export declare function renderTextReport(result: CheckResult, options?: {
3
3
  ascii?: boolean;
4
+ explain?: boolean;
4
5
  }): string;
@@ -1,3 +1,4 @@
1
+ import { findRuleDefinition } from "../rules/rule-catalog.js";
1
2
  function getCounts(result) {
2
3
  const failCount = result.findings.filter((finding) => finding.severity === "fail").length;
3
4
  const warnCount = result.findings.filter((finding) => finding.severity === "warn").length;
@@ -22,6 +23,7 @@ function getGlyphs(ascii) {
22
23
  }
23
24
  export function renderTextReport(result, options = {}) {
24
25
  const ascii = options.ascii ?? false;
26
+ const explain = options.explain ?? false;
25
27
  const glyphs = getGlyphs(ascii);
26
28
  const { failCount, warnCount, totalCount } = getCounts(result);
27
29
  const lines = [
@@ -58,6 +60,14 @@ export function renderTextReport(result, options = {}) {
58
60
  lines.push(` Message: ${finding.message}`);
59
61
  lines.push(` Impact: ${finding.impact}`);
60
62
  lines.push(` Suggested fix: ${finding.suggestedFix}`);
63
+ if (explain) {
64
+ const rule = findRuleDefinition(finding.id);
65
+ if (rule) {
66
+ lines.push(` Why: ${rule.why}`);
67
+ lines.push(` Fix detail: ${rule.fix}`);
68
+ lines.push(` Example: ${rule.example}`);
69
+ }
70
+ }
61
71
  }
62
72
  };
63
73
  appendSection("Failures", failures, glyphs.fail);
package/dist/run-cli.d.ts CHANGED
@@ -2,6 +2,7 @@ import { runCheck } from "./index.js";
2
2
  export interface CliIo {
3
3
  writeStdout(message: string): void;
4
4
  writeStderr(message: string): void;
5
+ readStdin?(prompt: string): Promise<string>;
5
6
  }
6
7
  export interface CliTerminalContext {
7
8
  stdoutIsTTY: boolean;
@@ -12,5 +13,6 @@ export interface CliTerminalContext {
12
13
  export interface RunCliOptions {
13
14
  terminalContext?: CliTerminalContext;
14
15
  runCheckImpl?: typeof runCheck;
16
+ resolveLatestVersion?: () => Promise<string>;
15
17
  }
16
18
  export declare function runCli(args: string[], io?: CliIo, options?: RunCliOptions): Promise<number>;
package/dist/run-cli.js CHANGED
@@ -1,5 +1,7 @@
1
+ import { execFile } from "node:child_process";
1
2
  import { writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
4
+ import { createInterface } from "node:readline/promises";
3
5
  import { fileURLToPath } from "node:url";
4
6
  import { discoverInstalledPlugins, filterInstalledPlugins } from "./core/discover-installed-plugins.js";
5
7
  import { appendValidationHistoryEntry, readValidationHistory, summarizeValidationHistory } from "./core/validation-history.js";
@@ -11,9 +13,9 @@ import { buildClineInstallPreview, renderClineInstallPreview } from "./compatibi
11
13
  import { buildWindsurfInstallPreview, renderWindsurfInstallPreview } from "./compatibility/windsurf-install-preview.js";
12
14
  import { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
13
15
  import { applyFixPlan, buildFixPlan, renderApplyFixResult, renderFixPlanJsonReport, renderFixPlan } from "./core/fix-plan.js";
14
- import { renderEnvironmentDoctor, renderEnvironmentDoctorJson } from "./core/environment-doctor.js";
16
+ import { renderClientDoctor, renderEnvironmentDoctor, renderEnvironmentDoctorJson } from "./core/environment-doctor.js";
15
17
  import { initCiWorkflow } from "./core/init-ci.js";
16
- import { initPluginPackage } from "./core/init-plugin.js";
18
+ import { initPluginPackage, initPluginTemplates, isInitPluginTemplate } from "./core/init-plugin.js";
17
19
  import { runCheck } from "./index.js";
18
20
  import { renderInstalledSummary } from "./reporting/render-installed-summary.js";
19
21
  import { renderBadgeJson, renderBadgeMarkdown } from "./reporting/render-badge-report.js";
@@ -36,10 +38,22 @@ const defaultIo = {
36
38
  },
37
39
  writeStderr(message) {
38
40
  process.stderr.write(`${message}\n`);
41
+ },
42
+ async readStdin(prompt) {
43
+ const readline = createInterface({
44
+ input: process.stdin,
45
+ output: process.stdout
46
+ });
47
+ try {
48
+ return await readline.question(prompt);
49
+ }
50
+ finally {
51
+ readline.close();
52
+ }
39
53
  }
40
54
  };
41
55
  function printUsage(io) {
42
- io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <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 fix <path> (--dry-run|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor doctor\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");
56
+ io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--verbose-runtime] [--explain] [--no-animations] [--ascii]\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 [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");
43
57
  }
44
58
  function renderInstalledPlugins(plugins) {
45
59
  const lines = [
@@ -113,6 +127,42 @@ function renderSelfTestReport(targetPath, validationStatus, findingsCount, compa
113
127
  renderCompatibilityScorecard(compatibilityMatrix)
114
128
  ].join("\n");
115
129
  }
130
+ async function resolveLatestNpmVersion() {
131
+ return new Promise((resolve, reject) => {
132
+ execFile("npm", ["view", "codex-plugin-doctor", "version"], { shell: process.platform === "win32" }, (error, stdout, stderr) => {
133
+ if (error) {
134
+ reject(new Error(stderr.trim() || error.message));
135
+ return;
136
+ }
137
+ resolve(stdout.trim());
138
+ });
139
+ });
140
+ }
141
+ function renderUpdateCheck(latestVersion) {
142
+ const updateAvailable = latestVersion !== packageVersion;
143
+ return [
144
+ "Codex Plugin Doctor Update Check",
145
+ "================================",
146
+ `Installed: ${packageVersion}`,
147
+ `Latest: ${latestVersion}`,
148
+ `Status: ${updateAvailable ? "UPDATE AVAILABLE" : "UP TO DATE"}`,
149
+ "",
150
+ updateAvailable
151
+ ? "Next: npm install -g codex-plugin-doctor@latest"
152
+ : "Next: no update needed"
153
+ ].join("\n");
154
+ }
155
+ function parseSelectedFixActionIndexes(answer, actionCount) {
156
+ if (!/^\d+(\s*,\s*\d+)*$/.test(answer)) {
157
+ return null;
158
+ }
159
+ const actionIndexes = [...new Set(answer.split(",").map((item) => Number(item.trim())))];
160
+ return actionIndexes.every((index) => Number.isInteger(index) &&
161
+ index >= 1 &&
162
+ index <= actionCount)
163
+ ? actionIndexes
164
+ : null;
165
+ }
116
166
  export async function runCli(args, io = defaultIo, options = {}) {
117
167
  const [command, maybePath, ...remainingArgs] = args;
118
168
  if (command === "--version" || command === "-v" || command === "version") {
@@ -133,6 +183,18 @@ export async function runCli(args, io = defaultIo, options = {}) {
133
183
  return 0;
134
184
  }
135
185
  if (command === "doctor") {
186
+ const doctorFlags = maybePath?.startsWith("--")
187
+ ? [maybePath, ...remainingArgs]
188
+ : remainingArgs;
189
+ if (doctorFlags.includes("--update-check")) {
190
+ const latestVersion = await (options.resolveLatestVersion ?? resolveLatestNpmVersion)();
191
+ io.writeStdout(renderUpdateCheck(latestVersion));
192
+ return 0;
193
+ }
194
+ if (maybePath === "clients") {
195
+ io.writeStdout(await renderClientDoctor(terminalContext));
196
+ return 0;
197
+ }
136
198
  io.writeStdout(maybePath === "--json"
137
199
  ? await renderEnvironmentDoctorJson(terminalContext)
138
200
  : await renderEnvironmentDoctor(terminalContext));
@@ -189,14 +251,37 @@ export async function runCli(args, io = defaultIo, options = {}) {
189
251
  }
190
252
  if (command === "init") {
191
253
  const targetPath = maybePath && !maybePath.startsWith("--") ? maybePath : ".";
192
- const result = await initPluginPackage(targetPath);
193
- io.writeStdout([
254
+ const initFlags = maybePath && maybePath.startsWith("--")
255
+ ? [maybePath, ...remainingArgs]
256
+ : remainingArgs;
257
+ const templateIndex = initFlags.indexOf("--template");
258
+ const templateName = templateIndex === -1 ? "skill-only" : initFlags[templateIndex + 1];
259
+ if (templateIndex !== -1 && (!templateName || templateName.startsWith("--"))) {
260
+ io.writeStderr("Missing template after --template.");
261
+ return 2;
262
+ }
263
+ if (!isInitPluginTemplate(templateName)) {
264
+ io.writeStderr(`Unknown init template: ${templateName}. Supported templates: ${initPluginTemplates.join(", ")}.`);
265
+ return 2;
266
+ }
267
+ const result = await initPluginPackage(targetPath, { template: templateName });
268
+ const lines = [
194
269
  "Initialized Codex plugin package",
270
+ `Template: ${result.template}`,
195
271
  `Root: ${result.rootPath}`,
196
272
  `Manifest: ${result.manifestPath}`,
197
- `Skill: ${result.skillPath}`,
273
+ `Skill: ${result.skillPath}`
274
+ ];
275
+ if (result.mcpConfigPath) {
276
+ lines.push(`MCP config: ${result.mcpConfigPath}`);
277
+ }
278
+ if (result.serverPath) {
279
+ lines.push(`Server: ${result.serverPath}`);
280
+ }
281
+ io.writeStdout([
282
+ ...lines,
198
283
  "",
199
- `Next: codex-plugin-doctor check ${result.rootPath}`
284
+ `Next: codex-plugin-doctor check ${result.rootPath}${result.template === "full-runtime" ? " --runtime" : ""}`
200
285
  ].join("\n"));
201
286
  return 0;
202
287
  }
@@ -212,19 +297,24 @@ export async function runCli(args, io = defaultIo, options = {}) {
212
297
  }
213
298
  if (command === "fix") {
214
299
  if (!maybePath || maybePath.startsWith("--")) {
215
- io.writeStderr("Missing target path. Usage: codex-plugin-doctor fix <path> (--dry-run|--apply --backup)");
300
+ io.writeStderr("Missing target path. Usage: codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)");
216
301
  return 2;
217
302
  }
218
303
  const dryRun = remainingArgs.includes("--dry-run");
219
304
  const apply = remainingArgs.includes("--apply");
305
+ const interactive = remainingArgs.includes("--interactive");
220
306
  const backup = remainingArgs.includes("--backup");
221
307
  const jsonOutput = remainingArgs.includes("--json");
222
- if (apply && !backup) {
223
- io.writeStderr("Fix apply requires --backup.");
308
+ if ((apply || interactive) && !backup) {
309
+ io.writeStderr("Fix mode requires --backup.");
310
+ return 2;
311
+ }
312
+ if ([dryRun, apply, interactive].filter(Boolean).length !== 1) {
313
+ io.writeStderr("Choose exactly one fix mode: --dry-run, --interactive --backup, or --apply --backup.");
224
314
  return 2;
225
315
  }
226
- if (dryRun === apply) {
227
- io.writeStderr("Choose exactly one fix mode: --dry-run or --apply --backup.");
316
+ if (interactive && jsonOutput) {
317
+ io.writeStderr("Interactive fix mode does not support --json.");
228
318
  return 2;
229
319
  }
230
320
  if (dryRun) {
@@ -234,6 +324,24 @@ export async function runCli(args, io = defaultIo, options = {}) {
234
324
  : renderFixPlan(plan, "dry-run"));
235
325
  return 0;
236
326
  }
327
+ if (interactive) {
328
+ const plan = await buildFixPlan(maybePath);
329
+ io.writeStdout([
330
+ renderFixPlan(plan, "interactive"),
331
+ "",
332
+ "Type yes to apply these fixes with a backup, or enter action numbers like 1,3. Anything else cancels."
333
+ ].join("\n"));
334
+ const answer = (await io.readStdin?.("Apply fixes? ") ?? "").trim().toLowerCase();
335
+ const selectedActionIndexes = answer === "yes"
336
+ ? null
337
+ : parseSelectedFixActionIndexes(answer, plan.actions.length);
338
+ if (answer !== "yes" && !selectedActionIndexes) {
339
+ io.writeStdout("Fix cancelled. No files changed.");
340
+ return 0;
341
+ }
342
+ io.writeStdout(renderApplyFixResult(await applyFixPlan(maybePath, selectedActionIndexes ? { actionIndexes: selectedActionIndexes } : {})));
343
+ return 0;
344
+ }
237
345
  const result = await applyFixPlan(maybePath);
238
346
  io.writeStdout(jsonOutput
239
347
  ? renderFixPlanJsonReport(result.plan, {
@@ -254,6 +362,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
254
362
  const installPreview = compatFlags.includes("--install-preview");
255
363
  const applyInstall = compatFlags.includes("--apply");
256
364
  const backupInstall = compatFlags.includes("--backup");
365
+ const allClients = compatFlags.includes("--all");
257
366
  const clientIndex = compatFlags.indexOf("--client");
258
367
  const clientFilter = clientIndex === -1 ? null : compatFlags[clientIndex + 1];
259
368
  const outputIndex = compatFlags.indexOf("--output");
@@ -262,6 +371,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
262
371
  io.writeStderr("Missing client after --client.");
263
372
  return 2;
264
373
  }
374
+ if (allClients && clientFilter) {
375
+ io.writeStderr("Use either --all or --client, not both.");
376
+ return 2;
377
+ }
265
378
  if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
266
379
  io.writeStderr("Missing path after --output.");
267
380
  return 2;
@@ -378,9 +491,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
378
491
  const sarifOutput = normalizedFlags.includes("--sarif");
379
492
  const runtimeProbeEnabled = normalizedFlags.includes("--runtime");
380
493
  const verboseRuntime = normalizedFlags.includes("--verbose-runtime");
494
+ const explainFindings = normalizedFlags.includes("--explain");
381
495
  const noAnimations = normalizedFlags.includes("--no-animations");
382
496
  const asciiMode = normalizedFlags.includes("--ascii");
383
497
  const installedSummary = normalizedFlags.includes("--all-summary");
498
+ const installedCompatibility = normalizedFlags.includes("--compat");
384
499
  const outputIndex = normalizedFlags.indexOf("--output");
385
500
  const outputPath = outputIndex === -1 ? null : normalizedFlags[outputIndex + 1];
386
501
  const configIndex = normalizedFlags.indexOf("--config");
@@ -441,6 +556,12 @@ export async function runCli(args, io = defaultIo, options = {}) {
441
556
  const checkedPlugins = [];
442
557
  for (const plugin of installedPlugins) {
443
558
  const config = await loadDoctorConfig(plugin.rootPath, configPath);
559
+ const compatibilityMatrix = installedCompatibility
560
+ ? await buildCompatibilityMatrix(plugin.rootPath, {
561
+ env: terminalContext.env,
562
+ platform: terminalContext.platform
563
+ })
564
+ : undefined;
444
565
  checkedPlugins.push({
445
566
  plugin,
446
567
  result: applyDoctorConfig(await runCheckImpl(plugin.rootPath, {
@@ -448,7 +569,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
448
569
  runtimeTranscript: effectiveRuntimeProbeEnabled && verboseRuntime
449
570
  ? (line) => io.writeStderr(line)
450
571
  : undefined
451
- }), applyCheckProfile(config, checkProfile))
572
+ }), applyCheckProfile(config, checkProfile)),
573
+ compatibilityMatrix
452
574
  });
453
575
  }
454
576
  const report = installedSummary
@@ -460,13 +582,19 @@ export async function runCli(args, io = defaultIo, options = {}) {
460
582
  ? buildMarkdownReport(item.result, { runtimeProbeEnabled: effectiveRuntimeProbeEnabled })
461
583
  : jsonOutput
462
584
  ? renderJsonReport(item.result, { runtimeProbeEnabled: effectiveRuntimeProbeEnabled })
463
- : renderTextReport(item.result, { ascii: outputPolicy.style === "ascii" }))
585
+ : renderTextReport(item.result, {
586
+ ascii: outputPolicy.style === "ascii",
587
+ explain: explainFindings
588
+ }))
464
589
  .join("\n\n");
465
590
  if (outputPath) {
466
591
  await writeFile(outputPath, report, "utf8");
467
592
  }
468
593
  io.writeStdout(report);
469
- return checkedPlugins.some((item) => item.result.exitCode === 1) ? 1 : 0;
594
+ return checkedPlugins.some((item) => item.result.exitCode === 1 ||
595
+ (item.compatibilityMatrix && matrixExitCode(item.compatibilityMatrix) === 1))
596
+ ? 1
597
+ : 0;
470
598
  }
471
599
  const renderer = outputPolicy.interactive
472
600
  && !verboseRuntime
@@ -497,7 +625,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
497
625
  ? renderBadgeJson(result)
498
626
  : badgeMarkdownOutput
499
627
  ? renderBadgeMarkdown(result)
500
- : renderTextReport(result, { ascii: outputPolicy.style === "ascii" });
628
+ : renderTextReport(result, {
629
+ ascii: outputPolicy.style === "ascii",
630
+ explain: explainFindings
631
+ });
501
632
  if (outputPath) {
502
633
  await writeFile(outputPath, report, "utf8");
503
634
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "CLI-first validator for Codex plugins, skills, and MCP package surfaces with runtime MCP protocol validation.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -26,6 +26,7 @@
26
26
  "generate-validation-artifacts": "node scripts/generate-validation-artifacts.mjs",
27
27
  "prepare-rc": "tsx scripts/prepare-release-candidate.ts",
28
28
  "prepare-release": "npm test && npm run build && npm pack --dry-run",
29
+ "release-check": "node scripts/release-check.mjs",
29
30
  "prepublishOnly": "npm test && npm run build",
30
31
  "test": "vitest run",
31
32
  "test:watch": "vitest"