@rainy-updates/cli 0.5.7 → 0.6.1

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 (105) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +90 -31
  3. package/dist/bin/cli.js +11 -126
  4. package/dist/bin/dispatch.js +35 -32
  5. package/dist/bin/help.js +79 -2
  6. package/dist/bin/main.d.ts +1 -0
  7. package/dist/bin/main.js +126 -0
  8. package/dist/cache/cache.js +13 -11
  9. package/dist/commands/audit/parser.js +38 -2
  10. package/dist/commands/audit/runner.js +41 -61
  11. package/dist/commands/audit/targets.js +13 -13
  12. package/dist/commands/bisect/oracle.js +31 -11
  13. package/dist/commands/bisect/parser.js +3 -3
  14. package/dist/commands/bisect/runner.js +16 -8
  15. package/dist/commands/changelog/fetcher.js +11 -5
  16. package/dist/commands/dashboard/parser.js +144 -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 +53 -4
  20. package/dist/commands/doctor/runner.js +2 -2
  21. package/dist/commands/ga/parser.js +43 -4
  22. package/dist/commands/ga/runner.js +22 -13
  23. package/dist/commands/health/parser.js +38 -2
  24. package/dist/commands/health/runner.js +5 -1
  25. package/dist/commands/hook/parser.d.ts +2 -0
  26. package/dist/commands/hook/parser.js +40 -0
  27. package/dist/commands/hook/runner.d.ts +2 -0
  28. package/dist/commands/hook/runner.js +174 -0
  29. package/dist/commands/licenses/parser.js +39 -0
  30. package/dist/commands/licenses/runner.js +9 -5
  31. package/dist/commands/resolve/graph/builder.js +5 -1
  32. package/dist/commands/resolve/parser.js +39 -0
  33. package/dist/commands/resolve/runner.js +14 -4
  34. package/dist/commands/review/parser.js +101 -4
  35. package/dist/commands/review/runner.js +31 -5
  36. package/dist/commands/snapshot/parser.js +39 -0
  37. package/dist/commands/snapshot/runner.js +21 -18
  38. package/dist/commands/snapshot/store.d.ts +0 -12
  39. package/dist/commands/snapshot/store.js +26 -38
  40. package/dist/commands/unused/parser.js +39 -0
  41. package/dist/commands/unused/runner.js +10 -8
  42. package/dist/commands/unused/scanner.d.ts +2 -1
  43. package/dist/commands/unused/scanner.js +65 -52
  44. package/dist/config/loader.d.ts +2 -2
  45. package/dist/config/loader.js +2 -5
  46. package/dist/config/policy.js +20 -11
  47. package/dist/core/analysis/run-silenced.js +0 -1
  48. package/dist/core/artifacts.js +6 -5
  49. package/dist/core/baseline.js +3 -5
  50. package/dist/core/check.js +7 -3
  51. package/dist/core/ci.js +52 -1
  52. package/dist/core/decision-plan.d.ts +14 -0
  53. package/dist/core/decision-plan.js +107 -0
  54. package/dist/core/doctor/result.js +8 -5
  55. package/dist/core/fix-pr-batch.js +38 -28
  56. package/dist/core/fix-pr.js +27 -24
  57. package/dist/core/init-ci.js +34 -28
  58. package/dist/core/options.d.ts +4 -1
  59. package/dist/core/options.js +152 -4
  60. package/dist/core/review-model.js +3 -0
  61. package/dist/core/summary.js +6 -0
  62. package/dist/core/upgrade.js +64 -2
  63. package/dist/core/verification.d.ts +2 -0
  64. package/dist/core/verification.js +108 -0
  65. package/dist/core/warm-cache.js +7 -3
  66. package/dist/generated/version.d.ts +1 -0
  67. package/dist/generated/version.js +2 -0
  68. package/dist/git/scope.d.ts +19 -0
  69. package/dist/git/scope.js +167 -0
  70. package/dist/index.d.ts +2 -1
  71. package/dist/index.js +1 -0
  72. package/dist/output/format.js +15 -0
  73. package/dist/output/github.js +6 -0
  74. package/dist/output/sarif.js +12 -18
  75. package/dist/parsers/package-json.js +2 -4
  76. package/dist/pm/detect.d.ts +40 -1
  77. package/dist/pm/detect.js +152 -9
  78. package/dist/pm/install.d.ts +3 -1
  79. package/dist/pm/install.js +18 -17
  80. package/dist/registry/npm.js +34 -76
  81. package/dist/rup +0 -0
  82. package/dist/types/index.d.ts +134 -5
  83. package/dist/ui/tui.d.ts +4 -1
  84. package/dist/ui/tui.js +156 -67
  85. package/dist/utils/io.js +5 -6
  86. package/dist/utils/lockfile.js +24 -19
  87. package/dist/utils/runtime-paths.d.ts +4 -0
  88. package/dist/utils/runtime-paths.js +35 -0
  89. package/dist/utils/runtime.d.ts +7 -0
  90. package/dist/utils/runtime.js +32 -0
  91. package/dist/workspace/discover.d.ts +7 -1
  92. package/dist/workspace/discover.js +67 -54
  93. package/package.json +24 -19
  94. package/dist/ui/dashboard/DashboardTUI.d.ts +0 -6
  95. package/dist/ui/dashboard/DashboardTUI.js +0 -34
  96. package/dist/ui/dashboard/components/DetailPanel.d.ts +0 -4
  97. package/dist/ui/dashboard/components/DetailPanel.js +0 -30
  98. package/dist/ui/dashboard/components/Footer.d.ts +0 -4
  99. package/dist/ui/dashboard/components/Footer.js +0 -9
  100. package/dist/ui/dashboard/components/Header.d.ts +0 -4
  101. package/dist/ui/dashboard/components/Header.js +0 -12
  102. package/dist/ui/dashboard/components/Sidebar.d.ts +0 -4
  103. package/dist/ui/dashboard/components/Sidebar.js +0 -23
  104. package/dist/ui/dashboard/store.d.ts +0 -34
  105. package/dist/ui/dashboard/store.js +0 -148
