@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
@@ -1,9 +1,9 @@
1
- import process from "node:process";
2
1
  import { discoverPackageDirs } from "../../workspace/discover.js";
3
2
  import { readManifest, collectDependencies, } from "../../parsers/package-json.js";
4
3
  import { asyncPool } from "../../utils/async-pool.js";
5
4
  import { stableStringify } from "../../utils/stable-json.js";
6
5
  import { writeFileAtomic } from "../../utils/io.js";
6
+ import { writeStderr, writeStdout } from "../../utils/runtime.js";
7
7
  import { generateSbom } from "./sbom.js";
8
8
  /**
9
9
  * Entry point for `rup licenses`. Lazy-loaded by cli.ts.
@@ -63,17 +63,17 @@ export async function runLicenses(options) {
63
63
  }
64
64
  result.totalViolations = result.violations.length;
65
65
  // Render
66
- process.stdout.write(renderLicenseTable(result) + "\n");
66
+ writeStdout(renderLicenseTable(result) + "\n");
67
67
  // SBOM output
68
68
  if (options.sbomFile) {
69
69
  const sbom = generateSbom(result.packages, options.cwd);
70
70
  await writeFileAtomic(options.sbomFile, stableStringify(sbom, 2) + "\n");
71
- process.stderr.write(`[licenses] SBOM written to ${options.sbomFile}\n`);
71
+ writeStderr(`[licenses] SBOM written to ${options.sbomFile}\n`);
72
72
  }
73
73
  // JSON output
74
74
  if (options.jsonFile) {
75
75
  await writeFileAtomic(options.jsonFile, stableStringify(result, 2) + "\n");
76
- process.stderr.write(`[licenses] JSON report written to ${options.jsonFile}\n`);
76
+ writeStderr(`[licenses] JSON report written to ${options.jsonFile}\n`);
77
77
  }
78
78
  return result;
79
79
  }
@@ -1,8 +1,8 @@
1
- import process from "node:process";
2
1
  import { buildPeerGraph } from "./graph/builder.js";
3
2
  import { resolvePeerConflicts } from "./graph/resolver.js";
4
3
  import { stableStringify } from "../../utils/stable-json.js";
5
4
  import { writeFileAtomic } from "../../utils/io.js";
5
+ import { writeStderr, writeStdout } from "../../utils/runtime.js";
6
6
  /**
7
7
  * Entry point for `rup resolve`. Lazy-loaded by cli.ts.
8
8
  *
@@ -26,7 +26,7 @@ export async function runResolve(options) {
26
26
  if (options.afterUpdate) {
27
27
  versionOverrides = await fetchProposedVersions(options);
28
28
  if (versionOverrides.size === 0 && !options.silent) {
29
- process.stderr.write("[resolve] No pending updates found — checking current state.\n");
29
+ writeStderr("[resolve] No pending updates found — checking current state.\n");
30
30
  }
31
31
  }
32
32
  let graph;
@@ -42,12 +42,12 @@ export async function runResolve(options) {
42
42
  result.errorConflicts = conflicts.filter((c) => c.severity === "error").length;
43
43
  result.warningConflicts = conflicts.filter((c) => c.severity === "warning").length;
44
44
  if (!options.silent) {
45
- process.stdout.write(renderConflictsTable(result, options) + "\n");
45
+ writeStdout(renderConflictsTable(result, options) + "\n");
46
46
  }
47
47
  if (options.jsonFile) {
48
48
  await writeFileAtomic(options.jsonFile, stableStringify(result, 2) + "\n");
49
49
  if (!options.silent) {
50
- process.stderr.write(`[resolve] JSON report written to ${options.jsonFile}\n`);
50
+ writeStderr(`[resolve] JSON report written to ${options.jsonFile}\n`);
51
51
  }
52
52
  }
53
53
  return result;
@@ -100,6 +100,11 @@ async function fetchProposedVersions(options) {
100
100
  interactive: false,
101
101
  showImpact: false,
102
102
  showHomepage: false,
103
+ decisionPlanFile: undefined,
104
+ verify: "none",
105
+ testCommand: undefined,
106
+ verificationReportFile: undefined,
107
+ ciGate: "check",
103
108
  });
104
109
  for (const update of checkResult.updates ?? []) {
105
110
  overrides.set(update.name, update.toVersionResolved);
@@ -1,9 +1,9 @@
1
1
  import path from "node:path";
2
- import process from "node:process";
3
2
  import { ensureRiskLevel } from "../../core/options.js";
3
+ import { exitProcess, getRuntimeCwd, writeStdout, } from "../../utils/runtime.js";
4
4
  export function parseReviewArgs(args) {
5
5
  const options = {
6
- cwd: process.cwd(),
6
+ cwd: getRuntimeCwd(),
7
7
  target: "latest",
8
8
  filter: undefined,
9
9
  reject: undefined,
@@ -47,6 +47,12 @@ export function parseReviewArgs(args) {
47
47
  diff: undefined,
48
48
  applySelected: false,
49
49
  showChangelog: false,
50
+ decisionPlanFile: undefined,
51
+ queueFocus: "all",
52
+ verify: "none",
53
+ testCommand: undefined,
54
+ verificationReportFile: undefined,
55
+ ciGate: "check",
50
56
  };
51
57
  for (let i = 0; i < args.length; i += 1) {
52
58
  const current = args[i];
@@ -94,6 +100,41 @@ export function parseReviewArgs(args) {
94
100
  options.applySelected = true;
95
101
  continue;
96
102
  }
103
+ if (current === "--plan-file" && next) {
104
+ options.decisionPlanFile = path.resolve(options.cwd, next);
105
+ i += 1;
106
+ continue;
107
+ }
108
+ if (current === "--plan-file")
109
+ throw new Error("Missing value for --plan-file");
110
+ if (current === "--verify" && next) {
111
+ if (next === "none" ||
112
+ next === "install" ||
113
+ next === "test" ||
114
+ next === "install,test") {
115
+ options.verify = next;
116
+ i += 1;
117
+ continue;
118
+ }
119
+ throw new Error("--verify must be none, install, test or install,test");
120
+ }
121
+ if (current === "--verify")
122
+ throw new Error("Missing value for --verify");
123
+ if (current === "--test-command" && next) {
124
+ options.testCommand = next;
125
+ i += 1;
126
+ continue;
127
+ }
128
+ if (current === "--test-command")
129
+ throw new Error("Missing value for --test-command");
130
+ if (current === "--verification-report-file" && next) {
131
+ options.verificationReportFile = path.resolve(options.cwd, next);
132
+ i += 1;
133
+ continue;
134
+ }
135
+ if (current === "--verification-report-file") {
136
+ throw new Error("Missing value for --verification-report-file");
137
+ }
97
138
  if (current === "--show-changelog") {
98
139
  options.showChangelog = true;
99
140
  continue;
@@ -148,13 +189,22 @@ export function parseReviewArgs(args) {
148
189
  throw new Error("Missing value for --registry-retries");
149
190
  }
150
191
  if (current === "--help" || current === "-h") {
151
- process.stdout.write(REVIEW_HELP);
152
- process.exit(0);
192
+ writeStdout(REVIEW_HELP);
193
+ exitProcess(0);
153
194
  }
154
195
  if (current.startsWith("-"))
155
196
  throw new Error(`Unknown review option: ${current}`);
156
197
  throw new Error(`Unexpected review argument: ${current}`);
157
198
  }
199
+ if (options.securityOnly) {
200
+ options.queueFocus = "security";
201
+ }
202
+ else if (options.risk === "critical" || options.risk === "high") {
203
+ options.queueFocus = "risk";
204
+ }
205
+ else if (options.diff === "major") {
206
+ options.queueFocus = "major";
207
+ }
158
208
  return options;
159
209
  }
160
210
  const REVIEW_HELP = `
@@ -169,6 +219,9 @@ Options:
169
219
  --risk <level> Minimum risk: critical, high, medium, low
170
220
  --diff <level> Filter by patch, minor, major, latest
171
221
  --apply-selected Apply all filtered updates after review
222
+ --plan-file <path> Write the selected decision set to a reusable plan file
223
+ --verify <mode> Run post-apply verification: none, install, test, install,test
224
+ --test-command <cmd> Override the command used for test verification
172
225
  --show-changelog Fetch release notes summaries for review output
173
226
  --workspace Scan all workspace packages
174
227
  --policy-file <path> Load policy overrides
@@ -1,16 +1,42 @@
1
- import process from "node:process";
2
- import { runTui } from "../../ui/tui.js";
3
1
  import { buildReviewResult, renderReviewResult } from "../../core/review-model.js";
4
2
  import { applySelectedUpdates } from "../../core/upgrade.js";
3
+ import { createDecisionPlan, writeDecisionPlan } from "../../core/decision-plan.js";
5
4
  import { stableStringify } from "../../utils/stable-json.js";
6
5
  import { writeFileAtomic } from "../../utils/io.js";
6
+ import { writeStdout } from "../../utils/runtime.js";
7
7
  export async function runReview(options) {
8
8
  const review = await buildReviewResult(options);
9
- let selectedItems = review.items;
10
9
  if (options.interactive && review.updates.length > 0) {
11
- selectedItems = await runTui(review.items);
10
+ const { runDashboard } = await import("../dashboard/runner.js");
11
+ const dashboard = await runDashboard({
12
+ ...options,
13
+ mode: options.applySelected ? "upgrade" : "review",
14
+ focus: options.queueFocus ?? "all",
15
+ applySelected: options.applySelected,
16
+ }, review);
17
+ review.summary.decisionPlan = dashboard.decisionPlanFile;
18
+ review.summary.interactiveSurface = "dashboard";
19
+ review.summary.queueFocus = options.queueFocus ?? "all";
20
+ if (options.jsonFile) {
21
+ await writeFileAtomic(options.jsonFile, stableStringify(review, 2) + "\n");
22
+ }
23
+ return review;
12
24
  }
25
+ let selectedItems = review.items;
13
26
  const selectedUpdates = selectedItems.map((item) => item.update);
27
+ if (options.decisionPlanFile) {
28
+ const decisionPlan = createDecisionPlan({
29
+ review,
30
+ selectedItems,
31
+ sourceCommand: "review",
32
+ mode: options.applySelected ? "upgrade" : "review",
33
+ focus: options.queueFocus ?? "all",
34
+ });
35
+ const decisionPlanFile = options.decisionPlanFile;
36
+ await writeDecisionPlan(decisionPlanFile, decisionPlan);
37
+ review.decisionPlan = decisionPlan;
38
+ review.summary.decisionPlan = decisionPlanFile;
39
+ }
14
40
  if (options.applySelected && selectedUpdates.length > 0) {
15
41
  await applySelectedUpdates({
16
42
  ...options,
@@ -19,7 +45,7 @@ export async function runReview(options) {
19
45
  sync: false,
20
46
  }, selectedUpdates);
21
47
  }
22
- process.stdout.write(renderReviewResult({
48
+ writeStdout(renderReviewResult({
23
49
  ...review,
24
50
  items: selectedItems,
25
51
  updates: selectedUpdates,
@@ -1,6 +1,6 @@
1
- import process from "node:process";
2
1
  import { discoverPackageDirs } from "../../workspace/discover.js";
3
2
  import { SnapshotStore, captureState, restoreState, diffManifests, } from "./store.js";
3
+ import { writeStderr, writeStdout } from "../../utils/runtime.js";
4
4
  /**
5
5
  * Entry point for `rup snapshot`. Lazy-loaded by cli.ts.
6
6
  *
@@ -27,7 +27,7 @@ export async function runSnapshot(options) {
27
27
  const entry = await store.saveSnapshot(manifests, lockfileHashes, label);
28
28
  result.snapshotId = entry.id;
29
29
  result.label = entry.label;
30
- process.stdout.write(`✔ Snapshot saved: ${entry.label} (${entry.id})\n`);
30
+ writeStdout(`✔ Snapshot saved: ${entry.label} (${entry.id})\n`);
31
31
  break;
32
32
  }
33
33
  // ─ list ──────────────────────────────────────────────────────────────────
@@ -39,20 +39,20 @@ export async function runSnapshot(options) {
39
39
  createdAt: new Date(e.createdAt).toISOString(),
40
40
  }));
41
41
  if (entries.length === 0) {
42
- process.stdout.write("No snapshots saved yet. Use `rup snapshot save` to create one.\n");
42
+ writeStdout("No snapshots saved yet. Use `rup snapshot save` to create one.\n");
43
43
  }
44
44
  else {
45
- process.stdout.write(`\n${entries.length} snapshot(s):\n\n`);
46
- process.stdout.write(" " + "ID".padEnd(30) + "Label".padEnd(30) + "Created\n");
47
- process.stdout.write(" " + "─".repeat(75) + "\n");
45
+ writeStdout(`\n${entries.length} snapshot(s):\n\n`);
46
+ writeStdout(" " + "ID".padEnd(30) + "Label".padEnd(30) + "Created\n");
47
+ writeStdout(" " + "─".repeat(75) + "\n");
48
48
  for (const e of entries) {
49
- process.stdout.write(" " +
49
+ writeStdout(" " +
50
50
  e.id.padEnd(30) +
51
51
  e.label.padEnd(30) +
52
52
  new Date(e.createdAt).toLocaleString() +
53
53
  "\n");
54
54
  }
55
- process.stdout.write("\n");
55
+ writeStdout("\n");
56
56
  }
57
57
  break;
58
58
  }
@@ -72,8 +72,8 @@ export async function runSnapshot(options) {
72
72
  result.snapshotId = entry.id;
73
73
  result.label = entry.label;
74
74
  const count = Object.keys(entry.manifests).length;
75
- process.stdout.write(`✔ Restored ${count} package.json file(s) from snapshot "${entry.label}" (${entry.id})\n`);
76
- process.stdout.write(" Re-run your package manager install to apply.\n");
75
+ writeStdout(`✔ Restored ${count} package.json file(s) from snapshot "${entry.label}" (${entry.id})\n`);
76
+ writeStdout(" Re-run your package manager install to apply.\n");
77
77
  break;
78
78
  }
79
79
  // ─ diff ──────────────────────────────────────────────────────────────────
@@ -92,23 +92,23 @@ export async function runSnapshot(options) {
92
92
  const changes = diffManifests(entry.manifests, currentManifests);
93
93
  result.diff = changes;
94
94
  if (changes.length === 0) {
95
- process.stdout.write(`✔ No dependency changes since snapshot "${entry.label}"\n`);
95
+ writeStdout(`✔ No dependency changes since snapshot "${entry.label}"\n`);
96
96
  }
97
97
  else {
98
- process.stdout.write(`\nDependency changes since snapshot "${entry.label}":\n\n`);
99
- process.stdout.write(" " + "Package".padEnd(35) + "Before".padEnd(20) + "After\n");
100
- process.stdout.write(" " + "─".repeat(65) + "\n");
98
+ writeStdout(`\nDependency changes since snapshot "${entry.label}":\n\n`);
99
+ writeStdout(" " + "Package".padEnd(35) + "Before".padEnd(20) + "After\n");
100
+ writeStdout(" " + "─".repeat(65) + "\n");
101
101
  for (const c of changes) {
102
- process.stdout.write(" " + c.name.padEnd(35) + c.from.padEnd(20) + c.to + "\n");
102
+ writeStdout(" " + c.name.padEnd(35) + c.from.padEnd(20) + c.to + "\n");
103
103
  }
104
- process.stdout.write("\n");
104
+ writeStdout("\n");
105
105
  }
106
106
  break;
107
107
  }
108
108
  }
109
109
  if (result.errors.length > 0) {
110
110
  for (const err of result.errors) {
111
- process.stderr.write(`[snapshot] ✖ ${err}\n`);
111
+ writeStderr(`[snapshot] ✖ ${err}\n`);
112
112
  }
113
113
  }
114
114
  return result;
@@ -1,13 +1,4 @@
1
1
  import type { SnapshotEntry } from "../../types/index.js";
2
- /**
3
- * Lightweight SQLite-free snapshot store (uses a JSON file in the project root).
4
- *
5
- * Design goals:
6
- * - No extra runtime dependencies (SQLite bindings vary by runtime)
7
- * - Human-readable store file (git-committable if desired)
8
- * - Atomic writes via tmp-rename to prevent corruption
9
- * - Fast: entire store fits in memory for typical use (< 50 snapshots)
10
- */
11
2
  export declare class SnapshotStore {
12
3
  private readonly storePath;
13
4
  private entries;
@@ -20,14 +11,11 @@ export declare class SnapshotStore {
20
11
  findSnapshot(idOrLabel: string): Promise<SnapshotEntry | null>;
21
12
  deleteSnapshot(idOrLabel: string): Promise<boolean>;
22
13
  }
23
- /** Captures current package.json and lockfile state for a set of directories. */
24
14
  export declare function captureState(packageDirs: string[]): Promise<{
25
15
  manifests: Record<string, string>;
26
16
  lockfileHashes: Record<string, string>;
27
17
  }>;
28
- /** Restores package.json files from a snapshot's manifest map. */
29
18
  export declare function restoreState(entry: SnapshotEntry): Promise<void>;
30
- /** Computes a diff of dependency versions between two manifest snapshots. */
31
19
  export declare function diffManifests(before: Record<string, string>, after: Record<string, string>): Array<{
32
20
  name: string;
33
21
  from: string;
@@ -1,16 +1,6 @@
1
- import { createHash } from "node:crypto";
2
- import { promises as fs } from "node:fs";
3
1
  import path from "node:path";
2
+ import { writeFileAtomic } from "../../utils/io.js";
4
3
  const DEFAULT_STORE_NAME = ".rup-snapshots.json";
5
- /**
6
- * Lightweight SQLite-free snapshot store (uses a JSON file in the project root).
7
- *
8
- * Design goals:
9
- * - No extra runtime dependencies (SQLite bindings vary by runtime)
10
- * - Human-readable store file (git-committable if desired)
11
- * - Atomic writes via tmp-rename to prevent corruption
12
- * - Fast: entire store fits in memory for typical use (< 50 snapshots)
13
- */
14
4
  export class SnapshotStore {
15
5
  storePath;
16
6
  entries = [];
@@ -26,8 +16,7 @@ export class SnapshotStore {
26
16
  if (this.loaded)
27
17
  return;
28
18
  try {
29
- const raw = await fs.readFile(this.storePath, "utf8");
30
- const parsed = JSON.parse(raw);
19
+ const parsed = (await Bun.file(this.storePath).json());
31
20
  if (Array.isArray(parsed)) {
32
21
  this.entries = parsed;
33
22
  }
@@ -38,9 +27,7 @@ export class SnapshotStore {
38
27
  this.loaded = true;
39
28
  }
40
29
  async save() {
41
- const tmp = this.storePath + ".tmp";
42
- await fs.writeFile(tmp, JSON.stringify(this.entries, null, 2) + "\n", "utf8");
43
- await fs.rename(tmp, this.storePath);
30
+ await writeFileAtomic(this.storePath, JSON.stringify(this.entries, null, 2) + "\n");
44
31
  }
45
32
  async saveSnapshot(manifests, lockfileHashes, label) {
46
33
  await this.load();
@@ -76,32 +63,30 @@ export class SnapshotStore {
76
63
  return false;
77
64
  }
78
65
  }
79
- /** Captures current package.json and lockfile state for a set of directories. */
80
66
  export async function captureState(packageDirs) {
81
67
  const manifests = {};
82
68
  const lockfileHashes = {};
83
- const LOCKFILES = [
69
+ const lockfiles = [
84
70
  "package-lock.json",
85
71
  "pnpm-lock.yaml",
86
72
  "yarn.lock",
73
+ "bun.lock",
87
74
  "bun.lockb",
88
75
  ];
89
76
  await Promise.all(packageDirs.map(async (dir) => {
90
- // Read package.json
91
77
  try {
92
- const content = await fs.readFile(path.join(dir, "package.json"), "utf8");
93
- manifests[dir] = content;
78
+ manifests[dir] = await Bun.file(path.join(dir, "package.json")).text();
94
79
  }
95
80
  catch {
96
81
  // No package.json — skip
97
82
  }
98
- // Hash the first lockfile found
99
- for (const lf of LOCKFILES) {
83
+ for (const lockfileName of lockfiles) {
84
+ const filePath = path.join(dir, lockfileName);
100
85
  try {
101
- const content = await fs.readFile(path.join(dir, lf));
102
- lockfileHashes[dir] = createHash("sha256")
103
- .update(content)
104
- .digest("hex");
86
+ const file = Bun.file(filePath);
87
+ if (!(await file.exists()))
88
+ continue;
89
+ lockfileHashes[dir] = await hashFile(filePath);
105
90
  break;
106
91
  }
107
92
  catch {
@@ -111,16 +96,11 @@ export async function captureState(packageDirs) {
111
96
  }));
112
97
  return { manifests, lockfileHashes };
113
98
  }
114
- /** Restores package.json files from a snapshot's manifest map. */
115
99
  export async function restoreState(entry) {
116
100
  await Promise.all(Object.entries(entry.manifests).map(async ([dir, content]) => {
117
- const manifestPath = path.join(dir, "package.json");
118
- const tmp = manifestPath + ".tmp";
119
- await fs.writeFile(tmp, content, "utf8");
120
- await fs.rename(tmp, manifestPath);
101
+ await writeFileAtomic(path.join(dir, "package.json"), content);
121
102
  }));
122
103
  }
123
- /** Computes a diff of dependency versions between two manifest snapshots. */
124
104
  export function diffManifests(before, after) {
125
105
  const changes = [];
126
106
  for (const [dir, afterJson] of Object.entries(after)) {
@@ -142,12 +122,15 @@ export function diffManifests(before, after) {
142
122
  "optionalDependencies",
143
123
  ];
144
124
  for (const field of fields) {
145
- const before = beforeManifest[field] ?? {};
146
- const after = afterManifest[field] ?? {};
147
- const allNames = new Set([...Object.keys(before), ...Object.keys(after)]);
125
+ const beforeDeps = beforeManifest[field] ?? {};
126
+ const afterDeps = afterManifest[field] ?? {};
127
+ const allNames = new Set([
128
+ ...Object.keys(beforeDeps),
129
+ ...Object.keys(afterDeps),
130
+ ]);
148
131
  for (const name of allNames) {
149
- const fromVer = before[name] ?? "(removed)";
150
- const toVer = after[name] ?? "(removed)";
132
+ const fromVer = beforeDeps[name] ?? "(removed)";
133
+ const toVer = afterDeps[name] ?? "(removed)";
151
134
  if (fromVer !== toVer) {
152
135
  changes.push({ name, from: fromVer, to: toVer });
153
136
  }
@@ -156,3 +139,8 @@ export function diffManifests(before, after) {
156
139
  }
157
140
  return changes;
158
141
  }
142
+ async function hashFile(filePath) {
143
+ const hasher = new Bun.CryptoHasher("sha256");
144
+ hasher.update(await Bun.file(filePath).bytes());
145
+ return hasher.digest("hex");
146
+ }
@@ -1,9 +1,9 @@
1
1
  import path from "node:path";
2
- import process from "node:process";
3
2
  import { readManifest, } from "../../parsers/package-json.js";
4
3
  import { discoverPackageDirs } from "../../workspace/discover.js";
5
4
  import { writeFileAtomic } from "../../utils/io.js";
6
5
  import { stableStringify } from "../../utils/stable-json.js";
6
+ import { writeStderr, writeStdout } from "../../utils/runtime.js";
7
7
  import { scanDirectory } from "./scanner.js";
8
8
  import { matchDependencies, removeUnusedFromManifest } from "./matcher.js";
9
9
  /**
@@ -60,16 +60,15 @@ export async function runUnused(options) {
60
60
  // ─ Apply fix ─────────────────────────────────────────────────────────────
61
61
  if (options.fix && unused.length > 0) {
62
62
  if (options.dryRun) {
63
- process.stderr.write(`[unused] --dry-run: would remove ${unused.length} unused dep(s) from ${packageDir}/package.json\n`);
63
+ writeStderr(`[unused] --dry-run: would remove ${unused.length} unused dep(s) from ${packageDir}/package.json\n`);
64
64
  }
65
65
  else {
66
66
  try {
67
- const { promises: fs } = await import("node:fs");
68
67
  const manifestPath = path.join(packageDir, "package.json");
69
- const originalJson = await fs.readFile(manifestPath, "utf8");
68
+ const originalJson = await Bun.file(manifestPath).text();
70
69
  const updatedJson = removeUnusedFromManifest(originalJson, unused);
71
70
  await writeFileAtomic(manifestPath, updatedJson);
72
- process.stderr.write(`[unused] Removed ${unused.length} unused dep(s) from ${packageDir}/package.json\n`);
71
+ writeStderr(`[unused] Removed ${unused.length} unused dep(s) from ${packageDir}/package.json\n`);
73
72
  }
74
73
  catch (error) {
75
74
  result.errors.push(`Failed to update package.json in ${packageDir}: ${String(error)}`);
@@ -80,11 +79,11 @@ export async function runUnused(options) {
80
79
  result.totalUnused = result.unused.length;
81
80
  result.totalMissing = result.missing.length;
82
81
  // ─ Render output ─────────────────────────────────────────────────────────
83
- process.stdout.write(renderUnusedTable(result) + "\n");
82
+ writeStdout(renderUnusedTable(result) + "\n");
84
83
  // ─ JSON report ───────────────────────────────────────────────────────────
85
84
  if (options.jsonFile) {
86
85
  await writeFileAtomic(options.jsonFile, stableStringify(result, 2) + "\n");
87
- process.stderr.write(`[unused] JSON report written to ${options.jsonFile}\n`);
86
+ writeStderr(`[unused] JSON report written to ${options.jsonFile}\n`);
88
87
  }
89
88
  return result;
90
89
  }
@@ -93,34 +93,31 @@ export async function scanDirectory(dir) {
93
93
  async function walkDirectory(dir, collector) {
94
94
  let entries;
95
95
  try {
96
- entries = await fs.readdir(dir);
96
+ entries = await fs.readdir(dir, { withFileTypes: true });
97
97
  }
98
98
  catch {
99
99
  return;
100
100
  }
101
101
  const tasks = [];
102
- for (const entryName of entries) {
102
+ for (const entry of entries) {
103
+ const entryName = entry.name;
103
104
  if (IGNORED_DIRS.has(entryName))
104
105
  continue;
105
106
  const fullPath = path.join(dir, entryName);
106
- tasks.push(fs
107
- .stat(fullPath)
108
- .then((stat) => {
109
- if (stat.isDirectory()) {
110
- return walkDirectory(fullPath, collector);
111
- }
112
- if (stat.isFile()) {
113
- const ext = path.extname(entryName).toLowerCase();
114
- if (!SOURCE_EXTENSIONS.has(ext))
115
- return;
116
- return fs
117
- .readFile(fullPath, "utf8")
118
- .then((source) => {
119
- for (const name of extractImportsFromSource(source)) {
120
- collector.add(name);
121
- }
122
- })
123
- .catch(() => undefined);
107
+ if (entry.isDirectory()) {
108
+ tasks.push(walkDirectory(fullPath, collector));
109
+ continue;
110
+ }
111
+ if (!entry.isFile())
112
+ continue;
113
+ const ext = path.extname(entryName).toLowerCase();
114
+ if (!SOURCE_EXTENSIONS.has(ext))
115
+ continue;
116
+ tasks.push(Bun.file(fullPath)
117
+ .text()
118
+ .then((source) => {
119
+ for (const name of extractImportsFromSource(source)) {
120
+ collector.add(name);
124
121
  }
125
122
  })
126
123
  .catch(() => undefined));
@@ -1,4 +1,4 @@
1
- import type { CiProfile, DependencyKind, FailOnLevel, GroupBy, LockfileMode, LogLevel, OutputFormat, TargetLevel } from "../types/index.js";
1
+ import type { CiProfile, SelectedPackageManager, DependencyKind, FailOnLevel, GroupBy, LockfileMode, LogLevel, OutputFormat, TargetLevel } from "../types/index.js";
2
2
  export interface FileConfig {
3
3
  target?: TargetLevel;
4
4
  filter?: string;
@@ -39,7 +39,7 @@ export interface FileConfig {
39
39
  showImpact?: boolean;
40
40
  showHomepage?: boolean;
41
41
  install?: boolean;
42
- packageManager?: "auto" | "npm" | "pnpm";
42
+ packageManager?: SelectedPackageManager;
43
43
  sync?: boolean;
44
44
  }
45
45
  export declare function loadConfig(cwd: string): Promise<FileConfig>;
@@ -1,4 +1,3 @@
1
- import { promises as fs } from "node:fs";
2
1
  import path from "node:path";
3
2
  export async function loadConfig(cwd) {
4
3
  const fromRc = await loadRcFile(cwd);
@@ -13,8 +12,7 @@ async function loadRcFile(cwd) {
13
12
  for (const candidate of candidates) {
14
13
  const filePath = path.join(cwd, candidate);
15
14
  try {
16
- const content = await fs.readFile(filePath, "utf8");
17
- return JSON.parse(content);
15
+ return (await Bun.file(filePath).json());
18
16
  }
19
17
  catch {
20
18
  // noop
@@ -25,8 +23,7 @@ async function loadRcFile(cwd) {
25
23
  async function loadPackageConfig(cwd) {
26
24
  const packagePath = path.join(cwd, "package.json");
27
25
  try {
28
- const content = await fs.readFile(packagePath, "utf8");
29
- const parsed = JSON.parse(content);
26
+ const parsed = (await Bun.file(packagePath).json());
30
27
  return parsed.rainyUpdates ?? {};
31
28
  }
32
29
  catch {