@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.
Files changed (102) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +90 -31
  3. package/dist/bin/cli.js +24 -482
  4. package/dist/bin/dispatch.d.ts +16 -0
  5. package/dist/bin/dispatch.js +147 -0
  6. package/dist/bin/help.d.ts +1 -0
  7. package/dist/bin/help.js +314 -0
  8. package/dist/cache/cache.js +13 -11
  9. package/dist/commands/audit/parser.js +2 -2
  10. package/dist/commands/audit/runner.js +27 -46
  11. package/dist/commands/audit/targets.js +13 -13
  12. package/dist/commands/bisect/oracle.js +28 -11
  13. package/dist/commands/bisect/parser.js +3 -3
  14. package/dist/commands/bisect/runner.js +15 -8
  15. package/dist/commands/changelog/fetcher.js +11 -5
  16. package/dist/commands/dashboard/parser.js +103 -1
  17. package/dist/commands/dashboard/runner.d.ts +2 -2
  18. package/dist/commands/dashboard/runner.js +67 -37
  19. package/dist/commands/doctor/parser.js +15 -4
  20. package/dist/commands/doctor/runner.js +6 -3
  21. package/dist/commands/ga/parser.js +4 -4
  22. package/dist/commands/ga/runner.js +13 -7
  23. package/dist/commands/health/parser.js +2 -2
  24. package/dist/commands/licenses/runner.js +4 -4
  25. package/dist/commands/resolve/runner.js +9 -4
  26. package/dist/commands/review/parser.js +57 -4
  27. package/dist/commands/review/runner.js +31 -5
  28. package/dist/commands/snapshot/runner.js +17 -17
  29. package/dist/commands/snapshot/store.d.ts +0 -12
  30. package/dist/commands/snapshot/store.js +26 -38
  31. package/dist/commands/unused/runner.js +6 -7
  32. package/dist/commands/unused/scanner.js +17 -20
  33. package/dist/config/loader.d.ts +2 -2
  34. package/dist/config/loader.js +2 -5
  35. package/dist/config/policy.js +20 -11
  36. package/dist/core/analysis/options.d.ts +6 -0
  37. package/dist/core/analysis/options.js +69 -0
  38. package/dist/core/analysis/review-items.d.ts +4 -0
  39. package/dist/core/analysis/review-items.js +128 -0
  40. package/dist/core/analysis/run-silenced.d.ts +1 -0
  41. package/dist/core/analysis/run-silenced.js +13 -0
  42. package/dist/core/analysis-bundle.js +3 -211
  43. package/dist/core/artifacts.js +6 -5
  44. package/dist/core/baseline.js +3 -5
  45. package/dist/core/check.js +2 -2
  46. package/dist/core/ci.js +52 -1
  47. package/dist/core/decision-plan.d.ts +14 -0
  48. package/dist/core/decision-plan.js +107 -0
  49. package/dist/core/doctor/findings.d.ts +2 -0
  50. package/dist/core/doctor/findings.js +166 -0
  51. package/dist/core/doctor/render.d.ts +3 -0
  52. package/dist/core/doctor/render.js +44 -0
  53. package/dist/core/doctor/result.d.ts +2 -0
  54. package/dist/core/doctor/result.js +58 -0
  55. package/dist/core/doctor/score.d.ts +5 -0
  56. package/dist/core/doctor/score.js +28 -0
  57. package/dist/core/fix-pr-batch.js +38 -28
  58. package/dist/core/fix-pr.js +27 -24
  59. package/dist/core/init-ci.js +25 -21
  60. package/dist/core/options.js +95 -4
  61. package/dist/core/review-model.d.ts +3 -3
  62. package/dist/core/review-model.js +6 -67
  63. package/dist/core/review-verdict.d.ts +2 -0
  64. package/dist/core/review-verdict.js +14 -0
  65. package/dist/core/summary.js +12 -0
  66. package/dist/core/upgrade.js +64 -2
  67. package/dist/core/verification.d.ts +2 -0
  68. package/dist/core/verification.js +106 -0
  69. package/dist/core/warm-cache.js +2 -2
  70. package/dist/output/format.js +22 -0
  71. package/dist/output/github.js +10 -0
  72. package/dist/output/sarif.js +16 -12
  73. package/dist/parsers/package-json.js +2 -4
  74. package/dist/pm/detect.d.ts +3 -1
  75. package/dist/pm/detect.js +24 -12
  76. package/dist/pm/install.d.ts +2 -1
  77. package/dist/pm/install.js +15 -16
  78. package/dist/registry/npm.js +34 -76
  79. package/dist/rup +0 -0
  80. package/dist/types/index.d.ts +104 -5
  81. package/dist/ui/tui.d.ts +4 -1
  82. package/dist/ui/tui.js +5 -4
  83. package/dist/utils/io.js +5 -6
  84. package/dist/utils/lockfile.js +24 -19
  85. package/dist/utils/runtime-paths.d.ts +4 -0
  86. package/dist/utils/runtime-paths.js +35 -0
  87. package/dist/utils/runtime.d.ts +7 -0
  88. package/dist/utils/runtime.js +32 -0
  89. package/dist/workspace/discover.js +55 -51
  90. package/package.json +16 -16
  91. package/dist/ui/dashboard/DashboardTUI.d.ts +0 -6
  92. package/dist/ui/dashboard/DashboardTUI.js +0 -34
  93. package/dist/ui/dashboard/components/DetailPanel.d.ts +0 -4
  94. package/dist/ui/dashboard/components/DetailPanel.js +0 -30
  95. package/dist/ui/dashboard/components/Footer.d.ts +0 -4
  96. package/dist/ui/dashboard/components/Footer.js +0 -9
  97. package/dist/ui/dashboard/components/Header.d.ts +0 -4
  98. package/dist/ui/dashboard/components/Header.js +0 -12
  99. package/dist/ui/dashboard/components/Sidebar.d.ts +0 -4
  100. package/dist/ui/dashboard/components/Sidebar.js +0 -23
  101. package/dist/ui/dashboard/store.d.ts +0 -34
  102. package/dist/ui/dashboard/store.js +0 -148
