@westbayberry/dg 2.1.0 → 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.
Files changed (138) hide show
  1. package/README.md +216 -226
  2. package/dist/agents/claude-code.js +113 -0
  3. package/dist/agents/codex.js +65 -0
  4. package/dist/agents/copilot-cli.js +115 -0
  5. package/dist/agents/cursor.js +113 -0
  6. package/dist/agents/gemini.js +107 -0
  7. package/dist/agents/persistence.js +285 -0
  8. package/dist/agents/registry.js +127 -0
  9. package/dist/agents/types.js +1 -0
  10. package/dist/agents/windsurf.js +93 -0
  11. package/dist/api/analyze.js +1 -1
  12. package/dist/audit/detectors.js +0 -11
  13. package/dist/audit/events.js +5 -21
  14. package/dist/audit-ui/AuditApp.js +2 -0
  15. package/dist/audit-ui/components/AuditResultsView.js +55 -92
  16. package/dist/audit-ui/export.js +0 -4
  17. package/dist/audit-ui/format.js +0 -3
  18. package/dist/audit-ui/launch.js +10 -1
  19. package/dist/auth/device-login.js +4 -5
  20. package/dist/auth/login-app.js +7 -7
  21. package/dist/auth/store.js +8 -3
  22. package/dist/bin/dg.js +59 -52
  23. package/dist/commands/agents.js +231 -0
  24. package/dist/commands/audit.js +22 -3
  25. package/dist/commands/config.js +15 -7
  26. package/dist/commands/cooldown.js +389 -0
  27. package/dist/commands/decisions.js +23 -9
  28. package/dist/commands/doctor.js +1 -1
  29. package/dist/commands/help.js +1 -1
  30. package/dist/commands/licenses.js +10 -22
  31. package/dist/commands/logout.js +4 -11
  32. package/dist/commands/router.js +6 -4
  33. package/dist/commands/sbom.js +206 -0
  34. package/dist/commands/scan.js +1 -1
  35. package/dist/commands/service.js +32 -13
  36. package/dist/commands/setup.js +197 -27
  37. package/dist/commands/types.js +1 -0
  38. package/dist/commands/update.js +17 -8
  39. package/dist/commands/verify.js +8 -5
  40. package/dist/config/settings.js +14 -44
  41. package/dist/decisions/remember-prompt.js +5 -10
  42. package/dist/export-ui/ExportDialog.js +198 -0
  43. package/dist/install-ui/LiveInstall.js +2 -2
  44. package/dist/install-ui/block-render.js +1 -1
  45. package/dist/install-ui/prep-spinner.js +32 -0
  46. package/dist/launcher/agent-check.js +466 -0
  47. package/dist/launcher/agent-hook-exec.js +70 -0
  48. package/dist/launcher/agent-hook-io.js +31 -0
  49. package/dist/launcher/cargo-cache.js +40 -0
  50. package/dist/launcher/classify.js +17 -6
  51. package/dist/launcher/env.js +71 -24
  52. package/dist/launcher/install-preflight.js +51 -14
  53. package/dist/launcher/live-install.js +25 -5
  54. package/dist/launcher/output-redaction.js +7 -4
  55. package/dist/launcher/preflight-prompt.js +14 -1
  56. package/dist/launcher/run.js +89 -86
  57. package/dist/launcher/spawn-invocation.js +21 -0
  58. package/dist/policy/cooldown.js +18 -5
  59. package/dist/policy/evaluate.js +5 -6
  60. package/dist/policy/pypi-name.js +17 -0
  61. package/dist/presentation/mode.js +3 -2
  62. package/dist/presentation/package-page.js +9 -0
  63. package/dist/presentation/theme.js +7 -7
  64. package/dist/project/dgfile.js +145 -6
  65. package/dist/proxy/auth.js +42 -0
  66. package/dist/proxy/ca.js +29 -9
  67. package/dist/proxy/cooldown-exemptions-file.js +33 -0
  68. package/dist/proxy/enforcement.js +55 -16
  69. package/dist/proxy/metadata-map.js +41 -3
  70. package/dist/proxy/preverified.js +55 -0
  71. package/dist/proxy/server.js +448 -49
  72. package/dist/proxy/worker.js +16 -1
  73. package/dist/publish-set/collect.js +1 -4
  74. package/dist/publish-set/npm.js +8 -5
  75. package/dist/publish-set/pack.js +9 -3
  76. package/dist/runtime/cli.js +0 -4
  77. package/dist/runtime/fatal.js +31 -0
  78. package/dist/runtime/first-run.js +12 -11
  79. package/dist/runtime/node-version.js +43 -6
  80. package/dist/runtime/nudges.js +35 -2
  81. package/dist/sbom/cyclonedx.js +211 -0
  82. package/dist/sbom-ui/SbomApp.js +158 -0
  83. package/dist/sbom-ui/components/SbomHeader.js +32 -0
  84. package/dist/sbom-ui/components/SbomList.js +52 -0
  85. package/dist/sbom-ui/inventory.js +128 -0
  86. package/dist/sbom-ui/launch.js +51 -0
  87. package/dist/sbom-ui/run.js +55 -0
  88. package/dist/sbom-ui/store.js +26 -0
  89. package/dist/scan/collect.js +10 -6
  90. package/dist/scan/command.js +17 -10
  91. package/dist/scan/discovery.js +11 -2
  92. package/dist/scan/render.js +28 -4
  93. package/dist/scan/scanner-report.js +15 -9
  94. package/dist/scan/staged.js +2 -1
  95. package/dist/scan-ui/LegacyApp.js +9 -13
  96. package/dist/scan-ui/alt-screen.js +5 -8
  97. package/dist/scan-ui/components/InteractiveResultsView.js +133 -120
  98. package/dist/scan-ui/components/ProgressBar.js +3 -14
  99. package/dist/scan-ui/components/ProjectSelector.js +1 -1
  100. package/dist/scan-ui/components/ScoreHeader.js +2 -3
  101. package/dist/scan-ui/components/SetupBanner.js +0 -6
  102. package/dist/scan-ui/format-helpers.js +61 -5
  103. package/dist/scan-ui/hooks/useResizeRepaint.js +25 -0
  104. package/dist/scan-ui/hooks/useScan.js +16 -2
  105. package/dist/scan-ui/launch.js +7 -4
  106. package/dist/scan-ui/shims.js +11 -4
  107. package/dist/scripts/detect.js +7 -2
  108. package/dist/scripts/gate.js +1 -1
  109. package/dist/service/state.js +27 -8
  110. package/dist/service/trust-refresh.js +92 -0
  111. package/dist/service/worker.js +23 -1
  112. package/dist/setup/activate-shell.js +28 -0
  113. package/dist/setup/git-hook.js +49 -4
  114. package/dist/setup/plan.js +73 -39
  115. package/dist/setup-ui/gate.js +39 -0
  116. package/dist/setup-ui/offer.js +42 -0
  117. package/dist/setup-ui/selector.js +27 -0
  118. package/dist/setup-ui/tasks.js +56 -0
  119. package/dist/setup-ui/wizard.js +225 -0
  120. package/dist/state/cooldown-held.js +66 -0
  121. package/dist/state/index.js +1 -0
  122. package/dist/state/locks.js +4 -2
  123. package/dist/state/store.js +2 -1
  124. package/dist/util/external-tool.js +25 -0
  125. package/dist/util/git.js +10 -3
  126. package/dist/util/report-writer.js +57 -0
  127. package/dist/verify/local.js +240 -42
  128. package/dist/verify/package-check.js +84 -28
  129. package/dist/verify/preflight.js +242 -49
  130. package/dist/verify/render.js +15 -1
  131. package/npm-shrinkwrap.json +2383 -0
  132. package/package.json +6 -8
  133. package/NOTICE +0 -5
  134. package/dist/commands/completion.js +0 -117
  135. package/dist/commands/explain.js +0 -236
  136. package/dist/commands/unavailable.js +0 -11
  137. package/dist/scripts/rebuild.js +0 -28
  138. package/dist/telemetry/events.js +0 -40
