@rainy-updates/cli 0.5.0-rc.1 → 0.5.0-rc.2

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,45 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [0.5.0-rc.2] - 2026-02-27
6
+
7
+ ### Added
8
+
9
+ - New fix-PR automation flags for CI branch workflows:
10
+ - `--fix-pr`
11
+ - `--fix-branch <name>`
12
+ - `--fix-commit-message <text>`
13
+ - `--fix-dry-run`
14
+ - `--no-pr-report`
15
+ - New summary metadata for fix-PR execution:
16
+ - `fixPrApplied`
17
+ - `fixBranchName`
18
+ - `fixCommitSha`
19
+ - New GitHub output values for fix-PR state:
20
+ - `fix_pr_applied`
21
+ - `fix_pr_branch`
22
+ - `fix_pr_commit`
23
+ - Added command-specific help output for `check --help`.
24
+
25
+ ### Changed
26
+
27
+ - `check --fix-pr` now executes update application flow to support branch+commit automation without requiring `upgrade`.
28
+ - Default PR report path is auto-assigned when `--fix-pr` is enabled: `.artifacts/deps-report.md`.
29
+ - CLI path-like options are resolved against the final effective `--cwd` value (stable behavior when option order varies).
30
+ - Workspace discovery now supports recursive patterns (`**`) and negated patterns (`!pattern`) with safer directory traversal defaults.
31
+ - Registry resolution now loads `.npmrc` scope mappings (`@scope:registry=...`) from user and project config.
32
+
33
+ ### Fixed
34
+
35
+ - Prevented stale output contracts by writing fix-PR metadata into JSON/GitHub/SARIF artifact flow after git automation is resolved.
36
+
37
+ ### Tests
38
+
39
+ - Added parser tests for fix-PR flags and final-cwd path resolution.
40
+ - Added workspace discovery coverage for recursive and negated patterns.
41
+ - Added fix-PR dry-run workflow test in temporary git repos.
42
+ - Extended GitHub output tests for new fix-PR keys.
43
+
5
44
  ## [0.5.0-rc.1] - 2026-02-27
6
45
 
7
46
  ### Added
package/README.md CHANGED
@@ -39,6 +39,9 @@ npx @rainy-updates/cli check --workspace --ci --format json --json-file .artifac
39
39
  # 3) Apply upgrades with workspace sync
40
40
  npx @rainy-updates/cli upgrade --target latest --workspace --sync --install
41
41
 
42
+ # 3b) Generate a fix branch + commit for CI automation
43
+ npx @rainy-updates/cli check --workspace --fix-pr --fix-branch chore/rainy-updates
44
+
42
45
  # 4) Warm cache for deterministic offline checks
43
46
  npx @rainy-updates/cli warm-cache --workspace --concurrency 32
44
47
  npx @rainy-updates/cli check --workspace --offline --ci
@@ -154,6 +157,11 @@ Schedule:
154
157
  - `--github-output <path>`
155
158
  - `--sarif-file <path>`
156
159
  - `--pr-report-file <path>`
160
+ - `--fix-pr`
161
+ - `--fix-branch <name>`
162
+ - `--fix-commit-message <text>`
163
+ - `--fix-dry-run`
164
+ - `--no-pr-report`
157
165
  - `--ci`
158
166
 
159
167
  ### Upgrade-only
@@ -189,6 +197,7 @@ rainy-updates --version
189
197
  - Node.js 20+ runtime.
190
198
  - Works with npm and pnpm workflows.
191
199
  - Uses optional `undici` pool path for high-throughput HTTP.
200
+ - Reads `.npmrc` default and scoped registries for private package environments.
192
201
  - Cache-first architecture for speed and resilience.
193
202
 
194
203
  ## CI/CD included
package/dist/bin/cli.js CHANGED
@@ -9,6 +9,7 @@ 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
11
  import { diffBaseline, saveBaseline } from "../core/baseline.js";
12
+ import { applyFixPr } from "../core/fix-pr.js";
12
13
  import { renderResult } from "../output/format.js";
13
14
  import { writeGitHubOutput } from "../output/github.js";
14
15
  import { createSarifReport } from "../output/sarif.js";
@@ -60,22 +61,22 @@ async function main() {
60
61
  process.exitCode = 1;
61
62
  return;
62
63
  }
