@rainy-updates/cli 0.5.7 → 0.6.1

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 (105) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +90 -31
  3. package/dist/bin/cli.js +11 -126
  4. package/dist/bin/dispatch.js +35 -32
  5. package/dist/bin/help.js +79 -2
  6. package/dist/bin/main.d.ts +1 -0
  7. package/dist/bin/main.js +126 -0
  8. package/dist/cache/cache.js +13 -11
  9. package/dist/commands/audit/parser.js +38 -2
  10. package/dist/commands/audit/runner.js +41 -61
  11. package/dist/commands/audit/targets.js +13 -13
  12. package/dist/commands/bisect/oracle.js +31 -11
  13. package/dist/commands/bisect/parser.js +3 -3
  14. package/dist/commands/bisect/runner.js +16 -8
  15. package/dist/commands/changelog/fetcher.js +11 -5
  16. package/dist/commands/dashboard/parser.js +144 -1
  17. package/dist/commands/dashboard/runner.d.ts +2 -2
  18. package/dist/commands/dashboard/runner.js +67 -37
  19. package/dist/commands/doctor/parser.js +53 -4
  20. package/dist/commands/doctor/runner.js +2 -2
  21. package/dist/commands/ga/parser.js +43 -4
  22. package/dist/commands/ga/runner.js +22 -13
  23. package/dist/commands/health/parser.js +38 -2
  24. package/dist/commands/health/runner.js +5 -1
  25. package/dist/commands/hook/parser.d.ts +2 -0
  26. package/dist/commands/hook/parser.js +40 -0
  27. package/dist/commands/hook/runner.d.ts +2 -0
  28. package/dist/commands/hook/runner.js +174 -0
  29. package/dist/commands/licenses/parser.js +39 -0
  30. package/dist/commands/licenses/runner.js +9 -5
  31. package/dist/commands/resolve/graph/builder.js +5 -1
  32. package/dist/commands/resolve/parser.js +39 -0
  33. package/dist/commands/resolve/runner.js +14 -4
  34. package/dist/commands/review/parser.js +101 -4
  35. package/dist/commands/review/runner.js +31 -5
  36. package/dist/commands/snapshot/parser.js +39 -0
  37. package/dist/commands/snapshot/runner.js +21 -18
  38. package/dist/commands/snapshot/store.d.ts +0 -12
  39. package/dist/commands/snapshot/store.js +26 -38
  40. package/dist/commands/unused/parser.js +39 -0
  41. package/dist/commands/unused/runner.js +10 -8
  42. package/dist/commands/unused/scanner.d.ts +2 -1
  43. package/dist/commands/unused/scanner.js +65 -52
  44. package/dist/config/loader.d.ts +2 -2
  45. package/dist/config/loader.js +2 -5
  46. package/dist/config/policy.js +20 -11
  47. package/dist/core/analysis/run-silenced.js +0 -1
  48. package/dist/core/artifacts.js +6 -5
  49. package/dist/core/baseline.js +3 -5
  50. package/dist/core/check.js +7 -3
  51. package/dist/core/ci.js +52 -1
  52. package/dist/core/decision-plan.d.ts +14 -0
  53. package/dist/core/decision-plan.js +107 -0
  54. package/dist/core/doctor/result.js +8 -5
  55. package/dist/core/fix-pr-batch.js +38 -28
  56. package/dist/core/fix-pr.js +27 -24
  57. package/dist/core/init-ci.js +34 -28
  58. package/dist/core/options.d.ts +4 -1
  59. package/dist/core/options.js +152 -4
  60. package/dist/core/review-model.js +3 -0
  61. package/dist/core/summary.js +6 -0
  62. package/dist/core/upgrade.js +64 -2
  63. package/dist/core/verification.d.ts +2 -0
  64. package/dist/core/verification.js +108 -0
  65. package/dist/core/warm-cache.js +7 -3
  66. package/dist/generated/version.d.ts +1 -0
  67. package/dist/generated/version.js +2 -0
  68. package/dist/git/scope.d.ts +19 -0
  69. package/dist/git/scope.js +167 -0
  70. package/dist/index.d.ts +2 -1
  71. package/dist/index.js +1 -0
  72. package/dist/output/format.js +15 -0
  73. package/dist/output/github.js +6 -0
  74. package/dist/output/sarif.js +12 -18
  75. package/dist/parsers/package-json.js +2 -4
  76. package/dist/pm/detect.d.ts +40 -1
  77. package/dist/pm/detect.js +152 -9
  78. package/dist/pm/install.d.ts +3 -1
  79. package/dist/pm/install.js +18 -17
  80. package/dist/registry/npm.js +34 -76
  81. package/dist/rup +0 -0
  82. package/dist/types/index.d.ts +134 -5
  83. package/dist/ui/tui.d.ts +4 -1
  84. package/dist/ui/tui.js +156 -67
  85. package/dist/utils/io.js +5 -6
  86. package/dist/utils/lockfile.js +24 -19
  87. package/dist/utils/runtime-paths.d.ts +4 -0
  88. package/dist/utils/runtime-paths.js +35 -0
  89. package/dist/utils/runtime.d.ts +7 -0
  90. package/dist/utils/runtime.js +32 -0
  91. package/dist/workspace/discover.d.ts +7 -1
  92. package/dist/workspace/discover.js +67 -54
  93. package/package.json +24 -19
  94. package/dist/ui/dashboard/DashboardTUI.d.ts +0 -6
  95. package/dist/ui/dashboard/DashboardTUI.js +0 -34
  96. package/dist/ui/dashboard/components/DetailPanel.d.ts +0 -4
  97. package/dist/ui/dashboard/components/DetailPanel.js +0 -30
  98. package/dist/ui/dashboard/components/Footer.d.ts +0 -4
  99. package/dist/ui/dashboard/components/Footer.js +0 -9
  100. package/dist/ui/dashboard/components/Header.d.ts +0 -4
  101. package/dist/ui/dashboard/components/Header.js +0 -12
  102. package/dist/ui/dashboard/components/Sidebar.d.ts +0 -4
  103. package/dist/ui/dashboard/components/Sidebar.js +0 -23
  104. package/dist/ui/dashboard/store.d.ts +0 -34
  105. package/dist/ui/dashboard/store.js +0 -148