@@ -2,17 +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 { writeFileSync } from "node:fs";
6
- import { resolve as resolvePath } from "node:path";
7
- import { isLoggedIn } from "../shims.js";
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";
8
10
  import { packageKey } from "../../decisions/apply.js";
9
- import { ScoreHeader, COMPACT_ROWS } from "./ScoreHeader.js";
11
+ import { ScoreHeader, COMPACT_ROWS, LOGO_MIN_COLS } from "./ScoreHeader.js";
12
+ import { renderLogo } from "../logo.js";
10
13
  import { useExpandAnimation } from "../hooks/useExpandAnimation.js";
11
14
  import { useTerminalSize } from "../hooks/useTerminalSize.js";
12
15
  import { clearScreen } from "../alt-screen.js";
13
16
  import { pad, truncate, groupPackages as sharedGroupPackages, formatUsage } from "../format-helpers.js";
14
17
  import { provenanceLabel, provenanceDowngradeLine } from "../../presentation/provenance.js";
15
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
+ };
16
33
  function groupPackages(packages) {
17
34
  return sharedGroupPackages(packages, "fingerprint");
18
35
  }
@@ -68,11 +85,24 @@ function packageDowngradeLine(pkg) {
68
85
  }
69
86
  const EVIDENCE_LIMIT = 2;
