@mutmutco/cli 3.85.0 → 3.87.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 +70 -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)();
@@ -23492,6 +23516,11 @@ function registerBootstrapCommands(program3) {
23492
23516
  if (!seed.classes.includes(o.class)) continue;
23493
23517
  if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
23494
23518
  if (!seedMatchesProjectType(seed, applyProjectType)) continue;
23519
+ const waiver = seed.waivers?.[slug];
23520
+ if (waiver) {
23521
+ actions.push({ action: "skip", target: seed.target, ownership: seed.ownership, reason: `waived: ${waiver}` });
23522
+ continue;
23523
+ }
23495
23524
  const resolved = { ...seed, target: seed.target.replace("{{REPO_SLUG}}", slug) };
23496
23525
  let exists = false;
23497
23526
  let sha;
@@ -27444,9 +27473,17 @@ function findNegatedClosings(text, closingNumbers) {
27444
27473
  return forIssue.length > 0 && forIssue.every((m) => m.negated);
27445
27474
  });
27446
27475
  }
27476
+ function commitMessagesText(commits) {
27477
+ if (!Array.isArray(commits)) return "";
27478
+ return commits.map((commit) => {
27479
+ const { messageHeadline, messageBody } = commit ?? {};
27480
+ return `${typeof messageHeadline === "string" ? messageHeadline : ""}
27481
+ ${typeof messageBody === "string" ? messageBody : ""}`;
27482
+ }).join("\n");
27483
+ }
27447
27484
  function parseClosingGuardInput(raw) {
27448
27485
  if (!raw || typeof raw !== "object") return void 0;
27449
- const { title, body, state, closingIssuesReferences } = raw;
27486
+ const { title, body, state, closingIssuesReferences, commits } = raw;
27450
27487
  if (typeof state !== "string" || !Array.isArray(closingIssuesReferences)) return void 0;
27451
27488
  const closing = [];
27452
27489
  for (const ref of closingIssuesReferences) {
@@ -27454,15 +27491,20 @@ function parseClosingGuardInput(raw) {
27454
27491
  if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) return void 0;
27455
27492
  closing.push(n);
27456
27493
  }
27494
+ const commitClosing = [...new Set(findClosingMentions(commitMessagesText(commits)).map((m) => m.issue))];
27457
27495
  const text = `${typeof title === "string" ? title : ""}
27458
27496
  ${typeof body === "string" ? body : ""}`;
27459
- return { state, text, closing };
27497
+ return { state, text, closing, commitClosing };
27460
27498
  }
27461
27499
  function negatedClosingRefusalMessage(negated, context = "pr merge") {
27462
27500
  const named = negated.map((n) => `#${n}`).join(", ");
27463
27501
  const first = `#${negated[0] ?? "N"}`;
27464
27502
  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
27503
  }
27504
+ function commitClosingRefusalMessage(closing, context = "pr merge") {
27505
+ const named = closing.map((n) => `#${n}`).join(", ");
27506
+ 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.`;
27507
+ }
27466
27508
  function evaluateClosingGuard(input, opts) {
27467
27509
  if (!input) {
27468
27510
  return {
@@ -27470,7 +27512,16 @@ function evaluateClosingGuard(input, opts) {
27470
27512
  message: `${opts.context}: could not read the PR's closing references \u2014 proceeding WITHOUT the negated-closing check (#3718).`
27471
27513
  };
27472
27514
  }
27473
- if (input.state !== "OPEN" || input.closing.length === 0) return { blocked: false };
27515
+ if (input.state !== "OPEN") return { blocked: false };
27516
+ const commitClosing = (input.commitClosing ?? []).filter((n) => !input.closing.includes(n));
27517
+ if (commitClosing.length) {
27518
+ if (!opts.force) return { blocked: true, message: commitClosingRefusalMessage(commitClosing, opts.context) };
27519
+ return {
27520
+ blocked: false,
27521
+ message: `${opts.context}: --force past the commit-message closing guard \u2014 the squash body will close ${commitClosing.map((n) => `#${n}`).join(", ")}.`
27522
+ };
27523
+ }
27524
+ if (input.closing.length === 0) return { blocked: false };
27474
27525
  const negated = findNegatedClosings(input.text, input.closing);
27475
27526
  if (!negated.length) return { blocked: false };
27476
27527
  if (!opts.force) return { blocked: true, message: negatedClosingRefusalMessage(negated, opts.context) };
@@ -31262,7 +31313,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
31262
31313
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
31263
31314
  const startingPath = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
31264
31315
  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);
31316
+ 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
31317
  const landClosingGuardVerdict = evaluateClosingGuard(parseClosingGuardInput(landClosingGuardRaw), { force: o.force, context: "pr land" });
31267
31318
  if (landClosingGuardVerdict.blocked) {
31268
31319
  console.error(landClosingGuardVerdict.message);
@@ -31380,7 +31431,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
31380
31431
  const repoForPostCleanup = await resolveRepo(o.repo) ?? o.repo;
31381
31432
  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
31433
  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);
31434
+ 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
31435
  const closingGuardVerdict = evaluateClosingGuard(parseClosingGuardInput(closingGuardRaw), { force: o.force, context: "pr merge" });
31385
31436
  if (closingGuardVerdict.blocked) {
31386
31437
  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.87.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",