@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.
- package/dist/main.cjs +142 -21
- 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
|
-
|
|
6338
|
+
const removed = [];
|
|
6339
|
+
const skipped = [];
|
|
6340
|
+
let stillDeferred = [];
|
|
6320
6341
|
for (let i = 0; i < attempts; i++) {
|
|
6321
|
-
|
|
6322
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
21221
|
-
|
|
21222
|
-
|
|
21223
|
-
|
|
21224
|
-
|
|
21225
|
-
|
|
21226
|
-
|
|
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
|
|
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.
|
|
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",
|