@rainy-updates/cli 0.5.1-rc.2 → 0.5.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 (44) hide show
  1. package/CHANGELOG.md +84 -1
  2. package/README.md +8 -1
  3. package/dist/bin/cli.js +62 -12
  4. package/dist/commands/audit/fetcher.d.ts +6 -0
  5. package/dist/commands/audit/fetcher.js +79 -0
  6. package/dist/commands/audit/mapper.d.ts +16 -0
  7. package/dist/commands/audit/mapper.js +61 -0
  8. package/dist/commands/audit/parser.d.ts +3 -0
  9. package/dist/commands/audit/parser.js +87 -0
  10. package/dist/commands/audit/runner.d.ts +7 -0
  11. package/dist/commands/audit/runner.js +64 -0
  12. package/dist/commands/bisect/engine.d.ts +12 -0
  13. package/dist/commands/bisect/engine.js +89 -0
  14. package/dist/commands/bisect/oracle.d.ts +7 -0
  15. package/dist/commands/bisect/oracle.js +36 -0
  16. package/dist/commands/bisect/parser.d.ts +2 -0
  17. package/dist/commands/bisect/parser.js +73 -0
  18. package/dist/commands/bisect/runner.d.ts +6 -0
  19. package/dist/commands/bisect/runner.js +27 -0
  20. package/dist/commands/health/parser.d.ts +2 -0
  21. package/dist/commands/health/parser.js +90 -0
  22. package/dist/commands/health/runner.d.ts +7 -0
  23. package/dist/commands/health/runner.js +130 -0
  24. package/dist/config/loader.d.ts +5 -1
  25. package/dist/config/policy.d.ts +4 -0
  26. package/dist/config/policy.js +2 -0
  27. package/dist/core/check.js +56 -3
  28. package/dist/core/fix-pr-batch.js +3 -2
  29. package/dist/core/fix-pr.js +19 -4
  30. package/dist/core/init-ci.js +3 -3
  31. package/dist/core/options.d.ts +10 -1
  32. package/dist/core/options.js +129 -13
  33. package/dist/core/summary.d.ts +1 -0
  34. package/dist/core/summary.js +11 -1
  35. package/dist/core/upgrade.js +10 -0
  36. package/dist/core/warm-cache.js +19 -1
  37. package/dist/output/format.js +4 -0
  38. package/dist/output/github.js +3 -0
  39. package/dist/registry/npm.d.ts +9 -2
  40. package/dist/registry/npm.js +87 -17
  41. package/dist/types/index.d.ts +83 -0
  42. package/dist/utils/lockfile.d.ts +5 -0
  43. package/dist/utils/lockfile.js +44 -0
  44. package/package.json +13 -4
