codex-plugin-doctor 0.1.5 → 0.2.1
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 +10 -1
- 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 +20 -0
- package/dist/compatibility/compatibility-matrix.js +244 -0
- package/dist/reporting/render-compatibility-report.d.ts +2 -0
- package/dist/reporting/render-compatibility-report.js +18 -0
- package/dist/run-cli.js +84 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -157,6 +157,13 @@ Run these from a Codex plugin package root:
|
|
|
157
157
|
```bash
|
|
158
158
|
codex-plugin-doctor --version
|
|
159
159
|
codex-plugin-doctor init my-plugin
|
|
160
|
+
codex-plugin-doctor compat .
|
|
161
|
+
codex-plugin-doctor compat . --client codex
|
|
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 . --json
|
|
166
|
+
codex-plugin-doctor compat . --json --output compatibility.json
|
|
160
167
|
codex-plugin-doctor check .
|
|
161
168
|
codex-plugin-doctor check . --json
|
|
162
169
|
codex-plugin-doctor check . --json --output report.json
|
|
@@ -169,6 +176,8 @@ codex-plugin-doctor check . --config .codex-doctor.json
|
|
|
169
176
|
codex-plugin-doctor check . --json --runtime --verbose-runtime
|
|
170
177
|
```
|
|
171
178
|
|
|
179
|
+
`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
|
+
|
|
172
181
|
Optional local policy file:
|
|
173
182
|
|
|
174
183
|
```json
|
|
@@ -202,7 +211,7 @@ jobs:
|
|
|
202
211
|
runs-on: ubuntu-latest
|
|
203
212
|
steps:
|
|
204
213
|
- uses: actions/checkout@v4
|
|
205
|
-
- uses: Esquetta/CodexPluginDoctor@v0.
|
|
214
|
+
- uses: Esquetta/CodexPluginDoctor@v0.2.0
|
|
206
215
|
with:
|
|
207
216
|
path: .
|
|
208
217
|
runtime: "false"
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type CompatibilityStatus = "pass" | "warn" | "fail" | "skipped";
|
|
2
|
+
export interface CompatibilityResult {
|
|
3
|
+
client: string;
|
|
4
|
+
status: CompatibilityStatus;
|
|
5
|
+
summary: string;
|
|
6
|
+
details: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface CompatibilityMatrix {
|
|
9
|
+
targetPath: string;
|
|
10
|
+
results: CompatibilityResult[];
|
|
11
|
+
}
|
|
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 buildCompatibilityMatrix(targetPath: string, environment?: CompatibilityEnvironment): Promise<CompatibilityMatrix>;
|
|
20
|
+
export declare function matrixExitCode(matrix: CompatibilityMatrix): 0 | 1;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { validatePlugin } from "../core/validate-plugin.js";
|
|
5
|
+
async function fileExists(targetPath) {
|
|
6
|
+
try {
|
|
7
|
+
const details = await stat(targetPath);
|
|
8
|
+
return details.isFile();
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
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
|
+
}
|
|
23
|
+
function statusFromCheckResult(result) {
|
|
24
|
+
if (result.status === "fail") {
|
|
25
|
+
return "fail";
|
|
26
|
+
}
|
|
27
|
+
if (result.status === "warn") {
|
|
28
|
+
return "warn";
|
|
29
|
+
}
|
|
30
|
+
return "pass";
|
|
31
|
+
}
|
|
32
|
+
export async function readMcpConfigPath(targetPath) {
|
|
33
|
+
const rootPath = path.resolve(targetPath);
|
|
34
|
+
const directMcpPath = path.join(rootPath, ".mcp.json");
|
|
35
|
+
if (await fileExists(directMcpPath)) {
|
|
36
|
+
return directMcpPath;
|
|
37
|
+
}
|
|
38
|
+
const manifestPath = path.join(rootPath, ".codex-plugin", "plugin.json");
|
|
39
|
+
if (!(await fileExists(manifestPath))) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
44
|
+
return typeof manifest.mcpServers === "string"
|
|
45
|
+
? path.resolve(rootPath, manifest.mcpServers)
|
|
46
|
+
: null;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function hasCodexManifest(targetPath) {
|
|
53
|
+
return fileExists(path.join(path.resolve(targetPath), ".codex-plugin", "plugin.json"));
|
|
54
|
+
}
|
|
55
|
+
async function checkGenericMcp(targetPath) {
|
|
56
|
+
const mcpConfigPath = await readMcpConfigPath(targetPath);
|
|
57
|
+
if (!mcpConfigPath || !(await fileExists(mcpConfigPath))) {
|
|
58
|
+
return {
|
|
59
|
+
client: "Generic MCP",
|
|
60
|
+
status: "skipped",
|
|
61
|
+
summary: "No MCP config found.",
|
|
62
|
+
details: ["Expected `.mcp.json` or manifest `mcpServers` reference."]
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(await readFile(mcpConfigPath, "utf8"));
|
|
67
|
+
const servers = parsed.mcpServers;
|
|
68
|
+
if (typeof servers !== "object" ||
|
|
69
|
+
servers === null ||
|
|
70
|
+
Array.isArray(servers) ||
|
|
71
|
+
Object.keys(servers).length === 0) {
|
|
72
|
+
return {
|
|
73
|
+
client: "Generic MCP",
|
|
74
|
+
status: "fail",
|
|
75
|
+
summary: "MCP config does not contain a non-empty `mcpServers` object.",
|
|
76
|
+
details: [mcpConfigPath]
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
client: "Generic MCP",
|
|
81
|
+
status: "pass",
|
|
82
|
+
summary: "MCP server config is valid.",
|
|
83
|
+
details: [mcpConfigPath]
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return {
|
|
88
|
+
client: "Generic MCP",
|
|
89
|
+
status: "fail",
|
|
90
|
+
summary: "MCP config is not valid JSON.",
|
|
91
|
+
details: [mcpConfigPath]
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
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
|
+
async function checkClaudeDesktop(targetPath, genericMcpResult, environment = {}) {
|
|
125
|
+
if (genericMcpResult.status !== "pass") {
|
|
126
|
+
return {
|
|
127
|
+
client: "Claude Desktop",
|
|
128
|
+
status: "skipped",
|
|
129
|
+
summary: "No valid MCP package config is available for Claude Desktop.",
|
|
130
|
+
details: ["Add a valid `.mcp.json` with a non-empty `mcpServers` object first."]
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const configPath = getClaudeDesktopConfigPath(environment);
|
|
134
|
+
if (!configPath) {
|
|
135
|
+
return {
|
|
136
|
+
client: "Claude Desktop",
|
|
137
|
+
status: "warn",
|
|
138
|
+
summary: "Claude Desktop config path could not be resolved on this platform.",
|
|
139
|
+
details: ["Claude Desktop local config detection currently supports Windows and macOS."]
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (!(await fileExists(configPath))) {
|
|
143
|
+
const configDirectory = path.dirname(configPath);
|
|
144
|
+
return {
|
|
145
|
+
client: "Claude Desktop",
|
|
146
|
+
status: await directoryExists(configDirectory) ? "pass" : "warn",
|
|
147
|
+
summary: await directoryExists(configDirectory)
|
|
148
|
+
? "Claude Desktop directory exists and a config file can be created."
|
|
149
|
+
: "Claude Desktop was not detected on this machine.",
|
|
150
|
+
details: [
|
|
151
|
+
configPath,
|
|
152
|
+
"Claude Desktop can add stdio MCP servers through `claude_desktop_config.json` when the app is installed."
|
|
153
|
+
]
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const parsed = JSON.parse(await readFile(configPath, "utf8"));
|
|
158
|
+
const servers = parsed.mcpServers;
|
|
159
|
+
if (servers !== undefined && (typeof servers !== "object" ||
|
|
160
|
+
servers === null ||
|
|
161
|
+
Array.isArray(servers))) {
|
|
162
|
+
return {
|
|
163
|
+
client: "Claude Desktop",
|
|
164
|
+
status: "fail",
|
|
165
|
+
summary: "Claude Desktop config has an invalid `mcpServers` shape.",
|
|
166
|
+
details: [configPath, "`mcpServers` must be an object before this package can be added safely."]
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const packageServerNames = await readMcpServerNames(targetPath);
|
|
170
|
+
const existingServerNames = typeof servers === "object" && servers !== null
|
|
171
|
+
? Object.keys(servers)
|
|
172
|
+
: [];
|
|
173
|
+
const duplicateServerNames = packageServerNames.filter((serverName) => existingServerNames.includes(serverName));
|
|
174
|
+
if (duplicateServerNames.length > 0) {
|
|
175
|
+
return {
|
|
176
|
+
client: "Claude Desktop",
|
|
177
|
+
status: "warn",
|
|
178
|
+
summary: "Claude Desktop already has MCP server names from this package.",
|
|
179
|
+
details: [
|
|
180
|
+
configPath,
|
|
181
|
+
...duplicateServerNames.map((serverName) => `Duplicate server: ${serverName}`)
|
|
182
|
+
]
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
client: "Claude Desktop",
|
|
187
|
+
status: "pass",
|
|
188
|
+
summary: "Claude Desktop config is valid and this MCP package can be added.",
|
|
189
|
+
details: [
|
|
190
|
+
configPath,
|
|
191
|
+
`Source package: ${path.resolve(targetPath)}`
|
|
192
|
+
]
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return {
|
|
197
|
+
client: "Claude Desktop",
|
|
198
|
+
status: "fail",
|
|
199
|
+
summary: "Claude Desktop config is not valid JSON.",
|
|
200
|
+
details: [configPath, "Repair the local Claude Desktop config before adding new MCP servers."]
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
export async function buildCompatibilityMatrix(targetPath, environment = {}) {
|
|
205
|
+
const rootPath = path.resolve(targetPath);
|
|
206
|
+
const genericMcpResult = await checkGenericMcp(rootPath);
|
|
207
|
+
const claudeDesktopResult = await checkClaudeDesktop(rootPath, genericMcpResult, environment);
|
|
208
|
+
const codexResult = await validatePlugin(rootPath);
|
|
209
|
+
const codexStatus = statusFromCheckResult(codexResult);
|
|
210
|
+
const codexCompatibility = !await hasCodexManifest(rootPath)
|
|
211
|
+
&& genericMcpResult.status === "pass"
|
|
212
|
+
? {
|
|
213
|
+
client: "Codex",
|
|
214
|
+
status: "skipped",
|
|
215
|
+
summary: "No Codex plugin manifest found; treating target as a standalone MCP package.",
|
|
216
|
+
details: ["Add `.codex-plugin/plugin.json` if this package should be installable as a Codex plugin."]
|
|
217
|
+
}
|
|
218
|
+
: {
|
|
219
|
+
client: "Codex",
|
|
220
|
+
status: codexStatus,
|
|
221
|
+
summary: codexStatus === "pass"
|
|
222
|
+
? "Codex plugin package validation passed."
|
|
223
|
+
: "Codex plugin package validation produced findings.",
|
|
224
|
+
details: codexResult.findings.map((finding) => finding.id)
|
|
225
|
+
};
|
|
226
|
+
const results = [
|
|
227
|
+
codexCompatibility,
|
|
228
|
+
genericMcpResult,
|
|
229
|
+
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
|
+
}
|
|
236
|
+
];
|
|
237
|
+
return {
|
|
238
|
+
targetPath: rootPath,
|
|
239
|
+
results
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
export function matrixExitCode(matrix) {
|
|
243
|
+
return matrix.results.some((result) => result.status === "fail") ? 1 : 0;
|
|
244
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function statusLabel(status) {
|
|
2
|
+
return status.toUpperCase();
|
|
3
|
+
}
|
|
4
|
+
export function renderCompatibilityReport(matrix) {
|
|
5
|
+
const lines = [
|
|
6
|
+
"Compatibility Matrix",
|
|
7
|
+
"====================",
|
|
8
|
+
`Target: ${matrix.targetPath}`,
|
|
9
|
+
""
|
|
10
|
+
];
|
|
11
|
+
for (const result of matrix.results) {
|
|
12
|
+
lines.push(`${result.client}: ${statusLabel(result.status)} - ${result.summary}`);
|
|
13
|
+
for (const detail of result.details) {
|
|
14
|
+
lines.push(` - ${detail}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return lines.join("\n");
|
|
18
|
+
}
|
package/dist/run-cli.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { writeFile } from "node:fs/promises";
|
|
2
2
|
import { discoverInstalledPlugins, filterInstalledPlugins } from "./core/discover-installed-plugins.js";
|
|
3
|
+
import { buildCompatibilityMatrix, matrixExitCode } from "./compatibility/compatibility-matrix.js";
|
|
4
|
+
import { buildClaudeDesktopInstallPreview, renderClaudeDesktopInstallPreview } from "./compatibility/claude-desktop-install-preview.js";
|
|
3
5
|
import { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
|
|
4
6
|
import { initPluginPackage } from "./core/init-plugin.js";
|
|
5
7
|
import { runCheck } from "./index.js";
|
|
6
8
|
import { renderInstalledSummary } from "./reporting/render-installed-summary.js";
|
|
9
|
+
import { renderCompatibilityReport } from "./reporting/render-compatibility-report.js";
|
|
7
10
|
import { renderJsonReport } from "./reporting/render-json-report.js";
|
|
8
11
|
import { buildMarkdownReport } from "./reporting/render-markdown-report.js";
|
|
9
12
|
import { renderRuleExplanation } from "./reporting/render-rule-explanation.js";
|
|
@@ -23,7 +26,7 @@ const defaultIo = {
|
|
|
23
26
|
}
|
|
24
27
|
};
|
|
25
28
|
function printUsage(io) {
|
|
26
|
-
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--json|--markdown] [--output <path>] [--runtime] [--verbose-runtime] [--no-animations] [--ascii]\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version");
|
|
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");
|
|
27
30
|
}
|
|
28
31
|
function renderInstalledPlugins(plugins) {
|
|
29
32
|
const lines = [
|
|
@@ -42,6 +45,25 @@ function renderInstalledPlugins(plugins) {
|
|
|
42
45
|
}
|
|
43
46
|
return lines.join("\n");
|
|
44
47
|
}
|
|
48
|
+
const compatibilityClientAliases = {
|
|
49
|
+
codex: "Codex",
|
|
50
|
+
"generic-mcp": "Generic MCP",
|
|
51
|
+
generic: "Generic MCP",
|
|
52
|
+
mcp: "Generic MCP",
|
|
53
|
+
"claude-desktop": "Claude Desktop",
|
|
54
|
+
claude: "Claude Desktop",
|
|
55
|
+
cursor: "Cursor"
|
|
56
|
+
};
|
|
57
|
+
function filterCompatibilityMatrix(matrix, clientFilter) {
|
|
58
|
+
const client = compatibilityClientAliases[clientFilter.toLowerCase()];
|
|
59
|
+
if (!client) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
...matrix,
|
|
64
|
+
results: matrix.results.filter((result) => result.client === client)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
45
67
|
export async function runCli(args, io = defaultIo, options = {}) {
|
|
46
68
|
const [command, maybePath, ...remainingArgs] = args;
|
|
47
69
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
@@ -86,6 +108,67 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
86
108
|
].join("\n"));
|
|
87
109
|
return 0;
|
|
88
110
|
}
|
|
111
|
+
if (command === "compat") {
|
|
112
|
+
const targetPath = maybePath && !maybePath.startsWith("--") ? maybePath : ".";
|
|
113
|
+
const compatFlags = maybePath && maybePath.startsWith("--")
|
|
114
|
+
? [maybePath, ...remainingArgs]
|
|
115
|
+
: remainingArgs;
|
|
116
|
+
const jsonOutput = compatFlags.includes("--json");
|
|
117
|
+
const installPreview = compatFlags.includes("--install-preview");
|
|
118
|
+
const clientIndex = compatFlags.indexOf("--client");
|
|
119
|
+
const clientFilter = clientIndex === -1 ? null : compatFlags[clientIndex + 1];
|
|
120
|
+
const outputIndex = compatFlags.indexOf("--output");
|
|
121
|
+
const outputPath = outputIndex === -1 ? null : compatFlags[outputIndex + 1];
|
|
122
|
+
if (clientIndex !== -1 && (!clientFilter || clientFilter.startsWith("--"))) {
|
|
123
|
+
io.writeStderr("Missing client after --client.");
|
|
124
|
+
return 2;
|
|
125
|
+
}
|
|
126
|
+
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
127
|
+
io.writeStderr("Missing path after --output.");
|
|
128
|
+
return 2;
|
|
129
|
+
}
|
|
130
|
+
if (installPreview && clientFilter?.toLowerCase() !== "claude-desktop") {
|
|
131
|
+
io.writeStderr("--install-preview requires --client claude-desktop.");
|
|
132
|
+
return 2;
|
|
133
|
+
}
|
|
134
|
+
if (installPreview) {
|
|
135
|
+
try {
|
|
136
|
+
const preview = await buildClaudeDesktopInstallPreview(targetPath, {
|
|
137
|
+
env: terminalContext.env
|
|
138
|
+
});
|
|
139
|
+
const report = renderClaudeDesktopInstallPreview(preview);
|
|
140
|
+
if (outputPath) {
|
|
141
|
+
await writeFile(outputPath, report, "utf8");
|
|
142
|
+
}
|
|
143
|
+
io.writeStdout(report);
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
const message = error instanceof Error ? error.message : "Unknown install preview error.";
|
|
148
|
+
io.writeStderr(message);
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
let matrix = await buildCompatibilityMatrix(targetPath, {
|
|
153
|
+
env: terminalContext.env
|
|
154
|
+
});
|
|
155
|
+
if (clientFilter) {
|
|
156
|
+
const filteredMatrix = filterCompatibilityMatrix(matrix, clientFilter);
|
|
157
|
+
if (!filteredMatrix) {
|
|
158
|
+
io.writeStderr(`Unknown compatibility client: ${clientFilter}`);
|
|
159
|
+
return 2;
|
|
160
|
+
}
|
|
161
|
+
matrix = filteredMatrix;
|
|
162
|
+
}
|
|
163
|
+
const report = jsonOutput
|
|
164
|
+
? JSON.stringify({ schemaVersion: "1.0.0", ...matrix }, null, 2)
|
|
165
|
+
: renderCompatibilityReport(matrix);
|
|
166
|
+
if (outputPath) {
|
|
167
|
+
await writeFile(outputPath, report, "utf8");
|
|
168
|
+
}
|
|
169
|
+
io.writeStdout(report);
|
|
170
|
+
return matrixExitCode(matrix);
|
|
171
|
+
}
|
|
89
172
|
if (command !== "check") {
|
|
90
173
|
printUsage(io);
|
|
91
174
|
return 2;
|
package/package.json
CHANGED