codex-plugin-doctor 0.9.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,10 +162,15 @@ 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 .
169
175
  codex-plugin-doctor compat . --all --scorecard
170
176
  codex-plugin-doctor compat . --client codex
@@ -207,7 +213,9 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
207
213
 
208
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`.
209
215
 
210
- `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.
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.
211
219
 
212
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.
213
221
 
@@ -217,6 +225,8 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
217
225
 
218
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`.
219
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.
229
+
220
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.
221
231
 
222
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.
@@ -225,7 +235,7 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
225
235
 
226
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.
227
237
 
228
- `fix --dry-run` renders safe automatic fix plans without changing files. `fix --interactive --backup` shows the same plan, then applies only after you type `yes`. `fix --apply --backup` applies 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.
229
239
 
230
240
  Optional local policy file:
231
241
 
@@ -240,9 +250,10 @@ Run these when you want Codex Plugin Doctor to find plugins from the local Codex
240
250
 
241
251
  ```bash
242
252
  codex-plugin-doctor list --installed
243
- codex-plugin-doctor check --installed
244
- codex-plugin-doctor check --installed --all-summary
245
- 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
246
257
  codex-plugin-doctor check --installed github --runtime --no-animations
247
258
  codex-plugin-doctor explain plugin.security.hard_coded_secret
248
259
  ```
@@ -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 [
@@ -43,6 +136,24 @@ export async function renderEnvironmentDoctor(terminalContext) {
43
136
  "codex-plugin-doctor init-ci ."
44
137
  ].join("\n");
45
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
+ }
46
157
  export async function buildEnvironmentDoctorReport(terminalContext) {
47
158
  const codexHome = resolveCodexHome(terminalContext.env);
48
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";
@@ -26,7 +29,7 @@ export interface FixPlanJsonReport {
26
29
  }
27
30
  export declare function buildFixPlan(targetPath: string): Promise<FixPlan>;
28
31
  export declare function renderFixPlan(plan: FixPlan, mode: "dry-run" | "interactive"): string;
29
- export declare function applyFixPlan(targetPath: string): Promise<ApplyFixPlanResult>;
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
  }
package/dist/run-cli.d.ts CHANGED
@@ -13,5 +13,6 @@ export interface CliTerminalContext {
13
13
  export interface RunCliOptions {
14
14
  terminalContext?: CliTerminalContext;
15
15
  runCheckImpl?: typeof runCheck;
16
+ resolveLatestVersion?: () => Promise<string>;
16
17
  }
17
18
  export declare function runCli(args: string[], io?: CliIo, options?: RunCliOptions): Promise<number>;
package/dist/run-cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { execFile } from "node:child_process";
1
2
  import { writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { createInterface } from "node:readline/promises";
@@ -12,9 +13,9 @@ import { buildClineInstallPreview, renderClineInstallPreview } from "./compatibi
12
13
  import { buildWindsurfInstallPreview, renderWindsurfInstallPreview } from "./compatibility/windsurf-install-preview.js";
13
14
  import { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
14
15
  import { applyFixPlan, buildFixPlan, renderApplyFixResult, renderFixPlanJsonReport, renderFixPlan } from "./core/fix-plan.js";
15
- import { renderEnvironmentDoctor, renderEnvironmentDoctorJson } from "./core/environment-doctor.js";
16
+ import { renderClientDoctor, renderEnvironmentDoctor, renderEnvironmentDoctorJson } from "./core/environment-doctor.js";
16
17
  import { initCiWorkflow } from "./core/init-ci.js";
17
- import { initPluginPackage } from "./core/init-plugin.js";
18
+ import { initPluginPackage, initPluginTemplates, isInitPluginTemplate } from "./core/init-plugin.js";
18
19
  import { runCheck } from "./index.js";
19
20
  import { renderInstalledSummary } from "./reporting/render-installed-summary.js";
20
21
  import { renderBadgeJson, renderBadgeMarkdown } from "./reporting/render-badge-report.js";
@@ -52,7 +53,7 @@ const defaultIo = {
52
53
  }
53
54
  };
54
55
  function printUsage(io) {
55
- io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--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\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");
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");
56
57
  }
57
58
  function renderInstalledPlugins(plugins) {
58
59
  const lines = [
@@ -126,6 +127,42 @@ function renderSelfTestReport(targetPath, validationStatus, findingsCount, compa
126
127
  renderCompatibilityScorecard(compatibilityMatrix)
127
128
  ].join("\n");
128
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
+ }
129
166
  export async function runCli(args, io = defaultIo, options = {}) {
130
167
  const [command, maybePath, ...remainingArgs] = args;
131
168
  if (command === "--version" || command === "-v" || command === "version") {
@@ -146,6 +183,18 @@ export async function runCli(args, io = defaultIo, options = {}) {
146
183
  return 0;
147
184
  }
148
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
+ }
149
198
  io.writeStdout(maybePath === "--json"
150
199
  ? await renderEnvironmentDoctorJson(terminalContext)
151
200
  : await renderEnvironmentDoctor(terminalContext));
@@ -202,14 +251,37 @@ export async function runCli(args, io = defaultIo, options = {}) {
202
251
  }
203
252
  if (command === "init") {
204
253
  const targetPath = maybePath && !maybePath.startsWith("--") ? maybePath : ".";
205
- const result = await initPluginPackage(targetPath);
206
- 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 = [
207
269
  "Initialized Codex plugin package",
270
+ `Template: ${result.template}`,
208
271
  `Root: ${result.rootPath}`,
209
272
  `Manifest: ${result.manifestPath}`,
210
- `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,
211
283
  "",