@@ -2,6 +2,11 @@ export function parseLicensesArgs(args) {
2
2
  const options = {
3
3
  cwd: process.cwd(),
4
4
  workspace: false,
5
+ affected: false,
6
+ staged: false,
7
+ baseRef: undefined,
8
+ headRef: undefined,
9
+ sinceRef: undefined,
5
10
  allow: undefined,
6
11
  deny: undefined,
7
12
  sbomFile: undefined,
@@ -25,6 +30,35 @@ export function parseLicensesArgs(args) {
25
30
  options.workspace = true;
26
31
  continue;
27
32
  }
33
+ if (current === "--affected") {
34
+ options.affected = true;
35
+ continue;
36
+ }
37
+ if (current === "--staged") {
38
+ options.staged = true;
39
+ continue;
40
+ }
41
+ if (current === "--base" && next) {
42
+ options.baseRef = next;
43
+ i++;
44
+ continue;
45
+ }
46
+ if (current === "--base")
47
+ throw new Error("Missing value for --base");
48
+ if (current === "--head" && next) {
49
+ options.headRef = next;
50
+ i++;
51
+ continue;
52
+ }
53
+ if (current === "--head")
54
+ throw new Error("Missing value for --head");
55
+ if (current === "--since" && next) {
56
+ options.sinceRef = next;
57
+ i++;
58
+ continue;
59
+ }
60
+ if (current === "--since")
61
+ throw new Error("Missing value for --since");
28
62
  if (current === "--diff") {
29
63
  options.diffMode = true;
30
64
  continue;
@@ -105,6 +139,11 @@ Options:
105
139
  --json-file <path> Write JSON report to file
106
140
  --diff Show only packages with a different license than last scan
107
141
  --workspace Scan all workspace packages
142
+ --affected Scan changed workspace packages and dependents
143
+ --staged Limit scanning to staged changes
144
+ --base <ref> Compare changes against a base git ref
145
+ --head <ref> Compare changes against a head git ref
146
+ --since <ref> Compare changes since a git ref
108
147
  --timeout <ms> Registry request timeout (default: 10000)
109
148
  --concurrency <n> Parallel registry requests (default: 12)
110
149
  --cwd <path> Working directory (default: cwd)
@@ -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.
@@ -20,7 +20,11 @@ export async function runLicenses(options) {
20
20
  errors: [],
21
21
  warnings: [],
22
22
  };
23
- const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
23
+ const packageDirs = await discoverPackageDirs(options.cwd, options.workspace, {
24
+ git: options,
25
+ includeKinds: ["dependencies", "devDependencies", "optionalDependencies"],
26
+ includeDependents: options.affected === true,
27
+ });
24
28
  const allDeps = new Map(); // name → resolved version
25
29
  for (const packageDir of packageDirs) {
26
30
  let manifest;
@@ -63,17 +67,17 @@ export async function runLicenses(options) {
63
67
  }
64
68
  result.totalViolations = result.violations.length;
65
69
  // Render
66
- process.stdout.write(renderLicenseTable(result) + "\n");
70
+ writeStdout(renderLicenseTable(result) + "\n");
67
71
  // SBOM output
68
72
  if (options.sbomFile) {
69
73
  const sbom = generateSbom(result.packages, options.cwd);
70
74
  await writeFileAtomic(options.sbomFile, stableStringify(sbom, 2) + "\n");
71
- process.stderr.write(`[licenses] SBOM written to ${options.sbomFile}\n`);
75
+ writeStderr(`[licenses] SBOM written to ${options.sbomFile}\n`);
72
76
  }
73
77
  // JSON output
74
78
  if (options.jsonFile) {
75
79
  await writeFileAtomic(options.jsonFile, stableStringify(result, 2) + "\n");
76
- process.stderr.write(`[licenses] JSON report written to ${options.jsonFile}\n`);
80
+ writeStderr(`[licenses] JSON report written to ${options.jsonFile}\n`);
77
81
  }
78
82
  return result;
79
83
  }
@@ -22,7 +22,11 @@ export async function buildPeerGraph(options,
22
22
  * to inject proposed upgrade versions before writing them to disk).
23
23
  */
24
24
  resolvedVersionOverrides) {
25
- const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
25
+ const packageDirs = await discoverPackageDirs(options.cwd, options.workspace, {
26
+ git: options,
27
+ includeKinds: ["dependencies", "devDependencies", "optionalDependencies"],
28
+ includeDependents: options.affected === true,
29
+ });
26
30
  const cache = await VersionCache.create();
27
31
  const registry = new NpmRegistryClient(options.cwd, {
28
32
  timeoutMs: options.registryTimeoutMs,
@@ -2,6 +2,11 @@ export function parseResolveArgs(args) {
2
2
  const options = {
3
3
  cwd: process.cwd(),
4
4
  workspace: false,
5
+ affected: false,
6
+ staged: false,
7
+ baseRef: undefined,
8
+ headRef: undefined,
9
+ sinceRef: undefined,
5
10
  afterUpdate: false,
6
11
  safe: false,
7
12
  jsonFile: undefined,
@@ -23,6 +28,35 @@ export function parseResolveArgs(args) {
23
28
  options.workspace = true;
24
29
  continue;
25
30
  }
31
+ if (current === "--affected") {
32
+ options.affected = true;
33
+ continue;
34
+ }
35
+ if (current === "--staged") {
36
+ options.staged = true;
37
+ continue;
38
+ }
39
+ if (current === "--base" && next) {
40
+ options.baseRef = next;
41
+ i++;
42
+ continue;
43
+ }
44
+ if (current === "--base")
45
+ throw new Error("Missing value for --base");
46
+ if (current === "--head" && next) {
47
+ options.headRef = next;
48
+ i++;
49
+ continue;
50
+ }
51
+ if (current === "--head")
52
+ throw new Error("Missing value for --head");
53
+ if (current === "--since" && next) {
54
+ options.sinceRef = next;
55
+ i++;
56
+ continue;
57
+ }
58
+ if (current === "--since")
59
+ throw new Error("Missing value for --since");
26
60
  if (current === "--after-update") {
27
61
  options.afterUpdate = true;
28
62
  continue;
@@ -77,6 +111,11 @@ Options:
77
111
  --after-update Simulate conflicts after applying pending \`rup check\` updates
78
112
  --safe Exit non-zero if any error-level conflicts exist
79
113
  --workspace Scan all workspace packages
114
+ --affected Scan changed workspace packages and their dependents
115
+ --staged Limit scanning to staged changes
116
+ --base <ref> Compare changes against a base git ref
117
+ --head <ref> Compare changes against a head git ref
118
+ --since <ref> Compare changes since a git ref
80
119
  --json-file <path> Write JSON conflict report to file
81
120
  --timeout <ms> Registry request timeout in ms (default: 10000)
82
121
  --concurrency <n> Parallel registry requests (default: 12)
@@ -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;
@@ -95,11 +95,21 @@ async function fetchProposedVersions(options) {
95
95
  cooldownDays: undefined,
96
96
  prLimit: undefined,
97
97
  onlyChanged: false,
98
+ affected: options.affected,
99
+ staged: options.staged,
100
+ baseRef: options.baseRef,
101
+ headRef: options.headRef,
102
+ sinceRef: options.sinceRef,
98
103
  ciProfile: "minimal",
99
104
  lockfileMode: "preserve",
100
105
  interactive: false,
101
106
  showImpact: false,
102
107
  showHomepage: false,
108
+ decisionPlanFile: undefined,
109
+ verify: "none",
110
+ testCommand: undefined,
111
+ verificationReportFile: undefined,
112
+ ciGate: "check",
103
113
  });
104
114
  for (const update of checkResult.updates ?? []) {
105
115
  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,
@@ -37,6 +37,11 @@ export function parseReviewArgs(args) {
37
37
  cooldownDays: undefined,
38
38
  prLimit: undefined,
39
39
  onlyChanged: false,
40
+ affected: false,
41
+ staged: false,
42
+ baseRef: undefined,
43
+ headRef: undefined,
44
+ sinceRef: undefined,
40
45
  ciProfile: "minimal",
41
46
  lockfileMode: "preserve",
42
47
  interactive: false,
@@ -47,6 +52,12 @@ export function parseReviewArgs(args) {
47
52
  diff: undefined,
48
53
  applySelected: false,
49
54
  showChangelog: false,
55
+ decisionPlanFile: undefined,
56
+ queueFocus: "all",
57
+ verify: "none",
58
+ testCommand: undefined,
59
+ verificationReportFile: undefined,
60
+ ciGate: "check",
50
61
  };
51
62
  for (let i = 0; i < args.length; i += 1) {
52
63
  const current = args[i];
@@ -62,6 +73,39 @@ export function parseReviewArgs(args) {
62
73
  options.workspace = true;
63
74
  continue;
64
75
  }
76
+ if (current === "--only-changed") {
77
+ options.onlyChanged = true;
78
+ continue;
79
+ }
80
+ if (current === "--affected") {
81
+ options.affected = true;
82
+ continue;
83
+ }
84
+ if (current === "--staged") {
85
+ options.staged = true;
86
+ continue;
87
+ }
88
+ if (current === "--base" && next) {
89
+ options.baseRef = next;
90
+ i += 1;
91
+ continue;
92
+ }
93
+ if (current === "--base")
94
+ throw new Error("Missing value for --base");
95
+ if (current === "--head" && next) {
96
+ options.headRef = next;
97
+ i += 1;
98
+ continue;
99
+ }
100
+ if (current === "--head")
101
+ throw new Error("Missing value for --head");
102
+ if (current === "--since" && next) {
103
+ options.sinceRef = next;
104
+ i += 1;
105
+ continue;
106
+ }
107
+ if (current === "--since")
108
+ throw new Error("Missing value for --since");
65
109
  if (current === "--interactive") {
66
110
  options.interactive = true;
67
111
  continue;
@@ -94,6 +138,41 @@ export function parseReviewArgs(args) {
94
138
  options.applySelected = true;
95
139
  continue;
96
140
  }
141
+ if (current === "--plan-file" && next) {
142
+ options.decisionPlanFile = path.resolve(options.cwd, next);
143
+ i += 1;
144
+ continue;
145
+ }
146
+ if (current === "--plan-file")
147
+ throw new Error("Missing value for --plan-file");
148
+ if (current === "--verify" && next) {
149
+ if (next === "none" ||
150
+ next === "install" ||
151
+ next === "test" ||
152
+ next === "install,test") {
153
+ options.verify = next;
154
+ i += 1;
155
+ continue;
156
+ }
157
+ throw new Error("--verify must be none, install, test or install,test");
158
+ }
159
+ if (current === "--verify")
160
+ throw new Error("Missing value for --verify");
161
+ if (current === "--test-command" && next) {
162
+ options.testCommand = next;
163
+ i += 1;
164
+ continue;
165
+ }
166
+ if (current === "--test-command")
167
+ throw new Error("Missing value for --test-command");
168
+ if (current === "--verification-report-file" && next) {
169
+ options.verificationReportFile = path.resolve(options.cwd, next);
170
+ i += 1;
171
+ continue;
172
+ }
173
+ if (current === "--verification-report-file") {
174
+ throw new Error("Missing value for --verification-report-file");
175
+ }
97
176
  if (current === "--show-changelog") {
98
177
  options.showChangelog = true;
99
178
  continue;
@@ -148,13 +227,22 @@ export function parseReviewArgs(args) {
148
227
  throw new Error("Missing value for --registry-retries");
149
228
  }
150
229
  if (current === "--help" || current === "-h") {
151
- process.stdout.write(REVIEW_HELP);
152
- process.exit(0);
230
+ writeStdout(REVIEW_HELP);
231
+ exitProcess(0);
153
232
  }
154
233
  if (current.startsWith("-"))
155
234
  throw new Error(`Unknown review option: ${current}`);
156
235
  throw new Error(`Unexpected review argument: ${current}`);
157
236
  }
237
+ if (options.securityOnly) {
238
+ options.queueFocus = "security";
239
+ }
240
+ else if (options.risk === "critical" || options.risk === "high") {
241
+ options.queueFocus = "risk";
242
+ }
243
+ else if (options.diff === "major") {
244
+ options.queueFocus = "major";
245
+ }
158
246
  return options;
159
247
  }
160
248
  const REVIEW_HELP = `
@@ -169,8 +257,17 @@ Options:
169
257
  --risk <level> Minimum risk: critical, high, medium, low
170
258
  --diff <level> Filter by patch, minor, major, latest
171
259
  --apply-selected Apply all filtered updates after review
260
+ --plan-file <path> Write the selected decision set to a reusable plan file
261
+ --verify <mode> Run post-apply verification: none, install, test, install,test
262
+ --test-command <cmd> Override the command used for test verification
172
263
  --show-changelog Fetch release notes summaries for review output
173
264
  --workspace Scan all workspace packages
265
+ --only-changed Limit analysis to changed packages
266
+ --affected Include changed packages and their dependents
267
+ --staged Limit analysis to staged changes
268
+ --base <ref> Compare changes against a base git ref
269
+ --head <ref> Compare changes against a head git ref
270
+ --since <ref> Compare changes since a git ref
174
271
  --policy-file <path> Load policy overrides
175
272
  --json-file <path> Write JSON review report to file
176
273
  --registry-timeout-ms <n>
@@ -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,
@@ -3,6 +3,11 @@ export function parseSnapshotArgs(args) {
3
3
  const options = {
4
4
  cwd: process.cwd(),
5
5
  workspace: false,
6
+ affected: false,
7
+ staged: false,
8
+ baseRef: undefined,
9
+ headRef: undefined,
10
+ sinceRef: undefined,
6
11
  action: "list",
7
12
  label: undefined,
8
13
  snapshotId: undefined,
@@ -25,6 +30,35 @@ export function parseSnapshotArgs(args) {
25
30
  options.workspace = true;
26
31
  continue;
27
32
  }
33
+ if (current === "--affected") {
34
+ options.affected = true;
35
+ continue;
36
+ }
37
+ if (current === "--staged") {
38
+ options.staged = true;
39
+ continue;
40
+ }
41
+ if (current === "--base" && next) {
42
+ options.baseRef = next;
43
+ i++;
44
+ continue;
45
+ }
46
+ if (current === "--base")
47
+ throw new Error("Missing value for --base");
48
+ if (current === "--head" && next) {
49
+ options.headRef = next;
50
+ i++;
51
+ continue;
52
+ }
53
+ if (current === "--head")
54
+ throw new Error("Missing value for --head");
55
+ if (current === "--since" && next) {
56
+ options.sinceRef = next;
57
+ i++;
58
+ continue;
59
+ }
60
+ if (current === "--since")
61
+ throw new Error("Missing value for --since");
28
62
  if (current === "--label" && next) {
29
63
  options.label = next;
30
64
  i++;
@@ -75,6 +109,11 @@ Options:
75
109
  --label <name> Human-readable label for the snapshot
76
110
  --store <path> Custom snapshot store file (default: .rup-snapshots.json)
77
111
  --workspace Include all workspace packages
112
+ --affected Include changed workspace packages and dependents
113
+ --staged Limit snapshot scope to staged changes
114
+ --base <ref> Compare changes against a base git ref
115
+ --head <ref> Compare changes against a head git ref
116
+ --since <ref> Compare changes since a git ref
78
117
  --cwd <path> Working directory (default: cwd)
79
118
  --help Show this help
80
119
  `.trimStart();
@@ -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
  *
@@ -16,7 +16,10 @@ export async function runSnapshot(options) {
16
16
  errors: [],
17
17
  warnings: [],
18
18
  };
19
- const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
19
+ const packageDirs = await discoverPackageDirs(options.cwd, options.workspace, {
20
+ git: options,
21
+ includeDependents: options.affected === true,
22
+ });
20
23
  const store = new SnapshotStore(options.cwd, options.storeFile);
21
24
  switch (options.action) {
22
25
  // ─ save ──────────────────────────────────────────────────────────────────
@@ -27,7 +30,7 @@ export async function runSnapshot(options) {
27
30
  const entry = await store.saveSnapshot(manifests, lockfileHashes, label);
28
31
  result.snapshotId = entry.id;
29
32
  result.label = entry.label;
30
- process.stdout.write(`✔ Snapshot saved: ${entry.label} (${entry.id})\n`);
33
+ writeStdout(`✔ Snapshot saved: ${entry.label} (${entry.id})\n`);
31
34
  break;
32
35
  }
33
36
  // ─ list ──────────────────────────────────────────────────────────────────
@@ -39,20 +42,20 @@ export async function runSnapshot(options) {
39
42
  createdAt: new Date(e.createdAt).toISOString(),
40
43
  }));
41
44
  if (entries.length === 0) {
42
- process.stdout.write("No snapshots saved yet. Use `rup snapshot save` to create one.\n");
45
+ writeStdout("No snapshots saved yet. Use `rup snapshot save` to create one.\n");
43
46
  }
44
47
  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");
48
+ writeStdout(`\n${entries.length} snapshot(s):\n\n`);
49
+ writeStdout(" " + "ID".padEnd(30) + "Label".padEnd(30) + "Created\n");
50
+ writeStdout(" " + "─".repeat(75) + "\n");
48
51
  for (const e of entries) {
49
- process.stdout.write(" " +
52
+ writeStdout(" " +
50
53
  e.id.padEnd(30) +
51
54
  e.label.padEnd(30) +
52
55
  new Date(e.createdAt).toLocaleString() +
53
56
  "\n");
54
57
  }
55
- process.stdout.write("\n");
58
+ writeStdout("\n");
56
59
  }
57
60
  break;
58
61
  }
@@ -72,8 +75,8 @@ export async function runSnapshot(options) {
72
75
  result.snapshotId = entry.id;
73
76
  result.label = entry.label;
74
77
  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");
78
+ writeStdout(`✔ Restored ${count} package.json file(s) from snapshot "${entry.label}" (${entry.id})\n`);
79
+ writeStdout(" Re-run your package manager install to apply.\n");
77
80
  break;
78
81
  }
79
82
  // ─ diff ──────────────────────────────────────────────────────────────────
@@ -92,23 +95,23 @@ export async function runSnapshot(options) {
92
95
  const changes = diffManifests(entry.manifests, currentManifests);
93
96
  result.diff = changes;
94
97
  if (changes.length === 0) {
95
- process.stdout.write(`✔ No dependency changes since snapshot "${entry.label}"\n`);
98
+ writeStdout(`✔ No dependency changes since snapshot "${entry.label}"\n`);
96
99
  }
97
100
  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");
101
+ writeStdout(`\nDependency changes since snapshot "${entry.label}":\n\n`);
102
+ writeStdout(" " + "Package".padEnd(35) + "Before".padEnd(20) + "After\n");
103
+ writeStdout(" " + "─".repeat(65) + "\n");
101
104
  for (const c of changes) {
102
- process.stdout.write(" " + c.name.padEnd(35) + c.from.padEnd(20) + c.to + "\n");
105
+ writeStdout(" " + c.name.padEnd(35) + c.from.padEnd(20) + c.to + "\n");
103
106
  }
104
- process.stdout.write("\n");
107
+ writeStdout("\n");
105
108
  }
106
109
  break;
107
110
  }
108
111
  }
109
112
  if (result.errors.length > 0) {
110
113
  for (const err of result.errors) {
111
- process.stderr.write(`[snapshot] ✖ ${err}\n`);
114
+ writeStderr(`[snapshot] ✖ ${err}\n`);
112
115
  }
113
116
  }
114
117
  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;