@rainy-updates/cli 0.6.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,64 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [0.6.2] - 2026-03-04
6
+
7
+ Dashboard hardening, cross-platform execution cleanup, and portable release operations for the next `v0.6` patch.
8
+
9
+ ### Added
10
+
11
+ - **Portable operator entrypoints**:
12
+ - added a root `Makefile` for common build/check/release flows,
13
+ - added a `ga` package script so readiness checks can be invoked consistently from Bun scripts and `make`.
14
+ - **Shared shell invocation layer** for Windows, macOS, and Linux command execution in verification and bisect flows.
15
+ - **Dedicated binary release workflow**:
16
+ - GitHub Releases now have a separate workflow from npm publishing,
17
+ - tag builds can produce standalone binaries for Linux, macOS, and Windows,
18
+ - packaged archives are uploaded with SHA-256 checksum files.
19
+ - **Distribution manifest generator**:
20
+ - generates a Homebrew formula from release asset checksums,
21
+ - generates a Scoop manifest from the Windows release asset checksum,
22
+ - release artifacts now include copy-ready Homebrew and Scoop metadata.
23
+ - **New test coverage** for:
24
+ - shared shell invocation behavior across POSIX and Windows,
25
+ - dashboard startup state derived from `--view` and `--focus`,
26
+ - updated doctor dashboard-first recommendations,
27
+ - GA readiness checks for automation entrypoints.
28
+
29
+ ### Changed
30
+
31
+ - Dashboard review operations were tightened for larger queues and cross-platform operators:
32
+ - initial dashboard focus and detail tabs now respect `--view` and `--focus`,
33
+ - the Ink queue renderer now windows large result sets instead of rendering the full queue every frame,
34
+ - bulk actions now include actionable/review selection in addition to safe and blocked flows,
35
+ - keyboard navigation now works cleanly with arrows and `hjkl`,
36
+ - terminal sizing is handled more defensively for smaller shells,
37
+ - the dashboard TUI was split into smaller focused components to reduce maintenance overhead.
38
+ - Doctor findings now point operators to the dashboard-first workflow instead of the older `review --interactive` wording.
39
+ - Verification and bisect shell execution are now portable across Windows, macOS, and Linux by routing command execution through a shared shell invocation layer.
40
+ - Release automation scripts were made portable:
41
+ - `clean` no longer depends on `rm -rf`,
42
+ - `test:prod` no longer depends on POSIX `test -x`,
43
+ - build and production validation now work with compiled Bun artifacts on Windows (`dist/rup.exe`) as well as POSIX (`dist/rup`).
44
+ - Release automation is now intentionally split:
45
+ - one workflow publishes the npm package,
46
+ - one workflow creates and uploads GitHub binary assets for standalone installation.
47
+ - `ga` readiness checks now also verify:
48
+ - portable automation entrypoints,
49
+ - obvious platform-specific script risks,
50
+ - compiled runtime artifacts in either POSIX or Windows form.
51
+
52
+ ### Tests
53
+
54
+ - Validation completed for `0.6.2`:
55
+ - `bun run lint`
56
+ - `bun run build`
57
+ - `bun run check`
58
+ - `bun run test:prod`
59
+ - `bun run ga`
60
+ - `bun run perf:check`
61
+ - `npx -y react-doctor@latest . --verbose --diff` (`99/100`)
62
+
5
63
  ## [0.6.1] - 2026-03-03
6
64
 
7
65
  Compatibility, git-aware workspace scoping, and release-readiness stabilization for the `v0.6` line.
package/README.md CHANGED
@@ -79,6 +79,18 @@ pnpm add -D @rainy-updates/cli
79
79
  bun add -d @rainy-updates/cli
