@rainy-updates/cli 0.5.4 → 0.5.7

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 (66) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +5 -0
  3. package/dist/bin/cli.js +16 -438
  4. package/dist/bin/dispatch.d.ts +16 -0
  5. package/dist/bin/dispatch.js +150 -0
  6. package/dist/bin/help.d.ts +1 -0
  7. package/dist/bin/help.js +284 -0
  8. package/dist/commands/audit/runner.js +43 -26
  9. package/dist/commands/dashboard/parser.d.ts +2 -0
  10. package/dist/commands/dashboard/parser.js +59 -0
  11. package/dist/commands/dashboard/runner.d.ts +2 -0
  12. package/dist/commands/dashboard/runner.js +47 -0
  13. package/dist/commands/doctor/parser.js +12 -0
  14. package/dist/commands/doctor/runner.js +5 -2
  15. package/dist/commands/ga/parser.d.ts +2 -0
  16. package/dist/commands/ga/parser.js +50 -0
  17. package/dist/commands/ga/runner.d.ts +2 -0
  18. package/dist/commands/ga/runner.js +129 -0
  19. package/dist/commands/resolve/runner.js +7 -3
  20. package/dist/commands/review/parser.js +6 -0
  21. package/dist/commands/review/runner.js +4 -3
  22. package/dist/core/analysis/options.d.ts +6 -0
  23. package/dist/core/analysis/options.js +69 -0
  24. package/dist/core/analysis/review-items.d.ts +4 -0
  25. package/dist/core/analysis/review-items.js +128 -0
  26. package/dist/core/analysis/run-silenced.d.ts +1 -0
  27. package/dist/core/analysis/run-silenced.js +14 -0
  28. package/dist/core/analysis-bundle.d.ts +4 -0
  29. package/dist/core/analysis-bundle.js +33 -0
  30. package/dist/core/artifacts.d.ts +3 -0
  31. package/dist/core/artifacts.js +48 -0
  32. package/dist/core/check.js +6 -1
  33. package/dist/core/doctor/findings.d.ts +2 -0
  34. package/dist/core/doctor/findings.js +166 -0
  35. package/dist/core/doctor/render.d.ts +3 -0
  36. package/dist/core/doctor/render.js +44 -0
  37. package/dist/core/doctor/result.d.ts +2 -0
  38. package/dist/core/doctor/result.js +55 -0
  39. package/dist/core/doctor/score.d.ts +5 -0
  40. package/dist/core/doctor/score.js +28 -0
  41. package/dist/core/options.d.ts +7 -1
  42. package/dist/core/options.js +14 -0
  43. package/dist/core/review-model.d.ts +3 -3
  44. package/dist/core/review-model.js +55 -245
  45. package/dist/core/review-verdict.d.ts +2 -0
  46. package/dist/core/review-verdict.js +14 -0
  47. package/dist/core/summary.js +19 -0
  48. package/dist/output/format.js +22 -0
  49. package/dist/output/github.js +12 -0
  50. package/dist/output/sarif.js +16 -0
  51. package/dist/types/index.d.ts +120 -0
  52. package/dist/ui/dashboard/DashboardTUI.d.ts +6 -0
  53. package/dist/ui/dashboard/DashboardTUI.js +34 -0
  54. package/dist/ui/dashboard/components/DetailPanel.d.ts +4 -0
  55. package/dist/ui/dashboard/components/DetailPanel.js +30 -0
  56. package/dist/ui/dashboard/components/Footer.d.ts +4 -0
  57. package/dist/ui/dashboard/components/Footer.js +9 -0
  58. package/dist/ui/dashboard/components/Header.d.ts +4 -0
  59. package/dist/ui/dashboard/components/Header.js +12 -0
  60. package/dist/ui/dashboard/components/Sidebar.d.ts +4 -0
  61. package/dist/ui/dashboard/components/Sidebar.js +23 -0
  62. package/dist/ui/dashboard/store.d.ts +34 -0
  63. package/dist/ui/dashboard/store.js +148 -0
  64. package/dist/ui/tui.d.ts +2 -2
  65. package/dist/ui/tui.js +310 -79
  66. package/package.json +1 -1
