@westbayberry/dg 1.0.52 → 1.0.56

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 (64) hide show
  1. package/README.md +5 -1
  2. package/dist/index.mjs +349 -168
  3. package/dist/packages/cli/src/alt-screen.js +36 -0
  4. package/dist/packages/cli/src/api.js +322 -0
  5. package/dist/packages/cli/src/auth.js +218 -0
  6. package/dist/packages/cli/src/bin.js +386 -0
  7. package/dist/packages/cli/src/config.js +228 -0
  8. package/dist/packages/cli/src/discover.js +126 -0
  9. package/dist/packages/cli/src/first-run.js +135 -0
  10. package/dist/packages/cli/src/hook.js +360 -0
  11. package/dist/packages/cli/src/lockfile.js +303 -0
  12. package/dist/packages/cli/src/npm-wrapper.js +218 -0
  13. package/dist/packages/cli/src/pip-wrapper.js +273 -0
  14. package/dist/packages/cli/src/sanitize.js +38 -0
  15. package/dist/packages/cli/src/scan-core.js +144 -0
  16. package/dist/packages/cli/src/setup-status.js +46 -0
  17. package/dist/packages/cli/src/static-output.js +625 -0
  18. package/dist/packages/cli/src/telemetry.js +141 -0
  19. package/dist/packages/cli/src/ui/App.js +137 -0
  20. package/dist/packages/cli/src/ui/InitApp.js +391 -0
  21. package/dist/packages/cli/src/ui/LoginApp.js +51 -0
  22. package/dist/packages/cli/src/ui/NpmWrapperApp.js +73 -0
  23. package/dist/packages/cli/src/ui/PipWrapperApp.js +72 -0
  24. package/dist/packages/cli/src/ui/components/ConfirmPrompt.js +24 -0
  25. package/dist/packages/cli/src/ui/components/DemoScanAnimation.js +26 -0
  26. package/dist/packages/cli/src/ui/components/DurationLine.js +7 -0
  27. package/dist/packages/cli/src/ui/components/ErrorView.js +30 -0
  28. package/dist/packages/cli/src/ui/components/FileSavePrompt.js +210 -0
  29. package/dist/packages/cli/src/ui/components/InteractiveResultsView.js +557 -0
  30. package/dist/packages/cli/src/ui/components/Mascot.js +33 -0
  31. package/dist/packages/cli/src/ui/components/ProgressBar.js +51 -0
  32. package/dist/packages/cli/src/ui/components/ProgressDots.js +35 -0
  33. package/dist/packages/cli/src/ui/components/ProjectSelector.js +60 -0
  34. package/dist/packages/cli/src/ui/components/ResultsView.js +105 -0
  35. package/dist/packages/cli/src/ui/components/ScanResultCard.js +54 -0
  36. package/dist/packages/cli/src/ui/components/ScoreHeader.js +142 -0
  37. package/dist/packages/cli/src/ui/components/SetupBanner.js +17 -0
  38. package/dist/packages/cli/src/ui/components/Spinner.js +11 -0
  39. package/dist/packages/cli/src/ui/hooks/useExpandAnimation.js +44 -0
  40. package/dist/packages/cli/src/ui/hooks/useInit.js +341 -0
  41. package/dist/packages/cli/src/ui/hooks/useLogin.js +121 -0
  42. package/dist/packages/cli/src/ui/hooks/useNpmWrapper.js +192 -0
  43. package/dist/packages/cli/src/ui/hooks/usePipWrapper.js +195 -0
  44. package/dist/packages/cli/src/ui/hooks/useScan.js +202 -0
  45. package/dist/packages/cli/src/ui/hooks/useTerminalSize.js +29 -0
  46. package/dist/packages/cli/src/update-check.js +152 -0
  47. package/dist/packages/cli/src/wizard-demo-data.js +63 -0
  48. package/dist/src/ecosystem.js +2 -0
  49. package/dist/src/lockfile/diff.js +38 -0
  50. package/dist/src/lockfile/parse_package_json.js +41 -0
  51. package/dist/src/lockfile/parse_package_lock.js +55 -0
  52. package/dist/src/lockfile/parse_pipfile_lock.js +69 -0
  53. package/dist/src/lockfile/parse_pnpm_lock.js +62 -0
  54. package/dist/src/lockfile/parse_poetry_lock.js +71 -0
  55. package/dist/src/lockfile/parse_requirements.js +83 -0
  56. package/dist/src/lockfile/parse_yarn_lock.js +66 -0
  57. package/dist/src/logger.js +21 -0
  58. package/dist/src/npm/h2pool.js +161 -0
  59. package/dist/src/npm/registry.js +299 -0
  60. package/dist/src/npm/tarball.js +274 -0
  61. package/dist/src/pypi/registry.js +299 -0
  62. package/dist/src/pypi/tarball.js +361 -0
  63. package/dist/src/types.js +2 -0
  64. package/package.json +6 -3
