@mutmutco/cli 3.118.0 → 3.120.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 +245 -28
- package/package.json +2 -2
package/dist/main.cjs
CHANGED
|
@@ -7411,6 +7411,7 @@ async function removeWorktreeWithRecovery(wtPath, deps) {
|
|
|
7411
7411
|
const backoff = deps.backoffMs ?? [250, 1e3];
|
|
7412
7412
|
let attempts = 0;
|
|
7413
7413
|
let lastError;
|
|
7414
|
+
const stillOnDisk = () => deps.pathExists?.(wtPath) ?? false;
|
|
7414
7415
|
let safeForGitPrimary = true;
|
|
7415
7416
|
if (deps.detachReparsePoints) {
|
|
7416
7417
|
try {
|
|
@@ -7424,7 +7425,9 @@ async function removeWorktreeWithRecovery(wtPath, deps) {
|
|
|
7424
7425
|
attempts++;
|
|
7425
7426
|
try {
|
|
7426
7427
|
await deps.git(["worktree", "remove", "--force", wtPath]);
|
|
7427
|
-
return { status: "removed", attempts, recovery: i > 0 ? "retry" : void 0 };
|
|
7428
|
+
if (!stillOnDisk()) return { status: "removed", attempts, recovery: i > 0 ? "retry" : void 0 };
|
|
7429
|
+
lastError = new Error(`worktree directory still present after 'git worktree remove --force': ${wtPath}`);
|
|
7430
|
+
break;
|
|
7428
7431
|
} catch (e) {
|
|
7429
7432
|
lastError = e;
|
|
7430
7433
|
const retriesLeft = i < maxAttempts - 1;
|
|
@@ -7439,12 +7442,18 @@ async function removeWorktreeWithRecovery(wtPath, deps) {
|
|
|
7439
7442
|
try {
|
|
7440
7443
|
await deps.removeWorktreeDir(wtPath);
|
|
7441
7444
|
await deps.git(["worktree", "prune"]).catch(() => "");
|
|
7442
|
-
return { status: "removed", attempts, recovery: "fallback" };
|
|
7445
|
+
if (!stillOnDisk()) return { status: "removed", attempts, recovery: "fallback" };
|
|
7446
|
+
lastError = new Error(`worktree directory still present after on-disk removal: ${wtPath}`);
|
|
7443
7447
|
} catch (fallbackError) {
|
|
7444
7448
|
lastError = fallbackError;
|
|
7445
7449
|
}
|
|
7446
7450
|
}
|
|
7447
|
-
return {
|
|
7451
|
+
return {
|
|
7452
|
+
status: "failed",
|
|
7453
|
+
attempts,
|
|
7454
|
+
error: errorMessage(lastError),
|
|
7455
|
+
...stillOnDisk() ? { remainsOnDisk: true } : {}
|
|
7456
|
+
};
|
|
7448
7457
|
}
|
|
7449
7458
|
var NO_NESTED_LINKS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
7450
7459
|
function baseName(p) {
|
|
@@ -8282,7 +8291,9 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
8282
8291
|
sleep: options.sleep ?? defaultSleep,
|
|
8283
8292
|
detachReparsePoints: options.detachReparsePoints,
|
|
8284
8293
|
// #3064: junction-safe deferred sweep too
|
|
8285
|
-
removeWorktreeDir: options.removeWorktreeDir
|
|
8294
|
+
removeWorktreeDir: options.removeWorktreeDir,
|
|
8295
|
+
pathExists: options.pathExists
|
|
8296
|
+
// #4850: a swept entry is only cleared once its directory is gone
|
|
8286
8297
|
};
|
|
8287
8298
|
await sweepDeferredWorktrees(options.deferredStore, removeDeps, options.removalContext).catch(() => void 0);
|
|
8288
8299
|
let stageTeardown;
|
|
@@ -8326,7 +8337,9 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
8326
8337
|
sleep: options.sleep ?? defaultSleep,
|
|
8327
8338
|
detachReparsePoints: options.detachReparsePoints,
|
|
8328
8339
|
// #3064: reach the actual pr-merge teardown path
|
|
8329
|
-
removeWorktreeDir: options.removeWorktreeDir
|
|
8340
|
+
removeWorktreeDir: options.removeWorktreeDir,
|
|
8341
|
+
pathExists: options.pathExists
|
|
8342
|
+
// #4850: never report `removed` while the directory survives
|
|
8330
8343
|
});
|
|
8331
8344
|
if (options.removalContext) {
|
|
8332
8345
|
recordWorktreeRemoval(options.removalContext.primaryRoot, {
|
|
@@ -9263,6 +9276,9 @@ function resolveWorktreeBase(from, remote) {
|
|
|
9263
9276
|
if (SHA_LIKE_RE.test(from)) return { base: from };
|
|
9264
9277
|
return { base: from, fetchBranch: from, preferRemote: `${remotePrefix}${from}` };
|
|
9265
9278
|
}
|
|
9279
|
+
function worktreeAddGitPrefix() {
|
|
9280
|
+
return process.platform === "win32" ? ["-c", "core.longpaths=true"] : [];
|
|
9281
|
+
}
|
|
9266
9282
|
var GIT_CONFIG_LOCK_RE = /could not lock config file|unable to write upstream branch configuration|unable to access ['"]?\.git\/config['"]?: Permission denied|unknown error occurred while reading the configuration files/i;
|
|
9267
9283
|
function isGitConfigLockError(error) {
|
|
9268
9284
|
return GIT_CONFIG_LOCK_RE.test(error instanceof Error ? error.message : String(error));
|
|
@@ -9304,6 +9320,8 @@ async function addWorktreeRobust(wtPath, branch, base, deps) {
|
|
|
9304
9320
|
await deps.git(["worktree", "add", wtPath, "-b", branch, base]);
|
|
9305
9321
|
return;
|
|
9306
9322
|
} catch (e) {
|
|
9323
|
+
await deps.cleanupPartial?.().catch(() => {
|
|
9324
|
+
});
|
|
9307
9325
|
await cleanupOrphanBranch(branch, base, preExistingOid, deps).catch(() => {
|
|
9308
9326
|
});
|
|
9309
9327
|
const retriesLeft = i < maxAttempts - 1;
|
|
@@ -9967,6 +9985,13 @@ var MANAGED_GITIGNORE_LINES = [
|
|
|
9967
9985
|
"docs/superpowers/",
|
|
9968
9986
|
".playwright-mcp/",
|
|
9969
9987
|
".claude/worktrees/",
|
|
9988
|
+
// `.mmi/` is org session scratch (session id, throttle traces, saga queue — AGENTS.md "No committed
|
|
9989
|
+
// `.mmi`"). #2319 retired this rule believing repo-local `.mmi` state was gone; #4833 proved repos
|
|
9990
|
+
// still get `.mmi/` written (older tooling on the machine, product-repo scripts), and dropping the
|
|
9991
|
+
// rule left that scratch untracked-visible one `git add .` away from a commit. Ignore the dir's
|
|
9992
|
+
// contents at ANY depth; an already-tracked `.mmi/config.json` keeps tracking regardless (git never
|
|
9993
|
+
// untracks a tracked file), so no re-inclusion line is needed.
|
|
9994
|
+
"**/.mmi/*",
|
|
9970
9995
|
// #3425: local agent config the org push wall (`mmi-no-agent-files-org`) forbids committing, at any
|
|
9971
9996
|
// depth — leaving it untracked is the ONLY legal state, so it is pure `git status` noise. Only the two
|
|
9972
9997
|
// dirs no repo has cause to track: `.claude/` is excluded because MMI-Hub tracks `.claude/settings.json`,
|
|
@@ -15820,10 +15845,10 @@ var rollout_plan_default = {
|
|
|
15820
15845
|
note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
|
|
15821
15846
|
},
|
|
15822
15847
|
baseline: {
|
|
15823
|
-
version: "3.
|
|
15824
|
-
tag: "v3.
|
|
15825
|
-
commit: "
|
|
15826
|
-
npm: "@mutmutco/cli@3.
|
|
15848
|
+
version: "3.120.0",
|
|
15849
|
+
tag: "v3.120.0",
|
|
15850
|
+
commit: "5e8fd4eaab15",
|
|
15851
|
+
npm: "@mutmutco/cli@3.120.0"
|
|
15827
15852
|
},
|
|
15828
15853
|
exitCriterion: "fleet-n-of-n",
|
|
15829
15854
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15840,14 +15865,14 @@ var rollout_plan_default = {
|
|
|
15840
15865
|
repo: "mutmutco/mmi-hub",
|
|
15841
15866
|
role: "canary",
|
|
15842
15867
|
schedule: "train",
|
|
15843
|
-
v3Target: "v3.
|
|
15868
|
+
v3Target: "v3.120.0"
|
|
15844
15869
|
}
|
|
15845
15870
|
],
|
|
15846
15871
|
rollbackTrigger: "Any red inside the post-cut soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a v3 client refused while the compat window must still admit it (SUPPORTED_MINOR_WINDOW=2, MIN_CLIENT_VERSION 0.0.0 \u2014 D6a), or npm consumer install/doctor failure on the v4 dist.",
|
|
15847
15872
|
rollback: {
|
|
15848
15873
|
independent: true,
|
|
15849
|
-
mechanism: "npm dist-tag latest -> 3.
|
|
15850
|
-
v3Target: "v3.
|
|
15874
|
+
mechanism: "npm dist-tag latest -> 3.120.0 and redeploy the Hub Lambda from tag v3.120.0 (5e8fd4eaab15); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
15875
|
+
v3Target: "v3.120.0 (@mutmutco/cli@3.120.0, tag commit 5e8fd4eaab15 \u2014 the preserved latest-v3 distribution, D6b)"
|
|
15851
15876
|
}
|
|
15852
15877
|
},
|
|
15853
15878
|
{
|
|
@@ -24657,6 +24682,7 @@ function buildCommand(cmd, path2) {
|
|
|
24657
24682
|
path: path2,
|
|
24658
24683
|
...aliases.length ? { aliases: [...aliases] } : {},
|
|
24659
24684
|
house,
|
|
24685
|
+
canonical: canonicalPathFor(path2) ?? path2,
|
|
24660
24686
|
arguments: cmd.registeredArguments.map(buildArgument),
|
|
24661
24687
|
// A hand-parsed command registers no Commander options, so its declared set is merged in (#3682).
|
|
24662
24688
|
options: [...cmd.options.map(buildOption), ...readDeclaredOptions(cmd)],
|
|
@@ -24695,7 +24721,7 @@ function collectLeaves(node, acc) {
|
|
|
24695
24721
|
function buildHouses(tree) {
|
|
24696
24722
|
const collect = (node, parentHouse, root, out) => {
|
|
24697
24723
|
if (node.house === root && parentHouse !== root) {
|
|
24698
|
-
const entry = { path: node.path };
|
|
24724
|
+
const entry = { path: node.path, canonical: node.canonical };
|
|
24699
24725
|
if (node.description) entry.description = node.description;
|
|
24700
24726
|
out.push(entry);
|
|
24701
24727
|
}
|
|
@@ -32881,7 +32907,7 @@ var defaultLeaseCloseExec = (_cmd, args) => execJervCli(args, { timeout: GIT_TIM
|
|
|
32881
32907
|
async function bestEffortLeaseClose(wtPath, exec = defaultLeaseCloseExec) {
|
|
32882
32908
|
const step = "close jerv worktree lease";
|
|
32883
32909
|
try {
|
|
32884
|
-
await exec("jerv-cli", ["lease", "close", "--ref", wtPath]);
|
|
32910
|
+
await exec("jerv-cli", ["cli", "lease", "close", "--ref", wtPath]);
|
|
32885
32911
|
return { step, status: "done" };
|
|
32886
32912
|
} catch (e) {
|
|
32887
32913
|
const err = e;
|
|
@@ -33255,7 +33281,9 @@ function worktreeRemoveDeps(execGit) {
|
|
|
33255
33281
|
// #3064: unlink any reparse point (esp. a `node_modules` junction to base) before the git-native
|
|
33256
33282
|
// `worktree remove --force`, which would otherwise recurse through it and empty the base checkout.
|
|
33257
33283
|
detachReparsePoints: (worktreePath) => detachReparsePoints(worktreePath, realWorktreeDirRemover),
|
|
33258
|
-
removeWorktreeDir: async (worktreePath) => removeWorktreeTree(worktreePath, await resolvePrimaryCheckout(execGit), realWorktreeDirRemover)
|
|
33284
|
+
removeWorktreeDir: async (worktreePath) => removeWorktreeTree(worktreePath, await resolvePrimaryCheckout(execGit), realWorktreeDirRemover),
|
|
33285
|
+
// #4850: verify the directory is actually gone before any caller reports completion.
|
|
33286
|
+
pathExists: (worktreePath) => (0, import_node_fs39.existsSync)(worktreePath)
|
|
33259
33287
|
};
|
|
33260
33288
|
}
|
|
33261
33289
|
async function worktreeHasStageState(worktreePath) {
|
|
@@ -34392,7 +34420,7 @@ function registerWorktreeCommands(program3) {
|
|
|
34392
34420
|
);
|
|
34393
34421
|
report.push({
|
|
34394
34422
|
step: "remove worktree",
|
|
34395
|
-
status: removeOutcome.status === "removed" ? removeOutcome.recovery ? `done (${removeOutcome.recovery})` : "done" : `failed: ${removeOutcome.error ?? "lock held"} \u2014 run: git -C "${primaryCheckout}" worktree remove --force "${wtPath}"`
|
|
34423
|
+
status: removeOutcome.status === "removed" ? removeOutcome.recovery ? `done (${removeOutcome.recovery})` : "done" : removeOutcome.remainsOnDisk ? `failed: ${removeOutcome.error ?? "lock held"} \u2014 the directory "${wtPath}" is still on disk; cd every shell out of it (a cwd inside holds it open on Windows), then delete that directory` : `failed: ${removeOutcome.error ?? "lock held"} \u2014 run: git -C "${primaryCheckout}" worktree remove --force "${wtPath}"`
|
|
34396
34424
|
});
|
|
34397
34425
|
if (!shouldContinueLandCleanup(removeOutcome.status)) {
|
|
34398
34426
|
const result2 = {
|
|
@@ -35787,8 +35815,9 @@ function formatExplainCommand(cmd, rootName) {
|
|
|
35787
35815
|
return lines.join("\n").trimEnd();
|
|
35788
35816
|
}
|
|
35789
35817
|
function formatExplainGroup(cmd, rootName) {
|
|
35818
|
+
const canonical = (node) => canonicalPathFor(node.path) ?? node.path;
|
|
35790
35819
|
const lines = [
|
|
35791
|
-
`${rootName} ${cmd
|
|
35820
|
+
`${rootName} ${canonical(cmd)}${cmd.description ? ` \u2014 ${cmd.description}` : ""}`,
|
|
35792
35821
|
`Category: ${cmd.category} \xB7 Discovery: ${cmd.discovery}`,
|
|
35793
35822
|
"",
|
|
35794
35823
|
"Commands:"
|
|
@@ -35798,7 +35827,7 @@ function formatExplainGroup(cmd, rootName) {
|
|
|
35798
35827
|
const name = arg.variadic ? `${arg.name}...` : arg.name;
|
|
35799
35828
|
return arg.required ? `<${name}>` : `[${name}]`;
|
|
35800
35829
|
}).join(" ");
|
|
35801
|
-
lines.push(` ${child2
|
|
35830
|
+
lines.push(` ${canonical(child2)}${args ? ` ${args}` : ""}${child2.description ? ` \u2014 ${child2.description}` : ""}`);
|
|
35802
35831
|
}
|
|
35803
35832
|
return lines.join("\n");
|
|
35804
35833
|
}
|
|
@@ -35812,7 +35841,7 @@ function formatExplainLoop(playbook) {
|
|
|
35812
35841
|
}
|
|
35813
35842
|
function findCommandInManifest(manifest, commandPath3) {
|
|
35814
35843
|
const visit = (command) => {
|
|
35815
|
-
if (command.path === commandPath3) return command;
|
|
35844
|
+
if (command.path === commandPath3 || canonicalPathFor(command.path) === commandPath3) return command;
|
|
35816
35845
|
for (const child2 of command.subcommands) {
|
|
35817
35846
|
const found = visit(child2);
|
|
35818
35847
|
if (found) return found;
|
|
@@ -35863,6 +35892,31 @@ function markerCommentBody(body, markerId) {
|
|
|
35863
35892
|
return `${prCommentMarker(markerId)}
|
|
35864
35893
|
${body}`;
|
|
35865
35894
|
}
|
|
35895
|
+
var CHECK_LOG_TAIL_LINES = 60;
|
|
35896
|
+
var CHECK_LOG_MAX_BUFFER = 32 * 1024 * 1024;
|
|
35897
|
+
function parseCheckJobId(details) {
|
|
35898
|
+
const match = /\/job\/(\d+)\b/.exec(details ?? "");
|
|
35899
|
+
return match ? match[1] : null;
|
|
35900
|
+
}
|
|
35901
|
+
function failingCheckRows(rows) {
|
|
35902
|
+
return rows.filter((row) => row.status === "fail" || row.status === "failure");
|
|
35903
|
+
}
|
|
35904
|
+
function tailLines(text, max = CHECK_LOG_TAIL_LINES) {
|
|
35905
|
+
const lines = text.split(/\r?\n/).filter((line) => line.trim());
|
|
35906
|
+
return { tail: lines.slice(-max).join("\n"), truncated: lines.length > max };
|
|
35907
|
+
}
|
|
35908
|
+
async function attachFailureLogs(rows, fetchLog) {
|
|
35909
|
+
for (const row of failingCheckRows(rows)) {
|
|
35910
|
+
const jobId = parseCheckJobId(row.details);
|
|
35911
|
+
if (!jobId) continue;
|
|
35912
|
+
try {
|
|
35913
|
+
const { tail, truncated } = tailLines(await fetchLog(jobId));
|
|
35914
|
+
if (tail) row.log = { jobId, tail, truncated };
|
|
35915
|
+
} catch {
|
|
35916
|
+
}
|
|
35917
|
+
}
|
|
35918
|
+
return rows;
|
|
35919
|
+
}
|
|
35866
35920
|
function parsePrChecksTable(stdout) {
|
|
35867
35921
|
const lines = stdout.trim().split(/\r?\n/).filter((l) => l.trim());
|
|
35868
35922
|
if (!lines.length) return [];
|
|
@@ -36188,7 +36242,7 @@ function registerPrLifecycleCommands(program3) {
|
|
|
36188
36242
|
return failGraceful(`pr update-branch: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
36189
36243
|
}
|
|
36190
36244
|
});
|
|
36191
|
-
pr2.command("checks <number>").description("one-shot non-blocking check snapshot; --watch streams state with per-check failure log pointers").option("--json", "machine-readable output").option("--watch", "poll until success/failure, streaming one JSON line per poll").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (number, o) => {
|
|
36245
|
+
pr2.command("checks <number>").description("one-shot non-blocking check snapshot; --watch streams state with per-check failure log pointers").option("--json", "machine-readable output").option("--watch", "poll until success/failure, streaming one JSON line per poll").option("--logs", `fold the last ${CHECK_LOG_TAIL_LINES} log lines of every FAILING check into its row, so reading why CI is red needs no second tool (#4853)`).option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (number, o) => {
|
|
36192
36246
|
const n = assertPositiveInt("pr checks", number);
|
|
36193
36247
|
let repo;
|
|
36194
36248
|
try {
|
|
@@ -36197,6 +36251,15 @@ function registerPrLifecycleCommands(program3) {
|
|
|
36197
36251
|
return fail(`pr checks: ${e.message}`);
|
|
36198
36252
|
}
|
|
36199
36253
|
const repoArgs = ["--repo", repo];
|
|
36254
|
+
const fetchLog = async (jobId) => {
|
|
36255
|
+
const { stdout } = await execFileP2(
|
|
36256
|
+
"gh",
|
|
36257
|
+
["run", "view", ...repoArgs, "--job", jobId, "--log-failed"],
|
|
36258
|
+
{ timeout: GC_GH_TIMEOUT_MS4, maxBuffer: CHECK_LOG_MAX_BUFFER }
|
|
36259
|
+
);
|
|
36260
|
+
return stdout;
|
|
36261
|
+
};
|
|
36262
|
+
const withLogs = async (rows) => o.logs ? attachFailureLogs(rows, fetchLog) : rows;
|
|
36200
36263
|
const snap = async () => {
|
|
36201
36264
|
let stdout = "";
|
|
36202
36265
|
let stderr = "";
|
|
@@ -36216,7 +36279,7 @@ function registerPrLifecycleCommands(program3) {
|
|
|
36216
36279
|
try {
|
|
36217
36280
|
if (!o.watch) {
|
|
36218
36281
|
const { state, rows } = await snap();
|
|
36219
|
-
const output = { state, checks: rows };
|
|
36282
|
+
const output = { state, checks: await withLogs(rows) };
|
|
36220
36283
|
console.log(JSON.stringify(output));
|
|
36221
36284
|
if (state === "failure" || state === "failing") process.exitCode = 1;
|
|
36222
36285
|
return;
|
|
@@ -36225,7 +36288,8 @@ function registerPrLifecycleCommands(program3) {
|
|
|
36225
36288
|
let lastState = "";
|
|
36226
36289
|
while (Date.now() < deadline) {
|
|
36227
36290
|
const { state, rows } = await snap();
|
|
36228
|
-
const
|
|
36291
|
+
const checks = state === "failure" ? await withLogs(rows) : rows;
|
|
36292
|
+
const payload = { state, timestamp: (/* @__PURE__ */ new Date()).toISOString(), checks };
|
|
36229
36293
|
console.log(JSON.stringify(payload));
|
|
36230
36294
|
if (state === "success") return;
|
|
36231
36295
|
if (state === "failure") {
|
|
@@ -39225,6 +39289,93 @@ function hasRepoLocalWorktrees() {
|
|
|
39225
39289
|
return root !== null && (0, import_node_fs46.existsSync)((0, import_node_path43.join)(root, ".worktrees"));
|
|
39226
39290
|
}
|
|
39227
39291
|
|
|
39292
|
+
// src/cross-repo-filing-issue.ts
|
|
39293
|
+
async function discoverSameOrgIssueNumber(client, currentRepo, number) {
|
|
39294
|
+
const [owner] = currentRepo.split("/");
|
|
39295
|
+
if (!owner) throw new Error(`cannot derive an organization from ${currentRepo}`);
|
|
39296
|
+
const repositories = await client.restPaginate(`orgs/${owner}/repos?type=all`);
|
|
39297
|
+
const hits = [];
|
|
39298
|
+
let next = 0;
|
|
39299
|
+
const worker = async () => {
|
|
39300
|
+
while (next < repositories.length) {
|
|
39301
|
+
const repo = repositories[next++]?.full_name;
|
|
39302
|
+
if (!repo || repo.toLowerCase() === currentRepo.toLowerCase()) continue;
|
|
39303
|
+
const issue2 = await readExactIssue(client, repo, number);
|
|
39304
|
+
if (issue2) hits.push(issue2);
|
|
39305
|
+
}
|
|
39306
|
+
};
|
|
39307
|
+
await Promise.all(Array.from({ length: Math.min(8, repositories.length) }, () => worker()));
|
|
39308
|
+
return hits.sort((left, right) => left.repo.localeCompare(right.repo));
|
|
39309
|
+
}
|
|
39310
|
+
async function readExactIssue(client, repo, number) {
|
|
39311
|
+
try {
|
|
39312
|
+
const issue2 = await client.rest("GET", `repos/${repo}/issues/${number}`);
|
|
39313
|
+
if (issue2.pull_request || issue2.number !== number || !issue2.title || !issue2.html_url) return void 0;
|
|
39314
|
+
const state = issue2.state?.toUpperCase();
|
|
39315
|
+
if (state !== "OPEN" && state !== "CLOSED") return void 0;
|
|
39316
|
+
return { repo, number, title: issue2.title, url: issue2.html_url, state };
|
|
39317
|
+
} catch (e) {
|
|
39318
|
+
if (e instanceof GitHubApiError && e.status === 404) return void 0;
|
|
39319
|
+
throw e;
|
|
39320
|
+
}
|
|
39321
|
+
}
|
|
39322
|
+
function explicitSameOrgIssueUrls(text, prRepo, closingNumbers) {
|
|
39323
|
+
const [owner] = prRepo.split("/");
|
|
39324
|
+
const found = /* @__PURE__ */ new Map();
|
|
39325
|
+
for (const match of text.matchAll(/https:\/\/github\.com\/([\w.-]+\/[\w.-]+)\/issues\/(\d+)\b/gi)) {
|
|
39326
|
+
const repo = match[1];
|
|
39327
|
+
const number = Number(match[2]);
|
|
39328
|
+
if (!repo || !Number.isInteger(number) || !closingNumbers.has(number)) continue;
|
|
39329
|
+
if (repo.split("/")[0]?.toLowerCase() !== owner.toLowerCase() || repo.toLowerCase() === prRepo.toLowerCase()) continue;
|
|
39330
|
+
found.set(`${repo.toLowerCase()}#${number}`, { repo, number });
|
|
39331
|
+
}
|
|
39332
|
+
return [...found.values()];
|
|
39333
|
+
}
|
|
39334
|
+
async function reconcileCrossRepoFilingIssue(client, prRepo, prNumber) {
|
|
39335
|
+
try {
|
|
39336
|
+
const pr2 = await client.rest("GET", `repos/${prRepo}/pulls/${prNumber}`);
|
|
39337
|
+
if (!pr2.merged_at) return { status: "not-applicable" };
|
|
39338
|
+
const text = `${pr2.title ?? ""}
|
|
39339
|
+
${pr2.body ?? ""}`;
|
|
39340
|
+
const closingNumbers = new Set(findClosingMentions(text).filter((mention) => !mention.negated).map((mention) => mention.issue));
|
|
39341
|
+
if (closingNumbers.size === 0) return { status: "not-applicable" };
|
|
39342
|
+
const explicit = explicitSameOrgIssueUrls(text, prRepo, closingNumbers);
|
|
39343
|
+
let target;
|
|
39344
|
+
if (explicit.length > 1) {
|
|
39345
|
+
return { status: "failed", error: `multiple same-org issue URLs match bare closing references (${explicit.map((x) => `${x.repo}#${x.number}`).join(", ")})` };
|
|
39346
|
+
}
|
|
39347
|
+
if (explicit.length === 1) {
|
|
39348
|
+
target = await readExactIssue(client, explicit[0].repo, explicit[0].number);
|
|
39349
|
+
if (!target) return { status: "failed", issue: `${explicit[0].repo}#${explicit[0].number}`, error: "the explicit foreign issue URL did not resolve to an issue" };
|
|
39350
|
+
} else {
|
|
39351
|
+
const branchNumber = Number(/^#?(\d+)(?:-|$)/.exec(pr2.head?.ref ?? "")?.[1]);
|
|
39352
|
+
if (!Number.isInteger(branchNumber) || !closingNumbers.has(branchNumber)) return { status: "not-applicable" };
|
|
39353
|
+
if (await readExactIssue(client, prRepo, branchNumber)) return { status: "not-applicable" };
|
|
39354
|
+
const matches = await discoverSameOrgIssueNumber(client, prRepo, branchNumber);
|
|
39355
|
+
if (matches.length !== 1) {
|
|
39356
|
+
return {
|
|
39357
|
+
status: "failed",
|
|
39358
|
+
error: matches.length === 0 ? `branch #${branchNumber} matches a bare closing reference, but no same-org filing issue was found` : `branch #${branchNumber} is ambiguous across ${matches.map((match) => match.repo).join(", ")}`
|
|
39359
|
+
};
|
|
39360
|
+
}
|
|
39361
|
+
target = matches[0];
|
|
39362
|
+
}
|
|
39363
|
+
const issue2 = `${target.repo}#${target.number}`;
|
|
39364
|
+
if (target.state === "CLOSED") return { status: "already-closed", issue: issue2, url: target.url };
|
|
39365
|
+
const prUrl = pr2.html_url ?? `https://github.com/${prRepo}/pull/${prNumber}`;
|
|
39366
|
+
const comment = await client.rest("POST", `repos/${target.repo}/issues/${target.number}/comments`, {
|
|
39367
|
+
body: { body: `Resolved by merged PR ${prUrl}.
|
|
39368
|
+
|
|
39369
|
+
<!-- mmi-cross-repo-close:${prRepo}#${prNumber} -->` }
|
|
39370
|
+
});
|
|
39371
|
+
if (!comment.html_url) throw new Error(`GitHub created no evidence URL on ${issue2}`);
|
|
39372
|
+
await closeIssue(client, { ref: issue2, reason: "completed", evidence: comment.html_url });
|
|
39373
|
+
return { status: "closed", issue: issue2, url: target.url, evidence: comment.html_url };
|
|
39374
|
+
} catch (e) {
|
|
39375
|
+
return { status: "failed", error: e.message };
|
|
39376
|
+
}
|
|
39377
|
+
}
|
|
39378
|
+
|
|
39228
39379
|
// src/index.ts
|
|
39229
39380
|
var execFileGitRun = async (file, args) => (await execFileP2(file, args, { timeout: GIT_TIMEOUT_MS })).stdout;
|
|
39230
39381
|
async function githubRepoReachProbe() {
|
|
@@ -39751,12 +39902,15 @@ function positionalTargetForm(cmd, opts = {}) {
|
|
|
39751
39902
|
const args = cmd.registeredArguments ?? [];
|
|
39752
39903
|
const first = args[0];
|
|
39753
39904
|
if (!first) return void 0;
|
|
39754
|
-
return formatPositionalTarget(commandPath2(cmd), first.name(), opts);
|
|
39905
|
+
return formatPositionalTarget(canonicalPathFor(commandPath2(cmd)) ?? commandPath2(cmd), first.name(), opts);
|
|
39755
39906
|
}
|
|
39756
39907
|
function resolveParseHint() {
|
|
39757
39908
|
if (lastParseErrorKind === "unknown-command") {
|
|
39758
|
-
const
|
|
39759
|
-
|
|
39909
|
+
const parent = resolveCommandFromArgv(program2, process.argv.slice(2));
|
|
39910
|
+
const candidates = parent && parent.commands.length ? parent.commands.filter((child2) => commandMetadata(child2)?.category !== "internal").map(commandPath2) : allCommandPaths();
|
|
39911
|
+
const path3 = lastUnknownCommand ? suggestCommandPath(lastUnknownCommand, candidates) : void 0;
|
|
39912
|
+
const canonical = path3 ? canonicalPathFor(path3) ?? path3 : void 0;
|
|
39913
|
+
return canonical ? `(did you mean \`mmi-cli ${canonical}\`? ${DISCOVERY_HINT})` : STALE_HINT;
|
|
39760
39914
|
}
|
|
39761
39915
|
if (lastParseErrorKind !== "bad-arguments") return STALE_HINT;
|
|
39762
39916
|
const cmd = resolveCommandFromArgv(program2, process.argv.slice(2));
|
|
@@ -40215,7 +40369,31 @@ withExamples(mutating(
|
|
|
40215
40369
|
`worktree create: issue ${selector.repo}#${selector.number} belongs to ${selector.repo}, but this checkout is ${resolvedRepo} \u2014 re-run from ${selector.repo}'s primary checkout. Refusing to attach a foreign issue branch here (MMI-Hub#4351).`
|
|
40216
40370
|
);
|
|
40217
40371
|
}
|
|
40218
|
-
const
|
|
40372
|
+
const issueTitle = await fetchIssueTitle(selector.repo, selector.number);
|
|
40373
|
+
if (!o.slug && !issueTitle && o.claim && /^#?\d+$/.test(target)) {
|
|
40374
|
+
try {
|
|
40375
|
+
const client = defaultGitHubClient();
|
|
40376
|
+
const local = await readExactIssue(client, selector.repo, selector.number);
|
|
40377
|
+
if (!local) {
|
|
40378
|
+
const matches = await discoverSameOrgIssueNumber(client, selector.repo, selector.number);
|
|
40379
|
+
if (matches.length === 1) {
|
|
40380
|
+
const foreign = matches[0];
|
|
40381
|
+
let board = "its registered board";
|
|
40382
|
+
try {
|
|
40383
|
+
const cfg = await loadConfigForBoardSelector2(`${foreign.repo}#${foreign.number}`, foreign.repo);
|
|
40384
|
+
if (cfg.projectOwner && cfg.projectNumber) board = `${cfg.projectOwner} project #${cfg.projectNumber}`;
|
|
40385
|
+
} catch {
|
|
40386
|
+
}
|
|
40387
|
+
return fail(
|
|
40388
|
+
`worktree create: issue #${selector.number} is not in ${selector.repo}; it exists as ${foreign.repo}#${foreign.number} on ${board}. Re-run from ${foreign.repo}'s primary checkout with \`mmi-cli worktree create ${foreign.repo}#${foreign.number} --claim\`.`
|
|
40389
|
+
);
|
|
40390
|
+
}
|
|
40391
|
+
}
|
|
40392
|
+
} catch (e) {
|
|
40393
|
+
console.warn(` warning: could not discover whether #${selector.number} belongs to another same-org repo (${e.message})`);
|
|
40394
|
+
}
|
|
40395
|
+
}
|
|
40396
|
+
const slug = o.slug ?? (issueTitle ? slugifyIssueTitle(issueTitle) : "");
|
|
40219
40397
|
branch = buildNewBranchName(selector.number, slug ?? "");
|
|
40220
40398
|
if (PROTECTED_BRANCHES2.has(branch)) {
|
|
40221
40399
|
return fail(`worktree create: generated branch name '${branch}' collides with a protected train branch \u2014 pass a branch name instead`);
|
|
@@ -40276,8 +40454,13 @@ withExamples(mutating(
|
|
|
40276
40454
|
}
|
|
40277
40455
|
if (!resumed) {
|
|
40278
40456
|
step = `git worktree add ${wtPath}`;
|
|
40457
|
+
const wtPathPreExisted = (0, import_node_fs47.existsSync)(wtPath);
|
|
40458
|
+
const partialRemove = worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout);
|
|
40279
40459
|
await withWorktreeAddLock(repoRoot2, () => addWorktreeRobust(wtPath, branch, base, {
|
|
40280
|
-
|
|
40460
|
+
// #4834: `-c core.longpaths=true` rides the add command itself — a Windows worktree path
|
|
40461
|
+
// pushes long repos past MAX_PATH while the main checkout still fits. Command-scoped, so
|
|
40462
|
+
// the shared `.git/config` this lock serializes is never touched.
|
|
40463
|
+
git: async (args) => (await execFileP2("git", [...worktreeAddGitPrefix(), ...args], { timeout: GH_MUTATION_TIMEOUT_MS })).stdout,
|
|
40281
40464
|
revParse: async (ref) => {
|
|
40282
40465
|
try {
|
|
40283
40466
|
return (await execFileP2("git", ["rev-parse", "--verify", ref], { timeout: GIT_TIMEOUT_MS })).stdout.trim() || void 0;
|
|
@@ -40286,6 +40469,13 @@ withExamples(mutating(
|
|
|
40286
40469
|
}
|
|
40287
40470
|
},
|
|
40288
40471
|
deleteBranch: (b) => execFileP2("git", ["branch", "-D", b], { timeout: GIT_TIMEOUT_MS }).then(() => void 0),
|
|
40472
|
+
cleanupPartial: async () => {
|
|
40473
|
+
if (wtPathPreExisted || !(0, import_node_fs47.existsSync)(wtPath)) return;
|
|
40474
|
+
partialRemove.detachReparsePoints(wtPath);
|
|
40475
|
+
await execFileP2("git", ["worktree", "remove", "--force", wtPath], { timeout: GIT_TIMEOUT_MS }).catch(() => partialRemove.removeWorktreeDir(wtPath).then(() => void 0));
|
|
40476
|
+
await execFileP2("git", ["worktree", "prune"], { timeout: GIT_TIMEOUT_MS }).catch(() => {
|
|
40477
|
+
});
|
|
40478
|
+
},
|
|
40289
40479
|
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
|
|
40290
40480
|
log: (m) => {
|
|
40291
40481
|
if (!o.json) console.error(` ${m}`);
|
|
@@ -40330,6 +40520,7 @@ withExamples(mutating(
|
|
|
40330
40520
|
let lease;
|
|
40331
40521
|
try {
|
|
40332
40522
|
await execJervCli([
|
|
40523
|
+
"cli",
|
|
40333
40524
|
"lease",
|
|
40334
40525
|
"open",
|
|
40335
40526
|
"--kind",
|
|
@@ -42312,6 +42503,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
42312
42503
|
};
|
|
42313
42504
|
}
|
|
42314
42505
|
const boardAdvance = await advanceClosedIssuesToDone2(number, repoForPostCleanup);
|
|
42506
|
+
const crossRepoFilingIssue = repoForPostCleanup ? await reconcileCrossRepoFilingIssue(defaultGitHubClient(), repoForPostCleanup, Number(number)) : { status: "failed", error: "could not resolve the PR repo for cross-repo filing-issue reconciliation" };
|
|
42315
42507
|
invalidateStatuslineBoardCache();
|
|
42316
42508
|
console.log(JSON.stringify({
|
|
42317
42509
|
...buildPrMergeResultPayload({
|
|
@@ -42325,11 +42517,16 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
42325
42517
|
// `boardAdvance` keeps its published array shape; `boardAdvanceStatus` is the field a caller reads to
|
|
42326
42518
|
// tell "merged clean" from "merged, but the board did not follow" without guessing from the exit code.
|
|
42327
42519
|
...boardAdvance.entries.length ? { boardAdvance: boardAdvance.entries } : {},
|
|
42328
|
-
boardAdvanceStatus: boardAdvance.status
|
|
42520
|
+
boardAdvanceStatus: boardAdvance.status,
|
|
42521
|
+
...crossRepoFilingIssue.status !== "not-applicable" ? { crossRepoFilingIssue } : {}
|
|
42329
42522
|
}));
|
|
42330
42523
|
const boardAdvanceMessage = boardAdvanceFailureMessage(boardAdvance);
|
|
42331
42524
|
if (boardAdvanceMessage) console.error(boardAdvanceMessage);
|
|
42525
|
+
if (crossRepoFilingIssue.status === "failed") {
|
|
42526
|
+
console.error(`pr merge: cross-repo filing-issue reconciliation failed (${crossRepoFilingIssue.error}) \u2014 the PR MERGED, but a foreign filing issue may remain open.`);
|
|
42527
|
+
}
|
|
42332
42528
|
process.exitCode = boardAdvanceExitCode(boardAdvance) ?? process.exitCode;
|
|
42529
|
+
if (crossRepoFilingIssue.status === "failed") process.exitCode = 1;
|
|
42333
42530
|
});
|
|
42334
42531
|
registerQueryCommands(program2);
|
|
42335
42532
|
registerWorktreeCommands(program2);
|
|
@@ -42469,6 +42666,21 @@ function renderAlignment(label, alignment) {
|
|
|
42469
42666
|
}
|
|
42470
42667
|
return `${label}: ALIGNMENT PR PENDING \u2014 land it with \`mmi-cli devops pr merge ${alignment.prNumber ?? "<number>"} --auto --merge\`${alignment.prUrl ? ` (${alignment.prUrl})` : ""}`;
|
|
42471
42668
|
}
|
|
42669
|
+
var JERV_POWERTOOLS_REPO = "mutmutco/Jerv-PowerTools";
|
|
42670
|
+
async function runPostReleaseJervDoctor(repo) {
|
|
42671
|
+
if (repo.toLowerCase() !== JERV_POWERTOOLS_REPO.toLowerCase()) return void 0;
|
|
42672
|
+
try {
|
|
42673
|
+
await execJervCli(["doctor"], { timeout: 10 * 6e4 });
|
|
42674
|
+
return { status: "completed", note: "jerv-cli doctor completed on this owner machine" };
|
|
42675
|
+
} catch (e) {
|
|
42676
|
+
const err = e;
|
|
42677
|
+
const detail = (err.stderr?.trim() || err.message || "unknown doctor failure").split(/\r?\n/)[0];
|
|
42678
|
+
return {
|
|
42679
|
+
status: "reported-issues",
|
|
42680
|
+
note: `jerv-cli doctor ran but reported issues (${detail})`
|
|
42681
|
+
};
|
|
42682
|
+
}
|
|
42683
|
+
}
|
|
42472
42684
|
function renderTrainApply(commandName, r) {
|
|
42473
42685
|
let base = `mmi-cli ${commandName}: promoted ${r.repo} \u2014 ${r.stage} at ${r.tag} [${r.deployModel}]; ${renderDeployLine(r)}`;
|
|
42474
42686
|
if (r.versionFold) base = `${base}; ${r.versionFold}`;
|
|
@@ -42487,6 +42699,9 @@ function renderTrainApply(commandName, r) {
|
|
|
42487
42699
|
if (r.projectInfoSync) {
|
|
42488
42700
|
base = `${base}; project info: ${r.projectInfoSync.note}`;
|
|
42489
42701
|
}
|
|
42702
|
+
if (r.postReleaseJervDoctor) {
|
|
42703
|
+
base = `${base}; Jerv convergence: ${r.postReleaseJervDoctor.note}`;
|
|
42704
|
+
}
|
|
42490
42705
|
if (r.announceNote) base = `${base}; announce: ${r.announceNote}`;
|
|
42491
42706
|
if (r.releaseVerdict) {
|
|
42492
42707
|
const v = r.releaseVerdict;
|
|
@@ -42640,6 +42855,7 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
42640
42855
|
const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
42641
42856
|
const result = await runTrainApply(commandName, trainApplyDeps(), { watch: o.watch, announceSummaryFile: o.announceSummaryFile, ack, dev: o.dev });
|
|
42642
42857
|
let projectInfoSync;
|
|
42858
|
+
const postReleaseJervDoctor = commandName === "release" ? await runPostReleaseJervDoctor(result.repo) : void 0;
|
|
42643
42859
|
if (commandName === "release") {
|
|
42644
42860
|
try {
|
|
42645
42861
|
projectInfoSync = await runProjectInfoSync(result.repo, true);
|
|
@@ -42666,6 +42882,7 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
42666
42882
|
const reported = {
|
|
42667
42883
|
...result,
|
|
42668
42884
|
...projectInfoSync ? { projectInfoSync } : {},
|
|
42885
|
+
...postReleaseJervDoctor ? { postReleaseJervDoctor } : {},
|
|
42669
42886
|
...releaseVerdict ? { releaseVerdict } : {}
|
|
42670
42887
|
};
|
|
42671
42888
|
printLine(o.json ? JSON.stringify(reported, null, 2) : renderTrainApply(commandName, reported));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.120.0",
|
|
4
4
|
"description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
|
-
"build": "node build.mjs",
|
|
33
|
+
"build": "node build.mjs && node ../scripts/refresh-distribution-bom.mjs",
|
|
34
34
|
"test": "vitest run",
|
|
35
35
|
"typecheck": "tsc --noEmit"
|
|
36
36
|
},
|