codex-plugin-doctor 0.12.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 +7 -1
- 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 +3 -0
- package/dist/index.js +3 -0
- package/dist/run-cli.js +89 -1
- 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
|
@@ -176,6 +176,12 @@ 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 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
|
|
179
185
|
codex-plugin-doctor doctor snapshot
|
|
180
186
|
codex-plugin-doctor doctor snapshot --json
|
|
181
187
|
codex-plugin-doctor doctor snapshot --output doctor-snapshot.json
|
|
@@ -240,7 +246,7 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
|
|
|
240
246
|
|
|
241
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`.
|
|
242
248
|
|
|
243
|
-
`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.
|
|
244
250
|
|
|
245
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.
|
|
246
252
|
|
|
@@ -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,6 +1,9 @@
|
|
|
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";
|
|
4
7
|
export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson, type EcosystemAuditReport } from "./audit/ecosystem-audit.js";
|
|
5
8
|
export { applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames, type PolicyPackName } from "./policy/policy-packs.js";
|
|
6
9
|
export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson, type GenericMcpDoctorReport } from "./mcp/generic-mcp-doctor.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
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";
|
|
4
7
|
export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson } from "./audit/ecosystem-audit.js";
|
|
5
8
|
export { applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames } from "./policy/policy-packs.js";
|
|
6
9
|
export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson } from "./mcp/generic-mcp-doctor.js";
|
package/dist/run-cli.js
CHANGED
|
@@ -14,6 +14,8 @@ import { buildClineInstallPreview, renderClineInstallPreview } from "./compatibi
|
|
|
14
14
|
import { buildWindsurfInstallPreview, renderWindsurfInstallPreview } from "./compatibility/windsurf-install-preview.js";
|
|
15
15
|
import { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
|
|
16
16
|
import { buildDoctorSnapshot, renderDoctorSnapshot, renderDoctorSnapshotJson } from "./core/doctor-snapshot.js";
|
|
17
|
+
import { buildDoctorRecommendations, renderDoctorRecommendations, renderDoctorRecommendationsJson } from "./core/doctor-recommendations.js";
|
|
18
|
+
import { buildDoctorExportBundle, renderDoctorExportBundle, renderDoctorExportBundleJson } from "./core/doctor-export-bundle.js";
|
|
17
19
|
import { applyFixPlan, buildFixPlan, renderApplyFixResult, renderFixPlanJsonReport, renderFixPlan } from "./core/fix-plan.js";
|
|
18
20
|
import { renderClientDoctor, renderEnvironmentDoctor, renderEnvironmentDoctorJson } from "./core/environment-doctor.js";
|
|
19
21
|
import { initCiWorkflow } from "./core/init-ci.js";
|
|
@@ -33,6 +35,7 @@ import { renderTextReport } from "./reporting/render-text-report.js";
|
|
|
33
35
|
import { applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames } from "./policy/policy-packs.js";
|
|
34
36
|
import { findRuleDefinition } from "./rules/rule-catalog.js";
|
|
35
37
|
import { buildSecurityAudit, renderSecurityAuditJson, renderSecurityScorecard } from "./security/security-audit.js";
|
|
38
|
+
import { buildTrustScore, renderTrustScore, renderTrustScoreJson } from "./security/trust-score.js";
|
|
36
39
|
import { createLiveStatusRenderer } from "./terminal/live-status-renderer.js";
|
|
37
40
|
import { determineOutputPolicy } from "./terminal/output-policy.js";
|
|
38
41
|
import { getSpinner } from "./terminal/spinner-registry.js";
|
|
@@ -58,7 +61,7 @@ const defaultIo = {
|
|
|
58
61
|
}
|
|
59
62
|
};
|
|
60
63
|
function printUsage(io) {
|
|
61
|
-
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 [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");
|
|
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");
|
|
62
65
|
}
|
|
63
66
|
function renderInstalledPlugins(plugins) {
|
|
64
67
|
const lines = [
|
|
@@ -191,6 +194,91 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
191
194
|
const doctorFlags = maybePath?.startsWith("--")
|
|
192
195
|
? [maybePath, ...remainingArgs]
|
|
193
196
|
: remainingArgs;
|
|
197
|
+
if (maybePath === "recommend") {
|
|
198
|
+
const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--")
|
|
199
|
+
? remainingArgs[0]
|
|
200
|
+
: ".";
|
|
201
|
+
const recommendFlags = remainingArgs[0] && !remainingArgs[0].startsWith("--")
|
|
202
|
+
? remainingArgs.slice(1)
|
|
203
|
+
: remainingArgs;
|
|
204
|
+
const jsonOutput = recommendFlags.includes("--json");
|
|
205
|
+
const outputIndex = recommendFlags.indexOf("--output");
|
|
206
|
+
const outputPath = outputIndex === -1 ? null : recommendFlags[outputIndex + 1];
|
|
207
|
+
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
208
|
+
io.writeStderr("Missing path after --output.");
|
|
209
|
+
return 2;
|
|
210
|
+
}
|
|
211
|
+
const report = await buildDoctorRecommendations(targetPath, {
|
|
212
|
+
environment: {
|
|
213
|
+
env: terminalContext.env,
|
|
214
|
+
platform: terminalContext.platform
|
|
215
|
+
},
|
|
216
|
+
runCheck: options.runCheckImpl
|
|
217
|
+
? (pathToCheck) => options.runCheckImpl(pathToCheck)
|
|
218
|
+
: undefined
|
|
219
|
+
});
|
|
220
|
+
const renderedReport = jsonOutput
|
|
221
|
+
? renderDoctorRecommendationsJson(report)
|
|
222
|
+
: renderDoctorRecommendations(report);
|
|
223
|
+
if (outputPath) {
|
|
224
|
+
await writeFile(outputPath, renderedReport, "utf8");
|
|
225
|
+
}
|
|
226
|
+
io.writeStdout(renderedReport);
|
|
227
|
+
return report.exitCode;
|
|
228
|
+
}
|
|
229
|
+
if (maybePath === "trust") {
|
|
230
|
+
const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--")
|
|
231
|
+
? remainingArgs[0]
|
|
232
|
+
: ".";
|
|
233
|
+
const trustFlags = remainingArgs[0] && !remainingArgs[0].startsWith("--")
|
|
234
|
+
? remainingArgs.slice(1)
|
|
235
|
+
: remainingArgs;
|
|
236
|
+
const jsonOutput = trustFlags.includes("--json");
|
|
237
|
+
const outputIndex = trustFlags.indexOf("--output");
|
|
238
|
+
const outputPath = outputIndex === -1 ? null : trustFlags[outputIndex + 1];
|
|
239
|
+
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
240
|
+
io.writeStderr("Missing path after --output.");
|
|
241
|
+
return 2;
|
|
242
|
+
}
|
|
243
|
+
const report = await buildTrustScore(targetPath);
|
|
244
|
+
const renderedReport = jsonOutput
|
|
245
|
+
? renderTrustScoreJson(report)
|
|
246
|
+
: renderTrustScore(report);
|
|
247
|
+
if (outputPath) {
|
|
248
|
+
await writeFile(outputPath, renderedReport, "utf8");
|
|
249
|
+
}
|
|
250
|
+
io.writeStdout(renderedReport);
|
|
251
|
+
return report.exitCode;
|
|
252
|
+
}
|
|
253
|
+
if (maybePath === "export") {
|
|
254
|
+
const bundleIndex = remainingArgs.indexOf("--bundle");
|
|
255
|
+
if (bundleIndex === -1) {
|
|
256
|
+
io.writeStderr("Usage: codex-plugin-doctor doctor export --bundle <path> [--json] [--output <path>]");
|
|
257
|
+
return 2;
|
|
258
|
+
}
|
|
259
|
+
const targetPath = remainingArgs[bundleIndex + 1] && !remainingArgs[bundleIndex + 1].startsWith("--")
|
|
260
|
+
? remainingArgs[bundleIndex + 1]
|
|
261
|
+
: ".";
|
|
262
|
+
const jsonOutput = remainingArgs.includes("--json");
|
|
263
|
+
const outputIndex = remainingArgs.indexOf("--output");
|
|
264
|
+
const outputPath = outputIndex === -1 ? null : remainingArgs[outputIndex + 1];
|
|
265
|
+
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
266
|
+
io.writeStderr("Missing path after --output.");
|
|
267
|
+
return 2;
|
|
268
|
+
}
|
|
269
|
+
const bundle = await buildDoctorExportBundle(targetPath, {
|
|
270
|
+
env: terminalContext.env,
|
|
271
|
+
platform: terminalContext.platform
|
|
272
|
+
});
|
|
273
|
+
const bundleJson = renderDoctorExportBundleJson(bundle);
|
|
274
|
+
if (outputPath) {
|
|
275
|
+
await writeFile(outputPath, bundleJson, "utf8");
|
|
276
|
+
}
|
|
277
|
+
io.writeStdout(jsonOutput
|
|
278
|
+
? bundleJson
|
|
279
|
+
: renderDoctorExportBundle(bundle, { outputPath }));
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
194
282
|
if (maybePath === "snapshot") {
|
|
195
283
|
const jsonOutput = doctorFlags.includes("--json");
|
|
196
284
|
const outputIndex = doctorFlags.indexOf("--output");
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Finding } from "../domain/types.js";
|
|
2
|
+
export interface TrustScoreReport {
|
|
3
|
+
schemaVersion: "1.0.0";
|
|
4
|
+
generatedAt: string;
|
|
5
|
+
targetPath: string;
|
|
6
|
+
status: "pass" | "warn" | "fail";
|
|
7
|
+
exitCode: 0 | 1;
|
|
8
|
+
score: number;
|
|
9
|
+
findingCounts: {
|
|
10
|
+
fail: number;
|
|
11
|
+
warn: number;
|
|
12
|
+
total: number;
|
|
13
|
+
};
|
|
14
|
+
packageJson: {
|
|
15
|
+
present: boolean;
|
|
16
|
+
scriptsChecked: number;
|
|
17
|
+
dependenciesChecked: number;
|
|
18
|
+
};
|
|
19
|
+
findings: Finding[];
|
|
20
|
+
}
|
|
21
|
+
export declare function buildTrustScore(targetPath: string): Promise<TrustScoreReport>;
|
|
22
|
+
export declare function renderTrustScoreJson(report: TrustScoreReport): string;
|
|
23
|
+
export declare function renderTrustScore(report: TrustScoreReport): string;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { discoverPackage } from "../core/discover-package.js";
|
|
4
|
+
import { parseJsonText } from "../core/read-json-file.js";
|
|
5
|
+
import { buildSecurityAudit } from "./security-audit.js";
|
|
6
|
+
const lifecycleScripts = new Set([
|
|
7
|
+
"preinstall",
|
|
8
|
+
"install",
|
|
9
|
+
"postinstall",
|
|
10
|
+
"prepublish",
|
|
11
|
+
"prepare"
|
|
12
|
+
]);
|
|
13
|
+
function buildFinding(severity, id, message, impact, suggestedFix) {
|
|
14
|
+
return {
|
|
15
|
+
id,
|
|
16
|
+
severity,
|
|
17
|
+
message,
|
|
18
|
+
impact,
|
|
19
|
+
suggestedFix
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async function fileExists(targetPath) {
|
|
23
|
+
try {
|
|
24
|
+
const details = await stat(targetPath);
|
|
25
|
+
return details.isFile();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function isPlainObject(value) {
|
|
32
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
function containsRemotePipeInstall(script) {
|
|
35
|
+
const normalized = script.toLowerCase();
|
|
36
|
+
return (/\b(curl|wget)\b[^|]*\|\s*(sh|bash)\b/.test(normalized) ||
|
|
37
|
+
/\b(iwr|irm|invoke-webrequest|invoke-restmethod)\b[^|]*\|\s*(iex|invoke-expression)\b/.test(normalized) ||
|
|
38
|
+
/\binvoke-expression\b/.test(normalized));
|
|
39
|
+
}
|
|
40
|
+
async function readPackageJson(rootPath) {
|
|
41
|
+
const packageJsonPath = path.join(rootPath, "package.json");
|
|
42
|
+
if (!(await fileExists(packageJsonPath))) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const parsed = parseJsonText(await readFile(packageJsonPath, "utf8"));
|
|
47
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function auditScripts(packageJson) {
|
|
54
|
+
const scripts = isPlainObject(packageJson.scripts) ? packageJson.scripts : {};
|
|
55
|
+
const findings = [];
|
|
56
|
+
let scriptsChecked = 0;
|
|
57
|
+
for (const [scriptName, scriptValue] of Object.entries(scripts)) {
|
|
58
|
+
if (!lifecycleScripts.has(scriptName) || typeof scriptValue !== "string") {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
scriptsChecked += 1;
|
|
62
|
+
if (containsRemotePipeInstall(scriptValue)) {
|
|
63
|
+
findings.push(buildFinding("fail", "trust.package.remote_pipe_install", `The package lifecycle script \`${scriptName}\` pipes remote content into a shell.`, "Remote download-and-execute scripts can run unreviewed code during install or publish workflows.", "Replace remote pipe execution with pinned package dependencies or a checked-in reviewed setup script."));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
findings.push(buildFinding("warn", "trust.package.lifecycle_script", `The package defines lifecycle script \`${scriptName}\`.`, "Lifecycle scripts execute automatically during package manager workflows and increase supply-chain review scope.", "Keep lifecycle scripts minimal, documented, and covered by release review."));
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
findings,
|
|
70
|
+
scriptsChecked
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function dependencySections(packageJson) {
|
|
74
|
+
return [
|
|
75
|
+
packageJson.dependencies,
|
|
76
|
+
packageJson.devDependencies,
|
|
77
|
+
packageJson.optionalDependencies,
|
|
78
|
+
packageJson.peerDependencies
|
|
79
|
+
].filter(isPlainObject);
|
|
80
|
+
}
|
|
81
|
+
function auditDependencies(packageJson) {
|
|
82
|
+
const findings = [];
|
|
83
|
+
let dependenciesChecked = 0;
|
|
84
|
+
for (const dependencies of dependencySections(packageJson)) {
|
|
85
|
+
for (const [dependencyName, versionSpec] of Object.entries(dependencies)) {
|
|
86
|
+
if (typeof versionSpec !== "string") {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
dependenciesChecked += 1;
|
|
90
|
+
if (versionSpec === "*" || versionSpec.toLowerCase() === "latest") {
|
|
91
|
+
findings.push(buildFinding("warn", "trust.package.unpinned_dependency", `The dependency \`${dependencyName}\` uses broad version spec \`${versionSpec}\`.`, "Broad dependency ranges make package resolution less reproducible across installs and releases.", "Pin the dependency to a specific compatible range or exact version."));
|
|
92
|
+
}
|
|
93
|
+
if (/^(git\+|github:|http:\/\/|https:\/\/)/i.test(versionSpec)) {
|
|
94
|
+
findings.push(buildFinding("warn", "trust.package.remote_dependency", `The dependency \`${dependencyName}\` resolves from remote spec \`${versionSpec}\`.`, "Remote dependency specs can change outside the npm registry's normal version and integrity workflow.", "Prefer registry-published dependencies with pinned semver ranges."));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
findings,
|
|
100
|
+
dependenciesChecked
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function dedupeFindings(findings) {
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
return findings.filter((finding) => {
|
|
106
|
+
const key = `${finding.id}\n${finding.message}`;
|
|
107
|
+
if (seen.has(key)) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
seen.add(key);
|
|
111
|
+
return true;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function scoreFindings(findings) {
|
|
115
|
+
const failCount = findings.filter((finding) => finding.severity === "fail").length;
|
|
116
|
+
const warnCount = findings.filter((finding) => finding.severity === "warn").length;
|
|
117
|
+
return Math.max(0, 100 - (failCount * 35) - (warnCount * 10));
|
|
118
|
+
}
|
|
119
|
+
export async function buildTrustScore(targetPath) {
|
|
120
|
+
const rootPath = path.resolve(targetPath);
|
|
121
|
+
const packageJson = await readPackageJson(rootPath);
|
|
122
|
+
const scriptAudit = packageJson
|
|
123
|
+
? auditScripts(packageJson)
|
|
124
|
+
: { findings: [], scriptsChecked: 0 };
|
|
125
|
+
const dependencyAudit = packageJson
|
|
126
|
+
? auditDependencies(packageJson)
|
|
127
|
+
: { findings: [], dependenciesChecked: 0 };
|
|
128
|
+
const discoveredPackage = await discoverPackage(rootPath);
|
|
129
|
+
const securityAudit = discoveredPackage
|
|
130
|
+
? await buildSecurityAudit(rootPath)
|
|
131
|
+
: null;
|
|
132
|
+
const findings = dedupeFindings([
|
|
133
|
+
...scriptAudit.findings,
|
|
134
|
+
...dependencyAudit.findings,
|
|
135
|
+
...(securityAudit?.findings ?? [])
|
|
136
|
+
]);
|
|
137
|
+
const fail = findings.filter((finding) => finding.severity === "fail").length;
|
|
138
|
+
const warn = findings.filter((finding) => finding.severity === "warn").length;
|
|
139
|
+
const score = scoreFindings(findings);
|
|
140
|
+
const status = fail > 0
|
|
141
|
+
? "fail"
|
|
142
|
+
: warn > 0
|
|
143
|
+
? "warn"
|
|
144
|
+
: "pass";
|
|
145
|
+
return {
|
|
146
|
+
schemaVersion: "1.0.0",
|
|
147
|
+
generatedAt: new Date().toISOString(),
|
|
148
|
+
targetPath: rootPath,
|
|
149
|
+
status,
|
|
150
|
+
exitCode: status === "fail" ? 1 : 0,
|
|
151
|
+
score,
|
|
152
|
+
findingCounts: {
|
|
153
|
+
fail,
|
|
154
|
+
warn,
|
|
155
|
+
total: findings.length
|
|
156
|
+
},
|
|
157
|
+
packageJson: {
|
|
158
|
+
present: packageJson !== null,
|
|
159
|
+
scriptsChecked: scriptAudit.scriptsChecked,
|
|
160
|
+
dependenciesChecked: dependencyAudit.dependenciesChecked
|
|
161
|
+
},
|
|
162
|
+
findings
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export function renderTrustScoreJson(report) {
|
|
166
|
+
return JSON.stringify(report, null, 2);
|
|
167
|
+
}
|
|
168
|
+
export function renderTrustScore(report) {
|
|
169
|
+
const lines = [
|
|
170
|
+
"Doctor Trust Score",
|
|
171
|
+
"==================",
|
|
172
|
+
`Target: ${report.targetPath}`,
|
|
173
|
+
`Status: ${report.status.toUpperCase()}`,
|
|
174
|
+
`Score: ${report.score}/100`,
|
|
175
|
+
`Summary: ${report.findingCounts.fail} fail, ${report.findingCounts.warn} warn, ${report.findingCounts.total} total`
|
|
176
|
+
];
|
|
177
|
+
if (report.findings.length === 0) {
|
|
178
|
+
lines.push("", "No trust findings.");
|
|
179
|
+
return lines.join("\n");
|
|
180
|
+
}
|
|
181
|
+
const appendSection = (title, findings, marker) => {
|
|
182
|
+
if (findings.length === 0) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
lines.push("", title, "--------");
|
|
186
|
+
for (const finding of findings) {
|
|
187
|
+
lines.push(`${marker} ${finding.id}`);
|
|
188
|
+
lines.push(` Message: ${finding.message}`);
|
|
189
|
+
lines.push(` Impact: ${finding.impact}`);
|
|
190
|
+
lines.push(` Suggested fix: ${finding.suggestedFix}`);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
appendSection("Failures", report.findings.filter((finding) => finding.severity === "fail"), "x");
|
|
194
|
+
appendSection("Warnings", report.findings.filter((finding) => finding.severity === "warn"), "!");
|
|
195
|
+
return lines.join("\n");
|
|
196
|
+
}
|
package/package.json
CHANGED