70
87
  const BADGE_COL = "Unverified".length + 1;
71
- // Fixed lines outside the scrollable group area:
72
- // 5 ScoreHeader box | 2 Flagged box top | 2 scroll indicators
73
- // 1 Flagged box bottom | 4 Clean/Duration box | 1 help bar | 1 margin
74
- const FIXED_CHROME = 21;
75
- const FIXED_CHROME_COMPACT = 14;
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
+ }
76
106
  function firstPackage(group) {
77
107
  const rep = group.packages[0];
78
108
  if (!rep)
@@ -149,6 +179,10 @@ function buildDetailLines(group, safeVersion, maxWidth) {
149
179
  const isFree = visibleFindings.length > 0 && !visibleFindings[0]?.title;
150
180
  const lines = [];
151
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
+ }
152
186
  lines.push(_jsx(Text, { children: "" }, "verdict-gap"));
153
187
  if (group.packages.length > 3) {
154
188
  lines.push(_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["Affects: ", affectsLine(group)] }, "affects"));
@@ -220,17 +254,15 @@ function viewReducer(_state, action) {
220
254
  return { ..._state, expandedKey: action.expandedKey, expandLevel: action.expandLevel, viewport: action.viewport };
221
255
  }
222
256
  }
223
- export const InteractiveResultsView = ({ result, config: _config, durationMs, onExit, onBack, discoveredTotal, userStatus, scanUsage: scanUsageProp, initialView, decisions, }) => {
224
- // Prefer the server's uniform usage block ("used / limit packages this
225
- // month"); fall back to the legacy freeScansRemaining field, then to the
226
- // generic placeholder from bin.ts. usageNearLimit drives the yellow + nudge.
257
+ export const InteractiveResultsView = ({ result, config: _config, durationMs, onExit, onBack, discoveredTotal, initialView, decisions, }) => {
227
258
  const usageDisplay = result.usage ? formatUsage(result.usage) : null;
228
259
  const scanUsage = usageDisplay
229
260
  ? usageDisplay.text
230
261
  : result.freeScansRemaining !== undefined
231
262
  ? `${result.freeScansRemaining.toLocaleString()} packages left`
232
- : scanUsageProp;
263
+ : undefined;
233
264
  const usageNearLimit = usageDisplay?.nearLimit ?? false;
265
+ const headerStatus = useMemo(() => accountHeaderLine(result.usage?.tier), [result.usage]);
234
266
  // Bucket by the server `action`, never by score: the server can return
235
267
  // block/warn at score 0 (policy verdict / yanked / cooldown), and those must
236
268
  // surface as flagged, not hide in "clean".
@@ -289,10 +321,6 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
289
321
  const [showHelp, setShowHelp] = useState(false);
290
322
  const showHelpRef = useRef(showHelp);
291
323
  showHelpRef.current = showHelp;
292
- // License-grouped overlay. Toggled with `l`. Shows packages grouped by
293
- // SPDX license id, sorted by count desc, color-coded by risk category.
294
- // Up/Down to select a row, Enter to drill into the packages for that
295
- // license, Esc to back out, l/Esc again to close the overlay.
296
324
  const [showLicenses, setShowLicenses] = useState(initialView === "licenses");
297
325
  const showLicensesRef = useRef(showLicenses);
298
326
  showLicensesRef.current = showLicenses;
@@ -303,20 +331,23 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
303
331
  const licenseDetailIdxRef = useRef(licenseDetailIdx);
304
332
  licenseDetailIdxRef.current = licenseDetailIdx;
305
333
  const [licenseDetailScroll, setLicenseDetailScroll] = useState(0);
306
- // Search inside the license drill-in: type-to-filter packages by name.
307
334
  const [licenseSearchMode, setLicenseSearchMode] = useState(false);
308
335
  const licenseSearchModeRef = useRef(licenseSearchMode);
309
336
  licenseSearchModeRef.current = licenseSearchMode;
310
337
  const [licenseSearchQuery, setLicenseSearchQuery] = useState("");
311
- // Export status (shown briefly in the footer after `e`).
312
338
  const [exportMsg, setExportMsg] = useState(null);
313
339
  const exportMsgRef = useRef(null);
314
- const showExportMsg = (s) => {
315
- setExportMsg(s);
340
+ const showExportMsg = (text, tone = "ok") => {
341
+ setExportMsg({ text, tone });
316
342
  if (exportMsgRef.current)
317
343
  clearTimeout(exportMsgRef.current);
318
344
  exportMsgRef.current = setTimeout(() => setExportMsg(null), 4000);
319
345
  };
346
+ const exportMsgText = exportMsg
347
+ ? exportMsg.tone === "nudge"
348
+ ? exportMsg.text
349
+ : (exportMsg.tone === "error" ? chalk.red : chalk.green)(exportMsg.text)
350
+ : null;
320
351
  useEffect(() => () => {
321
352
  if (exportMsgRef.current)
322
353
  clearTimeout(exportMsgRef.current);
@@ -324,17 +355,18 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
324
355
  const [exportMenu, setExportMenu] = useState(null);
325
356
  const exportMenuRef = useRef(exportMenu);
326
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), []);
327
362
  const openExportMenu = (defaultScope = "all") => {
328
363
  if (!isLoggedIn()) {
329
- // Saved reports are a logged-in feature. Show the calm nudge instead
330
- // of letting the user navigate a menu they can't actually use.
331
- showExportMsg("Export requires `dg login` (free account)");
364
+ // Saved reports are a logged-in feature.
365
+ showExportMsg(loginRequiredToast(), "nudge");
332
366
  return;
333
367
  }
334
368
  setExportMenu({ scope: defaultScope, format: "json", activeRow: "scope" });
335
369
  };
336
- /** Build the JS object for a given export scope. The `currentLicenseIdx`
337
- * parameter scopes "current-license" to the drill-in user is viewing. */
338
370
  const buildExportPayload = (scope, currentLicenseIdx) => {
339
371
  const blocked = result.packages.filter((p) => p.action === "block");
340
372
  const warned = result.packages.filter((p) => p.action === "warn");
@@ -395,7 +427,6 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
395
427
  reasons: p.reasons,
396
428
  }));
397
429
  }
