codex-plugin-doctor 0.2.1 → 0.3.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 +7 -0
- package/dist/compatibility/compatibility-matrix.d.ts +1 -0
- package/dist/compatibility/compatibility-matrix.js +86 -6
- package/dist/compatibility/cursor-install-preview.d.ts +10 -0
- package/dist/compatibility/cursor-install-preview.js +65 -0
- package/dist/reporting/render-compatibility-scorecard.d.ts +2 -0
- package/dist/reporting/render-compatibility-scorecard.js +21 -0
- package/dist/run-cli.js +18 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -162,6 +162,9 @@ codex-plugin-doctor compat . --client codex
|
|
|
162
162
|
codex-plugin-doctor compat . --client generic-mcp
|
|
163
163
|
codex-plugin-doctor compat . --client claude-desktop
|
|
164
164
|
codex-plugin-doctor compat . --client claude-desktop --install-preview
|
|
165
|
+
codex-plugin-doctor compat . --client cursor
|
|
166
|
+
codex-plugin-doctor compat . --client cursor --install-preview
|
|
167
|
+
codex-plugin-doctor compat . --scorecard
|
|
165
168
|
codex-plugin-doctor compat . --json
|
|
166
169
|
codex-plugin-doctor compat . --json --output compatibility.json
|
|
167
170
|
codex-plugin-doctor check .
|
|
@@ -178,6 +181,10 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
|
|
|
178
181
|
|
|
179
182
|
`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.
|
|
180
183
|
|
|
184
|
+
`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.
|
|
185
|
+
|
|
186
|
+
`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`.
|
|
187
|
+
|
|
181
188
|
Optional local policy file:
|
|
182
189
|
|
|
183
190
|
```json
|
|
@@ -16,5 +16,6 @@ export interface CompatibilityEnvironment {
|
|
|
16
16
|
}
|
|
17
17
|
export declare function readMcpConfigPath(targetPath: string): Promise<string | null>;
|
|
18
18
|
export declare function getClaudeDesktopConfigPath(environment?: CompatibilityEnvironment): string | null;
|
|
19
|
+
export declare function getCursorMcpConfigPath(targetPath: string, environment?: CompatibilityEnvironment): Promise<string>;
|
|
19
20
|
export declare function buildCompatibilityMatrix(targetPath: string, environment?: CompatibilityEnvironment): Promise<CompatibilityMatrix>;
|
|
20
21
|
export declare function matrixExitCode(matrix: CompatibilityMatrix): 0 | 1;
|
|
@@ -121,6 +121,18 @@ export function getClaudeDesktopConfigPath(environment = {}) {
|
|
|
121
121
|
}
|
|
122
122
|
return null;
|
|
123
123
|
}
|
|
124
|
+
function getHomeDirectory(environment = {}) {
|
|
125
|
+
const env = environment.env ?? process.env;
|
|
126
|
+
return env.USERPROFILE ?? env.HOME ?? environment.homedir ?? os.homedir();
|
|
127
|
+
}
|
|
128
|
+
export async function getCursorMcpConfigPath(targetPath, environment = {}) {
|
|
129
|
+
const rootPath = path.resolve(targetPath);
|
|
130
|
+
const projectConfigPath = path.join(rootPath, ".cursor", "mcp.json");
|
|
131
|
+
if (await fileExists(projectConfigPath)) {
|
|
132
|
+
return projectConfigPath;
|
|
133
|
+
}
|
|
134
|
+
return path.join(getHomeDirectory(environment), ".cursor", "mcp.json");
|
|
135
|
+
}
|
|
124
136
|
async function checkClaudeDesktop(targetPath, genericMcpResult, environment = {}) {
|
|
125
137
|
if (genericMcpResult.status !== "pass") {
|
|
126
138
|
return {
|
|
@@ -201,10 +213,83 @@ async function checkClaudeDesktop(targetPath, genericMcpResult, environment = {}
|
|
|
201
213
|
};
|
|
202
214
|
}
|
|
203
215
|
}
|
|
216
|
+
async function checkCursor(targetPath, genericMcpResult, environment = {}) {
|
|
217
|
+
if (genericMcpResult.status !== "pass") {
|
|
218
|
+
return {
|
|
219
|
+
client: "Cursor",
|
|
220
|
+
status: "skipped",
|
|
221
|
+
summary: "No valid MCP package config is available for Cursor.",
|
|
222
|
+
details: ["Add a valid `.mcp.json` with a non-empty `mcpServers` object first."]
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
const configPath = await getCursorMcpConfigPath(targetPath, environment);
|
|
226
|
+
if (!(await fileExists(configPath))) {
|
|
227
|
+
const configDirectory = path.dirname(configPath);
|
|
228
|
+
return {
|
|
229
|
+
client: "Cursor",
|
|
230
|
+
status: await directoryExists(configDirectory) ? "pass" : "warn",
|
|
231
|
+
summary: await directoryExists(configDirectory)
|
|
232
|
+
? "Cursor MCP config directory exists and a config file can be created."
|
|
233
|
+
: "Cursor was not detected on this machine.",
|
|
234
|
+
details: [
|
|
235
|
+
configPath,
|
|
236
|
+
"Cursor supports project `.cursor/mcp.json` and global `~/.cursor/mcp.json` MCP configs."
|
|
237
|
+
]
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const parsed = JSON.parse(await readFile(configPath, "utf8"));
|
|
242
|
+
const servers = parsed.mcpServers;
|
|
243
|
+
if (servers !== undefined && (typeof servers !== "object" ||
|
|
244
|
+
servers === null ||
|
|
245
|
+
Array.isArray(servers))) {
|
|
246
|
+
return {
|
|
247
|
+
client: "Cursor",
|
|
248
|
+
status: "fail",
|
|
249
|
+
summary: "Cursor MCP config has an invalid `mcpServers` shape.",
|
|
250
|
+
details: [configPath, "`mcpServers` must be an object before this package can be added safely."]
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const packageServerNames = await readMcpServerNames(targetPath);
|
|
254
|
+
const existingServerNames = typeof servers === "object" && servers !== null
|
|
255
|
+
? Object.keys(servers)
|
|
256
|
+
: [];
|
|
257
|
+
const duplicateServerNames = packageServerNames.filter((serverName) => existingServerNames.includes(serverName));
|
|
258
|
+
if (duplicateServerNames.length > 0) {
|
|
259
|
+
return {
|
|
260
|
+
client: "Cursor",
|
|
261
|
+
status: "warn",
|
|
262
|
+
summary: "Cursor already has MCP server names from this package.",
|
|
263
|
+
details: [
|
|
264
|
+
configPath,
|
|
265
|
+
...duplicateServerNames.map((serverName) => `Duplicate server: ${serverName}`)
|
|
266
|
+
]
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
client: "Cursor",
|
|
271
|
+
status: "pass",
|
|
272
|
+
summary: "Cursor global MCP config is valid and this package can be added.",
|
|
273
|
+
details: [
|
|
274
|
+
configPath,
|
|
275
|
+
`Source package: ${path.resolve(targetPath)}`
|
|
276
|
+
]
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return {
|
|
281
|
+
client: "Cursor",
|
|
282
|
+
status: "fail",
|
|
283
|
+
summary: "Cursor MCP config is not valid JSON.",
|
|
284
|
+
details: [configPath, "Repair the local Cursor MCP config before adding new MCP servers."]
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
204
288
|
export async function buildCompatibilityMatrix(targetPath, environment = {}) {
|
|
205
289
|
const rootPath = path.resolve(targetPath);
|
|
206
290
|
const genericMcpResult = await checkGenericMcp(rootPath);
|
|
207
291
|
const claudeDesktopResult = await checkClaudeDesktop(rootPath, genericMcpResult, environment);
|
|
292
|
+
const cursorResult = await checkCursor(rootPath, genericMcpResult, environment);
|
|
208
293
|
const codexResult = await validatePlugin(rootPath);
|
|
209
294
|
const codexStatus = statusFromCheckResult(codexResult);
|
|
210
295
|
const codexCompatibility = !await hasCodexManifest(rootPath)
|
|
@@ -227,12 +312,7 @@ export async function buildCompatibilityMatrix(targetPath, environment = {}) {
|
|
|
227
312
|
codexCompatibility,
|
|
228
313
|
genericMcpResult,
|
|
229
314
|
claudeDesktopResult,
|
|
230
|
-
|
|
231
|
-
client: "Cursor",
|
|
232
|
-
status: "skipped",
|
|
233
|
-
summary: "Client-specific package adapter is not implemented yet.",
|
|
234
|
-
details: ["Planned adapter after generic MCP compatibility is stable."]
|
|
235
|
-
}
|
|
315
|
+
cursorResult
|
|
236
316
|
];
|
|
237
317
|
return {
|
|
238
318
|
targetPath: rootPath,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type CompatibilityEnvironment } from "./compatibility-matrix.js";
|
|
2
|
+
export interface CursorInstallPreview {
|
|
3
|
+
targetPath: string;
|
|
4
|
+
configPath: string;
|
|
5
|
+
snippet: {
|
|
6
|
+
mcpServers: Record<string, unknown>;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export declare function buildCursorInstallPreview(targetPath: string, environment?: CompatibilityEnvironment): Promise<CursorInstallPreview>;
|
|
10
|
+
export declare function renderCursorInstallPreview(preview: CursorInstallPreview): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getCursorMcpConfigPath, 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 buildCursorInstallPreview(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: await getCursorMcpConfigPath(rootPath, 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 renderCursorInstallPreview(preview) {
|
|
54
|
+
return [
|
|
55
|
+
"Cursor Install Preview",
|
|
56
|
+
"======================",
|
|
57
|
+
`Target: ${preview.targetPath}`,
|
|
58
|
+
`Config: ${preview.configPath}`,
|
|
59
|
+
"",
|
|
60
|
+
"Add or merge this snippet into `mcp.json`:",
|
|
61
|
+
JSON.stringify(preview.snippet, null, 2),
|
|
62
|
+
"",
|
|
63
|
+
"No files were modified."
|
|
64
|
+
].join("\n");
|
|
65
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
function scoreForStatus(status) {
|
|
2
|
+
if (status === "pass") {
|
|
3
|
+
return 100;
|
|
4
|
+
}
|
|
5
|
+
if (status === "warn") {
|
|
6
|
+
return 70;
|
|
7
|
+
}
|
|
8
|
+
return 0;
|
|
9
|
+
}
|
|
10
|
+
export function renderCompatibilityScorecard(matrix) {
|
|
11
|
+
const lines = [
|
|
12
|
+
"Compatibility Scorecard",
|
|
13
|
+
"=======================",
|
|
14
|
+
`Target: ${matrix.targetPath}`,
|
|
15
|
+
""
|
|
16
|
+
];
|
|
17
|
+
for (const result of matrix.results) {
|
|
18
|
+
lines.push(`${result.client}: ${scoreForStatus(result.status)} (${result.status.toUpperCase()})`);
|
|
19
|
+
}
|
|
20
|
+
return lines.join("\n");
|
|
21
|
+
}
|
package/dist/run-cli.js
CHANGED
|
@@ -2,10 +2,12 @@ import { writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { discoverInstalledPlugins, filterInstalledPlugins } from "./core/discover-installed-plugins.js";
|
|
3
3
|
import { buildCompatibilityMatrix, matrixExitCode } from "./compatibility/compatibility-matrix.js";
|
|
4
4
|
import { buildClaudeDesktopInstallPreview, renderClaudeDesktopInstallPreview } from "./compatibility/claude-desktop-install-preview.js";
|
|
5
|
+
import { buildCursorInstallPreview, renderCursorInstallPreview } from "./compatibility/cursor-install-preview.js";
|
|
5
6
|
import { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
|
|
6
7
|
import { initPluginPackage } from "./core/init-plugin.js";
|
|
7
8
|
import { runCheck } from "./index.js";
|
|
8
9
|
import { renderInstalledSummary } from "./reporting/render-installed-summary.js";
|
|
10
|
+
import { renderCompatibilityScorecard } from "./reporting/render-compatibility-scorecard.js";
|
|
9
11
|
import { renderCompatibilityReport } from "./reporting/render-compatibility-report.js";
|
|
10
12
|
import { renderJsonReport } from "./reporting/render-json-report.js";
|
|
11
13
|
import { buildMarkdownReport } from "./reporting/render-markdown-report.js";
|
|
@@ -26,7 +28,7 @@ const defaultIo = {
|
|
|
26
28
|
}
|
|
27
29
|
};
|
|
28
30
|
function printUsage(io) {
|
|
29
|
-
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--json|--markdown] [--output <path>] [--runtime] [--verbose-runtime] [--no-animations] [--ascii]\n codex-plugin-doctor compat <path> [--client <client>] [--json] [--output <path>] [--install-preview]\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version");
|
|
31
|
+
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--json|--markdown] [--output <path>] [--runtime] [--verbose-runtime] [--no-animations] [--ascii]\n codex-plugin-doctor compat <path> [--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview]\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version");
|
|
30
32
|
}
|
|
31
33
|
function renderInstalledPlugins(plugins) {
|
|
32
34
|
const lines = [
|
|
@@ -114,6 +116,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
114
116
|
? [maybePath, ...remainingArgs]
|
|
115
117
|
: remainingArgs;
|
|
116
118
|
const jsonOutput = compatFlags.includes("--json");
|
|
119
|
+
const scorecardOutput = compatFlags.includes("--scorecard");
|
|
117
120
|
const installPreview = compatFlags.includes("--install-preview");
|
|
118
121
|
const clientIndex = compatFlags.indexOf("--client");
|
|
119
122
|
const clientFilter = clientIndex === -1 ? null : compatFlags[clientIndex + 1];
|
|
@@ -127,16 +130,21 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
127
130
|
io.writeStderr("Missing path after --output.");
|
|
128
131
|
return 2;
|
|
129
132
|
}
|
|
130
|
-
if (installPreview &&
|
|
131
|
-
|
|
133
|
+
if (installPreview &&
|
|
134
|
+
clientFilter?.toLowerCase() !== "claude-desktop" &&
|
|
135
|
+
clientFilter?.toLowerCase() !== "cursor") {
|
|
136
|
+
io.writeStderr("--install-preview requires --client claude-desktop or --client cursor.");
|
|
132
137
|
return 2;
|
|
133
138
|
}
|
|
134
139
|
if (installPreview) {
|
|
135
140
|
try {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
141
|
+
const report = clientFilter?.toLowerCase() === "cursor"
|
|
142
|
+
? renderCursorInstallPreview(await buildCursorInstallPreview(targetPath, {
|
|
143
|
+
env: terminalContext.env
|
|
144
|
+
}))
|
|
145
|
+
: renderClaudeDesktopInstallPreview(await buildClaudeDesktopInstallPreview(targetPath, {
|
|
146
|
+
env: terminalContext.env
|
|
147
|
+
}));
|
|
140
148
|
if (outputPath) {
|
|
141
149
|
await writeFile(outputPath, report, "utf8");
|
|
142
150
|
}
|
|
@@ -162,7 +170,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
162
170
|
}
|
|
163
171
|
const report = jsonOutput
|
|
164
172
|
? JSON.stringify({ schemaVersion: "1.0.0", ...matrix }, null, 2)
|
|
165
|
-
:
|
|
173
|
+
: scorecardOutput
|
|
174
|
+
? renderCompatibilityScorecard(matrix)
|
|
175
|
+
: renderCompatibilityReport(matrix);
|
|
166
176
|
if (outputPath) {
|
|
167
177
|
await writeFile(outputPath, report, "utf8");
|
|
168
178
|
}
|
package/package.json
CHANGED