@@ -0,0 +1,557 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.InteractiveResultsView = void 0;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const react_1 = require("react");
9
+ const ink_1 = require("ink");
10
+ const chalk_1 = __importDefault(require("chalk"));
11
+ const ScoreHeader_1 = require("./ScoreHeader");
12
+ const useExpandAnimation_1 = require("../hooks/useExpandAnimation");
13
+ const useTerminalSize_1 = require("../hooks/useTerminalSize");
14
+ function groupPackages(packages) {
15
+ const map = new Map();
16
+ for (const pkg of packages) {
17
+ const fingerprint = pkg.findings.length === 0
18
+ ? `__clean_${pkg.score}`
19
+ : pkg.findings
20
+ .map((f) => `${f.category ?? ""}:${f.severity}`)
21
+ .sort()
22
+ .join("|") + `|score:${pkg.score}`;
23
+ const group = map.get(fingerprint) ?? [];
24
+ group.push(pkg);
25
+ map.set(fingerprint, group);
26
+ }
27
+ return [...map.entries()]
28
+ .map(([fingerprint, pkgs]) => ({ packages: pkgs, key: fingerprint }))
29
+ .sort((a, b) => b.packages[0].score - a.packages[0].score);
30
+ }
31
+ const SEVERITY_LABELS = {
32
+ 5: "CRIT",
33
+ 4: "HIGH",
34
+ 3: "MED",
35
+ 2: "LOW",
36
+ 1: "INFO",
37
+ };
38
+ const SEVERITY_COLORS = {
39
+ 5: (s) => chalk_1.default.red.bold(s),
40
+ 4: (s) => chalk_1.default.red(s),
41
+ 3: (s) => chalk_1.default.yellow(s),
42
+ 2: (s) => chalk_1.default.cyan(s),
43
+ 1: (s) => chalk_1.default.gray(s),
44
+ };
45
+ function actionBadge(score) {
46
+ if (score >= 70)
47
+ return { label: "Block", color: chalk_1.default.red };
48
+ if (score >= 60)
49
+ return { label: "Warn", color: chalk_1.default.yellow };
50
+ return { label: "Pass", color: chalk_1.default.green };
51
+ }
52
+ function truncate(s, max) {
53
+ return s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
54
+ }
55
+ function pad(s, len) {
56
+ return s + " ".repeat(Math.max(0, len - s.length));
57
+ }
58
+ const EVIDENCE_LIMIT = 2;
59
+ // Fixed lines outside the scrollable group area:
60
+ // 5 ScoreHeader box | 2 Flagged box top | 2 scroll indicators
61
+ // 1 Flagged box bottom | 4 Clean/Duration box | 1 help bar | 1 margin
62
+ const FIXED_CHROME = 21;
63
+ function findingsSummaryHeight(group) {
64
+ const rep = group.packages[0];
65
+ const visibleFindings = rep.findings.filter((f) => f.severity > 1);
66
+ const isFree = visibleFindings.length > 0 && !visibleFindings[0].title;
67
+ let h = 0;
68
+ if (rep.license)
69
+ h += 1; // license info line
70
+ if (isFree) {
71
+ // Free tier: just the upgrade prompt line
72
+ h += 1;
73
+ }
74
+ else if (visibleFindings.length > 0) {
75
+ // Paid tier: one line per finding
76
+ h += visibleFindings.length;
77
+ }
78
+ else if (rep.score > 0) {
79
+ h += 1; // score-only fallback
80
+ }
81
+ if (group.packages.length > 3)
82
+ h += 1;
83
+ return h;
84
+ }
85
+ function findingsDetailHeight(group, safeVersions) {
86
+ const rep = group.packages[0];
87
+ const visibleFindings = rep.findings
88
+ .filter((f) => f.severity > 1)
89
+ .sort((a, b) => b.severity - a.severity);
90
+ let h = 0;
91
+ if (rep.license)
92
+ h += 1; // license info line
93
+ if (group.packages.length > 3)
94
+ h += 1;
95
+ // Free tier: reasons + upgrade hint
96
+ if (visibleFindings.length === 0 && rep.score > 0) {
97
+ h += (rep.reasons ?? []).length;
98
+ h += 1; // upgrade hint
99
+ }
100
+ const hasEvidence = visibleFindings.some((f) => f.evidence && f.evidence.length > 0);
101
+ for (const finding of visibleFindings) {
102
+ h += 1; // badge + id
103
+ h += 1; // title
104
+ const evidence = finding.evidence ?? [];
105
+ h += Math.min(evidence.length, EVIDENCE_LIMIT);
106
+ if (evidence.length > EVIDENCE_LIMIT)
107
+ h += 1;
108
+ }
109
+ // Upgrade hint for pro tier (findings but no evidence)
110
+ if (visibleFindings.length > 0 && !hasEvidence)
111
+ h += 1;
112
+ if (rep.recommendation)
113
+ h += 1;
114
+ if (safeVersions[rep.name])
115
+ h += 1;
116
+ return h;
117
+ }
118
+ function groupRowHeight(group, level, safeVersions) {
119
+ if (level === null)
120
+ return 1;
121
+ if (level === "summary")
122
+ return 1 + findingsSummaryHeight(group);
123
+ return 1 + findingsDetailHeight(group, safeVersions);
124
+ }
125
+ function groupNames(group) {
126
+ if (group.packages.length === 1)
127
+ return group.packages[0].name;
128
+ if (group.packages.length <= 3)
129
+ return group.packages.map((p) => p.name).join(", ");
130
+ return `${group.packages[0].name} + ${group.packages.length - 1} similar`;
131
+ }
132
+ function affectsLine(group) {
133
+ const names = group.packages.map((p) => p.name);
134
+ if (names.length <= 5)
135
+ return names.join(", ");
136
+ return names.slice(0, 5).join(", ") + ` + ${names.length - 5} more`;
137
+ }
138
+ // Chrome lines in detail pane mode:
139
+ // 7 ScoreHeader | 3 detail pane borders+header | 2 scroll indicators
140
+ // 4 Clean/Duration box | 1 separator | 1 help bar
141
+ const DETAIL_PANE_CHROME = 20;
142
+ function buildDetailLines(group, safeVersion, maxWidth) {
143
+ const rep = group.packages[0];
144
+ const visibleFindings = rep.findings
145
+ .filter((f) => f.severity > 1)
146
+ .sort((a, b) => b.severity - a.severity);
147
+ const lines = [];
148
+ if (group.packages.length > 3) {
149
+ lines.push((0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: ["Affects: ", affectsLine(group)] }, "affects"));
150
+ lines.push((0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }, "affects-gap"));
151
+ }
152
+ if (rep.score > 0) {
153
+ lines.push((0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: ["Score: ", rep.score, "/100"] }, "score-info"));
154
+ lines.push((0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }, "score-gap"));
155
+ }
156
+ // Paid tier: show findings with category + severity
157
+ if (visibleFindings.length > 0) {
158
+ for (let i = 0; i < visibleFindings.length; i++) {
159
+ const f = visibleFindings[i];
160
+ const sevLabel = SEVERITY_LABELS[f.severity] ?? "INFO";
161
+ const sevColor = SEVERITY_COLORS[f.severity] ?? SEVERITY_COLORS[1];
162
+ const connector = i === visibleFindings.length - 1 ? T.last : T.branch;
163
+ lines.push((0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [connector, " ", sevColor(pad(sevLabel, 8)), chalk_1.default.dim(f.category ?? "")] }, `finding-${i}`));
164
+ }
165
+ lines.push((0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }, "findings-gap"));
166
+ }
167
+ else if (rep.score > 0) {
168
+ // Free tier: no findings returned — show upgrade prompt
169
+ lines.push((0, jsx_runtime_1.jsxs)(ink_1.Text, { color: "yellow", children: [chalk_1.default.yellow("\u2192"), " Upgrade to Pro to see risk categories"] }, "upgrade"));
170
+ lines.push((0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }, "upgrade-gap"));
171
+ }
172
+ if (rep.recommendation) {
173
+ lines.push((0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [chalk_1.default.dim("Recommendation:"), " ", chalk_1.default.cyan(truncate(rep.recommendation, maxWidth - 18))] }, "recommendation"));
174
+ }
175
+ if (safeVersion) {
176
+ lines.push((0, jsx_runtime_1.jsx)(ink_1.Text, { children: chalk_1.default.green(`Safe version: ${rep.name}@${safeVersion}`) }, "safe"));
177
+ }
178
+ return lines;
179
+ }
180
+ function viewReducer(_state, action) {
181
+ switch (action.type) {
182
+ case "MOVE":
183
+ return { ..._state, cursor: action.cursor, viewport: action.viewport };
184
+ case "EXPAND":
185
+ return { ..._state, expandedIndex: action.expandedIndex, expandLevel: action.expandLevel, viewport: action.viewport };
186
+ case "MOVE_EXPAND":
187
+ return { cursor: action.cursor, expandedIndex: action.expandedIndex, expandLevel: action.expandLevel, viewport: action.viewport };
188
+ }
189
+ }
190
+ const InteractiveResultsView = ({ result, config: _config, durationMs, onExit, onBack, discoveredTotal, userStatus, scanUsage: scanUsageProp, }) => {
191
+ // If the scan returned trialScansRemaining (free tier), show it as the
192
+ // scan usage — overrides the generic "free tier" placeholder from bin.ts
193
+ const scanUsage = result.trialScansRemaining !== undefined
194
+ ? `${result.trialScansRemaining} scans left`
195
+ : scanUsageProp;
196
+ const flagged = (0, react_1.useMemo)(() => result.packages.filter((p) => p.score > 0), [result.packages]);
197
+ const clean = (0, react_1.useMemo)(() => result.packages.filter((p) => p.score === 0), [result.packages]);
198
+ const total = result.packages.length;
199
+ const [searchQuery, setSearchQuery] = (0, react_1.useState)("");
200
+ const allGroups = (0, react_1.useMemo)(() => groupPackages(flagged), [flagged]);
201
+ const allGroupCount = allGroups.length;
202
+ const groups = (0, react_1.useMemo)(() => {
203
+ if (!searchQuery)
204
+ return allGroups;
205
+ const q = searchQuery.toLowerCase();
206
+ return allGroups.filter(g => g.packages.some(p => p.name.toLowerCase().includes(q)));
207
+ }, [allGroups, searchQuery]);
208
+ const severityCounts = (0, react_1.useMemo)(() => {
209
+ const counts = {};
210
+ for (const pkg of flagged) {
211
+ const maxSev = pkg.findings.reduce((m, f) => Math.max(m, f.severity), 0);
212
+ if (maxSev >= 2)
213
+ counts[maxSev] = (counts[maxSev] ?? 0) + 1;
214
+ }
215
+ return counts;
216
+ }, [flagged]);
217
+ const [view, dispatchView] = (0, react_1.useReducer)(viewReducer, {
218
+ cursor: 0,
219
+ expandLevel: null,
220
+ expandedIndex: null,
221
+ viewport: 0,
222
+ });
223
+ const viewRef = (0, react_1.useRef)(view);
224
+ viewRef.current = view;
225
+ const [detailPane, setDetailPane] = (0, react_1.useState)(null);
226
+ const detailPaneRef = (0, react_1.useRef)(detailPane);
227
+ detailPaneRef.current = detailPane;
228
+ const [showHelp, setShowHelp] = (0, react_1.useState)(false);
229
+ const showHelpRef = (0, react_1.useRef)(showHelp);
230
+ showHelpRef.current = showHelp;
231
+ const [searchMode, setSearchMode] = (0, react_1.useState)(false);
232
+ const searchModeRef = (0, react_1.useRef)(searchMode);
233
+ searchModeRef.current = searchMode;
234
+ const { rows: termRows, cols: termCols } = (0, useTerminalSize_1.useTerminalSize)();
235
+ const availableRows = Math.max(5, termRows - FIXED_CHROME);
236
+ const innerWidth = Math.max(40, termCols - 6);
237
+ const detailGroupIdx = detailPane?.groupIndex ?? -1;
238
+ const detailLines = (0, react_1.useMemo)(() => {
239
+ if (detailGroupIdx < 0)
240
+ return [];
241
+ const group = groups[detailGroupIdx];
242
+ if (!group)
243
+ return [];
244
+ return buildDetailLines(group, result.safeVersions[group.packages[0].name], innerWidth);
245
+ }, [detailGroupIdx, groups, result.safeVersions, innerWidth]);
246
+ const detailContentRows = Math.max(3, termRows - DETAIL_PANE_CHROME);
247
+ const getLevel = (idx) => {
248
+ return view.expandedIndex === idx ? view.expandLevel : null;
249
+ };
250
+ const expandTargetHeight = (0, react_1.useMemo)(() => {
251
+ if (view.expandedIndex === null || view.expandLevel === null)
252
+ return 0;
253
+ const group = groups[view.expandedIndex];
254
+ if (!group)
255
+ return 0;
256
+ if (view.expandLevel === "summary")
257
+ return findingsSummaryHeight(group);
258
+ return findingsDetailHeight(group, result.safeVersions);
259
+ }, [view.expandedIndex, view.expandLevel, groups, result.safeVersions]);
260
+ const { visibleLines: animVisibleLines } = (0, useExpandAnimation_1.useExpandAnimation)(expandTargetHeight, view.expandedIndex !== null);
261
+ const animatedGroupHeight = (group, level, idx) => {
262
+ if (level === null)
263
+ return 1;
264
+ if (idx === view.expandedIndex)
265
+ return 1 + animVisibleLines;
266
+ return groupRowHeight(group, level, result.safeVersions);
267
+ };
268
+ const visibleEnd = (0, react_1.useMemo)(() => {
269
+ let consumed = 0;
270
+ let end = view.viewport;
271
+ while (end < groups.length) {
272
+ const level = getLevel(end);
273
+ const h = animatedGroupHeight(groups[end], level, end);
274
+ if (consumed + h > availableRows)
275
+ break;
276
+ consumed += h;
277
+ end++;
278
+ }
279
+ if (end === view.viewport && groups.length > 0)
280
+ end = view.viewport + 1;
281
+ return end;
282
+ }, [view.viewport, groups, view.expandedIndex, view.expandLevel, animVisibleLines, availableRows, result.safeVersions]);
283
+ const adjustViewport = (cursor, expIdx, expLvl, currentStart) => {
284
+ if (cursor < currentStart)
285
+ return cursor;
286
+ const getLvl = (i) => (expIdx === i ? expLvl : null);
287
+ let consumed = 0;
288
+ for (let i = currentStart; i <= cursor && i < groups.length; i++) {
289
+ consumed += groupRowHeight(groups[i], getLvl(i), result.safeVersions);
290
+ }
291
+ if (consumed <= availableRows)
292
+ return currentStart;
293
+ let newStart = currentStart;
294
+ while (newStart < cursor) {
295
+ newStart++;
296
+ consumed = 0;
297
+ for (let i = newStart; i <= cursor; i++) {
298
+ consumed += groupRowHeight(groups[i], getLvl(i), result.safeVersions);
299
+ }
300
+ if (consumed <= availableRows)
301
+ break;
302
+ }
303
+ return newStart;
304
+ };
305
+ // Re-clamp viewport when terminal is resized
306
+ (0, react_1.useEffect)(() => {
307
+ if (groups.length === 0)
308
+ return;
309
+ const { cursor, expandedIndex, expandLevel, viewport } = viewRef.current;
310
+ const clamped = Math.min(viewport, Math.max(0, groups.length - 1));
311
+ const newVp = adjustViewport(cursor, expandedIndex, expandLevel, clamped);
312
+ dispatchView({ type: "MOVE", cursor, viewport: newVp });
313
+ }, [availableRows]);
314
+ // Clamp detail pane scroll when terminal resizes
315
+ (0, react_1.useEffect)(() => {
316
+ const dp = detailPaneRef.current;
317
+ if (dp && detailLines.length > 0) {
318
+ const maxScroll = Math.max(0, detailLines.length - detailContentRows);
319
+ if (dp.scroll > maxScroll) {
320
+ setDetailPane({ groupIndex: dp.groupIndex, scroll: maxScroll });
321
+ }
322
+ }
323
+ }, [detailContentRows, detailLines.length]);
324
+ (0, ink_1.useInput)((input, key) => {
325
+ if (groups.length === 0) {
326
+ if (input === "q" || key.return)
327
+ onExit();
328
+ return;
329
+ }
330
+ // Help overlay
331
+ if (showHelpRef.current) {
332
+ if (input === "?" || key.escape)
333
+ setShowHelp(false);
334
+ else if (input === "q")
335
+ onExit();
336
+ return;
337
+ }
338
+ if (input === "?") {
339
+ setShowHelp(true);
340
+ return;
341
+ }
342
+ // Search mode — capture text input
343
+ if (searchModeRef.current) {
344
+ if (key.escape) {
345
+ setSearchMode(false);
346
+ setSearchQuery("");
347
+ dispatchView({ type: "MOVE", cursor: 0, viewport: 0 });
348
+ }
349
+ else if (key.return) {
350
+ setSearchMode(false);
351
+ }
352
+ else if (key.backspace || key.delete) {
353
+ setSearchQuery(prev => prev.slice(0, -1));
354
+ dispatchView({ type: "MOVE", cursor: 0, viewport: 0 });
355
+ }
356
+ else if (input && !key.upArrow && !key.downArrow && /^[\x20-\x7e]+$/.test(input)) {
357
+ setSearchQuery(prev => prev + input);
358
+ dispatchView({ type: "MOVE", cursor: 0, viewport: 0 });
359
+ }
360
+ return;
361
+ }
362
+ // Detail pane mode — scroll within the pane
363
+ const dp = detailPaneRef.current;
364
+ if (dp !== null) {
365
+ const maxScroll = Math.max(0, detailLines.length - detailContentRows);
366
+ if (key.upArrow || input === "k") {
367
+ setDetailPane({ groupIndex: dp.groupIndex, scroll: Math.max(0, dp.scroll - 1) });
368
+ }
369
+ else if (key.downArrow || input === "j") {
370
+ setDetailPane({ groupIndex: dp.groupIndex, scroll: Math.min(maxScroll, dp.scroll + 1) });
371
+ }
372
+ else if (input === "g") {
373
+ setDetailPane({ groupIndex: dp.groupIndex, scroll: 0 });
374
+ }
375
+ else if (input === "G") {
376
+ setDetailPane({ groupIndex: dp.groupIndex, scroll: maxScroll });
377
+ }
378
+ else if (input === "b" || key.escape) {
379
+ setDetailPane(null);
380
+ }
381
+ else if (input === "q") {
382
+ onExit();
383
+ }
384
+ return;
385
+ }
386
+ // List mode
387
+ if (groups.length === 0) {
388
+ if (input === "q")
389
+ onExit();
390
+ else if (input === "/")
391
+ setSearchMode(true);
392
+ return;
393
+ }
394
+ const { cursor, expandLevel: expLvl, expandedIndex: expIdx, viewport: vpStart } = viewRef.current;
395
+ if (key.upArrow || input === "k") {
396
+ const next = Math.max(0, cursor - 1);
397
+ const newVp = adjustViewport(next, expIdx, expLvl, vpStart < next ? vpStart : next);
398
+ dispatchView({ type: "MOVE", cursor: next, viewport: newVp });
399
+ }
400
+ else if (key.downArrow || input === "j") {
401
+ const next = Math.min(groups.length - 1, cursor + 1);
402
+ const newVp = adjustViewport(next, expIdx, expLvl, vpStart);
403
+ dispatchView({ type: "MOVE", cursor: next, viewport: newVp });
404
+ }
405
+ else if (input === "g") {
406
+ const newVp = adjustViewport(0, expIdx, expLvl, 0);
407
+ dispatchView({ type: "MOVE", cursor: 0, viewport: newVp });
408
+ }
409
+ else if (input === "G") {
410
+ const last = groups.length - 1;
411
+ const newVp = adjustViewport(last, expIdx, expLvl, vpStart);
412
+ dispatchView({ type: "MOVE", cursor: last, viewport: newVp });
413
+ }
414
+ else if (key.pageDown) {
415
+ const next = Math.min(groups.length - 1, cursor + availableRows);
416
+ const newVp = adjustViewport(next, expIdx, expLvl, vpStart);
417
+ dispatchView({ type: "MOVE", cursor: next, viewport: newVp });
418
+ }
419
+ else if (key.pageUp) {
420
+ const next = Math.max(0, cursor - availableRows);
421
+ const newVp = adjustViewport(next, expIdx, expLvl, next);
422
+ dispatchView({ type: "MOVE", cursor: next, viewport: newVp });
423
+ }
424
+ else if (key.return) {
425
+ // Enter: toggle inline expand (findings dropdown under the package row)
426
+ const newLevel = expIdx === cursor && expLvl === "summary" ? null : "summary";
427
+ const newExpIdx = newLevel === null ? null : cursor;
428
+ const newVp = adjustViewport(cursor, newExpIdx, newLevel, vpStart);
429
+ dispatchView({ type: "EXPAND", expandedIndex: newExpIdx, expandLevel: newLevel, viewport: newVp });
430
+ }
431
+ else if (input === "/") {
432
+ setSearchMode(true);
433
+ }
434
+ else if (input === "b" && onBack) {
435
+ onBack();
436
+ }
437
+ else if (input === "q") {
438
+ onExit();
439
+ }
440
+ });
441
+ const visibleGroups = groups.slice(view.viewport, visibleEnd);
442
+ const aboveCount = view.viewport;
443
+ const belowCount = groups.length - visibleEnd;
444
+ const lcCol = 16; // fixed width for license column
445
+ const nameCol = Math.max(20, innerWidth - 22 - lcCol);
446
+ // Clamp cursor to valid range (groups may shrink via search filter)
447
+ const clampedCursor = groups.length > 0 ? Math.min(view.cursor, groups.length - 1) : 0;
448
+ // ── Help overlay ──
449
+ if (showHelp) {
450
+ const isDetail = detailPane !== null;
451
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [(0, jsx_runtime_1.jsx)(ScoreHeader_1.ScoreHeader, { score: result.score, action: result.action, total: total, flagged: flagged.length, clean: clean.length, severityCounts: severityCounts, userStatus: userStatus, scanUsage: scanUsage }), (0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingLeft: 2, paddingRight: 2, width: "100%", children: [(0, jsx_runtime_1.jsxs)(ink_1.Text, { bold: true, children: [chalk_1.default.cyan("\u25C6"), " Keyboard Shortcuts"] }), (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }), (0, jsx_runtime_1.jsx)(ink_1.Text, { bold: true, children: " Navigation" }), (0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [" ", chalk_1.default.cyan("\u2191 k"), " ", chalk_1.default.dim("Move up")] }), (0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [" ", chalk_1.default.cyan("\u2193 j"), " ", chalk_1.default.dim("Move down")] }), (0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [" ", chalk_1.default.cyan("g"), " ", chalk_1.default.dim("Jump to top")] }), (0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [" ", chalk_1.default.cyan("G"), " ", chalk_1.default.dim("Jump to bottom")] }), !isDetail && (0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [" ", chalk_1.default.cyan("PgUp"), " ", chalk_1.default.dim("Page up")] }), !isDetail && (0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [" ", chalk_1.default.cyan("PgDn"), " ", chalk_1.default.dim("Page down")] }), (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }), (0, jsx_runtime_1.jsx)(ink_1.Text, { bold: true, children: " Actions" }), !isDetail && (0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [" ", chalk_1.default.cyan("\u23CE"), " ", chalk_1.default.dim("Expand findings")] }), isDetail && (0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [" ", chalk_1.default.cyan("Esc"), " ", chalk_1.default.dim("Back to list")] }), !isDetail && (0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [" ", chalk_1.default.cyan("/"), " ", chalk_1.default.dim("Search packages")] }), !isDetail && onBack && (0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [" ", chalk_1.default.cyan("b"), " ", chalk_1.default.dim("Back to project selector")] }), (0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [" ", chalk_1.default.cyan("q"), " ", chalk_1.default.dim("Quit")] }), (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }), (0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [" Press ", chalk_1.default.bold.cyan("?"), " or ", chalk_1.default.bold.cyan("Esc"), " to close"] })] })] }));
452
+ }
453
+ // ── Detail pane mode ──
454
+ if (detailPane !== null) {
455
+ const dpGroup = groups[detailPane.groupIndex];
456
+ if (!dpGroup) {
457
+ setDetailPane(null);
458
+ }
459
+ else {
460
+ const dpRep = dpGroup.packages[0];
461
+ const { color: dpColor } = actionBadge(dpRep.score);
462
+ const dpScroll = detailPane.scroll;
463
+ const dpAbove = dpScroll;
464
+ const dpBelow = Math.max(0, detailLines.length - dpScroll - detailContentRows);
465
+ const dpVisible = detailLines.slice(dpScroll, dpScroll + detailContentRows);
466
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [(0, jsx_runtime_1.jsx)(ScoreHeader_1.ScoreHeader, { score: result.score, action: result.action, total: total, flagged: flagged.length, clean: clean.length, severityCounts: severityCounts, userStatus: userStatus, scanUsage: scanUsage }), (0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingLeft: 1, paddingRight: 1, width: "100%", children: [(0, jsx_runtime_1.jsxs)(ink_1.Box, { justifyContent: "space-between", children: [(0, jsx_runtime_1.jsxs)(ink_1.Text, { bold: true, children: [groupNames(dpGroup), dpRep.license ? chalk_1.default.dim(" \u00B7 ") + (dpRep.license.riskCategory === "permissive" ? chalk_1.default.green(dpRep.license.spdx ?? dpRep.license.raw ?? "") : dpRep.license.riskCategory === "no-license" || dpRep.license.riskCategory === "network-copyleft" ? chalk_1.default.red(dpRep.license.spdx ?? dpRep.license.raw ?? "No license") : chalk_1.default.yellow(dpRep.license.spdx ?? dpRep.license.raw ?? "")) : ""] }), (0, jsx_runtime_1.jsx)(ink_1.Text, { children: dpColor(`score ${dpRep.score}`) })] }), dpAbove > 0 && ((0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [chalk_1.default.cyan(" \u2191"), " ", dpAbove, " more above"] })), (0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", marginLeft: 2, children: dpVisible }), dpBelow > 0 && ((0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [chalk_1.default.cyan(" \u2193"), " ", dpBelow, " more below"] }))] }), (0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingLeft: 1, paddingRight: 1, width: "100%", children: [clean.length > 0 && ((0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [chalk_1.default.green("\u2713"), " ", chalk_1.default.green.bold(String(clean.length)), " ", chalk_1.default.dim(`package${clean.length !== 1 ? "s" : ""} passed with score 0`)] })), (0, jsx_runtime_1.jsxs)(ink_1.Box, { justifyContent: "space-between", children: [(0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [(durationMs / 1000).toFixed(1), "s"] }), result.trialScansRemaining !== undefined && ((0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "Free tier \u00B7 dg login for higher scan limits" }))] })] }), (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: chalk_1.default.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), (0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [" ", chalk_1.default.bold.cyan("\u2191\u2193"), " ", chalk_1.default.dim("scroll"), " ", chalk_1.default.bold.cyan("Esc"), " ", chalk_1.default.dim("back"), " ", chalk_1.default.bold.cyan("q"), " ", chalk_1.default.dim("quit")] })] }));
467
+ }
468
+ }
469
+ // ── List mode ──
470
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [(0, jsx_runtime_1.jsx)(ScoreHeader_1.ScoreHeader, { score: result.score, action: result.action, total: total, flagged: flagged.length, clean: clean.length, severityCounts: severityCounts, userStatus: userStatus, scanUsage: scanUsage }), groups.length > 0 && ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingLeft: 1, paddingRight: 1, width: "100%", children: [(0, jsx_runtime_1.jsxs)(ink_1.Box, { justifyContent: "space-between", children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { bold: true, children: "Flagged Packages" }), (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: searchQuery ? `${groups.length} of ${allGroupCount}` : `${clampedCursor + 1}/${groups.length}` })] }), aboveCount > 0 && ((0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [chalk_1.default.cyan(" \u2191"), " ", aboveCount, " more above"] })), visibleGroups.map((group, visIdx) => {
471
+ const globalIdx = view.viewport + visIdx;
472
+ const isCursor = globalIdx === clampedCursor;
473
+ const level = getLevel(globalIdx);
474
+ const rep = group.packages[0];
475
+ const { label, color } = actionBadge(rep.score);
476
+ const names = groupNames(group);
477
+ const scoreStr = String(rep.score);
478
+ const lcInfo = rep.license;
479
+ const lcStr = lcInfo ? truncate(lcInfo.spdx ?? lcInfo.raw ?? "", lcCol - 2) : "";
480
+ const lcColor = !lcInfo ? chalk_1.default.dim
481
+ : lcInfo.riskCategory === "permissive" ? chalk_1.default.green
482
+ : (lcInfo.riskCategory === "no-license" || lcInfo.riskCategory === "unlicensed" || lcInfo.riskCategory === "network-copyleft") ? chalk_1.default.red
483
+ : chalk_1.default.yellow;
484
+ const arrow = level === "summary" ? "\u25BE" : "\u25B8"; // ▾ expanded, ▸ collapsed
485
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [isCursor ? ((0, jsx_runtime_1.jsxs)(ink_1.Text, { backgroundColor: "#1a1a2e", children: [chalk_1.default.cyan("\u258C"), " ", chalk_1.default.cyan(arrow), " ", ` `, color(pad(label, 6)), chalk_1.default.bold(pad(truncate(names, nameCol - 2), nameCol)), lcColor(pad(lcStr, lcCol)), color(scoreStr.padStart(3)), " "] })) : ((0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [` ${chalk_1.default.dim(arrow)} `, color(pad(label, 6)), pad(truncate(names, nameCol - 2), nameCol), lcColor(pad(lcStr, lcCol)), color(scoreStr.padStart(3))] })), level === "summary" && ((0, jsx_runtime_1.jsx)(FindingsSummary, { group: group, maxWidth: innerWidth - 8, maxLines: globalIdx === view.expandedIndex ? animVisibleLines : undefined }))] }, group.key));
486
+ }), belowCount > 0 && ((0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [chalk_1.default.cyan(" \u2193"), " ", belowCount, " more below"] }))] })), (0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingLeft: 1, paddingRight: 1, width: "100%", children: [clean.length > 0 && ((0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [chalk_1.default.green("\u2713"), " ", chalk_1.default.green.bold(String(clean.length)), " ", chalk_1.default.dim(`package${clean.length !== 1 ? "s" : ""} passed with score 0`)] })), discoveredTotal !== undefined && discoveredTotal > total && ((0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: ["Scanned ", total, " of ", discoveredTotal, " packages"] })), (0, jsx_runtime_1.jsxs)(ink_1.Box, { justifyContent: "space-between", children: [(0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [(durationMs / 1000).toFixed(1), "s"] }), result.trialScansRemaining !== undefined && ((0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "Free tier \u00B7 dg login for higher scan limits" }))] })] }), (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: chalk_1.default.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), searchMode ? ((0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [" ", chalk_1.default.bold.cyan("/"), " ", searchQuery, chalk_1.default.cyan("\u2588"), " ", chalk_1.default.dim("Esc clear")] })) : ((0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [" ", groups.length > 0 ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [chalk_1.default.bold.cyan("\u2191\u2193"), " ", chalk_1.default.dim("navigate"), " ", chalk_1.default.bold.cyan("\u23CE"), " ", chalk_1.default.dim("expand"), " ", chalk_1.default.bold.cyan("/"), " ", chalk_1.default.dim("search"), " ", onBack && (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [chalk_1.default.bold.cyan("b"), " ", chalk_1.default.dim("back"), " "] }), chalk_1.default.bold.cyan("q"), " ", chalk_1.default.dim("quit")] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["Press ", chalk_1.default.bold.cyan("q"), " or ", chalk_1.default.bold.cyan("Enter"), " ", chalk_1.default.dim("to exit")] }))] }))] }));
487
+ };
488
+ exports.InteractiveResultsView = InteractiveResultsView;
489
+ const T = {
490
+ branch: chalk_1.default.dim("\u251C\u2500\u2500"),
491
+ last: chalk_1.default.dim("\u2514\u2500\u2500"),
492
+ pipe: chalk_1.default.dim("\u2502"),
493
+ blank: " ",
494
+ };
495
+ const LICENSE_DESCRIPTIONS = {
496
+ "permissive": "Permissive \u2014 free to use, modify, and distribute. Include the copyright notice.",
497
+ "weak-copyleft": "Weak copyleft \u2014 changes to this library must be shared, but your code stays private.",
498
+ "strong-copyleft": "Strong copyleft \u2014 your entire project must be open-sourced under the same license.",
499
+ "network-copyleft": "Network copyleft \u2014 even SaaS/server use requires releasing your source code.",
500
+ "no-license": "No license found \u2014 legally all rights reserved. Use may require permission from the author.",
501
+ "unlicensed": "Explicitly unlicensed \u2014 proprietary software. A commercial agreement is required.",
502
+ "unknown": "Unrecognized license \u2014 have your legal team review before using.",
503
+ "deferred": "License declared in a file \u2014 check the LICENSE file in the package.",
504
+ };
505
+ function licenseLine(rep) {
506
+ const lc = rep.license;
507
+ if (!lc)
508
+ return null;
509
+ const spdx = lc.spdx ?? lc.raw ?? "";
510
+ const desc = LICENSE_DESCRIPTIONS[lc.riskCategory] ?? "";
511
+ const lcColor = lc.riskCategory === "permissive" ? chalk_1.default.green
512
+ : (lc.riskCategory === "no-license" || lc.riskCategory === "unlicensed" || lc.riskCategory === "network-copyleft") ? chalk_1.default.red
513
+ : chalk_1.default.yellow;
514
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [T.branch, " ", lcColor(spdx), " ", chalk_1.default.dim("\u2014"), " ", chalk_1.default.dim(desc)] }, "license-info"));
515
+ }
516
+ const FindingsSummary = ({ group, maxWidth, maxLines }) => {
517
+ const rep = group.packages[0];
518
+ const visibleFindings = rep.findings
519
+ .filter((f) => f.severity > 1)
520
+ .sort((a, b) => b.severity - a.severity);
521
+ const hasAffects = group.packages.length > 3;
522
+ const allLines = [];
523
+ // License info
524
+ const lcLine = licenseLine(rep);
525
+ if (lcLine)
526
+ allLines.push(lcLine);
527
+ // Render findings — API returns tier-gated data:
528
+ // Free: { category, severity } — don't show raw IDs, just upgrade prompt
529
+ // Pro: { category, severity, title } — show category + title
530
+ // Team: { category, severity, title, evidence } — show everything
531
+ const isFree = visibleFindings.length > 0 && !visibleFindings[0].title;
532
+ if (isFree) {
533
+ // Free tier: don't show raw category IDs — just the upgrade prompt
534
+ allLines.push((0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [hasAffects ? T.branch : T.last, " ", chalk_1.default.yellow("\u2192"), " ", chalk_1.default.yellow("Upgrade to Pro"), " for finding details"] }, "upgrade"));
535
+ }
536
+ else {
537
+ // Paid tier: show findings with category + title
538
+ for (let idx = 0; idx < visibleFindings.length; idx++) {
539
+ const f = visibleFindings[idx];
540
+ const isLast = !hasAffects && idx === visibleFindings.length - 1;
541
+ const connector = isLast ? T.last : T.branch;
542
+ const sevLabel = SEVERITY_LABELS[f.severity] ?? "INFO";
543
+ const sevColor = SEVERITY_COLORS[f.severity] ?? SEVERITY_COLORS[1];
544
+ const title = f.title ? `: ${f.title}` : "";
545
+ allLines.push((0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [connector, " ", sevColor(pad(sevLabel, 5)), " ", chalk_1.default.dim(f.category ?? ""), title] }, `finding-${idx}`));
546
+ }
547
+ }
548
+ if (visibleFindings.length === 0 && rep.score > 0) {
549
+ // No findings at all (shouldn't happen after API change, but safety fallback)
550
+ allLines.push((0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [hasAffects ? T.branch : T.last, " Score: ", rep.score, "/100"] }, "score-only"));
551
+ }
552
+ if (hasAffects) {
553
+ allLines.push((0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [T.last, " ", truncate(affectsLine(group), maxWidth - 8)] }, "affects"));
554
+ }
555
+ const linesToShow = maxLines !== undefined ? allLines.slice(0, maxLines) : allLines;
556
+ return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", marginLeft: 5, children: linesToShow }));
557
+ };
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Mascot = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const ink_1 = require("ink");
6
+ const FACES = {
7
+ idle: { left: "o", right: "o", mouth: "^" },
8
+ alert: { left: "O", right: "O", mouth: "^" },
9
+ scanning: { left: "-", right: "-", mouth: "^" },
10
+ alarmed: { left: "O", right: "O", mouth: "o" },
11
+ happy: { left: "^", right: "^", mouth: "v" },
12
+ curious: { left: "O", right: "o", mouth: "^" },
13
+ explaining: { left: "o", right: "o", mouth: "~" },
14
+ proud: { left: "^", right: "^", mouth: "^" },
15
+ wink: { left: "-", right: "o", mouth: "^" },
16
+ worried: { left: "o", right: "o", mouth: "_" },
17
+ };
18
+ const Mascot = ({ mood = "idle", color }) => {
19
+ const { left, right, mouth } = FACES[mood];
20
+ // Lines are kept as plain template strings so width is exact and verifiable
21
+ // by eye. Anything that drifts here breaks the alignment of the whole cat.
22
+ const lines = [
23
+ " /\\_____/\\",
24
+ ` / ${left} ${right} \\`,
25
+ ` ( == ${mouth} == )`,
26
+ " ) (",
27
+ " ( )",
28
+ " ( ( ) ( ) )",
29
+ "(__(__)___(__)__)",
30
+ ];
31
+ return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", children: lines.map((line, i) => ((0, jsx_runtime_1.jsx)(ink_1.Text, { color: color, children: line }, i))) }));
32
+ };
33
+ exports.Mascot = Mascot;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ProgressBar = void 0;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const react_1 = require("react");
9
+ const ink_1 = require("ink");
10
+ const ink_spinner_1 = __importDefault(require("ink-spinner"));
11
+ const chalk_1 = __importDefault(require("chalk"));
12
+ const useTerminalSize_1 = require("../hooks/useTerminalSize");
13
+ function estimateScanSeconds(packageCount) {
14
+ // Prefetch + analysis scales non-linearly with package count.
15
+ // Calibrated against local API: 45→6s, 100→25s, 200→60s
16
+ return Math.ceil(Math.max(5, packageCount * 0.35 - 10));
17
+ }
18
+ function formatTime(seconds) {
19
+ if (seconds < 60)
20
+ return `${seconds}s`;
21
+ const m = Math.floor(seconds / 60);
22
+ const s = seconds % 60;
23
+ return s > 0 ? `${m}m ${s}s` : `${m}m`;
24
+ }
25
+ const ProgressBar = ({ value, total, label, }) => {
26
+ const startRef = (0, react_1.useRef)(Date.now());
27
+ const [elapsed, setElapsed] = (0, react_1.useState)(0);
28
+ (0, react_1.useEffect)(() => {
29
+ const timer = setInterval(() => {
30
+ setElapsed(Math.floor((Date.now() - startRef.current) / 1000));
31
+ }, 1000);
32
+ return () => clearInterval(timer);
33
+ }, []);
34
+ // Subscribe to terminal resizes so the bar re-lays-out when the user
35
+ // resizes the window mid-scan. Reading process.stdout.columns directly
36
+ // only captures the width at mount time and goes stale on resize.
37
+ const { cols: termWidth } = (0, useTerminalSize_1.useTerminalSize)();
38
+ const percent = total > 0 ? Math.round((value / total) * 100) : 0;
39
+ const estimate = estimateScanSeconds(total);
40
+ const timeInfo = `${formatTime(elapsed)} / ~${formatTime(estimate)}`;
41
+ const counter = `${value}/${total} ${percent}%`;
42
+ // Reserve: 4 indent + counter length + 2 padding
43
+ const barWidth = Math.max(10, termWidth - counter.length - 8);
44
+ const fraction = total > 0 ? Math.min(1, value / total) : 0;
45
+ const filled = Math.round(fraction * barWidth);
46
+ const empty = barWidth - filled;
47
+ const filledBar = "\u2501".repeat(filled);
48
+ const emptyBar = "\u2501".repeat(empty);
49
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", paddingLeft: 2, children: [(0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [chalk_1.default.cyan("\u25C6"), " ", chalk_1.default.bold("Dependency Guardian")] }), (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }), (0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: "cyan", children: (0, jsx_runtime_1.jsx)(ink_spinner_1.default, { type: "dots" }) }), (0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [" Scanning ", total, " packages... "] }), (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: timeInfo })] }), (0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { children: " " }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: "green", children: filledBar }), (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: emptyBar }), (0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [" ", counter] })] }), label && ((0, jsx_runtime_1.jsx)(ink_1.Box, { children: (0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [" ", chalk_1.default.dim("\u203A"), " ", label] }) }))] }));
50
+ };
51
+ exports.ProgressBar = ProgressBar;