398
- // scope === "all"
399
430
  return {
400
431
  ...summary,
401
432
  packages: result.packages,
@@ -408,20 +439,15 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
408
439
  })),
409
440
  };
410
441
  };
411
- /** Quote a CSV cell — escape double-quotes by doubling, wrap in quotes if
412
- * the value contains a comma, quote, or newline. */
413
442
  const csvCell = (v) => {
414
443
  const s = v === null || v === undefined ? "" : String(v);
415
444
  return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
416
445
  };
417
- /** Convert a payload + scope to a serialized string in the chosen format
418
- * plus the file extension. */
419
446
  const formatExport = (payload, scope, format) => {
420
447
  if (format === "json") {
421
448
  return { body: JSON.stringify(payload, null, 2) + "\n", ext: "json" };
422
449
  }
423
450
  if (format === "csv") {
424
- // Tabular scopes only — flatten to header + rows.
425
451
  if (scope === "packages") {
426
452
  const rows = payload;
427
453
  const lines = ["name,version,score,license,riskCategory"];
@@ -473,10 +499,6 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
473
499
  }
474
500
  return { body: lines.join("\n") + "\n", ext: "csv" };
475
501
  }
476
- // Markdown — for `all`, render summary + packages table + licenses table
477
- // in one document so the file matches what the user sees in the TUI.
478
- // For `findings` with an empty list, surface that explicitly so the
479
- // file isn't just a title and nothing else.
480
502
  if (format === "md") {
481
503
  const lines = [`# Dependency Guardian — ${scope}`, ""];
482
504
  lines.push(`*Scanned at ${new Date().toISOString()}*`, "");
@@ -540,7 +562,6 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
540
562
  }
541
563
  return { body: lines.join("\n"), ext: "md" };
542
564
  }
