@rainy-updates/cli 0.4.4 → 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.
@@ -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
- constructor();
14
+ constructor(cwd?: string);
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
  }
@@ -1,26 +1,33 @@
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
- async resolveLatestVersion(packageName, timeoutMs = DEFAULT_TIMEOUT_MS) {
14
+ async resolvePackageMetadata(packageName, timeoutMs = DEFAULT_TIMEOUT_MS) {
10
15
  const requester = await this.requesterPromise;
11
16
  let lastError = null;
12
17
  for (let attempt = 1; attempt <= 3; attempt += 1) {
13
18
  try {
14
19
  const response = await requester(packageName, timeoutMs);
15
- if (response.status === 404)
16
- return null;
20
+ if (response.status === 404) {
21
+ return { latestVersion: null, versions: [] };
22
+ }
17
23
  if (response.status === 429 || response.status >= 500) {
18
24
  throw new Error(`Registry temporary error: ${response.status}`);
19
25
  }
20
26
  if (response.status < 200 || response.status >= 300) {
21
27
  throw new Error(`Registry request failed: ${response.status}`);
22
28
  }
23
- return response.data?.["dist-tags"]?.latest ?? null;
29
+ const versions = Object.keys(response.data?.versions ?? {});
30
+ return { latestVersion: response.data?.["dist-tags"]?.latest ?? null, versions };
24
31
  }
25
32
  catch (error) {
26
33
  lastError = String(error);
@@ -31,18 +38,22 @@ export class NpmRegistryClient {
31
38
  }
32
39
  throw new Error(`Unable to resolve ${packageName}: ${lastError ?? "unknown error"}`);
33
40
  }
34
- async resolveManyLatestVersions(packageNames, options) {
41
+ async resolveLatestVersion(packageName, timeoutMs = DEFAULT_TIMEOUT_MS) {
42
+ const metadata = await this.resolvePackageMetadata(packageName, timeoutMs);
43
+ return metadata.latestVersion;
44
+ }
45
+ async resolveManyPackageMetadata(packageNames, options) {
35
46
  const unique = Array.from(new Set(packageNames));
36
- const versions = new Map();
47
+ const metadata = new Map();
37
48
  const errors = new Map();
38
49
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
39
50
  const results = await asyncPool(options.concurrency, unique.map((pkg) => async () => {
40
51
  try {
41
- const latest = await this.resolveLatestVersion(pkg, timeoutMs);
42
- return { pkg, latest, error: null };
52
+ const packageMetadata = await this.resolvePackageMetadata(pkg, timeoutMs);
53
+ return { pkg, packageMetadata, error: null };
43
54
  }
44
55
  catch (error) {
45
- return { pkg, latest: null, error: String(error) };
56
+ return { pkg, packageMetadata: null, error: String(error) };
46
57
  }
47
58
  }));
48
59
  for (const result of results) {
@@ -52,25 +63,39 @@ export class NpmRegistryClient {
52
63
  if (result.error) {
53
64
  errors.set(result.pkg, result.error);
54
65
  }
55
- else {
56
- versions.set(result.pkg, result.latest);
66
+ else if (result.packageMetadata) {
67
+ metadata.set(result.pkg, result.packageMetadata);
57
68
  }
58
69
  }
59
- return { versions, errors };
70
+ return { metadata, errors };
71
+ }
72
+ async resolveManyLatestVersions(packageNames, options) {
73
+ const metadataResult = await this.resolveManyPackageMetadata(packageNames, options);
74
+ const versions = new Map();
75
+ for (const [name, value] of metadataResult.metadata) {
76
+ versions.set(name, value.latestVersion);
77
+ }
78
+ return {
79
+ versions,
80
+ errors: metadataResult.errors,
81
+ };
60
82
  }
61
83
  }
62
84
  function sleep(ms) {
63
85
  return new Promise((resolve) => setTimeout(resolve, ms));
64
86
  }
65
- async function createRequester() {
66
- const undiciRequester = await tryCreateUndiciRequester();
87
+ async function createRequester(cwd) {
88
+ const registryConfig = await loadRegistryConfig(cwd ?? process.cwd());
89
+ const undiciRequester = await tryCreateUndiciRequester(registryConfig);
67
90
  if (undiciRequester)
68
91
  return undiciRequester;
69
92
  return async (packageName, timeoutMs) => {
70
93
  const controller = new AbortController();
71
94
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
95
+ const registry = resolveRegistryForPackage(packageName, registryConfig);
96
+ const url = buildRegistryUrl(registry, packageName);
72
97
  try {
73
- const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`, {
98
+ const response = await fetch(url, {
74
99
  headers: {
75
100
  accept: "application/json",
76
101
  "user-agent": USER_AGENT,
@@ -85,21 +110,17 @@ async function createRequester() {
85
110
  }
86
111
  };
87
112
  }
88
- async function tryCreateUndiciRequester() {
113
+ async function tryCreateUndiciRequester(registryConfig) {
89
114
  try {
90
115
  const dynamicImport = Function("specifier", "return import(specifier)");
91
116
  const undici = await dynamicImport("undici");
92
- const pool = new undici.Pool("https://registry.npmjs.org", {
93
- connections: 20,
94
- pipelining: 10,
95
- allowH2: true,
96
- });
97
117
  return async (packageName, timeoutMs) => {
98
118
  const controller = new AbortController();
99
119
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
120
+ const registry = resolveRegistryForPackage(packageName, registryConfig);
121
+ const url = buildRegistryUrl(registry, packageName);
100
122
  try {
101
- const res = await pool.request({
102
- path: `/${encodeURIComponent(packageName)}`,
123
+ const res = await undici.request(url, {
103
124
  method: "GET",
104
125
  headers: {
105
126
  accept: "application/json",
@@ -126,3 +147,74 @@ async function tryCreateUndiciRequester() {
126
147
  return null;
127
148
  }
128
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
+ }
@@ -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,13 @@ export interface RunOptions {
18
19
  offline: boolean;
19
20
  policyFile?: string;
20
21
  prReportFile?: string;
22
+ failOn?: FailOnLevel;
23
+ maxUpdates?: number;
24
+ fixPr?: boolean;
25
+ fixBranch?: string;
26
+ fixCommitMessage?: string;
27
+ fixDryRun?: boolean;
28
+ noPrReport?: boolean;
21
29
  }
22
30
  export interface CheckOptions extends RunOptions {
23
31
  }
@@ -26,6 +34,13 @@ export interface UpgradeOptions extends RunOptions {
26
34
  packageManager: "auto" | "npm" | "pnpm";
27
35
  sync: boolean;
28
36
  }
37
+ export interface BaselineOptions {
38
+ cwd: string;
39
+ workspace: boolean;
40
+ includeKinds: DependencyKind[];
41
+ filePath: string;
42
+ ci: boolean;
43
+ }
29
44
  export interface PackageDependency {
30
45
  name: string;
31
46
  range: string;
@@ -50,6 +65,9 @@ export interface Summary {
50
65
  upgraded: number;
51
66
  skipped: number;
52
67
  warmedPackages: number;
68
+ fixPrApplied?: boolean;
69
+ fixBranchName?: string;
70
+ fixCommitSha?: string;
53
71
  }
54
72
  export interface CheckResult {
55
73
  projectPath: string;
@@ -78,6 +96,7 @@ export interface CachedVersion {
78
96
  packageName: string;
79
97
  target: TargetLevel;
80
98
  latestVersion: string;
99
+ availableVersions: string[];
81
100
  fetchedAt: number;
82
101
  ttlSeconds: number;
83
102
  }
@@ -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}`;
@@ -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.4.4",
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,