@mutmutco/cli 4.1.14 → 4.1.16

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 +140 -40
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -13181,10 +13181,10 @@ var rollout_plan_default = {
13181
13181
  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)."
13182
13182
  },
13183
13183
  baseline: {
13184
- version: "4.1.14",
13185
- tag: "v4.1.14",
13186
- commit: "e91bb87d9ce0",
13187
- npm: "@mutmutco/cli@4.1.14"
13184
+ version: "4.1.16",
13185
+ tag: "v4.1.16",
13186
+ commit: "3d2a1772d4b6",
13187
+ npm: "@mutmutco/cli@4.1.16"
13188
13188
  },
13189
13189
  exitCriterion: "fleet-n-of-n",
13190
13190
  hubOnlyShortcut: "forbidden",
@@ -13201,14 +13201,14 @@ var rollout_plan_default = {
13201
13201
  repo: "mutmutco/mmi-hub",
13202
13202
  role: "canary",
13203
13203
  schedule: "train",
13204
- v3Target: "v4.1.14"
13204
+ v3Target: "v4.1.16"
13205
13205
  }
13206
13206
  ],
13207
13207
  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.",
13208
13208
  rollback: {
13209
13209
  independent: true,
13210
- mechanism: "npm dist-tag latest -> 4.1.14 and redeploy the Hub Lambda from tag v4.1.14 (e91bb87d9ce0); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
13211
- v3Target: "v4.1.14 (@mutmutco/cli@4.1.14, tag commit e91bb87d9ce0 \u2014 last known-good release carrying the repo-index v4-only contract)"
13210
+ mechanism: "npm dist-tag latest -> 4.1.16 and redeploy the Hub Lambda from tag v4.1.16 (3d2a1772d4b6); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
13211
+ v3Target: "v4.1.16 (@mutmutco/cli@4.1.16, tag commit 3d2a1772d4b6 \u2014 last known-good release carrying the repo-index v4-only contract)"
13212
13212
  }
13213
13213
  },
13214
13214
  {
@@ -20410,13 +20410,44 @@ function crossRepoAmbiguousRefusalMessage(ambiguous, repo, context = "pr merge")
20410
20410
  const first = `#${ambiguous[0] ?? "N"}`;
20411
20411
  return `${context}: REFUSED \u2014 GitHub will close ${named} on merge (a bare \`#N\` after a closing keyword always means the repo the PR lives in), but this body also mentions ${first} qualified against a DIFFERENT repo elsewhere \u2014 check whether that closing keyword was meant to carry an explicit owner/repo#N qualifier instead. Reword the closing reference with the full owner/repo#N form, or re-run with --force to merge anyway and let ${named} close.`;
20412
20412
  }
20413
- function commitMessagesText(commits) {
20414
- if (!Array.isArray(commits)) return "";
20413
+ function parsedCommits(commits) {
20414
+ if (!Array.isArray(commits)) return [];
20415
20415
  return commits.map((commit) => {
20416
20416
  const { messageHeadline, messageBody } = commit ?? {};
20417
- return `${typeof messageHeadline === "string" ? messageHeadline : ""}
20418
- ${typeof messageBody === "string" ? messageBody : ""}`;
20419
- }).join("\n");
20417
+ return {
20418
+ headline: typeof messageHeadline === "string" ? messageHeadline : "",
20419
+ body: typeof messageBody === "string" ? messageBody : ""
20420
+ };
20421
+ });
20422
+ }
20423
+ function commitMessagesText(commits) {
20424
+ return parsedCommits(commits).map((commit) => `${commit.headline}
20425
+ ${commit.body}`).join("\n");
20426
+ }
20427
+ function defaultSquashBodyFromCommits(commits) {
20428
+ if (commits.length <= 1) {
20429
+ const only = commits[0];
20430
+ if (!only) return "";
20431
+ const body = only.body.trim();
20432
+ return body || only.headline.trim();
20433
+ }
20434
+ return commits.map((commit) => `${commit.headline}
20435
+
20436
+ ${commit.body}`.trim()).filter(Boolean).join("\n\n");
20437
+ }
20438
+ function rewriteUnregisteredClosingKeywords(text, allowedClosing) {
20439
+ if (!text.includes("#")) return text;
20440
+ const allowed = new Set(allowedClosing);
20441
+ CLOSING_MENTION_RE.lastIndex = 0;
20442
+ const segments = text.split(/(```[\s\S]*?```|~~~[\s\S]*?~~~|`[^`\n]*`)/g);
20443
+ return segments.map((seg, i) => {
20444
+ if (i % 2 === 1) return seg;
20445
+ return seg.replace(CLOSING_MENTION_RE, (match, _keyword, plain, linked) => {
20446
+ const n = Number(plain ?? linked);
20447
+ if (!Number.isInteger(n) || n <= 0 || allowed.has(n)) return match;
20448
+ return `part of #${n}`;
20449
+ });
20450
+ }).join("");
20420
20451
  }