543
- // format === "txt" — quick human-readable, mirrors the md structure
544
565
  const txt = [`Dependency Guardian — ${scope}`, "=".repeat(40), ""];
545
566
  const txtSummary = (s) => {
546
567
  txt.push(`Score: ${s.score} (${s.action})`);
@@ -603,33 +624,30 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
603
624
  }
604
625
  return { body: txt.join("\n") + "\n", ext: "txt" };
605
626
  };
606
- /** Run the export with the given scope/format. Writes
607
- * ./dg-scan-<ts>-<scope>.<ext> and shows a footer toast. Requires login —
608
- * saved reports are a logged-in feature. */
609
- const runExport = (scope, format, currentLicenseIdx) => {
610
- if (!isLoggedIn()) {
611
- showExportMsg("Export requires `dg login` (free account)");
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)
612
645
  return;
613
- }
614
- try {
615
- const ts = new Date().toISOString().replace(/[:T]/g, "-").replace(/\..*$/, "");
616
- const payload = buildExportPayload(scope, currentLicenseIdx);
617
- if (payload === null) {
618
- showExportMsg(`Nothing to export for scope "${scope}"`);
619
- return;
620
- }
621
- const { body, ext } = formatExport(payload, scope, format);
622
- const scopeTag = scope === "current-license" && currentLicenseIdx !== null
623
- ? `${(licenseGroups[currentLicenseIdx]?.spdx ?? "license").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 32)}`
624
- : scope;
625
- const filename = `dg-scan-${ts}-${scopeTag}.${ext}`;
626
- const path = resolvePath(process.cwd(), filename);
627
- writeFileSync(path, body, "utf-8");
628
- showExportMsg(`✓ Exported to ${filename}`);
629
- }
630
- catch (e) {
631
- showExportMsg(`Export failed: ${e.message}`);
632
- }
646
+ setExportMenu(null);
647
+ if ("path" in result)
648
+ showExportMsg(`✓ Exported to ${result.path}`);
649
+ else
650
+ showExportMsg(`Export failed: ${result.error}`, "error");
633
651
  };
