@rainy-updates/cli 0.5.7 → 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.
Files changed (84) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +90 -31
  3. package/dist/bin/cli.js +16 -16
  4. package/dist/bin/dispatch.js +29 -32
  5. package/dist/bin/help.js +32 -2
  6. package/dist/cache/cache.js +13 -11
  7. package/dist/commands/audit/parser.js +2 -2
  8. package/dist/commands/audit/runner.js +27 -46
  9. package/dist/commands/audit/targets.js +13 -13
  10. package/dist/commands/bisect/oracle.js +28 -11
  11. package/dist/commands/bisect/parser.js +3 -3
  12. package/dist/commands/bisect/runner.js +15 -8
  13. package/dist/commands/changelog/fetcher.js +11 -5
  14. package/dist/commands/dashboard/parser.js +103 -1
  15. package/dist/commands/dashboard/runner.d.ts +2 -2
  16. package/dist/commands/dashboard/runner.js +67 -37
  17. package/dist/commands/doctor/parser.js +9 -4
  18. package/dist/commands/doctor/runner.js +2 -2
  19. package/dist/commands/ga/parser.js +4 -4
  20. package/dist/commands/ga/runner.js +13 -7
  21. package/dist/commands/health/parser.js +2 -2
  22. package/dist/commands/licenses/runner.js +4 -4
  23. package/dist/commands/resolve/runner.js +9 -4
  24. package/dist/commands/review/parser.js +57 -4
  25. package/dist/commands/review/runner.js +31 -5
  26. package/dist/commands/snapshot/runner.js +17 -17
  27. package/dist/commands/snapshot/store.d.ts +0 -12
  28. package/dist/commands/snapshot/store.js +26 -38
  29. package/dist/commands/unused/runner.js +6 -7
  30. package/dist/commands/unused/scanner.js +17 -20
  31. package/dist/config/loader.d.ts +2 -2
  32. package/dist/config/loader.js +2 -5
  33. package/dist/config/policy.js +20 -11
  34. package/dist/core/analysis/run-silenced.js +0 -1
  35. package/dist/core/artifacts.js +6 -5
  36. package/dist/core/baseline.js +3 -5
  37. package/dist/core/check.js +2 -2
  38. package/dist/core/ci.js +52 -1
  39. package/dist/core/decision-plan.d.ts +14 -0
  40. package/dist/core/decision-plan.js +107 -0
  41. package/dist/core/doctor/result.js +8 -5
  42. package/dist/core/fix-pr-batch.js +38 -28
  43. package/dist/core/fix-pr.js +27 -24
  44. package/dist/core/init-ci.js +25 -21
  45. package/dist/core/options.js +95 -4
  46. package/dist/core/review-model.js +3 -0
  47. package/dist/core/summary.js +6 -0
  48. package/dist/core/upgrade.js +64 -2
  49. package/dist/core/verification.d.ts +2 -0
  50. package/dist/core/verification.js +106 -0
  51. package/dist/core/warm-cache.js +2 -2
  52. package/dist/output/format.js +15 -0
  53. package/dist/output/github.js +6 -0
  54. package/dist/output/sarif.js +12 -12
  55. package/dist/parsers/package-json.js +2 -4
  56. package/dist/pm/detect.d.ts +3 -1
  57. package/dist/pm/detect.js +24 -12
  58. package/dist/pm/install.d.ts +2 -1
  59. package/dist/pm/install.js +15 -16
  60. package/dist/registry/npm.js +34 -76
  61. package/dist/rup +0 -0
  62. package/dist/types/index.d.ts +76 -5
  63. package/dist/ui/tui.d.ts +4 -1
  64. package/dist/ui/tui.js +5 -4
  65. package/dist/utils/io.js +5 -6
  66. package/dist/utils/lockfile.js +24 -19
  67. package/dist/utils/runtime-paths.d.ts +4 -0
  68. package/dist/utils/runtime-paths.js +35 -0
  69. package/dist/utils/runtime.d.ts +7 -0
  70. package/dist/utils/runtime.js +32 -0
  71. package/dist/workspace/discover.js +55 -51
  72. package/package.json +16 -16
  73. package/dist/ui/dashboard/DashboardTUI.d.ts +0 -6
  74. package/dist/ui/dashboard/DashboardTUI.js +0 -34
  75. package/dist/ui/dashboard/components/DetailPanel.d.ts +0 -4
  76. package/dist/ui/dashboard/components/DetailPanel.js +0 -30
  77. package/dist/ui/dashboard/components/Footer.d.ts +0 -4
  78. package/dist/ui/dashboard/components/Footer.js +0 -9
  79. package/dist/ui/dashboard/components/Header.d.ts +0 -4
  80. package/dist/ui/dashboard/components/Header.js +0 -12
  81. package/dist/ui/dashboard/components/Sidebar.d.ts +0 -4
  82. package/dist/ui/dashboard/components/Sidebar.js +0 -23
  83. package/dist/ui/dashboard/store.d.ts +0 -34
  84. package/dist/ui/dashboard/store.js +0 -148