80
80
  ```
81
81
 
82
+ ### Standalone binaries from GitHub Releases
83
+
84
+ If you do not want to depend on global npm or a project-local `node_modules`, use the standalone compiled binaries from GitHub Releases.
85
+
86
+ Release assets are published for:
87
+
88
+ - Linux x64
89
+ - Linux arm64
90
+ - macOS x64
91
+ - macOS arm64
92
+ - Windows x64
93
+
82
94
  Once installed, three binary aliases are available in your `node_modules/.bin/`:
83
95
 
84
96
  | Alias | Use case |
package/dist/bin/help.js CHANGED
@@ -264,6 +264,7 @@ Options:
264
264
  --base <ref>
265
265
  --head <ref>
266
266
  --since <ref>
267
+ --view dependencies|security|health
267
268
  --mode check|review|upgrade
268
269
  --focus all|security|risk|major|blocked|workspace
269
270
  --apply-selected
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { buildAddInvocation, createPackageManagerProfile, detectPackageManagerDetails, } from "../../pm/detect.js";
3
+ import { buildShellInvocation } from "../../utils/shell.js";
3
4
  /**
4
5
  * The "oracle" for bisect: installs a specific version of a package
5
6
  * into the project's node_modules (via the shell), then runs --cmd.
@@ -29,8 +30,8 @@ export async function bisectOracle(packageName, version, options) {
29
30
  }
30
31
  async function runShell(command, cwd) {
31
32
  try {
32
- const shellCmd = process.env.SHELL || "sh";
33
- const proc = Bun.spawn([shellCmd, "-c", command], {
33
+ const invocation = buildShellInvocation(command);
34
+ const proc = Bun.spawn([invocation.shell, ...invocation.args], {
34
35
  cwd: path.resolve(cwd),
35
36
  stdout: "pipe",
36
37
  stderr: "pipe",
@@ -3,6 +3,7 @@ import { buildReviewResult, renderReviewResult } from "../../core/review-model.j
3
3
  import { applySelectedUpdates } from "../../core/upgrade.js";
4
4
  import { runVerification } from "../../core/verification.js";
5
5
  import { runTui } from "../../ui/tui.js";
6
+ import { deriveDashboardInitialFilter, deriveDashboardInitialTab, } from "../../ui/dashboard-state.js";
6
7
  import { writeStderr, writeStdout } from "../../utils/runtime.js";
7
8
  export async function runDashboard(options, prebuiltReview) {
8
9
  const review = prebuiltReview ?? (await buildReviewResult({
@@ -73,5 +74,7 @@ async function selectDashboardItems(options, visibleItems) {
73
74
  ? "Rainy Dashboard: Upgrade Queue"
74
75
  : "Rainy Dashboard: Review Queue",
75
76
  subtitle: `focus=${options.focus} mode=${options.mode} Enter confirms the selected decision set`,
77
+ initialFilter: deriveDashboardInitialFilter(options),
78
+ initialTab: deriveDashboardInitialTab(options),
76
79
  });
77
80
  }
@@ -42,14 +42,16 @@ export async function runGa(options) {
42
42
  ? "Built CLI entrypoint exists in dist/bin/cli.js."
43
43
  : "Built CLI entrypoint is missing; run the build before publishing a release artifact.",
44
44
  });
45
- const compiledBinaryExists = await fileExists(path.resolve(options.cwd, "dist/rup"));
45
+ const compiledBinaryExists = await detectCompiledBinary(options.cwd);
46
46
  checks.push({
47
47
  name: "runtime-artifacts",
48
48
  status: compiledBinaryExists ? "pass" : "warn",
49
49
  detail: compiledBinaryExists
50
- ? "Compiled Bun runtime artifact exists in dist/rup."
50
+ ? "Compiled Bun runtime artifact exists in dist/."
51
51
  : "Compiled Bun runtime artifact is missing; run bun run build:exe before publishing Bun-first release artifacts.",
52
52
  });
53
+ checks.push(await detectAutomationEntryPoints(options.cwd));
54
+ checks.push(await detectPlatformSupport(options.cwd));
53
55
  checks.push({
54
56
  name: "benchmark-gates",
55
57
  status: (await fileExists(path.resolve(options.cwd, "scripts/perf-smoke.mjs"))) &&
@@ -128,6 +130,65 @@ async function detectLockfile(cwd) {
128
130
  detail: "No supported lockfile was detected.",
129
131
  };
130
132
  }
133
+ async function detectCompiledBinary(cwd) {
134
+ return ((await fileExists(path.resolve(cwd, "dist/rup"))) ||
135
+ (await fileExists(path.resolve(cwd, "dist/rup.exe"))));
136
+ }
137
+ async function detectAutomationEntryPoints(cwd) {
138
+ const packageJsonPath = path.resolve(cwd, "package.json");
139
+ let scripts = {};
140
+ try {
141
+ const manifest = (await Bun.file(packageJsonPath).json());
142
+ scripts = manifest.scripts ?? {};
143
+ }
144
+ catch {
145
+ scripts = {};
146
+ }
147
+ const hasMakefile = (await fileExists(path.resolve(cwd, "Makefile"))) ||
148
+ (await fileExists(path.resolve(cwd, "makefile")));
149
+ const requiredScripts = ["build", "check", "test:prod"];
150
+ const missingScripts = requiredScripts.filter((script) => !scripts[script]);
151
+ if (hasMakefile && missingScripts.length === 0) {
152
+ return {
153
+ name: "automation-entrypoints",
154
+ status: "pass",
155
+ detail: "Portable automation entrypoints are available via package scripts and Makefile targets.",
156
+ };
157
+ }
158
+ if (missingScripts.length === 0) {
159
+ return {
160
+ name: "automation-entrypoints",
161
+ status: "pass",
162
+ detail: "Portable package scripts are available for build, check, and test:prod.",
163
+ };
164
+ }
165
+ return {
166
+ name: "automation-entrypoints",
167
+ status: "warn",
168
+ detail: `Missing automation entrypoints: ${missingScripts.join(", ")}.`,
169
+ };
170
+ }
171
+ async function detectPlatformSupport(cwd) {
172
+ const packageJsonPath = path.resolve(cwd, "package.json");
173
+ let scripts = {};
174
+ try {
175
+ const manifest = (await Bun.file(packageJsonPath).json());
176
+ scripts = manifest.scripts ?? {};
177
+ }
178
+ catch {
179
+ scripts = {};
180
+ }
181
+ const suspectScripts = Object.entries(scripts)
182
+ .filter(([, command]) => /\brm\s+-rf\b|\btest\s+-x\b|\bchmod\b|\bcp\s+-R\b/.test(command))
183
+ .map(([name]) => name);
184
+ return {
185
+ name: "platform-support",
186
+ status: suspectScripts.length === 0 ? "pass" : "warn",
187
+ detail: suspectScripts.length === 0
188
+ ? "No obvious POSIX-only package scripts were detected for release-critical automation."
189
+ : `These scripts still look POSIX-specific and may need extra Windows work: ${suspectScripts.join(", ")}.`,
190
+ };
191
+ }
131
192
  async function fileExists(filePath) {
132
193
  try {
133
194
  return await Bun.file(filePath).exists();
@@ -11,7 +11,7 @@ export function buildDoctorFindings(review) {
11
11
  summary: error,
12
12
  details: "Execution errors make the scan incomplete or require operator review.",
13
13
  help: "Resolve execution and registry issues before treating the run as clean.",
14
- recommendedAction: "Run `rup review --interactive` after fixing execution failures.",
14
+ recommendedAction: "Run `rup dashboard --mode review` after fixing execution failures.",
15
15
  evidence: [error],
16
16
  });
17
17
  }
@@ -43,7 +43,7 @@ export function buildDoctorFindings(review) {
43
43
  summary: `${item.update.name} has ${item.update.peerConflictSeverity} peer conflicts after the proposed upgrade.`,
44
44
  details: item.peerConflicts[0]?.suggestion,
45
45
  help: "Inspect peer dependency requirements before applying the update.",
46
- recommendedAction: item.update.recommendedAction ?? "Review peer requirements in `rup review --interactive`.",
46
+ recommendedAction: item.update.recommendedAction ?? "Review peer requirements in `rup dashboard --mode review`.",
47
47
  evidence: item.peerConflicts.map((conflict) => `${conflict.requester} -> ${conflict.peer}@${conflict.requiredRange}`),
48
48
  });
49
49
  }
@@ -59,7 +59,7 @@ export function buildDoctorFindings(review) {
59
59
  summary: `${item.update.name} violates the current license policy.`,
60
60
  details: item.license?.license,
61
61
  help: "Keep denied licenses out of the approved update set.",
62
- recommendedAction: item.update.recommendedAction ?? "Block this package in `rup review --interactive`.",
62
+ recommendedAction: item.update.recommendedAction ?? "Block this package in `rup dashboard --mode review --focus blocked`.",
63
63
  });
64
64
  }
65
65
  if ((item.update.advisoryCount ?? 0) > 0) {
@@ -106,7 +106,7 @@ export function buildDoctorFindings(review) {
106
106
  workspace,
107
107
  summary: `${item.update.name} is a major version upgrade.`,
108
108
  help: "Major upgrades should be reviewed explicitly before being applied.",
109
- recommendedAction: item.update.recommendedAction ?? "Review major changes in `rup review --interactive`.",
109
+ recommendedAction: item.update.recommendedAction ?? "Review major changes in `rup dashboard --mode review --focus major`.",
110
110
  });
111
111
  }
112
112
  if (item.update.healthStatus === "stale" || item.update.healthStatus === "archived") {
@@ -2,7 +2,7 @@ import { buildInstallInvocation, buildTestCommand, createPackageManagerProfile,
2
2
  import { installDependencies } from "../pm/install.js";
3
3
  import { stableStringify } from "../utils/stable-json.js";
4
4
  import { writeFileAtomic } from "../utils/io.js";
5
- import { readEnv } from "../utils/runtime.js";
5
+ import { buildShellInvocation } from "../utils/shell.js";
6
6
  export async function runVerification(options) {
7
7
  const mode = options.verify;
8
8
  if (mode === "none") {
@@ -48,8 +48,8 @@ function defaultTestCommand(profile) {
48
48
  async function runShellCheck(cwd, command) {
49
49
  const startedAt = Date.now();
50
50
  try {
51
- const shell = readEnv("SHELL") || "sh";
52
- const proc = Bun.spawn([shell, "-lc", command], {
51
+ const invocation = buildShellInvocation(command);
52
+ const proc = Bun.spawn([invocation.shell, ...invocation.args], {
53
53
  cwd,
54
54
  stdin: "inherit",
55
55
  stdout: "inherit",
@@ -1 +1 @@
1
- export declare const CLI_VERSION = "0.6.1";
1
+ export declare const CLI_VERSION = "0.6.2";
@@ -1,2 +1,2 @@
1
1
  // This file is generated by scripts/sync-version.mjs.
2
- export const CLI_VERSION = "0.6.1";
2
+ export const CLI_VERSION = "0.6.2";
package/dist/rup CHANGED
Binary file
@@ -730,7 +730,7 @@ export interface GaOptions {
730
730
  jsonFile?: string;
731
731
  }
732
732
  export interface GaCheck {
733
- name: "package-manager" | "workspace-discovery" | "lockfile" | "cache-backend" | "dist-build" | "runtime-artifacts" | "benchmark-gates" | "docs-contract";
733
+ name: "package-manager" | "workspace-discovery" | "lockfile" | "cache-backend" | "dist-build" | "runtime-artifacts" | "automation-entrypoints" | "platform-support" | "benchmark-gates" | "docs-contract";
734
734
  status: "pass" | "warn" | "fail";
735
735
  detail: string;
736
736
  }
@@ -0,0 +1,7 @@
1
+ import type { DashboardOptions } from "../types/index.js";
2
+ export declare const FILTER_ORDER: readonly ["all", "security", "risky", "major", "peer-conflict", "license", "unused", "blocked"];
3
+ export declare const DETAIL_TABS: readonly ["overview", "risk", "security", "peer", "license", "health", "changelog"];
4
+ export type DashboardFilterKey = (typeof FILTER_ORDER)[number];
5
+ export type DashboardDetailTab = (typeof DETAIL_TABS)[number];
6
+ export declare function deriveDashboardInitialFilter(options: Pick<DashboardOptions, "focus" | "view">): DashboardFilterKey;
7
+ export declare function deriveDashboardInitialTab(options: Pick<DashboardOptions, "focus" | "view">): DashboardDetailTab;
@@ -0,0 +1,44 @@
1
+ export const FILTER_ORDER = [
2
+ "all",
3
+ "security",
4
+ "risky",
5
+ "major",
6
+ "peer-conflict",
7
+ "license",
8
+ "unused",
9
+ "blocked",
10
+ ];
11
+ export const DETAIL_TABS = [
12
+ "overview",
13
+ "risk",
14
+ "security",
15
+ "peer",
16
+ "license",
17
+ "health",
18
+ "changelog",
19
+ ];
20
+ export function deriveDashboardInitialFilter(options) {
21
+ if (options.focus === "security")
22
+ return "security";
23
+ if (options.focus === "risk")
24
+ return "risky";
25
+ if (options.focus === "major")
26
+ return "major";
27
+ if (options.focus === "blocked")
28
+ return "blocked";
29
+ if (options.view === "security")
30
+ return "security";
31
+ if (options.view === "health")
32
+ return "risky";
33
+ return "all";
34
+ }
35
+ export function deriveDashboardInitialTab(options) {
36
+ if (options.focus === "security" || options.view === "security") {
37
+ return "security";
38
+ }
39
+ if (options.focus === "risk")
40
+ return "risk";
41
+ if (options.view === "health")
42
+ return "health";
43
+ return "overview";
44
+ }
package/dist/ui/tui.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import type { ReviewItem } from "../types/index.js";
2
+ import { type DashboardDetailTab, type DashboardFilterKey } from "./dashboard-state.js";
2
3
  export declare function runTui(items: ReviewItem[], options?: {
3
4
  title?: string;
4
5
  subtitle?: string;
6
+ initialFilter?: DashboardFilterKey;
7
+ initialTab?: DashboardDetailTab;
5
8
  }): Promise<ReviewItem[]>;
package/dist/ui/tui.js CHANGED
@@ -1,27 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React from "react";
3
- import { Box, render, Text, useInput } from "ink";
4
- const FILTER_ORDER = [
5
- "all",
6
- "security",
7
- "risky",
8
- "major",
9
- "peer-conflict",
10
- "license",
11
- "unused",
12
- "blocked",
13
- ];
3
+ import { Box, render, Text, useInput, useStdout } from "ink";
4
+ import { DETAIL_TABS, FILTER_ORDER, } from "./dashboard-state.js";
14
5
  const SORT_ORDER = ["risk", "advisories", "diff", "name", "workspace"];
15
6
  const GROUP_ORDER = ["none", "workspace", "scope", "risk", "decision"];
16
- const DETAIL_TABS = [
17
- "overview",
18
- "risk",
19
- "security",
20
- "peer",
21
- "license",
22
- "health",
23
- "changelog",
24
- ];
25
7
  function tuiReducer(state, action) {
26
8
  switch (action.type) {
27
9
  case "SET_SEARCH_MODE":
@@ -30,8 +12,6 @@ function tuiReducer(state, action) {
30
12
  searchMode: action.active,
31
13
  ...(action.active ? {} : { search: "", cursorIndex: 0 }),
32
14
  };
33
- case "SET_SEARCH":
34
- return { ...state, search: action.value, cursorIndex: 0 };
35
15
  case "APPEND_SEARCH":
36
16
  return { ...state, search: state.search + action.value, cursorIndex: 0 };
37
17
  case "BACKSPACE_SEARCH":
@@ -51,8 +31,6 @@ function tuiReducer(state, action) {
51
31
  ...state,
52
32
  cursorIndex: Math.min(action.max, Math.max(0, state.cursorIndex + action.direction)),
53
33
  };
54
- case "RESET_CURSOR":
55
- return { ...state, cursorIndex: 0 };
56
34
  case "CYCLE_SORT":
57
35
  return {
58
36
  ...state,
@@ -65,8 +43,10 @@ function tuiReducer(state, action) {
65
43
  groupIndex: (state.groupIndex + 1) % action.max,
66
44
  cursorIndex: 0,
67
45
  };
68
- case "CYCLE_TAB":
69
- return { ...state, tabIndex: (state.tabIndex + 1) % action.max };
46
+ case "CYCLE_TAB": {
47
+ const next = (state.tabIndex + action.direction + action.max) % action.max;
48
+ return { ...state, tabIndex: next };
49
+ }
70
50
  case "SET_SELECTED":
71
51
  return { ...state, selectedIndices: action.indices };
72
52
  case "TOGGLE_SELECTED": {
@@ -81,13 +61,15 @@ function tuiReducer(state, action) {
81
61
  return state;
82
62
  }
83
63
  }
84
- function TuiApp({ items, title, subtitle, onComplete }) {
64
+ function TuiApp({ items, title, subtitle, initialFilter = "all", initialTab = "overview", onComplete, }) {
65
+ const { stdout } = useStdout();
66
+ const { columns: stdoutWidth = 160, rows: stdoutHeight = 32 } = stdout;
85
67
  const [state, dispatch] = React.useReducer(tuiReducer, undefined, () => ({
86
68
  cursorIndex: 0,
87
- filterIndex: 0,
69
+ filterIndex: Math.max(0, FILTER_ORDER.indexOf(initialFilter)),
88
70
  sortIndex: 0,
89
71
  groupIndex: 0,
90
- tabIndex: 0,
72
+ tabIndex: Math.max(0, DETAIL_TABS.indexOf(initialTab)),
91
73
  showHelp: false,
92
74
  searchMode: false,
93
75
  search: "",
@@ -97,30 +79,34 @@ function TuiApp({ items, title, subtitle, onComplete }) {
97
79
  const activeSort = SORT_ORDER[state.sortIndex] ?? "risk";
98
80
  const activeGroup = GROUP_ORDER[state.groupIndex] ?? "none";
99
81
  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;
105
82
  const visibleRows = buildVisibleRows(items, {
106
83
  filter: activeFilter,
107
84
  sort: activeSort,
108
85
  group: activeGroup,
109
- search,
86
+ search: state.search,
110
87
  });
111
88
  const itemRows = visibleRows.filter((row) => row.kind === "item" && typeof row.index === "number");
112
89
  const boundedCursor = Math.min(state.cursorIndex, Math.max(0, itemRows.length - 1));
113
90
  const focusedIndex = itemRows[boundedCursor]?.index ?? 0;
114
91
  const focusedItem = items[focusedIndex];
92
+ const visibleMetrics = summarizeVisibleItems(itemRows, items, state.selectedIndices);
93
+ const renderWindow = createRenderWindow({
94
+ visibleRows,
95
+ focusedIndex,
96
+ stdoutHeight,
97
+ });
98
+ const rowPositionByIndex = createRowPositionMap(itemRows);
99
+ const layout = createDashboardLayout(stdoutWidth);
100
+ const platformLabel = process.platform === "win32" ? "windows" : "unix";
101
+ const selectedItems = items.filter((_, index) => state.selectedIndices.has(index));
115
102
  useInput((input, key) => {
116
- if (searchMode) {
103
+ if (state.searchMode) {
117
104
  if (key.escape) {
118
105
  dispatch({ type: "SET_SEARCH_MODE", active: false });
119
106
  return;
120
107
  }
121
108
  if (key.return) {
122
109
  dispatch({ type: "SET_SEARCH_MODE", active: false });
123
- dispatch({ type: "SET_SEARCH", value: search }); // keeps search but exits mode
124
110
  return;
125
111
  }
126
112
  if (key.backspace || key.delete) {
@@ -140,115 +126,161 @@ function TuiApp({ items, title, subtitle, onComplete }) {
140
126
  dispatch({ type: "TOGGLE_HELP" });
141
127
  return;
142
128
  }
143
- if (key.escape && showHelp) {
129
+ if (key.escape && state.showHelp) {
144
130
  dispatch({ type: "SET_HELP", active: false });
145
131
  return;
146
132
  }
147
- if (key.leftArrow) {
133
+ if (key.leftArrow || input === "h") {
148
134
  dispatch({
149
135
  type: "MOVE_FILTER",
150
136
  direction: -1,
151
137
  max: FILTER_ORDER.length - 1,
152
138
  });
139
+ return;
153
140
  }
154
- if (key.rightArrow) {
141
+ if (key.rightArrow || input === "l") {
155
142
  dispatch({
156
143
  type: "MOVE_FILTER",
157
144
  direction: 1,
158
145
  max: FILTER_ORDER.length - 1,
159
146
  });
147
+ return;
160
148
  }
161
- if (key.upArrow) {
149
+ if (key.upArrow || input === "k") {
162
150
  dispatch({
163
151
  type: "MOVE_CURSOR",
164
152
  direction: -1,
165
153
  max: itemRows.length - 1,
166
154
  });
155
+ return;
167
156
  }
168
- if (key.downArrow) {
169
- dispatch({ type: "MOVE_CURSOR", direction: 1, max: itemRows.length - 1 });
157
+ if (key.downArrow || input === "j") {
158
+ dispatch({
159
+ type: "MOVE_CURSOR",
160
+ direction: 1,
161
+ max: itemRows.length - 1,
162
+ });
163
+ return;
170
164
  }
171
165
  if (input === "o") {
172
166
  dispatch({ type: "CYCLE_SORT", max: SORT_ORDER.length });
167
+ return;
173
168
  }
174
169
  if (input === "g") {
175
170
  dispatch({ type: "CYCLE_GROUP", max: GROUP_ORDER.length });
171
+ return;
176
172
  }
177
173
  if (key.tab) {
178
- dispatch({ type: "CYCLE_TAB", max: DETAIL_TABS.length });
174
+ dispatch({
175
+ type: "CYCLE_TAB",
176
+ direction: key.shift ? -1 : 1,
177
+ max: DETAIL_TABS.length,
178
+ });
179
+ return;
179
180
  }
180
181
  if (input === "a") {
181
182
  dispatch({
182
183
  type: "SET_SELECTED",
183
- indices: addVisible(selectedIndices, itemRows),
184
+ indices: addVisible(state.selectedIndices, itemRows),
184
185
  });
186
+ return;
185
187
  }
186
188
  if (input === "n") {
187
189
  dispatch({
188
190
  type: "SET_SELECTED",
189
- indices: removeVisible(selectedIndices, itemRows),
191
+ indices: removeVisible(state.selectedIndices, itemRows),
190
192
  });
193
+ return;
191
194
  }
192
195
  if (input === "s") {
193
196
  dispatch({
194
197
  type: "SET_SELECTED",
195
- indices: selectSafe(selectedIndices, itemRows, items),
198
+ indices: selectSafe(state.selectedIndices, itemRows, items),
196
199
  });
200
+ return;
197
201
  }
198
202
  if (input === "b") {
199
203
  dispatch({
200
204
  type: "SET_SELECTED",
201
- indices: clearBlocked(selectedIndices, itemRows, items),
205
+ indices: clearBlocked(state.selectedIndices, itemRows, items),
206
+ });
207
+ return;
208
+ }
209
+ if (input === "x") {
210
+ dispatch({
211
+ type: "SET_SELECTED",
212
+ indices: selectActionable(state.selectedIndices, itemRows, items),
202
213
  });
214
+ return;
203
215
  }
204
216
  if (input === " ") {
205
217
  dispatch({ type: "TOGGLE_SELECTED", index: focusedIndex });
218
+ return;
206
219
  }
207
- if (input === "q" || key.escape) {
208
- onComplete(items.filter((_, index) => selectedIndices.has(index)));
220
+ if (input === "q" || (key.escape && !state.showHelp)) {
221
+ onComplete(selectedItems);
209
222
  return;
210
223
  }
211
224
  if (key.return) {
212
- onComplete(items.filter((_, index) => selectedIndices.has(index)));
225
+ onComplete(selectedItems);
213
226
  }
214
227
  });
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) => {
217
- if (row.kind === "group") {
218
- return (_jsx(Text, { bold: true, color: "gray", children: row.label }, `group:${row.label}`));
219
- }
220
- const index = row.index ?? 0;
221
- const item = items[index];
222
- const update = item.update;
223
- const decision = update.decisionState ?? deriveDecision(item);
224
- const itemPosition = itemRows.findIndex((candidate) => candidate.index === index);
225
- const isFocused = itemPosition === boundedCursor;
226
- const isSelected = selectedIndices.has(index);
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}`));
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] }));
228
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(DashboardHeader, { title: title, subtitle: subtitle, platformLabel: platformLabel, metrics: visibleMetrics }), _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(FilterRail, { width: layout.railWidth, filterIndex: state.filterIndex, search: state.search, searchMode: state.searchMode, activeSort: activeSort, activeGroup: activeGroup, activeTab: activeTab }), _jsx(QueuePanel, { width: layout.queueWidth, items: items, visibleRows: visibleRows, itemRows: itemRows, renderWindow: renderWindow, rowPositionByIndex: rowPositionByIndex, boundedCursor: boundedCursor, selectedIndices: state.selectedIndices }), _jsx(DecisionPanel, { width: layout.detailWidth, activeTab: activeTab, focusedItem: focusedItem })] }), _jsx(ActionBar, {}), state.showHelp ? _jsx(HelpPanel, {}) : null] }));
229
+ }
230
+ function DashboardHeader({ title, subtitle, platformLabel, metrics, }) {
231
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: "cyan", children: title ?? "Rainy Dashboard" }), _jsx(Text, { color: "gray", children: subtitle ??
232
+ "Check detects, doctor summarizes, dashboard decides, upgrade applies." }), _jsxs(Text, { color: "gray", children: [platformLabel, " keys: arrows or hjkl, Tab changes panel, / search, ? help, Enter confirm"] }), _jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsxs(Text, { children: ["visible ", metrics.total, " | selected ", metrics.selected, " | actionable", " ", metrics.actionable, " | blocked ", metrics.blocked, " | security", " ", metrics.security] }) })] }));
233
+ }
234
+ function FilterRail({ width, filterIndex, search, searchMode, activeSort, activeGroup, activeTab, }) {
235
+ return (_jsxs(Box, { width: width, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Filter Rail" }), FILTER_ORDER.map((filter, index) => (_jsxs(Text, { color: index === filterIndex ? "cyan" : "gray", children: [index === filterIndex ? ">" : " ", " ", filter] }, filter))), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Search" }), _jsx(Text, { color: searchMode ? "cyan" : "gray", children: searchMode ? `/${search}` : search ? `/${search}` : "inactive" })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Modes" }), _jsxs(Text, { color: "gray", children: ["sort: ", activeSort] }), _jsxs(Text, { color: "gray", children: ["group: ", activeGroup] }), _jsxs(Text, { color: "gray", children: ["tab: ", activeTab] })] })] }));
236
+ }
237
+ function QueuePanel({ width, items, visibleRows, itemRows, renderWindow, rowPositionByIndex, boundedCursor, selectedIndices, }) {
238
+ return (_jsxs(Box, { marginLeft: 1, width: width, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsxs(Text, { bold: true, children: ["Review Queue", renderWindow.start > 0 ? " [more above]" : "", renderWindow.end < visibleRows.length ? " [more below]" : ""] }), itemRows.length === 0 ? (_jsx(Text, { color: "gray", children: "No review candidates match this view." })) : (renderWindow.rows.map((row) => (_jsx(QueueRow, { row: row, items: items, rowPositionByIndex: rowPositionByIndex, boundedCursor: boundedCursor, selectedIndices: selectedIndices }, row.kind === "group" ? `group:${row.label}` : `${items[row.index ?? 0]?.update.packagePath}:${items[row.index ?? 0]?.update.name}`))))] }));
239
+ }
240
+ function QueueRow({ row, items, rowPositionByIndex, boundedCursor, selectedIndices, }) {
241
+ if (row.kind === "group") {
242
+ return (_jsx(Text, { bold: true, color: "gray", children: row.label }));
243
+ }
244
+ const index = row.index ?? 0;
245
+ const item = items[index];
246
+ const update = item.update;
247
+ const decision = update.decisionState ?? deriveDecision(item);
248
+ const itemPosition = rowPositionByIndex.get(index) ?? -1;
249
+ const isFocused = itemPosition === boundedCursor;
250
+ const isSelected = selectedIndices.has(index);
251
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: isFocused ? "cyan" : "gray", children: [isFocused ? ">" : " ", " ", isSelected ? "[x]" : "[ ]", " "] }), _jsx(Box, { width: 24, children: _jsx(Text, { bold: isFocused, children: truncate(update.name, 22) }) }), _jsx(Box, { width: 10, children: _jsx(Text, { color: diffColor(update.diffType), children: update.diffType }) }), _jsx(Box, { width: 11, children: _jsx(Text, { color: riskColor(update.riskLevel), children: update.riskLevel ?? "low" }) }), _jsx(Box, { width: 12, children: _jsx(Text, { color: decisionColor(decision), children: truncate(decision, 10) }) }), _jsx(Box, { width: 7, children: _jsx(Text, { color: decisionColor(decision), children: update.riskScore ?? "--" }) }), _jsxs(Text, { color: "gray", children: [truncate(update.fromRange, 12), " ", "->", " "] }), _jsx(Text, { color: "green", children: truncate(update.toVersionResolved, 12) })] }));
252
+ }
253
+ function DecisionPanel({ width, activeTab, focusedItem, }) {
254
+ return (_jsxs(Box, { marginLeft: 1, width: width, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Decision Panel" }), _jsxs(Text, { color: "gray", children: ["tab: ", activeTab] }), focusedItem ? (renderTab(focusedItem, activeTab)) : (_jsx(Text, { color: "gray", children: "No review candidate selected." }))] }));
255
+ }
256
+ function ActionBar() {
257
+ return (_jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsx(Text, { color: "gray", children: "A add visible | N clear visible | S safe | X actionable | B clear blocked | Space toggle | Q finish" }) }));
258
+ }
259
+ function HelpPanel() {
260
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Help" }), _jsx(Text, { color: "gray", children: "Use filters for queue slices, search with /, and switch panels with Tab." }), _jsx(Text, { color: "gray", children: "The queue is windowed around the focused package for faster rendering in large workspaces." }), _jsx(Text, { color: "gray", children: "Actionable items can be bulk-selected with X. Blocked items stay easy to clear with B." })] }));
229
261
  }
230
262
  function renderTab(item, tab) {
231
263
  const update = item.update;
232
264
  if (tab === "risk") {
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." }))] }));
265
+ return (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["state:", " ", _jsx(Text, { color: decisionColor(update.decisionState ?? deriveDecision(item)), children: update.decisionState ?? deriveDecision(item) })] }), _jsxs(Text, { children: ["policy:", " ", _jsx(Text, { color: decisionColor(policyToDecision(update.policyAction)), children: update.policyAction ?? "allow" })] }), _jsxs(Text, { children: ["risk score: ", update.riskScore ?? 0] }), _jsxs(Text, { children: ["impact score: ", update.impactScore?.score ?? 0] }), _jsxs(Text, { children: ["recommended:", " ", truncate(update.recommendedAction ?? "Safe to keep in the selected set.", 80)] }), update.riskReasons && update.riskReasons.length > 0 ? (update.riskReasons.slice(0, 5).map((reason) => (_jsxs(Text, { color: "gray", children: ["- ", truncate(reason, 48)] }, reason)))) : (_jsx(Text, { color: "gray", children: "No elevated risk reasons." }))] }));
234
266
  }