package/CHANGELOG.md CHANGED
@@ -2,6 +2,90 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [0.5.1] - 2026-02-27
6
+
7
+ ### Added
8
+
9
+ - **New `audit` command**: Scan dependencies for known CVEs using [OSV.dev](https://osv.dev) (Google's open vulnerability database). Runs queries in parallel for all installed packages.
10
+ - `--severity critical|high|medium|low` — Filter by minimum severity level
11
+ - `--fix` — Print the minimum-secure-version `npm install` command to patch advisories
12
+ - `--dry-run` — Preview without side effects
13
+ - `--report json` — Machine-readable JSON output
14
+ - `--json-file <path>` — Write JSON report to file for CI pipelines
15
+ - Exit code `1` when vulnerabilities are found; `0` when clean.
16
+
17
+ - **New `health` command**: Surface stale, deprecated, and unmaintained packages before they become liabilities.
18
+ - `--stale 12m|180d|365` — Flag packages with no release in the given period (supports months and days)
19
+ - `--deprecated` / `--no-deprecated` — Control deprecated package detection
20
+ - `--alternatives` — Suggest active alternatives for deprecated packages
21
+ - `--report json` — Machine-readable JSON output
22
+ - Exit code `1` when flagged packages are found.
23
+
24
+ - **New `bisect` command**: Binary search across semver versions to find the exact version that introduced a failing test or breaking change.
25
+ - `rup bisect <package> --cmd "<test command>"` — Specify test oracle command
26
+ - `--range <start>..<end>` — Narrow the search to a specific version range
27
+ - `--dry-run` — Simulate without installing anything
28
+ - Exit code `1` when a breaking version is identified.
29
+
30
+ - **New CLI binary aliases for developer ergonomics**:
31
+ - `rup` — Ultra-short power-user alias (e.g., `rup ci`, `rup audit`)
32
+ - `rainy-up` — Human-friendly alias (e.g., `rainy-up check`)
33
+ - `rainy-updates` retained for backwards compatibility with CI scripts.
34
+
35
+ ### Architecture
36
+
37
+ - `bisect`, `audit`, and `health` are fully isolated modules under `src/commands/`. They are lazy-loaded (dynamic `import()`) only when their command is invoked — zero startup cost penalty.
38
+ - `src/core/options.ts` now dispatches `bisect`, `audit`, and `health` to their isolated sub-parsers, keeping the command router clean and extensible.
39
+ - New type definitions: `AuditOptions`, `AuditResult`, `CveAdvisory`, `BisectOptions`, `BisectResult`, `HealthOptions`, `HealthResult`, `PackageHealthMetric`.
40
+
41
+ ### Changed
42
+
43
+ - CLI global help updated to list all 9 commands.
44
+ - Error messages now include `(rup)` in the binary identifier.
45
+ - `package.json` description updated to reflect DevOps-first positioning.
46
+
47
+ ## [0.5.1-rc.4] - 2026-02-27
48
+
49
+ ### Added
50
+
51
+ - New registry and stream controls:
52
+ - `--registry-timeout-ms <n>`
53
+ - `--registry-retries <n>`
54
+ - `--stream`
55
+ - New lockfile execution control:
56
+ - `--lockfile-mode preserve|update|error`
57
+ - Policy extensions:
58
+ - package rule `target` override
59
+ - package rule `autofix` control for fix-PR flows
60
+ - New additive summary/output metadata:
61
+ - `streamedEvents`
62
+ - `policyOverridesApplied`
63
+ - `registryAuthFailure`
64
+ - `streamed_events` GitHub output key
65
+ - `policy_overrides_applied` GitHub output key
66
+ - `registry_auth_failures` GitHub output key
67
+
68
+ ### Changed
69
+
70
+ - Registry client now supports configurable retry count and timeout defaults.
71
+ - Registry resolution now supports `.npmrc` auth token/basic auth parsing for scoped/private registries.
72
+ - Fix-PR automation now excludes updates with `autofix: false`.
73
+ - CI workflow templates generated by `init-ci` now include stream mode and registry control flags.
74
+ - Upgrade flow now enforces explicit lockfile policy semantics via `--lockfile-mode`.
75
+
76
+ ### Tests
77
+
78
+ - Extended options parsing tests for registry/stream/lockfile flags.
79
+ - Extended policy tests for `target` and `autofix` rule behavior.
80
+ - Updated output and summary tests for additive metadata fields.
81
+
82
+ ## [0.5.1-rc.3] - 2026-02-27
83
+
84
+ ### Fixed
85
+
86
+ - Resolved false dirty-worktree failures in `--fix-pr` flows caused by early PR report file creation.
87
+ - `deps-report.md` generation now runs after fix-PR git automation checks/operations.
88
+
5
89
  ## [0.5.1-rc.2] - 2026-02-27
6
90
 
7
91
  ### Added
@@ -175,7 +259,6 @@ All notable changes to this project are documented in this file.
175
259
  - `--schedule weekly|daily|off`
176
260
  - package-manager-aware install step generation (npm/pnpm)
177
261
 
178
-
179
262
  ## [0.4.0] - 2026-02-27
180
263
 
181
264
  ### Added
package/README.md CHANGED
@@ -65,7 +65,9 @@ npx @rainy-updates/cli baseline --check --file .artifacts/deps-baseline.json --w
65
65
  - Scans dependency groups: `dependencies`, `devDependencies`, `optionalDependencies`, `peerDependencies`.
66
66
  - Resolves versions per unique package to reduce duplicate network requests.
67
67
  - Uses network concurrency controls and resilient retries.
68
+ - Supports explicit registry retry/timeout tuning (`--registry-retries`, `--registry-timeout-ms`).
68
69
  - Supports stale-cache fallback when registry calls fail.
70
+ - Supports streamed progress output for long CI runs (`--stream`).
69
71
 
70
72
  ### Workspace support
71
73
 
@@ -80,6 +82,7 @@ npx @rainy-updates/cli baseline --check --file .artifacts/deps-baseline.json --w
80
82
  - Apply global ignore patterns.
81
83
  - Apply package-specific rules.
82
84
  - Enforce max upgrade target per package (for safer rollout).
85
+ - Support per-package target override and fix-pr inclusion (`target`, `autofix`).
83
86
 
84
87
  Example policy file:
85
88
 
@@ -87,7 +90,7 @@ Example policy file:
87
90
  {
88
91
  "ignore": ["@types/*", "eslint*"],
89
92
  "packageRules": {
90
- "react": { "maxTarget": "minor" },
93
+ "react": { "maxTarget": "minor", "target": "patch", "autofix": false },
91
94
  "typescript": { "ignore": true }
92
95
  }
93
96
  }
@@ -155,7 +158,10 @@ Schedule:
155
158
  - `--dep-kinds deps,dev,optional,peer`
156
159
  - `--concurrency <n>`
157
160
  - `--cache-ttl <seconds>`
161
+ - `--registry-timeout-ms <n>`
162
+ - `--registry-retries <n>`
158
163
  - `--offline`
164
+ - `--stream`
159
165
  - `--fail-on none|patch|minor|major|any`
160
166
  - `--max-updates <n>`
161
167
  - `--group-by none|name|scope|kind|risk`
@@ -175,6 +181,7 @@ Schedule:
175
181
  - `--fix-branch <name>`
176
182
  - `--fix-commit-message <text>`
177
183
  - `--fix-dry-run`
184
+ - `--lockfile-mode preserve|update|error`
178
185
  - `--no-pr-report`
179
186
  - `--ci`
180
187
 
package/dist/bin/cli.js CHANGED
@@ -66,20 +66,42 @@ async function main() {
66
66
  process.exitCode = 1;
67
67
  return;
68
68
  }
69
- const result = await runCommand(parsed);
70
- if (parsed.options.prReportFile) {
71
- const markdown = renderPrReport(result);
72
- await writeFileAtomic(parsed.options.prReportFile, markdown + "\n");
69
+ // ─── v0.5.1 commands: lazy-loaded, isolated from check pipeline ──────────
70
+ if (parsed.command === "bisect") {
71
+ const { runBisect } = await import("../commands/bisect/runner.js");
72
+ const result = await runBisect(parsed.options);
73
+ process.exitCode = result.breakingVersion ? 1 : 0;
74
+ return;
75
+ }
76
+ if (parsed.command === "audit") {
77
+ const { runAudit } = await import("../commands/audit/runner.js");
78
+ const result = await runAudit(parsed.options);
79
+ process.exitCode = result.advisories.length > 0 ? 1 : 0;
80
+ return;
73
81
  }
74
- if (parsed.options.fixPr && (parsed.command === "check" || parsed.command === "upgrade" || parsed.command === "ci")) {
82
+ if (parsed.command === "health") {
83
+ const { runHealth } = await import("../commands/health/runner.js");
84
+ const result = await runHealth(parsed.options);
85
+ process.exitCode = result.totalFlagged > 0 ? 1 : 0;
86
+ return;
87
+ }
88
+ const result = await runCommand(parsed);
89
+ if (parsed.options.fixPr &&
90
+ (parsed.command === "check" ||
91
+ parsed.command === "upgrade" ||
92
+ parsed.command === "ci")) {
75
93
  result.summary.fixPrApplied = false;
76
- result.summary.fixBranchName = parsed.options.fixBranch ?? "chore/rainy-updates";
94
+ result.summary.fixBranchName =
95
+ parsed.options.fixBranch ?? "chore/rainy-updates";
77
96
  result.summary.fixCommitSha = "";
78
97
  result.summary.fixPrBranchesCreated = 0;
79
98
  if (parsed.command === "ci") {
80
99
  const batched = await applyFixPrBatches(parsed.options, result);
81
100
  result.summary.fixPrApplied = batched.applied;
82
- result.summary.fixBranchName = batched.branches[0] ?? (parsed.options.fixBranch ?? "chore/rainy-updates");
101
+ result.summary.fixBranchName =
102
+ batched.branches[0] ??
103
+ parsed.options.fixBranch ??
104
+ "chore/rainy-updates";
83
105
  result.summary.fixCommitSha = batched.commits[0] ?? "";
84
106
  result.summary.fixPrBranchesCreated = batched.branches.length;
85
107
  if (batched.branches.length > 1) {
@@ -87,25 +109,32 @@ async function main() {
87
109
  }
88
110
  }
89
111
  else {
90
- const fixResult = await applyFixPr(parsed.options, result, parsed.options.prReportFile ? [parsed.options.prReportFile] : []);
112
+ const fixResult = await applyFixPr(parsed.options, result, []);
91
113
  result.summary.fixPrApplied = fixResult.applied;
92
114
  result.summary.fixBranchName = fixResult.branchName ?? "";
93
115
  result.summary.fixCommitSha = fixResult.commitSha ?? "";
94
116
  result.summary.fixPrBranchesCreated = fixResult.applied ? 1 : 0;
95
117
  }
96
118
  }
119
+ if (parsed.options.prReportFile) {
120
+ const markdown = renderPrReport(result);
121
+ await writeFileAtomic(parsed.options.prReportFile, markdown + "\n");
122
+ }
97
123
  result.summary.failReason = resolveFailReason(result.updates, result.errors, parsed.options.failOn, parsed.options.maxUpdates, parsed.options.ci);
98
124
  const renderStartedAt = Date.now();
99
125
  let rendered = renderResult(result, parsed.options.format);
100
126
  result.summary.durationMs.render = Math.max(0, Date.now() - renderStartedAt);
101
- if (parsed.options.format === "json" || parsed.options.format === "metrics") {
127
+ if (parsed.options.format === "json" ||
128
+ parsed.options.format === "metrics") {
102
129
  rendered = renderResult(result, parsed.options.format);
103
130
  }
104
131
  if (parsed.options.onlyChanged &&
105
132
  result.updates.length === 0 &&
106
133
  result.errors.length === 0 &&
107
134
  result.warnings.length === 0 &&
108
- (parsed.options.format === "table" || parsed.options.format === "minimal" || parsed.options.format === "github")) {
135
+ (parsed.options.format === "table" ||
136
+ parsed.options.format === "minimal" ||
137
+ parsed.options.format === "github")) {
109
138
  rendered = "";
110
139
  }
111
140
  if (parsed.options.jsonFile) {
@@ -122,7 +151,7 @@ async function main() {
122
151
  process.exitCode = resolveExitCode(result, result.summary.failReason);
123
152
  }
124
153
  catch (error) {
125
- process.stderr.write(`rainy-updates: ${String(error)}\n`);
154
+ process.stderr.write(`rainy-updates (rup): ${String(error)}\n`);
126
155
  process.exitCode = 2;
127
156
  }
128
157
  }
@@ -141,7 +170,10 @@ Options:
141
170
  --reject <pattern>
142
171
  --dep-kinds deps,dev,optional,peer
143
172
  --concurrency <n>
173
+ --registry-timeout-ms <n>
174
+ --registry-retries <n>
144
175
  --cache-ttl <seconds>
176
+ --stream
145
177
  --policy-file <path>
146
178
  --offline
147
179
  --fix-pr
@@ -162,6 +194,7 @@ Options:
162
194
  --cooldown-days <n>
163
195
  --pr-limit <n>
164
196
  --only-changed
197
+ --lockfile-mode preserve|update|error
165
198
  --log-level error|warn|info|debug
166
199
  --ci`;
167
200
  }
@@ -177,8 +210,11 @@ Options:
177
210
  --reject <pattern>
178
211
  --dep-kinds deps,dev,optional,peer
179
212
  --concurrency <n>
213
+ --registry-timeout-ms <n>
214
+ --registry-retries <n>
180
215
  --cache-ttl <seconds>
181
216
  --offline
217
+ --stream
182
218
  --json-file <path>
183
219
  --github-output <path>
184
220
  --sarif-file <path>
@@ -197,12 +233,15 @@ Options:
197
233
  --target patch|minor|major|latest
198
234
  --policy-file <path>
199
235
  --concurrency <n>
236
+ --registry-timeout-ms <n>
237
+ --registry-retries <n>
200
238
  --fix-pr
201
239
  --fix-branch <name>
202
240
  --fix-commit-message <text>
203
241
  --fix-dry-run
204
242
  --fix-pr-no-checkout
205
243
  --fix-pr-batch-size <n>
244
+ --lockfile-mode preserve|update|error
206
245
  --no-pr-report
207
246
  --json-file <path>
208
247
  --pr-report-file <path>`;
@@ -222,6 +261,9 @@ Options:
222
261
  --only-changed
223
262
  --offline
224
263
  --concurrency <n>
264
+ --registry-timeout-ms <n>
265
+ --registry-retries <n>
266
+ --stream
225
267
  --fix-pr
226
268
  --fix-branch <name>
227
269
  --fix-commit-message <text>
@@ -235,6 +277,7 @@ Options:
235
277
  --pr-report-file <path>
236
278
  --fail-on none|patch|minor|major|any
237
279
  --max-updates <n>
280
+ --lockfile-mode preserve|update|error
238
281
  --log-level error|warn|info|debug
239
282
  --ci`;
240
283
  }
@@ -262,7 +305,7 @@ Options:
262
305
  --dep-kinds deps,dev,optional,peer
263
306
  --ci`;
264
307
  }
265
- return `rainy-updates <command> [options]
308
+ return `rainy-updates (rup / rainy-up) <command> [options]
266
309
 
267
310
  Commands:
268
311
  check Detect available updates
@@ -271,6 +314,9 @@ Commands:
271
314
  warm-cache Warm local cache for fast/offline checks
272
315
  init-ci Scaffold GitHub Actions workflow
273
316
  baseline Save/check dependency baseline snapshots
317
+ audit Scan dependencies for CVEs (OSV.dev)
318
+ health Detect stale/deprecated/unmaintained packages
319
+ bisect Find which version of a dep introduced a failure
274
320
 
275
321
  Global options:
276
322
  --cwd <path>
@@ -299,8 +345,12 @@ Global options:
299
345
  --no-pr-report
300
346
  --log-level error|warn|info|debug
301
347
  --concurrency <n>
348
+ --registry-timeout-ms <n>
349
+ --registry-retries <n>
302
350
  --cache-ttl <seconds>
303
351
  --offline
352
+ --stream
353
+ --lockfile-mode preserve|update|error
304
354
  --ci
305
355
  --help, -h
306
356
  --version, -v`;
@@ -0,0 +1,6 @@
1
+ import type { CveAdvisory, AuditOptions } from "../../types/index.js";
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[]>;
@@ -0,0 +1,79 @@
1
+ import { asyncPool } from "../../utils/async-pool.js";
2
+ const OSV_API = "https://api.osv.dev/v1/query";
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
+ }
@@ -0,0 +1,16 @@
1
+ import type { CveAdvisory, AuditSeverity } from "../../types/index.js";
2
+ /**
3
+ * Filters advisories by minimum severity level.
4
+ * e.g. --severity high → keeps critical and high.
5
+ */
6
+ export declare function filterBySeverity(advisories: CveAdvisory[], minSeverity: AuditSeverity | undefined): CveAdvisory[];
7
+ /**
8
+ * For each advisory that has a known patchedVersion,
9
+ * produces a sorted, deduplicated map of package → minimum secure version.
10
+ * Used by --fix to determine what version to update to.
11
+ */
12
+ export declare function buildPatchMap(advisories: CveAdvisory[]): Map<string, string>;
13
+ /**
14
+ * Renders audit advisories as a formatted table string for terminal output.
15
+ */
16
+ export declare function renderAuditTable(advisories: CveAdvisory[]): string;
@@ -0,0 +1,61 @@
1
+ const SEVERITY_RANK = {
2
+ critical: 4,
3
+ high: 3,
4
+ medium: 2,
5
+ low: 1,
6
+ };
7
+ /**
8
+ * Filters advisories by minimum severity level.
9
+ * e.g. --severity high → keeps critical and high.
10
+ */
11
+ export function filterBySeverity(advisories, minSeverity) {
12
+ if (!minSeverity)
13
+ return advisories;
14
+ const minRank = SEVERITY_RANK[minSeverity];
15
+ return advisories.filter((a) => SEVERITY_RANK[a.severity] >= minRank);
16
+ }
17
+ /**
18
+ * For each advisory that has a known patchedVersion,
19
+ * produces a sorted, deduplicated map of package → minimum secure version.
20
+ * Used by --fix to determine what version to update to.
21
+ */
22
+ export function buildPatchMap(advisories) {
23
+ const patchMap = new Map();
24
+ for (const advisory of advisories) {
25
+ if (!advisory.patchedVersion)
26
+ continue;
27
+ const existing = patchMap.get(advisory.packageName);
28
+ if (!existing || advisory.patchedVersion > existing) {
29
+ patchMap.set(advisory.packageName, advisory.patchedVersion);
30
+ }
31
+ }
32
+ return patchMap;
33
+ }
34
+ /**
35
+ * Renders audit advisories as a formatted table string for terminal output.
36
+ */
37
+ export function renderAuditTable(advisories) {
38
+ if (advisories.length === 0) {
39
+ return "✔ No vulnerabilities found.\n";
40
+ }
41
+ const SEVERITY_ICON = {
42
+ critical: "🔴 CRITICAL",
43
+ high: "🟠 HIGH ",
44
+ medium: "🟡 MEDIUM ",
45
+ low: "⚪ LOW ",
46
+ };
47
+ const sorted = [...advisories].sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]);
48
+ const lines = [
49
+ `Found ${advisories.length} vulnerability${advisories.length === 1 ? "" : "ies"}:\n`,
50
+ "Package".padEnd(30) + "Severity".padEnd(20) + "CVE".padEnd(22) + "Patch",
51
+ "─".repeat(90),
52
+ ];
53
+ for (const adv of sorted) {
54
+ const name = adv.packageName.slice(0, 28).padEnd(30);
55
+ const sev = SEVERITY_ICON[adv.severity].padEnd(20);
56
+ const cve = adv.cveId.slice(0, 20).padEnd(22);
57
+ const patch = adv.patchedVersion ? `→ ${adv.patchedVersion}` : "no patch";
58
+ lines.push(`${name}${sev}${cve}${patch}`);
59
+ }
60
+ return lines.join("\n");
61
+ }
@@ -0,0 +1,3 @@
1
+ import type { AuditOptions, AuditSeverity } from "../../types/index.js";
2
+ export declare function parseSeverity(value: string): AuditSeverity;
3
+ export declare function parseAuditArgs(args: string[]): AuditOptions;
@@ -0,0 +1,87 @@
1
+ import path from "node:path";
2
+ import process from "node:process";
3
+ const SEVERITY_LEVELS = ["critical", "high", "medium", "low"];
4
+ export function parseSeverity(value) {
5
+ if (SEVERITY_LEVELS.includes(value)) {
6
+ return value;
7
+ }
8
+ throw new Error(`--severity must be critical, high, medium, or low. Got: ${value}`);
9
+ }
10
+ export function parseAuditArgs(args) {
11
+ const options = {
12
+ cwd: process.cwd(),
13
+ workspace: false,
14
+ severity: undefined,
15
+ fix: false,
16
+ dryRun: false,
17
+ reportFormat: "table",
18
+ jsonFile: undefined,
19
+ concurrency: 16,
20
+ registryTimeoutMs: 8000,
21
+ };
22
+ let index = 0;
23
+ while (index < args.length) {
24
+ const current = args[index];
25
+ const next = args[index + 1];
26
+ if (current === "--cwd" && next) {
27
+ options.cwd = path.resolve(next);
28
+ index += 2;
29
+ continue;
30
+ }
31
+ if (current === "--cwd")
32
+ throw new Error("Missing value for --cwd");
33
+ if (current === "--workspace") {
34
+ options.workspace = true;
35
+ index += 1;
36
+ continue;
37
+ }
38
+ if (current === "--severity" && next) {
39
+ options.severity = parseSeverity(next);
40
+ index += 2;
41
+ continue;
42
+ }
43
+ if (current === "--severity")
44
+ throw new Error("Missing value for --severity");
45
+ if (current === "--fix") {
46
+ options.fix = true;
47
+ index += 1;
48
+ continue;
49
+ }
50
+ if (current === "--dry-run") {
51
+ options.dryRun = true;
52
+ index += 1;
53
+ continue;
54
+ }
55
+ if (current === "--report" && next) {
56
+ if (next !== "table" && next !== "json") {
57
+ throw new Error("--report must be table or json");
58
+ }
59
+ options.reportFormat = next;
60
+ index += 2;
61
+ continue;
62
+ }
63
+ if (current === "--report")
64
+ throw new Error("Missing value for --report");
65
+ if (current === "--json-file" && next) {
66
+ options.jsonFile = path.resolve(options.cwd, next);
67
+ index += 2;
68
+ continue;
69
+ }
70
+ if (current === "--json-file")
71
+ throw new Error("Missing value for --json-file");
72
+ if (current === "--concurrency" && next) {
73
+ const parsed = Number(next);
74
+ if (!Number.isInteger(parsed) || parsed <= 0)
75
+ throw new Error("--concurrency must be a positive integer");
76
+ options.concurrency = parsed;
77
+ index += 2;
78
+ continue;
79
+ }
80
+ if (current === "--concurrency")
81
+ throw new Error("Missing value for --concurrency");
82
+ if (current.startsWith("-"))
83
+ throw new Error(`Unknown audit option: ${current}`);
84
+ throw new Error(`Unexpected audit argument: ${current}`);
85
+ }
86
+ return options;
87
+ }
@@ -0,0 +1,7 @@
1
+ import type { AuditOptions, AuditResult } from "../../types/index.js";
2
+ /**
3
+ * Entry point for `rup audit`. Lazy-loaded by cli.ts.
4
+ * Discovers packages, fetches CVE advisories, filters by severity, and
5
+ * optionally applies minimum-secure-version patches.
6
+ */
7
+ export declare function runAudit(options: AuditOptions): Promise<AuditResult>;
@@ -0,0 +1,64 @@
1
+ import { collectDependencies, readManifest, } from "../../parsers/package-json.js";
2
+ import { discoverPackageDirs } from "../../workspace/discover.js";
3
+ import { writeFileAtomic } from "../../utils/io.js";
4
+ import { stableStringify } from "../../utils/stable-json.js";
5
+ import { fetchAdvisories } from "./fetcher.js";
6
+ import { filterBySeverity, buildPatchMap, renderAuditTable } from "./mapper.js";
7
+ /**
8
+ * Entry point for `rup audit`. Lazy-loaded by cli.ts.
9
+ * Discovers packages, fetches CVE advisories, filters by severity, and
10
+ * optionally applies minimum-secure-version patches.
11
+ */
12
+ export async function runAudit(options) {
13
+ const result = {
14
+ advisories: [],
15
+ autoFixable: 0,
16
+ errors: [],
17
+ warnings: [],
18
+ };
19
+ const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
20
+ // Collect all unique package names
21
+ const packageNames = new Set();
22
+ for (const dir of packageDirs) {
23
+ let manifest;
24
+ try {
25
+ manifest = await readManifest(dir);
26
+ }
27
+ catch (error) {
28
+ result.errors.push(`Failed to read package.json in ${dir}: ${String(error)}`);
29
+ continue;
30
+ }
31
+ const deps = collectDependencies(manifest, [
32
+ "dependencies",
33
+ "devDependencies",
34
+ "optionalDependencies",
35
+ ]);
36
+ for (const dep of deps) {
37
+ packageNames.add(dep.name);
38
+ }
39
+ }
40
+ if (packageNames.size === 0) {
41
+ result.warnings.push("No dependencies found to audit.");
42
+ return result;
43
+ }
44
+ process.stderr.write(`[audit] Querying OSV.dev for ${packageNames.size} packages...\n`);
45
+ let advisories = await fetchAdvisories([...packageNames], {
46
+ concurrency: options.concurrency,
47
+ registryTimeoutMs: options.registryTimeoutMs,
48
+ });
49
+ advisories = filterBySeverity(advisories, options.severity);
50
+ result.advisories = advisories;
51
+ result.autoFixable = advisories.filter((a) => a.patchedVersion !== null).length;
52
+ if (options.reportFormat === "table" || !options.jsonFile) {
53
+ process.stdout.write(renderAuditTable(advisories) + "\n");
54
+ }
55
+ if (options.jsonFile) {
56
+ await writeFileAtomic(options.jsonFile, stableStringify({ advisories, errors: result.errors, warnings: result.warnings }, 2) + "\n");
57
+ process.stderr.write(`[audit] JSON report written to ${options.jsonFile}\n`);
58
+ }
59
+ if (options.fix && !options.dryRun && result.autoFixable > 0) {
60
+ const patchMap = buildPatchMap(advisories);
61
+ process.stderr.write(`[audit] --fix: ${patchMap.size} packages have available patches. Apply with: npm install ${[...patchMap.entries()].map(([n, v]) => `${n}@${v}`).join(" ")}\n`);
62
+ }
63
+ return result;
64
+ }
@@ -0,0 +1,12 @@
1
+ import type { BisectOptions, BisectResult } from "../../types/index.js";
2
+ /**
3
+ * Binary search engine for dependency bisecting.
4
+ * Given a sorted list of versions, finds the exact version that causes
5
+ * a test oracle to switch from "good" → "bad".
6
+ */
7
+ export declare function bisectVersions(versions: string[], options: BisectOptions): Promise<BisectResult>;
8
+ /**
9
+ * Fetches available versions for a package from registry/cache,
10
+ * optionally filtered to a user-specified range, sorted ascending.
11
+ */
12
+ export declare function fetchBisectVersions(options: BisectOptions): Promise<string[]>;