@rainy-updates/cli 0.5.2-rc.2 → 0.5.3

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 (58) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +105 -22
  3. package/dist/bin/cli.js +124 -9
  4. package/dist/cache/cache.d.ts +1 -0
  5. package/dist/cache/cache.js +9 -2
  6. package/dist/commands/audit/runner.js +8 -1
  7. package/dist/commands/audit/sources/index.js +8 -1
  8. package/dist/commands/doctor/parser.d.ts +2 -0
  9. package/dist/commands/doctor/parser.js +92 -0
  10. package/dist/commands/doctor/runner.d.ts +2 -0
  11. package/dist/commands/doctor/runner.js +13 -0
  12. package/dist/commands/resolve/runner.js +3 -0
  13. package/dist/commands/review/parser.d.ts +2 -0
  14. package/dist/commands/review/parser.js +174 -0
  15. package/dist/commands/review/runner.d.ts +2 -0
  16. package/dist/commands/review/runner.js +30 -0
  17. package/dist/config/loader.d.ts +3 -0
  18. package/dist/core/check.js +64 -5
  19. package/dist/core/errors.d.ts +11 -0
  20. package/dist/core/errors.js +6 -0
  21. package/dist/core/options.d.ts +8 -1
  22. package/dist/core/options.js +43 -0
  23. package/dist/core/review-model.d.ts +5 -0
  24. package/dist/core/review-model.js +372 -0
  25. package/dist/core/summary.js +11 -2
  26. package/dist/core/upgrade.d.ts +1 -0
  27. package/dist/core/upgrade.js +27 -21
  28. package/dist/core/warm-cache.js +28 -4
  29. package/dist/index.d.ts +3 -1
  30. package/dist/index.js +2 -0
  31. package/dist/output/format.d.ts +4 -1
  32. package/dist/output/format.js +41 -3
  33. package/dist/output/github.js +6 -1
  34. package/dist/output/sarif.js +14 -0
  35. package/dist/registry/npm.d.ts +22 -0
  36. package/dist/registry/npm.js +33 -4
  37. package/dist/risk/index.d.ts +3 -0
  38. package/dist/risk/index.js +24 -0
  39. package/dist/risk/scorer.d.ts +3 -0
  40. package/dist/risk/scorer.js +114 -0
  41. package/dist/risk/signals/fresh-package.d.ts +3 -0
  42. package/dist/risk/signals/fresh-package.js +22 -0
  43. package/dist/risk/signals/install-scripts.d.ts +3 -0
  44. package/dist/risk/signals/install-scripts.js +10 -0
  45. package/dist/risk/signals/maintainer-churn.d.ts +3 -0
  46. package/dist/risk/signals/maintainer-churn.js +11 -0
  47. package/dist/risk/signals/metadata.d.ts +3 -0
  48. package/dist/risk/signals/metadata.js +18 -0
  49. package/dist/risk/signals/mutable-source.d.ts +3 -0
  50. package/dist/risk/signals/mutable-source.js +24 -0
  51. package/dist/risk/signals/typosquat.d.ts +3 -0
  52. package/dist/risk/signals/typosquat.js +70 -0
  53. package/dist/risk/types.d.ts +15 -0
  54. package/dist/risk/types.js +1 -0
  55. package/dist/types/index.d.ts +85 -0
  56. package/dist/ui/tui.d.ts +0 -4
  57. package/dist/ui/tui.js +103 -21
  58. package/package.json +10 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,113 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [0.5.3] - 2026-03-01
