@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 +58 -0
- package/README.md +12 -0
- package/dist/bin/help.js +1 -0
- package/dist/commands/bisect/oracle.js +3 -2
- package/dist/commands/dashboard/runner.js +3 -0
- package/dist/commands/ga/runner.js +63 -2
- package/dist/core/doctor/findings.js +4 -4
- package/dist/core/verification.js +3 -3
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/rup +0 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/ui/dashboard-state.d.ts +7 -0
- package/dist/ui/dashboard-state.js +44 -0
- package/dist/ui/tui.d.ts +3 -0
- package/dist/ui/tui.js +192 -80
- package/dist/utils/shell.d.ts +6 -0
- package/dist/utils/shell.js +18 -0
- package/package.json +8 -5
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
|
@@ -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
|
|
33
|
-
const proc = Bun.spawn([
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 --
|
|
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 --
|
|
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 {
|
|
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
|
|
52
|
-
const proc = Bun.spawn([shell,
|
|
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
|
+
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.
|
|
2
|
+
export const CLI_VERSION = "0.6.2";
|
package/dist/rup
CHANGED
|
Binary file
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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({
|
|
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({
|
|
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(
|
|
220
|
+
if (input === "q" || (key.escape && !state.showHelp)) {
|
|
221
|
+
onComplete(selectedItems);
|
|
209
222
|
return;
|
|
210
223
|
}
|
|
211
224
|
if (key.return) {
|
|
212
|
-
onComplete(
|
|
225
|
+
onComplete(selectedItems);
|
|
213
226
|
}
|
|
214
227
|
});
|
|
215
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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,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.
|
|
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": "
|
|
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
|
|
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.
|
|
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.
|
|
96
|
+
"oxc-parser": "^0.116.0",
|
|
94
97
|
"react": "^19.2.4",
|
|
95
98
|
"zod": "^4.3.6"
|
|
96
99
|
}
|