@rainy-updates/cli 0.4.4 → 0.5.0-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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [0.5.0-rc.1] - 2026-02-27
6
+
7
+ ### Added
8
+
9
+ - New CI rollout controls:
10
+ - `--fail-on none|patch|minor|major|any`
11
+ - `--max-updates <n>`
12
+ - New baseline workflow command:
13
+ - `baseline --save --file <path>` to snapshot dependency state
14
+ - `baseline --check --file <path>` to detect dependency drift
15
+ - New `init-ci --mode enterprise` template:
16
+ - Node runtime matrix (`20`, `22`)
17
+ - stricter default permissions
18
+ - artifact retention policy
19
+ - built-in rollout gate flags (`--fail-on`, `--max-updates`)
20
+
21
+ ### Changed
22
+
23
+ - Dependency target selection now evaluates available package versions from registry metadata, improving `patch|minor|major` accuracy.
24
+ - CLI parser now rejects unknown options and missing option values with explicit errors (safer CI behavior).
25
+ - SARIF output now reports the actual package version dynamically.
26
+
27
+ ### Tests
28
+
29
+ - Added baseline snapshot/diff tests.
30
+ - Added enterprise workflow generation tests.
31
+ - Added semver target selection tests using available version sets.
32
+ - Added parser tests for baseline command, rollout flags, and unknown option rejection.
33
+
5
34
  ## [0.4.4] - 2026-02-27
6
35
 
7
36
  ### Changed
package/README.md CHANGED
@@ -25,6 +25,7 @@ pnpm add -D @rainy-updates/cli
25
25
  - `check`: analyze dependencies and report available updates.
26
26
  - `upgrade`: rewrite dependency ranges in manifests, optionally install lockfile updates.
27
27
  - `warm-cache`: prefetch package metadata for fast and offline checks.
28
+ - `baseline`: save and compare dependency baseline snapshots.
28
29
 
29
30
  ## Quick usage
30
31
 
@@ -41,6 +42,10 @@ npx @rainy-updates/cli upgrade --target latest --workspace --sync --install
41
42
  # 4) Warm cache for deterministic offline checks
42
43
  npx @rainy-updates/cli warm-cache --workspace --concurrency 32
43
44
  npx @rainy-updates/cli check --workspace --offline --ci
45
+
46
+ # 5) Save and compare baseline drift in CI
47
+ npx @rainy-updates/cli baseline --save --file .artifacts/deps-baseline.json --workspace
48
+ npx @rainy-updates/cli baseline --check --file .artifacts/deps-baseline.json --workspace --ci
44
49
  ```
45
50
 
46
51
  ## What it does in production
@@ -108,7 +113,7 @@ Generate a workflow in the target project automatically:
108
113
 
109
114
  ```bash
110
115
  # strict mode (recommended)
111
- npx @rainy-updates/cli init-ci --mode strict --schedule weekly
116
+ npx @rainy-updates/cli init-ci --mode enterprise --schedule weekly
112
117
 
113
118
  # lightweight mode
114
119
  npx @rainy-updates/cli init-ci --mode minimal --schedule daily
@@ -121,6 +126,7 @@ Generated file:
121
126
  Modes:
122
127
 
123
128
  - `strict`: warm-cache + offline check + artifacts + SARIF upload.
129
+ - `enterprise`: strict checks + runtime matrix + retention policy + rollout gates.
124
130
  - `minimal`: fast check-only workflow for quick adoption.
125
131
 
126
132
  Schedule:
@@ -140,6 +146,8 @@ Schedule:
140
146
  - `--concurrency <n>`
141
147
  - `--cache-ttl <seconds>`
142
148
  - `--offline`
149
+ - `--fail-on none|patch|minor|major|any`
150
+ - `--max-updates <n>`
143
151
  - `--policy-file <path>`
144
152
  - `--format table|json|minimal|github`
145
153
  - `--json-file <path>`
@@ -154,6 +162,12 @@ Schedule:
154
162
  - `--pm auto|npm|pnpm`
155
163
  - `--sync`
156
164
 
165
+ ### Baseline-only
166
+
167
+ - `--save`
168
+ - `--check`
169
+ - `--file <path>`
170
+
157
171
  ## Config support
158
172
 
159
173
  Configuration can be loaded from:
package/dist/bin/cli.js CHANGED
@@ -8,6 +8,7 @@ import { check } from "../core/check.js";
8
8
  import { upgrade } from "../core/upgrade.js";
9
9
  import { warmCache } from "../core/warm-cache.js";
10
10
  import { initCiWorkflow } from "../core/init-ci.js";
11
+ import { diffBaseline, saveBaseline } from "../core/baseline.js";
11
12
  import { renderResult } from "../output/format.js";
12
13
  import { writeGitHubOutput } from "../output/github.js";
13
14
  import { createSarifReport } from "../output/sarif.js";
@@ -34,6 +35,31 @@ async function main() {
34
35
  : `CI workflow already exists at ${workflow.path}. Use --force to overwrite.\n`);
35
36
  return;
36
37
  }
38
+ if (parsed.command === "baseline") {
39
+ if (parsed.options.action === "save") {
40
+ const saved = await saveBaseline(parsed.options);
41
+ process.stdout.write(`Saved baseline at ${saved.filePath} (${saved.entries} entries)\n`);
42
+ return;
43
+ }
44
+ const diff = await diffBaseline(parsed.options);
45
+ const changes = diff.added.length + diff.removed.length + diff.changed.length;
46
+ if (changes === 0) {
47
+ process.stdout.write(`No baseline drift detected (${diff.filePath}).\n`);
48
+ return;
49
+ }
50
+ process.stdout.write(`Baseline drift detected (${diff.filePath}).\n`);
51
+ if (diff.added.length > 0) {
52
+ process.stdout.write(`Added: ${diff.added.length}\n`);
53
+ }
54
+ if (diff.removed.length > 0) {
55
+ process.stdout.write(`Removed: ${diff.removed.length}\n`);
56
+ }
57
+ if (diff.changed.length > 0) {
58
+ process.stdout.write(`Changed: ${diff.changed.length}\n`);
59
+ }
60
+ process.exitCode = 1;
61
+ return;
62
+ }
37
63
  const result = parsed.command === "upgrade"
38
64
  ? await upgrade(parsed.options)
39
65
  : parsed.command === "warm-cache"
