Skip to content

Commit 11a4972

Browse files
committed
refactor(cli): consolidate rendering in render.ts, thin commands/findings.ts
- Move all print* functions from commands/findings.ts into render.ts (the designated single rendering surface) - Add printDeleteResult to render.ts (was only in commands/findings.ts) - commands/findings.ts is now pure dispatch: arg parse + domain call + render call + exit code. All print helpers come from ../render.js - Build clean, 37/37 tests pass
1 parent 4547617 commit 11a4972

3 files changed

Lines changed: 49 additions & 239 deletions

File tree

src/cli/commands/findings.ts

Lines changed: 12 additions & 230 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@ import {
2222
type FindingRestoreResult,
2323
type FindingDeleteResult,
2424
} from "../findings.js";
25+
import {
26+
printArchivedSummaries,
27+
printArchiveResult,
28+
printDeleteResult,
29+
printFindingDetail,
30+
printFindingDoctor,
31+
printFindingSummaries,
32+
printFindingTemplateResult,
33+
printFindingValidation,
34+
printRestoreResult,
35+
printWorkflowSummaries,
36+
} from "../render.js";
2537
import { usage } from "../usage.js";
2638
import { firstPositionalAfter, parseStatus, parseReason, wantsJson } from "./shared.js";
2739
import {
@@ -232,41 +244,6 @@ async function runFindingsDoctor(args: string[], json: boolean): Promise<void> {
232244
}
233245
}
234246

