@rainy-updates/cli 0.5.1-rc.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +84 -1
  2. package/README.md +8 -1
  3. package/dist/bin/cli.js +62 -12
  4. package/dist/commands/audit/fetcher.d.ts +6 -0
  5. package/dist/commands/audit/fetcher.js +79 -0
  6. package/dist/commands/audit/mapper.d.ts +16 -0
  7. package/dist/commands/audit/mapper.js +61 -0
  8. package/dist/commands/audit/parser.d.ts +3 -0
  9. package/dist/commands/audit/parser.js +87 -0
  10. package/dist/commands/audit/runner.d.ts +7 -0
  11. package/dist/commands/audit/runner.js +64 -0
  12. package/dist/commands/bisect/engine.d.ts +12 -0
  13. package/dist/commands/bisect/engine.js +89 -0
  14. package/dist/commands/bisect/oracle.d.ts +7 -0
  15. package/dist/commands/bisect/oracle.js +36 -0
  16. package/dist/commands/bisect/parser.d.ts +2 -0
  17. package/dist/commands/bisect/parser.js +73 -0
  18. package/dist/commands/bisect/runner.d.ts +6 -0
  19. package/dist/commands/bisect/runner.js +27 -0
  20. package/dist/commands/health/parser.d.ts +2 -0
  21. package/dist/commands/health/parser.js +90 -0
  22. package/dist/commands/health/runner.d.ts +7 -0
  23. package/dist/commands/health/runner.js +130 -0
  24. package/dist/config/loader.d.ts +5 -1
  25. package/dist/config/policy.d.ts +4 -0
  26. package/dist/config/policy.js +2 -0
  27. package/dist/core/check.js +56 -3
  28. package/dist/core/fix-pr-batch.js +3 -2
  29. package/dist/core/fix-pr.js +19 -4
  30. package/dist/core/init-ci.js +3 -3
  31. package/dist/core/options.d.ts +10 -1
  32. package/dist/core/options.js +129 -13
  33. package/dist/core/summary.d.ts +1 -0
  34. package/dist/core/summary.js +11 -1
  35. package/dist/core/upgrade.js +10 -0
  36. package/dist/core/warm-cache.js +19 -1
  37. package/dist/output/format.js +4 -0
  38. package/dist/output/github.js +3 -0
  39. package/dist/registry/npm.d.ts +9 -2
  40. package/dist/registry/npm.js +87 -17
  41. package/dist/types/index.d.ts +83 -0
  42. package/dist/utils/lockfile.d.ts +5 -0
  43. package/dist/utils/lockfile.js +44 -0
  44. package/package.json +13 -4
@@ -8,13 +8,17 @@ const USER_AGENT = "@rainy-updates/cli";
8
8
  const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
