@mutmutco/cli 3.85.0 → 3.87.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 +70 -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)();
|
|
@@ -23492,6 +23516,11 @@ function registerBootstrapCommands(program3) {
|
|
|
23492
23516
|
if (!seed.classes.includes(o.class)) continue;
|
|
23493
23517
|
if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
|
|
23494
23518
|
if (!seedMatchesProjectType(seed, applyProjectType)) continue;
|
|
23519
|
+
const waiver = seed.waivers?.[slug];
|
|
23520
|
+
if (waiver) {
|
|
23521
|
+
actions.push({ action: "skip", target: seed.target, ownership: seed.ownership, reason: `waived: ${waiver}` });
|
|
23522
|
+
continue;
|
|
23523
|
+
}
|
|
23495
23524
|
const resolved = { ...seed, target: seed.target.replace("{{REPO_SLUG}}", slug) };
|
|
23496
23525
|
let exists = false;
|
|
23497
23526
|
let sha;
|
|
@@ -27444,9 +27473,17 @@ function findNegatedClosings(text, closingNumbers) {
|
|
|
27444
27473
|
return forIssue.length > 0 && forIssue.every((m) => m.negated);
|
|
27445
27474
|
});
|
|
27446
27475
|
}
|
|
27476
|
+
function commitMessagesText(commits) {
|
|
27477
|
+
if (!Array.isArray(commits)) return "";
|
|
27478
|
+
return commits.map((commit) => {
|
|
27479
|
+
const { messageHeadline, messageBody } = commit ?? {};
|
|
27480
|
+
return `${typeof messageHeadline === "string" ? messageHeadline : ""}
|
|
27481
|
+
${typeof messageBody === "string" ? messageBody : ""}`;
|
|
27482
|
+
}).join("\n");
|
|
27483
|
+
}
|
|
27447
27484
|
function parseClosingGuardInput(raw) {
|
|
27448
27485
|
if (!raw || typeof raw !== "object") return void 0;
|
|
27449
|
-
const { title, body, state, closingIssuesReferences } = raw;
|
|
27486
|
+
const { title, body, state, closingIssuesReferences, commits } = raw;
|
|
27450
27487
|
if (typeof state !== "string" || !Array.isArray(closingIssuesReferences)) return void 0;
|
|
27451
27488
|
const closing = [];
|
|
27452
27489
|
for (const ref of closingIssuesReferences) {
|
|
@@ -27454,15 +27491,20 @@ function parseClosingGuardInput(raw) {
|
|
|
27454
27491
|
if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) return void 0;
|
|
27455
27492
|
closing.push(n);
|
|
27456
27493
|
}
|
|
27494
|
+
const commitClosing = [...new Set(findClosingMentions(commitMessagesText(commits)).map((m) => m.issue))];
|
|
27457
27495
|
const text = `${typeof title === "string" ? title : ""}
|
|
27458
27496
|
${typeof body === "string" ? body : ""}`;
|
|
27459
|
-
return { state, text, closing };
|
|
27497
|
+
return { state, text, closing, commitClosing };
|
|
27460
27498
|
}
|
|
27461
27499
|
function negatedClosingRefusalMessage(negated, context = "pr merge") {
|
|
27462
27500
|
const named = negated.map((n) => `#${n}`).join(", ");
|
|
27463
27501
|
const first = `#${negated[0] ?? "N"}`;
|
|
27464
27502
|
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
27503
|
}
|
|
27504
|
+
function commitClosingRefusalMessage(closing, context = "pr merge") {
|
|
27505
|
+
const named = closing.map((n) => `#${n}`).join(", ");
|
|
27506
|
+
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.`;
|
|
27507
|
+
}
|
|
27466
27508
|
function evaluateClosingGuard(input, opts) {
|
|
27467
27509
|
if (!input) {
|
|
27468
27510
|
return {
|
|
@@ -27470,7 +27512,16 @@ function evaluateClosingGuard(input, opts) {
|
|
|
27470
27512
|
message: `${opts.context}: could not read the PR's closing references \u2014 proceeding WITHOUT the negated-closing check (#3718).`
|
|
27471
27513
|
};
|
|
27472
27514
|
}
|
|
27473
|
-
if (input.state !== "OPEN"
|
|
27515
|
+
if (input.state !== "OPEN") return { blocked: false };
|
|
27516
|
+
const commitClosing = (input.commitClosing ?? []).filter((n) => !input.closing.includes(n));
|
|
27517
|
+
if (commitClosing.length) {
|
|
27518
|
+
if (!opts.force) return { blocked: true, message: commitClosingRefusalMessage(commitClosing, opts.context) };
|
|
27519
|
+
return {
|
|
27520
|
+
blocked: false,
|
|
27521
|
+
message: `${opts.context}: --force past the commit-message closing guard \u2014 the squash body will close ${commitClosing.map((n) => `#${n}`).join(", ")}.`
|
|
27522
|
+
};
|
|
27523
|
+
}
|
|
27524
|
+
if (input.closing.length === 0) return { blocked: false };
|
|
27474
27525
|
const negated = findNegatedClosings(input.text, input.closing);
|
|
27475
27526
|
if (!negated.length) return { blocked: false };
|
|
27476
27527
|
if (!opts.force) return { blocked: true, message: negatedClosingRefusalMessage(negated, opts.context) };
|
|
@@ -31262,7 +31313,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
31262
31313
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
31263
31314
|
const startingPath = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
31264
31315
|
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);
|
|
31316
|
+
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
31317
|
const landClosingGuardVerdict = evaluateClosingGuard(parseClosingGuardInput(landClosingGuardRaw), { force: o.force, context: "pr land" });
|
|
31267
31318
|
if (landClosingGuardVerdict.blocked) {
|
|
31268
31319
|
console.error(landClosingGuardVerdict.message);
|
|
@@ -31380,7 +31431,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
31380
31431
|
const repoForPostCleanup = await resolveRepo(o.repo) ?? o.repo;
|
|
31381
31432
|
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
31433
|
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);
|
|
31434
|
+
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
31435
|
const closingGuardVerdict = evaluateClosingGuard(parseClosingGuardInput(closingGuardRaw), { force: o.force, context: "pr merge" });
|
|
31385
31436
|
if (closingGuardVerdict.blocked) {
|
|
31386
31437
|
console.error(closingGuardVerdict.message);
|
package/package.json
CHANGED