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 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.1.4
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,2 @@
1
+ import type { CompatibilityMatrix } from "../compatibility/compatibility-matrix.js";
2
+ export declare function renderCompatibilityReport(matrix: CompatibilityMatrix): string;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "0.1.5",
3
+ "version": "0.2.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",