20421
20452
  function parseClosingGuardInput(raw, repo) {
20422
20453
  if (!raw || typeof raw !== "object") return void 0;
@@ -20428,17 +20459,25 @@ function parseClosingGuardInput(raw, repo) {
20428
20459
  if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) return void 0;
20429
20460
  closing.push(n);
20430
20461
  }
20462
+ const parsed = Array.isArray(commits) ? parsedCommits(commits) : [];
20431
20463
  const commitClosing = [...new Set(findClosingMentions(commitMessagesText(commits)).map((m) => m.issue))];
20432
20464
  const text = `${typeof title === "string" ? title : ""}
20433
20465
  ${typeof body === "string" ? body : ""}`;
20434
- return { state, text, closing, commitClosing, repo };
20466
+ return { state, text, closing, commitClosing, commits: parsed, repo };
20435
20467
  }
20436
- function restCommitMessagesText(commits) {
20437
- if (!Array.isArray(commits)) return "";
20468
+ function parsedRestCommits(commits) {
20469
+ if (!Array.isArray(commits)) return [];
20438
20470
  return commits.map((entry) => {
20439
20471
  const message = (entry ?? {}).commit?.message;
20440
- return typeof message === "string" ? message : "";
20441
- }).join("\n");
20472
+ if (typeof message !== "string" || !message) return { headline: "", body: "" };
20473
+ const nl = message.indexOf("\n");
20474
+ if (nl === -1) return { headline: message, body: "" };
20475
+ return { headline: message.slice(0, nl), body: message.slice(nl + 1).replace(/^\n/, "") };
20476
+ });
20477
+ }
20478
+ function restCommitMessagesText(commits) {
20479
+ return parsedRestCommits(commits).map((commit) => `${commit.headline}
20480
+ ${commit.body}`).join("\n");
20442
20481
  }
