@mutmutco/cli 3.55.0 → 3.57.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 +111 -67
- package/package.json +2 -2
package/dist/main.cjs
CHANGED
|
@@ -9605,6 +9605,23 @@ function describePrMergeEnqueuedReason(checks) {
|
|
|
9605
9605
|
};
|
|
9606
9606
|
}
|
|
9607
9607
|
}
|
|
9608
|
+
function decidePrMergeCleanupGate(input) {
|
|
9609
|
+
const state = (input.state ?? "").trim().toUpperCase();
|
|
9610
|
+
if (state === "MERGED") return { action: "cleanup" };
|
|
9611
|
+
if (!state) {
|
|
9612
|
+
return {
|
|
9613
|
+
action: "refuse",
|
|
9614
|
+
reason: "state-unreadable",
|
|
9615
|
+
message: `could not read the PR's state after the merge attempt${input.stateError ? `: ${input.stateError}` : ""} \u2014 cleanup skipped because the merge is unproven`
|
|
9616
|
+
};
|
|
9617
|
+
}
|
|
9618
|
+
if (input.autoRequested) return { action: "enqueued" };
|
|
9619
|
+
return {
|
|
9620
|
+
action: "refuse",
|
|
9621
|
+
reason: "not-merged",
|
|
9622
|
+
message: `the PR is ${state}, not MERGED \u2014 the merge did not happen, so the branch and worktree were left untouched`
|
|
9623
|
+
};
|
|
9624
|
+
}
|
|
9608
9625
|
function parseGhPrChecksOutput(stdout) {
|
|
9609
9626
|
const text = stdout.trim();
|
|
9610
9627
|
if (!text) return "no-checks-reported";
|
|
@@ -21808,6 +21825,54 @@ function formatStaleLeaks(leaks) {
|
|
|
21808
21825
|
async function repoRootOf() {
|
|
21809
21826
|
return (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
21810
21827
|
}
|
|
21828
|
+
function decideWorktreeLandRefCleanup(input) {
|
|
21829
|
+
if (input.keepRemote) return { action: "proceed" };
|
|
21830
|
+
if (!input.prs) {
|
|
21831
|
+
return {
|
|
21832
|
+
action: "keep-remote",
|
|
21833
|
+
reason: "pr-state-unreadable",
|
|
21834
|
+
message: `could not read the pull-request state for '${input.branch}' \u2014 keeping the remote branch so an unmerged head ref cannot be deleted on a guess`
|
|
21835
|
+
};
|
|
21836
|
+
}
|
|
21837
|
+
const open2 = input.prs.filter((pr2) => pr2.state.toUpperCase() === "OPEN");
|
|
21838
|
+
if (open2.length) {
|
|
21839
|
+
return {
|
|
21840
|
+
action: "refuse",
|
|
21841
|
+
reason: "open-pr",
|
|
21842
|
+
message: `'${input.branch}' still has an OPEN pull request (#${open2.map((pr2) => pr2.number).join(", #")}). Deleting its head ref would auto-close the PR and leave the work on no branch. Merge it first, or re-run with --keep-remote for a local-only clean`
|
|
21843
|
+
};
|
|
21844
|
+
}
|
|
21845
|
+
if (input.prs.length && !input.prs.some((pr2) => pr2.state.toUpperCase() === "MERGED")) {
|
|
21846
|
+
return {
|
|
21847
|
+
action: "refuse",
|
|
21848
|
+
reason: "unmerged-pr",
|
|
21849
|
+
message: `'${input.branch}' has a pull request that never merged (#${input.prs.map((pr2) => pr2.number).join(", #")}). The remote branch is the only thing still holding that work. Re-run with --keep-remote for a local-only clean`
|
|
21850
|
+
};
|
|
21851
|
+
}
|
|
21852
|
+
return { action: "proceed" };
|
|
21853
|
+
}
|
|
21854
|
+
async function fetchLandBranchPrs(branch, repo) {
|
|
21855
|
+
try {
|
|
21856
|
+
const { stdout } = await execFileP2("gh", [
|
|
21857
|
+
"pr",
|
|
21858
|
+
"list",
|
|
21859
|
+
"--head",
|
|
21860
|
+
branch,
|
|
21861
|
+
"--state",
|
|
21862
|
+
"all",
|
|
21863
|
+
"--limit",
|
|
21864
|
+
"20",
|
|
21865
|
+
...repo ? ["--repo", repo] : [],
|
|
21866
|
+
"--json",
|
|
21867
|
+
"number,state"
|
|
21868
|
+
], { timeout: GH_TIMEOUT_MS });
|
|
21869
|
+
const parsed = JSON.parse(stdout.trim() || "null");
|
|
21870
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
21871
|
+
return parsed.filter((pr2) => Boolean(pr2) && typeof pr2 === "object" && typeof pr2.number === "number" && typeof pr2.state === "string").map((pr2) => ({ number: pr2.number, state: pr2.state }));
|
|
21872
|
+
} catch {
|
|
21873
|
+
return void 0;
|
|
21874
|
+
}
|
|
21875
|
+
}
|
|
21811
21876
|
async function fetchIssueTitle(repo, number) {
|
|
21812
21877
|
try {
|
|
21813
21878
|
const { stdout } = await execFileP2("gh", ["issue", "view", String(number), "--repo", repo, "--json", "title", "--jq", ".title"], { timeout: GH_TIMEOUT_MS });
|
|
@@ -21869,6 +21934,16 @@ function registerWorktreeCommands(program3) {
|
|
|
21869
21934
|
if (landDirty) {
|
|
21870
21935
|
return fail(`worktree land: refusing to land '${branch}' \u2014 uncommitted changes in ${wtPath}; commit, stash, or remove the worktree manually`, { code: ERROR_CODES.ERR_BAD_ENUM });
|
|
21871
21936
|
}
|
|
21937
|
+
const landRefs = decideWorktreeLandRefCleanup({
|
|
21938
|
+
branch,
|
|
21939
|
+
prs: await fetchLandBranchPrs(branch),
|
|
21940
|
+
keepRemote: Boolean(o.keepRemote)
|
|
21941
|
+
});
|
|
21942
|
+
if (landRefs.action === "refuse") {
|
|
21943
|
+
return fail(`worktree land: ${landRefs.message}`, { code: ERROR_CODES.ERR_BAD_ENUM });
|
|
21944
|
+
}
|
|
21945
|
+
const keepRemoteBranch = Boolean(o.keepRemote) || landRefs.action === "keep-remote";
|
|
21946
|
+
if (landRefs.action === "keep-remote") console.warn(`worktree land: ${landRefs.message}.`);
|
|
21872
21947
|
const report = [];
|
|
21873
21948
|
if (hasStage) {
|
|
21874
21949
|
try {
|
|
@@ -21888,8 +21963,10 @@ function registerWorktreeCommands(program3) {
|
|
|
21888
21963
|
status: removeOutcome.status === "removed" ? removeOutcome.recovery ? `done (${removeOutcome.recovery})` : "done" : `failed: ${removeOutcome.error ?? "lock held"} \u2014 run: git -C "${primaryCheckout}" worktree remove --force "${wtPath}"`
|
|
21889
21964
|
});
|
|
21890
21965
|
report.push(await bestEffortGit(["branch", "-D", branch], primaryCheckout, `delete local branch ${branch}`));
|
|
21891
|
-
if (!
|
|
21966
|
+
if (!keepRemoteBranch) {
|
|
21892
21967
|
report.push(await bestEffortGit(["push", o.remote, "--delete", branch], void 0, `delete remote branch ${branch}`, GH_MUTATION_TIMEOUT_MS));
|
|
21968
|
+
} else if (!o.keepRemote) {
|
|
21969
|
+
report.push({ step: `delete remote branch ${branch}`, status: `skipped: ${landRefs.action === "keep-remote" ? landRefs.reason : "kept"}` });
|
|
21893
21970
|
}
|
|
21894
21971
|
report.push(await bestEffortGit(["worktree", "prune"], primaryCheckout, "prune worktree metadata"));
|
|
21895
21972
|
const result = { dryRun: false, ...plan, ...o.keepRemote ? { keepRemote: true } : {}, report };
|
|
@@ -24401,34 +24478,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
24401
24478
|
const cli = checkCliVersion(cliInput, releasedNote);
|
|
24402
24479
|
if (cli) checks.push(cli);
|
|
24403
24480
|
}
|
|
24404
|
-
|
|
24405
|
-
const cacheStale = cacheProbe.stale.length > 0 || cacheProbe.staging.length > 0;
|
|
24406
|
-
if (applyEnv && deps.prunePluginCache && cacheStale) {
|
|
24407
|
-
const pruned = await deps.prunePluginCache();
|
|
24408
|
-
const removed = pruned.removed ?? [];
|
|
24409
|
-
const pruneEvidence = [
|
|
24410
|
-
`cached versions: ${cacheProbe.cached}`,
|
|
24411
|
-
`stale: ${cacheProbe.stale.length ? cacheProbe.stale.join(", ") : "(none)"}`,
|
|
24412
|
-
`orphaned staging dirs: ${cacheProbe.staging.length ? cacheProbe.staging.join(", ") : "(none)"}`,
|
|
24413
|
-
`prune: ${pruned.detail}`,
|
|
24414
|
-
...removed.map((r) => `removed: ${r}`)
|
|
24415
|
-
];
|
|
24416
|
-
checks.push(pruned.ok ? {
|
|
24417
|
-
ok: true,
|
|
24418
|
-
label: "plugin cache",
|
|
24419
|
-
detail: removed.length ? `pruned ${removed.length} stale entr${removed.length === 1 ? "y" : "ies"}` : "nothing to prune",
|
|
24420
|
-
verbose: pruneEvidence
|
|
24421
|
-
} : {
|
|
24422
|
-
ok: false,
|
|
24423
|
-
label: "plugin cache",
|
|
24424
|
-
// Same split as the other env heals (#3489): a contention skip is not this run's gap.
|
|
24425
|
-
...pruned.skipped ? { reportOnly: true } : {},
|
|
24426
|
-
fix: pruned.skipped ? `${pruned.detail} \u2014 another doctor on this machine is pruning it now; re-run once it finishes` : `prune failed (${pruned.detail}) \u2014 run \`mmi-cli plugin prune --apply\``,
|
|
24427
|
-
verbose: pruneEvidence
|
|
24428
|
-
});
|
|
24429
|
-
} else {
|
|
24430
|
-
checks.push(checkPluginCache(cacheProbe));
|
|
24431
|
-
}
|
|
24481
|
+
checks.push(checkPluginCache(deps.pluginCache()));
|
|
24432
24482
|
if (deps.sessionPayload) {
|
|
24433
24483
|
const payload = checkSessionPayload(deps.sessionPayload());
|
|
24434
24484
|
if (payload) checks.push(payload);
|
|
@@ -24847,32 +24897,6 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
24847
24897
|
stagingBytes: plan.stagingBytes
|
|
24848
24898
|
};
|
|
24849
24899
|
},
|
|
24850
|
-
// #3485 wave 2 item 11: the --apply reap, through the SAME plan + reaper `plugin prune --apply` drives.
|
|
24851
|
-
// The plan's own keep-policy refuses the running and newest versions, so this cannot delete what is
|
|
24852
|
-
// loaded. Under the #3489 env-heal lock: deleting cache dirs while another doctor is mid-reinstall is
|
|
24853
|
-
// the same machine-global hazard the lock exists for.
|
|
24854
|
-
prunePluginCache: () => withEnvHealLock("plugin cache prune", async () => {
|
|
24855
|
-
const plan = buildPluginCachePlan(
|
|
24856
|
-
(0, import_node_os9.homedir)(),
|
|
24857
|
-
runningPluginVersion(process.env, resolveClientVersion()),
|
|
24858
|
-
pluginCacheFsDeps((0, import_node_os9.homedir)(), directoryBytes),
|
|
24859
|
-
{ withBytes: true }
|
|
24860
|
-
);
|
|
24861
|
-
const result = applyPluginCachePlan(
|
|
24862
|
-
plan,
|
|
24863
|
-
(p) => (0, import_node_fs32.rmSync)(p, { recursive: true, force: true }),
|
|
24864
|
-
stagingApplyFsGuard((0, import_node_os9.homedir)())
|
|
24865
|
-
);
|
|
24866
|
-
const removed = [...result.removed, ...result.removedStaging ?? []];
|
|
24867
|
-
if (result.failed.length) {
|
|
24868
|
-
return {
|
|
24869
|
-
ok: false,
|
|
24870
|
-
removed,
|
|
24871
|
-
detail: `${removed.length} removed, ${result.failed.length} failed \u2014 ${result.failed.map((f) => `${f.version}: ${f.error}`).join("; ")}`
|
|
24872
|
-
};
|
|
24873
|
-
}
|
|
24874
|
-
return { ok: true, removed, detail: removed.length ? `removed ${removed.join(", ")}` : "nothing to prune" };
|
|
24875
|
-
}),
|
|
24876
24900
|
// #3008: the schedules-notebook drift probe, compacted for the check. Full doctor only — the gather in
|
|
24877
24901
|
// runDoctorClean skips this dep entirely on fast/banner/preflight runs.
|
|
24878
24902
|
schedulesNotebook: async () => {
|
|
@@ -26567,6 +26591,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
26567
26591
|
jsonParity(pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch; on no-ci repos run pr ci-policy / checks-wait first (#1432)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m)`).option("--preserve-worktree", "keep the local PR worktree/stage/branch for an active multi-issue batch (#1888)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012)")).action(async (number, o) => {
|
|
26568
26592
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
26569
26593
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
26594
|
+
const repoForPostCleanup = await resolveRepo(o.repo) ?? o.repo;
|
|
26570
26595
|
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+/);
|
|
26571
26596
|
const headIsProtected = isProtectedBranch(headRef);
|
|
26572
26597
|
const startingPath = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
@@ -26642,18 +26667,37 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
26642
26667
|
}
|
|
26643
26668
|
if (!ghPrMergeLocalBranchDeleteWarning(message)) throw e;
|
|
26644
26669
|
});
|
|
26645
|
-
|
|
26646
|
-
|
|
26647
|
-
|
|
26648
|
-
|
|
26649
|
-
|
|
26650
|
-
|
|
26651
|
-
|
|
26652
|
-
|
|
26653
|
-
|
|
26654
|
-
|
|
26655
|
-
|
|
26670
|
+
const stateRead = await readGhPrStateWithRetry(
|
|
26671
|
+
() => execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS2 }).then((r) => r.stdout)
|
|
26672
|
+
);
|
|
26673
|
+
const gate = decidePrMergeCleanupGate({
|
|
26674
|
+
state: stateRead.ok ? stateRead.state : void 0,
|
|
26675
|
+
stateError: stateRead.ok ? void 0 : stateRead.error,
|
|
26676
|
+
autoRequested: Boolean(o.auto || upgradedToAuto)
|
|
26677
|
+
});
|
|
26678
|
+
if (gate.action === "enqueued") {
|
|
26679
|
+
const state = stateRead.ok ? stateRead.state : "";
|
|
26680
|
+
const enqueued = describePrMergeEnqueuedReason(await pollGhPrChecks(number, repoArgs).catch(() => void 0));
|
|
26681
|
+
console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", enqueuedReason: enqueued.reason, pr: number, branch: headRef, state: state || "unknown", upgradedToAuto: upgradedToAuto || void 0 }));
|
|
26682
|
+
console.warn(`pr merge: PR #${number} is ENQUEUED, not merged \u2014 ${enqueued.message}.`);
|
|
26683
|
+
if (upgradedToAuto && !o.auto) {
|
|
26684
|
+
console.warn(`pr merge: exiting ${PR_MERGE_ENQUEUED_EXIT_CODE} so a chained command does not treat this as a completed merge.`);
|
|
26685
|
+
process.exitCode = PR_MERGE_ENQUEUED_EXIT_CODE;
|
|
26656
26686
|
}
|
|
26687
|
+
return;
|
|
26688
|
+
}
|
|
26689
|
+
if (gate.action === "refuse") {
|
|
26690
|
+
console.log(JSON.stringify({
|
|
26691
|
+
mergeStatus: "not-merged",
|
|
26692
|
+
reason: gate.reason,
|
|
26693
|
+
pr: number,
|
|
26694
|
+
branch: headRef,
|
|
26695
|
+
state: stateRead.ok ? stateRead.state : "unknown",
|
|
26696
|
+
cleanupStatus: "skipped"
|
|
26697
|
+
}));
|
|
26698
|
+
console.error(`pr merge: ${gate.message}. Nothing was deleted \u2014 re-check the PR and retry.`);
|
|
26699
|
+
process.exitCode = 1;
|
|
26700
|
+
return;
|
|
26657
26701
|
}
|
|
26658
26702
|
if (!remoteNotAttemptedReason) remoteDeleteAttempted = true;
|
|
26659
26703
|
const remoteBranch = await buildPrMergeRemoteBranchCleanupReport(headRef, {
|
|
@@ -26694,7 +26738,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
26694
26738
|
}
|
|
26695
26739
|
};
|
|
26696
26740
|
}
|
|
26697
|
-
const boardAdvance = await advanceClosedIssuesToDone2(number,
|
|
26741
|
+
const boardAdvance = await advanceClosedIssuesToDone2(number, repoForPostCleanup);
|
|
26698
26742
|
console.log(JSON.stringify({
|
|
26699
26743
|
...buildPrMergeResultPayload({
|
|
26700
26744
|
number,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.57.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",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "node build.mjs",
|
|
34
|
-
"test": "vitest run
|
|
34
|
+
"test": "vitest run",
|
|
35
35
|
"typecheck": "tsc --noEmit"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|