@mutmutco/cli 4.5.63 → 4.5.64

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 (2) hide show
  1. package/dist/main.cjs +404 -118
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -587,7 +587,147 @@ var init_github_client = __esm({
587
587
  }
588
588
  });
589
589
 
590
+ // src/github-quota.ts
591
+ function hubQuotaResetWaitMs(resetAt, nowMs, capMs = RATE_LIMIT_WAIT_CAP_MS, jitterMs = 0) {
592
+ if (typeof resetAt !== "string" || !resetAt) return void 0;
593
+ const resetMs = Date.parse(resetAt);
594
+ if (!Number.isFinite(resetMs)) return void 0;
595
+ const wait = rateLimitWaitMs(resetMs / 1e3, nowMs, capMs);
596
+ if (wait === void 0) return void 0;
597
+ return wait + Math.max(0, jitterMs);
598
+ }
599
+ function isRateLimitedError(e) {
600
+ return e instanceof RateLimitedError;
601
+ }
602
+ function isRateLimitText(text) {
603
+ return /API rate limit already exceeded|rate limit already exceeded|RATE_LIMITED|secondary rate|abuse detection|HTTP\/?\d*(?:\.\d)?\s+429\b|\b429\b.*rate limit/i.test(text) || /rate limit/i.test(text) && !/not rate.?limit/i.test(text);
604
+ }
605
+ function isRateLimitRefusal(e) {
606
+ if (e instanceof GitHubApiError && e.rateLimited) return true;
607
+ if (e instanceof GitHubApiError && e.status === 403) {
608
+ return /rate limit|abuse detection|secondary rate/i.test(e.message);
609
+ }
610
+ if (e instanceof RateLimitedError) return true;
611
+ const text = e instanceof Error ? `${e.message}
612
+ ${e.stderr ?? ""}` : String(e ?? "");
613
+ return isRateLimitText(text);
614
+ }
615
+ function resetEpochFromError(e) {
616
+ if (e instanceof GitHubApiError && typeof e.rateLimitReset === "number") return e.rateLimitReset;
617
+ if (e instanceof RateLimitedError && typeof e.receipt.resetEpochSeconds === "number") {
618
+ return e.receipt.resetEpochSeconds;
619
+ }
620
+ return void 0;
621
+ }
622
+ function rateLimitWaitMs(resetEpochSeconds, nowMs, capMs = RATE_LIMIT_WAIT_CAP_MS) {
623
+ if (typeof resetEpochSeconds !== "number" || !Number.isFinite(resetEpochSeconds)) return void 0;
624
+ const wait = resetEpochSeconds * 1e3 - nowMs;
625
+ if (wait <= 0) return 0;
626
+ if (wait > capMs) return void 0;
627
+ return wait;
628
+ }
629
+ function rateLimitedReceipt(opts) {
630
+ const resetEpochSeconds = opts.resetEpochSeconds;
631
+ const resetAt = typeof resetEpochSeconds === "number" ? new Date(resetEpochSeconds * 1e3).toISOString() : void 0;
632
+ const note = rateLimitResetNote(resetEpochSeconds, opts.nowMs ?? Date.now());
633
+ const context = opts.context ?? "GitHub API";
634
+ const remaining = opts.remaining && (opts.remaining.graphql !== void 0 || opts.remaining.core !== void 0) ? opts.remaining : void 0;
635
+ return {
636
+ status: "rate_limited",
637
+ ...resetEpochSeconds !== void 0 ? { resetEpochSeconds } : {},
638
+ ...resetAt ? { resetAt } : {},
639
+ message: opts.message ?? `${context}: ${note}`,
640
+ retryCommand: opts.retryCommand,
641
+ ...remaining ? { remaining } : {}
642
+ };
643
+ }
644
+ async function runWithRateLimitBackoff(operation, opts) {
645
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
646
+ const now = opts.now ?? Date.now;
647
+ const log = opts.log ?? ((message2) => console.warn(message2));
648
+ const capMs = opts.capMs ?? RATE_LIMIT_WAIT_CAP_MS;
649
+ const tryOnce = async () => {
650
+ try {
651
+ return await operation();
652
+ } catch (e) {
653
+ if (!isRateLimitRefusal(e) || !opts.fallback) throw e;
654
+ try {
655
+ return await opts.fallback();
656
+ } catch (e2) {
657
+ if (isRateLimitRefusal(e2)) throw e2;
658
+ throw e2;
659
+ }
660
+ }
661
+ };
662
+ try {
663
+ return await tryOnce();
664
+ } catch (e) {
665
+ if (!isRateLimitRefusal(e)) throw e;
666
+ const reset = resetEpochFromError(e);
667
+ const waitMs = rateLimitWaitMs(reset, now(), capMs);
668
+ if (waitMs === void 0) {
669
+ throw new RateLimitedError(rateLimitedReceipt({
670
+ resetEpochSeconds: reset,
671
+ retryCommand: opts.retryCommand,
672
+ context: opts.context,
673
+ nowMs: now()
674
+ }));
675
+ }
676
+ if (waitMs > 0) {
677
+ log(`${opts.context}: rate-limited \u2014 waiting ${Math.round(waitMs / 1e3)}s for pool reset`);
678
+ await sleep2(waitMs);
679
+ }
680
+ try {
681
+ return await tryOnce();
682
+ } catch (e2) {
683
+ if (!isRateLimitRefusal(e2)) throw e2;
684
+ throw new RateLimitedError(rateLimitedReceipt({
685
+ resetEpochSeconds: resetEpochFromError(e2) ?? reset,
686
+ retryCommand: opts.retryCommand,
687
+ context: opts.context,
688
+ nowMs: now()
689
+ }));
690
+ }
691
+ }
692
+ }
693
+ var RATE_LIMIT_WAIT_CAP_MS, RateLimitedError;
694
+ var init_github_quota = __esm({
695
+ "src/github-quota.ts"() {
696
+ "use strict";
697
+ init_github_client();
698
+ RATE_LIMIT_WAIT_CAP_MS = 12e4;
699
+ RateLimitedError = class extends Error {
700
+ receipt;
701
+ constructor(receipt) {
702
+ super(receipt.message);
703
+ this.name = "RateLimitedError";
704
+ this.receipt = receipt;
705
+ }
706
+ };
707
+ }
708
+ });
709
+
590
710
  // src/fetch-retry.ts
