@rainy-updates/cli 0.5.1-rc.3 → 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 (78) hide show
  1. package/CHANGELOG.md +145 -2
  2. package/README.md +92 -26
  3. package/dist/bin/cli.js +87 -7
  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/commands/licenses/parser.d.ts +2 -0
  25. package/dist/commands/licenses/parser.js +116 -0
  26. package/dist/commands/licenses/runner.d.ts +9 -0
  27. package/dist/commands/licenses/runner.js +163 -0
  28. package/dist/commands/licenses/sbom.d.ts +10 -0
  29. package/dist/commands/licenses/sbom.js +70 -0
  30. package/dist/commands/resolve/graph/builder.d.ts +20 -0
  31. package/dist/commands/resolve/graph/builder.js +183 -0
  32. package/dist/commands/resolve/graph/conflict.d.ts +20 -0
  33. package/dist/commands/resolve/graph/conflict.js +52 -0
  34. package/dist/commands/resolve/graph/resolver.d.ts +17 -0
  35. package/dist/commands/resolve/graph/resolver.js +71 -0
  36. package/dist/commands/resolve/parser.d.ts +2 -0
  37. package/dist/commands/resolve/parser.js +89 -0
  38. package/dist/commands/resolve/runner.d.ts +13 -0
  39. package/dist/commands/resolve/runner.js +136 -0
  40. package/dist/commands/snapshot/parser.d.ts +2 -0
  41. package/dist/commands/snapshot/parser.js +80 -0
  42. package/dist/commands/snapshot/runner.d.ts +11 -0
  43. package/dist/commands/snapshot/runner.js +115 -0
  44. package/dist/commands/snapshot/store.d.ts +35 -0
  45. package/dist/commands/snapshot/store.js +158 -0
  46. package/dist/commands/unused/matcher.d.ts +22 -0
  47. package/dist/commands/unused/matcher.js +95 -0
  48. package/dist/commands/unused/parser.d.ts +2 -0
  49. package/dist/commands/unused/parser.js +95 -0
  50. package/dist/commands/unused/runner.d.ts +11 -0
  51. package/dist/commands/unused/runner.js +113 -0
  52. package/dist/commands/unused/scanner.d.ts +18 -0
  53. package/dist/commands/unused/scanner.js +129 -0
  54. package/dist/config/loader.d.ts +5 -1
  55. package/dist/config/policy.d.ts +4 -0
  56. package/dist/config/policy.js +2 -0
  57. package/dist/core/check.js +56 -3
  58. package/dist/core/fix-pr-batch.js +3 -2
  59. package/dist/core/fix-pr.js +19 -4
  60. package/dist/core/impact.d.ts +36 -0
  61. package/dist/core/impact.js +82 -0
  62. package/dist/core/init-ci.js +3 -3
  63. package/dist/core/options.d.ts +22 -1
  64. package/dist/core/options.js +151 -13
  65. package/dist/core/summary.d.ts +1 -0
  66. package/dist/core/summary.js +11 -1
  67. package/dist/core/upgrade.js +10 -0
  68. package/dist/core/warm-cache.js +19 -1
  69. package/dist/output/format.js +4 -0
  70. package/dist/output/github.js +3 -0
  71. package/dist/registry/npm.d.ts +9 -2
  72. package/dist/registry/npm.js +87 -17
  73. package/dist/types/index.d.ts +236 -0
  74. package/dist/utils/lockfile.d.ts +5 -0
  75. package/dist/utils/lockfile.js +44 -0
  76. package/dist/utils/semver.d.ts +18 -0
  77. package/dist/utils/semver.js +88 -3
  78. package/package.json +13 -4