235
267
  if (tab === "security") {
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." }))] }));
268
+ return (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["advisories: ", item.advisories.length] }), item.advisories.length > 0 ? (item.advisories.slice(0, 4).map((advisory) => (_jsxs(Text, { color: "gray", children: ["- ", truncate(`${advisory.severity} ${advisory.cveId}: ${advisory.title}`, 48)] }, `${advisory.packageName}:${advisory.cveId}`)))) : (_jsx(Text, { color: "gray", children: "No security advisories detected." }))] }));
237
269
  }
238
270
  if (tab === "peer") {
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." }))] }));
271
+ return (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["peer status: ", update.peerConflictSeverity ?? "none"] }), item.peerConflicts.length > 0 ? (item.peerConflicts.slice(0, 4).map((conflict) => (_jsxs(Text, { color: "gray", children: ["- ", truncate(`${conflict.requester} requires ${conflict.peer} ${conflict.requiredRange}`, 48)] }, `${conflict.requester}:${conflict.peer}`)))) : (_jsx(Text, { color: "gray", children: "No peer conflicts detected." }))] }));
240
272
  }
241
273
  if (tab === "license") {
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"] })] }));
274
+ return (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["license status: ", update.licenseStatus ?? "allowed"] }), _jsxs(Text, { children: ["repository: ", truncate(update.repository ?? "unavailable", 48)] }), _jsxs(Text, { children: ["homepage: ", truncate(update.homepage ?? "unavailable", 48)] })] }));
243
275
  }