212
- `Next: codex-plugin-doctor check ${result.rootPath}`
284
+ `Next: codex-plugin-doctor check ${result.rootPath}${result.template === "full-runtime" ? " --runtime" : ""}`
213
285
  ].join("\n"));
214
286
  return 0;
215
287
  }
@@ -257,14 +329,17 @@ export async function runCli(args, io = defaultIo, options = {}) {
257
329
  io.writeStdout([
258
330
  renderFixPlan(plan, "interactive"),
259
331
  "",
260
- "Type yes to apply these fixes with a backup. Anything else cancels."
332
+ "Type yes to apply these fixes with a backup, or enter action numbers like 1,3. Anything else cancels."
261
333
  ].join("\n"));
262
334
  const answer = (await io.readStdin?.("Apply fixes? ") ?? "").trim().toLowerCase();
263
- if (answer !== "yes") {
335
+ const selectedActionIndexes = answer === "yes"
336
+ ? null
337
+ : parseSelectedFixActionIndexes(answer, plan.actions.length);
338
+ if (answer !== "yes" && !selectedActionIndexes) {
264
339
  io.writeStdout("Fix cancelled. No files changed.");
265
340
  return 0;
266
341
  }
267
- io.writeStdout(renderApplyFixResult(await applyFixPlan(maybePath)));
342
+ io.writeStdout(renderApplyFixResult(await applyFixPlan(maybePath, selectedActionIndexes ? { actionIndexes: selectedActionIndexes } : {})));
268
343
  return 0;
269
344
  }
270
345
  const result = await applyFixPlan(maybePath);
@@ -420,6 +495,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
420
495
  const noAnimations = normalizedFlags.includes("--no-animations");
421
496
  const asciiMode = normalizedFlags.includes("--ascii");
422
497
  const installedSummary = normalizedFlags.includes("--all-summary");
498
+ const installedCompatibility = normalizedFlags.includes("--compat");
423
499
  const outputIndex = normalizedFlags.indexOf("--output");
424
500
  const outputPath = outputIndex === -1 ? null : normalizedFlags[outputIndex + 1];
425
501
  const configIndex = normalizedFlags.indexOf("--config");
@@ -480,6 +556,12 @@ export async function runCli(args, io = defaultIo, options = {}) {
480
556
  const checkedPlugins = [];
481
557
  for (const plugin of installedPlugins) {
482
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;
483
565
  checkedPlugins.push({
484
566
  plugin,
485
567
  result: applyDoctorConfig(await runCheckImpl(plugin.rootPath, {
@@ -487,7 +569,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
487
569
  runtimeTranscript: effectiveRuntimeProbeEnabled && verboseRuntime
488
570
  ? (line) => io.writeStderr(line)
489
571
  : undefined
490
- }), applyCheckProfile(config, checkProfile))
572
+ }), applyCheckProfile(config, checkProfile)),
573
+ compatibilityMatrix
491
574
  });
492
575
  }
493
576
  const report = installedSummary
@@ -508,7 +591,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
508
591
  await writeFile(outputPath, report, "utf8");
509
592
  }
510
593
  io.writeStdout(report);
511
- 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;
512
598
  }
513
599
  const renderer = outputPolicy.interactive
514
600
  && !verboseRuntime
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "0.9.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",