@mutmutco/cli 4.5.36 → 4.5.38

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 +296 -59
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -863,6 +863,9 @@ var init_house_map = __esm({
863
863
  });
864
864
 
865
865
  // src/stdin-inject.ts
866
+ function setInjectedStdin(payload) {
867
+ injectedStdin = payload;
868
+ }
866
869
  function stdinHasPipedInput(statFd = () => (0, import_node_fs4.fstatSync)(0), getIsTTY = () => process.stdin.isTTY) {
867
870
  try {
868
871
  const stat4 = statFd();
@@ -922,6 +925,34 @@ var init_stdin_inject = __esm({
922
925
  });
923
926
 
924
927
  // src/cli-shared.ts
928
+ var cli_shared_exports = {};
929
+ __export(cli_shared_exports, {
930
+ DEFAULT_EXEC_TIMEOUT_MS: () => DEFAULT_EXEC_TIMEOUT_MS,
931
+ ExecDeadlineError: () => ExecDeadlineError,
932
+ GIT_TIMEOUT_MS: () => GIT_TIMEOUT_MS,
933
+ consoleIo: () => consoleIo,
934
+ execFileHard: () => execFileHard,
935
+ execFileP: () => execFileP,
936
+ fail: () => fail,
937
+ failGraceful: () => failGraceful,
938
+ failGracefulEnvelope: () => failGracefulEnvelope,
939
+ gitOut: () => gitOut,
940
+ githubLogin: () => githubLogin,
941
+ hubHeaders: () => hubHeaders,
942
+ installBrokenPipeGuard: () => installBrokenPipeGuard,
943
+ installProcessBackstop: () => installProcessBackstop,
944
+ isOrgRepoRoot: () => isOrgRepoRoot,
945
+ jsonParity: () => jsonParity,
946
+ loadConfig: () => loadConfig,
947
+ mutating: () => mutating,
948
+ planMutation: () => planMutation,
949
+ printLine: () => printLine,
950
+ rawFlag: () => rawFlag,
951
+ rawValue: () => rawValue,
952
+ readStdin: () => readStdin,
953
+ repoProject: () => repoProject,
954
+ setInjectedStdin: () => setInjectedStdin
955
+ });
925
956
  function killProcessTree(pid) {
926
957
  try {
927
958
  if (process.platform === "win32") {
@@ -11654,11 +11685,11 @@ function writeMarketplacePinsOnDisk(path2, pins, succeeded, failedVerb, declineW
11654
11685
  return `could NOT ${failedVerb} ${[...pins.keys()].join(", ")} \u2014 set it by hand`;
11655
11686
  }
11656
11687
  const verify = readKnownMarketplacesFile(path2);
11657
- const failed = [...pins].filter(([name, want]) => {
11688
+ const failed2 = [...pins].filter(([name, want]) => {
11658
11689
  const got = readKnownMarketplace(verify, name);
11659
11690
  return typeof want.autoUpdate === "boolean" && got.declared !== want.autoUpdate || want.ref !== void 0 && got.ref !== want.ref;
11660
11691
  });
11661
- return failed.length ? `${failedVerb} did NOT take for ${failed.map(([n]) => n).join(", ")} \u2014 set it by hand` : succeeded(pins);
11692
+ return failed2.length ? `${failedVerb} did NOT take for ${failed2.map(([n]) => n).join(", ")} \u2014 set it by hand` : succeeded(pins);
11662
11693
  });
11663
11694
  } catch (error) {
11664
11695
  if (error instanceof Error && error.message.startsWith("shared-document lock timeout:")) return error.message;
@@ -16840,10 +16871,10 @@ var rollout_plan_default = {
16840
16871
  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)."
16841
16872
  },
16842
16873
  baseline: {
16843
- version: "4.5.36",
16844
- tag: "v4.5.36",
16845
- commit: "dfc9e43e72eb",
16846
- npm: "@mutmutco/cli@4.5.36"
16874
+ version: "4.5.38",
16875
+ tag: "v4.5.38",
16876
+ commit: "94bd7909821f",
16877
+ npm: "@mutmutco/cli@4.5.38"
16847
16878
  },
16848
16879
  exitCriterion: "fleet-n-of-n",
16849
16880
  hubOnlyShortcut: "forbidden",
@@ -16860,14 +16891,14 @@ var rollout_plan_default = {
16860
16891
  repo: "mutmutco/mmi-hub",
16861
16892
  role: "canary",
16862
16893
  schedule: "train",
16863
- v3Target: "v4.5.36"
16894
+ v3Target: "v4.5.38"
16864
16895
  }
16865
16896
  ],
16866
16897
  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.",
16867
16898
  rollback: {
16868
16899
  independent: true,
16869
- mechanism: "npm dist-tag latest -> 4.5.36 and redeploy the Hub Lambda from tag v4.5.36 (dfc9e43e72eb); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
16870
- v3Target: "v4.5.36 (@mutmutco/cli@4.5.36, tag commit dfc9e43e72eb \u2014 last known-good release carrying the repo-index v4-only contract)"
16900
+ mechanism: "npm dist-tag latest -> 4.5.38 and redeploy the Hub Lambda from tag v4.5.38 (94bd7909821f); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
16901
+ v3Target: "v4.5.38 (@mutmutco/cli@4.5.38, tag commit 94bd7909821f \u2014 last known-good release carrying the repo-index v4-only contract)"
16871
16902
  }
16872
16903
  },
16873
16904
  {
@@ -19633,9 +19664,9 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
19633
19664
  });
19634
19665
  const satisfiedNote = autoSatisfied.size ? `, auto-satisfied (never runs on a tag, see #2404): ${[...autoSatisfied].join(", ")}` : "";
19635
19666
  lastStatus = `${states.map(([c, s]) => `${c}=${s}`).join(", ")}${satisfiedNote}`;
19636
- const failed = states.filter(([, s]) => s === "failed").map(([c]) => c);
19637
- if (failed.length > 0) {
19638
- throw new Error(`required train check failed: ${failed.join(", ")} (${lastStatus}${fleetNote})`);
19667
+ const failed2 = states.filter(([, s]) => s === "failed").map(([c]) => c);
19668
+ if (failed2.length > 0) {
19669
+ throw new Error(`required train check failed: ${failed2.join(", ")} (${lastStatus}${fleetNote})`);
19639
19670
  }
19640
19671
  if (states.every(([, s]) => s === "success")) {
19641
19672
  return `required checks passed: ${pending.join(", ") || "(none pending)"}${satisfiedNote}${fleetNote}`;
@@ -19774,6 +19805,26 @@ function installerPageError(value) {
19774
19805
  if (url.search || url.hash) return "installer.page must not carry a query string or fragment";
19775
19806
  return null;
19776
19807
  }
19808
+ var INSTALLER_VERIFY_HOSTS = ["mutmut.work", "dev.mutmut.work", "rc.mutmut.work"];
19809
+ var INSTALLER_VERIFY_STAGES = ["dev", "rc", "main"];
19810
+ function installerVerifyError(value) {
19811
+ const shape = `installer.verify must be {dev?, rc?, main?} with at least one https URL on ${INSTALLER_VERIFY_HOSTS.join(", ")}`;
19812
+ if (!value || typeof value !== "object" || Array.isArray(value)) return shape;
19813
+ const entries = Object.entries(value);
19814
+ if (entries.length === 0) return shape;
19815
+ for (const [stage, url] of entries) {
19816
+ if (!INSTALLER_VERIFY_STAGES.includes(stage)) return `installer.verify.${stage} is not a stage \u2014 expected dev, rc or main`;
19817
+ let parsed;
19818
+ try {
19819
+ parsed = typeof url === "string" && url.trim() === url ? new URL(url) : null;
19820
+ } catch {
19821
+ parsed = null;
19822
+ }
19823
+ if (!parsed || parsed.protocol !== "https:" || parsed.username || parsed.password || !INSTALLER_VERIFY_HOSTS.includes(parsed.hostname)) return shape;
19824
+ if (parsed.search || parsed.hash) return `installer.verify.${stage} must not carry a query string or fragment`;
19825
+ }
19826
+ return null;
19827
+ }
19777
19828
  function installerVersionUrl(installer) {
19778
19829
  if (typeof installer.gate === "string" && installer.gate) return `${installer.gate.replace(/\/+$/, "")}/release/version`;
19779
19830
  return `https://${INSTALLER_FAMILY_HOSTS[installer.family]}/${installer.product}/release/version`;
@@ -19781,14 +19832,14 @@ function installerVersionUrl(installer) {
19781
19832
  function installerDeclarationError(value, division) {
19782
19833
  if (value === void 0 || value === null) return null;
19783
19834
  if (typeof value !== "object" || Array.isArray(value)) {
19784
- return "installer must be an object {product, family, login?, gate?}";
19835
+ return "installer must be an object {product, family, login?, gate?, page?, verify?}";
19785
19836
  }
19786
19837
  const record = (
19787
19838
  /** @type {Record<string, unknown>} */
19788
19839
  value
19789
19840
  );
19790
19841
  for (const key of Object.keys(record)) {
19791
- if (!["product", "family", "login", "gate", "page"].includes(key)) return `installer.${key} is not a field \u2014 expected product, family and optionally login, gate and page`;
19842
+ if (!["product", "family", "login", "gate", "page", "verify"].includes(key)) return `installer.${key} is not a field \u2014 expected product, family and optionally login, gate, page and verify`;
19792
19843
  }
19793
19844
  if (typeof record.product !== "string" || !PRODUCT_RE.test(record.product)) {
19794
19845
  return "installer.product must be a lowercase kebab name (e.g. mm-strategy) \u2014 it is the /<product> path on the family host";
@@ -19812,6 +19863,10 @@ function installerDeclarationError(value, division) {
19812
19863
  const pageError = installerPageError(record.page);
19813
19864
  if (pageError) return pageError;
19814
19865
  }
19866
+ if (record.verify !== void 0) {
19867
+ const verifyError = installerVerifyError(record.verify);
19868
+ if (verifyError) return verifyError;
19869
+ }
19815
19870
  const expected = expectedInstallerLogin(
19816
19871
  /** @type {'jerv' | 'mmi'} */
19817
19872
  record.family,
@@ -19937,9 +19992,9 @@ async function readVercelDeployment(api, repo, sha, target, since) {
19937
19992
  }
19938
19993
  if (newest.size === 0) return void 0;
19939
19994
  const reads = await Promise.all([...newest].map(async ([env, id]) => ({ env, id, ...await readOne(api, repo, id, since) })));
19940
- const failed = reads.find((r) => FAILED.has(r.state));
19995
+ const failed2 = reads.find((r) => FAILED.has(r.state));
19941
19996
  const unfinished = reads.find((r) => !isTerminalVercelState(r.state));
19942
- const pick = failed ?? unfinished ?? reads[0];
19997
+ const pick = failed2 ?? unfinished ?? reads[0];
19943
19998
  return {
19944
19999
  id: pick.id,
19945
20000
  environment: reads.map((r) => r.env).join(", "),
@@ -20332,11 +20387,11 @@ async function appendServedChannelProof(deps, input, dispatch) {
20332
20387
  const channel = servedChannelFor(input.repo, input.installer);
20333
20388
  if (!channel) return dispatch;
20334
20389
  const expected = input.tag.replace(/^v/, "");
20335
- const withProof = (conclusion, evidence, note, failed = false) => ({
20390
+ const withProof = (conclusion, evidence, note, failed2 = false) => ({
20336
20391
  ...dispatch,
20337
20392
  note: `${dispatch.note}; ${note}`,
20338
20393
  workflowRuns: [...dispatch.workflowRuns ?? [], servedChannelRow(channel.product, conclusion, evidence)],
20339
- ...failed ? { deployStatus: "failure" } : {}
20394
+ ...failed2 ? { deployStatus: "failure" } : {}
20340
20395
  });
20341
20396
  if (!input.shipped) {
20342
20397
  return withProof("pending", `${channel.url} not probed`, `served channel ${channel.url} not probed \u2014 deploy/publish are not proven green, so it is not expected to have moved`);
@@ -20611,8 +20666,8 @@ function shipLegsGreen(deploy, publish) {
20611
20666
  async function announceShippedRelease(deps, args, model, legs, deferred) {
20612
20667
  if (!deps.announce) return void 0;
20613
20668
  if (trainObservesShipment(model) && !shipLegsGreen(legs.deploy, legs.publish)) {
20614
- const failed = legs.deploy.state === "failed" || legs.publish.state === "failed";
20615
- return `not announced: deploy/publish ${failed ? "FAILED" : "not proven green"} \u2014 ${deferred}`;
20669
+ const failed2 = legs.deploy.state === "failed" || legs.publish.state === "failed";
20670
+ return `not announced: deploy/publish ${failed2 ? "FAILED" : "not proven green"} \u2014 ${deferred}`;
20616
20671
  }
20617
20672
  return (await deps.announce(args)).note;
20618
20673
  }
@@ -23378,8 +23433,8 @@ function ledgerFailedRemedy(ledger) {
23378
23433
  const openLegs = derived.legs.filter((l) => l.state === "pending" || l.state === "failed");
23379
23434
  const tails = formatFailedLegTails(ledger);
23380
23435
  if (hotfixRetryDeployShape(ledger)) {
23381
- const failed = openLegs.filter((l) => l.state === "failed").map((l) => l.phase);
23382
- return `the published ${ledger.tag} hotfix can retry the failed ${joinLegNames(failed)} of the same tag: \`mmi-cli devops hotfix release ${ledger.tag} --retry-deploy\` (the deploy helper is idempotent; alignment re-attempts the fold-port PR) (#6985)${tails}`;
23436
+ const failed2 = openLegs.filter((l) => l.state === "failed").map((l) => l.phase);
23437
+ return `the published ${ledger.tag} hotfix can retry the failed ${joinLegNames(failed2)} of the same tag: \`mmi-cli devops hotfix release ${ledger.tag} --retry-deploy\` (the deploy helper is idempotent; alignment re-attempts the fold-port PR) (#6985)${tails}`;
23383
23438
  }
23384
23439
  if (rcandRetryDeployShape(ledger)) {
23385
23440
  return `fix the rc deploy failure, then retry the failed deploy of the promoted ${ledger.tag} candidate (same tag and SHA, never re-cut): \`mmi-cli devops rcand --retry-deploy --watch\` (#7053)${tails}`;
@@ -23547,8 +23602,8 @@ async function supersedeContentCausedDeployLeg(deps, cwd) {
23547
23602
  `release --supersede-deploy: origin reports ${prior.tag} at ${liveSha.slice(0, 12) || "no tag"} but the ledger anchors ${prior.tagSha.slice(0, 12)} \u2014 an absent or divergent release is NOT supersedeable (release --abort --apply is the unpublished candidate's own exit); nothing was written (#6553)`
23548
23603
  );
23549
23604
  }
23550
- const failed = prior.phases.deploy.state === "failed";
23551
- const note = failed ? `superseded: content-caused deploy failure on the immutable promoted ${prior.tag} \u2014 the cause fix rides the next patch train (master-approved closure, #6553)` : `superseded: ${prior.tag} deploy outcome uncorrelatable from the release SHA (no failure recorded) \u2014 closed on the owner's evidence (master-approved closure, #6553, #7327)`;
23605
+ const failed2 = prior.phases.deploy.state === "failed";
23606
+ const note = failed2 ? `superseded: content-caused deploy failure on the immutable promoted ${prior.tag} \u2014 the cause fix rides the next patch train (master-approved closure, #6553)` : `superseded: ${prior.tag} deploy outcome uncorrelatable from the release SHA (no failure recorded) \u2014 closed on the owner's evidence (master-approved closure, #6553, #7327)`;
23552
23607
  await fence(() => atomicWriteJson(path2, {
23553
23608
  ...prior,
23554
23609
  phases: {
@@ -23562,7 +23617,7 @@ async function supersedeContentCausedDeployLeg(deps, cwd) {
23562
23617
  repo: prior.repo,
23563
23618
  tag: prior.tag,
23564
23619
  tagSha: prior.tagSha,
23565
- note: failed ? `closed the ${prior.tag} deploy leg as superseded (skipped) \u2014 the ledger is phases-green; rerun the train doctor to confirm, then run the next patch train (hotfix or release) to carry the cause fix` : `closed the ${prior.tag} uncorrelatable deploy leg as superseded (skipped) \u2014 the ledger is phases-green; rerun the train doctor to confirm`
23620
+ note: failed2 ? `closed the ${prior.tag} deploy leg as superseded (skipped) \u2014 the ledger is phases-green; rerun the train doctor to confirm, then run the next patch train (hotfix or release) to carry the cause fix` : `closed the ${prior.tag} uncorrelatable deploy leg as superseded (skipped) \u2014 the ledger is phases-green; rerun the train doctor to confirm`
23566
23621
  };
23567
23622
  });
23568
23623
  }
@@ -24537,17 +24592,17 @@ async function runTrainDoctor(input) {
24537
24592
  if (derived.phaseStatus !== "phases-green") {
24538
24593
  const openLegs = derived.legs.filter((l) => l.state === "pending" || l.state === "failed");
24539
24594
  const open2 = openLegs.map((l) => `${l.phase}=${l.state}`).join(", ");
24540
- const failed = derived.phaseStatus === "phases-failed";
24595
+ const failed2 = derived.phaseStatus === "phases-failed";
24541
24596
  const deployOnlyDeadlock = openLegs.length > 0 && openLegs.every((l) => l.phase === "deploy" && supersedeableDeployLegShape(l));
24542
24597
  const supersession = deployOnlyDeadlock ? `; when the deploy failure is content-caused on the already-published ${ledger.tag} (the immutable tag can never deploy green), the sanctioned exit is \`mmi-cli devops release --supersede-deploy --apply\` after explicit master approval \u2014 the next patch train carries the cause fix (#6553)` : "";
24543
24598
  add({
24544
- code: failed ? "ledger-failed" : "ledger-pending",
24599
+ code: failed2 ? "ledger-failed" : "ledger-pending",
24545
24600
  severity: "blocker",
24546
24601
  source: "local",
24547
24602
  // #6429: each lane finishes its own run (#6068) — an rcand-lane ledger named as a release
24548
24603
  // ledger, with `release --resume` as its remedy, sends the operator to a verb that refuses it.
24549
24604
  title: `a prior ${ledger.lane ?? "release"} ledger for ${ledger.tag} is ${derived.phaseStatus} (${open2}) \u2014 a new train must not stack on it (#5987)`,
24550
- remedy: failed ? ledgerFailedRemedy(ledger) : `finish it first: ${ledgerLaneResumeCommand(ledger)} (alignment-only pending legs close themselves with \`train doctor --heal\` once the PR merges, and a deploy/publish leg whose run already concluded folds under the same heal)${supersession}`
24605
+ remedy: failed2 ? ledgerFailedRemedy(ledger) : `finish it first: ${ledgerLaneResumeCommand(ledger)} (alignment-only pending legs close themselves with \`train doctor --heal\` once the PR merges, and a deploy/publish leg whose run already concluded folds under the same heal)${supersession}`
24551
24606
  });
24552
24607
  } else if (newestRemoteReleaseTag && newestRemoteReleaseTag !== ledger.tag) {
24553
24608
  add({
@@ -24594,21 +24649,21 @@ regenerate the projections (\`node scripts/check-skill-payload.mjs\`) and commit
24594
24649
  const installed = [];
24595
24650
  let pending = firstDir;
24596
24651
  let outstanding = failure;
24597
- let failed = null;
24652
+ let failed2 = null;
24598
24653
  while (pending && !installed.includes(pending)) {
24599
24654
  const dir = pending;
24600
24655
  try {
24601
24656
  await train.run("npm", ["--prefix", dir, "ci"]);
24602
24657
  } catch (e) {
24603
- failed = { dir, detail: `\`npm --prefix ${dir} ci\` failed: ${message(e)}` };
24658
+ failed2 = { dir, detail: `\`npm --prefix ${dir} ci\` failed: ${message(e)}` };
24604
24659
  break;
24605
24660
  }
24606
24661
  installed.push(dir);
24607
24662
  outstanding = await verifyDeps();
24608
24663
  pending = outstanding ? /in (cli|updater)\/node_modules/u.exec(outstanding)?.[1] ?? null : null;
24609
24664
  }
24610
- if (failed) {
24611
- add({ code: "hub-build-deps", severity: "blocker", source: "local", title: failed.detail, remedy: `repair ${failed.dir}/node_modules by hand, then rerun` });
24665
+ if (failed2) {
24666
+ add({ code: "hub-build-deps", severity: "blocker", source: "local", title: failed2.detail, remedy: `repair ${failed2.dir}/node_modules by hand, then rerun` });
24612
24667
  } else if (outstanding) {
24613
24668
  add({ code: "hub-build-deps", severity: "blocker", source: "local", title: `installing ${installed.join(", ")} from ${installed.length === 1 ? "its lockfile" : "their lockfiles"} did not resolve the build dependencies`, remedy: outstanding });
24614
24669
  } else {
@@ -24709,6 +24764,133 @@ function formatTrainDoctor(v) {
24709
24764
  return lines2.join("\n");
24710
24765
  }
24711
24766
 
24767
+ // src/vercel-version-env.ts
24768
+ var SKIPPED = { status: "skipped", note: "" };
24769
+ function vercelVersionEnvAheadNote(key) {
24770
+ return `a failed branch push can leave ${key} ahead of what is live`;
24771
+ }
24772
+ function safeReason(reason, token) {
24773
+ const compact = reason.replace(/\s+/g, " ").trim().slice(0, 180);
24774
+ const scrubbed = token && compact.includes(token) ? compact.split(token).join("[redacted]") : compact;
24775
+ return scrubbed || "unknown error";
24776
+ }
24777
+ function failed(deps, key, tag, reason) {
24778
+ const note = `vercel version env: FAILED to set ${key}=${tag} (${reason}) \u2014 /healthz will show the commit, not the tag; fix: set it and redeploy once`;
24779
+ deps.warn?.(note);
24780
+ return { status: "failed", note };
24781
+ }
24782
+ function targetLabel(stage) {
24783
+ return stage === "main" ? "production" : "preview";
24784
+ }
24785
+ function envCollectionUrl(project2, team, extra) {
24786
+ const url = new URL(`https://api.vercel.com/v10/projects/${encodeURIComponent(project2)}/env`);
24787
+ if (team) url.searchParams.set("teamId", team);
24788
+ for (const [key, value] of Object.entries(extra ?? {})) url.searchParams.set(key, value);
24789
+ return url.toString();
24790
+ }
24791
+ function envItemUrl(project2, id, team) {
24792
+ const url = new URL(`https://api.vercel.com/v9/projects/${encodeURIComponent(project2)}/env/${encodeURIComponent(id)}`);
24793
+ if (team) url.searchParams.set("teamId", team);
24794
+ return url.toString();
24795
+ }
24796
+ function isStageEnv(row, stage, key) {
24797
+ if (row.key !== key) return false;
24798
+ const target = Array.isArray(row.target) ? row.target.filter((item) => typeof item === "string") : [];
24799
+ if (stage === "main") return target.length === 1 && target[0] === "production";
24800
+ return target.length === 1 && target[0] === "preview" && row.gitBranch === "rc";
24801
+ }
24802
+ function envsOf(body) {
24803
+ if (Array.isArray(body)) return body;
24804
+ if (body && typeof body === "object" && Array.isArray(body.envs)) {
24805
+ return body.envs;
24806
+ }
24807
+ return null;
24808
+ }
24809
+ async function readBody(res, token) {
24810
+ let raw = "";
24811
+ try {
24812
+ raw = await res.text();
24813
+ } catch (e) {
24814
+ return { ok: false, reason: safeReason(e instanceof Error ? e.message : String(e), token) };
24815
+ }
24816
+ if (!res.ok) return { ok: false, reason: `HTTP ${res.status}${raw ? `: ${safeReason(raw, token)}` : ""}` };
24817
+ if (!raw.trim()) return { ok: true, body: {} };
24818
+ try {
24819
+ return { ok: true, body: JSON.parse(raw) };
24820
+ } catch {
24821
+ return { ok: false, reason: "response was not JSON" };
24822
+ }
24823
+ }
24824
+ async function setVercelVersionEnv(deps, meta, stage, tag) {
24825
+ if (meta?.deployModel !== "vercel" || !meta.vercelVersionEnv) return SKIPPED;
24826
+ const { project: project2, key, team } = meta.vercelVersionEnv;
24827
+ if (!project2 || !key) return failed(deps, key || "(missing key)", tag, "vercelVersionEnv is missing project or key");
24828
+ let token;
24829
+ try {
24830
+ token = await deps.readToken();
24831
+ } catch (e) {
24832
+ return failed(deps, key, tag, safeReason(e instanceof Error ? e.message : String(e), null));
24833
+ }
24834
+ if (!token) return failed(deps, key, tag, "no _org/vercel/API_TOKEN");
24835
+ const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
24836
+ let listed;
24837
+ try {
24838
+ const res = await deps.fetch(envCollectionUrl(project2, team, stage === "rc" ? { gitBranch: "rc" } : void 0), { method: "GET", headers });
24839
+ listed = await readBody(res, token);
24840
+ } catch (e) {
24841
+ return failed(deps, key, tag, safeReason(e instanceof Error ? e.message : String(e), token));
24842
+ }
24843
+ if (!listed.ok) return failed(deps, key, tag, listed.reason);
24844
+ const rows = envsOf(listed.body);
24845
+ if (!rows) return failed(deps, key, tag, "env list returned an unexpected shape");
24846
+ const existing = rows.find((row) => isStageEnv(row, stage, key) && typeof row.id === "string" && row.id.length > 0);
24847
+ const createBody = stage === "main" ? { key, value: tag, type: "plain", target: ["production"] } : { key, value: tag, type: "plain", target: ["preview"], gitBranch: "rc" };
24848
+ try {
24849
+ const res = existing ? await deps.fetch(envItemUrl(project2, existing.id, team), {
24850
+ method: "PATCH",
24851
+ headers,
24852
+ body: JSON.stringify({ value: tag })
24853
+ }) : await deps.fetch(envCollectionUrl(project2, team), {
24854
+ method: "POST",
24855
+ headers,
24856
+ body: JSON.stringify(createBody)
24857
+ });
24858
+ const written = await readBody(res, token);
24859
+ if (!written.ok) return failed(deps, key, tag, written.reason);
24860
+ } catch (e) {
24861
+ return failed(deps, key, tag, safeReason(e instanceof Error ? e.message : String(e), token));
24862
+ }
24863
+ const note = `vercel version env: ${key}=${tag} set on ${targetLabel(stage)}`;
24864
+ deps.warn?.(note);
24865
+ return { status: "set", note };
24866
+ }
24867
+ async function defaultVercelFetch(url, init) {
24868
+ const res = await fetch(url, { ...init, signal: AbortSignal.timeout(15e3) });
24869
+ return { ok: res.ok, status: res.status, text: () => res.text() };
24870
+ }
24871
+ async function readOrgVercelApiToken() {
24872
+ const { loadConfig: loadConfig2, hubHeaders: hubHeaders2, repoProject: repoProject2 } = await Promise.resolve().then(() => (init_cli_shared(), cli_shared_exports));
24873
+ const cfg = await loadConfig2();
24874
+ if (!cfg.sagaApiUrl) return null;
24875
+ const repo = `mutmutco/${(await repoProject2(cfg)).toLowerCase()}`;
24876
+ const res = await fetch(`${cfg.sagaApiUrl}/secrets/get`, {
24877
+ method: "POST",
24878
+ headers: await hubHeaders2({ "content-type": "application/json" }),
24879
+ body: JSON.stringify({ repo, key: "vercel/API_TOKEN", use: true, slug: "_org" }),
24880
+ signal: AbortSignal.timeout(8e3)
24881
+ });
24882
+ if (!res.ok) return null;
24883
+ const body = await res.json();
24884
+ return typeof body.value === "string" ? body.value : null;
24885
+ }
24886
+ function vercelVersionEnvDeps(deps) {
24887
+ return {
24888
+ fetch: deps.vercelFetch ?? defaultVercelFetch,
24889
+ readToken: deps.readVercelApiToken ?? readOrgVercelApiToken,
24890
+ warn: (message2) => deps.warn?.(message2)
24891
+ };
24892
+ }
24893
+
24712
24894
  // src/train-apply-phases.ts
24713
24895
  var TRAIN_BUMP_INTENTS = ["major", "minor", "patch"];
24714
24896
  function readLocalGateWorkflows() {
@@ -25313,14 +25495,14 @@ async function recoverFailedDispatchPhase(deps, ledger, anchors, phase, dispatch
25313
25495
  let fallbackRecovered = false;
25314
25496
  const failedRows = workflowRuns?.filter((run) => run.conclusion === "failure") ?? [];
25315
25497
  if (failedRows.length > 0) {
25316
- for (const failed of failedRows) {
25317
- const rowPhase = failed.workflow.includes("publish") ? "publish" : phase;
25498
+ for (const failed2 of failedRows) {
25499
+ const rowPhase = failed2.workflow.includes("publish") ? "publish" : phase;
25318
25500
  if (persisted?.phases[rowPhase]?.superseded === true) continue;
25319
25501
  const recovery = await recover(deps, ledger, anchors, rowPhase, {
25320
25502
  repo: input.repo,
25321
25503
  sha: input.sha,
25322
- failure: `watched ${rowPhase} run failed: ${failed.workflow} (${dispatch.note})`,
25323
- runs: [{ ...failed, repo: input.repo }],
25504
+ failure: `watched ${rowPhase} run failed: ${failed2.workflow} (${dispatch.note})`,
25505
+ runs: [{ ...failed2, repo: input.repo }],
25324
25506
  watched: input.watched
25325
25507
  });
25326
25508
  if (recovery.note) notes.push(recovery.note);
@@ -25386,6 +25568,27 @@ async function recordRcandDeployLegs(ledger, deps, anchors, deployModel, d, opts
25386
25568
  function releaseAnnounceDeferral(model, summaryFile) {
25387
25569
  return prodReleaseTargets(model).length > 0 || model === "vercel" ? `\`mmi-cli devops release --resume --watch${summaryFile ? ` --announce-summary-file ${summaryFile}` : ""}\` announces it once they are green` : "a central deploy is proven only under --watch, so this release stays unannounced";
25388
25570
  }
25571
+ function annotateVersionEnvDispatch(dispatch, result) {
25572
+ if (!result.note) return dispatch;
25573
+ return { ...dispatch, note: `${dispatch.note}; ${result.note}` };
25574
+ }
25575
+ function annotateVersionEnvAnnounce(note, result) {
25576
+ if (!result.note) return note;
25577
+ return note ? `${note}; ${result.note}` : result.note;
25578
+ }
25579
+ async function pushAfterVercelVersionEnv(deps, meta, stage, tag, pushArgs) {
25580
+ const result = await setVercelVersionEnv(vercelVersionEnvDeps(deps), meta, stage, tag);
25581
+ try {
25582
+ await runGitPush(deps, pushArgs);
25583
+ } catch (e) {
25584
+ const base = e instanceof Error ? e.message : String(e);
25585
+ const parts = [base];
25586
+ if (result.note) parts.push(result.note);
25587
+ if (result.status === "set" && meta.vercelVersionEnv?.key) parts.push(vercelVersionEnvAheadNote(meta.vercelVersionEnv.key));
25588
+ throw new Error(parts.join(" \u2014 "));
25589
+ }
25590
+ return result;
25591
+ }
25389
25592
  async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha, startBranch, preFold, tagProbe, branchHints, ledger, alignEarly) {
25390
25593
  const resumeCommand = options.dev ? "mmi-cli devops release --dev --apply" : "mmi-cli devops release --apply";
25391
25594
  const ledgerAnchors = { repo: ctx.repo, tag, tagSha: releaseSha };
@@ -25482,9 +25685,10 @@ ${recovery.note}`), recoveryInput);
25482
25685
  landed: "the immutable tag pushed at " + releaseSha.slice(0, 12)
25483
25686
  });
25484
25687
  if (trueMergeGateNote) checks = `${trueMergeGateNote}; ${checks}`;
25688
+ let versionEnv = { status: "skipped", note: "" };
25485
25689
  try {
25486
25690
  deps.warn?.("Release: Promoting the verified release");
25487
- await runGitPush(deps, ["push", "origin", "main"]);
25691
+ versionEnv = await pushAfterVercelVersionEnv(deps, meta, "main", tag, ["push", "origin", "main"]);
25488
25692
  } catch (e) {
25489
25693
  await recordPhase(ledger, deps, ledgerAnchors, "promotion", phaseEntry({ state: "failed", sha: releaseSha, error: `origin/main push failed after ${tag} was pushed: ${e instanceof Error ? e.message : String(e)}` }), { landed: "the immutable tag pushed at " + releaseSha.slice(0, 12) });
25490
25694
  throw partialTrainRecoveryError(e, recoveryInput);
@@ -25517,7 +25721,7 @@ ${recovery.note}`), recoveryInput);
25517
25721
  workflow: failedReleaseRun?.workflow ?? (isCentralDispatchModel(deployModel) ? "tenant-deploy.yml" : "deploy workflow")
25518
25722
  }
25519
25723
  );
25520
- const deployDispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, recoveredDeployDispatch);
25724
+ const deployDispatch = annotateVersionEnvDispatch(await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, recoveredDeployDispatch), versionEnv);
25521
25725
  const deployLeg = phaseEntry(validSuiteCutContext(ctx.repo, deps.suiteCut) && ctx.slug !== "mmi-katip" ? { state: "skipped", sha: releaseSha, note: "suite bundle-only: live backend not deployed (#7263)" } : deployPhaseInput(deployModel, deployDispatch, { releaseSha }));
25522
25726
  await recordPhase(ledger, deps, ledgerAnchors, "deploy", deployLeg, {
25523
25727
  landed: "the immutable tag, the green required-check wall, the origin/main fast-forward, and the verified GitHub Release"
@@ -25551,7 +25755,7 @@ ${recovery.note}`), recoveryInput);
25551
25755
  appendPublishDispatch(deployDispatch, publishDispatch)
25552
25756
  );
25553
25757
  alignment = await aligning;
25554
- const announceNote = await announcing;
25758
+ const announceNote = annotateVersionEnvAnnounce(await announcing, versionEnv);
25555
25759
  return { checks, releaseUrl, announceNote, dispatch, ledgerAnchors, ...alignment ? { alignment } : {} };
25556
25760
  }
25557
25761
  function resumeStateOf(status) {
@@ -25754,10 +25958,10 @@ ${spent.note}`);
25754
25958
  await recordPhase(ledger, deps, anchors, "ordinaryCi", phaseEntry({ state: "complete", sha: tagSha, note: "re-verified the required-check wall live on this resume" }), { strict, landed: "the immutable rc tag on origin at the resumed SHA" });
25755
25959
  const autoRunSince = (deps.now ?? Date.now)();
25756
25960
  deps.warn?.("Release: Promoting the verified release candidate");
25757
- await runGitPush(deps, ["push", "origin", `${tagSha}:refs/heads/rc`]);
25961
+ const versionEnv = await pushAfterVercelVersionEnv(deps, meta, "rc", tag, ["push", "origin", `${tagSha}:refs/heads/rc`]);
25758
25962
  await recordPhase(ledger, deps, anchors, "promotion", phaseEntry({ state: "complete", sha: tagSha, note: `resumed: ${tagSha.slice(0, 12)} pushed to origin/rc (tag preserved, never re-cut)` }), { strict, landed: "the immutable rc tag and the re-proven required-check wall" });
25759
25963
  await recordPhase(ledger, deps, anchors, "githubRelease", phaseEntry({ state: "skipped", sha: tagSha, note: "rcand mints no GitHub Release \u2014 the rc tag is the candidate" }), { strict, landed: "the immutable rc tag, the green wall, and origin/rc" });
25760
- const dispatch0 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel, watch, autoRunSince, tagSha, "report");
25964
+ const dispatch0 = annotateVersionEnvDispatch(await dispatchDeploy(deps, ctx, "rc", "rc", deployModel, watch, autoRunSince, tagSha, "report"), versionEnv);
25761
25965
  const dispatch = await recordRcandDeployLegs(ledger, deps, anchors, deployModel, dispatch0, { strict });
25762
25966
  const ledgerReport = releaseLedgerReport(await ledger.load(anchors).catch(() => void 0), ledger.path);
25763
25967
  return {
@@ -26037,7 +26241,7 @@ ${recovery.note}`), recoveryInput);
26037
26241
  }
26038
26242
  await recordPhase(ledger, deps, anchors, "ordinaryCi", phaseEntry({ state: "complete", sha: tagSha, note: "re-verified the required-check wall live on this resume" }), { strict: ledgerMode === "strict", landed: "the immutable tag on origin at the resumed SHA" });
26039
26243
  deps.warn?.("Release: Promoting the verified release");
26040
- await runGitPush(deps, ["push", "origin", `${tagSha}:refs/heads/main`]);
26244
+ const versionEnv = await pushAfterVercelVersionEnv(deps, meta, "main", tag, ["push", "origin", `${tagSha}:refs/heads/main`]);
26041
26245
  steps.push(`pushed ${tagSha.slice(0, 12)} to origin/main`);
26042
26246
  await recordPhase(ledger, deps, anchors, "promotion", phaseEntry({ state: "complete", sha: tagSha, note: `resumed: ${tagSha.slice(0, 12)} pushed to origin/main (tag preserved, never re-cut)` }), { strict: ledgerMode === "strict", landed: "the immutable tag and the re-proven required-check wall" });
26043
26247
  deps.warn?.("Release: Creating the GitHub Release");
@@ -26067,7 +26271,7 @@ ${recovery.note}`), recoveryInput);
26067
26271
  workflow: isCentralDispatchModel(deployModel) ? "tenant-deploy.yml" : "deploy workflow"
26068
26272
  }
26069
26273
  );
26070
- const deployDispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, recoveredDeployDispatch);
26274
+ const deployDispatch = annotateVersionEnvDispatch(await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, recoveredDeployDispatch), versionEnv);
26071
26275
  steps.push(`dispatched the ${deployModel} deploy path`);
26072
26276
  const deployLeg = persisted?.phases.deploy.superseded === true ? persisted.phases.deploy : phaseEntry(validSuiteCutContext(ctx.repo, deps.suiteCut) && ctx.slug !== "mmi-katip" ? { state: "skipped", sha: tagSha, note: "suite bundle-only: live backend not deployed (#7263)" } : deployPhaseInput(deployModel, deployDispatch, { releaseSha: tagSha }));
26073
26277
  if (persisted?.phases.deploy.superseded !== true) {
@@ -26108,7 +26312,7 @@ ${recovery.note}`), recoveryInput);
26108
26312
  devRollForward = landed.dev;
26109
26313
  rcAlignment = landed.rc;
26110
26314
  }
26111
- const announceNote = await announcing;
26315
+ const announceNote = annotateVersionEnvAnnounce(await announcing, versionEnv);
26112
26316
  steps.push(`development roll-forward: ${devRollForward.status}`);
26113
26317
  if (rcAlignment) steps.push(`rc alignment: ${rcAlignment.status}`);
26114
26318
  const followUp = deriveTrainFollowUpStatus({
@@ -26448,15 +26652,16 @@ ${spent.note}`), rcandRecovery(tag2));
26448
26652
  await recordPhase(ledger, deps, anchors, "ordinaryCi", phaseEntry({ state: "complete", sha: rcSha, note: checks2 }), { landed: "the immutable rc tag pushed at " + rcSha.slice(0, 12) });
26449
26653
  const autoRunSince = (deps.now ?? Date.now)();
26450
26654
  deps.warn?.("Release: Promoting the verified release candidate");
26655
+ let versionEnv = { status: "skipped", note: "" };
26451
26656
  try {
26452
- await runGitPush(deps, ["push", "origin", "rc"]);
26657
+ versionEnv = await pushAfterVercelVersionEnv(deps, meta, "rc", tag2, ["push", "origin", "rc"]);
26453
26658
  } catch (e) {
26454
26659
  await recordPhase(ledger, deps, anchors, "promotion", phaseEntry({ state: "failed", sha: rcSha, error: `origin/rc push failed after ${tag2} was pushed: ${e instanceof Error ? e.message : String(e)}` }), { landed: "the immutable rc tag pushed at " + rcSha.slice(0, 12) });
26455
26660
  throw partialTrainRecoveryError(e, rcandRecovery(tag2));
26456
26661
  }
26457
26662
  await recordPhase(ledger, deps, anchors, "promotion", phaseEntry({ state: "complete", sha: rcSha, note: `immutable ${tag2} pushed at ${rcSha.slice(0, 12)}; origin/rc fast-forwarded to it` }), { landed: "the immutable rc tag and the green required-check wall" });
26458
26663
  await recordPhase(ledger, deps, anchors, "githubRelease", phaseEntry({ state: "skipped", sha: rcSha, note: "rcand mints no GitHub Release \u2014 the rc tag is the candidate" }), { landed: "the immutable rc tag, the green wall, and origin/rc" });
26459
- const d0 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince, rcSha, "report");
26664
+ const d0 = annotateVersionEnvDispatch(await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince, rcSha, "report"), versionEnv);
26460
26665
  const d2 = await recordRcandDeployLegs(ledger, deps, anchors, deployModel2, d0);
26461
26666
  const rcLedgerReport = releaseLedgerReport(await ledger?.load(anchors).catch(() => void 0), ledger?.path ?? "(no ledger)");
26462
26667
  return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, bumpIntent, deployModel: deployModel2, promoted: true, checks: checks2, resumeNote, dispatch: d2.note, runId: d2.runId, runUrl: d2.runUrl, workflowRuns: d2.workflowRuns, deployStatus: d2.deployStatus, ...rcLedgerReport ? { ledger: rcLedgerReport } : {} };
@@ -28643,13 +28848,13 @@ function buildPluginCachePlan(home, running, deps, opts = {}) {
28643
28848
  }
28644
28849
  function applyPluginCachePlan(plan, remove2, stagingGuard) {
28645
28850
  const removed = [];
28646
- const failed = [];
28851
+ const failed2 = [];
28647
28852
  for (const version of plan.prune) {
28648
28853
  try {
28649
28854
  remove2(`${plan.cacheRoot}/${version}`);
28650
28855
  removed.push(version);
28651
28856
  } catch (e) {
28652
- failed.push({ version, error: e.message });
28857
+ failed2.push({ version, error: e.message });
28653
28858
  }
28654
28859
  }
28655
28860
  const removedStaging = [];
@@ -28682,7 +28887,7 @@ function applyPluginCachePlan(plan, remove2, stagingGuard) {
28682
28887
  }
28683
28888
  return {
28684
28889
  removed,
28685
- failed,
28890
+ failed: failed2,
28686
28891
  bytes: plan.bytes,
28687
28892
  removedStaging,
28688
28893
  failedStaging,
@@ -29694,7 +29899,7 @@ init_hub_auth();
29694
29899
  init_github_client();
29695
29900
 
29696
29901
  // src/project-set.ts
29697
- var UNSET_KEYS = ["projectAdmins", "installer", "oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "tenantTasks", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "releaseChannel", "releaseLanguage", "dsManifestPath", "consumesDesignSystem", "ci", "requiredChecks", "requiredCheckBranches", "ciExemptReason", "gate", "seedCanary", "notifyOnDeploy"];
29902
+ var UNSET_KEYS = ["projectAdmins", "installer", "oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "tenantTasks", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "releaseChannel", "releaseLanguage", "dsManifestPath", "consumesDesignSystem", "ci", "requiredChecks", "requiredCheckBranches", "ciExemptReason", "gate", "seedCanary", "notifyOnDeploy", "vercelVersionEnv"];
29698
29903
  var UNSET_KEY_SET = new Set(UNSET_KEYS);
29699
29904
  var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
29700
29905
  var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
@@ -30048,6 +30253,32 @@ function parseNotifyOnDeployVar(raw) {
30048
30253
  }
30049
30254
  return parsed;
30050
30255
  }
30256
+ var VERCEL_VERSION_ENV_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
30257
+ var VERCEL_VERSION_ENV_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
30258
+ function parseVercelVersionEnvVar(raw) {
30259
+ let parsed;
30260
+ try {
30261
+ parsed = JSON.parse(raw);
30262
+ } catch {
30263
+ throw new Error('org project set: vercelVersionEnv must be JSON, e.g. {"project":"mm-work","key":"MMW_VERSION","team":"team_xxx"}');
30264
+ }
30265
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
30266
+ throw new Error("org project set: vercelVersionEnv must be a JSON object { project, key, team? }");
30267
+ }
30268
+ const row = parsed;
30269
+ const unknown = Object.keys(row).filter((key) => key !== "project" && key !== "key" && key !== "team");
30270
+ if (unknown.length > 0) throw new Error(`org project set: vercelVersionEnv has unknown field(s): ${unknown.join(", ")}`);
30271
+ if (typeof row.project !== "string" || !VERCEL_VERSION_ENV_ID_RE.test(row.project)) {
30272
+ throw new Error("org project set: vercelVersionEnv.project must be a Vercel project id or name");
30273
+ }
30274
+ if (typeof row.key !== "string" || !VERCEL_VERSION_ENV_NAME_RE.test(row.key)) {
30275
+ throw new Error("org project set: vercelVersionEnv.key must be an env name (e.g. MMW_VERSION)");
30276
+ }
30277
+ if (row.team !== void 0 && (typeof row.team !== "string" || !VERCEL_VERSION_ENV_ID_RE.test(row.team))) {
30278
+ throw new Error("org project set: vercelVersionEnv.team must be a Vercel team id or slug");
30279
+ }
30280
+ return { project: row.project, key: row.key, ...typeof row.team === "string" ? { team: row.team } : {} };
30281
+ }
30051
30282
  function parseCiExemptReasonVar(raw) {
30052
30283
  const trimmed = raw.trim();
30053
30284
  if (!trimmed) throw new Error("org project set: ciExemptReason must be a non-empty reason (or use --unset ciExemptReason to clear it)");
@@ -30189,7 +30420,8 @@ var SETTABLE_VAR_KEYS = [
30189
30420
  "secrets",
30190
30421
  "seedCanary",
30191
30422
  "installer",
30192
- "notifyOnDeploy"
30423
+ "notifyOnDeploy",
30424
+ "vercelVersionEnv"
30193
30425
  ];
30194
30426
  var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
30195
30427
  var SETTABLE_VAR_HINTS = {
@@ -30219,6 +30451,7 @@ var SETTABLE_VAR_HINTS = {
30219
30451
  requiredChecks: 'JSON array, e.g. ["gate"] or [] for no-ci',
30220
30452
  requiredCheckBranches: "non-empty JSON branch-name array; exact product-ruleset scope (#5210)",
30221
30453
  notifyOnDeploy: 'JSON stage array, e.g. ["dev"] \u2014 successful tenant-deploy sends <stage>-deployed to the project repo (#7358)',
30454
+ vercelVersionEnv: "JSON {project,key,team?} \u2014 train sets this Vercel env to the release tag before the main/rc push (#7362)",
30222
30455
  ciExemptReason: "non-empty string \u2014 declares this deployable repo has no CI surface (#4265)",
30223
30456
  gate: "JSON {runtime,cmd,affectedCmd,workdir,cacheDepPath,pyVersion}"
30224
30457
  };
@@ -30364,6 +30597,8 @@ function buildProjectSetPatch(input) {
30364
30597
  patch[key] = parseRequiredCheckBranchesVar(raw);
30365
30598
  } else if (key === "notifyOnDeploy") {
30366
30599
  patch[key] = parseNotifyOnDeployVar(raw);
30600
+ } else if (key === "vercelVersionEnv") {
30601
+ patch[key] = parseVercelVersionEnvVar(raw);
30367
30602
  } else if (key === "ciExemptReason") {
30368
30603
  patch[key] = parseCiExemptReasonVar(raw);
30369
30604
  } else if (key === "gate") {
@@ -35014,7 +35249,7 @@ async function runHotfixStartLocked(deps, options, doctor) {
35014
35249
  throw new Error(`working tree must be clean before hotfix start${describeBlockingPaths(status)}`);
35015
35250
  }
35016
35251
  await deps.run("git", ["fetch", "origin", "--tags"]);
35017
- const { deployModel } = await hotfixPreflight(deps, ctx, "start");
35252
+ const { deployModel, meta } = await hotfixPreflight(deps, ctx, "start");
35018
35253
  const bumpNotes = [];
35019
35254
  let resolved = await resolveHotfixVersion(deps);
35020
35255
  for (; ; ) {
@@ -35136,6 +35371,8 @@ async function runHotfixStartLocked(deps, options, doctor) {
35136
35371
  await assertHotfixFoldBomStamp(deps, tag);
35137
35372
  await deps.run("git", ["push", "-u", "origin", branch]);
35138
35373
  }
35374
+ const versionEnv = await setVercelVersionEnv(vercelVersionEnvDeps(deps), meta, "main", tag);
35375
+ if (versionEnv.note) notes.push(versionEnv.note);
35139
35376
  const bumpNote = deployModel === "hub-serverless" ? " with the locked distribution bump" : foldPaths.length > 0 ? " with the package version bump" : "";
35140
35377
  const prUrl = clean3(await deps.run("gh", [
35141
35378
  "pr",
@@ -37387,12 +37624,12 @@ async function resolveBoardAdvanceForPr(prNumber, repoOption) {
37387
37624
  }
37388
37625
  function evaluatePrMergeHousekeeping(report, context, force = false) {
37389
37626
  const scratch = report.scratch;
37390
- const failed = scratch?.status === "failed";
37627
+ const failed2 = scratch?.status === "failed";
37391
37628
  const prunable = scratch?.safeAuto ?? 0;
37392
37629
  const unprunable = scratch?.skipped ?? 0;
37393
- if (!(failed || prunable > 0 || unprunable > 0)) return { blocked: false };
37630
+ if (!(failed2 || prunable > 0 || unprunable > 0)) return { blocked: false };
37394
37631
  if (force) return { blocked: false };
37395
- const detail = failed ? `scratch housekeeping failed${scratch?.error ? `: ${scratch.error}` : ""}` : `${prunable} prunable + ${unprunable} un-prunable scratch item(s) remain after cleanup`;
37632
+ const detail = failed2 ? `scratch housekeeping failed${scratch?.error ? `: ${scratch.error}` : ""}` : `${prunable} prunable + ${unprunable} un-prunable scratch item(s) remain after cleanup`;
37396
37633
  return {
37397
37634
  blocked: true,
37398
37635
  reason: `${context} housekeeping blocked merge: ${detail}. Run host-owned scratch cleanup, then retry \u2014 or re-run \`mmi-cli ${context} --force\` to acknowledge and land anyway. (Kept advisory plans/ scratch never blocks: it is gitignored and can never reach origin.)`
@@ -45137,15 +45374,15 @@ function renderReport(checks, opts) {
45137
45374
  }
45138
45375
  function renderTally(checks) {
45139
45376
  const total = checks.length;
45140
- const failed = checks.filter((c) => !c.ok).length;
45377
+ const failed2 = checks.filter((c) => !c.ok).length;
45141
45378
  const unverified = checks.filter((c) => c.ok && c.verified === false).length;
45142
45379
  const warned = checks.filter((c) => c.ok && c.verified !== false && c.warn).length;
45143
- const healthy = total - failed - unverified - warned;
45144
- if (failed === 0 && unverified === 0 && warned === 0) return `\u2713 all ${total} checks healthy`;
45380
+ const healthy = total - failed2 - unverified - warned;
45381
+ if (failed2 === 0 && unverified === 0 && warned === 0) return `\u2713 all ${total} checks healthy`;
45145
45382
  const parts = [`\u2713 ${healthy} verified healthy`];
45146
45383
  if (warned > 0) parts.push(`\u26A0 ${warned} warn`);
45147
45384
  if (unverified > 0) parts.push(`? ${unverified} unverified`);
45148
- if (failed > 0) parts.push(`\u2717 ${failed} failed`);
45385
+ if (failed2 > 0) parts.push(`\u2717 ${failed2} failed`);
45149
45386
  return `${parts.join(" \xB7 ")} \u2014 ${total} checks`;
45150
45387
  }
45151
45388
  function doctorReportExitCode(checks) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.36",
3
+ "version": "4.5.38",
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",