@rainy-updates/cli 0.5.1 → 0.5.2-rc.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 +93 -1
- package/README.md +88 -25
- package/dist/bin/cli.js +50 -1
- 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 +106 -10
- package/dist/commands/audit/parser.js +36 -2
- package/dist/commands/audit/runner.js +179 -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 +92 -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/licenses/parser.d.ts +2 -0
- package/dist/commands/licenses/parser.js +116 -0
- package/dist/commands/licenses/runner.d.ts +9 -0
- package/dist/commands/licenses/runner.js +163 -0
- package/dist/commands/licenses/sbom.d.ts +10 -0
- package/dist/commands/licenses/sbom.js +70 -0
- package/dist/commands/resolve/graph/builder.d.ts +20 -0
- package/dist/commands/resolve/graph/builder.js +183 -0
- package/dist/commands/resolve/graph/conflict.d.ts +20 -0
- package/dist/commands/resolve/graph/conflict.js +52 -0
- package/dist/commands/resolve/graph/resolver.d.ts +17 -0
- package/dist/commands/resolve/graph/resolver.js +71 -0
- package/dist/commands/resolve/parser.d.ts +2 -0
- package/dist/commands/resolve/parser.js +89 -0
- package/dist/commands/resolve/runner.d.ts +13 -0
- package/dist/commands/resolve/runner.js +136 -0
- package/dist/commands/snapshot/parser.d.ts +2 -0
- package/dist/commands/snapshot/parser.js +80 -0
- package/dist/commands/snapshot/runner.d.ts +11 -0
- package/dist/commands/snapshot/runner.js +115 -0
- package/dist/commands/snapshot/store.d.ts +35 -0
- package/dist/commands/snapshot/store.js +158 -0
- package/dist/commands/unused/matcher.d.ts +22 -0
- package/dist/commands/unused/matcher.js +95 -0
- package/dist/commands/unused/parser.d.ts +2 -0
- package/dist/commands/unused/parser.js +95 -0
- package/dist/commands/unused/runner.d.ts +11 -0
- package/dist/commands/unused/runner.js +113 -0
- package/dist/commands/unused/scanner.d.ts +18 -0
- package/dist/commands/unused/scanner.js +129 -0
- package/dist/core/impact.d.ts +36 -0
- package/dist/core/impact.js +82 -0
- package/dist/core/options.d.ts +13 -1
- package/dist/core/options.js +35 -13
- package/dist/types/index.d.ts +187 -1
- package/dist/ui/tui.d.ts +6 -0
- package/dist/ui/tui.js +50 -0
- package/dist/utils/semver.d.ts +18 -0
- package/dist/utils/semver.js +88 -3
- package/package.json +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,99 @@
|
|
|
1
|
-
#
|
|
1
|
+
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.5.2-rc.2] - 2026-02-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Audit RC2 overhaul**:
|
|
10
|
+
- `audit --summary` / `audit --report summary` groups noisy advisory lists into affected-package summaries.
|
|
11
|
+
- `audit --source auto|osv|github|all` adds multi-source security lookups, with `auto` querying **OSV.dev + GitHub Advisory Database** and merging results.
|
|
12
|
+
- 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.
|
|
13
|
+
- JSON audit output now includes package summaries, source metadata, and resolution statistics.
|
|
14
|
+
- Source-health reporting now distinguishes `ok`, `partial`, and `failed` advisory backends so partial coverage is explicit instead of silent.
|
|
15
|
+
- **Interactive TUI Engine**: An `ink`-based Terminal User Interface for interactive dependency updates, featuring semantic diff coloring and keyboard navigation (`src/ui/tui.tsx`).
|
|
16
|
+
- **Changelog Fetcher**: Implemented `changelog/fetcher.ts` to retrieve release notes dynamically from GitHub API.
|
|
17
|
+
- Utilizes `bun:sqlite` backed `VersionCache` to prevent API rate limit (403) errors.
|
|
18
|
+
- Strictly lazy-loaded to preserve zero-overhead startup time.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Audit patch planning now chooses the lowest safe patched version that clears all detected vulnerable ranges, avoiding unnecessary major jumps during `audit --fix`.
|
|
23
|
+
- Audit findings now record the current installed version and contributing advisory sources per finding.
|
|
24
|
+
- Audit now warns when one advisory source degrades and fails the run when all selected advisory sources are unavailable.
|
|
25
|
+
- Audit terminal output now shows advisory-source health directly in table and summary modes, so degraded coverage is visible without reading JSON.
|
|
26
|
+
- Resolved TypeScript JSX compiler errors by properly exposing `"jsx": "react-jsx"` in `tsconfig.json`.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## [0.5.2] - 2026-02-27
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **New `unused` command**: Detect unused and missing npm dependencies by statically scanning source files.
|
|
35
|
+
- Walks `src/` (configurable via `--src`) and extracts all import/require specifiers (ESM static, ESM dynamic, CJS, re-exports).
|
|
36
|
+
- Cross-references against `package.json` `dependencies`, `devDependencies`, and `optionalDependencies`.
|
|
37
|
+
- Reports two problem classes: `declared-not-imported` (unused bloat) and `imported-not-declared` (missing declarations).
|
|
38
|
+
- `--fix` — removes unused entries from `package.json` atomically (with `--dry-run` preview).
|
|
39
|
+
- `--no-dev` — skip `devDependencies` from the unused scan.
|
|
40
|
+
- `--json-file <path>` — write structured JSON report for CI pipelines.
|
|
41
|
+
- Exit code `1` when unused or missing dependencies are found.
|
|
42
|
+
|
|
43
|
+
- **New `resolve` command**: Pure-TS in-memory peer dependency conflict detector — **no `npm install` subprocess spawned**.
|
|
44
|
+
- Builds a `PeerGraph` from declared dependencies, enriched with `peerDependencies` fetched in parallel from the registry (cache-first — instant on warm cache, offline-capable).
|
|
45
|
+
- Performs a single-pass O(n × peers) BFS traversal using the new `satisfies()` semver util.
|
|
46
|
+
- Classifies conflicts as `error` (ERESOLVE-level, different major) or `warning` (soft peer incompatibility).
|
|
47
|
+
- Generates human-readable fix suggestions per conflict.
|
|
48
|
+
- `--after-update` — simulates proposed `rup check` updates in-memory _before_ writing anything, showing you peer conflicts before they happen.
|
|
49
|
+
- `--safe` — exits non-zero on any error-level conflict.
|
|
50
|
+
- `--json-file <path>` — write structured JSON conflict report.
|
|
51
|
+
- Exit code `1` when error-level conflicts are detected.
|
|
52
|
+
|
|
53
|
+
- **New `licenses` command**: SPDX license compliance scanning with SBOM generation.
|
|
54
|
+
- Fetches the `license` field from each dependency's npm packument in parallel.
|
|
55
|
+
- Normalizes raw license strings to SPDX 2.x identifiers.
|
|
56
|
+
- `--allow <spdx,...>` — allowlist mode: flag any package not in the list.
|
|
57
|
+
- `--deny <spdx,...>` — denylist mode: flag any package matching these identifiers.
|
|
58
|
+
- `--sbom <path>` — generate a standards-compliant **SPDX 2.3 JSON SBOM** document (`DESCRIBES` + `DEPENDS_ON` relationship graph, required by CISA/EU CRA mandates).
|
|
59
|
+
- `--json-file <path>` — write full license report as JSON.
|
|
60
|
+
- Exit code `1` when license violations are detected.
|
|
61
|
+
|
|
62
|
+
- **New `snapshot` command**: Save, list, restore, and diff dependency state snapshots.
|
|
63
|
+
- `rup snapshot save [--label <name>]` — captures `package.json` contents and lockfile hashes for all workspace packages into a lightweight JSON store (`.rup-snapshots.json`).
|
|
64
|
+
- `rup snapshot list` — shows all saved snapshots with timestamp and label.
|
|
65
|
+
- `rup snapshot restore <id|label>` — writes back captured `package.json` files atomically; prompts to re-run the package manager install.
|
|
66
|
+
- `rup snapshot diff <id|label>` — shows dependency version changes since the snapshot.
|
|
67
|
+
- JSON-file store (no SQLite dependency), human-readable and git-committable.
|
|
68
|
+
- `--store <path>` — custom store file location.
|
|
69
|
+
|
|
70
|
+
- **Impact Score engine** (`src/core/impact.ts`): Per-update risk assessment.
|
|
71
|
+
- Computes a 0–100 composite score: `diffTypeWeight` (patch=10, minor=25, major=55) + CVE presence bonus (+35) + workspace spread (up to +20).
|
|
72
|
+
- Ranks each update as `critical`, `high`, `medium`, or `low`.
|
|
73
|
+
- `applyImpactScores()` batch helper for the check/upgrade pipeline.
|
|
74
|
+
- ANSI `impactBadge()` for terminal table rendering (wired to `--show-impact` flag, coming in a follow-up).
|
|
75
|
+
|
|
76
|
+
- **`satisfies(version, range)` utility** (`src/utils/semver.ts`): Pure-TS semver range checker.
|
|
77
|
+
- Handles `^`, `~`, `>=`, `<=`, `>`, `<`, exact, `*`/empty (always true).
|
|
78
|
+
- Supports compound AND ranges (`>=1.0.0 <2.0.0`) and OR union ranges (`^16 || ^18`).
|
|
79
|
+
- Falls through gracefully on non-semver inputs (e.g., `workspace:*`, `latest`) — no false-positive conflicts.
|
|
80
|
+
- Used by `rup resolve` peer graph resolver.
|
|
81
|
+
|
|
82
|
+
### Architecture
|
|
83
|
+
|
|
84
|
+
- `unused`, `resolve`, `licenses`, and `snapshot` are fully isolated modules under `src/commands/`. All are lazy-loaded (dynamic `import()`) on first invocation — zero startup cost penalty.
|
|
85
|
+
- `src/core/options.ts` dispatches all 4 new commands to their isolated sub-parsers. `KNOWN_COMMANDS` now contains **13 entries**.
|
|
86
|
+
- `ParsedCliArgs` union extended with 4 new command variants.
|
|
87
|
+
- `src/types/index.ts` extended with: `ImpactScore`, `PeerNode`, `PeerGraph`, `PeerConflict`, `PeerConflictSeverity`, `UnusedKind`, `UnusedDependency`, `UnusedOptions`, `UnusedResult`, `PackageLicense`, `SbomDocument`, `SbomPackage`, `SbomRelationship`, `LicenseOptions`, `LicenseResult`, `SnapshotEntry`, `SnapshotAction`, `SnapshotOptions`, `SnapshotResult`, `ResolveOptions`, `ResolveResult`.
|
|
88
|
+
- `PackageUpdate` extended with optional `impactScore?: ImpactScore` and `homepage?: string` fields.
|
|
89
|
+
|
|
90
|
+
### Changed
|
|
91
|
+
|
|
92
|
+
- CLI global help updated to list all **13 commands** including `unused`, `resolve`, `licenses`, and `snapshot`.
|
|
93
|
+
- `src/bin/cli.ts` exit codes: `unused` exits `1` on any unused/missing dep; `resolve` exits `1` on error-level peer conflicts; `licenses` exits `1` on violations; `snapshot` exits `1` on store errors.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
5
97
|
## [0.5.1] - 2026-02-27
|
|
6
98
|
|
|
7
99
|
### Added
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @rainy-updates/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The fastest DevOps-first dependency CLI. Checks, audits, upgrades, bisects, and automates npm/pnpm dependencies in CI.
|
|
4
4
|
|
|
5
|
-
`@rainy-updates/cli` is built for teams that need fast dependency intelligence, 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 intelligence, security auditing, policy-aware upgrades, and automation-ready output for CI/CD and pull request workflows.
|
|
6
6
|
|
|
7
7
|
## Why this package
|
|
8
8
|
|
|
@@ -15,47 +15,108 @@ Agentic CLI to detect, control, and apply dependency updates across npm/pnpm pro
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
|
|
18
|
+
# As a project dev dependency (recommended for teams)
|
|
19
|
+
npm install --save-dev @rainy-updates/cli
|
|
19
20
|
# or
|
|
20
21
|
pnpm add -D @rainy-updates/cli
|
|
21
22
|
```
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
Once installed, three binary aliases are available in your `node_modules/.bin/`:
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
| Alias | Use case |
|
|
27
|
+
| --------------- | ------------------------------------------- |
|
|
28
|
+
| `rup` | Power-user shortcut — `rup ci`, `rup audit` |
|
|
29
|
+
| `rainy-up` | Human-friendly — `rainy-up check` |
|
|
30
|
+
| `rainy-updates` | Backwards-compatible (safe in CI scripts) |
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# All three are identical — use whichever you prefer:
|
|
34
|
+
rup check
|
|
35
|
+
rainy-up check
|
|
36
|
+
rainy-updates check
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### One-off usage with npx (no install required)
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Always works without installing:
|
|
43
|
+
npx @rainy-updates/cli check
|
|
44
|
+
npx @rainy-updates/cli audit --severity high
|
|
45
|
+
npx @rainy-updates/cli ci --workspace --mode strict
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
> **Note:** The short aliases (`rup`, `rainy-up`) only work after installing the package. For one-off `npx` runs, use `npx @rainy-updates/cli <command>`.
|
|
49
|
+
|
|
50
|
+
## Commands
|
|
51
|
+
|
|
52
|
+
### Dependency management
|
|
53
|
+
|
|
54
|
+
- `check` — analyze dependencies and report available updates
|
|
55
|
+
- `upgrade` — rewrite dependency ranges in manifests, optionally install lockfile updates
|
|
56
|
+
- `ci` — run CI-focused dependency automation (warm cache, check/upgrade, policy gates)
|
|
57
|
+
- `warm-cache` — prefetch package metadata for fast and offline checks
|
|
58
|
+
- `baseline` — save and compare dependency baseline snapshots
|
|
59
|
+
|
|
60
|
+
### Security & health (_new in v0.5.1_)
|
|
61
|
+
|
|
62
|
+
- `audit` — scan dependencies for CVEs using [OSV.dev](https://osv.dev) plus GitHub Advisory Database, with lockfile-aware version inference
|
|
63
|
+
- `health` — detect stale, deprecated, and unmaintained packages before they become liabilities
|
|
64
|
+
- `bisect` — binary-search across semver versions to find the exact version that broke your tests
|
|
30
65
|
|
|
31
66
|
## Quick usage
|
|
32
67
|
|
|
68
|
+
> Commands work with `npx` (no install) **or** with the `rup` / `rainy-up` shortcut if the package is installed.
|
|
69
|
+
|
|
33
70
|
```bash
|
|
34
71
|
# 1) Detect updates
|
|
35
72
|
npx @rainy-updates/cli check --format table
|
|
73
|
+
rup check --format table # if installed
|
|
36
74
|
|
|
37
75
|
# 2) Strict CI mode (non-zero when updates exist)
|
|
38
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
|
|
39
78
|
|
|
40
|
-
#
|
|
41
|
-
npx @rainy-updates/cli ci --workspace --mode strict --format github
|
|
79
|
+
# 3) CI orchestration with policy gates
|
|
80
|
+
npx @rainy-updates/cli ci --workspace --mode strict --format github
|
|
81
|
+
rup ci --workspace --mode strict --format github
|
|
42
82
|
|
|
43
|
-
#
|
|
83
|
+
# 4) Batch fix branches by scope (enterprise)
|
|
44
84
|
npx @rainy-updates/cli ci --workspace --mode enterprise --group-by scope --fix-pr --fix-pr-batch-size 2
|
|
85
|
+
rup ci --workspace --mode enterprise --group-by scope --fix-pr --fix-pr-batch-size 2
|
|
45
86
|
|
|
46
|
-
#
|
|
87
|
+
# 5) Apply upgrades with workspace sync
|
|
47
88
|
npx @rainy-updates/cli upgrade --target latest --workspace --sync --install
|
|
89
|
+
rup upgrade --target latest --workspace --sync --install
|
|
48
90
|
|
|
49
|
-
#
|
|
50
|
-
npx @rainy-updates/cli check --workspace --fix-pr --fix-branch chore/rainy-updates
|
|
51
|
-
|
|
52
|
-
# 4) Warm cache for deterministic offline checks
|
|
91
|
+
# 6) Warm cache → deterministic offline CI check
|
|
53
92
|
npx @rainy-updates/cli warm-cache --workspace --concurrency 32
|
|
54
93
|
npx @rainy-updates/cli check --workspace --offline --ci
|
|
55
94
|
|
|
56
|
-
#
|
|
95
|
+
# 7) Save and compare baseline drift
|
|
57
96
|
npx @rainy-updates/cli baseline --save --file .artifacts/deps-baseline.json --workspace
|
|
58
97
|
npx @rainy-updates/cli baseline --check --file .artifacts/deps-baseline.json --workspace --ci
|
|
98
|
+
|
|
99
|
+
# 8) Scan for known CVEs ── NEW in v0.5.1
|
|
100
|
+
npx @rainy-updates/cli audit
|
|
101
|
+
npx @rainy-updates/cli audit --severity high
|
|
102
|
+
npx @rainy-updates/cli audit --summary
|
|
103
|
+
npx @rainy-updates/cli audit --source osv
|
|
104
|
+
npx @rainy-updates/cli audit --fix # prints the patching npm install command
|
|
105
|
+
rup audit --severity high # if installed
|
|
106
|
+
|
|
107
|
+
`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
|
+
|
|
109
|
+
# 9) Check dependency maintenance health ── NEW in v0.5.1
|
|
110
|
+
npx @rainy-updates/cli health
|
|
111
|
+
npx @rainy-updates/cli health --stale 6m # flag packages with no release in 6 months
|
|
112
|
+
npx @rainy-updates/cli health --stale 180d # same but in days
|
|
113
|
+
rup health --stale 6m # if installed
|
|
114
|
+
|
|
115
|
+
# 10) Find which version introduced a breaking change ── NEW in v0.5.1
|
|
116
|
+
npx @rainy-updates/cli bisect axios --cmd "bun test"
|
|
117
|
+
npx @rainy-updates/cli bisect react --range "18.0.0..19.0.0" --cmd "npm test"
|
|
118
|
+
npx @rainy-updates/cli bisect lodash --cmd "npm run test:unit" --dry-run
|
|
119
|
+
rup bisect axios --cmd "bun test" # if installed
|
|
59
120
|
```
|
|
60
121
|
|
|
61
122
|
## What it does in production
|
|
@@ -119,17 +180,16 @@ npx @rainy-updates/cli check --policy-file .rainyupdates-policy.json
|
|
|
119
180
|
|
|
120
181
|
These outputs are designed for CI pipelines, security tooling, and PR review automation.
|
|
121
182
|
|
|
122
|
-
|
|
123
183
|
## Automatic CI bootstrap
|
|
124
184
|
|
|
125
185
|
Generate a workflow in the target project automatically:
|
|
126
186
|
|
|
127
187
|
```bash
|
|
128
|
-
#
|
|
129
|
-
|
|
188
|
+
# enterprise mode (recommended)
|
|
189
|
+
rup init-ci --mode enterprise --schedule weekly
|
|
130
190
|
|
|
131
191
|
# lightweight mode
|
|
132
|
-
|
|
192
|
+
rup init-ci --mode minimal --schedule daily
|
|
133
193
|
```
|
|
134
194
|
|
|
135
195
|
Generated file:
|
|
@@ -208,9 +268,13 @@ Configuration can be loaded from:
|
|
|
208
268
|
## CLI help
|
|
209
269
|
|
|
210
270
|
```bash
|
|
271
|
+
rup --help
|
|
272
|
+
rup <command> --help
|
|
273
|
+
rup --version
|
|
274
|
+
|
|
275
|
+
# or with the full name:
|
|
211
276
|
rainy-updates --help
|
|
212
|
-
rainy-updates
|
|
213
|
-
rainy-updates --version
|
|
277
|
+
npx @rainy-updates/cli --help
|
|
214
278
|
```
|
|
215
279
|
|
|
216
280
|
## Reliability characteristics
|
|
@@ -230,7 +294,6 @@ This package ships with production CI/CD pipelines in the repository:
|
|
|
230
294
|
- Tag-driven release pipeline for npm publishing with provenance.
|
|
231
295
|
- Release preflight validation for npm auth/scope checks before publishing.
|
|
232
296
|
|
|
233
|
-
|
|
234
297
|
## Product roadmap
|
|
235
298
|
|
|
236
299
|
The long-term roadmap is maintained in [`ROADMAP.md`](./ROADMAP.md).
|
package/dist/bin/cli.js
CHANGED
|
@@ -85,6 +85,32 @@ async function main() {
|
|
|
85
85
|
process.exitCode = result.totalFlagged > 0 ? 1 : 0;
|
|
86
86
|
return;
|
|
87
87
|
}
|
|
88
|
+
// ─── v0.5.2 commands ─────────────────────────────────────────────────────
|
|
89
|
+
if (parsed.command === "unused") {
|
|
90
|
+
const { runUnused } = await import("../commands/unused/runner.js");
|
|
91
|
+
const result = await runUnused(parsed.options);
|
|
92
|
+
process.exitCode =
|
|
93
|
+
result.totalUnused > 0 || result.totalMissing > 0 ? 1 : 0;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (parsed.command === "resolve") {
|
|
97
|
+
const { runResolve } = await import("../commands/resolve/runner.js");
|
|
98
|
+
const result = await runResolve(parsed.options);
|
|
99
|
+
process.exitCode = result.errorConflicts > 0 ? 1 : 0;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (parsed.command === "licenses") {
|
|
103
|
+
const { runLicenses } = await import("../commands/licenses/runner.js");
|
|
104
|
+
const result = await runLicenses(parsed.options);
|
|
105
|
+
process.exitCode = result.totalViolations > 0 ? 1 : 0;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (parsed.command === "snapshot") {
|
|
109
|
+
const { runSnapshot } = await import("../commands/snapshot/runner.js");
|
|
110
|
+
const result = await runSnapshot(parsed.options);
|
|
111
|
+
process.exitCode = result.errors.length > 0 ? 1 : 0;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
88
114
|
const result = await runCommand(parsed);
|
|
89
115
|
if (parsed.options.fixPr &&
|
|
90
116
|
(parsed.command === "check" ||
|
|
@@ -304,6 +330,25 @@ Options:
|
|
|
304
330
|
--workspace
|
|
305
331
|
--dep-kinds deps,dev,optional,peer
|
|
306
332
|
--ci`;
|
|
333
|
+
}
|
|
334
|
+
if (isCommand && command === "audit") {
|
|
335
|
+
return `rainy-updates audit [options]
|
|
336
|
+
|
|
337
|
+
Scan dependencies for CVEs using OSV.dev and GitHub Advisory Database.
|
|
338
|
+
|
|
339
|
+
Options:
|
|
340
|
+
--workspace
|
|
341
|
+
--severity critical|high|medium|low
|
|
342
|
+
--summary
|
|
343
|
+
--report table|summary|json
|
|
344
|
+
--source auto|osv|github|all
|
|
345
|
+
--fix
|
|
346
|
+
--dry-run
|
|
347
|
+
--commit
|
|
348
|
+
--pm auto|npm|pnpm|bun|yarn
|
|
349
|
+
--json-file <path>
|
|
350
|
+
--concurrency <n>
|
|
351
|
+
--registry-timeout-ms <n>`;
|
|
307
352
|
}
|
|
308
353
|
return `rainy-updates (rup / rainy-up) <command> [options]
|
|
309
354
|
|
|
@@ -314,9 +359,13 @@ Commands:
|
|
|
314
359
|
warm-cache Warm local cache for fast/offline checks
|
|
315
360
|
init-ci Scaffold GitHub Actions workflow
|
|
316
361
|
baseline Save/check dependency baseline snapshots
|
|
317
|
-
audit Scan dependencies for CVEs (OSV.dev)
|
|
362
|
+
audit Scan dependencies for CVEs (OSV.dev + GitHub)
|
|
318
363
|
health Detect stale/deprecated/unmaintained packages
|
|
319
364
|
bisect Find which version of a dep introduced a failure
|
|
365
|
+
unused Detect unused or missing npm dependencies
|
|
366
|
+
resolve Check peer dependency conflicts (pure-TS, no subprocess)
|
|
367
|
+
licenses Scan dependency licenses and generate SPDX SBOM
|
|
368
|
+
snapshot Save, list, restore, and diff dependency state snapshots
|
|
320
369
|
|
|
321
370
|
Global options:
|
|
322
371
|
--cwd <path>
|
|
@@ -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
|
*/
|
|
@@ -46,16 +94,64 @@ export function renderAuditTable(advisories) {
|
|
|
46
94
|
};
|
|
47
95
|
const sorted = [...advisories].sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]);
|
|
48
96
|
const lines = [
|
|
49
|
-
`Found ${advisories.length}
|
|
50
|
-
"Package".padEnd(
|
|
51
|
-
|
|
97
|
+
`Found ${advisories.length} ${advisories.length === 1 ? "vulnerability" : "vulnerabilities"}:\n`,
|
|
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
|
}
|