@westbayberry/dg 2.0.11 → 2.2.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 +216 -226
- package/dist/agents/claude-code.js +113 -0
- package/dist/agents/codex.js +65 -0
- package/dist/agents/copilot-cli.js +115 -0
- package/dist/agents/cursor.js +113 -0
- package/dist/agents/gemini.js +107 -0
- package/dist/agents/persistence.js +285 -0
- package/dist/agents/registry.js +127 -0
- package/dist/agents/types.js +1 -0
- package/dist/agents/windsurf.js +93 -0
- package/dist/api/analyze.js +6 -4
- package/dist/audit/detectors.js +0 -11
- package/dist/audit/events.js +5 -21
- package/dist/audit-ui/AuditApp.js +2 -0
- package/dist/audit-ui/components/AuditResultsView.js +55 -92
- package/dist/audit-ui/export.js +0 -4
- package/dist/audit-ui/format.js +0 -3
- package/dist/audit-ui/launch.js +10 -1
- package/dist/auth/device-login.js +4 -5
- package/dist/auth/login-app.js +7 -7
- package/dist/auth/store.js +8 -3
- package/dist/bin/dg.js +59 -52
- package/dist/commands/agents.js +231 -0
- package/dist/commands/audit.js +22 -3
- package/dist/commands/config.js +26 -10
- package/dist/commands/cooldown.js +389 -0
- package/dist/commands/decisions.js +169 -0
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/help.js +1 -1
- package/dist/commands/licenses.js +10 -22
- package/dist/commands/logout.js +4 -11
- package/dist/commands/router.js +8 -4
- package/dist/commands/sbom.js +206 -0
- package/dist/commands/scan.js +3 -2
- package/dist/commands/service.js +32 -13
- package/dist/commands/setup.js +197 -27
- package/dist/commands/status.js +5 -2
- package/dist/commands/types.js +1 -0
- package/dist/commands/update.js +17 -8
- package/dist/commands/verify.js +8 -5
- package/dist/config/settings.js +154 -65
- package/dist/decisions/apply.js +128 -0
- package/dist/decisions/remember-prompt.js +92 -0
- package/dist/export-ui/ExportDialog.js +198 -0
- package/dist/install-ui/LiveInstall.js +2 -2
- package/dist/install-ui/block-render.js +21 -4
- package/dist/install-ui/prep-spinner.js +32 -0
- package/dist/install-ui/prompt.js +14 -0
- package/dist/launcher/agent-check.js +466 -0
- package/dist/launcher/agent-hook-exec.js +70 -0
- package/dist/launcher/agent-hook-io.js +31 -0
- package/dist/launcher/cargo-cache.js +40 -0
- package/dist/launcher/classify.js +17 -6
- package/dist/launcher/env.js +71 -24
- package/dist/launcher/install-preflight.js +167 -17
- package/dist/launcher/live-install.js +25 -5
- package/dist/launcher/output-redaction.js +7 -4
- package/dist/launcher/preflight-prompt.js +43 -3
- package/dist/launcher/run.js +100 -86
- package/dist/launcher/spawn-invocation.js +21 -0
- package/dist/policy/cooldown.js +117 -0
- package/dist/policy/evaluate.js +5 -21
- package/dist/policy/pypi-name.js +17 -0
- package/dist/presentation/mode.js +3 -2
- package/dist/presentation/package-page.js +9 -0
- package/dist/presentation/provenance.js +23 -0
- package/dist/presentation/theme.js +7 -7
- package/dist/project/dgfile.js +446 -0
- package/dist/proxy/auth.js +42 -0
- package/dist/proxy/ca.js +29 -9
- package/dist/proxy/cooldown-exemptions-file.js +33 -0
- package/dist/proxy/enforcement.js +57 -17
- package/dist/proxy/metadata-map.js +66 -4
- package/dist/proxy/preverified.js +55 -0
- package/dist/proxy/server.js +473 -45
- package/dist/proxy/worker.js +16 -1
- package/dist/publish-set/collect.js +1 -4
- package/dist/publish-set/npm.js +8 -5
- package/dist/publish-set/pack.js +9 -3
- package/dist/runtime/cli.js +0 -4
- package/dist/runtime/fatal.js +31 -0
- package/dist/runtime/first-run.js +12 -11
- package/dist/runtime/node-version.js +43 -6
- package/dist/runtime/nudges.js +35 -2
- package/dist/sbom/cyclonedx.js +211 -0
- package/dist/sbom-ui/SbomApp.js +158 -0
- package/dist/sbom-ui/components/SbomHeader.js +32 -0
- package/dist/sbom-ui/components/SbomList.js +52 -0
- package/dist/sbom-ui/inventory.js +128 -0
- package/dist/sbom-ui/launch.js +51 -0
- package/dist/sbom-ui/run.js +55 -0
- package/dist/sbom-ui/store.js +26 -0
- package/dist/scan/collect.js +10 -6
- package/dist/scan/command.js +51 -17
- package/dist/scan/discovery.js +11 -2
- package/dist/scan/render.js +63 -8
- package/dist/scan/scanner-report.js +42 -9
- package/dist/scan/staged.js +71 -11
- package/dist/scan-ui/LegacyApp.js +12 -16
- package/dist/scan-ui/alt-screen.js +5 -8
- package/dist/scan-ui/components/InteractiveResultsView.js +193 -123
- package/dist/scan-ui/components/ProgressBar.js +3 -14
- package/dist/scan-ui/components/ProjectSelector.js +1 -1
- package/dist/scan-ui/components/ScoreHeader.js +2 -3
- package/dist/scan-ui/components/SetupBanner.js +0 -6
- package/dist/scan-ui/format-helpers.js +61 -5
- package/dist/scan-ui/hooks/useResizeRepaint.js +25 -0
- package/dist/scan-ui/hooks/useScan.js +46 -4
- package/dist/scan-ui/launch.js +7 -4
- package/dist/scan-ui/shims.js +14 -4
- package/dist/scripts/detect.js +158 -0
- package/dist/scripts/gate.js +170 -0
- package/dist/service/state.js +27 -8
- package/dist/service/trust-refresh.js +92 -0
- package/dist/service/worker.js +23 -1
- package/dist/setup/activate-shell.js +28 -0
- package/dist/setup/git-hook.js +49 -4
- package/dist/setup/plan.js +98 -29
- package/dist/setup-ui/gate.js +39 -0
- package/dist/setup-ui/offer.js +42 -0
- package/dist/setup-ui/selector.js +27 -0
- package/dist/setup-ui/tasks.js +56 -0
- package/dist/setup-ui/wizard.js +225 -0
- package/dist/state/cooldown-held.js +66 -0
- package/dist/state/index.js +1 -0
- package/dist/state/locks.js +4 -2
- package/dist/state/store.js +2 -1
- package/dist/util/external-tool.js +25 -0
- package/dist/util/git.js +10 -3
- package/dist/util/json-file.js +24 -0
- package/dist/util/report-writer.js +57 -0
- package/dist/util/tty-prompt.js +13 -6
- package/dist/verify/local.js +240 -42
- package/dist/verify/package-check.js +86 -18
- package/dist/verify/preflight.js +242 -49
- package/dist/verify/render.js +15 -1
- package/npm-shrinkwrap.json +2383 -0
- package/package.json +14 -8
- package/NOTICE +0 -5
- package/dist/commands/completion.js +0 -116
- package/dist/commands/explain.js +0 -232
- package/dist/commands/unavailable.js +0 -11
- package/dist/telemetry/events.js +0 -40
|
@@ -2,14 +2,34 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useReducer, useMemo, useRef, useEffect, useState } from "react";
|
|
3
3
|
import { Box, Text, useInput } from "ink";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { ExportDialog, loginRequiredToast } from "../../export-ui/ExportDialog.js";
|
|
6
|
+
import { resolvePresentation } from "../../presentation/mode.js";
|
|
7
|
+
import { createTheme } from "../../presentation/theme.js";
|
|
8
|
+
import { packagePageUrl } from "../../presentation/package-page.js";
|
|
9
|
+
import { accountHeaderLine, isLoggedIn } from "../shims.js";
|
|
10
|
+
import { packageKey } from "../../decisions/apply.js";
|
|
11
|
+
import { ScoreHeader, COMPACT_ROWS, LOGO_MIN_COLS } from "./ScoreHeader.js";
|
|
12
|
+
import { renderLogo } from "../logo.js";
|
|
9
13
|
import { useExpandAnimation } from "../hooks/useExpandAnimation.js";
|
|
10
14
|
import { useTerminalSize } from "../hooks/useTerminalSize.js";
|
|
11
15
|
import { clearScreen } from "../alt-screen.js";
|
|
12
16
|
import { pad, truncate, groupPackages as sharedGroupPackages, formatUsage } from "../format-helpers.js";
|
|
17
|
+
import { provenanceLabel, provenanceDowngradeLine } from "../../presentation/provenance.js";
|
|
18
|
+
const ACK_PREVIEW_LIMIT = 3;
|
|
19
|
+
const EXPORT_SCOPE_LABELS = {
|
|
20
|
+
all: "All",
|
|
21
|
+
summary: "Summary",
|
|
22
|
+
packages: "Packages",
|
|
23
|
+
licenses: "Licenses",
|
|
24
|
+
findings: "Findings (warn+block)",
|
|
25
|
+
"current-license": "Current license"
|
|
26
|
+
};
|
|
27
|
+
const EXPORT_FORMAT_LABELS = {
|
|
28
|
+
json: "JSON",
|
|
29
|
+
csv: "CSV",
|
|
30
|
+
md: "Markdown",
|
|
31
|
+
txt: "Plain text"
|
|
32
|
+
};
|
|
13
33
|
function groupPackages(packages) {
|
|
14
34
|
return sharedGroupPackages(packages, "fingerprint");
|
|
15
35
|
}
|
|
@@ -50,13 +70,39 @@ export function packageBadge(pkg) {
|
|
|
50
70
|
return { label: "Unverified", color: chalk.yellow };
|
|
51
71
|
return actionBadge(pkg.action);
|
|
52
72
|
}
|
|
73
|
+
export function provenanceMarker(pkg) {
|
|
74
|
+
const prov = pkg.provenance;
|
|
75
|
+
if (!prov)
|
|
76
|
+
return " ";
|
|
77
|
+
if (prov.downgrade)
|
|
78
|
+
return chalk.yellow("◇ ");
|
|
79
|
+
if (prov.status === "attested")
|
|
80
|
+
return chalk.dim("◆ ");
|
|
81
|
+
return " ";
|
|
82
|
+
}
|
|
83
|
+
function packageDowngradeLine(pkg) {
|
|
84
|
+
return pkg.provenance ? provenanceDowngradeLine(pkg.version, pkg.provenance) : null;
|
|
85
|
+
}
|
|
53
86
|
const EVIDENCE_LIMIT = 2;
|
|
54
87
|
const BADGE_COL = "Unverified".length + 1;
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
88
|
+
// Mirrors the list-mode render row-for-row; drift shows up as blank rows or
|
|
89
|
+
// overflow on short terminals (counts pinned by resolveResultsLayout tests).
|
|
90
|
+
// Header text column: title + score + counts (+usage), full mode adds 2 spacer
|
|
91
|
+
// lines and the box is as tall as the logo when shown. Full chrome adds the
|
|
92
|
+
// flagged divider + heading and divider + summary + divider + help bar;
|
|
93
|
+
// compact drops the logo, spacers, and all three dividers.
|
|
94
|
+
export function resolveResultsLayout(input) {
|
|
95
|
+
const textRows = input.hasUsage ? 4 : 3;
|
|
96
|
+
const fullHeader = 2 + Math.max(input.logoRows, textRows + 2);
|
|
97
|
+
const compactHeader = 2 + textRows;
|
|
98
|
+
const shared = input.ackSectionLines + input.extraLines;
|
|
99
|
+
const fullChrome = fullHeader + (input.hasGroups ? 2 : 0) + 4 + shared;
|
|
100
|
+
const compactChrome = compactHeader + (input.hasGroups ? 1 : 0) + 2 + shared;
|
|
101
|
+
const compact = input.termRows < COMPACT_ROWS && input.listRows > input.termRows - fullChrome;
|
|
102
|
+
const maxList = input.termRows - (compact ? compactChrome : fullChrome);
|
|
103
|
+
const availableRows = Math.max(5, input.listRows <= maxList ? maxList : maxList - 2);
|
|
104
|
+
return { compact, availableRows };
|
|
105
|
+
}
|
|
60
106
|
function firstPackage(group) {
|
|
61
107
|
const rep = group.packages[0];
|
|
62
108
|
if (!rep)
|
|
@@ -80,6 +126,8 @@ function findingsSummaryHeight(group) {
|
|
|
80
126
|
let h = 0;
|
|
81
127
|
if (rep.license)
|
|
82
128
|
h += 1;
|
|
129
|
+
if (packageDowngradeLine(rep))
|
|
130
|
+
h += 1;
|
|
83
131
|
if (isFree) {
|
|
84
132
|
h += 1;
|
|
85
133
|
}
|
|
@@ -131,6 +179,10 @@ function buildDetailLines(group, safeVersion, maxWidth) {
|
|
|
131
179
|
const isFree = visibleFindings.length > 0 && !visibleFindings[0]?.title;
|
|
132
180
|
const lines = [];
|
|
133
181
|
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"));
|
|
182
|
+
const page = packagePageUrl(rep.ecosystem ?? "", rep.name);
|
|
183
|
+
if (page) {
|
|
184
|
+
lines.push(_jsx(Text, { dimColor: true, wrap: "truncate-end", children: `Full report ${page}` }, "page"));
|
|
185
|
+
}
|
|
134
186
|
lines.push(_jsx(Text, { children: "" }, "verdict-gap"));
|
|
135
187
|
if (group.packages.length > 3) {
|
|
136
188
|
lines.push(_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["Affects: ", affectsLine(group)] }, "affects"));
|
|
@@ -140,6 +192,14 @@ function buildDetailLines(group, safeVersion, maxWidth) {
|
|
|
140
192
|
lines.push(_jsxs(Text, { dimColor: true, children: ["Score: ", rep.score, "/100"] }, "score-info"));
|
|
141
193
|
lines.push(_jsx(Text, { children: "" }, "score-gap"));
|
|
142
194
|
}
|
|
195
|
+
if (rep.provenance) {
|
|
196
|
+
lines.push(_jsxs(Text, { dimColor: true, children: ["Provenance: ", provenanceLabel(rep.provenance)] }, "provenance"));
|
|
197
|
+
const downgrade = packageDowngradeLine(rep);
|
|
198
|
+
if (downgrade) {
|
|
199
|
+
lines.push(_jsx(Text, { wrap: "truncate-end", children: chalk.yellow(downgrade) }, "provenance-downgrade"));
|
|
200
|
+
}
|
|
201
|
+
lines.push(_jsx(Text, { children: "" }, "provenance-gap"));
|
|
202
|
+
}
|
|
143
203
|
if (visibleFindings.length > 0) {
|
|
144
204
|
for (let i = 0; i < visibleFindings.length; i++) {
|
|
145
205
|
const f = visibleFindings[i];
|
|
@@ -194,17 +254,15 @@ function viewReducer(_state, action) {
|
|
|
194
254
|
return { ..._state, expandedKey: action.expandedKey, expandLevel: action.expandLevel, viewport: action.viewport };
|
|
195
255
|
}
|
|
196
256
|
}
|
|
197
|
-
export const InteractiveResultsView = ({ result, config: _config, durationMs, onExit, onBack, discoveredTotal,
|
|
198
|
-
// Prefer the server's uniform usage block ("used / limit packages this
|
|
199
|
-
// month"); fall back to the legacy freeScansRemaining field, then to the
|
|
200
|
-
// generic placeholder from bin.ts. usageNearLimit drives the yellow + nudge.
|
|
257
|
+
export const InteractiveResultsView = ({ result, config: _config, durationMs, onExit, onBack, discoveredTotal, initialView, decisions, }) => {
|
|
201
258
|
const usageDisplay = result.usage ? formatUsage(result.usage) : null;
|
|
202
259
|
const scanUsage = usageDisplay
|
|
203
260
|
? usageDisplay.text
|
|
204
261
|
: result.freeScansRemaining !== undefined
|
|
205
262
|
? `${result.freeScansRemaining.toLocaleString()} packages left`
|
|
206
|
-
:
|
|
263
|
+
: undefined;
|
|
207
264
|
const usageNearLimit = usageDisplay?.nearLimit ?? false;
|
|
265
|
+
const headerStatus = useMemo(() => accountHeaderLine(result.usage?.tier), [result.usage]);
|
|
208
266
|
// Bucket by the server `action`, never by score: the server can return
|
|
209
267
|
// block/warn at score 0 (policy verdict / yanked / cooldown), and those must
|
|
210
268
|
// surface as flagged, not hide in "clean".
|
|
@@ -212,7 +270,20 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
212
270
|
const clean = useMemo(() => result.packages.filter((p) => (p.action ?? "pass") === "pass"), [result.packages]);
|
|
213
271
|
const total = result.packages.length;
|
|
214
272
|
const [searchQuery, setSearchQuery] = useState("");
|
|
215
|
-
const
|
|
273
|
+
const ackByKey = useMemo(() => {
|
|
274
|
+
const map = new Map();
|
|
275
|
+
if (decisions) {
|
|
276
|
+
for (const [key, annotation] of Object.entries(decisions.packages)) {
|
|
277
|
+
if (annotation.acknowledged)
|
|
278
|
+
map.set(key, annotation.acknowledged);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return map;
|
|
282
|
+
}, [decisions]);
|
|
283
|
+
const activeFlagged = useMemo(() => flagged.filter((p) => !ackByKey.has(packageKey(p.name, p.version))), [flagged, ackByKey]);
|
|
284
|
+
const acked = useMemo(() => flagged.filter((p) => ackByKey.has(packageKey(p.name, p.version))), [flagged, ackByKey]);
|
|
285
|
+
const ackGroups = useMemo(() => groupPackages(acked), [acked]);
|
|
286
|
+
const allGroups = useMemo(() => groupPackages(activeFlagged), [activeFlagged]);
|
|
216
287
|
const groups = useMemo(() => {
|
|
217
288
|
if (!searchQuery)
|
|
218
289
|
return allGroups;
|
|
@@ -223,13 +294,18 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
223
294
|
if (members.length > 0)
|
|
224
295
|
matched.push({ packages: members, key: g.key });
|
|
225
296
|
}
|
|
297
|
+
for (const p of acked) {
|
|
298
|
+
if (p.name.toLowerCase().includes(q)) {
|
|
299
|
+
matched.push({ packages: [p], key: `ack|${p.name}@${p.version ?? ""}` });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
226
302
|
for (const p of clean) {
|
|
227
303
|
if (p.name.toLowerCase().includes(q)) {
|
|
228
304
|
matched.push({ packages: [p], key: `pass|${p.name}@${p.version ?? ""}` });
|
|
229
305
|
}
|
|
230
306
|
}
|
|
231
307
|
return matched;
|
|
232
|
-
}, [allGroups, clean, searchQuery]);
|
|
308
|
+
}, [allGroups, acked, clean, searchQuery]);
|
|
233
309
|
const matchCount = useMemo(() => groups.reduce((n, g) => n + g.packages.length, 0), [groups]);
|
|
234
310
|
const [view, dispatchView] = useReducer(viewReducer, {
|
|
235
311
|
cursor: 0,
|
|
@@ -245,10 +321,6 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
245
321
|
const [showHelp, setShowHelp] = useState(false);
|
|
246
322
|
const showHelpRef = useRef(showHelp);
|
|
247
323
|
showHelpRef.current = showHelp;
|
|
248
|
-
// License-grouped overlay. Toggled with `l`. Shows packages grouped by
|
|
249
|
-
// SPDX license id, sorted by count desc, color-coded by risk category.
|
|
250
|
-
// Up/Down to select a row, Enter to drill into the packages for that
|
|
251
|
-
// license, Esc to back out, l/Esc again to close the overlay.
|
|
252
324
|
const [showLicenses, setShowLicenses] = useState(initialView === "licenses");
|
|
253
325
|
const showLicensesRef = useRef(showLicenses);
|
|
254
326
|
showLicensesRef.current = showLicenses;
|
|
@@ -259,20 +331,23 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
259
331
|
const licenseDetailIdxRef = useRef(licenseDetailIdx);
|
|
260
332
|
licenseDetailIdxRef.current = licenseDetailIdx;
|
|
261
333
|
const [licenseDetailScroll, setLicenseDetailScroll] = useState(0);
|
|
262
|
-
// Search inside the license drill-in: type-to-filter packages by name.
|
|
263
334
|
const [licenseSearchMode, setLicenseSearchMode] = useState(false);
|
|
264
335
|
const licenseSearchModeRef = useRef(licenseSearchMode);
|
|
265
336
|
licenseSearchModeRef.current = licenseSearchMode;
|
|
266
337
|
const [licenseSearchQuery, setLicenseSearchQuery] = useState("");
|
|
267
|
-
// Export status (shown briefly in the footer after `e`).
|
|
268
338
|
const [exportMsg, setExportMsg] = useState(null);
|
|
269
339
|
const exportMsgRef = useRef(null);
|
|
270
|
-
const showExportMsg = (
|
|
271
|
-
setExportMsg(
|
|
340
|
+
const showExportMsg = (text, tone = "ok") => {
|
|
341
|
+
setExportMsg({ text, tone });
|
|
272
342
|
if (exportMsgRef.current)
|
|
273
343
|
clearTimeout(exportMsgRef.current);
|
|
274
344
|
exportMsgRef.current = setTimeout(() => setExportMsg(null), 4000);
|
|
275
345
|
};
|
|
346
|
+
const exportMsgText = exportMsg
|
|
347
|
+
? exportMsg.tone === "nudge"
|
|
348
|
+
? exportMsg.text
|
|
349
|
+
: (exportMsg.tone === "error" ? chalk.red : chalk.green)(exportMsg.text)
|
|
350
|
+
: null;
|
|
276
351
|
useEffect(() => () => {
|
|
277
352
|
if (exportMsgRef.current)
|
|
278
353
|
clearTimeout(exportMsgRef.current);
|
|
@@ -280,17 +355,18 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
280
355
|
const [exportMenu, setExportMenu] = useState(null);
|
|
281
356
|
const exportMenuRef = useRef(exportMenu);
|
|
282
357
|
exportMenuRef.current = exportMenu;
|
|
358
|
+
const [exportDialog, setExportDialog] = useState(null);
|
|
359
|
+
const exportDialogRef = useRef(exportDialog);
|
|
360
|
+
exportDialogRef.current = exportDialog;
|
|
361
|
+
const theme = useMemo(() => createTheme(resolvePresentation().color), []);
|
|
283
362
|
const openExportMenu = (defaultScope = "all") => {
|
|
284
363
|
if (!isLoggedIn()) {
|
|
285
|
-
// Saved reports are a logged-in feature.
|
|
286
|
-
|
|
287
|
-
showExportMsg("Export requires `dg login` (free account)");
|
|
364
|
+
// Saved reports are a logged-in feature.
|
|
365
|
+
showExportMsg(loginRequiredToast(), "nudge");
|
|
288
366
|
return;
|
|
289
367
|
}
|
|
290
368
|
setExportMenu({ scope: defaultScope, format: "json", activeRow: "scope" });
|
|
291
369
|
};
|
|
292
|
-
/** Build the JS object for a given export scope. The `currentLicenseIdx`
|
|
293
|
-
* parameter scopes "current-license" to the drill-in user is viewing. */
|
|
294
370
|
const buildExportPayload = (scope, currentLicenseIdx) => {
|
|
295
371
|
const blocked = result.packages.filter((p) => p.action === "block");
|
|
296
372
|
const warned = result.packages.filter((p) => p.action === "warn");
|
|
@@ -351,7 +427,6 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
351
427
|
reasons: p.reasons,
|
|
352
428
|
}));
|
|
353
429
|
}
|
|
354
|
-
// scope === "all"
|
|
355
430
|
return {
|
|
356
431
|
...summary,
|
|
357
432
|
packages: result.packages,
|
|
@@ -364,20 +439,15 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
364
439
|
})),
|
|
365
440
|
};
|
|
366
441
|
};
|
|
367
|
-
/** Quote a CSV cell — escape double-quotes by doubling, wrap in quotes if
|
|
368
|
-
* the value contains a comma, quote, or newline. */
|
|
369
442
|
const csvCell = (v) => {
|
|
370
443
|
const s = v === null || v === undefined ? "" : String(v);
|
|
371
444
|
return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
372
445
|
};
|
|
373
|
-
/** Convert a payload + scope to a serialized string in the chosen format
|
|
374
|
-
* plus the file extension. */
|
|
375
446
|
const formatExport = (payload, scope, format) => {
|
|
376
447
|
if (format === "json") {
|
|
377
448
|
return { body: JSON.stringify(payload, null, 2) + "\n", ext: "json" };
|
|
378
449
|
}
|
|
379
450
|
if (format === "csv") {
|
|
380
|
-
// Tabular scopes only — flatten to header + rows.
|
|
381
451
|
if (scope === "packages") {
|
|
382
452
|
const rows = payload;
|
|
383
453
|
const lines = ["name,version,score,license,riskCategory"];
|
|
@@ -429,10 +499,6 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
429
499
|
}
|
|
430
500
|
return { body: lines.join("\n") + "\n", ext: "csv" };
|
|
431
501
|
}
|
|
432
|
-
// Markdown — for `all`, render summary + packages table + licenses table
|
|
433
|
-
// in one document so the file matches what the user sees in the TUI.
|
|
434
|
-
// For `findings` with an empty list, surface that explicitly so the
|
|
435
|
-
// file isn't just a title and nothing else.
|
|
436
502
|
if (format === "md") {
|
|
437
503
|
const lines = [`# Dependency Guardian — ${scope}`, ""];
|
|
438
504
|
lines.push(`*Scanned at ${new Date().toISOString()}*`, "");
|
|
@@ -496,7 +562,6 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
496
562
|
}
|
|
497
563
|
return { body: lines.join("\n"), ext: "md" };
|
|
498
564
|
}
|
|
499
|
-
// format === "txt" — quick human-readable, mirrors the md structure
|
|
500
565
|
const txt = [`Dependency Guardian — ${scope}`, "=".repeat(40), ""];
|
|
501
566
|
const txtSummary = (s) => {
|
|
502
567
|
txt.push(`Score: ${s.score} (${s.action})`);
|
|
@@ -559,33 +624,30 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
559
624
|
}
|
|
560
625
|
return { body: txt.join("\n") + "\n", ext: "txt" };
|
|
561
626
|
};
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
627
|
+
const buildExportOption = (scope, format, currentLicenseIdx) => {
|
|
628
|
+
const payload = buildExportPayload(scope, currentLicenseIdx);
|
|
629
|
+
if (payload === null)
|
|
630
|
+
return null;
|
|
631
|
+
const { body, ext } = formatExport(payload, scope, format);
|
|
632
|
+
const scopeTag = scope === "current-license" && currentLicenseIdx !== null
|
|
633
|
+
? `${(licenseGroups[currentLicenseIdx]?.spdx ?? "license").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 32)}`
|
|
634
|
+
: scope;
|
|
635
|
+
return {
|
|
636
|
+
label: `${EXPORT_SCOPE_LABELS[scope]} · ${EXPORT_FORMAT_LABELS[format]}`,
|
|
637
|
+
defaultName: scope === "all" ? `dg-scan.${ext}` : `dg-scan-${scopeTag}.${ext}`,
|
|
638
|
+
render: () => body
|
|
639
|
+
};
|
|
640
|
+
};
|
|
641
|
+
const handleExportDone = (result) => {
|
|
642
|
+
setExportDialog(null);
|
|
643
|
+
clearScreen();
|
|
644
|
+
if (result === null)
|
|
568
645
|
return;
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
showExportMsg(`Nothing to export for scope "${scope}"`);
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
const { body, ext } = formatExport(payload, scope, format);
|
|
578
|
-
const scopeTag = scope === "current-license" && currentLicenseIdx !== null
|
|
579
|
-
? `${(licenseGroups[currentLicenseIdx]?.spdx ?? "license").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 32)}`
|
|
580
|
-
: scope;
|
|
581
|
-
const filename = `dg-scan-${ts}-${scopeTag}.${ext}`;
|
|
582
|
-
const path = resolvePath(process.cwd(), filename);
|
|
583
|
-
writeFileSync(path, body, "utf-8");
|
|
584
|
-
showExportMsg(`✓ Exported to ${filename}`);
|
|
585
|
-
}
|
|
586
|
-
catch (e) {
|
|
587
|
-
showExportMsg(`Export failed: ${e.message}`);
|
|
588
|
-
}
|
|
646
|
+
setExportMenu(null);
|
|
647
|
+
if ("path" in result)
|
|
648
|
+
showExportMsg(`✓ Exported to ${result.path}`);
|
|
649
|
+
else
|
|
650
|
+
showExportMsg(`Export failed: ${result.error}`, "error");
|
|
589
651
|
};
|
|
590
652
|
const licenseGroups = useMemo(() => {
|
|
591
653
|
const buckets = new Map();
|
|
@@ -618,8 +680,10 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
618
680
|
const searchModeRef = useRef(searchMode);
|
|
619
681
|
searchModeRef.current = searchMode;
|
|
620
682
|
const { rows: termRows, cols: termCols } = useTerminalSize();
|
|
621
|
-
const
|
|
622
|
-
const
|
|
683
|
+
const subviewCompact = termRows < COMPACT_ROWS;
|
|
684
|
+
const ackSectionLines = acked.length > 0
|
|
685
|
+
? 2 + Math.min(ackGroups.length, ACK_PREVIEW_LIMIT) + (ackGroups.length > ACK_PREVIEW_LIMIT ? 1 : 0)
|
|
686
|
+
: 0;
|
|
623
687
|
const innerWidth = Math.max(40, termCols - 6);
|
|
624
688
|
const detailGroup = useMemo(() => {
|
|
625
689
|
if (!detailPane)
|
|
@@ -651,6 +715,17 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
651
715
|
return 1 + animVisibleLines;
|
|
652
716
|
return groupRowHeight(group, level);
|
|
653
717
|
};
|
|
718
|
+
const listRows = groups.reduce((sum, group, idx) => sum + animatedGroupHeight(group, getLevel(idx)), 0);
|
|
719
|
+
const { compact, availableRows } = resolveResultsLayout({
|
|
720
|
+
termRows,
|
|
721
|
+
logoRows: termCols >= LOGO_MIN_COLS ? renderLogo(result.action).length : 0,
|
|
722
|
+
listRows,
|
|
723
|
+
ackSectionLines,
|
|
724
|
+
hasGroups: groups.length > 0,
|
|
725
|
+
hasUsage: Boolean(scanUsage),
|
|
726
|
+
extraLines: (discoveredTotal !== undefined && discoveredTotal > total ? 1 : 0) +
|
|
727
|
+
(searchQuery && groups.length === 0 ? 1 : 0),
|
|
728
|
+
});
|
|
654
729
|
const visibleEnd = useMemo(() => {
|
|
655
730
|
let consumed = 0;
|
|
656
731
|
let end = view.viewport;
|
|
@@ -697,7 +772,6 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
697
772
|
}
|
|
698
773
|
return newStart;
|
|
699
774
|
};
|
|
700
|
-
// Re-clamp viewport when terminal is resized
|
|
701
775
|
useEffect(() => {
|
|
702
776
|
if (groups.length === 0)
|
|
703
777
|
return;
|
|
@@ -706,7 +780,6 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
706
780
|
const newVp = adjustViewport(cursor, expandedKey, expandLevel, clamped);
|
|
707
781
|
dispatchView({ type: "MOVE", cursor, viewport: newVp });
|
|
708
782
|
}, [availableRows]);
|
|
709
|
-
// Clamp detail pane scroll when terminal resizes
|
|
710
783
|
useEffect(() => {
|
|
711
784
|
const dp = detailPaneRef.current;
|
|
712
785
|
if (dp && detailLines.length > 0) {
|
|
@@ -722,10 +795,8 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
722
795
|
}
|
|
723
796
|
}, [detailPane, detailGroup]);
|
|
724
797
|
useInput((input, key) => {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
// row, ←→ or Tab switches rows. No letter shortcuts (the user found
|
|
728
|
-
// them clunky and they collide with `q` / other meaningful keys).
|
|
798
|
+
if (exportDialogRef.current)
|
|
799
|
+
return;
|
|
729
800
|
if (exportMenuRef.current) {
|
|
730
801
|
const menu = exportMenuRef.current;
|
|
731
802
|
const SCOPES = ["all", "summary", "packages", "licenses", "findings"];
|
|
@@ -746,9 +817,14 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
746
817
|
}
|
|
747
818
|
if (key.return) {
|
|
748
819
|
const m = exportMenuRef.current;
|
|
749
|
-
|
|
820
|
+
const option = buildExportOption(m.scope, m.format, licenseDetailIdxRef.current);
|
|
750
821
|
clearScreen();
|
|
751
|
-
|
|
822
|
+
if (!option) {
|
|
823
|
+
setExportMenu(null);
|
|
824
|
+
showExportMsg(`Nothing to export for scope "${m.scope}"`);
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
setExportDialog(option);
|
|
752
828
|
return;
|
|
753
829
|
}
|
|
754
830
|
if (key.tab || key.leftArrow || key.rightArrow) {
|
|
@@ -787,9 +863,7 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
787
863
|
return;
|
|
788
864
|
}
|
|
789
865
|
if (showLicensesRef.current) {
|
|
790
|
-
// Drill-in mode: viewing the package list for one license.
|
|
791
866
|
if (licenseDetailIdxRef.current !== null) {
|
|
792
|
-
// Search-typing mode inside drill-in: capture text input.
|
|
793
867
|
if (licenseSearchModeRef.current) {
|
|
794
868
|
if (key.escape) {
|
|
795
869
|
setLicenseSearchMode(false);
|
|
@@ -1013,32 +1087,23 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
1013
1087
|
const visibleGroups = groups.slice(view.viewport, visibleEnd);
|
|
1014
1088
|
const aboveCount = view.viewport;
|
|
1015
1089
|
const belowCount = groups.length - visibleEnd;
|
|
1016
|
-
const lcCol = 16;
|
|
1017
|
-
const nameCol = Math.max(20, innerWidth - BADGE_COL -
|
|
1090
|
+
const lcCol = 16;
|
|
1091
|
+
const nameCol = Math.max(20, innerWidth - BADGE_COL - 16 - lcCol);
|
|
1018
1092
|
// Clamp cursor to valid range (groups may shrink via search filter)
|
|
1019
1093
|
const clampedCursor = groups.length > 0 ? Math.min(view.cursor, groups.length - 1) : 0;
|
|
1020
|
-
|
|
1094
|
+
if (exportDialog) {
|
|
1095
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, compact: subviewCompact, total: total, flagged: flagged.length, clean: clean.length, userStatus: headerStatus, scanUsage: scanUsage, usageNearLimit: usageNearLimit }), _jsx(ExportDialog, { options: [exportDialog], theme: theme, cwd: process.cwd(), onDone: handleExportDone })] }));
|
|
1096
|
+
}
|
|
1021
1097
|
if (exportMenu) {
|
|
1022
|
-
const SCOPES_RENDER = [
|
|
1023
|
-
{ value: "all", label: "All" },
|
|
1024
|
-
{ value: "summary", label: "Summary" },
|
|
1025
|
-
{ value: "packages", label: "Packages" },
|
|
1026
|
-
{ value: "licenses", label: "Licenses" },
|
|
1027
|
-
{ value: "findings", label: "Findings (warn+block)" },
|
|
1028
|
-
];
|
|
1098
|
+
const SCOPES_RENDER = ["all", "summary", "packages", "licenses", "findings"].map((value) => ({ value, label: EXPORT_SCOPE_LABELS[value] }));
|
|
1029
1099
|
if (licenseDetailIdxRef.current !== null) {
|
|
1030
1100
|
const focus = licenseGroups[licenseDetailIdxRef.current];
|
|
1031
1101
|
SCOPES_RENDER.push({
|
|
1032
1102
|
value: "current-license",
|
|
1033
|
-
label:
|
|
1103
|
+
label: `${EXPORT_SCOPE_LABELS["current-license"]} (${focus?.spdx ?? "—"})`,
|
|
1034
1104
|
});
|
|
1035
1105
|
}
|
|
1036
|
-
const FORMATS_RENDER = [
|
|
1037
|
-
{ value: "json", label: "JSON" },
|
|
1038
|
-
{ value: "csv", label: "CSV" },
|
|
1039
|
-
{ value: "md", label: "Markdown" },
|
|
1040
|
-
{ value: "txt", label: "Plain text" },
|
|
1041
|
-
];
|
|
1106
|
+
const FORMATS_RENDER = ["json", "csv", "md", "txt"].map((value) => ({ value, label: EXPORT_FORMAT_LABELS[value] }));
|
|
1042
1107
|
const stackedLayout = termCols < 80;
|
|
1043
1108
|
const colWidth = stackedLayout ? undefined : Math.max(24, Math.floor((termCols - 8) / 2));
|
|
1044
1109
|
const renderColumn = (title, rows, current, isActive) => (_jsxs(Box, { flexDirection: "column", width: colWidth, flexShrink: 1, marginBottom: stackedLayout ? 1 : 0, children: [_jsxs(Text, { children: [isActive ? chalk.cyan("▌ ") : " ", isActive ? chalk.bold(title) : chalk.dim(title)] }), rows.map((r) => {
|
|
@@ -1049,14 +1114,12 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
1049
1114
|
const text = selected ? chalk.bold(r.label) : r.label;
|
|
1050
1115
|
return (_jsxs(Box, { children: [_jsx(Box, { width: 5, flexShrink: 0, children: _jsxs(Text, { children: [" ", bullet] }) }), _jsx(Box, { flexShrink: 1, children: _jsx(Text, { wrap: "truncate-end", children: text }) })] }, r.value));
|
|
1051
1116
|
})] }));
|
|
1052
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, total: total, flagged: flagged.length, clean: clean.length, userStatus:
|
|
1117
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, compact: subviewCompact, total: total, flagged: flagged.length, clean: clean.length, userStatus: headerStatus, scanUsage: scanUsage, usageNearLimit: usageNearLimit }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingLeft: 2, paddingRight: 2, children: [_jsx(Text, { bold: true, children: "Export" }), _jsx(Text, { children: "" }), _jsxs(Box, { flexDirection: stackedLayout ? "column" : "row", children: [renderColumn("What", SCOPES_RENDER, exportMenu.scope, exportMenu.activeRow === "scope"), renderColumn("Format", FORMATS_RENDER, exportMenu.format, exportMenu.activeRow === "format")] })] }), _jsx(Text, { dimColor: true, children: chalk.dim("─".repeat(Math.max(20, termCols - 4))) }), _jsxs(Text, { children: [" ", chalk.bold.cyan("↑↓"), " ", chalk.dim("scroll"), " ", chalk.bold.cyan("←→/Tab"), " ", chalk.dim("switch row"), " ", chalk.bold.cyan("⏎"), " ", chalk.dim("export"), " ", chalk.bold.cyan("Esc"), " ", chalk.dim("cancel")] })] }));
|
|
1053
1118
|
}
|
|
1054
|
-
// ── Help overlay ──
|
|
1055
1119
|
if (showHelp) {
|
|
1056
1120
|
const isDetail = detailPane !== null;
|
|
1057
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, total: total, flagged: flagged.length, clean: clean.length, userStatus:
|
|
1121
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, compact: subviewCompact, total: total, flagged: flagged.length, clean: clean.length, userStatus: headerStatus, 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"] })] })] }));
|
|
1058
1122
|
}
|
|
1059
|
-
// ── License overlay ──
|
|
1060
1123
|
if (showLicenses) {
|
|
1061
1124
|
const lcColor = (risk) => {
|
|
1062
1125
|
if (risk === "permissive")
|
|
@@ -1085,35 +1148,34 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
1085
1148
|
const _above = top;
|
|
1086
1149
|
const _below = filtered.length - bottom;
|
|
1087
1150
|
const _slice = filtered.slice(top, bottom);
|
|
1088
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, total: total, flagged: flagged.length, clean: clean.length, userStatus:
|
|
1151
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, compact: subviewCompact, total: total, flagged: flagged.length, clean: clean.length, userStatus: headerStatus, scanUsage: scanUsage, usageNearLimit: usageNearLimit }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingLeft: 2, paddingRight: 2, width: "100%", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: color(g.spdx) }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", g.risk, " \u00B7 ", g.count, " package", g.count !== 1 ? "s" : "", q ? ` · ${filtered.length} match${filtered.length !== 1 ? "es" : ""}` : ""] })] }), licenseSearchMode || q ? (_jsx(Box, { children: _jsxs(Text, { children: [" ", chalk.bold.cyan("/"), " ", q || chalk.dim("(type to filter; Esc clears, Enter confirms)"), licenseSearchMode ? chalk.cyan("█") : ""] }) })) : (_jsx(Text, { children: "" })), _above > 0 && _jsx(Text, { dimColor: true, children: ` ↑ ${_above} more above` }), _slice.length === 0 && _jsx(Text, { dimColor: true, children: ` (no packages match "${q}")` }), _slice.map((p, i) => {
|
|
1089
1152
|
const absIdx = top + i;
|
|
1090
1153
|
const isSelected = absIdx === cursorIdx;
|
|
1091
1154
|
const nameCol = Math.max(28, Math.floor(termCols * 0.55));
|
|
1092
1155
|
if (isSelected) {
|
|
1093
|
-
return (_jsxs(Text, { backgroundColor: "#1a1a2e", children: [chalk.cyan("▌"), " ", chalk.bold(pad(truncate(p.name, nameCol - 2), nameCol)), chalk.dim(pad(p.version, 16))] }, `${p.name}@${p.version}`));
|
|
1156
|
+
return (_jsxs(Text, { backgroundColor: "#1a1a2e", wrap: "truncate-end", children: [chalk.cyan("▌"), " ", chalk.bold(pad(truncate(p.name, nameCol - 2), nameCol)), chalk.dim(pad(truncate(p.version, 15), 16))] }, `${p.name}@${p.version}`));
|
|
1094
1157
|
}
|
|
1095
|
-
return (_jsxs(Text, { children: [" ", pad(truncate(p.name, nameCol - 2), nameCol), chalk.dim(pad(p.version, 16))] }, `${p.name}@${p.version}`));
|
|
1096
|
-
}), _below > 0 && _jsx(Text, { dimColor: true, children: ` ↓ ${_below} more below` })] }), _jsx(Text, { dimColor: true, children: chalk.dim("─".repeat(Math.max(20, termCols - 4))) }),
|
|
1158
|
+
return (_jsxs(Text, { wrap: "truncate-end", children: [" ", pad(truncate(p.name, nameCol - 2), nameCol), chalk.dim(pad(truncate(p.version, 15), 16))] }, `${p.name}@${p.version}`));
|
|
1159
|
+
}), _below > 0 && _jsx(Text, { dimColor: true, children: ` ↓ ${_below} more below` })] }), _jsx(Text, { dimColor: true, children: chalk.dim("─".repeat(Math.max(20, termCols - 4))) }), (() => {
|
|
1160
|
+
const sel = filtered[cursorIdx];
|
|
1161
|
+
const page = sel ? packagePageUrl(sel.ecosystem ?? "", sel.name) : null;
|
|
1162
|
+
return page ? _jsxs(Text, { wrap: "truncate-end", children: [" ", chalk.dim(`→ ${page}`)] }) : null;
|
|
1163
|
+
})(), _jsxs(Text, { children: [" ", chalk.bold.cyan("↑↓"), " ", chalk.dim("scroll"), " ", chalk.bold.cyan("/"), " ", chalk.dim("search"), " ", chalk.bold.cyan("e"), " ", chalk.dim("export"), " ", chalk.bold.cyan("Esc"), " ", chalk.dim("back"), " ", chalk.bold.cyan("q"), " ", chalk.dim("quit"), exportMsgText && _jsxs(_Fragment, { children: [" ", exportMsgText] })] })] }));
|
|
1097
1164
|
}
|
|
1098
|
-
// ── License list (default overlay view) ──
|
|
1099
1165
|
// Column widths: pick from terminal so long SPDX strings (e.g.
|
|
1100
1166
|
// "(BSD-2-Clause OR MIT OR Apache-2.0)") don't blow the layout.
|
|
1101
|
-
const innerCols = Math.max(
|
|
1167
|
+
const innerCols = Math.max(20, termCols - 6);
|
|
1102
1168
|
const spdxCol = Math.min(40, Math.max(22, Math.floor(innerCols * 0.35)));
|
|
1103
1169
|
const riskCol = 18;
|
|
1104
1170
|
const countCol = 7;
|
|
1105
1171
|
const barCol = Math.max(8, innerCols - spdxCol - riskCol - countCol - 4);
|
|
1106
1172
|
const visibleRows = Math.max(5, termRows - 20);
|
|
1107
1173
|
const cursor = Math.min(licenseCursor, licenseGroups.length - 1);
|
|
1108
|
-
// Scrolling viewport: keep the cursor visible inside a window of
|
|
1109
|
-
// `visibleRows` rows, clamped at the start/end. Replaces the prior
|
|
1110
|
-
// "resize terminal to see all" dead end — ↑↓ now genuinely scrolls
|
|
1111
|
-
// through the full list on any terminal height.
|
|
1112
1174
|
const top = Math.max(0, Math.min(cursor - Math.floor((visibleRows - 1) / 2), licenseGroups.length - visibleRows));
|
|
1113
1175
|
const bottom = Math.min(top + visibleRows, licenseGroups.length);
|
|
1114
1176
|
const hiddenAbove = top;
|
|
1115
1177
|
const hiddenBelow = licenseGroups.length - bottom;
|
|
1116
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, total: total, flagged: flagged.length, clean: clean.length, userStatus:
|
|
1178
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, compact: subviewCompact, total: total, flagged: flagged.length, clean: clean.length, userStatus: headerStatus, scanUsage: scanUsage, usageNearLimit: usageNearLimit }), _jsx(Text, { dimColor: true, children: chalk.dim("─".repeat(Math.max(20, termCols - 4))) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 2, paddingRight: 2, width: "100%", children: [_jsxs(Text, { bold: true, children: ["Licenses", " ", chalk.dim(`(${licenseGroups.length} unique across ${totalCount} packages)`)] }), _jsx(Text, { children: "" }), _jsxs(Box, { children: [_jsx(Box, { width: 2, flexShrink: 0, children: _jsx(Text, { children: " " }) }), _jsx(Box, { width: spdxCol, flexShrink: 1, children: _jsx(Text, { dimColor: true, children: "SPDX" }) }), _jsx(Box, { width: riskCol, flexShrink: 0, children: _jsx(Text, { dimColor: true, children: "Risk" }) }), _jsx(Box, { width: countCol, flexShrink: 0, children: _jsx(Text, { dimColor: true, children: "Count" }) }), _jsx(Box, { width: barCol, flexShrink: 1, children: _jsx(Text, { dimColor: true, children: "Share" }) })] }), hiddenAbove > 0 && (_jsx(Text, { dimColor: true, children: ` ↑ ${hiddenAbove} more above` })), licenseGroups.slice(top, bottom).map((g, i) => {
|
|
1117
1179
|
const absIdx = top + i;
|
|
1118
1180
|
const color = lcColor(g.risk);
|
|
1119
1181
|
const barLen = Math.max(1, Math.round((g.count / maxCount) * (barCol - 2)));
|
|
@@ -1122,12 +1184,11 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
1122
1184
|
const spdxText = truncate(g.spdx, spdxCol - 1);
|
|
1123
1185
|
const riskText = g.risk;
|
|
1124
1186
|
if (isSelected) {
|
|
1125
|
-
return (_jsxs(Text, { backgroundColor: "#1a1a2e", children: [chalk.cyan("▌"), " ", chalk.bold(color(pad(spdxText, spdxCol))), chalk.dim(pad(riskText, riskCol)), chalk.bold(pad(String(g.count), countCol)), color(bar)] }, `${g.risk}::${g.spdx}::${absIdx}`));
|
|
1187
|
+
return (_jsxs(Text, { backgroundColor: "#1a1a2e", wrap: "truncate-end", children: [chalk.cyan("▌"), " ", chalk.bold(color(pad(spdxText, spdxCol))), chalk.dim(pad(riskText, riskCol)), chalk.bold(pad(String(g.count), countCol)), color(bar)] }, `${g.risk}::${g.spdx}::${absIdx}`));
|
|
1126
1188
|
}
|
|
1127
|
-
return (_jsxs(Text, { children: [" ", color(pad(spdxText, spdxCol)), chalk.dim(pad(riskText, riskCol)), chalk.bold(pad(String(g.count), countCol)), color(bar)] }, `${g.risk}::${g.spdx}::${absIdx}`));
|
|
1128
|
-
}), hiddenBelow > 0 && (_jsx(Text, { dimColor: true, children: ` ↓ ${hiddenBelow} more below` }))] }), _jsx(Text, { dimColor: true, children: chalk.dim("─".repeat(Math.max(20, termCols - 4))) }), _jsxs(Text, { children: [" ", chalk.bold.cyan("↑↓"), " ", chalk.dim("navigate"), " ", chalk.bold.cyan("⏎"), " ", chalk.dim("view packages"), " ", chalk.bold.cyan("e"), " ", chalk.dim("export"), " ", chalk.bold.cyan("Esc"), " ", chalk.dim("close"), " ", chalk.bold.cyan("q"), " ", chalk.dim("quit"),
|
|
1189
|
+
return (_jsxs(Text, { wrap: "truncate-end", children: [" ", color(pad(spdxText, spdxCol)), chalk.dim(pad(riskText, riskCol)), chalk.bold(pad(String(g.count), countCol)), color(bar)] }, `${g.risk}::${g.spdx}::${absIdx}`));
|
|
1190
|
+
}), hiddenBelow > 0 && (_jsx(Text, { dimColor: true, children: ` ↓ ${hiddenBelow} more below` }))] }), _jsx(Text, { dimColor: true, children: chalk.dim("─".repeat(Math.max(20, termCols - 4))) }), _jsxs(Text, { children: [" ", chalk.bold.cyan("↑↓"), " ", chalk.dim("navigate"), " ", chalk.bold.cyan("⏎"), " ", chalk.dim("view packages"), " ", chalk.bold.cyan("e"), " ", chalk.dim("export"), " ", chalk.bold.cyan("Esc"), " ", chalk.dim("close"), " ", chalk.bold.cyan("q"), " ", chalk.dim("quit"), exportMsgText && _jsxs(_Fragment, { children: [" ", exportMsgText] })] })] }));
|
|
1129
1191
|
}
|
|
1130
|
-
// ── Detail pane mode ──
|
|
1131
1192
|
if (detailPane !== null) {
|
|
1132
1193
|
const dpGroup = detailGroup;
|
|
1133
1194
|
if (dpGroup) {
|
|
@@ -1137,11 +1198,10 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
1137
1198
|
const dpAbove = dpScroll;
|
|
1138
1199
|
const dpBelow = Math.max(0, detailLines.length - dpScroll - detailContentRows);
|
|
1139
1200
|
const dpVisible = detailLines.slice(dpScroll, dpScroll + detailContentRows);
|
|
1140
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, total: total, flagged: flagged.length, clean: clean.length, userStatus:
|
|
1201
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, compact: subviewCompact, total: total, flagged: flagged.length, clean: clean.length, userStatus: headerStatus, 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")] })] }));
|
|
1141
1202
|
}
|
|
1142
1203
|
}
|
|
1143
|
-
|
|
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) => {
|
|
1204
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScoreHeader, { score: result.score, action: result.action, compact: compact, total: total, flagged: flagged.length, clean: clean.length, userStatus: headerStatus, 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) => {
|
|
1145
1205
|
const globalIdx = view.viewport + visIdx;
|
|
1146
1206
|
const isCursor = globalIdx === clampedCursor;
|
|
1147
1207
|
const level = getLevel(globalIdx);
|
|
@@ -1156,8 +1216,18 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
|
|
|
1156
1216
|
: (lcInfo.riskCategory === "no-license" || lcInfo.riskCategory === "unlicensed" || lcInfo.riskCategory === "network-copyleft") ? chalk.red
|
|
1157
1217
|
: chalk.yellow;
|
|
1158
1218
|
const arrow = level === "summary" ? "\u25BE" : "\u25B8"; // ▾ expanded, ▸ collapsed
|
|
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}"` })),
|
|
1219
|
+
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)), provenanceMarker(rep), 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), provenanceMarker(rep), 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));
|
|
1220
|
+
}), 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}"` })), acked.length > 0 && !searchQuery && (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: [_jsx(Text, { bold: true, dimColor: true, children: `Acknowledged (${acked.length}) \u00b7 dg.json` }), ackGroups.slice(0, ACK_PREVIEW_LIMIT).map((group) => {
|
|
1221
|
+
const rep = firstPackage(group);
|
|
1222
|
+
const ack = ackByKey.get(packageKey(rep.name, rep.version));
|
|
1223
|
+
const who = ack?.by ?? "unknown";
|
|
1224
|
+
const when = ack?.at ? ` on ${ack.at.slice(0, 10)}` : "";
|
|
1225
|
+
return (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [" ", chalk.yellow("\u25b8"), " ", groupNames(group), " ", chalk.dim(`accepted by ${who}${when}`)] }, `ack-${group.key}`));
|
|
1226
|
+
}), ackGroups.length > ACK_PREVIEW_LIMIT && (_jsx(Text, { dimColor: true, children: ` +${ackGroups.length - ACK_PREVIEW_LIMIT} more \u2014 dg decisions` }))] })), !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"] })), acked.length > 0 && (_jsxs(Text, { wrap: "truncate-end", children: [chalk.yellow("\u26a0"), " ", chalk.yellow(String(acked.length)), " ", chalk.dim(`acknowledged warn${acked.length !== 1 ? "s" : ""} \u00b7 dg.json \u00b7 review with 'dg decisions'`)] })), _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))) }), (() => {
|
|
1227
|
+
const cur = groups.length > 0 ? groups[Math.min(view.cursor, groups.length - 1)] : undefined;
|
|
1228
|
+
const page = cur ? packagePageUrl(firstPackage(cur).ecosystem ?? "", firstPackage(cur).name) : null;
|
|
1229
|
+
return page ? _jsxs(Text, { wrap: "truncate-end", children: [" ", chalk.dim(`\u2192 ${page}`)] }) : null;
|
|
1230
|
+
})(), searchMode ? (_jsxs(Text, { wrap: "truncate-end", children: [" ", chalk.bold.cyan("/"), " ", searchQuery, chalk.cyan("\u2588"), " ", chalk.dim("Esc clear")] })) : exportMsg ? (_jsxs(Text, { wrap: "truncate-end", children: [" ", exportMsgText] })) : 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")] })) : (_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")] }))] }));
|
|
1161
1231
|
};
|
|
1162
1232
|
const T = {
|
|
1163
1233
|
branch: chalk.dim("\u251C\u2500\u2500"),
|
|
@@ -1193,21 +1263,22 @@ const FindingsSummary = ({ group, maxWidth, maxLines }) => {
|
|
|
1193
1263
|
.sort((a, b) => b.severity - a.severity);
|
|
1194
1264
|
const hasAffects = group.packages.length > 3;
|
|
1195
1265
|
const allLines = [];
|
|
1196
|
-
// License info
|
|
1197
1266
|
const lcLine = licenseLine(rep);
|
|
1198
1267
|
if (lcLine)
|
|
1199
1268
|
allLines.push(lcLine);
|
|
1269
|
+
const downgrade = packageDowngradeLine(rep);
|
|
1270
|
+
if (downgrade) {
|
|
1271
|
+
allLines.push(_jsxs(Text, { wrap: "truncate-end", children: [T.branch, " ", chalk.yellow(downgrade)] }, "provenance-downgrade"));
|
|
1272
|
+
}
|
|
1200
1273
|
// Render findings — API returns tier-gated data:
|
|
1201
1274
|
// Free: { category, severity } — don't show raw IDs, just upgrade prompt
|
|
1202
1275
|
// Pro: { category, severity, title } — show category + title
|
|
1203
1276
|
// Team: { category, severity, title, evidence } — show everything
|
|
1204
1277
|
const isFree = visibleFindings.length > 0 && !visibleFindings[0]?.title;
|
|
1205
1278
|
if (isFree) {
|
|
1206
|
-
// Free tier: don't show raw category IDs — just the upgrade prompt
|
|
1207
1279
|
allLines.push(_jsxs(Text, { dimColor: true, children: [hasAffects ? T.branch : T.last, " ", chalk.yellow("\u2192"), " ", chalk.yellow("Upgrade to Pro"), " for finding details"] }, "upgrade"));
|
|
1208
1280
|
}
|
|
1209
1281
|
else {
|
|
1210
|
-
// Paid tier: show findings with category + title
|
|
1211
1282
|
for (let idx = 0; idx < visibleFindings.length; idx++) {
|
|
1212
1283
|
const f = visibleFindings[idx];
|
|
1213
1284
|
if (!f)
|
|
@@ -1221,7 +1292,6 @@ const FindingsSummary = ({ group, maxWidth, maxLines }) => {
|
|
|
1221
1292
|
}
|
|
1222
1293
|
}
|
|
1223
1294
|
if (visibleFindings.length === 0 && rep.score > 0) {
|
|
1224
|
-
// No findings at all (shouldn't happen after API change, but safety fallback)
|
|
1225
1295
|
allLines.push(_jsxs(Text, { dimColor: true, children: [hasAffects ? T.branch : T.last, " Score: ", rep.score, "/100"] }, "score-only"));
|
|
1226
1296
|
}
|
|
1227
1297
|
if (visibleFindings.length === 0 && rep.score === 0) {
|