244
276
  if (tab === "health") {
245
277
  return (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["health: ", update.healthStatus ?? "healthy"] }), _jsxs(Text, { children: ["maintainers: ", update.maintainerCount ?? "unknown"] }), _jsxs(Text, { children: ["publish age days: ", update.publishAgeDays ?? "unknown"] }), _jsxs(Text, { children: ["maintainer churn: ", update.maintainerChurn ?? "unknown"] })] }));
246
278
  }
247
279
  if (tab === "changelog") {
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." })] }));
280
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { children: truncate(update.releaseNotesSummary?.title ?? "Release notes unavailable", 48) }), _jsx(Text, { color: "gray", children: truncate(update.releaseNotesSummary?.excerpt ??
281
+ "Run review with changelog support or inspect the repository manually.", 96) })] }));
250
282
  }
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."] })] }));
283
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { children: update.name }), _jsxs(Text, { color: "gray", children: ["package: ", truncate(update.packagePath, 48)] }), _jsxs(Text, { children: ["state:", " ", _jsx(Text, { color: decisionColor(update.decisionState ?? deriveDecision(item)), children: update.decisionState ?? deriveDecision(item) })] }), _jsxs(Text, { children: ["diff: ", _jsx(Text, { color: diffColor(update.diffType), children: update.diffType })] }), _jsxs(Text, { children: ["risk:", " ", _jsx(Text, { color: riskColor(update.riskLevel), children: update.riskLevel ?? "low" })] }), _jsxs(Text, { children: ["policy: ", update.policyAction ?? "allow"] }), _jsxs(Text, { children: ["workspace: ", update.workspaceGroup ?? "root"] }), _jsxs(Text, { children: ["group: ", update.groupKey ?? "none"] }), _jsxs(Text, { children: ["action:", " ", truncate(update.recommendedAction ?? "Safe to keep in the selected set.", 80)] })] }));
252
284
  }
