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 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 init my-plugin
167
- codex-plugin-doctor compat .
168
- codex-plugin-doctor compat . --client codex
169
- codex-plugin-doctor compat . --client generic-mcp
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 . --scorecard
177
- codex-plugin-doctor compat . --json
178
- codex-plugin-doctor compat . --json --output compatibility.json
179
- codex-plugin-doctor check .
180
- codex-plugin-doctor check . --json
181
- codex-plugin-doctor check . --json --output report.json
182
- codex-plugin-doctor check . --markdown --output report.md
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
- `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.
200
-
201
- `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.
202
-
203
- `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`.
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
- Optional local policy file:
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.6.0
258
+ - uses: Esquetta/CodexPluginDoctor@v0.7.0
243
259
  with:
244
- version: "0.6.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,2 @@
1
+ import type { CliTerminalContext } from "../run-cli.js";
2
+ export declare function renderEnvironmentDoctor(terminalContext: CliTerminalContext): Promise<string>;
@@ -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
- io.writeStderr("--install-preview and --apply require --client claude-desktop or --client cursor.");
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 preview = clientFilter?.toLowerCase() === "cursor"
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
- : await buildClaudeDesktopInstallPreview(targetPath, {
216
- env: terminalContext.env,
217
- platform: terminalContext.platform
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(clientFilter?.toLowerCase() === "cursor" ? "Cursor" : "Claude Desktop", preview))
221
- : clientFilter?.toLowerCase() === "cursor"
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
- : renderClaudeDesktopInstallPreview(preview);
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: runtimeProbeEnabled,
338
- runtimeTranscript: runtimeProbeEnabled && verboseRuntime
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: runtimeProbeEnabled,
368
- runtimeTranscript: runtimeProbeEnabled && verboseRuntime
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, { runtimeProbeEnabled });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "0.6.0",
3
+ "version": "0.7.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",