codex-plugin-doctor 0.7.0 → 0.9.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 +16 -10
- package/dist/compatibility/compatibility-matrix.d.ts +1 -0
- package/dist/compatibility/compatibility-matrix.js +78 -1
- package/dist/compatibility/windsurf-install-preview.d.ts +10 -0
- package/dist/compatibility/windsurf-install-preview.js +65 -0
- package/dist/core/environment-doctor.d.ts +17 -0
- package/dist/core/environment-doctor.js +39 -10
- package/dist/core/fix-plan.d.ts +16 -1
- package/dist/core/fix-plan.js +128 -1
- package/dist/core/init-ci.d.ts +5 -0
- package/dist/core/init-ci.js +36 -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 +1 -0
- package/dist/run-cli.js +106 -23
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -166,6 +166,7 @@ codex-plugin-doctor self-test
|
|
|
166
166
|
codex-plugin-doctor doctor
|
|
167
167
|
codex-plugin-doctor init my-plugin
|
|
168
168
|
codex-plugin-doctor compat .
|
|
169
|
+
codex-plugin-doctor compat . --all --scorecard
|
|
169
170
|
codex-plugin-doctor compat . --client codex
|
|
170
171
|
codex-plugin-doctor compat . --client generic-mcp
|
|
171
172
|
codex-plugin-doctor compat . --client claude-desktop
|
|
@@ -184,6 +185,7 @@ codex-plugin-doctor check . --profile ci
|
|
|
184
185
|
codex-plugin-doctor check . --profile strict
|
|
185
186
|
codex-plugin-doctor check . --profile publish
|
|
186
187
|
codex-plugin-doctor check . --json
|
|
188
|
+
codex-plugin-doctor check . --explain
|
|
187
189
|
codex-plugin-doctor check . --json --output report.json
|
|
188
190
|
codex-plugin-doctor check . --markdown --output report.md
|
|
189
191
|
codex-plugin-doctor check . --badge-json --output doctor-badge.json
|
|
@@ -198,13 +200,14 @@ codex-plugin-doctor history validation-history.jsonl
|
|
|
198
200
|
codex-plugin-doctor history validation-history.jsonl --json
|
|
199
201
|
codex-plugin-doctor history validation-history.jsonl --fail-on-regression
|
|
200
202
|
codex-plugin-doctor fix . --dry-run
|
|
203
|
+
codex-plugin-doctor fix . --interactive --backup
|
|
201
204
|
codex-plugin-doctor fix . --apply --backup
|
|
202
205
|
codex-plugin-doctor check . --json --runtime --verbose-runtime
|
|
203
206
|
```
|
|
204
207
|
|
|
205
208
|
`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
209
|
|
|
207
|
-
`doctor` checks the local environment, including package version, platform, Node version, npm global prefix, Codex home, and Codex plugin cache visibility.
|
|
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.
|
|
208
211
|
|
|
209
212
|
`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
213
|
|
|
@@ -212,15 +215,17 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
|
|
|
212
215
|
|
|
213
216
|
`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
217
|
|
|
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`.
|
|
218
|
+
`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`.
|
|
216
219
|
|
|
217
220
|
`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
221
|
|
|
222
|
+
`check --explain` adds inline rule catalog context to text reports, including why a finding matters, a more detailed fix path, and a compact example.
|
|
223
|
+
|
|
219
224
|
`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
225
|
|
|
221
226
|
`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
227
|
|
|
223
|
-
`fix --dry-run` renders safe automatic fix plans without changing files. `fix --
|
|
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.
|
|
224
229
|
|
|
225
230
|
Optional local policy file:
|
|
226
231
|
|
|
@@ -255,9 +260,9 @@ jobs:
|
|
|
255
260
|
runs-on: ubuntu-latest
|
|
256
261
|
steps:
|
|
257
262
|
- uses: actions/checkout@v4
|
|
258
|
-
- uses: Esquetta/CodexPluginDoctor@v0.
|
|
263
|
+
- uses: Esquetta/CodexPluginDoctor@v0.9.0
|
|
259
264
|
with:
|
|
260
|
-
version: "0.
|
|
265
|
+
version: "0.9.0"
|
|
261
266
|
path: .
|
|
262
267
|
runtime: "false"
|
|
263
268
|
```
|
|
@@ -300,11 +305,12 @@ Recent validation waves covered:
|
|
|
300
305
|
|
|
301
306
|
Release preparation is reproducible from the repository:
|
|
302
307
|
|
|
303
|
-
```bash
|
|
304
|
-
npm run prepare-release
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
+
```bash
|
|
309
|
+
npm run prepare-release
|
|
310
|
+
npm run release-check
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
`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
314
|
|
|
309
315
|
Related docs:
|
|
310
316
|
|
|
@@ -18,5 +18,6 @@ export declare function readMcpConfigPath(targetPath: string): Promise<string |
|
|
|
18
18
|
export declare function getClaudeDesktopConfigPath(environment?: CompatibilityEnvironment): string | null;
|
|
19
19
|
export declare function getCursorMcpConfigPath(targetPath: string, environment?: CompatibilityEnvironment): Promise<string>;
|
|
20
20
|
export declare function getClineMcpConfigPath(environment?: CompatibilityEnvironment): string;
|
|
21
|
+
export declare function getWindsurfMcpConfigPath(environment?: CompatibilityEnvironment): string;
|
|
21
22
|
export declare function buildCompatibilityMatrix(targetPath: string, environment?: CompatibilityEnvironment): Promise<CompatibilityMatrix>;
|
|
22
23
|
export declare function matrixExitCode(matrix: CompatibilityMatrix): 0 | 1;
|
|
@@ -141,6 +141,9 @@ export function getClineMcpConfigPath(environment = {}) {
|
|
|
141
141
|
: path.join(getHomeDirectory(environment), ".cline");
|
|
142
142
|
return path.join(clineDirectory, "data", "settings", "cline_mcp_settings.json");
|
|
143
143
|
}
|
|
144
|
+
export function getWindsurfMcpConfigPath(environment = {}) {
|
|
145
|
+
return path.join(getHomeDirectory(environment), ".codeium", "windsurf", "mcp_config.json");
|
|
146
|
+
}
|
|
144
147
|
async function checkClaudeDesktop(targetPath, genericMcpResult, environment = {}) {
|
|
145
148
|
if (genericMcpResult.status !== "pass") {
|
|
146
149
|
return {
|
|
@@ -365,12 +368,85 @@ async function checkCline(targetPath, genericMcpResult, environment = {}) {
|
|
|
365
368
|
};
|
|
366
369
|
}
|
|
367
370
|
}
|
|
371
|
+
async function checkWindsurf(targetPath, genericMcpResult, environment = {}) {
|
|
372
|
+
if (genericMcpResult.status !== "pass") {
|
|
373
|
+
return {
|
|
374
|
+
client: "Windsurf",
|
|
375
|
+
status: "skipped",
|
|
376
|
+
summary: "No valid MCP package config is available for Windsurf.",
|
|
377
|
+
details: ["Add a valid `.mcp.json` with a non-empty `mcpServers` object first."]
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const configPath = getWindsurfMcpConfigPath(environment);
|
|
381
|
+
if (!(await fileExists(configPath))) {
|
|
382
|
+
const configDirectory = path.dirname(configPath);
|
|
383
|
+
return {
|
|
384
|
+
client: "Windsurf",
|
|
385
|
+
status: await directoryExists(configDirectory) ? "pass" : "warn",
|
|
386
|
+
summary: await directoryExists(configDirectory)
|
|
387
|
+
? "Windsurf MCP config directory exists and a config file can be created."
|
|
388
|
+
: "Windsurf was not detected on this machine.",
|
|
389
|
+
details: [
|
|
390
|
+
configPath,
|
|
391
|
+
"Windsurf stores Cascade MCP servers in `mcp_config.json` under `~/.codeium/windsurf`."
|
|
392
|
+
]
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
const parsed = await readJsonFile(configPath);
|
|
397
|
+
const servers = parsed.mcpServers;
|
|
398
|
+
if (servers !== undefined && (typeof servers !== "object" ||
|
|
399
|
+
servers === null ||
|
|
400
|
+
Array.isArray(servers))) {
|
|
401
|
+
return {
|
|
402
|
+
client: "Windsurf",
|
|
403
|
+
status: "fail",
|
|
404
|
+
summary: "Windsurf MCP config has an invalid `mcpServers` shape.",
|
|
405
|
+
details: [configPath, "`mcpServers` must be an object before this package can be added safely."]
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
const packageServerNames = await readMcpServerNames(targetPath);
|
|
409
|
+
const existingServerNames = typeof servers === "object" && servers !== null
|
|
410
|
+
? Object.keys(servers)
|
|
411
|
+
: [];
|
|
412
|
+
const duplicateServerNames = packageServerNames.filter((serverName) => existingServerNames.includes(serverName));
|
|
413
|
+
if (duplicateServerNames.length > 0) {
|
|
414
|
+
return {
|
|
415
|
+
client: "Windsurf",
|
|
416
|
+
status: "warn",
|
|
417
|
+
summary: "Windsurf already has MCP server names from this package.",
|
|
418
|
+
details: [
|
|
419
|
+
configPath,
|
|
420
|
+
...duplicateServerNames.map((serverName) => `Duplicate server: ${serverName}`)
|
|
421
|
+
]
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
client: "Windsurf",
|
|
426
|
+
status: "pass",
|
|
427
|
+
summary: "Windsurf MCP config is valid and this package can be added.",
|
|
428
|
+
details: [
|
|
429
|
+
configPath,
|
|
430
|
+
`Source package: ${path.resolve(targetPath)}`
|
|
431
|
+
]
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
return {
|
|
436
|
+
client: "Windsurf",
|
|
437
|
+
status: "fail",
|
|
438
|
+
summary: "Windsurf MCP config is not valid JSON.",
|
|
439
|
+
details: [configPath, "Repair the local Windsurf MCP config before adding new MCP servers."]
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
368
443
|
export async function buildCompatibilityMatrix(targetPath, environment = {}) {
|
|
369
444
|
const rootPath = path.resolve(targetPath);
|
|
370
445
|
const genericMcpResult = await checkGenericMcp(rootPath);
|
|
371
446
|
const claudeDesktopResult = await checkClaudeDesktop(rootPath, genericMcpResult, environment);
|
|
372
447
|
const cursorResult = await checkCursor(rootPath, genericMcpResult, environment);
|
|
373
448
|
const clineResult = await checkCline(rootPath, genericMcpResult, environment);
|
|
449
|
+
const windsurfResult = await checkWindsurf(rootPath, genericMcpResult, environment);
|
|
374
450
|
const codexResult = await validatePlugin(rootPath);
|
|
375
451
|
const codexStatus = statusFromCheckResult(codexResult);
|
|
376
452
|
const codexCompatibility = !await hasCodexManifest(rootPath)
|
|
@@ -394,7 +470,8 @@ export async function buildCompatibilityMatrix(targetPath, environment = {}) {
|
|
|
394
470
|
genericMcpResult,
|
|
395
471
|
claudeDesktopResult,
|
|
396
472
|
cursorResult,
|
|
397
|
-
clineResult
|
|
473
|
+
clineResult,
|
|
474
|
+
windsurfResult
|
|
398
475
|
];
|
|
399
476
|
return {
|
|
400
477
|
targetPath: rootPath,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type CompatibilityEnvironment } from "./compatibility-matrix.js";
|
|
2
|
+
export interface WindsurfInstallPreview {
|
|
3
|
+
targetPath: string;
|
|
4
|
+
configPath: string;
|
|
5
|
+
snippet: {
|
|
6
|
+
mcpServers: Record<string, unknown>;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export declare function buildWindsurfInstallPreview(targetPath: string, environment?: CompatibilityEnvironment): Promise<WindsurfInstallPreview>;
|
|
10
|
+
export declare function renderWindsurfInstallPreview(preview: WindsurfInstallPreview): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getWindsurfMcpConfigPath, readMcpConfigPath } from "./compatibility-matrix.js";
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function isRelativeLocalPath(value) {
|
|
8
|
+
return value.startsWith("./") ||
|
|
9
|
+
value.startsWith("../") ||
|
|
10
|
+
value.startsWith(".\\") ||
|
|
11
|
+
value.startsWith("..\\");
|
|
12
|
+
}
|
|
13
|
+
function normalizeLocalPathArgument(value, rootPath) {
|
|
14
|
+
return typeof value === "string" && isRelativeLocalPath(value)
|
|
15
|
+
? path.resolve(rootPath, value)
|
|
16
|
+
: value;
|
|
17
|
+
}
|
|
18
|
+
function normalizeServerConfig(serverConfig, rootPath) {
|
|
19
|
+
if (!isRecord(serverConfig)) {
|
|
20
|
+
return serverConfig;
|
|
21
|
+
}
|
|
22
|
+
const normalized = { ...serverConfig };
|
|
23
|
+
if (typeof normalized.command === "string" && isRelativeLocalPath(normalized.command)) {
|
|
24
|
+
normalized.command = path.resolve(rootPath, normalized.command);
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(normalized.args)) {
|
|
27
|
+
normalized.args = normalized.args.map((argument) => normalizeLocalPathArgument(argument, rootPath));
|
|
28
|
+
}
|
|
29
|
+
return normalized;
|
|
30
|
+
}
|
|
31
|
+
export async function buildWindsurfInstallPreview(targetPath, environment = {}) {
|
|
32
|
+
const rootPath = path.resolve(targetPath);
|
|
33
|
+
const mcpConfigPath = await readMcpConfigPath(rootPath);
|
|
34
|
+
if (!mcpConfigPath) {
|
|
35
|
+
throw new Error("No MCP config found for install preview.");
|
|
36
|
+
}
|
|
37
|
+
const parsed = JSON.parse(await readFile(mcpConfigPath, "utf8"));
|
|
38
|
+
const servers = parsed.mcpServers;
|
|
39
|
+
if (!isRecord(servers) || Object.keys(servers).length === 0) {
|
|
40
|
+
throw new Error("MCP config does not contain a non-empty `mcpServers` object.");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
targetPath: rootPath,
|
|
44
|
+
configPath: getWindsurfMcpConfigPath(environment),
|
|
45
|
+
snippet: {
|
|
46
|
+
mcpServers: Object.fromEntries(Object.entries(servers).map(([serverName, serverConfig]) => [
|
|
47
|
+
serverName,
|
|
48
|
+
normalizeServerConfig(serverConfig, rootPath)
|
|
49
|
+
]))
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function renderWindsurfInstallPreview(preview) {
|
|
54
|
+
return [
|
|
55
|
+
"Windsurf Install Preview",
|
|
56
|
+
"========================",
|
|
57
|
+
`Target: ${preview.targetPath}`,
|
|
58
|
+
`Config: ${preview.configPath}`,
|
|
59
|
+
"",
|
|
60
|
+
"Add or merge this snippet into `mcp_config.json`:",
|
|
61
|
+
JSON.stringify(preview.snippet, null, 2),
|
|
62
|
+
"",
|
|
63
|
+
"No files were modified."
|
|
64
|
+
].join("\n");
|
|
65
|
+
}
|
|
@@ -1,2 +1,19 @@
|
|
|
1
1
|
import type { CliTerminalContext } from "../run-cli.js";
|
|
2
|
+
export interface EnvironmentDoctorReport {
|
|
3
|
+
schemaVersion: "1.0.0";
|
|
4
|
+
version: string;
|
|
5
|
+
platform: string;
|
|
6
|
+
node: string;
|
|
7
|
+
npmGlobalPrefix: string;
|
|
8
|
+
codexHome: {
|
|
9
|
+
status: "pass" | "warn";
|
|
10
|
+
path: string | null;
|
|
11
|
+
};
|
|
12
|
+
codexPluginCache: {
|
|
13
|
+
status: "pass" | "warn";
|
|
14
|
+
path: string | null;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
2
17
|
export declare function renderEnvironmentDoctor(terminalContext: CliTerminalContext): Promise<string>;
|
|
18
|
+
export declare function buildEnvironmentDoctorReport(terminalContext: CliTerminalContext): Promise<EnvironmentDoctorReport>;
|
|
19
|
+
export declare function renderEnvironmentDoctorJson(terminalContext: CliTerminalContext): Promise<string>;
|
|
@@ -23,19 +23,48 @@ function resolveCodexHome(env) {
|
|
|
23
23
|
return null;
|
|
24
24
|
}
|
|
25
25
|
export async function renderEnvironmentDoctor(terminalContext) {
|
|
26
|
+
const report = await buildEnvironmentDoctorReport(terminalContext);
|
|
27
|
+
return [
|
|
28
|
+
"Codex Plugin Doctor Environment",
|
|
29
|
+
"===============================",
|
|
30
|
+
`Version: ${report.version}`,
|
|
31
|
+
`Platform: ${report.platform}`,
|
|
32
|
+
`Node: ${report.node}`,
|
|
33
|
+
`npm global prefix: ${report.npmGlobalPrefix}`,
|
|
34
|
+
`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})` : ""}`,
|
|
36
|
+
"",
|
|
37
|
+
"Recommended next commands",
|
|
38
|
+
"-------------------------",
|
|
39
|
+
"codex-plugin-doctor self-test",
|
|
40
|
+
"codex-plugin-doctor list --installed",
|
|
41
|
+
"codex-plugin-doctor check . --runtime --explain",
|
|
42
|
+
"codex-plugin-doctor compat . --all --scorecard",
|
|
43
|
+
"codex-plugin-doctor init-ci ."
|
|
44
|
+
].join("\n");
|
|
45
|
+
}
|
|
46
|
+
export async function buildEnvironmentDoctorReport(terminalContext) {
|
|
26
47
|
const codexHome = resolveCodexHome(terminalContext.env);
|
|
27
48
|
const codexHomeExists = codexHome ? await pathExists(codexHome) : false;
|
|
28
49
|
const pluginCache = codexHome ? path.join(codexHome, "plugins", "cache") : null;
|
|
29
50
|
const pluginCacheExists = pluginCache ? await pathExists(pluginCache) : false;
|
|
30
51
|
const npmPrefix = terminalContext.env.npm_config_prefix ?? "(unknown)";
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
52
|
+
return {
|
|
53
|
+
schemaVersion: "1.0.0",
|
|
54
|
+
version: packageVersion,
|
|
55
|
+
platform: terminalContext.platform ?? process.platform,
|
|
56
|
+
node: process.version,
|
|
57
|
+
npmGlobalPrefix: npmPrefix,
|
|
58
|
+
codexHome: {
|
|
59
|
+
status: codexHomeExists ? "pass" : "warn",
|
|
60
|
+
path: codexHome
|
|
61
|
+
},
|
|
62
|
+
codexPluginCache: {
|
|
63
|
+
status: pluginCacheExists ? "pass" : "warn",
|
|
64
|
+
path: pluginCache
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export async function renderEnvironmentDoctorJson(terminalContext) {
|
|
69
|
+
return JSON.stringify(await buildEnvironmentDoctorReport(terminalContext), null, 2);
|
|
41
70
|
}
|
package/dist/core/fix-plan.d.ts
CHANGED
|
@@ -14,7 +14,22 @@ export interface ApplyFixPlanResult {
|
|
|
14
14
|
filesChanged: number;
|
|
15
15
|
backupDirectory: string;
|
|
16
16
|
}
|
|
17
|
+
export interface FixPlanJsonReport {
|
|
18
|
+
schemaVersion: "1.0.0";
|
|
19
|
+
mode: "dry-run" | "apply";
|
|
20
|
+
targetPath: string;
|
|
21
|
+
filesChanged: number;
|
|
22
|
+
backupDirectory: string | null;
|
|
23
|
+
actions: Array<FixPlanAction & {
|
|
24
|
+
relativePath: string;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
17
27
|
export declare function buildFixPlan(targetPath: string): Promise<FixPlan>;
|
|
18
|
-
export declare function renderFixPlan(plan: FixPlan, mode: "dry-run"): string;
|
|
28
|
+
export declare function renderFixPlan(plan: FixPlan, mode: "dry-run" | "interactive"): string;
|
|
19
29
|
export declare function applyFixPlan(targetPath: string): Promise<ApplyFixPlanResult>;
|
|
20
30
|
export declare function renderApplyFixResult(result: ApplyFixPlanResult): string;
|
|
31
|
+
export declare function renderFixPlanJsonReport(plan: FixPlan, options: {
|
|
32
|
+
mode: "dry-run" | "apply";
|
|
33
|
+
filesChanged?: number;
|
|
34
|
+
backupDirectory?: string | null;
|
|
35
|
+
}): string;
|
package/dist/core/fix-plan.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { copyFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { validatePlugin } from "./validate-plugin.js";
|
|
4
4
|
function relativeToTarget(targetPath, candidatePath) {
|
|
@@ -33,6 +33,53 @@ export async function buildFixPlan(targetPath) {
|
|
|
33
33
|
details: "Create the skills directory referenced by the manifest."
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
|
+
const manifest = await readFile(manifestPath, "utf8")
|
|
37
|
+
.then((content) => JSON.parse(content))
|
|
38
|
+
.catch(() => ({}));
|
|
39
|
+
if (typeof manifest.skills === "string") {
|
|
40
|
+
const skillsPath = path.resolve(rootPath, manifest.skills);
|
|
41
|
+
if (await directoryExists(skillsPath)) {
|
|
42
|
+
for (const entry of await readdir(skillsPath, { withFileTypes: true })) {
|
|
43
|
+
if (!entry.isDirectory()) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const skillFilePath = path.join(skillsPath, entry.name, "SKILL.md");
|
|
47
|
+
if (!(await fileExists(skillFilePath))) {
|
|
48
|
+
actions.push({
|
|
49
|
+
id: "skill.scaffold_skill_md",
|
|
50
|
+
title: `Create missing SKILL.md for ${entry.name}`,
|
|
51
|
+
targetPath: skillFilePath,
|
|
52
|
+
operation: "update-json",
|
|
53
|
+
details: `Create ${relativeToTarget(rootPath, skillFilePath)} with safe frontmatter.`
|
|
54
|
+
});
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const skillContent = await readFile(skillFilePath, "utf8");
|
|
58
|
+
const frontmatter = skillContent.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? "";
|
|
59
|
+
if (!/^name\s*:/im.test(frontmatter) || !/^description\s*:/im.test(frontmatter)) {
|
|
60
|
+
actions.push({
|
|
61
|
+
id: "skill.safe_frontmatter_defaults",
|
|
62
|
+
title: `Add missing skill frontmatter defaults for ${entry.name}`,
|
|
63
|
+
targetPath: skillFilePath,
|
|
64
|
+
operation: "update-json",
|
|
65
|
+
details: `Set missing name/description fields in ${relativeToTarget(rootPath, skillFilePath)}.`
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (typeof manifest.mcpServers === "string") {
|
|
72
|
+
const mcpConfigPath = path.resolve(rootPath, manifest.mcpServers);
|
|
73
|
+
if (!(await fileExists(mcpConfigPath))) {
|
|
74
|
+
actions.push({
|
|
75
|
+
id: "mcp.scaffold_config",
|
|
76
|
+
title: "Create missing MCP config",
|
|
77
|
+
targetPath: mcpConfigPath,
|
|
78
|
+
operation: "update-json",
|
|
79
|
+
details: `Create ${relativeToTarget(rootPath, mcpConfigPath)} with an empty mcpServers object.`
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
36
83
|
return {
|
|
37
84
|
targetPath: rootPath,
|
|
38
85
|
actions
|
|
@@ -66,6 +113,54 @@ function timestampForPath() {
|
|
|
66
113
|
function defaultManifestDescription() {
|
|
67
114
|
return "Codex plugin package.";
|
|
68
115
|
}
|
|
116
|
+
async function fileExists(targetPath) {
|
|
117
|
+
try {
|
|
118
|
+
const details = await stat(targetPath);
|
|
119
|
+
return details.isFile();
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function directoryExists(targetPath) {
|
|
126
|
+
try {
|
|
127
|
+
const details = await stat(targetPath);
|
|
128
|
+
return details.isDirectory();
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function skillDescription(skillName) {
|
|
135
|
+
return `Use when running the ${skillName} skill.`;
|
|
136
|
+
}
|
|
137
|
+
function renderSkillScaffold(skillName, body = "") {
|
|
138
|
+
return [
|
|
139
|
+
"---",
|
|
140
|
+
`name: ${skillName}`,
|
|
141
|
+
`description: ${skillDescription(skillName)}`,
|
|
142
|
+
"---",
|
|
143
|
+
"",
|
|
144
|
+
body.trim() || `# ${skillName}`
|
|
145
|
+
].join("\n") + "\n";
|
|
146
|
+
}
|
|
147
|
+
function replaceFrontmatter(content, skillName) {
|
|
148
|
+
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
149
|
+
const frontmatter = frontmatterMatch?.[1] ?? "";
|
|
150
|
+
const body = frontmatterMatch ? content.slice(frontmatterMatch[0].length) : content;
|
|
151
|
+
const lines = frontmatter.split(/\r?\n/).filter((line) => line.trim());
|
|
152
|
+
const hasName = lines.some((line) => /^name\s*:/i.test(line));
|
|
153
|
+
const hasDescription = lines.some((line) => /^description\s*:/i.test(line));
|
|
154
|
+
return [
|
|
155
|
+
"---",
|
|
156
|
+
...(hasName ? [] : [`name: ${skillName}`]),
|
|
157
|
+
...(hasDescription ? [] : [`description: ${skillDescription(skillName)}`]),
|
|
158
|
+
...lines,
|
|
159
|
+
"---",
|
|
160
|
+
"",
|
|
161
|
+
body.trim() || `# ${skillName}`
|
|
162
|
+
].join("\n") + "\n";
|
|
163
|
+
}
|
|
69
164
|
async function backupFile(rootPath, backupDirectory, filePath) {
|
|
70
165
|
const relativePath = path.relative(rootPath, filePath);
|
|
71
166
|
const backupPath = path.join(backupDirectory, relativePath);
|
|
@@ -94,6 +189,24 @@ export async function applyFixPlan(targetPath) {
|
|
|
94
189
|
if (action.operation === "mkdir" && action.id === "skills.create_directory") {
|
|
95
190
|
await mkdir(action.targetPath, { recursive: true });
|
|
96
191
|
filesChanged += 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (action.operation === "update-json" && action.id === "skill.scaffold_skill_md") {
|
|
195
|
+
await mkdir(path.dirname(action.targetPath), { recursive: true });
|
|
196
|
+
await writeFile(action.targetPath, renderSkillScaffold(path.basename(path.dirname(action.targetPath))), "utf8");
|
|
197
|
+
filesChanged += 1;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (action.operation === "update-json" && action.id === "skill.safe_frontmatter_defaults") {
|
|
201
|
+
await backupFile(plan.targetPath, backupDirectory, action.targetPath);
|
|
202
|
+
await writeFile(action.targetPath, replaceFrontmatter(await readFile(action.targetPath, "utf8"), path.basename(path.dirname(action.targetPath))), "utf8");
|
|
203
|
+
filesChanged += 1;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (action.operation === "update-json" && action.id === "mcp.scaffold_config") {
|
|
207
|
+
await mkdir(path.dirname(action.targetPath), { recursive: true });
|
|
208
|
+
await writeFile(action.targetPath, `${JSON.stringify({ mcpServers: {} }, null, 2)}\n`, "utf8");
|
|
209
|
+
filesChanged += 1;
|
|
97
210
|
}
|
|
98
211
|
}
|
|
99
212
|
return {
|
|
@@ -112,3 +225,17 @@ export function renderApplyFixResult(result) {
|
|
|
112
225
|
`Backup: ${relativeToTarget(result.plan.targetPath, result.backupDirectory)}`
|
|
113
226
|
].join("\n");
|
|
114
227
|
}
|
|
228
|
+
export function renderFixPlanJsonReport(plan, options) {
|
|
229
|
+
const report = {
|
|
230
|
+
schemaVersion: "1.0.0",
|
|
231
|
+
mode: options.mode,
|
|
232
|
+
targetPath: plan.targetPath,
|
|
233
|
+
filesChanged: options.filesChanged ?? 0,
|
|
234
|
+
backupDirectory: options.backupDirectory ?? null,
|
|
235
|
+
actions: plan.actions.map((action) => ({
|
|
236
|
+
...action,
|
|
237
|
+
relativePath: relativeToTarget(plan.targetPath, action.targetPath)
|
|
238
|
+
}))
|
|
239
|
+
};
|
|
240
|
+
return JSON.stringify(report, null, 2);
|
|
241
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { packageVersion } from "../version.js";
|
|
4
|
+
function buildWorkflow() {
|
|
5
|
+
return [
|
|
6
|
+
"name: Validate Codex plugin",
|
|
7
|
+
"",
|
|
8
|
+
"on:",
|
|
9
|
+
" pull_request:",
|
|
10
|
+
" push:",
|
|
11
|
+
" branches:",
|
|
12
|
+
" - main",
|
|
13
|
+
"",
|
|
14
|
+
"jobs:",
|
|
15
|
+
" doctor:",
|
|
16
|
+
" runs-on: ubuntu-latest",
|
|
17
|
+
" steps:",
|
|
18
|
+
" - uses: actions/checkout@v4",
|
|
19
|
+
` - uses: Esquetta/CodexPluginDoctor@v${packageVersion}`,
|
|
20
|
+
" with:",
|
|
21
|
+
` version: \"${packageVersion}\"`,
|
|
22
|
+
" path: .",
|
|
23
|
+
" runtime: \"true\"",
|
|
24
|
+
""
|
|
25
|
+
].join("\n");
|
|
26
|
+
}
|
|
27
|
+
export async function initCiWorkflow(targetPath) {
|
|
28
|
+
const rootPath = path.resolve(targetPath);
|
|
29
|
+
const workflowPath = path.join(rootPath, ".github", "workflows", "codex-plugin-doctor.yml");
|
|
30
|
+
await mkdir(path.dirname(workflowPath), { recursive: true });
|
|
31
|
+
await writeFile(workflowPath, buildWorkflow(), "utf8");
|
|
32
|
+
return {
|
|
33
|
+
rootPath,
|
|
34
|
+
workflowPath
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -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
package/dist/run-cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { discoverInstalledPlugins, filterInstalledPlugins } from "./core/discover-installed-plugins.js";
|
|
5
6
|
import { appendValidationHistoryEntry, readValidationHistory, summarizeValidationHistory } from "./core/validation-history.js";
|
|
@@ -8,9 +9,11 @@ import { applyInstallPreview, renderApplyInstallResult } from "./compatibility/a
|
|
|
8
9
|
import { buildClaudeDesktopInstallPreview, renderClaudeDesktopInstallPreview } from "./compatibility/claude-desktop-install-preview.js";
|
|
9
10
|
import { buildCursorInstallPreview, renderCursorInstallPreview } from "./compatibility/cursor-install-preview.js";
|
|
10
11
|
import { buildClineInstallPreview, renderClineInstallPreview } from "./compatibility/cline-install-preview.js";
|
|
12
|
+
import { buildWindsurfInstallPreview, renderWindsurfInstallPreview } from "./compatibility/windsurf-install-preview.js";
|
|
11
13
|
import { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
|
|
12
|
-
import { applyFixPlan, buildFixPlan, renderApplyFixResult, renderFixPlan } from "./core/fix-plan.js";
|
|
13
|
-
import { renderEnvironmentDoctor } from "./core/environment-doctor.js";
|
|
14
|
+
import { applyFixPlan, buildFixPlan, renderApplyFixResult, renderFixPlanJsonReport, renderFixPlan } from "./core/fix-plan.js";
|
|
15
|
+
import { renderEnvironmentDoctor, renderEnvironmentDoctorJson } from "./core/environment-doctor.js";
|
|
16
|
+
import { initCiWorkflow } from "./core/init-ci.js";
|
|
14
17
|
import { initPluginPackage } from "./core/init-plugin.js";
|
|
15
18
|
import { runCheck } from "./index.js";
|
|
16
19
|
import { renderInstalledSummary } from "./reporting/render-installed-summary.js";
|
|
@@ -34,10 +37,22 @@ const defaultIo = {
|
|
|
34
37
|
},
|
|
35
38
|
writeStderr(message) {
|
|
36
39
|
process.stderr.write(`${message}\n`);
|
|
40
|
+
},
|
|
41
|
+
async readStdin(prompt) {
|
|
42
|
+
const readline = createInterface({
|
|
43
|
+
input: process.stdin,
|
|
44
|
+
output: process.stdout
|
|
45
|
+
});
|
|
46
|
+
try {
|
|
47
|
+
return await readline.question(prompt);
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
readline.close();
|
|
51
|
+
}
|
|
37
52
|
}
|
|
38
53
|
};
|
|
39
54
|
function printUsage(io) {
|
|
40
|
-
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 self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version");
|
|
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");
|
|
41
56
|
}
|
|
42
57
|
function renderInstalledPlugins(plugins) {
|
|
43
58
|
const lines = [
|
|
@@ -64,7 +79,8 @@ const compatibilityClientAliases = {
|
|
|
64
79
|
"claude-desktop": "Claude Desktop",
|
|
65
80
|
claude: "Claude Desktop",
|
|
66
81
|
cursor: "Cursor",
|
|
67
|
-
cline: "Cline"
|
|
82
|
+
cline: "Cline",
|
|
83
|
+
windsurf: "Windsurf"
|
|
68
84
|
};
|
|
69
85
|
const checkProfiles = ["ci", "strict", "publish"];
|
|
70
86
|
function parseCheckProfile(value) {
|
|
@@ -130,7 +146,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
130
146
|
return 0;
|
|
131
147
|
}
|
|
132
148
|
if (command === "doctor") {
|
|
133
|
-
io.writeStdout(
|
|
149
|
+
io.writeStdout(maybePath === "--json"
|
|
150
|
+
? await renderEnvironmentDoctorJson(terminalContext)
|
|
151
|
+
: await renderEnvironmentDoctor(terminalContext));
|
|
134
152
|
return 0;
|
|
135
153
|
}
|
|
136
154
|
if (command === "explain") {
|
|
@@ -195,25 +213,68 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
195
213
|
].join("\n"));
|
|
196
214
|
return 0;
|
|
197
215
|
}
|
|
216
|
+
if (command === "init-ci") {
|
|
217
|
+
const targetPath = maybePath && !maybePath.startsWith("--") ? maybePath : ".";
|
|
218
|
+
const result = await initCiWorkflow(targetPath);
|
|
219
|
+
io.writeStdout([
|
|
220
|
+
"Initialized Codex Plugin Doctor workflow",
|
|
221
|
+
`Root: ${result.rootPath}`,
|
|
222
|
+
`Workflow: ${result.workflowPath}`
|
|
223
|
+
].join("\n"));
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
198
226
|
if (command === "fix") {
|
|
199
227
|
if (!maybePath || maybePath.startsWith("--")) {
|
|
200
|
-
io.writeStderr("Missing target path. Usage: codex-plugin-doctor fix <path> (--dry-run|--apply --backup)");
|
|
228
|
+
io.writeStderr("Missing target path. Usage: codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)");
|
|
201
229
|
return 2;
|
|
202
230
|
}
|
|
203
231
|
const dryRun = remainingArgs.includes("--dry-run");
|
|
204
232
|
const apply = remainingArgs.includes("--apply");
|
|
233
|
+
const interactive = remainingArgs.includes("--interactive");
|
|
205
234
|
const backup = remainingArgs.includes("--backup");
|
|
206
|
-
|
|
207
|
-
|
|
235
|
+
const jsonOutput = remainingArgs.includes("--json");
|
|
236
|
+
if ((apply || interactive) && !backup) {
|
|
237
|
+
io.writeStderr("Fix mode requires --backup.");
|
|
238
|
+
return 2;
|
|
239
|
+
}
|
|
240
|
+
if ([dryRun, apply, interactive].filter(Boolean).length !== 1) {
|
|
241
|
+
io.writeStderr("Choose exactly one fix mode: --dry-run, --interactive --backup, or --apply --backup.");
|
|
208
242
|
return 2;
|
|
209
243
|
}
|
|
210
|
-
if (
|
|
211
|
-
io.writeStderr("
|
|
244
|
+
if (interactive && jsonOutput) {
|
|
245
|
+
io.writeStderr("Interactive fix mode does not support --json.");
|
|
212
246
|
return 2;
|
|
213
247
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
248
|
+
if (dryRun) {
|
|
249
|
+
const plan = await buildFixPlan(maybePath);
|
|
250
|
+
io.writeStdout(jsonOutput
|
|
251
|
+
? renderFixPlanJsonReport(plan, { mode: "dry-run" })
|
|
252
|
+
: renderFixPlan(plan, "dry-run"));
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
if (interactive) {
|
|
256
|
+
const plan = await buildFixPlan(maybePath);
|
|
257
|
+
io.writeStdout([
|
|
258
|
+
renderFixPlan(plan, "interactive"),
|
|
259
|
+
"",
|
|
260
|
+
"Type yes to apply these fixes with a backup. Anything else cancels."
|
|
261
|
+
].join("\n"));
|
|
262
|
+
const answer = (await io.readStdin?.("Apply fixes? ") ?? "").trim().toLowerCase();
|
|
263
|
+
if (answer !== "yes") {
|
|
264
|
+
io.writeStdout("Fix cancelled. No files changed.");
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
io.writeStdout(renderApplyFixResult(await applyFixPlan(maybePath)));
|
|
268
|
+
return 0;
|
|
269
|
+
}
|
|
270
|
+
const result = await applyFixPlan(maybePath);
|
|
271
|
+
io.writeStdout(jsonOutput
|
|
272
|
+
? renderFixPlanJsonReport(result.plan, {
|
|
273
|
+
mode: "apply",
|
|
274
|
+
filesChanged: result.filesChanged,
|
|
275
|
+
backupDirectory: result.backupDirectory
|
|
276
|
+
})
|
|
277
|
+
: renderApplyFixResult(result));
|
|
217
278
|
return 0;
|
|
218
279
|
}
|
|
219
280
|
if (command === "compat") {
|
|
@@ -226,6 +287,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
226
287
|
const installPreview = compatFlags.includes("--install-preview");
|
|
227
288
|
const applyInstall = compatFlags.includes("--apply");
|
|
228
289
|
const backupInstall = compatFlags.includes("--backup");
|
|
290
|
+
const allClients = compatFlags.includes("--all");
|
|
229
291
|
const clientIndex = compatFlags.indexOf("--client");
|
|
230
292
|
const clientFilter = clientIndex === -1 ? null : compatFlags[clientIndex + 1];
|
|
231
293
|
const outputIndex = compatFlags.indexOf("--output");
|
|
@@ -234,6 +296,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
234
296
|
io.writeStderr("Missing client after --client.");
|
|
235
297
|
return 2;
|
|
236
298
|
}
|
|
299
|
+
if (allClients && clientFilter) {
|
|
300
|
+
io.writeStderr("Use either --all or --client, not both.");
|
|
301
|
+
return 2;
|
|
302
|
+
}
|
|
237
303
|
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
238
304
|
io.writeStderr("Missing path after --output.");
|
|
239
305
|
return 2;
|
|
@@ -241,8 +307,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
241
307
|
if ((installPreview || applyInstall) &&
|
|
242
308
|
clientFilter?.toLowerCase() !== "claude-desktop" &&
|
|
243
309
|
clientFilter?.toLowerCase() !== "cursor" &&
|
|
244
|
-
clientFilter?.toLowerCase() !== "cline"
|
|
245
|
-
|
|
310
|
+
clientFilter?.toLowerCase() !== "cline" &&
|
|
311
|
+
clientFilter?.toLowerCase() !== "windsurf") {
|
|
312
|
+
io.writeStderr("--install-preview and --apply require --client claude-desktop, cursor, cline, or windsurf.");
|
|
246
313
|
return 2;
|
|
247
314
|
}
|
|
248
315
|
if (installPreview && applyInstall) {
|
|
@@ -266,21 +333,30 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
266
333
|
env: terminalContext.env,
|
|
267
334
|
platform: terminalContext.platform
|
|
268
335
|
})
|
|
269
|
-
:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
336
|
+
: normalizedClient === "windsurf"
|
|
337
|
+
? await buildWindsurfInstallPreview(targetPath, {
|
|
338
|
+
env: terminalContext.env,
|
|
339
|
+
platform: terminalContext.platform
|
|
340
|
+
})
|
|
341
|
+
: await buildClaudeDesktopInstallPreview(targetPath, {
|
|
342
|
+
env: terminalContext.env,
|
|
343
|
+
platform: terminalContext.platform
|
|
344
|
+
});
|
|
273
345
|
const report = applyInstall
|
|
274
346
|
? renderApplyInstallResult(await applyInstallPreview(normalizedClient === "cursor"
|
|
275
347
|
? "Cursor"
|
|
276
348
|
: normalizedClient === "cline"
|
|
277
349
|
? "Cline"
|
|
278
|
-
:
|
|
350
|
+
: normalizedClient === "windsurf"
|
|
351
|
+
? "Windsurf"
|
|
352
|
+
: "Claude Desktop", preview))
|
|
279
353
|
: normalizedClient === "cursor"
|
|
280
354
|
? renderCursorInstallPreview(preview)
|
|
281
355
|
: normalizedClient === "cline"
|
|
282
356
|
? renderClineInstallPreview(preview)
|
|
283
|
-
:
|
|
357
|
+
: normalizedClient === "windsurf"
|
|
358
|
+
? renderWindsurfInstallPreview(preview)
|
|
359
|
+
: renderClaudeDesktopInstallPreview(preview);
|
|
284
360
|
if (outputPath) {
|
|
285
361
|
await writeFile(outputPath, report, "utf8");
|
|
286
362
|
}
|
|
@@ -340,6 +416,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
340
416
|
const sarifOutput = normalizedFlags.includes("--sarif");
|
|
341
417
|
const runtimeProbeEnabled = normalizedFlags.includes("--runtime");
|
|
342
418
|
const verboseRuntime = normalizedFlags.includes("--verbose-runtime");
|
|
419
|
+
const explainFindings = normalizedFlags.includes("--explain");
|
|
343
420
|
const noAnimations = normalizedFlags.includes("--no-animations");
|
|
344
421
|
const asciiMode = normalizedFlags.includes("--ascii");
|
|
345
422
|
const installedSummary = normalizedFlags.includes("--all-summary");
|
|
@@ -422,7 +499,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
422
499
|
? buildMarkdownReport(item.result, { runtimeProbeEnabled: effectiveRuntimeProbeEnabled })
|
|
423
500
|
: jsonOutput
|
|
424
501
|
? renderJsonReport(item.result, { runtimeProbeEnabled: effectiveRuntimeProbeEnabled })
|
|
425
|
-
: renderTextReport(item.result, {
|
|
502
|
+
: renderTextReport(item.result, {
|
|
503
|
+
ascii: outputPolicy.style === "ascii",
|
|
504
|
+
explain: explainFindings
|
|
505
|
+
}))
|
|
426
506
|
.join("\n\n");
|
|
427
507
|
if (outputPath) {
|
|
428
508
|
await writeFile(outputPath, report, "utf8");
|
|
@@ -459,7 +539,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
459
539
|
? renderBadgeJson(result)
|
|
460
540
|
: badgeMarkdownOutput
|
|
461
541
|
? renderBadgeMarkdown(result)
|
|
462
|
-
: renderTextReport(result, {
|
|
542
|
+
: renderTextReport(result, {
|
|
543
|
+
ascii: outputPolicy.style === "ascii",
|
|
544
|
+
explain: explainFindings
|
|
545
|
+
});
|
|
463
546
|
if (outputPath) {
|
|
464
547
|
await writeFile(outputPath, report, "utf8");
|
|
465
548
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-plugin-doctor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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"
|