63
- const result = parsed.command === "upgrade"
64
- ? await upgrade(parsed.options)
65
- : parsed.command === "warm-cache"
66
- ? await warmCache(parsed.options)
67
- : await check(parsed.options);
68
- const rendered = renderResult(result, parsed.options.format);
69
- process.stdout.write(rendered + "\n");
70
- if (parsed.options.jsonFile) {
71
- await fs.mkdir(path.dirname(parsed.options.jsonFile), { recursive: true });
72
- await fs.writeFile(parsed.options.jsonFile, JSON.stringify(result, null, 2) + "\n", "utf8");
73
- }
64
+ const result = await runCommand(parsed);
74
65
  if (parsed.options.prReportFile) {
75
66
  const markdown = renderPrReport(result);
76
67
  await fs.mkdir(path.dirname(parsed.options.prReportFile), { recursive: true });
77
68
  await fs.writeFile(parsed.options.prReportFile, markdown + "\n", "utf8");
78
69
  }
70
+ if (parsed.options.fixPr && (parsed.command === "check" || parsed.command === "upgrade")) {
71
+ const fixResult = await applyFixPr(parsed.options, result, parsed.options.prReportFile ? [parsed.options.prReportFile] : []);
72
+ result.summary.fixPrApplied = fixResult.applied;
73
+ result.summary.fixBranchName = fixResult.branchName;
74
+ result.summary.fixCommitSha = fixResult.commitSha;
75
+ }
76
+ if (parsed.options.jsonFile) {
77
+ await fs.mkdir(path.dirname(parsed.options.jsonFile), { recursive: true });
78
+ await fs.writeFile(parsed.options.jsonFile, JSON.stringify(result, null, 2) + "\n", "utf8");
79
+ }
79
80
  if (parsed.options.githubOutputFile) {
80
81
  await writeGitHubOutput(parsed.options.githubOutputFile, result);
81
82
  }
@@ -84,6 +85,8 @@ async function main() {
84
85
  await fs.mkdir(path.dirname(parsed.options.sarifFile), { recursive: true });
85
86
  await fs.writeFile(parsed.options.sarifFile, JSON.stringify(sarif, null, 2) + "\n", "utf8");
86
87
  }
88
+ const rendered = renderResult(result, parsed.options.format);
89
+ process.stdout.write(rendered + "\n");
87
90
  process.exitCode = resolveExitCode(result, parsed.options.failOn, parsed.options.maxUpdates, parsed.options.ci);
88
91
  }
