codex-plugin-doctor 0.13.0 → 0.14.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 -2
- package/dist/audit/ecosystem-audit-cache.d.ts +15 -0
- package/dist/audit/ecosystem-audit-cache.js +78 -0
- package/dist/audit/ecosystem-audit.d.ts +8 -0
- package/dist/audit/ecosystem-audit.js +63 -6
- package/dist/core/doctor-export-bundle.d.ts +4 -4
- package/dist/core/doctor-export-bundle.js +3 -30
- package/dist/core/doctor-recommendations.d.ts +2 -2
- package/dist/core/doctor-recommendations.js +5 -135
- package/dist/core/package-analysis.d.ts +28 -0
- package/dist/core/package-analysis.js +180 -0
- package/dist/core/performance-report.d.ts +37 -0
- package/dist/core/performance-report.js +141 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/run-cli.js +49 -3
- package/dist/security/trust-score.d.ts +5 -1
- package/dist/security/trust-score.js +6 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -180,6 +180,8 @@ codex-plugin-doctor doctor recommend .
|
|
|
180
180
|
codex-plugin-doctor doctor recommend . --json --output recommendations.json
|
|
181
181
|
codex-plugin-doctor doctor trust .
|
|
182
182
|
codex-plugin-doctor doctor trust . --json --output trust-score.json
|
|
183
|
+
codex-plugin-doctor doctor perf .
|
|
184
|
+
codex-plugin-doctor doctor perf . --json --output perf.json
|
|
183
185
|
codex-plugin-doctor doctor export --bundle .
|
|
184
186
|
codex-plugin-doctor doctor export --bundle . --output doctor-bundle.json
|
|
185
187
|
codex-plugin-doctor doctor snapshot
|
|
@@ -190,6 +192,8 @@ codex-plugin-doctor doctor --update-check
|
|
|
190
192
|
codex-plugin-doctor audit --installed
|
|
191
193
|
codex-plugin-doctor audit --installed --security --compat
|
|
192
194
|
codex-plugin-doctor audit --installed --security --compat --json --output local-audit.json
|
|
195
|
+
codex-plugin-doctor audit --installed --security --compat --cache
|
|
196
|
+
codex-plugin-doctor audit --installed --changed --cache
|
|
193
197
|
codex-plugin-doctor mcp .
|
|
194
198
|
codex-plugin-doctor mcp . --json
|
|
195
199
|
codex-plugin-doctor mcp . --json --output mcp-doctor.json
|
|
@@ -246,9 +250,9 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
|
|
|
246
250
|
|
|
247
251
|
`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`.
|
|
248
252
|
|
|
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.
|
|
253
|
+
`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 perf <path>` profiles the shared package analysis pipeline and reports per-stage durations for validation, config, security, compatibility, trust, recommendations, and total runtime. `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
254
|
|
|
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.
|
|
255
|
+
`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. Add `--cache` to reuse unchanged plugin results between runs; add `--changed` to only report plugins whose fingerprint changed since the last cached audit. Use `--cache-file path/to/audit-cache.json` when CI or scripted runs need an explicit cache location.
|
|
252
256
|
|
|
253
257
|
`--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
258
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { EcosystemAuditPluginResult } from "./ecosystem-audit.js";
|
|
2
|
+
import type { InstalledPlugin } from "../core/discover-installed-plugins.js";
|
|
3
|
+
export interface EcosystemAuditCacheEntry {
|
|
4
|
+
fingerprint: string;
|
|
5
|
+
cachedAt: string;
|
|
6
|
+
result: EcosystemAuditPluginResult;
|
|
7
|
+
}
|
|
8
|
+
export interface EcosystemAuditCacheFile {
|
|
9
|
+
schemaVersion: "1.0.0";
|
|
10
|
+
entries: Record<string, EcosystemAuditCacheEntry>;
|
|
11
|
+
}
|
|
12
|
+
export declare function resolveEcosystemAuditCachePath(env?: Record<string, string | undefined>, cachePath?: string | null): string;
|
|
13
|
+
export declare function loadEcosystemAuditCache(cachePath: string): Promise<EcosystemAuditCacheFile>;
|
|
14
|
+
export declare function writeEcosystemAuditCache(cachePath: string, cache: EcosystemAuditCacheFile): Promise<void>;
|
|
15
|
+
export declare function fingerprintInstalledPlugin(plugin: InstalledPlugin): Promise<string>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
const skippedDirectories = new Set([
|
|
6
|
+
".git",
|
|
7
|
+
"coverage",
|
|
8
|
+
"dist",
|
|
9
|
+
"node_modules",
|
|
10
|
+
"validation-artifacts-local",
|
|
11
|
+
"validation-sessions"
|
|
12
|
+
]);
|
|
13
|
+
export function resolveEcosystemAuditCachePath(env = process.env, cachePath) {
|
|
14
|
+
if (cachePath) {
|
|
15
|
+
return path.resolve(cachePath);
|
|
16
|
+
}
|
|
17
|
+
const codexHome = env.CODEX_HOME
|
|
18
|
+
? path.resolve(env.CODEX_HOME)
|
|
19
|
+
: path.join(os.homedir(), ".codex");
|
|
20
|
+
return path.join(codexHome, "plugin-doctor", "audit-cache.json");
|
|
21
|
+
}
|
|
22
|
+
export async function loadEcosystemAuditCache(cachePath) {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(await readFile(cachePath, "utf8"));
|
|
25
|
+
if (parsed.schemaVersion === "1.0.0" && parsed.entries && typeof parsed.entries === "object") {
|
|
26
|
+
return {
|
|
27
|
+
schemaVersion: "1.0.0",
|
|
28
|
+
entries: parsed.entries
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Invalid or missing cache files are treated as cold caches.
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
schemaVersion: "1.0.0",
|
|
37
|
+
entries: {}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export async function writeEcosystemAuditCache(cachePath, cache) {
|
|
41
|
+
await mkdir(path.dirname(cachePath), { recursive: true });
|
|
42
|
+
await writeFile(cachePath, JSON.stringify(cache, null, 2), "utf8");
|
|
43
|
+
}
|
|
44
|
+
async function walkFingerprintEntries(rootPath, currentPath = rootPath) {
|
|
45
|
+
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
46
|
+
const fingerprintEntries = [];
|
|
47
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
48
|
+
const entryPath = path.join(currentPath, entry.name);
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
if (skippedDirectories.has(entry.name)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
fingerprintEntries.push(...(await walkFingerprintEntries(rootPath, entryPath)));
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (!entry.isFile()) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const details = await stat(entryPath);
|
|
60
|
+
const relativePath = path.relative(rootPath, entryPath).replace(/\\/g, "/");
|
|
61
|
+
fingerprintEntries.push(`${relativePath}\t${details.size}\t${Math.trunc(details.mtimeMs)}`);
|
|
62
|
+
}
|
|
63
|
+
return fingerprintEntries;
|
|
64
|
+
}
|
|
65
|
+
export async function fingerprintInstalledPlugin(plugin) {
|
|
66
|
+
const hash = createHash("sha256");
|
|
67
|
+
hash.update(plugin.name);
|
|
68
|
+
hash.update("\n");
|
|
69
|
+
hash.update(plugin.version ?? "");
|
|
70
|
+
hash.update("\n");
|
|
71
|
+
hash.update(plugin.relativePath);
|
|
72
|
+
hash.update("\n");
|
|
73
|
+
for (const entry of await walkFingerprintEntries(plugin.rootPath)) {
|
|
74
|
+
hash.update(entry);
|
|
75
|
+
hash.update("\n");
|
|
76
|
+
}
|
|
77
|
+
return hash.digest("hex");
|
|
78
|
+
}
|
|
@@ -9,12 +9,18 @@ export interface EcosystemAuditOptions {
|
|
|
9
9
|
includeSecurity?: boolean;
|
|
10
10
|
includeCompatibility?: boolean;
|
|
11
11
|
failOnWarnings?: boolean;
|
|
12
|
+
cache?: {
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
changedOnly?: boolean;
|
|
15
|
+
cachePath?: string | null;
|
|
16
|
+
};
|
|
12
17
|
validatePlugin: (targetPath: string) => Promise<CheckResult>;
|
|
13
18
|
}
|
|
14
19
|
export interface EcosystemAuditPluginResult {
|
|
15
20
|
plugin: InstalledPlugin;
|
|
16
21
|
status: "pass" | "warn" | "fail";
|
|
17
22
|
validation: CheckResult;
|
|
23
|
+
cached?: boolean;
|
|
18
24
|
security?: SecurityAudit;
|
|
19
25
|
compatibility?: CompatibilityMatrix;
|
|
20
26
|
}
|
|
@@ -27,6 +33,8 @@ export interface EcosystemAuditReport {
|
|
|
27
33
|
pass: number;
|
|
28
34
|
warn: number;
|
|
29
35
|
fail: number;
|
|
36
|
+
cached: number;
|
|
37
|
+
skippedUnchanged: number;
|
|
30
38
|
};
|
|
31
39
|
plugins: EcosystemAuditPluginResult[];
|
|
32
40
|
priorityActions: string[];
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { buildCompatibilityMatrix, matrixExitCode } from "../compatibility/compatibility-matrix.js";
|
|
2
3
|
import { discoverInstalledPlugins, filterInstalledPlugins } from "../core/discover-installed-plugins.js";
|
|
3
4
|
import { buildSecurityAudit } from "../security/security-audit.js";
|
|
5
|
+
import { packageVersion } from "../version.js";
|
|
6
|
+
import { fingerprintInstalledPlugin, loadEcosystemAuditCache, resolveEcosystemAuditCachePath, writeEcosystemAuditCache } from "./ecosystem-audit-cache.js";
|
|
4
7
|
function mergeStatus(statuses) {
|
|
5
8
|
if (statuses.includes("fail")) {
|
|
6
9
|
return "fail";
|
|
@@ -19,12 +22,23 @@ function compatibilityStatus(matrix) {
|
|
|
19
22
|
}
|
|
20
23
|
return matrix.results.some((result) => result.status === "warn") ? "warn" : "pass";
|
|
21
24
|
}
|
|
22
|
-
function
|
|
25
|
+
function cacheKeyForPlugin(plugin, options) {
|
|
26
|
+
return [
|
|
27
|
+
path.resolve(plugin.rootPath).toLowerCase(),
|
|
28
|
+
`version=${packageVersion}`,
|
|
29
|
+
`security=${Boolean(options.includeSecurity)}`,
|
|
30
|
+
`compatibility=${Boolean(options.includeCompatibility)}`,
|
|
31
|
+
`failOnWarnings=${Boolean(options.failOnWarnings)}`
|
|
32
|
+
].join("\n");
|
|
33
|
+
}
|
|
34
|
+
function summarizePlugins(plugins, skippedUnchanged) {
|
|
23
35
|
return {
|
|
24
36
|
totalPlugins: plugins.length,
|
|
25
37
|
pass: plugins.filter((plugin) => plugin.status === "pass").length,
|
|
26
38
|
warn: plugins.filter((plugin) => plugin.status === "warn").length,
|
|
27
|
-
fail: plugins.filter((plugin) => plugin.status === "fail").length
|
|
39
|
+
fail: plugins.filter((plugin) => plugin.status === "fail").length,
|
|
40
|
+
cached: plugins.filter((plugin) => plugin.cached).length,
|
|
41
|
+
skippedUnchanged
|
|
28
42
|
};
|
|
29
43
|
}
|
|
30
44
|
function buildPriorityActions(plugins) {
|
|
@@ -58,7 +72,35 @@ export async function buildEcosystemAudit(options) {
|
|
|
58
72
|
platform: options.platform
|
|
59
73
|
};
|
|
60
74
|
const plugins = [];
|
|
75
|
+
const cacheEnabled = Boolean(options.cache?.enabled);
|
|
76
|
+
const changedOnly = Boolean(options.cache?.changedOnly);
|
|
77
|
+
const cachePath = cacheEnabled
|
|
78
|
+
? resolveEcosystemAuditCachePath(options.env, options.cache?.cachePath)
|
|
79
|
+
: null;
|
|
80
|
+
const cache = cachePath
|
|
81
|
+
? await loadEcosystemAuditCache(cachePath)
|
|
82
|
+
: null;
|
|
83
|
+
let skippedUnchanged = 0;
|
|
61
84
|
for (const plugin of installedPlugins) {
|
|
85
|
+
const cacheKey = cacheKeyForPlugin(plugin, options);
|
|
86
|
+
const fingerprint = cache
|
|
87
|
+
? await fingerprintInstalledPlugin(plugin)
|
|
88
|
+
: null;
|
|
89
|
+
const cachedEntry = cache && fingerprint
|
|
90
|
+
? cache.entries[cacheKey]
|
|
91
|
+
: undefined;
|
|
92
|
+
if (cachedEntry && cachedEntry.fingerprint === fingerprint) {
|
|
93
|
+
if (changedOnly) {
|
|
94
|
+
skippedUnchanged += 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
plugins.push({
|
|
98
|
+
...cachedEntry.result,
|
|
99
|
+
plugin,
|
|
100
|
+
cached: true
|
|
101
|
+
});
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
62
104
|
const validation = await options.validatePlugin(plugin.rootPath);
|
|
63
105
|
const security = options.includeSecurity
|
|
64
106
|
? await buildSecurityAudit(plugin.rootPath)
|
|
@@ -74,15 +116,26 @@ export async function buildEcosystemAudit(options) {
|
|
|
74
116
|
const status = options.failOnWarnings && rawStatus === "warn"
|
|
75
117
|
? "fail"
|
|
76
118
|
: rawStatus;
|
|
77
|
-
|
|
119
|
+
const result = {
|
|
78
120
|
plugin,
|
|
79
121
|
status,
|
|
80
122
|
validation,
|
|
81
123
|
...(security ? { security } : {}),
|
|
82
124
|
...(compatibility ? { compatibility } : {})
|
|
83
|
-
}
|
|
125
|
+
};
|
|
126
|
+
plugins.push(result);
|
|
127
|
+
if (cache && fingerprint) {
|
|
128
|
+
cache.entries[cacheKey] = {
|
|
129
|
+
fingerprint,
|
|
130
|
+
cachedAt: new Date().toISOString(),
|
|
131
|
+
result
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (cache && cachePath) {
|
|
136
|
+
await writeEcosystemAuditCache(cachePath, cache);
|
|
84
137
|
}
|
|
85
|
-
const summary = summarizePlugins(plugins);
|
|
138
|
+
const summary = summarizePlugins(plugins, skippedUnchanged);
|
|
86
139
|
const status = summary.fail > 0
|
|
87
140
|
? "fail"
|
|
88
141
|
: summary.warn > 0
|
|
@@ -106,7 +159,8 @@ export function renderEcosystemAudit(report) {
|
|
|
106
159
|
"=====================",
|
|
107
160
|
`Status: ${report.status.toUpperCase()}`,
|
|
108
161
|
`Installed plugins: ${report.summary.totalPlugins}`,
|
|
109
|
-
`Summary: ${report.summary.fail} fail, ${report.summary.warn} warn, ${report.summary.pass} pass
|
|
162
|
+
`Summary: ${report.summary.fail} fail, ${report.summary.warn} warn, ${report.summary.pass} pass`,
|
|
163
|
+
`Cache: ${report.summary.cached} reused, ${report.summary.skippedUnchanged} skipped unchanged`
|
|
110
164
|
];
|
|
111
165
|
if (report.plugins.length === 0) {
|
|
112
166
|
lines.push("", "No installed Codex plugins found.");
|
|
@@ -126,6 +180,9 @@ export function renderEcosystemAudit(report) {
|
|
|
126
180
|
.join(", ");
|
|
127
181
|
lines.push(` Compatibility: ${compatibilitySummary}`);
|
|
128
182
|
}
|
|
183
|
+
if (item.cached) {
|
|
184
|
+
lines.push(" Cache: reused");
|
|
185
|
+
}
|
|
129
186
|
}
|
|
130
187
|
if (report.priorityActions.length > 0) {
|
|
131
188
|
lines.push("", "Priority Actions", "----------------");
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { CompatibilityEnvironment, CompatibilityMatrix } from "../compatibility/compatibility-matrix.js";
|
|
2
2
|
import type { JsonReport } from "../domain/types.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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
6
|
export interface DoctorExportBundle {
|
|
7
7
|
schemaVersion: "1.0.0";
|
|
8
8
|
generatedAt: string;
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
import
|
|
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";
|
|
1
|
+
import { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./package-analysis.js";
|
|
10
2
|
function redactString(value) {
|
|
11
3
|
return value
|
|
12
4
|
.replace(/sk-[A-Za-z0-9_-]{12,}/g, "[REDACTED_SECRET]")
|
|
@@ -30,27 +22,8 @@ function redactValue(value) {
|
|
|
30
22
|
return value;
|
|
31
23
|
}
|
|
32
24
|
export async function buildDoctorExportBundle(targetPath, environment = {}) {
|
|
33
|
-
const
|
|
34
|
-
|
|
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
|
-
};
|
|
25
|
+
const analysis = await buildPackageAnalysis(targetPath, { environment });
|
|
26
|
+
return buildDoctorExportBundleFromAnalysis(analysis, buildDoctorRecommendationsFromAnalysis(analysis));
|
|
54
27
|
}
|
|
55
28
|
export function renderDoctorExportBundleJson(bundle) {
|
|
56
29
|
return JSON.stringify(redactValue(bundle), null, 2);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { CompatibilityEnvironment } from "../compatibility/compatibility-matrix.js";
|
|
2
2
|
import type { CheckResult } from "../domain/types.js";
|
|
3
|
-
import {
|
|
3
|
+
import type { SecurityAudit } from "../security/security-audit.js";
|
|
4
4
|
export type RecommendationPriority = "blocker" | "high" | "medium" | "info";
|
|
5
5
|
export type RecommendationCategory = "validation" | "security" | "compatibility" | "release";
|
|
6
6
|
export interface DoctorRecommendationAction {
|
|
@@ -1,139 +1,9 @@
|
|
|
1
|
-
import
|
|
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
|
-
}
|
|
1
|
+
import { buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./package-analysis.js";
|
|
80
2
|
export async function buildDoctorRecommendations(targetPath, options = {}) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
};
|
|
3
|
+
return buildDoctorRecommendationsFromAnalysis(await buildPackageAnalysis(targetPath, {
|
|
4
|
+
environment: options.environment,
|
|
5
|
+
runCheck: options.runCheck
|
|
6
|
+
}));
|
|
137
7
|
}
|
|
138
8
|
export function renderDoctorRecommendationsJson(report) {
|
|
139
9
|
return JSON.stringify(report, null, 2);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type CompatibilityEnvironment, type CompatibilityMatrix } from "../compatibility/compatibility-matrix.js";
|
|
2
|
+
import type { CheckResult, JsonReport } from "../domain/types.js";
|
|
3
|
+
import type { DoctorRecommendationsReport } from "./doctor-recommendations.js";
|
|
4
|
+
import type { DoctorExportBundle } from "./doctor-export-bundle.js";
|
|
5
|
+
import { type SecurityAudit } from "../security/security-audit.js";
|
|
6
|
+
import { type TrustScoreReport } from "../security/trust-score.js";
|
|
7
|
+
export interface PackageAnalysis {
|
|
8
|
+
generatedAt: string;
|
|
9
|
+
targetPath: string;
|
|
10
|
+
validation: CheckResult;
|
|
11
|
+
validationJson: JsonReport;
|
|
12
|
+
security: SecurityAudit;
|
|
13
|
+
compatibility: CompatibilityMatrix;
|
|
14
|
+
trust: TrustScoreReport;
|
|
15
|
+
}
|
|
16
|
+
export type PackageAnalysisStage = "validation" | "doctorConfig" | "security" | "compatibility" | "trust";
|
|
17
|
+
export interface PackageAnalysisTiming {
|
|
18
|
+
stage: PackageAnalysisStage;
|
|
19
|
+
durationMs: number;
|
|
20
|
+
}
|
|
21
|
+
export interface PackageAnalysisOptions {
|
|
22
|
+
environment?: CompatibilityEnvironment;
|
|
23
|
+
recordTiming?: (timing: PackageAnalysisTiming) => void;
|
|
24
|
+
runCheck?: (targetPath: string) => Promise<CheckResult>;
|
|
25
|
+
}
|
|
26
|
+
export declare function buildPackageAnalysis(targetPath: string, options?: PackageAnalysisOptions): Promise<PackageAnalysis>;
|
|
27
|
+
export declare function buildDoctorRecommendationsFromAnalysis(analysis: PackageAnalysis): DoctorRecommendationsReport;
|
|
28
|
+
export declare function buildDoctorExportBundleFromAnalysis(analysis: PackageAnalysis, recommendations?: DoctorRecommendationsReport): DoctorExportBundle;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { buildCompatibilityMatrix, matrixExitCode } from "../compatibility/compatibility-matrix.js";
|
|
4
|
+
import { applyDoctorConfig, loadDoctorConfig } from "./doctor-config.js";
|
|
5
|
+
import { buildJsonReport } from "../reporting/render-json-report.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
|
+
const priorityRank = {
|
|
11
|
+
blocker: 0,
|
|
12
|
+
high: 1,
|
|
13
|
+
medium: 2,
|
|
14
|
+
info: 3
|
|
15
|
+
};
|
|
16
|
+
function countActions(actions) {
|
|
17
|
+
return {
|
|
18
|
+
blocker: actions.filter((action) => action.priority === "blocker").length,
|
|
19
|
+
high: actions.filter((action) => action.priority === "high").length,
|
|
20
|
+
medium: actions.filter((action) => action.priority === "medium").length,
|
|
21
|
+
info: actions.filter((action) => action.priority === "info").length
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function priorityForFinding(finding) {
|
|
25
|
+
if (finding.severity === "fail") {
|
|
26
|
+
return "blocker";
|
|
27
|
+
}
|
|
28
|
+
return finding.id.startsWith("plugin.security.") ? "high" : "medium";
|
|
29
|
+
}
|
|
30
|
+
function categoryForFinding(finding) {
|
|
31
|
+
return finding.id.startsWith("plugin.security.") ? "security" : "validation";
|
|
32
|
+
}
|
|
33
|
+
function commandForCategory(category, targetPath) {
|
|
34
|
+
if (category === "security") {
|
|
35
|
+
return `codex-plugin-doctor security ${targetPath} --scorecard`;
|
|
36
|
+
}
|
|
37
|
+
if (category === "compatibility") {
|
|
38
|
+
return `codex-plugin-doctor compat ${targetPath} --all --scorecard`;
|
|
39
|
+
}
|
|
40
|
+
return `codex-plugin-doctor check ${targetPath} --explain`;
|
|
41
|
+
}
|
|
42
|
+
function actionFromFinding(finding, targetPath) {
|
|
43
|
+
const category = categoryForFinding(finding);
|
|
44
|
+
return {
|
|
45
|
+
priority: priorityForFinding(finding),
|
|
46
|
+
category,
|
|
47
|
+
findingId: finding.id,
|
|
48
|
+
title: finding.message,
|
|
49
|
+
reason: finding.impact,
|
|
50
|
+
nextCommand: commandForCategory(category, targetPath)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function dedupeActions(actions) {
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
return actions.filter((action) => {
|
|
56
|
+
const key = `${action.category}\n${action.findingId ?? action.title}\n${action.reason}`;
|
|
57
|
+
if (seen.has(key)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
seen.add(key);
|
|
61
|
+
return true;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function actionsFromCompatibility(matrix, targetPath) {
|
|
65
|
+
return matrix.results
|
|
66
|
+
.filter((result) => result.status === "fail")
|
|
67
|
+
.map((result) => ({
|
|
68
|
+
priority: "high",
|
|
69
|
+
category: "compatibility",
|
|
70
|
+
title: `${result.client} compatibility failed.`,
|
|
71
|
+
reason: result.summary,
|
|
72
|
+
nextCommand: commandForCategory("compatibility", targetPath)
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
function sortActions(actions) {
|
|
76
|
+
return [...actions].sort((left, right) => {
|
|
77
|
+
const priorityDelta = priorityRank[left.priority] - priorityRank[right.priority];
|
|
78
|
+
if (priorityDelta !== 0) {
|
|
79
|
+
return priorityDelta;
|
|
80
|
+
}
|
|
81
|
+
return left.category.localeCompare(right.category);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async function measureStage(stage, operation, recordTiming) {
|
|
85
|
+
const startedAt = performance.now();
|
|
86
|
+
try {
|
|
87
|
+
return await operation();
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
recordTiming?.({
|
|
91
|
+
stage,
|
|
92
|
+
durationMs: performance.now() - startedAt
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export async function buildPackageAnalysis(targetPath, options = {}) {
|
|
97
|
+
const rootPath = path.resolve(targetPath);
|
|
98
|
+
const runCheck = options.runCheck ?? validatePlugin;
|
|
99
|
+
const [rawValidation, doctorConfig, security, compatibility] = await Promise.all([
|
|
100
|
+
measureStage("validation", () => runCheck(rootPath), options.recordTiming),
|
|
101
|
+
measureStage("doctorConfig", () => loadDoctorConfig(rootPath), options.recordTiming),
|
|
102
|
+
measureStage("security", () => buildSecurityAudit(rootPath), options.recordTiming),
|
|
103
|
+
measureStage("compatibility", () => buildCompatibilityMatrix(rootPath, options.environment ?? {}), options.recordTiming)
|
|
104
|
+
]);
|
|
105
|
+
const validation = applyDoctorConfig(rawValidation, doctorConfig);
|
|
106
|
+
const trust = await measureStage("trust", () => buildTrustScore(rootPath, { securityAudit: security }), options.recordTiming);
|
|
107
|
+
return {
|
|
108
|
+
generatedAt: new Date().toISOString(),
|
|
109
|
+
targetPath: rootPath,
|
|
110
|
+
validation,
|
|
111
|
+
validationJson: buildJsonReport(validation, { runtimeProbeEnabled: false }),
|
|
112
|
+
security,
|
|
113
|
+
compatibility,
|
|
114
|
+
trust
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export function buildDoctorRecommendationsFromAnalysis(analysis) {
|
|
118
|
+
const actions = sortActions(dedupeActions([
|
|
119
|
+
...analysis.validation.findings.map((finding) => actionFromFinding(finding, analysis.targetPath)),
|
|
120
|
+
...analysis.security.findings.map((finding) => actionFromFinding(finding, analysis.targetPath)),
|
|
121
|
+
...actionsFromCompatibility(analysis.compatibility, analysis.targetPath)
|
|
122
|
+
]));
|
|
123
|
+
const finalActions = actions.length > 0
|
|
124
|
+
? actions
|
|
125
|
+
: [
|
|
126
|
+
{
|
|
127
|
+
priority: "info",
|
|
128
|
+
category: "release",
|
|
129
|
+
title: "No blocker actions.",
|
|
130
|
+
reason: "The package has no validation, security, or compatibility blockers in this recommendation pass.",
|
|
131
|
+
nextCommand: `codex-plugin-doctor check ${analysis.targetPath} --profile publish`
|
|
132
|
+
}
|
|
133
|
+
];
|
|
134
|
+
const status = finalActions.some((action) => action.priority === "blocker")
|
|
135
|
+
? "fail"
|
|
136
|
+
: finalActions.some((action) => action.priority === "high" || action.priority === "medium")
|
|
137
|
+
? "warn"
|
|
138
|
+
: "pass";
|
|
139
|
+
return {
|
|
140
|
+
schemaVersion: "1.0.0",
|
|
141
|
+
generatedAt: analysis.generatedAt,
|
|
142
|
+
targetPath: analysis.targetPath,
|
|
143
|
+
status,
|
|
144
|
+
exitCode: status === "fail" ? 1 : 0,
|
|
145
|
+
summary: {
|
|
146
|
+
actionCounts: countActions(finalActions)
|
|
147
|
+
},
|
|
148
|
+
validation: {
|
|
149
|
+
status: analysis.validation.status,
|
|
150
|
+
findingCount: analysis.validation.findings.length
|
|
151
|
+
},
|
|
152
|
+
security: {
|
|
153
|
+
status: analysis.security.status,
|
|
154
|
+
score: analysis.security.score,
|
|
155
|
+
findingCount: analysis.security.findings.length
|
|
156
|
+
},
|
|
157
|
+
compatibility: {
|
|
158
|
+
failedClients: matrixExitCode(analysis.compatibility) === 1
|
|
159
|
+
? analysis.compatibility.results
|
|
160
|
+
.filter((result) => result.status === "fail")
|
|
161
|
+
.map((result) => result.client)
|
|
162
|
+
: []
|
|
163
|
+
},
|
|
164
|
+
actions: finalActions
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
export function buildDoctorExportBundleFromAnalysis(analysis, recommendations = buildDoctorRecommendationsFromAnalysis(analysis)) {
|
|
168
|
+
return {
|
|
169
|
+
schemaVersion: "1.0.0",
|
|
170
|
+
generatedAt: analysis.generatedAt,
|
|
171
|
+
kind: "doctor.export.bundle",
|
|
172
|
+
version: packageVersion,
|
|
173
|
+
targetPath: analysis.targetPath,
|
|
174
|
+
validation: analysis.validationJson,
|
|
175
|
+
security: analysis.security,
|
|
176
|
+
compatibility: analysis.compatibility,
|
|
177
|
+
recommendations,
|
|
178
|
+
trust: analysis.trust
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type CompatibilityEnvironment } from "../compatibility/compatibility-matrix.js";
|
|
2
|
+
import type { CheckResult } from "../domain/types.js";
|
|
3
|
+
import { type PackageAnalysisStage } from "./package-analysis.js";
|
|
4
|
+
export type DoctorPerformanceStageName = PackageAnalysisStage | "recommendations" | "total";
|
|
5
|
+
export interface DoctorPerformanceStage {
|
|
6
|
+
name: DoctorPerformanceStageName;
|
|
7
|
+
durationMs: number;
|
|
8
|
+
status?: "pass" | "warn" | "fail";
|
|
9
|
+
itemCount?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface DoctorPerformanceReport {
|
|
12
|
+
schemaVersion: "1.0.0";
|
|
13
|
+
generatedAt: string;
|
|
14
|
+
kind: "doctor.perf";
|
|
15
|
+
targetPath: string;
|
|
16
|
+
status: "pass";
|
|
17
|
+
exitCode: 0;
|
|
18
|
+
summary: {
|
|
19
|
+
stageCount: number;
|
|
20
|
+
slowestStage: DoctorPerformanceStageName;
|
|
21
|
+
totalDurationMs: number;
|
|
22
|
+
validationStatus: "pass" | "warn" | "fail";
|
|
23
|
+
securityStatus: "pass" | "warn" | "fail";
|
|
24
|
+
trustScore: number;
|
|
25
|
+
compatibilityFailures: number;
|
|
26
|
+
};
|
|
27
|
+
stages: DoctorPerformanceStage[];
|
|
28
|
+
}
|
|
29
|
+
export interface BuildDoctorPerformanceReportOptions {
|
|
30
|
+
environment?: CompatibilityEnvironment;
|
|
31
|
+
runCheck?: (targetPath: string) => Promise<CheckResult>;
|
|
32
|
+
}
|
|
33
|
+
export declare function buildDoctorPerformanceReport(targetPath: string, options?: BuildDoctorPerformanceReportOptions): Promise<DoctorPerformanceReport>;
|
|
34
|
+
export declare function renderDoctorPerformanceReportJson(report: DoctorPerformanceReport): string;
|
|
35
|
+
export declare function renderDoctorPerformanceReport(report: DoctorPerformanceReport, options?: {
|
|
36
|
+
outputPath?: string | null;
|
|
37
|
+
}): string;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { matrixExitCode } from "../compatibility/compatibility-matrix.js";
|
|
3
|
+
import { buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./package-analysis.js";
|
|
4
|
+
const stageOrder = [
|
|
5
|
+
"validation",
|
|
6
|
+
"doctorConfig",
|
|
7
|
+
"security",
|
|
8
|
+
"compatibility",
|
|
9
|
+
"trust",
|
|
10
|
+
"recommendations",
|
|
11
|
+
"total"
|
|
12
|
+
];
|
|
13
|
+
function roundDuration(durationMs) {
|
|
14
|
+
return Number(durationMs.toFixed(3));
|
|
15
|
+
}
|
|
16
|
+
function compatibilityStatus(exitCode, warningCount) {
|
|
17
|
+
if (exitCode === 1) {
|
|
18
|
+
return "fail";
|
|
19
|
+
}
|
|
20
|
+
return warningCount > 0 ? "warn" : "pass";
|
|
21
|
+
}
|
|
22
|
+
function slowestStage(stages) {
|
|
23
|
+
return stages
|
|
24
|
+
.filter((stage) => stage.name !== "total")
|
|
25
|
+
.reduce((slowest, stage) => (stage.durationMs > slowest.durationMs ? stage : slowest), stages[0]).name;
|
|
26
|
+
}
|
|
27
|
+
export async function buildDoctorPerformanceReport(targetPath, options = {}) {
|
|
28
|
+
const timings = [];
|
|
29
|
+
const startedAt = performance.now();
|
|
30
|
+
const analysis = await buildPackageAnalysis(targetPath, {
|
|
31
|
+
environment: options.environment,
|
|
32
|
+
recordTiming: (timing) => timings.push(timing),
|
|
33
|
+
runCheck: options.runCheck
|
|
34
|
+
});
|
|
35
|
+
const recommendationsStartedAt = performance.now();
|
|
36
|
+
const recommendations = buildDoctorRecommendationsFromAnalysis(analysis);
|
|
37
|
+
const recommendationTiming = performance.now() - recommendationsStartedAt;
|
|
38
|
+
const totalDurationMs = performance.now() - startedAt;
|
|
39
|
+
const compatibilityFailures = analysis.compatibility.results
|
|
40
|
+
.filter((result) => result.status === "fail").length;
|
|
41
|
+
const compatibilityWarnings = analysis.compatibility.results
|
|
42
|
+
.filter((result) => result.status === "warn").length;
|
|
43
|
+
const timingByStage = new Map(timings.map((timing) => [timing.stage, timing.durationMs]));
|
|
44
|
+
const stages = stageOrder.map((stageName) => {
|
|
45
|
+
if (stageName === "validation") {
|
|
46
|
+
return {
|
|
47
|
+
name: stageName,
|
|
48
|
+
durationMs: roundDuration(timingByStage.get(stageName) ?? 0),
|
|
49
|
+
status: analysis.validation.status,
|
|
50
|
+
itemCount: analysis.validation.findings.length
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (stageName === "doctorConfig") {
|
|
54
|
+
return {
|
|
55
|
+
name: stageName,
|
|
56
|
+
durationMs: roundDuration(timingByStage.get(stageName) ?? 0),
|
|
57
|
+
status: "pass"
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (stageName === "security") {
|
|
61
|
+
return {
|
|
62
|
+
name: stageName,
|
|
63
|
+
durationMs: roundDuration(timingByStage.get(stageName) ?? 0),
|
|
64
|
+
status: analysis.security.status,
|
|
65
|
+
itemCount: analysis.security.findings.length
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (stageName === "compatibility") {
|
|
69
|
+
return {
|
|
70
|
+
name: stageName,
|
|
71
|
+
durationMs: roundDuration(timingByStage.get(stageName) ?? 0),
|
|
72
|
+
status: compatibilityStatus(matrixExitCode(analysis.compatibility), compatibilityWarnings),
|
|
73
|
+
itemCount: analysis.compatibility.results.length
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (stageName === "trust") {
|
|
77
|
+
return {
|
|
78
|
+
name: stageName,
|
|
79
|
+
durationMs: roundDuration(timingByStage.get(stageName) ?? 0),
|
|
80
|
+
status: analysis.trust.status,
|
|
81
|
+
itemCount: analysis.trust.findings.length
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (stageName === "recommendations") {
|
|
85
|
+
return {
|
|
86
|
+
name: stageName,
|
|
87
|
+
durationMs: roundDuration(recommendationTiming),
|
|
88
|
+
status: recommendations.status,
|
|
89
|
+
itemCount: recommendations.actions.length
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
name: "total",
|
|
94
|
+
durationMs: roundDuration(totalDurationMs)
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
schemaVersion: "1.0.0",
|
|
99
|
+
generatedAt: analysis.generatedAt,
|
|
100
|
+
kind: "doctor.perf",
|
|
101
|
+
targetPath: analysis.targetPath,
|
|
102
|
+
status: "pass",
|
|
103
|
+
exitCode: 0,
|
|
104
|
+
summary: {
|
|
105
|
+
stageCount: stages.length,
|
|
106
|
+
slowestStage: slowestStage(stages),
|
|
107
|
+
totalDurationMs: roundDuration(totalDurationMs),
|
|
108
|
+
validationStatus: analysis.validation.status,
|
|
109
|
+
securityStatus: analysis.security.status,
|
|
110
|
+
trustScore: analysis.trust.score,
|
|
111
|
+
compatibilityFailures
|
|
112
|
+
},
|
|
113
|
+
stages
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export function renderDoctorPerformanceReportJson(report) {
|
|
117
|
+
return JSON.stringify(report, null, 2);
|
|
118
|
+
}
|
|
119
|
+
export function renderDoctorPerformanceReport(report, options = {}) {
|
|
120
|
+
const lines = [
|
|
121
|
+
"Doctor Performance",
|
|
122
|
+
"==================",
|
|
123
|
+
`Target: ${report.targetPath}`,
|
|
124
|
+
`Status: ${report.status.toUpperCase()}`,
|
|
125
|
+
`Total: ${report.summary.totalDurationMs}ms`,
|
|
126
|
+
`Slowest: ${report.summary.slowestStage}`,
|
|
127
|
+
`Validation: ${report.summary.validationStatus.toUpperCase()}`,
|
|
128
|
+
`Security: ${report.summary.securityStatus.toUpperCase()}`,
|
|
129
|
+
`Trust: ${report.summary.trustScore}/100`
|
|
130
|
+
];
|
|
131
|
+
if (options.outputPath) {
|
|
132
|
+
lines.push(`Output: ${options.outputPath}`);
|
|
133
|
+
}
|
|
134
|
+
lines.push("", "Stages", "------");
|
|
135
|
+
for (const stage of report.stages) {
|
|
136
|
+
const status = stage.status ? ` (${stage.status.toUpperCase()})` : "";
|
|
137
|
+
const count = stage.itemCount === undefined ? "" : `, items: ${stage.itemCount}`;
|
|
138
|
+
lines.push(`${stage.name}: ${stage.durationMs}ms${status}${count}`);
|
|
139
|
+
}
|
|
140
|
+
return lines.join("\n");
|
|
141
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
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
|
+
export { buildTrustScore, renderTrustScore, renderTrustScoreJson, type BuildTrustScoreOptions, type TrustScoreReport } from "./security/trust-score.js";
|
|
4
4
|
export { buildDoctorSnapshot, renderDoctorSnapshot, renderDoctorSnapshotJson, type DoctorSnapshot } from "./core/doctor-snapshot.js";
|
|
5
5
|
export { buildDoctorRecommendations, renderDoctorRecommendations, renderDoctorRecommendationsJson, type DoctorRecommendationAction, type DoctorRecommendationsReport } from "./core/doctor-recommendations.js";
|
|
6
6
|
export { buildDoctorExportBundle, renderDoctorExportBundle, renderDoctorExportBundleJson, type DoctorExportBundle } from "./core/doctor-export-bundle.js";
|
|
7
|
+
export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis, type PackageAnalysis, type PackageAnalysisOptions, type PackageAnalysisStage, type PackageAnalysisTiming } from "./core/package-analysis.js";
|
|
8
|
+
export { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson, type BuildDoctorPerformanceReportOptions, type DoctorPerformanceReport, type DoctorPerformanceStage, type DoctorPerformanceStageName } from "./core/performance-report.js";
|
|
7
9
|
export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson, type EcosystemAuditReport } from "./audit/ecosystem-audit.js";
|
|
8
10
|
export { applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames, type PolicyPackName } from "./policy/policy-packs.js";
|
|
9
11
|
export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson, type GenericMcpDoctorReport } from "./mcp/generic-mcp-doctor.js";
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ export { buildTrustScore, renderTrustScore, renderTrustScoreJson } from "./secur
|
|
|
4
4
|
export { buildDoctorSnapshot, renderDoctorSnapshot, renderDoctorSnapshotJson } from "./core/doctor-snapshot.js";
|
|
5
5
|
export { buildDoctorRecommendations, renderDoctorRecommendations, renderDoctorRecommendationsJson } from "./core/doctor-recommendations.js";
|
|
6
6
|
export { buildDoctorExportBundle, renderDoctorExportBundle, renderDoctorExportBundleJson } from "./core/doctor-export-bundle.js";
|
|
7
|
+
export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./core/package-analysis.js";
|
|
8
|
+
export { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson } from "./core/performance-report.js";
|
|
7
9
|
export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson } from "./audit/ecosystem-audit.js";
|
|
8
10
|
export { applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames } from "./policy/policy-packs.js";
|
|
9
11
|
export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson } from "./mcp/generic-mcp-doctor.js";
|
package/dist/run-cli.js
CHANGED
|
@@ -16,6 +16,7 @@ import { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
|
|
|
16
16
|
import { buildDoctorSnapshot, renderDoctorSnapshot, renderDoctorSnapshotJson } from "./core/doctor-snapshot.js";
|
|
17
17
|
import { buildDoctorRecommendations, renderDoctorRecommendations, renderDoctorRecommendationsJson } from "./core/doctor-recommendations.js";
|
|
18
18
|
import { buildDoctorExportBundle, renderDoctorExportBundle, renderDoctorExportBundleJson } from "./core/doctor-export-bundle.js";
|
|
19
|
+
import { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson } from "./core/performance-report.js";
|
|
19
20
|
import { applyFixPlan, buildFixPlan, renderApplyFixResult, renderFixPlanJsonReport, renderFixPlan } from "./core/fix-plan.js";
|
|
20
21
|
import { renderClientDoctor, renderEnvironmentDoctor, renderEnvironmentDoctorJson } from "./core/environment-doctor.js";
|
|
21
22
|
import { initCiWorkflow } from "./core/init-ci.js";
|
|
@@ -61,7 +62,7 @@ const defaultIo = {
|
|
|
61
62
|
}
|
|
62
63
|
};
|
|
63
64
|
function printUsage(io) {
|
|
64
|
-
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--verbose-runtime] [--explain] [--no-animations] [--ascii]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>]\n codex-plugin-doctor mcp <path> [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor doctor [recommend <path>|trust <path>|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
|
|
65
|
+
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--verbose-runtime] [--explain] [--no-animations] [--ascii]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor mcp <path> [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor doctor [recommend <path>|trust <path>|perf <path>|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
|
|
65
66
|
}
|
|
66
67
|
function renderInstalledPlugins(plugins) {
|
|
67
68
|
const lines = [
|
|
@@ -250,6 +251,38 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
250
251
|
io.writeStdout(renderedReport);
|
|
251
252
|
return report.exitCode;
|
|
252
253
|
}
|
|
254
|
+
if (maybePath === "perf") {
|
|
255
|
+
const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--")
|
|
256
|
+
? remainingArgs[0]
|
|
257
|
+
: ".";
|
|
258
|
+
const perfFlags = remainingArgs[0] && !remainingArgs[0].startsWith("--")
|
|
259
|
+
? remainingArgs.slice(1)
|
|
260
|
+
: remainingArgs;
|
|
261
|
+
const jsonOutput = perfFlags.includes("--json");
|
|
262
|
+
const outputIndex = perfFlags.indexOf("--output");
|
|
263
|
+
const outputPath = outputIndex === -1 ? null : perfFlags[outputIndex + 1];
|
|
264
|
+
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
265
|
+
io.writeStderr("Missing path after --output.");
|
|
266
|
+
return 2;
|
|
267
|
+
}
|
|
268
|
+
const report = await buildDoctorPerformanceReport(targetPath, {
|
|
269
|
+
environment: {
|
|
270
|
+
env: terminalContext.env,
|
|
271
|
+
platform: terminalContext.platform
|
|
272
|
+
},
|
|
273
|
+
runCheck: options.runCheckImpl
|
|
274
|
+
? (pathToCheck) => options.runCheckImpl(pathToCheck)
|
|
275
|
+
: undefined
|
|
276
|
+
});
|
|
277
|
+
const renderedReport = jsonOutput
|
|
278
|
+
? renderDoctorPerformanceReportJson(report)
|
|
279
|
+
: renderDoctorPerformanceReport(report, { outputPath });
|
|
280
|
+
if (outputPath) {
|
|
281
|
+
await writeFile(outputPath, renderedReport, "utf8");
|
|
282
|
+
}
|
|
283
|
+
io.writeStdout(renderedReport);
|
|
284
|
+
return report.exitCode;
|
|
285
|
+
}
|
|
253
286
|
if (maybePath === "export") {
|
|
254
287
|
const bundleIndex = remainingArgs.indexOf("--bundle");
|
|
255
288
|
if (bundleIndex === -1) {
|
|
@@ -520,7 +553,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
520
553
|
const auditFlags = maybePath ? [maybePath, ...remainingArgs] : remainingArgs;
|
|
521
554
|
const installed = auditFlags.includes("--installed");
|
|
522
555
|
if (!installed) {
|
|
523
|
-
io.writeStderr("Usage: codex-plugin-doctor audit --installed [filter] [--security] [--compat] [--json] [--output <path>]");
|
|
556
|
+
io.writeStderr("Usage: codex-plugin-doctor audit --installed [filter] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]");
|
|
524
557
|
return 2;
|
|
525
558
|
}
|
|
526
559
|
const installedIndex = auditFlags.indexOf("--installed");
|
|
@@ -535,6 +568,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
535
568
|
const policyIndex = auditFlags.indexOf("--policy");
|
|
536
569
|
const policyName = policyIndex === -1 ? null : auditFlags[policyIndex + 1];
|
|
537
570
|
const policy = parsePolicyPack(policyName);
|
|
571
|
+
const cacheEnabled = auditFlags.includes("--cache") || auditFlags.includes("--changed");
|
|
572
|
+
const changedOnly = auditFlags.includes("--changed");
|
|
573
|
+
const cacheFileIndex = auditFlags.indexOf("--cache-file");
|
|
574
|
+
const cachePath = cacheFileIndex === -1 ? null : auditFlags[cacheFileIndex + 1];
|
|
538
575
|
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
539
576
|
io.writeStderr("Missing path after --output.");
|
|
540
577
|
return 2;
|
|
@@ -543,6 +580,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
543
580
|
io.writeStderr("Missing policy after --policy.");
|
|
544
581
|
return 2;
|
|
545
582
|
}
|
|
583
|
+
if (cacheFileIndex !== -1 && (!cachePath || cachePath.startsWith("--"))) {
|
|
584
|
+
io.writeStderr("Missing path after --cache-file.");
|
|
585
|
+
return 2;
|
|
586
|
+
}
|
|
546
587
|
if (policyIndex !== -1 && !policy) {
|
|
547
588
|
io.writeStderr(`Unknown policy: ${policyName}. Supported policies: ${policyPackNames.join(", ")}.`);
|
|
548
589
|
return 2;
|
|
@@ -554,9 +595,14 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
554
595
|
includeSecurity,
|
|
555
596
|
includeCompatibility,
|
|
556
597
|
failOnWarnings: policyFailsOnWarnings(policy),
|
|
598
|
+
cache: {
|
|
599
|
+
enabled: cacheEnabled,
|
|
600
|
+
changedOnly,
|
|
601
|
+
cachePath
|
|
602
|
+
},
|
|
557
603
|
validatePlugin: options.runCheckImpl ?? runCheck
|
|
558
604
|
});
|
|
559
|
-
if (report.summary.totalPlugins === 0) {
|
|
605
|
+
if (report.summary.totalPlugins === 0 && !changedOnly) {
|
|
560
606
|
io.writeStderr(installedFilter
|
|
561
607
|
? `No installed Codex plugins matched '${installedFilter}'.`
|
|
562
608
|
: "No installed Codex plugins found.");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Finding } from "../domain/types.js";
|
|
2
|
+
import { type SecurityAudit } from "./security-audit.js";
|
|
2
3
|
export interface TrustScoreReport {
|
|
3
4
|
schemaVersion: "1.0.0";
|
|
4
5
|
generatedAt: string;
|
|
@@ -18,6 +19,9 @@ export interface TrustScoreReport {
|
|
|
18
19
|
};
|
|
19
20
|
findings: Finding[];
|
|
20
21
|
}
|
|
21
|
-
export
|
|
22
|
+
export interface BuildTrustScoreOptions {
|
|
23
|
+
securityAudit?: SecurityAudit | null;
|
|
24
|
+
}
|
|
25
|
+
export declare function buildTrustScore(targetPath: string, options?: BuildTrustScoreOptions): Promise<TrustScoreReport>;
|
|
22
26
|
export declare function renderTrustScoreJson(report: TrustScoreReport): string;
|
|
23
27
|
export declare function renderTrustScore(report: TrustScoreReport): string;
|
|
@@ -116,7 +116,7 @@ function scoreFindings(findings) {
|
|
|
116
116
|
const warnCount = findings.filter((finding) => finding.severity === "warn").length;
|
|
117
117
|
return Math.max(0, 100 - (failCount * 35) - (warnCount * 10));
|
|
118
118
|
}
|
|
119
|
-
export async function buildTrustScore(targetPath) {
|
|
119
|
+
export async function buildTrustScore(targetPath, options = {}) {
|
|
120
120
|
const rootPath = path.resolve(targetPath);
|
|
121
121
|
const packageJson = await readPackageJson(rootPath);
|
|
122
122
|
const scriptAudit = packageJson
|
|
@@ -125,10 +125,11 @@ export async function buildTrustScore(targetPath) {
|
|
|
125
125
|
const dependencyAudit = packageJson
|
|
126
126
|
? auditDependencies(packageJson)
|
|
127
127
|
: { findings: [], dependenciesChecked: 0 };
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
const securityAudit = options.securityAudit !== undefined
|
|
129
|
+
? options.securityAudit
|
|
130
|
+
: await discoverPackage(rootPath)
|
|
131
|
+
? await buildSecurityAudit(rootPath)
|
|
132
|
+
: null;
|
|
132
133
|
const findings = dedupeFindings([
|
|
133
134
|
...scriptAudit.findings,
|
|
134
135
|
...dependencyAudit.findings,
|
package/package.json
CHANGED