@rainy-updates/cli 0.5.1 → 0.5.2-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +68 -1
  2. package/README.md +84 -25
  3. package/dist/bin/cli.js +30 -0
  4. package/dist/commands/audit/mapper.js +1 -1
  5. package/dist/commands/licenses/parser.d.ts +2 -0
  6. package/dist/commands/licenses/parser.js +116 -0
  7. package/dist/commands/licenses/runner.d.ts +9 -0
  8. package/dist/commands/licenses/runner.js +163 -0
  9. package/dist/commands/licenses/sbom.d.ts +10 -0
  10. package/dist/commands/licenses/sbom.js +70 -0
  11. package/dist/commands/resolve/graph/builder.d.ts +20 -0
  12. package/dist/commands/resolve/graph/builder.js +183 -0
  13. package/dist/commands/resolve/graph/conflict.d.ts +20 -0
  14. package/dist/commands/resolve/graph/conflict.js +52 -0
  15. package/dist/commands/resolve/graph/resolver.d.ts +17 -0
  16. package/dist/commands/resolve/graph/resolver.js +71 -0
  17. package/dist/commands/resolve/parser.d.ts +2 -0
  18. package/dist/commands/resolve/parser.js +89 -0
  19. package/dist/commands/resolve/runner.d.ts +13 -0
  20. package/dist/commands/resolve/runner.js +136 -0
  21. package/dist/commands/snapshot/parser.d.ts +2 -0
  22. package/dist/commands/snapshot/parser.js +80 -0
  23. package/dist/commands/snapshot/runner.d.ts +11 -0
  24. package/dist/commands/snapshot/runner.js +115 -0
  25. package/dist/commands/snapshot/store.d.ts +35 -0
  26. package/dist/commands/snapshot/store.js +158 -0
  27. package/dist/commands/unused/matcher.d.ts +22 -0
  28. package/dist/commands/unused/matcher.js +95 -0
  29. package/dist/commands/unused/parser.d.ts +2 -0
  30. package/dist/commands/unused/parser.js +95 -0
  31. package/dist/commands/unused/runner.d.ts +11 -0
  32. package/dist/commands/unused/runner.js +113 -0
  33. package/dist/commands/unused/scanner.d.ts +18 -0
  34. package/dist/commands/unused/scanner.js +129 -0
  35. package/dist/core/impact.d.ts +36 -0
  36. package/dist/core/impact.js +82 -0
  37. package/dist/core/options.d.ts +13 -1
  38. package/dist/core/options.js +35 -13
  39. package/dist/types/index.d.ts +153 -0
  40. package/dist/utils/semver.d.ts +18 -0
  41. package/dist/utils/semver.js +88 -3
  42. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,7 +1,74 @@