6
+
7
+ GA stabilization and review-centered workflow refinement.
8
+
9
+ ### Added
10
+
11
+ - **Dedicated risk engine layer** under `src/risk/`:
12
+ - formal risk scoring with `riskScore`, `riskLevel`, `riskReasons`, `riskCategories`, and `recommendedAction`,
13
+ - deterministic scoring for:
14
+ - known vulnerabilities,
15
+ - install lifecycle scripts,
16
+ - typosquatting heuristic,
17
+ - newly published packages,
18
+ - suspicious metadata,
19
+ - mutable git/http dependencies,
20
+ - maintainer stability heuristic,
21
+ - peer conflicts,
22
+ - license violations,
23
+ - stale/deprecated health signals,
24
+ - major version jumps.
25
+ - **Benchmark tooling and methodology**:
26
+ - `scripts/generate-benchmark-fixtures.mjs`,
27
+ - `scripts/benchmark.mjs`,
28
+ - generated fixtures under `benchmarks/fixtures/`,
29
+ - benchmark methodology doc: `docs/benchmarks.md`.
30
+ - **New workflow docs**:
31
+ - `docs/command-model.md`
32
+ - `docs/review-workflow.md`
33
+ - `docs/risk-engine.md`
34
+
35
+ ### Changed
36
+
37
+ - `review` is now the explicit product center:
38
+ - `check` detects,
39
+ - `doctor` summarizes,
40
+ - `review` decides,
41
+ - `upgrade` applies.
42
+ - CLI help, README, and docs now reflect the review-centered workflow instead of a flat command surface.
43
+ - Review outputs now carry formal risk engine results instead of ad hoc composite signals.
44
+ - TUI language has been upgraded to decision-oriented semantics:
45
+ - review queue,
46
+ - decision panel,
47
+ - explicit state labels,
48
+ - recommended action per candidate.
49
+ - GitHub annotations, SARIF, and human-readable output now expose formal risk score/action metadata additively.
50
+
51
+ ### Benchmarks
52
+
53
+ - Added package scripts for reproducible benchmark runs:
54
+ - `bench:fixtures`
55
+ - `bench:check`
56
+ - `bench:review`
57
+ - `bench:resolve`
58
+ - `bench:ci`
59
+
60
+ ## [0.5.2] - 2026-03-01
61
+
62
+ ### Added
63
+
64
+ - **New `review` command**: Aggregates pending updates with security, peer-conflict, license, health, and unused-dependency signals for guided dependency review.
65
+ - `--interactive` launches the upgraded Ink review TUI.
66
+ - `--security-only`, `--risk <level>`, and `--diff <level>` filter the review set.
67
+ - `--apply-selected` can apply the filtered/selected updates after review.
68
+ - **New `doctor` command**: Produces a fast dependency verdict for local triage and CI summaries.
69
+ - Verdict classes: `safe`, `review`, `blocked`, `actionable`.
70
+ - `--verdict-only` prints a one-line CI-friendly summary.
71
+ - **Interactive review TUI overhaul**:
72
+ - multi-pane layout with filters, selection state, detail panel, and status bar,
73
+ - risk/security/peer/license context inline per package,
74
+ - explicit selection controls for interactive upgrade review.
75
+ - **Additive output contract metadata**:
76
+ - summary fields: `verdict`, `riskPackages`, `securityPackages`, `peerConflictPackages`, `licenseViolationPackages`, `interactiveSession`,
77
+ - GitHub outputs: `verdict`, `risk_packages`, `security_packages`, `peer_conflict_packages`, `license_violation_packages`,
78
+ - SARIF result properties for impact/risk/advisory/license context.
79
+ - **New display controls**:
80
+ - `--interactive`
81
+ - `--show-impact`
82
+ - `--show-homepage`
83
+ - **RC3 hardening layer on top of GA surfaces**:
84
+ - centralized classified error taxonomy in `src/core/errors.ts`,
85
+ - compatibility coverage for scoped private registries and cache backend fallback,
86
+ - explicit cache fallback reason reporting for SQLite → file cache degradation,
87
+ - dedicated performance scenarios for `check`, `resolve`, and `ci`,
88
+ - new comparison document: `docs/why-rainy-vs-dependabot-renovate.md`.
89
+
90
+ ### Changed
91
+
92
+ - `check` now computes impact scores in the core pipeline and carries homepage metadata when available.
93
+ - `upgrade --interactive` now routes through the guided review flow before applying selected updates.
94
+ - CLI help and package exports now cover the new review/verdict surfaces.
95
+ - `doctor` output now follows the RC3 quick-verdict shape:
96
+ - `State`
97
+ - `PrimaryRisk`
98
+ - `NextAction`
99
+ - `check`, `warm-cache`, and `audit` now emit RC3-style classified warnings/errors for:
100
+ - registry failures,
101
+ - auth failures,
102
+ - advisory-source degradation/outage,
103
+ - cache backend fallback.
104
+
105
+ ### Tests
106
+
107
+ - Added compatibility tests for:
108
+ - scoped `.npmrc` private registry resolution,
109
+ - forced cache backend fallback behavior.
110
+ - Updated audit coverage to assert the new classified advisory degradation warning format.
111
+
5
112
  ## [0.5.2-rc.2] - 2026-02-27