9
9
  export class NpmRegistryClient {
10
10
  requesterPromise;
11
- constructor(cwd) {
11
+ defaultTimeoutMs;
12
+ defaultRetries;
13
+ constructor(cwd, options) {
12
14
  this.requesterPromise = createRequester(cwd);
15
+ this.defaultTimeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
16
+ this.defaultRetries = Math.max(1, options?.retries ?? 3);
13
17
  }
14
- async resolvePackageMetadata(packageName, timeoutMs = DEFAULT_TIMEOUT_MS) {
18
+ async resolvePackageMetadata(packageName, timeoutMs = this.defaultTimeoutMs, retries = this.defaultRetries) {
15
19
  const requester = await this.requesterPromise;
16
20
  let lastError = null;
17
- for (let attempt = 1; attempt <= 3; attempt += 1) {
21
+ for (let attempt = 1; attempt <= retries; attempt += 1) {
18
22
  try {
19
23
  const response = await requester(packageName, timeoutMs);
20
24
  if (response.status === 404) {
@@ -35,7 +39,7 @@ export class NpmRegistryClient {
35
39
  }
36
40
  catch (error) {
37
41
  lastError = String(error);
38
- if (attempt < 3) {
42
+ if (attempt < retries) {
39
43
  const backoffMs = error instanceof RetryableRegistryError ? error.waitMs : computeBackoffMs(attempt);
40
44
  await sleep(backoffMs);
41
45
  }
@@ -43,18 +47,19 @@ export class NpmRegistryClient {
43
47
  }
44
48
  throw new Error(`Unable to resolve ${packageName}: ${lastError ?? "unknown error"}`);
45
49
  }
46
- async resolveLatestVersion(packageName, timeoutMs = DEFAULT_TIMEOUT_MS) {
47
- const metadata = await this.resolvePackageMetadata(packageName, timeoutMs);
50
+ async resolveLatestVersion(packageName, timeoutMs = this.defaultTimeoutMs) {
51
+ const metadata = await this.resolvePackageMetadata(packageName, timeoutMs, this.defaultRetries);
48
52
  return metadata.latestVersion;
49
53
  }
50
54
  async resolveManyPackageMetadata(packageNames, options) {
51
55
  const unique = Array.from(new Set(packageNames));
52
56
  const metadata = new Map();
53
57
  const errors = new Map();
54
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
58
+ const timeoutMs = options.timeoutMs ?? this.defaultTimeoutMs;
59
+ const retries = options.retries ?? this.defaultRetries;
55
60
  const results = await asyncPool(options.concurrency, unique.map((pkg) => async () => {
56
61
  try {
57
- const packageMetadata = await this.resolvePackageMetadata(pkg, timeoutMs);
62
+ const packageMetadata = await this.resolvePackageMetadata(pkg, timeoutMs, retries);
58
63
  return { pkg, packageMetadata, error: null };
59
64
  }
60
65
  catch (error) {
@@ -111,12 +116,17 @@ async function createRequester(cwd) {
111
116
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
112
117
  const registry = resolveRegistryForPackage(packageName, registryConfig);
113
118
  const url = buildRegistryUrl(registry, packageName);
119
+ const authHeader = resolveAuthHeader(registry, registryConfig);
120
+ const headers = {
121
+ accept: "application/json",
122
+ "user-agent": USER_AGENT,
123
+ };
124
+ if (authHeader) {
125
+ headers.authorization = authHeader;
126
+ }
114
127
  try {
115
128
  const response = await fetch(url, {
116
- headers: {
117
- accept: "application/json",
118
- "user-agent": USER_AGENT,
119
- },
129
+ headers,
120
130
  signal: controller.signal,
121
131
  });
122
132
  const data = (await response.json().catch(() => null));
@@ -140,13 +150,18 @@ async function tryCreateUndiciRequester(registryConfig) {
140
150
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
141
151
  const registry = resolveRegistryForPackage(packageName, registryConfig);
142
152
  const url = buildRegistryUrl(registry, packageName);
153
+ const authHeader = resolveAuthHeader(registry, registryConfig);
154
+ const headers = {
155
+ accept: "application/json",
156
+ "user-agent": USER_AGENT,
157
+ };
158
+ if (authHeader) {
159
+ headers.authorization = authHeader;
160
+ }
143
161
  try {
144
162
  const res = await undici.request(url, {
145
163
  method: "GET",
146
- headers: {
147
- accept: "application/json",
148
- "user-agent": USER_AGENT,
149
- },
164
+ headers,
150
165
  signal: controller.signal,
151
166
  });
152
167
  const bodyText = await res.body.text();
@@ -198,6 +213,7 @@ async function loadRegistryConfig(cwd) {
198
213
  }
199
214
  const defaultRegistry = normalizeRegistryUrl(merged.get("registry") ?? DEFAULT_REGISTRY);
200
215
  const scopedRegistries = new Map();
216
+ const authByRegistry = new Map();
201
217
  for (const [key, value] of merged) {
202
218
  if (!key.startsWith("@") || !key.endsWith(":registry"))
203
219
  continue;
@@ -206,7 +222,32 @@ async function loadRegistryConfig(cwd) {
206
222
  scopedRegistries.set(scope, normalizeRegistryUrl(value));
207
223
  }
208
224
  }
209
- return { defaultRegistry, scopedRegistries };
225
+ for (const [key, value] of merged) {
226
+ if (!key.startsWith("//"))
227
+ continue;
228
+ const [registryKey, authKey] = key.split(/:(.+)/).filter(Boolean);
229
+ if (!registryKey || !authKey)
230
+ continue;
231
+ const registry = normalizeRegistryUrl(`https:${registryKey}`);
232
+ const current = authByRegistry.get(registry) ?? { alwaysAuth: false };
233
+ const resolvedValue = substituteEnvValue(value);
234
+ if (authKey === "_authToken") {
235
+ current.token = resolvedValue;
236
+ }
237
+ else if (authKey === "_auth") {
238
+ current.basicAuth = resolvedValue;
239
+ }
240
+ else if (authKey === "always-auth") {
241
+ current.alwaysAuth = resolvedValue === "true";
242
+ }
243
+ authByRegistry.set(registry, current);
244
+ }
245
+ if (merged.get("always-auth") === "true") {
246
+ const current = authByRegistry.get(defaultRegistry) ?? { alwaysAuth: false };
247
+ current.alwaysAuth = true;
248
+ authByRegistry.set(defaultRegistry, current);
249
+ }
250
+ return { defaultRegistry, scopedRegistries, authByRegistry };
210
251
  }
211
252
  function parseNpmrc(content) {
212
253
  const values = new Map();
@@ -226,6 +267,9 @@ function parseNpmrc(content) {
226
267
  }
227
268
  return values;
228
269
  }
270
+ function substituteEnvValue(value) {
271
+ return value.replace(/\$\{([^}]+)\}/g, (_match, name) => process.env[name] ?? "");
272
+ }
229
273
  function normalizeRegistryUrl(value) {
230
274
  const normalized = value.endsWith("/") ? value : `${value}/`;
231
275
  return normalized;
@@ -251,6 +295,32 @@ function buildRegistryUrl(registry, packageName) {
251
295
  const base = normalizeRegistryUrl(registry);
252
296
  return new URL(encodeURIComponent(packageName), base).toString();
253
297
  }
298
+ function resolveAuthHeader(registry, config) {
299
+ const registryUrl = normalizeRegistryUrl(registry);
300
+ const auth = findRegistryAuth(registryUrl, config.authByRegistry);
301
+ if (!auth)
302
+ return undefined;
303
+ if (!auth.alwaysAuth && !registryUrl.startsWith("https://"))
304
+ return undefined;
305
+ if (auth.token)
306
+ return `Bearer ${auth.token}`;
307
+ if (auth.basicAuth)
308
+ return `Basic ${auth.basicAuth}`;
309
+ return undefined;
310
+ }
311
+ function findRegistryAuth(registry, authByRegistry) {
312
+ let matched;
313
+ let longest = -1;
314
+ for (const [candidate, auth] of authByRegistry) {
315
+ if (!registry.startsWith(candidate))
316
+ continue;
317
+ if (candidate.length > longest) {
318
+ matched = auth;
319
+ longest = candidate.length;
320
+ }
321
+ }
322
+ return matched;
323
+ }
254
324
  function parseRetryAfterHeader(value) {
255
325
  if (!value)
256
326
  return null;
@@ -2,6 +2,7 @@ export type DependencyKind = "dependencies" | "devDependencies" | "optionalDepen
2
2
  export type TargetLevel = "patch" | "minor" | "major" | "latest";
3
3
  export type GroupBy = "none" | "name" | "scope" | "kind" | "risk";
4
4
  export type CiProfile = "minimal" | "strict" | "enterprise";
5
+ export type LockfileMode = "preserve" | "update" | "error";
5
6
  export type OutputFormat = "table" | "json" | "minimal" | "github" | "metrics";
6
7
  export type FailOnLevel = "none" | "patch" | "minor" | "major" | "any";
7
8
  export type LogLevel = "error" | "warn" | "info" | "debug";
@@ -20,7 +21,10 @@ export interface RunOptions {
20
21
  githubOutputFile?: string;
21
22
  sarifFile?: string;
22
23
  concurrency: number;
24
+ registryTimeoutMs: number;
25
+ registryRetries: number;
23
26
  offline: boolean;
27
+ stream: boolean;
24
28
  policyFile?: string;
25
29
  prReportFile?: string;
26
30
  failOn?: FailOnLevel;
@@ -39,6 +43,7 @@ export interface RunOptions {
39
43
  prLimit?: number;
40
44
  onlyChanged: boolean;
41
45
  ciProfile: CiProfile;
46
+ lockfileMode: LockfileMode;
42
47
  }
43
48
  export interface CheckOptions extends RunOptions {
44
49
  }
@@ -68,6 +73,7 @@ export interface PackageUpdate {
68
73
  toVersionResolved: string;
69
74
  diffType: TargetLevel;
70
75
  filtered: boolean;
76
+ autofix: boolean;
71
77
  reason?: string;
72
78
  }
73
79
  export interface Summary {
@@ -84,6 +90,7 @@ export interface Summary {
84
90
  total: number;
85
91
  offlineCacheMiss: number;
86
92
  registryFailure: number;
93
+ registryAuthFailure: number;
87
94
  other: number;
88
95
  };
89
96
  warningCounts: {
@@ -106,6 +113,8 @@ export interface Summary {
106
113
  cooldownSkipped: number;
107
114
  ciProfile: CiProfile;
108
115
  prLimitHit: boolean;
116
+ streamedEvents: number;
117
+ policyOverridesApplied: number;
109
118
  }
110
119
  export interface CheckResult {
111
120
  projectPath: string;
@@ -141,3 +150,77 @@ export interface CachedVersion {
141
150
  export interface VersionResolver {
142
151
  resolveLatestVersion(packageName: string): Promise<string | null>;
143
152
  }
153
+ export type AuditSeverity = "critical" | "high" | "medium" | "low";
154
+ export type AuditReportFormat = "table" | "json";
155
+ export interface AuditOptions {
156
+ cwd: string;
157
+ workspace: boolean;
158
+ severity?: AuditSeverity;
159
+ fix: boolean;
160
+ dryRun: boolean;
161
+ reportFormat: AuditReportFormat;
162
+ jsonFile?: string;
163
+ concurrency: number;
164
+ registryTimeoutMs: number;
165
+ }
166
+ export interface CveAdvisory {
167
+ cveId: string;
168
+ packageName: string;
169
+ severity: AuditSeverity;
170
+ vulnerableRange: string;
171
+ patchedVersion: string | null;
172
+ title: string;
173
+ url: string;
174
+ }
175
+ export interface AuditResult {
176
+ advisories: CveAdvisory[];
177
+ autoFixable: number;
178
+ errors: string[];
179
+ warnings: string[];
180
+ }
181
+ export interface BisectOptions {
182
+ cwd: string;
183
+ packageName: string;
184
+ versionRange?: string;
185
+ testCommand: string;
186
+ concurrency: number;
187
+ registryTimeoutMs: number;
188
+ cacheTtlSeconds: number;
189
+ dryRun: boolean;
190
+ }
191
+ export type BisectOutcome = "good" | "bad" | "skip";
192
+ export interface BisectResult {
193
+ packageName: string;
194
+ breakingVersion: string | null;
195
+ lastGoodVersion: string | null;
196
+ totalVersionsTested: number;
197
+ iterations: number;
198
+ }
199
+ export type HealthFlag = "stale" | "deprecated" | "archived" | "unmaintained";
200
+ export interface HealthOptions {
201
+ cwd: string;
202
+ workspace: boolean;
203
+ staleDays: number;
204
+ includeDeprecated: boolean;
205
+ includeAlternatives: boolean;
206
+ reportFormat: "table" | "json";
207
+ jsonFile?: string;
208
+ concurrency: number;
209
+ registryTimeoutMs: number;
210
+ }
211
+ export interface PackageHealthMetric {
212
+ name: string;
213
+ currentVersion: string;
214
+ lastPublished: string | null;
215
+ isDeprecated: boolean;
216
+ deprecatedMessage?: string;
217
+ isArchived: boolean;
218
+ daysSinceLastRelease: number | null;
219
+ flags: HealthFlag[];
220
+ }
221
+ export interface HealthResult {
222
+ metrics: PackageHealthMetric[];
223
+ totalFlagged: number;
224
+ errors: string[];
225
+ warnings: string[];
226
+ }
@@ -0,0 +1,5 @@
1
+ import type { LockfileMode } from "../types/index.js";
2
+ export type LockfileSnapshot = Map<string, string | null>;
3
+ export declare function captureLockfileSnapshot(cwd: string): Promise<LockfileSnapshot>;
4
+ export declare function changedLockfiles(cwd: string, before: LockfileSnapshot): Promise<string[]>;
5
+ export declare function validateLockfileMode(mode: LockfileMode, install: boolean): void;
@@ -0,0 +1,44 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ const LOCKFILE_NAMES = ["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock"];
5
+ export async function captureLockfileSnapshot(cwd) {
6
+ const snapshot = new Map();
7
+ for (const name of LOCKFILE_NAMES) {
8
+ const filePath = path.join(cwd, name);
9
+ try {
10
+ const content = await fs.readFile(filePath);
11
+ snapshot.set(filePath, hashBuffer(content));
12
+ }
13
+ catch {
14
+ snapshot.set(filePath, null);
15
+ }
16
+ }
17
+ return snapshot;
18
+ }
19
+ export async function changedLockfiles(cwd, before) {
20
+ const changed = [];
21
+ for (const name of LOCKFILE_NAMES) {
22
+ const filePath = path.join(cwd, name);
23
+ let current = null;
24
+ try {
25
+ const content = await fs.readFile(filePath);
26
+ current = hashBuffer(content);
27
+ }
28
+ catch {
29
+ current = null;
30
+ }
31
+ if ((before.get(filePath) ?? null) !== current) {
32
+ changed.push(filePath);
33
+ }
34
+ }
35
+ return changed.sort((a, b) => a.localeCompare(b));
36
+ }
37
+ export function validateLockfileMode(mode, install) {
38
+ if (mode === "update" && !install) {
39
+ throw new Error("--lockfile-mode update requires --install to update lockfiles deterministically.");
40
+ }
41
+ }
42
+ function hashBuffer(value) {
43
+ return createHash("sha256").update(value).digest("hex");
44
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rainy-updates/cli",
3
- "version": "0.5.1-rc.2",
4
- "description": "Agentic CLI to check and upgrade npm/pnpm dependencies for CI workflows",
3
+ "version": "0.5.1",
4
+ "description": "The fastest DevOps-first dependency CLI. Checks, audits, upgrades, bisects, and automates npm/pnpm dependencies in CI.",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "license": "MIT",
@@ -18,12 +18,21 @@
18
18
  "updates",
19
19
  "cli",
20
20
  "ci",
21
+ "devops",
21
22
  "npm",
22
23
  "pnpm",
23
- "monorepo"
24
+ "monorepo",
25
+ "audit",
26
+ "security",
27
+ "bisect",
28
+ "ncu",
29
+ "taze",
30
+ "renovate"
24
31
  ],
25
32
  "bin": {
26
- "rainy-updates": "./dist/bin/cli.js"
33
+ "rainy-updates": "./dist/bin/cli.js",
34
+ "rainy-up": "./dist/bin/cli.js",
35
+ "rup": "./dist/bin/cli.js"
27
36
  },
28
37
  "types": "./dist/index.d.ts",
29
38
  "exports": {