1
- # Changelog
1
+ # CHANGELOG
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [0.5.2] - 2026-02-27
6
+
7
+ ### Added
8
+
9
+ - **New `unused` command**: Detect unused and missing npm dependencies by statically scanning source files.
10
+ - Walks `src/` (configurable via `--src`) and extracts all import/require specifiers (ESM static, ESM dynamic, CJS, re-exports).
11
+ - Cross-references against `package.json` `dependencies`, `devDependencies`, and `optionalDependencies`.
12
+ - Reports two problem classes: `declared-not-imported` (unused bloat) and `imported-not-declared` (missing declarations).
13
+ - `--fix` — removes unused entries from `package.json` atomically (with `--dry-run` preview).
14
+ - `--no-dev` — skip `devDependencies` from the unused scan.
15
+ - `--json-file <path>` — write structured JSON report for CI pipelines.
16
+ - Exit code `1` when unused or missing dependencies are found.
17
+
18
+ - **New `resolve` command**: Pure-TS in-memory peer dependency conflict detector — **no `npm install` subprocess spawned**.
19
+ - Builds a `PeerGraph` from declared dependencies, enriched with `peerDependencies` fetched in parallel from the registry (cache-first — instant on warm cache, offline-capable).
20
+ - Performs a single-pass O(n × peers) BFS traversal using the new `satisfies()` semver util.
21
+ - Classifies conflicts as `error` (ERESOLVE-level, different major) or `warning` (soft peer incompatibility).
22
+ - Generates human-readable fix suggestions per conflict.
23
+ - `--after-update` — simulates proposed `rup check` updates in-memory _before_ writing anything, showing you peer conflicts before they happen.
24
+ - `--safe` — exits non-zero on any error-level conflict.
25
+ - `--json-file <path>` — write structured JSON conflict report.
26
+ - Exit code `1` when error-level conflicts are detected.
27
+
28
+ - **New `licenses` command**: SPDX license compliance scanning with SBOM generation.
29
+ - Fetches the `license` field from each dependency's npm packument in parallel.
30
+ - Normalizes raw license strings to SPDX 2.x identifiers.
31
+ - `--allow <spdx,...>` — allowlist mode: flag any package not in the list.
32
+ - `--deny <spdx,...>` — denylist mode: flag any package matching these identifiers.
33
+ - `--sbom <path>` — generate a standards-compliant **SPDX 2.3 JSON SBOM** document (`DESCRIBES` + `DEPENDS_ON` relationship graph, required by CISA/EU CRA mandates).
34
+ - `--json-file <path>` — write full license report as JSON.
35
+ - Exit code `1` when license violations are detected.
36
+
37
+ - **New `snapshot` command**: Save, list, restore, and diff dependency state snapshots.
38
+ - `rup snapshot save [--label <name>]` — captures `package.json` contents and lockfile hashes for all workspace packages into a lightweight JSON store (`.rup-snapshots.json`).
39
+ - `rup snapshot list` — shows all saved snapshots with timestamp and label.
40
+ - `rup snapshot restore <id|label>` — writes back captured `package.json` files atomically; prompts to re-run the package manager install.
41
+ - `rup snapshot diff <id|label>` — shows dependency version changes since the snapshot.
42
+ - JSON-file store (no SQLite dependency), human-readable and git-committable.
43
+ - `--store <path>` — custom store file location.
44
+
45
+ - **Impact Score engine** (`src/core/impact.ts`): Per-update risk assessment.
46
+ - Computes a 0–100 composite score: `diffTypeWeight` (patch=10, minor=25, major=55) + CVE presence bonus (+35) + workspace spread (up to +20).
47
+ - Ranks each update as `critical`, `high`, `medium`, or `low`.
48
+ - `applyImpactScores()` batch helper for the check/upgrade pipeline.
49
+ - ANSI `impactBadge()` for terminal table rendering (wired to `--show-impact` flag, coming in a follow-up).
50
+
51
+ - **`satisfies(version, range)` utility** (`src/utils/semver.ts`): Pure-TS semver range checker.
52
+ - Handles `^`, `~`, `>=`, `<=`, `>`, `<`, exact, `*`/empty (always true).
53
+ - Supports compound AND ranges (`>=1.0.0 <2.0.0`) and OR union ranges (`^16 || ^18`).
54
+ - Falls through gracefully on non-semver inputs (e.g., `workspace:*`, `latest`) — no false-positive conflicts.
55
+ - Used by `rup resolve` peer graph resolver.
56
+
57
+ ### Architecture
58
+
59
+ - `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.
60
+ - `src/core/options.ts` dispatches all 4 new commands to their isolated sub-parsers. `KNOWN_COMMANDS` now contains **13 entries**.
61
+ - `ParsedCliArgs` union extended with 4 new command variants.
62
+ - `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`.
63
+ - `PackageUpdate` extended with optional `impactScore?: ImpactScore` and `homepage?: string` fields.
64
+
65
+ ### Changed
66
+
67
+ - CLI global help updated to list all **13 commands** including `unused`, `resolve`, `licenses`, and `snapshot`.
68
+ - `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.
69
+
70
+ ---
71
+
5
72
  ## [0.5.1] - 2026-02-27
6
73
 
7
74
  ### Added
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @rainy-updates/cli
2
2
 
3
- Agentic CLI to detect, control, and apply dependency updates across npm/pnpm projects and monorepos.
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,104 @@ Agentic CLI to detect, control, and apply dependency updates across npm/pnpm pro
15
15
  ## Install
16
16
 
