@rainy-updates/cli 0.5.4 → 0.5.6

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