@rainy-updates/cli 0.6.0 → 0.6.2

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 (63) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/README.md +12 -0
  3. package/dist/bin/cli.js +12 -127
  4. package/dist/bin/dispatch.js +6 -0
  5. package/dist/bin/help.js +48 -0
  6. package/dist/bin/main.d.ts +1 -0
  7. package/dist/bin/main.js +126 -0
  8. package/dist/commands/audit/parser.js +36 -0
  9. package/dist/commands/audit/runner.js +17 -18
  10. package/dist/commands/bisect/oracle.js +21 -17
  11. package/dist/commands/bisect/runner.js +4 -3
  12. package/dist/commands/dashboard/parser.js +41 -0
  13. package/dist/commands/dashboard/runner.js +3 -0
  14. package/dist/commands/doctor/parser.js +44 -0
  15. package/dist/commands/ga/parser.js +39 -0
  16. package/dist/commands/ga/runner.js +73 -9
  17. package/dist/commands/health/parser.js +36 -0
  18. package/dist/commands/health/runner.js +5 -1
  19. package/dist/commands/hook/parser.d.ts +2 -0
  20. package/dist/commands/hook/parser.js +40 -0
  21. package/dist/commands/hook/runner.d.ts +2 -0
  22. package/dist/commands/hook/runner.js +174 -0
  23. package/dist/commands/licenses/parser.js +39 -0
  24. package/dist/commands/licenses/runner.js +5 -1
  25. package/dist/commands/resolve/graph/builder.js +5 -1
  26. package/dist/commands/resolve/parser.js +39 -0
  27. package/dist/commands/resolve/runner.js +5 -0
  28. package/dist/commands/review/parser.js +44 -0
  29. package/dist/commands/snapshot/parser.js +39 -0
  30. package/dist/commands/snapshot/runner.js +4 -1
  31. package/dist/commands/unused/parser.js +39 -0
  32. package/dist/commands/unused/runner.js +4 -1
  33. package/dist/commands/unused/scanner.d.ts +2 -1
  34. package/dist/commands/unused/scanner.js +60 -44
  35. package/dist/core/check.js +5 -1
  36. package/dist/core/doctor/findings.js +4 -4
  37. package/dist/core/init-ci.js +28 -26
  38. package/dist/core/options.d.ts +4 -1
  39. package/dist/core/options.js +57 -0
  40. package/dist/core/verification.js +11 -9
  41. package/dist/core/warm-cache.js +5 -1
  42. package/dist/generated/version.d.ts +1 -0
  43. package/dist/generated/version.js +2 -0
  44. package/dist/git/scope.d.ts +19 -0
  45. package/dist/git/scope.js +167 -0
  46. package/dist/index.d.ts +2 -1
  47. package/dist/index.js +1 -0
  48. package/dist/output/sarif.js +2 -8
  49. package/dist/pm/detect.d.ts +37 -0
  50. package/dist/pm/detect.js +133 -2
  51. package/dist/pm/install.d.ts +2 -1
  52. package/dist/pm/install.js +7 -5
  53. package/dist/rup +0 -0
  54. package/dist/types/index.d.ts +59 -1
  55. package/dist/ui/dashboard-state.d.ts +7 -0
  56. package/dist/ui/dashboard-state.js +44 -0
  57. package/dist/ui/tui.d.ts +3 -0
  58. package/dist/ui/tui.js +311 -111
  59. package/dist/utils/shell.d.ts +6 -0
  60. package/dist/utils/shell.js +18 -0
  61. package/dist/workspace/discover.d.ts +7 -1
  62. package/dist/workspace/discover.js +12 -3
  63. package/package.json +16 -8
package/dist/ui/tui.js CHANGED
@@ -1,178 +1,286 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useState } from "react";
3
- import { Box, render, Text, useInput } from "ink";
4
- const FILTER_ORDER = [
5
- "all",
6
- "security",
7
- "risky",
8
- "major",
9
- "peer-conflict",
10
- "license",
11
- "unused",
12
- "blocked",
13
- ];
2
+ import React from "react";
3
+ import { Box, render, Text, useInput, useStdout } from "ink";
4
+ import { DETAIL_TABS, FILTER_ORDER, } from "./dashboard-state.js";
14
5
  const SORT_ORDER = ["risk", "advisories", "diff", "name", "workspace"];
