codex-plugin-doctor 0.6.0 → 0.7.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 +44 -28
- package/dist/compatibility/cline-install-preview.d.ts +10 -0
- package/dist/compatibility/cline-install-preview.js +65 -0
- package/dist/compatibility/compatibility-matrix.d.ts +1 -0
- package/dist/compatibility/compatibility-matrix.js +82 -1
- package/dist/core/environment-doctor.d.ts +2 -0
- package/dist/core/environment-doctor.js +41 -0
- package/dist/core/fix-plan.d.ts +20 -0
- package/dist/core/fix-plan.js +114 -0
- package/dist/run-cli.js +97 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -162,24 +162,30 @@ Run these from a Codex plugin package root:
|
|
|
162
162
|
|
|
163
163
|
```bash
|
|
164
164
|
codex-plugin-doctor --version
|
|
165
|
-
codex-plugin-doctor self-test
|
|
166
|
-
codex-plugin-doctor
|
|
167
|
-
codex-plugin-doctor
|
|
168
|
-
codex-plugin-doctor compat .
|
|
169
|
-
codex-plugin-doctor compat . --client
|
|
165
|
+
codex-plugin-doctor self-test
|
|
166
|
+
codex-plugin-doctor doctor
|
|
167
|
+
codex-plugin-doctor init my-plugin
|
|
168
|
+
codex-plugin-doctor compat .
|
|
169
|
+
codex-plugin-doctor compat . --client codex
|
|
170
|
+
codex-plugin-doctor compat . --client generic-mcp
|
|
170
171
|
codex-plugin-doctor compat . --client claude-desktop
|
|
171
172
|
codex-plugin-doctor compat . --client claude-desktop --install-preview
|
|
172
173
|
codex-plugin-doctor compat . --client claude-desktop --apply --backup
|
|
173
|
-
codex-plugin-doctor compat . --client cursor
|
|
174
|
-
codex-plugin-doctor compat . --client cursor --install-preview
|
|
175
|
-
codex-plugin-doctor compat . --client cursor --apply --backup
|
|
176
|
-
codex-plugin-doctor compat . --
|
|
177
|
-
codex-plugin-doctor compat . --
|
|
178
|
-
codex-plugin-doctor compat . --
|
|
179
|
-
codex-plugin-doctor
|
|
180
|
-
codex-plugin-doctor
|
|
181
|
-
codex-plugin-doctor check .
|
|
182
|
-
codex-plugin-doctor check . --
|
|
174
|
+
codex-plugin-doctor compat . --client cursor
|
|
175
|
+
codex-plugin-doctor compat . --client cursor --install-preview
|
|
176
|
+
codex-plugin-doctor compat . --client cursor --apply --backup
|
|
177
|
+
codex-plugin-doctor compat . --client cline
|
|
178
|
+
codex-plugin-doctor compat . --client cline --install-preview
|
|
179
|
+
codex-plugin-doctor compat . --scorecard
|
|
180
|
+
codex-plugin-doctor compat . --json
|
|
181
|
+
codex-plugin-doctor compat . --json --output compatibility.json
|
|
182
|
+
codex-plugin-doctor check .
|
|
183
|
+
codex-plugin-doctor check . --profile ci
|
|
184
|
+
codex-plugin-doctor check . --profile strict
|
|
185
|
+
codex-plugin-doctor check . --profile publish
|
|
186
|
+
codex-plugin-doctor check . --json
|
|
187
|
+
codex-plugin-doctor check . --json --output report.json
|
|
188
|
+
codex-plugin-doctor check . --markdown --output report.md
|
|
183
189
|
codex-plugin-doctor check . --badge-json --output doctor-badge.json
|
|
184
190
|
codex-plugin-doctor check . --badge-markdown
|
|
185
191
|
codex-plugin-doctor check . --sarif --output results.sarif
|
|
@@ -191,22 +197,32 @@ codex-plugin-doctor check . --history validation-history.jsonl
|
|
|
191
197
|
codex-plugin-doctor history validation-history.jsonl
|
|
192
198
|
codex-plugin-doctor history validation-history.jsonl --json
|
|
193
199
|
codex-plugin-doctor history validation-history.jsonl --fail-on-regression
|
|
200
|
+
codex-plugin-doctor fix . --dry-run
|
|
201
|
+
codex-plugin-doctor fix . --apply --backup
|
|
194
202
|
codex-plugin-doctor check . --json --runtime --verbose-runtime
|
|
195
203
|
```
|
|
196
|
-
|
|
197
|
-
`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`.
|
|
198
|
-
|
|
199
|
-
`
|
|
200
|
-
|
|
201
|
-
`compat --client
|
|
202
|
-
|
|
203
|
-
`compat --
|
|
204
|
-
|
|
204
|
+
|
|
205
|
+
`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
|
+
|
|
207
|
+
`doctor` checks the local environment, including package version, platform, Node version, npm global prefix, Codex home, and Codex plugin cache visibility.
|
|
208
|
+
|
|
209
|
+
`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
|
+
|
|
211
|
+
`compat --client cursor` checks whether the MCP package can be added to Cursor. It prefers a project-level `.cursor/mcp.json` when one already exists in the target package, then falls back to the global `~/.cursor/mcp.json` path. A valid existing config returns `PASS`, a missing Cursor config returns `WARN`, malformed JSON returns `FAIL`, and duplicate MCP server names return `WARN`. Add `--install-preview` to print the JSON snippet that should be merged into Cursor's `mcp.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.
|
|
212
|
+
|
|
213
|
+
`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
|
+
|
|
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`.
|
|
216
|
+
|
|
217
|
+
`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
|
+
|
|
205
219
|
`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`.
|
|
206
220
|
|
|
207
221
|
`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.
|
|
208
|
-
|
|
209
|
-
|
|
222
|
+
|
|
223
|
+
`fix --dry-run` renders safe automatic fix plans without changing files. `fix --apply --backup` applies only supported safe fixes, such as manifest defaults and missing skills directories, after creating backups.
|
|
224
|
+
|
|
225
|
+
Optional local policy file:
|
|
210
226
|
|
|
211
227
|
```json
|
|
212
228
|
{
|
|
@@ -239,9 +255,9 @@ jobs:
|
|
|
239
255
|
runs-on: ubuntu-latest
|
|
240
256
|
steps:
|
|
241
257
|
- uses: actions/checkout@v4
|
|
242
|
-
- uses: Esquetta/CodexPluginDoctor@v0.
|
|
258
|
+
- uses: Esquetta/CodexPluginDoctor@v0.7.0
|
|
243
259
|
with:
|
|
244
|
-
version: "0.
|
|
260
|
+
version: "0.7.0"
|
|
245
261
|
path: .
|
|
246
262
|
runtime: "false"
|
|
247
263
|
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type CompatibilityEnvironment } from "./compatibility-matrix.js";
|
|
2
|
+
export interface ClineInstallPreview {
|
|
3
|
+
targetPath: string;
|
|
4
|
+
configPath: string;
|
|
5
|
+
snippet: {
|
|
6
|
+
mcpServers: Record<string, unknown>;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export declare function buildClineInstallPreview(targetPath: string, environment?: CompatibilityEnvironment): Promise<ClineInstallPreview>;
|
|
10
|
+
export declare function renderClineInstallPreview(preview: ClineInstallPreview): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getClineMcpConfigPath, 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 buildClineInstallPreview(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: getClineMcpConfigPath(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 renderClineInstallPreview(preview) {
|
|
54
|
+
return [
|
|
55
|
+
"Cline Install Preview",
|
|
56
|
+
"=====================",
|
|
57
|
+
`Target: ${preview.targetPath}`,
|
|
58
|
+
`Config: ${preview.configPath}`,
|
|
59
|
+
"",
|
|
60
|
+
"Add or merge this snippet into `cline_mcp_settings.json`:",
|
|
61
|
+
JSON.stringify(preview.snippet, null, 2),
|
|
62
|
+
"",
|
|
63
|
+
"No files were modified."
|
|
64
|
+
].join("\n");
|
|
65
|
+
}
|
|
@@ -17,5 +17,6 @@ export interface CompatibilityEnvironment {
|
|
|
17
17
|
export declare function readMcpConfigPath(targetPath: string): Promise<string | null>;
|
|
18
18
|
export declare function getClaudeDesktopConfigPath(environment?: CompatibilityEnvironment): string | null;
|
|
19
19
|
export declare function getCursorMcpConfigPath(targetPath: string, environment?: CompatibilityEnvironment): Promise<string>;
|
|
20
|
+
export declare function getClineMcpConfigPath(environment?: CompatibilityEnvironment): string;
|
|
20
21
|
export declare function buildCompatibilityMatrix(targetPath: string, environment?: CompatibilityEnvironment): Promise<CompatibilityMatrix>;
|
|
21
22
|
export declare function matrixExitCode(matrix: CompatibilityMatrix): 0 | 1;
|
|
@@ -134,6 +134,13 @@ export async function getCursorMcpConfigPath(targetPath, environment = {}) {
|
|
|
134
134
|
}
|
|
135
135
|
return path.join(getHomeDirectory(environment), ".cursor", "mcp.json");
|
|
136
136
|
}
|
|
137
|
+
export function getClineMcpConfigPath(environment = {}) {
|
|
138
|
+
const env = environment.env ?? process.env;
|
|
139
|
+
const clineDirectory = env.CLINE_DIR
|
|
140
|
+
? path.resolve(env.CLINE_DIR)
|
|
141
|
+
: path.join(getHomeDirectory(environment), ".cline");
|
|
142
|
+
return path.join(clineDirectory, "data", "settings", "cline_mcp_settings.json");
|
|
143
|
+
}
|
|
137
144
|
async function checkClaudeDesktop(targetPath, genericMcpResult, environment = {}) {
|
|
138
145
|
if (genericMcpResult.status !== "pass") {
|
|
139
146
|
return {
|
|
@@ -286,11 +293,84 @@ async function checkCursor(targetPath, genericMcpResult, environment = {}) {
|
|
|
286
293
|
};
|
|
287
294
|
}
|
|
288
295
|
}
|
|
296
|
+
async function checkCline(targetPath, genericMcpResult, environment = {}) {
|
|
297
|
+
if (genericMcpResult.status !== "pass") {
|
|
298
|
+
return {
|
|
299
|
+
client: "Cline",
|
|
300
|
+
status: "skipped",
|
|
301
|
+
summary: "No valid MCP package config is available for Cline.",
|
|
302
|
+
details: ["Add a valid `.mcp.json` with a non-empty `mcpServers` object first."]
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const configPath = getClineMcpConfigPath(environment);
|
|
306
|
+
if (!(await fileExists(configPath))) {
|
|
307
|
+
const configDirectory = path.dirname(configPath);
|
|
308
|
+
return {
|
|
309
|
+
client: "Cline",
|
|
310
|
+
status: await directoryExists(configDirectory) ? "pass" : "warn",
|
|
311
|
+
summary: await directoryExists(configDirectory)
|
|
312
|
+
? "Cline MCP settings directory exists and a config file can be created."
|
|
313
|
+
: "Cline was not detected on this machine.",
|
|
314
|
+
details: [
|
|
315
|
+
configPath,
|
|
316
|
+
"Cline stores MCP servers in `cline_mcp_settings.json` under its settings directory."
|
|
317
|
+
]
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
const parsed = await readJsonFile(configPath);
|
|
322
|
+
const servers = parsed.mcpServers;
|
|
323
|
+
if (servers !== undefined && (typeof servers !== "object" ||
|
|
324
|
+
servers === null ||
|
|
325
|
+
Array.isArray(servers))) {
|
|
326
|
+
return {
|
|
327
|
+
client: "Cline",
|
|
328
|
+
status: "fail",
|
|
329
|
+
summary: "Cline MCP config has an invalid `mcpServers` shape.",
|
|
330
|
+
details: [configPath, "`mcpServers` must be an object before this package can be added safely."]
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const packageServerNames = await readMcpServerNames(targetPath);
|
|
334
|
+
const existingServerNames = typeof servers === "object" && servers !== null
|
|
335
|
+
? Object.keys(servers)
|
|
336
|
+
: [];
|
|
337
|
+
const duplicateServerNames = packageServerNames.filter((serverName) => existingServerNames.includes(serverName));
|
|
338
|
+
if (duplicateServerNames.length > 0) {
|
|
339
|
+
return {
|
|
340
|
+
client: "Cline",
|
|
341
|
+
status: "warn",
|
|
342
|
+
summary: "Cline already has MCP server names from this package.",
|
|
343
|
+
details: [
|
|
344
|
+
configPath,
|
|
345
|
+
...duplicateServerNames.map((serverName) => `Duplicate server: ${serverName}`)
|
|
346
|
+
]
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
client: "Cline",
|
|
351
|
+
status: "pass",
|
|
352
|
+
summary: "Cline MCP config is valid and this package can be added.",
|
|
353
|
+
details: [
|
|
354
|
+
configPath,
|
|
355
|
+
`Source package: ${path.resolve(targetPath)}`
|
|
356
|
+
]
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
return {
|
|
361
|
+
client: "Cline",
|
|
362
|
+
status: "fail",
|
|
363
|
+
summary: "Cline MCP config is not valid JSON.",
|
|
364
|
+
details: [configPath, "Repair the local Cline MCP config before adding new MCP servers."]
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
}
|
|
289
368
|
export async function buildCompatibilityMatrix(targetPath, environment = {}) {
|
|
290
369
|
const rootPath = path.resolve(targetPath);
|
|
291
370
|
const genericMcpResult = await checkGenericMcp(rootPath);
|
|
292
371
|
const claudeDesktopResult = await checkClaudeDesktop(rootPath, genericMcpResult, environment);
|
|
293
372
|
const cursorResult = await checkCursor(rootPath, genericMcpResult, environment);
|
|
373
|
+
const clineResult = await checkCline(rootPath, genericMcpResult, environment);
|
|
294
374
|
const codexResult = await validatePlugin(rootPath);
|
|
295
375
|
const codexStatus = statusFromCheckResult(codexResult);
|
|
296
376
|
const codexCompatibility = !await hasCodexManifest(rootPath)
|
|
@@ -313,7 +393,8 @@ export async function buildCompatibilityMatrix(targetPath, environment = {}) {
|
|
|
313
393
|
codexCompatibility,
|
|
314
394
|
genericMcpResult,
|
|
315
395
|
claudeDesktopResult,
|
|
316
|
-
cursorResult
|
|
396
|
+
cursorResult,
|
|
397
|
+
clineResult
|
|
317
398
|
];
|
|
318
399
|
return {
|
|
319
400
|
targetPath: rootPath,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { packageVersion } from "../version.js";
|
|
4
|
+
async function pathExists(targetPath) {
|
|
5
|
+
try {
|
|
6
|
+
await access(targetPath);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function resolveCodexHome(env) {
|
|
14
|
+
if (env.CODEX_HOME) {
|
|
15
|
+
return path.resolve(env.CODEX_HOME);
|
|
16
|
+
}
|
|
17
|
+
if (env.USERPROFILE) {
|
|
18
|
+
return path.join(env.USERPROFILE, ".codex");
|
|
19
|
+
}
|
|
20
|
+
if (env.HOME) {
|
|
21
|
+
return path.join(env.HOME, ".codex");
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
export async function renderEnvironmentDoctor(terminalContext) {
|
|
26
|
+
const codexHome = resolveCodexHome(terminalContext.env);
|
|
27
|
+
const codexHomeExists = codexHome ? await pathExists(codexHome) : false;
|
|
28
|
+
const pluginCache = codexHome ? path.join(codexHome, "plugins", "cache") : null;
|
|
29
|
+
const pluginCacheExists = pluginCache ? await pathExists(pluginCache) : false;
|
|
30
|
+
const npmPrefix = terminalContext.env.npm_config_prefix ?? "(unknown)";
|
|
31
|
+
return [
|
|
32
|
+
"Codex Plugin Doctor Environment",
|
|
33
|
+
"===============================",
|
|
34
|
+
`Version: ${packageVersion}`,
|
|
35
|
+
`Platform: ${terminalContext.platform ?? process.platform}`,
|
|
36
|
+
`Node: ${process.version}`,
|
|
37
|
+
`npm global prefix: ${npmPrefix}`,
|
|
38
|
+
`Codex home: ${codexHomeExists ? "PASS" : "WARN"}${codexHome ? ` (${codexHome})` : ""}`,
|
|
39
|
+
`Codex plugin cache: ${pluginCacheExists ? "PASS" : "WARN"}${pluginCache ? ` (${pluginCache})` : ""}`
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface FixPlanAction {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
targetPath: string;
|
|
5
|
+
operation: "update-json" | "mkdir";
|
|
6
|
+
details: string;
|
|
7
|
+
}
|
|
8
|
+
export interface FixPlan {
|
|
9
|
+
targetPath: string;
|
|
10
|
+
actions: FixPlanAction[];
|
|
11
|
+
}
|
|
12
|
+
export interface ApplyFixPlanResult {
|
|
13
|
+
plan: FixPlan;
|
|
14
|
+
filesChanged: number;
|
|
15
|
+
backupDirectory: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function buildFixPlan(targetPath: string): Promise<FixPlan>;
|
|
18
|
+
export declare function renderFixPlan(plan: FixPlan, mode: "dry-run"): string;
|
|
19
|
+
export declare function applyFixPlan(targetPath: string): Promise<ApplyFixPlanResult>;
|
|
20
|
+
export declare function renderApplyFixResult(result: ApplyFixPlanResult): string;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { validatePlugin } from "./validate-plugin.js";
|
|
4
|
+
function relativeToTarget(targetPath, candidatePath) {
|
|
5
|
+
return path.relative(targetPath, candidatePath).replace(/\\/g, "/");
|
|
6
|
+
}
|
|
7
|
+
export async function buildFixPlan(targetPath) {
|
|
8
|
+
const result = await validatePlugin(targetPath);
|
|
9
|
+
const rootPath = result.targetPath;
|
|
10
|
+
const actions = [];
|
|
11
|
+
const findingIds = new Set(result.findings.map((finding) => finding.id));
|
|
12
|
+
const manifestPath = path.join(rootPath, ".codex-plugin", "plugin.json");
|
|
13
|
+
if (findingIds.has("plugin.manifest.version.missing") ||
|
|
14
|
+
findingIds.has("plugin.manifest.description.missing")) {
|
|
15
|
+
const fields = [
|
|
16
|
+
findingIds.has("plugin.manifest.version.missing") ? "`version`" : null,
|
|
17
|
+
findingIds.has("plugin.manifest.description.missing") ? "`description`" : null
|
|
18
|
+
].filter(Boolean);
|
|
19
|
+
actions.push({
|
|
20
|
+
id: "manifest.safe_defaults",
|
|
21
|
+
title: "Add missing safe manifest defaults",
|
|
22
|
+
targetPath: manifestPath,
|
|
23
|
+
operation: "update-json",
|
|
24
|
+
details: `Set ${fields.join(" and ")} in ${relativeToTarget(rootPath, manifestPath)}.`
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (findingIds.has("plugin.skills.path.missing")) {
|
|
28
|
+
actions.push({
|
|
29
|
+
id: "skills.create_directory",
|
|
30
|
+
title: "Create missing skills directory",
|
|
31
|
+
targetPath: path.join(rootPath, "skills"),
|
|
32
|
+
operation: "mkdir",
|
|
33
|
+
details: "Create the skills directory referenced by the manifest."
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
targetPath: rootPath,
|
|
38
|
+
actions
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function renderFixPlan(plan, mode) {
|
|
42
|
+
const lines = [
|
|
43
|
+
"Fix Plan",
|
|
44
|
+
"========",
|
|
45
|
+
`Mode: ${mode}`,
|
|
46
|
+
`Target: ${plan.targetPath}`
|
|
47
|
+
];
|
|
48
|
+
if (plan.actions.length === 0) {
|
|
49
|
+
lines.push("", "No safe automatic fixes available.");
|
|
50
|
+
return lines.join("\n");
|
|
51
|
+
}
|
|
52
|
+
lines.push("", "Actions");
|
|
53
|
+
lines.push("-------");
|
|
54
|
+
plan.actions.forEach((action, index) => {
|
|
55
|
+
lines.push(`${index + 1}. ${action.title}`);
|
|
56
|
+
lines.push(` Path: ${relativeToTarget(plan.targetPath, action.targetPath)}`);
|
|
57
|
+
lines.push(` Operation: ${action.operation}`);
|
|
58
|
+
lines.push(` Details: ${action.details}`);
|
|
59
|
+
});
|
|
60
|
+
lines.push("", "No files changed.");
|
|
61
|
+
return lines.join("\n");
|
|
62
|
+
}
|
|
63
|
+
function timestampForPath() {
|
|
64
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
65
|
+
}
|
|
66
|
+
function defaultManifestDescription() {
|
|
67
|
+
return "Codex plugin package.";
|
|
68
|
+
}
|
|
69
|
+
async function backupFile(rootPath, backupDirectory, filePath) {
|
|
70
|
+
const relativePath = path.relative(rootPath, filePath);
|
|
71
|
+
const backupPath = path.join(backupDirectory, relativePath);
|
|
72
|
+
await mkdir(path.dirname(backupPath), { recursive: true });
|
|
73
|
+
await copyFile(filePath, backupPath);
|
|
74
|
+
}
|
|
75
|
+
export async function applyFixPlan(targetPath) {
|
|
76
|
+
const plan = await buildFixPlan(targetPath);
|
|
77
|
+
const backupDirectory = path.join(plan.targetPath, ".codex-doctor-backups", timestampForPath());
|
|
78
|
+
let filesChanged = 0;
|
|
79
|
+
for (const action of plan.actions) {
|
|
80
|
+
if (action.operation === "update-json" && action.id === "manifest.safe_defaults") {
|
|
81
|
+
await backupFile(plan.targetPath, backupDirectory, action.targetPath);
|
|
82
|
+
const manifest = JSON.parse(await readFile(action.targetPath, "utf8"));
|
|
83
|
+
if (typeof manifest.version !== "string" || manifest.version.trim() === "") {
|
|
84
|
+
manifest.version = "0.1.0";
|
|
85
|
+
}
|
|
86
|
+
if (typeof manifest.description !== "string" ||
|
|
87
|
+
manifest.description.trim() === "") {
|
|
88
|
+
manifest.description = defaultManifestDescription();
|
|
89
|
+
}
|
|
90
|
+
await writeFile(action.targetPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
91
|
+
filesChanged += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (action.operation === "mkdir" && action.id === "skills.create_directory") {
|
|
95
|
+
await mkdir(action.targetPath, { recursive: true });
|
|
96
|
+
filesChanged += 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
plan,
|
|
101
|
+
filesChanged,
|
|
102
|
+
backupDirectory
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export function renderApplyFixResult(result) {
|
|
106
|
+
return [
|
|
107
|
+
"Fix Plan",
|
|
108
|
+
"========",
|
|
109
|
+
"Mode: apply",
|
|
110
|
+
`Target: ${result.plan.targetPath}`,
|
|
111
|
+
`Files changed: ${result.filesChanged}`,
|
|
112
|
+
`Backup: ${relativeToTarget(result.plan.targetPath, result.backupDirectory)}`
|
|
113
|
+
].join("\n");
|
|
114
|
+
}
|
package/dist/run-cli.js
CHANGED
|
@@ -7,7 +7,10 @@ import { buildCompatibilityMatrix, matrixExitCode } from "./compatibility/compat
|
|
|
7
7
|
import { applyInstallPreview, renderApplyInstallResult } from "./compatibility/apply-install-preview.js";
|
|
8
8
|
import { buildClaudeDesktopInstallPreview, renderClaudeDesktopInstallPreview } from "./compatibility/claude-desktop-install-preview.js";
|
|
9
9
|
import { buildCursorInstallPreview, renderCursorInstallPreview } from "./compatibility/cursor-install-preview.js";
|
|
10
|
+
import { buildClineInstallPreview, renderClineInstallPreview } from "./compatibility/cline-install-preview.js";
|
|
10
11
|
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";
|
|
11
14
|
import { initPluginPackage } from "./core/init-plugin.js";
|
|
12
15
|
import { runCheck } from "./index.js";
|
|
13
16
|
import { renderInstalledSummary } from "./reporting/render-installed-summary.js";
|
|
@@ -34,7 +37,7 @@ const defaultIo = {
|
|
|
34
37
|
}
|
|
35
38
|
};
|
|
36
39
|
function printUsage(io) {
|
|
37
|
-
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 history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version");
|
|
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");
|
|
38
41
|
}
|
|
39
42
|
function renderInstalledPlugins(plugins) {
|
|
40
43
|
const lines = [
|
|
@@ -60,8 +63,27 @@ const compatibilityClientAliases = {
|
|
|
60
63
|
mcp: "Generic MCP",
|
|
61
64
|
"claude-desktop": "Claude Desktop",
|
|
62
65
|
claude: "Claude Desktop",
|
|
63
|
-
cursor: "Cursor"
|
|
66
|
+
cursor: "Cursor",
|
|
67
|
+
cline: "Cline"
|
|
64
68
|
};
|
|
69
|
+
const checkProfiles = ["ci", "strict", "publish"];
|
|
70
|
+
function parseCheckProfile(value) {
|
|
71
|
+
if (!value) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return checkProfiles.includes(value)
|
|
75
|
+
? value
|
|
76
|
+
: null;
|
|
77
|
+
}
|
|
78
|
+
function applyCheckProfile(config, profile) {
|
|
79
|
+
if (profile === "strict" || profile === "publish") {
|
|
80
|
+
return {
|
|
81
|
+
...config,
|
|
82
|
+
failOnWarnings: true
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return config;
|
|
86
|
+
}
|
|
65
87
|
function filterCompatibilityMatrix(matrix, clientFilter) {
|
|
66
88
|
const client = compatibilityClientAliases[clientFilter.toLowerCase()];
|
|
67
89
|
if (!client) {
|
|
@@ -107,6 +129,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
107
129
|
io.writeStdout(renderInstalledPlugins(installedPlugins));
|
|
108
130
|
return 0;
|
|
109
131
|
}
|
|
132
|
+
if (command === "doctor") {
|
|
133
|
+
io.writeStdout(await renderEnvironmentDoctor(terminalContext));
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
110
136
|
if (command === "explain") {
|
|
111
137
|
if (!maybePath || maybePath.startsWith("--")) {
|
|
112
138
|
io.writeStderr("Missing finding id. Usage: codex-plugin-doctor explain <finding-id>");
|
|
@@ -169,6 +195,27 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
169
195
|
].join("\n"));
|
|
170
196
|
return 0;
|
|
171
197
|
}
|
|
198
|
+
if (command === "fix") {
|
|
199
|
+
if (!maybePath || maybePath.startsWith("--")) {
|
|
200
|
+
io.writeStderr("Missing target path. Usage: codex-plugin-doctor fix <path> (--dry-run|--apply --backup)");
|
|
201
|
+
return 2;
|
|
202
|
+
}
|
|
203
|
+
const dryRun = remainingArgs.includes("--dry-run");
|
|
204
|
+
const apply = remainingArgs.includes("--apply");
|
|
205
|
+
const backup = remainingArgs.includes("--backup");
|
|
206
|
+
if (apply && !backup) {
|
|
207
|
+
io.writeStderr("Fix apply requires --backup.");
|
|
208
|
+
return 2;
|
|
209
|
+
}
|
|
210
|
+
if (dryRun === apply) {
|
|
211
|
+
io.writeStderr("Choose exactly one fix mode: --dry-run or --apply --backup.");
|
|
212
|
+
return 2;
|
|
213
|
+
}
|
|
214
|
+
io.writeStdout(dryRun
|
|
215
|
+
? renderFixPlan(await buildFixPlan(maybePath), "dry-run")
|
|
216
|
+
: renderApplyFixResult(await applyFixPlan(maybePath)));
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
172
219
|
if (command === "compat") {
|
|
173
220
|
const targetPath = maybePath && !maybePath.startsWith("--") ? maybePath : ".";
|
|
174
221
|
const compatFlags = maybePath && maybePath.startsWith("--")
|
|
@@ -193,8 +240,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
193
240
|
}
|
|
194
241
|
if ((installPreview || applyInstall) &&
|
|
195
242
|
clientFilter?.toLowerCase() !== "claude-desktop" &&
|
|
196
|
-
clientFilter?.toLowerCase() !== "cursor"
|
|
197
|
-
|
|
243
|
+
clientFilter?.toLowerCase() !== "cursor" &&
|
|
244
|
+
clientFilter?.toLowerCase() !== "cline") {
|
|
245
|
+
io.writeStderr("--install-preview and --apply require --client claude-desktop, cursor, or cline.");
|
|
198
246
|
return 2;
|
|
199
247
|
}
|
|
200
248
|
if (installPreview && applyInstall) {
|
|
@@ -207,20 +255,32 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
207
255
|
}
|
|
208
256
|
if (installPreview || applyInstall) {
|
|
209
257
|
try {
|
|
210
|
-
const
|
|
258
|
+
const normalizedClient = clientFilter?.toLowerCase();
|
|
259
|
+
const preview = normalizedClient === "cursor"
|
|
211
260
|
? await buildCursorInstallPreview(targetPath, {
|
|
212
261
|
env: terminalContext.env,
|
|
213
262
|
platform: terminalContext.platform
|
|
214
263
|
})
|
|
215
|
-
:
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
264
|
+
: normalizedClient === "cline"
|
|
265
|
+
? await buildClineInstallPreview(targetPath, {
|
|
266
|
+
env: terminalContext.env,
|
|
267
|
+
platform: terminalContext.platform
|
|
268
|
+
})
|
|
269
|
+
: await buildClaudeDesktopInstallPreview(targetPath, {
|
|
270
|
+
env: terminalContext.env,
|
|
271
|
+
platform: terminalContext.platform
|
|
272
|
+
});
|
|
219
273
|
const report = applyInstall
|
|
220
|
-
? renderApplyInstallResult(await applyInstallPreview(
|
|
221
|
-
|
|
274
|
+
? renderApplyInstallResult(await applyInstallPreview(normalizedClient === "cursor"
|
|
275
|
+
? "Cursor"
|
|
276
|
+
: normalizedClient === "cline"
|
|
277
|
+
? "Cline"
|
|
278
|
+
: "Claude Desktop", preview))
|
|
279
|
+
: normalizedClient === "cursor"
|
|
222
280
|
? renderCursorInstallPreview(preview)
|
|
223
|
-
:
|
|
281
|
+
: normalizedClient === "cline"
|
|
282
|
+
? renderClineInstallPreview(preview)
|
|
283
|
+
: renderClaudeDesktopInstallPreview(preview);
|
|
224
284
|
if (outputPath) {
|
|
225
285
|
await writeFile(outputPath, report, "utf8");
|
|
226
286
|
}
|
|
@@ -287,6 +347,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
287
347
|
const outputPath = outputIndex === -1 ? null : normalizedFlags[outputIndex + 1];
|
|
288
348
|
const configIndex = normalizedFlags.indexOf("--config");
|
|
289
349
|
const configPath = configIndex === -1 ? null : normalizedFlags[configIndex + 1];
|
|
350
|
+
const profileIndex = normalizedFlags.indexOf("--profile");
|
|
351
|
+
const profileName = profileIndex === -1 ? null : normalizedFlags[profileIndex + 1];
|
|
352
|
+
const checkProfile = parseCheckProfile(profileName);
|
|
290
353
|
const historyIndex = normalizedFlags.indexOf("--history");
|
|
291
354
|
const historyPath = historyIndex === -1 ? null : normalizedFlags[historyIndex + 1];
|
|
292
355
|
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
@@ -297,6 +360,14 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
297
360
|
io.writeStderr("Missing path after --config.");
|
|
298
361
|
return 2;
|
|
299
362
|
}
|
|
363
|
+
if (profileIndex !== -1 && (!profileName || profileName.startsWith("--"))) {
|
|
364
|
+
io.writeStderr("Missing profile after --profile.");
|
|
365
|
+
return 2;
|
|
366
|
+
}
|
|
367
|
+
if (profileIndex !== -1 && !checkProfile) {
|
|
368
|
+
io.writeStderr("Unknown profile. Supported profiles: ci, strict, publish.");
|
|
369
|
+
return 2;
|
|
370
|
+
}
|
|
300
371
|
if (historyIndex !== -1 && (!historyPath || historyPath.startsWith("--"))) {
|
|
301
372
|
io.writeStderr("Missing path after --history.");
|
|
302
373
|
return 2;
|
|
@@ -309,6 +380,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
309
380
|
io.writeStderr("History output requires a single package target.");
|
|
310
381
|
return 2;
|
|
311
382
|
}
|
|
383
|
+
const effectiveRuntimeProbeEnabled = runtimeProbeEnabled || checkProfile === "publish";
|
|
312
384
|
const outputPolicy = determineOutputPolicy({
|
|
313
385
|
jsonOutput: jsonOutput || badgeJsonOutput,
|
|
314
386
|
markdownOutput: markdownOutput || badgeMarkdownOutput,
|
|
@@ -334,11 +406,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
334
406
|
checkedPlugins.push({
|
|
335
407
|
plugin,
|
|
336
408
|
result: applyDoctorConfig(await runCheckImpl(plugin.rootPath, {
|
|
337
|
-
runtime:
|
|
338
|
-
runtimeTranscript:
|
|
409
|
+
runtime: effectiveRuntimeProbeEnabled,
|
|
410
|
+
runtimeTranscript: effectiveRuntimeProbeEnabled && verboseRuntime
|
|
339
411
|
? (line) => io.writeStderr(line)
|
|
340
412
|
: undefined
|
|
341
|
-
}), config)
|
|
413
|
+
}), applyCheckProfile(config, checkProfile))
|
|
342
414
|
});
|
|
343
415
|
}
|
|
344
416
|
const report = installedSummary
|
|
@@ -347,9 +419,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
347
419
|
.map((item) => sarifOutput
|
|
348
420
|
? renderSarifReport(item.result)
|
|
349
421
|
: markdownOutput
|
|
350
|
-
? buildMarkdownReport(item.result, { runtimeProbeEnabled })
|
|
422
|
+
? buildMarkdownReport(item.result, { runtimeProbeEnabled: effectiveRuntimeProbeEnabled })
|
|
351
423
|
: jsonOutput
|
|
352
|
-
? renderJsonReport(item.result, { runtimeProbeEnabled })
|
|
424
|
+
? renderJsonReport(item.result, { runtimeProbeEnabled: effectiveRuntimeProbeEnabled })
|
|
353
425
|
: renderTextReport(item.result, { ascii: outputPolicy.style === "ascii" }))
|
|
354
426
|
.join("\n\n");
|
|
355
427
|
if (outputPath) {
|
|
@@ -364,11 +436,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
364
436
|
: null;
|
|
365
437
|
renderer?.start("Validating package");
|
|
366
438
|
const result = applyDoctorConfig(await runCheckImpl(targetPath, {
|
|
367
|
-
runtime:
|
|
368
|
-
runtimeTranscript:
|
|
439
|
+
runtime: effectiveRuntimeProbeEnabled,
|
|
440
|
+
runtimeTranscript: effectiveRuntimeProbeEnabled && verboseRuntime
|
|
369
441
|
? (line) => io.writeStderr(line)
|
|
370
442
|
: undefined
|
|
371
|
-
}), await loadDoctorConfig(targetPath, configPath));
|
|
443
|
+
}), applyCheckProfile(await loadDoctorConfig(targetPath, configPath), checkProfile));
|
|
372
444
|
if (renderer) {
|
|
373
445
|
if (result.status === "fail") {
|
|
374
446
|
renderer.stopFailure("Validation failed");
|
|
@@ -378,11 +450,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
378
450
|
}
|
|
379
451
|
}
|
|
380
452
|
const report = markdownOutput
|
|
381
|
-
? buildMarkdownReport(result, { runtimeProbeEnabled })
|
|
453
|
+
? buildMarkdownReport(result, { runtimeProbeEnabled: effectiveRuntimeProbeEnabled })
|
|
382
454
|
: sarifOutput
|
|
383
455
|
? renderSarifReport(result)
|
|
384
456
|
: jsonOutput
|
|
385
|
-
? renderJsonReport(result, { runtimeProbeEnabled })
|
|
457
|
+
? renderJsonReport(result, { runtimeProbeEnabled: effectiveRuntimeProbeEnabled })
|
|
386
458
|
: badgeJsonOutput
|
|
387
459
|
? renderBadgeJson(result)
|
|
388
460
|
: badgeMarkdownOutput
|
|
@@ -392,7 +464,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
392
464
|
await writeFile(outputPath, report, "utf8");
|
|
393
465
|
}
|
|
394
466
|
if (historyPath) {
|
|
395
|
-
await appendValidationHistoryEntry(historyPath, result, {
|
|
467
|
+
await appendValidationHistoryEntry(historyPath, result, {
|
|
468
|
+
runtimeProbeEnabled: effectiveRuntimeProbeEnabled
|
|
469
|
+
});
|
|
396
470
|
}
|
|
397
471
|
io.writeStdout(report);
|
|
398
472
|
return result.exitCode;
|
package/package.json
CHANGED