634
652
  const licenseGroups = useMemo(() => {
635
653
  const buckets = new Map();
@@ -662,11 +680,10 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
662
680
  const searchModeRef = useRef(searchMode);
663
681
  searchModeRef.current = searchMode;
664
682
  const { rows: termRows, cols: termCols } = useTerminalSize();
665
- const compact = termRows < COMPACT_ROWS;
683
+ const subviewCompact = termRows < COMPACT_ROWS;
666
684
  const ackSectionLines = acked.length > 0
667
685
  ? 2 + Math.min(ackGroups.length, ACK_PREVIEW_LIMIT) + (ackGroups.length > ACK_PREVIEW_LIMIT ? 1 : 0)
668
686
  : 0;
669
- const availableRows = Math.max(5, termRows - (compact ? FIXED_CHROME_COMPACT : FIXED_CHROME) - ackSectionLines);
670
687
  const innerWidth = Math.max(40, termCols - 6);
671
688
  const detailGroup = useMemo(() => {
672
689
  if (!detailPane)
@@ -698,6 +715,17 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
698
715
  return 1 + animVisibleLines;
699
716
  return groupRowHeight(group, level);
700
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
+ });
701
729
  const visibleEnd = useMemo(() => {
702
730
  let consumed = 0;
703
731
  let end = view.viewport;
@@ -744,7 +772,6 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
744
772
  }
745
773
  return newStart;
746
774
  };
747
- // Re-clamp viewport when terminal is resized
748
775
  useEffect(() => {
749
776
  if (groups.length === 0)
750
777
  return;
@@ -753,7 +780,6 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
753
780
  const newVp = adjustViewport(cursor, expandedKey, expandLevel, clamped);
754
781
  dispatchView({ type: "MOVE", cursor, viewport: newVp });
755
782
  }, [availableRows]);
756
- // Clamp detail pane scroll when terminal resizes
757
783
  useEffect(() => {
758
784
  const dp = detailPaneRef.current;
759
785
  if (dp && detailLines.length > 0) {
@@ -769,10 +795,8 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
769
795
  }
770
796
  }, [detailPane, detailGroup]);
771
797
  useInput((input, key) => {
772
- // Export menu has the highest priority — once opened, all keys go here
773
- // until ⏎/Esc dismisses it. Arrow-driven: ↑↓ scrolls within the active
774
- // row, ←→ or Tab switches rows. No letter shortcuts (the user found
775
- // them clunky and they collide with `q` / other meaningful keys).
798
+ if (exportDialogRef.current)
799
+ return;
776
800
  if (exportMenuRef.current) {
777
801
  const menu = exportMenuRef.current;
778
802
  const SCOPES = ["all", "summary", "packages", "licenses", "findings"];
@@ -793,9 +817,14 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
793
817
  }
794
818
  if (key.return) {
795
819
  const m = exportMenuRef.current;
796
- setExportMenu(null);
820
+ const option = buildExportOption(m.scope, m.format, licenseDetailIdxRef.current);
797
821
  clearScreen();
798
- runExport(m.scope, m.format, licenseDetailIdxRef.current);
822
+ if (!option) {
823
+ setExportMenu(null);
824
+ showExportMsg(`Nothing to export for scope "${m.scope}"`);
825
+ return;
826
+ }
827
+ setExportDialog(option);
799
828
  return;
800
829
  }
801
830
  if (key.tab || key.leftArrow || key.rightArrow) {
@@ -834,9 +863,7 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
834
863
  return;
835
864
  }
836
865
  if (showLicensesRef.current) {
837
- // Drill-in mode: viewing the package list for one license.
838
866
  if (licenseDetailIdxRef.current !== null) {
839
- // Search-typing mode inside drill-in: capture text input.
840
867
  if (licenseSearchModeRef.current) {
841
868
  if (key.escape) {
842
869
  setLicenseSearchMode(false);
@@ -1060,32 +1087,23 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
1060
1087
  const visibleGroups = groups.slice(view.viewport, visibleEnd);
1061
1088
  const aboveCount = view.viewport;
1062
1089
  const belowCount = groups.length - visibleEnd;
1063
- const lcCol = 16; // fixed width for license column
1090
+ const lcCol = 16;
1064
1091
  const nameCol = Math.max(20, innerWidth - BADGE_COL - 16 - lcCol);
1065
1092
  // Clamp cursor to valid range (groups may shrink via search filter)
1066
1093
  const clampedCursor = groups.length > 0 ? Math.min(view.cursor, groups.length - 1) : 0;
1067
- // ── Export menu overlay ──
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
+ }
1068
1097
  if (exportMenu) {
1069
- const SCOPES_RENDER = [
1070
- { value: "all", label: "All" },
1071
- { value: "summary", label: "Summary" },
1072
- { value: "packages", label: "Packages" },
1073
- { value: "licenses", label: "Licenses" },
1074
- { value: "findings", label: "Findings (warn+block)" },
1075
- ];
1098
+ const SCOPES_RENDER = ["all", "summary", "packages", "licenses", "findings"].map((value) => ({ value, label: EXPORT_SCOPE_LABELS[value] }));
1076
1099
  if (licenseDetailIdxRef.current !== null) {
1077
1100
  const focus = licenseGroups[licenseDetailIdxRef.current];
1078
1101
  SCOPES_RENDER.push({
1079
1102
  value: "current-license",
1080
- label: `Current license (${focus?.spdx ?? "—"})`,
1103
+ label: `${EXPORT_SCOPE_LABELS["current-license"]} (${focus?.spdx ?? "—"})`,
1081
1104
  });
1082
1105
  }
1083
- const FORMATS_RENDER = [
1084
- { value: "json", label: "JSON" },
1085
- { value: "csv", label: "CSV" },
1086
- { value: "md", label: "Markdown" },
1087
- { value: "txt", label: "Plain text" },
1088
- ];
1106
+ const FORMATS_RENDER = ["json", "csv", "md", "txt"].map((value) => ({ value, label: EXPORT_FORMAT_LABELS[value] }));
1089
1107
  const stackedLayout = termCols < 80;
1090
1108
  const colWidth = stackedLayout ? undefined : Math.max(24, Math.floor((termCols - 8) / 2));
1091
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) => {
@@ -1096,14 +1114,12 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
1096
1114
  const text = selected ? chalk.bold(r.label) : r.label;
1097
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));
1098
1116
  })] }));
