codex-plugin-doctor 0.2.0 → 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 +11 -0
- package/dist/compatibility/claude-desktop-install-preview.d.ts +10 -0
- package/dist/compatibility/claude-desktop-install-preview.js +69 -0
- package/dist/compatibility/compatibility-matrix.d.ts +9 -1
- package/dist/compatibility/compatibility-matrix.js +209 -14
- 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 +39 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -160,6 +160,11 @@ codex-plugin-doctor init my-plugin
|
|
|
160
160
|
codex-plugin-doctor compat .
|
|
161
161
|
codex-plugin-doctor compat . --client codex
|
|
162
162
|
codex-plugin-doctor compat . --client generic-mcp
|
|
163
|
+
codex-plugin-doctor compat . --client claude-desktop
|
|
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
|
|
163
168
|
codex-plugin-doctor compat . --json
|
|
164
169
|
codex-plugin-doctor compat . --json --output compatibility.json
|
|
165
170
|
codex-plugin-doctor check .
|
|
@@ -174,6 +179,12 @@ codex-plugin-doctor check . --config .codex-doctor.json
|
|
|
174
179
|
codex-plugin-doctor check . --json --runtime --verbose-runtime
|
|
175
180
|
```
|
|
176
181
|
|
|
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.
|
|
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
|
+
|
|
177
188
|
Optional local policy file:
|
|
178
189
|
|
|
179
190
|
```json
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type CompatibilityEnvironment } from "./compatibility-matrix.js";
|
|
2
|
+
export interface ClaudeDesktopInstallPreview {
|
|
3
|
+
targetPath: string;
|
|
4
|
+
configPath: string;
|
|
5
|
+
snippet: {
|
|
6
|
+
mcpServers: Record<string, unknown>;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export declare function buildClaudeDesktopInstallPreview(targetPath: string, environment?: CompatibilityEnvironment): Promise<ClaudeDesktopInstallPreview>;
|
|
10
|
+
export declare function renderClaudeDesktopInstallPreview(preview: ClaudeDesktopInstallPreview): string;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getClaudeDesktopConfigPath, 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 buildClaudeDesktopInstallPreview(targetPath, environment = {}) {
|
|
32
|
+
const rootPath = path.resolve(targetPath);
|
|
33
|
+
const configPath = getClaudeDesktopConfigPath(environment);
|
|
34
|
+
if (!configPath) {
|
|
35
|
+
throw new Error("Claude Desktop config path could not be resolved on this platform.");
|
|
36
|
+
}
|
|
37
|
+
const mcpConfigPath = await readMcpConfigPath(rootPath);
|
|
38
|
+
if (!mcpConfigPath) {
|
|
39
|
+
throw new Error("No MCP config found for install preview.");
|
|
40
|
+
}
|
|
41
|
+
const parsed = JSON.parse(await readFile(mcpConfigPath, "utf8"));
|
|
42
|
+
const servers = parsed.mcpServers;
|
|
43
|
+
if (!isRecord(servers) || Object.keys(servers).length === 0) {
|
|
44
|
+
throw new Error("MCP config does not contain a non-empty `mcpServers` object.");
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
targetPath: rootPath,
|
|
48
|
+
configPath,
|
|
49
|
+
snippet: {
|
|
50
|
+
mcpServers: Object.fromEntries(Object.entries(servers).map(([serverName, serverConfig]) => [
|
|
51
|
+
serverName,
|
|
52
|
+
normalizeServerConfig(serverConfig, rootPath)
|
|
53
|
+
]))
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function renderClaudeDesktopInstallPreview(preview) {
|
|
58
|
+
return [
|
|
59
|
+
"Claude Desktop Install Preview",
|
|
60
|
+
"==============================",
|
|
61
|
+
`Target: ${preview.targetPath}`,
|
|
62
|
+
`Config: ${preview.configPath}`,
|
|
63
|
+
"",
|
|
64
|
+
"Add or merge this snippet into `claude_desktop_config.json`:",
|
|
65
|
+
JSON.stringify(preview.snippet, null, 2),
|
|
66
|
+
"",
|
|
67
|
+
"No files were modified."
|
|
68
|
+
].join("\n");
|
|
69
|
+
}
|
|
@@ -9,5 +9,13 @@ export interface CompatibilityMatrix {
|
|
|
9
9
|
targetPath: string;
|
|
10
10
|
results: CompatibilityResult[];
|
|
11
11
|
}
|
|
12
|
-
export
|
|
12
|
+
export interface CompatibilityEnvironment {
|
|
13
|
+
env?: Record<string, string | undefined>;
|
|
14
|
+
platform?: NodeJS.Platform;
|
|
15
|
+
homedir?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function readMcpConfigPath(targetPath: string): Promise<string | null>;
|
|
18
|
+
export declare function getClaudeDesktopConfigPath(environment?: CompatibilityEnvironment): string | null;
|
|
19
|
+
export declare function getCursorMcpConfigPath(targetPath: string, environment?: CompatibilityEnvironment): Promise<string>;
|
|
20
|
+
export declare function buildCompatibilityMatrix(targetPath: string, environment?: CompatibilityEnvironment): Promise<CompatibilityMatrix>;
|
|
13
21
|
export declare function matrixExitCode(matrix: CompatibilityMatrix): 0 | 1;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { validatePlugin } from "../core/validate-plugin.js";
|
|
4
5
|
async function fileExists(targetPath) {
|
|
@@ -10,6 +11,15 @@ async function fileExists(targetPath) {
|
|
|
10
11
|
return false;
|
|
11
12
|
}
|
|
12
13
|
}
|
|
14
|
+
async function directoryExists(targetPath) {
|
|
15
|
+
try {
|
|
16
|
+
const details = await stat(targetPath);
|
|
17
|
+
return details.isDirectory();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
13
23
|
function statusFromCheckResult(result) {
|
|
14
24
|
if (result.status === "fail") {
|
|
15
25
|
return "fail";
|
|
@@ -19,7 +29,7 @@ function statusFromCheckResult(result) {
|
|
|
19
29
|
}
|
|
20
30
|
return "pass";
|
|
21
31
|
}
|
|
22
|
-
async function readMcpConfigPath(targetPath) {
|
|
32
|
+
export async function readMcpConfigPath(targetPath) {
|
|
23
33
|
const rootPath = path.resolve(targetPath);
|
|
24
34
|
const directMcpPath = path.join(rootPath, ".mcp.json");
|
|
25
35
|
if (await fileExists(directMcpPath)) {
|
|
@@ -82,9 +92,204 @@ async function checkGenericMcp(targetPath) {
|
|
|
82
92
|
};
|
|
83
93
|
}
|
|
84
94
|
}
|
|
85
|
-
|
|
95
|
+
async function readMcpServerNames(targetPath) {
|
|
96
|
+
const mcpConfigPath = await readMcpConfigPath(targetPath);
|
|
97
|
+
if (!mcpConfigPath || !(await fileExists(mcpConfigPath))) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(await readFile(mcpConfigPath, "utf8"));
|
|
102
|
+
const servers = parsed.mcpServers;
|
|
103
|
+
return typeof servers === "object" && servers !== null && !Array.isArray(servers)
|
|
104
|
+
? Object.keys(servers)
|
|
105
|
+
: [];
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function getClaudeDesktopConfigPath(environment = {}) {
|
|
112
|
+
const platform = environment.platform ?? process.platform;
|
|
113
|
+
const env = environment.env ?? process.env;
|
|
114
|
+
const homeDirectory = environment.homedir ?? os.homedir();
|
|
115
|
+
if (platform === "win32") {
|
|
116
|
+
const appData = env.APPDATA;
|
|
117
|
+
return appData ? path.join(appData, "Claude", "claude_desktop_config.json") : null;
|
|
118
|
+
}
|
|
119
|
+
if (platform === "darwin") {
|
|
120
|
+
return path.join(homeDirectory, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
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
|
+
}
|
|
136
|
+
async function checkClaudeDesktop(targetPath, genericMcpResult, environment = {}) {
|
|
137
|
+
if (genericMcpResult.status !== "pass") {
|
|
138
|
+
return {
|
|
139
|
+
client: "Claude Desktop",
|
|
140
|
+
status: "skipped",
|
|
141
|
+
summary: "No valid MCP package config is available for Claude Desktop.",
|
|
142
|
+
details: ["Add a valid `.mcp.json` with a non-empty `mcpServers` object first."]
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const configPath = getClaudeDesktopConfigPath(environment);
|
|
146
|
+
if (!configPath) {
|
|
147
|
+
return {
|
|
148
|
+
client: "Claude Desktop",
|
|
149
|
+
status: "warn",
|
|
150
|
+
summary: "Claude Desktop config path could not be resolved on this platform.",
|
|
151
|
+
details: ["Claude Desktop local config detection currently supports Windows and macOS."]
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (!(await fileExists(configPath))) {
|
|
155
|
+
const configDirectory = path.dirname(configPath);
|
|
156
|
+
return {
|
|
157
|
+
client: "Claude Desktop",
|
|
158
|
+
status: await directoryExists(configDirectory) ? "pass" : "warn",
|
|
159
|
+
summary: await directoryExists(configDirectory)
|
|
160
|
+
? "Claude Desktop directory exists and a config file can be created."
|
|
161
|
+
: "Claude Desktop was not detected on this machine.",
|
|
162
|
+
details: [
|
|
163
|
+
configPath,
|
|
164
|
+
"Claude Desktop can add stdio MCP servers through `claude_desktop_config.json` when the app is installed."
|
|
165
|
+
]
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const parsed = JSON.parse(await readFile(configPath, "utf8"));
|
|
170
|
+
const servers = parsed.mcpServers;
|
|
171
|
+
if (servers !== undefined && (typeof servers !== "object" ||
|
|
172
|
+
servers === null ||
|
|
173
|
+
Array.isArray(servers))) {
|
|
174
|
+
return {
|
|
175
|
+
client: "Claude Desktop",
|
|
176
|
+
status: "fail",
|
|
177
|
+
summary: "Claude Desktop config has an invalid `mcpServers` shape.",
|
|
178
|
+
details: [configPath, "`mcpServers` must be an object before this package can be added safely."]
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const packageServerNames = await readMcpServerNames(targetPath);
|
|
182
|
+
const existingServerNames = typeof servers === "object" && servers !== null
|
|
183
|
+
? Object.keys(servers)
|
|
184
|
+
: [];
|
|
185
|
+
const duplicateServerNames = packageServerNames.filter((serverName) => existingServerNames.includes(serverName));
|
|
186
|
+
if (duplicateServerNames.length > 0) {
|
|
187
|
+
return {
|
|
188
|
+
client: "Claude Desktop",
|
|
189
|
+
status: "warn",
|
|
190
|
+
summary: "Claude Desktop already has MCP server names from this package.",
|
|
191
|
+
details: [
|
|
192
|
+
configPath,
|
|
193
|
+
...duplicateServerNames.map((serverName) => `Duplicate server: ${serverName}`)
|
|
194
|
+
]
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
client: "Claude Desktop",
|
|
199
|
+
status: "pass",
|
|
200
|
+
summary: "Claude Desktop config is valid and this MCP package can be added.",
|
|
201
|
+
details: [
|
|
202
|
+
configPath,
|
|
203
|
+
`Source package: ${path.resolve(targetPath)}`
|
|
204
|
+
]
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return {
|
|
209
|
+
client: "Claude Desktop",
|
|
210
|
+
status: "fail",
|
|
211
|
+
summary: "Claude Desktop config is not valid JSON.",
|
|
212
|
+
details: [configPath, "Repair the local Claude Desktop config before adding new MCP servers."]
|
|
213
|
+
};
|
|
214
|
+
}
|
|
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
|
+
}
|
|
288
|
+
export async function buildCompatibilityMatrix(targetPath, environment = {}) {
|
|
86
289
|
const rootPath = path.resolve(targetPath);
|
|
87
290
|
const genericMcpResult = await checkGenericMcp(rootPath);
|
|
291
|
+
const claudeDesktopResult = await checkClaudeDesktop(rootPath, genericMcpResult, environment);
|
|
292
|
+
const cursorResult = await checkCursor(rootPath, genericMcpResult, environment);
|
|
88
293
|
const codexResult = await validatePlugin(rootPath);
|
|
89
294
|
const codexStatus = statusFromCheckResult(codexResult);
|
|
90
295
|
const codexCompatibility = !await hasCodexManifest(rootPath)
|
|
@@ -106,18 +311,8 @@ export async function buildCompatibilityMatrix(targetPath) {
|
|
|
106
311
|
const results = [
|
|
107
312
|
codexCompatibility,
|
|
108
313
|
genericMcpResult,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
status: "skipped",
|
|
112
|
-
summary: "Client-specific package adapter is not implemented yet.",
|
|
113
|
-
details: ["Planned adapter after generic MCP compatibility is stable."]
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
client: "Cursor",
|
|
117
|
-
status: "skipped",
|
|
118
|
-
summary: "Client-specific package adapter is not implemented yet.",
|
|
119
|
-
details: ["Planned adapter after generic MCP compatibility is stable."]
|
|
120
|
-
}
|
|
314
|
+
claudeDesktopResult,
|
|
315
|
+
cursorResult
|
|
121
316
|
];
|
|
122
317
|
return {
|
|
123
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
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
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
|
+
import { buildClaudeDesktopInstallPreview, renderClaudeDesktopInstallPreview } from "./compatibility/claude-desktop-install-preview.js";
|
|
5
|
+
import { buildCursorInstallPreview, renderCursorInstallPreview } from "./compatibility/cursor-install-preview.js";
|
|
4
6
|
import { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
|
|
5
7
|
import { initPluginPackage } from "./core/init-plugin.js";
|
|
6
8
|
import { runCheck } from "./index.js";
|
|
7
9
|
import { renderInstalledSummary } from "./reporting/render-installed-summary.js";
|
|
10
|
+
import { renderCompatibilityScorecard } from "./reporting/render-compatibility-scorecard.js";
|
|
8
11
|
import { renderCompatibilityReport } from "./reporting/render-compatibility-report.js";
|
|
9
12
|
import { renderJsonReport } from "./reporting/render-json-report.js";
|
|
10
13
|
import { buildMarkdownReport } from "./reporting/render-markdown-report.js";
|
|
@@ -25,7 +28,7 @@ const defaultIo = {
|
|
|
25
28
|
}
|
|
26
29
|
};
|
|
27
30
|
function printUsage(io) {
|
|
28
|
-
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> [--json] [--output <path>]\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");
|
|
29
32
|
}
|
|
30
33
|
function renderInstalledPlugins(plugins) {
|
|
31
34
|
const lines = [
|
|
@@ -113,6 +116,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
113
116
|
? [maybePath, ...remainingArgs]
|
|
114
117
|
: remainingArgs;
|
|
115
118
|
const jsonOutput = compatFlags.includes("--json");
|
|
119
|
+
const scorecardOutput = compatFlags.includes("--scorecard");
|
|
120
|
+
const installPreview = compatFlags.includes("--install-preview");
|
|
116
121
|
const clientIndex = compatFlags.indexOf("--client");
|
|
117
122
|
const clientFilter = clientIndex === -1 ? null : compatFlags[clientIndex + 1];
|
|
118
123
|
const outputIndex = compatFlags.indexOf("--output");
|
|
@@ -125,7 +130,36 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
125
130
|
io.writeStderr("Missing path after --output.");
|
|
126
131
|
return 2;
|
|
127
132
|
}
|
|
128
|
-
|
|
133
|
+
if (installPreview &&
|
|
134
|
+
clientFilter?.toLowerCase() !== "claude-desktop" &&
|
|
135
|
+
clientFilter?.toLowerCase() !== "cursor") {
|
|
136
|
+
io.writeStderr("--install-preview requires --client claude-desktop or --client cursor.");
|
|
137
|
+
return 2;
|
|
138
|
+
}
|
|
139
|
+
if (installPreview) {
|
|
140
|
+
try {
|
|
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
|
+
}));
|
|
148
|
+
if (outputPath) {
|
|
149
|
+
await writeFile(outputPath, report, "utf8");
|
|
150
|
+
}
|
|
151
|
+
io.writeStdout(report);
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
const message = error instanceof Error ? error.message : "Unknown install preview error.";
|
|
156
|
+
io.writeStderr(message);
|
|
157
|
+
return 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
let matrix = await buildCompatibilityMatrix(targetPath, {
|
|
161
|
+
env: terminalContext.env
|
|
162
|
+
});
|
|
129
163
|
if (clientFilter) {
|
|
130
164
|
const filteredMatrix = filterCompatibilityMatrix(matrix, clientFilter);
|
|
131
165
|
if (!filteredMatrix) {
|
|
@@ -136,7 +170,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
136
170
|
}
|
|
137
171
|
const report = jsonOutput
|
|
138
172
|
? JSON.stringify({ schemaVersion: "1.0.0", ...matrix }, null, 2)
|
|
139
|
-
:
|
|
173
|
+
: scorecardOutput
|
|
174
|
+
? renderCompatibilityScorecard(matrix)
|
|
175
|
+
: renderCompatibilityReport(matrix);
|
|
140
176
|
if (outputPath) {
|
|
141
177
|
await writeFile(outputPath, report, "utf8");
|
|
142
178
|
}
|
package/package.json
CHANGED