20443
20482
  function parseRestClosingGuardInput(prRaw, commitsRaw, repo) {
20444
20483
  if (!prRaw || typeof prRaw !== "object") return void 0;
@@ -20446,10 +20485,11 @@ function parseRestClosingGuardInput(prRaw, commitsRaw, repo) {
20446
20485
  if (typeof state !== "string") return void 0;
20447
20486
  const bodyText = typeof body === "string" ? body : "";
20448
20487
  const closing = [...new Set(findClosingMentions(bodyText).map((m) => m.issue))];
20488
+ const parsed = parsedRestCommits(commitsRaw);
20449
20489
  const commitClosing = [...new Set(findClosingMentions(restCommitMessagesText(commitsRaw)).map((m) => m.issue))];
20450
20490
  const text = `${typeof title === "string" ? title : ""}
20451
20491
  ${bodyText}`;
20452
- return { state: merged === true ? "MERGED" : state.toUpperCase(), text, closing, commitClosing, repo };
20492
+ return { state: merged === true ? "MERGED" : state.toUpperCase(), text, closing, commitClosing, commits: parsed, repo };
20453
20493
  }
20454
20494
  function negatedClosingRefusalMessage(negated, context = "pr merge") {
20455
20495
  const named = negated.map((n) => `#${n}`).join(", ");
@@ -20459,7 +20499,7 @@ function negatedClosingRefusalMessage(negated, context = "pr merge") {
20459
20499
  function commitClosingRefusalMessage(closing, context = "pr merge") {
20460
20500
  const named = closing.map((n) => `#${n}`).join(", ");
20461
20501
  const first = `#${closing[0] ?? "N"}`;
20462
- return `${context}: REFUSED \u2014 commit messages will close ${named} through the squash body, but GitHub omitted ${named} from closingIssuesReferences. Remove close/fix/resolve + ${first} from the commit message (if the issue must stay open, use "Part of ${first}" / "Refs ${first}" / "leaves ${first} open" \u2014 never "does not close ${first}"), or re-run with --force to merge anyway and let ${named} close.`;
20502
+ return `${context}: REFUSED \u2014 commit messages will close ${named} through the squash body, but GitHub omitted ${named} from closingIssuesReferences. Remove close/fix/resolve + ${first} from the commit message (if the issue must stay open, use "Part of ${first}" / "Refs ${first}" / "leaves ${first} open" \u2014 never "does not close ${first}"), edit the squash body at merge time (--squash-body-file), or re-run with --force to merge anyway and let ${named} close.`;
20463
20503
  }
20464
20504
  function alreadyClosedCommitClosingMessage(closed, context = "pr merge") {
20465
20505
  const named = closed.map((n) => `#${n}`).join(", ");
@@ -20481,8 +20521,9 @@ function evaluateClosingGuard(input, opts) {
20481
20521
  };
20482
20522
  }
20483
20523
  if (input.state !== "OPEN") return { blocked: false };
20524
+ const squashBodyClosing = opts.squashBodyText ? [...new Set(findClosingMentions(opts.squashBodyText).map((m) => m.issue))] : void 0;
20484
20525
  const { open: commitClosing, skippedClosed } = partitionCommitClosings(
20485
- input.commitClosing ?? [],
20526
+ squashBodyClosing ?? input.commitClosing ?? [],
20486
20527
  input.closing,
20487
20528
  input.alreadyClosed
20488
20529
  );
@@ -33451,29 +33492,47 @@ ${err?.stdout ?? ""}`.split("\n").map((line) => line.trim()).find((line) => line
33451
33492
  var defaultGhMergeAutoIo = {
33452
33493
  gh: async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout
33453
33494
  };
33454
- async function composeOverrideBodyFile(prNumber, repoArgs, gh) {
33495
+ function resolveSquashMergeBodyText(commits, allowedClosing, cwd) {
33496
+ const policy = squashBodyWithOverride(commits, cwd);
33497
+ const base = policy ?? defaultSquashBodyFromCommits(commits);
33498
+ if (!base) return null;
33499
+ const rewritten = rewriteUnregisteredClosingKeywords(base, allowedClosing);
33500
+ if (policy) return rewritten;
33501
+ return rewritten === base ? null : rewritten;
33502
+ }
33503
+ function writeSquashBodyFile(body) {
33504
+ const dir = (0, import_node_fs40.mkdtempSync)((0, import_node_path37.join)((0, import_node_os18.tmpdir)(), "mmi-squash-body-"));
33505
+ const path2 = (0, import_node_path37.join)(dir, "body.txt");
33506
+ (0, import_node_fs40.writeFileSync)(path2, body.endsWith("\n") ? body : `${body}
33507
+ `, "utf8");
33508
+ return { path: path2, cleanup: () => {
33509
+ try {
33510
+ (0, import_node_fs40.rmSync)(dir, { recursive: true, force: true });
33511
+ } catch {
33512
+ }
33513
+ } };
33514
+ }
33515
+ async function composeOverrideBodyFile(prNumber, repoArgs, gh, opts) {
33516
+ if (opts?.bodyText != null && opts.bodyText !== "") {
33517
+ return { ...writeSquashBodyFile(opts.bodyText), text: opts.bodyText };
33518
+ }
33455
33519
  try {
33456
33520
  const raw = await gh(["pr", "view", prNumber, ...repoArgs, "--json", "commits"], GC_GH_TIMEOUT_MS);
33457
- const commits = JSON.parse(raw).commits ?? [];
33458
- const body = squashBodyWithOverride(commits.map((c) => ({ headline: c.messageHeadline ?? "", body: c.messageBody ?? "" })), process.cwd());
33521
+ const commits = (JSON.parse(raw).commits ?? []).map((c) => ({ headline: c.messageHeadline ?? "", body: c.messageBody ?? "" }));
33522
+ const body = resolveSquashMergeBodyText(commits, opts?.allowedClosing ?? [], process.cwd());
33459
33523
  if (!body) return void 0;
33460
- const dir = (0, import_node_fs40.mkdtempSync)((0, import_node_path37.join)((0, import_node_os18.tmpdir)(), "mmi-squash-body-"));
33461
- const path2 = (0, import_node_path37.join)(dir, "body.txt");
33462
- (0, import_node_fs40.writeFileSync)(path2, `${body}
33463
- `, "utf8");
33464
- return { path: path2, cleanup: () => {
33465
- try {
33466
- (0, import_node_fs40.rmSync)(dir, { recursive: true, force: true });
33467
- } catch {
33468
- }
33469
- } };
33524
+ return { ...writeSquashBodyFile(body), text: body };
33470
33525
  } catch {
33471
33526
  return void 0;
33472
33527
  }
33473
33528
  }
33474
- async function ghMergeAutoEnqueue(prNumber, repo, method, io = defaultGhMergeAutoIo) {
33529
+ async function ghMergeAutoEnqueue(prNumber, repo, method, extra) {
33530
+ const io = extra?.io ?? defaultGhMergeAutoIo;
33475
33531
  const args = repo ? ["--repo", repo] : [];
33476
- const overrideBody = await composeOverrideBodyFile(prNumber, args, io.gh);
33532
+ const overrideBody = await composeOverrideBodyFile(prNumber, args, io.gh, {
33533
+ allowedClosing: method === "--squash" ? extra?.allowedClosing : void 0,
33534
+ bodyText: method === "--squash" ? extra?.bodyText : void 0
33535
+ });
33477
33536
  try {
33478
33537
  return await mergeAutoEnqueueWithBody(prNumber, args, method, io, overrideBody?.path);
33479
33538
  } finally {
@@ -42839,6 +42898,20 @@ async function readClosingGuardInput(number, repoArgs, repo, context) {
42839
42898
  return input;
42840
42899
  }
42841
42900
  }
42901
+ function squashBodyTextForMerge(input, methodIsSquash, explicitBody) {
42902
+ if (!methodIsSquash) return void 0;
42903
+ if (explicitBody != null && explicitBody !== "") return explicitBody;
42904
+ if (!input) return void 0;
42905
+ return resolveSquashMergeBodyText(input.commits ?? [], input.closing, process.cwd()) ?? void 0;
42906
+ }
42907
+ function warnSquashBodyClosingStrip(context, input, squashBodyText) {
42908
+ if (!input || squashBodyText == null) return;
42909
+ const { open: open2 } = partitionCommitClosings(input.commitClosing ?? [], input.closing, input.alreadyClosed);
42910
+ if (!open2.length) return;
42911
+ console.warn(
42912
+ `${context}: composing a squash body that leaves unregistered ${open2.map((n) => `#${n}`).join(", ")} open \u2014 GitHub's default COMMIT_MESSAGES composition would close them (#5723).`
42913
+ );
42914
+ }
42842
42915
  function ciAuditDeps() {
42843
42916
  const cfgPromise = loadConfig();
42844
42917
  const root = hubRoot();
@@ -42988,7 +43061,13 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
42988
43061
  return typeof viewed.state === "string" ? viewed.state : void 0;
42989
43062
  }
42990
43063
  );
