@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
@@ -4,11 +4,12 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { readManifest, writeManifest } from "../parsers/package-json.js";
6
6
  export async function applyFixPrBatches(options, result) {
7
- if (!options.fixPr || result.updates.length === 0) {
7
+ const autofixUpdates = result.updates.filter((update) => update.autofix !== false);
8
+ if (!options.fixPr || autofixUpdates.length === 0) {
8
9
  return { applied: false, branches: [], commits: [] };
9
10
  }
10
11
  const baseRef = await resolveBaseRef(options.cwd, options.fixPrNoCheckout);
11
- const groups = groupUpdates(result.updates, options.groupBy);
12
+ const groups = groupUpdates(autofixUpdates, options.groupBy);
12
13
  const plans = planFixPrBatches(groups, options.fixBranch ?? "chore/rainy-updates", options.fixPrBatchSize ?? 1);
13
14
  if (options.fixDryRun) {
14
15
  return { applied: false, branches: plans.map((plan) => plan.branchName), commits: [] };
@@ -3,7 +3,8 @@ import path from "node:path";
3
3
  export async function applyFixPr(options, result, extraFiles) {
4
4
  if (!options.fixPr)
5
5
  return { applied: false };
6
- if (result.updates.length === 0) {
6
+ const autofixUpdates = result.updates.filter((update) => update.autofix !== false);
7
+ if (autofixUpdates.length === 0) {
7
8
  return {
8
9
  applied: false,
9
10
  branchName: options.fixBranch ?? "chore/rainy-updates",
@@ -35,8 +36,11 @@ export async function applyFixPr(options, result, extraFiles) {
35
36
  commitSha: "",
36
37
  };
37
38
  }
38
- const manifestFiles = Array.from(new Set(result.updates.map((update) => path.resolve(update.packagePath, "package.json"))));
39
- const filesToStage = Array.from(new Set([...manifestFiles, ...extraFiles]
39
+ const manifestFiles = Array.from(new Set(autofixUpdates.map((update) => path.resolve(update.packagePath, "package.json"))));
40
+ const lockfileFiles = options.lockfileMode === "update"
41
+ ? (await collectChangedLockfiles(options.cwd)).map((entry) => path.resolve(options.cwd, entry))
42
+ : [];
43
+ const filesToStage = Array.from(new Set([...manifestFiles, ...extraFiles, ...lockfileFiles]
40
44
  .map((entry) => path.resolve(options.cwd, entry))
41
45
  .filter((entry) => entry.startsWith(path.resolve(options.cwd) + path.sep) || entry === path.resolve(options.cwd)))).sort((a, b) => a.localeCompare(b));
42
46
  if (filesToStage.length > 0) {
@@ -50,7 +54,7 @@ export async function applyFixPr(options, result, extraFiles) {
50
54
  commitSha: "",
51
55
  };
52
56
  }
53
- const message = options.fixCommitMessage ?? `chore(deps): apply rainy-updates (${result.updates.length} updates)`;
57
+ const message = options.fixCommitMessage ?? `chore(deps): apply rainy-updates (${autofixUpdates.length} updates)`;
54
58
  await runGit(options.cwd, ["commit", "-m", message]);
55
59
  const rev = await runGit(options.cwd, ["rev-parse", "HEAD"]);
56
60
  return {
@@ -81,3 +85,14 @@ async function runGit(cwd, args, allowNonZero = false) {
81
85
  });
82
86
  });
83
87
  }
88
+ async function collectChangedLockfiles(cwd) {
89
+ const status = await runGit(cwd, ["status", "--porcelain"], true);
90
+ const allowed = new Set(["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock"]);
91
+ const changed = status.stdout
92
+ .split(/\r?\n/)
93
+ .map((line) => line.trim())
94
+ .filter(Boolean)
95
+ .map((line) => line.slice(3).trim())
96
+ .filter((entry) => allowed.has(path.basename(entry)));
97
+ return Array.from(new Set(changed)).sort((a, b) => a.localeCompare(b));
98
+ }
@@ -46,14 +46,14 @@ function installStep(packageManager) {
46
46
  return ` - name: Install dependencies\n run: npm ci`;
47
47
  }
48
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 ci \\\n --workspace \\\n --mode minimal \\\n --ci \\\n --format table\n`;
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 ci \\\n --workspace \\\n --mode minimal \\\n --ci \\\n --stream \\\n --registry-timeout-ms 12000 \\\n --registry-retries 4 \\\n --format table\n`;
50
50
  }
51
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 ci \\\n --workspace \\\n --mode strict \\\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`;
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 --registry-timeout-ms 12000 --registry-retries 4\n\n - name: Run strict dependency check\n run: |\n npx @rainy-updates/cli ci \\\n --workspace \\\n --mode strict \\\n --ci \\\n --concurrency 32 \\\n --stream \\\n --registry-timeout-ms 12000 \\\n --registry-retries 4 \\\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
53
  }
54
54
  function enterpriseWorkflowTemplate(scheduleBlock, packageManager) {
55
55
  const detectedPmInstall = packageManager === "pnpm"
56
56
  ? "corepack enable && corepack prepare pnpm@9 --activate && pnpm install --frozen-lockfile"
57
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 ci \\\n --workspace \\\n --mode enterprise \\\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`;
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 --registry-timeout-ms 12000 --registry-retries 4\n\n - name: Check updates with rollout controls\n run: |\n npx @rainy-updates/cli ci \\\n --workspace \\\n --mode enterprise \\\n --concurrency 32 \\\n --stream \\\n --registry-timeout-ms 12000 \\\n --registry-retries 4 \\\n --lockfile-mode preserve \\\n --format github \\\n --fail-on minor \\\n --max-updates 50 \\\n --json-file .artifacts/deps-report-node-\${{ matrix.node }}.json \\\n --pr-report-file .artifacts/deps-report-node-\${{ matrix.node }}.md \\\n --sarif-file .artifacts/deps-report-node-\${{ matrix.node }}.sarif \\\n --github-output $GITHUB_OUTPUT\n\n - name: Upload report artifacts\n uses: actions/upload-artifact@v4\n with:\n name: rainy-updates-report-node-\${{ matrix.node }}\n path: .artifacts/\n retention-days: 14\n\n - name: Upload SARIF\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: .artifacts/deps-report-node-\${{ matrix.node }}.sarif\n`;
59
59
  }
@@ -1,4 +1,4 @@
1
- import type { BaselineOptions, CheckOptions, UpgradeOptions } from "../types/index.js";
1
+ import type { BaselineOptions, CheckOptions, UpgradeOptions, AuditOptions, BisectOptions, HealthOptions } from "../types/index.js";
2
2
  import type { InitCiMode, InitCiSchedule } from "./init-ci.js";
3
3
  export type ParsedCliArgs = {
4
4
  command: "check";
@@ -25,5 +25,14 @@ export type ParsedCliArgs = {
25
25
  options: BaselineOptions & {
26
26
  action: "save" | "check";
27
27
  };
28
+ } | {
29
+ command: "bisect";
30
+ options: BisectOptions;
31
+ } | {
32
+ command: "audit";
33
+ options: AuditOptions;
34
+ } | {
35
+ command: "health";
36
+ options: HealthOptions;
28
37
  };
29
38
  export declare function parseCliArgs(argv: string[]): Promise<ParsedCliArgs>;
@@ -7,7 +7,17 @@ const DEFAULT_INCLUDE_KINDS = [
7
7
  "optionalDependencies",
8
8
  "peerDependencies",
9
9
  ];
10
- const KNOWN_COMMANDS = ["check", "upgrade", "warm-cache", "init-ci", "baseline", "ci"];
10
+ const KNOWN_COMMANDS = [
11
+ "check",
12
+ "upgrade",
13
+ "warm-cache",
14
+ "init-ci",
15
+ "baseline",
16
+ "ci",
17
+ "bisect",
18
+ "audit",
19
+ "health",
20
+ ];
11
21
  export async function parseCliArgs(argv) {
12
22
  const firstArg = argv[0];
13
23
  const isKnownCommand = KNOWN_COMMANDS.includes(firstArg);
@@ -31,7 +41,10 @@ export async function parseCliArgs(argv) {
31
41
  githubOutputFile: undefined,
32
42
  sarifFile: undefined,
33
43
  concurrency: 16,
44
+ registryTimeoutMs: 8000,
45
+ registryRetries: 3,
34
46
  offline: false,
47
+ stream: false,
35
48
  policyFile: undefined,
36
49
  prReportFile: undefined,
37
50
  failOn: "none",
@@ -50,6 +63,7 @@ export async function parseCliArgs(argv) {
50
63
  prLimit: undefined,
51
64
  onlyChanged: false,
52
65
  ciProfile: "minimal",
66
+ lockfileMode: "preserve",
53
67
  };
54
68
  let force = false;
55
69
  let initCiMode = "enterprise";
@@ -165,10 +179,38 @@ export async function parseCliArgs(argv) {
165
179
  if (current === "--concurrency") {
166
180
  throw new Error("Missing value for --concurrency");
167
181
  }
182
+ if (current === "--registry-timeout-ms" && next) {
183
+ const parsed = Number(next);
184
+ if (!Number.isInteger(parsed) || parsed <= 0) {
185
+ throw new Error("--registry-timeout-ms must be a positive integer");
186
+ }
187
+ base.registryTimeoutMs = parsed;
188
+ index += 1;
189
+ continue;
190
+ }
191
+ if (current === "--registry-timeout-ms") {
192
+ throw new Error("Missing value for --registry-timeout-ms");
193
+ }
194
+ if (current === "--registry-retries" && next) {
195
+ const parsed = Number(next);
196
+ if (!Number.isInteger(parsed) || parsed < 1) {
197
+ throw new Error("--registry-retries must be a positive integer");
198
+ }
199
+ base.registryRetries = parsed;
200
+ index += 1;
201
+ continue;
202
+ }
203
+ if (current === "--registry-retries") {
204
+ throw new Error("Missing value for --registry-retries");
205
+ }
168
206
  if (current === "--offline") {
169
207
  base.offline = true;
170
208
  continue;
171
209
  }
210
+ if (current === "--stream") {
211
+ base.stream = true;
212
+ continue;
213
+ }
172
214
  if (current === "--policy-file" && next) {
173
215
  policyFileRaw = next;
174
216
  index += 1;
@@ -352,6 +394,14 @@ export async function parseCliArgs(argv) {
352
394
  base.onlyChanged = true;
353
395
  continue;
354
396
  }
397
+ if (current === "--lockfile-mode" && next) {
398
+ base.lockfileMode = ensureLockfileMode(next);
399
+ index += 1;
400
+ continue;
401
+ }
402
+ if (current === "--lockfile-mode") {
403
+ throw new Error("Missing value for --lockfile-mode");
404
+ }
355
405
  if (current === "--save") {
356
406
  baselineAction = "save";
357
407
  continue;
@@ -435,6 +485,19 @@ export async function parseCliArgs(argv) {
435
485
  },
436
486
  };
437
487
  }
488
+ // ─── New v0.5.1 commands: lazy-parsed by isolated sub-parsers ────────────
489
+ if (command === "bisect") {
490
+ const { parseBisectArgs } = await import("../commands/bisect/parser.js");
491
+ return { command, options: parseBisectArgs(args) };
492
+ }
493
+ if (command === "audit") {
494
+ const { parseAuditArgs } = await import("../commands/audit/parser.js");
495
+ return { command, options: parseAuditArgs(args) };
496
+ }
497
+ if (command === "health") {
498
+ const { parseHealthArgs } = await import("../commands/health/parser.js");
499
+ return { command, options: parseHealthArgs(args) };
500
+ }
438
501
  return {
439
502
  command: "check",
440
503
  options: base,
@@ -465,12 +528,27 @@ function applyConfig(base, config) {
465
528
  if (typeof config.sarifFile === "string") {
466
529
  base.sarifFile = path.resolve(base.cwd, config.sarifFile);
467
530
  }
468
- if (typeof config.concurrency === "number" && Number.isInteger(config.concurrency) && config.concurrency > 0) {
531
+ if (typeof config.concurrency === "number" &&
532
+ Number.isInteger(config.concurrency) &&
533
+ config.concurrency > 0) {
469
534
  base.concurrency = config.concurrency;
470
535
  }
536
+ if (typeof config.registryTimeoutMs === "number" &&
537
+ Number.isInteger(config.registryTimeoutMs) &&
538
+ config.registryTimeoutMs > 0) {
539
+ base.registryTimeoutMs = config.registryTimeoutMs;
540
+ }
541
+ if (typeof config.registryRetries === "number" &&
542
+ Number.isInteger(config.registryRetries) &&
543
+ config.registryRetries > 0) {
544
+ base.registryRetries = config.registryRetries;
545
+ }
471
546
  if (typeof config.offline === "boolean") {
472
547
  base.offline = config.offline;
473
548
  }
549
+ if (typeof config.stream === "boolean") {
550
+ base.stream = config.stream;
551
+ }
474
552
  if (typeof config.policyFile === "string") {
475
553
  base.policyFile = path.resolve(base.cwd, config.policyFile);
476
554
  }
@@ -480,7 +558,9 @@ function applyConfig(base, config) {
480
558
  if (typeof config.failOn === "string") {
481
559
  base.failOn = ensureFailOn(config.failOn);
482
560
  }
483
- if (typeof config.maxUpdates === "number" && Number.isInteger(config.maxUpdates) && config.maxUpdates >= 0) {
561
+ if (typeof config.maxUpdates === "number" &&
562
+ Number.isInteger(config.maxUpdates) &&
563
+ config.maxUpdates >= 0) {
484
564
  base.maxUpdates = config.maxUpdates;
485
565
  }
486
566
  if (typeof config.fixPr === "boolean") {
@@ -489,7 +569,8 @@ function applyConfig(base, config) {
489
569
  if (typeof config.fixBranch === "string" && config.fixBranch.length > 0) {
490
570
  base.fixBranch = config.fixBranch;
491
571
  }
492
- if (typeof config.fixCommitMessage === "string" && config.fixCommitMessage.length > 0) {
572
+ if (typeof config.fixCommitMessage === "string" &&
573
+ config.fixCommitMessage.length > 0) {
493
574
  base.fixCommitMessage = config.fixCommitMessage;
494
575
  }
495
576
  if (typeof config.fixDryRun === "boolean") {
@@ -498,7 +579,9 @@ function applyConfig(base, config) {
498
579
  if (typeof config.fixPrNoCheckout === "boolean") {
499
580
  base.fixPrNoCheckout = config.fixPrNoCheckout;
500
581
  }
501
- if (typeof config.fixPrBatchSize === "number" && Number.isInteger(config.fixPrBatchSize) && config.fixPrBatchSize > 0) {
582
+ if (typeof config.fixPrBatchSize === "number" &&
583
+ Number.isInteger(config.fixPrBatchSize) &&
584
+ config.fixPrBatchSize > 0) {
502
585
  base.fixPrBatchSize = config.fixPrBatchSize;
503
586
  }
504
587
  if (typeof config.noPrReport === "boolean") {
@@ -510,13 +593,19 @@ function applyConfig(base, config) {
510
593
  if (typeof config.groupBy === "string") {
511
594
  base.groupBy = ensureGroupBy(config.groupBy);
512
595
  }
513
- if (typeof config.groupMax === "number" && Number.isInteger(config.groupMax) && config.groupMax > 0) {
596
+ if (typeof config.groupMax === "number" &&
597
+ Number.isInteger(config.groupMax) &&
598
+ config.groupMax > 0) {
514
599
  base.groupMax = config.groupMax;
515
600
  }
516
- if (typeof config.cooldownDays === "number" && Number.isInteger(config.cooldownDays) && config.cooldownDays >= 0) {
601
+ if (typeof config.cooldownDays === "number" &&
602
+ Number.isInteger(config.cooldownDays) &&
603
+ config.cooldownDays >= 0) {
517
604
  base.cooldownDays = config.cooldownDays;
518
605
  }
519
- if (typeof config.prLimit === "number" && Number.isInteger(config.prLimit) && config.prLimit > 0) {
606
+ if (typeof config.prLimit === "number" &&
607
+ Number.isInteger(config.prLimit) &&
608
+ config.prLimit > 0) {
520
609
  base.prLimit = config.prLimit;
521
610
  }
522
611
  if (typeof config.onlyChanged === "boolean") {
@@ -525,6 +614,9 @@ function applyConfig(base, config) {
525
614
  if (typeof config.ciProfile === "string") {
526
615
  base.ciProfile = ensureCiProfile(config.ciProfile);
527
616
  }
617
+ if (typeof config.lockfileMode === "string") {
618
+ base.lockfileMode = ensureLockfileMode(config.lockfileMode);
619
+ }
528
620
  }
529
621
  function parsePackageManager(args) {
530
622
  const index = args.indexOf("--pm");
@@ -537,19 +629,29 @@ function parsePackageManager(args) {
537
629
  throw new Error("--pm must be auto, npm or pnpm");
538
630
  }
539
631
  function ensureTarget(value) {
540
- if (value === "patch" || value === "minor" || value === "major" || value === "latest") {
632
+ if (value === "patch" ||
633
+ value === "minor" ||
634
+ value === "major" ||
635
+ value === "latest") {
541
636
  return value;
542
637
  }
543
638
  throw new Error("--target must be patch, minor, major, latest");
544
639
  }
545
640
  function ensureFormat(value) {
546
- if (value === "table" || value === "json" || value === "minimal" || value === "github" || value === "metrics") {
641
+ if (value === "table" ||
642
+ value === "json" ||
643
+ value === "minimal" ||
644
+ value === "github" ||
645
+ value === "metrics") {
547
646
  return value;
548
647
  }
549
648
  throw new Error("--format must be table, json, minimal, github or metrics");
550
649
  }
551
650
  function ensureLogLevel(value) {
552
- if (value === "error" || value === "warn" || value === "info" || value === "debug") {
651
+ if (value === "error" ||
652
+ value === "warn" ||
653
+ value === "info" ||
654
+ value === "debug") {
553
655
  return value;
554
656
  }
555
657
  throw new Error("--log-level must be error, warn, info or debug");
@@ -588,13 +690,21 @@ function ensureInitCiSchedule(value) {
588
690
  throw new Error("--schedule must be weekly, daily or off");
589
691
  }
590
692
  function ensureFailOn(value) {
591
- if (value === "none" || value === "patch" || value === "minor" || value === "major" || value === "any") {
693
+ if (value === "none" ||
694
+ value === "patch" ||
695
+ value === "minor" ||
696
+ value === "major" ||
697
+ value === "any") {
592
698
  return value;
593
699
  }
594
700
  throw new Error("--fail-on must be none, patch, minor, major or any");
595
701
  }
596
702
  function ensureGroupBy(value) {
597
- if (value === "none" || value === "name" || value === "scope" || value === "kind" || value === "risk") {
703
+ if (value === "none" ||
704
+ value === "name" ||
705
+ value === "scope" ||
706
+ value === "kind" ||
707
+ value === "risk") {
598
708
  return value;
599
709
  }
600
710
  throw new Error("--group-by must be none, name, scope, kind or risk");
@@ -605,3 +715,9 @@ function ensureCiProfile(value) {
605
715
  }
606
716
  throw new Error("--mode must be minimal, strict or enterprise");
607
717
  }
718
+ function ensureLockfileMode(value) {
719
+ if (value === "preserve" || value === "update" || value === "error") {
720
+ return value;
721
+ }
722
+ throw new Error("--lockfile-mode must be preserve, update or error");
723
+ }
@@ -20,6 +20,7 @@ export declare function createSummary(input: {
20
20
  cooldownSkipped?: number;
21
21
  ciProfile?: Summary["ciProfile"];
22
22
  prLimitHit?: boolean;
23
+ policyOverridesApplied?: number;
23
24
  }): Summary;
24
25
  export declare function finalizeSummary(summary: Summary): Summary;
25
26
  export declare function resolveFailReason(updates: PackageUpdate[], errors: string[], failOn: FailOnLevel | undefined, maxUpdates: number | undefined, ciMode: boolean): FailReason;
@@ -1,6 +1,7 @@
1
1
  export function createSummary(input) {
2
2
  const offlineCacheMiss = input.errors.filter((error) => isOfflineCacheMissError(error)).length;
3
3
  const registryFailure = input.errors.filter((error) => isRegistryFailureError(error)).length;
4
+ const registryAuthFailure = input.errors.filter((error) => isRegistryAuthError(error)).length;
4
5
  const staleCache = input.warnings.filter((warning) => warning.includes("Using stale cache")).length;
5
6
  return {
6
7
  contractVersion: "2",
@@ -16,6 +17,7 @@ export function createSummary(input) {
16
17
  total: input.errors.length,
17
18
  offlineCacheMiss,
18
19
  registryFailure,
20
+ registryAuthFailure,
19
21
  other: 0,
20
22
  },
21
23
  warningCounts: {
@@ -38,10 +40,15 @@ export function createSummary(input) {
38
40
  cooldownSkipped: Math.max(0, Math.round(input.cooldownSkipped ?? 0)),
39
41
  ciProfile: input.ciProfile ?? "minimal",
40
42
  prLimitHit: input.prLimitHit === true,
43
+ streamedEvents: 0,
44
+ policyOverridesApplied: Math.max(0, Math.round(input.policyOverridesApplied ?? 0)),
41
45
  };
42
46
  }
43
47
  export function finalizeSummary(summary) {
44
- const errorOther = summary.errorCounts.total - summary.errorCounts.offlineCacheMiss - summary.errorCounts.registryFailure;
48
+ const errorOther = summary.errorCounts.total -
49
+ summary.errorCounts.offlineCacheMiss -
50
+ summary.errorCounts.registryFailure -
51
+ summary.errorCounts.registryAuthFailure;
45
52
  const warningOther = summary.warningCounts.total - summary.warningCounts.staleCache;
46
53
  summary.errorCounts.other = Math.max(0, errorOther);
47
54
  summary.warningCounts.other = Math.max(0, warningOther);
@@ -81,3 +88,6 @@ function isRegistryFailureError(value) {
81
88
  value.includes("Registry request failed") ||
82
89
  value.includes("Registry temporary error"));
83
90
  }
91
+ function isRegistryAuthError(value) {
92
+ return value.includes("Registry authentication failed") || value.includes("401");
93
+ }
@@ -3,7 +3,10 @@ import { readManifest, writeManifest } from "../parsers/package-json.js";
3
3
  import { installDependencies } from "../pm/install.js";
4
4
  import { applyRangeStyle, parseVersion, compareVersions } from "../utils/semver.js";
5
5
  import { buildWorkspaceGraph } from "../workspace/graph.js";
6
+ import { captureLockfileSnapshot, changedLockfiles, validateLockfileMode } from "../utils/lockfile.js";
6
7
  export async function upgrade(options) {
8
+ validateLockfileMode(options.lockfileMode, options.install);
9
+ const lockfilesBefore = await captureLockfileSnapshot(options.cwd);
7
10
  const checkResult = await check(options);
8
11
  if (checkResult.updates.length === 0) {
9
12
  return {
@@ -34,6 +37,13 @@ export async function upgrade(options) {
34
37
  if (options.install) {
35
38
  await installDependencies(options.cwd, options.packageManager, checkResult.packageManager);
36
39
  }
40
+ const lockfileChanges = await changedLockfiles(options.cwd, lockfilesBefore);
41
+ if (lockfileChanges.length > 0 && (options.lockfileMode === "preserve" || options.lockfileMode === "error")) {
42
+ throw new Error(`Lockfile changes detected in ${options.lockfileMode} mode: ${lockfileChanges.join(", ")}`);
43
+ }
44
+ if (lockfileChanges.length > 0 && options.lockfileMode === "update") {
45
+ checkResult.warnings.push(`Lockfiles changed: ${lockfileChanges.map((item) => item.split("/").pop()).join(", ")}`);
46
+ }
37
47
  return {
38
48
  ...checkResult,
39
49
  changed: true,
@@ -1,3 +1,4 @@
1
+ import process from "node:process";
1
2
  import { collectDependencies, readManifest } from "../parsers/package-json.js";
2
3
  import { matchesPattern } from "../utils/pattern.js";
3
4
  import { VersionCache } from "../cache/cache.js";
@@ -15,7 +16,10 @@ export async function warmCache(options) {
15
16
  const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
16
17
  discoveryMs += Date.now() - discoveryStartedAt;
17
18
  const cache = await VersionCache.create();
18
- const registryClient = new NpmRegistryClient(options.cwd);
19
+ const registryClient = new NpmRegistryClient(options.cwd, {
20
+ timeoutMs: options.registryTimeoutMs,
21
+ retries: options.registryRetries,
22
+ });
19
23
  const errors = [];
20
24
  const warnings = [];
21
25
  if (cache.degraded) {
@@ -23,6 +27,13 @@ export async function warmCache(options) {
23
27
  }
24
28
  let totalDependencies = 0;
25
29
  const packageNames = new Set();
30
+ let streamedEvents = 0;
31
+ const emitStream = (message) => {
32
+ if (!options.stream)
33
+ return;
34
+ streamedEvents += 1;
35
+ process.stdout.write(`${message}\n`);
36
+ };
26
37
  for (const packageDir of packageDirs) {
27
38
  try {
28
39
  const manifest = await readManifest(packageDir);
@@ -57,10 +68,12 @@ export async function warmCache(options) {
57
68
  const stale = await cache.getAny(pkg, options.target);
58
69
  if (stale) {
59
70
  warnings.push(`Using stale cache for ${pkg} in offline warm-cache mode.`);
71
+ emitStream(`[warm-cache-stale] ${pkg}`);
60
72
  warmed += 1;
61
73
  }
62
74
  else {
63
75
  errors.push(`Offline cache miss for ${pkg}. Cannot warm cache in --offline mode.`);
76
+ emitStream(`[error] Offline cache miss for ${pkg}`);
64
77
  }
65
78
  }
66
79
  cacheMs += Date.now() - cacheFallbackStartedAt;
@@ -69,6 +82,8 @@ export async function warmCache(options) {
69
82
  const registryStartedAt = Date.now();
70
83
  const fetched = await registryClient.resolveManyPackageMetadata(needsFetch, {
71
84
  concurrency: options.concurrency,
85
+ retries: options.registryRetries,
86
+ timeoutMs: options.registryTimeoutMs,
72
87
  });
73
88
  registryMs += Date.now() - registryStartedAt;
74
89
  const cacheWriteStartedAt = Date.now();
@@ -76,11 +91,13 @@ export async function warmCache(options) {
76
91
  if (metadata.latestVersion) {
77
92
  await cache.set(pkg, options.target, metadata.latestVersion, metadata.versions, options.cacheTtlSeconds);
78
93
  warmed += 1;
94
+ emitStream(`[warmed] ${pkg}@${metadata.latestVersion}`);
79
95
  }
80
96
  }
81
97
  cacheMs += Date.now() - cacheWriteStartedAt;
82
98
  for (const [pkg, error] of fetched.errors) {
83
99
  errors.push(`Unable to warm ${pkg}: ${error}`);
100
+ emitStream(`[error] Unable to warm ${pkg}: ${error}`);
84
101
  }
85
102
  }
86
103
  }
@@ -104,6 +121,7 @@ export async function warmCache(options) {
104
121
  },
105
122
  ciProfile: options.ciProfile,
106
123
  }));
124
+ summary.streamedEvents = streamedEvents;
107
125
  return {
108
126
  projectPath: options.cwd,
109
127
  packagePaths: packageDirs,
@@ -36,6 +36,9 @@ export function renderResult(result, format) {
36
36
  `cooldown_skipped=${result.summary.cooldownSkipped}`,
37
37
  `ci_profile=${result.summary.ciProfile}`,
38
38
  `pr_limit_hit=${result.summary.prLimitHit ? "1" : "0"}`,
39
+ `streamed_events=${result.summary.streamedEvents}`,
40
+ `policy_overrides_applied=${result.summary.policyOverridesApplied}`,
41
+ `registry_auth_failures=${result.summary.errorCounts.registryAuthFailure}`,
39
42
  `fix_pr_branches_created=${result.summary.fixPrBranchesCreated}`,
40
43
  ].join("\n");
41
44
  }
@@ -76,6 +79,7 @@ export function renderResult(result, format) {
76
79
  lines.push("");
77
80
  lines.push(`Summary: ${result.summary.updatesFound} updates, ${result.summary.checkedDependencies}/${result.summary.totalDependencies} checked, ${result.summary.warmedPackages} warmed`);
78
81
  lines.push(`Groups=${result.summary.groupedUpdates}, cooldownSkipped=${result.summary.cooldownSkipped}, ciProfile=${result.summary.ciProfile}, prLimitHit=${result.summary.prLimitHit ? "yes" : "no"}`);
82
+ lines.push(`StreamedEvents=${result.summary.streamedEvents}, policyOverrides=${result.summary.policyOverridesApplied}, registryAuthFailures=${result.summary.errorCounts.registryAuthFailure}`);
79
83
  lines.push(`Contract v${result.summary.contractVersion}, failReason=${result.summary.failReason}, duration=${result.summary.durationMs.total}ms`);
80
84
  if (result.summary.fixPrApplied) {
81
85
  lines.push(`Fix PR: applied on branch ${result.summary.fixBranchName ?? "unknown"} (${result.summary.fixCommitSha ?? "no-commit"})`);
@@ -18,6 +18,9 @@ export async function writeGitHubOutput(filePath, result) {
18
18
  `cooldown_skipped=${result.summary.cooldownSkipped}`,
19
19
  `ci_profile=${result.summary.ciProfile}`,
20
20
  `pr_limit_hit=${result.summary.prLimitHit === true ? "1" : "0"}`,
21
+ `streamed_events=${result.summary.streamedEvents}`,
22
+ `policy_overrides_applied=${result.summary.policyOverridesApplied}`,
23
+ `registry_auth_failures=${result.summary.errorCounts.registryAuthFailure}`,
21
24
  `fix_pr_applied=${result.summary.fixPrApplied === true ? "1" : "0"}`,
22
25
  `fix_pr_branches_created=${result.summary.fixPrBranchesCreated}`,
23
26
  `fix_pr_branch=${result.summary.fixBranchName ?? ""}`,
@@ -1,6 +1,11 @@
1
1
  export interface ResolveManyOptions {
2
2
  concurrency: number;
3
3
  timeoutMs?: number;
4
+ retries?: number;
5
+ }
6
+ export interface RegistryClientOptions {
7
+ timeoutMs?: number;
8
+ retries?: number;
4
9
  }
5
10
  export interface ResolveManyResult {
6
11
  metadata: Map<string, {
@@ -12,8 +17,10 @@ export interface ResolveManyResult {
12
17
  }
13
18
  export declare class NpmRegistryClient {
14
19
  private readonly requesterPromise;
15
- constructor(cwd?: string);
16
- resolvePackageMetadata(packageName: string, timeoutMs?: number): Promise<{
20
+ private readonly defaultTimeoutMs;
21
+ private readonly defaultRetries;
22
+ constructor(cwd?: string, options?: RegistryClientOptions);
23
+ resolvePackageMetadata(packageName: string, timeoutMs?: number, retries?: number): Promise<{
17
24
  latestVersion: string | null;
18
25
  versions: string[];
19
26
  publishedAtByVersion: Record<string, number>;