@mutmutco/cli 3.85.0 → 3.86.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/main.cjs +65 -19
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -6371,6 +6371,14 @@ var LOCAL_ONLY_FILES = [".claude/settings.local.json"];
6371
6371
  var PKG = "package.json";
6372
6372
  var LOCKFILE = "package-lock.json";
6373
6373
  var NODE_MODULES = "node_modules";
6374
+ var LOCKFILE_INSTALLS = [
6375
+ { lockfile: "bun.lock", command: "bun install --frozen-lockfile" },
6376
+ { lockfile: "bun.lockb", command: "bun install --frozen-lockfile" },
6377
+ { lockfile: "pnpm-lock.yaml", command: "pnpm install --frozen-lockfile" },
6378
+ { lockfile: "yarn.lock", command: "yarn install --immutable" },
6379
+ { lockfile: LOCKFILE, command: "npm ci" }
6380
+ ];
6381
+ var NO_LOCKFILE_INSTALL = "npm install --no-package-lock";
6374
6382
  var realFsProbe = {
6375
6383
  isDir: (p) => {
6376
6384
  try {
@@ -6397,18 +6405,19 @@ var realFsProbe = {
6397
6405
  function scanInstallDirs(root, fs2 = realFsProbe) {
6398
6406
  const factsFor = (dir) => {
6399
6407
  const abs = dir ? (0, import_node_path10.join)(root, dir) : root;
6408
+ const match = LOCKFILE_INSTALLS.find((c) => fs2.isFile((0, import_node_path10.join)(abs, c.lockfile)));
6400
6409
  return {
6401
6410
  dir,
6402
6411
  hasPackageJson: fs2.isFile((0, import_node_path10.join)(abs, PKG)),
6403
- hasLockfile: fs2.isFile((0, import_node_path10.join)(abs, LOCKFILE)),
6404
- hasNodeModules: fs2.isDir((0, import_node_path10.join)(abs, NODE_MODULES))
6412
+ hasNodeModules: fs2.isDir((0, import_node_path10.join)(abs, NODE_MODULES)),
6413
+ install: match?.command
6405
6414
  };
6406
6415
  };
6407
6416
  const children = fs2.listDirs(root).filter((name) => name !== NODE_MODULES && name !== ".git");
6408
6417
  return [factsFor(""), ...children.map(factsFor).filter((f) => f.hasPackageJson)];
6409
6418
  }
6410
6419
  function npmInstallTargets(dirs) {
6411
- return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install --no-package-lock" }));
6420
+ return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.install ?? NO_LOCKFILE_INSTALL }));
6412
6421
  }
6413
6422
  function isLinkedWorktree(root, fs2 = realFsProbe) {
6414
6423
  return fs2.isFile((0, import_node_path10.join)(root, ".git"));
@@ -6432,9 +6441,9 @@ async function provisionWorktree(worktreeRoot, deps) {
6432
6441
  const allDirs = scanInstallDirs(worktreeRoot, fs2);
6433
6442
  const targets = npmInstallTargets(allDirs);
6434
6443
  if (deps.validateInstall) {
6435
- for (const dir of allDirs.filter((d) => d.hasPackageJson && d.hasLockfile && d.hasNodeModules)) {
6444
+ for (const dir of allDirs.filter((d) => d.hasPackageJson && d.install && d.hasNodeModules)) {
6436
6445
  const cwd = dir.dir ? (0, import_node_path10.join)(worktreeRoot, dir.dir) : worktreeRoot;
6437
- if (!await deps.validateInstall(cwd)) targets.push({ dir: dir.dir, command: "npm ci" });
6446
+ if (!await deps.validateInstall(cwd)) targets.push({ dir: dir.dir, command: dir.install });
6438
6447
  }
6439
6448
  }
6440
6449
  const targetDirs = new Set(targets.map((target) => target.dir));
@@ -8485,20 +8494,30 @@ async function rollDevelopmentForward(deps, ctx, tag) {
8485
8494
  const number = parsePrNumber(url);
8486
8495
  return enqueueAlignmentAutoMerge(deps, ctx, number, url || void 0, `development requires checks (${required.join(", ")}); opened alignment PR ${url || "(url unavailable)"}`);
8487
8496
  }
8497
+ var ALIGNMENT_ARM_ATTEMPTS = 3;
8498
+ var ALIGNMENT_ARM_BACKOFF_MS = 2e3;
8488
8499
  async function enqueueAlignmentAutoMerge(deps, ctx, prNumber, prUrl, openedNote) {
8489
8500
  const base = { status: "pr-pending", prNumber, prUrl };
8490
8501
  const retryCommand = `mmi-cli pr merge ${prNumber ?? "<number>"} --auto --merge`;
8491
8502
  const manual = `${openedNote} \u2014 land it with \`${retryCommand}\``;
8492
8503
  if (deps.mergeAuto && prNumber !== void 0) {
8493
- try {
8494
- const res = await deps.mergeAuto(String(prNumber), ctx.repo);
8495
- if (res.mergeStatus !== "failed") {
8496
- return { ...base, autoMergeEnqueued: true, note: `${openedNote} \u2014 auto-merge enqueued (merges when checks pass)` };
8504
+ const sleep3 = resolveSleep(deps);
8505
+ let lastError;
8506
+ for (let attempt = 1; attempt <= ALIGNMENT_ARM_ATTEMPTS; attempt += 1) {
8507
+ try {
8508
+ const res = await deps.mergeAuto(String(prNumber), ctx.repo);
8509
+ if (res.mergeStatus !== "failed") {
8510
+ return { ...base, autoMergeEnqueued: true, note: `${openedNote} \u2014 auto-merge enqueued (merges when checks pass)` };
8511
+ }
8512
+ lastError = res.error ?? "unknown error";
8513
+ } catch (e) {
8514
+ lastError = `enqueue threw (${e instanceof Error ? e.message : String(e)})`;
8497
8515
  }
8498
- deps.warn?.(`alignment PR #${prNumber} auto-merge not armed (${res.error ?? "unknown error"}) \u2014 re-run: ${retryCommand}`);
8499
- } catch (e) {
8500
- deps.warn?.(`alignment PR #${prNumber} auto-merge enqueue threw (${e instanceof Error ? e.message : String(e)}) \u2014 re-run: ${retryCommand}`);
8516
+ if (attempt < ALIGNMENT_ARM_ATTEMPTS) await sleep3(ALIGNMENT_ARM_BACKOFF_MS * attempt);
8501
8517
  }
8518
+ deps.warn?.(
8519
+ `alignment PR #${prNumber} auto-merge not armed after ${ALIGNMENT_ARM_ATTEMPTS} attempts (${lastError ?? "unknown error"}) \u2014 re-run: ${retryCommand}`
8520
+ );
8502
8521
  }
8503
8522
  return { ...base, note: manual };
8504
8523
  }
@@ -8836,6 +8855,10 @@ async function enumerateOwnWorkflowRuns(deps, repo, headSha) {
8836
8855
  const runs = await discoverShaWorkflowRuns(deps, repo, headSha, /* @__PURE__ */ new Set());
8837
8856
  return runs.length ? runs : [{ workflow: `sha-enumeration(${headSha.slice(0, 7)}) no runs yet`, conclusion: "pending" }];
8838
8857
  }
8858
+ function withReleaseRunObligation(runs) {
8859
+ if (runs.some((r) => r.event === "release")) return runs;
8860
+ return [...runs, { workflow: "release-event deploy run not created yet", conclusion: "pending" }];
8861
+ }
8839
8862
  var NON_DEPLOY_EVENTS = /* @__PURE__ */ new Set([
8840
8863
  "pull_request",
8841
8864
  "pull_request_target",
@@ -8885,6 +8908,7 @@ async function discoverShaWorkflowRuns(deps, repo, headSha, seenRunIds) {
8885
8908
  workflow: row.workflowName ?? `run:${row.databaseId}`,
8886
8909
  runId: row.databaseId,
8887
8910
  runUrl: row.url,
8911
+ event: row.event,
8888
8912
  // An in-flight run is reported UNVERIFIED, never watched. Blocking on an arbitrary discovered run
8889
8913
  // would let one long push workflow hang the release command and mask the failed rows behind it, and
8890
8914
  // `gh run watch` has no timeout. The NAMED targets are the ones worth blocking on (also #3382 review).
@@ -8914,7 +8938,7 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
8914
8938
  const note = ref === "rc" ? "no dispatch on rc: registry-publish repos have no rc-stage Release to publish from" : "no central dispatch: this repo's own publish.yml auto-fires on the published Release (#2428)";
8915
8939
  if (ref === "rc" || !autoRunHeadSha) return { note, deployStatus: "pending" };
8916
8940
  if (!watch) {
8917
- const listed = await enumerateOwnWorkflowRuns(deps, ctx.repo, autoRunHeadSha);
8941
+ const listed = withReleaseRunObligation(await enumerateOwnWorkflowRuns(deps, ctx.repo, autoRunHeadSha));
8918
8942
  return { note, workflowRuns: listed, deployStatus: aggregateWorkflowRuns(listed) };
8919
8943
  }
8920
8944
  const since = autoRunSince ?? (deps.now ?? Date.now)();
@@ -8934,7 +8958,7 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
8934
8958
  if (!autoRunHeadSha) return { note, deployStatus: "pending" };
8935
8959
  if (!watch) {
8936
8960
  if (ref === "rc") return { note, deployStatus: "pending" };
8937
- const listed = await enumerateOwnWorkflowRuns(deps, HUB_REPO, autoRunHeadSha);
8961
+ const listed = withReleaseRunObligation(await enumerateOwnWorkflowRuns(deps, HUB_REPO, autoRunHeadSha));
8938
8962
  return { note, workflowRuns: listed, deployStatus: aggregateWorkflowRuns(listed) };
8939
8963
  }
8940
8964
  const since = autoRunSince ?? (deps.now ?? Date.now)();
@@ -27444,9 +27468,17 @@ function findNegatedClosings(text, closingNumbers) {
27444
27468
  return forIssue.length > 0 && forIssue.every((m) => m.negated);
27445
27469
  });
27446
27470
  }
27471
+ function commitMessagesText(commits) {
27472
+ if (!Array.isArray(commits)) return "";
27473
+ return commits.map((commit) => {
27474
+ const { messageHeadline, messageBody } = commit ?? {};
27475
+ return `${typeof messageHeadline === "string" ? messageHeadline : ""}
27476
+ ${typeof messageBody === "string" ? messageBody : ""}`;
27477
+ }).join("\n");
27478
+ }
27447
27479
  function parseClosingGuardInput(raw) {
27448
27480
  if (!raw || typeof raw !== "object") return void 0;
27449
- const { title, body, state, closingIssuesReferences } = raw;
27481
+ const { title, body, state, closingIssuesReferences, commits } = raw;
27450
27482
  if (typeof state !== "string" || !Array.isArray(closingIssuesReferences)) return void 0;
27451
27483
  const closing = [];
27452
27484
  for (const ref of closingIssuesReferences) {
@@ -27454,15 +27486,20 @@ function parseClosingGuardInput(raw) {
27454
27486
  if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) return void 0;
27455
27487
  closing.push(n);
27456
27488
  }
27489
+ const commitClosing = [...new Set(findClosingMentions(commitMessagesText(commits)).map((m) => m.issue))];
27457
27490
  const text = `${typeof title === "string" ? title : ""}
27458
27491
  ${typeof body === "string" ? body : ""}`;
27459
- return { state, text, closing };
27492
+ return { state, text, closing, commitClosing };
27460
27493
  }
27461
27494
  function negatedClosingRefusalMessage(negated, context = "pr merge") {
27462
27495
  const named = negated.map((n) => `#${n}`).join(", ");
27463
27496
  const first = `#${negated[0] ?? "N"}`;
27464
27497
  return `${context}: REFUSED \u2014 GitHub will close ${named} on merge, but the PR body says it does not (GitHub's closing-keyword parser is negation-blind: "does not close ${first}" still closes it). Reword the body so the keyword is gone (e.g. "leaves ${first} open"), or re-run with --force to merge anyway and let ${named} close.`;
27465
27498
  }
27499
+ function commitClosingRefusalMessage(closing, context = "pr merge") {
27500
+ const named = closing.map((n) => `#${n}`).join(", ");
27501
+ return `${context}: REFUSED \u2014 commit messages will close ${named} through the squash body, but GitHub omitted ${named} from closingIssuesReferences. Remove the closing keyword from the commit message, or re-run with --force to merge anyway and let ${named} close.`;
27502
+ }
27466
27503
  function evaluateClosingGuard(input, opts) {
27467
27504
  if (!input) {
27468
27505
  return {
@@ -27470,7 +27507,16 @@ function evaluateClosingGuard(input, opts) {
27470
27507
  message: `${opts.context}: could not read the PR's closing references \u2014 proceeding WITHOUT the negated-closing check (#3718).`
27471
27508
  };
27472
27509
  }
27473
- if (input.state !== "OPEN" || input.closing.length === 0) return { blocked: false };
27510
+ if (input.state !== "OPEN") return { blocked: false };
27511
+ const commitClosing = (input.commitClosing ?? []).filter((n) => !input.closing.includes(n));
27512
+ if (commitClosing.length) {
27513
+ if (!opts.force) return { blocked: true, message: commitClosingRefusalMessage(commitClosing, opts.context) };
27514
+ return {
27515
+ blocked: false,
27516
+ message: `${opts.context}: --force past the commit-message closing guard \u2014 the squash body will close ${commitClosing.map((n) => `#${n}`).join(", ")}.`
27517
+ };
27518
+ }
27519
+ if (input.closing.length === 0) return { blocked: false };
27474
27520
  const negated = findNegatedClosings(input.text, input.closing);
27475
27521
  if (!negated.length) return { blocked: false };
27476
27522
  if (!opts.force) return { blocked: true, message: negatedClosingRefusalMessage(negated, opts.context) };
@@ -31262,7 +31308,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
31262
31308
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
31263
31309
  const startingPath = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
31264
31310
  assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr land", { force: o.force });
31265
- const landClosingGuardRaw = await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "title,body,state,closingIssuesReferences"], { timeout: GC_GH_TIMEOUT_MS2 }).then((r) => JSON.parse(r.stdout)).catch(() => void 0);
31311
+ const landClosingGuardRaw = await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "title,body,state,closingIssuesReferences,commits"], { timeout: GC_GH_TIMEOUT_MS2 }).then((r) => JSON.parse(r.stdout)).catch(() => void 0);
31266
31312
  const landClosingGuardVerdict = evaluateClosingGuard(parseClosingGuardInput(landClosingGuardRaw), { force: o.force, context: "pr land" });
31267
31313
  if (landClosingGuardVerdict.blocked) {
31268
31314
  console.error(landClosingGuardVerdict.message);
@@ -31380,7 +31426,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
31380
31426
  const repoForPostCleanup = await resolveRepo(o.repo) ?? o.repo;
31381
31427
  const [headRef, baseRef, headRefOid] = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName,baseRefName,headRefOid", "--jq", '.headRefName + " " + .baseRefName + " " + (.headRefOid // "")'], { timeout: GC_GH_TIMEOUT_MS2 })).stdout.trim().split(/\s+/);
31382
31428
  const headIsProtected = isProtectedBranch(headRef);
31383
- const closingGuardRaw = await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "title,body,state,closingIssuesReferences"], { timeout: GC_GH_TIMEOUT_MS2 }).then((r) => JSON.parse(r.stdout)).catch(() => void 0);
31429
+ const closingGuardRaw = await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "title,body,state,closingIssuesReferences,commits"], { timeout: GC_GH_TIMEOUT_MS2 }).then((r) => JSON.parse(r.stdout)).catch(() => void 0);
31384
31430
  const closingGuardVerdict = evaluateClosingGuard(parseClosingGuardInput(closingGuardRaw), { force: o.force, context: "pr merge" });
31385
31431
  if (closingGuardVerdict.blocked) {
31386
31432
  console.error(closingGuardVerdict.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.85.0",
3
+ "version": "3.86.0",
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",