package/dist/ui/tui.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useState } from "react";
2
+ import React from "react";
3
3
  import { Box, render, Text, useInput } from "ink";
4
4
  const FILTER_ORDER = [
5
5
  "all",
@@ -22,20 +22,86 @@ const DETAIL_TABS = [
22
22
  "health",
23
23
  "changelog",
24
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";
25
+ function tuiReducer(state, action) {
26
+ switch (action.type) {
27
+ case "SET_SEARCH_MODE":
28
+ return {
29
+ ...state,
30
+ searchMode: action.active,
31
+ ...(action.active ? {} : { search: "", cursorIndex: 0 }),
32
+ };
33
+ case "SET_SEARCH":
34
+ return { ...state, search: action.value, cursorIndex: 0 };
35
+ case "APPEND_SEARCH":
36
+ return { ...state, search: state.search + action.value, cursorIndex: 0 };
37
+ case "BACKSPACE_SEARCH":
38
+ return { ...state, search: state.search.slice(0, -1), cursorIndex: 0 };
39
+ case "TOGGLE_HELP":
40
+ return { ...state, showHelp: !state.showHelp };
41
+ case "SET_HELP":
42
+ return { ...state, showHelp: action.active };
43
+ case "MOVE_FILTER":
44
+ return {
45
+ ...state,
46
+ filterIndex: Math.min(action.max, Math.max(0, state.filterIndex + action.direction)),
47
+ cursorIndex: 0,
48
+ };
49
+ case "MOVE_CURSOR":
50
+ return {
51
+ ...state,
52
+ cursorIndex: Math.min(action.max, Math.max(0, state.cursorIndex + action.direction)),
53
+ };
54
+ case "RESET_CURSOR":
55
+ return { ...state, cursorIndex: 0 };
56
+ case "CYCLE_SORT":
57
+ return {
58
+ ...state,
59
+ sortIndex: (state.sortIndex + 1) % action.max,
60
+ cursorIndex: 0,
61
+ };
62
+ case "CYCLE_GROUP":
63
+ return {
64
+ ...state,
65
+ groupIndex: (state.groupIndex + 1) % action.max,
66
+ cursorIndex: 0,
67
+ };
68
+ case "CYCLE_TAB":
69
+ return { ...state, tabIndex: (state.tabIndex + 1) % action.max };
70
+ case "SET_SELECTED":
71
+ return { ...state, selectedIndices: action.indices };
72
+ case "TOGGLE_SELECTED": {
73
+ const next = new Set(state.selectedIndices);
74
+ if (next.has(action.index))
75
+ next.delete(action.index);
76
+ else
77
+ next.add(action.index);
78
+ return { ...state, selectedIndices: next };
79
+ }
80
+ default:
81
+ return state;
82
+ }
83
+ }
84
+ function TuiApp({ items, title, subtitle, onComplete }) {
85
+ const [state, dispatch] = React.useReducer(tuiReducer, undefined, () => ({
86
+ cursorIndex: 0,
87
+ filterIndex: 0,
88
+ sortIndex: 0,
89
+ groupIndex: 0,
90
+ tabIndex: 0,
91
+ showHelp: false,
92
+ searchMode: false,
93
+ search: "",
94
+ selectedIndices: new Set(items.flatMap((item, index) => item.update.selectedByDefault === false ? [] : [index])),
95
+ }));
96
+ const activeFilter = FILTER_ORDER[state.filterIndex] ?? "all";
97
+ const activeSort = SORT_ORDER[state.sortIndex] ?? "risk";
98
+ const activeGroup = GROUP_ORDER[state.groupIndex] ?? "none";
99
+ const activeTab = DETAIL_TABS[state.tabIndex] ?? "overview";
100
+ const searchMode = state.searchMode;
101
+ const search = state.search;
102
+ const showHelp = state.showHelp;
103
+ const selectedIndices = state.selectedIndices;
104
+ const filterIndex = state.filterIndex;
39
105
  const visibleRows = buildVisibleRows(items, {
40
106
  filter: activeFilter,
41
107
  sort: activeSort,
@@ -43,91 +109,100 @@ function TuiApp({ items, onComplete }) {
43
109
  search,
44
110
  });
45
111
  const itemRows = visibleRows.filter((row) => row.kind === "item" && typeof row.index === "number");
46
- const boundedCursor = Math.min(cursorIndex, Math.max(0, itemRows.length - 1));
112
+ const boundedCursor = Math.min(state.cursorIndex, Math.max(0, itemRows.length - 1));
47
113
  const focusedIndex = itemRows[boundedCursor]?.index ?? 0;
48
114
  const focusedItem = items[focusedIndex];
49
115
  useInput((input, key) => {
50
116
  if (searchMode) {
51
117
  if (key.escape) {
52
- setSearchMode(false);
53
- setSearch("");
54
- setCursorIndex(0);
118
+ dispatch({ type: "SET_SEARCH_MODE", active: false });
55
119
  return;
56
120
  }
57
121
  if (key.return) {
58
- setSearchMode(false);
59
- setCursorIndex(0);
122
+ dispatch({ type: "SET_SEARCH_MODE", active: false });
123
+ dispatch({ type: "SET_SEARCH", value: search }); // keeps search but exits mode
60
124
  return;
61
125
  }
62
126
  if (key.backspace || key.delete) {
63
- setSearch((value) => value.slice(0, -1));
64
- setCursorIndex(0);
127
+ dispatch({ type: "BACKSPACE_SEARCH" });
65
128
  return;
66
129
  }
67
130
  if (input && !key.ctrl && !key.meta) {
68
- setSearch((value) => value + input);
69
- setCursorIndex(0);
131
+ dispatch({ type: "APPEND_SEARCH", value: input });
70
132
  }
71
133
  return;
72
134
  }
73
135
  if (input === "/") {
74
- setSearchMode(true);
136
+ dispatch({ type: "SET_SEARCH_MODE", active: true });
75
137
  return;
76
138
  }
77
139
  if (input === "?") {
78
- setShowHelp((value) => !value);
140
+ dispatch({ type: "TOGGLE_HELP" });
79
141
  return;
80
142
  }
81
143
  if (key.escape && showHelp) {
82
- setShowHelp(false);
144
+ dispatch({ type: "SET_HELP", active: false });
83
145
  return;
84
146
  }
85
147
  if (key.leftArrow) {
86
- setFilterIndex((prev) => Math.max(0, prev - 1));
87
- setCursorIndex(0);
148
+ dispatch({
149
+ type: "MOVE_FILTER",
150
+ direction: -1,
151
+ max: FILTER_ORDER.length - 1,
152
+ });
88
153
  }
89
154
  if (key.rightArrow) {
90
- setFilterIndex((prev) => Math.min(FILTER_ORDER.length - 1, prev + 1));
91
- setCursorIndex(0);
155
+ dispatch({
156
+ type: "MOVE_FILTER",
157
+ direction: 1,
158
+ max: FILTER_ORDER.length - 1,
159
+ });
92
160
  }
93
161
  if (key.upArrow) {
94
- setCursorIndex((prev) => Math.max(0, prev - 1));
162
+ dispatch({
163
+ type: "MOVE_CURSOR",
164
+ direction: -1,
165
+ max: itemRows.length - 1,
166
+ });
95
167
  }
96
168
  if (key.downArrow) {
97
- setCursorIndex((prev) => Math.min(itemRows.length - 1, Math.max(0, prev + 1)));
169
+ dispatch({ type: "MOVE_CURSOR", direction: 1, max: itemRows.length - 1 });
98
170
  }
99
171
  if (input === "o") {
100
- setSortIndex((prev) => (prev + 1) % SORT_ORDER.length);
101
- setCursorIndex(0);
172
+ dispatch({ type: "CYCLE_SORT", max: SORT_ORDER.length });
102
173
  }
103
174
  if (input === "g") {
104
- setGroupIndex((prev) => (prev + 1) % GROUP_ORDER.length);
105
- setCursorIndex(0);
175
+ dispatch({ type: "CYCLE_GROUP", max: GROUP_ORDER.length });
106
176
  }
107
177
  if (key.tab) {
108
- setTabIndex((prev) => (prev + 1) % DETAIL_TABS.length);
178
+ dispatch({ type: "CYCLE_TAB", max: DETAIL_TABS.length });
109
179
  }
110
180
  if (input === "a") {
111
- setSelectedIndices((prev) => addVisible(prev, itemRows));
181
+ dispatch({
182
+ type: "SET_SELECTED",
183
+ indices: addVisible(selectedIndices, itemRows),
184
+ });
112
185
  }
113
186
  if (input === "n") {
114
- setSelectedIndices((prev) => removeVisible(prev, itemRows));
187
+ dispatch({
188
+ type: "SET_SELECTED",
189
+ indices: removeVisible(selectedIndices, itemRows),
190
+ });
115
191
  }
116
192
  if (input === "s") {
117
- setSelectedIndices((prev) => selectSafe(prev, itemRows, items));
193
+ dispatch({
194
+ type: "SET_SELECTED",
195
+ indices: selectSafe(selectedIndices, itemRows, items),
196
+ });
118
197
  }
119
198
  if (input === "b") {
120
- setSelectedIndices((prev) => clearBlocked(prev, itemRows, items));
199
+ dispatch({
200
+ type: "SET_SELECTED",
201
+ indices: clearBlocked(selectedIndices, itemRows, items),
202
+ });
121
203
  }
122
204
  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
- });
205
+ dispatch({ type: "TOGGLE_SELECTED", index: focusedIndex });
131
206
  }
132
207
  if (input === "q" || key.escape) {
133
208
  onComplete(items.filter((_, index) => selectedIndices.has(index)));
@@ -137,7 +212,8 @@ function TuiApp({ items, onComplete }) {
137
212
  onComplete(items.filter((_, index) => selectedIndices.has(index)));
138
213
  }
139
214
  });
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) => {
215
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: title ?? "Rainy Dashboard" }), _jsx(Text, { color: "gray", children: subtitle ??
216
+ "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) => {
141
217
  if (row.kind === "group") {
142
218
  return (_jsx(Text, { bold: true, color: "gray", children: row.label }, `group:${row.label}`));
143
219
  }
@@ -149,18 +225,18 @@ function TuiApp({ items, onComplete }) {
149
225
  const isFocused = itemPosition === boundedCursor;
150
226
  const isSelected = selectedIndices.has(index);
151
227
  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] }));