6
113
 
7
114
  ### Added
package/README.md CHANGED
@@ -1,16 +1,65 @@
1
1
  # @rainy-updates/cli
2
2
 
3
- The fastest DevOps-first dependency CLI. Checks, audits, upgrades, bisects, and automates npm/pnpm dependencies in CI.
3
+ Rainy Updates is a deterministic dependency review and upgrade operator for Node monorepos and CI.
4
4
 
5
- `@rainy-updates/cli` is built for teams that need fast dependency intelligence, security auditing, policy-aware upgrades, and automation-ready output for CI/CD and pull request workflows.
5
+ `@rainy-updates/cli` is built for teams that need fast dependency detection, trustworthy review, controlled upgrades, and automation-ready outputs for CI/CD.
6
6
 
7
- ## Why this package
7
+ Comparison:
8
+ [Why Rainy vs Dependabot and Renovate](./docs/why-rainy-vs-dependabot-renovate.md)
9
+
10
+ Command model:
11
+ [Check → Doctor → Review → Upgrade](./docs/command-model.md)
12
+
13
+ Review workflow:
14
+ [Review workflow guide](./docs/review-workflow.md)
15
+
16
+ Risk engine:
17
+ [Risk engine guide](./docs/risk-engine.md)
18
+
19
+ Benchmarks:
20
+ [Benchmark methodology](./docs/benchmarks.md)
21
+
22
+ ## What it is
23
+
24
+ Rainy Updates gives teams one dependency lifecycle:
25
+
26
+ - `check` detects candidate updates.
27
+ - `doctor` summarizes the current situation.
28
+ - `review` decides what should happen.
29
+ - `upgrade` applies the approved change set.
30
+
31
+ Everything else supports that lifecycle: CI orchestration, advisory lookup, peer resolution, licenses, snapshots, baselines, and fix-PR automation.
32
+
33
+ ## Who it is for
34
+
35
+ - Node monorepo teams that want deterministic CI artifacts.
36
+ - Engineers who want to review dependency risk locally before applying changes.
37
+ - Teams that need fewer, better upgrade decisions instead of noisy automated PR churn.
38
+
39
+ ## 60-second workflow
40
+
41
+ ```bash
42
+ # 1) Detect what changed
43
+ npx @rainy-updates/cli check --workspace --show-impact
44
+
45
+ # 2) Summarize what matters
46
+ npx @rainy-updates/cli doctor --workspace
47
+
48
+ # 3) Decide in the review surface
49
+ npx @rainy-updates/cli review --interactive
50
+
51
+ # 4) Apply the approved set
52
+ npx @rainy-updates/cli upgrade --interactive
53
+ ```
54
+
55
+ ## Why teams use it
8
56
 
9
57
  - Detects updates quickly across single-package repos and workspaces.
58
+ - Centralizes security, peer, license, health, and behavioral risk review.
10
59
  - Applies updates safely with configurable targets (`patch`, `minor`, `major`, `latest`).
11
- - Enforces policy rules per package (ignore rules and max upgrade level).
60
+ - Enforces policy rules per package.
12
61
  - Supports offline and cache-warmed execution for deterministic CI runs.
13
- - Produces machine-readable artifacts (JSON, SARIF, GitHub outputs, PR markdown report).
62
+ - Produces machine-readable artifacts: JSON, SARIF, GitHub outputs, and PR reports.
14
63
 
15
64
  ## Install
16
65
 
@@ -49,10 +98,15 @@ npx @rainy-updates/cli ci --workspace --mode strict
49
98
 
50
99
  ## Commands
51
100
 