@@ -0,0 +1,129 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ /**
4
+ * Extracts all imported package names from a single source file.
5
+ *
6
+ * Handles:
7
+ * - ESM static: import ... from "pkg"
8
+ * - ESM dynamic: import("pkg")
9
+ * - CJS: require("pkg")
10
+ *
11
+ * Strips subpath imports (e.g. "lodash/merge" → "lodash"),
12
+ * skips relative imports and node: builtins.
13
+ */
14
+ export function extractImportsFromSource(source) {
15
+ const names = new Set();
16
+ // ESM static import: from "pkg" or from 'pkg'
17
+ const staticImport = /from\s+['"]([^'"]+)['"]/g;
18
+ for (const match of source.matchAll(staticImport)) {
19
+ addPackageName(names, match[1]);
20
+ }
21
+ // ESM dynamic import: import("pkg") or import('pkg')
22
+ const dynamicImport = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
23
+ for (const match of source.matchAll(dynamicImport)) {
24
+ addPackageName(names, match[1]);
25
+ }
26
+ // CJS require: require("pkg") or require('pkg')
27
+ const cjsRequire = /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
28
+ for (const match of source.matchAll(cjsRequire)) {
29
+ addPackageName(names, match[1]);
30
+ }
31
+ // export ... from "pkg"
32
+ const reExport = /\bexport\s+(?:\*|\{[^}]*\})\s+from\s+['"]([^'"]+)['"]/g;
33
+ for (const match of source.matchAll(reExport)) {
34
+ addPackageName(names, match[1]);
35
+ }
36
+ return names;
37
+ }
38
+ function addPackageName(names, specifier) {
39
+ // Skip relative imports, node: builtins, and bare file paths
40
+ if (specifier.startsWith(".") || specifier.startsWith("/"))
41
+ return;
42
+ if (specifier.startsWith("node:"))
43
+ return;
44
+ if (specifier.startsWith("bun:"))
45
+ return;
46
+ // Normalize package name (strip subpath): "lodash/merge" → "lodash"
47
+ // "@scope/pkg/subpath" → "@scope/pkg"
48
+ const name = extractPackageName(specifier);
49
+ if (name)
50
+ names.add(name);
51
+ }
52
+ export function extractPackageName(specifier) {
53
+ if (!specifier)
54
+ return null;
55
+ if (specifier.startsWith("@")) {
56
+ // Scoped: @scope/pkg or @scope/pkg/subpath
57
+ const parts = specifier.split("/");
58
+ if (parts.length < 2)
59
+ return null;
60
+ return `${parts[0]}/${parts[1]}`;
61
+ }
62
+ // Unscoped: pkg or pkg/subpath
63
+ return specifier.split("/")[0] ?? null;
64
+ }
65
+ const SOURCE_EXTENSIONS = new Set([
66
+ ".ts",
67
+ ".tsx",
68
+ ".js",
69
+ ".jsx",
70
+ ".mjs",
71
+ ".cjs",
72
+ ".mts",
73
+ ".cts",
74
+ ]);
75
+ const IGNORED_DIRS = new Set([
76
+ "node_modules",
77
+ ".git",
78
+ "dist",
79
+ "build",
80
+ "out",
81
+ ".next",
82
+ ".nuxt",
83
+ ]);
84
+ /**
85
+ * Recursively scans a directory and returns all imported package names
86
+ * found across all source files.
87
+ */
88
+ export async function scanDirectory(dir) {
89
+ const allImports = new Set();
90
+ await walkDirectory(dir, allImports);
91
+ return allImports;
92
+ }
93
+ async function walkDirectory(dir, collector) {
94
+ let entries;
95
+ try {
96
+ entries = await fs.readdir(dir);
97
+ }
98
+ catch {
99
+ return;
100
+ }
101
+ const tasks = [];
102
+ for (const entryName of entries) {
103
+ if (IGNORED_DIRS.has(entryName))
104
+ continue;
105
+ const fullPath = path.join(dir, entryName);
106
+ tasks.push(fs
107
+ .stat(fullPath)
108
+ .then((stat) => {
109
+ if (stat.isDirectory()) {
110
+ return walkDirectory(fullPath, collector);
111
+ }
112
+ if (stat.isFile()) {
113
+ const ext = path.extname(entryName).toLowerCase();
114
+ if (!SOURCE_EXTENSIONS.has(ext))
115
+ return;
116
+ return fs
117
+ .readFile(fullPath, "utf8")
118
+ .then((source) => {
119
+ for (const name of extractImportsFromSource(source)) {
120
+ collector.add(name);
121
+ }
122
+ })
123
+ .catch(() => undefined);
124
+ }
125
+ })
126
+ .catch(() => undefined));
127
+ }
128
+ await Promise.all(tasks);
129
+ }
@@ -1,4 +1,4 @@
1
- import type { CiProfile, DependencyKind, FailOnLevel, GroupBy, LogLevel, OutputFormat, TargetLevel } from "../types/index.js";
1
+ import type { CiProfile, DependencyKind, FailOnLevel, GroupBy, LockfileMode, LogLevel, OutputFormat, TargetLevel } from "../types/index.js";
2
2
  export interface FileConfig {
3
3
  target?: TargetLevel;
4
4
  filter?: string;
@@ -12,7 +12,10 @@ export interface FileConfig {
12
12
  githubOutputFile?: string;
13
13
  sarifFile?: string;
14
14
  concurrency?: number;
15
+ registryTimeoutMs?: number;
16
+ registryRetries?: number;
15
17
  offline?: boolean;
18
+ stream?: boolean;
16
19
  policyFile?: string;
17
20
  prReportFile?: string;
18
21
  failOn?: FailOnLevel;
@@ -31,6 +34,7 @@ export interface FileConfig {
31
34
  prLimit?: number;
32
35
  onlyChanged?: boolean;
33
36
  ciProfile?: CiProfile;
37
+ lockfileMode?: LockfileMode;
34
38
  install?: boolean;
35
39
  packageManager?: "auto" | "npm" | "pnpm";
36
40
  sync?: boolean;
@@ -11,6 +11,8 @@ export interface PolicyConfig {
11
11
  allowPrerelease?: boolean;
12
12
  group?: string;
13
13
  priority?: number;
14
+ target?: TargetLevel;
15
+ autofix?: boolean;
14
16
  }>;
15
17
  }
16
18
  export interface PolicyRule {
@@ -22,6 +24,8 @@ export interface PolicyRule {
22
24
  allowPrerelease?: boolean;
23
25
  group?: string;
24
26
  priority?: number;
27
+ target?: TargetLevel;
28
+ autofix?: boolean;
25
29
  }
26
30
  export interface ResolvedPolicy {
27
31
  ignorePatterns: string[];
@@ -46,6 +46,8 @@ function normalizeRule(rule) {
46
46
  allowPrerelease: rule.allowPrerelease === true,
47
47
  group: typeof rule.group === "string" && rule.group.trim().length > 0 ? rule.group.trim() : undefined,
48
48
  priority: asNonNegativeInt(rule.priority),
49
+ target: rule.target,
50
+ autofix: rule.autofix !== false,
49
51
  };
50
52
  }
51
53
  function asNonNegativeInt(value) {
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import process from "node:process";
2
3
  import { collectDependencies, readManifest } from "../parsers/package-json.js";
3
4
  import { matchesPattern } from "../utils/pattern.js";
4
5
  import { applyRangeStyle, classifyDiff, clampTarget, pickTargetVersionFromAvailable } from "../utils/semver.js";
@@ -18,7 +19,10 @@ export async function check(options) {
18
19
  const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
19
20
  discoveryMs += Date.now() - discoveryStartedAt;
20
21
  const cache = await VersionCache.create();
21
- const registryClient = new NpmRegistryClient(options.cwd);
22
+ const registryClient = new NpmRegistryClient(options.cwd, {
23
+ timeoutMs: options.registryTimeoutMs,
24
+ retries: options.registryRetries,
25
+ });
22
26
  const policy = await loadPolicy(options.cwd, options.policyFile);
23
27
  const updates = [];
24
28
  const errors = [];
@@ -30,6 +34,14 @@ export async function check(options) {
30
34
  const tasks = [];
31
35
  let skipped = 0;
32
36
  let cooldownSkipped = 0;
37
+ let streamedEvents = 0;
38
+ let policyOverridesApplied = 0;
39
+ const emitStream = (message) => {
40
+ if (!options.stream)
41
+ return;
42
+ streamedEvents += 1;
43
+ process.stdout.write(`${message}\n`);
44
+ };
33
45
  for (const packageDir of packageDirs) {
34
46
  let manifest;
35
47
  try {
@@ -49,10 +61,14 @@ export async function check(options) {
49
61
  const rule = resolvePolicyRule(dep.name, policy);
50
62
  if (rule?.ignore === true) {
51
63
  skipped += 1;
64
+ policyOverridesApplied += 1;
65
+ emitStream(`[policy-ignore] ${dep.name}`);
52
66
  continue;
53
67
  }
54
68
  if (policy.ignorePatterns.some((pattern) => matchesPattern(dep.name, pattern))) {
55
69
  skipped += 1;
70
+ policyOverridesApplied += 1;
71
+ emitStream(`[policy-ignore-pattern] ${dep.name}`);
56
72
  continue;
57
73
  }
58
74
  tasks.push({ packageDir, dependency: dep });
@@ -99,6 +115,8 @@ export async function check(options) {
99
115
  const registryStartedAt = Date.now();
100
116
  const fetched = await registryClient.resolveManyPackageMetadata(unresolvedPackages, {
101
117
  concurrency: options.concurrency,
118
+ retries: options.registryRetries,
119
+ timeoutMs: options.registryTimeoutMs,
102
120
  });
103
121
  registryMs += Date.now() - registryStartedAt;
104
122
  const cacheWriteStartedAt = Date.now();
@@ -126,6 +144,7 @@ export async function check(options) {
126
144
  }
127
145
  else {
128
146
  errors.push(`Unable to resolve ${packageName}: ${error}`);
147
+ emitStream(`[error] Unable to resolve ${packageName}: ${error}`);
129
148
  }
130
149
  }
131
150
  cacheMs += Date.now() - cacheStaleStartedAt;
@@ -136,12 +155,17 @@ export async function check(options) {
136
155
  if (!metadata?.latestVersion)
137
156
  continue;
138
157
  const rule = resolvePolicyRule(task.dependency.name, policy);
139
- const effectiveTarget = clampTarget(options.target, rule?.maxTarget);
158
+ const baseTarget = rule?.target ?? options.target;
159
+ const effectiveTarget = clampTarget(baseTarget, rule?.maxTarget);
160
+ if (rule?.target || rule?.maxTarget || rule?.autofix === false) {
161
+ policyOverridesApplied += 1;
162
+ }
140
163
  const picked = pickTargetVersionFromAvailable(task.dependency.range, metadata.availableVersions, metadata.latestVersion, effectiveTarget);
141
164
  if (!picked)
142
165
  continue;
143
166
  if (shouldSkipByCooldown(picked, metadata.publishedAtByVersion, options.cooldownDays, policy.cooldownDays, rule?.cooldownDays)) {
144
167
  cooldownSkipped += 1;
168
+ emitStream(`[cooldown-skip] ${task.dependency.name}@${picked}`);
145
169
  continue;
146
170
  }
147
171
  const nextRange = applyRangeStyle(task.dependency.range, picked);
@@ -156,14 +180,17 @@ export async function check(options) {
156
180
  toVersionResolved: picked,
157
181
  diffType: classifyDiff(task.dependency.range, picked),
158
182
  filtered: false,
183
+ autofix: rule?.autofix !== false,
159
184
  reason: rule?.maxTarget ? `policy maxTarget=${rule.maxTarget}` : undefined,
160
185
  });
186
+ emitStream(`[update] ${task.dependency.name} ${task.dependency.range} -> ${nextRange} (${classifyDiff(task.dependency.range, picked)})`);
161
187
  }
162
188
  const grouped = groupUpdates(updates, options.groupBy);
163
189
  const groupedUpdates = grouped.length;
164
190
  const groupedSorted = sortUpdates(grouped.flatMap((group) => group.items));
165
191
  const groupedCapped = typeof options.groupMax === "number" ? groupedSorted.slice(0, options.groupMax) : groupedSorted;
166
- const ruleLimited = applyRuleUpdateCaps(groupedCapped, policy);
192
+ const prioritized = applyPolicyPrioritySort(groupedCapped, policy);
193
+ const ruleLimited = applyRuleUpdateCaps(prioritized, policy);
167
194
  const prLimited = typeof options.prLimit === "number" ? ruleLimited.slice(0, options.prLimit) : ruleLimited;
168
195
  const limitedUpdates = sortUpdates(prLimited);
169
196
  const prLimitHit = typeof options.prLimit === "number" && groupedSorted.length > options.prLimit;
@@ -189,7 +216,9 @@ export async function check(options) {
189
216
  cooldownSkipped,
190
217
  ciProfile: options.ciProfile,
191
218
  prLimitHit,
219
+ policyOverridesApplied,
192
220
  }));
221
+ summary.streamedEvents = streamedEvents;
193
222
  return {
194
223
  projectPath: options.cwd,
195
224
  packagePaths: packageDirs,
@@ -266,6 +295,30 @@ function applyRuleUpdateCaps(updates, policy) {
266
295
  }
267
296
  return limited;
268
297
  }
298
+ function applyPolicyPrioritySort(updates, policy) {
299
+ return [...updates].sort((left, right) => {
300
+ const leftPriority = resolvePolicyRule(left.name, policy)?.priority ?? 0;
301
+ const rightPriority = resolvePolicyRule(right.name, policy)?.priority ?? 0;
302
+ if (leftPriority !== rightPriority)
303
+ return rightPriority - leftPriority;
304
+ const byRisk = riskRank(left.diffType) - riskRank(right.diffType);
305
+ if (byRisk !== 0)
306
+ return byRisk;
307
+ const byName = left.name.localeCompare(right.name);
308
+ if (byName !== 0)
309
+ return byName;
310
+ return left.packagePath.localeCompare(right.packagePath);
311
+ });
312
+ }
313
+ function riskRank(value) {
314
+ if (value === "patch")
315
+ return 0;
316
+ if (value === "minor")
317
+ return 1;
318
+ if (value === "major")
319
+ return 2;
320
+ return 3;
321
+ }
269
322
  function sortUpdates(updates) {
270
323
  return [...updates].sort((left, right) => {
271
324
  const byPath = left.packagePath.localeCompare(right.packagePath);
@@ -4,11 +4,12 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { readManifest, writeManifest } from "../parsers/package-json.js";
6
6
  export async function applyFixPrBatches(options, result) {
7
- if (!options.fixPr || result.updates.length === 0) {
7
+ const autofixUpdates = result.updates.filter((update) => update.autofix !== false);
8
+ if (!options.fixPr || autofixUpdates.length === 0) {
8
9
  return { applied: false, branches: [], commits: [] };
9
10
  }
10
11
  const baseRef = await resolveBaseRef(options.cwd, options.fixPrNoCheckout);
11
- const groups = groupUpdates(result.updates, options.groupBy);
12
+ const groups = groupUpdates(autofixUpdates, options.groupBy);
12
13
  const plans = planFixPrBatches(groups, options.fixBranch ?? "chore/rainy-updates", options.fixPrBatchSize ?? 1);
13
14
  if (options.fixDryRun) {
14
15
  return { applied: false, branches: plans.map((plan) => plan.branchName), commits: [] };
@@ -3,7 +3,8 @@ import path from "node:path";
3
3
  export async function applyFixPr(options, result, extraFiles) {
4
4
  if (!options.fixPr)
5
5
  return { applied: false };
6
- if (result.updates.length === 0) {
6
+ const autofixUpdates = result.updates.filter((update) => update.autofix !== false);
7
+ if (autofixUpdates.length === 0) {
7
8
  return {
8
9
  applied: false,
9
10
  branchName: options.fixBranch ?? "chore/rainy-updates",
@@ -35,8 +36,11 @@ export async function applyFixPr(options, result, extraFiles) {
35
36
  commitSha: "",
36
37
  };
37
38
  }
38
- const manifestFiles = Array.from(new Set(result.updates.map((update) => path.resolve(update.packagePath, "package.json"))));
39
- const filesToStage = Array.from(new Set([...manifestFiles, ...extraFiles]
39
+ const manifestFiles = Array.from(new Set(autofixUpdates.map((update) => path.resolve(update.packagePath, "package.json"))));
40
+ const lockfileFiles = options.lockfileMode === "update"
41
+ ? (await collectChangedLockfiles(options.cwd)).map((entry) => path.resolve(options.cwd, entry))
42
+ : [];
43
+ const filesToStage = Array.from(new Set([...manifestFiles, ...extraFiles, ...lockfileFiles]
40
44
  .map((entry) => path.resolve(options.cwd, entry))
41
45
  .filter((entry) => entry.startsWith(path.resolve(options.cwd) + path.sep) || entry === path.resolve(options.cwd)))).sort((a, b) => a.localeCompare(b));
42
46
  if (filesToStage.length > 0) {
@@ -50,7 +54,7 @@ export async function applyFixPr(options, result, extraFiles) {
50
54
  commitSha: "",
51
55
  };
52
56
  }
53
- const message = options.fixCommitMessage ?? `chore(deps): apply rainy-updates (${result.updates.length} updates)`;
57
+ const message = options.fixCommitMessage ?? `chore(deps): apply rainy-updates (${autofixUpdates.length} updates)`;
54
58
  await runGit(options.cwd, ["commit", "-m", message]);
55
59
  const rev = await runGit(options.cwd, ["rev-parse", "HEAD"]);
56
60
  return {
@@ -81,3 +85,14 @@ async function runGit(cwd, args, allowNonZero = false) {
81
85
  });
82
86
  });
83
87
  }
88
+ async function collectChangedLockfiles(cwd) {
89
+ const status = await runGit(cwd, ["status", "--porcelain"], true);
90
+ const allowed = new Set(["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock"]);
91
+ const changed = status.stdout
92
+ .split(/\r?\n/)
93
+ .map((line) => line.trim())
94
+ .filter(Boolean)
95
+ .map((line) => line.slice(3).trim())
96
+ .filter((entry) => allowed.has(path.basename(entry)));
97
+ return Array.from(new Set(changed)).sort((a, b) => a.localeCompare(b));
98
+ }
@@ -0,0 +1,36 @@
1
+ import type { ImpactScore, PackageUpdate } from "../types/index.js";
2
+ /**
3
+ * Context fed into the impact scorer from the outer check/upgrade pipeline.
4
+ * All fields are optional — missing data degrades gracefully.
5
+ */
6
+ export interface ImpactContext {
7
+ /** Names of packages that have a known CVE advisory (from `rup audit` cache). */
8
+ advisoryPackages?: ReadonlySet<string>;
9
+ /** How many workspace packages depend on this package (reverse-dep count). */
10
+ workspaceDependentCount?: (name: string) => number;
11
+ }
12
+ /**
13
+ * Compute an `ImpactScore` for a single pending package update.
14
+ *
15
+ * Algorithm:
16
+ * score = diffWeight
17
+ * + (advisoryBonus if CVE exists)
18
+ * + min(workspaceCount × perPkg, cap)
19
+ *
20
+ * Clamped to [0, 100]. Rank thresholds:
21
+ * ≥ 70 → critical
22
+ * ≥ 45 → high
23
+ * ≥ 20 → medium
24
+ * < 20 → low
25
+ */
26
+ export declare function computeImpactScore(update: PackageUpdate, context?: ImpactContext): ImpactScore;
27
+ /**
28
+ * Batch-compute impact scores for all updates, returning a new array with
29
+ * `impactScore` populated on each entry. Non-mutating.
30
+ */
31
+ export declare function applyImpactScores(updates: PackageUpdate[], context?: ImpactContext): PackageUpdate[];
32
+ /**
33
+ * Returns the ANSI color badge string for terminal output.
34
+ * Used by the table renderer when --show-impact is active.
35
+ */
36
+ export declare function impactBadge(score: ImpactScore): string;
@@ -0,0 +1,82 @@
1
+ /** Weights for each semver diff type. */
2
+ const DIFF_WEIGHT = {
3
+ patch: 10,
4
+ minor: 25,
5
+ major: 55,
6
+ latest: 30,
7
+ };
8
+ /** Advisory presence bonus — lifts total score significantly. */
9
+ const ADVISORY_BONUS = 35;
10
+ /** Points added per additional workspace package that uses this dep. */
11
+ const WORKSPACE_SPREAD_PER_PKG = 5;
12
+ /** Maximum workspace spread contribution (cap). */
13
+ const WORKSPACE_SPREAD_CAP = 20;
14
+ /**
15
+ * Compute an `ImpactScore` for a single pending package update.
16
+ *
17
+ * Algorithm:
18
+ * score = diffWeight
19
+ * + (advisoryBonus if CVE exists)
20
+ * + min(workspaceCount × perPkg, cap)
21
+ *
22
+ * Clamped to [0, 100]. Rank thresholds:
23
+ * ≥ 70 → critical
24
+ * ≥ 45 → high
25
+ * ≥ 20 → medium
26
+ * < 20 → low
27
+ */
28
+ export function computeImpactScore(update, context = {}) {
29
+ const diffTypeWeight = DIFF_WEIGHT[update.diffType] ?? DIFF_WEIGHT.latest;
30
+ const hasAdvisory = context.advisoryPackages?.has(update.name) ?? false;
31
+ const rawWorkspaceCount = context.workspaceDependentCount?.(update.name) ?? 0;
32
+ const affectedWorkspaceCount = Math.max(0, rawWorkspaceCount);
33
+ const advisoryPoints = hasAdvisory ? ADVISORY_BONUS : 0;
34
+ const workspacePoints = Math.min(affectedWorkspaceCount * WORKSPACE_SPREAD_PER_PKG, WORKSPACE_SPREAD_CAP);
35
+ const rawScore = diffTypeWeight + advisoryPoints + workspacePoints;
36
+ const score = Math.min(100, Math.max(0, rawScore));
37
+ const rank = scoreToRank(score);
38
+ return {
39
+ rank,
40
+ score,
41
+ factors: {
42
+ diffTypeWeight,
43
+ hasAdvisory,
44
+ affectedWorkspaceCount,
45
+ },
46
+ };
47
+ }
48
+ /**
49
+ * Batch-compute impact scores for all updates, returning a new array with
50
+ * `impactScore` populated on each entry. Non-mutating.
51
+ */
52
+ export function applyImpactScores(updates, context = {}) {
53
+ return updates.map((u) => ({
54
+ ...u,
55
+ impactScore: computeImpactScore(u, context),
56
+ }));
57
+ }
58
+ function scoreToRank(score) {
59
+ if (score >= 70)
60
+ return "critical";
61
+ if (score >= 45)
62
+ return "high";
63
+ if (score >= 20)
64
+ return "medium";
65
+ return "low";
66
+ }
67
+ /**
68
+ * Returns the ANSI color badge string for terminal output.
69
+ * Used by the table renderer when --show-impact is active.
70
+ */
71
+ export function impactBadge(score) {
72
+ switch (score.rank) {
73
+ case "critical":
74
+ return "\x1b[41m\x1b[97m CRITICAL \x1b[0m";
75
+ case "high":
76
+ return "\x1b[31m HIGH \x1b[0m";
77
+ case "medium":
78
+ return "\x1b[33m MED \x1b[0m";
79
+ case "low":
80
+ return "\x1b[32m LOW \x1b[0m";
81
+ }
82
+ }
@@ -46,14 +46,14 @@ function installStep(packageManager) {
46
46
  return ` - name: Install dependencies\n run: npm ci`;
47
47
  }
48
48
  function minimalWorkflowTemplate(scheduleBlock, packageManager) {
49
- return `name: Rainy Updates\n\non:\n${scheduleBlock}\n\njobs:\n dependency-check:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Setup Node\n uses: actions/setup-node@v4\n with:\n node-version: '20'\n\n${installStep(packageManager)}\n\n - name: Run dependency check\n run: |\n npx @rainy-updates/cli ci \\\n --workspace \\\n --mode minimal \\\n --ci \\\n --format table\n`;
49
+ return `name: Rainy Updates\n\non:\n${scheduleBlock}\n\njobs:\n dependency-check:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Setup Node\n uses: actions/setup-node@v4\n with:\n node-version: '20'\n\n${installStep(packageManager)}\n\n - name: Run dependency check\n run: |\n npx @rainy-updates/cli ci \\\n --workspace \\\n --mode minimal \\\n --ci \\\n --stream \\\n --registry-timeout-ms 12000 \\\n --registry-retries 4 \\\n --format table\n`;
50
50
  }
51
51
  function strictWorkflowTemplate(scheduleBlock, packageManager) {
52
- return `name: Rainy Updates\n\non:\n${scheduleBlock}\n\npermissions:\n contents: read\n security-events: write\n\njobs:\n dependency-check:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Setup Node\n uses: actions/setup-node@v4\n with:\n node-version: '20'\n\n${installStep(packageManager)}\n\n - name: Warm cache\n run: npx @rainy-updates/cli warm-cache --workspace --concurrency 32\n\n - name: Run strict dependency check\n run: |\n npx @rainy-updates/cli ci \\\n --workspace \\\n --mode strict \\\n --ci \\\n --concurrency 32 \\\n --format github \\\n --json-file .artifacts/deps-report.json \\\n --pr-report-file .artifacts/deps-report.md \\\n --sarif-file .artifacts/deps-report.sarif \\\n --github-output $GITHUB_OUTPUT\n\n - name: Upload report artifacts\n uses: actions/upload-artifact@v4\n with:\n name: rainy-updates-report\n path: .artifacts/\n\n - name: Upload SARIF\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: .artifacts/deps-report.sarif\n`;
52
+ return `name: Rainy Updates\n\non:\n${scheduleBlock}\n\npermissions:\n contents: read\n security-events: write\n\njobs:\n dependency-check:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Setup Node\n uses: actions/setup-node@v4\n with:\n node-version: '20'\n\n${installStep(packageManager)}\n\n - name: Warm cache\n run: npx @rainy-updates/cli warm-cache --workspace --concurrency 32 --registry-timeout-ms 12000 --registry-retries 4\n\n - name: Run strict dependency check\n run: |\n npx @rainy-updates/cli ci \\\n --workspace \\\n --mode strict \\\n --ci \\\n --concurrency 32 \\\n --stream \\\n --registry-timeout-ms 12000 \\\n --registry-retries 4 \\\n --format github \\\n --json-file .artifacts/deps-report.json \\\n --pr-report-file .artifacts/deps-report.md \\\n --sarif-file .artifacts/deps-report.sarif \\\n --github-output $GITHUB_OUTPUT\n\n - name: Upload report artifacts\n uses: actions/upload-artifact@v4\n with:\n name: rainy-updates-report\n path: .artifacts/\n\n - name: Upload SARIF\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: .artifacts/deps-report.sarif\n`;
53
53
  }
54
54
  function enterpriseWorkflowTemplate(scheduleBlock, packageManager) {
55
55
  const detectedPmInstall = packageManager === "pnpm"
56
56
  ? "corepack enable && corepack prepare pnpm@9 --activate && pnpm install --frozen-lockfile"
57
57
  : "npm ci";
58
- return `name: Rainy Updates Enterprise\n\non:\n${scheduleBlock}\n\npermissions:\n contents: read\n security-events: write\n actions: read\n\nconcurrency:\n group: rainy-updates-\${{ github.ref }}\n cancel-in-progress: false\n\njobs:\n dependency-check:\n runs-on: ubuntu-latest\n strategy:\n fail-fast: false\n matrix:\n node: [20, 22]\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Setup Node\n uses: actions/setup-node@v4\n with:\n node-version: \${{ matrix.node }}\n\n - name: Install dependencies\n run: ${detectedPmInstall}\n\n - name: Warm cache\n run: npx @rainy-updates/cli warm-cache --workspace --concurrency 32\n\n - name: Check updates with rollout controls\n run: |\n npx @rainy-updates/cli ci \\\n --workspace \\\n --mode enterprise \\\n --concurrency 32 \\\n --format github \\\n --fail-on minor \\\n --max-updates 50 \\\n --json-file .artifacts/deps-report-node-\${{ matrix.node }}.json \\\n --pr-report-file .artifacts/deps-report-node-\${{ matrix.node }}.md \\\n --sarif-file .artifacts/deps-report-node-\${{ matrix.node }}.sarif \\\n --github-output $GITHUB_OUTPUT\n\n - name: Upload report artifacts\n uses: actions/upload-artifact@v4\n with:\n name: rainy-updates-report-node-\${{ matrix.node }}\n path: .artifacts/\n retention-days: 14\n\n - name: Upload SARIF\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: .artifacts/deps-report-node-\${{ matrix.node }}.sarif\n`;
58
+ return `name: Rainy Updates Enterprise\n\non:\n${scheduleBlock}\n\npermissions:\n contents: read\n security-events: write\n actions: read\n\nconcurrency:\n group: rainy-updates-\${{ github.ref }}\n cancel-in-progress: false\n\njobs:\n dependency-check:\n runs-on: ubuntu-latest\n strategy:\n fail-fast: false\n matrix:\n node: [20, 22]\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Setup Node\n uses: actions/setup-node@v4\n with:\n node-version: \${{ matrix.node }}\n\n - name: Install dependencies\n run: ${detectedPmInstall}\n\n - name: Warm cache\n run: npx @rainy-updates/cli warm-cache --workspace --concurrency 32 --registry-timeout-ms 12000 --registry-retries 4\n\n - name: Check updates with rollout controls\n run: |\n npx @rainy-updates/cli ci \\\n --workspace \\\n --mode enterprise \\\n --concurrency 32 \\\n --stream \\\n --registry-timeout-ms 12000 \\\n --registry-retries 4 \\\n --lockfile-mode preserve \\\n --format github \\\n --fail-on minor \\\n --max-updates 50 \\\n --json-file .artifacts/deps-report-node-\${{ matrix.node }}.json \\\n --pr-report-file .artifacts/deps-report-node-\${{ matrix.node }}.md \\\n --sarif-file .artifacts/deps-report-node-\${{ matrix.node }}.sarif \\\n --github-output $GITHUB_OUTPUT\n\n - name: Upload report artifacts\n uses: actions/upload-artifact@v4\n with:\n name: rainy-updates-report-node-\${{ matrix.node }}\n path: .artifacts/\n retention-days: 14\n\n - name: Upload SARIF\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: .artifacts/deps-report-node-\${{ matrix.node }}.sarif\n`;
59
59
  }
@@ -1,4 +1,4 @@
1
- import type { BaselineOptions, CheckOptions, UpgradeOptions } from "../types/index.js";
1
+ import type { BaselineOptions, CheckOptions, UpgradeOptions, AuditOptions, BisectOptions, HealthOptions, UnusedOptions, ResolveOptions, LicenseOptions, SnapshotOptions } from "../types/index.js";
2
2
  import type { InitCiMode, InitCiSchedule } from "./init-ci.js";
3
3
  export type ParsedCliArgs = {
4
4
  command: "check";
@@ -25,5 +25,26 @@ export type ParsedCliArgs = {
25
25
  options: BaselineOptions & {
26
26
  action: "save" | "check";
27
27
  };
28
+ } | {
29
+ command: "bisect";
30
+ options: BisectOptions;
31
+ } | {
32
+ command: "audit";
33
+ options: AuditOptions;
34
+ } | {
35
+ command: "health";
36
+ options: HealthOptions;
37
+ } | {
38
+ command: "unused";
39
+ options: UnusedOptions;
40
+ } | {
41
+ command: "resolve";
42
+ options: ResolveOptions;
43
+ } | {
44
+ command: "licenses";
45
+ options: LicenseOptions;
46
+ } | {
47
+ command: "snapshot";
48
+ options: SnapshotOptions;
28
49
  };
29
50
  export declare function parseCliArgs(argv: string[]): Promise<ParsedCliArgs>;