@rainy-updates/cli 0.5.6 → 0.6.0
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 +133 -0
- package/README.md +90 -31
- package/dist/bin/cli.js +24 -482
- package/dist/bin/dispatch.d.ts +16 -0
- package/dist/bin/dispatch.js +147 -0
- package/dist/bin/help.d.ts +1 -0
- package/dist/bin/help.js +314 -0
- package/dist/cache/cache.js +13 -11
- package/dist/commands/audit/parser.js +2 -2
- package/dist/commands/audit/runner.js +27 -46
- package/dist/commands/audit/targets.js +13 -13
- package/dist/commands/bisect/oracle.js +28 -11
- package/dist/commands/bisect/parser.js +3 -3
- package/dist/commands/bisect/runner.js +15 -8
- package/dist/commands/changelog/fetcher.js +11 -5
- package/dist/commands/dashboard/parser.js +103 -1
- package/dist/commands/dashboard/runner.d.ts +2 -2
- package/dist/commands/dashboard/runner.js +67 -37
- package/dist/commands/doctor/parser.js +15 -4
- package/dist/commands/doctor/runner.js +6 -3
- package/dist/commands/ga/parser.js +4 -4
- package/dist/commands/ga/runner.js +13 -7
- package/dist/commands/health/parser.js +2 -2
- package/dist/commands/licenses/runner.js +4 -4
- package/dist/commands/resolve/runner.js +9 -4
- package/dist/commands/review/parser.js +57 -4
- package/dist/commands/review/runner.js +31 -5
- package/dist/commands/snapshot/runner.js +17 -17
- package/dist/commands/snapshot/store.d.ts +0 -12
- package/dist/commands/snapshot/store.js +26 -38
- package/dist/commands/unused/runner.js +6 -7
- package/dist/commands/unused/scanner.js +17 -20
- package/dist/config/loader.d.ts +2 -2
- package/dist/config/loader.js +2 -5
- package/dist/config/policy.js +20 -11
- package/dist/core/analysis/options.d.ts +6 -0
- package/dist/core/analysis/options.js +69 -0
- package/dist/core/analysis/review-items.d.ts +4 -0
- package/dist/core/analysis/review-items.js +128 -0
- package/dist/core/analysis/run-silenced.d.ts +1 -0
- package/dist/core/analysis/run-silenced.js +13 -0
- package/dist/core/analysis-bundle.js +3 -211
- package/dist/core/artifacts.js +6 -5
- package/dist/core/baseline.js +3 -5
- package/dist/core/check.js +2 -2
- package/dist/core/ci.js +52 -1
- package/dist/core/decision-plan.d.ts +14 -0
- package/dist/core/decision-plan.js +107 -0
- package/dist/core/doctor/findings.d.ts +2 -0
- package/dist/core/doctor/findings.js +166 -0
- package/dist/core/doctor/render.d.ts +3 -0
- package/dist/core/doctor/render.js +44 -0
- package/dist/core/doctor/result.d.ts +2 -0
- package/dist/core/doctor/result.js +58 -0
- package/dist/core/doctor/score.d.ts +5 -0
- package/dist/core/doctor/score.js +28 -0
- package/dist/core/fix-pr-batch.js +38 -28
- package/dist/core/fix-pr.js +27 -24
- package/dist/core/init-ci.js +25 -21
- package/dist/core/options.js +95 -4
- package/dist/core/review-model.d.ts +3 -3
- package/dist/core/review-model.js +6 -67
- package/dist/core/review-verdict.d.ts +2 -0
- package/dist/core/review-verdict.js +14 -0
- package/dist/core/summary.js +12 -0
- package/dist/core/upgrade.js +64 -2
- package/dist/core/verification.d.ts +2 -0
- package/dist/core/verification.js +106 -0
- package/dist/core/warm-cache.js +2 -2
- package/dist/output/format.js +22 -0
- package/dist/output/github.js +10 -0
- package/dist/output/sarif.js +16 -12
- package/dist/parsers/package-json.js +2 -4
- package/dist/pm/detect.d.ts +3 -1
- package/dist/pm/detect.js +24 -12
- package/dist/pm/install.d.ts +2 -1
- package/dist/pm/install.js +15 -16
- package/dist/registry/npm.js +34 -76
- package/dist/rup +0 -0
- package/dist/types/index.d.ts +104 -5
- package/dist/ui/tui.d.ts +4 -1
- package/dist/ui/tui.js +5 -4
- package/dist/utils/io.js +5 -6
- package/dist/utils/lockfile.js +24 -19
- package/dist/utils/runtime-paths.d.ts +4 -0
- package/dist/utils/runtime-paths.js +35 -0
- package/dist/utils/runtime.d.ts +7 -0
- package/dist/utils/runtime.js +32 -0
- package/dist/workspace/discover.js +55 -51
- package/package.json +16 -16
- package/dist/ui/dashboard/DashboardTUI.d.ts +0 -6
- package/dist/ui/dashboard/DashboardTUI.js +0 -34
- package/dist/ui/dashboard/components/DetailPanel.d.ts +0 -4
- package/dist/ui/dashboard/components/DetailPanel.js +0 -30
- package/dist/ui/dashboard/components/Footer.d.ts +0 -4
- package/dist/ui/dashboard/components/Footer.js +0 -9
- package/dist/ui/dashboard/components/Header.d.ts +0 -4
- package/dist/ui/dashboard/components/Header.js +0 -12
- package/dist/ui/dashboard/components/Sidebar.d.ts +0 -4
- package/dist/ui/dashboard/components/Sidebar.js +0 -23
- package/dist/ui/dashboard/store.d.ts +0 -34
- package/dist/ui/dashboard/store.js +0 -148
package/dist/utils/lockfile.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
const LOCKFILE_NAMES = [
|
|
3
|
+
"package-lock.json",
|
|
4
|
+
"npm-shrinkwrap.json",
|
|
5
|
+
"pnpm-lock.yaml",
|
|
6
|
+
"yarn.lock",
|
|
7
|
+
"bun.lock",
|
|
8
|
+
];
|
|
5
9
|
export async function captureLockfileSnapshot(cwd) {
|
|
6
10
|
const snapshot = new Map();
|
|
7
11
|
for (const name of LOCKFILE_NAMES) {
|
|
8
12
|
const filePath = path.join(cwd, name);
|
|
9
|
-
|
|
10
|
-
const content = await fs.readFile(filePath);
|
|
11
|
-
snapshot.set(filePath, hashBuffer(content));
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
snapshot.set(filePath, null);
|
|
15
|
-
}
|
|
13
|
+
snapshot.set(filePath, await readLockfileHash(filePath));
|
|
16
14
|
}
|
|
17
15
|
return snapshot;
|
|
18
16
|
}
|
|
@@ -20,14 +18,7 @@ export async function changedLockfiles(cwd, before) {
|
|
|
20
18
|
const changed = [];
|
|
21
19
|
for (const name of LOCKFILE_NAMES) {
|
|
22
20
|
const filePath = path.join(cwd, name);
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
const content = await fs.readFile(filePath);
|
|
26
|
-
current = hashBuffer(content);
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
current = null;
|
|
30
|
-
}
|
|
21
|
+
const current = await readLockfileHash(filePath);
|
|
31
22
|
if ((before.get(filePath) ?? null) !== current) {
|
|
32
23
|
changed.push(filePath);
|
|
33
24
|
}
|
|
@@ -39,6 +30,20 @@ export function validateLockfileMode(mode, install) {
|
|
|
39
30
|
throw new Error("--lockfile-mode update requires --install to update lockfiles deterministically.");
|
|
40
31
|
}
|
|
41
32
|
}
|
|
33
|
+
async function readLockfileHash(filePath) {
|
|
34
|
+
try {
|
|
35
|
+
const file = Bun.file(filePath);
|
|
36
|
+
if (!(await file.exists()))
|
|
37
|
+
return null;
|
|
38
|
+
const content = await file.bytes();
|
|
39
|
+
return hashBuffer(content);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
42
45
|
function hashBuffer(value) {
|
|
43
|
-
|
|
46
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
47
|
+
hasher.update(value);
|
|
48
|
+
return hasher.digest("hex");
|
|
44
49
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getRuntimeCwd, readEnv } from "./runtime.js";
|
|
4
|
+
function resolveHomeDir() {
|
|
5
|
+
return readEnv("HOME") ?? readEnv("USERPROFILE") ?? getRuntimeCwd();
|
|
6
|
+
}
|
|
7
|
+
export function getHomeDir() {
|
|
8
|
+
return resolveHomeDir();
|
|
9
|
+
}
|
|
10
|
+
export function getCacheDir(appName = "rainy-updates") {
|
|
11
|
+
return path.join(readEnv("XDG_CACHE_HOME") ?? path.join(resolveHomeDir(), ".cache"), appName);
|
|
12
|
+
}
|
|
13
|
+
export function getTempDir() {
|
|
14
|
+
return (readEnv("BUN_TMPDIR") ??
|
|
15
|
+
readEnv("TMPDIR") ??
|
|
16
|
+
readEnv("TMP") ??
|
|
17
|
+
readEnv("TEMP") ??
|
|
18
|
+
"/tmp");
|
|
19
|
+
}
|
|
20
|
+
export async function createTempDir(prefix) {
|
|
21
|
+
const baseDir = getTempDir();
|
|
22
|
+
for (let attempt = 0; attempt < 16; attempt += 1) {
|
|
23
|
+
const candidate = path.join(baseDir, `${prefix}${crypto.randomUUID()}`);
|
|
24
|
+
try {
|
|
25
|
+
await mkdir(candidate, { recursive: false });
|
|
26
|
+
return candidate;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST") {
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`Unable to create temp directory for prefix ${prefix}`);
|
|
35
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function getRuntimeCwd(): string;
|
|
2
|
+
export declare function getRuntimeArgv(): string[];
|
|
3
|
+
export declare function readEnv(name: string): string | undefined;
|
|
4
|
+
export declare function writeStdout(message: string): void;
|
|
5
|
+
export declare function writeStderr(message: string): void;
|
|
6
|
+
export declare function setRuntimeExitCode(code: number): void;
|
|
7
|
+
export declare function exitProcess(code: number): never;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function getRuntimeCwd() {
|
|
2
|
+
return process.cwd();
|
|
3
|
+
}
|
|
4
|
+
export function getRuntimeArgv() {
|
|
5
|
+
if (typeof process !== "undefined" && Array.isArray(process.argv)) {
|
|
6
|
+
return process.argv.slice(2);
|
|
7
|
+
}
|
|
8
|
+
if (typeof Bun !== "undefined" && Array.isArray(Bun.argv)) {
|
|
9
|
+
return Bun.argv.slice(2);
|
|
10
|
+
}
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
export function readEnv(name) {
|
|
14
|
+
if (typeof Bun !== "undefined") {
|
|
15
|
+
const value = Bun.env[name];
|
|
16
|
+
if (value)
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
return process.env[name];
|
|
20
|
+
}
|
|
21
|
+
export function writeStdout(message) {
|
|
22
|
+
process.stdout.write(message);
|
|
23
|
+
}
|
|
24
|
+
export function writeStderr(message) {
|
|
25
|
+
process.stderr.write(message);
|
|
26
|
+
}
|
|
27
|
+
export function setRuntimeExitCode(code) {
|
|
28
|
+
process.exitCode = code;
|
|
29
|
+
}
|
|
30
|
+
export function exitProcess(code) {
|
|
31
|
+
process.exit(code);
|
|
32
|
+
}
|
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
3
|
-
const HARD_IGNORE_DIRS = new Set([
|
|
2
|
+
const HARD_IGNORE_DIRS = new Set([
|
|
3
|
+
"node_modules",
|
|
4
|
+
".git",
|
|
5
|
+
".turbo",
|
|
6
|
+
".next",
|
|
7
|
+
"dist",
|
|
8
|
+
"coverage",
|
|
9
|
+
]);
|
|
4
10
|
const MAX_DISCOVERED_DIRS = 20000;
|
|
5
11
|
export async function discoverPackageDirs(cwd, workspaceMode) {
|
|
6
12
|
if (!workspaceMode) {
|
|
7
13
|
return [cwd];
|
|
8
14
|
}
|
|
9
15
|
const roots = new Set([cwd]);
|
|
10
|
-
const patterns = [
|
|
16
|
+
const patterns = [
|
|
17
|
+
...(await readPackageJsonWorkspacePatterns(cwd)),
|
|
18
|
+
...(await readPnpmWorkspacePatterns(cwd)),
|
|
19
|
+
];
|
|
11
20
|
const include = patterns.filter((item) => !item.startsWith("!"));
|
|
12
|
-
const exclude = patterns
|
|
21
|
+
const exclude = patterns
|
|
22
|
+
.filter((item) => item.startsWith("!"))
|
|
23
|
+
.map((item) => item.slice(1));
|
|
13
24
|
for (const pattern of include) {
|
|
14
25
|
const dirs = await expandWorkspacePattern(cwd, pattern);
|
|
15
26
|
for (const dir of dirs) {
|
|
@@ -25,21 +36,24 @@ export async function discoverPackageDirs(cwd, workspaceMode) {
|
|
|
25
36
|
const existing = [];
|
|
26
37
|
for (const dir of roots) {
|
|
27
38
|
const packageJsonPath = path.join(dir, "package.json");
|
|
28
|
-
|
|
29
|
-
await fs.access(packageJsonPath);
|
|
39
|
+
if (await packageFileExists(packageJsonPath)) {
|
|
30
40
|
existing.push(dir);
|
|
31
41
|
}
|
|
32
|
-
catch {
|
|
33
|
-
// ignore missing package.json
|
|
34
|
-
}
|
|
35
42
|
}
|
|
36
43
|
return existing.sort();
|
|
37
44
|
}
|
|
45
|
+
async function packageFileExists(packageJsonPath) {
|
|
46
|
+
try {
|
|
47
|
+
return await Bun.file(packageJsonPath).exists();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
38
53
|
async function readPackageJsonWorkspacePatterns(cwd) {
|
|
39
54
|
const packagePath = path.join(cwd, "package.json");
|
|
40
55
|
try {
|
|
41
|
-
const
|
|
42
|
-
const parsed = JSON.parse(content);
|
|
56
|
+
const parsed = (await Bun.file(packagePath).json());
|
|
43
57
|
if (Array.isArray(parsed.workspaces)) {
|
|
44
58
|
return parsed.workspaces;
|
|
45
59
|
}
|
|
@@ -55,7 +69,7 @@ async function readPackageJsonWorkspacePatterns(cwd) {
|
|
|
55
69
|
async function readPnpmWorkspacePatterns(cwd) {
|
|
56
70
|
const workspacePath = path.join(cwd, "pnpm-workspace.yaml");
|
|
57
71
|
try {
|
|
58
|
-
const content = await
|
|
72
|
+
const content = await Bun.file(workspacePath).text();
|
|
59
73
|
const lines = content.split(/\r?\n/);
|
|
60
74
|
const patterns = [];
|
|
61
75
|
for (const line of lines) {
|
|
@@ -74,53 +88,43 @@ async function readPnpmWorkspacePatterns(cwd) {
|
|
|
74
88
|
}
|
|
75
89
|
}
|
|
76
90
|
async function expandWorkspacePattern(cwd, pattern) {
|
|
77
|
-
const normalized = pattern
|
|
91
|
+
const normalized = pattern
|
|
92
|
+
.replace(/\\/g, "/")
|
|
93
|
+
.replace(/^\.\//, "")
|
|
94
|
+
.replace(/\/+$/, "");
|
|
78
95
|
if (normalized.length === 0)
|
|
79
96
|
return [];
|
|
80
97
|
if (!normalized.includes("*")) {
|
|
81
98
|
return [path.resolve(cwd, normalized)];
|
|
82
99
|
}
|
|
83
|
-
const segments = normalized.split("/").filter(Boolean);
|
|
84
100
|
const results = new Set();
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
await collectMatches(child, segments, index, out);
|
|
102
|
-
}
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (segment === "*") {
|
|
106
|
-
const children = await readChildDirs(baseDir);
|
|
107
|
-
for (const child of children) {
|
|
108
|
-
await collectMatches(child, segments, index + 1, out);
|
|
101
|
+
const manifestPattern = normalized.endsWith("/package.json")
|
|
102
|
+
? normalized
|
|
103
|
+
: `${normalized}/package.json`;
|
|
104
|
+
const glob = new Bun.Glob(manifestPattern);
|
|
105
|
+
for await (const match of glob.scan({
|
|
106
|
+
cwd,
|
|
107
|
+
absolute: true,
|
|
108
|
+
dot: false,
|
|
109
|
+
onlyFiles: true,
|
|
110
|
+
})) {
|
|
111
|
+
const dir = path.dirname(match);
|
|
112
|
+
if (shouldIgnoreWorkspaceDir(cwd, dir))
|
|
113
|
+
continue;
|
|
114
|
+
results.add(dir);
|
|
115
|
+
if (results.size > MAX_DISCOVERED_DIRS) {
|
|
116
|
+
throw new Error(`Workspace discovery exceeded ${MAX_DISCOVERED_DIRS} directories. Refine workspace patterns.`);
|
|
109
117
|
}
|
|
110
|
-
return;
|
|
111
118
|
}
|
|
112
|
-
|
|
119
|
+
return Array.from(results);
|
|
113
120
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return
|
|
118
|
-
.filter((entry) => entry.isDirectory())
|
|
119
|
-
.filter((entry) => !HARD_IGNORE_DIRS.has(entry.name))
|
|
120
|
-
.filter((entry) => !entry.name.startsWith("."))
|
|
121
|
-
.map((entry) => path.join(dir, entry.name));
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
return [];
|
|
121
|
+
function shouldIgnoreWorkspaceDir(cwd, dir) {
|
|
122
|
+
const relative = path.relative(cwd, dir);
|
|
123
|
+
if (relative.length === 0 || relative.startsWith("..")) {
|
|
124
|
+
return false;
|
|
125
125
|
}
|
|
126
|
+
return relative
|
|
127
|
+
.split(path.sep)
|
|
128
|
+
.filter(Boolean)
|
|
129
|
+
.some((segment) => HARD_IGNORE_DIRS.has(segment) || segment.startsWith("."));
|
|
126
130
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rainy-updates/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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,
|
|
@@ -52,23 +52,25 @@
|
|
|
52
52
|
"scripts": {
|
|
53
53
|
"clean": "rm -rf dist",
|
|
54
54
|
"build": "tsc -p tsconfig.build.json",
|
|
55
|
+
"build:exe": "bun build ./src/bin/cli.ts --compile --outfile dist/rup",
|
|
55
56
|
"typecheck": "tsc --noEmit",
|
|
56
57
|
"test": "bun test",
|
|
57
58
|
"lint": "bunx biome check src tests",
|
|
58
59
|
"check": "bun run typecheck && bun test",
|
|
59
|
-
"bench:fixtures": "
|
|
60
|
-
"bench:check": "bun run build && bun run bench:fixtures &&
|
|
61
|
-
"bench:review": "bun run build && bun run bench:fixtures &&
|
|
62
|
-
"bench:resolve": "bun run build && bun run bench:fixtures &&
|
|
63
|
-
"bench:ci": "bun run build && bun run bench:fixtures &&
|
|
64
|
-
"perf:smoke": "
|
|
65
|
-
"perf:check": "
|
|
66
|
-
"perf:resolve": "RAINY_UPDATES_PERF_SCENARIO=resolve
|
|
67
|
-
"perf:ci": "RAINY_UPDATES_PERF_SCENARIO=ci
|
|
68
|
-
"test:prod": "
|
|
69
|
-
"prepublishOnly": "bun run check && bun run build && bun run test:prod"
|
|
60
|
+
"bench:fixtures": "bun run scripts/generate-benchmark-fixtures.mjs",
|
|
61
|
+
"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",
|
|
62
|
+
"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",
|
|
63
|
+
"bench:resolve": "bun run build && bun run bench:fixtures && bun run scripts/benchmark.mjs single-100 resolve cold 3 && bun run scripts/benchmark.mjs single-100 resolve warm 3",
|
|
64
|
+
"bench:ci": "bun run build && bun run bench:fixtures && bun run scripts/benchmark.mjs mono-1000 ci cold 3 && bun run scripts/benchmark.mjs mono-1000 ci warm 3",
|
|
65
|
+
"perf:smoke": "bun run scripts/perf-smoke.mjs && RAINY_UPDATES_PERF_SCENARIO=resolve bun run scripts/perf-smoke.mjs && RAINY_UPDATES_PERF_SCENARIO=ci bun run scripts/perf-smoke.mjs",
|
|
66
|
+
"perf:check": "bun run scripts/perf-smoke.mjs",
|
|
67
|
+
"perf:resolve": "RAINY_UPDATES_PERF_SCENARIO=resolve bun run scripts/perf-smoke.mjs",
|
|
68
|
+
"perf:ci": "RAINY_UPDATES_PERF_SCENARIO=ci bun run scripts/perf-smoke.mjs",
|
|
69
|
+
"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",
|
|
70
|
+
"prepublishOnly": "bun run check && bun run build && bun run build:exe && bun run test:prod"
|
|
70
71
|
},
|
|
71
72
|
"engines": {
|
|
73
|
+
"bun": ">=1.3.9",
|
|
72
74
|
"node": ">=20"
|
|
73
75
|
},
|
|
74
76
|
"publishConfig": {
|
|
@@ -76,14 +78,12 @@
|
|
|
76
78
|
"provenance": true
|
|
77
79
|
},
|
|
78
80
|
"devDependencies": {
|
|
79
|
-
"@types/bun": "
|
|
81
|
+
"@types/bun": "1.3.9",
|
|
80
82
|
"@types/react": "^19.2.14",
|
|
81
83
|
"ink-testing-library": "^4.0.0",
|
|
84
|
+
"react-devtools-core": "^7.0.1",
|
|
82
85
|
"typescript": "^5.9.3"
|
|
83
86
|
},
|
|
84
|
-
"optionalDependencies": {
|
|
85
|
-
"undici": "^7.22.0"
|
|
86
|
-
},
|
|
87
87
|
"dependencies": {
|
|
88
88
|
"ink": "^6.8.0",
|
|
89
89
|
"react": "^19.2.4",
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { DashboardOptions, CheckResult } from "../../types/index.js";
|
|
2
|
-
export interface DashboardTUIProps {
|
|
3
|
-
options: DashboardOptions;
|
|
4
|
-
initialResult: CheckResult;
|
|
5
|
-
}
|
|
6
|
-
export declare function DashboardTUI({ options, initialResult }: DashboardTUIProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, useInput, useApp } from "ink";
|
|
3
|
-
import { initStore, dashboardActions } from "./store.js";
|
|
4
|
-
import { Header } from "./components/Header.js";
|
|
5
|
-
import { Sidebar } from "./components/Sidebar.js";
|
|
6
|
-
import { DetailPanel } from "./components/DetailPanel.js";
|
|
7
|
-
import { Footer } from "./components/Footer.js";
|
|
8
|
-
export function DashboardTUI({ options, initialResult }) {
|
|
9
|
-
const { exit } = useApp();
|
|
10
|
-
// Initialize the singleton store synchronously before rendering children
|
|
11
|
-
// so that components can access it on the first render pass natively.
|
|
12
|
-
initStore(options, initialResult);
|
|
13
|
-
// Handle global keyboard input (doesn't trigger React re-renders unless store state affects this component)
|
|
14
|
-
// Our static layout theoretically will not re-render off this hook alone.
|
|
15
|
-
useInput((input, key) => {
|
|
16
|
-
if (key.upArrow) {
|
|
17
|
-
dashboardActions.moveCursorUp();
|
|
18
|
-
}
|
|
19
|
-
if (key.downArrow) {
|
|
20
|
-
dashboardActions.moveCursorDown();
|
|
21
|
-
}
|
|
22
|
-
if (key.return) {
|
|
23
|
-
dashboardActions.setShouldApply(true);
|
|
24
|
-
exit();
|
|
25
|
-
}
|
|
26
|
-
if (input === "r") {
|
|
27
|
-
void dashboardActions.runResolveAction();
|
|
28
|
-
}
|
|
29
|
-
if (input === "a") {
|
|
30
|
-
void dashboardActions.runAuditAction();
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
return (_jsxs(Box, { flexDirection: "column", minHeight: 25, children: [_jsx(Header, {}), _jsxs(Box, { flexDirection: "row", width: "100%", children: [_jsx(Sidebar, {}), _jsx(DetailPanel, {})] }), _jsx(Footer, {})] }));
|
|
34
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React from "react";
|
|
3
|
-
import { Box, Text } from "ink";
|
|
4
|
-
import { useDashboardStore } from "../store.js";
|
|
5
|
-
function getRiskColor(risk) {
|
|
6
|
-
switch (risk) {
|
|
7
|
-
case "critical":
|
|
8
|
-
return "red";
|
|
9
|
-
case "high":
|
|
10
|
-
return "red";
|
|
11
|
-
case "medium":
|
|
12
|
-
return "yellow";
|
|
13
|
-
case "low":
|
|
14
|
-
return "blue";
|
|
15
|
-
default:
|
|
16
|
-
return "gray";
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
function DetailPanelComponent() {
|
|
20
|
-
const selectedIndex = useDashboardStore((s) => s.selectedIndex);
|
|
21
|
-
const updates = useDashboardStore((s) => s.updates);
|
|
22
|
-
// Fallback if list is empty
|
|
23
|
-
const update = updates[selectedIndex];
|
|
24
|
-
if (!update) {
|
|
25
|
-
return (_jsx(Box, { width: "50%", height: 22, padding: 1, borderStyle: "single", borderColor: "gray", children: _jsx(Text, { dimColor: true, children: "No package selected." }) }));
|
|
26
|
-
}
|
|
27
|
-
const { name, publishedAt, publishAgeDays, riskLevel, homepage, advisoryCount, peerConflictSeverity, } = update;
|
|
28
|
-
return (_jsxs(Box, { width: "50%", height: 22, flexDirection: "column", paddingX: 2, borderStyle: "single", borderColor: "gray", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: name }) }), _jsxs(Box, { marginBottom: 2, flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Risk: " }), _jsx(Text, { color: getRiskColor(riskLevel), children: riskLevel || "unknown" })] }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Advisories: " }), _jsx(Text, { color: advisoryCount ? "red" : "green", children: advisoryCount || 0 })] }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Peer Conflicts: " }), _jsx(Text, { color: peerConflictSeverity ? "red" : "green", children: peerConflictSeverity || "none" })] })] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Text, { color: "gray", children: homepage || "No homepage provided" }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Published ", publishAgeDays, " days ago", " ", publishedAt ? `(${publishedAt})` : ""] }) })] }));
|
|
29
|
-
}
|
|
30
|
-
export const DetailPanel = React.memo(DetailPanelComponent);
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React from "react";
|
|
3
|
-
import { Box, Text } from "ink";
|
|
4
|
-
import { useDashboardStore } from "../store.js";
|
|
5
|
-
function FooterComponent() {
|
|
6
|
-
const modal = useDashboardStore((s) => s.modal);
|
|
7
|
-
return (_jsxs(Box, { width: "100%", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, color: "white", children: "\u2191\u2193" }), " ", "Navigate |", _jsxs(Text, { bold: true, color: "white", children: [" ", "Enter"] }), " ", "Apply |", _jsxs(Text, { bold: true, color: "white", children: [" ", "r"] }), "esolve |", _jsxs(Text, { bold: true, color: "white", children: [" ", "a"] }), "udit |", _jsxs(Text, { bold: true, color: "white", children: [" ", "i"] }), "gnore"] }) }), _jsx(Box, { children: _jsx(Text, { color: "yellow", children: modal !== "none" ? `Status: ${modal}...` : "Status: Idle" }) })] }));
|
|
8
|
-
}
|
|
9
|
-
export const Footer = React.memo(FooterComponent);
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React from "react";
|
|
3
|
-
import { Box, Text } from "ink";
|
|
4
|
-
import { useDashboardStore } from "../store.js";
|
|
5
|
-
function HeaderComponent() {
|
|
6
|
-
const scanned = useDashboardStore((s) => s.summary.scannedPackages);
|
|
7
|
-
const totalUpdates = useDashboardStore((s) => s.updates.length);
|
|
8
|
-
const view = useDashboardStore((s) => s.view);
|
|
9
|
-
return (_jsxs(Box, { width: "100%", paddingX: 1, borderStyle: "single", borderColor: "blue", flexDirection: "row", justifyContent: "space-between", children: [_jsx(Box, { children: _jsx(Text, { color: "cyan", bold: true, children: "\uD83C\uDF27\uFE0F Rainy Updates Dashboard" }) }), _jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { color: view === "dependencies" ? "green" : "gray", children: "[Dependencies]" }), " ", _jsx(Text, { color: view === "security" ? "green" : "gray", children: "[Security]" }), " ", _jsx(Text, { color: view === "health" ? "green" : "gray", children: "[Health]" })] }) }), _jsx(Box, { children: _jsxs(Text, { dimColor: true, children: ["Packages: ", scanned, " | Found: ", totalUpdates] }) })] }));
|
|
10
|
-
}
|
|
11
|
-
// Memoize to ensure Header only renders when deeply affected (which is rare)
|
|
12
|
-
export const Header = React.memo(HeaderComponent);
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React from "react";
|
|
3
|
-
import { Box, Text } from "ink";
|
|
4
|
-
import { useDashboardStore } from "../store.js";
|
|
5
|
-
// A heavily memoized single row
|
|
6
|
-
const DependencyRow = React.memo(({ update, isActive }) => {
|
|
7
|
-
return (_jsxs(Box, { paddingX: 1, width: "100%", children: [_jsx(Box, { width: 2, children: _jsx(Text, { color: "cyan", children: isActive ? "> " : " " }) }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: isActive ? "white" : "gray", bold: isActive, children: update.name }) }), _jsx(Box, { width: 15, justifyContent: "flex-end", children: _jsx(Text, { dimColor: true, children: update.fromRange }) }), _jsx(Box, { width: 3, justifyContent: "center", children: _jsx(Text, { dimColor: true, children: "\u2192" }) }), _jsx(Box, { width: 15, children: _jsx(Text, { color: "green", children: update.toRange }) })] }));
|
|
8
|
-
});
|
|
9
|
-
DependencyRow.displayName = "DependencyRow";
|
|
10
|
-
function SidebarComponent() {
|
|
11
|
-
const updates = useDashboardStore((s) => s.updates);
|
|
12
|
-
const selectedIndex = useDashboardStore((s) => s.selectedIndex);
|
|
13
|
-
// Simple windowing: in a real robust TUI we'd calculate terminal height
|
|
14
|
-
// For now we'll just slice the array based on a fixed viewport (e.g., 20 items)
|
|
15
|
-
const windowSize = 20;
|
|
16
|
-
const start = Math.max(0, Math.min(selectedIndex - windowSize / 2, updates.length - windowSize));
|
|
17
|
-
const visibleUpdates = updates.slice(start, start + windowSize);
|
|
18
|
-
return (_jsxs(Box, { width: "50%", flexDirection: "column", borderStyle: "single", borderColor: "gray", height: windowSize + 2, children: [visibleUpdates.map((update, i) => {
|
|
19
|
-
const actualIndex = start + i;
|
|
20
|
-
return (_jsx(DependencyRow, { update: update, index: actualIndex, isActive: actualIndex === selectedIndex }, `${update.name}-${update.toRange}`));
|
|
21
|
-
}), updates.length === 0 && (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "No updates found." }) }))] }));
|
|
22
|
-
}
|
|
23
|
-
export const Sidebar = React.memo(SidebarComponent);
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { DashboardOptions, CheckResult, PackageUpdate } from "../../types/index.js";
|
|
2
|
-
export interface DashboardState {
|
|
3
|
-
selectedIndex: number;
|
|
4
|
-
view: "dependencies" | "security" | "health";
|
|
5
|
-
modal: "none" | "resolving" | "auditing" | "applying";
|
|
6
|
-
updates: PackageUpdate[];
|
|
7
|
-
summary: CheckResult["summary"];
|
|
8
|
-
options: DashboardOptions;
|
|
9
|
-
error?: string;
|
|
10
|
-
shouldApply: boolean;
|
|
11
|
-
}
|
|
12
|
-
type Listener = () => void;
|
|
13
|
-
declare class DashboardStore {
|
|
14
|
-
private state;
|
|
15
|
-
private listeners;
|
|
16
|
-
constructor(initialState: DashboardState);
|
|
17
|
-
getState: () => DashboardState;
|
|
18
|
-
setState: (partial: Partial<DashboardState> | ((state: DashboardState) => Partial<DashboardState>)) => void;
|
|
19
|
-
subscribe: (listener: Listener) => () => boolean;
|
|
20
|
-
private emit;
|
|
21
|
-
}
|
|
22
|
-
export declare function initStore(options: DashboardOptions, initialResult: CheckResult): DashboardStore;
|
|
23
|
-
export declare function useDashboardStore<T>(selector: (state: DashboardState) => T): T;
|
|
24
|
-
export declare const dashboardActions: {
|
|
25
|
-
moveCursorUp: () => void;
|
|
26
|
-
moveCursorDown: () => void;
|
|
27
|
-
setView: (view: DashboardState["view"]) => void;
|
|
28
|
-
setModal: (modal: DashboardState["modal"]) => void;
|
|
29
|
-
setShouldApply: (shouldApply: boolean) => void;
|
|
30
|
-
runResolveAction: () => Promise<void>;
|
|
31
|
-
runAuditAction: () => Promise<void>;
|
|
32
|
-
};
|
|
33
|
-
export declare function getStore(): DashboardStore | null;
|
|
34
|
-
export {};
|