@rainy-updates/cli 0.5.2-rc.1 → 0.5.2
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.
- package/CHANGELOG.md +77 -0
- package/README.md +34 -1
- package/dist/bin/cli.js +128 -3
- package/dist/cache/cache.d.ts +1 -0
- package/dist/cache/cache.js +9 -2
- package/dist/commands/audit/fetcher.d.ts +2 -6
- package/dist/commands/audit/fetcher.js +2 -79
- package/dist/commands/audit/mapper.d.ts +8 -1
- package/dist/commands/audit/mapper.js +105 -9
- package/dist/commands/audit/parser.js +36 -2
- package/dist/commands/audit/runner.js +186 -15
- package/dist/commands/audit/sources/github.d.ts +2 -0
- package/dist/commands/audit/sources/github.js +125 -0
- package/dist/commands/audit/sources/index.d.ts +6 -0
- package/dist/commands/audit/sources/index.js +99 -0
- package/dist/commands/audit/sources/osv.d.ts +2 -0
- package/dist/commands/audit/sources/osv.js +131 -0
- package/dist/commands/audit/sources/types.d.ts +21 -0
- package/dist/commands/audit/sources/types.js +1 -0
- package/dist/commands/audit/targets.d.ts +20 -0
- package/dist/commands/audit/targets.js +314 -0
- package/dist/commands/changelog/fetcher.d.ts +9 -0
- package/dist/commands/changelog/fetcher.js +130 -0
- package/dist/commands/doctor/parser.d.ts +2 -0
- package/dist/commands/doctor/parser.js +92 -0
- package/dist/commands/doctor/runner.d.ts +2 -0
- package/dist/commands/doctor/runner.js +13 -0
- package/dist/commands/resolve/runner.js +3 -0
- package/dist/commands/review/parser.d.ts +2 -0
- package/dist/commands/review/parser.js +174 -0
- package/dist/commands/review/runner.d.ts +2 -0
- package/dist/commands/review/runner.js +30 -0
- package/dist/config/loader.d.ts +3 -0
- package/dist/core/check.js +39 -5
- package/dist/core/errors.d.ts +11 -0
- package/dist/core/errors.js +6 -0
- package/dist/core/options.d.ts +8 -1
- package/dist/core/options.js +43 -0
- package/dist/core/review-model.d.ts +5 -0
- package/dist/core/review-model.js +382 -0
- package/dist/core/summary.js +11 -2
- package/dist/core/upgrade.d.ts +1 -0
- package/dist/core/upgrade.js +27 -21
- package/dist/core/warm-cache.js +28 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/output/format.d.ts +4 -1
- package/dist/output/format.js +29 -3
- package/dist/output/github.js +5 -0
- package/dist/output/sarif.js +11 -0
- package/dist/registry/npm.d.ts +20 -0
- package/dist/registry/npm.js +27 -4
- package/dist/types/index.d.ts +91 -1
- package/dist/ui/tui.d.ts +2 -0
- package/dist/ui/tui.js +107 -0
- package/package.json +12 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,83 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.5.2] - 2026-03-01
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **New `review` command**: Aggregates pending updates with security, peer-conflict, license, health, and unused-dependency signals for guided dependency review.
|
|
10
|
+
- `--interactive` launches the upgraded Ink review TUI.
|
|
11
|
+
- `--security-only`, `--risk <level>`, and `--diff <level>` filter the review set.
|
|
12
|
+
- `--apply-selected` can apply the filtered/selected updates after review.
|
|
13
|
+
- **New `doctor` command**: Produces a fast dependency verdict for local triage and CI summaries.
|
|
14
|
+
- Verdict classes: `safe`, `review`, `blocked`, `actionable`.
|
|
15
|
+
- `--verdict-only` prints a one-line CI-friendly summary.
|
|
16
|
+
- **Interactive review TUI overhaul**:
|
|
17
|
+
- multi-pane layout with filters, selection state, detail panel, and status bar,
|
|
18
|
+
- risk/security/peer/license context inline per package,
|
|
19
|
+
- explicit selection controls for interactive upgrade review.
|
|
20
|
+
- **Additive output contract metadata**:
|
|
21
|
+
- summary fields: `verdict`, `riskPackages`, `securityPackages`, `peerConflictPackages`, `licenseViolationPackages`, `interactiveSession`,
|
|
22
|
+
- GitHub outputs: `verdict`, `risk_packages`, `security_packages`, `peer_conflict_packages`, `license_violation_packages`,
|
|
23
|
+
- SARIF result properties for impact/risk/advisory/license context.
|
|
24
|
+
- **New display controls**:
|
|
25
|
+
- `--interactive`
|
|
26
|
+
- `--show-impact`
|
|
27
|
+
- `--show-homepage`
|
|
28
|
+
- **RC3 hardening layer on top of GA surfaces**:
|
|
29
|
+
- centralized classified error taxonomy in `src/core/errors.ts`,
|
|
30
|
+
- compatibility coverage for scoped private registries and cache backend fallback,
|
|
31
|
+
- explicit cache fallback reason reporting for SQLite → file cache degradation,
|
|
32
|
+
- dedicated performance scenarios for `check`, `resolve`, and `ci`,
|
|
33
|
+
- new comparison document: `docs/why-rainy-vs-dependabot-renovate.md`.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- `check` now computes impact scores in the core pipeline and carries homepage metadata when available.
|
|
38
|
+
- `upgrade --interactive` now routes through the guided review flow before applying selected updates.
|
|
39
|
+
- CLI help and package exports now cover the new review/verdict surfaces.
|
|
40
|
+
- `doctor` output now follows the RC3 quick-verdict shape:
|
|
41
|
+
- `State`
|
|
42
|
+
- `PrimaryRisk`
|
|
43
|
+
- `NextAction`
|
|
44
|
+
- `check`, `warm-cache`, and `audit` now emit RC3-style classified warnings/errors for:
|
|
45
|
+
- registry failures,
|
|
46
|
+
- auth failures,
|
|
47
|
+
- advisory-source degradation/outage,
|
|
48
|
+
- cache backend fallback.
|
|
49
|
+
|
|
50
|
+
### Tests
|
|
51
|
+
|
|
52
|
+
- Added compatibility tests for:
|
|
53
|
+
- scoped `.npmrc` private registry resolution,
|
|
54
|
+
- forced cache backend fallback behavior.
|
|
55
|
+
- Updated audit coverage to assert the new classified advisory degradation warning format.
|
|
56
|
+
|
|
57
|
+
## [0.5.2-rc.2] - 2026-02-27
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- **Audit RC2 overhaul**:
|
|
62
|
+
- `audit --summary` / `audit --report summary` groups noisy advisory lists into affected-package summaries.
|
|
63
|
+
- `audit --source auto|osv|github|all` adds multi-source security lookups, with `auto` querying **OSV.dev + GitHub Advisory Database** and merging results.
|
|
64
|
+
- Lockfile-backed version inference for `package-lock.json`, `npm-shrinkwrap.json`, `pnpm-lock.yaml`, and basic `bun.lock` workspace entries resolves real installed versions for complex ranges.
|
|
65
|
+
- JSON audit output now includes package summaries, source metadata, and resolution statistics.
|
|
66
|
+
- Source-health reporting now distinguishes `ok`, `partial`, and `failed` advisory backends so partial coverage is explicit instead of silent.
|
|
67
|
+
- **Interactive TUI Engine**: An `ink`-based Terminal User Interface for interactive dependency updates, featuring semantic diff coloring and keyboard navigation (`src/ui/tui.tsx`).
|
|
68
|
+
- **Changelog Fetcher**: Implemented `changelog/fetcher.ts` to retrieve release notes dynamically from GitHub API.
|
|
69
|
+
- Utilizes `bun:sqlite` backed `VersionCache` to prevent API rate limit (403) errors.
|
|
70
|
+
- Strictly lazy-loaded to preserve zero-overhead startup time.
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
|
|
74
|
+
- Audit patch planning now chooses the lowest safe patched version that clears all detected vulnerable ranges, avoiding unnecessary major jumps during `audit --fix`.
|
|
75
|
+
- Audit findings now record the current installed version and contributing advisory sources per finding.
|
|
76
|
+
- Audit now warns when one advisory source degrades and fails the run when all selected advisory sources are unavailable.
|
|
77
|
+
- Audit terminal output now shows advisory-source health directly in table and summary modes, so degraded coverage is visible without reading JSON.
|
|
78
|
+
- Resolved TypeScript JSX compiler errors by properly exposing `"jsx": "react-jsx"` in `tsconfig.json`.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
5
82
|
## [0.5.2] - 2026-02-27
|
|
6
83
|
|
|
7
84
|
### Added
|
package/README.md
CHANGED
|
@@ -4,6 +4,9 @@ The fastest DevOps-first dependency CLI. Checks, audits, upgrades, bisects, and
|
|
|
4
4
|
|
|
5
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.
|
|
6
6
|
|
|
7
|
+
Comparison:
|
|
8
|
+
[Why Rainy vs Dependabot and Renovate](./docs/why-rainy-vs-dependabot-renovate.md)
|
|
9
|
+
|
|
7
10
|
## Why this package
|
|
8
11
|
|
|
9
12
|
- Detects updates quickly across single-package repos and workspaces.
|
|
@@ -56,10 +59,12 @@ npx @rainy-updates/cli ci --workspace --mode strict
|
|
|
56
59
|
- `ci` — run CI-focused dependency automation (warm cache, check/upgrade, policy gates)
|
|
57
60
|
- `warm-cache` — prefetch package metadata for fast and offline checks
|
|
58
61
|
- `baseline` — save and compare dependency baseline snapshots
|
|
62
|
+
- `review` — guided review across updates, security, peer conflicts, licenses, and risk
|
|
63
|
+
- `doctor` — fast verdict command for local triage and CI summaries
|
|
59
64
|
|
|
60
65
|
### Security & health (_new in v0.5.1_)
|
|
61
66
|
|
|
62
|
-
- `audit` — scan dependencies for CVEs using [OSV.dev](https://osv.dev)
|
|
67
|
+
- `audit` — scan dependencies for CVEs using [OSV.dev](https://osv.dev) plus GitHub Advisory Database, with lockfile-aware version inference
|
|
63
68
|
- `health` — detect stale, deprecated, and unmaintained packages before they become liabilities
|
|
64
69
|
- `bisect` — binary-search across semver versions to find the exact version that broke your tests
|
|
65
70
|
|
|
@@ -99,9 +104,13 @@ npx @rainy-updates/cli baseline --check --file .artifacts/deps-baseline.json --w
|
|
|
99
104
|
# 8) Scan for known CVEs ── NEW in v0.5.1
|
|
100
105
|
npx @rainy-updates/cli audit
|
|
101
106
|
npx @rainy-updates/cli audit --severity high
|
|
107
|
+
npx @rainy-updates/cli audit --summary
|
|
108
|
+
npx @rainy-updates/cli audit --source osv
|
|
102
109
|
npx @rainy-updates/cli audit --fix # prints the patching npm install command
|
|
103
110
|
rup audit --severity high # if installed
|
|
104
111
|
|
|
112
|
+
`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.
|
|
113
|
+
|
|
105
114
|
# 9) Check dependency maintenance health ── NEW in v0.5.1
|
|
106
115
|
npx @rainy-updates/cli health
|
|
107
116
|
npx @rainy-updates/cli health --stale 6m # flag packages with no release in 6 months
|
|
@@ -113,6 +122,15 @@ npx @rainy-updates/cli bisect axios --cmd "bun test"
|
|
|
113
122
|
npx @rainy-updates/cli bisect react --range "18.0.0..19.0.0" --cmd "npm test"
|
|
114
123
|
npx @rainy-updates/cli bisect lodash --cmd "npm run test:unit" --dry-run
|
|
115
124
|
rup bisect axios --cmd "bun test" # if installed
|
|
125
|
+
|
|
126
|
+
# 11) Review updates with risk and security context ── NEW in v0.5.2 GA
|
|
127
|
+
npx @rainy-updates/cli review --security-only
|
|
128
|
+
rup review --interactive
|
|
129
|
+
rup review --risk high --diff major
|
|
130
|
+
|
|
131
|
+
# 12) Get a fast dependency verdict for CI or local triage ── NEW in v0.5.2 GA
|
|
132
|
+
npx @rainy-updates/cli doctor
|
|
133
|
+
rup doctor --verdict-only
|
|
116
134
|
```
|
|
117
135
|
|
|
118
136
|
## What it does in production
|
|
@@ -125,6 +143,7 @@ rup bisect axios --cmd "bun test" # if installed
|
|
|
125
143
|
- Supports explicit registry retry/timeout tuning (`--registry-retries`, `--registry-timeout-ms`).
|
|
126
144
|
- Supports stale-cache fallback when registry calls fail.
|
|
127
145
|
- Supports streamed progress output for long CI runs (`--stream`).
|
|
146
|
+
- Exposes impact/risk metadata and homepage context in update output (`--show-impact`, `--show-homepage`).
|
|
128
147
|
|
|
129
148
|
### Workspace support
|
|
130
149
|
|
|
@@ -225,6 +244,9 @@ Schedule:
|
|
|
225
244
|
- `--cooldown-days <n>`
|
|
226
245
|
- `--pr-limit <n>`
|
|
227
246
|
- `--only-changed`
|
|
247
|
+
- `--interactive`
|
|
248
|
+
- `--show-impact`
|
|
249
|
+
- `--show-homepage`
|
|
228
250
|
- `--mode minimal|strict|enterprise` (for `ci`)
|
|
229
251
|
- `--fix-pr-batch-size <n>` (for batched fix branches in `ci`)
|
|
230
252
|
- `--policy-file <path>`
|
|
@@ -247,6 +269,17 @@ Schedule:
|
|
|
247
269
|
- `--pm auto|npm|pnpm`
|
|
248
270
|
- `--sync`
|
|
249
271
|
|
|
272
|
+
### Review-only
|
|
273
|
+
|
|
274
|
+
- `--security-only`
|
|
275
|
+
- `--risk critical|high|medium|low`
|
|
276
|
+
- `--diff patch|minor|major|latest`
|
|
277
|
+
- `--apply-selected`
|
|
278
|
+
|
|
279
|
+
### Doctor-only
|
|
280
|
+
|
|
281
|
+
- `--verdict-only`
|
|
282
|
+
|
|
250
283
|
### Baseline-only
|
|
251
284
|
|
|
252
285
|
- `--save`
|
package/dist/bin/cli.js
CHANGED
|
@@ -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 &&
|
|
@@ -220,6 +259,9 @@ Options:
|
|
|
220
259
|
--cooldown-days <n>
|
|
221
260
|
--pr-limit <n>
|
|
222
261
|
--only-changed
|
|
262
|
+
--interactive
|
|
263
|
+
--show-impact
|
|
264
|
+
--show-homepage
|
|
223
265
|
--lockfile-mode preserve|update|error
|
|
224
266
|
--log-level error|warn|info|debug
|
|
225
267
|
--ci`;
|
|
@@ -267,6 +309,7 @@ Options:
|
|
|
267
309
|
--fix-dry-run
|
|
268
310
|
--fix-pr-no-checkout
|
|
269
311
|
--fix-pr-batch-size <n>
|
|
312
|
+
--interactive
|
|
270
313
|
--lockfile-mode preserve|update|error
|
|
271
314
|
--no-pr-report
|
|
272
315
|
--json-file <path>
|
|
@@ -330,6 +373,53 @@ Options:
|
|
|
330
373
|
--workspace
|
|
331
374
|
--dep-kinds deps,dev,optional,peer
|
|
332
375
|
--ci`;
|
|
376
|
+
}
|
|
377
|
+
if (isCommand && command === "audit") {
|
|
378
|
+
return `rainy-updates audit [options]
|
|
379
|
+
|
|
380
|
+
Scan dependencies for CVEs using OSV.dev and GitHub Advisory Database.
|
|
381
|
+
|
|
382
|
+
Options:
|
|
383
|
+
--workspace
|
|
384
|
+
--severity critical|high|medium|low
|
|
385
|
+
--summary
|
|
386
|
+
--report table|summary|json
|
|
387
|
+
--source auto|osv|github|all
|
|
388
|
+
--fix
|
|
389
|
+
--dry-run
|
|
390
|
+
--commit
|
|
391
|
+
--pm auto|npm|pnpm|bun|yarn
|
|
392
|
+
--json-file <path>
|
|
393
|
+
--concurrency <n>
|
|
394
|
+
--registry-timeout-ms <n>`;
|
|
395
|
+
}
|
|
396
|
+
if (isCommand && command === "review") {
|
|
397
|
+
return `rainy-updates review [options]
|
|
398
|
+
|
|
399
|
+
Review updates with risk, security, peer, and policy context.
|
|
400
|
+
|
|
401
|
+
Options:
|
|
402
|
+
--workspace
|
|
403
|
+
--interactive
|
|
404
|
+
--security-only
|
|
405
|
+
--risk critical|high|medium|low
|
|
406
|
+
--diff patch|minor|major|latest
|
|
407
|
+
--apply-selected
|
|
408
|
+
--policy-file <path>
|
|
409
|
+
--json-file <path>
|
|
410
|
+
--concurrency <n>
|
|
411
|
+
--registry-timeout-ms <n>
|
|
412
|
+
--registry-retries <n>`;
|
|
413
|
+
}
|
|
414
|
+
if (isCommand && command === "doctor") {
|
|
415
|
+
return `rainy-updates doctor [options]
|
|
416
|
+
|
|
417
|
+
Produce a fast overall dependency verdict.
|
|
418
|
+
|
|
419
|
+
Options:
|
|
420
|
+
--workspace
|
|
421
|
+
--verdict-only
|
|
422
|
+
--json-file <path>`;
|
|
333
423
|
}
|
|
334
424
|
return `rainy-updates (rup / rainy-up) <command> [options]
|
|
335
425
|
|
|
@@ -340,13 +430,15 @@ Commands:
|
|
|
340
430
|
warm-cache Warm local cache for fast/offline checks
|
|
341
431
|
init-ci Scaffold GitHub Actions workflow
|
|
342
432
|
baseline Save/check dependency baseline snapshots
|
|
343
|
-
audit Scan dependencies for CVEs (OSV.dev)
|
|
433
|
+
audit Scan dependencies for CVEs (OSV.dev + GitHub)
|
|
344
434
|
health Detect stale/deprecated/unmaintained packages
|
|
345
435
|
bisect Find which version of a dep introduced a failure
|
|
346
436
|
unused Detect unused or missing npm dependencies
|
|
347
437
|
resolve Check peer dependency conflicts (pure-TS, no subprocess)
|
|
348
438
|
licenses Scan dependency licenses and generate SPDX SBOM
|
|
349
439
|
snapshot Save, list, restore, and diff dependency state snapshots
|
|
440
|
+
review Guided dependency review with risk/security context
|
|
441
|
+
doctor Fast dependency verdict for local or CI use
|
|
350
442
|
|
|
351
443
|
Global options:
|
|
352
444
|
--cwd <path>
|
|
@@ -365,6 +457,9 @@ Global options:
|
|
|
365
457
|
--cooldown-days <n>
|
|
366
458
|
--pr-limit <n>
|
|
367
459
|
--only-changed
|
|
460
|
+
--interactive
|
|
461
|
+
--show-impact
|
|
462
|
+
--show-homepage
|
|
368
463
|
--mode minimal|strict|enterprise
|
|
369
464
|
--fix-pr
|
|
370
465
|
--fix-branch <name>
|
|
@@ -386,6 +481,36 @@ Global options:
|
|
|
386
481
|
--version, -v`;
|
|
387
482
|
}
|
|
388
483
|
async function runCommand(parsed) {
|
|
484
|
+
if (parsed.command === "review") {
|
|
485
|
+
const { runReview } = await import("../commands/review/runner.js");
|
|
486
|
+
const result = await runReview(parsed.options);
|
|
487
|
+
return {
|
|
488
|
+
projectPath: result.projectPath,
|
|
489
|
+
packagePaths: result.items.map((item) => item.update.packagePath),
|
|
490
|
+
packageManager: "unknown",
|
|
491
|
+
target: result.target,
|
|
492
|
+
timestamp: new Date().toISOString(),
|
|
493
|
+
summary: result.summary,
|
|
494
|
+
updates: result.updates,
|
|
495
|
+
errors: result.errors,
|
|
496
|
+
warnings: result.warnings,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
if (parsed.command === "doctor") {
|
|
500
|
+
const { runDoctor } = await import("../commands/doctor/runner.js");
|
|
501
|
+
const result = await runDoctor(parsed.options);
|
|
502
|
+
return {
|
|
503
|
+
projectPath: result.review.projectPath,
|
|
504
|
+
packagePaths: result.review.items.map((item) => item.update.packagePath),
|
|
505
|
+
packageManager: "unknown",
|
|
506
|
+
target: result.review.target,
|
|
507
|
+
timestamp: new Date().toISOString(),
|
|
508
|
+
summary: result.summary,
|
|
509
|
+
updates: result.review.updates,
|
|
510
|
+
errors: result.review.errors,
|
|
511
|
+
warnings: result.review.warnings,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
389
514
|
if (parsed.command === "upgrade") {
|
|
390
515
|
return await upgrade(parsed.options);
|
|
391
516
|
}
|
package/dist/cache/cache.d.ts
CHANGED
|
@@ -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>;
|
package/dist/cache/cache.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
|
@@ -1,6 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Fetches CVE advisories for all given package names in parallel.
|
|
4
|
-
* Uses OSV.dev as primary source.
|
|
5
|
-
*/
|
|
6
|
-
export declare function fetchAdvisories(packageNames: string[], options: Pick<AuditOptions, "concurrency" | "registryTimeoutMs">): Promise<CveAdvisory[]>;
|
|
1
|
+
export { extractAuditVersion } from "./targets.js";
|
|
2
|
+
export { fetchAdvisoriesFromSources as fetchAdvisories } from "./sources/index.js";
|
|
@@ -1,79 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const GITHUB_ADVISORY_API = "https://api.github.com/advisories";
|
|
4
|
-
/**
|
|
5
|
-
* Queries OSV.dev for advisories for a single npm package.
|
|
6
|
-
*/
|
|
7
|
-
async function queryOsv(packageName, timeoutMs) {
|
|
8
|
-
const body = JSON.stringify({
|
|
9
|
-
package: { name: packageName, ecosystem: "npm" },
|
|
10
|
-
});
|
|
11
|
-
let response;
|
|
12
|
-
try {
|
|
13
|
-
const controller = new AbortController();
|
|
14
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
15
|
-
response = await fetch(OSV_API, {
|
|
16
|
-
method: "POST",
|
|
17
|
-
headers: { "Content-Type": "application/json" },
|
|
18
|
-
body,
|
|
19
|
-
signal: controller.signal,
|
|
20
|
-
});
|
|
21
|
-
clearTimeout(timer);
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return [];
|
|
25
|
-
}
|
|
26
|
-
if (!response.ok)
|
|
27
|
-
return [];
|
|
28
|
-
const data = (await response.json());
|
|
29
|
-
const advisories = [];
|
|
30
|
-
for (const vuln of data.vulns ?? []) {
|
|
31
|
-
const cveId = vuln.id ?? "UNKNOWN";
|
|
32
|
-
const rawSeverity = (vuln.database_specific?.severity ?? "medium").toLowerCase();
|
|
33
|
-
const severity = (["critical", "high", "medium", "low"].includes(rawSeverity)
|
|
34
|
-
? rawSeverity
|
|
35
|
-
: "medium");
|
|
36
|
-
let patchedVersion = null;
|
|
37
|
-
let vulnerableRange = "*";
|
|
38
|
-
for (const affected of vuln.affected ?? []) {
|
|
39
|
-
if (affected.package?.name !== packageName)
|
|
40
|
-
continue;
|
|
41
|
-
for (const range of affected.ranges ?? []) {
|
|
42
|
-
const fixedEvent = range.events?.find((e) => e.fixed);
|
|
43
|
-
if (fixedEvent?.fixed) {
|
|
44
|
-
patchedVersion = fixedEvent.fixed;
|
|
45
|
-
const introducedEvent = range.events?.find((e) => e.introduced);
|
|
46
|
-
vulnerableRange = introducedEvent?.introduced
|
|
47
|
-
? `>=${introducedEvent.introduced} <${patchedVersion}`
|
|
48
|
-
: `<${patchedVersion}`;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
advisories.push({
|
|
53
|
-
cveId,
|
|
54
|
-
packageName,
|
|
55
|
-
severity,
|
|
56
|
-
vulnerableRange,
|
|
57
|
-
patchedVersion,
|
|
58
|
-
title: vuln.summary ?? cveId,
|
|
59
|
-
url: vuln.references?.[0]?.url ?? `https://osv.dev/vulnerability/${cveId}`,
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
return advisories;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Fetches CVE advisories for all given package names in parallel.
|
|
66
|
-
* Uses OSV.dev as primary source.
|
|
67
|
-
*/
|
|
68
|
-
export async function fetchAdvisories(packageNames, options) {
|
|
69
|
-
const tasks = packageNames.map((name) => () => queryOsv(name, options.registryTimeoutMs));
|
|
70
|
-
const results = await asyncPool(options.concurrency, tasks);
|
|
71
|
-
const advisories = [];
|
|
72
|
-
for (const r of results) {
|
|
73
|
-
if (!(r instanceof Error)) {
|
|
74
|
-
for (const adv of r)
|
|
75
|
-
advisories.push(adv);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return advisories;
|
|
79
|
-
}
|
|
1
|
+
export { extractAuditVersion } from "./targets.js";
|
|
2
|
+
export { fetchAdvisoriesFromSources as fetchAdvisories } from "./sources/index.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CveAdvisory, AuditSeverity } from "../../types/index.js";
|
|
1
|
+
import type { AuditPackageSummary, AuditSourceStatus, CveAdvisory, AuditSeverity } from "../../types/index.js";
|
|
2
2
|
/**
|
|
3
3
|
* Filters advisories by minimum severity level.
|
|
4
4
|
* e.g. --severity high → keeps critical and high.
|
|
@@ -8,9 +8,16 @@ export declare function filterBySeverity(advisories: CveAdvisory[], minSeverity:
|
|
|
8
8
|
* For each advisory that has a known patchedVersion,
|
|
9
9
|
* produces a sorted, deduplicated map of package → minimum secure version.
|
|
10
10
|
* Used by --fix to determine what version to update to.
|
|
11
|
+
*
|
|
12
|
+
* Uses proper semver numeric comparison — NOT string comparison — so that
|
|
13
|
+
* e.g. "5.19.1" correctly beats "5.5.1" (lexicographically "5.5.1" > "5.19.1"
|
|
14
|
+
* because "5" > "1" at the third character, which is the classic semver trap).
|
|
11
15
|
*/
|
|
12
16
|
export declare function buildPatchMap(advisories: CveAdvisory[]): Map<string, string>;
|
|
17
|
+
export declare function summarizeAdvisories(advisories: CveAdvisory[]): AuditPackageSummary[];
|
|
13
18
|
/**
|
|
14
19
|
* Renders audit advisories as a formatted table string for terminal output.
|
|
15
20
|
*/
|
|
16
21
|
export declare function renderAuditTable(advisories: CveAdvisory[]): string;
|
|
22
|
+
export declare function renderAuditSummary(packages: AuditPackageSummary[]): string;
|
|
23
|
+
export declare function renderAuditSourceHealth(sourceHealth: AuditSourceStatus[]): string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compareVersions, parseVersion, satisfies } from "../../utils/semver.js";
|
|
1
2
|
const SEVERITY_RANK = {
|
|
2
3
|
critical: 4,
|
|
3
4
|
high: 3,
|
|
@@ -18,19 +19,66 @@ export function filterBySeverity(advisories, minSeverity) {
|
|
|
18
19
|
* For each advisory that has a known patchedVersion,
|
|
19
20
|
* produces a sorted, deduplicated map of package → minimum secure version.
|
|
20
21
|
* Used by --fix to determine what version to update to.
|
|
22
|
+
*
|
|
23
|
+
* Uses proper semver numeric comparison — NOT string comparison — so that
|
|
24
|
+
* e.g. "5.19.1" correctly beats "5.5.1" (lexicographically "5.5.1" > "5.19.1"
|
|
25
|
+
* because "5" > "1" at the third character, which is the classic semver trap).
|
|
21
26
|
*/
|
|
22
27
|
export function buildPatchMap(advisories) {
|
|
23
28
|
const patchMap = new Map();
|
|
29
|
+
const byPackage = new Map();
|
|
24
30
|
for (const advisory of advisories) {
|
|
25
|
-
|
|
31
|
+
const items = byPackage.get(advisory.packageName) ?? [];
|
|
32
|
+
items.push(advisory);
|
|
33
|
+
byPackage.set(advisory.packageName, items);
|
|
34
|
+
}
|
|
35
|
+
for (const [packageName, items] of byPackage) {
|
|
36
|
+
const candidates = [...new Set(items.flatMap((item) => item.patchedVersion ? [item.patchedVersion] : []))].sort(compareSemverAsc);
|
|
37
|
+
if (candidates.length === 0)
|
|
26
38
|
continue;
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
patchMap.set(advisory.packageName, advisory.patchedVersion);
|
|
30
|
-
}
|
|
39
|
+
const safeCandidate = candidates.find((candidate) => items.every((item) => !satisfies(candidate, item.vulnerableRange)));
|
|
40
|
+
patchMap.set(packageName, safeCandidate ?? candidates[candidates.length - 1]);
|
|
31
41
|
}
|
|
32
42
|
return patchMap;
|
|
33
43
|
}
|
|
44
|
+
function compareSemverAsc(a, b) {
|
|
45
|
+
const pa = parseVersion(a);
|
|
46
|
+
const pb = parseVersion(b);
|
|
47
|
+
if (pa && pb)
|
|
48
|
+
return compareVersions(pa, pb);
|
|
49
|
+
if (a === b)
|
|
50
|
+
return 0;
|
|
51
|
+
return a < b ? -1 : 1;
|
|
52
|
+
}
|
|
53
|
+
export function summarizeAdvisories(advisories) {
|
|
54
|
+
const byPackage = new Map();
|
|
55
|
+
for (const advisory of advisories) {
|
|
56
|
+
const key = `${advisory.packageName}|${advisory.currentVersion ?? "?"}`;
|
|
57
|
+
const items = byPackage.get(key) ?? [];
|
|
58
|
+
items.push(advisory);
|
|
59
|
+
byPackage.set(key, items);
|
|
60
|
+
}
|
|
61
|
+
const summaries = [];
|
|
62
|
+
for (const [, items] of byPackage) {
|
|
63
|
+
const sorted = [...items].sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]);
|
|
64
|
+
const representative = sorted[0];
|
|
65
|
+
const patchMap = buildPatchMap(items);
|
|
66
|
+
summaries.push({
|
|
67
|
+
packageName: representative.packageName,
|
|
68
|
+
currentVersion: representative.currentVersion,
|
|
69
|
+
severity: representative.severity,
|
|
70
|
+
advisoryCount: items.length,
|
|
71
|
+
patchedVersion: patchMap.get(representative.packageName) ?? null,
|
|
72
|
+
sources: [...new Set(items.flatMap((item) => item.sources))].sort(),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return summaries.sort((a, b) => {
|
|
76
|
+
const severityDiff = SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity];
|
|
77
|
+
if (severityDiff !== 0)
|
|
78
|
+
return severityDiff;
|
|
79
|
+
return a.packageName.localeCompare(b.packageName);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
34
82
|
/**
|
|
35
83
|
* Renders audit advisories as a formatted table string for terminal output.
|
|
36
84
|
*/
|
|
@@ -47,15 +95,63 @@ export function renderAuditTable(advisories) {
|
|
|
47
95
|
const sorted = [...advisories].sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]);
|
|
48
96
|
const lines = [
|
|
49
97
|
`Found ${advisories.length} ${advisories.length === 1 ? "vulnerability" : "vulnerabilities"}:\n`,
|
|
50
|
-
"Package".padEnd(
|
|
51
|
-
|
|
98
|
+
"Package".padEnd(24) +
|
|
99
|
+
"Current".padEnd(14) +
|
|
100
|
+
"Severity".padEnd(20) +
|
|
101
|
+
"CVE".padEnd(22) +
|
|
102
|
+
"Patch",
|
|
103
|
+
"─".repeat(104),
|
|
52
104
|
];
|
|
53
105
|
for (const adv of sorted) {
|
|
54
|
-
const name = adv.packageName.slice(0,
|
|
106
|
+
const name = adv.packageName.slice(0, 22).padEnd(24);
|
|
107
|
+
const current = (adv.currentVersion ?? "?").slice(0, 12).padEnd(14);
|
|
55
108
|
const sev = SEVERITY_ICON[adv.severity].padEnd(20);
|
|
56
109
|
const cve = adv.cveId.slice(0, 20).padEnd(22);
|
|
57
110
|
const patch = adv.patchedVersion ? `→ ${adv.patchedVersion}` : "no patch";
|
|
58
|
-
lines.push(`${name}${sev}${cve}${patch}`);
|
|
111
|
+
lines.push(`${name}${current}${sev}${cve}${patch}`);
|
|
112
|
+
}
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
115
|
+
export function renderAuditSummary(packages) {
|
|
116
|
+
if (packages.length === 0) {
|
|
117
|
+
return "✔ No vulnerable packages found.\n";
|
|
118
|
+
}
|
|
119
|
+
const SEVERITY_ICON = {
|
|
120
|
+
critical: "🔴 CRITICAL",
|
|
121
|
+
high: "🟠 HIGH ",
|
|
122
|
+
medium: "🟡 MEDIUM ",
|
|
123
|
+
low: "⚪ LOW ",
|
|
124
|
+
};
|
|
125
|
+
const lines = [
|
|
126
|
+
`Found ${packages.length} affected ${packages.length === 1 ? "package" : "packages"}:\n`,
|
|
127
|
+
"Package".padEnd(24) +
|
|
128
|
+
"Current".padEnd(14) +
|
|
129
|
+
"Severity".padEnd(20) +
|
|
130
|
+
"Advisories".padEnd(12) +
|
|
131
|
+
"Patch",
|
|
132
|
+
"─".repeat(98),
|
|
133
|
+
];
|
|
134
|
+
for (const item of packages) {
|
|
135
|
+
const name = item.packageName.slice(0, 22).padEnd(24);
|
|
136
|
+
const current = (item.currentVersion ?? "?").slice(0, 12).padEnd(14);
|
|
137
|
+
const sev = SEVERITY_ICON[item.severity].padEnd(20);
|
|
138
|
+
const count = String(item.advisoryCount).padEnd(12);
|
|
139
|
+
const patch = item.patchedVersion ? `→ ${item.patchedVersion}` : "no patch";
|
|
140
|
+
lines.push(`${name}${current}${sev}${count}${patch}`);
|
|
141
|
+
}
|
|
142
|
+
return lines.join("\n");
|
|
143
|
+
}
|
|
144
|
+
export function renderAuditSourceHealth(sourceHealth) {
|
|
145
|
+
if (sourceHealth.length === 0)
|
|
146
|
+
return "";
|
|
147
|
+
const lines = ["", "Sources:"];
|
|
148
|
+
for (const item of sourceHealth) {
|
|
149
|
+
const label = item.source === "osv" ? "OSV.dev" : "GitHub Advisory DB";
|
|
150
|
+
const status = item.status.toUpperCase().padEnd(7);
|
|
151
|
+
const coverage = `${item.successfulTargets}/${item.attemptedTargets} targets`;
|
|
152
|
+
const advisories = `${item.advisoriesFound} advisories`;
|
|
153
|
+
const suffix = item.message ? ` (${item.message})` : "";
|
|
154
|
+
lines.push(` ${label.padEnd(22)} ${status} ${coverage}, ${advisories}${suffix}`);
|
|
59
155
|
}
|
|
60
156
|
return lines.join("\n");
|
|
61
157
|
}
|