1099
- 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, 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")] })] }));
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")] })] }));
1100
1118
  }
1101
- // ── Help overlay ──
1102
1119
  if (showHelp) {
1103
1120
  const isDetail = detailPane !== null;
1104
- 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"] })] })] }));
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"] })] })] }));
1105
1122
  }
1106
- // ── License overlay ──
1107
1123
  if (showLicenses) {
1108
1124
  const lcColor = (risk) => {
1109
1125
  if (risk === "permissive")
@@ -1132,35 +1148,34 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
1132
1148
  const _above = top;
1133
1149
  const _below = filtered.length - bottom;
1134
1150
  const _slice = filtered.slice(top, bottom);
1135
- 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: [_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) => {
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) => {
1136
1152
  const absIdx = top + i;
1137
1153
  const isSelected = absIdx === cursorIdx;
1138
1154
  const nameCol = Math.max(28, Math.floor(termCols * 0.55));
1139
1155
  if (isSelected) {
1140
- 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}`));
1141
1157
  }
1142
- return (_jsxs(Text, { children: [" ", pad(truncate(p.name, nameCol - 2), nameCol), chalk.dim(pad(p.version, 16))] }, `${p.name}@${p.version}`));
1143
- }), _below > 0 && _jsx(Text, { dimColor: true, children: ` ↓ ${_below} more below` })] }), _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("/"), " ", chalk.dim("search"), " ", chalk.bold.cyan("e"), " ", chalk.dim("export"), " ", chalk.bold.cyan("Esc"), " ", chalk.dim("back"), " ", chalk.bold.cyan("q"), " ", chalk.dim("quit"), exportMsg && _jsxs(_Fragment, { children: [" ", chalk.green(exportMsg)] })] })] }));
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] })] })] }));
1144
1164
  }
1145
- // ── License list (default overlay view) ──
1146
1165
  // Column widths: pick from terminal so long SPDX strings (e.g.
1147
1166
  // "(BSD-2-Clause OR MIT OR Apache-2.0)") don't blow the layout.
1148
- const innerCols = Math.max(60, termCols - 6);
1167
+ const innerCols = Math.max(20, termCols - 6);
1149
1168
  const spdxCol = Math.min(40, Math.max(22, Math.floor(innerCols * 0.35)));
1150
1169
  const riskCol = 18;
1151
1170
  const countCol = 7;
1152
1171
  const barCol = Math.max(8, innerCols - spdxCol - riskCol - countCol - 4);
1153
1172
  const visibleRows = Math.max(5, termRows - 20);
1154
1173
  const cursor = Math.min(licenseCursor, licenseGroups.length - 1);
1155
- // Scrolling viewport: keep the cursor visible inside a window of
1156
- // `visibleRows` rows, clamped at the start/end. Replaces the prior
1157
- // "resize terminal to see all" dead end — ↑↓ now genuinely scrolls
1158
- // through the full list on any terminal height.
1159
1174
  const top = Math.max(0, Math.min(cursor - Math.floor((visibleRows - 1) / 2), licenseGroups.length - visibleRows));
1160
1175
  const bottom = Math.min(top + visibleRows, licenseGroups.length);
1161
1176
  const hiddenAbove = top;
1162
1177
  const hiddenBelow = licenseGroups.length - bottom;
1163
- 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("─".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) => {
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) => {
1164
1179
  const absIdx = top + i;
1165
1180
  const color = lcColor(g.risk);
1166
1181
  const barLen = Math.max(1, Math.round((g.count / maxCount) * (barCol - 2)));
@@ -1169,12 +1184,11 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
1169
1184
  const spdxText = truncate(g.spdx, spdxCol - 1);
1170
1185
  const riskText = g.risk;
1171
1186
  if (isSelected) {
1172
- 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}`));
1173
1188
  }