42991
- const landClosingGuardVerdict = evaluateClosingGuard(landClosingGuardInput, { force: o.force, context: "pr land" });
43064
+ const landSquashBody = squashBodyTextForMerge(landClosingGuardInput, true);
43065
+ warnSquashBodyClosingStrip("pr land", landClosingGuardInput, landSquashBody);
43066
+ const landClosingGuardVerdict = evaluateClosingGuard(landClosingGuardInput, {
43067
+ force: o.force,
43068
+ context: "pr land",
43069
+ squashBodyText: landSquashBody
43070
+ });
42992
43071
  if (landClosingGuardVerdict.blocked) {
42993
43072
  console.error(landClosingGuardVerdict.message);
42994
43073
  process.exitCode = 1;
@@ -43063,7 +43142,10 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
43063
43142
  if (!result2.ok) throw new Error(`could not read PR state: ${result2.error}`);
43064
43143
  return JSON.parse(result2.state);
43065
43144
  },
43066
- mergeAuto: (prNumber, repo) => ghMergeAutoEnqueue(prNumber, repo, "--squash"),
43145
+ mergeAuto: (prNumber, repo) => ghMergeAutoEnqueue(prNumber, repo, "--squash", {
43146
+ allowedClosing: landClosingGuardInput?.closing,
43147
+ bodyText: landSquashBody
43148
+ }),
43067
43149
  pollMerged: async (prNumber, repo, deadlineMs) => {
43068
43150
  let lastFailure;
43069
43151
  while (Date.now() < deadlineMs) {
@@ -43124,7 +43206,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
43124
43206
  else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
43125
43207
  if (result.status === "failed") process.exitCode = 1;
43126
43208
  });