52
- ### Dependency management
101
+ ### Primary workflow
102
+
103
+ - `check` — detect candidate dependency updates
104
+ - `doctor` — summarize the current dependency situation
105
+ - `review` — decide what to do with security, risk, peer, and policy context
106
+ - `upgrade` — apply the approved change set
107
+
108
+ ### Supporting workflow
53
109
 
54
- - `check` — analyze dependencies and report available updates
55
- - `upgrade` — rewrite dependency ranges in manifests, optionally install lockfile updates
56
110
  - `ci` — run CI-focused dependency automation (warm cache, check/upgrade, policy gates)
57
111
  - `warm-cache` — prefetch package metadata for fast and offline checks
58
112
  - `baseline` — save and compare dependency baseline snapshots
@@ -72,31 +126,35 @@ npx @rainy-updates/cli ci --workspace --mode strict
72
126
  npx @rainy-updates/cli check --format table
73
127
  rup check --format table # if installed
74
128
 
75
- # 2) Strict CI mode (non-zero when updates exist)
76
- npx @rainy-updates/cli check --workspace --ci --format json --json-file .artifacts/updates.json
77
- rup check --workspace --ci --format json --json-file .artifacts/updates.json
129
+ # 2) Summarize the state
130
+ npx @rainy-updates/cli doctor --workspace
131
+ rup doctor --workspace
78
132
 
79
- # 3) CI orchestration with policy gates
133
+ # 3) Review and decide
134
+ npx @rainy-updates/cli review --security-only
135
+ rup review --interactive
136
+
137
+ # 4) Apply upgrades with workspace sync
138
+ npx @rainy-updates/cli upgrade --target latest --workspace --sync --install
139
+ rup upgrade --target latest --workspace --sync --install
140
+
141
+ # 5) CI orchestration with policy gates
80
142
  npx @rainy-updates/cli ci --workspace --mode strict --format github
81
143
  rup ci --workspace --mode strict --format github
82
144
 
83
- # 4) Batch fix branches by scope (enterprise)
145
+ # 6) Batch fix branches by scope (enterprise)
84
146
  npx @rainy-updates/cli ci --workspace --mode enterprise --group-by scope --fix-pr --fix-pr-batch-size 2
85
147
  rup ci --workspace --mode enterprise --group-by scope --fix-pr --fix-pr-batch-size 2
86
148
 
87
- # 5) Apply upgrades with workspace sync
88
- npx @rainy-updates/cli upgrade --target latest --workspace --sync --install
89
- rup upgrade --target latest --workspace --sync --install
90
-
91
- # 6) Warm cache → deterministic offline CI check
149
+ # 7) Warm cache deterministic offline CI check
92
150
  npx @rainy-updates/cli warm-cache --workspace --concurrency 32
93
151
  npx @rainy-updates/cli check --workspace --offline --ci
94
152
 
95
- # 7) Save and compare baseline drift
153
+ # 8) Save and compare baseline drift
96
154
  npx @rainy-updates/cli baseline --save --file .artifacts/deps-baseline.json --workspace
97
155
  npx @rainy-updates/cli baseline --check --file .artifacts/deps-baseline.json --workspace --ci
98
156
 
99
- # 8) Scan for known CVEs ── NEW in v0.5.1
157
+ # 9) Scan for known CVEs
100
158
  npx @rainy-updates/cli audit
101
159
  npx @rainy-updates/cli audit --severity high
102
160
  npx @rainy-updates/cli audit --summary
@@ -106,17 +164,20 @@ rup audit --severity high # if installed
106
164
 
107
165
  `audit` prefers npm/pnpm lockfiles today for exact installed-version inference, and now also reads simple `bun.lock` workspace entries when available. It reports source-health warnings when OSV or GitHub returns only partial coverage.
108
166
 
109
- # 9) Check dependency maintenance health ── NEW in v0.5.1
167
+ # 10) Check dependency maintenance health
110
168
  npx @rainy-updates/cli health
111
169
  npx @rainy-updates/cli health --stale 6m # flag packages with no release in 6 months
112
170
  npx @rainy-updates/cli health --stale 180d # same but in days
113
171
  rup health --stale 6m # if installed
114
172
 
115
- # 10) Find which version introduced a breaking change ── NEW in v0.5.1
173
+ # 11) Find which version introduced a breaking change
116
174
  npx @rainy-updates/cli bisect axios --cmd "bun test"
