@mutmutco/cli 3.15.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 +57 -12
  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) {
@@ -19137,6 +19161,18 @@ function checkClaudePlugin(probe) {
19137
19161
  }
19138
19162
  return { ok: true, label: installed ? `Claude plugin ${installed}` : "Claude plugin" };
19139
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
+ }
19140
19176
  function planGitignore(current) {
19141
19177
  const { content, changed } = upsertManagedGitignoreBlock(current);
19142
19178
  return changed ? { ok: false, content } : { ok: true };
@@ -19175,6 +19211,11 @@ async function runDoctorClean(opts, io, deps) {
19175
19211
  checks.push(plugin);
19176
19212
  if (!plugin.ok) restartPending = true;
19177
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
+ }
19178
19219
  const repoRoot2 = await deps.repoRoot();
19179
19220
  try {
19180
19221
  const plan = await deps.gcPlan("origin", 200);
@@ -19438,6 +19479,7 @@ async function applyGcPlan(plan, remote) {
19438
19479
  }
19439
19480
  return result;
19440
19481
  }
19482
+ var execFileGitRun = async (file, args) => (await execFileP2(file, args, { timeout: GIT_TIMEOUT_MS })).stdout;
19441
19483
  function mmiDoctorDeps() {
19442
19484
  return {
19443
19485
  githubLogin,
@@ -19452,7 +19494,10 @@ function mmiDoctorDeps() {
19452
19494
  repoRoot,
19453
19495
  gcPlan,
19454
19496
  applyGcPlan,
19455
- 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)
19456
19501
  };
19457
19502
  }
19458
19503
  async function requireFreshTrainCli(commandName) {
@@ -19611,10 +19656,11 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
19611
19656
  worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout)
19612
19657
  );
19613
19658
  if (o.json) return console.log(JSON.stringify(result));
19614
- if (!o.quiet || result.removed.length || result.stillDeferred.length) {
19659
+ if (!o.quiet || result.removed.length || result.stillDeferred.length || result.skipped.length) {
19615
19660
  if (result.removed.length) console.log(`gc sweep-deferred: removed ${result.removed.length} worktree(s)`);
19616
19661
  if (result.stillDeferred.length) console.log(`gc sweep-deferred: ${result.stillDeferred.length} still queued (will retry on next session)`);
19617
- 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");
19618
19664
  }
19619
19665
  } catch (e) {
19620
19666
  fail(`gc sweep-deferred: ${e.message}`);
@@ -22567,8 +22613,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
22567
22613
  // release pushed to origin (incl. the deferred main -> development alignment merge). Bounded git,
22568
22614
  // fail-soft, silent unless a branch moved.
22569
22615
  syncTrain: async (io) => {
22570
- const gitRun = async (file, args) => (await execFileP2(file, args, { timeout: GIT_TIMEOUT_MS })).stdout;
22571
- 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}`) });
22572
22617
  const line = localTrainSyncBannerLine(result);
22573
22618
  if (line) io.log(line);
22574
22619
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.15.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",