@rainy-updates/cli 0.4.0 → 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.
@@ -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,10 @@
1
- export declare function initCiWorkflow(cwd: string, force: boolean): Promise<{
1
+ export type InitCiMode = "minimal" | "strict" | "enterprise";
2
+ export type InitCiSchedule = "weekly" | "daily" | "off";
3
+ export interface InitCiOptions {
4
+ mode: InitCiMode;
5
+ schedule: InitCiSchedule;
6
+ }
7
+ export declare function initCiWorkflow(cwd: string, force: boolean, options: InitCiOptions): Promise<{
2
8
  path: string;
3
9
  created: boolean;
4
10
  }>;
@@ -1,20 +1,59 @@
1
- import { promises as fs } from "node:fs";
1
+ import { access, writeFile, mkdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
- export async function initCiWorkflow(cwd, force) {
3
+ export async function initCiWorkflow(cwd, force, options) {
4
4
  const workflowPath = path.join(cwd, ".github", "workflows", "rainy-updates.yml");
5
5
  try {
6
6
  if (!force) {
7
- await fs.access(workflowPath);
7
+ await access(workflowPath);
8
8
  return { path: workflowPath, created: false };
9
9
  }
10
10
  }
11
11
  catch {
12
12
  // missing file, continue create
13
13
  }
14
- await fs.mkdir(path.dirname(workflowPath), { recursive: true });
15
- await fs.writeFile(workflowPath, workflowTemplate(), "utf8");
14
+ const packageManager = await detectPackageManager(cwd);
15
+ const scheduleBlock = renderScheduleBlock(options.schedule);
16
+ const workflow = options.mode === "minimal"
17
+ ? minimalWorkflowTemplate(scheduleBlock, packageManager)
18
+ : options.mode === "strict"
19
+ ? strictWorkflowTemplate(scheduleBlock, packageManager)
20
+ : enterpriseWorkflowTemplate(scheduleBlock, packageManager);
21
+ await mkdir(path.dirname(workflowPath), { recursive: true });
22
+ await writeFile(workflowPath, workflow, "utf8");
16
23
  return { path: workflowPath, created: true };
17
24
  }
18
- function workflowTemplate() {
19
- return `name: Rainy Updates\n\non:\n schedule:\n - cron: '0 8 * * 1'\n workflow_dispatch:\n\njobs:\n dependency-updates:\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 - name: Run rainy updates\n run: |\n npx @rainy-updates/cli check \\\n --workspace \\\n --ci \\\n --concurrency 32 \\\n --format github \\\n --json-file .artifacts/deps-report.json \\\n --sarif-file .artifacts/deps-report.sarif \\\n --github-output $GITHUB_OUTPUT\n\n - name: Upload SARIF\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: .artifacts/deps-report.sarif\n`;
25
+ async function detectPackageManager(cwd) {
26
+ const pnpmLock = path.join(cwd, "pnpm-lock.yaml");
27
+ try {
28
+ await access(pnpmLock);
29
+ return "pnpm";
30
+ }
31
+ catch {
32
+ return "npm";
33
+ }
34
+ }
35
+ function renderScheduleBlock(schedule) {
36
+ if (schedule === "off") {
37
+ return " workflow_dispatch:";
38
+ }
39
+ const cron = schedule === "daily" ? "0 8 * * *" : "0 8 * * 1";
40
+ return ` schedule:\n - cron: '${cron}'\n workflow_dispatch:`;
41
+ }
42
+ function installStep(packageManager) {
43
+ if (packageManager === "pnpm") {
44
+ return ` - name: Setup pnpm\n uses: pnpm/action-setup@v4\n with:\n version: 9\n\n - name: Install dependencies\n run: pnpm install --frozen-lockfile`;
45
+ }
46
+ return ` - name: Install dependencies\n run: npm ci`;
47
+ }
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 check \\\n --workspace \\\n --ci \\\n --format table\n`;
50
+ }
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 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`;
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`;
20
59
  }
@@ -1,4 +1,5 @@
1
- import type { CheckOptions, UpgradeOptions } from "../types/index.js";
1
+ import type { BaselineOptions, CheckOptions, UpgradeOptions } from "../types/index.js";
2
+ import type { InitCiMode, InitCiSchedule } from "./init-ci.js";
2
3
  export type ParsedCliArgs = {
3
4
  command: "check";
4
5
  options: CheckOptions;
@@ -10,8 +11,16 @@ export type ParsedCliArgs = {
10
11
  options: CheckOptions;
11
12
  } | {
12
13
  command: "init-ci";
13
- options: CheckOptions & {
14
+ options: {
15
+ cwd: string;
14
16
  force: boolean;
17
+ mode: InitCiMode;
18
+ schedule: InitCiSchedule;
19
+ };
20
+ } | {
21
+ command: "baseline";
22
+ options: BaselineOptions & {
23
+ action: "save" | "check";
15
24
  };
16
25
  };
17
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,8 +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;
41
+ let initCiMode = "enterprise";
42
+ let initCiSchedule = "weekly";
43
+ let baselineAction = "check";
44
+ let baselineFilePath = path.resolve(base.cwd, ".rainy-updates-baseline.json");
39
45
  let resolvedConfig = await loadConfig(base.cwd);
40
46
  applyConfig(base, resolvedConfig);
41
47
  for (let index = 0; index < args.length; index += 1) {
@@ -46,23 +52,36 @@ export async function parseCliArgs(argv) {
46
52
  index += 1;
47
53
  continue;
48
54
  }
55
+ if (current === "--target") {
56
+ throw new Error("Missing value for --target");
57
+ }
49
58
  if (current === "--filter" && next) {
50
59
  base.filter = next;
51
60
  index += 1;
52
61
  continue;
53
62
  }
63
+ if (current === "--filter") {
64
+ throw new Error("Missing value for --filter");
65
+ }
54
66
  if (current === "--reject" && next) {
55
67
  base.reject = next;
56
68
  index += 1;
57
69
  continue;
58
70
  }
71
+ if (current === "--reject") {
72
+ throw new Error("Missing value for --reject");
73
+ }
59
74
  if (current === "--cwd" && next) {
60
75
  base.cwd = path.resolve(next);
61
76
  resolvedConfig = await loadConfig(base.cwd);
62
77
  applyConfig(base, resolvedConfig);
78
+ baselineFilePath = path.resolve(base.cwd, ".rainy-updates-baseline.json");
63
79
  index += 1;
64
80
  continue;
65
81
  }
82
+ if (current === "--cwd") {
83
+ throw new Error("Missing value for --cwd");
84
+ }
66
85
  if (current === "--cache-ttl" && next) {
67
86
  const parsed = Number(next);
68
87
  if (!Number.isFinite(parsed) || parsed < 0) {
@@ -72,11 +91,17 @@ export async function parseCliArgs(argv) {
72
91
  index += 1;
73
92
  continue;
74
93
  }
94
+ if (current === "--cache-ttl") {
95
+ throw new Error("Missing value for --cache-ttl");
96
+ }
75
97
  if (current === "--format" && next) {
76
98
  base.format = ensureFormat(next);
77
99
  index += 1;
78
100
  continue;
79
101
  }
102
+ if (current === "--format") {
103
+ throw new Error("Missing value for --format");
104
+ }
80
105
  if (current === "--ci") {
81
106
  base.ci = true;
82
107
  continue;
@@ -90,16 +115,25 @@ export async function parseCliArgs(argv) {
90
115
  index += 1;
91
116
  continue;
92
117
  }
118
+ if (current === "--json-file") {
119
+ throw new Error("Missing value for --json-file");
120
+ }
93
121
  if (current === "--github-output" && next) {
94
122
  base.githubOutputFile = path.resolve(next);
95
123
  index += 1;
96
124
  continue;
97
125
  }
126
+ if (current === "--github-output") {
127
+ throw new Error("Missing value for --github-output");
128
+ }
98
129
  if (current === "--sarif-file" && next) {
99
130
  base.sarifFile = path.resolve(next);
100
131
  index += 1;
101
132
  continue;
102
133
  }
134
+ if (current === "--sarif-file") {
135
+ throw new Error("Missing value for --sarif-file");
136
+ }
103
137
  if (current === "--concurrency" && next) {
104
138
  const parsed = Number(next);
105
139
  if (!Number.isInteger(parsed) || parsed <= 0) {
@@ -109,6 +143,9 @@ export async function parseCliArgs(argv) {
109
143
  index += 1;
110
144
  continue;
111
145
  }
146
+ if (current === "--concurrency") {
147
+ throw new Error("Missing value for --concurrency");
148
+ }
112
149
  if (current === "--offline") {
113
150
  base.offline = true;
114
151
  continue;
@@ -118,20 +155,99 @@ export async function parseCliArgs(argv) {
118
155
  index += 1;
119
156
  continue;
120
157
  }
158
+ if (current === "--policy-file") {
159
+ throw new Error("Missing value for --policy-file");
160
+ }
121
161
  if (current === "--pr-report-file" && next) {
122
162
  base.prReportFile = path.resolve(next);
123
163
  index += 1;
124
164
  continue;
125
165
  }
166
+ if (current === "--pr-report-file") {
167
+ throw new Error("Missing value for --pr-report-file");
168
+ }
126
169
  if (current === "--force") {
127
170
  force = true;
128
171
  continue;
129
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
+ }
187
+ if (current === "--mode" && next) {
188
+ initCiMode = ensureInitCiMode(next);
189
+ index += 1;
190
+ continue;
191
+ }
192
+ if (current === "--mode") {
193
+ throw new Error("Missing value for --mode");
194
+ }
195
+ if (current === "--schedule" && next) {
196
+ initCiSchedule = ensureInitCiSchedule(next);
197
+ index += 1;
198
+ continue;
199
+ }
200
+ if (current === "--schedule") {
201
+ throw new Error("Missing value for --schedule");
202
+ }
130
203
  if (current === "--dep-kinds" && next) {
131
204
  base.includeKinds = parseDependencyKinds(next);
132
205
  index += 1;
133
206
  continue;
134
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}`);
135
251
  }
136
252
  if (command === "upgrade") {
137
253
  const configPm = resolvedConfig.packageManager;
@@ -148,7 +264,28 @@ export async function parseCliArgs(argv) {
148
264
  return { command, options: base };
149
265
  }
150
266
  if (command === "init-ci") {
151
- return { command, options: { ...base, force } };
267
+ return {
268
+ command,
269
+ options: {
270
+ cwd: base.cwd,
271
+ force,
272
+ mode: initCiMode,
273
+ schedule: initCiSchedule,
274
+ },
275
+ };
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
+ };
152
289
  }
153
290
  return {
154
291
  command: "check",
@@ -192,6 +329,12 @@ function applyConfig(base, config) {
192
329
  if (typeof config.prReportFile === "string") {
193
330
  base.prReportFile = path.resolve(base.cwd, config.prReportFile);
194
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
+ }
195
338
  }
196
339
  function parsePackageManager(args) {
197
340
  const index = args.indexOf("--pm");
@@ -236,3 +379,21 @@ function parseDependencyKinds(value) {
236
379
  }
237
380
  return Array.from(new Set(mapped));
238
381
  }
382
+ function ensureInitCiMode(value) {
383
+ if (value === "minimal" || value === "strict" || value === "enterprise") {
384
+ return value;
385
+ }
386
+ throw new Error("--mode must be minimal, strict or enterprise");
387
+ }
388
+ function ensureInitCiSchedule(value) {
389
+ if (value === "weekly" || value === "daily" || value === "off") {
390
+ return value;
391
+ }
392
+ throw new Error("--schedule must be weekly, daily or off");
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
  }