711
+ async function hubQuota503(res, opts) {
712
+ if (res.status !== 503) return "pass";
713
+ let body;
714
+ try {
715
+ body = await res.clone().json();
716
+ } catch {
717
+ return "pass";
718
+ }
719
+ const code = body?.code;
720
+ if (code !== "github-rate-limited") return "pass";
721
+ const q = opts.hubQuotaWait;
722
+ const resetAt = body?.resetAt;
723
+ const wait = hubQuotaResetWaitMs(
724
+ typeof resetAt === "string" ? resetAt : void 0,
725
+ q?.now?.() ?? Date.now(),
726
+ q?.capMs ?? RATE_LIMIT_WAIT_CAP_MS,
727
+ (q?.jitterMs ?? (() => Math.floor(Math.random() * HUB_QUOTA_JITTER_MAX_MS)))()
728
+ );
729
+ return wait === void 0 ? "fail-fast" : { waitMs: wait };
730
+ }
591
731
  async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
592
732
  const attempts = opts.attempts ?? 3;
593
733
  const baseDelayMs = opts.baseDelayMs ?? 250;
@@ -599,9 +739,17 @@ async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
599
739
  const attemptInit = opts.timeoutMs ? { ...init, signal: AbortSignal.timeout(opts.timeoutMs) } : init;
600
740
  try {
601
741
  const res = await fetchImpl(url, attemptInit);
602
- if (!isLast && retryOn(res)) {
603
- await sleep2(baseDelayMs * 2 ** i);
604
- continue;
742
+ if (!isLast) {
743
+ const quota = await hubQuota503(res, opts);
744
+ if (typeof quota === "object") {
745
+ await sleep2(quota.waitMs);
746
+ continue;
747
+ }
748
+ if (quota === "fail-fast") return res;
749
+ if (retryOn(res)) {
750
+ await sleep2(baseDelayMs * 2 ** i);
751
+ continue;
752
+ }
605
753
  }
606
754
  return res;
607
755
  } catch (e) {
@@ -612,9 +760,12 @@ async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
612
760
  }
613
761
  throw lastErr;
614
762
  }
763
+ var HUB_QUOTA_JITTER_MAX_MS;
615
764
  var init_fetch_retry = __esm({
616
765
  "src/fetch-retry.ts"() {
617
766
  "use strict";
767
+ init_github_quota();
768
+ HUB_QUOTA_JITTER_MAX_MS = 5e3;
618
769
  }
619
770
  });
620
771
 
@@ -5568,114 +5719,12 @@ async function fetchHubBoardSnapshot(request, deps) {
5568
5719
  return { state: "ok", snapshot: body };
5569
5720
  }
5570
5721
 
