@mutmutco/cli 3.14.0 → 3.16.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 +142 -21
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -6299,30 +6299,54 @@ async function registerDeferredWorktree(store, entry) {
6299
6299
  return { entries, newlyRegistered: true };
6300
6300
  }
6301
6301
  async function sweepDeferredWorktrees(store, deps) {
6302
- if (!store) return { removed: [], stillDeferred: [] };
6302
+ if (!store) return { removed: [], stillDeferred: [], skipped: [] };
6303
6303
  const entries = await store.read();
6304
- if (!entries.length) return { removed: [], stillDeferred: [] };
6304
+ if (!entries.length) return { removed: [], stillDeferred: [], skipped: [] };
6305
+ let live;
6306
+ try {
6307
+ live = parseWorktreePorcelain(await deps.git(["worktree", "list", "--porcelain"]));
6308
+ } catch {
6309
+ live = [];
6310
+ }
6305
6311
  const removed = [];
6306
6312
  const stillDeferred = [];
6313
+ const skipped = [];
6307
6314
  for (const entry of entries) {
6315
+ const current = live.find((w) => samePath(w.path, entry.path));
6316
+ if (current && current.branch !== entry.branch) {
6317
+ skipped.push(entry);
6318
+ continue;
6319
+ }
6320
+ if (current) {
6321
+ const dirty = (await deps.git(["-C", entry.path, "status", "--porcelain"]).catch(() => "dirty") || "").trim().length > 0;
6322
+ if (dirty) {
6323
+ stillDeferred.push(entry);
6324
+ continue;
6325
+ }
6326
+ }
6308
6327
  const outcome = await removeWorktreeWithRecovery(entry.path, deps);
6309
6328
  if (outcome.status === "removed") removed.push(entry.path);
6310
6329
  else stillDeferred.push(entry);
6311
6330
  }
6312
6331
  await store.write(stillDeferred);
6313
- return { removed, stillDeferred };
6332
+ return { removed, stillDeferred, skipped };
6314
6333
  }
6315
6334
  async function sweepDeferredWorktreesWithRetry(store, deps, opts = {}) {
6316
6335
  const attempts = opts.attempts ?? 4;
6317
6336
  const backoff = opts.backoffMs ?? [500, 2e3, 5e3, 1e4];
6318
6337
  const sleep = opts.sleep ?? defaultSleep;
6319
- let last = { removed: [], stillDeferred: [] };
6338
+ const removed = [];
6339
+ const skipped = [];
6340
+ let stillDeferred = [];
6320
6341
  for (let i = 0; i < attempts; i++) {
6321
- last = await sweepDeferredWorktrees(store, deps);
6322
- if (!last.stillDeferred.length) return last;
6342
+ const result = await sweepDeferredWorktrees(store, deps);
6343
+ removed.push(...result.removed);
6344
+ skipped.push(...result.skipped);
6345
+ stillDeferred = result.stillDeferred;
6346
+ if (!stillDeferred.length) break;
6323
6347
  if (i < attempts - 1) await sleep(backoff[Math.min(i, backoff.length - 1)]);
6324
6348
  }
6325
- return last;
6349
+ return { removed, stillDeferred, skipped };
6326
6350
  }
6327
6351
  var defaultSleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