1174
- 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}`));
1175
- }), 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"), exportMsg && _jsxs(_Fragment, { children: [" ", chalk.green(exportMsg)] })] })] }));
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] })] })] }));
1176
1191
  }
1177
- // ── Detail pane mode ──
1178
1192
  if (detailPane !== null) {
1179
1193
  const dpGroup = detailGroup;
1180
1194
  if (dpGroup) {
@@ -1184,11 +1198,10 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
1184
1198
  const dpAbove = dpScroll;
1185
1199
  const dpBelow = Math.max(0, detailLines.length - dpScroll - detailContentRows);
1186
1200
  const dpVisible = detailLines.slice(dpScroll, dpScroll + detailContentRows);
1187
- 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")] })] }));
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")] })] }));
1188
1202
  }
1189
1203
  }
1190
- // ── List mode ──
1191
- 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) => {
1192
1205
  const globalIdx = view.viewport + visIdx;
1193
1206
  const isCursor = globalIdx === clampedCursor;
1194
1207
  const level = getLevel(globalIdx);
@@ -1204,13 +1217,17 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
1204
1217
  : chalk.yellow;
1205
1218
  const arrow = level === "summary" ? "\u25BE" : "\u25B8"; // ▾ expanded, ▸ collapsed
1206
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));
1207
- }), 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: [_jsxs(Text, { bold: true, dimColor: true, children: ["Acknowledged (", acked.length, ") \\u00b7 dg.json"] }), ackGroups.slice(0, ACK_PREVIEW_LIMIT).map((group) => {
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) => {
1208
1221
  const rep = firstPackage(group);
1209
1222
  const ack = ackByKey.get(packageKey(rep.name, rep.version));
1210
1223
  const who = ack?.by ?? "unknown";
1211
1224
  const when = ack?.at ? ` on ${ack.at.slice(0, 10)}` : "";
1212
1225
  return (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [" ", chalk.yellow("\u25b8"), " ", groupNames(group), " ", chalk.dim(`accepted by ${who}${when}`)] }, `ack-${group.key}`));
1213
- }), 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))) }), 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: [" ", chalk.green(exportMsg)] })) : 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")] }))] }));
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")] }))] }));
1214
1231
  };
1215
1232
  const T = {
1216
1233
  branch: chalk.dim("\u251C\u2500\u2500"),
@@ -1246,7 +1263,6 @@ const FindingsSummary = ({ group, maxWidth, maxLines }) => {
1246
1263
  .sort((a, b) => b.severity - a.severity);
1247
1264
  const hasAffects = group.packages.length > 3;
1248
1265
  const allLines = [];
1249
- // License info
1250
1266
  const lcLine = licenseLine(rep);
1251
1267
  if (lcLine)
1252
1268
  allLines.push(lcLine);
@@ -1260,11 +1276,9 @@ const FindingsSummary = ({ group, maxWidth, maxLines }) => {
1260
1276
  // Team: { category, severity, title, evidence } — show everything
1261
1277
  const isFree = visibleFindings.length > 0 && !visibleFindings[0]?.title;
1262
1278
  if (isFree) {
1263
- // Free tier: don't show raw category IDs — just the upgrade prompt
1264
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"));
1265
1280
  }
1266
1281
  else {
1267
- // Paid tier: show findings with category + title
1268
1282
  for (let idx = 0; idx < visibleFindings.length; idx++) {
1269
1283
  const f = visibleFindings[idx];
1270
1284
  if (!f)
@@ -1278,7 +1292,6 @@ const FindingsSummary = ({ group, maxWidth, maxLines }) => {
1278
1292
  }
1279
1293
  }
1280
1294
  if (visibleFindings.length === 0 && rep.score > 0) {
1281
- // No findings at all (shouldn't happen after API change, but safety fallback)
1282
1295
  allLines.push(_jsxs(Text, { dimColor: true, children: [hasAffects ? T.branch : T.last, " Score: ", rep.score, "/100"] }, "score-only"));
1283
1296
  }
1284
1297
  if (visibleFindings.length === 0 && rep.score === 0) {