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 +34 -17
- package/dist/core/environment-doctor.d.ts +10 -0
- package/dist/core/environment-doctor.js +120 -1
- package/dist/core/fix-plan.d.ts +5 -2
- package/dist/core/fix-plan.js +11 -3
- package/dist/core/init-plugin.d.ts +10 -1
- package/dist/core/init-plugin.js +162 -15
- package/dist/reporting/render-installed-summary.d.ts +2 -0
- package/dist/reporting/render-installed-summary.js +34 -0
- package/dist/reporting/render-text-report.d.ts +1 -0
- package/dist/reporting/render-text-report.js +10 -0
- package/dist/run-cli.d.ts +2 -0
- package/dist/run-cli.js +147 -16
- package/package.json +2 -1
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
|
|
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 --
|
|
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
|
|
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.
|
|
274
|
+
- uses: Esquetta/CodexPluginDoctor@v0.9.0
|
|
259
275
|
with:
|
|
260
|
-
version: "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
|
-
|
|
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;
|
package/dist/core/fix-plan.d.ts
CHANGED
|
@@ -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";
|
package/dist/core/fix-plan.js
CHANGED
|
@@ -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
|
|
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
|
|
14
|
+
export declare function isInitPluginTemplate(value: string): value is InitPluginTemplate;
|
|
15
|
+
export declare function initPluginPackage(targetPath: string, options?: InitPluginOptions): Promise<InitPluginResult>;
|
package/dist/core/init-plugin.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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,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
|
|
193
|
-
|
|
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
|
|
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 (
|
|
227
|
-
io.writeStderr("
|
|
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, {
|
|
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
|
|
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, {
|
|
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.
|
|
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"
|