@mutmutco/cli 3.56.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 +110 -13
- 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 };
|
|
@@ -26514,6 +26591,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
26514
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) => {
|
|
26515
26592
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
26516
26593
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
26594
|
+
const repoForPostCleanup = await resolveRepo(o.repo) ?? o.repo;
|
|
26517
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+/);
|
|
26518
26596
|
const headIsProtected = isProtectedBranch(headRef);
|
|
26519
26597
|
const startingPath = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
@@ -26589,18 +26667,37 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
26589
26667
|
}
|
|
26590
26668
|
if (!ghPrMergeLocalBranchDeleteWarning(message)) throw e;
|
|
26591
26669
|
});
|
|
26592
|
-
|
|
26593
|
-
|
|
26594
|
-
|
|
26595
|
-
|
|
26596
|
-
|
|
26597
|
-
|
|
26598
|
-
|
|
26599
|
-
|
|
26600
|
-
|
|
26601
|
-
|
|
26602
|
-
|
|
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;
|
|
26603
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;
|
|
26604
26701
|
}
|
|
26605
26702
|
if (!remoteNotAttemptedReason) remoteDeleteAttempted = true;
|
|
26606
26703
|
const remoteBranch = await buildPrMergeRemoteBranchCleanupReport(headRef, {
|
|
@@ -26641,7 +26738,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
26641
26738
|
}
|
|
26642
26739
|
};
|
|
26643
26740
|
}
|
|
26644
|
-
const boardAdvance = await advanceClosedIssuesToDone2(number,
|
|
26741
|
+
const boardAdvance = await advanceClosedIssuesToDone2(number, repoForPostCleanup);
|
|
26645
26742
|
console.log(JSON.stringify({
|
|
26646
26743
|
...buildPrMergeResultPayload({
|
|
26647
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": {
|