codex-plugin-doctor 0.1.5 → 0.2.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 +6 -1
- package/dist/compatibility/compatibility-matrix.d.ts +13 -0
- package/dist/compatibility/compatibility-matrix.js +129 -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 +58 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -157,6 +157,11 @@ 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 . --json
|
|
164
|
+
codex-plugin-doctor compat . --json --output compatibility.json
|
|
160
165
|
codex-plugin-doctor check .
|
|
161
166
|
codex-plugin-doctor check . --json
|
|
162
167
|
codex-plugin-doctor check . --json --output report.json
|
|
@@ -202,7 +207,7 @@ jobs:
|
|
|
202
207
|
runs-on: ubuntu-latest
|
|
203
208
|
steps:
|
|
204
209
|
- uses: actions/checkout@v4
|
|
205
|
-
- uses: Esquetta/CodexPluginDoctor@v0.
|
|
210
|
+
- uses: Esquetta/CodexPluginDoctor@v0.2.0
|
|
206
211
|
with:
|
|
207
212
|
path: .
|
|
208
213
|
runtime: "false"
|
|
@@ -0,0 +1,13 @@
|
|
|
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 declare function buildCompatibilityMatrix(targetPath: string): Promise<CompatibilityMatrix>;
|
|
13
|
+
export declare function matrixExitCode(matrix: CompatibilityMatrix): 0 | 1;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { validatePlugin } from "../core/validate-plugin.js";
|
|
4
|
+
async function fileExists(targetPath) {
|
|
5
|
+
try {
|
|
6
|
+
const details = await stat(targetPath);
|
|
7
|
+
return details.isFile();
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function statusFromCheckResult(result) {
|
|
14
|
+
if (result.status === "fail") {
|
|
15
|
+
return "fail";
|
|
16
|
+
}
|
|
17
|
+
if (result.status === "warn") {
|
|
18
|
+
return "warn";
|
|
19
|
+
}
|
|
20
|
+
return "pass";
|
|
21
|
+
}
|
|
22
|
+
async function readMcpConfigPath(targetPath) {
|
|
23
|
+
const rootPath = path.resolve(targetPath);
|
|
24
|
+
const directMcpPath = path.join(rootPath, ".mcp.json");
|
|
25
|
+
if (await fileExists(directMcpPath)) {
|
|
26
|
+
return directMcpPath;
|
|
27
|
+
}
|
|
28
|
+
const manifestPath = path.join(rootPath, ".codex-plugin", "plugin.json");
|
|
29
|
+
if (!(await fileExists(manifestPath))) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
34
|
+
return typeof manifest.mcpServers === "string"
|
|
35
|
+
? path.resolve(rootPath, manifest.mcpServers)
|
|
36
|
+
: null;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function hasCodexManifest(targetPath) {
|
|
43
|
+
return fileExists(path.join(path.resolve(targetPath), ".codex-plugin", "plugin.json"));
|
|
44
|
+
}
|
|
45
|
+
async function checkGenericMcp(targetPath) {
|
|
46
|
+
const mcpConfigPath = await readMcpConfigPath(targetPath);
|
|
47
|
+
if (!mcpConfigPath || !(await fileExists(mcpConfigPath))) {
|
|
48
|
+
return {
|
|
49
|
+
client: "Generic MCP",
|
|
50
|
+
status: "skipped",
|
|
51
|
+
summary: "No MCP config found.",
|
|
52
|
+
details: ["Expected `.mcp.json` or manifest `mcpServers` reference."]
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(await readFile(mcpConfigPath, "utf8"));
|
|
57
|
+
const servers = parsed.mcpServers;
|
|
58
|
+
if (typeof servers !== "object" ||
|
|
59
|
+
servers === null ||
|
|
60
|
+
Array.isArray(servers) ||
|
|
61
|
+
Object.keys(servers).length === 0) {
|
|
62
|
+
return {
|
|
63
|
+
client: "Generic MCP",
|
|
64
|
+
status: "fail",
|
|
65
|
+
summary: "MCP config does not contain a non-empty `mcpServers` object.",
|
|
66
|
+
details: [mcpConfigPath]
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
client: "Generic MCP",
|
|
71
|
+
status: "pass",
|
|
72
|
+
summary: "MCP server config is valid.",
|
|
73
|
+
details: [mcpConfigPath]
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return {
|
|
78
|
+
client: "Generic MCP",
|
|
79
|
+
status: "fail",
|
|
80
|
+
summary: "MCP config is not valid JSON.",
|
|
81
|
+
details: [mcpConfigPath]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export async function buildCompatibilityMatrix(targetPath) {
|
|
86
|
+
const rootPath = path.resolve(targetPath);
|
|
87
|
+
const genericMcpResult = await checkGenericMcp(rootPath);
|
|
88
|
+
const codexResult = await validatePlugin(rootPath);
|
|
89
|
+
const codexStatus = statusFromCheckResult(codexResult);
|
|
90
|
+
const codexCompatibility = !await hasCodexManifest(rootPath)
|
|
91
|
+
&& genericMcpResult.status === "pass"
|
|
92
|
+
? {
|
|
93
|
+
client: "Codex",
|
|
94
|
+
status: "skipped",
|
|
95
|
+
summary: "No Codex plugin manifest found; treating target as a standalone MCP package.",
|
|
96
|
+
details: ["Add `.codex-plugin/plugin.json` if this package should be installable as a Codex plugin."]
|
|
97
|
+
}
|
|
98
|
+
: {
|
|
99
|
+
client: "Codex",
|
|
100
|
+
status: codexStatus,
|
|
101
|
+
summary: codexStatus === "pass"
|
|
102
|
+
? "Codex plugin package validation passed."
|
|
103
|
+
: "Codex plugin package validation produced findings.",
|
|
104
|
+
details: codexResult.findings.map((finding) => finding.id)
|
|
105
|
+
};
|
|
106
|
+
const results = [
|
|
107
|
+
codexCompatibility,
|
|
108
|
+
genericMcpResult,
|
|
109
|
+
{
|
|
110
|
+
client: "Claude Desktop",
|
|
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
|
+
}
|
|
121
|
+
];
|
|
122
|
+
return {
|
|
123
|
+
targetPath: rootPath,
|
|
124
|
+
results
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
export function matrixExitCode(matrix) {
|
|
128
|
+
return matrix.results.some((result) => result.status === "fail") ? 1 : 0;
|
|
129
|
+
}
|
|
@@ -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,11 @@
|
|
|
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";
|
|
3
4
|
import { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
|
|
4
5
|
import { initPluginPackage } from "./core/init-plugin.js";
|
|
5
6
|
import { runCheck } from "./index.js";
|
|
6
7
|
import { renderInstalledSummary } from "./reporting/render-installed-summary.js";
|
|
8
|
+
import { renderCompatibilityReport } from "./reporting/render-compatibility-report.js";
|
|
7
9
|
import { renderJsonReport } from "./reporting/render-json-report.js";
|
|
8
10
|
import { buildMarkdownReport } from "./reporting/render-markdown-report.js";
|
|
9
11
|
import { renderRuleExplanation } from "./reporting/render-rule-explanation.js";
|
|
@@ -23,7 +25,7 @@ const defaultIo = {
|
|
|
23
25
|
}
|
|
24
26
|
};
|
|
25
27
|
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");
|
|
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");
|
|
27
29
|
}
|
|
28
30
|
function renderInstalledPlugins(plugins) {
|
|
29
31
|
const lines = [
|
|
@@ -42,6 +44,25 @@ function renderInstalledPlugins(plugins) {
|
|
|
42
44
|
}
|
|
43
45
|
return lines.join("\n");
|
|
44
46
|
}
|
|
47
|
+
const compatibilityClientAliases = {
|
|
48
|
+
codex: "Codex",
|
|
49
|
+
"generic-mcp": "Generic MCP",
|
|
50
|
+
generic: "Generic MCP",
|
|
51
|
+
mcp: "Generic MCP",
|
|
52
|
+
"claude-desktop": "Claude Desktop",
|
|
53
|
+
claude: "Claude Desktop",
|
|
54
|
+
cursor: "Cursor"
|
|
55
|
+
};
|
|
56
|
+
function filterCompatibilityMatrix(matrix, clientFilter) {
|
|
57
|
+
const client = compatibilityClientAliases[clientFilter.toLowerCase()];
|
|
58
|
+
if (!client) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
...matrix,
|
|
63
|
+
results: matrix.results.filter((result) => result.client === client)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
45
66
|
export async function runCli(args, io = defaultIo, options = {}) {
|
|
46
67
|
const [command, maybePath, ...remainingArgs] = args;
|
|
47
68
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
@@ -86,6 +107,42 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
86
107
|
].join("\n"));
|
|
87
108
|
return 0;
|
|
88
109
|
}
|
|
110
|
+
if (command === "compat") {
|
|
111
|
+
const targetPath = maybePath && !maybePath.startsWith("--") ? maybePath : ".";
|
|
112
|
+
const compatFlags = maybePath && maybePath.startsWith("--")
|
|
113
|
+
? [maybePath, ...remainingArgs]
|
|
114
|
+
: remainingArgs;
|
|
115
|
+
const jsonOutput = compatFlags.includes("--json");
|
|
116
|
+
const clientIndex = compatFlags.indexOf("--client");
|
|
117
|
+
const clientFilter = clientIndex === -1 ? null : compatFlags[clientIndex + 1];
|
|
118
|
+
const outputIndex = compatFlags.indexOf("--output");
|
|
119
|
+
const outputPath = outputIndex === -1 ? null : compatFlags[outputIndex + 1];
|
|
120
|
+
if (clientIndex !== -1 && (!clientFilter || clientFilter.startsWith("--"))) {
|
|
121
|
+
io.writeStderr("Missing client after --client.");
|
|
122
|
+
return 2;
|
|
123
|
+
}
|
|
124
|
+
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
125
|
+
io.writeStderr("Missing path after --output.");
|
|
126
|
+
return 2;
|
|
127
|
+
}
|
|
128
|
+
let matrix = await buildCompatibilityMatrix(targetPath);
|
|
129
|
+
if (clientFilter) {
|
|
130
|
+
const filteredMatrix = filterCompatibilityMatrix(matrix, clientFilter);
|
|
131
|
+
if (!filteredMatrix) {
|
|
132
|
+
io.writeStderr(`Unknown compatibility client: ${clientFilter}`);
|
|
133
|
+
return 2;
|
|
134
|
+
}
|
|
135
|
+
matrix = filteredMatrix;
|
|
136
|
+
}
|
|
137
|
+
const report = jsonOutput
|
|
138
|
+
? JSON.stringify({ schemaVersion: "1.0.0", ...matrix }, null, 2)
|
|
139
|
+
: renderCompatibilityReport(matrix);
|
|
140
|
+
if (outputPath) {
|
|
141
|
+
await writeFile(outputPath, report, "utf8");
|
|
142
|
+
}
|
|
143
|
+
io.writeStdout(report);
|
|
144
|
+
return matrixExitCode(matrix);
|
|
145
|
+
}
|
|
89
146
|
if (command !== "check") {
|
|
90
147
|
printUsage(io);
|
|
91
148
|
return 2;
|
package/package.json
CHANGED