43127
- jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal")).action(async (number, o) => {
43209
+ jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal")).action(async (number, o) => {
43128
43210
  const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
43129
43211
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
43130
43212
  const repoForPostCleanup = await resolveRepo(o.repo) ?? o.repo;
@@ -43147,7 +43229,20 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
43147
43229
  return typeof viewed.state === "string" ? viewed.state : void 0;
43148
43230
  }
43149
43231
  );
43150
- const closingGuardVerdict = evaluateClosingGuard(closingGuardInput, { force: o.force, context: "pr merge" });
43232
+ if (o.squashBodyFile && method !== "--squash") {
43233
+ throw new Error("pr merge: --squash-body-file applies only to squash merges");
43234
+ }
43235
+ const mergeSquashBody = squashBodyTextForMerge(
43236
+ closingGuardInput,
43237
+ method === "--squash",
43238
+ o.squashBodyFile ? (0, import_node_fs48.readFileSync)(o.squashBodyFile, "utf8") : void 0
43239
+ );
43240
+ if (!o.squashBodyFile) warnSquashBodyClosingStrip("pr merge", closingGuardInput, mergeSquashBody);
43241
+ const closingGuardVerdict = evaluateClosingGuard(closingGuardInput, {
43242
+ force: o.force,
43243
+ context: "pr merge",
43244
+ squashBodyText: mergeSquashBody
43245
+ });
43151
43246
  if (closingGuardVerdict.blocked) {
43152
43247
  console.error(closingGuardVerdict.message);
43153
43248
  process.exitCode = 1;
@@ -43202,7 +43297,12 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
43202
43297
  const remoteBefore = await remoteBranchExists2(headRef);
43203
43298
  let upgradedToAuto = false;
43204
43299
  let remoteNotAttemptedReason = "host-owned-lifecycle";
43205
- const overrideBody = await composeOverrideBodyFile(number, repoArgs, async (a, t) => (await execFileP2("gh", a, { timeout: t })).stdout);
43300
+ const overrideBody = mergeSquashBody ? { ...writeSquashBodyFile(mergeSquashBody), text: mergeSquashBody } : await composeOverrideBodyFile(
43301
+ number,
43302
+ repoArgs,
43303
+ async (a, t) => (await execFileP2("gh", a, { timeout: t })).stdout,
43304
+ { allowedClosing: method === "--squash" ? closingGuardInput?.closing : void 0 }
43305
+ );
43206
43306
  const bodyFile = overrideBody?.path;
43207
43307
  try {
43208
43308
  await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto, deleteBranch: false, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch(async (e) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.1.14",
3
+ "version": "4.1.16",
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",