@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.
- package/CHANGELOG.md +136 -0
- package/README.md +5 -0
- package/dist/bin/cli.js +16 -438
- package/dist/bin/dispatch.d.ts +16 -0
- package/dist/bin/dispatch.js +150 -0
- package/dist/bin/help.d.ts +1 -0
- package/dist/bin/help.js +284 -0
- package/dist/commands/audit/runner.js +43 -26
- package/dist/commands/dashboard/parser.d.ts +2 -0
- package/dist/commands/dashboard/parser.js +59 -0
- package/dist/commands/dashboard/runner.d.ts +2 -0
- package/dist/commands/dashboard/runner.js +47 -0
- package/dist/commands/doctor/parser.js +12 -0
- package/dist/commands/doctor/runner.js +5 -2
- package/dist/commands/ga/parser.d.ts +2 -0
- package/dist/commands/ga/parser.js +50 -0
- package/dist/commands/ga/runner.d.ts +2 -0
- package/dist/commands/ga/runner.js +129 -0
- package/dist/commands/resolve/runner.js +7 -3
- package/dist/commands/review/parser.js +6 -0
- package/dist/commands/review/runner.js +4 -3
- package/dist/core/analysis/options.d.ts +6 -0
- package/dist/core/analysis/options.js +69 -0
- package/dist/core/analysis/review-items.d.ts +4 -0
- package/dist/core/analysis/review-items.js +128 -0
- package/dist/core/analysis/run-silenced.d.ts +1 -0
- package/dist/core/analysis/run-silenced.js +14 -0
- package/dist/core/analysis-bundle.d.ts +4 -0
- package/dist/core/analysis-bundle.js +33 -0
- package/dist/core/artifacts.d.ts +3 -0
- package/dist/core/artifacts.js +48 -0
- package/dist/core/check.js +6 -1
- package/dist/core/doctor/findings.d.ts +2 -0
- package/dist/core/doctor/findings.js +166 -0
- package/dist/core/doctor/render.d.ts +3 -0
- package/dist/core/doctor/render.js +44 -0
- package/dist/core/doctor/result.d.ts +2 -0
- package/dist/core/doctor/result.js +55 -0
- package/dist/core/doctor/score.d.ts +5 -0
- package/dist/core/doctor/score.js +28 -0
- package/dist/core/options.d.ts +7 -1
- package/dist/core/options.js +14 -0
- package/dist/core/review-model.d.ts +3 -3
- package/dist/core/review-model.js +55 -245
- package/dist/core/review-verdict.d.ts +2 -0
- package/dist/core/review-verdict.js +14 -0
- package/dist/core/summary.js +19 -0
- package/dist/output/format.js +22 -0
- package/dist/output/github.js +12 -0
- package/dist/output/sarif.js +16 -0
- package/dist/types/index.d.ts +120 -0
- package/dist/ui/dashboard/DashboardTUI.d.ts +6 -0
- package/dist/ui/dashboard/DashboardTUI.js +34 -0
- package/dist/ui/dashboard/components/DetailPanel.d.ts +4 -0
- package/dist/ui/dashboard/components/DetailPanel.js +30 -0
- package/dist/ui/dashboard/components/Footer.d.ts +4 -0
- package/dist/ui/dashboard/components/Footer.js +9 -0
- package/dist/ui/dashboard/components/Header.d.ts +4 -0
- package/dist/ui/dashboard/components/Header.js +12 -0
- package/dist/ui/dashboard/components/Sidebar.d.ts +4 -0
- package/dist/ui/dashboard/components/Sidebar.js +23 -0
- package/dist/ui/dashboard/store.d.ts +34 -0
- package/dist/ui/dashboard/store.js +148 -0
- package/dist/ui/tui.d.ts +2 -2
- package/dist/ui/tui.js +310 -79
- 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
|
-
|
|
11
|
-
|
|
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
|
|
38
|
-
if (
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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, {
|
|
358
|
+
const { unmount } = render(_jsx(TuiApp, { items: items, onComplete: (selected) => {
|
|
128
359
|
unmount();
|
|
129
360
|
resolve(selected);
|
|
130
361
|
} }));
|
package/package.json
CHANGED