package/dist/ui/tui.js CHANGED
@@ -6,9 +6,307 @@ const FILTER_ORDER = [
6
6
  "security",
7
7
  "risky",
8
8
  "major",
9
+ "peer-conflict",
10
+ "license",
11
+ "unused",
12
+ "blocked",
9
13
  ];
10
- function VersionDiff({ from, to }) {
11
- return (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: from }), _jsxs(Text, { color: "gray", children: [" ", " -> ", " "] }), _jsx(Text, { color: "green", children: to })] }));
14
+ const SORT_ORDER = ["risk", "advisories", "diff", "name", "workspace"];
15
+ const GROUP_ORDER = ["none", "workspace", "scope", "risk", "decision"];
16
+ const DETAIL_TABS = [
17
+ "overview",
18
+ "risk",
19
+ "security",
20
+ "peer",
21
+ "license",
22
+ "health",
23
+ "changelog",
24
+ ];
25
+ function TuiApp({ items, onComplete }) {
26
+ const [cursorIndex, setCursorIndex] = useState(0);
27
+ const [filterIndex, setFilterIndex] = useState(0);
28
+ const [sortIndex, setSortIndex] = useState(0);
29
+ const [groupIndex, setGroupIndex] = useState(0);
30
+ const [tabIndex, setTabIndex] = useState(0);
31
+ const [showHelp, setShowHelp] = useState(false);
32
+ const [searchMode, setSearchMode] = useState(false);
33
+ const [search, setSearch] = useState("");
34
+ const [selectedIndices, setSelectedIndices] = useState(new Set(items.flatMap((item, index) => item.update.selectedByDefault === false ? [] : [index])));
35
+ const activeFilter = FILTER_ORDER[filterIndex] ?? "all";
36
+ const activeSort = SORT_ORDER[sortIndex] ?? "risk";
37
+ const activeGroup = GROUP_ORDER[groupIndex] ?? "none";
38
+ const activeTab = DETAIL_TABS[tabIndex] ?? "overview";
39
+ const visibleRows = buildVisibleRows(items, {
40
+ filter: activeFilter,
41
+ sort: activeSort,
42
+ group: activeGroup,
43
+ search,
44
+ });
45
+ const itemRows = visibleRows.filter((row) => row.kind === "item" && typeof row.index === "number");
46
+ const boundedCursor = Math.min(cursorIndex, Math.max(0, itemRows.length - 1));
47
+ const focusedIndex = itemRows[boundedCursor]?.index ?? 0;
48
+ const focusedItem = items[focusedIndex];
49
+ useInput((input, key) => {
50
+ if (searchMode) {
51
+ if (key.escape) {
52
+ setSearchMode(false);
53
+ setSearch("");
54
+ setCursorIndex(0);
55
+ return;
56
+ }
57
+ if (key.return) {
58
+ setSearchMode(false);
59
+ setCursorIndex(0);
60
+ return;
61
+ }
62
+ if (key.backspace || key.delete) {
63
+ setSearch((value) => value.slice(0, -1));
64
+ setCursorIndex(0);
65
+ return;
66
+ }
67
+ if (input && !key.ctrl && !key.meta) {
68
+ setSearch((value) => value + input);
69
+ setCursorIndex(0);
70
+ }
71
+ return;
72
+ }
73
+ if (input === "/") {
74
+ setSearchMode(true);
75
+ return;
76
+ }
77
+ if (input === "?") {
78
+ setShowHelp((value) => !value);
79
+ return;
80
+ }
81
+ if (key.escape && showHelp) {
82
+ setShowHelp(false);
83
+ return;
84
+ }
85
+ if (key.leftArrow) {
86
+ setFilterIndex((prev) => Math.max(0, prev - 1));
87
+ setCursorIndex(0);
88
+ }
89
+ if (key.rightArrow) {
90
+ setFilterIndex((prev) => Math.min(FILTER_ORDER.length - 1, prev + 1));
91
+ setCursorIndex(0);
92
+ }
93
+ if (key.upArrow) {
94
+ setCursorIndex((prev) => Math.max(0, prev - 1));
95
+ }
96
+ if (key.downArrow) {
97
+ setCursorIndex((prev) => Math.min(itemRows.length - 1, Math.max(0, prev + 1)));
98
+ }
99
+ if (input === "o") {
100
+ setSortIndex((prev) => (prev + 1) % SORT_ORDER.length);
101
+ setCursorIndex(0);
102
+ }
103
+ if (input === "g") {
104
+ setGroupIndex((prev) => (prev + 1) % GROUP_ORDER.length);
105
+ setCursorIndex(0);
106
+ }
107
+ if (key.tab) {
108
+ setTabIndex((prev) => (prev + 1) % DETAIL_TABS.length);
109
+ }
110
+ if (input === "a") {
111
+ setSelectedIndices((prev) => addVisible(prev, itemRows));
112
+ }
113
+ if (input === "n") {
114
+ setSelectedIndices((prev) => removeVisible(prev, itemRows));
115
+ }
116
+ if (input === "s") {
117
+ setSelectedIndices((prev) => selectSafe(prev, itemRows, items));
118
+ }
119
+ if (input === "b") {
120
+ setSelectedIndices((prev) => clearBlocked(prev, itemRows, items));
121
+ }
122
+ if (input === " ") {
123
+ setSelectedIndices((prev) => {
124
+ const next = new Set(prev);
125
+ if (next.has(focusedIndex))
126
+ next.delete(focusedIndex);
127
+ else
128
+ next.add(focusedIndex);
129
+ return next;
130
+ });
131
+ }
132
+ if (input === "q" || key.escape) {
133
+ onComplete(items.filter((_, index) => selectedIndices.has(index)));
134
+ return;
135
+ }
136
+ if (key.return) {
137
+ onComplete(items.filter((_, index) => selectedIndices.has(index)));
138
+ }
139
+ });
140
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Rainy Review Queue" }), _jsx(Text, { color: "gray", children: "Detect with check, summarize with doctor, decide here in review, then apply with upgrade." }), _jsx(Text, { color: "gray", children: "Filters: \u2190/\u2192 Sort: o Group: g Tabs: Tab Search: / Help: ? Space: toggle Enter: confirm" }), _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsxs(Box, { width: 24, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Filter Rail" }), FILTER_ORDER.map((filter, index) => (_jsxs(Text, { color: index === filterIndex ? "cyan" : "gray", children: [index === filterIndex ? ">" : " ", " ", filter] }, filter))), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Search" }), _jsx(Text, { color: searchMode ? "cyan" : "gray", children: searchMode ? `/${search}` : search ? `/${search}` : "inactive" })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Modes" }), _jsxs(Text, { color: "gray", children: ["sort: ", activeSort] }), _jsxs(Text, { color: "gray", children: ["group: ", activeGroup] }), _jsxs(Text, { color: "gray", children: ["tab: ", activeTab] })] })] }), _jsxs(Box, { marginLeft: 1, width: 82, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Review Queue" }), itemRows.length === 0 ? (_jsx(Text, { color: "gray", children: "No review candidates match this view." })) : (visibleRows.map((row, visibleIndex) => {
141
+ if (row.kind === "group") {
142
+ return (_jsx(Text, { bold: true, color: "gray", children: row.label }, `group:${row.label}`));
143
+ }
144
+ const index = row.index ?? 0;
145
+ const item = items[index];
146
+ const update = item.update;
147
+ const decision = update.decisionState ?? deriveDecision(item);
148
+ const itemPosition = itemRows.findIndex((candidate) => candidate.index === index);
149
+ const isFocused = itemPosition === boundedCursor;
150
+ const isSelected = selectedIndices.has(index);
151
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: isFocused ? "cyan" : "gray", children: [isFocused ? ">" : " ", " ", isSelected ? "[x]" : "[ ]", " "] }), _jsx(Box, { width: 22, children: _jsx(Text, { bold: isFocused, children: update.name }) }), _jsx(Box, { width: 14, children: _jsx(Text, { color: diffColor(update.diffType), children: update.diffType }) }), _jsx(Box, { width: 14, children: _jsx(Text, { color: riskColor(update.riskLevel), children: update.riskLevel ?? "low" }) }), _jsx(Box, { width: 14, children: _jsx(Text, { color: decisionColor(decision), children: decision }) }), _jsx(Box, { width: 10, children: _jsx(Text, { color: decisionColor(decision), children: update.riskScore ?? "--" }) }), _jsx(Text, { color: "gray", children: update.fromRange }), _jsx(Text, { color: "gray", children: " \u2192 " }), _jsx(Text, { color: "green", children: update.toVersionResolved })] }, `${update.packagePath}:${update.name}`));
152
+ }))] }), _jsxs(Box, { marginLeft: 1, width: 54, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Decision Panel" }), _jsxs(Text, { color: "gray", children: ["tab: ", activeTab] }), focusedItem ? renderTab(focusedItem, activeTab) : _jsx(Text, { color: "gray", children: "No review candidate selected." })] })] }), _jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsxs(Text, { color: "gray", children: [selectedIndices.size, " selected of ", items.length, ". view=", activeFilter, " sort=", activeSort, " group=", activeGroup] }) }), _jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsx(Text, { color: "gray", children: "A select visible N clear visible S select safe B clear blocked Q finish Esc clears search/help" }) }), showHelp ? (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Help" }), _jsx(Text, { color: "gray", children: "Use review as the decision center. Search packages with / and inspect details with Tab." }), _jsx(Text, { color: "gray", children: "Blocked items default to deselected. Safe items can be bulk-selected with S." })] })) : null] }));
153
+ }
154
+ function renderTab(item, tab) {
155
+ const update = item.update;
156
+ if (tab === "risk") {
157
+ return (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["state: ", _jsx(Text, { color: decisionColor(update.decisionState ?? deriveDecision(item)), children: update.decisionState ?? deriveDecision(item) })] }), _jsxs(Text, { children: ["policy: ", _jsx(Text, { color: decisionColor(policyToDecision(update.policyAction)), children: update.policyAction ?? "allow" })] }), _jsxs(Text, { children: ["risk score: ", update.riskScore ?? 0] }), _jsxs(Text, { children: ["impact score: ", update.impactScore?.score ?? 0] }), _jsxs(Text, { children: ["recommended action: ", update.recommendedAction ?? "Safe to keep in the selected set."] }), update.riskReasons && update.riskReasons.length > 0 ? update.riskReasons.slice(0, 5).map((reason) => (_jsxs(Text, { color: "gray", children: ["- ", reason] }, reason))) : _jsx(Text, { color: "gray", children: "No elevated risk reasons." })] }));
158
+ }
159
+ if (tab === "security") {
160
+ return (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["advisories: ", item.advisories.length] }), item.advisories.length > 0 ? item.advisories.slice(0, 4).map((advisory) => (_jsxs(Text, { color: "gray", children: ["- ", advisory.severity, " ", advisory.cveId, ": ", advisory.title] }, `${advisory.packageName}:${advisory.cveId}`))) : _jsx(Text, { color: "gray", children: "No security advisories detected." })] }));
161
+ }
162
+ if (tab === "peer") {
163
+ return (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["peer status: ", update.peerConflictSeverity ?? "none"] }), item.peerConflicts.length > 0 ? item.peerConflicts.slice(0, 4).map((conflict) => (_jsxs(Text, { color: "gray", children: ["- ", conflict.requester, " requires ", conflict.peer, " ", conflict.requiredRange] }, `${conflict.requester}:${conflict.peer}`))) : _jsx(Text, { color: "gray", children: "No peer conflicts detected." })] }));
164
+ }
165
+ if (tab === "license") {
166
+ return (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["license status: ", update.licenseStatus ?? "allowed"] }), _jsxs(Text, { children: ["repository: ", update.repository ?? "unavailable"] }), _jsxs(Text, { children: ["homepage: ", update.homepage ?? "unavailable"] })] }));
167
+ }
168
+ if (tab === "health") {
169
+ return (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["health: ", update.healthStatus ?? "healthy"] }), _jsxs(Text, { children: ["maintainers: ", update.maintainerCount ?? "unknown"] }), _jsxs(Text, { children: ["publish age days: ", update.publishAgeDays ?? "unknown"] }), _jsxs(Text, { children: ["maintainer churn: ", update.maintainerChurn ?? "unknown"] })] }));
170
+ }
171
+ if (tab === "changelog") {
172
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { children: update.releaseNotesSummary?.title ?? "Release notes unavailable" }), _jsx(Text, { color: "gray", children: update.releaseNotesSummary?.excerpt ?? "Run review with changelog support or inspect the repository manually." })] }));
173
+ }
174
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { children: update.name }), _jsxs(Text, { color: "gray", children: ["package: ", update.packagePath] }), _jsxs(Text, { children: ["state: ", _jsx(Text, { color: decisionColor(update.decisionState ?? deriveDecision(item)), children: update.decisionState ?? deriveDecision(item) })] }), _jsxs(Text, { children: ["diff: ", _jsx(Text, { color: diffColor(update.diffType), children: update.diffType })] }), _jsxs(Text, { children: ["risk: ", _jsx(Text, { color: riskColor(update.riskLevel), children: update.riskLevel ?? "low" })] }), _jsxs(Text, { children: ["policy: ", update.policyAction ?? "allow"] }), _jsxs(Text, { children: ["workspace: ", update.workspaceGroup ?? "root"] }), _jsxs(Text, { children: ["group: ", update.groupKey ?? "none"] }), _jsxs(Text, { children: ["action: ", update.recommendedAction ?? "Safe to keep in the selected set."] })] }));
175
+ }
176
+ function buildVisibleRows(items, config) {
177
+ const filtered = items
178
+ .map((item, index) => ({ item, index }))
179
+ .filter(({ item }) => matchesFilter(item, config.filter))
180
+ .filter(({ item }) => matchesSearch(item, config.search))
181
+ .sort((left, right) => compareItems(left.item, right.item, config.sort));
182
+ if (config.group === "none") {
183
+ return filtered.map(({ item, index }) => ({ kind: "item", label: item.update.name, index }));
184
+ }
185
+ const rows = [];
186
+ let currentGroup = "";
187
+ for (const entry of filtered) {
188
+ const nextGroup = groupLabel(entry.item, config.group);
189
+ if (nextGroup !== currentGroup) {
190
+ currentGroup = nextGroup;
191
+ rows.push({ kind: "group", label: nextGroup });
192
+ }
193
+ rows.push({ kind: "item", label: entry.item.update.name, index: entry.index });
194
+ }
195
+ return rows;
196
+ }
197
+ function matchesFilter(item, filter) {
198
+ if (filter === "security")
199
+ return item.advisories.length > 0;
200
+ if (filter === "risky")
201
+ return item.update.riskLevel === "critical" || item.update.riskLevel === "high";
202
+ if (filter === "major")
203
+ return item.update.diffType === "major";
204
+ if (filter === "peer-conflict")
205
+ return item.peerConflicts.length > 0;
206
+ if (filter === "license")
207
+ return item.update.licenseStatus === "denied";
208
+ if (filter === "unused")
209
+ return item.unusedIssues.length > 0;
210
+ if (filter === "blocked")
211
+ return (item.update.decisionState ?? deriveDecision(item)) === "blocked";
212
+ return true;
213
+ }
214
+ function matchesSearch(item, search) {
215
+ if (!search.trim())
216
+ return true;
217
+ const query = search.toLowerCase();
218
+ return (item.update.name.toLowerCase().includes(query) ||
219
+ item.update.packagePath.toLowerCase().includes(query));
220
+ }
221
+ function compareItems(left, right, sort) {
222
+ if (sort === "advisories") {
223
+ const byAdvisories = (right.advisories.length ?? 0) - (left.advisories.length ?? 0);
224
+ if (byAdvisories !== 0)
225
+ return byAdvisories;
226
+ }
227
+ if (sort === "diff") {
228
+ const byDiff = diffWeight(right.update.diffType) - diffWeight(left.update.diffType);
229
+ if (byDiff !== 0)
230
+ return byDiff;
231
+ }
232
+ if (sort === "workspace") {
233
+ const byWorkspace = (left.update.workspaceGroup ?? left.update.packagePath).localeCompare(right.update.workspaceGroup ?? right.update.packagePath);
234
+ if (byWorkspace !== 0)
235
+ return byWorkspace;
236
+ }
237
+ if (sort === "name") {
238
+ const byName = left.update.name.localeCompare(right.update.name);
239
+ if (byName !== 0)
240
+ return byName;
241
+ }
242
+ const byRisk = (right.update.riskScore ?? 0) - (left.update.riskScore ?? 0);
243
+ if (byRisk !== 0)
244
+ return byRisk;
245
+ return left.update.name.localeCompare(right.update.name);
246
+ }
247
+ function groupLabel(item, group) {
248
+ if (group === "workspace")
249
+ return item.update.workspaceGroup ?? "root";
250
+ if (group === "scope") {
251
+ if (item.update.name.startsWith("@")) {
252
+ return item.update.name.split("/")[0] ?? "unscoped";
253
+ }
254
+ return "unscoped";
255
+ }
256
+ if (group === "risk")
257
+ return item.update.riskLevel ?? "low";
258
+ if (group === "decision")
259
+ return item.update.decisionState ?? deriveDecision(item);
260
+ return "all";
261
+ }
262
+ function addVisible(selected, rows) {
263
+ const next = new Set(selected);
264
+ for (const row of rows)
265
+ next.add(row.index);
266
+ return next;
267
+ }
268
+ function removeVisible(selected, rows) {
269
+ const next = new Set(selected);
270
+ for (const row of rows)
271
+ next.delete(row.index);
272
+ return next;
273
+ }
274
+ function selectSafe(selected, rows, items) {
275
+ const next = new Set(selected);
276
+ for (const row of rows) {
277
+ if ((items[row.index]?.update.decisionState ?? deriveDecision(items[row.index])) === "safe") {
278
+ next.add(row.index);
279
+ }
280
+ }
281
+ return next;
282
+ }
283
+ function clearBlocked(selected, rows, items) {
284
+ const next = new Set(selected);
285
+ for (const row of rows) {
286
+ if ((items[row.index]?.update.decisionState ?? deriveDecision(items[row.index])) === "blocked") {
287
+ next.delete(row.index);
288
+ }
289
+ }
290
+ return next;
291
+ }
292
+ function deriveDecision(item) {
293
+ if (item.update.peerConflictSeverity === "error" || item.update.licenseStatus === "denied") {
294
+ return "blocked";
295
+ }
296
+ if ((item.update.advisoryCount ?? 0) > 0 || item.update.riskLevel === "critical") {
297
+ return "actionable";
298
+ }
299
+ if (item.update.riskLevel === "high" || item.update.diffType === "major") {
300
+ return "review";
301
+ }
302
+ return "safe";
303
+ }
304
+ function policyToDecision(value) {
305
+ if (value === "block")
306
+ return "blocked";
307
+ if (value === "review")
308
+ return "review";
309
+ return "safe";
12
310
  }