117
175
  npx @rainy-updates/cli bisect react --range "18.0.0..19.0.0" --cmd "npm test"
118
176
  npx @rainy-updates/cli bisect lodash --cmd "npm run test:unit" --dry-run
119
177
  rup bisect axios --cmd "bun test" # if installed
178
+
179
+ # 12) Focus review on high-risk changes
180
+ rup review --risk high --diff major
120
181
  ```
121
182
 
122
183
  ## What it does in production
@@ -129,6 +190,7 @@ rup bisect axios --cmd "bun test" # if installed
129
190
  - Supports explicit registry retry/timeout tuning (`--registry-retries`, `--registry-timeout-ms`).
130
191
  - Supports stale-cache fallback when registry calls fail.
131
192
  - Supports streamed progress output for long CI runs (`--stream`).
193
+ - Exposes impact/risk metadata and homepage context in update output (`--show-impact`, `--show-homepage`).
132
194
 
133
195
  ### Workspace support
134
196
 
@@ -170,6 +232,13 @@ npx @rainy-updates/cli check --policy-file .rainyupdates-policy.json
170
232
  - `--format table`
171
233
  - `--format minimal`
172
234
 
235
+ Review-centered outputs:
236
+
237
+ - `check` is optimized for detection.
238
+ - `doctor` is optimized for summary.
239
+ - `review` is optimized for decision-making.
240
+ - `upgrade` is optimized for safe application.
241
+
173
242
  ### Automation output
174
243
 
175
244
  - `--format json`
@@ -229,6 +298,9 @@ Schedule:
229
298
  - `--cooldown-days <n>`
230
299
  - `--pr-limit <n>`
231
300
  - `--only-changed`
301
+ - `--interactive`
302
+ - `--show-impact`
303
+ - `--show-homepage`
232
304
  - `--mode minimal|strict|enterprise` (for `ci`)
233
305
  - `--fix-pr-batch-size <n>` (for batched fix branches in `ci`)
234
306
  - `--policy-file <path>`
@@ -251,6 +323,17 @@ Schedule:
251
323
  - `--pm auto|npm|pnpm`
252
324
  - `--sync`
253
325
 
326
+ ### Review-only
327
+
328
+ - `--security-only`
329
+ - `--risk critical|high|medium|low`
330
+ - `--diff patch|minor|major|latest`
331
+ - `--apply-selected`
332
+
333
+ ### Doctor-only
334
+
335
+ - `--verdict-only`
336
+
254
337
  ### Baseline-only
255
338
 
256
339
  - `--save`
package/dist/bin/cli.js CHANGED
@@ -85,7 +85,7 @@ async function main() {
85
85
  process.exitCode = result.totalFlagged > 0 ? 1 : 0;
86
86
  return;
87
87
  }
88
- // ─── v0.5.2 commands ─────────────────────────────────────────────────────
88
+ // ─── v0.5.3 commands ─────────────────────────────────────────────────────
89
89
  if (parsed.command === "unused") {
90
90
  const { runUnused } = await import("../commands/unused/runner.js");
91
91
  const result = await runUnused(parsed.options);
@@ -111,6 +111,39 @@ async function main() {
111
111
  process.exitCode = result.errors.length > 0 ? 1 : 0;
112
112
  return;
113
113
  }
114
+ if (parsed.command === "review") {
115
+ const { runReview } = await import("../commands/review/runner.js");
116
+ const result = await runReview(parsed.options);
117
+ process.exitCode =
118
+ result.summary.verdict === "blocked" ||
119
+ result.summary.verdict === "actionable" ||
120
+ result.summary.verdict === "review"
121
+ ? 1
122
+ : 0;
123
+ return;
124
+ }
125
+ if (parsed.command === "doctor") {
126
+ const { runDoctor } = await import("../commands/doctor/runner.js");
127
+ const result = await runDoctor(parsed.options);
128
+ process.exitCode = result.verdict === "safe" ? 0 : 1;
129
+ return;
130
+ }
131
+ if (parsed.options.interactive &&
132
+ (parsed.command === "check" ||
133
+ parsed.command === "upgrade" ||
134
+ parsed.command === "ci")) {
135
+ const { runReview } = await import("../commands/review/runner.js");
136
+ const result = await runReview({
137
+ ...parsed.options,
138
+ securityOnly: false,
139
+ risk: undefined,
140
+ diff: undefined,
141
+ applySelected: parsed.command === "upgrade",
142
+ });
143
+ process.exitCode =
144
+ result.summary.verdict === "safe" && result.updates.length === 0 ? 0 : 1;
145
+ return;
146
+ }
114
147
  const result = await runCommand(parsed);
115
148
  if (parsed.options.fixPr &&
116
149
  (parsed.command === "check" ||
@@ -148,11 +181,17 @@ async function main() {
148
181
  }
149
182
  result.summary.failReason = resolveFailReason(result.updates, result.errors, parsed.options.failOn, parsed.options.maxUpdates, parsed.options.ci);
150
183
  const renderStartedAt = Date.now();
151
- let rendered = renderResult(result, parsed.options.format);
184
+ let rendered = renderResult(result, parsed.options.format, {
185
+ showImpact: parsed.options.showImpact,
186
+ showHomepage: parsed.options.showHomepage,
187
+ });
152
188
  result.summary.durationMs.render = Math.max(0, Date.now() - renderStartedAt);
153
189
  if (parsed.options.format === "json" ||
154
190
  parsed.options.format === "metrics") {
155
- rendered = renderResult(result, parsed.options.format);
191
+ rendered = renderResult(result, parsed.options.format, {
192
+ showImpact: parsed.options.showImpact,
193
+ showHomepage: parsed.options.showHomepage,
194
+ });
156
195
  }
157
196
  if (parsed.options.onlyChanged &&
158
197
  result.updates.length === 0 &&
@@ -187,7 +226,11 @@ function renderHelp(command) {
187
226
  if (isCommand && command === "check") {
188
227
  return `rainy-updates check [options]