17
17
  ```bash
18
- npm i -D @rainy-updates/cli
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
- ## Core commands
24
+ Once installed, three binary aliases are available in your `node_modules/.bin/`:
24
25
 
25
- - `check`: analyze dependencies and report available updates.
26
- - `upgrade`: rewrite dependency ranges in manifests, optionally install lockfile updates.
27
- - `ci`: run CI-focused dependency automation (warm cache, check/upgrade, policy gates).
28
- - `warm-cache`: prefetch package metadata for fast and offline checks.
29
- - `baseline`: save and compare dependency baseline snapshots.
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) (Google's open vulnerability database)
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
- # 2b) CI orchestration mode
41
- npx @rainy-updates/cli ci --workspace --mode strict --format github --json-file .artifacts/updates.json
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
- # 2c) Batch fix branches by scope
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
- # 3) Apply upgrades with workspace sync
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
- # 3b) Generate a fix branch + commit for CI automation
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
- # 5) Save and compare baseline drift in CI
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 --fix # prints the patching npm install command
103
+ rup audit --severity high # if installed
104
+
105
+ # 9) Check dependency maintenance health ── NEW in v0.5.1
106
+ npx @rainy-updates/cli health
107
+ npx @rainy-updates/cli health --stale 6m # flag packages with no release in 6 months
108
+ npx @rainy-updates/cli health --stale 180d # same but in days
109
+ rup health --stale 6m # if installed
110
+
111
+ # 10) Find which version introduced a breaking change ── NEW in v0.5.1
112
+ npx @rainy-updates/cli bisect axios --cmd "bun test"
113
+ npx @rainy-updates/cli bisect react --range "18.0.0..19.0.0" --cmd "npm test"
114
+ npx @rainy-updates/cli bisect lodash --cmd "npm run test:unit" --dry-run
115
+ rup bisect axios --cmd "bun test" # if installed
59
116
  ```
60
117
 
61
118
  ## What it does in production
@@ -119,17 +176,16 @@ npx @rainy-updates/cli check --policy-file .rainyupdates-policy.json
119
176
 
120
177
  These outputs are designed for CI pipelines, security tooling, and PR review automation.
121
178
 
122
-
123
179
  ## Automatic CI bootstrap
124
180
 
125
181
  Generate a workflow in the target project automatically:
126
182
 
127
183
  ```bash
128
- # strict mode (recommended)
129
- npx @rainy-updates/cli init-ci --mode enterprise --schedule weekly
184
+ # enterprise mode (recommended)
185
+ rup init-ci --mode enterprise --schedule weekly
130
186
 
131
187
  # lightweight mode
132
- npx @rainy-updates/cli init-ci --mode minimal --schedule daily
188
+ rup init-ci --mode minimal --schedule daily
133
189
  ```
134
190
 
135
191
  Generated file:
@@ -208,9 +264,13 @@ Configuration can be loaded from:
208
264
  ## CLI help
209
265
 
210
266
  ```bash
267
+ rup --help
268
+ rup <command> --help
269
+ rup --version
270
+
271
+ # or with the full name:
211
272
  rainy-updates --help
212
- rainy-updates <command> --help
213
- rainy-updates --version
273
+ npx @rainy-updates/cli --help
214
274
  ```
215
275
 
216
276
  ## Reliability characteristics
@@ -230,7 +290,6 @@ This package ships with production CI/CD pipelines in the repository:
230
290
  - Tag-driven release pipeline for npm publishing with provenance.
231
291
  - Release preflight validation for npm auth/scope checks before publishing.
232
292
 
233
-
234
293
  ## Product roadmap
235
294
 
236
295
  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" ||
@@ -317,6 +343,10 @@ Commands:
317
343
  audit Scan dependencies for CVEs (OSV.dev)
318
344
  health Detect stale/deprecated/unmaintained packages
319
345
  bisect Find which version of a dep introduced a failure
346
+ unused Detect unused or missing npm dependencies
347
+ resolve Check peer dependency conflicts (pure-TS, no subprocess)
348
+ licenses Scan dependency licenses and generate SPDX SBOM
349
+ snapshot Save, list, restore, and diff dependency state snapshots
320
350
 
321
351
  Global options:
322
352
  --cwd <path>
@@ -46,7 +46,7 @@ export function renderAuditTable(advisories) {
46
46
  };
47
47
  const sorted = [...advisories].sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]);
48
48
  const lines = [
49
- `Found ${advisories.length} vulnerability${advisories.length === 1 ? "" : "ies"}:\n`,
49
+ `Found ${advisories.length} ${advisories.length === 1 ? "vulnerability" : "vulnerabilities"}:\n`,
50
50
  "Package".padEnd(30) + "Severity".padEnd(20) + "CVE".padEnd(22) + "Patch",
51
51
  "─".repeat(90),
52
52
  ];
