codex-plugin-doctor 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -176,10 +176,16 @@ Run these from a Codex plugin package root:
176
176
  codex-plugin-doctor --version
177
177
  codex-plugin-doctor self-test
178
178
  codex-plugin-doctor doctor
179
+ codex-plugin-doctor doctor npm codex-plugin-doctor
180
+ codex-plugin-doctor doctor npm codex-plugin-doctor --json --output npm-preinstall.json
181
+ codex-plugin-doctor doctor diff --before ./old-plugin --after ./new-plugin
182
+ codex-plugin-doctor doctor diff --before ./old-plugin --after ./new-plugin --json --output risk-diff.json
179
183
  codex-plugin-doctor doctor recommend .
180
184
  codex-plugin-doctor doctor recommend . --json --output recommendations.json
181
185
  codex-plugin-doctor doctor trust .
182
186
  codex-plugin-doctor doctor trust . --json --output trust-score.json
187
+ codex-plugin-doctor doctor perf .
188
+ codex-plugin-doctor doctor perf . --json --output perf.json
183
189
  codex-plugin-doctor doctor export --bundle .
184
190
  codex-plugin-doctor doctor export --bundle . --output doctor-bundle.json
185
191
  codex-plugin-doctor doctor snapshot
@@ -190,6 +196,8 @@ codex-plugin-doctor doctor --update-check
190
196
  codex-plugin-doctor audit --installed
191
197
  codex-plugin-doctor audit --installed --security --compat
192
198
  codex-plugin-doctor audit --installed --security --compat --json --output local-audit.json
199
+ codex-plugin-doctor audit --installed --security --compat --cache
200
+ codex-plugin-doctor audit --installed --changed --cache
193
201
  codex-plugin-doctor mcp .
194
202
  codex-plugin-doctor mcp . --json
195
203
  codex-plugin-doctor mcp . --json --output mcp-doctor.json
@@ -246,9 +254,9 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
246
254
 
247
255
  `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
256
 
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.
257
+ `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 npm <package>` runs a preinstall scan by packing the npm package with scripts disabled, extracting the publish tarball, and running validation, security, trust, and recommendation checks against the shipped contents. Add `--json` for automation or `--output npm-preinstall.json` to write the report to disk. `doctor diff --before <path> --after <path>` compares two package roots and reports new findings, resolved findings, trust score delta, and whether risk increased. `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
258
 
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.
259
+ `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
260
 
253
261
  `--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
262
 
@@ -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 summarizePlugins(plugins) {
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
- plugins.push({
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 { type CompatibilityEnvironment, type CompatibilityMatrix } from "../compatibility/compatibility-matrix.js";
1
+ import type { CompatibilityEnvironment, CompatibilityMatrix } from "../compatibility/compatibility-matrix.js";
2
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";
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 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";
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 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
- };
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 { type CompatibilityEnvironment } from "../compatibility/compatibility-matrix.js";
1
+ import type { CompatibilityEnvironment } from "../compatibility/compatibility-matrix.js";
2
2
  import type { CheckResult } from "../domain/types.js";
3
- import { type SecurityAudit } from "../security/security-audit.js";
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 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
- }
1
+ import { buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./package-analysis.js";
80
2
  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
- };
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,33 @@
1
+ import type { CompatibilityEnvironment } from "../compatibility/compatibility-matrix.js";
2
+ import type { JsonReport } from "../domain/types.js";
3
+ import type { SecurityAudit } from "../security/security-audit.js";
4
+ import type { TrustScoreReport } from "../security/trust-score.js";
5
+ import type { DoctorRecommendationsReport } from "./doctor-recommendations.js";
6
+ export interface DoctorNpmPackageReport {
7
+ schemaVersion: "1.0.0";
8
+ generatedAt: string;
9
+ kind: "doctor.npm";
10
+ packageSpec: string;
11
+ package: {
12
+ name: string | null;
13
+ version: string | null;
14
+ fileCount: number | null;
15
+ };
16
+ summary: {
17
+ status: "pass" | "warn" | "fail";
18
+ exitCode: 0 | 1;
19
+ safeToInstall: boolean;
20
+ };
21
+ validation: JsonReport;
22
+ security: SecurityAudit;
23
+ trust: TrustScoreReport;
24
+ recommendations: DoctorRecommendationsReport;
25
+ }
26
+ export interface BuildDoctorNpmPackageReportOptions {
27
+ environment?: CompatibilityEnvironment;
28
+ }
29
+ export declare function buildDoctorNpmPackageReport(packageSpec: string, options?: BuildDoctorNpmPackageReportOptions): Promise<DoctorNpmPackageReport>;
30
+ export declare function renderDoctorNpmPackageReportJson(report: DoctorNpmPackageReport): string;
31
+ export declare function renderDoctorNpmPackageReport(report: DoctorNpmPackageReport, options?: {
32
+ outputPath?: string | null;
33
+ }): string;