6328
6352
  async function removeWorktreeWithRecovery(wtPath, deps) {
@@ -6456,6 +6480,9 @@ function basePolicyBlocksImmediateMerge(message) {
6456
6480
  function ghPrMergeLocalBranchDeleteWarning(message) {
6457
6481
  return /used by worktree|cannot delete branch/i.test(message);
6458
6482
  }
6483
+ function mergeAutoRejectedPrAlreadyClean(message) {
6484
+ return /clean status \(enablePullRequestAutoMerge\)|is in clean status/i.test(message);
6485
+ }
6459
6486
  async function checkRemoteBranchExists(branch, deps, options = {}) {
6460
6487
  if (!branch) return void 0;
6461
6488
  try {
@@ -11313,6 +11340,17 @@ async function preflight(deps, ctx, stage2, meta) {
11313
11340
  throw new Error(`${ctx.repo} is not Hub-deployed (deployModel=none) \u2014 the release train does not apply; use the project's own release path`);
11314
11341
  }
11315
11342
  await deps.runSelf(["secrets", "preflight", "--stage", stage2, "--repo", ctx.repo]);
11343
+ if (stage2 === "main" && (model === "registry-publish" || meta.publishRequired === true)) {
11344
+ const publishDir = typeof meta.publishDir === "string" && meta.publishDir.trim() ? meta.publishDir.trim() : ".";
11345
+ const publishArgs = ["publish", ...publishDir !== "." ? [`./${publishDir}`] : [], "--dry-run", "--ignore-scripts"];
11346
+ try {
11347
+ await deps.run("npm", publishArgs);
11348
+ } catch (e) {
11349
+ throw new Error(
11350
+ `${ctx.repo}: pre-tag publish dry run failed for '${publishDir}' (npm ${publishArgs.join(" ")}) \u2014 refusing to tag a release that would fail to publish (#2753). Fix the publish surface (deleted dir, malformed package.json, missing files), then rerun. Underlying error: ${e instanceof Error ? e.message : String(e)}`
11351
+ );
11352
+ }
11353
+ }
11316
11354
  return model;
11317
11355
  }
11318
11356
  async function preflightMergeToMain(deps, deployModel, remoteRef, blockingPrefix, realignMessage) {
@@ -18584,6 +18622,15 @@ function parseLinkedIssues(body) {
18584
18622
  }
18585
18623
  return [...refs].sort((a, b) => Number(a.slice(1)) - Number(b.slice(1)));
18586
18624
  }
18625
+ function parseClosingIssues(body) {
18626
+ const refs = /* @__PURE__ */ new Set();
18627
+ const re = /\b(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s+#(\d+)\b/gi;
18628
+ let m;
18629
+ while ((m = re.exec(body)) !== null) {
18630
+ refs.add(`#${m[1]}`);
18631
+ }
18632
+ return [...refs].sort((a, b) => Number(a.slice(1)) - Number(b.slice(1)));
18633
+ }
18587
18634
  function assertPositiveInt(label, n) {
18588
18635
  const num = Number(n);
18589
18636
  if (!Number.isInteger(num) || num <= 0) {
@@ -19114,6 +19161,18 @@ function checkClaudePlugin(probe) {
19114
19161
  }
19115
19162
  return { ok: true, label: installed ? `Claude plugin ${installed}` : "Claude plugin" };
19116
19163
  }
19164
+ function checkTrainSync(result) {
19165
+ const problems = result.branches.filter((b) => b.status === "skipped" && (b.reason === "diverged" || b.reason === "ff-failed"));
19166
+ if (problems.length) {
19167
+ return { ok: false, label: "train branches", fix: problems.map((b) => b.note).join("; ") };
19168
+ }
19169
+ const moved = result.branches.filter((b) => b.status === "synced");
19170
+ return {
19171
+ ok: true,
19172
+ label: "train branches",
19173
+ detail: moved.length ? `fast-forwarded ${moved.map((b) => b.branch).join(", ")}` : void 0
19174
+ };
19175
+ }
19117
19176
  function planGitignore(current) {
19118
19177
  const { content, changed } = upsertManagedGitignoreBlock(current);
19119
19178
  return changed ? { ok: false, content } : { ok: true };
@@ -19152,6 +19211,11 @@ async function runDoctorClean(opts, io, deps) {
19152
19211
  checks.push(plugin);
19153
19212
  if (!plugin.ok) restartPending = true;
19154
19213
  if (isOrgRepo && !opts.fast && !opts.banner) {
19214
+ try {
19215
+ checks.push(checkTrainSync(await deps.syncTrain()));
19216
+ } catch (e) {
19217
+ checks.push({ ok: false, label: "train branches", fix: `sync failed \u2014 ${e instanceof Error ? e.message : String(e)}` });
19218
+ }
19155
19219
  const repoRoot2 = await deps.repoRoot();
19156
19220
  try {
19157
19221
  const plan = await deps.gcPlan("origin", 200);
@@ -19286,6 +19350,27 @@ function repoFromSelector2(selector) {
19286
19350
  async function loadConfigForBoardSelector(selector, repoOption) {
19287
19351
  return loadConfigForRepo(repoFromSelector2(selector) ?? repoOption);
19288
19352
  }
19353
+ async function advanceClosedIssuesToDone(prNumber, repoOption) {
19354
+ const repoArgs = repoOption ? ["--repo", repoOption] : [];
19355
+ let body;
19356
+ try {
19357
+ body = (await execFileP2("gh", ["pr", "view", prNumber, ...repoArgs, "--json", "body", "--jq", ".body"], { timeout: GC_GH_TIMEOUT_MS5 })).stdout;
19358
+ } catch {
19359
+ return void 0;
19360
+ }
19361
+ const closing = parseClosingIssues(body ?? "");
19362
+ if (closing.length === 0) return void 0;
19363
+ const results = [];
19364
+ for (const ref of closing) {
19365
+ try {
19366
+ const moved = await moveBoardItem({ config: await loadConfigForBoardSelector(ref, repoOption), selector: ref, status: "Done", repo: repoOption, allowPartial: true });
19367
+ results.push(moved.partial ? { issue: ref, moved: false, error: moved.warning } : { issue: ref, moved: true });
19368
+ } catch (e) {
19369
+ results.push({ issue: ref, moved: false, error: e.message });
19370
+ }
19371
+ }
19372
+ return results;
19373
+ }
19289
19374
  function renderGcApplyResult(result) {
19290
19375
  const lines = ["gc apply result:"];
19291
19376
  lines.push(` branches removed: ${result.removedBranches.length ? result.removedBranches.join(", ") : "none"}`);
@@ -19394,6 +19479,7 @@ async function applyGcPlan(plan, remote) {
19394
19479
  }
19395
19480
  return result;
19396
19481
  }
19482
+ var execFileGitRun = async (file, args) => (await execFileP2(file, args, { timeout: GIT_TIMEOUT_MS })).stdout;
19397
19483
  function mmiDoctorDeps() {
19398
19484
  return {
19399
19485
  githubLogin,
@@ -19408,7 +19494,10 @@ function mmiDoctorDeps() {
19408
19494
  repoRoot,
19409
19495
  gcPlan,
19410
19496
  applyGcPlan,
19411
- executeScratchGc: (root, o) => executeScratchGc(root, { apply: o.apply })
19497
+ executeScratchGc: (root, o) => executeScratchGc(root, { apply: o.apply }),
19498
+ // #2759: same fetch+ff-only sync SessionStart runs, wired here too so an on-demand `mmi-cli doctor`
19499
+ // self-heals a checkout a long-running session left stale.
19500
+ syncTrain: () => syncLocalTrainBranches(execFileGitRun)
19412
19501
  };
19413
19502
  }
19414
19503
  async function requireFreshTrainCli(commandName) {
@@ -19567,10 +19656,11 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
19567
19656
  worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout)
19568
19657
  );
19569
19658
  if (o.json) return console.log(JSON.stringify(result));
19570
- if (!o.quiet || result.removed.length || result.stillDeferred.length) {
19659
+ if (!o.quiet || result.removed.length || result.stillDeferred.length || result.skipped.length) {
19571
19660
  if (result.removed.length) console.log(`gc sweep-deferred: removed ${result.removed.length} worktree(s)`);
19572
19661
  if (result.stillDeferred.length) console.log(`gc sweep-deferred: ${result.stillDeferred.length} still queued (will retry on next session)`);
19573
- if (!result.removed.length && !result.stillDeferred.length) console.log("gc sweep-deferred: nothing queued");
19662
+ if (result.skipped.length) console.log(`gc sweep-deferred: ${result.skipped.length} dropped (repurposed for different work since queued, left untouched)`);
19663
+ if (!result.removed.length && !result.stillDeferred.length && !result.skipped.length) console.log("gc sweep-deferred: nothing queued");
19574
19664
  }
19575
19665
  } catch (e) {
19576
19666
  fail(`gc sweep-deferred: ${e.message}`);
@@ -20922,6 +21012,21 @@ async function ghMergeAutoEnqueue(prNumber, repo, method) {
20922
21012
  if (/already been merged/i.test(message)) return { mergeStatus: "merged" };
20923
21013
  const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
20924
21014
  if (note) return { mergeStatus: "failed", error: note };
21015
+ if (mergeAutoRejectedPrAlreadyClean(message)) {
21016
+ try {
21017
+ await execFileP2("gh", buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: false }), { timeout: GH_MUTATION_TIMEOUT_MS });
21018
+ } catch (e2) {
21019
+ const m2 = String(e2.message || "");
21020
+ if (/already been merged/i.test(m2)) return { mergeStatus: "merged" };
21021
+ if (!ghPrMergeLocalBranchDeleteWarning(m2)) return { mergeStatus: "failed", error: m2.split("\n")[0] };
21022
+ }
21023
+ const stateRead2 = await readMergeState();
21024
+ if (stateRead2.ok && stateRead2.state === "MERGED") return { mergeStatus: "merged" };
21025
+ return {
21026
+ mergeStatus: "failed",
21027
+ error: stateRead2.ok ? "direct merge did not land after clean-status fallback" : `could not confirm PR state after clean-status fallback: ${stateRead2.error}`
21028
+ };
21029
+ }
20925
21030
  if (ghPrMergeLocalBranchDeleteWarning(message)) {
20926
21031
  const stateRead2 = await readMergeState();
20927
21032
  if (stateRead2.ok && stateRead2.state === "MERGED") return { mergeStatus: "merged" };
@@ -21161,7 +21266,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
21161
21266
  const remoteBefore = await remoteBranchExists2(headRef);
21162
21267
  let remoteDeleteAttempted = false;
21163
21268
  let remoteNotAttemptedReason;
21164
- await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e) => {
21269
+ await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch(async (e) => {
21165
21270
  const message = String(e.message || "");
21166
21271
  if (/already been merged/i.test(message)) {
21167
21272
  remoteNotAttemptedReason = "pr-already-merged";
@@ -21169,6 +21274,19 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
21169
21274
  }
21170
21275
  const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
21171
21276
  if (note) throw new Error(`gh pr merge ${number}: ${note}`);
21277
+ if (o.auto && mergeAutoRejectedPrAlreadyClean(message)) {
21278
+ await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: false }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
21279
+ const m2 = String(e2.message || "");
21280
+ if (/already been merged/i.test(m2)) {
21281
+ remoteNotAttemptedReason = "pr-already-merged";
21282
+ return;
21283
+ }
21284
+ const note2 = timeoutKillNote(e2, GH_MUTATION_TIMEOUT_MS);
21285
+ if (note2) throw new Error(`gh pr merge ${number}: ${note2}`);
21286
+ if (!ghPrMergeLocalBranchDeleteWarning(m2)) throw e2;
21287
+ });
21288
+ return;
21289
+ }
21172
21290
  if (!o.auto && basePolicyBlocksImmediateMerge(message)) {
21173
21291
  throw new Error(`gh pr merge ${number}: the base-branch policy blocks an immediate merge \u2014 re-run with --auto to merge once required checks pass.`);
21174
21292
  }
@@ -21217,14 +21335,18 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
21217
21335
  }
21218
21336
  };
21219
21337
  }
21220
- console.log(JSON.stringify(buildPrMergeResultPayload({
21221
- number,
21222
- branch: headRef,
21223
- method: method.slice(2),
21224
- remoteBranch,
21225
- localCleanup,
21226
- housekeeping
21227
- })));
21338
+ const boardAdvance = await advanceClosedIssuesToDone(number, o.repo);
21339
+ console.log(JSON.stringify({
21340
+ ...buildPrMergeResultPayload({
21341
+ number,
21342
+ branch: headRef,
21343
+ method: method.slice(2),
21344
+ remoteBranch,
21345
+ localCleanup,
21346
+ housekeeping
21347
+ }),
21348
+ ...boardAdvance ? { boardAdvance } : {}
21349
+ }));
21228
21350
  });
21229
21351
  registerQueryCommands(program2);
21230
21352
  registerWorktreeCommands(program2);
@@ -22491,8 +22613,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
22491
22613
  // release pushed to origin (incl. the deferred main -> development alignment merge). Bounded git,
22492
22614
  // fail-soft, silent unless a branch moved.
22493
22615
  syncTrain: async (io) => {
22494
- const gitRun = async (file, args) => (await execFileP2(file, args, { timeout: GIT_TIMEOUT_MS })).stdout;
22495
- const result = await syncLocalTrainBranches(gitRun, { warn: (m) => io.err(`[mmi-hook] train sync: ${m}`) });
22616
+ const result = await syncLocalTrainBranches(execFileGitRun, { warn: (m) => io.err(`[mmi-hook] train sync: ${m}`) });
22496
22617
  const line = localTrainSyncBannerLine(result);
22497
22618
  if (line) io.log(line);
22498
22619
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.14.0",
3
+ "version": "3.16.0",
4
4
  "description": "MMI Future CLI — the org dev toolbox (board, registry, keyless secrets, release train, bootstrap, doctor) and the cross-IDE engine the plugin's session-start hook drives.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",