189
228
 
190
- Detect available dependency updates.
229
+ Detect candidate dependency updates. This is the first step in the flow:
230
+ check detects
231
+ doctor summarizes
232
+ review decides
233
+ upgrade applies
191
234
 
192
235
  Options:
193
236
  --workspace
@@ -220,6 +263,9 @@ Options:
220
263
  --cooldown-days <n>
221
264
  --pr-limit <n>
222
265
  --only-changed
266
+ --interactive
267
+ --show-impact
268
+ --show-homepage
223
269
  --lockfile-mode preserve|update|error
224
270
  --log-level error|warn|info|debug
225
271
  --ci`;
@@ -249,7 +295,7 @@ Options:
249
295
  if (isCommand && command === "upgrade") {
250
296
  return `rainy-updates upgrade [options]
251
297
 
252
- Apply dependency updates to package.json manifests.
298
+ Apply an approved change set to package.json manifests.
253
299
 
254
300
  Options:
255
301
  --workspace
@@ -267,6 +313,7 @@ Options:
267
313
  --fix-dry-run
268
314
  --fix-pr-no-checkout
269
315
  --fix-pr-batch-size <n>
316
+ --interactive
270
317
  --lockfile-mode preserve|update|error
271
318
  --no-pr-report
272
319
  --json-file <path>
@@ -275,7 +322,11 @@ Options:
275
322
  if (isCommand && command === "ci") {
276
323
  return `rainy-updates ci [options]
277
324
 
278
- Run CI-oriented dependency automation pipeline.
325
+ Run CI-oriented automation around the same lifecycle:
326
+ check detects
327
+ doctor summarizes
328
+ review decides
329
+ upgrade applies
279
330
 
280
331
  Options:
281
332
  --workspace
@@ -349,13 +400,44 @@ Options:
349
400
  --json-file <path>
350
401
  --concurrency <n>
351
402
  --registry-timeout-ms <n>`;
403
+ }
404
+ if (isCommand && command === "review") {
405
+ return `rainy-updates review [options]
406
+
407
+ Review is the decision center of Rainy Updates.
408
+ Use it to inspect risk, security, peer, license, and policy context before applying changes.
409
+
410
+ Options:
411
+ --workspace
412
+ --interactive
413
+ --security-only
414
+ --risk critical|high|medium|low
415
+ --diff patch|minor|major|latest
416
+ --apply-selected
417
+ --policy-file <path>
418
+ --json-file <path>
419
+ --concurrency <n>
420
+ --registry-timeout-ms <n>
421
+ --registry-retries <n>`;
422
+ }
423
+ if (isCommand && command === "doctor") {
424
+ return `rainy-updates doctor [options]
425
+
426
+ Produce a fast summary verdict and point the operator to review when action is needed.
427
+
428
+ Options:
429
+ --workspace
430
+ --verdict-only
431
+ --json-file <path>`;
352
432
  }
