@praneeth_54/agentdoctor 0.2.0-beta → 0.3.0-beta
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/CHANGELOG.md +32 -3
- package/README.md +46 -25
- package/dist/cli/commands/doctor.js +1 -1
- package/dist/cli/commands/explain.js +2 -2
- package/dist/cli/commands/fix.d.ts +7 -5
- package/dist/cli/commands/fix.js +57 -12
- package/dist/cli/commands/verify.d.ts +13 -0
- package/dist/cli/commands/verify.js +56 -0
- package/dist/cli/program.js +53 -30
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/core/fix/apply.d.ts +10 -0
- package/dist/core/fix/apply.js +27 -0
- package/dist/core/fix/patterns.d.ts +7 -0
- package/dist/core/fix/patterns.js +21 -0
- package/dist/core/fix/plan.d.ts +9 -0
- package/dist/core/fix/plan.js +140 -0
- package/dist/core/fix/render.d.ts +6 -0
- package/dist/core/fix/render.js +68 -0
- package/dist/core/fix/run.d.ts +15 -0
- package/dist/core/fix/run.js +54 -0
- package/dist/core/fix/types.d.ts +34 -0
- package/dist/core/fix/types.js +2 -0
- package/dist/core/fix/writers/cursorignore.d.ts +13 -0
- package/dist/core/fix/writers/cursorignore.js +74 -0
- package/dist/core/rules/context/generated-directory.js +10 -0
- package/dist/core/rules/instructions/missing-path-reference.js +90 -1
- package/dist/core/rules/path-kind.d.ts +14 -0
- package/dist/core/rules/path-kind.js +77 -0
- package/dist/core/rules/security/env-file-exposure.js +14 -14
- package/dist/core/rules/security/private-key-file.js +4 -0
- package/dist/core/verify/compare.d.ts +29 -0
- package/dist/core/verify/compare.js +56 -0
- package/dist/core/verify/load-baseline.d.ts +15 -0
- package/dist/core/verify/load-baseline.js +71 -0
- package/dist/core/verify/verify.d.ts +26 -0
- package/dist/core/verify/verify.js +40 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +5 -0
- package/dist/reporters/terminal/report.js +7 -1
- package/dist/reporters/verify/json.d.ts +5 -0
- package/dist/reporters/verify/json.js +51 -0
- package/dist/reporters/verify/terminal.d.ts +2 -0
- package/dist/reporters/verify/terminal.js +45 -0
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSampleOrTestPath } from "../path-kind.js";
|
|
1
2
|
const TEMPLATE_BASENAMES = new Set([".env.example", ".env.sample", ".env.template", ".env.dist"]);
|
|
2
3
|
const BACKUP_BASENAMES = new Set([".env_backup", ".env_old", ".env_local"]);
|
|
3
4
|
/**
|
|
@@ -10,11 +11,18 @@ export function classifyEnvBasename(base) {
|
|
|
10
11
|
return null;
|
|
11
12
|
}
|
|
12
13
|
const lower = normalized.toLowerCase();
|
|
14
|
+
if (BACKUP_BASENAMES.has(lower)) {
|
|
15
|
+
return "backup";
|
|
16
|
+
}
|
|
13
17
|
if (TEMPLATE_BASENAMES.has(lower)) {
|
|
14
18
|
return "template";
|
|
15
19
|
}
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
// .env.local.example, .env.testing.example, .env.example.local, etc.
|
|
21
|
+
if (/\.(?:example|sample|template)(?:\.|$)/i.test(normalized)) {
|
|
22
|
+
return "template";
|
|
23
|
+
}
|
|
24
|
+
if (/\.env(?:\.[A-Za-z0-9_-]+)*\.(?:example|sample|template)$/i.test(normalized)) {
|
|
25
|
+
return "template";
|
|
18
26
|
}
|
|
19
27
|
if (lower === ".env") {
|
|
20
28
|
return "runtime";
|
|
@@ -93,19 +101,11 @@ export const envFileExposureRule = {
|
|
|
93
101
|
if (!kind) {
|
|
94
102
|
continue;
|
|
95
103
|
}
|
|
104
|
+
if (isSampleOrTestPath(file.relativePath)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// Templates document variable names; they are not agent-readiness findings.
|
|
96
108
|
if (kind === "template") {
|
|
97
|
-
findings.push({
|
|
98
|
-
ruleId: "security/env-file-exposure",
|
|
99
|
-
category: "security",
|
|
100
|
-
severity: "info",
|
|
101
|
-
title: "Environment template file present",
|
|
102
|
-
message: `Environment template file detected at ${file.relativePath}`,
|
|
103
|
-
whyItMatters: "Template env files are usually placeholders, but they can still document secret names. Filename classification only — contents were not inspected.",
|
|
104
|
-
recommendation: "Keep templates free of real credentials. Prefer documenting variable names without values.",
|
|
105
|
-
affectedAgents: [],
|
|
106
|
-
evidence: { path: file.relativePath, detail: "template" },
|
|
107
|
-
fixability: "review",
|
|
108
|
-
});
|
|
109
109
|
continue;
|
|
110
110
|
}
|
|
111
111
|
if (!agentsPresent) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSampleOrTestPath } from "../path-kind.js";
|
|
1
2
|
const PRIVATE_KEY_PATTERNS = [
|
|
2
3
|
{ test: (b) => /^id_rsa$/i.test(b), label: "SSH private key" },
|
|
3
4
|
{ test: (b) => /^id_ed25519$/i.test(b), label: "SSH private key" },
|
|
@@ -40,6 +41,9 @@ export const privateKeyFileRule = {
|
|
|
40
41
|
const findings = [];
|
|
41
42
|
const affected = context.agents.filter((a) => a.detected || a.configured).map((a) => a.id);
|
|
42
43
|
for (const file of context.discovery.files) {
|
|
44
|
+
if (isSampleOrTestPath(file.relativePath)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
43
47
|
const base = file.relativePath.split("/").pop() ?? file.relativePath;
|
|
44
48
|
const label = classify(base);
|
|
45
49
|
if (!label) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Finding, FindingEvidence, Severity } from "../../types/index.js";
|
|
2
|
+
/** Stable subset of a finding for verify reports. */
|
|
3
|
+
export interface VerifyFindingRef {
|
|
4
|
+
id: string;
|
|
5
|
+
ruleId: string;
|
|
6
|
+
severity: Severity;
|
|
7
|
+
title: string;
|
|
8
|
+
message: string;
|
|
9
|
+
evidence?: FindingEvidence;
|
|
10
|
+
}
|
|
11
|
+
export interface FindingCompareResult {
|
|
12
|
+
fixed: VerifyFindingRef[];
|
|
13
|
+
remaining: VerifyFindingRef[];
|
|
14
|
+
new: VerifyFindingRef[];
|
|
15
|
+
unchanged: VerifyFindingRef[];
|
|
16
|
+
summary: {
|
|
17
|
+
fixed: number;
|
|
18
|
+
remaining: number;
|
|
19
|
+
new: number;
|
|
20
|
+
unchanged: number;
|
|
21
|
+
before: number;
|
|
22
|
+
after: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export declare function toVerifyFindingRef(finding: Finding): VerifyFindingRef;
|
|
26
|
+
/**
|
|
27
|
+
* Deterministic finding set comparison keyed by stable `Finding.id`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function compareFindings(before: readonly Finding[], after: readonly Finding[]): FindingCompareResult;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export function toVerifyFindingRef(finding) {
|
|
2
|
+
const ref = {
|
|
3
|
+
id: finding.id,
|
|
4
|
+
ruleId: finding.ruleId,
|
|
5
|
+
severity: finding.severity,
|
|
6
|
+
title: finding.title,
|
|
7
|
+
message: finding.message,
|
|
8
|
+
};
|
|
9
|
+
if (finding.evidence !== undefined) {
|
|
10
|
+
ref.evidence = { ...finding.evidence };
|
|
11
|
+
}
|
|
12
|
+
return ref;
|
|
13
|
+
}
|
|
14
|
+
function sortRefs(refs) {
|
|
15
|
+
return [...refs].sort((a, b) => a.id.localeCompare(b.id));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Deterministic finding set comparison keyed by stable `Finding.id`.
|
|
19
|
+
*/
|
|
20
|
+
export function compareFindings(before, after) {
|
|
21
|
+
const beforeById = new Map(before.map((f) => [f.id, f]));
|
|
22
|
+
const afterById = new Map(after.map((f) => [f.id, f]));
|
|
23
|
+
const fixed = [];
|
|
24
|
+
const remaining = [];
|
|
25
|
+
const newly = [];
|
|
26
|
+
for (const [id, finding] of beforeById) {
|
|
27
|
+
if (!afterById.has(id)) {
|
|
28
|
+
fixed.push(toVerifyFindingRef(finding));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
remaining.push(toVerifyFindingRef(afterById.get(id)));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
for (const [id, finding] of afterById) {
|
|
35
|
+
if (!beforeById.has(id)) {
|
|
36
|
+
newly.push(toVerifyFindingRef(finding));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const fixedSorted = sortRefs(fixed);
|
|
40
|
+
const remainingSorted = sortRefs(remaining);
|
|
41
|
+
const newSorted = sortRefs(newly);
|
|
42
|
+
return {
|
|
43
|
+
fixed: fixedSorted,
|
|
44
|
+
remaining: remainingSorted,
|
|
45
|
+
new: newSorted,
|
|
46
|
+
unchanged: remainingSorted,
|
|
47
|
+
summary: {
|
|
48
|
+
fixed: fixedSorted.length,
|
|
49
|
+
remaining: remainingSorted.length,
|
|
50
|
+
new: newSorted.length,
|
|
51
|
+
unchanged: remainingSorted.length,
|
|
52
|
+
before: before.length,
|
|
53
|
+
after: after.length,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Finding } from "../../types/index.js";
|
|
2
|
+
declare const DEFAULT_BASELINE_NAMES: readonly ["agentdoctor-report.json", ".agentdoctor-baseline.json"];
|
|
3
|
+
export interface LoadedBaseline {
|
|
4
|
+
path: string;
|
|
5
|
+
findings: Finding[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Load findings from a prior `scan --json` report (or a bare `{ findings: [...] }` file).
|
|
9
|
+
*/
|
|
10
|
+
export declare function loadBaselineFindings(baselinePath: string): Promise<LoadedBaseline>;
|
|
11
|
+
/**
|
|
12
|
+
* Resolve baseline path: explicit flag, else first default filename that exists under root.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveBaselinePath(root: string, explicitBaseline?: string): Promise<string | undefined>;
|
|
15
|
+
export { DEFAULT_BASELINE_NAMES };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const DEFAULT_BASELINE_NAMES = ["agentdoctor-report.json", ".agentdoctor-baseline.json"];
|
|
4
|
+
function isFindingLike(value) {
|
|
5
|
+
if (!value || typeof value !== "object") {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
const record = value;
|
|
9
|
+
return (typeof record.id === "string" &&
|
|
10
|
+
typeof record.ruleId === "string" &&
|
|
11
|
+
typeof record.severity === "string" &&
|
|
12
|
+
typeof record.title === "string" &&
|
|
13
|
+
typeof record.message === "string");
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Load findings from a prior `scan --json` report (or a bare `{ findings: [...] }` file).
|
|
17
|
+
*/
|
|
18
|
+
export async function loadBaselineFindings(baselinePath) {
|
|
19
|
+
const absolute = path.resolve(baselinePath);
|
|
20
|
+
let raw;
|
|
21
|
+
try {
|
|
22
|
+
raw = await fs.readFile(absolute, "utf8");
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new Error(`baseline not found or unreadable: ${absolute}`);
|
|
26
|
+
}
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = JSON.parse(raw);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new Error(`baseline is not valid JSON: ${absolute}`);
|
|
33
|
+
}
|
|
34
|
+
if (!parsed || typeof parsed !== "object") {
|
|
35
|
+
throw new Error(`baseline JSON must be an object: ${absolute}`);
|
|
36
|
+
}
|
|
37
|
+
const findingsUnknown = parsed.findings;
|
|
38
|
+
if (!Array.isArray(findingsUnknown)) {
|
|
39
|
+
throw new Error(`baseline JSON must include a findings array: ${absolute}`);
|
|
40
|
+
}
|
|
41
|
+
const findings = [];
|
|
42
|
+
for (const item of findingsUnknown) {
|
|
43
|
+
if (!isFindingLike(item)) {
|
|
44
|
+
throw new Error(`baseline contains an invalid finding entry: ${absolute}`);
|
|
45
|
+
}
|
|
46
|
+
findings.push(item);
|
|
47
|
+
}
|
|
48
|
+
return { path: absolute, findings };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve baseline path: explicit flag, else first default filename that exists under root.
|
|
52
|
+
*/
|
|
53
|
+
export async function resolveBaselinePath(root, explicitBaseline) {
|
|
54
|
+
if (explicitBaseline) {
|
|
55
|
+
return path.isAbsolute(explicitBaseline)
|
|
56
|
+
? explicitBaseline
|
|
57
|
+
: path.resolve(root, explicitBaseline);
|
|
58
|
+
}
|
|
59
|
+
for (const name of DEFAULT_BASELINE_NAMES) {
|
|
60
|
+
const candidate = path.join(root, name);
|
|
61
|
+
try {
|
|
62
|
+
await fs.access(candidate);
|
|
63
|
+
return candidate;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// try next
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
export { DEFAULT_BASELINE_NAMES };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Scores, ScanResult } from "../../types/index.js";
|
|
2
|
+
import { type FindingCompareResult } from "./compare.js";
|
|
3
|
+
export interface VerifyOptions {
|
|
4
|
+
cwd?: string;
|
|
5
|
+
/** Path to a prior scan JSON report. Defaults to agentdoctor-report.json if present. */
|
|
6
|
+
baselinePath?: string;
|
|
7
|
+
verbose?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface VerifyResult extends FindingCompareResult {
|
|
10
|
+
version: string;
|
|
11
|
+
repositoryRoot: string;
|
|
12
|
+
baselinePath: string;
|
|
13
|
+
after: ScanResult;
|
|
14
|
+
scores: Scores | null;
|
|
15
|
+
scoringAvailable: boolean;
|
|
16
|
+
timing: {
|
|
17
|
+
totalMs: number;
|
|
18
|
+
scanMs: number;
|
|
19
|
+
compareMs: number;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Re-scan the repository and compare against a prior scan JSON baseline.
|
|
24
|
+
* Reuses the public `scan()` pipeline — no duplicated rule engine.
|
|
25
|
+
*/
|
|
26
|
+
export declare function verify(options?: VerifyOptions): Promise<VerifyResult>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { PACKAGE_VERSION } from "../../constants.js";
|
|
2
|
+
import { scan } from "../scanner/scan.js";
|
|
3
|
+
import { compareFindings } from "./compare.js";
|
|
4
|
+
import { loadBaselineFindings, resolveBaselinePath } from "./load-baseline.js";
|
|
5
|
+
/**
|
|
6
|
+
* Re-scan the repository and compare against a prior scan JSON baseline.
|
|
7
|
+
* Reuses the public `scan()` pipeline — no duplicated rule engine.
|
|
8
|
+
*/
|
|
9
|
+
export async function verify(options = {}) {
|
|
10
|
+
const totalStarted = performance.now();
|
|
11
|
+
const cwd = options.cwd ?? process.cwd();
|
|
12
|
+
const resolvedBaseline = await resolveBaselinePath(cwd, options.baselinePath);
|
|
13
|
+
if (!resolvedBaseline) {
|
|
14
|
+
throw new Error("No verify baseline found. Run `agentdoctor scan --json > agentdoctor-report.json`, apply fixes, then re-run verify.");
|
|
15
|
+
}
|
|
16
|
+
const baseline = await loadBaselineFindings(resolvedBaseline);
|
|
17
|
+
const scanStarted = performance.now();
|
|
18
|
+
const after = await scan({
|
|
19
|
+
cwd,
|
|
20
|
+
verbose: options.verbose ?? false,
|
|
21
|
+
});
|
|
22
|
+
const scanMs = Math.round(performance.now() - scanStarted);
|
|
23
|
+
const compareStarted = performance.now();
|
|
24
|
+
const compared = compareFindings(baseline.findings, after.findings);
|
|
25
|
+
const compareMs = Math.round(performance.now() - compareStarted);
|
|
26
|
+
return {
|
|
27
|
+
version: PACKAGE_VERSION,
|
|
28
|
+
repositoryRoot: after.repository.root,
|
|
29
|
+
baselinePath: baseline.path,
|
|
30
|
+
...compared,
|
|
31
|
+
after,
|
|
32
|
+
scores: after.scores,
|
|
33
|
+
scoringAvailable: after.scoringAvailable,
|
|
34
|
+
timing: {
|
|
35
|
+
totalMs: Math.round(performance.now() - totalStarted),
|
|
36
|
+
scanMs,
|
|
37
|
+
compareMs,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
export { scan } from "./core/scanner/scan.js";
|
|
2
|
+
export { verify } from "./core/verify/verify.js";
|
|
3
|
+
export { compareFindings } from "./core/verify/compare.js";
|
|
2
4
|
export { PACKAGE_VERSION } from "./constants.js";
|
|
3
5
|
export type { ScanOptions, ScanResult, RepositoryInfo, Finding, FindingsSummary, Scores, AgentPresence, AgentDetectionResult, AgentSecurityAnalysisMode, CliOptions, ExitCode, } from "./types/index.js";
|
|
6
|
+
export type { VerifyResult, VerifyOptions } from "./core/verify/verify.js";
|
|
7
|
+
export type { VerifyFindingRef, FindingCompareResult } from "./core/verify/compare.js";
|
|
4
8
|
export { EXIT_CODES } from "./types/index.js";
|
|
5
9
|
export { agentRegistry } from "./agents/registry.js";
|
|
6
10
|
export { detectAgents } from "./agents/detect-agents.js";
|
|
7
11
|
export { ruleRegistry, getRuleById } from "./core/rules/registry.js";
|
|
8
12
|
export { runRules } from "./core/rules/run-rules.js";
|
|
13
|
+
export { buildFixPlan } from "./core/fix/plan.js";
|
|
14
|
+
export { applyFixPlan } from "./core/fix/apply.js";
|
|
15
|
+
export { runFix } from "./core/fix/run.js";
|
|
16
|
+
export type { FixPlan, FixAction, FixApplyResult } from "./core/fix/types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export { scan } from "./core/scanner/scan.js";
|
|
2
|
+
export { verify } from "./core/verify/verify.js";
|
|
3
|
+
export { compareFindings } from "./core/verify/compare.js";
|
|
2
4
|
export { PACKAGE_VERSION } from "./constants.js";
|
|
3
5
|
export { EXIT_CODES } from "./types/index.js";
|
|
4
6
|
export { agentRegistry } from "./agents/registry.js";
|
|
5
7
|
export { detectAgents } from "./agents/detect-agents.js";
|
|
6
8
|
export { ruleRegistry, getRuleById } from "./core/rules/registry.js";
|
|
7
9
|
export { runRules } from "./core/rules/run-rules.js";
|
|
10
|
+
export { buildFixPlan } from "./core/fix/plan.js";
|
|
11
|
+
export { applyFixPlan } from "./core/fix/apply.js";
|
|
12
|
+
export { runFix } from "./core/fix/run.js";
|
|
@@ -159,7 +159,13 @@ export function renderTerminalReport(result, options = {}) {
|
|
|
159
159
|
lines.push(` ${result.summary.warning} warning`);
|
|
160
160
|
lines.push(` ${result.summary.info} info`);
|
|
161
161
|
lines.push("");
|
|
162
|
-
|
|
162
|
+
if (result.scoringAvailable && result.scores) {
|
|
163
|
+
lines.push(` Readiness: ${result.scores.overall}/100`);
|
|
164
|
+
lines.push(colors.dim(" Category and agent scores: agentdoctor scan --json"));
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
lines.push(colors.dim(" Readiness scoring unavailable for this scan"));
|
|
168
|
+
}
|
|
163
169
|
lines.push("");
|
|
164
170
|
if (verbose) {
|
|
165
171
|
lines.push(colors.dim("Timing"));
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { sanitizeForOutput } from "../../utils/path.js";
|
|
2
|
+
/**
|
|
3
|
+
* Machine-readable verify report.
|
|
4
|
+
*/
|
|
5
|
+
export function renderVerifyJsonReport(result) {
|
|
6
|
+
const payload = {
|
|
7
|
+
version: result.version,
|
|
8
|
+
baselinePath: sanitizeForOutput(result.baselinePath),
|
|
9
|
+
repositoryRoot: sanitizeForOutput(result.repositoryRoot),
|
|
10
|
+
fixed: result.fixed.map((f) => ({
|
|
11
|
+
...f,
|
|
12
|
+
message: sanitizeForOutput(f.message),
|
|
13
|
+
title: sanitizeForOutput(f.title),
|
|
14
|
+
...(f.evidence?.path !== undefined
|
|
15
|
+
? { evidence: { ...f.evidence, path: sanitizeForOutput(f.evidence.path) } }
|
|
16
|
+
: f.evidence !== undefined
|
|
17
|
+
? { evidence: f.evidence }
|
|
18
|
+
: {}),
|
|
19
|
+
})),
|
|
20
|
+
remaining: result.remaining.map((f) => ({
|
|
21
|
+
...f,
|
|
22
|
+
message: sanitizeForOutput(f.message),
|
|
23
|
+
title: sanitizeForOutput(f.title),
|
|
24
|
+
...(f.evidence?.path !== undefined
|
|
25
|
+
? { evidence: { ...f.evidence, path: sanitizeForOutput(f.evidence.path) } }
|
|
26
|
+
: f.evidence !== undefined
|
|
27
|
+
? { evidence: f.evidence }
|
|
28
|
+
: {}),
|
|
29
|
+
})),
|
|
30
|
+
new: result.new.map((f) => ({
|
|
31
|
+
...f,
|
|
32
|
+
message: sanitizeForOutput(f.message),
|
|
33
|
+
title: sanitizeForOutput(f.title),
|
|
34
|
+
...(f.evidence?.path !== undefined
|
|
35
|
+
? { evidence: { ...f.evidence, path: sanitizeForOutput(f.evidence.path) } }
|
|
36
|
+
: f.evidence !== undefined
|
|
37
|
+
? { evidence: f.evidence }
|
|
38
|
+
: {}),
|
|
39
|
+
})),
|
|
40
|
+
unchanged: result.unchanged.map((f) => ({
|
|
41
|
+
id: f.id,
|
|
42
|
+
ruleId: f.ruleId,
|
|
43
|
+
severity: f.severity,
|
|
44
|
+
})),
|
|
45
|
+
summary: result.summary,
|
|
46
|
+
scoringAvailable: result.scoringAvailable,
|
|
47
|
+
scores: result.scores,
|
|
48
|
+
timing: result.timing,
|
|
49
|
+
};
|
|
50
|
+
return `${JSON.stringify(payload, null, 2)}\n`;
|
|
51
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { PACKAGE_VERSION } from "../../constants.js";
|
|
2
|
+
import { colors, symbolFail, symbolOk, symbolWarn } from "../../utils/colors.js";
|
|
3
|
+
import { sanitizeForOutput } from "../../utils/path.js";
|
|
4
|
+
function renderBucket(title, symbol, color, items, limit) {
|
|
5
|
+
const lines = [];
|
|
6
|
+
lines.push(color(`${symbol} ${title} (${items.length})`));
|
|
7
|
+
if (items.length === 0) {
|
|
8
|
+
lines.push(colors.dim(" (none)"));
|
|
9
|
+
lines.push("");
|
|
10
|
+
return lines;
|
|
11
|
+
}
|
|
12
|
+
const shown = items.slice(0, limit);
|
|
13
|
+
for (const item of shown) {
|
|
14
|
+
const path = item.evidence?.path ? ` — ${sanitizeForOutput(item.evidence.path)}` : "";
|
|
15
|
+
lines.push(` ${sanitizeForOutput(item.ruleId)}${path}`);
|
|
16
|
+
lines.push(colors.dim(` ${sanitizeForOutput(item.title)}`));
|
|
17
|
+
}
|
|
18
|
+
if (items.length > limit) {
|
|
19
|
+
lines.push(colors.dim(` … and ${items.length - limit} more`));
|
|
20
|
+
}
|
|
21
|
+
lines.push("");
|
|
22
|
+
return lines;
|
|
23
|
+
}
|
|
24
|
+
export function renderVerifyTerminalReport(result, verbose = false) {
|
|
25
|
+
const lines = [];
|
|
26
|
+
const limit = verbose ? 50 : 12;
|
|
27
|
+
lines.push("");
|
|
28
|
+
lines.push(colors.bold(`AgentDoctor verify v${PACKAGE_VERSION}`));
|
|
29
|
+
lines.push(colors.dim(`Baseline: ${sanitizeForOutput(result.baselinePath)}`));
|
|
30
|
+
lines.push("");
|
|
31
|
+
lines.push(...renderBucket("Fixed", symbolOk(), colors.green, result.fixed, limit));
|
|
32
|
+
lines.push(...renderBucket("Remaining", symbolWarn(), colors.yellow, result.remaining, limit));
|
|
33
|
+
lines.push(...renderBucket("New", symbolFail(), colors.red, result.new, limit));
|
|
34
|
+
lines.push(colors.bold("Summary"));
|
|
35
|
+
lines.push(` Fixed ${result.summary.fixed} · Remaining ${result.summary.remaining} · New ${result.summary.new} · Unchanged ${result.summary.unchanged}`);
|
|
36
|
+
lines.push(colors.dim(` Before ${result.summary.before} → After ${result.summary.after} findings`));
|
|
37
|
+
if (result.scores) {
|
|
38
|
+
lines.push(colors.dim(` Score ${result.scores.overall}/100`));
|
|
39
|
+
}
|
|
40
|
+
if (verbose) {
|
|
41
|
+
lines.push(colors.dim(` Timing scan=${result.timing.scanMs}ms compare=${result.timing.compareMs}ms total=${result.timing.totalMs}ms`));
|
|
42
|
+
}
|
|
43
|
+
lines.push("");
|
|
44
|
+
return `${lines.join("\n")}\n`;
|
|
45
|
+
}
|