235-
function printFindingDoctor(result: FindingDoctorResult): void {
236-
console.log(title(`doctor ${result.id}`));
237-
console.log(
238-
panel("readiness", [
239-
...kv([
240-
["status", statusBadge(result.status as "candidate" | "confirmed" | "blocked")],
241-
["validation", validationBadge(result.validationOk)],
242-
["evidence", readiness(result.evidenceScore)],
243-
["submission", readiness(result.submissionScore)],
244-
["threshold", String(result.submissionThreshold)],
245-
["report ready", result.reportReady ? "yes" : "no"],
246-
["next", cmd(result.nextAction)],
247-
]),
248-
]),
249-
);
250-
251-
if (result.issues.length === 0) {
252-
console.log(empty("No readiness issues."));
253-
return;
254-
}
255-
256-
console.log(section("Issues"));
257-
console.log(
258-
table(
259-
["", "severity", "message", "next action"],
260-
result.issues.map((issue) => [
261-
statusIcon(issue.severity === "error" ? "fail" : issue.severity === "warning" ? "warn" : "pass"),
262-
issue.severity,
263-
truncate(issue.message, 64),
264-
cmd(truncate(issue.nextAction, 40)),
265-
]),
266-
),
267-
);
268-
}
269-
270247
async function runFindingsPromote(args: string[], json: boolean): Promise<void> {
271248
const target = firstPositionalAfter(args, "promote");
272249
const status = parseStatus(args);
@@ -352,198 +329,3 @@ async function runFindingsDelete(args: string[], json: boolean): Promise<void> {
352329
}
353330
printDeleteResult(result);
354331
}
355-
356-
function printFindingSummaries(findings: FindingSummary[]): void {
357-
console.log(title("active findings"));
358-
console.log(
359-
table(
360-
["id", "status", "evidence", "submission", "package", "vulnerability"],
361-
findings.map((finding) => [
362-
truncate(finding.id, 36),
363-
statusBadge(finding.status),
364-
readiness(finding.evidenceScore),
365-
readiness(finding.submissionScore),
366-
truncate(`${finding.ecosystem}:${finding.package}`, 42),
367-
truncate(finding.vulnerability, 36),
368-
]),
369-
),
370-
);
371-
}
372-
373-
function printWorkflowSummaries(findings: FindingWorkflowSummary[]): void {
374-
console.log(title("workflow queue"));
375-
console.log(
376-
table(
377-
["id", "priority", "status", "evidence", "submission", "next action", "package", "vulnerability"],
378-
findings.map((finding) => [
379-
truncate(finding.id, 30),
380-
String(finding.priority),
381-
statusBadge(finding.status),
382-
readiness(finding.evidenceScore),
383-
readiness(finding.submissionScore),
384-
cmd(truncate(finding.nextAction, 46)),
385-
truncate(`${finding.ecosystem}:${finding.package}`, 34),
386-
truncate(finding.vulnerability, 30),
387-
]),
388-
),
389-
);
390-
}
391-
392-
function printFindingDetail(finding: FindingDetail): void {
393-
console.log(title(`finding ${finding.id}`));
394-
const lines = kv([
395-
["status", statusBadge(finding.status)],
396-
["evidence", readiness(finding.evidenceScore)],
397-
["submission", readiness(finding.submissionScore)],
398-
["verdict", `${finding.verdict.exploitability}/${finding.verdict.confidence}`],
399-
["validation", validationBadge(finding.validation.ok)],
400-
["priority", `${finding.priority} (${finding.priorityReason})`],
401-
["path", finding.path],
402-
["package", `${finding.ecosystem}:${finding.package}`],
403-
["vulnerability", finding.vulnerability],
404-
]);
405-
if (finding.reproArtifacts.length > 0) {
406-
lines.push(...kv([["repro artifacts", String(finding.reproArtifacts.length)]]));
407-
}
408-
if (finding.threatMap) {
409-
lines.push(...kv([["threat map", finding.threatMap.path]]));
410-
lines.push("", muted("threat map"));
411-
lines.push(...finding.threatMap.rendered.map((item) => ` ${item}`));
412-
}
413-
if (finding.archived) {
414-
lines.push(...kv([
415-
["archived", finding.archivedAt ?? "unknown"],
416-
["reason", finding.archiveReason ?? "unknown"],
417-
]));
418-
}
419-
lines.push(...kv([["next", cmd(finding.nextAction)]]));
420-
if (finding.validation.errors.length > 0) {
421-
lines.push("", tuiError("errors"));
422-
lines.push(...finding.validation.errors.map((item) => ` ${item}`));
423-
}
424-
if (finding.validation.warnings.length > 0) {
425-
lines.push("", warn("warnings"));
426-
lines.push(...finding.validation.warnings.slice(0, 8).map((item) => ` ${item}`));
427-
if (finding.validation.warnings.length > 8) {
428-
lines.push(muted(` ... ${finding.validation.warnings.length - 8} more warning(s)`));
429-
}
430-
}
431-
if (finding.missingFields.length > 0) {
432-
lines.push("", muted(`missing ${finding.missingFields.join(", ")}`));
433-
}
434-
console.log(panel(finding.id, lines));
435-
}
436-
437-
function printArchivedSummaries(findings: ArchivedFindingSummary[]): void {
438-
if (findings.length === 0) {
439-
console.log(empty("No archived findings."));
440-
return;
441-
}
442-
console.log(title("archive"));
443-
console.log(
444-
table(
445-
["id", "status", "archived", "reason", "package"],
446-
findings.map((finding) => [
447-
truncate(finding.id, 34),
448-
statusBadge(finding.status),
449-
truncate(finding.archivedAt, 27),
450-
truncate(finding.archiveReason, 26),
451-
truncate(`${finding.ecosystem}:${finding.package}`, 40),
452-
]),
453-
),
454-
);
455-
}
456-
457-
function printFindingValidation(result: FindingValidation): void {
458-
const lines = kv([
459-
["status", statusBadge(result.status)],
460-
["evidence", readiness(result.evidenceScore)],
461-
["submission", readiness(result.submissionScore)],
462-
["validation", validationBadge(result.ok)],
463-
["path", result.path],
464-
]);
465-
if (result.errors.length > 0) {
466-
lines.push("", tuiError("errors"));
467-
lines.push(...result.errors.map((item) => ` ${item}`));
468-
}
469-
if (result.warnings.length > 0) {
470-
lines.push("", warn("warnings"));
471-
lines.push(...result.warnings.map((item) => ` ${item}`));
472-
}
473-
console.log(panel(result.id, lines));
474-
}
475-
476-
function printFindingTemplateResult(result: FindingTemplateResult): void {
477-
console.log(
478-
panel("finding created", [
479-
...kv([
480-
["id", result.id],
481-
["path", result.path],
482-
["status", statusBadge(result.status)],
483-
["next", cmd(`omv findings show ${result.id}`)],
484-
]),
485-
]),
486-
);
487-
}
488-
489-
function printArchiveResult(result: FindingArchiveResult): void {
490-
console.log(
491-
panel("finding archived", [
492-
...kv([
493-
["id", result.id],
494-
["status", statusBadge(result.status)],
495-
["from", result.from],
496-
["to", result.to],
497-
["reason", result.archiveReason],
498-
["reports", result.reportArtifactPaths.length > 0 ? `${result.reportArtifactPaths.length} artifact(s)` : "none"],
499-
["submissions", result.submissionRecords.length > 0 ? `${result.submissionRecords.length} record(s)` : "none"],
500-
["next", cmd(`omv findings show ${result.id} --archived`)],
501-
]),
502-
...result.warnings.map((item) => warn(`warning ${item}`)),
503-
]),
504-
);
505-
}
506-
507-
function printRestoreResult(result: FindingRestoreResult): void {
508-
console.log(
509-
panel("finding restored", [
510-
...kv([
511-
["id", result.id],
512-
["status", statusBadge(result.status)],
513-
["from", result.from],
514-
["to", result.to],
515-
["next", cmd("omv findings workflow")],
516-
]),
517-
]),
518-
);
519-
}
520-
521-
function printDeleteResult(result: FindingDeleteResult): void {
522-
if (!result.deleted) {
523-
console.log(
524-
panel("finding delete preview", [
525-
...kv([
526-
["id", result.id],
527-
["action", "preview only"],
528-
["next", cmd(`omv findings delete ${result.id} --force`)],
529-
]),
530-
"",
531-
muted("paths to delete"),
532-
...result.paths.map((p) => ` ${p}`),
533-
]),
534-
);
535-
return;
536-
}
537-
538-
const finalState = result.errors.length > 0 ? "warn" : "pass";
539-
console.log(
540-
panel("finding deleted", [
541-
...kv([
542-
["id", result.id],
543-
["result", outcomeBadge(finalState)],
544-
["paths", `${result.paths.length} file(s) removed`],
545-
]),
546-
...(result.errors.length > 0 ? ["", warn("errors"), ...result.errors.map((e) => ` ${e}`)] : []),
547-
]),
548-
);
549-
}