@@ -0,0 +1,2 @@
1
+ import type { LicenseOptions } from "../../types/index.js";
2
+ export declare function parseLicensesArgs(args: string[]): LicenseOptions;
@@ -0,0 +1,116 @@
1
+ export function parseLicensesArgs(args) {
2
+ const options = {
3
+ cwd: process.cwd(),
4
+ workspace: false,
5
+ allow: undefined,
6
+ deny: undefined,
7
+ sbomFile: undefined,
8
+ jsonFile: undefined,
9
+ diffMode: false,
10
+ concurrency: 12,
11
+ registryTimeoutMs: 10_000,
12
+ cacheTtlSeconds: 3600,
13
+ };
14
+ for (let i = 0; i < args.length; i++) {
15
+ const current = args[i];
16
+ const next = args[i + 1];
17
+ if (current === "--cwd" && next) {
18
+ options.cwd = next;
19
+ i++;
20
+ continue;
21
+ }
22
+ if (current === "--cwd")
23
+ throw new Error("Missing value for --cwd");
24
+ if (current === "--workspace") {
25
+ options.workspace = true;
26
+ continue;
27
+ }
28
+ if (current === "--diff") {
29
+ options.diffMode = true;
30
+ continue;
31
+ }
32
+ if (current === "--allow" && next) {
33
+ options.allow = next
34
+ .split(",")
35
+ .map((s) => s.trim())
36
+ .filter(Boolean);
37
+ i++;
38
+ continue;
39
+ }
40
+ if (current === "--allow")
41
+ throw new Error("Missing value for --allow");
42
+ if (current === "--deny" && next) {
43
+ options.deny = next
44
+ .split(",")
45
+ .map((s) => s.trim())
46
+ .filter(Boolean);
47
+ i++;
48
+ continue;
49
+ }
50
+ if (current === "--deny")
51
+ throw new Error("Missing value for --deny");
52
+ if (current === "--sbom" && next) {
53
+ options.sbomFile = next;
54
+ i++;
55
+ continue;
56
+ }
57
+ if (current === "--sbom")
58
+ throw new Error("Missing value for --sbom");
59
+ if (current === "--json-file" && next) {
60
+ options.jsonFile = next;
61
+ i++;
62
+ continue;
63
+ }
64
+ if (current === "--json-file")
65
+ throw new Error("Missing value for --json-file");
66
+ if (current === "--concurrency" && next) {
67
+ const n = Number(next);
68
+ if (!Number.isInteger(n) || n <= 0)
69
+ throw new Error("--concurrency must be a positive integer");
70
+ options.concurrency = n;
71
+ i++;
72
+ continue;
73
+ }
74
+ if (current === "--concurrency")
75
+ throw new Error("Missing value for --concurrency");
76
+ if (current === "--timeout" && next) {
77
+ const ms = Number(next);
78
+ if (!Number.isFinite(ms) || ms <= 0)
79
+ throw new Error("--timeout must be a positive number");
80
+ options.registryTimeoutMs = ms;
81
+ i++;
82
+ continue;
83
+ }
84
+ if (current === "--timeout")
85
+ throw new Error("Missing value for --timeout");
86
+ if (current === "--help" || current === "-h") {
87
+ process.stdout.write(LICENSES_HELP);
88
+ process.exit(0);
89
+ }
90
+ if (current.startsWith("-"))
91
+ throw new Error(`Unknown option: ${current}`);
92
+ }
93
+ return options;
94
+ }
95
+ const LICENSES_HELP = `
96
+ rup licenses — Scan dependency licenses and generate SPDX SBOM
97
+
98
+ Usage:
99
+ rup licenses [options]
100
+
101
+ Options:
102
+ --allow <spdx,...> Allow only these SPDX identifiers (e.g. MIT,Apache-2.0)
103
+ --deny <spdx,...> Deny these SPDX identifiers (e.g. GPL-3.0)
104
+ --sbom <path> Write SPDX 2.3 SBOM JSON to file
105
+ --json-file <path> Write JSON report to file
106
+ --diff Show only packages with a different license than last scan
107
+ --workspace Scan all workspace packages
108
+ --timeout <ms> Registry request timeout (default: 10000)
109
+ --concurrency <n> Parallel registry requests (default: 12)
110
+ --cwd <path> Working directory (default: cwd)
111
+ --help Show this help
112
+
113
+ Exit codes:
114
+ 0 No violations
115
+ 1 License violations detected
116
+ `.trimStart();
@@ -0,0 +1,9 @@
1
+ import type { LicenseOptions, LicenseResult } from "../../types/index.js";
2
+ /**
3
+ * Entry point for `rup licenses`. Lazy-loaded by cli.ts.
4
+ *
5
+ * Fetches the SPDX license field from each dependency's packument,
6
+ * checks it against --allow/--deny lists, and optionally generates
7
+ * an SPDX 2.3 SBOM JSON document.
8
+ */
9
+ export declare function runLicenses(options: LicenseOptions): Promise<LicenseResult>;
@@ -0,0 +1,163 @@
1
+ import process from "node:process";
2
+ import { discoverPackageDirs } from "../../workspace/discover.js";
3
+ import { readManifest, collectDependencies, } from "../../parsers/package-json.js";
4
+ import { asyncPool } from "../../utils/async-pool.js";
5
+ import { stableStringify } from "../../utils/stable-json.js";
6
+ import { writeFileAtomic } from "../../utils/io.js";
7
+ import { generateSbom } from "./sbom.js";
8
+ /**
9
+ * Entry point for `rup licenses`. Lazy-loaded by cli.ts.
10
+ *
11
+ * Fetches the SPDX license field from each dependency's packument,
12
+ * checks it against --allow/--deny lists, and optionally generates
13
+ * an SPDX 2.3 SBOM JSON document.
14
+ */
15
+ export async function runLicenses(options) {
16
+ const result = {
17
+ packages: [],
18
+ violations: [],
19
+ totalViolations: 0,
20
+ errors: [],
21
+ warnings: [],
22
+ };
23
+ const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
24
+ const allDeps = new Map(); // name → resolved version
25
+ for (const packageDir of packageDirs) {
26
+ let manifest;
27
+ try {
28
+ manifest = await readManifest(packageDir);
29
+ }
30
+ catch (err) {
31
+ result.errors.push(`${packageDir}: ${String(err)}`);
32
+ continue;
33
+ }
34
+ const deps = collectDependencies(manifest, [
35
+ "dependencies",
36
+ "devDependencies",
37
+ "optionalDependencies",
38
+ ]);
39
+ for (const dep of deps) {
40
+ if (!allDeps.has(dep.name)) {
41
+ const bare = dep.range.replace(/^[~^>=<]/, "").split(" ")[0] ?? dep.range;
42
+ allDeps.set(dep.name, bare);
43
+ }
44
+ }
45
+ }
46
+ // Fetch license info from npm registry in parallel
47
+ const names = Array.from(allDeps.keys());
48
+ const fetchTasks = names.map((name) => async () => {
49
+ const version = allDeps.get(name) ?? "latest";
50
+ return fetchLicenseInfo(name, version, options.registryTimeoutMs);
51
+ });
52
+ const licenseInfos = await asyncPool(options.concurrency, fetchTasks);
53
+ for (const info of licenseInfos) {
54
+ if (!info || info instanceof Error)
55
+ continue;
56
+ result.packages.push(info);
57
+ }
58
+ // Evaluate allow/deny lists
59
+ for (const pkg of result.packages) {
60
+ if (isViolation(pkg, options)) {
61
+ result.violations.push(pkg);
62
+ }
63
+ }
64
+ result.totalViolations = result.violations.length;
65
+ // Render
66
+ process.stdout.write(renderLicenseTable(result) + "\n");
67
+ // SBOM output
68
+ if (options.sbomFile) {
69
+ const sbom = generateSbom(result.packages, options.cwd);
70
+ await writeFileAtomic(options.sbomFile, stableStringify(sbom, 2) + "\n");
71
+ process.stderr.write(`[licenses] SBOM written to ${options.sbomFile}\n`);
72
+ }
73
+ // JSON output
74
+ if (options.jsonFile) {
75
+ await writeFileAtomic(options.jsonFile, stableStringify(result, 2) + "\n");
76
+ process.stderr.write(`[licenses] JSON report written to ${options.jsonFile}\n`);
77
+ }
78
+ return result;
79
+ }
80
+ async function fetchLicenseInfo(name, version, timeoutMs) {
81
+ try {
82
+ const controller = new AbortController();
83
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
84
+ const url = `https://registry.npmjs.org/${encodeURIComponent(name)}/${encodeURIComponent(version)}`;
85
+ const res = await fetch(url, {
86
+ signal: controller.signal,
87
+ headers: { accept: "application/json" },
88
+ });
89
+ clearTimeout(timer);
90
+ if (!res.ok)
91
+ return null;
92
+ const data = (await res.json());
93
+ const rawLicense = data.license ?? "UNKNOWN";
94
+ const repo = typeof data.repository === "object"
95
+ ? data.repository?.url
96
+ : data.repository;
97
+ return {
98
+ name,
99
+ version,
100
+ license: rawLicense,
101
+ spdxExpression: normalizeSpdx(rawLicense),
102
+ homepage: data.homepage,
103
+ repository: repo,
104
+ };
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ }
110
+ /** Normalizes common license strings to SPDX identifiers. */
111
+ function normalizeSpdx(raw) {
112
+ const known = {
113
+ MIT: "MIT",
114
+ ISC: "ISC",
115
+ "Apache-2.0": "Apache-2.0",
116
+ "BSD-2-Clause": "BSD-2-Clause",
117
+ "BSD-3-Clause": "BSD-3-Clause",
118
+ "GPL-3.0": "GPL-3.0",
119
+ "GPL-2.0": "GPL-2.0",
120
+ "LGPL-2.1": "LGPL-2.1",
121
+ "LGPL-3.0": "LGPL-3.0",
122
+ "MPL-2.0": "MPL-2.0",
123
+ "CC0-1.0": "CC0-1.0",
124
+ Unlicense: "Unlicense",
125
+ "AGPL-3.0": "AGPL-3.0",
126
+ };
127
+ return known[raw.trim()] ?? (raw.includes("-") ? raw : null);
128
+ }
129
+ function isViolation(pkg, options) {
130
+ const spdx = pkg.spdxExpression ?? pkg.license;
131
+ if (options.deny && options.deny.includes(spdx))
132
+ return true;
133
+ if (options.allow &&
134
+ options.allow.length > 0 &&
135
+ !options.allow.includes(spdx))
136
+ return true;
137
+ return false;
138
+ }
139
+ function renderLicenseTable(result) {
140
+ const lines = [];
141
+ if (result.violations.length > 0) {
142
+ lines.push(`\n✖ License violations (${result.violations.length}):\n`);
143
+ for (const pkg of result.violations) {
144
+ lines.push(` \x1b[31m✖\x1b[0m ${pkg.name.padEnd(35)} ${pkg.spdxExpression ?? pkg.license}`);
145
+ }
146
+ lines.push("");
147
+ }
148
+ lines.push(`📄 ${result.packages.length} packages scanned:\n`);
149
+ lines.push(" " + "Package".padEnd(35) + "Version".padEnd(12) + "License");
150
+ lines.push(" " + "─".repeat(60));
151
+ for (const pkg of result.packages) {
152
+ const isViolating = result.violations.some((v) => v.name === pkg.name);
153
+ const prefix = isViolating ? "\x1b[31m" : "";
154
+ const suffix = isViolating ? "\x1b[0m" : "";
155
+ lines.push(" " +
156
+ prefix +
157
+ pkg.name.padEnd(35) +
158
+ pkg.version.padEnd(12) +
159
+ (pkg.spdxExpression ?? pkg.license) +
160
+ suffix);
161
+ }
162
+ return lines.join("\n");
163
+ }
@@ -0,0 +1,10 @@
1
+ import type { PackageLicense, SbomDocument } from "../../types/index.js";
2
+ /**
3
+ * Generates an SPDX 2.3 compliant SBOM JSON document from a list of
4
+ * scanned package licenses.
5
+ *
6
+ * SPDX 2.3 spec: https://spdx.github.io/spdx-spec/v2.3/
7
+ * Required by: CISA SBOM mandate, EU Cyber Resilience Act, many enterprise
8
+ * security standards.
9
+ */
10
+ export declare function generateSbom(packages: PackageLicense[], projectName: string): SbomDocument;