codex-plugin-doctor 0.1.4 → 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 +41 -0
- package/dist/compatibility/compatibility-matrix.d.ts +13 -0
- package/dist/compatibility/compatibility-matrix.js +129 -0
- package/dist/core/doctor-config.d.ts +7 -0
- package/dist/core/doctor-config.js +45 -0
- package/dist/core/init-plugin.d.ts +6 -0
- package/dist/core/init-plugin.js +39 -0
- package/dist/core/validate-plugin.js +14 -0
- package/dist/reporting/render-compatibility-report.d.ts +2 -0
- package/dist/reporting/render-compatibility-report.js +18 -0
- package/dist/reporting/render-installed-summary.d.ts +7 -0
- package/dist/reporting/render-installed-summary.js +22 -0
- package/dist/reporting/render-rule-explanation.d.ts +2 -0
- package/dist/reporting/render-rule-explanation.js +26 -0
- package/dist/reporting/render-sarif-report.d.ts +2 -0
- package/dist/reporting/render-sarif-report.js +55 -0
- package/dist/rules/rule-catalog.d.ts +13 -0
- package/dist/rules/rule-catalog.js +194 -0
- package/dist/run-cli.js +128 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,7 +82,9 @@ If you already have Codex installed locally and do not know plugin paths, discov
|
|
|
82
82
|
```bash
|
|
83
83
|
codex-plugin-doctor list --installed
|
|
84
84
|
codex-plugin-doctor check --installed
|
|
85
|
+
codex-plugin-doctor check --installed --all-summary
|
|
85
86
|
codex-plugin-doctor check --installed github
|
|
87
|
+
codex-plugin-doctor explain plugin.manifest.missing
|
|
86
88
|
```
|
|
87
89
|
|
|
88
90
|
Run from source:
|
|
@@ -154,23 +156,61 @@ Run these from a Codex plugin package root:
|
|
|
154
156
|
|
|
155
157
|
```bash
|
|
156
158
|
codex-plugin-doctor --version
|
|
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
|
|
157
165
|
codex-plugin-doctor check .
|
|
158
166
|
codex-plugin-doctor check . --json
|
|
159
167
|
codex-plugin-doctor check . --json --output report.json
|
|
160
168
|
codex-plugin-doctor check . --markdown --output report.md
|
|
169
|
+
codex-plugin-doctor check . --sarif --output results.sarif
|
|
161
170
|
codex-plugin-doctor check . --ascii
|
|
162
171
|
codex-plugin-doctor check . --no-animations
|
|
163
172
|
codex-plugin-doctor check . --runtime
|
|
173
|
+
codex-plugin-doctor check . --config .codex-doctor.json
|
|
164
174
|
codex-plugin-doctor check . --json --runtime --verbose-runtime
|
|
165
175
|
```
|
|
166
176
|
|
|
177
|
+
Optional local policy file:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"ignoreRules": ["plugin.heuristic.description.too_long"],
|
|
182
|
+
"failOnWarnings": true
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
167
186
|
Run these when you want Codex Plugin Doctor to find plugins from the local Codex installation:
|
|
168
187
|
|
|
169
188
|
```bash
|
|
170
189
|
codex-plugin-doctor list --installed
|
|
171
190
|
codex-plugin-doctor check --installed
|
|
191
|
+
codex-plugin-doctor check --installed --all-summary
|
|
172
192
|
codex-plugin-doctor check --installed github
|
|
173
193
|
codex-plugin-doctor check --installed github --runtime --no-animations
|
|
194
|
+
codex-plugin-doctor explain plugin.security.hard_coded_secret
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## GitHub Action
|
|
198
|
+
|
|
199
|
+
```yaml
|
|
200
|
+
name: Validate Codex plugin
|
|
201
|
+
|
|
202
|
+
on:
|
|
203
|
+
pull_request:
|
|
204
|
+
|
|
205
|
+
jobs:
|
|
206
|
+
doctor:
|
|
207
|
+
runs-on: ubuntu-latest
|
|
208
|
+
steps:
|
|
209
|
+
- uses: actions/checkout@v4
|
|
210
|
+
- uses: Esquetta/CodexPluginDoctor@v0.2.0
|
|
211
|
+
with:
|
|
212
|
+
path: .
|
|
213
|
+
runtime: "false"
|
|
174
214
|
```
|
|
175
215
|
|
|
176
216
|
To self-test this repository after cloning it:
|
|
@@ -196,6 +236,7 @@ The validator is tuned against local fixtures and real marketplace-style plugin
|
|
|
196
236
|
- [Real-World Validation Workflow](./docs/engineering/real-world-validation-workflow.md)
|
|
197
237
|
- [Validation Sessions](./validation-sessions/README.md)
|
|
198
238
|
- [Examples](./examples/README.md)
|
|
239
|
+
- [Rule Catalog](./docs/rules/catalog.md)
|
|
199
240
|
|
|
200
241
|
Recent validation waves covered:
|
|
201
242
|
|
|
@@ -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,7 @@
|
|
|
1
|
+
import type { CheckResult } from "../domain/types.js";
|
|
2
|
+
export interface DoctorConfig {
|
|
3
|
+
ignoreRules: string[];
|
|
4
|
+
failOnWarnings: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function loadDoctorConfig(targetPath: string, explicitConfigPath?: string | null): Promise<DoctorConfig>;
|
|
7
|
+
export declare function applyDoctorConfig(result: CheckResult, config: DoctorConfig): CheckResult;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const defaultConfig = {
|
|
4
|
+
ignoreRules: [],
|
|
5
|
+
failOnWarnings: false
|
|
6
|
+
};
|
|
7
|
+
function isStringArray(value) {
|
|
8
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
9
|
+
}
|
|
10
|
+
export async function loadDoctorConfig(targetPath, explicitConfigPath) {
|
|
11
|
+
const configPath = explicitConfigPath
|
|
12
|
+
? path.resolve(explicitConfigPath)
|
|
13
|
+
: path.join(path.resolve(targetPath), ".codex-doctor.json");
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(await readFile(configPath, "utf8"));
|
|
16
|
+
return {
|
|
17
|
+
ignoreRules: isStringArray(parsed.ignoreRules)
|
|
18
|
+
? parsed.ignoreRules
|
|
19
|
+
: defaultConfig.ignoreRules,
|
|
20
|
+
failOnWarnings: typeof parsed.failOnWarnings === "boolean"
|
|
21
|
+
? parsed.failOnWarnings
|
|
22
|
+
: defaultConfig.failOnWarnings
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return defaultConfig;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function applyDoctorConfig(result, config) {
|
|
30
|
+
const findings = result.findings.filter((finding) => !config.ignoreRules.includes(finding.id));
|
|
31
|
+
const hasFailures = findings.some((finding) => finding.severity === "fail");
|
|
32
|
+
const hasWarnings = findings.some((finding) => finding.severity === "warn");
|
|
33
|
+
const warningFailure = config.failOnWarnings && hasWarnings;
|
|
34
|
+
const status = hasFailures || warningFailure
|
|
35
|
+
? "fail"
|
|
36
|
+
: hasWarnings
|
|
37
|
+
? "warn"
|
|
38
|
+
: "pass";
|
|
39
|
+
return {
|
|
40
|
+
...result,
|
|
41
|
+
status,
|
|
42
|
+
exitCode: status === "fail" ? 1 : 0,
|
|
43
|
+
findings
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
function toPackageName(inputPath) {
|
|
4
|
+
return path.basename(path.resolve(inputPath))
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
7
|
+
.replace(/^-+|-+$/g, "") || "codex-plugin";
|
|
8
|
+
}
|
|
9
|
+
export async function initPluginPackage(targetPath) {
|
|
10
|
+
const rootPath = path.resolve(targetPath);
|
|
11
|
+
const manifestDirectory = path.join(rootPath, ".codex-plugin");
|
|
12
|
+
const skillsDirectory = path.join(rootPath, "skills", "hello");
|
|
13
|
+
const manifestPath = path.join(manifestDirectory, "plugin.json");
|
|
14
|
+
const skillPath = path.join(skillsDirectory, "SKILL.md");
|
|
15
|
+
await mkdir(manifestDirectory, { recursive: true });
|
|
16
|
+
await mkdir(skillsDirectory, { recursive: true });
|
|
17
|
+
await writeFile(manifestPath, `${JSON.stringify({
|
|
18
|
+
name: toPackageName(rootPath),
|
|
19
|
+
version: "0.1.0",
|
|
20
|
+
description: "A Codex plugin package scaffolded by Codex Plugin Doctor.",
|
|
21
|
+
skills: "skills"
|
|
22
|
+
}, null, 2)}\n`, "utf8");
|
|
23
|
+
await writeFile(skillPath, [
|
|
24
|
+
"---",
|
|
25
|
+
"name: hello",
|
|
26
|
+
"description: Use when verifying that this Codex plugin package loads correctly.",
|
|
27
|
+
"---",
|
|
28
|
+
"",
|
|
29
|
+
"# Hello",
|
|
30
|
+
"",
|
|
31
|
+
"This starter skill confirms the plugin package structure is valid.",
|
|
32
|
+
""
|
|
33
|
+
].join("\n"), "utf8");
|
|
34
|
+
return {
|
|
35
|
+
rootPath,
|
|
36
|
+
manifestPath,
|
|
37
|
+
skillPath
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -131,6 +131,12 @@ function countMatches(input, pattern) {
|
|
|
131
131
|
const matches = input.match(pattern);
|
|
132
132
|
return matches ? matches.length : 0;
|
|
133
133
|
}
|
|
134
|
+
function extractSupportAssetReferences(content) {
|
|
135
|
+
const references = [...content.matchAll(/`((?:scripts|templates|assets|examples)\/[^`]+)`/g)]
|
|
136
|
+
.map((match) => match[1].trim())
|
|
137
|
+
.filter((reference) => reference.length > 0);
|
|
138
|
+
return [...new Set(references)];
|
|
139
|
+
}
|
|
134
140
|
function isDescriptionLikelyVerbose(description, mode) {
|
|
135
141
|
const trimmed = description.trim();
|
|
136
142
|
const length = trimmed.length;
|
|
@@ -252,6 +258,14 @@ async function validateSkillDefinitions(discoveredPackage) {
|
|
|
252
258
|
isDescriptionLikelyVerbose(frontmatter.description, "skill")) {
|
|
253
259
|
findings.push(buildWarning("plugin.heuristic.skill_description.too_long", `The skill \`${skillDirectory.name}\` description is likely too verbose.`, "Overly long skill descriptions increase context cost and reduce the precision of skill matching.", `Shorten the \`description\` field in \`${skillFilePath}\` to a tightly scoped summary.`));
|
|
254
260
|
}
|
|
261
|
+
for (const supportReference of extractSupportAssetReferences(skillContent)) {
|
|
262
|
+
const supportPath = path.resolve(skillRoot, supportReference);
|
|
263
|
+
const supportFileExists = await fileExists(supportPath);
|
|
264
|
+
const supportDirectoryExists = await directoryExists(supportPath);
|
|
265
|
+
if (!supportFileExists && !supportDirectoryExists) {
|
|
266
|
+
findings.push(buildWarning("plugin.skill.asset_reference.missing", `The skill \`${skillDirectory.name}\` references missing support asset \`${supportReference}\`.`, "Skills that point to missing scripts, templates, assets, or examples are harder to execute and can fail when an agent follows the instructions.", `Create \`${supportPath}\` or update the reference in \`${skillFilePath}\`.`));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
255
269
|
}
|
|
256
270
|
return findings;
|
|
257
271
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CheckResult } from "../domain/types.js";
|
|
2
|
+
import type { InstalledPlugin } from "../core/discover-installed-plugins.js";
|
|
3
|
+
export interface InstalledPluginCheckResult {
|
|
4
|
+
plugin: InstalledPlugin;
|
|
5
|
+
result: CheckResult;
|
|
6
|
+
}
|
|
7
|
+
export declare function renderInstalledSummary(checkedPlugins: InstalledPluginCheckResult[]): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function renderInstalledSummary(checkedPlugins) {
|
|
2
|
+
const passCount = checkedPlugins.filter((item) => item.result.status === "pass").length;
|
|
3
|
+
const warnCount = checkedPlugins.filter((item) => item.result.status === "warn").length;
|
|
4
|
+
const failCount = checkedPlugins.filter((item) => item.result.status === "fail").length;
|
|
5
|
+
const lines = [
|
|
6
|
+
"Installed Plugin Summary",
|
|
7
|
+
"========================",
|
|
8
|
+
`Checked: ${checkedPlugins.length}`,
|
|
9
|
+
`Pass: ${passCount}`,
|
|
10
|
+
`Warn: ${warnCount}`,
|
|
11
|
+
`Fail: ${failCount}`,
|
|
12
|
+
"",
|
|
13
|
+
"Plugins",
|
|
14
|
+
"-------"
|
|
15
|
+
];
|
|
16
|
+
for (const item of checkedPlugins) {
|
|
17
|
+
const findingIds = item.result.findings.map((finding) => finding.id);
|
|
18
|
+
const suffix = findingIds.length > 0 ? ` (${findingIds.join(", ")})` : "";
|
|
19
|
+
lines.push(`${item.result.status.toUpperCase()} ${item.plugin.name} - ${item.plugin.relativePath}${suffix}`);
|
|
20
|
+
}
|
|
21
|
+
return lines.join("\n");
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function renderRuleExplanation(rule) {
|
|
2
|
+
return [
|
|
3
|
+
`Rule: ${rule.id}`,
|
|
4
|
+
"==============================",
|
|
5
|
+
`Category: ${rule.category}`,
|
|
6
|
+
`Default severity: ${rule.defaultSeverity}`,
|
|
7
|
+
"",
|
|
8
|
+
"Summary",
|
|
9
|
+
"-------",
|
|
10
|
+
rule.summary,
|
|
11
|
+
"",
|
|
12
|
+
"Why it matters",
|
|
13
|
+
"--------------",
|
|
14
|
+
rule.why,
|
|
15
|
+
"",
|
|
16
|
+
"Suggested fix",
|
|
17
|
+
"-------------",
|
|
18
|
+
rule.fix,
|
|
19
|
+
"",
|
|
20
|
+
"Example",
|
|
21
|
+
"-------",
|
|
22
|
+
rule.example,
|
|
23
|
+
"",
|
|
24
|
+
"Full catalog: docs/rules/catalog.md"
|
|
25
|
+
].join("\n");
|
|
26
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { findRuleDefinition } from "../rules/rule-catalog.js";
|
|
3
|
+
function levelForFinding(finding) {
|
|
4
|
+
return finding.severity === "fail" ? "error" : "warning";
|
|
5
|
+
}
|
|
6
|
+
export function renderSarifReport(result) {
|
|
7
|
+
const rules = [...new Set(result.findings.map((finding) => finding.id))]
|
|
8
|
+
.map((ruleId) => {
|
|
9
|
+
const catalogEntry = findRuleDefinition(ruleId);
|
|
10
|
+
return {
|
|
11
|
+
id: ruleId,
|
|
12
|
+
name: ruleId,
|
|
13
|
+
shortDescription: {
|
|
14
|
+
text: catalogEntry?.summary ?? ruleId
|
|
15
|
+
},
|
|
16
|
+
help: {
|
|
17
|
+
text: catalogEntry
|
|
18
|
+
? `${catalogEntry.why}\n\nSuggested fix: ${catalogEntry.fix}`
|
|
19
|
+
: "See the Codex Plugin Doctor report for remediation guidance."
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
const sarif = {
|
|
24
|
+
version: "2.1.0",
|
|
25
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
26
|
+
runs: [
|
|
27
|
+
{
|
|
28
|
+
tool: {
|
|
29
|
+
driver: {
|
|
30
|
+
name: "Codex Plugin Doctor",
|
|
31
|
+
informationUri: "https://github.com/Esquetta/CodexPluginDoctor",
|
|
32
|
+
rules
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
results: result.findings.map((finding) => ({
|
|
36
|
+
ruleId: finding.id,
|
|
37
|
+
level: levelForFinding(finding),
|
|
38
|
+
message: {
|
|
39
|
+
text: `${finding.message} Suggested fix: ${finding.suggestedFix}`
|
|
40
|
+
},
|
|
41
|
+
locations: [
|
|
42
|
+
{
|
|
43
|
+
physicalLocation: {
|
|
44
|
+
artifactLocation: {
|
|
45
|
+
uri: path.basename(result.targetPath)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}))
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
};
|
|
54
|
+
return JSON.stringify(sarif, null, 2);
|
|
55
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type RuleCategory = "package" | "skill" | "mcp" | "runtime" | "security";
|
|
2
|
+
export type RuleSeverity = "fail" | "warn";
|
|
3
|
+
export interface RuleDefinition {
|
|
4
|
+
id: string;
|
|
5
|
+
category: RuleCategory;
|
|
6
|
+
defaultSeverity: RuleSeverity;
|
|
7
|
+
summary: string;
|
|
8
|
+
why: string;
|
|
9
|
+
fix: string;
|
|
10
|
+
example: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const ruleCatalog: RuleDefinition[];
|
|
13
|
+
export declare function findRuleDefinition(id: string): RuleDefinition | null;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
export const ruleCatalog = [
|
|
2
|
+
{
|
|
3
|
+
id: "plugin.manifest.missing",
|
|
4
|
+
category: "package",
|
|
5
|
+
defaultSeverity: "fail",
|
|
6
|
+
summary: "The target directory is missing `.codex-plugin/plugin.json`.",
|
|
7
|
+
why: "Codex needs the plugin manifest as the package entry point. Without it, the directory cannot be treated as a plugin package.",
|
|
8
|
+
fix: "Run the doctor against a plugin package root, or create `.codex-plugin/plugin.json` with at least `name`, `version`, and `description`.",
|
|
9
|
+
example: '{ "name": "my-plugin", "version": "0.1.0", "description": "Adds focused Codex workflow helpers." }'
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: "plugin.manifest.name.missing",
|
|
13
|
+
category: "package",
|
|
14
|
+
defaultSeverity: "fail",
|
|
15
|
+
summary: "The plugin manifest is missing a stable `name` field.",
|
|
16
|
+
why: "Codex and release tooling need a stable package name for display, matching, and diagnostics.",
|
|
17
|
+
fix: "Add a kebab-case `name` field to `.codex-plugin/plugin.json`.",
|
|
18
|
+
example: '{ "name": "github-workflow-doctor" }'
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "plugin.manifest.version.missing",
|
|
22
|
+
category: "package",
|
|
23
|
+
defaultSeverity: "fail",
|
|
24
|
+
summary: "The plugin manifest is missing a `version` field.",
|
|
25
|
+
why: "Compatibility checks and release workflows cannot reason about package changes without a version.",
|
|
26
|
+
fix: "Add a semantic `version` field to `.codex-plugin/plugin.json`.",
|
|
27
|
+
example: '{ "version": "0.1.0" }'
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "plugin.manifest.description.missing",
|
|
31
|
+
category: "package",
|
|
32
|
+
defaultSeverity: "fail",
|
|
33
|
+
summary: "The plugin manifest is missing a `description` field.",
|
|
34
|
+
why: "Plugin surfaces and reviewers need concise package metadata to understand what the plugin does.",
|
|
35
|
+
fix: "Add a short, specific `description` field to `.codex-plugin/plugin.json`.",
|
|
36
|
+
example: '{ "description": "Validates GitHub PR automation workflows before release." }'
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "plugin.heuristic.description.too_long",
|
|
40
|
+
category: "package",
|
|
41
|
+
defaultSeverity: "warn",
|
|
42
|
+
summary: "The plugin manifest description is likely too verbose.",
|
|
43
|
+
why: "Verbose package metadata increases context cost and can dilute plugin discovery quality.",
|
|
44
|
+
fix: "Shorten the manifest description to a precise one- or two-sentence summary.",
|
|
45
|
+
example: "Good: `Audits Codex plugin packages before publishing.`"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "plugin.skills.path.missing",
|
|
49
|
+
category: "skill",
|
|
50
|
+
defaultSeverity: "fail",
|
|
51
|
+
summary: "The manifest points to a missing skills directory.",
|
|
52
|
+
why: "Codex cannot load packaged skills when the manifest references a directory that does not exist.",
|
|
53
|
+
fix: "Create the referenced skills directory or update the `skills` path in `.codex-plugin/plugin.json`.",
|
|
54
|
+
example: '{ "skills": "skills" }'
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "plugin.skill.skill_md.missing",
|
|
58
|
+
category: "skill",
|
|
59
|
+
defaultSeverity: "fail",
|
|
60
|
+
summary: "A skill directory does not contain `SKILL.md`.",
|
|
61
|
+
why: "`SKILL.md` is the required entry point for Codex to load skill instructions and metadata.",
|
|
62
|
+
fix: "Add `SKILL.md` with frontmatter containing at least `name` and `description`.",
|
|
63
|
+
example: "---\nname: repo-auditor\ndescription: Use when auditing repository health.\n---"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "plugin.skill.name.missing",
|
|
67
|
+
category: "skill",
|
|
68
|
+
defaultSeverity: "fail",
|
|
69
|
+
summary: "A skill `SKILL.md` file is missing `name` frontmatter.",
|
|
70
|
+
why: "Codex needs a stable skill name for matching, display, and diagnostics.",
|
|
71
|
+
fix: "Add a `name` field to the skill frontmatter.",
|
|
72
|
+
example: "---\nname: release-checker\n---"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "plugin.skill.description.missing",
|
|
76
|
+
category: "skill",
|
|
77
|
+
defaultSeverity: "fail",
|
|
78
|
+
summary: "A skill `SKILL.md` file is missing `description` frontmatter.",
|
|
79
|
+
why: "Skill descriptions drive discovery and implicit matching, so missing descriptions make skills harder to use.",
|
|
80
|
+
fix: "Add a scoped `description` field that says when the skill should be used.",
|
|
81
|
+
example: "---\ndescription: Use when preparing an npm release with verification gates.\n---"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "plugin.heuristic.skill_description.too_long",
|
|
85
|
+
category: "skill",
|
|
86
|
+
defaultSeverity: "warn",
|
|
87
|
+
summary: "A skill description is likely too verbose.",
|
|
88
|
+
why: "Long, vague descriptions increase context cost and reduce skill matching precision.",
|
|
89
|
+
fix: "Shorten the description while keeping concrete triggers, inputs, and output expectations.",
|
|
90
|
+
example: "Good: `Use when creating GitHub Actions release workflows for Node CLIs.`"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "plugin.skill.asset_reference.missing",
|
|
94
|
+
category: "skill",
|
|
95
|
+
defaultSeverity: "warn",
|
|
96
|
+
summary: "A skill references a missing local support asset.",
|
|
97
|
+
why: "Skills that point to missing scripts, templates, assets, or examples can fail when an agent follows the instructions.",
|
|
98
|
+
fix: "Create the referenced support file or update the backticked reference in `SKILL.md`.",
|
|
99
|
+
example: "If `SKILL.md` says `scripts/setup.ps1`, make sure that file exists inside the skill directory."
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "plugin.mcp.path.missing",
|
|
103
|
+
category: "mcp",
|
|
104
|
+
defaultSeverity: "fail",
|
|
105
|
+
summary: "The manifest points to a missing `.mcp.json` file.",
|
|
106
|
+
why: "Codex cannot load bundled MCP server definitions if the referenced config file does not exist.",
|
|
107
|
+
fix: "Create the referenced `.mcp.json` file or update the `mcpServers` path in the manifest.",
|
|
108
|
+
example: '{ "mcpServers": ".mcp.json" }'
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: "plugin.mcp.invalid_json",
|
|
112
|
+
category: "mcp",
|
|
113
|
+
defaultSeverity: "fail",
|
|
114
|
+
summary: "The referenced `.mcp.json` file is not valid JSON.",
|
|
115
|
+
why: "Codex must parse MCP configuration before it can start bundled servers.",
|
|
116
|
+
fix: "Fix the JSON syntax in the referenced `.mcp.json` file.",
|
|
117
|
+
example: '{ "mcpServers": { "doctor": { "command": "node", "args": ["server.js"] } } }'
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "plugin.mcp.invalid_shape",
|
|
121
|
+
category: "mcp",
|
|
122
|
+
defaultSeverity: "fail",
|
|
123
|
+
summary: "The `.mcp.json` file does not expose a valid `mcpServers` object.",
|
|
124
|
+
why: "Codex expects MCP configuration to be object-shaped with named server entries.",
|
|
125
|
+
fix: "Define a non-empty top-level `mcpServers` object.",
|
|
126
|
+
example: '{ "mcpServers": { "doctor": { "command": "node", "args": ["server.js"] } } }'
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: "plugin.mcp.server.invalid",
|
|
130
|
+
category: "mcp",
|
|
131
|
+
defaultSeverity: "fail",
|
|
132
|
+
summary: "An MCP server entry is not an object.",
|
|
133
|
+
why: "Codex cannot interpret server settings unless each server is represented as an object.",
|
|
134
|
+
fix: "Change the server entry to an object with transport options.",
|
|
135
|
+
example: '{ "mcpServers": { "doctor": { "command": "node" } } }'
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: "plugin.mcp.server.transport.missing",
|
|
139
|
+
category: "mcp",
|
|
140
|
+
defaultSeverity: "fail",
|
|
141
|
+
summary: "An MCP server entry is missing both `command` and `url`.",
|
|
142
|
+
why: "Codex needs either a stdio command or a streamable HTTP URL to connect to a server.",
|
|
143
|
+
fix: "Add `command` for stdio servers or `url` for remote servers.",
|
|
144
|
+
example: '{ "command": "node", "args": ["server.js"] }'
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "plugin.security.path_traversal",
|
|
148
|
+
category: "security",
|
|
149
|
+
defaultSeverity: "fail",
|
|
150
|
+
summary: "A manifest path escapes the plugin package root.",
|
|
151
|
+
why: "Paths outside the package root can expose unintended files and make package review unreliable.",
|
|
152
|
+
fix: "Keep manifest paths such as `skills` and `mcpServers` inside the plugin root.",
|
|
153
|
+
example: '{ "skills": "skills", "mcpServers": ".mcp.json" }'
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: "plugin.security.hard_coded_secret",
|
|
157
|
+
category: "security",
|
|
158
|
+
defaultSeverity: "fail",
|
|
159
|
+
summary: "An MCP server config contains a hard-coded secret-like env value.",
|
|
160
|
+
why: "Bundled credentials can leak through source control, npm packages, logs, or support bundles.",
|
|
161
|
+
fix: "Replace literal secrets with environment references or externally injected secrets.",
|
|
162
|
+
example: '{ "env": { "OPENAI_API_KEY": "${OPENAI_API_KEY}" } }'
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "plugin.runtime.exited_early",
|
|
166
|
+
category: "runtime",
|
|
167
|
+
defaultSeverity: "fail",
|
|
168
|
+
summary: "An MCP server exited before the startup probe completed.",
|
|
169
|
+
why: "A server that exits immediately is unlikely to remain available during normal Codex use.",
|
|
170
|
+
fix: "Run the configured command manually, inspect stderr, and fix startup exceptions or missing dependencies.",
|
|
171
|
+
example: "node server.js"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: "plugin.runtime.initialize.timeout",
|
|
175
|
+
category: "runtime",
|
|
176
|
+
defaultSeverity: "fail",
|
|
177
|
+
summary: "An MCP server did not answer `initialize` in time.",
|
|
178
|
+
why: "Codex cannot negotiate capabilities with a server that does not complete initialization.",
|
|
179
|
+
fix: "Ensure the server reads JSON-RPC from stdin, writes responses to stdout, and avoids slow startup work.",
|
|
180
|
+
example: "Respond to the `initialize` request before starting expensive background tasks."
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: "plugin.runtime.protocol.invalid_message",
|
|
184
|
+
category: "runtime",
|
|
185
|
+
defaultSeverity: "fail",
|
|
186
|
+
summary: "An MCP server wrote invalid JSON-RPC data to stdout.",
|
|
187
|
+
why: "MCP stdio transport requires newline-delimited JSON-RPC messages on stdout.",
|
|
188
|
+
fix: "Send logs to stderr and reserve stdout for JSON-RPC protocol messages only.",
|
|
189
|
+
example: "Use `console.error` for diagnostics in Node stdio servers."
|
|
190
|
+
}
|
|
191
|
+
];
|
|
192
|
+
export function findRuleDefinition(id) {
|
|
193
|
+
return ruleCatalog.find((rule) => rule.id === id) ?? null;
|
|
194
|
+
}
|
package/dist/run-cli.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
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 { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
|
|
5
|
+
import { initPluginPackage } from "./core/init-plugin.js";
|
|
3
6
|
import { runCheck } from "./index.js";
|
|
7
|
+
import { renderInstalledSummary } from "./reporting/render-installed-summary.js";
|
|
8
|
+
import { renderCompatibilityReport } from "./reporting/render-compatibility-report.js";
|
|
4
9
|
import { renderJsonReport } from "./reporting/render-json-report.js";
|
|
5
10
|
import { buildMarkdownReport } from "./reporting/render-markdown-report.js";
|
|
11
|
+
import { renderRuleExplanation } from "./reporting/render-rule-explanation.js";
|
|
12
|
+
import { renderSarifReport } from "./reporting/render-sarif-report.js";
|
|
6
13
|
import { renderTextReport } from "./reporting/render-text-report.js";
|
|
14
|
+
import { findRuleDefinition } from "./rules/rule-catalog.js";
|
|
7
15
|
import { createLiveStatusRenderer } from "./terminal/live-status-renderer.js";
|
|
8
16
|
import { determineOutputPolicy } from "./terminal/output-policy.js";
|
|
9
17
|
import { getSpinner } from "./terminal/spinner-registry.js";
|
|
@@ -17,7 +25,7 @@ const defaultIo = {
|
|
|
17
25
|
}
|
|
18
26
|
};
|
|
19
27
|
function printUsage(io) {
|
|
20
|
-
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 --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");
|
|
21
29
|
}
|
|
22
30
|
function renderInstalledPlugins(plugins) {
|
|
23
31
|
const lines = [
|
|
@@ -36,6 +44,25 @@ function renderInstalledPlugins(plugins) {
|
|
|
36
44
|
}
|
|
37
45
|
return lines.join("\n");
|
|
38
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
|
+
}
|
|
39
66
|
export async function runCli(args, io = defaultIo, options = {}) {
|
|
40
67
|
const [command, maybePath, ...remainingArgs] = args;
|
|
41
68
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
@@ -54,6 +81,68 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
54
81
|
io.writeStdout(renderInstalledPlugins(installedPlugins));
|
|
55
82
|
return 0;
|
|
56
83
|
}
|
|
84
|
+
if (command === "explain") {
|
|
85
|
+
if (!maybePath || maybePath.startsWith("--")) {
|
|
86
|
+
io.writeStderr("Missing finding id. Usage: codex-plugin-doctor explain <finding-id>");
|
|
87
|
+
return 2;
|
|
88
|
+
}
|
|
89
|
+
const rule = findRuleDefinition(maybePath);
|
|
90
|
+
if (!rule) {
|
|
91
|
+
io.writeStderr(`Unknown finding id: ${maybePath}`);
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
io.writeStdout(renderRuleExplanation(rule));
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
if (command === "init") {
|
|
98
|
+
const targetPath = maybePath && !maybePath.startsWith("--") ? maybePath : ".";
|
|
99
|
+
const result = await initPluginPackage(targetPath);
|
|
100
|
+
io.writeStdout([
|
|
101
|
+
"Initialized Codex plugin package",
|
|
102
|
+
`Root: ${result.rootPath}`,
|
|
103
|
+
`Manifest: ${result.manifestPath}`,
|
|
104
|
+
`Skill: ${result.skillPath}`,
|
|
105
|
+
"",
|
|
106
|
+
`Next: codex-plugin-doctor check ${result.rootPath}`
|
|
107
|
+
].join("\n"));
|
|
108
|
+
return 0;
|
|
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
|
+
}
|
|
57
146
|
if (command !== "check") {
|
|
58
147
|
printUsage(io);
|
|
59
148
|
return 2;
|
|
@@ -73,16 +162,24 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
73
162
|
: remainingArgs;
|
|
74
163
|
const jsonOutput = normalizedFlags.includes("--json");
|
|
75
164
|
const markdownOutput = normalizedFlags.includes("--markdown");
|
|
165
|
+
const sarifOutput = normalizedFlags.includes("--sarif");
|
|
76
166
|
const runtimeProbeEnabled = normalizedFlags.includes("--runtime");
|
|
77
167
|
const verboseRuntime = normalizedFlags.includes("--verbose-runtime");
|
|
78
168
|
const noAnimations = normalizedFlags.includes("--no-animations");
|
|
79
169
|
const asciiMode = normalizedFlags.includes("--ascii");
|
|
170
|
+
const installedSummary = normalizedFlags.includes("--all-summary");
|
|
80
171
|
const outputIndex = normalizedFlags.indexOf("--output");
|
|
81
172
|
const outputPath = outputIndex === -1 ? null : normalizedFlags[outputIndex + 1];
|
|
173
|
+
const configIndex = normalizedFlags.indexOf("--config");
|
|
174
|
+
const configPath = configIndex === -1 ? null : normalizedFlags[configIndex + 1];
|
|
82
175
|
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
83
176
|
io.writeStderr("Missing path after --output.");
|
|
84
177
|
return 2;
|
|
85
178
|
}
|
|
179
|
+
if (configIndex !== -1 && (!configPath || configPath.startsWith("--"))) {
|
|
180
|
+
io.writeStderr("Missing path after --config.");
|
|
181
|
+
return 2;
|
|
182
|
+
}
|
|
86
183
|
const outputPolicy = determineOutputPolicy({
|
|
87
184
|
jsonOutput,
|
|
88
185
|
markdownOutput,
|
|
@@ -102,39 +199,47 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
102
199
|
: "No installed Codex plugins found.");
|
|
103
200
|
return 1;
|
|
104
201
|
}
|
|
105
|
-
const
|
|
202
|
+
const checkedPlugins = [];
|
|
106
203
|
for (const plugin of installedPlugins) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
:
|
|
112
|
-
|
|
204
|
+
const config = await loadDoctorConfig(plugin.rootPath, configPath);
|
|
205
|
+
checkedPlugins.push({
|
|
206
|
+
plugin,
|
|
207
|
+
result: applyDoctorConfig(await runCheckImpl(plugin.rootPath, {
|
|
208
|
+
runtime: runtimeProbeEnabled,
|
|
209
|
+
runtimeTranscript: runtimeProbeEnabled && verboseRuntime
|
|
210
|
+
? (line) => io.writeStderr(line)
|
|
211
|
+
: undefined
|
|
212
|
+
}), config)
|
|
213
|
+
});
|
|
113
214
|
}
|
|
114
|
-
const report =
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
?
|
|
119
|
-
:
|
|
120
|
-
|
|
215
|
+
const report = installedSummary
|
|
216
|
+
? renderInstalledSummary(checkedPlugins)
|
|
217
|
+
: checkedPlugins
|
|
218
|
+
.map((item) => sarifOutput
|
|
219
|
+
? renderSarifReport(item.result)
|
|
220
|
+
: markdownOutput
|
|
221
|
+
? buildMarkdownReport(item.result, { runtimeProbeEnabled })
|
|
222
|
+
: jsonOutput
|
|
223
|
+
? renderJsonReport(item.result, { runtimeProbeEnabled })
|
|
224
|
+
: renderTextReport(item.result, { ascii: outputPolicy.style === "ascii" }))
|
|
225
|
+
.join("\n\n");
|
|
121
226
|
if (outputPath) {
|
|
122
227
|
await writeFile(outputPath, report, "utf8");
|
|
123
228
|
}
|
|
124
229
|
io.writeStdout(report);
|
|
125
|
-
return
|
|
230
|
+
return checkedPlugins.some((item) => item.result.exitCode === 1) ? 1 : 0;
|
|
126
231
|
}
|
|
127
232
|
const renderer = outputPolicy.interactive
|
|
128
233
|
&& !verboseRuntime
|
|
129
234
|
? createLiveStatusRenderer(io, getSpinner(outputPolicy.style === "ascii" ? "ascii" : "doctor"))
|
|
130
235
|
: null;
|
|
131
236
|
renderer?.start("Validating package");
|
|
132
|
-
const result = await runCheckImpl(targetPath, {
|
|
237
|
+
const result = applyDoctorConfig(await runCheckImpl(targetPath, {
|
|
133
238
|
runtime: runtimeProbeEnabled,
|
|
134
239
|
runtimeTranscript: runtimeProbeEnabled && verboseRuntime
|
|
135
240
|
? (line) => io.writeStderr(line)
|
|
136
241
|
: undefined
|
|
137
|
-
});
|
|
242
|
+
}), await loadDoctorConfig(targetPath, configPath));
|
|
138
243
|
if (renderer) {
|
|
139
244
|
if (result.status === "fail") {
|
|
140
245
|
renderer.stopFailure("Validation failed");
|
|
@@ -145,9 +250,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
145
250
|
}
|
|
146
251
|
const report = markdownOutput
|
|
147
252
|
? buildMarkdownReport(result, { runtimeProbeEnabled })
|
|
148
|
-
:
|
|
149
|
-
?
|
|
150
|
-
:
|
|
253
|
+
: sarifOutput
|
|
254
|
+
? renderSarifReport(result)
|
|
255
|
+
: jsonOutput
|
|
256
|
+
? renderJsonReport(result, { runtimeProbeEnabled })
|
|
257
|
+
: renderTextReport(result, { ascii: outputPolicy.style === "ascii" });
|
|
151
258
|
if (outputPath) {
|
|
152
259
|
await writeFile(outputPath, report, "utf8");
|
|
153
260
|
}
|
package/package.json
CHANGED