@mutmutco/cli 2.18.0 → 2.19.0

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/index.cjs +79 -133
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -5590,7 +5590,6 @@ function trainPlan(command, options = {}) {
5590
5590
  rcandVersionStep(options),
5591
5591
  { label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
5592
5592
  { label: "preflight required rc secret names", command: "mmi-cli secrets preflight --stage rc --repo <owner/repo>", gated: true },
5593
- { label: "for Hub distribution changes, verify the release bump is already landed before touching rc", command: "node scripts/release-distribution.mjs verify <release-tag> --skip-npm-view; if missing: mmi-cli train prep --apply", gated: true },
5594
5593
  { label: "merge development to rc", gated: true },
5595
5594
  { label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch; hub-serverless: no manual dispatch, deploy.yml auto-fires on rc push, correlate/watch that run", gated: true },
5596
5595
  { label: "after a failed deploy, retry the existing rc ref (no re-tag/merge)", command: "mmi-cli tenant redeploy <owner/repo> rc --watch", gated: true }
@@ -5603,8 +5602,8 @@ function trainPlan(command, options = {}) {
5603
5602
  { label: "verify current branch is development", gated: true },
5604
5603
  { label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
5605
5604
  { label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
5606
- { label: "direct-track release skips rc; verify any distribution bump is already landed on development", command: "node scripts/release-distribution.mjs verify <release-tag> --skip-npm-view", gated: true },
5607
5605
  { label: "merge development to main", gated: true },
5606
+ { label: "fold the version bump into the release commit (Hub: full distribution set; app repos: root package manifest) \u2014 runs inside the apply step, no separate bump PR", gated: true },
5608
5607
  { label: "tag release and publish GitHub Release", gated: true },
5609
5608
  { label: "trigger the repo deploy path from the release event", command: "hub-serverless: deploy.yml + publish.yml auto-fire on the release; other models deploy via their own workflow", gated: true },
5610
5609
  { label: "roll development forward", gated: true }
@@ -5617,7 +5616,7 @@ function trainPlan(command, options = {}) {
5617
5616
  { label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
5618
5617
  { label: "verify every main-only hotfix commit is covered by the rc candidate (the guard runs automatically inside the apply step below; --ack <sha> overrides a verified, trailer-less port)", command: "mmi-cli release --apply [--ack <sha>]", gated: true },
5619
5618
  { label: "merge rc to main", gated: true },
5620
- { label: "for Hub distribution changes, verify the promoted SHA carries the release bump", command: "node scripts/release-distribution.mjs verify <release-tag> --skip-npm-view; if missing: mmi-cli train prep --apply", gated: true },
5619
+ { label: "fold the version bump into the release commit (app repos: root package manifest) \u2014 runs inside the apply step, no separate bump PR", gated: true },
5621
5620
  { label: "tag release and publish GitHub Release", gated: true },
5622
5621
  { label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch; hub-serverless: no manual dispatch, deploy.yml + publish.yml auto-fire on the release, correlate/watch those runs", gated: true },
5623
5622
  { label: "roll development forward", gated: true }
@@ -5627,7 +5626,7 @@ function trainPlan(command, options = {}) {
5627
5626
  { label: "verify the fix is merged on development (the only hotfix origin)", gated: true },
5628
5627
  { label: "branch hotfix from main and cherry-pick the dev commits", command: "git cherry-pick -x <dev-sha>", gated: true },
5629
5628
  { label: "land on main via PR, tag, deploy prod", gated: true },
5630
- { label: "forward-bump development distribution manifests (Hub only; no back-merge)", gated: true }
5629
+ { label: "no residue: development already has the fix; the next /release fold + back-merge re-aligns version manifests" }
5631
5630
  ];
5632
5631
  }
5633
5632
  function bootstrapPlan(repo, repoClass) {
@@ -5709,9 +5708,9 @@ function stalePosixFields(config, shell2) {
5709
5708
  }
5710
5709
  function sanitizeLocalStage(local, stale) {
5711
5710
  if (!stale.length) return local;
5712
- const clean4 = { ...local };
5713
- for (const field of stale) delete clean4[field];
5714
- return clean4;
5711
+ const clean3 = { ...local };
5712
+ for (const field of stale) delete clean3[field];
5713
+ return clean3;
5715
5714
  }
5716
5715
  function staleNote(staleFields, outcome) {
5717
5716
  const list = staleFields.join(", ");
@@ -6516,9 +6515,35 @@ function requireValue(value, label) {
6516
6515
  if (!value) throw new Error(`${label} could not be resolved`);
6517
6516
  return value;
6518
6517
  }
6519
- async function verifyHubDistributionVersion(deps, model, releaseTag) {
6520
- if (model !== "hub-serverless") return;
6521
- await deps.run("node", ["scripts/release-distribution.mjs", "verify", releaseTag, "--skip-npm-view"]);
6518
+ async function resolveFoldPaths(deps, model) {
6519
+ if (model === "hub-serverless") {
6520
+ return (await deps.run("node", ["scripts/release-distribution.mjs", "changed-files"])).split("\n").map((s) => s.trim()).filter(Boolean);
6521
+ }
6522
+ try {
6523
+ await deps.run("git", ["cat-file", "-e", "origin/main:package.json"]);
6524
+ } catch {
6525
+ return [];
6526
+ }
6527
+ return ["package.json", "package-lock.json"];
6528
+ }
6529
+ async function foldReleaseVersion(deps, model, tag, foldPaths) {
6530
+ if (foldPaths.length === 0) return "no version manifest to fold \u2014 the tag is the version";
6531
+ const version = tag.replace(/^v/, "");
6532
+ if (model === "hub-serverless") {
6533
+ await deps.run("node", ["scripts/release-distribution.mjs", "prepare", version]);
6534
+ } else {
6535
+ await deps.run("npm", ["version", version, "--no-git-tag-version", "--allow-same-version"]);
6536
+ }
6537
+ for (const path2 of foldPaths) {
6538
+ await deps.run("git", ["add", "--", path2]).catch(() => void 0);
6539
+ }
6540
+ const staged = (await deps.run("git", ["diff", "--cached", "--name-only"])).trim();
6541
+ if (!staged) return `version fold: manifests already at ${version} \u2014 nothing committed`;
6542
+ await deps.run("git", ["commit", "-m", `chore(release): bump distribution to ${tag}`]);
6543
+ if (model === "hub-serverless") {
6544
+ await deps.run("node", ["scripts/release-distribution.mjs", "verify", version, "--skip-npm-view"]);
6545
+ }
6546
+ return `version fold: distribution bumped to ${version} in the release commit`;
6522
6547
  }
6523
6548
  async function verifyPublishedRelease(deps, repo, tag, expectedTarget, expectedSha) {
6524
6549
  const out = await deps.run("gh", ["release", "view", tag, "--repo", repo, "--json", "tagName,targetCommitish"]);
@@ -6568,18 +6593,18 @@ async function predictMergeConflicts(deps, ours, theirs) {
6568
6593
  return files;
6569
6594
  }
6570
6595
  }
6571
- async function mergeWithSpineResolution(deps, sourceRef, label, resolve) {
6596
+ async function mergeWithSpineResolution(deps, sourceRef, label, resolve, extraTolerated = []) {
6572
6597
  try {
6573
6598
  await deps.run("git", ["merge", sourceRef, "--no-edit"]);
6574
6599
  return;
6575
6600
  } catch {
6576
6601
  }
6577
6602
  const unmerged = (await deps.run("git", ["diff", "--name-only", "--diff-filter=U"])).split("\n").map((s) => s.trim()).filter(Boolean);
6578
- const nonSpine = unmerged.filter((f) => !isSpinePath(f));
6579
- if (unmerged.length === 0 || nonSpine.length > 0) {
6603
+ const blocking = unmerged.filter((f) => !isSpinePath(f) && !extraTolerated.includes(f));
6604
+ if (unmerged.length === 0 || blocking.length > 0) {
6580
6605
  await deps.run("git", ["merge", "--abort"]);
6581
6606
  throw new Error(
6582
- unmerged.length === 0 ? `${label} merge failed without conflicted paths \u2014 merge aborted; inspect the repo state and rerun` : `${label} merge conflicts on non-spine path(s): ${nonSpine.join(", ")} \u2014 merge aborted (the train is misaligned; reconcile the branches via an approved alignment PR, then rerun)`
6607
+ unmerged.length === 0 ? `${label} merge failed without conflicted paths \u2014 merge aborted; inspect the repo state and rerun` : `${label} merge conflicts on non-spine path(s): ${blocking.join(", ")} \u2014 merge aborted (the train is misaligned; reconcile the branches via an approved alignment PR, then rerun)`
6583
6608
  );
6584
6609
  }
6585
6610
  await deps.run("git", ["checkout", `--${resolve}`, "--", ...unmerged]);
@@ -6934,7 +6959,6 @@ async function runTrainApply(command, deps, options = {}) {
6934
6959
  );
6935
6960
  const deployModel2 = await preflight(deps, ctx, "rc", meta);
6936
6961
  const releaseBase = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
6937
- await verifyHubDistributionVersion(deps, deployModel2, releaseBase);
6938
6962
  await deps.run("git", ["checkout", "rc"]);
6939
6963
  await deps.run("git", ["pull", "--ff-only", "origin", "rc"]);
6940
6964
  await deps.run("git", ["merge", "development", "--no-edit"]);
@@ -6959,12 +6983,12 @@ async function runTrainApply(command, deps, options = {}) {
6959
6983
  );
6960
6984
  const deployModel2 = await preflight(deps, ctx, "main", meta);
6961
6985
  const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
6962
- await verifyHubDistributionVersion(deps, deployModel2, tag2);
6986
+ const foldPaths2 = await resolveFoldPaths(deps, deployModel2);
6963
6987
  const predicted2 = await predictMergeConflicts(deps, "origin/main", "origin/development");
6964
- const predictedNonSpine2 = predicted2.filter((f) => !isSpinePath(f));
6965
- if (predictedNonSpine2.length > 0) {
6988
+ const predictedBlocking2 = predicted2.filter((f) => !isSpinePath(f) && !foldPaths2.includes(f));
6989
+ if (predictedBlocking2.length > 0) {
6966
6990
  throw new Error(
6967
- `development -> main merge would conflict on non-spine path(s): ${predictedNonSpine2.join(", ")} \u2014 no merge was started. The train is misaligned: reconcile main and development via an approved alignment PR, then rerun release.`
6991
+ `development -> main merge would conflict on non-spine path(s): ${predictedBlocking2.join(", ")} \u2014 no merge was started. The train is misaligned: reconcile main and development via an approved alignment PR, then rerun release.`
6968
6992
  );
6969
6993
  }
6970
6994
  await deps.run("git", ["checkout", "main"]);
@@ -6972,8 +6996,9 @@ async function runTrainApply(command, deps, options = {}) {
6972
6996
  if (predicted2.length === 0) {
6973
6997
  await deps.run("git", ["merge", "development", "--no-edit"]);
6974
6998
  } else {
6975
- await mergeWithSpineResolution(deps, "development", "development -> main", "theirs");
6999
+ await mergeWithSpineResolution(deps, "development", "development -> main", "theirs", foldPaths2);
6976
7000
  }
7001
+ const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
6977
7002
  const releaseSha2 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
6978
7003
  await ensureTagPushed(deps, tag2, releaseSha2);
6979
7004
  const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
@@ -6996,6 +7021,7 @@ async function runTrainApply(command, deps, options = {}) {
6996
7021
  deployModel: deployModel2,
6997
7022
  promoted: true,
6998
7023
  checks: checks2,
7024
+ versionFold: versionFold2,
6999
7025
  dispatch: d2.note,
7000
7026
  runId: d2.runId,
7001
7027
  runUrl: d2.runUrl,
@@ -7013,11 +7039,12 @@ async function runTrainApply(command, deps, options = {}) {
7013
7039
  "nothing to release: origin/rc is not ahead of origin/main"
7014
7040
  );
7015
7041
  const deployModel = await preflight(deps, ctx, "main", meta);
7042
+ const foldPaths = await resolveFoldPaths(deps, deployModel);
7016
7043
  const predicted = await predictMergeConflicts(deps, "origin/main", "origin/rc");
7017
- const predictedNonSpine = predicted.filter((f) => !isSpinePath(f));
7018
- if (predictedNonSpine.length > 0) {
7044
+ const predictedBlocking = predicted.filter((f) => !isSpinePath(f) && !foldPaths.includes(f));
7045
+ if (predictedBlocking.length > 0) {
7019
7046
  throw new Error(
7020
- `rc -> main merge would conflict on non-spine path(s): ${predictedNonSpine.join(", ")} \u2014 no merge was started. The train is misaligned: reconcile main and rc via an approved alignment PR (do not hand-resolve on main), then rerun release.`
7047
+ `rc -> main merge would conflict on non-spine path(s): ${predictedBlocking.join(", ")} \u2014 no merge was started. The train is misaligned: reconcile main and rc via an approved alignment PR (do not hand-resolve on main), then rerun release.`
7021
7048
  );
7022
7049
  }
7023
7050
  const coverage = deps.hotfixCoverage({ mainRef: "origin/main", rcRef: "origin/rc", ack: options.ack ?? [] });
@@ -7033,10 +7060,10 @@ async function runTrainApply(command, deps, options = {}) {
7033
7060
  if (predicted.length === 0) {
7034
7061
  await deps.run("git", ["merge", "rc", "--no-edit"]);
7035
7062
  } else {
7036
- await mergeWithSpineResolution(deps, "rc", "rc -> main", "theirs");
7063
+ await mergeWithSpineResolution(deps, "rc", "rc -> main", "theirs", foldPaths);
7037
7064
  }
7038
7065
  const tag = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
7039
- await verifyHubDistributionVersion(deps, deployModel, tag);
7066
+ const versionFold = await foldReleaseVersion(deps, deployModel, tag, foldPaths);
7040
7067
  const releaseSha = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
7041
7068
  await ensureTagPushed(deps, tag, releaseSha);
7042
7069
  const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
@@ -7062,6 +7089,7 @@ async function runTrainApply(command, deps, options = {}) {
7062
7089
  deployModel,
7063
7090
  promoted: true,
7064
7091
  checks,
7092
+ versionFold,
7065
7093
  dispatch: d.note,
7066
7094
  runId: d.runId,
7067
7095
  runUrl: d.runUrl,
@@ -7288,34 +7316,6 @@ function checkHotfixCoverage(options = {}) {
7288
7316
  return { ok: uncovered.length === 0, mainRef, rcRef, commits, uncovered };
7289
7317
  }
7290
7318
 
7291
- // src/train-prep.ts
7292
- function clean2(text) {
7293
- return text.trim();
7294
- }
7295
- function normalizeVersion2(target) {
7296
- const version = target.replace(/^v/, "");
7297
- if (!/^\d+\.\d+\.\d+$/.test(version)) {
7298
- throw new Error(`invalid train prep target ${target || "(empty)"}`);
7299
- }
7300
- return version;
7301
- }
7302
- function parseChangedFiles(out) {
7303
- return out.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
7304
- }
7305
- async function runTrainPrep(deps, options = {}) {
7306
- const target = clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"]));
7307
- const version = normalizeVersion2(target);
7308
- if (!options.apply) {
7309
- return { target, version, applied: false, command: "mmi-cli train prep --apply", stagedFiles: [] };
7310
- }
7311
- await deps.run("node", ["scripts/release-distribution.mjs", "prepare", target]);
7312
- const stagedFiles = parseChangedFiles(await deps.run("node", ["scripts/release-distribution.mjs", "changed-files"]));
7313
- if (stagedFiles.length === 0) throw new Error("no locked distribution files returned by release-distribution changed-files");
7314
- await deps.run("git", ["add", "--", ...stagedFiles]);
7315
- await deps.run("node", ["scripts/release-distribution.mjs", "verify", target, "--skip-npm-view"]);
7316
- return { target, version, applied: true, command: "mmi-cli pr create --base development --head <current-branch>", stagedFiles };
7317
- }
7318
-
7319
7319
  // src/tenant-sweep.ts
7320
7320
  function isRcBearingTenant(p) {
7321
7321
  if ((p.deployModel ?? "") !== "tenant-container") return false;
@@ -7383,7 +7383,7 @@ function renderSweep(r) {
7383
7383
  var HOTFIX_RELEASE_WORKFLOWS = ["deploy.yml", "publish.yml"];
7384
7384
  var HOTFIX_RUN_FIND_ATTEMPTS = 10;
7385
7385
  var HOTFIX_RUN_FIND_DELAY_MS = 15e3;
7386
- function clean3(out) {
7386
+ function clean2(out) {
7387
7387
  return out.trim();
7388
7388
  }
7389
7389
  function sleeper(deps) {
@@ -7437,7 +7437,7 @@ async function resolveHotfixSource(deps, ctx, from) {
7437
7437
  if (!sha2) throw new Error(`PR #${num} has no merge commit recorded \u2014 name the commit SHA explicitly`);
7438
7438
  return { sha: sha2, label: `PR #${num} (${sha2.slice(0, 7)})` };
7439
7439
  }
7440
- const sha = clean3(await deps.run("git", ["rev-parse", "--verify", `${from}^{commit}`]));
7440
+ const sha = clean2(await deps.run("git", ["rev-parse", "--verify", `${from}^{commit}`]));
7441
7441
  if (!sha) throw new Error(`could not resolve commit ${from}`);
7442
7442
  return { sha, label: sha.slice(0, 7) };
7443
7443
  }
@@ -7464,7 +7464,7 @@ async function runHotfixStart(deps, options) {
7464
7464
  };
7465
7465
  }
7466
7466
  const { sha, label } = await resolveHotfixSource(deps, ctx, options.from);
7467
- const remoteBranch = clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
7467
+ const remoteBranch = clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
7468
7468
  if (remoteBranch) {
7469
7469
  await deps.run("git", ["checkout", branch]);
7470
7470
  await deps.run("git", ["pull", "--ff-only", "origin", branch]);
@@ -7490,7 +7490,7 @@ async function runHotfixStart(deps, options) {
7490
7490
  }
7491
7491
  await deps.run("git", ["push", "-u", "origin", branch]);
7492
7492
  }
7493
- const prUrl = clean3(await deps.run("gh", [
7493
+ const prUrl = clean2(await deps.run("gh", [
7494
7494
  "pr",
7495
7495
  "create",
7496
7496
  "--repo",
@@ -7573,7 +7573,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
7573
7573
  if (releaseExists) {
7574
7574
  releaseNote = `Release ${tag} already exists \u2014 resumed without recreating`;
7575
7575
  } else {
7576
- const tagCommit = clean3(await deps.run("git", ["rev-parse", `${tag}^{commit}`]));
7576
+ const tagCommit = clean2(await deps.run("git", ["rev-parse", `${tag}^{commit}`]));
7577
7577
  await deps.run("gh", ["release", "create", tag, "--repo", ctx.repo, "--target", tagCommit, "--generate-notes", "--latest"]);
7578
7578
  releaseNote = `Release ${tag} created (target ${tagCommit.slice(0, 7)})`;
7579
7579
  if (deps.announce) {
@@ -7584,7 +7584,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
7584
7584
  for (const workflow of HOTFIX_RELEASE_WORKFLOWS) {
7585
7585
  runs.push(await watchReleaseRun(deps, ctx, workflow, mergedSha));
7586
7586
  }
7587
- const previousRef = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
7587
+ const previousRef = clean2(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
7588
7588
  let verifyNote;
7589
7589
  const publishSucceeded = runs.find((r) => r.workflow === "publish.yml")?.conclusion === "success";
7590
7590
  try {
@@ -7597,15 +7597,6 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
7597
7597
  }
7598
7598
  return { ...ctx, command: "hotfix-release", tag, mergedSha, checks, tagNote, releaseNote, runs, verifyNote, announceNote };
7599
7599
  }
7600
- function versionAtLeast2(actual, wanted) {
7601
- const pa = actual.split(".").map(Number);
7602
- const pw = wanted.split(".").map(Number);
7603
- if (pa.length < 3 || pa.some(Number.isNaN) || pw.length < 3 || pw.some(Number.isNaN)) return false;
7604
- for (let i = 0; i < 3; i += 1) {
7605
- if (pa[i] !== pw[i]) return pa[i] > pw[i];
7606
- }
7607
- return true;
7608
- }
7609
7600
  function deriveHotfixState(f) {
7610
7601
  if (!f.branchExists && !f.pr && !f.tagPushed && !f.releaseExists) {
7611
7602
  return { state: "not-started", next: `mmi-cli hotfix start --from <pr#|sha> (would mint ${f.tag})` };
@@ -7622,13 +7613,7 @@ function deriveHotfixState(f) {
7622
7613
  if (!f.tagPushed || !f.releaseExists) {
7623
7614
  return { state: "pr-merged (not released)", next: `mmi-cli hotfix release ${f.tag}` };
7624
7615
  }
7625
- if (!f.devDistribution.aligned) {
7626
- return {
7627
- state: `UNFINISHED \u2014 released but development distribution manifests behind (dev ${f.devDistribution.version} < ${f.version})`,
7628
- next: `forward-bump development (never a back-merge): branch from origin/development, node scripts/release-distribution.mjs prepare ${f.version}, land by PR (/hotfix Step 5)`
7629
- };
7630
- }
7631
- return { state: "complete", next: "nothing \u2014 pipeline complete (rc absorbs the fix at the next /rcand; /release guards coverage)" };
7616
+ return { state: "complete", next: "nothing \u2014 pipeline complete (rc absorbs the fix at the next /rcand; /release guards coverage and re-aligns the version manifests via its fold + back-merge)" };
7632
7617
  }
7633
7618
  async function runHotfixStatus(deps, versionInput) {
7634
7619
  const ctx = await buildTrainApplyContext(deps);
@@ -7652,9 +7637,9 @@ async function runHotfixStatus(deps, versionInput) {
7652
7637
  return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts) };
7653
7638
  }
7654
7639
  async function gatherHotfixFacts(deps, ctx, tag, version) {
7655
- const branchExists = Boolean(clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
7640
+ const branchExists = Boolean(clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
7656
7641
  const pr2 = await findHotfixPr(deps, ctx, tag);
7657
- const remoteTag = clean3(await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]));
7642
+ const remoteTag = clean2(await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]));
7658
7643
  const tagPushed = Boolean(remoteTag);
7659
7644
  const tagSha = remoteTag.split(/\s+/)[0] || "";
7660
7645
  let releaseExists = false;
@@ -7688,19 +7673,8 @@ async function gatherHotfixFacts(deps, ctx, tag, version) {
7688
7673
  }
7689
7674
  }
7690
7675
  }
7691
- const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean3, () => "unknown");
7692
- const devVersion = await deps.run("git", ["show", "origin/development:cli/package.json"]).then(
7693
- (out) => {
7694
- try {
7695
- return JSON.parse(out).version ?? "unknown";
7696
- } catch {
7697
- return "unknown";
7698
- }
7699
- },
7700
- () => "unknown"
7701
- );
7702
- const devDistribution = { version: devVersion, aligned: versionAtLeast2(devVersion, version) };
7703
- return { tag, version, branchExists, pr: pr2 ? { number: pr2.number, state: pr2.state, url: pr2.url } : null, tagPushed, releaseExists, runs, npmVersion, devDistribution };
7676
+ const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean2, () => "unknown");
7677
+ return { tag, version, branchExists, pr: pr2 ? { number: pr2.number, state: pr2.state, url: pr2.url } : null, tagPushed, releaseExists, runs, npmVersion };
7704
7678
  }
7705
7679
 
7706
7680
  // src/release-announce.ts
@@ -9601,8 +9575,8 @@ function resolveKbSource(rawBase) {
9601
9575
  return { owner: m[1], repo: m[2], ref: m[3] };
9602
9576
  }
9603
9577
  function buildKbGetArgs(src, path2) {
9604
- const clean4 = path2.replace(/^\/+/, "");
9605
- return ["api", `repos/${src.owner}/${src.repo}/contents/${clean4}?ref=${src.ref}`, "-H", "Accept: application/vnd.github.raw"];
9578
+ const clean3 = path2.replace(/^\/+/, "");
9579
+ return ["api", `repos/${src.owner}/${src.repo}/contents/${clean3}?ref=${src.ref}`, "-H", "Accept: application/vnd.github.raw"];
9606
9580
  }
9607
9581
  function buildKbTreeArgs(src) {
9608
9582
  return ["api", `repos/${src.owner}/${src.repo}/git/trees/${src.ref}?recursive=1`];
@@ -10690,12 +10664,10 @@ async function applyVersionAutoUpdate(report, log) {
10690
10664
  async function requireFreshTrainCli(commandName) {
10691
10665
  const report = buildVersionLagReport({
10692
10666
  currentVersion: resolveClientVersion(),
10693
- repoVersion: readRepoVersion(),
10694
10667
  releasedVersion: await fetchReleasedVersion()
10695
10668
  });
10696
10669
  if (report.ok) return;
10697
- const target = report.staleAgainst === "released" ? `released ${report.releasedVersion}` : `repo ${report.repoVersion}`;
10698
- throw new Error(`running mmi-cli ${report.currentVersion} is stale against ${target}; run doctor/update first so ${commandName} uses the current train path`);
10670
+ throw new Error(`running mmi-cli ${report.currentVersion} is stale against released ${report.releasedVersion}; run doctor/update first so ${commandName} uses the current train path`);
10699
10671
  }
10700
10672
  var consoleIo = { log: (m) => console.log(m), err: (m) => console.error(m) };
10701
10673
  var CLAUDE_PLUGIN_TIMEOUT_MS = 12e4;
@@ -12163,11 +12135,12 @@ program2.command("stage-live").description("explain that remote rc/live environm
12163
12135
  var GH_TRAIN_TIMEOUT_MS = 3e4;
12164
12136
  var GH_RUN_WATCH_TIMEOUT_MS = 20 * 6e4;
12165
12137
  var NODE_PREPARE_TIMEOUT_MS = 10 * 6e4;
12138
+ var NPM_TRAIN_TIMEOUT_MS = 6e4;
12166
12139
  function trainApplyDeps() {
12167
12140
  return {
12168
12141
  run: async (file, args) => {
12169
- const timeout = file === "node" && args[1] === "prepare" ? NODE_PREPARE_TIMEOUT_MS : file !== "gh" ? GIT_TIMEOUT_MS : args[0] === "run" && args[1] === "watch" ? GH_RUN_WATCH_TIMEOUT_MS : GH_TRAIN_TIMEOUT_MS;
12170
- return (await execFileP4(file, args, { timeout })).stdout;
12142
+ const timeout = file === "node" && args[1] === "prepare" ? NODE_PREPARE_TIMEOUT_MS : file === "npm" ? NPM_TRAIN_TIMEOUT_MS : file !== "gh" ? GIT_TIMEOUT_MS : args[0] === "run" && args[1] === "watch" ? GH_RUN_WATCH_TIMEOUT_MS : GH_TRAIN_TIMEOUT_MS;
12143
+ return isWin && file === "npm" ? (await execFileP4("cmd.exe", ["/c", "npm", ...args], { timeout })).stdout : (await execFileP4(file, args, { timeout })).stdout;
12171
12144
  },
12172
12145
  runSelf: async (args) => (await execFileP4(process.execPath, [process.argv[1], ...args], { timeout: 3e4 })).stdout,
12173
12146
  trainAuthority: async (repo) => {
@@ -12183,10 +12156,11 @@ function trainApplyDeps() {
12183
12156
  throw new Error(`tenant deploy dispatch failed: ${detail}`);
12184
12157
  }
12185
12158
  },
12186
- // Hotfix-coverage guard (#958): runs against the local clone via real git. manifestPaths is empty —
12187
- // product repos carry no main-only distribution bump (that exemption is Hub-distribution-specific, and
12188
- // the Hub is direct-track, so it never reaches this guard).
12189
- hotfixCoverage: (input) => checkHotfixCoverage({ ...input, manifestPaths: [] }),
12159
+ // Hotfix-coverage guard (#958): runs against the local clone via real git. manifestPaths exempts the
12160
+ // release version fold (#976) — a main-only commit touching ONLY the root package manifest is the
12161
+ // fold's version metadata, which the candidate replaces with its own. (The Hub's wider distribution
12162
+ // set never reaches this guard: the Hub is direct-track.)
12163
+ hotfixCoverage: (input) => checkHotfixCoverage({ ...input, manifestPaths: ["package.json", "package-lock.json"] }),
12190
12164
  // Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
12191
12165
  announce: (args) => announceRelease({
12192
12166
  run: async (file, cmdArgs) => (await execFileP4(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
@@ -12201,14 +12175,6 @@ function trainApplyDeps() {
12201
12175
  }
12202
12176
  };
12203
12177
  }
12204
- function trainPrepDeps() {
12205
- return {
12206
- run: async (file, args) => {
12207
- const timeout = file === "node" && args[1] === "prepare" ? NODE_PREPARE_TIMEOUT_MS : GIT_TIMEOUT_MS;
12208
- return (await execFileP4(file, args, { timeout })).stdout;
12209
- }
12210
- };
12211
- }
12212
12178
  function formatWorkflowRun(r) {
12213
12179
  const ref = r.runUrl ?? (r.runId != null ? String(r.runId) : "unresolved");
12214
12180
  return `${r.workflow} ${ref} ${r.conclusion.toUpperCase()}`;
@@ -12224,6 +12190,7 @@ function renderDeployLine(d) {
12224
12190
  }
12225
12191
  function renderTrainApply(commandName, r) {
12226
12192
  let base = `mmi-cli ${commandName}: promoted ${r.repo} \u2192 ${r.stage} at ${r.tag} [${r.deployModel}]; ${renderDeployLine(r)}`;
12193
+ if (r.versionFold) base = `${base}; ${r.versionFold}`;
12227
12194
  if (r.resumeNote) base = `${base}; ${r.resumeNote}`;
12228
12195
  if (r.rcRetirement) base = `${base}; rc retirement: ${r.rcRetirement.toUpperCase()} (${r.rcRetirementNote ?? ""})`;
12229
12196
  return r.announceNote ? `${base}; announce: ${r.announceNote}` : base;
@@ -12239,27 +12206,6 @@ async function resolveRcandPlanTargets() {
12239
12206
  return { plannedReleaseError: e.message, existingRcReleaseError: e.message };
12240
12207
  }
12241
12208
  }
12242
- function renderTrainPrep(r) {
12243
- if (!r.applied) {
12244
- return `mmi-cli train prep: target ${r.target}; run ${r.command} to prepare and stage locked distribution files`;
12245
- }
12246
- return [
12247
- `mmi-cli train prep: prepared ${r.target}`,
12248
- ` - staged locked files: ${r.stagedFiles.join(", ")}`,
12249
- " - stopped before promotion; no push, merge, tag, release, or deploy was run",
12250
- ` - next: ${r.command}`
12251
- ].join("\n");
12252
- }
12253
- var trainCmd = program2.command("train").description("release train helpers that stop before promotion");
12254
- trainCmd.command("prep").description("prepare and stage the Hub distribution bump for the next release train").option("--json", "machine-readable output").option("--apply", "run release-distribution prepare, exact-file stage, verify, then stop").action(async (o) => {
12255
- try {
12256
- await requireFreshTrainCli("train prep");
12257
- const result = await runTrainPrep(trainPrepDeps(), { apply: o.apply });
12258
- printLine(o.json ? JSON.stringify(result, null, 2) : renderTrainPrep(result));
12259
- } catch (e) {
12260
- fail(`train prep: ${e.message}`);
12261
- }
12262
- });
12263
12209
  for (const commandName of ["rcand", "release"]) {
12264
12210
  program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval").option("--announce-summary-file <path>", "release only: agent-curated summary lines for the Hub Slack announcement (#883)").option("--ack <shas>", "release only: comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)").action(async (o) => {
12265
12211
  try {
@@ -12299,7 +12245,7 @@ function renderHotfixRelease(r) {
12299
12245
  ...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
12300
12246
  ` - ${r.verifyNote}`,
12301
12247
  ...r.announceNote ? [` - announce: ${r.announceNote}`] : [],
12302
- ` - next: mmi-cli hotfix status ${r.tag} (no back-merge \u2014 development already has the fix; align its distribution manifests by PR if status says behind)`
12248
+ ` - next: mmi-cli hotfix status ${r.tag} (no back-merge \u2014 development already has the fix; the next /release back-merge aligns the version manifests)`
12303
12249
  ].join("\n");
12304
12250
  }
12305
12251
  function renderHotfixStatus(r) {
@@ -12307,7 +12253,7 @@ function renderHotfixStatus(r) {
12307
12253
  `mmi-cli hotfix status: ${r.tag} on ${r.repo} \u2014 ${r.state}`,
12308
12254
  ` - branch: ${r.branchExists ? "pushed" : "absent"} \xB7 PR: ${r.pr ? `#${r.pr.number} ${r.pr.state}` : "none"} \xB7 tag: ${r.tagPushed ? "pushed" : "absent"} \xB7 Release: ${r.releaseExists ? "exists" : "absent"}`,
12309
12255
  ...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
12310
- ` - npm @mutmutco/cli: ${r.npmVersion} \xB7 development manifests: ${r.devDistribution.version}${r.devDistribution.aligned ? " (aligned)" : " (behind)"}`,
12256
+ ` - npm @mutmutco/cli: ${r.npmVersion}`,
12311
12257
  ` - next: ${r.next}`
12312
12258
  ].join("\n");
12313
12259
  }
@@ -12971,7 +12917,7 @@ async function runDoctor(opts, io = consoleIo) {
12971
12917
  io.log(gaps.length ? `
12972
12918
  ${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
12973
12919
  }
12974
- program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, Hub API default, plugin git clone, plugin install record, .gitignore managed block, plugin config drift, installed plugin version, Cursor Team Marketplace plugin install), print fixes, and report per-surface versions + update recipes").option("--banner", "one-line resume summary; silent when all gates pass").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (read-only inspection \u2014 performs no repairs)").option("--apply", "perform the same auto-repairs as the interactive run (combine with --json for a machine-readable repair run)").option("--no-repo-writes", "env/plugin repairs only \u2014 never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for release-train prep)").action((opts) => (
12920
+ program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, Hub API default, plugin git clone, plugin install record, .gitignore managed block, plugin config drift, installed plugin version, Cursor Team Marketplace plugin install), print fixes, and report per-surface versions + update recipes").option("--banner", "one-line resume summary; silent when all gates pass").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (read-only inspection \u2014 performs no repairs)").option("--apply", "perform the same auto-repairs as the interactive run (combine with --json for a machine-readable repair run)").option("--no-repo-writes", "env/plugin repairs only \u2014 never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for train preflights)").action((opts) => (
12975
12921
  // Commander maps `--no-repo-writes` to `repoWrites: false`; translate to the explicit `noRepoWrites` flag.
12976
12922
  runDoctor({ ...opts, noRepoWrites: opts.repoWrites === false })
12977
12923
  ));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "2.18.0",
3
+ "version": "2.19.0",
4
4
  "description": "MMI Future CLI — delivers the org rules (whole-file), plus saga and KB access. The cross-IDE engine the plugin's SessionStart hook drives.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",