5571
- // src/github-quota.ts
5572
- init_github_client();
5573
- var RATE_LIMIT_WAIT_CAP_MS = 12e4;
5574
- var RateLimitedError = class extends Error {
5575
- receipt;
5576
- constructor(receipt) {
5577
- super(receipt.message);
5578
- this.name = "RateLimitedError";
5579
- this.receipt = receipt;
5580
- }
5581
- };
5582
- function isRateLimitedError(e) {
5583
- return e instanceof RateLimitedError;
5584
- }
5585
- function isRateLimitText(text) {
5586
- return /API rate limit already exceeded|rate limit already exceeded|RATE_LIMITED|secondary rate|abuse detection|HTTP\/?\d*(?:\.\d)?\s+429\b|\b429\b.*rate limit/i.test(text) || /rate limit/i.test(text) && !/not rate.?limit/i.test(text);
5587
- }
5588
- function isRateLimitRefusal(e) {
5589
- if (e instanceof GitHubApiError && e.rateLimited) return true;
5590
- if (e instanceof GitHubApiError && e.status === 403) {
5591
- return /rate limit|abuse detection|secondary rate/i.test(e.message);
5592
- }
5593
- if (e instanceof RateLimitedError) return true;
5594
- const text = e instanceof Error ? `${e.message}
5595
- ${e.stderr ?? ""}` : String(e ?? "");
5596
- return isRateLimitText(text);
5597
- }
5598
- function resetEpochFromError(e) {
5599
- if (e instanceof GitHubApiError && typeof e.rateLimitReset === "number") return e.rateLimitReset;
5600
- if (e instanceof RateLimitedError && typeof e.receipt.resetEpochSeconds === "number") {
5601
- return e.receipt.resetEpochSeconds;
5602
- }
5603
- return void 0;
5604
- }
5605
- function rateLimitWaitMs(resetEpochSeconds, nowMs, capMs = RATE_LIMIT_WAIT_CAP_MS) {
5606
- if (typeof resetEpochSeconds !== "number" || !Number.isFinite(resetEpochSeconds)) return void 0;
5607
- const wait = resetEpochSeconds * 1e3 - nowMs;
5608
- if (wait <= 0) return 0;
5609
- if (wait > capMs) return void 0;
5610
- return wait;
5611
- }
5612
- function rateLimitedReceipt(opts) {
5613
- const resetEpochSeconds = opts.resetEpochSeconds;
5614
- const resetAt = typeof resetEpochSeconds === "number" ? new Date(resetEpochSeconds * 1e3).toISOString() : void 0;
5615
- const note = rateLimitResetNote(resetEpochSeconds, opts.nowMs ?? Date.now());
5616
- const context = opts.context ?? "GitHub API";
5617
- const remaining = opts.remaining && (opts.remaining.graphql !== void 0 || opts.remaining.core !== void 0) ? opts.remaining : void 0;
5618
- return {
5619
- status: "rate_limited",
5620
- ...resetEpochSeconds !== void 0 ? { resetEpochSeconds } : {},
5621
- ...resetAt ? { resetAt } : {},
5622
- message: opts.message ?? `${context}: ${note}`,
5623
- retryCommand: opts.retryCommand,
5624
- ...remaining ? { remaining } : {}
5625
- };
5626
- }
5627
- async function runWithRateLimitBackoff(operation, opts) {
5628
- const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
5629
- const now = opts.now ?? Date.now;
5630
- const log = opts.log ?? ((message2) => console.warn(message2));
5631
- const capMs = opts.capMs ?? RATE_LIMIT_WAIT_CAP_MS;
5632
- const tryOnce = async () => {
5633
- try {
5634
- return await operation();
5635
- } catch (e) {
5636
- if (!isRateLimitRefusal(e) || !opts.fallback) throw e;
5637
- try {
5638
- return await opts.fallback();
5639
- } catch (e2) {
5640
- if (isRateLimitRefusal(e2)) throw e2;
5641
- throw e2;
5642
- }
5643
- }
5644
- };
5645
- try {
5646
- return await tryOnce();
5647
- } catch (e) {
5648
- if (!isRateLimitRefusal(e)) throw e;
5649
- const reset = resetEpochFromError(e);
5650
- const waitMs = rateLimitWaitMs(reset, now(), capMs);
5651
- if (waitMs === void 0) {
5652
- throw new RateLimitedError(rateLimitedReceipt({
5653
- resetEpochSeconds: reset,
5654
- retryCommand: opts.retryCommand,
5655
- context: opts.context,
5656
- nowMs: now()
5657
- }));
5658
- }
5659
- if (waitMs > 0) {
5660
- log(`${opts.context}: rate-limited \u2014 waiting ${Math.round(waitMs / 1e3)}s for pool reset`);
5661
- await sleep2(waitMs);
5662
- }
5663
- try {
5664
- return await tryOnce();
5665
- } catch (e2) {
5666
- if (!isRateLimitRefusal(e2)) throw e2;
5667
- throw new RateLimitedError(rateLimitedReceipt({
5668
- resetEpochSeconds: resetEpochFromError(e2) ?? reset,
5669
- retryCommand: opts.retryCommand,
5670
- context: opts.context,
5671
- nowMs: now()
5672
- }));
5673
- }
5674
- }
5675
- }
5722
+ // src/board-read.ts
5723
+ init_github_quota();
5676
5724
 
5677
5725
  // src/claim-marker-client.ts
5678
5726
  init_github_client();
5727
+ init_github_quota();
5679
5728
 
5680
5729
  // src/pr-checks-rest.ts
5681
5730
  init_cli_shared();
@@ -6519,6 +6568,7 @@ function withAppClaimMarkerRead(client, appClient) {
6519
6568
  }
6520
6569
 
6521
6570
  // src/board-dependency.ts
6571
+ init_github_quota();
6522
6572
  var DEPENDS_ON_LINE = /^\s*[-*]?\s*\*\*Depends on:\*\*\s*(.+)$/im;