89
92
  catch (error) {
@@ -94,6 +97,34 @@ async function main() {
94
97
  void main();
95
98
  function renderHelp(command) {
96
99
  const isCommand = command && !command.startsWith("-");
100
+ if (isCommand && command === "check") {
101
+ return `rainy-updates check [options]
102
+
103
+ Detect available dependency updates.
104
+
105
+ Options:
106
+ --workspace
107
+ --target patch|minor|major|latest
108
+ --filter <pattern>
109
+ --reject <pattern>
110
+ --dep-kinds deps,dev,optional,peer
111
+ --concurrency <n>
112
+ --cache-ttl <seconds>
113
+ --policy-file <path>
114
+ --offline
115
+ --fix-pr
116
+ --fix-branch <name>
117
+ --fix-commit-message <text>
118
+ --fix-dry-run
119
+ --no-pr-report
120
+ --json-file <path>
121
+ --github-output <path>
122
+ --sarif-file <path>
123
+ --pr-report-file <path>
124
+ --fail-on none|patch|minor|major|any
125
+ --max-updates <n>
126
+ --ci`;
127
+ }
97
128
  if (isCommand && command === "warm-cache") {
98
129
  return `rainy-updates warm-cache [options]
99
130
 
@@ -126,6 +157,11 @@ Options:
126
157
  --target patch|minor|major|latest
127
158
  --policy-file <path>
128
159
  --concurrency <n>
160
+ --fix-pr
161
+ --fix-branch <name>
162
+ --fix-commit-message <text>
163
+ --fix-dry-run
164
+ --no-pr-report
129
165
  --json-file <path>
130
166
  --pr-report-file <path>`;
131
167
  }
@@ -174,6 +210,11 @@ Global options:
174
210
  --policy-file <path>
175
211
  --fail-on none|patch|minor|major|any
176
212
  --max-updates <n>
213
+ --fix-pr
214
+ --fix-branch <name>
215
+ --fix-commit-message <text>
216
+ --fix-dry-run
217
+ --no-pr-report
177
218
  --concurrency <n>
178
219
  --cache-ttl <seconds>
179
220
  --offline
@@ -181,6 +222,24 @@ Global options:
181
222
  --help, -h
182
223
  --version, -v`;
183
224
  }
225
+ async function runCommand(parsed) {
226
+ if (parsed.command === "upgrade") {
227
+ return await upgrade(parsed.options);
228
+ }
229
+ if (parsed.command === "warm-cache") {
230
+ return await warmCache(parsed.options);
231
+ }
232
+ if (parsed.options.fixPr) {
233
+ const upgradeOptions = {
234
+ ...parsed.options,
235
+ install: false,
236
+ packageManager: "auto",
237
+ sync: false,
238
+ };
239
+ return await upgrade(upgradeOptions);
240
+ }
241
+ return await check(parsed.options);
242
+ }
184
243
  async function readPackageVersion() {
185
244
  const currentFile = fileURLToPath(import.meta.url);
186
245
  const packageJsonPath = path.resolve(path.dirname(currentFile), "../../package.json");
@@ -17,6 +17,11 @@ export interface FileConfig {
17
17
  prReportFile?: string;
18
18
  failOn?: FailOnLevel;
19
19
  maxUpdates?: number;
20
+ fixPr?: boolean;
21
+ fixBranch?: string;
22
+ fixCommitMessage?: string;
23
+ fixDryRun?: boolean;
24
+ noPrReport?: boolean;
20
25
  install?: boolean;
21
26
  packageManager?: "auto" | "npm" | "pnpm";
22
27
  sync?: boolean;
@@ -11,7 +11,7 @@ export async function check(options) {
11
11
  const packageManager = await detectPackageManager(options.cwd);
12
12
  const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
13
13
  const cache = await VersionCache.create();
14
- const registryClient = new NpmRegistryClient();
14
+ const registryClient = new NpmRegistryClient(options.cwd);
15
15
  const policy = await loadPolicy(options.cwd, options.policyFile);
16
16
  const updates = [];
17
17
  const errors = [];
@@ -0,0 +1,7 @@
1
+ import type { CheckResult, RunOptions } from "../types/index.js";
2
+ export interface FixPrResult {
3
+ applied: boolean;
4
+ branchName?: string;
5
+ commitSha?: string;
6
+ }
7
+ export declare function applyFixPr(options: RunOptions, result: CheckResult, extraFiles: string[]): Promise<FixPrResult>;
@@ -0,0 +1,68 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ export async function applyFixPr(options, result, extraFiles) {
4
+ if (!options.fixPr)
5
+ return { applied: false };
6
+ if (result.updates.length === 0)
7
+ return { applied: false };
8
+ const status = await runGit(options.cwd, ["status", "--porcelain"]);
9
+ if (status.stdout.trim().length > 0) {
10
+ throw new Error("Cannot run --fix-pr with a dirty git working tree.");
11
+ }
12
+ const branch = options.fixBranch ?? "chore/rainy-updates";
13
+ const branchCheck = await runGit(options.cwd, ["rev-parse", "--verify", "--quiet", branch], true);
14
+ if (branchCheck.code === 0) {
15
+ await runGit(options.cwd, ["checkout", branch]);
16
+ }
17
+ else {
18
+ await runGit(options.cwd, ["checkout", "-b", branch]);
19
+ }
20
+ if (options.fixDryRun) {
21
+ return {
22
+ applied: false,
23
+ branchName: branch,
24
+ };
25
+ }
26
+ const manifestFiles = Array.from(new Set(result.packagePaths.map((pkgPath) => path.join(pkgPath, "package.json"))));
27
+ const filesToStage = Array.from(new Set([...manifestFiles, ...extraFiles]));
28
+ if (filesToStage.length > 0) {
29
+ await runGit(options.cwd, ["add", "--", ...filesToStage]);
30
+ }
31
+ const stagedCheck = await runGit(options.cwd, ["diff", "--cached", "--quiet"], true);
32
+ if (stagedCheck.code === 0) {
33
+ return {
34
+ applied: false,
35
+ branchName: branch,
36
+ };
37
+ }
38
+ const message = options.fixCommitMessage ?? `chore(deps): apply rainy-updates (${result.updates.length} updates)`;
39
+ await runGit(options.cwd, ["commit", "-m", message]);
40
+ const rev = await runGit(options.cwd, ["rev-parse", "HEAD"]);
41
+ return {
42
+ applied: true,
43
+ branchName: branch,
44
+ commitSha: rev.stdout.trim(),
45
+ };
46
+ }
47
+ async function runGit(cwd, args, allowNonZero = false) {
48
+ return await new Promise((resolve, reject) => {
49
+ const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
50
+ let stdout = "";
51
+ let stderr = "";
52
+ child.stdout.on("data", (chunk) => {
53
+ stdout += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
54
+ });
55
+ child.stderr.on("data", (chunk) => {
56
+ stderr += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
57
+ });
58
+ child.on("error", reject);
59
+ child.on("exit", (code) => {
60
+ const normalized = code ?? 1;
61
+ if (normalized !== 0 && !allowNonZero) {
62
+ reject(new Error(`git ${args.join(" ")} failed (${normalized}): ${stderr.trim()}`));
63
+ return;
64
+ }
65
+ resolve({ code: normalized, stdout, stderr });
66
+ });
67
+ });
68
+ }
@@ -36,12 +36,22 @@ export async function parseCliArgs(argv) {
36
36
  prReportFile: undefined,
37
37
  failOn: "none",
38
38
  maxUpdates: undefined,
39
+ fixPr: false,
40
+ fixBranch: "chore/rainy-updates",
41
+ fixCommitMessage: undefined,
42
+ fixDryRun: false,
43
+ noPrReport: false,
39
44
  };
40
45
  let force = false;
41
46
  let initCiMode = "enterprise";
42
47
  let initCiSchedule = "weekly";
43
48
  let baselineAction = "check";
44
49
  let baselineFilePath = path.resolve(base.cwd, ".rainy-updates-baseline.json");
50
+ let jsonFileRaw;
51
+ let githubOutputRaw;
52
+ let sarifFileRaw;
53
+ let policyFileRaw;
54
+ let prReportFileRaw;
45
55
  let resolvedConfig = await loadConfig(base.cwd);
46
56
  applyConfig(base, resolvedConfig);
47
57
  for (let index = 0; index < args.length; index += 1) {
@@ -111,7 +121,7 @@ export async function parseCliArgs(argv) {
111
121
  continue;
112
122
  }
113
123
  if (current === "--json-file" && next) {
114
- base.jsonFile = path.resolve(next);
124
+ jsonFileRaw = next;
115
125
  index += 1;
116
126
  continue;
117
127
  }
@@ -119,7 +129,7 @@ export async function parseCliArgs(argv) {
119
129
  throw new Error("Missing value for --json-file");
120
130
  }
121
131
  if (current === "--github-output" && next) {
122
- base.githubOutputFile = path.resolve(next);
132
+ githubOutputRaw = next;
123
133
  index += 1;
124
134
  continue;
125
135
  }
@@ -127,7 +137,7 @@ export async function parseCliArgs(argv) {
127
137
  throw new Error("Missing value for --github-output");
128
138
  }
129
139
  if (current === "--sarif-file" && next) {
130
- base.sarifFile = path.resolve(next);
140
+ sarifFileRaw = next;
131
141
  index += 1;
132
142
  continue;
133
143
  }
@@ -151,7 +161,7 @@ export async function parseCliArgs(argv) {
151
161
  continue;
152
162
  }
153
163
  if (current === "--policy-file" && next) {
154
- base.policyFile = path.resolve(next);
164
+ policyFileRaw = next;
155
165
  index += 1;
156
166
  continue;
157
167
  }
@@ -159,7 +169,7 @@ export async function parseCliArgs(argv) {
159
169
  throw new Error("Missing value for --policy-file");
160
170
  }
161
171
  if (current === "--pr-report-file" && next) {
162
- base.prReportFile = path.resolve(next);
172
+ prReportFileRaw = next;
163
173
  index += 1;
164
174
  continue;
165
175
  }
@@ -170,6 +180,34 @@ export async function parseCliArgs(argv) {
170
180
  force = true;
171
181
  continue;
172
182
  }
183
+ if (current === "--fix-pr") {
184
+ base.fixPr = true;
185
+ continue;
186
+ }
187
+ if (current === "--fix-branch" && next) {
188
+ base.fixBranch = next;
189
+ index += 1;
190
+ continue;
191
+ }
192
+ if (current === "--fix-branch") {
193
+ throw new Error("Missing value for --fix-branch");
194
+ }
195
+ if (current === "--fix-commit-message" && next) {
196
+ base.fixCommitMessage = next;
197
+ index += 1;
198
+ continue;
199
+ }
200
+ if (current === "--fix-commit-message") {
201
+ throw new Error("Missing value for --fix-commit-message");
202
+ }
203
+ if (current === "--fix-dry-run") {
204
+ base.fixDryRun = true;
205
+ continue;
206
+ }
207
+ if (current === "--no-pr-report") {
208
+ base.noPrReport = true;
209
+ continue;
210
+ }
173
211
  if (current === "--install" && command === "upgrade") {
174
212
  continue;
175
213
  }
@@ -249,6 +287,27 @@ export async function parseCliArgs(argv) {
249
287
  }
250
288
  throw new Error(`Unexpected argument: ${current}`);
251
289
  }
290
+ if (jsonFileRaw) {
291
+ base.jsonFile = path.resolve(base.cwd, jsonFileRaw);
292
+ }
293
+ if (githubOutputRaw) {
294
+ base.githubOutputFile = path.resolve(base.cwd, githubOutputRaw);
295
+ }
296
+ if (sarifFileRaw) {
297
+ base.sarifFile = path.resolve(base.cwd, sarifFileRaw);
298
+ }
299
+ if (policyFileRaw) {
300
+ base.policyFile = path.resolve(base.cwd, policyFileRaw);
301
+ }
302
+ if (prReportFileRaw) {
303
+ base.prReportFile = path.resolve(base.cwd, prReportFileRaw);
304
+ }
305
+ if (base.noPrReport) {
306
+ base.prReportFile = undefined;
307
+ }
308
+ else if (base.fixPr && !base.prReportFile) {
309
+ base.prReportFile = path.resolve(base.cwd, ".artifacts/deps-report.md");
310
+ }
252
311
  if (command === "upgrade") {
253
312
  const configPm = resolvedConfig.packageManager;
254
313
  const cliPm = parsePackageManager(args);
@@ -335,6 +394,21 @@ function applyConfig(base, config) {
335
394
  if (typeof config.maxUpdates === "number" && Number.isInteger(config.maxUpdates) && config.maxUpdates >= 0) {
336
395
  base.maxUpdates = config.maxUpdates;
337
396
  }
397
+ if (typeof config.fixPr === "boolean") {
398
+ base.fixPr = config.fixPr;
399
+ }
400
+ if (typeof config.fixBranch === "string" && config.fixBranch.length > 0) {
401
+ base.fixBranch = config.fixBranch;
402
+ }
403
+ if (typeof config.fixCommitMessage === "string" && config.fixCommitMessage.length > 0) {
404
+ base.fixCommitMessage = config.fixCommitMessage;
405
+ }
406
+ if (typeof config.fixDryRun === "boolean") {
407
+ base.fixDryRun = config.fixDryRun;
408
+ }
409
+ if (typeof config.noPrReport === "boolean") {
410
+ base.noPrReport = config.noPrReport;
411
+ }
338
412
  }
339
413
  function parsePackageManager(args) {
340
414
  const index = args.indexOf("--pm");
@@ -8,7 +8,7 @@ export async function warmCache(options) {
8
8
  const packageManager = await detectPackageManager(options.cwd);
9
9
  const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
10
10
  const cache = await VersionCache.create();
11
- const registryClient = new NpmRegistryClient();
11
+ const registryClient = new NpmRegistryClient(options.cwd);
12
12
  const errors = [];
13
13
  const warnings = [];
14
14
  let totalDependencies = 0;
@@ -52,5 +52,8 @@ export function renderResult(result, format) {
52
52
  }
53
53
  lines.push("");
54
54
  lines.push(`Summary: ${result.summary.updatesFound} updates, ${result.summary.checkedDependencies}/${result.summary.totalDependencies} checked, ${result.summary.warmedPackages} warmed`);
55
+ if (result.summary.fixPrApplied) {
56
+ lines.push(`Fix PR: applied on branch ${result.summary.fixBranchName ?? "unknown"} (${result.summary.fixCommitSha ?? "no-commit"})`);
57
+ }
55
58
  return lines.join("\n");
56
59
  }
@@ -8,6 +8,9 @@ export async function writeGitHubOutput(filePath, result) {
8
8
  `checked_dependencies=${result.summary.checkedDependencies}`,
9
9
  `scanned_packages=${result.summary.scannedPackages}`,
10
10
  `warmed_packages=${result.summary.warmedPackages}`,
11
+ `fix_pr_applied=${result.summary.fixPrApplied === true ? "1" : "0"}`,
12
+ `fix_pr_branch=${result.summary.fixBranchName ?? ""}`,
13
+ `fix_pr_commit=${result.summary.fixCommitSha ?? ""}`,
11
14
  ];
12
15
  await fs.mkdir(path.dirname(filePath), { recursive: true });
13
16
  await fs.writeFile(filePath, lines.join("\n") + "\n", "utf8");
@@ -11,7 +11,7 @@ export interface ResolveManyResult {
11
11
  }
12
12
  export declare class NpmRegistryClient {
13
13
  private readonly requesterPromise;
14
- constructor();
14
+ constructor(cwd?: string);
15
15
  resolvePackageMetadata(packageName: string, timeoutMs?: number): Promise<{
16
16
  latestVersion: string | null;
17
17
  versions: string[];
@@ -1,10 +1,15 @@
1
+ import { promises as fs } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import process from "node:process";
1
5
  import { asyncPool } from "../utils/async-pool.js";
2
6
  const DEFAULT_TIMEOUT_MS = 8000;
3
7
  const USER_AGENT = "@rainy-updates/cli";
8
+ const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
4
9
  export class NpmRegistryClient {
5
10
  requesterPromise;
6
- constructor() {
7
- this.requesterPromise = createRequester();
11
+ constructor(cwd) {
12
+ this.requesterPromise = createRequester(cwd);
8
13
  }
9
14
  async resolvePackageMetadata(packageName, timeoutMs = DEFAULT_TIMEOUT_MS) {
10
15
  const requester = await this.requesterPromise;
@@ -79,15 +84,18 @@ export class NpmRegistryClient {
79
84
  function sleep(ms) {
80
85
  return new Promise((resolve) => setTimeout(resolve, ms));
81
86
  }
82
- async function createRequester() {
83
- const undiciRequester = await tryCreateUndiciRequester();
87
+ async function createRequester(cwd) {
88
+ const registryConfig = await loadRegistryConfig(cwd ?? process.cwd());
89
+ const undiciRequester = await tryCreateUndiciRequester(registryConfig);
84
90
  if (undiciRequester)
85
91
  return undiciRequester;
86
92
  return async (packageName, timeoutMs) => {
87
93
  const controller = new AbortController();
88
94
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
95
+ const registry = resolveRegistryForPackage(packageName, registryConfig);
96
+ const url = buildRegistryUrl(registry, packageName);
89
97
  try {
90
- const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`, {
98
+ const response = await fetch(url, {
91
99
  headers: {
92
100
  accept: "application/json",
93
101
  "user-agent": USER_AGENT,
@@ -102,21 +110,17 @@ async function createRequester() {
102
110
  }
103
111
  };
104
112
  }
105
- async function tryCreateUndiciRequester() {
113
+ async function tryCreateUndiciRequester(registryConfig) {
106
114
  try {
107
115
  const dynamicImport = Function("specifier", "return import(specifier)");
108
116
  const undici = await dynamicImport("undici");
109
- const pool = new undici.Pool("https://registry.npmjs.org", {
110
- connections: 20,
111
- pipelining: 10,
112
- allowH2: true,
113
- });
114
117
  return async (packageName, timeoutMs) => {
115
118
  const controller = new AbortController();
116
119
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
120
+ const registry = resolveRegistryForPackage(packageName, registryConfig);
121
+ const url = buildRegistryUrl(registry, packageName);
117
122
  try {
118
- const res = await pool.request({
119
- path: `/${encodeURIComponent(packageName)}`,
123
+ const res = await undici.request(url, {
120
124
  method: "GET",
121
125
  headers: {
122
126
  accept: "application/json",
@@ -143,3 +147,74 @@ async function tryCreateUndiciRequester() {
143
147
  return null;
144
148
  }
145
149
  }
150
+ async function loadRegistryConfig(cwd) {
151
+ const homeNpmrc = path.join(os.homedir(), ".npmrc");
152
+ const projectNpmrc = path.join(cwd, ".npmrc");
153
+ const merged = new Map();
154
+ for (const filePath of [homeNpmrc, projectNpmrc]) {
155
+ try {
156
+ const content = await fs.readFile(filePath, "utf8");
157
+ const parsed = parseNpmrc(content);
158
+ for (const [key, value] of parsed) {
159
+ merged.set(key, value);
160
+ }
161
+ }
162
+ catch {
163
+ // ignore missing/unreadable file
164
+ }
165
+ }
166
+ const defaultRegistry = normalizeRegistryUrl(merged.get("registry") ?? DEFAULT_REGISTRY);
167
+ const scopedRegistries = new Map();
168
+ for (const [key, value] of merged) {
169
+ if (!key.startsWith("@") || !key.endsWith(":registry"))
170
+ continue;
171
+ const scope = key.slice(0, key.indexOf(":registry"));
172
+ if (scope.length > 1) {
173
+ scopedRegistries.set(scope, normalizeRegistryUrl(value));
174
+ }
175
+ }
176
+ return { defaultRegistry, scopedRegistries };
177
+ }
178
+ function parseNpmrc(content) {
179
+ const values = new Map();
180
+ const lines = content.split(/\r?\n/);
181
+ for (const line of lines) {
182
+ const trimmed = line.trim();
183
+ if (trimmed.length === 0 || trimmed.startsWith("#") || trimmed.startsWith(";"))
184
+ continue;
185
+ const separator = trimmed.indexOf("=");
186
+ if (separator <= 0)
187
+ continue;
188
+ const key = trimmed.slice(0, separator).trim();
189
+ const value = trimmed.slice(separator + 1).trim();
190
+ if (key.length > 0 && value.length > 0) {
191
+ values.set(key, value);
192
+ }
193
+ }
194
+ return values;
195
+ }
196
+ function normalizeRegistryUrl(value) {
197
+ const normalized = value.endsWith("/") ? value : `${value}/`;
198
+ return normalized;
199
+ }
200
+ function resolveRegistryForPackage(packageName, config) {
201
+ const scope = extractScope(packageName);
202
+ if (scope) {
203
+ const scoped = config.scopedRegistries.get(scope);
204
+ if (scoped)
205
+ return scoped;
206
+ }
207
+ return config.defaultRegistry;
208
+ }
209
+ function extractScope(packageName) {
210
+ if (!packageName.startsWith("@"))
211
+ return null;
212
+ const firstSlash = packageName.indexOf("/");
213
+ if (firstSlash <= 1)
214
+ return null;
215
+ return packageName.slice(0, firstSlash);
216
+ }
217
+ function buildRegistryUrl(registry, packageName) {
218
+ const base = normalizeRegistryUrl(registry);
219
+ return new URL(encodeURIComponent(packageName), base).toString();
220
+ }
@@ -21,6 +21,11 @@ export interface RunOptions {
21
21
  prReportFile?: string;
22
22
  failOn?: FailOnLevel;
23
23
  maxUpdates?: number;
24
+ fixPr?: boolean;
25
+ fixBranch?: string;
26
+ fixCommitMessage?: string;
27
+ fixDryRun?: boolean;
28
+ noPrReport?: boolean;
24
29
  }
25
30
  export interface CheckOptions extends RunOptions {
26
31
  }
@@ -60,6 +65,9 @@ export interface Summary {
60
65
  upgraded: number;
61
66
  skipped: number;
62
67
  warmedPackages: number;
68
+ fixPrApplied?: boolean;
69
+ fixBranchName?: string;
70
+ fixCommitSha?: string;
63
71
  }
64
72
  export interface CheckResult {
65
73
  projectPath: string;
@@ -5,14 +5,21 @@ export async function discoverPackageDirs(cwd, workspaceMode) {
5
5
  return [cwd];
6
6
  }
7
7
  const roots = new Set([cwd]);
8
- const packagePatterns = await readPackageJsonWorkspacePatterns(cwd);
9
- const pnpmPatterns = await readPnpmWorkspacePatterns(cwd);
10
- for (const pattern of [...packagePatterns, ...pnpmPatterns]) {
11
- const dirs = await expandSingleLevelPattern(cwd, pattern);
8
+ const patterns = [...(await readPackageJsonWorkspacePatterns(cwd)), ...(await readPnpmWorkspacePatterns(cwd))];
9
+ const include = patterns.filter((item) => !item.startsWith("!"));
10
+ const exclude = patterns.filter((item) => item.startsWith("!")).map((item) => item.slice(1));
11
+ for (const pattern of include) {
12
+ const dirs = await expandWorkspacePattern(cwd, pattern);
12
13
  for (const dir of dirs) {
13
14
  roots.add(dir);
14
15
  }
15
16
  }
17
+ for (const pattern of exclude) {
18
+ const dirs = await expandWorkspacePattern(cwd, pattern);
19
+ for (const dir of dirs) {
20
+ roots.delete(dir);
21
+ }
22
+ }
16
23
  const existing = [];
17
24
  for (const dir of roots) {
18
25
  const packageJsonPath = path.join(dir, "package.json");
@@ -21,7 +28,7 @@ export async function discoverPackageDirs(cwd, workspaceMode) {
21
28
  existing.push(dir);
22
29
  }
23
30
  catch {
24
- // ignore
31
+ // ignore missing package.json
25
32
  }
26
33
  }
27
34
  return existing.sort();
@@ -53,7 +60,7 @@ async function readPnpmWorkspacePatterns(cwd) {
53
60
  const trimmed = line.trim();
54
61
  if (!trimmed.startsWith("-"))
55
62
  continue;
56
- const value = trimmed.replace(/^-\s*/, "").replace(/^['\"]|['\"]$/g, "");
63
+ const value = trimmed.replace(/^-\s*/, "").replace(/^['"]|['"]$/g, "");
57
64
  if (value.length > 0) {
58
65
  patterns.push(value);
59
66
  }
@@ -64,23 +71,48 @@ async function readPnpmWorkspacePatterns(cwd) {
64
71
  return [];
65
72
  }
66
73
  }
67
- async function expandSingleLevelPattern(cwd, pattern) {
68
- if (!pattern.includes("*")) {
69
- return [path.resolve(cwd, pattern)];
70
- }
71
- const normalized = pattern.replace(/\\/g, "/");
72
- const starIndex = normalized.indexOf("*");
73
- const basePart = normalized.slice(0, starIndex).replace(/\/$/, "");
74
- const suffix = normalized.slice(starIndex + 1);
75
- if (suffix.length > 0 && suffix !== "/") {
74
+ async function expandWorkspacePattern(cwd, pattern) {
75
+ const normalized = pattern.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
76
+ if (normalized.length === 0)
76
77
  return [];
78
+ if (!normalized.includes("*")) {
79
+ return [path.resolve(cwd, normalized)];
80
+ }
81
+ const segments = normalized.split("/").filter(Boolean);
82
+ const results = new Set();
83
+ await collectMatches(path.resolve(cwd), segments, 0, results);
84
+ return Array.from(results);
85
+ }
86
+ async function collectMatches(baseDir, segments, index, out) {
87
+ if (index >= segments.length) {
88
+ out.add(baseDir);
89
+ return;
77
90
  }
78
- const baseDir = path.resolve(cwd, basePart);
91
+ const segment = segments[index];
92
+ if (segment === "**") {
93
+ await collectMatches(baseDir, segments, index + 1, out);
94
+ const children = await readChildDirs(baseDir);
95
+ for (const child of children) {
96
+ await collectMatches(child, segments, index, out);
97
+ }
98
+ return;
99
+ }
100
+ if (segment === "*") {
101
+ const children = await readChildDirs(baseDir);
102
+ for (const child of children) {
103
+ await collectMatches(child, segments, index + 1, out);
104
+ }
105
+ return;
106
+ }
107
+ await collectMatches(path.join(baseDir, segment), segments, index + 1, out);
108
+ }
109
+ async function readChildDirs(dir) {
79
110
  try {
80
- const entries = await fs.readdir(baseDir, { withFileTypes: true });
111
+ const entries = await fs.readdir(dir, { withFileTypes: true });
81
112
  return entries
82
113
  .filter((entry) => entry.isDirectory())
83
- .map((entry) => path.join(baseDir, entry.name));
114
+ .filter((entry) => entry.name !== "node_modules" && !entry.name.startsWith("."))
115
+ .map((entry) => path.join(dir, entry.name));
84
116
  }
85
117
  catch {
86
118
  return [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainy-updates/cli",
3
- "version": "0.5.0-rc.1",
3
+ "version": "0.5.0-rc.2",
4
4
  "description": "Agentic CLI to check and upgrade npm/pnpm dependencies for CI workflows",
5
5
  "type": "module",
6
6
  "private": false,