@mutmutco/cli 3.85.0 → 3.86.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 +65 -19
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -6371,6 +6371,14 @@ var LOCAL_ONLY_FILES = [".claude/settings.local.json"];
|
|
|
6371
6371
|
var PKG = "package.json";
|
|
6372
6372
|
var LOCKFILE = "package-lock.json";
|
|
6373
6373
|
var NODE_MODULES = "node_modules";
|
|
6374
|
+
var LOCKFILE_INSTALLS = [
|
|
6375
|
+
{ lockfile: "bun.lock", command: "bun install --frozen-lockfile" },
|
|
6376
|
+
{ lockfile: "bun.lockb", command: "bun install --frozen-lockfile" },
|
|
6377
|
+
{ lockfile: "pnpm-lock.yaml", command: "pnpm install --frozen-lockfile" },
|
|
6378
|
+
{ lockfile: "yarn.lock", command: "yarn install --immutable" },
|
|
6379
|
+
{ lockfile: LOCKFILE, command: "npm ci" }
|
|
6380
|
+
];
|
|
6381
|
+
var NO_LOCKFILE_INSTALL = "npm install --no-package-lock";
|
|
6374
6382
|
var realFsProbe = {
|
|
6375
6383
|
isDir: (p) => {
|
|
6376
6384
|
try {
|
|
@@ -6397,18 +6405,19 @@ var realFsProbe = {
|
|
|
6397
6405
|
function scanInstallDirs(root, fs2 = realFsProbe) {
|
|
6398
6406
|
const factsFor = (dir) => {
|
|
6399
6407
|
const abs = dir ? (0, import_node_path10.join)(root, dir) : root;
|
|
6408
|
+
const match = LOCKFILE_INSTALLS.find((c) => fs2.isFile((0, import_node_path10.join)(abs, c.lockfile)));
|
|
6400
6409
|
return {
|
|
6401
6410
|
dir,
|
|
6402
6411
|
hasPackageJson: fs2.isFile((0, import_node_path10.join)(abs, PKG)),
|
|
6403
|
-
|
|
6404
|
-
|
|
6412
|
+
hasNodeModules: fs2.isDir((0, import_node_path10.join)(abs, NODE_MODULES)),
|
|
6413
|
+
install: match?.command
|
|
6405
6414
|
};
|
|
6406
6415
|
};
|
|
6407
6416
|
const children = fs2.listDirs(root).filter((name) => name !== NODE_MODULES && name !== ".git");
|
|
6408
6417
|
return [factsFor(""), ...children.map(factsFor).filter((f) => f.hasPackageJson)];
|
|
6409
6418
|
}
|
|
6410
6419
|
function npmInstallTargets(dirs) {
|
|
6411
|
-
return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.
|
|
6420
|
+
return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.install ?? NO_LOCKFILE_INSTALL }));
|
|
6412
6421
|
}
|
|
6413
6422
|
function isLinkedWorktree(root, fs2 = realFsProbe) {
|
|
6414
6423
|
return fs2.isFile((0, import_node_path10.join)(root, ".git"));
|
|
@@ -6432,9 +6441,9 @@ async function provisionWorktree(worktreeRoot, deps) {
|
|
|
6432
6441
|
const allDirs = scanInstallDirs(worktreeRoot, fs2);
|
|
6433
6442
|
const targets = npmInstallTargets(allDirs);
|
|
6434
6443
|
if (deps.validateInstall) {
|
|
6435
|
-
for (const dir of allDirs.filter((d) => d.hasPackageJson && d.
|
|
6444
|
+
for (const dir of allDirs.filter((d) => d.hasPackageJson && d.install && d.hasNodeModules)) {
|
|
6436
6445
|
const cwd = dir.dir ? (0, import_node_path10.join)(worktreeRoot, dir.dir) : worktreeRoot;
|
|
6437
|
-
if (!await deps.validateInstall(cwd)) targets.push({ dir: dir.dir, command:
|
|
6446
|
+
if (!await deps.validateInstall(cwd)) targets.push({ dir: dir.dir, command: dir.install });
|
|
6438
6447
|
}
|
|
6439
6448
|
}
|
|
6440
6449
|
const targetDirs = new Set(targets.map((target) => target.dir));
|
|
@@ -8485,20 +8494,30 @@ async function rollDevelopmentForward(deps, ctx, tag) {
|
|
|
8485
8494
|
const number = parsePrNumber(url);
|
|
8486
8495
|
return enqueueAlignmentAutoMerge(deps, ctx, number, url || void 0, `development requires checks (${required.join(", ")}); opened alignment PR ${url || "(url unavailable)"}`);
|
|
8487
8496
|
}
|
|
8497
|
+
var ALIGNMENT_ARM_ATTEMPTS = 3;
|
|
8498
|
+
var ALIGNMENT_ARM_BACKOFF_MS = 2e3;
|
|
8488
8499
|
async function enqueueAlignmentAutoMerge(deps, ctx, prNumber, prUrl, openedNote) {
|
|
8489
8500
|
const base = { status: "pr-pending", prNumber, prUrl };
|
|
8490
8501
|
const retryCommand = `mmi-cli pr merge ${prNumber ?? "<number>"} --auto --merge`;
|
|
8491
8502
|
const manual = `${openedNote} \u2014 land it with \`${retryCommand}\``;
|
|
8492
8503
|
if (deps.mergeAuto && prNumber !== void 0) {
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8504
|
+
const sleep3 = resolveSleep(deps);
|
|
8505
|
+
let lastError;
|
|
8506
|
+
for (let attempt = 1; attempt <= ALIGNMENT_ARM_ATTEMPTS; attempt += 1) {
|
|
8507
|
+
try {
|
|
8508
|
+
const res = await deps.mergeAuto(String(prNumber), ctx.repo);
|
|
8509
|
+
if (res.mergeStatus !== "failed") {
|
|
8510
|
+
return { ...base, autoMergeEnqueued: true, note: `${openedNote} \u2014 auto-merge enqueued (merges when checks pass)` };
|
|
8511
|
+
}
|
|
8512
|
+
lastError = res.error ?? "unknown error";
|
|
8513
|
+
} catch (e) {
|
|
8514
|
+
lastError = `enqueue threw (${e instanceof Error ? e.message : String(e)})`;
|
|
8497
8515
|
}
|
|
8498
|
-
|
|
8499
|
-
} catch (e) {
|
|
8500
|
-
deps.warn?.(`alignment PR #${prNumber} auto-merge enqueue threw (${e instanceof Error ? e.message : String(e)}) \u2014 re-run: ${retryCommand}`);
|
|
8516
|
+
if (attempt < ALIGNMENT_ARM_ATTEMPTS) await sleep3(ALIGNMENT_ARM_BACKOFF_MS * attempt);
|
|
8501
8517
|
}
|
|
8518
|
+
deps.warn?.(
|
|
8519
|
+
`alignment PR #${prNumber} auto-merge not armed after ${ALIGNMENT_ARM_ATTEMPTS} attempts (${lastError ?? "unknown error"}) \u2014 re-run: ${retryCommand}`
|
|
8520
|
+
);
|
|
8502
8521
|
}
|
|
8503
8522
|
return { ...base, note: manual };
|
|
8504
8523
|
}
|
|
@@ -8836,6 +8855,10 @@ async function enumerateOwnWorkflowRuns(deps, repo, headSha) {
|
|
|
8836
8855
|
const runs = await discoverShaWorkflowRuns(deps, repo, headSha, /* @__PURE__ */ new Set());
|
|
8837
8856
|
return runs.length ? runs : [{ workflow: `sha-enumeration(${headSha.slice(0, 7)}) no runs yet`, conclusion: "pending" }];
|
|
8838
8857
|
}
|
|
8858
|
+
function withReleaseRunObligation(runs) {
|
|
8859
|
+
if (runs.some((r) => r.event === "release")) return runs;
|
|
8860
|
+
return [...runs, { workflow: "release-event deploy run not created yet", conclusion: "pending" }];
|
|
8861
|
+
}
|
|
8839
8862
|
var NON_DEPLOY_EVENTS = /* @__PURE__ */ new Set([
|
|
8840
8863
|
"pull_request",
|
|
8841
8864
|
"pull_request_target",
|
|
@@ -8885,6 +8908,7 @@ async function discoverShaWorkflowRuns(deps, repo, headSha, seenRunIds) {
|
|
|
8885
8908
|
workflow: row.workflowName ?? `run:${row.databaseId}`,
|
|
8886
8909
|
runId: row.databaseId,
|
|
8887
8910
|
runUrl: row.url,
|
|
8911
|
+
event: row.event,
|
|
8888
8912
|
// An in-flight run is reported UNVERIFIED, never watched. Blocking on an arbitrary discovered run
|
|
8889
8913
|
// would let one long push workflow hang the release command and mask the failed rows behind it, and
|
|
8890
8914
|
// `gh run watch` has no timeout. The NAMED targets are the ones worth blocking on (also #3382 review).
|
|
@@ -8914,7 +8938,7 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
|
|
|
8914
8938
|
const note = ref === "rc" ? "no dispatch on rc: registry-publish repos have no rc-stage Release to publish from" : "no central dispatch: this repo's own publish.yml auto-fires on the published Release (#2428)";
|
|
8915
8939
|
if (ref === "rc" || !autoRunHeadSha) return { note, deployStatus: "pending" };
|
|
8916
8940
|
if (!watch) {
|
|
8917
|
-
const listed = await enumerateOwnWorkflowRuns(deps, ctx.repo, autoRunHeadSha);
|
|
8941
|
+
const listed = withReleaseRunObligation(await enumerateOwnWorkflowRuns(deps, ctx.repo, autoRunHeadSha));
|
|
8918
8942
|
return { note, workflowRuns: listed, deployStatus: aggregateWorkflowRuns(listed) };
|
|
8919
8943
|
}
|
|
8920
8944
|
const since = autoRunSince ?? (deps.now ?? Date.now)();
|
|
@@ -8934,7 +8958,7 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
|
|
|
8934
8958
|
if (!autoRunHeadSha) return { note, deployStatus: "pending" };
|
|
8935
8959
|
if (!watch) {
|
|
8936
8960
|
if (ref === "rc") return { note, deployStatus: "pending" };
|
|
8937
|
-
const listed = await enumerateOwnWorkflowRuns(deps, HUB_REPO, autoRunHeadSha);
|
|
8961
|
+
const listed = withReleaseRunObligation(await enumerateOwnWorkflowRuns(deps, HUB_REPO, autoRunHeadSha));
|
|
8938
8962
|
return { note, workflowRuns: listed, deployStatus: aggregateWorkflowRuns(listed) };
|
|
8939
8963
|
}
|
|
8940
8964
|
const since = autoRunSince ?? (deps.now ?? Date.now)();
|
|
@@ -27444,9 +27468,17 @@ function findNegatedClosings(text, closingNumbers) {
|
|
|
27444
27468
|
return forIssue.length > 0 && forIssue.every((m) => m.negated);
|
|
27445
27469
|
});
|
|
27446
27470
|
}
|
|
27471
|
+
function commitMessagesText(commits) {
|
|
27472
|
+
if (!Array.isArray(commits)) return "";
|
|
27473
|
+
return commits.map((commit) => {
|
|
27474
|
+
const { messageHeadline, messageBody } = commit ?? {};
|
|
27475
|
+
return `${typeof messageHeadline === "string" ? messageHeadline : ""}
|
|
27476
|
+
${typeof messageBody === "string" ? messageBody : ""}`;
|
|
27477
|
+
}).join("\n");
|
|
27478
|
+
}
|
|
27447
27479
|
function parseClosingGuardInput(raw) {
|
|
27448
27480
|
if (!raw || typeof raw !== "object") return void 0;
|
|
27449
|
-
const { title, body, state, closingIssuesReferences } = raw;
|
|
27481
|
+
const { title, body, state, closingIssuesReferences, commits } = raw;
|
|
27450
27482
|
if (typeof state !== "string" || !Array.isArray(closingIssuesReferences)) return void 0;
|
|
27451
27483
|
const closing = [];
|
|
27452
27484
|
for (const ref of closingIssuesReferences) {
|
|
@@ -27454,15 +27486,20 @@ function parseClosingGuardInput(raw) {
|
|
|
27454
27486
|
if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) return void 0;
|
|
27455
27487
|
closing.push(n);
|
|
27456
27488
|
}
|
|
27489
|
+
const commitClosing = [...new Set(findClosingMentions(commitMessagesText(commits)).map((m) => m.issue))];
|
|
27457
27490
|
const text = `${typeof title === "string" ? title : ""}
|
|
27458
27491
|
${typeof body === "string" ? body : ""}`;
|
|
27459
|
-
return { state, text, closing };
|
|
27492
|
+
return { state, text, closing, commitClosing };
|
|
27460
27493
|
}
|
|
27461
27494
|
function negatedClosingRefusalMessage(negated, context = "pr merge") {
|
|
27462
27495
|
const named = negated.map((n) => `#${n}`).join(", ");
|
|
27463
27496
|
const first = `#${negated[0] ?? "N"}`;
|
|
27464
27497
|
return `${context}: REFUSED \u2014 GitHub will close ${named} on merge, but the PR body says it does not (GitHub's closing-keyword parser is negation-blind: "does not close ${first}" still closes it). Reword the body so the keyword is gone (e.g. "leaves ${first} open"), or re-run with --force to merge anyway and let ${named} close.`;
|
|
27465
27498
|
}
|
|
27499
|
+
function commitClosingRefusalMessage(closing, context = "pr merge") {
|
|
27500
|
+
const named = closing.map((n) => `#${n}`).join(", ");
|
|
27501
|
+
return `${context}: REFUSED \u2014 commit messages will close ${named} through the squash body, but GitHub omitted ${named} from closingIssuesReferences. Remove the closing keyword from the commit message, or re-run with --force to merge anyway and let ${named} close.`;
|
|
27502
|
+
}
|
|
27466
27503
|
function evaluateClosingGuard(input, opts) {
|
|
27467
27504
|
if (!input) {
|
|
27468
27505
|
return {
|
|
@@ -27470,7 +27507,16 @@ function evaluateClosingGuard(input, opts) {
|
|
|
27470
27507
|
message: `${opts.context}: could not read the PR's closing references \u2014 proceeding WITHOUT the negated-closing check (#3718).`
|
|
27471
27508
|
};
|
|
27472
27509
|
}
|
|
27473
|
-
if (input.state !== "OPEN"
|
|
27510
|
+
if (input.state !== "OPEN") return { blocked: false };
|
|
27511
|
+
const commitClosing = (input.commitClosing ?? []).filter((n) => !input.closing.includes(n));
|
|
27512
|
+
if (commitClosing.length) {
|
|
27513
|
+
if (!opts.force) return { blocked: true, message: commitClosingRefusalMessage(commitClosing, opts.context) };
|
|
27514
|
+
return {
|
|
27515
|
+
blocked: false,
|
|
27516
|
+
message: `${opts.context}: --force past the commit-message closing guard \u2014 the squash body will close ${commitClosing.map((n) => `#${n}`).join(", ")}.`
|
|
27517
|
+
};
|
|
27518
|
+
}
|
|
27519
|
+
if (input.closing.length === 0) return { blocked: false };
|
|
27474
27520
|
const negated = findNegatedClosings(input.text, input.closing);
|
|
27475
27521
|
if (!negated.length) return { blocked: false };
|
|
27476
27522
|
if (!opts.force) return { blocked: true, message: negatedClosingRefusalMessage(negated, opts.context) };
|
|
@@ -31262,7 +31308,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
31262
31308
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
31263
31309
|
const startingPath = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
31264
31310
|
assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr land", { force: o.force });
|
|
31265
|
-
const landClosingGuardRaw = await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "title,body,state,closingIssuesReferences"], { timeout: GC_GH_TIMEOUT_MS2 }).then((r) => JSON.parse(r.stdout)).catch(() => void 0);
|
|
31311
|
+
const landClosingGuardRaw = await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "title,body,state,closingIssuesReferences,commits"], { timeout: GC_GH_TIMEOUT_MS2 }).then((r) => JSON.parse(r.stdout)).catch(() => void 0);
|
|
31266
31312
|
const landClosingGuardVerdict = evaluateClosingGuard(parseClosingGuardInput(landClosingGuardRaw), { force: o.force, context: "pr land" });
|
|
31267
31313
|
if (landClosingGuardVerdict.blocked) {
|
|
31268
31314
|
console.error(landClosingGuardVerdict.message);
|
|
@@ -31380,7 +31426,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
31380
31426
|
const repoForPostCleanup = await resolveRepo(o.repo) ?? o.repo;
|
|
31381
31427
|
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+/);
|
|
31382
31428
|
const headIsProtected = isProtectedBranch(headRef);
|
|
31383
|
-
const closingGuardRaw = await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "title,body,state,closingIssuesReferences"], { timeout: GC_GH_TIMEOUT_MS2 }).then((r) => JSON.parse(r.stdout)).catch(() => void 0);
|
|
31429
|
+
const closingGuardRaw = await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "title,body,state,closingIssuesReferences,commits"], { timeout: GC_GH_TIMEOUT_MS2 }).then((r) => JSON.parse(r.stdout)).catch(() => void 0);
|
|
31384
31430
|
const closingGuardVerdict = evaluateClosingGuard(parseClosingGuardInput(closingGuardRaw), { force: o.force, context: "pr merge" });
|
|
31385
31431
|
if (closingGuardVerdict.blocked) {
|
|
31386
31432
|
console.error(closingGuardVerdict.message);
|
package/package.json
CHANGED