6523
6573
  var ISSUE_REF = /([a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+)#(\d+)/g;
6524
6574
  var DEPENDENCY_FETCH_CONCURRENCY = 8;
@@ -7209,6 +7259,9 @@ function jobNeeds(job) {
7209
7259
  function unquote(s) {
7210
7260
  return s.replace(/#.*$/, "").trim().replace(/^['"]|['"]$/g, "");
7211
7261
  }
7262
+ function contextMatchesJobId(jobId, context) {
7263
+ return context === jobId || context.startsWith(`${jobId} / `);
7264
+ }
7212
7265
  function unblockingGateJobs(workflows, required) {
7213
7266
  const unblocking = /* @__PURE__ */ new Set();
7214
7267
  for (const wf of workflows) {
@@ -7216,7 +7269,7 @@ function unblockingGateJobs(workflows, required) {
7216
7269
  const jobs = parseWorkflowJobs(wf.body);
7217
7270
  const needs = new Map(jobs.map((job) => [job.id, jobNeeds(job)]));
7218
7271
  const covered = /* @__PURE__ */ new Set();
7219
- const stack = jobs.map((job) => job.id).filter((id) => required.has(id));
7272
+ const stack = jobs.map((job) => job.id).filter((id) => [...required].some((context) => contextMatchesJobId(id, context)));
7220
7273
  while (stack.length) {
7221
7274
  const id = stack.pop();
7222
7275
  if (covered.has(id)) continue;
@@ -9174,6 +9227,7 @@ function isPriorityFieldConfigured(cfg) {
9174
9227
  // src/gh-create.ts
9175
9228
  init_cli_shared();
9176
9229
  init_github_client();
9230
+ init_github_quota();
9177
9231
  var ISSUE_TYPES = ["bug", "feature", "task"];
9178
9232
  var GH_MUTATION_TIMEOUT_MS = 12e4;
9179
9233
  function timeoutKillNote(err, timeoutMs) {
@@ -13483,9 +13537,11 @@ function ghError(e) {
13483
13537
 
13484
13538
  // src/board-claim-move.ts
13485
13539
  init_github_client();
13540
+ init_github_quota();
13486
13541
 
13487
13542
  // src/sub-issue.ts
13488
13543
  init_error_codes();
13544
+ init_github_quota();
13489
13545
  function buildResolveIdArgs(ref) {
13490
13546
  const args = ["issue", "view", String(ref.number), "--json", "id", "--jq", ".id"];
13491
13547
  if (ref.repo) args.push("--repo", ref.repo);
@@ -17172,10 +17228,10 @@ var rollout_plan_default = {
17172
17228
  note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
17173
17229
  },
17174
17230
  baseline: {
17175
- version: "4.5.63",
17176
- tag: "v4.5.63",
17177
- commit: "89c6b4bfae7e",
17178
- npm: "@mutmutco/cli@4.5.63"
17231
+ version: "4.5.64",
17232
+ tag: "v4.5.64",
17233
+ commit: "f201a5b12b79",
17234
+ npm: "@mutmutco/cli@4.5.64"
17179
17235
  },
17180
17236
  exitCriterion: "fleet-n-of-n",
17181
17237
  hubOnlyShortcut: "forbidden",
@@ -17192,14 +17248,14 @@ var rollout_plan_default = {
17192
17248
  repo: "mutmutco/mmi-hub",
17193
17249
  role: "canary",
17194
17250
  schedule: "train",
17195
- v3Target: "v4.5.63"
17251
+ v3Target: "v4.5.64"
17196
17252
  }
17197
17253
  ],
17198
17254
  rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
17199
17255
  rollback: {
17200
17256
  independent: true,
17201
- mechanism: "npm dist-tag latest -> 4.5.63 and redeploy the Hub Lambda from tag v4.5.63 (89c6b4bfae7e); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17202
- v3Target: "v4.5.63 (@mutmutco/cli@4.5.63, tag commit 89c6b4bfae7e \u2014 last known-good release carrying the repo-index v4-only contract)"
17257
+ mechanism: "npm dist-tag latest -> 4.5.64 and redeploy the Hub Lambda from tag v4.5.64 (f201a5b12b79); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17258
+ v3Target: "v4.5.64 (@mutmutco/cli@4.5.64, tag commit f201a5b12b79 \u2014 last known-good release carrying the repo-index v4-only contract)"
17203
17259
  }
17204
17260
  },
17205
17261
  {
@@ -27982,7 +28038,7 @@ async function auditRepoCi(repo, callerDeps) {
27982
28038
  if (hasRequiredChecks) {
27983
28039
  const gateFiles = new Set(gateWorkflowFiles(prWorkflowPaths));
27984
28040
  const gateWorkflows = (await getBranchWorkflows() ?? []).filter((wf) => gateFiles.has(wf.path.slice(wf.path.lastIndexOf("/") + 1)));
27985
- const unblocking = unblockingGateJobs(gateWorkflows, new Set(liveContexts)).filter((job) => !AGENT_PR_BOOKKEEPING_CONTEXTS.has(job) && !branchPlan?.pending.some((entry) => entry.branch === baseBranch && entry.contexts.includes(job)));
28041
+ const unblocking = unblockingGateJobs(gateWorkflows, new Set(liveContexts)).filter((job) => !AGENT_PR_BOOKKEEPING_CONTEXTS.has(job) && !branchPlan?.pending.some((entry) => entry.branch === baseBranch && entry.contexts.some((context) => contextMatchesJobId(job, context))));
27986
28042
  checks.push({
27987
28043
  ok: unblocking.length === 0,
27988
28044
  label: UNBLOCKING_GATE_JOBS_LABEL,
@@ -28067,7 +28123,7 @@ async function auditRepoCi(repo, callerDeps) {
28067
28123
  const body = await fetchFileContent(deps, repo, baseBranch, path2);
28068
28124
  if (body == null) continue;
28069
28125
  if (isGateWorkflowPath(path2)) gateBodies.push({ path: path2, body });
28070
- if (!/^\s*concurrency:/m.test(body) || !/cancel-in-progress:\s*true/.test(body)) {
28126
+ if (!/^\s*concurrency:/m.test(body) || !/^\s*cancel-in-progress:[ \t]*(?:true|\$\{\{\s*github\.event_name\s*==\s*(['"])pull_request\1\s*\}\})[ \t]*(?:#.*)?$/m.test(body)) {
28071
28127
  if (NO_CANCEL_WAIVER.test(body)) waived.push(path2);
28072
28128
  else missing.push(path2);
28073
28129
  }
@@ -29683,6 +29739,7 @@ async function activateAppActor(commandPath3, env, mint, note = (line) => proces
29683
29739
  init_cli_shared();
29684
29740
 
29685
29741
  // src/attach-to-project.ts
29742
+ init_github_quota();
29686
29743
  async function attachViaHubApp(cfg, contentNodeId) {
29687
29744
  const { addIssueToProjectViaHub: addIssueToProjectViaHub2 } = await Promise.resolve().then(() => (init_board_write(), board_write_exports));
29688
29745
  return addIssueToProjectViaHub2(cfg, contentNodeId);
@@ -29843,6 +29900,7 @@ async function waitBoardEvents(since, waitS, deps) {
29843
29900
  }
29844
29901
 
29845
29902
  // src/board-commands.ts
29903
+ init_github_quota();
29846
29904
  function refuseRateLimited(e, json) {
29847
29905
  if (!isRateLimitedError(e)) return false;
29848
29906
  if (json) console.log(JSON.stringify(e.receipt));
@@ -35848,6 +35906,194 @@ Each member's live release must serve <data>/main/module.js at its main pin path
35848
35906
  return record;
35849
35907
  }
35850
35908
 
35909
+ // src/suite-rc-data.ts
35910
+ var RC_DATA_VAULT_REPO = "mutmutco/MM-Work";
35911
+ var RC_DATA_VERCEL_PROJECT = "prj_2Q1UcZ6tscIxHr4lPEhSD3jU1ZGm";
35912
+ var RC_DATA_VERCEL_TEAM = "team_ntTdWEQ896Eh20RT5Ri5XJDB";
35913
+ var RC_DATA_ALIAS = "rc.mutmut.work";
35914
+ var RC_DATA_GIT_BRANCH = "rc";
35915
+ var RC_DATA_BOUND_KEYS = [
35916
+ { envKey: "DATABASE_URL", liveVaultKey: "main/DATABASE_URL", rcVaultKey: "rc/DATABASE_URL" },
35917
+ { envKey: "MIGRATION_DATABASE_URL", liveVaultKey: "main/MIGRATION_DATABASE_URL", rcVaultKey: "rc/MIGRATION_DATABASE_URL" },
35918
+ { envKey: "MMW_DATA_KEY", liveVaultKey: "main/MMW_DATA_KEY", rcVaultKey: "rc/MMW_DATA_KEY" },
35919
+ { envKey: "GOOGLE_CLIENT_ID", liveVaultKey: "GOOGLE_CLIENT_ID", rcVaultKey: null },
35920
+ { envKey: "GOOGLE_CLIENT_SECRET", liveVaultKey: "GOOGLE_CLIENT_SECRET", rcVaultKey: null }
35921
+ ];
35922
+ var POLL_ATTEMPTS = 60;
35923
+ var POLL_INTERVAL_MS = 5e3;
35924
+ function withTeam(path2) {
35925
+ const url = new URL(`https://api.vercel.com${path2}`);
35926
+ url.searchParams.set("teamId", RC_DATA_VERCEL_TEAM);
35927
+ return url.toString();
35928
+ }
35929
+ function envCollectionUrl2() {
35930
+ return withTeam(`/v10/projects/${encodeURIComponent(RC_DATA_VERCEL_PROJECT)}/env`);
35931
+ }
35932
+ function envItemUrl2(id) {
35933
+ return withTeam(`/v9/projects/${encodeURIComponent(RC_DATA_VERCEL_PROJECT)}/env/${encodeURIComponent(id)}`);
35934
+ }
35935
+ function aliasUrl() {
35936
+ return withTeam(`/v4/aliases/${RC_DATA_ALIAS}`);
35937
+ }
35938
+ function deploymentUrl(id) {
35939
+ return withTeam(`/v13/deployments/${encodeURIComponent(id)}`);
35940
+ }
35941
+ function redeployUrl() {
35942
+ const url = new URL("https://api.vercel.com/v13/deployments");
35943
+ url.searchParams.set("forceNew", "1");
35944
+ url.searchParams.set("teamId", RC_DATA_VERCEL_TEAM);
35945
+ return url.toString();
35946
+ }
35947
+ function isRcBranchEnv(row, key) {
35948
+ if (row.key !== key) return false;
35949
+ if (row.gitBranch !== RC_DATA_GIT_BRANCH) return false;
35950
+ const target = Array.isArray(row.target) ? row.target.filter((item) => typeof item === "string") : [];
35951
+ return target.includes("preview");
35952
+ }
35953
+ function envsOf2(body) {
35954
+ if (Array.isArray(body)) return body;
35955
+ if (body && typeof body === "object" && Array.isArray(body.envs)) {
35956
+ return body.envs;
35957
+ }
35958
+ return null;
35959
+ }
35960
+ function scrub(reason, secrets) {
35961
+ let out = reason.replace(/\s+/g, " ").trim().slice(0, 300);
35962
+ for (const secret of secrets) {
35963
+ if (secret) out = out.split(secret).join("[redacted]");
35964
+ }
35965
+ return out || "unknown error";
35966
+ }
35967
+ async function readJson(deps, label, url, secrets, init) {
35968
+ const token = secrets[0];
35969
+ const headers = { ...init?.headers ?? {}, ...token ? { Authorization: `Bearer ${token}` } : {} };
35970
+ const method = init?.method ?? "GET";
35971
+ let res;
35972
+ try {
35973
+ res = await deps.fetch(url, { ...init, method, headers });
35974
+ } catch (e) {
35975
+ throw new Error(`${label} failed: ${scrub(e instanceof Error ? e.message : String(e), secrets)}`);
35976
+ }
35977
+ let raw = "";
35978
+ try {
35979
+ raw = await res.text();
35980
+ } catch (e) {
35981
+ throw new Error(`${label} failed: ${scrub(e instanceof Error ? e.message : String(e), secrets)}`);
35982
+ }
35983
+ if (!res.ok) throw new Error(`${label} failed: HTTP ${res.status}${raw ? `: ${scrub(raw, secrets)}` : ""}`);
35984
+ if (!raw.trim()) return {};
35985
+ try {
35986
+ return JSON.parse(raw);
35987
+ } catch {
35988
+ throw new Error(`${label} failed: response was not JSON`);
35989
+ }
35990
+ }
35991
+ function deploymentReadyState(body) {
35992
+ if (body && typeof body === "object") {
35993
+ const row = body;
35994
+ if (typeof row.status === "string") return row.status;
35995
+ if (typeof row.readyState === "string") return row.readyState;
35996
+ }
35997
+ return null;
35998
+ }
35999
+ function deploymentName(body) {
36000
+ if (body && typeof body === "object" && typeof body.name === "string") {
36001
+ return body.name;
36002
+ }
36003
+ return null;
36004
+ }
36005
+ function aliasDeploymentId(body) {
36006
+ if (body && typeof body === "object" && typeof body.deploymentId === "string") {
36007
+ return body.deploymentId;
36008
+ }
36009
+ return null;
36010
+ }
36011
+ async function runRcDataSwitch(deps, opts) {
36012
+ if (opts.to !== "live" && opts.to !== "rc") {
36013
+ throw new Error(`--to must be live or rc (got ${String(opts.to ?? "nothing")}); nothing was written`);
36014
+ }
36015
+ if (opts.apply && !opts.ownerGo) {
36016
+ throw new Error("--apply requires --owner-go (it touches the live data key); nothing was written");
36017
+ }
36018
+ const secrets = [];
36019
+ const token = await deps.readToken();
36020
+ secrets.push(token);
36021
+ if (!token) throw new Error("no _org/vercel/API_TOKEN; nothing was written");
36022
+ const listed = await readJson(deps, "GET envs", envCollectionUrl2(), secrets, { method: "GET" });
36023
+ const rows = envsOf2(listed);
36024
+ if (!rows) throw new Error("env list returned an unexpected shape; nothing was written");
36025
+ const plan = RC_DATA_BOUND_KEYS.map((bound) => {
36026
+ const source = opts.to === "live" ? bound.liveVaultKey : bound.rcVaultKey;
36027
+ const existing = rows.find((row) => isRcBranchEnv(row, bound.envKey) && typeof row.id === "string" && row.id.length > 0);
36028
+ const entryId = existing && typeof existing.id === "string" ? existing.id : null;
36029
+ if (source === null) return { action: existing ? "delete" : "noop", key: bound.envKey, entryId, source };
36030
+ return { action: entryId ? "patch" : "create", key: bound.envKey, entryId, source };
36031
+ });
36032
+ if (!opts.apply) {
36033
+ return { command: "suite rc-data", direction: opts.to, dryRun: true, plan, changed: [] };
36034
+ }
36035
+ const values = /* @__PURE__ */ new Map();
36036
+ for (const step of plan) {
36037
+ if (step.action !== "patch" && step.action !== "create") continue;
36038
+ const value = await deps.readVault(step.source);
36039
+ secrets.push(value);
36040
+ if (!value) throw new Error(`vault ${step.source} is missing or unreadable; nothing was written`);
36041
+ values.set(step.key, value);
36042
+ }
36043
+ const changed = [];
36044
+ for (const step of plan) {
36045
+ try {
36046
+ if (step.action === "patch") {
36047
+ await readJson(deps, `PATCH ${step.key}`, envItemUrl2(step.entryId), secrets, {
36048
+ method: "PATCH",
36049
+ headers: { "Content-Type": "application/json" },
36050
+ body: JSON.stringify({ value: values.get(step.key), type: "sensitive" })
36051
+ });
36052
+ changed.push(step.key);
36053
+ } else if (step.action === "create") {
36054
+ await readJson(deps, `POST ${step.key}`, envCollectionUrl2(), secrets, {
36055
+ method: "POST",
36056
+ headers: { "Content-Type": "application/json" },
36057
+ body: JSON.stringify({ key: step.key, value: values.get(step.key), type: "sensitive", target: ["preview"], gitBranch: RC_DATA_GIT_BRANCH })
36058
+ });
36059
+ changed.push(step.key);
36060
+ } else if (step.action === "delete") {
36061
+ await readJson(deps, `DELETE ${step.key}`, envItemUrl2(step.entryId), secrets, { method: "DELETE" });
36062
+ changed.push(step.key);
36063
+ }
36064
+ } catch (e) {
36065
+ const changedLabel = changed.length ? changed.join(", ") : "none";
36066
+ throw new Error(`${e.message}; changed: ${changedLabel}; no redeploy`);
36067
+ }
36068
+ }
36069
+ const aliasBefore = aliasDeploymentId(await readJson(deps, "GET alias", aliasUrl(), secrets, { method: "GET" }));
36070
+ if (!aliasBefore) throw new Error(`alias ${RC_DATA_ALIAS} has no deployment; envs changed: ${changed.join(", ")}; no redeploy`);
36071
+ const name = deploymentName(await readJson(deps, "GET deployment", deploymentUrl(aliasBefore), secrets, { method: "GET" }));
36072
+ if (!name) throw new Error(`deployment ${aliasBefore} has no name; envs changed: ${changed.join(", ")}; no redeploy`);
36073
+ const created = await readJson(deps, "POST redeploy", redeployUrl(), secrets, {
36074
+ method: "POST",
36075
+ headers: { "Content-Type": "application/json" },
36076
+ body: JSON.stringify({ name, deploymentId: aliasBefore, meta: { redeployReason: `MMI-Hub#7588 rc-data switch to ${opts.to}` } })
36077
+ });
36078
+ const newId = aliasDeploymentId({ deploymentId: created.id });
36079
+ if (!newId) throw new Error(`redeploy answered without an id; envs changed: ${changed.join(", ")}`);
36080
+ let state = null;
36081
+ for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt += 1) {
36082
+ state = deploymentReadyState(await readJson(deps, "GET deployment", deploymentUrl(newId), secrets, { method: "GET" }));
36083
+ if (state === "READY") break;
36084
+ if (state === "ERROR" || state === "CANCELED" || state === "FAILED") {
36085
+ throw new Error(`redeploy ${newId} ended ${state}; envs changed: ${changed.join(", ")}`);
36086
+ }
36087
+ await deps.sleep(POLL_INTERVAL_MS);
36088
+ }
36089
+ if (state !== "READY") throw new Error(`redeploy ${newId} never reached READY (last: ${state ?? "unknown"}); envs changed: ${changed.join(", ")}`);
36090
+ const aliasAfter = aliasDeploymentId(await readJson(deps, "GET alias", aliasUrl(), secrets, { method: "GET" }));
36091
+ if (aliasAfter !== newId) {
36092
+ throw new Error(`alias ${RC_DATA_ALIAS} still points at ${aliasAfter ?? "nothing"} (expected ${newId}); envs changed: ${changed.join(", ")}`);
36093
+ }
36094
+ return { command: "suite rc-data", direction: opts.to, dryRun: false, plan, changed, deploymentId: newId, alias: RC_DATA_ALIAS };
36095
+ }
36096
+
35851
36097
  // src/suite-train-io.ts
35852
36098
  var import_node_fs43 = require("node:fs");
35853
36099
  var import_node_path43 = require("node:path");
@@ -39437,6 +39683,7 @@ function applyChecklistCheck(body, query, checked) {
39437
39683
 
39438
39684
  // src/command-register-collaboration.ts
39439
39685
  init_github_client();
39686
+ init_github_quota();
39440
39687
 
39441
39688
  // src/issue-commands.ts
39442
39689
  var import_node_fs34 = require("node:fs");
@@ -40377,6 +40624,7 @@ var import_node_path35 = require("node:path");
40377
40624
 
40378
40625
  // src/cross-repo-filing-issue.ts
40379
40626
  init_github_client();
40627
+ init_github_quota();
40380
40628
  function crossRepoFilingRetryCommand(prRepo, prNumber) {
40381
40629
  return `mmi-cli devops pr merge ${prNumber} --repo ${prRepo}`;
40382
40630
  }
@@ -40479,6 +40727,7 @@ ${pr.body ?? ""}`;
40479
40727
  }
40480
40728
 
40481
40729
  // src/post-merge-recon.ts
40730
+ init_github_quota();
40482
40731
  var POST_MERGE_RECON_SCHEMA_VERSION = 1;
40483
40732
  function postMergeReconRetryCommand(repo, pr) {
40484
40733
  return crossRepoFilingRetryCommand(repo, Number(pr));
@@ -41281,6 +41530,7 @@ async function checkDocsIndexAtHead(opts, deps) {
41281
41530
 
41282
41531
  // src/pr-create-claim-guard.ts
41283
41532
  init_github_client();
41533
+ init_github_quota();
41284
41534
  var CLAIM_GUARD_RATE_LIMIT_WAIT_CAP_MS = 3e4;
41285
41535
  function withRateLimitRetry(client, seams = {}) {
41286
41536
  const throttledPaths = /* @__PURE__ */ new Set();
@@ -46051,6 +46301,17 @@ function parseMemberSummaryFiles(entries) {
46051
46301
  }
46052
46302
  return memberSummaryFiles;
46053
46303
  }
46304
+ function suiteRcDataProdDeps() {
46305
+ return {
46306
+ fetch: async (url, init) => {
46307
+ const res = await fetch(url, { ...init, signal: AbortSignal.timeout(15e3) });
46308
+ return { ok: res.ok, status: res.status, text: () => res.text() };
46309
+ },
46310
+ readToken: readOrgVercelApiToken,
46311
+ readVault: async (vaultKey) => fetchSecretValue(makeSecretsDeps(await loadConfig()), vaultKey, { repo: RC_DATA_VAULT_REPO }),
46312
+ sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms))
46313
+ };
46314
+ }
46054
46315
  function registerSuiteCommands(program3) {
46055
46316
  const suite = program3.command("suite").description("MM-Work suite status, candidate cut and release (#7262/#7263)");
46056
46317
  const leadRoot = async () => {
@@ -46123,6 +46384,31 @@ function registerSuiteCommands(program3) {
46123
46384
  return fail(`suite release: ${e.message}`);
46124
46385
  }
46125
46386
  });
46387
+ suite.command("rc-data").description("switch the rc Preview envs between the live and rc data sets as one bound switch (#7588)").option("--to <direction>", "switch source: live or rc").option("--apply", "write the switch (default is a dry-run plan)").option("--owner-go", "explicit approval: touches the live data key").option("--json", "structured receipt").action(async (o) => {
46388
+ try {
46389
+ if (o.to !== "live" && o.to !== "rc") throw new Error(`--to must be live or rc (got ${o.to ?? "nothing"}); nothing was written`);
46390
+ const result = await runRcDataSwitch(suiteRcDataProdDeps(), { to: o.to, apply: o.apply === true, ownerGo: o.ownerGo === true });
46391
+ if (o.json) {
46392
+ console.log(JSON.stringify(result, null, 2));
46393
+ return;
46394
+ }
46395
+ console.log([
46396
+ `suite rc-data ${result.dryRun ? "dry-run" : "receipt"} (--to ${result.direction}):`,
46397
+ ...result.plan.map((step) => {
46398
+ if (step.action === "patch") return ` - patch ${step.key} rc entry ${step.entryId} <- vault ${step.source}`;
46399
+ if (step.action === "create") return ` - create ${step.key} rc preview entry <- vault ${step.source}`;
46400
+ if (step.action === "delete") return ` - delete ${step.key} rc entry ${step.entryId} (generic Preview applies again)`;
46401
+ return ` - keep ${step.key} (no rc entry; generic Preview already applies)`;
46402
+ }),
46403
+ ...result.dryRun ? [" rerun with --apply --owner-go to write, then redeploy rc.mutmut.work"] : [
46404
+ ` - changed: ${result.changed.join(", ") || "none"}`,
46405
+ ` - redeployed ${result.deploymentId}; alias ${result.alias} verified`
46406
+ ]
46407
+ ].join("\n"));
46408
+ } catch (e) {
46409
+ return fail(`suite rc-data: ${e.message}`);
46410
+ }
46411
+ });
46126
46412
  suite.command("status").description("compare served versions with development, rc and main branch heads across MM-Work modules").option("--suite <name>", "suite name (mm-work)", "mm-work").option("--json", "structured output").action(async (o) => {
46127
46413
  if (o.suite !== "mm-work") return fail("suite status: only mm-work is supported");
46128
46414
  const github = defaultGitHubClient();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.63",
3
+ "version": "4.5.64",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",