13
311
  function riskColor(level) {
14
312
  switch (level) {
@@ -34,17 +332,14 @@ function diffColor(level) {
34
332
  return "cyan";
35
333
  }
36
334
  }
37
- function decisionLabel(update) {
38
- if (update.peerConflictSeverity === "error" || update.licenseStatus === "denied") {
39
- return "blocked";
40
- }
41
- if (update.advisoryCount && update.advisoryCount > 0) {
42
- return "actionable";
43
- }
44
- if (update.riskLevel === "critical" || update.riskLevel === "high") {
45
- return "review";
46
- }
47
- return "safe";
335
+ function diffWeight(level) {
336
+ if (level === "major")
337
+ return 4;
338
+ if (level === "minor")
339
+ return 3;
340
+ if (level === "patch")
341
+ return 2;
342
+ return 1;
48
343
  }
49
344
  function decisionColor(label) {
50
345
  switch (label) {
@@ -58,73 +353,9 @@ function decisionColor(label) {
58
353
  return "green";
59
354
  }
60
355
  }
61
- function TuiApp({ updates, onComplete }) {
62
- const [cursorIndex, setCursorIndex] = useState(0);
63
- const [filterIndex, setFilterIndex] = useState(0);
64
- const [selectedIndices, setSelectedIndices] = useState(new Set(updates.map((_, index) => index)));
65
- const activeFilter = FILTER_ORDER[filterIndex] ?? "all";
66
- const filteredIndices = updates
67
- .map((update, index) => ({ update, index }))
68
- .filter(({ update }) => {
69
- if (activeFilter === "security")
70
- return (update.advisoryCount ?? 0) > 0;
71
- if (activeFilter === "risky") {
72
- return update.riskLevel === "critical" || update.riskLevel === "high";
73
- }
74
- if (activeFilter === "major")
75
- return update.diffType === "major";
76
- return true;
77
- })
78
- .map(({ index }) => index);
79
- const boundedCursor = Math.min(cursorIndex, Math.max(0, filteredIndices.length - 1));
80
- const focusedIndex = filteredIndices[boundedCursor] ?? 0;
81
- const focusedUpdate = updates[focusedIndex];
82
- useInput((input, key) => {
83
- if (key.leftArrow) {
84
- setFilterIndex((prev) => Math.max(0, prev - 1));
85
- setCursorIndex(0);
86
- }
87
- if (key.rightArrow) {
88
- setFilterIndex((prev) => Math.min(FILTER_ORDER.length - 1, prev + 1));
89
- setCursorIndex(0);
90
- }
91
- if (key.upArrow) {
92
- setCursorIndex((prev) => Math.max(0, prev - 1));
93
- }
94
- if (key.downArrow) {
95
- setCursorIndex((prev) => Math.min(filteredIndices.length - 1, Math.max(0, prev + 1)));
96
- }
97
- if (input === "a") {
98
- setSelectedIndices(new Set(filteredIndices));
99
- }
100
- if (input === "n") {
101
- setSelectedIndices(new Set());
102
- }
103
- if (input === " ") {
104
- setSelectedIndices((prev) => {
105
- const next = new Set(prev);
106
- if (next.has(focusedIndex))
107
- next.delete(focusedIndex);
108
- else
109
- next.add(focusedIndex);
110
- return next;
111
- });
112
- }
113
- if (key.return) {
114
- onComplete(updates.filter((_, index) => selectedIndices.has(index)));
115
- }
116
- });
117
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Rainy Review Queue" }), _jsx(Text, { color: "gray", children: "Detect with check, summarize with doctor, decide here in review, then apply with upgrade." }), _jsx(Text, { color: "gray", children: "Left/Right filter Up/Down move Space toggle A select visible N clear Enter confirm" }), _jsx(Box, { marginTop: 1, children: FILTER_ORDER.map((filter, index) => (_jsx(Box, { marginRight: 2, children: _jsxs(Text, { color: index === filterIndex ? "cyan" : "gray", children: ["[", filter, "]"] }) }, filter))) }), _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsxs(Box, { width: 72, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Review Queue" }), filteredIndices.length === 0 ? (_jsx(Text, { color: "gray", children: "No review candidates match this filter." })) : (filteredIndices.map((index, visibleIndex) => {
118
- const update = updates[index];
119
- const isFocused = visibleIndex === boundedCursor;
120
- const isSelected = selectedIndices.has(index);
121
- const decision = decisionLabel(update);
122
- return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: isFocused ? "cyan" : "gray", children: [isFocused ? ">" : " ", " ", isSelected ? "[x]" : "[ ]", " "] }), _jsx(Box, { width: 22, children: _jsx(Text, { bold: isFocused, children: update.name }) }), _jsx(Box, { width: 10, children: _jsx(Text, { color: diffColor(update.diffType), children: update.diffType }) }), _jsx(Box, { width: 18, children: _jsx(Text, { color: riskColor(update.riskLevel), children: update.riskLevel ?? update.impactScore?.rank ?? "low" }) }), _jsx(Box, { width: 12, children: _jsx(Text, { color: decisionColor(decision), children: decision }) }), _jsx(Box, { width: 10, children: _jsx(Text, { color: decisionColor(decision), children: typeof update.riskScore === "number" ? update.riskScore : "--" }) }), _jsx(VersionDiff, { from: update.fromRange, to: update.toVersionResolved })] }, `${update.packagePath}:${update.name}`));
123
- }))] }), _jsxs(Box, { marginLeft: 1, width: 46, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Decision Panel" }), focusedUpdate ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: focusedUpdate.name }), _jsxs(Text, { color: "gray", children: ["package: ", focusedUpdate.packagePath] }), _jsxs(Text, { children: ["state:", " ", _jsx(Text, { color: decisionColor(decisionLabel(focusedUpdate)), children: decisionLabel(focusedUpdate) })] }), _jsxs(Text, { children: ["diff: ", _jsx(Text, { color: diffColor(focusedUpdate.diffType), children: focusedUpdate.diffType })] }), _jsxs(Text, { children: ["risk: ", _jsx(Text, { color: riskColor(focusedUpdate.riskLevel), children: focusedUpdate.riskLevel ?? focusedUpdate.impactScore?.rank ?? "low" })] }), _jsxs(Text, { children: ["risk score: ", focusedUpdate.riskScore ?? 0] }), _jsxs(Text, { children: ["impact score: ", focusedUpdate.impactScore?.score ?? 0] }), _jsxs(Text, { children: ["advisories: ", focusedUpdate.advisoryCount ?? 0] }), _jsxs(Text, { children: ["peer: ", focusedUpdate.peerConflictSeverity ?? "none"] }), _jsxs(Text, { children: ["license: ", focusedUpdate.licenseStatus ?? "allowed"] }), _jsxs(Text, { children: ["health: ", focusedUpdate.healthStatus ?? "healthy"] }), _jsxs(Text, { children: ["action:", " ", _jsx(Text, { color: decisionColor(decisionLabel(focusedUpdate)), children: focusedUpdate.recommendedAction ?? "Safe to keep in the review queue." })] }), focusedUpdate.homepage ? (_jsxs(Text, { color: "blue", children: ["homepage: ", focusedUpdate.homepage] })) : (_jsx(Text, { color: "gray", children: "homepage: unavailable" })), focusedUpdate.riskReasons && focusedUpdate.riskReasons.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Reasons" }), focusedUpdate.riskReasons.slice(0, 4).map((reason) => (_jsxs(Text, { color: "gray", children: ["- ", reason] }, reason)))] })) : (_jsx(Text, { color: "gray", children: "No elevated risk reasons." }))] })) : (_jsx(Text, { color: "gray", children: "No review candidate selected." }))] })] }), _jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsxs(Text, { color: "gray", children: [selectedIndices.size, " selected for apply of ", updates.length, ". Filter: ", activeFilter, ". Enter confirms the review decision set."] }) })] }));
124
- }
125
- export async function runTui(updates) {
356
+ export async function runTui(items) {
126
357
  return new Promise((resolve) => {
127
- const { unmount } = render(_jsx(TuiApp, { updates: updates, onComplete: (selected) => {
358
+ const { unmount } = render(_jsx(TuiApp, { items: items, onComplete: (selected) => {
128
359
  unmount();
129
360
  resolve(selected);
130
361
  } }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainy-updates/cli",
3
- "version": "0.5.4",
3
+ "version": "0.5.7",
4
4
  "description": "The fastest DevOps-first dependency CLI. Checks, audits, upgrades, bisects, and automates npm/pnpm dependencies in CI.",
5
5
  "type": "module",
6
6
  "private": false,