353
433
  return `rainy-updates (rup / rainy-up) <command> [options]
354
434
 
355
435
  Commands:
356
- check Detect available updates
357
- upgrade Apply updates to manifests
358
- ci Run CI-focused update pipeline
436
+ check Detect candidate updates
437
+ doctor Summarize what matters
438
+ review Decide what to do
439
+ upgrade Apply the approved change set
440
+ ci Run CI-focused orchestration
359
441
  warm-cache Warm local cache for fast/offline checks
360
442
  init-ci Scaffold GitHub Actions workflow
361
443
  baseline Save/check dependency baseline snapshots
@@ -384,6 +466,9 @@ Global options:
384
466
  --cooldown-days <n>
385
467
  --pr-limit <n>
386
468
  --only-changed
469
+ --interactive
470
+ --show-impact
471
+ --show-homepage
387
472
  --mode minimal|strict|enterprise
388
473
  --fix-pr
389
474
  --fix-branch <name>
@@ -405,6 +490,36 @@ Global options:
405
490
  --version, -v`;
406
491
  }
407
492
  async function runCommand(parsed) {
493
+ if (parsed.command === "review") {
494
+ const { runReview } = await import("../commands/review/runner.js");
495
+ const result = await runReview(parsed.options);
496
+ return {
497
+ projectPath: result.projectPath,
498
+ packagePaths: result.items.map((item) => item.update.packagePath),
499
+ packageManager: "unknown",
500
+ target: result.target,
501
+ timestamp: new Date().toISOString(),
502
+ summary: result.summary,
503
+ updates: result.updates,
504
+ errors: result.errors,
505
+ warnings: result.warnings,
506
+ };
507
+ }
508
+ if (parsed.command === "doctor") {
509
+ const { runDoctor } = await import("../commands/doctor/runner.js");
510
+ const result = await runDoctor(parsed.options);
511
+ return {
512
+ projectPath: result.review.projectPath,
513
+ packagePaths: result.review.items.map((item) => item.update.packagePath),
514
+ packageManager: "unknown",
515
+ target: result.review.target,
516
+ timestamp: new Date().toISOString(),
517
+ summary: result.summary,
518
+ updates: result.review.updates,
519
+ errors: result.review.errors,
520
+ warnings: result.review.warnings,
521
+ };
522
+ }
408
523
  if (parsed.command === "upgrade") {
409
524
  return await upgrade(parsed.options);
410
525
  }
@@ -3,6 +3,7 @@ export declare class VersionCache {
3
3
  private readonly store;
4
4
  readonly backend: "sqlite" | "file";
5
5
  readonly degraded: boolean;
6
+ readonly fallbackReason?: string;
6
7
  private constructor();
7
8
  static create(customPath?: string): Promise<VersionCache>;
8
9
  getValid(packageName: string, target: TargetLevel): Promise<CachedVersion | null>;
@@ -1,6 +1,7 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import process from "node:process";
4
5
  class FileCacheStore {
5
6
  filePath;
6
7
  constructor(filePath) {
@@ -108,20 +109,26 @@ export class VersionCache {
108
109
  store;
109
110
  backend;
110
111
  degraded;
111
- constructor(store, backend, degraded) {
112
+ fallbackReason;
113
+ constructor(store, backend, degraded, fallbackReason) {
112
114
  this.store = store;
113
115
  this.backend = backend;
114
116
  this.degraded = degraded;
117
+ this.fallbackReason = fallbackReason;
115
118
  }
116
119
  static async create(customPath) {
117
120
  const basePath = customPath ?? path.join(os.homedir(), ".cache", "rainy-updates");
121
+ if (process.env.RAINY_UPDATES_CACHE_BACKEND === "file") {
122
+ const jsonPath = path.join(basePath, "cache.json");
123
+ return new VersionCache(new FileCacheStore(jsonPath), "file", true, "forced via RAINY_UPDATES_CACHE_BACKEND=file");
124
+ }
118
125
  const sqlitePath = path.join(basePath, "cache.db");
119
126
  const sqliteStore = await tryCreateSqliteStore(sqlitePath);
120
127
  if (sqliteStore)
121
128
  return new VersionCache(sqliteStore, "sqlite", false);
122
129
  const jsonPath = path.join(basePath, "cache.json");
123
130
  const degraded = typeof Bun !== "undefined";
124
- return new VersionCache(new FileCacheStore(jsonPath), "file", degraded);
131
+ return new VersionCache(new FileCacheStore(jsonPath), "file", degraded, degraded ? "bun:sqlite unavailable; using file cache backend" : undefined);
125
132
  }
126
133
  async getValid(packageName, target) {
127
134
  const entry = await this.store.get(packageName, target);
@@ -8,6 +8,7 @@ import { stableStringify } from "../../utils/stable-json.js";
8
8
  import { fetchAdvisories } from "./fetcher.js";
9
9
  import { resolveAuditTargets } from "./targets.js";
10
10
  import { filterBySeverity, buildPatchMap, renderAuditSourceHealth, renderAuditSummary, renderAuditTable, summarizeAdvisories, } from "./mapper.js";
11
+ import { formatClassifiedMessage } from "../../core/errors.js";
11
12
  /**
12
13
  * Entry point for `rup audit`. Lazy-loaded by cli.ts.
13
14
  * Discovers packages, fetches CVE advisories, filters by severity, and
@@ -63,7 +64,13 @@ export async function runAudit(options) {
63
64
  result.sourceHealth = fetched.sourceHealth;
64
65
  result.warnings.push(...fetched.warnings);
65
66
  if (fetched.sourceHealth.every((item) => item.status === "failed")) {
66
- result.errors.push("All advisory sources failed. Audit coverage is unavailable for this run.");
67
+ result.errors.push(formatClassifiedMessage({
68
+ code: "ADVISORY_SOURCE_DOWN",
69
+ whatFailed: "All advisory sources failed.",
70
+ intact: "Dependency target resolution completed, but no advisory coverage was returned.",
71
+ validity: "invalid",
72
+ next: "Retry `rup audit` later or select a single healthy source with --source.",
73
+ }));
67
74
  }
68
75
  let advisories = fetched.advisories;
69
76
  advisories = filterBySeverity(advisories, options.severity);
@@ -1,6 +1,7 @@
1
1
  import { compareVersions, parseVersion } from "../../../utils/semver.js";
2
2
  import { githubAuditSource } from "./github.js";
3
3
  import { osvAuditSource } from "./osv.js";
4
+ import { formatClassifiedMessage } from "../../../core/errors.js";
4
5
  const SOURCE_MAP = {
5
6
  osv: osvAuditSource,
6
7
  github: githubAuditSource,
@@ -62,7 +63,13 @@ function normalizeSourceWarnings(warnings, sourceHealth) {
62
63
  const successfulNames = successful
63
64
  .map((item) => formatSourceName(item.source))
64
65
  .join(", ");
65
- normalized.push(`Continuing with partial advisory coverage: ${failedNames} failed, ${successfulNames} still returned results.`);
66
+ normalized.push(formatClassifiedMessage({
67
+ code: "ADVISORY_SOURCE_DEGRADED",
68
+ whatFailed: `${failedNames} advisory source(s) failed during the audit query.`,
69
+ intact: `${successfulNames} still returned advisory results.`,
70
+ validity: "partial",
71
+ next: "Retry `rup audit` later or pin `--source` to a healthy backend.",
72
+ }));
66
73
  }
67
74
  return normalized;
68
75
  }
@@ -0,0 +1,2 @@
1
+ import type { DoctorOptions } from "../../types/index.js";
2
+ export declare function parseDoctorArgs(args: string[]): DoctorOptions;