@westbayberry/dg 2.0.7 → 2.0.10
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 +17 -12
- package/dist/api/analyze.js +134 -34
- package/dist/audit-ui/export.js +3 -4
- package/dist/auth/device-login.js +13 -9
- package/dist/auth/store.js +43 -26
- package/dist/bin/dg.js +5 -0
- package/dist/commands/audit.js +14 -4
- package/dist/commands/config.js +3 -5
- package/dist/commands/doctor.js +3 -3
- package/dist/commands/explain.js +138 -6
- package/dist/commands/licenses.js +37 -24
- package/dist/commands/login.js +12 -3
- package/dist/commands/logout.js +15 -4
- package/dist/commands/scan.js +1 -1
- package/dist/commands/service.js +76 -24
- package/dist/commands/status.js +38 -4
- package/dist/commands/types.js +1 -0
- package/dist/config/settings.js +102 -22
- package/dist/install-ui/prompt.js +5 -2
- package/dist/launcher/install-preflight.js +158 -0
- package/dist/launcher/live-install.js +11 -2
- package/dist/launcher/output-redaction.js +5 -3
- package/dist/launcher/pip-report.js +18 -2
- package/dist/launcher/preflight-prompt.js +31 -12
- package/dist/launcher/run.js +87 -8
- package/dist/proxy/ca.js +69 -29
- package/dist/proxy/enforcement.js +41 -3
- package/dist/proxy/worker.js +21 -9
- package/dist/runtime/first-run.js +33 -2
- package/dist/runtime/nudges.js +9 -2
- package/dist/scan/analyze-worker.js +18 -8
- package/dist/scan/collect.js +35 -28
- package/dist/scan/command.js +80 -40
- package/dist/scan/discovery.js +9 -3
- package/dist/scan/render.js +22 -6
- package/dist/scan/scanner-report.js +89 -12
- package/dist/scan/staged.js +69 -7
- package/dist/scan-ui/LegacyApp.js +10 -48
- package/dist/scan-ui/components/InteractiveResultsView.js +171 -111
- package/dist/scan-ui/components/ProjectSelector.js +3 -3
- package/dist/scan-ui/components/ScoreHeader.js +8 -4
- package/dist/scan-ui/hooks/useScan.js +74 -27
- package/dist/scan-ui/launch.js +18 -4
- package/dist/service/state.js +15 -4
- package/dist/service/trust-store.js +23 -2
- package/dist/setup/git-hook.js +28 -17
- package/dist/setup/plan.js +302 -18
- package/dist/state/cleanup-registry.js +65 -8
- package/dist/state/locks.js +95 -9
- package/dist/state/sessions.js +66 -2
- package/dist/verify/package-check.js +22 -3
- package/dist/verify/preflight.js +328 -170
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ import chalk from "chalk";
|
|
|
5
5
|
import { writeFileSync } from "node:fs";
|
|
6
6
|
import { resolve as resolvePath } from "node:path";
|
|
7
7
|
import { isLoggedIn } from "../shims.js";
|
|
8
|
-
import { ScoreHeader } from "./ScoreHeader.js";
|
|
8
|
+
import { ScoreHeader, COMPACT_ROWS } from "./ScoreHeader.js";
|
|
9
9
|
import { useExpandAnimation } from "../hooks/useExpandAnimation.js";
|
|
10
10
|
import { useTerminalSize } from "../hooks/useTerminalSize.js";
|
|
11
11
|
import { clearScreen } from "../alt-screen.js";
|
|
@@ -51,77 +51,55 @@ export function packageBadge(pkg) {
|
|
|
51
51
|
return actionBadge(pkg.action);
|
|
52
52
|
}
|
|
53
53
|
const EVIDENCE_LIMIT = 2;
|
|
54
|
+
const BADGE_COL = "Unverified".length + 1;
|
|
54
55
|
// Fixed lines outside the scrollable group area:
|
|
55
56
|
// 5 ScoreHeader box | 2 Flagged box top | 2 scroll indicators
|
|
56
57
|
// 1 Flagged box bottom | 4 Clean/Duration box | 1 help bar | 1 margin
|
|
57
58
|
const FIXED_CHROME = 21;
|
|
59
|
+
const FIXED_CHROME_COMPACT = 14;
|
|
58
60
|
function firstPackage(group) {
|
|
59
61
|
const rep = group.packages[0];
|
|
60
62
|
if (!rep)
|
|
61
63
|
throw new Error("package group cannot be empty");
|
|
62
64
|
return rep;
|
|
63
65
|
}
|
|
66
|
+
function statusSummaryLines(rep) {
|
|
67
|
+
if ((rep.action ?? "pass") === "pass")
|
|
68
|
+
return ["No findings"];
|
|
69
|
+
const reasons = rep.reasons.length > 0
|
|
70
|
+
? [...rep.reasons]
|
|
71
|
+
: rep.findings.map((f) => f.title ?? "").filter((t) => t.length > 0);
|
|
72
|
+
if (reasons.length > 0)
|
|
73
|
+
return reasons;
|
|
74
|
+
return rep.action === "analysis_incomplete" ? ["Analysis incomplete"] : [];
|
|
75
|
+
}
|
|
64
76
|
function findingsSummaryHeight(group) {
|
|
65
77
|
const rep = firstPackage(group);
|
|
66
78
|
const visibleFindings = rep.findings.filter((f) => f.severity > 1);
|
|
67
79
|
const isFree = visibleFindings.length > 0 && !visibleFindings[0]?.title;
|
|
68
80
|
let h = 0;
|
|
69
81
|
if (rep.license)
|
|
70
|
-
h += 1;
|
|
82
|
+
h += 1;
|
|
71
83
|
if (isFree) {
|
|
72
|
-
// Free tier: just the upgrade prompt line
|
|
73
84
|
h += 1;
|
|
74
85
|
}
|
|
75
86
|
else if (visibleFindings.length > 0) {
|
|
76
|
-
// Paid tier: one line per finding
|
|
77
87
|
h += visibleFindings.length;
|
|
78
88
|
}
|
|
79
89
|
else if (rep.score > 0) {
|
|
80
|
-
h += 1; // score-only fallback
|
|
81
|
-
}
|
|
82
|
-
if (group.packages.length > 3)
|
|
83
|
-
h += 1;
|
|
84
|
-
return h;
|
|
85
|
-
}
|
|
86
|
-
function findingsDetailHeight(group, safeVersions) {
|
|
87
|
-
const rep = firstPackage(group);
|
|
88
|
-
const visibleFindings = rep.findings
|
|
89
|
-
.filter((f) => f.severity > 1)
|
|
90
|
-
.sort((a, b) => b.severity - a.severity);
|
|
91
|
-
let h = 0;
|
|
92
|
-
if (rep.license)
|
|
93
|
-
h += 1; // license info line
|
|
94
|
-
if (group.packages.length > 3)
|
|
95
90
|
h += 1;
|
|
96
|
-
// Free tier: reasons + upgrade hint
|
|
97
|
-
if (visibleFindings.length === 0 && rep.score > 0) {
|
|
98
|
-
h += (rep.reasons ?? []).length;
|
|
99
|
-
h += 1; // upgrade hint
|
|
100
91
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
h += 1; // badge + id
|
|
104
|
-
h += 1; // title
|
|
105
|
-
const evidence = finding.evidence ?? [];
|
|
106
|
-
h += Math.min(evidence.length, EVIDENCE_LIMIT);
|
|
107
|
-
if (evidence.length > EVIDENCE_LIMIT)
|
|
108
|
-
h += 1;
|
|
92
|
+
else {
|
|
93
|
+
h += statusSummaryLines(rep).length;
|
|
109
94
|
}
|
|
110
|
-
|
|
111
|
-
if (visibleFindings.length > 0 && !hasEvidence)
|
|
112
|
-
h += 1;
|
|
113
|
-
if (rep.recommendation)
|
|
114
|
-
h += 1;
|
|
115
|
-
if (safeVersions[rep.name])
|
|
95
|
+
if (group.packages.length > 3)
|
|
116
96
|
h += 1;
|
|
117
97
|
return h;
|
|
118
98
|
}
|
|
119
|
-
function groupRowHeight(group, level
|
|
99
|
+
function groupRowHeight(group, level) {
|
|
120
100
|
if (level === null)
|
|
121
101
|
return 1;
|
|
122
|
-
|
|
123
|
-
return 1 + findingsSummaryHeight(group);
|
|
124
|
-
return 1 + findingsDetailHeight(group, safeVersions);
|
|
102
|
+
return 1 + findingsSummaryHeight(group);
|
|
125
103
|
}
|
|
126
104
|
function nameVer(p) {
|
|
127
105
|
return p.version ? `${p.name}@${p.version}` : p.name;
|
|
@@ -145,19 +123,23 @@ function affectsLine(group) {
|
|
|
145
123
|
const DETAIL_PANE_CHROME = 20;
|
|
146
124
|
function buildDetailLines(group, safeVersion, maxWidth) {
|
|
147
125
|
const rep = firstPackage(group);
|
|
126
|
+
const badge = packageBadge(rep);
|
|
127
|
+
const analyzedAt = rep.analyzedAt;
|
|
148
128
|
const visibleFindings = rep.findings
|
|
149
129
|
.filter((f) => f.severity > 1)
|
|
150
130
|
.sort((a, b) => b.severity - a.severity);
|
|
131
|
+
const isFree = visibleFindings.length > 0 && !visibleFindings[0]?.title;
|
|
151
132
|
const lines = [];
|
|
133
|
+
lines.push(_jsxs(Text, { wrap: "truncate-end", children: [chalk.dim("Verdict"), " ", badge.color(badge.label), rep.version ? chalk.dim(` · v${rep.version}`) : "", analyzedAt ? chalk.dim(` · analyzed ${analyzedAt}`) : ""] }, "verdict"));
|
|
134
|
+
lines.push(_jsx(Text, { children: "" }, "verdict-gap"));
|
|
152
135
|
if (group.packages.length > 3) {
|
|
153
|
-
lines.push(_jsxs(Text, { dimColor: true, children: ["Affects: ", affectsLine(group)] }, "affects"));
|
|
136
|
+
lines.push(_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["Affects: ", affectsLine(group)] }, "affects"));
|
|
154
137
|
lines.push(_jsx(Text, { children: "" }, "affects-gap"));
|
|
155
138
|
}
|
|
156
139
|
if (rep.score > 0) {
|
|
157
140
|
lines.push(_jsxs(Text, { dimColor: true, children: ["Score: ", rep.score, "/100"] }, "score-info"));
|
|
158
141
|
lines.push(_jsx(Text, { children: "" }, "score-gap"));
|
|
159
142
|
}
|
|
160
|
-
// Paid tier: show findings with category + severity
|
|
161
143
|
if (visibleFindings.length > 0) {
|
|
162
144
|
for (let i = 0; i < visibleFindings.length; i++) {
|
|
163
145
|
const f = visibleFindings[i];
|
|
@@ -166,15 +148,36 @@ function buildDetailLines(group, safeVersion, maxWidth) {
|
|
|
166
148
|
const sevLabel = SEVERITY_LABELS[f.severity] ?? "INFO";
|
|
167
149
|
const sevColor = SEVERITY_COLORS[f.severity] ?? SEVERITY_COLORS[1] ?? chalk.dim;
|
|
168
150
|
const connector = i === visibleFindings.length - 1 ? T.last : T.branch;
|
|
169
|
-
lines.push(_jsxs(Text, { children: [connector, " ", sevColor(pad(sevLabel,
|
|
151
|
+
lines.push(_jsxs(Text, { wrap: "truncate-end", children: [connector, " ", sevColor(pad(sevLabel, 5)), " ", chalk.dim(f.category ?? ""), f.title ? `: ${f.title}` : ""] }, `finding-${i}`));
|
|
152
|
+
const evidence = f.evidence ?? [];
|
|
153
|
+
for (let e = 0; e < Math.min(evidence.length, EVIDENCE_LIMIT); e++) {
|
|
154
|
+
lines.push(_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [" ", T.pipe, " ", evidence[e]] }, `evidence-${i}-${e}`));
|
|
155
|
+
}
|
|
156
|
+
if (evidence.length > EVIDENCE_LIMIT) {
|
|
157
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", T.pipe, " + ", evidence.length - EVIDENCE_LIMIT, " more"] }, `evidence-more-${i}`));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (isFree) {
|
|
161
|
+
lines.push(_jsxs(Text, { color: "yellow", children: [chalk.yellow("→"), " Upgrade to Pro for finding details"] }, "upgrade"));
|
|
170
162
|
}
|
|
171
163
|
lines.push(_jsx(Text, { children: "" }, "findings-gap"));
|
|
172
164
|
}
|
|
173
165
|
else if (rep.score > 0) {
|
|
174
|
-
// Free tier: no findings returned — show upgrade prompt
|
|
175
166
|
lines.push(_jsxs(Text, { color: "yellow", children: [chalk.yellow("\u2192"), " Upgrade to Pro to see risk categories"] }, "upgrade"));
|
|
176
167
|
lines.push(_jsx(Text, { children: "" }, "upgrade-gap"));
|
|
177
168
|
}
|
|
169
|
+
else {
|
|
170
|
+
const statusLines = statusSummaryLines(rep);
|
|
171
|
+
statusLines.forEach((line, i) => {
|
|
172
|
+
lines.push(_jsx(Text, { wrap: "truncate-end", children: rep.action === "analysis_incomplete"
|
|
173
|
+
? chalk.yellow(line)
|
|
174
|
+
: (rep.action ?? "pass") === "pass"
|
|
175
|
+
? chalk.green(`✓ ${line}`)
|
|
176
|
+
: chalk.dim(line) }, `status-${i}`));
|
|
177
|
+
});
|
|
178
|
+
if (statusLines.length > 0)
|
|
179
|
+
lines.push(_jsx(Text, { children: "" }, "status-gap"));
|
|
180
|
+
}
|
|
178
181
|
if (rep.recommendation) {
|
|
179
182
|
lines.push(_jsxs(Text, { children: [chalk.dim("Recommendation:"), " ", chalk.cyan(truncate(rep.recommendation, maxWidth - 18))] }, "recommendation"));
|
|
180
183
|
}
|
|
@@ -188,9 +191,7 @@ function viewReducer(_state, action) {
|
|
|
188
191
|
case "MOVE":
|
|
189
192
|
return { ..._state, cursor: action.cursor, viewport: action.viewport };
|
|
190
193
|
case "EXPAND":
|
|
191
|
-
return { ..._state,
|
|
192
|
-
case "MOVE_EXPAND":
|
|
193
|
-
return { cursor: action.cursor, expandedIndex: action.expandedIndex, expandLevel: action.expandLevel, viewport: action.viewport };
|
|
194
|
+
return { ..._state, expandedKey: action.expandedKey, expandLevel: action.expandLevel, viewport: action.viewport };
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
export const InteractiveResultsView = ({ result, config: _config, durationMs, onExit, onBack, discoveredTotal, userStatus, scanUsage: scanUsageProp, initialView, }) => {
|
|
@@ -212,17 +213,28 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
212
213
|
const total = result.packages.length;
|
|
213
214
|
const [searchQuery, setSearchQuery] = useState("");
|
|
214
215
|
const allGroups = useMemo(() => groupPackages(flagged), [flagged]);
|
|
215
|
-
const allGroupCount = allGroups.length;
|
|
216
216
|
const groups = useMemo(() => {
|
|
217
217
|
if (!searchQuery)
|
|
218
218
|
return allGroups;
|
|
219
219
|
const q = searchQuery.toLowerCase();
|
|
220
|
-
|
|
221
|
-
|
|
220
|
+
const matched = [];
|
|
221
|
+
for (const g of allGroups) {
|
|
222
|
+
const members = g.packages.filter((p) => p.name.toLowerCase().includes(q));
|
|
223
|
+
if (members.length > 0)
|
|
224
|
+
matched.push({ packages: members, key: g.key });
|
|
225
|
+
}
|
|
226
|
+
for (const p of clean) {
|
|
227
|
+
if (p.name.toLowerCase().includes(q)) {
|
|
228
|
+
matched.push({ packages: [p], key: `pass|${p.name}@${p.version ?? ""}` });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return matched;
|
|
232
|
+
}, [allGroups, clean, searchQuery]);
|
|
233
|
+
const matchCount = useMemo(() => groups.reduce((n, g) => n + g.packages.length, 0), [groups]);
|
|
222
234
|
const [view, dispatchView] = useReducer(viewReducer, {
|
|
223
235
|
cursor: 0,
|
|
224
236
|
expandLevel: null,
|
|
225
|
-
|
|
237
|
+
expandedKey: null,
|
|
226
238
|
viewport: 0,
|
|
227
239
|
});
|
|
228
240
|
const viewRef = useRef(view);
|
|
@@ -261,6 +273,10 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
261
273
|
clearTimeout(exportMsgRef.current);
|
|
262
274
|
exportMsgRef.current = setTimeout(() => setExportMsg(null), 4000);
|
|
263
275
|
};
|
|
276
|
+
useEffect(() => () => {
|
|
277
|
+
if (exportMsgRef.current)
|
|
278
|
+
clearTimeout(exportMsgRef.current);
|
|
279
|
+
}, []);
|
|
264
280
|
const [exportMenu, setExportMenu] = useState(null);
|
|
265
281
|
const exportMenuRef = useRef(exportMenu);
|
|
266
282
|
exportMenuRef.current = exportMenu;
|
|
@@ -355,8 +371,7 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
355
371
|
return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
356
372
|
};
|
|
357
373
|
/** Convert a payload + scope to a serialized string in the chosen format
|
|
358
|
-
* plus the file extension.
|
|
359
|
-
* full scan tree); we degrade those to JSON with a comment. */
|
|
374
|
+
* plus the file extension. */
|
|
360
375
|
const formatExport = (payload, scope, format) => {
|
|
361
376
|
if (format === "json") {
|
|
362
377
|
return { body: JSON.stringify(payload, null, 2) + "\n", ext: "json" };
|
|
@@ -397,8 +412,22 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
397
412
|
}
|
|
398
413
|
return { body: lines.join("\n") + "\n", ext: "csv" };
|
|
399
414
|
}
|
|
400
|
-
|
|
401
|
-
|
|
415
|
+
if (scope === "summary") {
|
|
416
|
+
const s = payload;
|
|
417
|
+
const lines = [
|
|
418
|
+
"score,action,packages_scanned,blocked,warned,clean,duration_ms",
|
|
419
|
+
[s.score, s.action, s.packagesScanned, s.blocked, s.warned, s.clean, s.durationMs].map(csvCell).join(","),
|
|
420
|
+
];
|
|
421
|
+
return { body: lines.join("\n") + "\n", ext: "csv" };
|
|
422
|
+
}
|
|
423
|
+
const a = payload;
|
|
424
|
+
const lines = [
|
|
425
|
+
"scan_score,scan_action,name,version,score,action,license,riskCategory",
|
|
426
|
+
];
|
|
427
|
+
for (const p of a.packages) {
|
|
428
|
+
lines.push([a.score, a.action, p.name, p.version, p.score, p.action, p.license?.spdx ?? p.license?.raw ?? null, p.license?.riskCategory ?? null].map(csvCell).join(","));
|
|
429
|
+
}
|
|
430
|
+
return { body: lines.join("\n") + "\n", ext: "csv" };
|
|
402
431
|
}
|
|
403
432
|
// Markdown — for `all`, render summary + packages table + licenses table
|
|
404
433
|
// in one document so the file matches what the user sees in the TUI.
|
|
@@ -589,38 +618,38 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
589
618
|
const searchModeRef = useRef(searchMode);
|
|
590
619
|
searchModeRef.current = searchMode;
|
|
591
620
|
const { rows: termRows, cols: termCols } = useTerminalSize();
|
|
592
|
-
const
|
|
621
|
+
const compact = termRows < COMPACT_ROWS;
|
|
622
|
+
const availableRows = Math.max(5, termRows - (compact ? FIXED_CHROME_COMPACT : FIXED_CHROME));
|
|
593
623
|
const innerWidth = Math.max(40, termCols - 6);
|
|
594
|
-
const
|
|
624
|
+
const detailGroup = useMemo(() => {
|
|
625
|
+
if (!detailPane)
|
|
626
|
+
return null;
|
|
627
|
+
return groups.find((g) => g.key === detailPane.groupKey) ?? null;
|
|
628
|
+
}, [detailPane, groups]);
|
|
595
629
|
const detailLines = useMemo(() => {
|
|
596
|
-
if (
|
|
630
|
+
if (!detailGroup)
|
|
597
631
|
return [];
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
return [];
|
|
601
|
-
return buildDetailLines(group, result.safeVersions[firstPackage(group).name], innerWidth);
|
|
602
|
-
}, [detailGroupIdx, groups, result.safeVersions, innerWidth]);
|
|
632
|
+
return buildDetailLines(detailGroup, result.safeVersions[firstPackage(detailGroup).name], innerWidth);
|
|
633
|
+
}, [detailGroup, result.safeVersions, innerWidth]);
|
|
603
634
|
const detailContentRows = Math.max(3, termRows - DETAIL_PANE_CHROME);
|
|
604
635
|
const getLevel = (idx) => {
|
|
605
|
-
return view.
|
|
636
|
+
return view.expandedKey !== null && groups[idx]?.key === view.expandedKey ? view.expandLevel : null;
|
|
606
637
|
};
|
|
607
638
|
const expandTargetHeight = useMemo(() => {
|
|
608
|
-
if (view.
|
|
639
|
+
if (view.expandedKey === null || view.expandLevel === null)
|
|
609
640
|
return 0;
|
|
610
|
-
const group = groups
|
|
641
|
+
const group = groups.find((g) => g.key === view.expandedKey);
|
|
611
642
|
if (!group)
|
|
612
643
|
return 0;
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
const { visibleLines: animVisibleLines } = useExpandAnimation(expandTargetHeight, view.expandedIndex !== null);
|
|
618
|
-
const animatedGroupHeight = (group, level, idx) => {
|
|
644
|
+
return findingsSummaryHeight(group);
|
|
645
|
+
}, [view.expandedKey, view.expandLevel, groups]);
|
|
646
|
+
const { visibleLines: animVisibleLines } = useExpandAnimation(expandTargetHeight, view.expandedKey !== null);
|
|
647
|
+
const animatedGroupHeight = (group, level) => {
|
|
619
648
|
if (level === null)
|
|
620
649
|
return 1;
|
|
621
|
-
if (
|
|
650
|
+
if (group.key === view.expandedKey)
|
|
622
651
|
return 1 + animVisibleLines;
|
|
623
|
-
return groupRowHeight(group, level
|
|
652
|
+
return groupRowHeight(group, level);
|
|
624
653
|
};
|
|
625
654
|
const visibleEnd = useMemo(() => {
|
|
626
655
|
let consumed = 0;
|
|
@@ -630,7 +659,7 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
630
659
|
const endGroup = groups[end];
|
|
631
660
|
if (!endGroup)
|
|
632
661
|
break;
|
|
633
|
-
const h = animatedGroupHeight(endGroup, level
|
|
662
|
+
const h = animatedGroupHeight(endGroup, level);
|
|
634
663
|
if (consumed + h > availableRows)
|
|
635
664
|
break;
|
|
636
665
|
consumed += h;
|
|
@@ -639,17 +668,17 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
639
668
|
if (end === view.viewport && groups.length > 0)
|
|
640
669
|
end = view.viewport + 1;
|
|
641
670
|
return end;
|
|
642
|
-
}, [view.viewport, groups, view.
|
|
643
|
-
const adjustViewport = (cursor,
|
|
671
|
+
}, [view.viewport, groups, view.expandedKey, view.expandLevel, animVisibleLines, availableRows]);
|
|
672
|
+
const adjustViewport = (cursor, expKey, expLvl, currentStart) => {
|
|
644
673
|
if (cursor < currentStart)
|
|
645
674
|
return cursor;
|
|
646
|
-
const getLvl = (i) => (
|
|
675
|
+
const getLvl = (i) => (expKey !== null && groups[i]?.key === expKey ? expLvl : null);
|
|
647
676
|
let consumed = 0;
|
|
648
677
|
for (let i = currentStart; i <= cursor && i < groups.length; i++) {
|
|
649
678
|
const g = groups[i];
|
|
650
679
|
if (!g)
|
|
651
680
|
continue;
|
|
652
|
-
consumed += groupRowHeight(g, getLvl(i)
|
|
681
|
+
consumed += groupRowHeight(g, getLvl(i));
|
|
653
682
|
}
|
|
654
683
|
if (consumed <= availableRows)
|
|
655
684
|
return currentStart;
|
|
@@ -661,7 +690,7 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
661
690
|
const g = groups[i];
|
|
662
691
|
if (!g)
|
|
663
692
|
continue;
|
|
664
|
-
consumed += groupRowHeight(g, getLvl(i)
|
|
693
|
+
consumed += groupRowHeight(g, getLvl(i));
|
|
665
694
|
}
|
|
666
695
|
if (consumed <= availableRows)
|
|
667
696
|
break;
|
|
@@ -672,9 +701,9 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
672
701
|
useEffect(() => {
|
|
673
702
|
if (groups.length === 0)
|
|
674
703
|
return;
|
|
675
|
-
const { cursor,
|
|
704
|
+
const { cursor, expandedKey, expandLevel, viewport } = viewRef.current;
|
|
676
705
|
const clamped = Math.min(viewport, Math.max(0, groups.length - 1));
|
|
677
|
-
const newVp = adjustViewport(cursor,
|
|
706
|
+
const newVp = adjustViewport(cursor, expandedKey, expandLevel, clamped);
|
|
678
707
|
dispatchView({ type: "MOVE", cursor, viewport: newVp });
|
|
679
708
|
}, [availableRows]);
|
|
680
709
|
// Clamp detail pane scroll when terminal resizes
|
|
@@ -683,15 +712,15 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
683
712
|
if (dp && detailLines.length > 0) {
|
|
684
713
|
const maxScroll = Math.max(0, detailLines.length - detailContentRows);
|
|
685
714
|
if (dp.scroll > maxScroll) {
|
|
686
|
-
setDetailPane({
|
|
715
|
+
setDetailPane({ groupKey: dp.groupKey, scroll: maxScroll });
|
|
687
716
|
}
|
|
688
717
|
}
|
|
689
718
|
}, [detailContentRows, detailLines.length]);
|
|
690
719
|
useEffect(() => {
|
|
691
|
-
if (detailPane !== null && !
|
|
720
|
+
if (detailPane !== null && !detailGroup) {
|
|
692
721
|
setDetailPane(null);
|
|
693
722
|
}
|
|
694
|
-
}, [detailPane,
|
|
723
|
+
}, [detailPane, detailGroup]);
|
|
695
724
|
useInput((input, key) => {
|
|
696
725
|
// Export menu has the highest priority — once opened, all keys go here
|
|
697
726
|
// until ⏎/Esc dismisses it. Arrow-driven: ↑↓ scrolls within the active
|
|
@@ -874,16 +903,19 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
874
903
|
if (dp !== null) {
|
|
875
904
|
const maxScroll = Math.max(0, detailLines.length - detailContentRows);
|
|
876
905
|
if (key.upArrow || input === "k") {
|
|
877
|
-
setDetailPane({
|
|
906
|
+
setDetailPane({ groupKey: dp.groupKey, scroll: Math.max(0, dp.scroll - 1) });
|
|
878
907
|
}
|
|
879
908
|
else if (key.downArrow || input === "j") {
|
|
880
|
-
setDetailPane({
|
|
909
|
+
setDetailPane({ groupKey: dp.groupKey, scroll: Math.min(maxScroll, dp.scroll + 1) });
|
|
881
910
|
}
|
|
882
911
|
else if (input === "g") {
|
|
883
|
-
setDetailPane({
|
|
912
|
+
setDetailPane({ groupKey: dp.groupKey, scroll: 0 });
|
|
884
913
|
}
|
|
885
914
|
else if (input === "G") {
|
|
886
|
-
setDetailPane({
|
|
915
|
+
setDetailPane({ groupKey: dp.groupKey, scroll: maxScroll });
|
|
916
|
+
}
|
|
917
|
+
else if (input === "?") {
|
|
918
|
+
setShowHelp(true);
|
|
887
919
|
}
|
|
888
920
|
else if (key.escape) {
|
|
889
921
|
setDetailPane(null);
|
|
@@ -906,8 +938,19 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
906
938
|
openExportMenu("all");
|
|
907
939
|
return;
|
|
908
940
|
}
|
|
909
|
-
if (key.escape
|
|
910
|
-
|
|
941
|
+
if (key.escape) {
|
|
942
|
+
const { expandedKey: openKey, viewport } = viewRef.current;
|
|
943
|
+
if (openKey !== null) {
|
|
944
|
+
dispatchView({ type: "EXPAND", expandedKey: null, expandLevel: null, viewport });
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
if (searchQuery) {
|
|
948
|
+
setSearchQuery("");
|
|
949
|
+
dispatchView({ type: "MOVE", cursor: 0, viewport: 0 });
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
if (onBack)
|
|
953
|
+
onBack();
|
|
911
954
|
return;
|
|
912
955
|
}
|
|
913
956
|
if (input === "q") {
|
|
@@ -921,41 +964,47 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
921
964
|
setSearchMode(true);
|
|
922
965
|
return;
|
|
923
966
|
}
|
|
924
|
-
const { cursor, expandLevel: expLvl,
|
|
967
|
+
const { cursor, expandLevel: expLvl, expandedKey: expKey, viewport: vpStart } = viewRef.current;
|
|
925
968
|
if (key.upArrow || input === "k") {
|
|
926
969
|
const next = Math.max(0, cursor - 1);
|
|
927
|
-
const newVp = adjustViewport(next,
|
|
970
|
+
const newVp = adjustViewport(next, expKey, expLvl, vpStart < next ? vpStart : next);
|
|
928
971
|
dispatchView({ type: "MOVE", cursor: next, viewport: newVp });
|
|
929
972
|
}
|
|
930
973
|
else if (key.downArrow || input === "j") {
|
|
931
974
|
const next = Math.min(groups.length - 1, cursor + 1);
|
|
932
|
-
const newVp = adjustViewport(next,
|
|
975
|
+
const newVp = adjustViewport(next, expKey, expLvl, vpStart);
|
|
933
976
|
dispatchView({ type: "MOVE", cursor: next, viewport: newVp });
|
|
934
977
|
}
|
|
935
978
|
else if (input === "g") {
|
|
936
|
-
const newVp = adjustViewport(0,
|
|
979
|
+
const newVp = adjustViewport(0, expKey, expLvl, 0);
|
|
937
980
|
dispatchView({ type: "MOVE", cursor: 0, viewport: newVp });
|
|
938
981
|
}
|
|
939
982
|
else if (input === "G") {
|
|
940
983
|
const last = groups.length - 1;
|
|
941
|
-
const newVp = adjustViewport(last,
|
|
984
|
+
const newVp = adjustViewport(last, expKey, expLvl, vpStart);
|
|
942
985
|
dispatchView({ type: "MOVE", cursor: last, viewport: newVp });
|
|
943
986
|
}
|
|
944
987
|
else if (key.pageDown) {
|
|
945
988
|
const next = Math.min(groups.length - 1, cursor + availableRows);
|
|
946
|
-
const newVp = adjustViewport(next,
|
|
989
|
+
const newVp = adjustViewport(next, expKey, expLvl, vpStart);
|
|
947
990
|
dispatchView({ type: "MOVE", cursor: next, viewport: newVp });
|
|
948
991
|
}
|
|
949
992
|
else if (key.pageUp) {
|
|
950
993
|
const next = Math.max(0, cursor - availableRows);
|
|
951
|
-
const newVp = adjustViewport(next,
|
|
994
|
+
const newVp = adjustViewport(next, expKey, expLvl, next);
|
|
952
995
|
dispatchView({ type: "MOVE", cursor: next, viewport: newVp });
|
|
953
996
|
}
|
|
954
997
|
else if (key.return) {
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
998
|
+
const curGroup = groups[Math.min(cursor, groups.length - 1)];
|
|
999
|
+
if (!curGroup)
|
|
1000
|
+
return;
|
|
1001
|
+
if (expKey === curGroup.key && expLvl === "summary") {
|
|
1002
|
+
setDetailPane({ groupKey: curGroup.key, scroll: 0 });
|
|
1003
|
+
}
|
|
1004
|
+
else {
|
|
1005
|
+
const newVp = adjustViewport(cursor, curGroup.key, "summary", vpStart);
|
|
1006
|
+
dispatchView({ type: "EXPAND", expandedKey: curGroup.key, expandLevel: "summary", viewport: newVp });
|
|
1007
|
+
}
|
|
959
1008
|
}
|
|
960
1009
|
else if (input === "/") {
|
|
961
1010
|
setSearchMode(true);
|
|
@@ -965,7 +1014,7 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
965
1014
|
const aboveCount = view.viewport;
|
|
966
1015
|
const belowCount = groups.length - visibleEnd;
|
|
967
1016
|
const lcCol = 16; // fixed width for license column
|
|
968
|
-
const nameCol = Math.max(20, innerWidth -
|
|
1017
|
+
const nameCol = Math.max(20, innerWidth - BADGE_COL - 14 - lcCol);
|
|
969
1018
|
// Clamp cursor to valid range (groups may shrink via search filter)
|
|
970
1019
|
const clampedCursor = groups.length > 0 ? Math.min(view.cursor, groups.length - 1) : 0;
|
|
971
1020
|
// ── Export menu overlay ──
|
|
@@ -1005,7 +1054,7 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
1005
1054
|
// ── Help overlay ──
|
|
1006
1055
|
if (showHelp) {
|
|
1007
1056
|
const isDetail = detailPane !== null;
|
|
1008
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, total: total, flagged: flagged.length, clean: clean.length, userStatus: userStatus, scanUsage: scanUsage, usageNearLimit: usageNearLimit }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingLeft: 2, paddingRight: 2, width: "100%", children: [_jsx(Text, { bold: true, children: "Keyboard Shortcuts" }), _jsx(Text, { children: "" }), _jsx(Text, { bold: true, children: " Navigation" }), _jsxs(Text, { children: [" ", chalk.cyan("\u2191 k"), " ", chalk.dim("Move up")] }), _jsxs(Text, { children: [" ", chalk.cyan("\u2193 j"), " ", chalk.dim("Move down")] }), _jsxs(Text, { children: [" ", chalk.cyan("g"), " ", chalk.dim("Jump to top")] }), _jsxs(Text, { children: [" ", chalk.cyan("G"), " ", chalk.dim("Jump to bottom")] }), !isDetail && _jsxs(Text, { children: [" ", chalk.cyan("PgUp"), " ", chalk.dim("Page up")] }), !isDetail && _jsxs(Text, { children: [" ", chalk.cyan("PgDn"), " ", chalk.dim("Page down")] }), _jsx(Text, { children: "" }), _jsx(Text, { bold: true, children: " Actions" }), !isDetail && _jsxs(Text, { children: [" ", chalk.cyan("\u23CE"), " ", chalk.dim("Expand findings")] }), isDetail && _jsxs(Text, { children: [" ", chalk.cyan("Esc"), " ", chalk.dim("Back to list")] }), !isDetail && _jsxs(Text, { children: [" ", chalk.cyan("/"), " ", chalk.dim("Search packages")] }), !isDetail && _jsxs(Text, { children: [" ", chalk.cyan("l"), " ", chalk.dim("License breakdown (browse + drill-in)")] }), !isDetail && _jsxs(Text, { children: [" ", chalk.cyan("e"), " ", chalk.dim("Export menu — pick scope (all / summary / packages / licenses / findings) and format (JSON / CSV / Markdown / text)")] }), !isDetail &&
|
|
1057
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, total: total, flagged: flagged.length, clean: clean.length, userStatus: userStatus, scanUsage: scanUsage, usageNearLimit: usageNearLimit }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingLeft: 2, paddingRight: 2, width: "100%", children: [_jsx(Text, { bold: true, children: "Keyboard Shortcuts" }), _jsx(Text, { children: "" }), _jsx(Text, { bold: true, children: " Navigation" }), _jsxs(Text, { children: [" ", chalk.cyan("\u2191 k"), " ", chalk.dim("Move up")] }), _jsxs(Text, { children: [" ", chalk.cyan("\u2193 j"), " ", chalk.dim("Move down")] }), _jsxs(Text, { children: [" ", chalk.cyan("g"), " ", chalk.dim("Jump to top")] }), _jsxs(Text, { children: [" ", chalk.cyan("G"), " ", chalk.dim("Jump to bottom")] }), !isDetail && _jsxs(Text, { children: [" ", chalk.cyan("PgUp"), " ", chalk.dim("Page up")] }), !isDetail && _jsxs(Text, { children: [" ", chalk.cyan("PgDn"), " ", chalk.dim("Page down")] }), _jsx(Text, { children: "" }), _jsx(Text, { bold: true, children: " Actions" }), !isDetail && _jsxs(Text, { children: [" ", chalk.cyan("\u23CE"), " ", chalk.dim("Expand findings (press again for full details)")] }), isDetail && _jsxs(Text, { children: [" ", chalk.cyan("Esc"), " ", chalk.dim("Back to list")] }), !isDetail && _jsxs(Text, { children: [" ", chalk.cyan("/"), " ", chalk.dim("Search all scanned packages (incl. passed)")] }), !isDetail && _jsxs(Text, { children: [" ", chalk.cyan("l"), " ", chalk.dim("License breakdown (browse + drill-in)")] }), !isDetail && _jsxs(Text, { children: [" ", chalk.cyan("e"), " ", chalk.dim("Export menu — pick scope (all / summary / packages / licenses / findings) and format (JSON / CSV / Markdown / text)")] }), !isDetail && _jsxs(Text, { children: [" ", chalk.cyan("Esc"), " ", chalk.dim(onBack ? "Collapse row / clear search / back to project selector" : "Collapse row / clear search")] }), _jsxs(Text, { children: [" ", chalk.cyan("q"), " ", chalk.dim("Quit")] }), _jsx(Text, { children: "" }), _jsxs(Text, { dimColor: true, children: [" Press ", chalk.bold.cyan("?"), " or ", chalk.bold.cyan("Esc"), " to close"] })] })] }));
|
|
1009
1058
|
}
|
|
1010
1059
|
// ── License overlay ──
|
|
1011
1060
|
if (showLicenses) {
|
|
@@ -1080,7 +1129,7 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
1080
1129
|
}
|
|
1081
1130
|
// ── Detail pane mode ──
|
|
1082
1131
|
if (detailPane !== null) {
|
|
1083
|
-
const dpGroup =
|
|
1132
|
+
const dpGroup = detailGroup;
|
|
1084
1133
|
if (dpGroup) {
|
|
1085
1134
|
const dpRep = firstPackage(dpGroup);
|
|
1086
1135
|
const { color: dpColor } = packageBadge(dpRep);
|
|
@@ -1088,11 +1137,11 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
1088
1137
|
const dpAbove = dpScroll;
|
|
1089
1138
|
const dpBelow = Math.max(0, detailLines.length - dpScroll - detailContentRows);
|
|
1090
1139
|
const dpVisible = detailLines.slice(dpScroll, dpScroll + detailContentRows);
|
|
1091
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, total: total, flagged: flagged.length, clean: clean.length, userStatus: userStatus, scanUsage: scanUsage, usageNearLimit: usageNearLimit }), _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { bold: true, children: [groupNames(dpGroup), dpRep.license ? chalk.dim(" \u00B7 ") + (dpRep.license.riskCategory === "permissive" ? chalk.green(dpRep.license.spdx ?? dpRep.license.raw ?? "") : dpRep.license.riskCategory === "no-license" || dpRep.license.riskCategory === "network-copyleft" ? chalk.red(dpRep.license.spdx ?? dpRep.license.raw ?? "No license") : chalk.yellow(dpRep.license.spdx ?? dpRep.license.raw ?? "")) : ""] }), _jsx(Text, { children: dpColor(`score ${dpRep.score}`) })] }), dpAbove > 0 && (_jsxs(Text, { dimColor: true, children: [chalk.cyan(" \u2191"), " ", dpAbove, " more above"] })), _jsx(Box, { flexDirection: "column", marginLeft: 2, children: dpVisible }), dpBelow > 0 && (_jsxs(Text, { dimColor: true, children: [chalk.cyan(" \u2193"), " ", dpBelow, " more below"] }))] }), _jsx(Text, { dimColor: true, children: chalk.dim("─".repeat(Math.max(20, termCols - 4))) }), _jsx(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: _jsxs(Box, { justifyContent: "space-between", children: [clean.length > 0 ? (_jsxs(Text, { children: [chalk.green("\u2713"), " ", chalk.green.bold(String(clean.length)), " ", chalk.dim(`package${clean.length !== 1 ? "s" : ""} passed`), " ", chalk.dim(`\u00b7 ${(durationMs / 1000).toFixed(1)}s`)] })) : (_jsxs(Text, { dimColor: true, children: [(durationMs / 1000).toFixed(1), "s"] })), result.freeScansRemaining !== undefined && (_jsx(Text, { dimColor: true, children: "Free tier \u00B7 dg login for higher scan limits" }))] }) }), _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), _jsxs(Text, { children: [" ", chalk.bold.cyan("\u2191\u2193"), " ", chalk.dim("scroll"), " ", chalk.bold.cyan("Esc"), " ", chalk.dim("back"), " ", chalk.bold.cyan("q"), " ", chalk.dim("quit")] })] }));
|
|
1140
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, total: total, flagged: flagged.length, clean: clean.length, userStatus: userStatus, scanUsage: scanUsage, usageNearLimit: usageNearLimit }), _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [groupNames(dpGroup), dpRep.license ? chalk.dim(" \u00B7 ") + (dpRep.license.riskCategory === "permissive" ? chalk.green(dpRep.license.spdx ?? dpRep.license.raw ?? "") : dpRep.license.riskCategory === "no-license" || dpRep.license.riskCategory === "network-copyleft" ? chalk.red(dpRep.license.spdx ?? dpRep.license.raw ?? "No license") : chalk.yellow(dpRep.license.spdx ?? dpRep.license.raw ?? "")) : ""] }), _jsx(Text, { children: dpColor(`score ${dpRep.score}`) })] }), dpAbove > 0 && (_jsxs(Text, { dimColor: true, children: [chalk.cyan(" \u2191"), " ", dpAbove, " more above"] })), _jsx(Box, { flexDirection: "column", marginLeft: 2, children: dpVisible }), dpBelow > 0 && (_jsxs(Text, { dimColor: true, children: [chalk.cyan(" \u2193"), " ", dpBelow, " more below"] }))] }), _jsx(Text, { dimColor: true, children: chalk.dim("─".repeat(Math.max(20, termCols - 4))) }), _jsx(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: _jsxs(Box, { justifyContent: "space-between", children: [clean.length > 0 ? (_jsxs(Text, { wrap: "truncate-end", children: [chalk.green("\u2713"), " ", chalk.green.bold(String(clean.length)), " ", chalk.dim(`package${clean.length !== 1 ? "s" : ""} passed`), " ", chalk.dim(`\u00b7 ${(durationMs / 1000).toFixed(1)}s`)] })) : (_jsxs(Text, { dimColor: true, children: [(durationMs / 1000).toFixed(1), "s"] })), result.freeScansRemaining !== undefined && (_jsx(Text, { dimColor: true, children: "Free tier \u00B7 dg login for higher scan limits" }))] }) }), _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), _jsxs(Text, { children: [" ", chalk.bold.cyan("\u2191\u2193"), " ", chalk.dim("scroll"), " ", chalk.bold.cyan("Esc"), " ", chalk.dim("back"), " ", chalk.bold.cyan("q"), " ", chalk.dim("quit")] })] }));
|
|
1092
1141
|
}
|
|
1093
1142
|
}
|
|
1094
1143
|
// ── List mode ──
|
|
1095
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, total: total, flagged: flagged.length, clean: clean.length, userStatus: userStatus, scanUsage: scanUsage, usageNearLimit: usageNearLimit }), groups.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: chalk.dim("─".repeat(Math.max(20, termCols - 4))) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, children: "Flagged Packages" }), _jsx(Text, { dimColor: true, children: searchQuery ? `${
|
|
1144
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, total: total, flagged: flagged.length, clean: clean.length, userStatus: userStatus, scanUsage: scanUsage, usageNearLimit: usageNearLimit }), groups.length > 0 && (_jsxs(_Fragment, { children: [!compact && _jsx(Text, { dimColor: true, children: chalk.dim("─".repeat(Math.max(20, termCols - 4))) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, children: searchQuery ? "Search Results" : "Flagged Packages" }), _jsx(Text, { dimColor: true, children: searchQuery ? `${matchCount} of ${total} packages` : `${clampedCursor + 1}/${groups.length}` })] }), aboveCount > 0 && (_jsxs(Text, { dimColor: true, children: [chalk.cyan(" \u2191"), " ", aboveCount, " more above"] })), visibleGroups.map((group, visIdx) => {
|
|
1096
1145
|
const globalIdx = view.viewport + visIdx;
|
|
1097
1146
|
const isCursor = globalIdx === clampedCursor;
|
|
1098
1147
|
const level = getLevel(globalIdx);
|
|
@@ -1107,8 +1156,8 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
1107
1156
|
: (lcInfo.riskCategory === "no-license" || lcInfo.riskCategory === "unlicensed" || lcInfo.riskCategory === "network-copyleft") ? chalk.red
|
|
1108
1157
|
: chalk.yellow;
|
|
1109
1158
|
const arrow = level === "summary" ? "\u25BE" : "\u25B8"; // ▾ expanded, ▸ collapsed
|
|
1110
|
-
return (_jsxs(Box, { flexDirection: "column", children: [isCursor ? (_jsxs(Text, { backgroundColor: "#1a1a2e", children: [chalk.cyan("\u258C"), " ", chalk.cyan(arrow), " ", ` `, color(pad(label,
|
|
1111
|
-
}), belowCount > 0 && (_jsxs(Text, { dimColor: true, children: [chalk.cyan(" \u2193"), " ", belowCount, " more below"] }))] })] })), searchQuery && groups.length === 0 && (_jsx(Text, { dimColor: true, children: ` No
|
|
1159
|
+
return (_jsxs(Box, { flexDirection: "column", children: [isCursor ? (_jsxs(Text, { backgroundColor: "#1a1a2e", wrap: "truncate-end", children: [chalk.cyan("\u258C"), " ", chalk.cyan(arrow), " ", ` `, color(pad(label, BADGE_COL)), chalk.bold(pad(truncate(names, nameCol - 2), nameCol)), lcColor(pad(lcStr, lcCol)), color(scoreStr.padStart(3)), " "] })) : (_jsxs(Text, { wrap: "truncate-end", children: [` ${chalk.dim(arrow)} `, color(pad(label, BADGE_COL)), pad(truncate(names, nameCol - 2), nameCol), lcColor(pad(lcStr, lcCol)), color(scoreStr.padStart(3))] })), level === "summary" && (_jsx(FindingsSummary, { group: group, maxWidth: innerWidth - 8, maxLines: group.key === view.expandedKey ? animVisibleLines : undefined }))] }, group.key));
|
|
1160
|
+
}), belowCount > 0 && (_jsxs(Text, { dimColor: true, children: [chalk.cyan(" \u2193"), " ", belowCount, " more below"] }))] })] })), searchQuery && groups.length === 0 && (_jsx(Text, { dimColor: true, children: ` No packages match "${searchQuery}"` })), !compact && _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: [discoveredTotal !== undefined && discoveredTotal > total && (_jsxs(Text, { dimColor: true, children: ["Scanned ", total, " of ", discoveredTotal, " packages"] })), _jsxs(Box, { justifyContent: "space-between", children: [clean.length > 0 ? (_jsxs(Text, { wrap: "truncate-end", children: [chalk.green("\u2713"), " ", chalk.green.bold(String(clean.length)), " ", chalk.dim(`package${clean.length !== 1 ? "s" : ""} passed`), " ", chalk.dim(`\u00b7 ${(durationMs / 1000).toFixed(1)}s`)] })) : (_jsxs(Text, { dimColor: true, children: [(durationMs / 1000).toFixed(1), "s"] })), result.freeScansRemaining !== undefined && (_jsx(Text, { dimColor: true, children: "Free tier \u00B7 dg login for higher scan limits" }))] })] }), !compact && _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), searchMode ? (_jsxs(Text, { wrap: "truncate-end", children: [" ", chalk.bold.cyan("/"), " ", searchQuery, chalk.cyan("\u2588"), " ", chalk.dim("Esc clear")] })) : searchQuery ? (_jsxs(Text, { wrap: "truncate-end", children: [" ", chalk.bold.cyan("/"), " ", searchQuery, " ", chalk.dim(`${matchCount} of ${total} packages`), " ", chalk.bold.cyan("Esc"), " ", chalk.dim("clear"), " ", chalk.bold.cyan("q"), " ", chalk.dim("quit"), exportMsg && _jsxs(_Fragment, { children: [" ", chalk.green(exportMsg)] })] })) : (_jsxs(Text, { wrap: "truncate-end", children: [" ", groups.length > 0 && (_jsxs(_Fragment, { children: [chalk.bold.cyan("\u2191\u2193"), " ", chalk.dim("navigate"), " ", chalk.bold.cyan("\u23CE"), " ", chalk.dim("expand"), " "] })), total > 0 && (_jsxs(_Fragment, { children: [chalk.bold.cyan("/"), " ", chalk.dim("search"), " "] })), chalk.bold.cyan("l"), " ", chalk.dim("licenses"), " ", chalk.bold.cyan("e"), " ", chalk.dim("export"), " ", onBack && _jsxs(_Fragment, { children: [chalk.bold.cyan("Esc"), " ", chalk.dim("back"), " "] }), chalk.bold.cyan("q"), " ", chalk.dim("quit"), " ", chalk.dim("\u00B7 Ctrl+C or q to exit"), exportMsg && _jsxs(_Fragment, { children: [" ", chalk.green(exportMsg)] })] }))] }));
|
|
1112
1161
|
};
|
|
1113
1162
|
const T = {
|
|
1114
1163
|
branch: chalk.dim("\u251C\u2500\u2500"),
|
|
@@ -1168,13 +1217,24 @@ const FindingsSummary = ({ group, maxWidth, maxLines }) => {
|
|
|
1168
1217
|
const sevLabel = SEVERITY_LABELS[f.severity] ?? "INFO";
|
|
1169
1218
|
const sevColor = SEVERITY_COLORS[f.severity] ?? SEVERITY_COLORS[1] ?? chalk.dim;
|
|
1170
1219
|
const title = f.title ? `: ${f.title}` : "";
|
|
1171
|
-
allLines.push(_jsxs(Text, { children: [connector, " ", sevColor(pad(sevLabel, 5)), " ", chalk.dim(f.category ?? ""), title] }, `finding-${idx}`));
|
|
1220
|
+
allLines.push(_jsxs(Text, { wrap: "truncate-end", children: [connector, " ", sevColor(pad(sevLabel, 5)), " ", chalk.dim(f.category ?? ""), title] }, `finding-${idx}`));
|
|
1172
1221
|
}
|
|
1173
1222
|
}
|
|
1174
1223
|
if (visibleFindings.length === 0 && rep.score > 0) {
|
|
1175
1224
|
// No findings at all (shouldn't happen after API change, but safety fallback)
|
|
1176
1225
|
allLines.push(_jsxs(Text, { dimColor: true, children: [hasAffects ? T.branch : T.last, " Score: ", rep.score, "/100"] }, "score-only"));
|
|
1177
1226
|
}
|
|
1227
|
+
if (visibleFindings.length === 0 && rep.score === 0) {
|
|
1228
|
+
const statusLines = statusSummaryLines(rep);
|
|
1229
|
+
statusLines.forEach((line, i) => {
|
|
1230
|
+
const isLast = !hasAffects && i === statusLines.length - 1;
|
|
1231
|
+
allLines.push(_jsxs(Text, { wrap: "truncate-end", children: [isLast ? T.last : T.branch, " ", rep.action === "analysis_incomplete"
|
|
1232
|
+
? chalk.yellow(line)
|
|
1233
|
+
: (rep.action ?? "pass") === "pass"
|
|
1234
|
+
? chalk.green(`✓ ${line}`)
|
|
1235
|
+
: chalk.dim(line)] }, `status-${i}`));
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1178
1238
|
if (hasAffects) {
|
|
1179
1239
|
allLines.push(_jsxs(Text, { dimColor: true, children: [T.last, " ", truncate(affectsLine(group), maxWidth - 8)] }, "affects"));
|
|
1180
1240
|
}
|
|
@@ -9,10 +9,10 @@ export const ProjectSelector = ({ projects, onConfirm, onCancel, userStatus }) =
|
|
|
9
9
|
const [selected, setSelected] = useState(() => new Set(projects.map((_, i) => i)));
|
|
10
10
|
const { cols: termCols } = useTerminalSize();
|
|
11
11
|
useInput((input, key) => {
|
|
12
|
-
if (key.upArrow) {
|
|
12
|
+
if (key.upArrow || input === "k") {
|
|
13
13
|
setCursor((c) => Math.max(0, c - 1));
|
|
14
14
|
}
|
|
15
|
-
else if (key.downArrow) {
|
|
15
|
+
else if (key.downArrow || input === "j") {
|
|
16
16
|
setCursor((c) => Math.min(projects.length - 1, c + 1));
|
|
17
17
|
}
|
|
18
18
|
else if (input === " ") {
|
|
@@ -37,7 +37,7 @@ export const ProjectSelector = ({ projects, onConfirm, onCancel, userStatus }) =
|
|
|
37
37
|
if (picked.length > 0)
|
|
38
38
|
onConfirm(picked);
|
|
39
39
|
}
|
|
40
|
-
else if (input === "q") {
|
|
40
|
+
else if (input === "q" || key.escape) {
|
|
41
41
|
onCancel();
|
|
42
42
|
}
|
|
43
43
|
});
|