codex-plugin-doctor 0.11.0 → 0.13.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 +28 -3
- package/dist/audit/ecosystem-audit.d.ts +36 -0
- package/dist/audit/ecosystem-audit.js +135 -0
- package/dist/core/doctor-export-bundle.d.ts +22 -0
- package/dist/core/doctor-export-bundle.js +79 -0
- package/dist/core/doctor-recommendations.d.ts +42 -0
- package/dist/core/doctor-recommendations.js +161 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/mcp/generic-mcp-doctor.d.ts +16 -0
- package/dist/mcp/generic-mcp-doctor.js +166 -0
- package/dist/policy/policy-packs.d.ts +9 -0
- package/dist/policy/policy-packs.js +33 -0
- package/dist/run-cli.js +200 -5
- package/dist/security/security-audit.d.ts +2 -0
- package/dist/security/security-audit.js +40 -33
- package/dist/security/trust-score.d.ts +23 -0
- package/dist/security/trust-score.js +196 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -98,9 +98,12 @@ codex-plugin-doctor list --installed
|
|
|
98
98
|
codex-plugin-doctor check --installed
|
|
99
99
|
codex-plugin-doctor check --installed --all-summary
|
|
100
100
|
codex-plugin-doctor check --installed --compat --all-summary
|
|
101
|
+
codex-plugin-doctor audit --installed --security --compat
|
|
102
|
+
codex-plugin-doctor audit --installed --security --compat --policy security
|
|
103
|
+
codex-plugin-doctor mcp path/to/mcp-package
|
|
101
104
|
codex-plugin-doctor check --installed github
|
|
102
|
-
codex-plugin-doctor explain plugin.manifest.missing
|
|
103
|
-
```
|
|
105
|
+
codex-plugin-doctor explain plugin.manifest.missing
|
|
106
|
+
```
|
|
104
107
|
|
|
105
108
|
Run from source:
|
|
106
109
|
|
|
@@ -173,11 +176,23 @@ Run these from a Codex plugin package root:
|
|
|
173
176
|
codex-plugin-doctor --version
|
|
174
177
|
codex-plugin-doctor self-test
|
|
175
178
|
codex-plugin-doctor doctor
|
|
179
|
+
codex-plugin-doctor doctor recommend .
|
|
180
|
+
codex-plugin-doctor doctor recommend . --json --output recommendations.json
|
|
181
|
+
codex-plugin-doctor doctor trust .
|
|
182
|
+
codex-plugin-doctor doctor trust . --json --output trust-score.json
|
|
183
|
+
codex-plugin-doctor doctor export --bundle .
|
|
184
|
+
codex-plugin-doctor doctor export --bundle . --output doctor-bundle.json
|
|
176
185
|
codex-plugin-doctor doctor snapshot
|
|
177
186
|
codex-plugin-doctor doctor snapshot --json
|
|
178
187
|
codex-plugin-doctor doctor snapshot --output doctor-snapshot.json
|
|
179
188
|
codex-plugin-doctor doctor clients
|
|
180
189
|
codex-plugin-doctor doctor --update-check
|
|
190
|
+
codex-plugin-doctor audit --installed
|
|
191
|
+
codex-plugin-doctor audit --installed --security --compat
|
|
192
|
+
codex-plugin-doctor audit --installed --security --compat --json --output local-audit.json
|
|
193
|
+
codex-plugin-doctor mcp .
|
|
194
|
+
codex-plugin-doctor mcp . --json
|
|
195
|
+
codex-plugin-doctor mcp . --json --output mcp-doctor.json
|
|
181
196
|
codex-plugin-doctor init my-plugin
|
|
182
197
|
codex-plugin-doctor init my-mcp --template mcp-stdio
|
|
183
198
|
codex-plugin-doctor init remote-mcp --template mcp-http
|
|
@@ -185,6 +200,7 @@ codex-plugin-doctor init runtime-demo --template full-runtime
|
|
|
185
200
|
codex-plugin-doctor security .
|
|
186
201
|
codex-plugin-doctor security . --scorecard
|
|
187
202
|
codex-plugin-doctor security . --json
|
|
203
|
+
codex-plugin-doctor security . --policy security
|
|
188
204
|
codex-plugin-doctor compat .
|
|
189
205
|
codex-plugin-doctor compat . --all --scorecard
|
|
190
206
|
codex-plugin-doctor compat . --client codex
|
|
@@ -204,6 +220,9 @@ codex-plugin-doctor check .
|
|
|
204
220
|
codex-plugin-doctor check . --profile ci
|
|
205
221
|
codex-plugin-doctor check . --profile strict
|
|
206
222
|
codex-plugin-doctor check . --profile publish
|
|
223
|
+
codex-plugin-doctor check . --policy codex-publish
|
|
224
|
+
codex-plugin-doctor check . --policy mcp-strict
|
|
225
|
+
codex-plugin-doctor check . --policy security
|
|
207
226
|
codex-plugin-doctor check . --json
|
|
208
227
|
codex-plugin-doctor check . --explain
|
|
209
228
|
codex-plugin-doctor check . --json --output report.json
|
|
@@ -227,7 +246,13 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
|
|
|
227
246
|
|
|
228
247
|
`self-test` runs the bundled runtime-complete sample through static validation, runtime MCP probes, and the compatibility scorecard. It is the fastest post-install check after `npm install -g codex-plugin-doctor`.
|
|
229
248
|
|
|
230
|
-
`doctor` checks the local environment, including package version, platform, Node version, npm global prefix, Codex home, and Codex plugin cache visibility. The text output also includes recommended next commands for self-test, installed plugin discovery, runtime checks, compatibility scoring, and CI setup. `doctor snapshot` creates a redacted diagnostics bundle with environment health, client config readiness, installed plugin metadata, and next commands. Add `--json` for machine-readable output or `--output doctor-snapshot.json` to write the bundle to disk. `doctor clients` reports local Codex, Claude Desktop, Cursor, Cline, and Windsurf config readiness. `doctor --update-check` compares the installed CLI version with the latest npm version and prints the upgrade command when a newer release is available.
|
|
249
|
+
`doctor` checks the local environment, including package version, platform, Node version, npm global prefix, Codex home, and Codex plugin cache visibility. The text output also includes recommended next commands for self-test, installed plugin discovery, runtime checks, compatibility scoring, and CI setup. `doctor recommend <path>` turns validation, security, and compatibility signals into a prioritized action plan with blocker, high, medium, and info actions. Add `--json` for automation or `--output recommendations.json` to write the report to disk. `doctor trust <path>` creates a local trust score from package lifecycle scripts, dependency specs, and MCP security findings. Use it before release when you want supply-chain risks summarized as one score. `doctor export --bundle <path>` creates a redacted operator handoff bundle that includes validation JSON, security scorecard data, compatibility matrix, recommendations, and trust score in one file. `doctor snapshot` creates a redacted diagnostics bundle with environment health, client config readiness, installed plugin metadata, and next commands. Add `--json` for machine-readable output or `--output doctor-snapshot.json` to write the bundle to disk. `doctor clients` reports local Codex, Claude Desktop, Cursor, Cline, and Windsurf config readiness. `doctor --update-check` compares the installed CLI version with the latest npm version and prints the upgrade command when a newer release is available.
|
|
250
|
+
|
|
251
|
+
`audit --installed` runs a local ecosystem audit against every discovered Codex plugin in the installed plugin cache. Add `--security` to include security scorecards, `--compat` to include the all-client compatibility matrix, and `--json --output local-audit.json` when you want a shareable machine-readable report.
|
|
252
|
+
|
|
253
|
+
`--policy codex-publish|mcp-strict|security` applies opinionated gates without requiring a local `.codex-doctor.json`. `codex-publish` fails warnings and enables runtime probes for release checks, `mcp-strict` does the same for MCP-heavy packages, and `security` fails warning-level security findings so advisory risks can block a local audit or CI gate.
|
|
254
|
+
|
|
255
|
+
`mcp <path>` diagnoses generic MCP packages that may not have a Codex plugin manifest. It looks for `.mcp.json` or a manifest `mcpServers` reference, validates the top-level `mcpServers` object and server transports, adds MCP command-surface security findings, and includes the all-client compatibility matrix in the same report.
|
|
231
256
|
|
|
232
257
|
`init [path] --template ...` creates targeted starter packages. `skill-only` is the default minimal skill package, `mcp-stdio` adds a local stdio MCP config and mock server, `mcp-http` scaffolds a streamable HTTP MCP config, and `full-runtime` generates a stdio sample that passes the runtime protocol probes.
|
|
233
258
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { CompatibilityMatrix } from "../compatibility/compatibility-matrix.js";
|
|
2
|
+
import type { InstalledPlugin } from "../core/discover-installed-plugins.js";
|
|
3
|
+
import type { CheckResult } from "../domain/types.js";
|
|
4
|
+
import type { SecurityAudit } from "../security/security-audit.js";
|
|
5
|
+
export interface EcosystemAuditOptions {
|
|
6
|
+
env?: Record<string, string | undefined>;
|
|
7
|
+
platform?: NodeJS.Platform;
|
|
8
|
+
filter?: string | null;
|
|
9
|
+
includeSecurity?: boolean;
|
|
10
|
+
includeCompatibility?: boolean;
|
|
11
|
+
failOnWarnings?: boolean;
|
|
12
|
+
validatePlugin: (targetPath: string) => Promise<CheckResult>;
|
|
13
|
+
}
|
|
14
|
+
export interface EcosystemAuditPluginResult {
|
|
15
|
+
plugin: InstalledPlugin;
|
|
16
|
+
status: "pass" | "warn" | "fail";
|
|
17
|
+
validation: CheckResult;
|
|
18
|
+
security?: SecurityAudit;
|
|
19
|
+
compatibility?: CompatibilityMatrix;
|
|
20
|
+
}
|
|
21
|
+
export interface EcosystemAuditReport {
|
|
22
|
+
schemaVersion: "1.0.0";
|
|
23
|
+
generatedAt: string;
|
|
24
|
+
status: "pass" | "warn" | "fail";
|
|
25
|
+
summary: {
|
|
26
|
+
totalPlugins: number;
|
|
27
|
+
pass: number;
|
|
28
|
+
warn: number;
|
|
29
|
+
fail: number;
|
|
30
|
+
};
|
|
31
|
+
plugins: EcosystemAuditPluginResult[];
|
|
32
|
+
priorityActions: string[];
|
|
33
|
+
}
|
|
34
|
+
export declare function buildEcosystemAudit(options: EcosystemAuditOptions): Promise<EcosystemAuditReport>;
|
|
35
|
+
export declare function renderEcosystemAuditJson(report: EcosystemAuditReport): string;
|
|
36
|
+
export declare function renderEcosystemAudit(report: EcosystemAuditReport): string;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { buildCompatibilityMatrix, matrixExitCode } from "../compatibility/compatibility-matrix.js";
|
|
2
|
+
import { discoverInstalledPlugins, filterInstalledPlugins } from "../core/discover-installed-plugins.js";
|
|
3
|
+
import { buildSecurityAudit } from "../security/security-audit.js";
|
|
4
|
+
function mergeStatus(statuses) {
|
|
5
|
+
if (statuses.includes("fail")) {
|
|
6
|
+
return "fail";
|
|
7
|
+
}
|
|
8
|
+
if (statuses.includes("warn")) {
|
|
9
|
+
return "warn";
|
|
10
|
+
}
|
|
11
|
+
return "pass";
|
|
12
|
+
}
|
|
13
|
+
function compatibilityStatus(matrix) {
|
|
14
|
+
if (!matrix) {
|
|
15
|
+
return "pass";
|
|
16
|
+
}
|
|
17
|
+
if (matrixExitCode(matrix) === 1) {
|
|
18
|
+
return "fail";
|
|
19
|
+
}
|
|
20
|
+
return matrix.results.some((result) => result.status === "warn") ? "warn" : "pass";
|
|
21
|
+
}
|
|
22
|
+
function summarizePlugins(plugins) {
|
|
23
|
+
return {
|
|
24
|
+
totalPlugins: plugins.length,
|
|
25
|
+
pass: plugins.filter((plugin) => plugin.status === "pass").length,
|
|
26
|
+
warn: plugins.filter((plugin) => plugin.status === "warn").length,
|
|
27
|
+
fail: plugins.filter((plugin) => plugin.status === "fail").length
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function buildPriorityActions(plugins) {
|
|
31
|
+
const actions = [];
|
|
32
|
+
const cleanReason = (reason) => reason.replace(/[.。]+$/u, "");
|
|
33
|
+
for (const plugin of plugins.filter((item) => item.status === "fail")) {
|
|
34
|
+
const validationFinding = plugin.validation.findings[0];
|
|
35
|
+
const securityFinding = plugin.security?.findings[0];
|
|
36
|
+
const compatibilityFinding = plugin.compatibility?.results.find((result) => result.status === "fail");
|
|
37
|
+
const reason = validationFinding?.id ??
|
|
38
|
+
securityFinding?.id ??
|
|
39
|
+
compatibilityFinding?.summary ??
|
|
40
|
+
"unknown failure";
|
|
41
|
+
actions.push(`${plugin.plugin.name}: fix ${cleanReason(reason)}.`);
|
|
42
|
+
}
|
|
43
|
+
for (const plugin of plugins.filter((item) => item.status === "warn")) {
|
|
44
|
+
const securityFinding = plugin.security?.findings[0];
|
|
45
|
+
const compatibilityFinding = plugin.compatibility?.results.find((result) => result.status === "warn");
|
|
46
|
+
const reason = securityFinding?.id ??
|
|
47
|
+
compatibilityFinding?.summary ??
|
|
48
|
+
plugin.validation.findings[0]?.id ??
|
|
49
|
+
"warning";
|
|
50
|
+
actions.push(`${plugin.plugin.name}: review ${cleanReason(reason)}.`);
|
|
51
|
+
}
|
|
52
|
+
return actions;
|
|
53
|
+
}
|
|
54
|
+
export async function buildEcosystemAudit(options) {
|
|
55
|
+
const installedPlugins = filterInstalledPlugins(await discoverInstalledPlugins({ env: options.env }), options.filter ?? null);
|
|
56
|
+
const environment = {
|
|
57
|
+
env: options.env,
|
|
58
|
+
platform: options.platform
|
|
59
|
+
};
|
|
60
|
+
const plugins = [];
|
|
61
|
+
for (const plugin of installedPlugins) {
|
|
62
|
+
const validation = await options.validatePlugin(plugin.rootPath);
|
|
63
|
+
const security = options.includeSecurity
|
|
64
|
+
? await buildSecurityAudit(plugin.rootPath)
|
|
65
|
+
: undefined;
|
|
66
|
+
const compatibility = options.includeCompatibility
|
|
67
|
+
? await buildCompatibilityMatrix(plugin.rootPath, environment)
|
|
68
|
+
: undefined;
|
|
69
|
+
const rawStatus = mergeStatus([
|
|
70
|
+
validation.status,
|
|
71
|
+
security?.status ?? "pass",
|
|
72
|
+
compatibilityStatus(compatibility)
|
|
73
|
+
]);
|
|
74
|
+
const status = options.failOnWarnings && rawStatus === "warn"
|
|
75
|
+
? "fail"
|
|
76
|
+
: rawStatus;
|
|
77
|
+
plugins.push({
|
|
78
|
+
plugin,
|
|
79
|
+
status,
|
|
80
|
+
validation,
|
|
81
|
+
...(security ? { security } : {}),
|
|
82
|
+
...(compatibility ? { compatibility } : {})
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const summary = summarizePlugins(plugins);
|
|
86
|
+
const status = summary.fail > 0
|
|
87
|
+
? "fail"
|
|
88
|
+
: summary.warn > 0
|
|
89
|
+
? "warn"
|
|
90
|
+
: "pass";
|
|
91
|
+
return {
|
|
92
|
+
schemaVersion: "1.0.0",
|
|
93
|
+
generatedAt: new Date().toISOString(),
|
|
94
|
+
status,
|
|
95
|
+
summary,
|
|
96
|
+
plugins,
|
|
97
|
+
priorityActions: buildPriorityActions(plugins)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function renderEcosystemAuditJson(report) {
|
|
101
|
+
return JSON.stringify(report, null, 2);
|
|
102
|
+
}
|
|
103
|
+
export function renderEcosystemAudit(report) {
|
|
104
|
+
const lines = [
|
|
105
|
+
"Local Ecosystem Audit",
|
|
106
|
+
"=====================",
|
|
107
|
+
`Status: ${report.status.toUpperCase()}`,
|
|
108
|
+
`Installed plugins: ${report.summary.totalPlugins}`,
|
|
109
|
+
`Summary: ${report.summary.fail} fail, ${report.summary.warn} warn, ${report.summary.pass} pass`
|
|
110
|
+
];
|
|
111
|
+
if (report.plugins.length === 0) {
|
|
112
|
+
lines.push("", "No installed Codex plugins found.");
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
115
|
+
lines.push("", "Plugins", "-------");
|
|
116
|
+
for (const item of report.plugins) {
|
|
117
|
+
const version = item.plugin.version ? `@${item.plugin.version}` : "";
|
|
118
|
+
lines.push(`- ${item.plugin.name}${version}: ${item.status.toUpperCase()}`);
|
|
119
|
+
lines.push(` Validation: ${item.validation.status.toUpperCase()}`);
|
|
120
|
+
if (item.security) {
|
|
121
|
+
lines.push(` Security: ${item.security.status.toUpperCase()} (${item.security.score}/100)`);
|
|
122
|
+
}
|
|
123
|
+
if (item.compatibility) {
|
|
124
|
+
const compatibilitySummary = item.compatibility.results
|
|
125
|
+
.map((result) => `${result.client}=${result.status}`)
|
|
126
|
+
.join(", ");
|
|
127
|
+
lines.push(` Compatibility: ${compatibilitySummary}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (report.priorityActions.length > 0) {
|
|
131
|
+
lines.push("", "Priority Actions", "----------------");
|
|
132
|
+
lines.push(...report.priorityActions.map((action) => `- ${action}`));
|
|
133
|
+
}
|
|
134
|
+
return lines.join("\n");
|
|
135
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type CompatibilityEnvironment, type CompatibilityMatrix } from "../compatibility/compatibility-matrix.js";
|
|
2
|
+
import type { JsonReport } from "../domain/types.js";
|
|
3
|
+
import { type DoctorRecommendationsReport } from "./doctor-recommendations.js";
|
|
4
|
+
import { type SecurityAudit } from "../security/security-audit.js";
|
|
5
|
+
import { type TrustScoreReport } from "../security/trust-score.js";
|
|
6
|
+
export interface DoctorExportBundle {
|
|
7
|
+
schemaVersion: "1.0.0";
|
|
8
|
+
generatedAt: string;
|
|
9
|
+
kind: "doctor.export.bundle";
|
|
10
|
+
version: string;
|
|
11
|
+
targetPath: string;
|
|
12
|
+
validation: JsonReport;
|
|
13
|
+
security: SecurityAudit;
|
|
14
|
+
compatibility: CompatibilityMatrix;
|
|
15
|
+
recommendations: DoctorRecommendationsReport;
|
|
16
|
+
trust: TrustScoreReport;
|
|
17
|
+
}
|
|
18
|
+
export declare function buildDoctorExportBundle(targetPath: string, environment?: CompatibilityEnvironment): Promise<DoctorExportBundle>;
|
|
19
|
+
export declare function renderDoctorExportBundleJson(bundle: DoctorExportBundle): string;
|
|
20
|
+
export declare function renderDoctorExportBundle(bundle: DoctorExportBundle, options?: {
|
|
21
|
+
outputPath?: string | null;
|
|
22
|
+
}): string;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { buildCompatibilityMatrix } from "../compatibility/compatibility-matrix.js";
|
|
3
|
+
import { applyDoctorConfig, loadDoctorConfig } from "./doctor-config.js";
|
|
4
|
+
import { buildJsonReport } from "../reporting/render-json-report.js";
|
|
5
|
+
import { buildDoctorRecommendations } from "./doctor-recommendations.js";
|
|
6
|
+
import { buildSecurityAudit } from "../security/security-audit.js";
|
|
7
|
+
import { buildTrustScore } from "../security/trust-score.js";
|
|
8
|
+
import { validatePlugin } from "./validate-plugin.js";
|
|
9
|
+
import { packageVersion } from "../version.js";
|
|
10
|
+
function redactString(value) {
|
|
11
|
+
return value
|
|
12
|
+
.replace(/sk-[A-Za-z0-9_-]{12,}/g, "[REDACTED_SECRET]")
|
|
13
|
+
.replace(/npm_[A-Za-z0-9_-]{12,}/g, "[REDACTED_SECRET]")
|
|
14
|
+
.replace(/gh[pousr]_[A-Za-z0-9_]{12,}/g, "[REDACTED_SECRET]")
|
|
15
|
+
.replace(/SHOULD_NOT_LEAK/g, "[REDACTED_SECRET]");
|
|
16
|
+
}
|
|
17
|
+
function redactValue(value) {
|
|
18
|
+
if (typeof value === "string") {
|
|
19
|
+
return redactString(value);
|
|
20
|
+
}
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
return value.map(redactValue);
|
|
23
|
+
}
|
|
24
|
+
if (typeof value === "object" && value !== null) {
|
|
25
|
+
return Object.fromEntries(Object.entries(value).map(([key, nestedValue]) => [
|
|
26
|
+
key,
|
|
27
|
+
redactValue(nestedValue)
|
|
28
|
+
]));
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
export async function buildDoctorExportBundle(targetPath, environment = {}) {
|
|
33
|
+
const rootPath = path.resolve(targetPath);
|
|
34
|
+
const [rawValidation, security, compatibility, recommendations, trust] = await Promise.all([
|
|
35
|
+
validatePlugin(rootPath),
|
|
36
|
+
buildSecurityAudit(rootPath),
|
|
37
|
+
buildCompatibilityMatrix(rootPath, environment),
|
|
38
|
+
buildDoctorRecommendations(rootPath, { environment }),
|
|
39
|
+
buildTrustScore(rootPath)
|
|
40
|
+
]);
|
|
41
|
+
const validation = applyDoctorConfig(rawValidation, await loadDoctorConfig(rootPath));
|
|
42
|
+
return {
|
|
43
|
+
schemaVersion: "1.0.0",
|
|
44
|
+
generatedAt: new Date().toISOString(),
|
|
45
|
+
kind: "doctor.export.bundle",
|
|
46
|
+
version: packageVersion,
|
|
47
|
+
targetPath: rootPath,
|
|
48
|
+
validation: buildJsonReport(validation, { runtimeProbeEnabled: false }),
|
|
49
|
+
security,
|
|
50
|
+
compatibility,
|
|
51
|
+
recommendations,
|
|
52
|
+
trust
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function renderDoctorExportBundleJson(bundle) {
|
|
56
|
+
return JSON.stringify(redactValue(bundle), null, 2);
|
|
57
|
+
}
|
|
58
|
+
export function renderDoctorExportBundle(bundle, options = {}) {
|
|
59
|
+
const lines = [
|
|
60
|
+
"Doctor Export Bundle",
|
|
61
|
+
"====================",
|
|
62
|
+
`Target: ${bundle.targetPath}`,
|
|
63
|
+
`Version: ${bundle.version}`,
|
|
64
|
+
`Validation: ${bundle.validation.summary.status.toUpperCase()}`,
|
|
65
|
+
`Security: ${bundle.security.status.toUpperCase()} (${bundle.security.score}/100)`,
|
|
66
|
+
`Trust: ${bundle.trust.status.toUpperCase()} (${bundle.trust.score}/100)`,
|
|
67
|
+
`Recommendations: ${bundle.recommendations.actions.length}`
|
|
68
|
+
];
|
|
69
|
+
if (options.outputPath) {
|
|
70
|
+
lines.push(`Output: ${options.outputPath}`);
|
|
71
|
+
}
|
|
72
|
+
lines.push("", "Bundle sections", "---------------");
|
|
73
|
+
lines.push("validation");
|
|
74
|
+
lines.push("security");
|
|
75
|
+
lines.push("compatibility");
|
|
76
|
+
lines.push("recommendations");
|
|
77
|
+
lines.push("trust");
|
|
78
|
+
return lines.join("\n");
|
|
79
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type CompatibilityEnvironment } from "../compatibility/compatibility-matrix.js";
|
|
2
|
+
import type { CheckResult } from "../domain/types.js";
|
|
3
|
+
import { type SecurityAudit } from "../security/security-audit.js";
|
|
4
|
+
export type RecommendationPriority = "blocker" | "high" | "medium" | "info";
|
|
5
|
+
export type RecommendationCategory = "validation" | "security" | "compatibility" | "release";
|
|
6
|
+
export interface DoctorRecommendationAction {
|
|
7
|
+
priority: RecommendationPriority;
|
|
8
|
+
category: RecommendationCategory;
|
|
9
|
+
title: string;
|
|
10
|
+
reason: string;
|
|
11
|
+
nextCommand: string;
|
|
12
|
+
findingId?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface DoctorRecommendationsReport {
|
|
15
|
+
schemaVersion: "1.0.0";
|
|
16
|
+
generatedAt: string;
|
|
17
|
+
targetPath: string;
|
|
18
|
+
status: "pass" | "warn" | "fail";
|
|
19
|
+
exitCode: 0 | 1;
|
|
20
|
+
summary: {
|
|
21
|
+
actionCounts: Record<RecommendationPriority, number>;
|
|
22
|
+
};
|
|
23
|
+
validation: {
|
|
24
|
+
status: CheckResult["status"];
|
|
25
|
+
findingCount: number;
|
|
26
|
+
};
|
|
27
|
+
security: {
|
|
28
|
+
status: SecurityAudit["status"];
|
|
29
|
+
score: number;
|
|
30
|
+
findingCount: number;
|
|
31
|
+
};
|
|
32
|
+
compatibility: {
|
|
33
|
+
failedClients: string[];
|
|
34
|
+
};
|
|
35
|
+
actions: DoctorRecommendationAction[];
|
|
36
|
+
}
|
|
37
|
+
export declare function buildDoctorRecommendations(targetPath: string, options?: {
|
|
38
|
+
environment?: CompatibilityEnvironment;
|
|
39
|
+
runCheck?: (targetPath: string) => Promise<CheckResult>;
|
|
40
|
+
}): Promise<DoctorRecommendationsReport>;
|
|
41
|
+
export declare function renderDoctorRecommendationsJson(report: DoctorRecommendationsReport): string;
|
|
42
|
+
export declare function renderDoctorRecommendations(report: DoctorRecommendationsReport): string;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { buildCompatibilityMatrix, matrixExitCode } from "../compatibility/compatibility-matrix.js";
|
|
3
|
+
import { applyDoctorConfig, loadDoctorConfig } from "./doctor-config.js";
|
|
4
|
+
import { buildSecurityAudit } from "../security/security-audit.js";
|
|
5
|
+
import { validatePlugin } from "./validate-plugin.js";
|
|
6
|
+
const priorityRank = {
|
|
7
|
+
blocker: 0,
|
|
8
|
+
high: 1,
|
|
9
|
+
medium: 2,
|
|
10
|
+
info: 3
|
|
11
|
+
};
|
|
12
|
+
function countActions(actions) {
|
|
13
|
+
return {
|
|
14
|
+
blocker: actions.filter((action) => action.priority === "blocker").length,
|
|
15
|
+
high: actions.filter((action) => action.priority === "high").length,
|
|
16
|
+
medium: actions.filter((action) => action.priority === "medium").length,
|
|
17
|
+
info: actions.filter((action) => action.priority === "info").length
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function priorityForFinding(finding) {
|
|
21
|
+
if (finding.severity === "fail") {
|
|
22
|
+
return "blocker";
|
|
23
|
+
}
|
|
24
|
+
return finding.id.startsWith("plugin.security.") ? "high" : "medium";
|
|
25
|
+
}
|
|
26
|
+
function categoryForFinding(finding) {
|
|
27
|
+
return finding.id.startsWith("plugin.security.") ? "security" : "validation";
|
|
28
|
+
}
|
|
29
|
+
function commandForCategory(category, targetPath) {
|
|
30
|
+
if (category === "security") {
|
|
31
|
+
return `codex-plugin-doctor security ${targetPath} --scorecard`;
|
|
32
|
+
}
|
|
33
|
+
if (category === "compatibility") {
|
|
34
|
+
return `codex-plugin-doctor compat ${targetPath} --all --scorecard`;
|
|
35
|
+
}
|
|
36
|
+
return `codex-plugin-doctor check ${targetPath} --explain`;
|
|
37
|
+
}
|
|
38
|
+
function actionFromFinding(finding, targetPath) {
|
|
39
|
+
const category = categoryForFinding(finding);
|
|
40
|
+
return {
|
|
41
|
+
priority: priorityForFinding(finding),
|
|
42
|
+
category,
|
|
43
|
+
findingId: finding.id,
|
|
44
|
+
title: finding.message,
|
|
45
|
+
reason: finding.impact,
|
|
46
|
+
nextCommand: commandForCategory(category, targetPath)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function dedupeActions(actions) {
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
return actions.filter((action) => {
|
|
52
|
+
const key = `${action.category}\n${action.findingId ?? action.title}\n${action.reason}`;
|
|
53
|
+
if (seen.has(key)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
seen.add(key);
|
|
57
|
+
return true;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function actionsFromCompatibility(matrix, targetPath) {
|
|
61
|
+
return matrix.results
|
|
62
|
+
.filter((result) => result.status === "fail")
|
|
63
|
+
.map((result) => ({
|
|
64
|
+
priority: "high",
|
|
65
|
+
category: "compatibility",
|
|
66
|
+
title: `${result.client} compatibility failed.`,
|
|
67
|
+
reason: result.summary,
|
|
68
|
+
nextCommand: commandForCategory("compatibility", targetPath)
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
function sortActions(actions) {
|
|
72
|
+
return [...actions].sort((left, right) => {
|
|
73
|
+
const priorityDelta = priorityRank[left.priority] - priorityRank[right.priority];
|
|
74
|
+
if (priorityDelta !== 0) {
|
|
75
|
+
return priorityDelta;
|
|
76
|
+
}
|
|
77
|
+
return left.category.localeCompare(right.category);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
export async function buildDoctorRecommendations(targetPath, options = {}) {
|
|
81
|
+
const rootPath = path.resolve(targetPath);
|
|
82
|
+
const runCheck = options.runCheck ?? validatePlugin;
|
|
83
|
+
const [rawValidation, security, compatibility] = await Promise.all([
|
|
84
|
+
runCheck(rootPath),
|
|
85
|
+
buildSecurityAudit(rootPath),
|
|
86
|
+
buildCompatibilityMatrix(rootPath, options.environment ?? {})
|
|
87
|
+
]);
|
|
88
|
+
const validation = applyDoctorConfig(rawValidation, await loadDoctorConfig(rootPath));
|
|
89
|
+
const actions = sortActions(dedupeActions([
|
|
90
|
+
...validation.findings.map((finding) => actionFromFinding(finding, rootPath)),
|
|
91
|
+
...security.findings.map((finding) => actionFromFinding(finding, rootPath)),
|
|
92
|
+
...actionsFromCompatibility(compatibility, rootPath)
|
|
93
|
+
]));
|
|
94
|
+
const finalActions = actions.length > 0
|
|
95
|
+
? actions
|
|
96
|
+
: [
|
|
97
|
+
{
|
|
98
|
+
priority: "info",
|
|
99
|
+
category: "release",
|
|
100
|
+
title: "No blocker actions.",
|
|
101
|
+
reason: "The package has no validation, security, or compatibility blockers in this recommendation pass.",
|
|
102
|
+
nextCommand: `codex-plugin-doctor check ${rootPath} --profile publish`
|
|
103
|
+
}
|
|
104
|
+
];
|
|
105
|
+
const status = finalActions.some((action) => action.priority === "blocker")
|
|
106
|
+
? "fail"
|
|
107
|
+
: finalActions.some((action) => action.priority === "high" || action.priority === "medium")
|
|
108
|
+
? "warn"
|
|
109
|
+
: "pass";
|
|
110
|
+
return {
|
|
111
|
+
schemaVersion: "1.0.0",
|
|
112
|
+
generatedAt: new Date().toISOString(),
|
|
113
|
+
targetPath: rootPath,
|
|
114
|
+
status,
|
|
115
|
+
exitCode: status === "fail" ? 1 : 0,
|
|
116
|
+
summary: {
|
|
117
|
+
actionCounts: countActions(finalActions)
|
|
118
|
+
},
|
|
119
|
+
validation: {
|
|
120
|
+
status: validation.status,
|
|
121
|
+
findingCount: validation.findings.length
|
|
122
|
+
},
|
|
123
|
+
security: {
|
|
124
|
+
status: security.status,
|
|
125
|
+
score: security.score,
|
|
126
|
+
findingCount: security.findings.length
|
|
127
|
+
},
|
|
128
|
+
compatibility: {
|
|
129
|
+
failedClients: matrixExitCode(compatibility) === 1
|
|
130
|
+
? compatibility.results
|
|
131
|
+
.filter((result) => result.status === "fail")
|
|
132
|
+
.map((result) => result.client)
|
|
133
|
+
: []
|
|
134
|
+
},
|
|
135
|
+
actions: finalActions
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export function renderDoctorRecommendationsJson(report) {
|
|
139
|
+
return JSON.stringify(report, null, 2);
|
|
140
|
+
}
|
|
141
|
+
export function renderDoctorRecommendations(report) {
|
|
142
|
+
const lines = [
|
|
143
|
+
"Doctor Recommendations",
|
|
144
|
+
"======================",
|
|
145
|
+
`Target: ${report.targetPath}`,
|
|
146
|
+
`Status: ${report.status.toUpperCase()}`,
|
|
147
|
+
`Actions: ${report.summary.actionCounts.blocker} blocker, ${report.summary.actionCounts.high} high, ${report.summary.actionCounts.medium} medium, ${report.summary.actionCounts.info} info`,
|
|
148
|
+
`Security: ${report.security.status.toUpperCase()} (${report.security.score}/100)`
|
|
149
|
+
];
|
|
150
|
+
lines.push("", "Actions", "-------");
|
|
151
|
+
for (const action of report.actions) {
|
|
152
|
+
lines.push(`[${action.priority.toUpperCase()}] ${action.title}`);
|
|
153
|
+
if (action.findingId) {
|
|
154
|
+
lines.push(` Finding: ${action.findingId}`);
|
|
155
|
+
}
|
|
156
|
+
lines.push(` Category: ${action.category}`);
|
|
157
|
+
lines.push(` Reason: ${action.reason}`);
|
|
158
|
+
lines.push(` Next: ${action.nextCommand}`);
|
|
159
|
+
}
|
|
160
|
+
return lines.join("\n");
|
|
161
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { CheckOptions, CheckResult } from "./domain/types.js";
|
|
2
2
|
export { buildSecurityAudit, renderSecurityAuditJson, renderSecurityScorecard, type SecurityAudit } from "./security/security-audit.js";
|
|
3
|
+
export { buildTrustScore, renderTrustScore, renderTrustScoreJson, type TrustScoreReport } from "./security/trust-score.js";
|
|
3
4
|
export { buildDoctorSnapshot, renderDoctorSnapshot, renderDoctorSnapshotJson, type DoctorSnapshot } from "./core/doctor-snapshot.js";
|
|
5
|
+
export { buildDoctorRecommendations, renderDoctorRecommendations, renderDoctorRecommendationsJson, type DoctorRecommendationAction, type DoctorRecommendationsReport } from "./core/doctor-recommendations.js";
|
|
6
|
+
export { buildDoctorExportBundle, renderDoctorExportBundle, renderDoctorExportBundleJson, type DoctorExportBundle } from "./core/doctor-export-bundle.js";
|
|
7
|
+
export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson, type EcosystemAuditReport } from "./audit/ecosystem-audit.js";
|
|
8
|
+
export { applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames, type PolicyPackName } from "./policy/policy-packs.js";
|
|
9
|
+
export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson, type GenericMcpDoctorReport } from "./mcp/generic-mcp-doctor.js";
|
|
4
10
|
export declare function runCheck(targetPath: string, options?: CheckOptions): Promise<CheckResult>;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { validatePlugin } from "./core/validate-plugin.js";
|
|
2
2
|
export { buildSecurityAudit, renderSecurityAuditJson, renderSecurityScorecard } from "./security/security-audit.js";
|
|
3
|
+
export { buildTrustScore, renderTrustScore, renderTrustScoreJson } from "./security/trust-score.js";
|
|
3
4
|
export { buildDoctorSnapshot, renderDoctorSnapshot, renderDoctorSnapshotJson } from "./core/doctor-snapshot.js";
|
|
5
|
+
export { buildDoctorRecommendations, renderDoctorRecommendations, renderDoctorRecommendationsJson } from "./core/doctor-recommendations.js";
|
|
6
|
+
export { buildDoctorExportBundle, renderDoctorExportBundle, renderDoctorExportBundleJson } from "./core/doctor-export-bundle.js";
|
|
7
|
+
export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson } from "./audit/ecosystem-audit.js";
|
|
8
|
+
export { applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames } from "./policy/policy-packs.js";
|
|
9
|
+
export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson } from "./mcp/generic-mcp-doctor.js";
|
|
4
10
|
export async function runCheck(targetPath, options = {}) {
|
|
5
11
|
return validatePlugin(targetPath, options);
|
|
6
12
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type CompatibilityEnvironment, type CompatibilityMatrix } from "../compatibility/compatibility-matrix.js";
|
|
2
|
+
import type { Finding } from "../domain/types.js";
|
|
3
|
+
import { type SecurityAudit } from "../security/security-audit.js";
|
|
4
|
+
export interface GenericMcpDoctorReport {
|
|
5
|
+
targetPath: string;
|
|
6
|
+
status: "pass" | "warn" | "fail";
|
|
7
|
+
exitCode: 0 | 1;
|
|
8
|
+
mcpConfigPath: string | null;
|
|
9
|
+
serverCount: number;
|
|
10
|
+
findings: Finding[];
|
|
11
|
+
security: SecurityAudit;
|
|
12
|
+
compatibility: CompatibilityMatrix;
|
|
13
|
+
}
|
|
14
|
+
export declare function buildGenericMcpDoctor(targetPath: string, environment?: CompatibilityEnvironment): Promise<GenericMcpDoctorReport>;
|
|
15
|
+
export declare function renderGenericMcpDoctorJson(report: GenericMcpDoctorReport): string;
|
|
16
|
+
export declare function renderGenericMcpDoctor(report: GenericMcpDoctorReport): string;
|