@@ -34,6 +34,16 @@ export async function writeGitHubOutput(filePath, result) {
34
34
  `cache_backend=${result.summary.cacheBackend ?? ""}`,
35
35
  `degraded_sources=${(result.summary.degradedSources ?? []).join(",")}`,
36
36
  `ga_ready=${result.summary.gaReady === true ? "1" : "0"}`,
37
+ `dependency_health_score=${result.summary.dependencyHealthScore ?? ""}`,
38
+ `primary_finding_code=${result.summary.primaryFindingCode ?? ""}`,
39
+ `primary_finding_category=${result.summary.primaryFindingCategory ?? ""}`,
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}`,
37
47
  `fix_pr_applied=${result.summary.fixPrApplied === true ? "1" : "0"}`,
38
48
  `fix_pr_branches_created=${result.summary.fixPrBranchesCreated}`,
39
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
  },
@@ -101,6 +105,10 @@ export function createSarifReport(result) {
101
105
  monitorPackages: result.summary.monitorPackages ?? 0,
102
106
  degradedSources: result.summary.degradedSources ?? [],
103
107
  cacheBackend: result.summary.cacheBackend,
108
+ dependencyHealthScore: result.summary.dependencyHealthScore,
109
+ primaryFindingCode: result.summary.primaryFindingCode,
110
+ primaryFindingCategory: result.summary.primaryFindingCategory,
111
+ nextActionReason: result.summary.nextActionReason,
104
112
  },
105
113
  },
106
114
  ],
@@ -111,15 +119,11 @@ function getToolVersion() {
111
119
  if (TOOL_VERSION_CACHE)
112
120
  return TOOL_VERSION_CACHE;
113
121
  try {
114
- const currentFile = fileURLToPath(import.meta.url);
115
- const packageJsonPath = path.resolve(path.dirname(currentFile), "../../package.json");
116
- const content = readFileSync(packageJsonPath, "utf8");
117
- const parsed = JSON.parse(content);
118
- TOOL_VERSION_CACHE = parsed.version ?? "0.0.0";
119
- 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";
120
124
  }
121
125
  catch {
122
126
  TOOL_VERSION_CACHE = "0.0.0";
123
- return TOOL_VERSION_CACHE;
124
127
  }
128
+ return TOOL_VERSION_CACHE;
125
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,22 @@
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";
17
+ export type DoctorFindingSeverity = "error" | "warning";
18
+ export type DoctorScoreLabel = "Strong" | "Needs Review" | "Action Needed" | "Blocked / Critical";
19
+ export type DoctorFindingCategory = "Security" | "Compatibility" | "Policy" | "Operational Health" | "Licensing" | "Unused / Cleanup" | "Release Risk" | "Registry / Execution" | "Workspace Integrity";
8
20
  export type RiskCategory = "known-vulnerability" | "behavioral-risk" | "operational-health";
9
21
  export type MaintainerChurnStatus = "unknown" | "stable" | "elevated-change";
10
22
  export type PolicyAction = "allow" | "review" | "block" | "monitor";
@@ -53,13 +65,19 @@ export interface RunOptions {
53
65
  interactive: boolean;
54
66
  showImpact: boolean;
55
67
  showHomepage: boolean;
68
+ decisionPlanFile?: string;
69
+ verify: VerificationMode;
70
+ testCommand?: string;
71
+ verificationReportFile?: string;
72
+ ciGate: CiGate;
56
73
  }
57
74
  export interface CheckOptions extends RunOptions {
58
75
  }
59
76
  export interface UpgradeOptions extends RunOptions {
60
77
  install: boolean;
61
- packageManager: "auto" | "npm" | "pnpm";
78
+ packageManager: SelectedPackageManager;
62
79
  sync: boolean;
80
+ fromPlanFile?: string;
63
81
  }
64
82
  export interface BaselineOptions {
65
83
  cwd: string;
@@ -143,8 +161,51 @@ export interface ArtifactManifest {
143
161
  githubOutputFile?: string;
144
162
  sarifFile?: string;
145
163
  prReportFile?: string;
164
+ verificationReportFile?: string;
146
165
  };
147
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
+ }
148
209
  export interface Summary {
149
210
  contractVersion: "2";
150
211
  scannedPackages: number;
@@ -204,11 +265,23 @@ export interface Summary {
204
265
  cacheBackend?: "sqlite" | "file";
205
266
  binaryRecommended?: boolean;
206
267
  gaReady?: boolean;
268
+ dependencyHealthScore?: number;
269
+ findingCountsByCategory?: Partial<Record<DoctorFindingCategory, number>>;
270
+ findingCountsBySeverity?: Partial<Record<DoctorFindingSeverity, number>>;
271
+ primaryFindingCode?: string;
272
+ primaryFindingCategory?: DoctorFindingCategory;
273
+ nextActionReason?: string;
274
+ suggestedCommand?: string;
275
+ decisionPlan?: string;
276
+ interactiveSurface?: InteractiveSurface;
277
+ queueFocus?: QueueFocus;
278
+ verificationState?: VerificationState;
279
+ verificationFailures?: number;
207
280
  }
208
281
  export interface CheckResult {
209
282
  projectPath: string;
210
283
  packagePaths: string[];
211
- packageManager: "npm" | "pnpm" | "unknown";
284
+ packageManager: DetectedPackageManager;
212
285
  target: TargetLevel;
213
286
  timestamp: string;
214
287
  summary: Summary;
@@ -251,7 +324,7 @@ export interface AuditOptions {
251
324
  fix: boolean;
252
325
  dryRun: boolean;
253
326
  commit: boolean;
254
- packageManager: "auto" | "npm" | "pnpm" | "bun" | "yarn";
327
+ packageManager: SelectedPackageManager;
255
328
  reportFormat: AuditReportFormat;
256
329
  sourceMode: AuditSourceMode;
257
330
  jsonFile?: string;
@@ -423,6 +496,7 @@ export interface ReviewResult {
423
496
  updates: PackageUpdate[];
424
497
  errors: string[];
425
498
  warnings: string[];
499
+ decisionPlan?: DecisionPlan;
426
500
  }
427
501
  export interface ReviewOptions extends CheckOptions {
428
502
  securityOnly: boolean;
@@ -430,17 +504,37 @@ export interface ReviewOptions extends CheckOptions {
430
504
  diff?: TargetLevel;
431
505
  applySelected: boolean;
432
506
  showChangelog?: boolean;
507
+ queueFocus?: QueueFocus;
433
508
  }
434
509
  export interface DoctorOptions extends CheckOptions {
435
510
  verdictOnly: boolean;
436
511
  includeChangelog?: boolean;
512
+ agentReport?: boolean;
513
+ }
514
+ export interface DoctorFinding {
515
+ id: string;
516
+ code: string;
517
+ category: DoctorFindingCategory;
518
+ severity: DoctorFindingSeverity;
519
+ scope: "project" | "package";
520
+ packageName?: string;
521
+ workspace?: string;
522
+ summary: string;
523
+ details?: string;
524
+ help?: string;
525
+ recommendedAction?: string;
526
+ evidence?: string[];
437
527
  }
438
528
  export interface DoctorResult {
439
529
  verdict: Verdict;
530
+ score: number;
531
+ scoreLabel: DoctorScoreLabel;
440
532
  summary: Summary;
441
533
  review: ReviewResult;
534
+ findings: DoctorFinding[];
442
535
  primaryFindings: string[];
443
536
  recommendedCommand: string;
537
+ nextActionReason: string;
444
538
  }
445
539
  export interface AnalysisBundle {
446
540
  check: CheckResult;
@@ -454,11 +548,16 @@ export interface AnalysisBundle {
454
548
  }
455
549
  export interface DashboardOptions extends CheckOptions {
456
550
  view?: "dependencies" | "security" | "health";
551
+ mode: DashboardMode;
552
+ focus: QueueFocus;
553
+ applySelected: boolean;
457
554
  }
458
555
  export interface DashboardResult {
459
556
  completed: boolean;
460
557
  errors: string[];
461
558
  warnings: string[];
559
+ selectedUpdates: number;
560
+ decisionPlanFile?: string;
462
561
  }
463
562
  export type UnusedKind = "declared-not-imported" | "imported-not-declared";
464
563
  export interface UnusedDependency {
@@ -573,14 +672,14 @@ export interface GaOptions {
573
672
  jsonFile?: string;
574
673
  }
575
674
  export interface GaCheck {
576
- 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";
577
676
  status: "pass" | "warn" | "fail";
578
677
  detail: string;
579
678
  }
580
679
  export interface GaResult {
581
680
  ready: boolean;
582
681
  projectPath: string;
583
- packageManager: "npm" | "pnpm" | "unknown";
682
+ packageManager: DetectedPackageManager;
584
683
  workspacePackages: number;
585
684
  cacheBackend: "sqlite" | "file";
586
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
  }