15
6
  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, title, subtitle, 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";
7
+ function tuiReducer(state, action) {
8
+ switch (action.type) {
9
+ case "SET_SEARCH_MODE":
10
+ return {
11
+ ...state,
12
+ searchMode: action.active,
13
+ ...(action.active ? {} : { search: "", cursorIndex: 0 }),
14
+ };
15
+ case "APPEND_SEARCH":
16
+ return { ...state, search: state.search + action.value, cursorIndex: 0 };
17
+ case "BACKSPACE_SEARCH":
18
+ return { ...state, search: state.search.slice(0, -1), cursorIndex: 0 };
19
+ case "TOGGLE_HELP":
20
+ return { ...state, showHelp: !state.showHelp };
21
+ case "SET_HELP":
22
+ return { ...state, showHelp: action.active };
23
+ case "MOVE_FILTER":
24
+ return {
25
+ ...state,
26
+ filterIndex: Math.min(action.max, Math.max(0, state.filterIndex + action.direction)),
27
+ cursorIndex: 0,
28
+ };
29
+ case "MOVE_CURSOR":
30
+ return {
31
+ ...state,
32
+ cursorIndex: Math.min(action.max, Math.max(0, state.cursorIndex + action.direction)),
33
+ };
34
+ case "CYCLE_SORT":
35
+ return {
36
+ ...state,
37
+ sortIndex: (state.sortIndex + 1) % action.max,
38
+ cursorIndex: 0,
39
+ };
40
+ case "CYCLE_GROUP":
41
+ return {
42
+ ...state,
43
+ groupIndex: (state.groupIndex + 1) % action.max,
44
+ cursorIndex: 0,
45
+ };
46
+ case "CYCLE_TAB": {
47
+ const next = (state.tabIndex + action.direction + action.max) % action.max;
48
+ return { ...state, tabIndex: next };
49
+ }
50
+ case "SET_SELECTED":
51
+ return { ...state, selectedIndices: action.indices };
52
+ case "TOGGLE_SELECTED": {
53
+ const next = new Set(state.selectedIndices);
54
+ if (next.has(action.index))
55
+ next.delete(action.index);
56
+ else
57
+ next.add(action.index);
58
+ return { ...state, selectedIndices: next };
59
+ }
60
+ default:
61
+ return state;
62
+ }
63
+ }
64
+ function TuiApp({ items, title, subtitle, initialFilter = "all", initialTab = "overview", onComplete, }) {
65
+ const { stdout } = useStdout();
66
+ const { columns: stdoutWidth = 160, rows: stdoutHeight = 32 } = stdout;
67
+ const [state, dispatch] = React.useReducer(tuiReducer, undefined, () => ({
68
+ cursorIndex: 0,
69
+ filterIndex: Math.max(0, FILTER_ORDER.indexOf(initialFilter)),
70
+ sortIndex: 0,
71
+ groupIndex: 0,
72
+ tabIndex: Math.max(0, DETAIL_TABS.indexOf(initialTab)),
73
+ showHelp: false,
74
+ searchMode: false,
75
+ search: "",
76
+ selectedIndices: new Set(items.flatMap((item, index) => item.update.selectedByDefault === false ? [] : [index])),
77
+ }));
78
+ const activeFilter = FILTER_ORDER[state.filterIndex] ?? "all";
79
+ const activeSort = SORT_ORDER[state.sortIndex] ?? "risk";
80
+ const activeGroup = GROUP_ORDER[state.groupIndex] ?? "none";
81
+ const activeTab = DETAIL_TABS[state.tabIndex] ?? "overview";
39
82
  const visibleRows = buildVisibleRows(items, {
40
83
  filter: activeFilter,
41
84
  sort: activeSort,
42
85
  group: activeGroup,
43
- search,
86
+ search: state.search,
44
87
  });
45
88
  const itemRows = visibleRows.filter((row) => row.kind === "item" && typeof row.index === "number");
46
- const boundedCursor = Math.min(cursorIndex, Math.max(0, itemRows.length - 1));
89
+ const boundedCursor = Math.min(state.cursorIndex, Math.max(0, itemRows.length - 1));
47
90
  const focusedIndex = itemRows[boundedCursor]?.index ?? 0;
48
91
  const focusedItem = items[focusedIndex];
92
+ const visibleMetrics = summarizeVisibleItems(itemRows, items, state.selectedIndices);
93
+ const renderWindow = createRenderWindow({
94
+ visibleRows,
95
+ focusedIndex,
96
+ stdoutHeight,
97
+ });
98
+ const rowPositionByIndex = createRowPositionMap(itemRows);
99
+ const layout = createDashboardLayout(stdoutWidth);
100
+ const platformLabel = process.platform === "win32" ? "windows" : "unix";
101
+ const selectedItems = items.filter((_, index) => state.selectedIndices.has(index));
49
102
  useInput((input, key) => {
50
- if (searchMode) {
103
+ if (state.searchMode) {
51
104
  if (key.escape) {
52
- setSearchMode(false);
53
- setSearch("");
54
- setCursorIndex(0);
105
+ dispatch({ type: "SET_SEARCH_MODE", active: false });
55
106
  return;
56
107
  }
57
108
  if (key.return) {
58
- setSearchMode(false);
59
- setCursorIndex(0);
109
+ dispatch({ type: "SET_SEARCH_MODE", active: false });
60
110
  return;
61
111
  }
62
112
  if (key.backspace || key.delete) {
63
- setSearch((value) => value.slice(0, -1));
64
- setCursorIndex(0);
113
+ dispatch({ type: "BACKSPACE_SEARCH" });
65
114
  return;
66
115
  }
67
116
  if (input && !key.ctrl && !key.meta) {
68
- setSearch((value) => value + input);
69
- setCursorIndex(0);
117
+ dispatch({ type: "APPEND_SEARCH", value: input });
70
118
  }
71
119
  return;
72
120
  }
73
121
  if (input === "/") {
74
- setSearchMode(true);
122
+ dispatch({ type: "SET_SEARCH_MODE", active: true });
75
123
  return;
76
124
  }
77
125
  if (input === "?") {
78
- setShowHelp((value) => !value);
126
+ dispatch({ type: "TOGGLE_HELP" });
79
127
  return;
80
128
  }
81
- if (key.escape && showHelp) {
82
- setShowHelp(false);
129
+ if (key.escape && state.showHelp) {
130
+ dispatch({ type: "SET_HELP", active: false });
83
131
  return;
84
132
  }
85
- if (key.leftArrow) {
86
- setFilterIndex((prev) => Math.max(0, prev - 1));
87
- setCursorIndex(0);
133
+ if (key.leftArrow || input === "h") {
134
+ dispatch({
135
+ type: "MOVE_FILTER",
136
+ direction: -1,
137
+ max: FILTER_ORDER.length - 1,
138
+ });
139
+ return;
88
140
  }
89
- if (key.rightArrow) {
90
- setFilterIndex((prev) => Math.min(FILTER_ORDER.length - 1, prev + 1));
91
- setCursorIndex(0);
141
+ if (key.rightArrow || input === "l") {
142
+ dispatch({
143
+ type: "MOVE_FILTER",
144
+ direction: 1,
145
+ max: FILTER_ORDER.length - 1,
146
+ });
147
+ return;
92
148
  }
93
- if (key.upArrow) {
94
- setCursorIndex((prev) => Math.max(0, prev - 1));
149
+ if (key.upArrow || input === "k") {
150
+ dispatch({
151
+ type: "MOVE_CURSOR",
152
+ direction: -1,
153
+ max: itemRows.length - 1,
154
+ });
155
+ return;
95
156
  }
96
- if (key.downArrow) {
97
- setCursorIndex((prev) => Math.min(itemRows.length - 1, Math.max(0, prev + 1)));
157
+ if (key.downArrow || input === "j") {
158
+ dispatch({
159
+ type: "MOVE_CURSOR",
160
+ direction: 1,
161
+ max: itemRows.length - 1,
162
+ });
163
+ return;
98
164
  }
99
165
  if (input === "o") {
100
- setSortIndex((prev) => (prev + 1) % SORT_ORDER.length);
101
- setCursorIndex(0);
166
+ dispatch({ type: "CYCLE_SORT", max: SORT_ORDER.length });
167
+ return;
102
168
  }
103
169
  if (input === "g") {
104
- setGroupIndex((prev) => (prev + 1) % GROUP_ORDER.length);
105
- setCursorIndex(0);
170
+ dispatch({ type: "CYCLE_GROUP", max: GROUP_ORDER.length });
171
+ return;
106
172
  }
107
173
  if (key.tab) {
108
- setTabIndex((prev) => (prev + 1) % DETAIL_TABS.length);
174
+ dispatch({
175
+ type: "CYCLE_TAB",
176
+ direction: key.shift ? -1 : 1,
177
+ max: DETAIL_TABS.length,
178
+ });
179
+ return;
109
180
  }
110
181
  if (input === "a") {
111
- setSelectedIndices((prev) => addVisible(prev, itemRows));
182
+ dispatch({
183
+ type: "SET_SELECTED",
184
+ indices: addVisible(state.selectedIndices, itemRows),
185
+ });
186
+ return;
112
187
  }
113
188
  if (input === "n") {
114
- setSelectedIndices((prev) => removeVisible(prev, itemRows));
189
+ dispatch({
190
+ type: "SET_SELECTED",
191
+ indices: removeVisible(state.selectedIndices, itemRows),
192
+ });
193
+ return;
115
194
  }
116
195
  if (input === "s") {
117
- setSelectedIndices((prev) => selectSafe(prev, itemRows, items));
196
+ dispatch({
197
+ type: "SET_SELECTED",
198
+ indices: selectSafe(state.selectedIndices, itemRows, items),
199
+ });
200
+ return;
118
201
  }
119
202
  if (input === "b") {
120
- setSelectedIndices((prev) => clearBlocked(prev, itemRows, items));
203
+ dispatch({
204
+ type: "SET_SELECTED",
205
+ indices: clearBlocked(state.selectedIndices, itemRows, items),
206
+ });
207
+ return;
121
208
  }
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;
209
+ if (input === "x") {
210
+ dispatch({
211
+ type: "SET_SELECTED",
212
+ indices: selectActionable(state.selectedIndices, itemRows, items),
130
213
  });
214
+ return;
215
+ }
216
+ if (input === " ") {
217
+ dispatch({ type: "TOGGLE_SELECTED", index: focusedIndex });
218
+ return;
131
219
  }
132
- if (input === "q" || key.escape) {
133
- onComplete(items.filter((_, index) => selectedIndices.has(index)));
220
+ if (input === "q" || (key.escape && !state.showHelp)) {
221
+ onComplete(selectedItems);
134
222
  return;
135
223
  }
136
224
  if (key.return) {
137
- onComplete(items.filter((_, index) => selectedIndices.has(index)));
225
+ onComplete(selectedItems);
138
226
  }
139
227
  });
140
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: title ?? "Rainy Dashboard" }), _jsx(Text, { color: "gray", children: subtitle ??
141
- "Check detects, doctor summarizes, dashboard decides, upgrade applies." }), _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) => {
142
- if (row.kind === "group") {
143
- return (_jsx(Text, { bold: true, color: "gray", children: row.label }, `group:${row.label}`));
144
- }
145
- const index = row.index ?? 0;
146
- const item = items[index];
147
- const update = item.update;
148
- const decision = update.decisionState ?? deriveDecision(item);
149
- const itemPosition = itemRows.findIndex((candidate) => candidate.index === index);
150
- const isFocused = itemPosition === boundedCursor;
151
- const isSelected = selectedIndices.has(index);
152
- 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}`));
153
- }))] }), _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] }));
228
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(DashboardHeader, { title: title, subtitle: subtitle, platformLabel: platformLabel, metrics: visibleMetrics }), _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(FilterRail, { width: layout.railWidth, filterIndex: state.filterIndex, search: state.search, searchMode: state.searchMode, activeSort: activeSort, activeGroup: activeGroup, activeTab: activeTab }), _jsx(QueuePanel, { width: layout.queueWidth, items: items, visibleRows: visibleRows, itemRows: itemRows, renderWindow: renderWindow, rowPositionByIndex: rowPositionByIndex, boundedCursor: boundedCursor, selectedIndices: state.selectedIndices }), _jsx(DecisionPanel, { width: layout.detailWidth, activeTab: activeTab, focusedItem: focusedItem })] }), _jsx(ActionBar, {}), state.showHelp ? _jsx(HelpPanel, {}) : null] }));
229
+ }
230
+ function DashboardHeader({ title, subtitle, platformLabel, metrics, }) {
231
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: "cyan", children: title ?? "Rainy Dashboard" }), _jsx(Text, { color: "gray", children: subtitle ??
232
+ "Check detects, doctor summarizes, dashboard decides, upgrade applies." }), _jsxs(Text, { color: "gray", children: [platformLabel, " keys: arrows or hjkl, Tab changes panel, / search, ? help, Enter confirm"] }), _jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsxs(Text, { children: ["visible ", metrics.total, " | selected ", metrics.selected, " | actionable", " ", metrics.actionable, " | blocked ", metrics.blocked, " | security", " ", metrics.security] }) })] }));
233
+ }
234
+ function FilterRail({ width, filterIndex, search, searchMode, activeSort, activeGroup, activeTab, }) {
235
+ return (_jsxs(Box, { width: width, 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] })] })] }));
236
+ }
237
+ function QueuePanel({ width, items, visibleRows, itemRows, renderWindow, rowPositionByIndex, boundedCursor, selectedIndices, }) {
238
+ return (_jsxs(Box, { marginLeft: 1, width: width, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsxs(Text, { bold: true, children: ["Review Queue", renderWindow.start > 0 ? " [more above]" : "", renderWindow.end < visibleRows.length ? " [more below]" : ""] }), itemRows.length === 0 ? (_jsx(Text, { color: "gray", children: "No review candidates match this view." })) : (renderWindow.rows.map((row) => (_jsx(QueueRow, { row: row, items: items, rowPositionByIndex: rowPositionByIndex, boundedCursor: boundedCursor, selectedIndices: selectedIndices }, row.kind === "group" ? `group:${row.label}` : `${items[row.index ?? 0]?.update.packagePath}:${items[row.index ?? 0]?.update.name}`))))] }));
239
+ }
240
+ function QueueRow({ row, items, rowPositionByIndex, boundedCursor, selectedIndices, }) {
241
+ if (row.kind === "group") {
242
+ return (_jsx(Text, { bold: true, color: "gray", children: row.label }));
243
+ }
244
+ const index = row.index ?? 0;
245
+ const item = items[index];
246
+ const update = item.update;
247
+ const decision = update.decisionState ?? deriveDecision(item);
248
+ const itemPosition = rowPositionByIndex.get(index) ?? -1;
249
+ const isFocused = itemPosition === boundedCursor;
250
+ const isSelected = selectedIndices.has(index);
251
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: isFocused ? "cyan" : "gray", children: [isFocused ? ">" : " ", " ", isSelected ? "[x]" : "[ ]", " "] }), _jsx(Box, { width: 24, children: _jsx(Text, { bold: isFocused, children: truncate(update.name, 22) }) }), _jsx(Box, { width: 10, children: _jsx(Text, { color: diffColor(update.diffType), children: update.diffType }) }), _jsx(Box, { width: 11, children: _jsx(Text, { color: riskColor(update.riskLevel), children: update.riskLevel ?? "low" }) }), _jsx(Box, { width: 12, children: _jsx(Text, { color: decisionColor(decision), children: truncate(decision, 10) }) }), _jsx(Box, { width: 7, children: _jsx(Text, { color: decisionColor(decision), children: update.riskScore ?? "--" }) }), _jsxs(Text, { color: "gray", children: [truncate(update.fromRange, 12), " ", "->", " "] }), _jsx(Text, { color: "green", children: truncate(update.toVersionResolved, 12) })] }));
252
+ }
253
+ function DecisionPanel({ width, activeTab, focusedItem, }) {
254
+ return (_jsxs(Box, { marginLeft: 1, width: width, 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." }))] }));
255
+ }
256
+ function ActionBar() {
257
+ return (_jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsx(Text, { color: "gray", children: "A add visible | N clear visible | S safe | X actionable | B clear blocked | Space toggle | Q finish" }) }));
258
+ }
259
+ function HelpPanel() {
260
+ return (_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 filters for queue slices, search with /, and switch panels with Tab." }), _jsx(Text, { color: "gray", children: "The queue is windowed around the focused package for faster rendering in large workspaces." }), _jsx(Text, { color: "gray", children: "Actionable items can be bulk-selected with X. Blocked items stay easy to clear with B." })] }));
154
261
  }
155
262
  function renderTab(item, tab) {
156
263
  const update = item.update;
157
264
  if (tab === "risk") {
158
- 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." })] }));
265
+ 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:", " ", truncate(update.recommendedAction ?? "Safe to keep in the selected set.", 80)] }), update.riskReasons && update.riskReasons.length > 0 ? (update.riskReasons.slice(0, 5).map((reason) => (_jsxs(Text, { color: "gray", children: ["- ", truncate(reason, 48)] }, reason)))) : (_jsx(Text, { color: "gray", children: "No elevated risk reasons." }))] }));
159
266
  }
160
267
  if (tab === "security") {
161
- 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." })] }));
268
+ 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: ["- ", truncate(`${advisory.severity} ${advisory.cveId}: ${advisory.title}`, 48)] }, `${advisory.packageName}:${advisory.cveId}`)))) : (_jsx(Text, { color: "gray", children: "No security advisories detected." }))] }));
162
269
  }
163
270
  if (tab === "peer") {
164
- 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." })] }));
271
+ 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: ["- ", truncate(`${conflict.requester} requires ${conflict.peer} ${conflict.requiredRange}`, 48)] }, `${conflict.requester}:${conflict.peer}`)))) : (_jsx(Text, { color: "gray", children: "No peer conflicts detected." }))] }));
165
272
  }
166
273
  if (tab === "license") {
167
- 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"] })] }));
274
+ return (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["license status: ", update.licenseStatus ?? "allowed"] }), _jsxs(Text, { children: ["repository: ", truncate(update.repository ?? "unavailable", 48)] }), _jsxs(Text, { children: ["homepage: ", truncate(update.homepage ?? "unavailable", 48)] })] }));
168
275
  }
169
276
  if (tab === "health") {
170
277
  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"] })] }));
171
278
  }
172
279
  if (tab === "changelog") {
173
- 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." })] }));
280
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { children: truncate(update.releaseNotesSummary?.title ?? "Release notes unavailable", 48) }), _jsx(Text, { color: "gray", children: truncate(update.releaseNotesSummary?.excerpt ??
281
+ "Run review with changelog support or inspect the repository manually.", 96) })] }));
174
282
  }
175
- 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."] })] }));
283
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { children: update.name }), _jsxs(Text, { color: "gray", children: ["package: ", truncate(update.packagePath, 48)] }), _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:", " ", truncate(update.recommendedAction ?? "Safe to keep in the selected set.", 80)] })] }));
176
284
  }
177
285
  function buildVisibleRows(items, config) {
178
286
  const filtered = items
@@ -181,7 +289,11 @@ function buildVisibleRows(items, config) {
181
289
  .filter(({ item }) => matchesSearch(item, config.search))
182
290
  .sort((left, right) => compareItems(left.item, right.item, config.sort));
183
291
  if (config.group === "none") {
184
- return filtered.map(({ item, index }) => ({ kind: "item", label: item.update.name, index }));
292
+ return filtered.map(({ item, index }) => ({
293
+ kind: "item",
294
+ label: item.update.name,
295
+ index,
296
+ }));
185
297
  }
186
298
  const rows = [];
187
299
  let currentGroup = "";
@@ -191,15 +303,20 @@ function buildVisibleRows(items, config) {
191
303
  currentGroup = nextGroup;
192
304
  rows.push({ kind: "group", label: nextGroup });
193
305
  }
194
- rows.push({ kind: "item", label: entry.item.update.name, index: entry.index });
306
+ rows.push({
307
+ kind: "item",
308
+ label: entry.item.update.name,
309
+ index: entry.index,
310
+ });
195
311
  }
196
312
  return rows;
197
313
  }
198
314
  function matchesFilter(item, filter) {
199
315
  if (filter === "security")
200
316
  return item.advisories.length > 0;
201
- if (filter === "risky")
317
+ if (filter === "risky") {
202
318
  return item.update.riskLevel === "critical" || item.update.riskLevel === "high";
319
+ }
203
320
  if (filter === "major")
204
321
  return item.update.diffType === "major";
205
322
  if (filter === "peer-conflict")
@@ -208,8 +325,9 @@ function matchesFilter(item, filter) {
208
325
  return item.update.licenseStatus === "denied";
209
326
  if (filter === "unused")
210
327
  return item.unusedIssues.length > 0;
211
- if (filter === "blocked")
328
+ if (filter === "blocked") {
212
329
  return (item.update.decisionState ?? deriveDecision(item)) === "blocked";
330
+ }
213
331
  return true;
214
332
  }
215
333
  function matchesSearch(item, search) {
@@ -221,7 +339,7 @@ function matchesSearch(item, search) {
221
339
  }
222
340
  function compareItems(left, right, sort) {
223
341
  if (sort === "advisories") {
224
- const byAdvisories = (right.advisories.length ?? 0) - (left.advisories.length ?? 0);
342
+ const byAdvisories = right.advisories.length - left.advisories.length;
225
343
  if (byAdvisories !== 0)
226
344
  return byAdvisories;
227
345
  }
@@ -256,8 +374,9 @@ function groupLabel(item, group) {
256
374
  }
257
375
  if (group === "risk")
258
376
  return item.update.riskLevel ?? "low";
259
- if (group === "decision")
377
+ if (group === "decision") {
260
378
  return item.update.decisionState ?? deriveDecision(item);
379
+ }
261
380
  return "all";
262
381
  }
263
382
  function addVisible(selected, rows) {
@@ -275,7 +394,8 @@ function removeVisible(selected, rows) {
275
394
  function selectSafe(selected, rows, items) {
276
395
  const next = new Set(selected);
277
396
  for (const row of rows) {
278
- if ((items[row.index]?.update.decisionState ?? deriveDecision(items[row.index])) === "safe") {
397
+ if ((items[row.index]?.update.decisionState ??
398
+ deriveDecision(items[row.index])) === "safe") {
279
399
  next.add(row.index);
280
400
  }
281
401
  }
@@ -284,14 +404,27 @@ function selectSafe(selected, rows, items) {
284
404
  function clearBlocked(selected, rows, items) {
285
405
  const next = new Set(selected);
286
406
  for (const row of rows) {
287
- if ((items[row.index]?.update.decisionState ?? deriveDecision(items[row.index])) === "blocked") {
407
+ if ((items[row.index]?.update.decisionState ??
408
+ deriveDecision(items[row.index])) === "blocked") {
288
409
  next.delete(row.index);
289
410
  }
290
411
  }
291
412
  return next;
292
413
  }
414
+ function selectActionable(selected, rows, items) {
415
+ const next = new Set(selected);
416
+ for (const row of rows) {
417
+ const decision = items[row.index]?.update.decisionState ??
418
+ deriveDecision(items[row.index]);
419
+ if (decision === "actionable" || decision === "review") {
420
+ next.add(row.index);
421
+ }
422
+ }
423
+ return next;
424
+ }
293
425
  function deriveDecision(item) {
294
- if (item.update.peerConflictSeverity === "error" || item.update.licenseStatus === "denied") {
426
+ if (item.update.peerConflictSeverity === "error" ||
427
+ item.update.licenseStatus === "denied") {
295
428
  return "blocked";
296
429
  }
297
430
  if ((item.update.advisoryCount ?? 0) > 0 || item.update.riskLevel === "critical") {
@@ -354,9 +487,76 @@ function decisionColor(label) {
354
487
  return "green";
355
488
  }
356
489
  }
490
+ function clamp(value, min, max) {
491
+ return Math.min(max, Math.max(min, value));
492
+ }
493
+ function createDashboardLayout(stdoutWidth) {
494
+ const railWidth = clamp(Math.floor(stdoutWidth * 0.2), 24, 30);
495
+ const detailWidth = clamp(Math.floor(stdoutWidth * 0.26), 36, 54);
496
+ const queueWidth = Math.max(48, stdoutWidth - railWidth - detailWidth - 8);
497
+ return { railWidth, detailWidth, queueWidth };
498
+ }
499
+ function truncate(value, maxLength) {
500
+ if (value.length <= maxLength)
501
+ return value;
502
+ if (maxLength <= 1)
503
+ return value.slice(0, maxLength);
504
+ return `${value.slice(0, Math.max(1, maxLength - 1))}…`;
505
+ }
506
+ function createRowPositionMap(rows) {
507
+ const map = new Map();
508
+ rows.forEach((row, position) => {
509
+ map.set(row.index, position);
510
+ });
511
+ return map;
512
+ }
513
+ function createRenderWindow(config) {
514
+ const maxRows = clamp(config.stdoutHeight - 16, 8, 18);
515
+ if (config.visibleRows.length <= maxRows) {
516
+ return {
517
+ rows: config.visibleRows,
518
+ start: 0,
519
+ end: config.visibleRows.length,
520
+ };
521
+ }
522
+ const focusedRow = Math.max(0, config.visibleRows.findIndex((row) => row.kind === "item" && row.index === config.focusedIndex));
523
+ let start = Math.max(0, focusedRow - Math.floor(maxRows / 2));
524
+ let end = Math.min(config.visibleRows.length, start + maxRows);
525
+ start = Math.max(0, end - maxRows);
526
+ return {
527
+ rows: config.visibleRows.slice(start, end),
528
+ start,
529
+ end,
530
+ };
531
+ }
532
+ function summarizeVisibleItems(rows, items, selected) {
533
+ let actionable = 0;
534
+ let blocked = 0;
535
+ let security = 0;
536
+ let selectedCount = 0;
537
+ for (const row of rows) {
538
+ const item = items[row.index];
539
+ const decision = item.update.decisionState ?? deriveDecision(item);
540
+ if (selected.has(row.index))
541
+ selectedCount += 1;
542
+ if (decision === "actionable" || decision === "review")
543
+ actionable += 1;
544
+ if (decision === "blocked")
545
+ blocked += 1;
546
+ if (item.advisories.length > 0)
547
+ security += 1;
548
+ }
549
+ return {
550
+ total: rows.length,
551
+ selected: selectedCount,
552
+ actionable,
553
+ blocked,
554
+ security,
555
+ };
556
+ }
357
557
  export async function runTui(items, options) {
358
558
  return new Promise((resolve) => {
359
- const { unmount } = render(_jsx(TuiApp, { items: items, title: options?.title, subtitle: options?.subtitle, onComplete: (selected) => {
559
+ const { unmount } = render(_jsx(TuiApp, { items: items, title: options?.title, subtitle: options?.subtitle, initialFilter: options?.initialFilter, initialTab: options?.initialTab, onComplete: (selected) => {
360
560
  unmount();
361
561
  resolve(selected);
362
562
  } }));
@@ -0,0 +1,6 @@
1
+ export interface ShellInvocation {
2
+ shell: string;
3
+ args: string[];
4
+ display: string;
5
+ }
6
+ export declare function buildShellInvocation(command: string, runtimePlatform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): ShellInvocation;
@@ -0,0 +1,18 @@
1
+ export function buildShellInvocation(command, runtimePlatform = process.platform, env = process.env) {
2
+ if (runtimePlatform === "win32") {
3
+ const shell = env.COMSPEC?.trim() || "cmd.exe";
4
+ const args = ["/d", "/s", "/c", command];
5
+ return {
6
+ shell,
7
+ args,
8
+ display: [shell, ...args].join(" "),
9
+ };
10
+ }
11
+ const shell = env.SHELL?.trim() || "sh";
12
+ const args = ["-lc", command];
13
+ return {
14
+ shell,
15
+ args,
16
+ display: [shell, ...args].join(" "),
17
+ };
18
+ }
@@ -1 +1,7 @@
1
- export declare function discoverPackageDirs(cwd: string, workspaceMode: boolean): Promise<string[]>;
1
+ import type { DependencyKind } from "../types/index.js";
2
+ import { type GitScopeOptions } from "../git/scope.js";
3
+ export declare function discoverPackageDirs(cwd: string, workspaceMode: boolean, options?: {
4
+ git?: GitScopeOptions;
5
+ includeKinds?: DependencyKind[];
6
+ includeDependents?: boolean;
7
+ }): Promise<string[]>;