@@ -58,13 +84,7 @@ async function main() {
58
84
  await fs.mkdir(path.dirname(parsed.options.sarifFile), { recursive: true });
59
85
  await fs.writeFile(parsed.options.sarifFile, JSON.stringify(sarif, null, 2) + "\n", "utf8");
60
86
  }
61
- if (parsed.options.ci && result.updates.length > 0) {
62
- process.exitCode = 1;
63
- return;
64
- }
65
- if (result.errors.length > 0) {
66
- process.exitCode = 2;
67
- }
87
+ process.exitCode = resolveExitCode(result, parsed.options.failOn, parsed.options.maxUpdates, parsed.options.ci);
68
88
  }
69
89
  catch (error) {
70
90
  process.stderr.write(`rainy-updates: ${String(error)}\n`);
@@ -117,8 +137,21 @@ Create a GitHub Actions workflow template at:
117
137
 
118
138
  Options:
119
139
  --force
120
- --mode minimal|strict
140
+ --mode minimal|strict|enterprise
121
141
  --schedule weekly|daily|off`;
142
+ }
143
+ if (isCommand && command === "baseline") {
144
+ return `rainy-updates baseline [options]
145
+
146
+ Save or compare dependency baseline snapshots.
147
+
148
+ Options:
149
+ --save
150
+ --check
151
+ --file <path>
152
+ --workspace
153
+ --dep-kinds deps,dev,optional,peer
154
+ --ci`;
122
155
  }
123
156
  return `rainy-updates <command> [options]
124
157
 
@@ -127,6 +160,7 @@ Commands:
127
160
  upgrade Apply updates to manifests
128
161
  warm-cache Warm local cache for fast/offline checks
129
162
  init-ci Scaffold GitHub Actions workflow
163
+ baseline Save/check dependency baseline snapshots
130
164
 
131
165
  Global options:
132
166
  --cwd <path>
@@ -138,6 +172,8 @@ Global options:
138
172
  --sarif-file <path>
139
173
  --pr-report-file <path>
140
174
  --policy-file <path>
175
+ --fail-on none|patch|minor|major|any
176
+ --max-updates <n>
141
177
  --concurrency <n>
142
178
  --cache-ttl <seconds>
143
179
  --offline
@@ -152,3 +188,22 @@ async function readPackageVersion() {
152
188
  const parsed = JSON.parse(content);
153
189
  return parsed.version ?? "0.0.0";
154
190
  }
191
+ function resolveExitCode(result, failOn, maxUpdates, ciMode) {
192
+ if (result.errors.length > 0)
193
+ return 2;
194
+ if (typeof maxUpdates === "number" && result.updates.length > maxUpdates)
195
+ return 1;
196
+ const effectiveFailOn = failOn && failOn !== "none" ? failOn : ciMode ? "any" : "none";
197
+ if (!shouldFailForUpdates(result.updates, effectiveFailOn))
198
+ return 0;
199
+ return 1;
200
+ }
201
+ function shouldFailForUpdates(updates, failOn) {
202
+ if (failOn === "none")
203
+ return false;
204
+ if (failOn === "any" || failOn === "patch")
205
+ return updates.length > 0;
206
+ if (failOn === "minor")
207
+ return updates.some((update) => update.diffType === "minor" || update.diffType === "major");
208
+ return updates.some((update) => update.diffType === "major");
209
+ }
@@ -5,5 +5,5 @@ export declare class VersionCache {
5
5
  static create(customPath?: string): Promise<VersionCache>;
6
6
  getValid(packageName: string, target: TargetLevel): Promise<CachedVersion | null>;
7
7
  getAny(packageName: string, target: TargetLevel): Promise<CachedVersion | null>;
8
- set(packageName: string, target: TargetLevel, latestVersion: string, ttlSeconds: number): Promise<void>;
8
+ set(packageName: string, target: TargetLevel, latestVersion: string, availableVersions: string[], ttlSeconds: number): Promise<void>;
9
9
  }
@@ -9,7 +9,13 @@ class FileCacheStore {
9
9
  async get(packageName, target) {
10
10
  const entries = await this.readEntries();
11
11
  const key = this.getKey(packageName, target);
12
- return entries[key] ?? null;
12
+ const entry = entries[key];
13
+ if (!entry)
14
+ return null;
15
+ return {
16
+ ...entry,
17
+ availableVersions: Array.isArray(entry.availableVersions) ? entry.availableVersions : [entry.latestVersion],
18
+ };
13
19
  }
14
20
  async set(entry) {
15
21
  const entries = await this.readEntries();
@@ -39,31 +45,63 @@ class SqliteCacheStore {
39
45
  package_name TEXT NOT NULL,
40
46
  target TEXT NOT NULL,
41
47
  latest_version TEXT NOT NULL,
48
+ available_versions TEXT NOT NULL,
42
49
  fetched_at INTEGER NOT NULL,
43
50
  ttl_seconds INTEGER NOT NULL,
44
51
  PRIMARY KEY (package_name, target)
45
52
  );
46
53
  `);
54
+ this.ensureSchema();
47
55
  }
48
56
  async get(packageName, target) {
49
- const row = this.db
50
- .prepare(`SELECT package_name, target, latest_version, fetched_at, ttl_seconds FROM versions WHERE package_name = ? AND target = ?`)
51
- .get(packageName, target);
57
+ let row;
58
+ try {
59
+ row = this.db
60
+ .prepare(`SELECT package_name, target, latest_version, available_versions, fetched_at, ttl_seconds FROM versions WHERE package_name = ? AND target = ?`)
61
+ .get(packageName, target);
62
+ }
63
+ catch {
64
+ row = this.db
65
+ .prepare(`SELECT package_name, target, latest_version, fetched_at, ttl_seconds FROM versions WHERE package_name = ? AND target = ?`)
66
+ .get(packageName, target);
67
+ }
52
68
  if (!row)
53
69
  return null;
54
70
  return {
55
71
  packageName: row.package_name,
56
72
  target: row.target,
57
73
  latestVersion: row.latest_version,
74
+ availableVersions: parseJsonArray(row.available_versions ?? row.latest_version, row.latest_version),
58
75
  fetchedAt: row.fetched_at,
59
76
  ttlSeconds: row.ttl_seconds,
60
77
  };
61
78
  }
62
79
  async set(entry) {
63
- this.db
64
- .prepare(`INSERT OR REPLACE INTO versions (package_name, target, latest_version, fetched_at, ttl_seconds)
65
- VALUES (?, ?, ?, ?, ?)`)
66
- .run(entry.packageName, entry.target, entry.latestVersion, entry.fetchedAt, entry.ttlSeconds);
80
+ try {
81
+ this.db
82
+ .prepare(`INSERT OR REPLACE INTO versions (package_name, target, latest_version, available_versions, fetched_at, ttl_seconds)
83
+ VALUES (?, ?, ?, ?, ?, ?)`)
84
+ .run(entry.packageName, entry.target, entry.latestVersion, JSON.stringify(entry.availableVersions), entry.fetchedAt, entry.ttlSeconds);
85
+ }
86
+ catch {
87
+ this.db
88
+ .prepare(`INSERT OR REPLACE INTO versions (package_name, target, latest_version, fetched_at, ttl_seconds)
89
+ VALUES (?, ?, ?, ?, ?)`)
90
+ .run(entry.packageName, entry.target, entry.latestVersion, entry.fetchedAt, entry.ttlSeconds);
91
+ }
92
+ }
93
+ ensureSchema() {
94
+ try {
95
+ const columns = this.db.prepare("PRAGMA table_info(versions);").all();
96
+ const hasAvailableVersions = columns.some((column) => column.name === "available_versions");
97
+ if (!hasAvailableVersions) {
98
+ this.db.exec("ALTER TABLE versions ADD COLUMN available_versions TEXT;");
99
+ }
100
+ this.db.exec("UPDATE versions SET available_versions = latest_version WHERE available_versions IS NULL;");
101
+ }
102
+ catch {
103
+ // Best-effort migration.
104
+ }
67
105
  }
68
106
  }
69
107
  export class VersionCache {
@@ -92,11 +130,12 @@ export class VersionCache {
92
130
  async getAny(packageName, target) {
93
131
  return this.store.get(packageName, target);
94
132
  }
95
- async set(packageName, target, latestVersion, ttlSeconds) {
133
+ async set(packageName, target, latestVersion, availableVersions, ttlSeconds) {
96
134
  await this.store.set({
97
135
  packageName,
98
136
  target,
99
137
  latestVersion,
138
+ availableVersions,
100
139
  fetchedAt: Date.now(),
101
140
  ttlSeconds,
102
141
  });
@@ -115,3 +154,17 @@ async function tryCreateSqliteStore(dbPath) {
115
154
  }
116
155
  return null;
117
156
  }
157
+ function parseJsonArray(raw, fallback) {
158
+ if (typeof raw !== "string")
159
+ return [fallback];
160
+ try {
161
+ const parsed = JSON.parse(raw);
162
+ if (!Array.isArray(parsed))
163
+ return [fallback];
164
+ const values = parsed.filter((value) => typeof value === "string");
165
+ return values.length > 0 ? values : [fallback];
166
+ }
167
+ catch {
168
+ return [fallback];
169
+ }
170
+ }
@@ -1,4 +1,4 @@
1
- import type { DependencyKind, OutputFormat, TargetLevel } from "../types/index.js";
1
+ import type { DependencyKind, FailOnLevel, OutputFormat, TargetLevel } from "../types/index.js";
2
2
  export interface FileConfig {
3
3
  target?: TargetLevel;
4
4
  filter?: string;
@@ -15,6 +15,8 @@ export interface FileConfig {
15
15
  offline?: boolean;
16
16
  policyFile?: string;
17
17
  prReportFile?: string;
18
+ failOn?: FailOnLevel;
19
+ maxUpdates?: number;
18
20
  install?: boolean;
19
21
  packageManager?: "auto" | "npm" | "pnpm";
20
22
  sync?: boolean;
@@ -0,0 +1,23 @@
1
+ import type { BaselineOptions, DependencyKind } from "../types/index.js";
2
+ interface BaselineEntry {
3
+ packagePath: string;
4
+ kind: DependencyKind;
5
+ name: string;
6
+ range: string;
7
+ }
8
+ export interface BaselineSaveResult {
9
+ filePath: string;
10
+ entries: number;
11
+ }
12
+ export interface BaselineDiffResult {
13
+ filePath: string;
14
+ added: BaselineEntry[];
15
+ removed: BaselineEntry[];
16
+ changed: Array<{
17
+ before: BaselineEntry;
18
+ after: BaselineEntry;
19
+ }>;
20
+ }
21
+ export declare function saveBaseline(options: BaselineOptions): Promise<BaselineSaveResult>;
22
+ export declare function diffBaseline(options: BaselineOptions): Promise<BaselineDiffResult>;
23
+ export {};
@@ -0,0 +1,72 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { collectDependencies, readManifest } from "../parsers/package-json.js";
4
+ import { discoverPackageDirs } from "../workspace/discover.js";
5
+ export async function saveBaseline(options) {
6
+ const entries = await collectBaselineEntries(options.cwd, options.workspace, options.includeKinds);
7
+ const payload = {
8
+ version: 1,
9
+ createdAt: new Date().toISOString(),
10
+ entries,
11
+ };
12
+ await fs.mkdir(path.dirname(options.filePath), { recursive: true });
13
+ await fs.writeFile(options.filePath, JSON.stringify(payload, null, 2) + "\n", "utf8");
14
+ return {
15
+ filePath: options.filePath,
16
+ entries: entries.length,
17
+ };
18
+ }
19
+ export async function diffBaseline(options) {
20
+ const content = await fs.readFile(options.filePath, "utf8");
21
+ const baseline = JSON.parse(content);
22
+ const currentEntries = await collectBaselineEntries(options.cwd, options.workspace, options.includeKinds);
23
+ const baselineMap = new Map(baseline.entries.map((entry) => [toKey(entry), entry]));
24
+ const currentMap = new Map(currentEntries.map((entry) => [toKey(entry), entry]));
25
+ const added = [];
26
+ const removed = [];
27
+ const changed = [];
28
+ for (const [key, current] of currentMap) {
29
+ const base = baselineMap.get(key);
30
+ if (!base) {
31
+ added.push(current);
32
+ continue;
33
+ }
34
+ if (base.range !== current.range) {
35
+ changed.push({ before: base, after: current });
36
+ }
37
+ }
38
+ for (const [key, base] of baselineMap) {
39
+ if (!currentMap.has(key)) {
40
+ removed.push(base);
41
+ }
42
+ }
43
+ return {
44
+ filePath: options.filePath,
45
+ added: sortEntries(added),
46
+ removed: sortEntries(removed),
47
+ changed: changed.sort((a, b) => toKey(a.after).localeCompare(toKey(b.after))),
48
+ };
49
+ }
50
+ async function collectBaselineEntries(cwd, workspace, includeKinds) {
51
+ const packageDirs = await discoverPackageDirs(cwd, workspace);
52
+ const entries = [];
53
+ for (const packageDir of packageDirs) {
54
+ const manifest = await readManifest(packageDir);
55
+ const deps = collectDependencies(manifest, includeKinds);
56
+ for (const dep of deps) {
57
+ entries.push({
58
+ packagePath: path.relative(cwd, packageDir) || ".",
59
+ kind: dep.kind,
60
+ name: dep.name,
61
+ range: dep.range,
62
+ });
63
+ }
64
+ }
65
+ return sortEntries(entries);
66
+ }
67
+ function toKey(entry) {
68
+ return `${entry.packagePath}::${entry.kind}::${entry.name}`;
69
+ }
70
+ function sortEntries(entries) {
71
+ return [...entries].sort((a, b) => toKey(a).localeCompare(toKey(b)));
72
+ }
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { collectDependencies, readManifest } from "../parsers/package-json.js";
3
3
  import { matchesPattern } from "../utils/pattern.js";
4
- import { applyRangeStyle, classifyDiff, clampTarget, pickTargetVersion } from "../utils/semver.js";
4
+ import { applyRangeStyle, classifyDiff, clampTarget, pickTargetVersionFromAvailable } from "../utils/semver.js";
5
5
  import { VersionCache } from "../cache/cache.js";
6
6
  import { NpmRegistryClient } from "../registry/npm.js";
7
7
  import { detectPackageManager } from "../pm/detect.js";
@@ -53,7 +53,10 @@ export async function check(options) {
53
53
  for (const packageName of uniquePackageNames) {
54
54
  const cached = await cache.getValid(packageName, options.target);
55
55
  if (cached) {
56
- resolvedVersions.set(packageName, cached.latestVersion);
56
+ resolvedVersions.set(packageName, {
57
+ latestVersion: cached.latestVersion,
58
+ availableVersions: cached.availableVersions,
59
+ });
57
60
  }
58
61
  else {
59
62
  unresolvedPackages.push(packageName);
@@ -64,7 +67,10 @@ export async function check(options) {
64
67
  for (const packageName of unresolvedPackages) {
65
68
  const stale = await cache.getAny(packageName, options.target);
66
69
  if (stale) {
67
- resolvedVersions.set(packageName, stale.latestVersion);
70
+ resolvedVersions.set(packageName, {
71
+ latestVersion: stale.latestVersion,
72
+ availableVersions: stale.availableVersions,
73
+ });
68
74
  warnings.push(`Using stale cache for ${packageName} because --offline is enabled.`);
69
75
  }
70
76
  else {
@@ -73,19 +79,25 @@ export async function check(options) {
73
79
  }
74
80
  }
75
81
  else {
76
- const fetched = await registryClient.resolveManyLatestVersions(unresolvedPackages, {
82
+ const fetched = await registryClient.resolveManyPackageMetadata(unresolvedPackages, {
77
83
  concurrency: options.concurrency,
78
84
  });
79
- for (const [packageName, version] of fetched.versions) {
80
- resolvedVersions.set(packageName, version);
81
- if (version) {
82
- await cache.set(packageName, options.target, version, options.cacheTtlSeconds);
85
+ for (const [packageName, metadata] of fetched.metadata) {
86
+ resolvedVersions.set(packageName, {
87
+ latestVersion: metadata.latestVersion,
88
+ availableVersions: metadata.versions,
89
+ });
90
+ if (metadata.latestVersion) {
91
+ await cache.set(packageName, options.target, metadata.latestVersion, metadata.versions, options.cacheTtlSeconds);
83
92
  }
84
93
  }
85
94
  for (const [packageName, error] of fetched.errors) {
86
95
  const stale = await cache.getAny(packageName, options.target);
87
96
  if (stale) {
88
- resolvedVersions.set(packageName, stale.latestVersion);
97
+ resolvedVersions.set(packageName, {
98
+ latestVersion: stale.latestVersion,
99
+ availableVersions: stale.availableVersions,
100
+ });
89
101
  warnings.push(`Using stale cache for ${packageName} due to registry error: ${error}`);
90
102
  }
91
103
  else {
@@ -95,12 +107,12 @@ export async function check(options) {
95
107
  }
96
108
  }
97
109
  for (const task of tasks) {
98
- const latestVersion = resolvedVersions.get(task.dependency.name);
99
- if (!latestVersion)
110
+ const metadata = resolvedVersions.get(task.dependency.name);
111
+ if (!metadata?.latestVersion)
100
112
  continue;
101
113
  const rule = policy.packageRules.get(task.dependency.name);
102
114
  const effectiveTarget = clampTarget(options.target, rule?.maxTarget);
103
- const picked = pickTargetVersion(task.dependency.range, latestVersion, effectiveTarget);
115
+ const picked = pickTargetVersionFromAvailable(task.dependency.range, metadata.availableVersions, metadata.latestVersion, effectiveTarget);
104
116
  if (!picked)
105
117
  continue;
106
118
  const nextRange = applyRangeStyle(task.dependency.range, picked);
@@ -1,4 +1,4 @@
1
- export type InitCiMode = "minimal" | "strict";
1
+ export type InitCiMode = "minimal" | "strict" | "enterprise";
2
2
  export type InitCiSchedule = "weekly" | "daily" | "off";
3
3
  export interface InitCiOptions {
4
4
  mode: InitCiMode;
@@ -15,7 +15,9 @@ export async function initCiWorkflow(cwd, force, options) {
15
15
  const scheduleBlock = renderScheduleBlock(options.schedule);
16
16
  const workflow = options.mode === "minimal"
17
17
  ? minimalWorkflowTemplate(scheduleBlock, packageManager)
18
- : strictWorkflowTemplate(scheduleBlock, packageManager);
18
+ : options.mode === "strict"
19
+ ? strictWorkflowTemplate(scheduleBlock, packageManager)
20
+ : enterpriseWorkflowTemplate(scheduleBlock, packageManager);
19
21
  await mkdir(path.dirname(workflowPath), { recursive: true });
20
22
  await writeFile(workflowPath, workflow, "utf8");
21
23
  return { path: workflowPath, created: true };
@@ -49,3 +51,9 @@ function minimalWorkflowTemplate(scheduleBlock, packageManager) {
49
51
  function strictWorkflowTemplate(scheduleBlock, packageManager) {
50
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 check \\\n --workspace \\\n --offline \\\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`;
51
53
  }
54
+ function enterpriseWorkflowTemplate(scheduleBlock, packageManager) {
55
+ const detectedPmInstall = packageManager === "pnpm"
56
+ ? "corepack enable && corepack prepare pnpm@9 --activate && pnpm install --frozen-lockfile"
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 check \\\n --workspace \\\n --offline \\\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`;
59
+ }
@@ -1,4 +1,4 @@
1
- import type { CheckOptions, UpgradeOptions } from "../types/index.js";
1
+ import type { BaselineOptions, CheckOptions, UpgradeOptions } from "../types/index.js";
2
2
  import type { InitCiMode, InitCiSchedule } from "./init-ci.js";
3
3
  export type ParsedCliArgs = {
4
4
  command: "check";
@@ -17,5 +17,10 @@ export type ParsedCliArgs = {
17
17
  mode: InitCiMode;
18
18
  schedule: InitCiSchedule;
19
19
  };
20
+ } | {
21
+ command: "baseline";
22
+ options: BaselineOptions & {
23
+ action: "save" | "check";
24
+ };
20
25
  };
21
26
  export declare function parseCliArgs(argv: string[]): Promise<ParsedCliArgs>;
@@ -7,7 +7,7 @@ const DEFAULT_INCLUDE_KINDS = [
7
7
  "optionalDependencies",
8
8
  "peerDependencies",
9
9
  ];
10
- const KNOWN_COMMANDS = ["check", "upgrade", "warm-cache", "init-ci"];
10
+ const KNOWN_COMMANDS = ["check", "upgrade", "warm-cache", "init-ci", "baseline"];
11
11
  export async function parseCliArgs(argv) {
12
12
  const firstArg = argv[0];
13
13
  const isKnownCommand = KNOWN_COMMANDS.includes(firstArg);
@@ -34,10 +34,14 @@ export async function parseCliArgs(argv) {
34
34
  offline: false,
35
35
  policyFile: undefined,
36
36
  prReportFile: undefined,
37
+ failOn: "none",
38
+ maxUpdates: undefined,
37
39
  };
38
40
  let force = false;
39
- let initCiMode = "strict";
41
+ let initCiMode = "enterprise";
40
42
  let initCiSchedule = "weekly";
43
+ let baselineAction = "check";
44
+ let baselineFilePath = path.resolve(base.cwd, ".rainy-updates-baseline.json");
41
45
  let resolvedConfig = await loadConfig(base.cwd);
42
46
  applyConfig(base, resolvedConfig);
43
47
  for (let index = 0; index < args.length; index += 1) {
@@ -48,23 +52,36 @@ export async function parseCliArgs(argv) {
48
52
  index += 1;
49
53
  continue;
50
54
  }
55
+ if (current === "--target") {
56
+ throw new Error("Missing value for --target");
57
+ }
51
58
  if (current === "--filter" && next) {
52
59
  base.filter = next;
53
60
  index += 1;
54
61
  continue;
55
62
  }
63
+ if (current === "--filter") {
64
+ throw new Error("Missing value for --filter");
65
+ }
56
66
  if (current === "--reject" && next) {
57
67
  base.reject = next;
58
68
  index += 1;
59
69
  continue;
60
70
  }
71
+ if (current === "--reject") {
72
+ throw new Error("Missing value for --reject");
73
+ }
61
74
  if (current === "--cwd" && next) {
62
75
  base.cwd = path.resolve(next);
63
76
  resolvedConfig = await loadConfig(base.cwd);
64
77
  applyConfig(base, resolvedConfig);
78
+ baselineFilePath = path.resolve(base.cwd, ".rainy-updates-baseline.json");
65
79
  index += 1;
66
80
  continue;
67
81
  }
82
+ if (current === "--cwd") {
83
+ throw new Error("Missing value for --cwd");
84
+ }
68
85
  if (current === "--cache-ttl" && next) {
69
86
  const parsed = Number(next);
70
87
  if (!Number.isFinite(parsed) || parsed < 0) {
@@ -74,11 +91,17 @@ export async function parseCliArgs(argv) {
74
91
  index += 1;
75
92
  continue;
76
93
  }
94
+ if (current === "--cache-ttl") {
95
+ throw new Error("Missing value for --cache-ttl");
96
+ }
77
97
  if (current === "--format" && next) {
78
98
  base.format = ensureFormat(next);
79
99
  index += 1;
80
100
  continue;
81
101
  }
102
+ if (current === "--format") {
103
+ throw new Error("Missing value for --format");
104
+ }
82
105
  if (current === "--ci") {
83
106
  base.ci = true;
84
107
  continue;
@@ -92,16 +115,25 @@ export async function parseCliArgs(argv) {
92
115
  index += 1;
93
116
  continue;
94
117
  }
118
+ if (current === "--json-file") {
119
+ throw new Error("Missing value for --json-file");
120
+ }
95
121
  if (current === "--github-output" && next) {
96
122
  base.githubOutputFile = path.resolve(next);
97
123
  index += 1;
98
124
  continue;
99
125
  }
126
+ if (current === "--github-output") {
127
+ throw new Error("Missing value for --github-output");
128
+ }
100
129
  if (current === "--sarif-file" && next) {
101
130
  base.sarifFile = path.resolve(next);
102
131
  index += 1;
103
132
  continue;
104
133
  }
134
+ if (current === "--sarif-file") {
135
+ throw new Error("Missing value for --sarif-file");
136
+ }
105
137
  if (current === "--concurrency" && next) {
106
138
  const parsed = Number(next);
107
139
  if (!Number.isInteger(parsed) || parsed <= 0) {
@@ -111,6 +143,9 @@ export async function parseCliArgs(argv) {
111
143
  index += 1;
112
144
  continue;
113
145
  }
146
+ if (current === "--concurrency") {
147
+ throw new Error("Missing value for --concurrency");
148
+ }
114
149
  if (current === "--offline") {
115
150
  base.offline = true;
116
151
  continue;
@@ -120,30 +155,99 @@ export async function parseCliArgs(argv) {
120
155
  index += 1;
121
156
  continue;
122
157
  }
158
+ if (current === "--policy-file") {
159
+ throw new Error("Missing value for --policy-file");
160
+ }
123
161
  if (current === "--pr-report-file" && next) {
124
162
  base.prReportFile = path.resolve(next);
125
163
  index += 1;
126
164
  continue;
127
165
  }
166
+ if (current === "--pr-report-file") {
167
+ throw new Error("Missing value for --pr-report-file");
168
+ }
128
169
  if (current === "--force") {
129
170
  force = true;
130
171
  continue;
131
172
  }
173
+ if (current === "--install" && command === "upgrade") {
174
+ continue;
175
+ }
176
+ if (current === "--sync" && command === "upgrade") {
177
+ continue;
178
+ }
179
+ if (current === "--pm" && next && command === "upgrade") {
180
+ parsePackageManager(args);
181
+ index += 1;
182
+ continue;
183
+ }
184
+ if (current === "--pm" && command === "upgrade") {
185
+ throw new Error("Missing value for --pm");
186
+ }
132
187
  if (current === "--mode" && next) {
133
188
  initCiMode = ensureInitCiMode(next);
134
189
  index += 1;
135
190
  continue;
136
191
  }
192
+ if (current === "--mode") {
193
+ throw new Error("Missing value for --mode");
194
+ }
137
195
  if (current === "--schedule" && next) {
138
196
  initCiSchedule = ensureInitCiSchedule(next);
139
197
  index += 1;
140
198
  continue;
141
199
  }
200
+ if (current === "--schedule") {
201
+ throw new Error("Missing value for --schedule");
202
+ }
142
203
  if (current === "--dep-kinds" && next) {
143
204
  base.includeKinds = parseDependencyKinds(next);
144
205
  index += 1;
145
206
  continue;
146
207
  }
208
+ if (current === "--dep-kinds") {
209
+ throw new Error("Missing value for --dep-kinds");
210
+ }
211
+ if (current === "--fail-on" && next) {
212
+ base.failOn = ensureFailOn(next);
213
+ index += 1;
214
+ continue;
215
+ }
216
+ if (current === "--fail-on") {
217
+ throw new Error("Missing value for --fail-on");
218
+ }
219
+ if (current === "--max-updates" && next) {
220
+ const parsed = Number(next);
221
+ if (!Number.isInteger(parsed) || parsed < 0) {
222
+ throw new Error("--max-updates must be a non-negative integer");
223
+ }
224
+ base.maxUpdates = parsed;
225
+ index += 1;
226
+ continue;
227
+ }
228
+ if (current === "--max-updates") {
229
+ throw new Error("Missing value for --max-updates");
230
+ }
231
+ if (current === "--save") {
232
+ baselineAction = "save";
233
+ continue;
234
+ }
235
+ if (current === "--check") {
236
+ baselineAction = "check";
237
+ continue;
238
+ }
239
+ if (current === "--file" && next) {
240
+ baselineFilePath = path.resolve(base.cwd, next);
241
+ index += 1;
242
+ continue;
243
+ }
244
+ if (current === "--file") {
245
+ throw new Error("Missing value for --file");
246
+ }
247
+ if (current.startsWith("-")) {
248
+ throw new Error(`Unknown option: ${current}`);
249
+ }
250
+ throw new Error(`Unexpected argument: ${current}`);
147
251
  }
148
252
  if (command === "upgrade") {
149
253
  const configPm = resolvedConfig.packageManager;
@@ -170,6 +274,19 @@ export async function parseCliArgs(argv) {
170
274
  },
171
275
  };
172
276
  }
277
+ if (command === "baseline") {
278
+ return {
279
+ command,
280
+ options: {
281
+ action: baselineAction,
282
+ cwd: base.cwd,
283
+ workspace: base.workspace,
284
+ includeKinds: base.includeKinds,
285
+ filePath: baselineFilePath,
286
+ ci: base.ci,
287
+ },
288
+ };
289
+ }
173
290
  return {
174
291
  command: "check",
175
292
  options: base,
@@ -212,6 +329,12 @@ function applyConfig(base, config) {
212
329
  if (typeof config.prReportFile === "string") {
213
330
  base.prReportFile = path.resolve(base.cwd, config.prReportFile);
214
331
  }
332
+ if (typeof config.failOn === "string") {
333
+ base.failOn = ensureFailOn(config.failOn);
334
+ }
335
+ if (typeof config.maxUpdates === "number" && Number.isInteger(config.maxUpdates) && config.maxUpdates >= 0) {
336
+ base.maxUpdates = config.maxUpdates;
337
+ }
215
338
  }
216
339
  function parsePackageManager(args) {
217
340
  const index = args.indexOf("--pm");
@@ -257,10 +380,10 @@ function parseDependencyKinds(value) {
257
380
  return Array.from(new Set(mapped));
258
381
  }
259
382
  function ensureInitCiMode(value) {
260
- if (value === "minimal" || value === "strict") {
383
+ if (value === "minimal" || value === "strict" || value === "enterprise") {
261
384
  return value;
262
385
  }
263
- throw new Error("--mode must be minimal or strict");
386
+ throw new Error("--mode must be minimal, strict or enterprise");
264
387
  }
265
388
  function ensureInitCiSchedule(value) {
266
389
  if (value === "weekly" || value === "daily" || value === "off") {
@@ -268,3 +391,9 @@ function ensureInitCiSchedule(value) {
268
391
  }
269
392
  throw new Error("--schedule must be weekly, daily or off");
270
393
  }
394
+ function ensureFailOn(value) {
395
+ if (value === "none" || value === "patch" || value === "minor" || value === "major" || value === "any") {
396
+ return value;
397
+ }
398
+ throw new Error("--fail-on must be none, patch, minor, major or any");
399
+ }
@@ -52,12 +52,12 @@ export async function warmCache(options) {
52
52
  }
53
53
  }
54
54
  else {
55
- const fetched = await registryClient.resolveManyLatestVersions(needsFetch, {
55
+ const fetched = await registryClient.resolveManyPackageMetadata(needsFetch, {
56
56
  concurrency: options.concurrency,
57
57
  });
58
- for (const [pkg, version] of fetched.versions) {
59
- if (version) {
60
- await cache.set(pkg, options.target, version, options.cacheTtlSeconds);
58
+ for (const [pkg, metadata] of fetched.metadata) {
59
+ if (metadata.latestVersion) {
60
+ await cache.set(pkg, options.target, metadata.latestVersion, metadata.versions, options.cacheTtlSeconds);
61
61
  warmed += 1;
62
62
  }
63
63
  }
package/dist/index.d.ts CHANGED
@@ -2,7 +2,8 @@ export { check } from "./core/check.js";
2
2
  export { upgrade } from "./core/upgrade.js";
3
3
  export { warmCache } from "./core/warm-cache.js";
4
4
  export { initCiWorkflow } from "./core/init-ci.js";
5
+ export { saveBaseline, diffBaseline } from "./core/baseline.js";
5
6
  export { createSarifReport } from "./output/sarif.js";
6
7
  export { writeGitHubOutput, renderGitHubAnnotations } from "./output/github.js";
7
8
  export { renderPrReport } from "./output/pr-report.js";
8
- export type { CheckOptions, CheckResult, DependencyKind, OutputFormat, PackageUpdate, RunOptions, TargetLevel, UpgradeOptions, UpgradeResult, } from "./types/index.js";
9
+ export type { CheckOptions, CheckResult, DependencyKind, FailOnLevel, OutputFormat, PackageUpdate, RunOptions, TargetLevel, UpgradeOptions, UpgradeResult, } from "./types/index.js";
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ export { check } from "./core/check.js";
2
2
  export { upgrade } from "./core/upgrade.js";
3
3
  export { warmCache } from "./core/warm-cache.js";
4
4
  export { initCiWorkflow } from "./core/init-ci.js";
5
+ export { saveBaseline, diffBaseline } from "./core/baseline.js";
5
6
  export { createSarifReport } from "./output/sarif.js";
6
7
  export { writeGitHubOutput, renderGitHubAnnotations } from "./output/github.js";
7
8
  export { renderPrReport } from "./output/pr-report.js";
@@ -1,3 +1,6 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
1
4
  export function createSarifReport(result) {
2
5
  const dependencyRuleId = "rainy-updates/dependency-update";
3
6
  const runtimeRuleId = "rainy-updates/runtime-error";
@@ -38,7 +41,7 @@ export function createSarifReport(result) {
38
41
  tool: {
39
42
  driver: {
40
43
  name: "@rainy-updates/cli",
41
- version: "0.1.0",
44
+ version: getToolVersion(),
42
45
  rules: [
43
46
  {
44
47
  id: dependencyRuleId,
@@ -58,3 +61,20 @@ export function createSarifReport(result) {
58
61
  ],
59
62
  };
60
63
  }
64
+ let TOOL_VERSION_CACHE = null;
65
+ function getToolVersion() {
66
+ if (TOOL_VERSION_CACHE)
67
+ return TOOL_VERSION_CACHE;
68
+ try {
69
+ const currentFile = fileURLToPath(import.meta.url);
70
+ const packageJsonPath = path.resolve(path.dirname(currentFile), "../../package.json");
71
+ const content = readFileSync(packageJsonPath, "utf8");
72
+ const parsed = JSON.parse(content);
73
+ TOOL_VERSION_CACHE = parsed.version ?? "0.0.0";
74
+ return TOOL_VERSION_CACHE;
75
+ }
76
+ catch {
77
+ TOOL_VERSION_CACHE = "0.0.0";
78
+ return TOOL_VERSION_CACHE;
79
+ }
80
+ }
@@ -3,12 +3,23 @@ export interface ResolveManyOptions {
3
3
  timeoutMs?: number;
4
4
  }
5
5
  export interface ResolveManyResult {
6
- versions: Map<string, string | null>;
6
+ metadata: Map<string, {
7
+ latestVersion: string | null;
8
+ versions: string[];
9
+ }>;
7
10
  errors: Map<string, string>;
8
11
  }
9
12
  export declare class NpmRegistryClient {
10
13
  private readonly requesterPromise;
11
14
  constructor();
15
+ resolvePackageMetadata(packageName: string, timeoutMs?: number): Promise<{
16
+ latestVersion: string | null;
17
+ versions: string[];
18
+ }>;
12
19
  resolveLatestVersion(packageName: string, timeoutMs?: number): Promise<string | null>;
13
- resolveManyLatestVersions(packageNames: string[], options: ResolveManyOptions): Promise<ResolveManyResult>;
20
+ resolveManyPackageMetadata(packageNames: string[], options: ResolveManyOptions): Promise<ResolveManyResult>;
21
+ resolveManyLatestVersions(packageNames: string[], options: ResolveManyOptions): Promise<{
22
+ versions: Map<string, string | null>;
23
+ errors: Map<string, string>;
24
+ }>;
14
25
  }
@@ -6,21 +6,23 @@ export class NpmRegistryClient {
6
6
  constructor() {
7
7
  this.requesterPromise = createRequester();
8
8
  }
9
- async resolveLatestVersion(packageName, timeoutMs = DEFAULT_TIMEOUT_MS) {
9
+ async resolvePackageMetadata(packageName, timeoutMs = DEFAULT_TIMEOUT_MS) {
10
10
  const requester = await this.requesterPromise;
11
11
  let lastError = null;
12
12
  for (let attempt = 1; attempt <= 3; attempt += 1) {
13
13
  try {
14
14
  const response = await requester(packageName, timeoutMs);
15
- if (response.status === 404)
16
- return null;
15
+ if (response.status === 404) {
16
+ return { latestVersion: null, versions: [] };
17
+ }
17
18
  if (response.status === 429 || response.status >= 500) {
18
19
  throw new Error(`Registry temporary error: ${response.status}`);
19
20
  }
20
21
  if (response.status < 200 || response.status >= 300) {
21
22
  throw new Error(`Registry request failed: ${response.status}`);
22
23
  }
23
- return response.data?.["dist-tags"]?.latest ?? null;
24
+ const versions = Object.keys(response.data?.versions ?? {});
25
+ return { latestVersion: response.data?.["dist-tags"]?.latest ?? null, versions };
24
26
  }
25
27
  catch (error) {
26
28
  lastError = String(error);
@@ -31,18 +33,22 @@ export class NpmRegistryClient {
31
33
  }
32
34
  throw new Error(`Unable to resolve ${packageName}: ${lastError ?? "unknown error"}`);
33
35
  }
34
- async resolveManyLatestVersions(packageNames, options) {
36
+ async resolveLatestVersion(packageName, timeoutMs = DEFAULT_TIMEOUT_MS) {
37
+ const metadata = await this.resolvePackageMetadata(packageName, timeoutMs);
38
+ return metadata.latestVersion;
39
+ }
40
+ async resolveManyPackageMetadata(packageNames, options) {
35
41
  const unique = Array.from(new Set(packageNames));
36
- const versions = new Map();
42
+ const metadata = new Map();
37
43
  const errors = new Map();
38
44
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
39
45
  const results = await asyncPool(options.concurrency, unique.map((pkg) => async () => {
40
46
  try {
41
- const latest = await this.resolveLatestVersion(pkg, timeoutMs);
42
- return { pkg, latest, error: null };
47
+ const packageMetadata = await this.resolvePackageMetadata(pkg, timeoutMs);
48
+ return { pkg, packageMetadata, error: null };
43
49
  }
44
50
  catch (error) {
45
- return { pkg, latest: null, error: String(error) };
51
+ return { pkg, packageMetadata: null, error: String(error) };
46
52
  }
47
53
  }));
48
54
  for (const result of results) {
@@ -52,11 +58,22 @@ export class NpmRegistryClient {
52
58
  if (result.error) {
53
59
  errors.set(result.pkg, result.error);
54
60
  }
55
- else {
56
- versions.set(result.pkg, result.latest);
61
+ else if (result.packageMetadata) {
62
+ metadata.set(result.pkg, result.packageMetadata);
57
63
  }
58
64
  }
59
- return { versions, errors };
65
+ return { metadata, errors };
66
+ }
67
+ async resolveManyLatestVersions(packageNames, options) {
68
+ const metadataResult = await this.resolveManyPackageMetadata(packageNames, options);
69
+ const versions = new Map();
70
+ for (const [name, value] of metadataResult.metadata) {
71
+ versions.set(name, value.latestVersion);
72
+ }
73
+ return {
74
+ versions,
75
+ errors: metadataResult.errors,
76
+ };
60
77
  }
61
78
  }
62
79
  function sleep(ms) {
@@ -1,6 +1,7 @@
1
1
  export type DependencyKind = "dependencies" | "devDependencies" | "optionalDependencies" | "peerDependencies";
2
2
  export type TargetLevel = "patch" | "minor" | "major" | "latest";
3
3
  export type OutputFormat = "table" | "json" | "minimal" | "github";
4
+ export type FailOnLevel = "none" | "patch" | "minor" | "major" | "any";
4
5
  export interface RunOptions {
5
6
  cwd: string;
6
7
  target: TargetLevel;
@@ -18,6 +19,8 @@ export interface RunOptions {
18
19
  offline: boolean;
19
20
  policyFile?: string;
20
21
  prReportFile?: string;
22
+ failOn?: FailOnLevel;
23
+ maxUpdates?: number;
21
24
  }
22
25
  export interface CheckOptions extends RunOptions {
23
26
  }
@@ -26,6 +29,13 @@ export interface UpgradeOptions extends RunOptions {
26
29
  packageManager: "auto" | "npm" | "pnpm";
27
30
  sync: boolean;
28
31
  }
32
+ export interface BaselineOptions {
33
+ cwd: string;
34
+ workspace: boolean;
35
+ includeKinds: DependencyKind[];
36
+ filePath: string;
37
+ ci: boolean;
38
+ }
29
39
  export interface PackageDependency {
30
40
  name: string;
31
41
  range: string;
@@ -78,6 +88,7 @@ export interface CachedVersion {
78
88
  packageName: string;
79
89
  target: TargetLevel;
80
90
  latestVersion: string;
91
+ availableVersions: string[];
81
92
  fetchedAt: number;
82
93
  ttlSeconds: number;
83
94
  }
@@ -9,5 +9,6 @@ export declare function parseVersion(raw: string): ParsedVersion | null;
9
9
  export declare function compareVersions(a: ParsedVersion, b: ParsedVersion): number;
10
10
  export declare function classifyDiff(currentRange: string, nextVersion: string): TargetLevel;
11
11
  export declare function pickTargetVersion(currentRange: string, latestVersion: string, target: TargetLevel): string | null;
12
+ export declare function pickTargetVersionFromAvailable(currentRange: string, availableVersions: string[], latestVersion: string, target: TargetLevel): string | null;
12
13
  export declare function applyRangeStyle(previousRange: string, version: string): string;
13
14
  export declare function clampTarget(requested: TargetLevel, maxAllowed?: TargetLevel): TargetLevel;
@@ -64,6 +64,30 @@ export function pickTargetVersion(currentRange, latestVersion, target) {
64
64
  }
65
65
  return latestVersion;
66
66
  }
67
+ export function pickTargetVersionFromAvailable(currentRange, availableVersions, latestVersion, target) {
68
+ const current = parseVersion(currentRange);
69
+ if (!current || target === "latest")
70
+ return latestVersion;
71
+ const parsed = availableVersions
72
+ .map((version) => ({ raw: version, parsed: parseVersion(version) }))
73
+ .filter((item) => item.parsed !== null)
74
+ .filter((item) => compareVersions(item.parsed, current) > 0)
75
+ .sort((a, b) => compareVersions(a.parsed, b.parsed));
76
+ if (parsed.length === 0)
77
+ return null;
78
+ if (target === "major") {
79
+ return parsed[parsed.length - 1]?.raw ?? null;
80
+ }
81
+ if (target === "minor") {
82
+ const sameMajor = parsed.filter((item) => item.parsed.major === current.major);
83
+ return sameMajor.length > 0 ? sameMajor[sameMajor.length - 1].raw : null;
84
+ }
85
+ if (target === "patch") {
86
+ const sameLine = parsed.filter((item) => item.parsed.major === current.major && item.parsed.minor === current.minor);
87
+ return sameLine.length > 0 ? sameLine[sameLine.length - 1].raw : null;
88
+ }
89
+ return latestVersion;
90
+ }
67
91
  export function applyRangeStyle(previousRange, version) {
68
92
  const prefix = normalizeRangePrefix(previousRange);
69
93
  return `${prefix}${version}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainy-updates/cli",
3
- "version": "0.4.4",
3
+ "version": "0.5.0-rc.1",
4
4
  "description": "Agentic CLI to check and upgrade npm/pnpm dependencies for CI workflows",
5
5
  "type": "module",
6
6
  "private": false,