src/cli/render.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Check, DoctorResult } from "./doctor.js";
22
import type {
33
ArchivedFindingSummary,
44
FindingArchiveResult,
5+
FindingDeleteResult,
56
FindingDetail,
67
FindingDoctorResult,
78
FindingRestoreResult,
@@ -449,6 +450,36 @@ export function printRestoreResult(result: FindingRestoreResult): void {
449450
);
450451
}
451452

453+
export function printDeleteResult(result: FindingDeleteResult): void {
454+
if (!result.deleted) {
455+
console.log(
456+
panel("finding delete preview", [
457+
...kv([
458+
["id", result.id],
459+
["action", "preview only"],
460+
["next", cmd(`omv findings delete ${result.id} --force`)],
461+
]),
462+
"",
463+
muted("paths to delete"),
464+
...result.paths.map((p) => ` ${p}`),
465+
]),
466+
);
467+
return;
468+
}
469+
470+
const finalState = result.errors.length > 0 ? "warn" : "pass";
471+
console.log(
472+
panel("finding deleted", [
473+
...kv([
474+
["id", result.id],
475+
["result", outcomeBadge(finalState)],
476+
["paths", `${result.paths.length} file(s) removed`],
477+
]),
478+
...(result.errors.length > 0 ? ["", warn("errors"), ...result.errors.map((e) => ` ${e}`)] : []),
479+
]),
480+
);
481+
}
482+
452483
function formatCheckDetail(check: Check): string {
453484
return `${statusIcon(check.status)} ${check.name}: ${check.message}`;
454485
}

src/cli/repro.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,11 @@ export async function checkReportArtifacts(id: string, projectRoot = process.cwd
8686
if (!existsSync(findingPath)) {
8787
throw new Error(`${findingPath} does not exist`);
8888
}
89-
const parsed = (await readEvidence(findingPath)).data;
90-
const status = getString(parsed, "status") || "unknown";
89+
const { data } = await readEvidence(findingPath);
90+
const status = getString(data, "status") || "unknown";
9191
const reportsPath = findingReportsDir(normalizedId, projectRoot);
9292
const reproPath = findingReproDir(normalizedId, projectRoot);
93-
const listedReproArtifacts = getList(parsed, "evidence.repro_artifacts").map(String).filter((value) => value.trim() !== "");
93+
const listedReproArtifacts = getList(data, "evidence.repro_artifacts").map(String).filter((value) => value.trim() !== "");
9494
const existingReproArtifacts = existingArtifactPaths(listedReproArtifacts, projectRoot);
9595
const missingReproArtifacts = listedReproArtifacts.filter((value) => !artifactExists(value, projectRoot));
9696
const reportArtifactPaths = await listReportArtifacts(normalizedId, projectRoot);
@@ -149,7 +149,7 @@ Do not place secrets, live target data, or private disclosure material here.
149149
async function mergeReproArtifacts(findingPath: string, artifacts: string[]): Promise<boolean> {
150150
const text = await readFile(findingPath, "utf-8");
151151
const doc = parseDocument(text);
152-
const values = getList(parseEvidenceYaml(text).data, "evidence.repro_artifacts").map(String);
152+
const values = getList((await parseEvidenceYaml(text)).data, "evidence.repro_artifacts").map(String);
153153
const merged = [...values];
154154
for (const artifact of artifacts) {
155155
if (!merged.includes(artifact)) {
@@ -258,8 +258,8 @@ function getValue(data: Record<string, unknown>, path: string): unknown {
258258
return current;
259259
}
260260

261-
function parseEvidenceYaml(text: string): { data: Record<string, unknown>; errors: string[] } {
262-
const { parse: parseYaml } = requireModule("yaml");
261+
async function parseEvidenceYaml(text: string): Promise<{ data: Record<string, unknown>; errors: string[] }> {
262+
const { parse: parseYaml } = await import("yaml");
263263
try {
264264
const parsed = parseYaml(text);
265265
if (!isRecord(parsed)) {
@@ -281,6 +281,3 @@ async function readEvidence(path: string): Promise<{ data: Record<string, unknow
281281
function isRecord(value: unknown): value is Record<string, unknown> {
282282
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
283283
}
284-
285-
import { createRequire } from "module";
286-
const requireModule = createRequire(import.meta.url);

0 commit comments

Comments
 (0)