253
285
  function buildVisibleRows(items, config) {
254
286
  const filtered = items
@@ -282,8 +314,9 @@ function buildVisibleRows(items, config) {
282
314
  function matchesFilter(item, filter) {
283
315
  if (filter === "security")
284
316
  return item.advisories.length > 0;
285
- if (filter === "risky")
286
- return (item.update.riskLevel === "critical" || item.update.riskLevel === "high");
317
+ if (filter === "risky") {
318
+ return item.update.riskLevel === "critical" || item.update.riskLevel === "high";
319
+ }
287
320
  if (filter === "major")
288
321
  return item.update.diffType === "major";
289
322
  if (filter === "peer-conflict")
@@ -292,8 +325,9 @@ function matchesFilter(item, filter) {
292
325
  return item.update.licenseStatus === "denied";
293
326
  if (filter === "unused")
294
327
  return item.unusedIssues.length > 0;
295
- if (filter === "blocked")
328
+ if (filter === "blocked") {
296
329
  return (item.update.decisionState ?? deriveDecision(item)) === "blocked";
330
+ }
297
331
  return true;
298
332
  }
299
333
  function matchesSearch(item, search) {
@@ -305,7 +339,7 @@ function matchesSearch(item, search) {
305
339
  }
306
340
  function compareItems(left, right, sort) {
307
341
  if (sort === "advisories") {
308
- const byAdvisories = (right.advisories.length ?? 0) - (left.advisories.length ?? 0);
342
+ const byAdvisories = right.advisories.length - left.advisories.length;
309
343
  if (byAdvisories !== 0)
310
344
  return byAdvisories;
311
345
  }
@@ -340,8 +374,9 @@ function groupLabel(item, group) {
340
374
  }
341
375
  if (group === "risk")
342
376
  return item.update.riskLevel ?? "low";
343
- if (group === "decision")
377
+ if (group === "decision") {
344
378
  return item.update.decisionState ?? deriveDecision(item);
379
+ }
345
380
  return "all";
346
381
  }
347
382
  function addVisible(selected, rows) {
@@ -376,13 +411,23 @@ function clearBlocked(selected, rows, items) {
376
411
  }
377
412
  return next;
378
413
  }
414
+ function selectActionable(selected, rows, items) {
415
+ const next = new Set(selected);
416
+ for (const row of rows) {
417
+ const decision = items[row.index]?.update.decisionState ??
418
+ deriveDecision(items[row.index]);
419
+ if (decision === "actionable" || decision === "review") {
420
+ next.add(row.index);
421
+ }
422
+ }
423
+ return next;
424
+ }
379
425
  function deriveDecision(item) {
380
426
  if (item.update.peerConflictSeverity === "error" ||
381
427
  item.update.licenseStatus === "denied") {
382
428
  return "blocked";
383
429
  }
384
- if ((item.update.advisoryCount ?? 0) > 0 ||
385
- item.update.riskLevel === "critical") {
430
+ if ((item.update.advisoryCount ?? 0) > 0 || item.update.riskLevel === "critical") {
386
431
  return "actionable";
387
432
  }
388
433
  if (item.update.riskLevel === "high" || item.update.diffType === "major") {
@@ -442,9 +487,76 @@ function decisionColor(label) {
442
487
  return "green";
443
488
  }
444
489
  }
490
+ function clamp(value, min, max) {
491
+ return Math.min(max, Math.max(min, value));
492
+ }
493
+ function createDashboardLayout(stdoutWidth) {
494
+ const railWidth = clamp(Math.floor(stdoutWidth * 0.2), 24, 30);
495
+ const detailWidth = clamp(Math.floor(stdoutWidth * 0.26), 36, 54);
496
+ const queueWidth = Math.max(48, stdoutWidth - railWidth - detailWidth - 8);
497
+ return { railWidth, detailWidth, queueWidth };
498
+ }
499
+ function truncate(value, maxLength) {
500
+ if (value.length <= maxLength)
501
+ return value;
502
+ if (maxLength <= 1)
503
+ return value.slice(0, maxLength);
504
+ return `${value.slice(0, Math.max(1, maxLength - 1))}…`;
505
+ }
506
+ function createRowPositionMap(rows) {
507
+ const map = new Map();
508
+ rows.forEach((row, position) => {
509
+ map.set(row.index, position);
510
+ });
511
+ return map;
512
+ }
513
+ function createRenderWindow(config) {
514
+ const maxRows = clamp(config.stdoutHeight - 16, 8, 18);
515
+ if (config.visibleRows.length <= maxRows) {
516
+ return {
517
+ rows: config.visibleRows,
518
+ start: 0,
519
+ end: config.visibleRows.length,
520
+ };
521
+ }
522
+ const focusedRow = Math.max(0, config.visibleRows.findIndex((row) => row.kind === "item" && row.index === config.focusedIndex));
523
+ let start = Math.max(0, focusedRow - Math.floor(maxRows / 2));
524
+ let end = Math.min(config.visibleRows.length, start + maxRows);
525
+ start = Math.max(0, end - maxRows);
526
+ return {
527
+ rows: config.visibleRows.slice(start, end),
528
+ start,
529
+ end,
530
+ };
531
+ }
532
+ function summarizeVisibleItems(rows, items, selected) {
533
+ let actionable = 0;
534
+ let blocked = 0;
535
+ let security = 0;
536
+ let selectedCount = 0;
537
+ for (const row of rows) {
538
+ const item = items[row.index];
539
+ const decision = item.update.decisionState ?? deriveDecision(item);
540
+ if (selected.has(row.index))
541
+ selectedCount += 1;
542
+ if (decision === "actionable" || decision === "review")
543
+ actionable += 1;
544
+ if (decision === "blocked")
545
+ blocked += 1;
546
+ if (item.advisories.length > 0)
547
+ security += 1;
548
+ }
549
+ return {
550
+ total: rows.length,
551
+ selected: selectedCount,
552
+ actionable,
553
+ blocked,
554
+ security,
555
+ };
556
+ }
445
557
  export async function runTui(items, options) {
446
558
  return new Promise((resolve) => {
447
- const { unmount } = render(_jsx(TuiApp, { items: items, title: options?.title, subtitle: options?.subtitle, onComplete: (selected) => {
559
+ const { unmount } = render(_jsx(TuiApp, { items: items, title: options?.title, subtitle: options?.subtitle, initialFilter: options?.initialFilter, initialTab: options?.initialTab, onComplete: (selected) => {
448
560
  unmount();
449
561
  resolve(selected);
450
562
  } }));
@@ -0,0 +1,6 @@
1
+ export interface ShellInvocation {
2
+ shell: string;
3
+ args: string[];
4
+ display: string;
5
+ }
6
+ export declare function buildShellInvocation(command: string, runtimePlatform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): ShellInvocation;
@@ -0,0 +1,18 @@
1
+ export function buildShellInvocation(command, runtimePlatform = process.platform, env = process.env) {
2
+ if (runtimePlatform === "win32") {
3
+ const shell = env.COMSPEC?.trim() || "cmd.exe";
4
+ const args = ["/d", "/s", "/c", command];
5
+ return {
6
+ shell,
7
+ args,
8
+ display: [shell, ...args].join(" "),
9
+ };
10
+ }
11
+ const shell = env.SHELL?.trim() || "sh";
12
+ const args = ["-lc", command];
13
+ return {
14
+ shell,
15
+ args,
16
+ display: [shell, ...args].join(" "),
17
+ };
18
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainy-updates/cli",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
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,
@@ -51,16 +51,19 @@
51
51
  "CODE_OF_CONDUCT.md"
52
52
  ],
53
53
  "scripts": {
54
- "clean": "rm -rf dist",
54
+ "clean": "bun scripts/clean.mjs",
55
55
  "version:sync": "bun scripts/sync-version.mjs",
56
56
  "version:check": "bun scripts/sync-version.mjs --check",
57
57
  "version": "bun run version:sync",
58
58
  "build": "bun run version:sync && tsc -p tsconfig.build.json",
59
59
  "build:exe": "bun run version:sync && bun build ./src/bin/cli.ts --compile --outfile dist/rup",
60
+ "build:release-binary": "bun scripts/build-release-binary.mjs",
61
+ "generate:distribution-manifests": "bun scripts/generate-distribution-manifests.mjs",
60
62
  "typecheck": "bun run version:check && tsc --noEmit",
61
63
  "test": "bun run version:check && bun test",
62
64
  "lint": "bunx biome check src tests",
63
65
  "check": "bun run typecheck && bun test",
66
+ "ga": "bun ./src/bin/cli.ts ga",
64
67
  "bench:fixtures": "bun run scripts/generate-benchmark-fixtures.mjs",
65
68
  "bench:check": "bun run build && bun run bench:fixtures && bun run scripts/benchmark.mjs single-100 check cold 3 && bun run scripts/benchmark.mjs single-100 check warm 3",
66
69
  "bench:review": "bun run build && bun run bench:fixtures && bun run scripts/benchmark.mjs single-100 review cold 3 && bun run scripts/benchmark.mjs single-100 review warm 3",
@@ -70,7 +73,7 @@
70
73
  "perf:check": "bun run scripts/perf-smoke.mjs",
71
74
  "perf:resolve": "RAINY_UPDATES_PERF_SCENARIO=resolve bun run scripts/perf-smoke.mjs",
72
75
  "perf:ci": "RAINY_UPDATES_PERF_SCENARIO=ci bun run scripts/perf-smoke.mjs",
73
- "test:prod": "bun ./dist/bin/cli.js --help && bun ./dist/bin/cli.js --version && (test -x ./dist/rup || bun run build:exe) && ./dist/rup --help && ./dist/rup --version",
76
+ "test:prod": "bun scripts/test-prod.mjs",
74
77
  "prepublishOnly": "bun run check && bun run build && bun run build:exe && bun run test:prod"
75
78
  },
76
79
  "engines": {
@@ -82,7 +85,7 @@
82
85
  "provenance": true
83
86
  },
84
87
  "devDependencies": {
85
- "@types/bun": "1.3.9",
88
+ "@types/bun": "1.3.10",
86
89
  "@types/react": "^19.2.14",
87
90
  "ink-testing-library": "^4.0.0",
88
91
  "react-devtools-core": "^7.0.1",
@@ -90,7 +93,7 @@
90
93
  },
91
94
  "dependencies": {
92
95
  "ink": "^6.8.0",
93
- "oxc-parser": "^0.115.0",
96
+ "oxc-parser": "^0.116.0",
94
97
  "react": "^19.2.4",
95
98
  "zod": "^4.3.6"
96
99
  }