228
+ }))] }), _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
229
  }
154
230
  function renderTab(item, tab) {
155
231
  const update = item.update;
156
232
  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." })] }));
233
+ 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
234
  }
159
235
  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." })] }));
236
+ 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
237
  }
162
238
  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." })] }));
239
+ 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
240
  }
165
241
  if (tab === "license") {
166
242
  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"] })] }));
@@ -169,9 +245,10 @@ function renderTab(item, tab) {
169
245
  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
246
  }
171
247
  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." })] }));
248
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { children: update.releaseNotesSummary?.title ?? "Release notes unavailable" }), _jsx(Text, { color: "gray", children: update.releaseNotesSummary?.excerpt ??
249
+ "Run review with changelog support or inspect the repository manually." })] }));
173
250
  }
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."] })] }));
251
+ 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
252
  }
176
253
  function buildVisibleRows(items, config) {
177
254
  const filtered = items
@@ -180,7 +257,11 @@ function buildVisibleRows(items, config) {
180
257
  .filter(({ item }) => matchesSearch(item, config.search))
181
258
  .sort((left, right) => compareItems(left.item, right.item, config.sort));
182
259
  if (config.group === "none") {
183
- return filtered.map(({ item, index }) => ({ kind: "item", label: item.update.name, index }));
260
+ return filtered.map(({ item, index }) => ({
261
+ kind: "item",
262
+ label: item.update.name,
263
+ index,
264
+ }));
184
265
  }
185
266
  const rows = [];
186
267
  let currentGroup = "";
@@ -190,7 +271,11 @@ function buildVisibleRows(items, config) {
190
271
  currentGroup = nextGroup;
191
272
  rows.push({ kind: "group", label: nextGroup });
192
273
  }
193
- rows.push({ kind: "item", label: entry.item.update.name, index: entry.index });
274
+ rows.push({
275
+ kind: "item",
276
+ label: entry.item.update.name,
277
+ index: entry.index,
278
+ });
194
279
  }
195
280
  return rows;
196
281
  }
