codex-plugin-doctor 0.5.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 +61 -38
- 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/core/validation-history.d.ts +33 -0
- package/dist/core/validation-history.js +64 -0
- package/dist/reporting/render-history-summary.d.ts +2 -0
- package/dist/reporting/render-history-summary.js +23 -0
- package/dist/run-cli.js +136 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,8 +65,9 @@ Output formats:
|
|
|
65
65
|
- human text output
|
|
66
66
|
- JSON reports
|
|
67
67
|
- Markdown reports
|
|
68
|
-
- Shields-compatible badge JSON and static badge Markdown
|
|
69
|
-
-
|
|
68
|
+
- Shields-compatible badge JSON and static badge Markdown
|
|
69
|
+
- validation history JSONL and trend summaries
|
|
70
|
+
- `--output` file writing
|
|
70
71
|
- CI summary and artifact generation
|
|
71
72
|
|
|
72
73
|
## Quick Start
|
|
@@ -161,45 +162,67 @@ Run these from a Codex plugin package root:
|
|
|
161
162
|
|
|
162
163
|
```bash
|
|
163
164
|
codex-plugin-doctor --version
|
|
164
|
-
codex-plugin-doctor self-test
|
|
165
|
-
codex-plugin-doctor
|
|
166
|
-
codex-plugin-doctor
|
|
167
|
-
codex-plugin-doctor compat .
|
|
168
|
-
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
|
|
169
171
|
codex-plugin-doctor compat . --client claude-desktop
|
|
170
172
|
codex-plugin-doctor compat . --client claude-desktop --install-preview
|
|
171
173
|
codex-plugin-doctor compat . --client claude-desktop --apply --backup
|
|
172
|
-
codex-plugin-doctor compat . --client cursor
|
|
173
|
-
codex-plugin-doctor compat . --client cursor --install-preview
|
|
174
|
-
codex-plugin-doctor compat . --client cursor --apply --backup
|
|
175
|
-
codex-plugin-doctor compat . --
|
|
176
|
-
codex-plugin-doctor compat . --
|
|
177
|
-
codex-plugin-doctor compat . --
|
|
178
|
-
codex-plugin-doctor
|
|
179
|
-
codex-plugin-doctor
|
|
180
|
-
codex-plugin-doctor check .
|
|
181
|
-
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
|
|
182
189
|
codex-plugin-doctor check . --badge-json --output doctor-badge.json
|
|
183
190
|
codex-plugin-doctor check . --badge-markdown
|
|
184
191
|
codex-plugin-doctor check . --sarif --output results.sarif
|
|
185
192
|
codex-plugin-doctor check . --ascii
|
|
186
193
|
codex-plugin-doctor check . --no-animations
|
|
187
|
-
codex-plugin-doctor check . --runtime
|
|
188
|
-
codex-plugin-doctor check . --config .codex-doctor.json
|
|
189
|
-
codex-plugin-doctor check . --
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
`
|
|
199
|
-
|
|
200
|
-
`
|
|
201
|
-
|
|
202
|
-
|
|
194
|
+
codex-plugin-doctor check . --runtime
|
|
195
|
+
codex-plugin-doctor check . --config .codex-doctor.json
|
|
196
|
+
codex-plugin-doctor check . --history validation-history.jsonl
|
|
197
|
+
codex-plugin-doctor history validation-history.jsonl
|
|
198
|
+
codex-plugin-doctor history validation-history.jsonl --json
|
|
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
|
|
202
|
+
codex-plugin-doctor check . --json --runtime --verbose-runtime
|
|
203
|
+
```
|
|
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
|
+
|
|
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`.
|
|
220
|
+
|
|
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.
|
|
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:
|
|
203
226
|
|
|
204
227
|
```json
|
|
205
228
|
{
|
|
@@ -232,11 +255,11 @@ jobs:
|
|
|
232
255
|
runs-on: ubuntu-latest
|
|
233
256
|
steps:
|
|
234
257
|
- uses: actions/checkout@v4
|
|
235
|
-
- uses: Esquetta/CodexPluginDoctor@v0.
|
|
236
|
-
with:
|
|
237
|
-
version: "0.
|
|
238
|
-
path: .
|
|
239
|
-
runtime: "false"
|
|
258
|
+
- uses: Esquetta/CodexPluginDoctor@v0.7.0
|
|
259
|
+
with:
|
|
260
|
+
version: "0.7.0"
|
|
261
|
+
path: .
|
|
262
|
+
runtime: "false"
|
|
240
263
|
```
|
|
241
264
|
|
|
242
265
|
For runtime probing, SARIF output, installed plugin cache checks, and pinned release examples, see [GitHub Action Usage](./docs/engineering/github-action-usage.md).
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { CheckResult } from "../domain/types.js";
|
|
2
|
+
export interface ValidationHistoryEntry {
|
|
3
|
+
schemaVersion: "1.0.0";
|
|
4
|
+
generatedAt: string;
|
|
5
|
+
targetPath: string;
|
|
6
|
+
status: CheckResult["status"];
|
|
7
|
+
runtimeProbeEnabled: boolean;
|
|
8
|
+
findingCounts: {
|
|
9
|
+
fail: number;
|
|
10
|
+
warn: number;
|
|
11
|
+
total: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface ValidationHistorySummary {
|
|
15
|
+
schemaVersion: "1.0.0";
|
|
16
|
+
runs: number;
|
|
17
|
+
latest: ValidationHistoryEntry;
|
|
18
|
+
previous: ValidationHistoryEntry | null;
|
|
19
|
+
delta: {
|
|
20
|
+
fail: number;
|
|
21
|
+
warn: number;
|
|
22
|
+
total: number;
|
|
23
|
+
};
|
|
24
|
+
regression: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function buildValidationHistoryEntry(result: CheckResult, options: {
|
|
27
|
+
runtimeProbeEnabled: boolean;
|
|
28
|
+
}): ValidationHistoryEntry;
|
|
29
|
+
export declare function appendValidationHistoryEntry(historyPath: string, result: CheckResult, options: {
|
|
30
|
+
runtimeProbeEnabled: boolean;
|
|
31
|
+
}): Promise<void>;
|
|
32
|
+
export declare function readValidationHistory(historyPath: string): Promise<ValidationHistoryEntry[]>;
|
|
33
|
+
export declare function summarizeValidationHistory(entries: ValidationHistoryEntry[]): ValidationHistorySummary;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const statusRank = {
|
|
4
|
+
pass: 0,
|
|
5
|
+
warn: 1,
|
|
6
|
+
fail: 2
|
|
7
|
+
};
|
|
8
|
+
function countFindings(result) {
|
|
9
|
+
return {
|
|
10
|
+
fail: result.findings.filter((finding) => finding.severity === "fail").length,
|
|
11
|
+
warn: result.findings.filter((finding) => finding.severity === "warn").length,
|
|
12
|
+
total: result.findings.length
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function buildValidationHistoryEntry(result, options) {
|
|
16
|
+
return {
|
|
17
|
+
schemaVersion: "1.0.0",
|
|
18
|
+
generatedAt: new Date().toISOString(),
|
|
19
|
+
targetPath: result.targetPath,
|
|
20
|
+
status: result.status,
|
|
21
|
+
runtimeProbeEnabled: options.runtimeProbeEnabled,
|
|
22
|
+
findingCounts: countFindings(result)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export async function appendValidationHistoryEntry(historyPath, result, options) {
|
|
26
|
+
const absoluteHistoryPath = path.resolve(historyPath);
|
|
27
|
+
await mkdir(path.dirname(absoluteHistoryPath), { recursive: true });
|
|
28
|
+
await writeFile(absoluteHistoryPath, `${JSON.stringify(buildValidationHistoryEntry(result, options))}\n`, { encoding: "utf8", flag: "a" });
|
|
29
|
+
}
|
|
30
|
+
export async function readValidationHistory(historyPath) {
|
|
31
|
+
const content = await readFile(path.resolve(historyPath), "utf8");
|
|
32
|
+
return content
|
|
33
|
+
.split(/\r?\n/)
|
|
34
|
+
.map((line) => line.trim())
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.map((line) => JSON.parse(line));
|
|
37
|
+
}
|
|
38
|
+
export function summarizeValidationHistory(entries) {
|
|
39
|
+
if (entries.length === 0) {
|
|
40
|
+
throw new Error("No validation history entries found.");
|
|
41
|
+
}
|
|
42
|
+
const latest = entries[entries.length - 1];
|
|
43
|
+
const previous = entries.length > 1 ? entries[entries.length - 2] : null;
|
|
44
|
+
const delta = previous
|
|
45
|
+
? {
|
|
46
|
+
fail: latest.findingCounts.fail - previous.findingCounts.fail,
|
|
47
|
+
warn: latest.findingCounts.warn - previous.findingCounts.warn,
|
|
48
|
+
total: latest.findingCounts.total - previous.findingCounts.total
|
|
49
|
+
}
|
|
50
|
+
: { fail: 0, warn: 0, total: 0 };
|
|
51
|
+
const regression = previous
|
|
52
|
+
? statusRank[latest.status] > statusRank[previous.status]
|
|
53
|
+
|| delta.fail > 0
|
|
54
|
+
|| delta.warn > 0
|
|
55
|
+
: false;
|
|
56
|
+
return {
|
|
57
|
+
schemaVersion: "1.0.0",
|
|
58
|
+
runs: entries.length,
|
|
59
|
+
latest,
|
|
60
|
+
previous,
|
|
61
|
+
delta,
|
|
62
|
+
regression
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { summarizeValidationHistory } from "../core/validation-history.js";
|
|
2
|
+
function formatDelta(value) {
|
|
3
|
+
return value > 0 ? `+${value}` : String(value);
|
|
4
|
+
}
|
|
5
|
+
export function renderHistorySummary(entries) {
|
|
6
|
+
const summary = summarizeValidationHistory(entries);
|
|
7
|
+
const { latest, previous } = summary;
|
|
8
|
+
const lines = [
|
|
9
|
+
"Validation History",
|
|
10
|
+
"==================",
|
|
11
|
+
`Runs: ${summary.runs}`,
|
|
12
|
+
`Latest: ${latest.status.toUpperCase()}`,
|
|
13
|
+
`Target: ${latest.targetPath}`,
|
|
14
|
+
`Generated: ${latest.generatedAt}`,
|
|
15
|
+
`Fail findings: ${latest.findingCounts.fail}`,
|
|
16
|
+
`Warn findings: ${latest.findingCounts.warn}`,
|
|
17
|
+
`Regression: ${summary.regression ? "YES" : "NO"}`
|
|
18
|
+
];
|
|
19
|
+
if (previous) {
|
|
20
|
+
lines.push("", `Previous: ${previous.status.toUpperCase()}`, `Fail findings: ${formatDelta(summary.delta.fail)}`, `Warn findings: ${formatDelta(summary.delta.warn)}`);
|
|
21
|
+
}
|
|
22
|
+
return lines.join("\n");
|
|
23
|
+
}
|
package/dist/run-cli.js
CHANGED
|
@@ -2,17 +2,22 @@ import { writeFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { discoverInstalledPlugins, filterInstalledPlugins } from "./core/discover-installed-plugins.js";
|
|
5
|
+
import { appendValidationHistoryEntry, readValidationHistory, summarizeValidationHistory } from "./core/validation-history.js";
|
|
5
6
|
import { buildCompatibilityMatrix, matrixExitCode } from "./compatibility/compatibility-matrix.js";
|
|
6
7
|
import { applyInstallPreview, renderApplyInstallResult } from "./compatibility/apply-install-preview.js";
|
|
7
8
|
import { buildClaudeDesktopInstallPreview, renderClaudeDesktopInstallPreview } from "./compatibility/claude-desktop-install-preview.js";
|
|
8
9
|
import { buildCursorInstallPreview, renderCursorInstallPreview } from "./compatibility/cursor-install-preview.js";
|
|
10
|
+
import { buildClineInstallPreview, renderClineInstallPreview } from "./compatibility/cline-install-preview.js";
|
|
9
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";
|
|
10
14
|
import { initPluginPackage } from "./core/init-plugin.js";
|
|
11
15
|
import { runCheck } from "./index.js";
|
|
12
16
|
import { renderInstalledSummary } from "./reporting/render-installed-summary.js";
|
|
13
17
|
import { renderBadgeJson, renderBadgeMarkdown } from "./reporting/render-badge-report.js";
|
|
14
18
|
import { renderCompatibilityScorecard } from "./reporting/render-compatibility-scorecard.js";
|
|
15
19
|
import { renderCompatibilityReport } from "./reporting/render-compatibility-report.js";
|
|
20
|
+
import { renderHistorySummary } from "./reporting/render-history-summary.js";
|
|
16
21
|
import { renderJsonReport } from "./reporting/render-json-report.js";
|
|
17
22
|
import { buildMarkdownReport } from "./reporting/render-markdown-report.js";
|
|
18
23
|
import { renderRuleExplanation } from "./reporting/render-rule-explanation.js";
|
|
@@ -32,7 +37,7 @@ const defaultIo = {
|
|
|
32
37
|
}
|
|
33
38
|
};
|
|
34
39
|
function printUsage(io) {
|
|
35
|
-
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--json|--markdown|--badge-json|--badge-markdown] [--output <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 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");
|
|
36
41
|
}
|
|
37
42
|
function renderInstalledPlugins(plugins) {
|
|
38
43
|
const lines = [
|
|
@@ -58,8 +63,27 @@ const compatibilityClientAliases = {
|
|
|
58
63
|
mcp: "Generic MCP",
|
|
59
64
|
"claude-desktop": "Claude Desktop",
|
|
60
65
|
claude: "Claude Desktop",
|
|
61
|
-
cursor: "Cursor"
|
|
66
|
+
cursor: "Cursor",
|
|
67
|
+
cline: "Cline"
|
|
62
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
|
+
}
|
|
63
87
|
function filterCompatibilityMatrix(matrix, clientFilter) {
|
|
64
88
|
const client = compatibilityClientAliases[clientFilter.toLowerCase()];
|
|
65
89
|
if (!client) {
|
|
@@ -105,6 +129,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
105
129
|
io.writeStdout(renderInstalledPlugins(installedPlugins));
|
|
106
130
|
return 0;
|
|
107
131
|
}
|
|
132
|
+
if (command === "doctor") {
|
|
133
|
+
io.writeStdout(await renderEnvironmentDoctor(terminalContext));
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
108
136
|
if (command === "explain") {
|
|
109
137
|
if (!maybePath || maybePath.startsWith("--")) {
|
|
110
138
|
io.writeStderr("Missing finding id. Usage: codex-plugin-doctor explain <finding-id>");
|
|
@@ -118,6 +146,31 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
118
146
|
io.writeStdout(renderRuleExplanation(rule));
|
|
119
147
|
return 0;
|
|
120
148
|
}
|
|
149
|
+
if (command === "history") {
|
|
150
|
+
if (!maybePath || maybePath.startsWith("--")) {
|
|
151
|
+
io.writeStderr("Missing history path. Usage: codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]");
|
|
152
|
+
return 2;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const entries = await readValidationHistory(maybePath);
|
|
156
|
+
const summary = summarizeValidationHistory(entries);
|
|
157
|
+
const jsonOutput = remainingArgs.includes("--json");
|
|
158
|
+
const failOnRegression = remainingArgs.includes("--fail-on-regression");
|
|
159
|
+
io.writeStdout(jsonOutput
|
|
160
|
+
? JSON.stringify(summary, null, 2)
|
|
161
|
+
: renderHistorySummary(entries));
|
|
162
|
+
if (failOnRegression && summary.regression) {
|
|
163
|
+
io.writeStderr("Validation history regression detected.");
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
const message = error instanceof Error ? error.message : "Unable to read validation history.";
|
|
170
|
+
io.writeStderr(message);
|
|
171
|
+
return 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
121
174
|
if (command === "self-test" || command === "demo") {
|
|
122
175
|
const targetPath = resolveBundledSelfTestTarget();
|
|
123
176
|
const runCheckImpl = options.runCheckImpl ?? runCheck;
|
|
@@ -142,6 +195,27 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
142
195
|
].join("\n"));
|
|
143
196
|
return 0;
|
|
144
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
|
+
}
|
|
145
219
|
if (command === "compat") {
|
|
146
220
|
const targetPath = maybePath && !maybePath.startsWith("--") ? maybePath : ".";
|
|
147
221
|
const compatFlags = maybePath && maybePath.startsWith("--")
|
|
@@ -166,8 +240,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
166
240
|
}
|
|
167
241
|
if ((installPreview || applyInstall) &&
|
|
168
242
|
clientFilter?.toLowerCase() !== "claude-desktop" &&
|
|
169
|
-
clientFilter?.toLowerCase() !== "cursor"
|
|
170
|
-
|
|
243
|
+
clientFilter?.toLowerCase() !== "cursor" &&
|
|
244
|
+
clientFilter?.toLowerCase() !== "cline") {
|
|
245
|
+
io.writeStderr("--install-preview and --apply require --client claude-desktop, cursor, or cline.");
|
|
171
246
|
return 2;
|
|
172
247
|
}
|
|
173
248
|
if (installPreview && applyInstall) {
|
|
@@ -180,20 +255,32 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
180
255
|
}
|
|
181
256
|
if (installPreview || applyInstall) {
|
|
182
257
|
try {
|
|
183
|
-
const
|
|
258
|
+
const normalizedClient = clientFilter?.toLowerCase();
|
|
259
|
+
const preview = normalizedClient === "cursor"
|
|
184
260
|
? await buildCursorInstallPreview(targetPath, {
|
|
185
261
|
env: terminalContext.env,
|
|
186
262
|
platform: terminalContext.platform
|
|
187
263
|
})
|
|
188
|
-
:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
+
});
|
|
192
273
|
const report = applyInstall
|
|
193
|
-
? renderApplyInstallResult(await applyInstallPreview(
|
|
194
|
-
|
|
274
|
+
? renderApplyInstallResult(await applyInstallPreview(normalizedClient === "cursor"
|
|
275
|
+
? "Cursor"
|
|
276
|
+
: normalizedClient === "cline"
|
|
277
|
+
? "Cline"
|
|
278
|
+
: "Claude Desktop", preview))
|
|
279
|
+
: normalizedClient === "cursor"
|
|
195
280
|
? renderCursorInstallPreview(preview)
|
|
196
|
-
:
|
|
281
|
+
: normalizedClient === "cline"
|
|
282
|
+
? renderClineInstallPreview(preview)
|
|
283
|
+
: renderClaudeDesktopInstallPreview(preview);
|
|
197
284
|
if (outputPath) {
|
|
198
285
|
await writeFile(outputPath, report, "utf8");
|
|
199
286
|
}
|
|
@@ -260,6 +347,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
260
347
|
const outputPath = outputIndex === -1 ? null : normalizedFlags[outputIndex + 1];
|
|
261
348
|
const configIndex = normalizedFlags.indexOf("--config");
|
|
262
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);
|
|
353
|
+
const historyIndex = normalizedFlags.indexOf("--history");
|
|
354
|
+
const historyPath = historyIndex === -1 ? null : normalizedFlags[historyIndex + 1];
|
|
263
355
|
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
264
356
|
io.writeStderr("Missing path after --output.");
|
|
265
357
|
return 2;
|
|
@@ -268,10 +360,27 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
268
360
|
io.writeStderr("Missing path after --config.");
|
|
269
361
|
return 2;
|
|
270
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
|
+
}
|
|
371
|
+
if (historyIndex !== -1 && (!historyPath || historyPath.startsWith("--"))) {
|
|
372
|
+
io.writeStderr("Missing path after --history.");
|
|
373
|
+
return 2;
|
|
374
|
+
}
|
|
271
375
|
if (checkInstalled && (badgeJsonOutput || badgeMarkdownOutput)) {
|
|
272
376
|
io.writeStderr("Badge output requires a single package target.");
|
|
273
377
|
return 2;
|
|
274
378
|
}
|
|
379
|
+
if (checkInstalled && historyPath) {
|
|
380
|
+
io.writeStderr("History output requires a single package target.");
|
|
381
|
+
return 2;
|
|
382
|
+
}
|
|
383
|
+
const effectiveRuntimeProbeEnabled = runtimeProbeEnabled || checkProfile === "publish";
|
|
275
384
|
const outputPolicy = determineOutputPolicy({
|
|
276
385
|
jsonOutput: jsonOutput || badgeJsonOutput,
|
|
277
386
|
markdownOutput: markdownOutput || badgeMarkdownOutput,
|
|
@@ -297,11 +406,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
297
406
|
checkedPlugins.push({
|
|
298
407
|
plugin,
|
|
299
408
|
result: applyDoctorConfig(await runCheckImpl(plugin.rootPath, {
|
|
300
|
-
runtime:
|
|
301
|
-
runtimeTranscript:
|
|
409
|
+
runtime: effectiveRuntimeProbeEnabled,
|
|
410
|
+
runtimeTranscript: effectiveRuntimeProbeEnabled && verboseRuntime
|
|
302
411
|
? (line) => io.writeStderr(line)
|
|
303
412
|
: undefined
|
|
304
|
-
}), config)
|
|
413
|
+
}), applyCheckProfile(config, checkProfile))
|
|
305
414
|
});
|
|
306
415
|
}
|
|
307
416
|
const report = installedSummary
|
|
@@ -310,9 +419,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
310
419
|
.map((item) => sarifOutput
|
|
311
420
|
? renderSarifReport(item.result)
|
|
312
421
|
: markdownOutput
|
|
313
|
-
? buildMarkdownReport(item.result, { runtimeProbeEnabled })
|
|
422
|
+
? buildMarkdownReport(item.result, { runtimeProbeEnabled: effectiveRuntimeProbeEnabled })
|
|
314
423
|
: jsonOutput
|
|
315
|
-
? renderJsonReport(item.result, { runtimeProbeEnabled })
|
|
424
|
+
? renderJsonReport(item.result, { runtimeProbeEnabled: effectiveRuntimeProbeEnabled })
|
|
316
425
|
: renderTextReport(item.result, { ascii: outputPolicy.style === "ascii" }))
|
|
317
426
|
.join("\n\n");
|
|
318
427
|
if (outputPath) {
|
|
@@ -327,11 +436,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
327
436
|
: null;
|
|
328
437
|
renderer?.start("Validating package");
|
|
329
438
|
const result = applyDoctorConfig(await runCheckImpl(targetPath, {
|
|
330
|
-
runtime:
|
|
331
|
-
runtimeTranscript:
|
|
439
|
+
runtime: effectiveRuntimeProbeEnabled,
|
|
440
|
+
runtimeTranscript: effectiveRuntimeProbeEnabled && verboseRuntime
|
|
332
441
|
? (line) => io.writeStderr(line)
|
|
333
442
|
: undefined
|
|
334
|
-
}), await loadDoctorConfig(targetPath, configPath));
|
|
443
|
+
}), applyCheckProfile(await loadDoctorConfig(targetPath, configPath), checkProfile));
|
|
335
444
|
if (renderer) {
|
|
336
445
|
if (result.status === "fail") {
|
|
337
446
|
renderer.stopFailure("Validation failed");
|
|
@@ -341,11 +450,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
341
450
|
}
|
|
342
451
|
}
|
|
343
452
|
const report = markdownOutput
|
|
344
|
-
? buildMarkdownReport(result, { runtimeProbeEnabled })
|
|
453
|
+
? buildMarkdownReport(result, { runtimeProbeEnabled: effectiveRuntimeProbeEnabled })
|
|
345
454
|
: sarifOutput
|
|
346
455
|
? renderSarifReport(result)
|
|
347
456
|
: jsonOutput
|
|
348
|
-
? renderJsonReport(result, { runtimeProbeEnabled })
|
|
457
|
+
? renderJsonReport(result, { runtimeProbeEnabled: effectiveRuntimeProbeEnabled })
|
|
349
458
|
: badgeJsonOutput
|
|
350
459
|
? renderBadgeJson(result)
|
|
351
460
|
: badgeMarkdownOutput
|
|
@@ -354,6 +463,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
354
463
|
if (outputPath) {
|
|
355
464
|
await writeFile(outputPath, report, "utf8");
|
|
356
465
|
}
|
|
466
|
+
if (historyPath) {
|
|
467
|
+
await appendValidationHistoryEntry(historyPath, result, {
|
|
468
|
+
runtimeProbeEnabled: effectiveRuntimeProbeEnabled
|
|
469
|
+
});
|
|
470
|
+
}
|
|
357
471
|
io.writeStdout(report);
|
|
358
472
|
return result.exitCode;
|
|
359
473
|
}
|
package/package.json
CHANGED