@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.
- 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 +1 -1
- 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 +15 -7
- package/dist/commands/cooldown.js +389 -0
- package/dist/commands/decisions.js +23 -9
- 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 +6 -4
- package/dist/commands/sbom.js +206 -0
- package/dist/commands/scan.js +1 -1
- package/dist/commands/service.js +32 -13
- package/dist/commands/setup.js +197 -27
- 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 +14 -44
- package/dist/decisions/remember-prompt.js +5 -10
- package/dist/export-ui/ExportDialog.js +198 -0
- package/dist/install-ui/LiveInstall.js +2 -2
- package/dist/install-ui/block-render.js +1 -1
- package/dist/install-ui/prep-spinner.js +32 -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 +51 -14
- package/dist/launcher/live-install.js +25 -5
- package/dist/launcher/output-redaction.js +7 -4
- package/dist/launcher/preflight-prompt.js +14 -1
- package/dist/launcher/run.js +89 -86
- package/dist/launcher/spawn-invocation.js +21 -0
- package/dist/policy/cooldown.js +18 -5
- package/dist/policy/evaluate.js +5 -6
- 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/theme.js +7 -7
- package/dist/project/dgfile.js +145 -6
- 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 +55 -16
- package/dist/proxy/metadata-map.js +41 -3
- package/dist/proxy/preverified.js +55 -0
- package/dist/proxy/server.js +448 -49
- 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 +17 -10
- package/dist/scan/discovery.js +11 -2
- package/dist/scan/render.js +28 -4
- package/dist/scan/scanner-report.js +15 -9
- package/dist/scan/staged.js +2 -1
- package/dist/scan-ui/LegacyApp.js +9 -13
- package/dist/scan-ui/alt-screen.js +5 -8
- package/dist/scan-ui/components/InteractiveResultsView.js +133 -120
- 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 +16 -2
- package/dist/scan-ui/launch.js +7 -4
- package/dist/scan-ui/shims.js +11 -4
- package/dist/scripts/detect.js +7 -2
- package/dist/scripts/gate.js +1 -1
- 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 +73 -39
- 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/report-writer.js +57 -0
- package/dist/verify/local.js +240 -42
- package/dist/verify/package-check.js +84 -28
- package/dist/verify/preflight.js +242 -49
- package/dist/verify/render.js +15 -1
- package/npm-shrinkwrap.json +2383 -0
- package/package.json +6 -8
- package/NOTICE +0 -5
- package/dist/commands/completion.js +0 -117
- package/dist/commands/explain.js +0 -236
- package/dist/commands/unavailable.js +0 -11
- package/dist/scripts/rebuild.js +0 -28
- 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 {
|
|
6
|
-
import {
|
|
7
|
-
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";
|
|
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
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
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,
|
|
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
|
-
:
|
|
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 = (
|
|
315
|
-
setExportMsg(
|
|
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.
|
|
330
|
-
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
|
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
|
-
|
|
773
|
-
|
|
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
|
-
|
|
820
|
+
const option = buildExportOption(m.scope, m.format, licenseDetailIdxRef.current);
|
|
797
821
|
clearScreen();
|
|
798
|
-
|
|
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;
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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))) }),
|
|
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(
|
|
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:
|
|
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"),
|
|
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:
|
|
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
|
-
|
|
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: [
|
|
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))) }),
|
|
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) {
|