@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,7 +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";
2
+ import { writeFileAtomic } from "../utils/io.js";
3
+ import { getCacheDir } from "../utils/runtime-paths.js";
4
+ import { readEnv } from "../utils/runtime.js";
5
5
  class FileCacheStore {
6
6
  filePath;
7
7
  constructor(filePath) {
@@ -15,19 +15,19 @@ class FileCacheStore {
15
15
  return null;
16
16
  return {
17
17
  ...entry,
18
- availableVersions: Array.isArray(entry.availableVersions) ? entry.availableVersions : [entry.latestVersion],
18
+ availableVersions: Array.isArray(entry.availableVersions)
19
+ ? entry.availableVersions
20
+ : [entry.latestVersion],
19
21
  };
20
22
  }
21
23
  async set(entry) {
22
24
  const entries = await this.readEntries();
23
25
  entries[this.getKey(entry.packageName, entry.target)] = entry;
24
- await fs.mkdir(path.dirname(this.filePath), { recursive: true });
25
- await fs.writeFile(this.filePath, JSON.stringify(entries), "utf8");
26
+ await writeFileAtomic(this.filePath, JSON.stringify(entries));
26
27
  }
27
28
  async readEntries() {
28
29
  try {
29
- const content = await fs.readFile(this.filePath, "utf8");
30
- return JSON.parse(content);
30
+ return (await Bun.file(this.filePath).json());
31
31
  }
32
32
  catch {
33
33
  return {};
@@ -93,7 +93,9 @@ class SqliteCacheStore {
93
93
  }
94
94
  ensureSchema() {
95
95
  try {
96
- const columns = this.db.prepare("PRAGMA table_info(versions);").all();
96
+ const columns = this.db
97
+ .prepare("PRAGMA table_info(versions);")
98
+ .all();
97
99
  const hasAvailableVersions = columns.some((column) => column.name === "available_versions");
98
100
  if (!hasAvailableVersions) {
99
101
  this.db.exec("ALTER TABLE versions ADD COLUMN available_versions TEXT;");
@@ -117,8 +119,8 @@ export class VersionCache {
117
119
  this.fallbackReason = fallbackReason;
118
120
  }
119
121
  static async create(customPath) {
120
- const basePath = customPath ?? path.join(os.homedir(), ".cache", "rainy-updates");
121
- if (process.env.RAINY_UPDATES_CACHE_BACKEND === "file") {
122
+ const basePath = customPath ?? getCacheDir();
123
+ if (readEnv("RAINY_UPDATES_CACHE_BACKEND") === "file") {
122
124
  const jsonPath = path.join(basePath, "cache.json");
123
125
  return new VersionCache(new FileCacheStore(jsonPath), "file", true, "forced via RAINY_UPDATES_CACHE_BACKEND=file");
124
126
  }
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import process from "node:process";
2
+ import { getRuntimeCwd } from "../../utils/runtime.js";
3
3
  const SEVERITY_LEVELS = ["critical", "high", "medium", "low"];
4
4
  const SOURCE_MODES = ["auto", "osv", "github", "all"];
5
5
  export function parseSeverity(value) {
@@ -10,7 +10,7 @@ export function parseSeverity(value) {
10
10
  }
11
11
  export function parseAuditArgs(args) {
12
12
  const options = {
13
- cwd: process.cwd(),
13
+ cwd: getRuntimeCwd(),
14
14
  workspace: false,
15
15
  severity: undefined,
16
16
  fix: false,
@@ -1,10 +1,9 @@
1
- import { spawn } from "node:child_process";
2
- import { promises as fs } from "node:fs";
3
- import path from "node:path";
4
1
  import { collectDependencies, readManifest, } from "../../parsers/package-json.js";
2
+ import { detectPackageManager as detectProjectPackageManager, resolvePackageManager, } from "../../pm/detect.js";
5
3
  import { discoverPackageDirs } from "../../workspace/discover.js";
6
4
  import { writeFileAtomic } from "../../utils/io.js";
7
5
  import { stableStringify } from "../../utils/stable-json.js";
6
+ import { writeStderr, writeStdout } from "../../utils/runtime.js";
8
7
  import { fetchAdvisories } from "./fetcher.js";
9
8
  import { resolveAuditTargets } from "./targets.js";
10
9
  import { filterBySeverity, buildPatchMap, renderAuditSourceHealth, renderAuditSummary, renderAuditTable, summarizeAdvisories, } from "./mapper.js";
@@ -55,7 +54,7 @@ export async function runAudit(options) {
55
54
  return result;
56
55
  }
57
56
  if (!options.silent) {
58
- process.stderr.write(`[audit] Querying ${describeSourceMode(options.sourceMode)} for ${targetResolution.targets.length} dependency version${targetResolution.targets.length === 1 ? "" : "s"}...\n`);
57
+ writeStderr(`[audit] Querying ${describeSourceMode(options.sourceMode)} for ${targetResolution.targets.length} dependency version${targetResolution.targets.length === 1 ? "" : "s"}...\n`);
59
58
  }
60
59
  const fetched = await fetchAdvisories(targetResolution.targets, {
61
60
  concurrency: options.concurrency,
@@ -81,12 +80,12 @@ export async function runAudit(options) {
81
80
  result.autoFixable = advisories.filter((a) => a.patchedVersion !== null).length;
82
81
  if (!options.silent) {
83
82
  if (options.reportFormat === "summary") {
84
- process.stdout.write(renderAuditSummary(result.packages) +
83
+ writeStdout(renderAuditSummary(result.packages) +
85
84
  renderAuditSourceHealth(result.sourceHealth) +
86
85
  "\n");
87
86
  }
88
87
  else if (options.reportFormat === "table" || !options.jsonFile) {
89
- process.stdout.write(renderAuditTable(advisories) +
88
+ writeStdout(renderAuditTable(advisories) +
90
89
  renderAuditSourceHealth(result.sourceHealth) +
91
90
  "\n");
92
91
  }
@@ -102,7 +101,7 @@ export async function runAudit(options) {
102
101
  warnings: result.warnings,
103
102
  }, 2) + "\n");
104
103
  if (!options.silent) {
105
- process.stderr.write(`[audit] JSON report written to ${options.jsonFile}\n`);
104
+ writeStderr(`[audit] JSON report written to ${options.jsonFile}\n`);
106
105
  }
107
106
  }
108
107
  if (options.fix && result.autoFixable > 0) {
@@ -122,40 +121,41 @@ async function applyFix(advisories, options) {
122
121
  const patchMap = buildPatchMap(advisories);
123
122
  if (patchMap.size === 0)
124
123
  return;
125
- const pm = await detectPackageManager(options.cwd, options.packageManager);
124
+ const detected = await detectProjectPackageManager(options.cwd);
125
+ const pm = resolvePackageManager(options.packageManager, detected);
126
126
  const installArgs = buildInstallArgs(pm, patchMap);
127
127
  const installCmd = `${pm} ${installArgs.join(" ")}`;
128
128
  if (options.dryRun) {
129
129
  if (!options.silent) {
130
- process.stderr.write(`[audit] --dry-run: would execute:\n ${installCmd}\n`);
130
+ writeStderr(`[audit] --dry-run: would execute:\n ${installCmd}\n`);
131
131
  if (options.commit) {
132
132
  const msg = buildCommitMessage(patchMap);
133
- process.stderr.write(`[audit] --dry-run: would commit:\n git commit -m "${msg}"\n`);
133
+ writeStderr(`[audit] --dry-run: would commit:\n git commit -m "${msg}"\n`);
134
134
  }
135
135
  }
136
136
  return;
137
137
  }
138
138
  if (!options.silent) {
139
- process.stderr.write(`[audit] Applying ${patchMap.size} fix(es)...\n`);
140
- process.stderr.write(` → ${installCmd}\n`);
139
+ writeStderr(`[audit] Applying ${patchMap.size} fix(es)...\n`);
140
+ writeStderr(` → ${installCmd}\n`);
141
141
  }
142
142
  try {
143
143
  await runCommand(pm, installArgs, options.cwd);
144
144
  }
145
145
  catch (err) {
146
146
  if (!options.silent) {
147
- process.stderr.write(`[audit] Install failed: ${String(err)}\n`);
147
+ writeStderr(`[audit] Install failed: ${String(err)}\n`);
148
148
  }
149
149
  return;
150
150
  }
151
151
  if (!options.silent) {
152
- process.stderr.write(`[audit] ✔ Patches applied successfully.\n`);
152
+ writeStderr(`[audit] ✔ Patches applied successfully.\n`);
153
153
  }
154
154
  if (options.commit) {
155
155
  await commitFix(patchMap, options.cwd, options.silent);
156
156
  }
157
157
  else if (!options.silent) {
158
- process.stderr.write(`[audit] Tip: run with --commit to automatically commit the changes.\n`);
158
+ writeStderr(`[audit] Tip: run with --commit to automatically commit the changes.\n`);
159
159
  }
160
160
  }
161
161
  function buildInstallArgs(pm, patchMap) {
@@ -205,48 +205,29 @@ function buildCommitMessage(patchMap) {
205
205
  const names = items.map(([n]) => n).join(", ");
206
206
  return `fix(security): patch ${items.length} vulnerabilities — ${names} (rup audit)`;
207
207
  }
208
- /** Detects the package manager in use by checking for lockfiles. */
209
- async function detectPackageManager(cwd, explicit) {
210
- if (explicit !== "auto")
211
- return explicit;
212
- const checks = [
213
- ["bun.lock", "bun"],
214
- ["bun.lockb", "bun"],
215
- ["pnpm-lock.yaml", "pnpm"],
216
- ["yarn.lock", "yarn"],
217
- ];
218
- for (const [lockfile, pm] of checks) {
219
- try {
220
- await fs.access(path.join(cwd, lockfile));
221
- return pm;
222
- }
223
- catch {
224
- // not found, try next
225
- }
226
- }
227
- return "npm"; // default
228
- }
229
208
  /** Spawns a subprocess, pipes stdio live to the terminal. */
230
209
  function runCommand(cmd, args, cwd, ignoreErrors = false) {
231
- return new Promise((resolve, reject) => {
232
- const child = spawn(cmd, args, {
233
- cwd,
234
- stdio: "inherit", // stream stdout/stderr live
235
- shell: process.platform === "win32",
236
- });
237
- child.on("close", (code) => {
210
+ return new Promise(async (resolve, reject) => {
211
+ try {
212
+ const proc = Bun.spawn([cmd, ...args], {
213
+ cwd,
214
+ stdin: "inherit",
215
+ stdout: "inherit",
216
+ stderr: "inherit",
217
+ });
218
+ const code = await proc.exited;
238
219
  if (code === 0 || ignoreErrors) {
239
220
  resolve();
240
221
  }
241
222
  else {
242
223
  reject(new Error(`${cmd} exited with code ${code}`));
243
224
  }
244
- });
245
- child.on("error", (err) => {
225
+ }
226
+ catch (err) {
246
227
  if (ignoreErrors)
247
228
  resolve();
248
229
  else
249
230
  reject(err);
250
- });
231
+ }
251
232
  });
252
233
  }
@@ -1,4 +1,3 @@
1
- import { promises as fs } from "node:fs";
2
1
  import path from "node:path";
3
2
  const LOCKFILE_PRIORITY = [
4
3
  "package-lock.json",
@@ -86,13 +85,9 @@ async function findNearestLockfiles(rootCwd, startDir) {
86
85
  while (true) {
87
86
  for (const fileName of LOCKFILE_PRIORITY) {
88
87
  const candidate = path.join(current, fileName);
89
- try {
90
- await fs.access(candidate);
88
+ if (await fileExists(candidate)) {
91
89
  found.push(candidate);
92
90
  }
93
- catch {
94
- // ignore missing
95
- }
96
91
  }
97
92
  if (current === rootCwd)
98
93
  break;
@@ -115,9 +110,6 @@ async function resolveFromPackageLock(lockfilePath, packageDir, packageName) {
115
110
  if (version)
116
111
  return version;
117
112
  }
118
- if (!relDir) {
119
- return parsed.dependencies?.[packageName]?.version ?? null;
120
- }
121
113
  return parsed.dependencies?.[packageName]?.version ?? null;
122
114
  }
123
115
  async function resolveFromPnpmLock(lockfilePath, packageDir, packageName) {
@@ -149,8 +141,8 @@ async function resolveFromBunLock(lockfilePath, packageDir, packageName) {
149
141
  async function readPackageLock(lockfilePath) {
150
142
  let promise = packageLockCache.get(lockfilePath);
151
143
  if (!promise) {
152
- promise = fs
153
- .readFile(lockfilePath, "utf8")
144
+ promise = Bun.file(lockfilePath)
145
+ .text()
154
146
  .then((content) => JSON.parse(content));
155
147
  packageLockCache.set(lockfilePath, promise);
156
148
  }
@@ -159,7 +151,7 @@ async function readPackageLock(lockfilePath) {
159
151
  async function readPnpmLock(lockfilePath) {
160
152
  let promise = pnpmLockCache.get(lockfilePath);
161
153
  if (!promise) {
162
- promise = fs.readFile(lockfilePath, "utf8").then(parsePnpmLock);
154
+ promise = Bun.file(lockfilePath).text().then(parsePnpmLock);
163
155
  pnpmLockCache.set(lockfilePath, promise);
164
156
  }
165
157
  return await promise;
@@ -167,7 +159,7 @@ async function readPnpmLock(lockfilePath) {
167
159
  async function readBunLock(lockfilePath) {
168
160
  let promise = bunLockCache.get(lockfilePath);
169
161
  if (!promise) {
170
- promise = fs.readFile(lockfilePath, "utf8").then(parseBunLock);
162
+ promise = Bun.file(lockfilePath).text().then(parseBunLock);
171
163
  bunLockCache.set(lockfilePath, promise);
172
164
  }
173
165
  return await promise;
@@ -312,3 +304,11 @@ function normalizePnpmVersion(value) {
312
304
  function trimYamlKey(value) {
313
305
  return value.trim().replace(/^['"]|['"]$/g, "");
314
306
  }
307
+ async function fileExists(filePath) {
308
+ try {
309
+ return await Bun.file(filePath).exists();
310
+ }
311
+ catch {
312
+ return false;
313
+ }
314
+ }
@@ -1,5 +1,5 @@
1
- import { spawn } from "node:child_process";
2
1
  import path from "node:path";
2
+ import { detectPackageManager, resolvePackageManager } from "../../pm/detect.js";
3
3
  /**
4
4
  * The "oracle" for bisect: installs a specific version of a package
5
5
  * into the project's node_modules (via the shell), then runs --cmd.
@@ -11,7 +11,9 @@ export async function bisectOracle(packageName, version, options) {
11
11
  process.stderr.write(`[bisect:dry-run] Would test ${packageName}@${version}\n`);
12
12
  return "skip";
13
13
  }
14
- const installResult = await runShell(`npm install --no-save ${packageName}@${version}`, options.cwd);
14
+ const detected = await detectPackageManager(options.cwd);
15
+ const packageManager = resolvePackageManager("auto", detected, "bun");
16
+ const installResult = await runShell(buildInstallCommand(packageManager, packageName, version), options.cwd);
15
17
  if (installResult !== 0) {
16
18
  process.stderr.write(`[bisect] Failed to install ${packageName}@${version}, skipping.\n`);
17
19
  return "skip";
@@ -22,15 +24,30 @@ export async function bisectOracle(packageName, version, options) {
22
24
  process.stderr.write(`[bisect] ${packageName}@${version} → ${outcome}\n`);
23
25
  return outcome;
24
26
  }
25
- function runShell(command, cwd) {
26
- return new Promise((resolve) => {
27
- const [bin, ...args] = command.split(" ");
28
- const child = spawn(bin, args, {
27
+ async function runShell(command, cwd) {
28
+ try {
29
+ const shellCmd = process.env.SHELL || "sh";
30
+ const proc = Bun.spawn([shellCmd, "-c", command], {
29
31
  cwd: path.resolve(cwd),
30
- shell: true,
31
- stdio: "pipe",
32
+ stdout: "pipe",
33
+ stderr: "pipe",
32
34
  });
33
- child.on("close", (code) => resolve(code ?? 1));
34
- child.on("error", () => resolve(1));
35
- });
35
+ return await proc.exited;
36
+ }
37
+ catch {
38
+ return 1;
39
+ }
40
+ }
41
+ function buildInstallCommand(packageManager, packageName, version) {
42
+ const spec = `${packageName}@${version}`;
43
+ switch (packageManager) {
44
+ case "bun":
45
+ return `bun add --exact --no-save ${spec}`;
46
+ case "pnpm":
47
+ return `pnpm add --save-exact --no-save ${spec}`;
48
+ case "yarn":
49
+ return `npm install --no-save --save-exact ${spec}`;
50
+ default:
51
+ return `npm install --no-save --save-exact ${spec}`;
52
+ }
36
53
  }
@@ -1,11 +1,11 @@
1
1
  import path from "node:path";
2
- import process from "node:process";
2
+ import { getRuntimeCwd } from "../../utils/runtime.js";
3
3
  export function parseBisectArgs(args) {
4
4
  const options = {
5
- cwd: process.cwd(),
5
+ cwd: getRuntimeCwd(),
6
6
  packageName: "",
7
7
  versionRange: undefined,
8
- testCommand: "npm test",
8
+ testCommand: "",
9
9
  concurrency: 4,
10
10
  registryTimeoutMs: 8000,
11
11
  cacheTtlSeconds: 3600,
@@ -1,26 +1,33 @@
1
+ import { detectPackageManager, resolvePackageManager } from "../../pm/detect.js";
1
2
  import { fetchBisectVersions, bisectVersions } from "./engine.js";
2
3
  /**
3
4
  * Entry point for the `bisect` command. Lazy-loaded by cli.ts.
4
5
  * Fully isolated: does NOT import anything from core/options.ts.
5
6
  */
6
7
  export async function runBisect(options) {
7
- process.stderr.write(`\n[bisect] Fetching available versions for ${options.packageName}...\n`);
8
- const versions = await fetchBisectVersions(options);
8
+ const detected = await detectPackageManager(options.cwd);
9
+ const runtimeOptions = {
10
+ ...options,
11
+ testCommand: options.testCommand ||
12
+ `${resolvePackageManager("auto", detected, "bun")} test`,
13
+ };
14
+ process.stderr.write(`\n[bisect] Fetching available versions for ${runtimeOptions.packageName}...\n`);
15
+ const versions = await fetchBisectVersions(runtimeOptions);
9
16
  if (versions.length === 0) {
10
- throw new Error(`No versions found for package "${options.packageName}".`);
17
+ throw new Error(`No versions found for package "${runtimeOptions.packageName}".`);
11
18
  }
12
19
  process.stderr.write(`[bisect] Found ${versions.length} versions. Starting binary search...\n`);
13
- if (options.versionRange) {
14
- process.stderr.write(`[bisect] Range: ${options.versionRange}\n`);
20
+ if (runtimeOptions.versionRange) {
21
+ process.stderr.write(`[bisect] Range: ${runtimeOptions.versionRange}\n`);
15
22
  }
16
- const result = await bisectVersions(versions, options);
23
+ const result = await bisectVersions(versions, runtimeOptions);
17
24
  if (result.breakingVersion) {
18
- process.stdout.write(`\n✖ Break introduced in ${options.packageName}@${result.breakingVersion}\n` +
25
+ process.stdout.write(`\n✖ Break introduced in ${runtimeOptions.packageName}@${result.breakingVersion}\n` +
19
26
  ` Last good version: ${result.lastGoodVersion ?? "none"}\n` +
20
27
  ` Tested: ${result.totalVersionsTested} versions in ${result.iterations} iterations\n`);
21
28
  }
22
29
  else {
23
- process.stdout.write(`\n✔ No breaking version found for ${options.packageName} (all versions passed).\n` +
30
+ process.stdout.write(`\n✔ No breaking version found for ${runtimeOptions.packageName} (all versions passed).\n` +
24
31
  ` Tested: ${result.totalVersionsTested} versions\n`);
25
32
  }
26
33
  return result;
@@ -1,12 +1,12 @@
1
- import os from "node:os";
1
+ import { mkdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { promises as fs } from "node:fs";
3
+ import { getCacheDir } from "../../utils/runtime-paths.js";
4
4
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
5
5
  class ChangelogCache {
6
6
  db = null;
7
7
  dbPath;
8
8
  constructor() {
9
- const basePath = path.join(os.homedir(), ".cache", "rainy-updates");
9
+ const basePath = getCacheDir();
10
10
  this.dbPath = path.join(basePath, "cache.db");
11
11
  }
12
12
  async init() {
@@ -14,7 +14,7 @@ class ChangelogCache {
14
14
  return;
15
15
  try {
16
16
  if (typeof Bun !== "undefined") {
17
- await fs.mkdir(path.dirname(this.dbPath), { recursive: true });
17
+ await mkdir(path.dirname(this.dbPath), { recursive: true });
18
18
  const mod = await import("bun:sqlite");
19
19
  this.db = new mod.Database(this.dbPath, { create: true });
20
20
  this.db.exec(`
@@ -111,7 +111,7 @@ export async function fetchChangelog(packageName, repositoryUrl) {
111
111
  if (contentsRes.ok) {
112
112
  const fileContent = await contentsRes.json();
113
113
  if (fileContent.content && fileContent.encoding === "base64") {
114
- content = Buffer.from(fileContent.content, "base64").toString("utf-8");
114
+ content = decodeBase64Utf8(fileContent.content);
115
115
  }
116
116
  }
117
117
  }
@@ -128,3 +128,9 @@ export async function fetchChangelog(packageName, repositoryUrl) {
128
128
  return null;
129
129
  }
130
130
  }
131
+ function decodeBase64Utf8(value) {
132
+ const normalized = value.replace(/\s+/g, "");
133
+ const binary = atob(normalized);
134
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
135
+ return new TextDecoder().decode(bytes);
136
+ }
@@ -1,7 +1,10 @@
1
+ import path from "node:path";
1
2
  export function parseDashboardArgs(args) {
2
3
  const options = {
3
4
  cwd: process.cwd(),
4
5
  target: "latest",
6
+ filter: undefined,
7
+ reject: undefined,
5
8
  includeKinds: [
6
9
  "dependencies",
7
10
  "devDependencies",
@@ -12,19 +15,44 @@ export function parseDashboardArgs(args) {
12
15
  ci: false,
13
16
  format: "table",
14
17
  workspace: false,
18
+ jsonFile: undefined,
19
+ githubOutputFile: undefined,
20
+ sarifFile: undefined,
15
21
  concurrency: 16,
16
22
  registryTimeoutMs: 8000,
17
23
  registryRetries: 3,
18
24
  offline: false,
19
25
  stream: false,
26
+ policyFile: undefined,
27
+ prReportFile: undefined,
28
+ failOn: "none",
29
+ maxUpdates: undefined,
30
+ fixPr: false,
31
+ fixBranch: "chore/rainy-updates",
32
+ fixCommitMessage: undefined,
33
+ fixDryRun: false,
34
+ fixPrNoCheckout: false,
35
+ fixPrBatchSize: undefined,
36
+ noPrReport: false,
20
37
  logLevel: "info",
21
38
  groupBy: "none",
39
+ groupMax: undefined,
40
+ cooldownDays: undefined,
41
+ prLimit: undefined,
22
42
  onlyChanged: false,
23
43
  ciProfile: "minimal",
24
44
  lockfileMode: "preserve",
25
45
  interactive: true,
26
46
  showImpact: false,
27
47
  showHomepage: true,
48
+ mode: "review",
49
+ focus: "all",
50
+ applySelected: false,
51
+ decisionPlanFile: undefined,
52
+ verify: "none",
53
+ testCommand: undefined,
54
+ verificationReportFile: undefined,
55
+ ciGate: "check",
28
56
  };
29
57
  for (let i = 0; i < args.length; i++) {
30
58
  const arg = args[i];
@@ -44,16 +72,90 @@ export function parseDashboardArgs(args) {
44
72
  if (arg === "--view") {
45
73
  throw new Error("Missing value for --view");
46
74
  }
75
+ if (arg === "--mode" && nextArg) {
76
+ if (nextArg === "check" ||
77
+ nextArg === "review" ||
78
+ nextArg === "upgrade") {
79
+ options.mode = nextArg;
80
+ i++;
81
+ continue;
82
+ }
83
+ throw new Error(`Invalid --mode: ${nextArg}`);
84
+ }
85
+ if (arg === "--mode") {
86
+ throw new Error("Missing value for --mode");
87
+ }
88
+ if (arg === "--focus" && nextArg) {
89
+ if (nextArg === "all" ||
90
+ nextArg === "security" ||
91
+ nextArg === "risk" ||
92
+ nextArg === "major" ||
93
+ nextArg === "blocked" ||
94
+ nextArg === "workspace") {
95
+ options.focus = nextArg;
96
+ i++;
97
+ continue;
98
+ }
99
+ throw new Error(`Invalid --focus: ${nextArg}`);
100
+ }
101
+ if (arg === "--focus") {
102
+ throw new Error("Missing value for --focus");
103
+ }
104
+ if (arg === "--apply-selected") {
105
+ options.applySelected = true;
106
+ continue;
107
+ }
108
+ if (arg === "--verify" && nextArg) {
109
+ if (nextArg === "none" ||
110
+ nextArg === "install" ||
111
+ nextArg === "test" ||
112
+ nextArg === "install,test") {
113
+ options.verify = nextArg;
114
+ i++;
115
+ continue;
116
+ }
117
+ throw new Error(`Invalid --verify: ${nextArg}`);
118
+ }
119
+ if (arg === "--verify") {
120
+ throw new Error("Missing value for --verify");
121
+ }
122
+ if (arg === "--test-command" && nextArg) {
123
+ options.testCommand = nextArg;
124
+ i++;
125
+ continue;
126
+ }
127
+ if (arg === "--test-command") {
128
+ throw new Error("Missing value for --test-command");
129
+ }
130
+ if (arg === "--verification-report-file" && nextArg) {
131
+ options.verificationReportFile = path.resolve(options.cwd, nextArg);
132
+ i++;
133
+ continue;
134
+ }
135
+ if (arg === "--verification-report-file") {
136
+ throw new Error("Missing value for --verification-report-file");
137
+ }
138
+ if (arg === "--plan-file" && nextArg) {
139
+ options.decisionPlanFile = path.resolve(options.cwd, nextArg);
140
+ i++;
141
+ continue;
142
+ }
143
+ if (arg === "--plan-file") {
144
+ throw new Error("Missing value for --plan-file");
145
+ }
47
146
  // Pass through common workspace / cwd args
48
147
  if (arg === "--workspace") {
49
148
  options.workspace = true;
50
149
  continue;
51
150
  }
52
151
  if (arg === "--cwd" && nextArg) {
53
- options.cwd = nextArg;
152
+ options.cwd = path.resolve(nextArg);
54
153
  i++;
55
154
  continue;
56
155
  }
156
+ if (arg.startsWith("-")) {
157
+ throw new Error(`Unknown dashboard option: ${arg}`);
158
+ }
57
159
  }
58
160
  return options;
59
161
  }
@@ -1,2 +1,2 @@
1
- import type { DashboardOptions, DashboardResult } from "../../types/index.js";
2
- export declare function runDashboard(options: DashboardOptions): Promise<DashboardResult>;
1
+ import type { DashboardOptions, DashboardResult, ReviewResult } from "../../types/index.js";
2
+ export declare function runDashboard(options: DashboardOptions, prebuiltReview?: ReviewResult): Promise<DashboardResult>;