@@ -1,4 +1,3 @@
1
- import process from "node:process";
2
1
  import { collectDependencies, readManifest } from "../parsers/package-json.js";
3
2
  import { matchesPattern } from "../utils/pattern.js";
4
3
  import { VersionCache } from "../cache/cache.js";
@@ -7,6 +6,7 @@ import { detectPackageManager } from "../pm/detect.js";
7
6
  import { discoverPackageDirs } from "../workspace/discover.js";
8
7
  import { createSummary, finalizeSummary } from "./summary.js";
9
8
  import { formatClassifiedMessage } from "./errors.js";
9
+ import { writeStdout } from "../utils/runtime.js";
10
10
  export async function warmCache(options) {
11
11
  const startedAt = Date.now();
12
12
  let discoveryMs = 0;
@@ -39,7 +39,7 @@ export async function warmCache(options) {
39
39
  if (!options.stream)
40
40
  return;
41
41
  streamedEvents += 1;
42
- process.stdout.write(`${message}\n`);
42
+ writeStdout(`${message}\n`);
43
43
  };
44
44
  for (const packageDir of packageDirs) {
45
45
  try {
@@ -74,6 +74,12 @@ export function renderResult(result, format, display = {}) {
74
74
  `primary_finding_code=${result.summary.primaryFindingCode ?? ""}`,
75
75
  `primary_finding_category=${result.summary.primaryFindingCategory ?? ""}`,
76
76
  `next_action_reason=${result.summary.nextActionReason ?? ""}`,
77
+ `suggested_command=${result.summary.suggestedCommand ?? ""}`,
78
+ `decision_plan=${result.summary.decisionPlan ?? ""}`,
79
+ `interactive_surface=${result.summary.interactiveSurface ?? ""}`,
80
+ `queue_focus=${result.summary.queueFocus ?? ""}`,
81
+ `verification_state=${result.summary.verificationState ?? "not-run"}`,
82
+ `verification_failures=${result.summary.verificationFailures ?? 0}`,
77
83
  ].join("\n");
78
84
  }
79
85
  const lines = [];
@@ -137,6 +143,15 @@ export function renderResult(result, format, display = {}) {
137
143
  if (typeof result.summary.dependencyHealthScore === "number") {
138
144
  lines.push(`DependencyHealthScore=${result.summary.dependencyHealthScore}, primaryFinding=${result.summary.primaryFindingCode ?? "none"}, category=${result.summary.primaryFindingCategory ?? "none"}`);
139
145
  }
146
+ if (result.summary.suggestedCommand) {
147
+ lines.push(`SuggestedCommand=${result.summary.suggestedCommand}`);
148
+ }
149
+ if (result.summary.decisionPlan) {
150
+ lines.push(`DecisionPlan=${result.summary.decisionPlan}, surface=${result.summary.interactiveSurface ?? "none"}, focus=${result.summary.queueFocus ?? "all"}`);
151
+ }
152
+ if (result.summary.verificationState && result.summary.verificationState !== "not-run") {
153
+ lines.push(`Verification=${result.summary.verificationState}, failures=${result.summary.verificationFailures ?? 0}`);
154
+ }
140
155
  if (result.summary.runId) {
141
156
  lines.push(`RunId=${result.summary.runId}, artifactManifest=${result.summary.artifactManifest ?? "none"}, blockedPackages=${result.summary.blockedPackages ?? 0}, reviewPackages=${result.summary.reviewPackages ?? 0}, monitorPackages=${result.summary.monitorPackages ?? 0}`);
142
157
  }
@@ -38,6 +38,12 @@ export async function writeGitHubOutput(filePath, result) {
38
38
  `primary_finding_code=${result.summary.primaryFindingCode ?? ""}`,
39
39
  `primary_finding_category=${result.summary.primaryFindingCategory ?? ""}`,
40
40
  `next_action_reason=${result.summary.nextActionReason ?? ""}`,
41
+ `suggested_command=${result.summary.suggestedCommand ?? ""}`,
42
+ `decision_plan=${result.summary.decisionPlan ?? ""}`,
43
+ `interactive_surface=${result.summary.interactiveSurface ?? ""}`,
44
+ `queue_focus=${result.summary.queueFocus ?? ""}`,
45
+ `verification_state=${result.summary.verificationState ?? "not-run"}`,
46
+ `verification_failures=${result.summary.verificationFailures ?? 0}`,
41
47
  `fix_pr_applied=${result.summary.fixPrApplied === true ? "1" : "0"}`,
42
48
  `fix_pr_branches_created=${result.summary.fixPrBranchesCreated}`,
43
49
  `fix_pr_branch=${result.summary.fixBranchName ?? ""}`,
@@ -1,6 +1,4 @@
1
1
  import { readFileSync } from "node:fs";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
2
  export function createSarifReport(result) {
5
3
  const dependencyRuleId = "rainy-updates/dependency-update";
6
4
  const runtimeRuleId = "rainy-updates/runtime-error";
@@ -46,7 +44,9 @@ export function createSarifReport(result) {
46
44
  runId: result.summary.runId,
47
45
  },
48
46
  }));
49
- const errorResults = [...result.errors].sort((a, b) => a.localeCompare(b)).map((error) => ({
47
+ const errorResults = [...result.errors]
48
+ .sort((a, b) => a.localeCompare(b))
49
+ .map((error) => ({
50
50
  ruleId: runtimeRuleId,
51
51
  level: "error",
52
52
  message: {
@@ -66,12 +66,16 @@ export function createSarifReport(result) {
66
66
  {
67
67
  id: dependencyRuleId,
68
68
  shortDescription: { text: "Dependency update available" },
69
- fullDescription: { text: "A dependency has a newer version according to configured target." },
69
+ fullDescription: {
70
+ text: "A dependency has a newer version according to configured target.",
71
+ },
70
72
  },
71
73
  {
72
74
  id: runtimeRuleId,
73
75
  shortDescription: { text: "Dependency resolution error" },
74
- fullDescription: { text: "The resolver could not fetch or parse package metadata." },
76
+ fullDescription: {
77
+ text: "The resolver could not fetch or parse package metadata.",
78
+ },
75
79
  },
76
80
  ],
77
81
  },
@@ -115,15 +119,11 @@ function getToolVersion() {
115
119
  if (TOOL_VERSION_CACHE)
116
120
  return TOOL_VERSION_CACHE;
117
121
  try {
118
- const currentFile = fileURLToPath(import.meta.url);
119
- const packageJsonPath = path.resolve(path.dirname(currentFile), "../../package.json");
120
- const content = readFileSync(packageJsonPath, "utf8");
121
- const parsed = JSON.parse(content);
122
- TOOL_VERSION_CACHE = parsed.version ?? "0.0.0";
123
- return TOOL_VERSION_CACHE;
122
+ const packageJson = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
123
+ TOOL_VERSION_CACHE = packageJson.version ?? "0.0.0";
124
124
  }
125
125
  catch {
126
126
  TOOL_VERSION_CACHE = "0.0.0";
127
- return TOOL_VERSION_CACHE;
128
127
  }
128
+ return TOOL_VERSION_CACHE;
129
129
  }
@@ -1,4 +1,3 @@
1
- import { promises as fs } from "node:fs";
2
1
  import path from "node:path";
3
2
  const DEPENDENCY_KINDS = [
4
3
  "dependencies",
@@ -11,13 +10,12 @@ export function getPackageJsonPath(cwd) {
11
10
  }
12
11
  export async function readManifest(cwd) {
13
12
  const filePath = getPackageJsonPath(cwd);
14
- const content = await fs.readFile(filePath, "utf8");
15
- return JSON.parse(content);
13
+ return (await Bun.file(filePath).json());
16
14
  }
17
15
  export async function writeManifest(cwd, manifest) {
18
16
  const filePath = getPackageJsonPath(cwd);
19
17
  const content = JSON.stringify(manifest, null, 2) + "\n";
20
- await fs.writeFile(filePath, content, "utf8");
18
+ await Bun.write(filePath, content);
21
19
  }
22
20
  export function collectDependencies(manifest, includeKinds) {
23
21
  const deps = [];
@@ -1 +1,3 @@
1
- export declare function detectPackageManager(cwd: string): Promise<"npm" | "pnpm" | "unknown">;
1
+ import type { DetectedPackageManager, SelectedPackageManager, SupportedPackageManager } from "../types/index.js";
2
+ export declare function detectPackageManager(cwd: string): Promise<DetectedPackageManager>;
3
+ export declare function resolvePackageManager(requested: SelectedPackageManager, detected: DetectedPackageManager, fallback?: SupportedPackageManager): SupportedPackageManager;
package/dist/pm/detect.js CHANGED
@@ -1,20 +1,32 @@
1
- import { access } from "node:fs/promises";
2
1
  import path from "node:path";
2
+ const PACKAGE_MANAGER_LOCKFILES = [
3
+ ["bun.lock", "bun"],
4
+ ["bun.lockb", "bun"],
5
+ ["pnpm-lock.yaml", "pnpm"],
6
+ ["package-lock.json", "npm"],
7
+ ["npm-shrinkwrap.json", "npm"],
8
+ ["yarn.lock", "yarn"],
9
+ ];
3
10
  export async function detectPackageManager(cwd) {
4
- const pnpmLock = path.join(cwd, "pnpm-lock.yaml");
5
- const npmLock = path.join(cwd, "package-lock.json");
6
- try {
7
- await access(pnpmLock);
8
- return "pnpm";
9
- }
10
- catch {
11
- // noop
11
+ for (const [lockfile, packageManager] of PACKAGE_MANAGER_LOCKFILES) {
12
+ if (await fileExists(path.join(cwd, lockfile))) {
13
+ return packageManager;
14
+ }
12
15
  }
16
+ return "unknown";
17
+ }
18
+ export function resolvePackageManager(requested, detected, fallback = "npm") {
19
+ if (requested !== "auto")
20
+ return requested;
21
+ if (detected !== "unknown")
22
+ return detected;
23
+ return fallback;
24
+ }
25
+ async function fileExists(filePath) {
13
26
  try {
14
- await access(npmLock);
15
- return "npm";
27
+ return await Bun.file(filePath).exists();
16
28
  }
17
29
  catch {
18
- return "unknown";
30
+ return false;
19
31
  }
20
32
  }
@@ -1 +1,2 @@
1
- export declare function installDependencies(cwd: string, packageManager: "auto" | "npm" | "pnpm", detected: "npm" | "pnpm" | "unknown"): Promise<void>;
1
+ import type { DetectedPackageManager, SelectedPackageManager } from "../types/index.js";
2
+ export declare function installDependencies(cwd: string, packageManager: SelectedPackageManager, detected: DetectedPackageManager): Promise<void>;
@@ -1,21 +1,20 @@
1
- import { spawn } from "node:child_process";
1
+ import { resolvePackageManager } from "./detect.js";
2
2
  export async function installDependencies(cwd, packageManager, detected) {
3
- const selected = packageManager === "auto" ? (detected === "unknown" ? "npm" : detected) : packageManager;
4
- const command = selected;
3
+ const command = resolvePackageManager(packageManager, detected);
5
4
  const args = ["install"];
6
- await new Promise((resolve, reject) => {
7
- const child = spawn(command, args, {
5
+ try {
6
+ const proc = Bun.spawn([command, ...args], {
8
7
  cwd,
9
- stdio: "inherit",
10
- shell: process.platform === "win32",
8
+ stdin: "inherit",
9
+ stdout: "inherit",
10
+ stderr: "inherit",
11
11
  });
12
- child.on("exit", (code) => {
13
- if (code === 0) {
14
- resolve();
15
- return;
16
- }
17
- reject(new Error(`${command} ${args.join(" ")} failed with exit code ${code ?? "unknown"}`));
18
- });
19
- child.on("error", reject);
20
- });
12
+ const code = await proc.exited;
13
+ if (code !== 0) {
14
+ throw new Error(`${command} ${args.join(" ")} failed with exit code ${code}`);
15
+ }
16
+ }
17
+ catch (err) {
18
+ throw err instanceof Error ? err : new Error(String(err));
19
+ }
21
20
  }
@@ -1,8 +1,7 @@
1
- import { promises as fs } from "node:fs";
2
- import os from "node:os";
3
1
  import path from "node:path";
4
- import process from "node:process";
5
2
  import { asyncPool } from "../utils/async-pool.js";
3
+ import { getHomeDir } from "../utils/runtime-paths.js";
4
+ import { getRuntimeCwd, readEnv } from "../utils/runtime.js";
6
5
  const DEFAULT_TIMEOUT_MS = 8000;
7
6
  const USER_AGENT = "@rainy-updates/cli";
8
7
  const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
@@ -52,7 +51,9 @@ export class NpmRegistryClient {
52
51
  catch (error) {
53
52
  lastError = String(error);
54
53
  if (attempt < retries) {
55
- const backoffMs = error instanceof RetryableRegistryError ? error.waitMs : computeBackoffMs(attempt);
54
+ const backoffMs = error instanceof RetryableRegistryError
55
+ ? error.waitMs
56
+ : computeBackoffMs(attempt);
56
57
  await sleep(backoffMs);
57
58
  }
58
59
  }
@@ -136,10 +137,7 @@ class RetryableRegistryError extends Error {
136
137
  }
137
138
  }
138
139
  async function createRequester(cwd) {
139
- const registryConfig = await loadRegistryConfig(cwd ?? process.cwd());
140
- const undiciRequester = await tryCreateUndiciRequester(registryConfig);
141
- if (undiciRequester)
142
- return undiciRequester;
140
+ const registryConfig = await loadRegistryConfig(cwd ?? getRuntimeCwd());
143
141
  return async (packageName, timeoutMs) => {
144
142
  const controller = new AbortController();
145
143
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
@@ -158,7 +156,9 @@ async function createRequester(cwd) {
158
156
  headers,
159
157
  signal: controller.signal,
160
158
  });
161
- const data = (await response.json().catch(() => null));
159
+ const data = (await response
160
+ .json()
161
+ .catch(() => null));
162
162
  return {
163
163
  status: response.status,
164
164
  data,
@@ -170,74 +170,17 @@ async function createRequester(cwd) {
170
170
  }
171
171
  };
172
172
  }
173
- async function tryCreateUndiciRequester(registryConfig) {
174
- try {
175
- const dynamicImport = Function("specifier", "return import(specifier)");
176
- const undici = await dynamicImport("undici");
177
- return async (packageName, timeoutMs) => {
178
- const controller = new AbortController();
179
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
180
- const registry = resolveRegistryForPackage(packageName, registryConfig);
181
- const url = buildRegistryUrl(registry, packageName);
182
- const authHeader = resolveAuthHeader(registry, registryConfig);
183
- const headers = {
184
- accept: "application/json",
185
- "user-agent": USER_AGENT,
186
- };
187
- if (authHeader) {
188
- headers.authorization = authHeader;
189
- }
190
- try {
191
- const res = await undici.request(url, {
192
- method: "GET",
193
- headers,
194
- signal: controller.signal,
195
- });
196
- const bodyText = await res.body.text();
197
- let data = null;
198
- try {
199
- data = JSON.parse(bodyText);
200
- }
201
- catch {
202
- data = null;
203
- }
204
- const retryAfter = (() => {
205
- const header = res.headers["retry-after"];
206
- if (Array.isArray(header))
207
- return header[0] ?? null;
208
- if (typeof header === "string")
209
- return header;
210
- return null;
211
- })();
212
- return {
213
- status: res.statusCode,
214
- data,
215
- retryAfterMs: parseRetryAfterHeader(retryAfter),
216
- };
217
- }
218
- finally {
219
- clearTimeout(timeout);
220
- }
221
- };
222
- }
223
- catch {
224
- return null;
225
- }
226
- }
227
173
  export async function loadRegistryConfig(cwd) {
228
- const homeNpmrc = path.join(os.homedir(), ".npmrc");
174
+ const homeNpmrc = path.join(getHomeDir(), ".npmrc");
229
175
  const projectNpmrc = path.join(cwd, ".npmrc");
230
176
  const merged = new Map();
231
177
  for (const filePath of [homeNpmrc, projectNpmrc]) {
232
- try {
233
- const content = await fs.readFile(filePath, "utf8");
234
- const parsed = parseNpmrc(content);
235
- for (const [key, value] of parsed) {
236
- merged.set(key, value);
237
- }
238
- }
239
- catch {
240
- // ignore missing/unreadable file
178
+ const content = await readOptionalTextFile(filePath);
179
+ if (!content)
180
+ continue;
181
+ const parsed = parseNpmrc(content);
182
+ for (const [key, value] of parsed) {
183
+ merged.set(key, value);
241
184
  }
242
185
  }
243
186
  const defaultRegistry = normalizeRegistryUrl(merged.get("registry") ?? DEFAULT_REGISTRY);
@@ -272,18 +215,33 @@ export async function loadRegistryConfig(cwd) {
272
215
  authByRegistry.set(registry, current);
273
216
  }
274
217
  if (merged.get("always-auth") === "true") {
275
- const current = authByRegistry.get(defaultRegistry) ?? { alwaysAuth: false };
218
+ const current = authByRegistry.get(defaultRegistry) ?? {
219
+ alwaysAuth: false,
220
+ };
276
221
  current.alwaysAuth = true;
277
222
  authByRegistry.set(defaultRegistry, current);
278
223
  }
279
224
  return { defaultRegistry, scopedRegistries, authByRegistry };
280
225
  }
226
+ async function readOptionalTextFile(filePath) {
227
+ try {
228
+ const file = Bun.file(filePath);
229
+ if (!(await file.exists()))
230
+ return null;
231
+ return await file.text();
232
+ }
233
+ catch {
234
+ return null;
235
+ }
236
+ }
281
237
  function parseNpmrc(content) {
282
238
  const values = new Map();
283
239
  const lines = content.split(/\r?\n/);
284
240
  for (const line of lines) {
285
241
  const trimmed = line.trim();
286
- if (trimmed.length === 0 || trimmed.startsWith("#") || trimmed.startsWith(";"))
242
+ if (trimmed.length === 0 ||
243
+ trimmed.startsWith("#") ||
244
+ trimmed.startsWith(";"))
287
245
  continue;
288
246
  const separator = trimmed.indexOf("=");
289
247
  if (separator <= 0)
@@ -297,7 +255,7 @@ function parseNpmrc(content) {
297
255
  return values;
298
256
  }
299
257
  function substituteEnvValue(value) {
300
- return value.replace(/\$\{([^}]+)\}/g, (_match, name) => process.env[name] ?? "");
258
+ return value.replace(/\$\{([^}]+)\}/g, (_match, name) => readEnv(name) ?? "");
301
259
  }
302
260
  function normalizeRegistryUrl(value) {
303
261
  const normalized = value.endsWith("/") ? value : `${value}/`;
package/dist/rup ADDED
Binary file
@@ -1,10 +1,19 @@
1
1
  export type DependencyKind = "dependencies" | "devDependencies" | "optionalDependencies" | "peerDependencies";
2
+ export type SupportedPackageManager = "bun" | "npm" | "pnpm" | "yarn";
3
+ export type DetectedPackageManager = SupportedPackageManager | "unknown";
4
+ export type SelectedPackageManager = "auto" | SupportedPackageManager;
2
5
  export type TargetLevel = "patch" | "minor" | "major" | "latest";
3
6
  export type GroupBy = "none" | "name" | "scope" | "kind" | "risk";
4
7
  export type CiProfile = "minimal" | "strict" | "enterprise";
5
8
  export type LockfileMode = "preserve" | "update" | "error";
6
9
  export type Verdict = "safe" | "review" | "blocked" | "actionable";
7
10
  export type RiskLevel = "critical" | "high" | "medium" | "low";
11
+ export type DashboardMode = "check" | "review" | "upgrade";
12
+ export type QueueFocus = "all" | "security" | "risk" | "major" | "blocked" | "workspace";
13
+ export type InteractiveSurface = "dashboard";
14
+ export type VerificationState = "not-run" | "passed" | "failed";
15
+ export type VerificationMode = "none" | "install" | "test" | "install,test";
16
+ export type CiGate = "check" | "doctor" | "review" | "upgrade";
8
17
  export type DoctorFindingSeverity = "error" | "warning";
9
18
  export type DoctorScoreLabel = "Strong" | "Needs Review" | "Action Needed" | "Blocked / Critical";
10
19
  export type DoctorFindingCategory = "Security" | "Compatibility" | "Policy" | "Operational Health" | "Licensing" | "Unused / Cleanup" | "Release Risk" | "Registry / Execution" | "Workspace Integrity";
@@ -56,13 +65,19 @@ export interface RunOptions {
56
65
  interactive: boolean;
57
66
  showImpact: boolean;
58
67
  showHomepage: boolean;
68
+ decisionPlanFile?: string;
69
+ verify: VerificationMode;
70
+ testCommand?: string;
71
+ verificationReportFile?: string;
72
+ ciGate: CiGate;
59
73
  }
60
74
  export interface CheckOptions extends RunOptions {
61
75
  }
62
76
  export interface UpgradeOptions extends RunOptions {
63
77
  install: boolean;
64
- packageManager: "auto" | "npm" | "pnpm";
78
+ packageManager: SelectedPackageManager;
65
79
  sync: boolean;
80
+ fromPlanFile?: string;
66
81
  }
67
82
  export interface BaselineOptions {
68
83
  cwd: string;
@@ -146,8 +161,51 @@ export interface ArtifactManifest {
146
161
  githubOutputFile?: string;
147
162
  sarifFile?: string;
148
163
  prReportFile?: string;
164
+ verificationReportFile?: string;
149
165
  };
150
166
  }
167
+ export interface VerificationCheck {
168
+ name: "install" | "test";
169
+ command: string;
170
+ passed: boolean;
171
+ exitCode: number;
172
+ durationMs: number;
173
+ error?: string;
174
+ }
175
+ export interface VerificationResult {
176
+ mode: VerificationMode;
177
+ passed: boolean;
178
+ checks: VerificationCheck[];
179
+ }
180
+ export interface DecisionPlanItem {
181
+ packagePath: string;
182
+ name: string;
183
+ kind: DependencyKind;
184
+ fromRange: string;
185
+ toRange: string;
186
+ toVersionResolved: string;
187
+ diffType: TargetLevel;
188
+ riskLevel?: RiskLevel;
189
+ riskScore?: number;
190
+ policyAction?: PolicyAction;
191
+ decisionState?: DecisionState;
192
+ selected: boolean;
193
+ }
194
+ export interface DecisionPlan {
195
+ contractVersion: "1";
196
+ createdAt: string;
197
+ sourceCommand: string;
198
+ mode: DashboardMode;
199
+ focus: QueueFocus;
200
+ projectPath: string;
201
+ target: TargetLevel;
202
+ interactiveSurface: InteractiveSurface;
203
+ summary: {
204
+ totalItems: number;
205
+ selectedItems: number;
206
+ };
207
+ items: DecisionPlanItem[];
208
+ }
151
209
  export interface Summary {
152
210
  contractVersion: "2";
153
211
  scannedPackages: number;
@@ -213,11 +271,17 @@ export interface Summary {
213
271
  primaryFindingCode?: string;
214
272
  primaryFindingCategory?: DoctorFindingCategory;
215
273
  nextActionReason?: string;
274
+ suggestedCommand?: string;
275
+ decisionPlan?: string;
276
+ interactiveSurface?: InteractiveSurface;
277
+ queueFocus?: QueueFocus;
278
+ verificationState?: VerificationState;
279
+ verificationFailures?: number;
216
280
  }
217
281
  export interface CheckResult {
218
282
  projectPath: string;
219
283
  packagePaths: string[];
220
- packageManager: "npm" | "pnpm" | "unknown";
284
+ packageManager: DetectedPackageManager;
221
285
  target: TargetLevel;
222
286
  timestamp: string;
223
287
  summary: Summary;
@@ -260,7 +324,7 @@ export interface AuditOptions {
260
324
  fix: boolean;
261
325
  dryRun: boolean;
262
326
  commit: boolean;
263
- packageManager: "auto" | "npm" | "pnpm" | "bun" | "yarn";
327
+ packageManager: SelectedPackageManager;
264
328
  reportFormat: AuditReportFormat;
265
329
  sourceMode: AuditSourceMode;
266
330
  jsonFile?: string;
@@ -432,6 +496,7 @@ export interface ReviewResult {
432
496
  updates: PackageUpdate[];
433
497
  errors: string[];
434
498
  warnings: string[];
499
+ decisionPlan?: DecisionPlan;
435
500
  }
436
501
  export interface ReviewOptions extends CheckOptions {
437
502
  securityOnly: boolean;
@@ -439,6 +504,7 @@ export interface ReviewOptions extends CheckOptions {
439
504
  diff?: TargetLevel;
440
505
  applySelected: boolean;
441
506
  showChangelog?: boolean;
507
+ queueFocus?: QueueFocus;
442
508
  }
443
509
  export interface DoctorOptions extends CheckOptions {
444
510
  verdictOnly: boolean;
@@ -482,11 +548,16 @@ export interface AnalysisBundle {
482
548
  }
483
549
  export interface DashboardOptions extends CheckOptions {
484
550
  view?: "dependencies" | "security" | "health";
551
+ mode: DashboardMode;
552
+ focus: QueueFocus;
553
+ applySelected: boolean;
485
554
  }
486
555
  export interface DashboardResult {
487
556
  completed: boolean;
488
557
  errors: string[];
489
558
  warnings: string[];
559
+ selectedUpdates: number;
560
+ decisionPlanFile?: string;
490
561
  }
491
562
  export type UnusedKind = "declared-not-imported" | "imported-not-declared";
492
563
  export interface UnusedDependency {
@@ -601,14 +672,14 @@ export interface GaOptions {
601
672
  jsonFile?: string;
602
673
  }
603
674
  export interface GaCheck {
604
- name: "package-manager" | "workspace-discovery" | "lockfile" | "cache-backend" | "dist-build" | "benchmark-gates" | "docs-contract";
675
+ name: "package-manager" | "workspace-discovery" | "lockfile" | "cache-backend" | "dist-build" | "runtime-artifacts" | "benchmark-gates" | "docs-contract";
605
676
  status: "pass" | "warn" | "fail";
606
677
  detail: string;
607
678
  }
608
679
  export interface GaResult {
609
680
  ready: boolean;
610
681
  projectPath: string;
611
- packageManager: "npm" | "pnpm" | "unknown";
682
+ packageManager: DetectedPackageManager;
612
683
  workspacePackages: number;
613
684
  cacheBackend: "sqlite" | "file";
614
685
  checks: GaCheck[];
package/dist/ui/tui.d.ts CHANGED
@@ -1,2 +1,5 @@
1
1
  import type { ReviewItem } from "../types/index.js";
2
- export declare function runTui(items: ReviewItem[]): Promise<ReviewItem[]>;
2
+ export declare function runTui(items: ReviewItem[], options?: {
3
+ title?: string;
4
+ subtitle?: string;
5
+ }): Promise<ReviewItem[]>;
package/dist/ui/tui.js CHANGED
@@ -22,7 +22,7 @@ const DETAIL_TABS = [
22
22
  "health",
23
23
  "changelog",
24
24
  ];
25
- function TuiApp({ items, onComplete }) {
25
+ function TuiApp({ items, title, subtitle, onComplete }) {
26
26
  const [cursorIndex, setCursorIndex] = useState(0);
27
27
  const [filterIndex, setFilterIndex] = useState(0);
28
28
  const [sortIndex, setSortIndex] = useState(0);
@@ -137,7 +137,8 @@ function TuiApp({ items, onComplete }) {
137
137
  onComplete(items.filter((_, index) => selectedIndices.has(index)));
138
138
  }
139
139
  });
140
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Rainy Review Queue" }), _jsx(Text, { color: "gray", children: "Detect with check, summarize with doctor, decide here in review, then apply with upgrade." }), _jsx(Text, { color: "gray", children: "Filters: \u2190/\u2192 Sort: o Group: g Tabs: Tab Search: / Help: ? Space: toggle Enter: confirm" }), _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsxs(Box, { width: 24, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Filter Rail" }), FILTER_ORDER.map((filter, index) => (_jsxs(Text, { color: index === filterIndex ? "cyan" : "gray", children: [index === filterIndex ? ">" : " ", " ", filter] }, filter))), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Search" }), _jsx(Text, { color: searchMode ? "cyan" : "gray", children: searchMode ? `/${search}` : search ? `/${search}` : "inactive" })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Modes" }), _jsxs(Text, { color: "gray", children: ["sort: ", activeSort] }), _jsxs(Text, { color: "gray", children: ["group: ", activeGroup] }), _jsxs(Text, { color: "gray", children: ["tab: ", activeTab] })] })] }), _jsxs(Box, { marginLeft: 1, width: 82, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Review Queue" }), itemRows.length === 0 ? (_jsx(Text, { color: "gray", children: "No review candidates match this view." })) : (visibleRows.map((row, visibleIndex) => {
140
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: title ?? "Rainy Dashboard" }), _jsx(Text, { color: "gray", children: subtitle ??
141
+ "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) => {
141
142
  if (row.kind === "group") {
142
143
  return (_jsx(Text, { bold: true, color: "gray", children: row.label }, `group:${row.label}`));
143
144
  }
@@ -353,9 +354,9 @@ function decisionColor(label) {
353
354
  return "green";
354
355
  }
355
356
  }
356
- export async function runTui(items) {
357
+ export async function runTui(items, options) {
357
358
  return new Promise((resolve) => {
358
- const { unmount } = render(_jsx(TuiApp, { items: items, onComplete: (selected) => {
359
+ const { unmount } = render(_jsx(TuiApp, { items: items, title: options?.title, subtitle: options?.subtitle, onComplete: (selected) => {
359
360
  unmount();
360
361
  resolve(selected);
361
362
  } }));
package/dist/utils/io.js CHANGED
@@ -1,10 +1,9 @@
1
- import { promises as fs } from "node:fs";
1
+ import { mkdir, rename } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import crypto from "node:crypto";
4
3
  export async function writeFileAtomic(filePath, content) {
5
4
  const dir = path.dirname(filePath);
6
- await fs.mkdir(dir, { recursive: true });
7
- const tempPath = path.join(dir, `.tmp-${path.basename(filePath)}-${crypto.randomUUID()}`);
8
- await fs.writeFile(tempPath, content, "utf8");
9
- await fs.rename(tempPath, filePath);
5
+ const tempFile = path.join(dir, `.tmp-${path.basename(filePath)}-${crypto.randomUUID()}`);
6
+ await mkdir(dir, { recursive: true });
7
+ await Bun.write(tempFile, content);
8
+ await rename(tempFile, filePath);
10
9
  }