@@ -198,7 +283,7 @@ function matchesFilter(item, filter) {
198
283
  if (filter === "security")
199
284
  return item.advisories.length > 0;
200
285
  if (filter === "risky")
201
- return item.update.riskLevel === "critical" || item.update.riskLevel === "high";
286
+ return (item.update.riskLevel === "critical" || item.update.riskLevel === "high");
202
287
  if (filter === "major")
203
288
  return item.update.diffType === "major";
204
289
  if (filter === "peer-conflict")
@@ -274,7 +359,8 @@ function removeVisible(selected, rows) {
274
359
  function selectSafe(selected, rows, items) {
275
360
  const next = new Set(selected);
276
361
  for (const row of rows) {
277
- if ((items[row.index]?.update.decisionState ?? deriveDecision(items[row.index])) === "safe") {
362
+ if ((items[row.index]?.update.decisionState ??
363
+ deriveDecision(items[row.index])) === "safe") {
278
364
  next.add(row.index);
279
365
  }
280
366
  }
@@ -283,17 +369,20 @@ function selectSafe(selected, rows, items) {
283
369
  function clearBlocked(selected, rows, items) {
284
370
  const next = new Set(selected);
285
371
  for (const row of rows) {
286
- if ((items[row.index]?.update.decisionState ?? deriveDecision(items[row.index])) === "blocked") {
372
+ if ((items[row.index]?.update.decisionState ??
373
+ deriveDecision(items[row.index])) === "blocked") {
287
374
  next.delete(row.index);
288
375
  }
289
376
  }
290
377
  return next;
291
378
  }
292
379
  function deriveDecision(item) {
293
- if (item.update.peerConflictSeverity === "error" || item.update.licenseStatus === "denied") {
380
+ if (item.update.peerConflictSeverity === "error" ||
381
+ item.update.licenseStatus === "denied") {
294
382
  return "blocked";
295
383
  }
296
- if ((item.update.advisoryCount ?? 0) > 0 || item.update.riskLevel === "critical") {
384
+ if ((item.update.advisoryCount ?? 0) > 0 ||
385
+ item.update.riskLevel === "critical") {
297
386
  return "actionable";
298
387
  }
299
388
  if (item.update.riskLevel === "high" || item.update.diffType === "major") {
@@ -353,9 +442,9 @@ function decisionColor(label) {
353
442
  return "green";
354
443
  }
355
444
  }
356
- export async function runTui(items) {
445
+ export async function runTui(items, options) {
357
446
  return new Promise((resolve) => {
358
- const { unmount } = render(_jsx(TuiApp, { items: items, onComplete: (selected) => {
447
+ const { unmount } = render(_jsx(TuiApp, { items: items, title: options?.title, subtitle: options?.subtitle, onComplete: (selected) => {
359
448
  unmount();
360
449
  resolve(selected);
361
450
  } }));
package/dist/utils/io.js CHANGED
@@ -1,10 +1,9 @@
1
- import { promises as fs } from "node:fs";
1
+ import { mkdir, rename } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import crypto from "node:crypto";
4
3
  export async function writeFileAtomic(filePath, content) {
5
4
  const dir = path.dirname(filePath);
6
- await fs.mkdir(dir, { recursive: true });
7
- const tempPath = path.join(dir, `.tmp-${path.basename(filePath)}-${crypto.randomUUID()}`);
8
- await fs.writeFile(tempPath, content, "utf8");
9
- await fs.rename(tempPath, filePath);
5
+ const tempFile = path.join(dir, `.tmp-${path.basename(filePath)}-${crypto.randomUUID()}`);
6
+ await mkdir(dir, { recursive: true });
7
+ await Bun.write(tempFile, content);
8
+ await rename(tempFile, filePath);
10
9
  }
@@ -1,18 +1,16 @@
1
- import { promises as fs } from "node:fs";
2
1
  import path from "node:path";
3
- import { createHash } from "node:crypto";
4
- const LOCKFILE_NAMES = ["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock"];
2
+ const LOCKFILE_NAMES = [
3
+ "package-lock.json",
4
+ "npm-shrinkwrap.json",
5
+ "pnpm-lock.yaml",
6
+ "yarn.lock",
7
+ "bun.lock",
8
+ ];
5
9
  export async function captureLockfileSnapshot(cwd) {
6
10
  const snapshot = new Map();
7
11
  for (const name of LOCKFILE_NAMES) {
8
12
  const filePath = path.join(cwd, name);
9
- try {
10
- const content = await fs.readFile(filePath);
11
- snapshot.set(filePath, hashBuffer(content));
12
- }
13
- catch {
14
- snapshot.set(filePath, null);
15
- }
13
+ snapshot.set(filePath, await readLockfileHash(filePath));
16
14
  }
17
15
  return snapshot;
18
16
  }
@@ -20,14 +18,7 @@ export async function changedLockfiles(cwd, before) {
20
18
  const changed = [];
21
19
  for (const name of LOCKFILE_NAMES) {
22
20
  const filePath = path.join(cwd, name);
23
- let current = null;
24
- try {
25
- const content = await fs.readFile(filePath);
26
- current = hashBuffer(content);
27
- }
28
- catch {
29
- current = null;
30
- }
21
+ const current = await readLockfileHash(filePath);
31
22
  if ((before.get(filePath) ?? null) !== current) {
32
23
  changed.push(filePath);
33
24
  }
@@ -39,6 +30,20 @@ export function validateLockfileMode(mode, install) {
39
30
  throw new Error("--lockfile-mode update requires --install to update lockfiles deterministically.");
40
31
  }
41
32
  }
33
+ async function readLockfileHash(filePath) {
34
+ try {
35
+ const file = Bun.file(filePath);
36
+ if (!(await file.exists()))
37
+ return null;
38
+ const content = await file.bytes();
39
+ return hashBuffer(content);
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
42
45
  function hashBuffer(value) {
43
- return createHash("sha256").update(value).digest("hex");
46
+ const hasher = new Bun.CryptoHasher("sha256");
47
+ hasher.update(value);
48
+ return hasher.digest("hex");
44
49
  }
@@ -0,0 +1,4 @@
1
+ export declare function getHomeDir(): string;
2
+ export declare function getCacheDir(appName?: string): string;
3
+ export declare function getTempDir(): string;
4
+ export declare function createTempDir(prefix: string): Promise<string>;
@@ -0,0 +1,35 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { getRuntimeCwd, readEnv } from "./runtime.js";
4
+ function resolveHomeDir() {
5
+ return readEnv("HOME") ?? readEnv("USERPROFILE") ?? getRuntimeCwd();
6
+ }
7
+ export function getHomeDir() {
8
+ return resolveHomeDir();
9
+ }
10
+ export function getCacheDir(appName = "rainy-updates") {
11
+ return path.join(readEnv("XDG_CACHE_HOME") ?? path.join(resolveHomeDir(), ".cache"), appName);
12
+ }
13
+ export function getTempDir() {
14
+ return (readEnv("BUN_TMPDIR") ??
15
+ readEnv("TMPDIR") ??
16
+ readEnv("TMP") ??
17
+ readEnv("TEMP") ??
18
+ "/tmp");
19
+ }
20
+ export async function createTempDir(prefix) {
21
+ const baseDir = getTempDir();
22
+ for (let attempt = 0; attempt < 16; attempt += 1) {
23
+ const candidate = path.join(baseDir, `${prefix}${crypto.randomUUID()}`);
24
+ try {
25
+ await mkdir(candidate, { recursive: false });
26
+ return candidate;
27
+ }
28
+ catch (error) {
29
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST") {
30
+ throw error;
31
+ }
32
+ }
33
+ }
34
+ throw new Error(`Unable to create temp directory for prefix ${prefix}`);
35
+ }
@@ -0,0 +1,7 @@
1
+ export declare function getRuntimeCwd(): string;
2
+ export declare function getRuntimeArgv(): string[];
3
+ export declare function readEnv(name: string): string | undefined;
4
+ export declare function writeStdout(message: string): void;
5
+ export declare function writeStderr(message: string): void;
6
+ export declare function setRuntimeExitCode(code: number): void;
7
+ export declare function exitProcess(code: number): never;
@@ -0,0 +1,32 @@
1
+ export function getRuntimeCwd() {
2
+ return process.cwd();
3
+ }
4
+ export function getRuntimeArgv() {
5
+ if (typeof process !== "undefined" && Array.isArray(process.argv)) {
6
+ return process.argv.slice(2);
7
+ }
8
+ if (typeof Bun !== "undefined" && Array.isArray(Bun.argv)) {
9
+ return Bun.argv.slice(2);
10
+ }
11
+ return [];
12
+ }
13
+ export function readEnv(name) {
14
+ if (typeof Bun !== "undefined") {
15
+ const value = Bun.env[name];
16
+ if (value)
17
+ return value;
18
+ }
19
+ return process.env[name];
20
+ }
21
+ export function writeStdout(message) {
22
+ process.stdout.write(message);
23
+ }
24
+ export function writeStderr(message) {
25
+ process.stderr.write(message);
26
+ }
27
+ export function setRuntimeExitCode(code) {
28
+ process.exitCode = code;
29
+ }
30
+ export function exitProcess(code) {
31
+ process.exit(code);
32
+ }
@@ -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[]>;