@mutmutco/cli 3.42.0 → 3.44.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 +521 -165
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -3410,7 +3410,7 @@ var program = new Command();
|
|
|
3410
3410
|
|
|
3411
3411
|
// src/index.ts
|
|
3412
3412
|
var import_promises7 = require("node:fs/promises");
|
|
3413
|
-
var
|
|
3413
|
+
var import_node_fs30 = require("node:fs");
|
|
3414
3414
|
var import_node_child_process13 = require("node:child_process");
|
|
3415
3415
|
|
|
3416
3416
|
// src/cli-shared.ts
|
|
@@ -4061,6 +4061,12 @@ function commandPath(cmd) {
|
|
|
4061
4061
|
}
|
|
4062
4062
|
return parts.join(" ") || cmd.name();
|
|
4063
4063
|
}
|
|
4064
|
+
function jsonParity(cmd) {
|
|
4065
|
+
if (!cmd.options.some((o) => o.long === "--json")) {
|
|
4066
|
+
cmd.option("--json", "machine-readable output (default; accepted for parity)");
|
|
4067
|
+
}
|
|
4068
|
+
return cmd;
|
|
4069
|
+
}
|
|
4064
4070
|
function mutating(cmd, planFn) {
|
|
4065
4071
|
const marker = cmd;
|
|
4066
4072
|
if (marker.__mmiMutating) return cmd;
|
|
@@ -5604,6 +5610,9 @@ function samePath(a, b) {
|
|
|
5604
5610
|
function normPath(p) {
|
|
5605
5611
|
return p.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
5606
5612
|
}
|
|
5613
|
+
function toNativePath(p) {
|
|
5614
|
+
return process.platform === "win32" ? p.replace(/\//g, "\\") : p;
|
|
5615
|
+
}
|
|
5607
5616
|
function parseComposeLs(stdout) {
|
|
5608
5617
|
const text = stdout.trim();
|
|
5609
5618
|
if (!text) return [];
|
|
@@ -8910,9 +8919,10 @@ async function resolveWhoami(deps) {
|
|
|
8910
8919
|
if (ghLogin) return { login: ghLogin, source: "github", sessionExpiresAt: session?.expiresAt };
|
|
8911
8920
|
return { source: "unknown" };
|
|
8912
8921
|
}
|
|
8913
|
-
function whoamiLine(report) {
|
|
8922
|
+
function whoamiLine(report, caveat) {
|
|
8914
8923
|
if (!report.login) return null;
|
|
8915
|
-
|
|
8924
|
+
const line = `current human: ${report.login} (source: ${report.source}) \u2014 act for this login (e.g. claim --for ${report.login}); do not ask who the user is.`;
|
|
8925
|
+
return caveat?.trim() ? `${line} ${caveat.trim()}` : line;
|
|
8916
8926
|
}
|
|
8917
8927
|
|
|
8918
8928
|
// src/command-ladder-hint.ts
|
|
@@ -9143,6 +9153,10 @@ async function waitForPrChecks(deps) {
|
|
|
9143
9153
|
if (state === "failure") {
|
|
9144
9154
|
failureStreak += 1;
|
|
9145
9155
|
if (failureStreak >= PR_CHECKS_FAILURE_CONFIRMATIONS) {
|
|
9156
|
+
const diagnosis = deps.diagnoseFailure ? await deps.diagnoseFailure().catch(() => null) : null;
|
|
9157
|
+
if (diagnosis?.cause === "ci-budget-kill") {
|
|
9158
|
+
return { policy, status: "failure", reason: diagnosis.reason, detail: "ci-budget-kill" };
|
|
9159
|
+
}
|
|
9146
9160
|
return { policy, status: "failure", reason, detail: "checks-failure" };
|
|
9147
9161
|
}
|
|
9148
9162
|
lastDetail = "confirming-failure";
|
|
@@ -9459,7 +9473,7 @@ function planManagedGitignore(current) {
|
|
|
9459
9473
|
}
|
|
9460
9474
|
|
|
9461
9475
|
// src/gate-budget.ts
|
|
9462
|
-
var BLESSED_RUN_WITH_BUDGET_SHA = "
|
|
9476
|
+
var BLESSED_RUN_WITH_BUDGET_SHA = "ea2cf8710949dec53566c29836e29cd7515a4e23";
|
|
9463
9477
|
var REMOTE_USE = /^mutmutco\/MMI-Hub\/\.github\/actions\/run-with-budget@(\S+)$/;
|
|
9464
9478
|
var LOCAL_USE = "./.github/actions/run-with-budget";
|
|
9465
9479
|
var FULL_SHA = /^[0-9a-f]{40}$/;
|
|
@@ -11229,7 +11243,9 @@ async function collectWaveStatus(deps) {
|
|
|
11229
11243
|
}
|
|
11230
11244
|
const headText = await deps.fetchHeadText?.(wt.branch).catch(() => void 0);
|
|
11231
11245
|
rows.push({
|
|
11232
|
-
|
|
11246
|
+
// #3338: emit NATIVE separators (see toNativePath). `-C wt.path` above and the `primary` compare
|
|
11247
|
+
// below keep git's raw form — only the emitted value is converted.
|
|
11248
|
+
path: toNativePath(wt.path),
|
|
11233
11249
|
branch: wt.branch,
|
|
11234
11250
|
primary: wt.path === primaryPath,
|
|
11235
11251
|
dirty,
|
|
@@ -11615,6 +11631,25 @@ async function unlinkSubIssue(runGh, parentRef, childRef, defaultRepo) {
|
|
|
11615
11631
|
${stdout.trim() || "(empty)"}`);
|
|
11616
11632
|
return result;
|
|
11617
11633
|
}
|
|
11634
|
+
function parseParentIssueUrl(apiUrl) {
|
|
11635
|
+
if (typeof apiUrl !== "string" || !apiUrl.trim()) return null;
|
|
11636
|
+
const match = /\/repos\/([^/]+\/[^/]+)\/issues\/(\d+)\/?$/.exec(apiUrl.trim());
|
|
11637
|
+
if (!match) return null;
|
|
11638
|
+
const [, repo, number] = match;
|
|
11639
|
+
return { repo, number: Number(number), url: `https://github.com/${repo}/issues/${number}` };
|
|
11640
|
+
}
|
|
11641
|
+
function resolveParentField(payload) {
|
|
11642
|
+
if (!payload || typeof payload !== "object") {
|
|
11643
|
+
return { parentReadError: "issue payload was empty or not an object" };
|
|
11644
|
+
}
|
|
11645
|
+
if (!("sub_issues_summary" in payload)) {
|
|
11646
|
+
return { parentReadError: "issue payload carries no sub-issue fields \u2014 parent state is unknown, not absent" };
|
|
11647
|
+
}
|
|
11648
|
+
const raw = payload.parent_issue_url;
|
|
11649
|
+
if (raw === void 0 || raw === null) return { parent: null };
|
|
11650
|
+
const parsed = typeof raw === "string" ? parseParentIssueUrl(raw) : null;
|
|
11651
|
+
return parsed ? { parent: parsed } : { parentReadError: `unrecognised parent_issue_url: ${JSON.stringify(raw)}` };
|
|
11652
|
+
}
|
|
11618
11653
|
function parentLinkFields(result, error) {
|
|
11619
11654
|
if (result) return { parent: result };
|
|
11620
11655
|
if (error) return { parentLinkError: error };
|
|
@@ -13332,7 +13367,7 @@ function appendPublishDispatch(deploy, publish) {
|
|
|
13332
13367
|
deployStatus: deploy.deployStatus === "failure" || publish.deployStatus === "failure" ? "failure" : deploy.deployStatus === "pending" || publish.deployStatus === "pending" ? "pending" : "success"
|
|
13333
13368
|
};
|
|
13334
13369
|
}
|
|
13335
|
-
async function watchOwnWorkflowRuns(deps, repo, targets, since, headSha) {
|
|
13370
|
+
async function watchOwnWorkflowRuns(deps, repo, targets, since, headSha, enumerateSha = false) {
|
|
13336
13371
|
const workflowRuns = [];
|
|
13337
13372
|
for (const target of targets) {
|
|
13338
13373
|
try {
|
|
@@ -13342,8 +13377,68 @@ async function watchOwnWorkflowRuns(deps, repo, targets, since, headSha) {
|
|
|
13342
13377
|
workflowRuns.push({ workflow: target.workflow, conclusion: "failure" });
|
|
13343
13378
|
}
|
|
13344
13379
|
}
|
|
13380
|
+
if (!enumerateSha) return workflowRuns;
|
|
13381
|
+
const seen = new Set(workflowRuns.map((r) => r.runId).filter((id) => typeof id === "number"));
|
|
13382
|
+
workflowRuns.push(...await discoverShaWorkflowRuns(deps, repo, headSha, seen));
|
|
13345
13383
|
return workflowRuns;
|
|
13346
13384
|
}
|
|
13385
|
+
var NON_DEPLOY_EVENTS = /* @__PURE__ */ new Set([
|
|
13386
|
+
"pull_request",
|
|
13387
|
+
"pull_request_target",
|
|
13388
|
+
"pull_request_review",
|
|
13389
|
+
"pull_request_review_comment",
|
|
13390
|
+
"schedule",
|
|
13391
|
+
"issues",
|
|
13392
|
+
"issue_comment",
|
|
13393
|
+
"discussion",
|
|
13394
|
+
"discussion_comment",
|
|
13395
|
+
"fork",
|
|
13396
|
+
"watch",
|
|
13397
|
+
"star",
|
|
13398
|
+
"public",
|
|
13399
|
+
"gollum",
|
|
13400
|
+
"create",
|
|
13401
|
+
"delete"
|
|
13402
|
+
]);
|
|
13403
|
+
var SHA_ENUM_LIMIT = 50;
|
|
13404
|
+
var NON_FAILING_CONCLUSIONS = /* @__PURE__ */ new Set(["success", "skipped", "neutral"]);
|
|
13405
|
+
async function discoverShaWorkflowRuns(deps, repo, headSha, seenRunIds) {
|
|
13406
|
+
const marker = `sha-enumeration(${headSha.slice(0, 7)})`;
|
|
13407
|
+
let rows;
|
|
13408
|
+
try {
|
|
13409
|
+
rows = JSON.parse(await deps.run("gh", [
|
|
13410
|
+
"run",
|
|
13411
|
+
"list",
|
|
13412
|
+
"--repo",
|
|
13413
|
+
repo,
|
|
13414
|
+
"--commit",
|
|
13415
|
+
headSha,
|
|
13416
|
+
"--limit",
|
|
13417
|
+
String(SHA_ENUM_LIMIT),
|
|
13418
|
+
"--json",
|
|
13419
|
+
"databaseId,url,event,status,conclusion,workflowName"
|
|
13420
|
+
]));
|
|
13421
|
+
if (!Array.isArray(rows)) throw new Error("run list did not return an array");
|
|
13422
|
+
} catch {
|
|
13423
|
+
return [{ workflow: marker, conclusion: "pending" }];
|
|
13424
|
+
}
|
|
13425
|
+
const extra = [];
|
|
13426
|
+
if (rows.length >= SHA_ENUM_LIMIT) extra.push({ workflow: `${marker} truncated`, conclusion: "pending" });
|
|
13427
|
+
for (const row of rows) {
|
|
13428
|
+
if (typeof row.databaseId !== "number" || seenRunIds.has(row.databaseId)) continue;
|
|
13429
|
+
if (NON_DEPLOY_EVENTS.has(row.event ?? "")) continue;
|
|
13430
|
+
extra.push({
|
|
13431
|
+
workflow: row.workflowName ?? `run:${row.databaseId}`,
|
|
13432
|
+
runId: row.databaseId,
|
|
13433
|
+
runUrl: row.url,
|
|
13434
|
+
// An in-flight run is reported UNVERIFIED, never watched. Blocking on an arbitrary discovered run
|
|
13435
|
+
// would let one long push workflow hang the release command and mask the failed rows behind it, and
|
|
13436
|
+
// `gh run watch` has no timeout. The NAMED targets are the ones worth blocking on (also #3382 review).
|
|
13437
|
+
conclusion: row.status === "completed" ? NON_FAILING_CONCLUSIONS.has(row.conclusion ?? "") ? "success" : "failure" : "pending"
|
|
13438
|
+
});
|
|
13439
|
+
}
|
|
13440
|
+
return extra;
|
|
13441
|
+
}
|
|
13347
13442
|
async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince, autoRunHeadSha, dispatchFailure = "throw", publishDir) {
|
|
13348
13443
|
if (model === "tenant-container" || model === "solo-container") {
|
|
13349
13444
|
const since = (deps.now ?? Date.now)();
|
|
@@ -13370,7 +13465,8 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
|
|
|
13370
13465
|
ctx.repo,
|
|
13371
13466
|
[{ workflow: "publish.yml", event: "release" }],
|
|
13372
13467
|
since,
|
|
13373
|
-
autoRunHeadSha
|
|
13468
|
+
autoRunHeadSha,
|
|
13469
|
+
true
|
|
13374
13470
|
);
|
|
13375
13471
|
const primary = workflowRuns[0];
|
|
13376
13472
|
return { note, runId: primary?.runId, runUrl: primary?.runUrl, workflowRuns, deployStatus: aggregateWorkflowRuns(workflowRuns) };
|
|
@@ -13384,7 +13480,7 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
|
|
|
13384
13480
|
{ workflow: "deploy.yml", event: "release" },
|
|
13385
13481
|
{ workflow: "publish.yml", event: "release" }
|
|
13386
13482
|
];
|
|
13387
|
-
const workflowRuns = await watchOwnWorkflowRuns(deps, HUB_REPO3, targets, since, autoRunHeadSha);
|
|
13483
|
+
const workflowRuns = await watchOwnWorkflowRuns(deps, HUB_REPO3, targets, since, autoRunHeadSha, ref !== "rc");
|
|
13388
13484
|
const primary = workflowRuns[0];
|
|
13389
13485
|
return {
|
|
13390
13486
|
note,
|
|
@@ -15386,8 +15482,7 @@ var LINK_RE = /\]\(([^)#\s]+?\.md)(?:#[^)]*)?\)/g;
|
|
|
15386
15482
|
var CMD_RE = /`mmi-cli ((?:[a-z][a-z-]*)(?: [a-z][a-z-]*){0,3})/g;
|
|
15387
15483
|
var RETIRED_RE = /\b(retired|historical|removed|deleted|gone|no longer|superseded|legacy)\b/i;
|
|
15388
15484
|
var ROOT_DOCS = ["README.md", "architecture.md"];
|
|
15389
|
-
var SKIP_WALK = ["docs/Archive/", "docs/incidents/"];
|
|
15390
|
-
var PINNED_DOCS = ["docs/Reference/runtime-contracts.md"];
|
|
15485
|
+
var SKIP_WALK = ["docs/Archive/", "docs/incidents/", "docs/research/"];
|
|
15391
15486
|
function stripFences(markdown) {
|
|
15392
15487
|
let inFence = false;
|
|
15393
15488
|
return markdown.split(/\r?\n/).map((line) => {
|
|
@@ -15413,14 +15508,9 @@ function extractPins(markdown) {
|
|
|
15413
15508
|
});
|
|
15414
15509
|
return pins;
|
|
15415
15510
|
}
|
|
15416
|
-
function checkPins(root, readFile6) {
|
|
15511
|
+
function checkPins(root, readFile6, docs2) {
|
|
15417
15512
|
const findings = [];
|
|
15418
|
-
for (const doc of
|
|
15419
|
-
const markdown = readFile6((0, import_node_path16.join)(root, doc));
|
|
15420
|
-
if (markdown == null) {
|
|
15421
|
-
findings.push({ kind: "missing-doc", doc, line: 0, detail: doc });
|
|
15422
|
-
continue;
|
|
15423
|
-
}
|
|
15513
|
+
for (const [doc, markdown] of Object.entries(docs2)) {
|
|
15424
15514
|
for (const pin of extractPins(markdown)) {
|
|
15425
15515
|
if ("malformed" in pin) {
|
|
15426
15516
|
findings.push({ kind: "malformed-pin", doc, line: pin.line, detail: pin.text });
|
|
@@ -15493,13 +15583,18 @@ function checkRefs(root, deps, docs2) {
|
|
|
15493
15583
|
const docDir = import_node_path16.posix.dirname(doc);
|
|
15494
15584
|
for (const { target, line } of extractLinks(markdown)) {
|
|
15495
15585
|
const resolved = import_node_path16.posix.normalize(import_node_path16.posix.join(docDir === "." ? "" : docDir, target));
|
|
15586
|
+
if (resolved.startsWith("..")) {
|
|
15587
|
+
candidates.push({ kind: "missing-link", doc, line, detail: target, escapesRoot: true });
|
|
15588
|
+
continue;
|
|
15589
|
+
}
|
|
15496
15590
|
if (!exists((0, import_node_path16.join)(root, resolved))) {
|
|
15497
15591
|
candidates.push({ kind: "missing-link", doc, line, detail: target, resolved });
|
|
15498
15592
|
}
|
|
15499
15593
|
}
|
|
15500
15594
|
}
|
|
15501
|
-
const
|
|
15502
|
-
const
|
|
15595
|
+
const queryable = candidates.filter((c) => !c.escapesRoot);
|
|
15596
|
+
const ignored = queryable.length ? isIgnored(queryable.map((c) => c.kind === "missing-link" ? c.resolved : c.detail)) : /* @__PURE__ */ new Set();
|
|
15597
|
+
const findings = candidates.filter((c) => c.escapesRoot || !ignored.has(c.kind === "missing-link" ? c.resolved : c.detail)).map(({ resolved: _resolved, escapesRoot: _escapesRoot, ...finding }) => finding);
|
|
15503
15598
|
return { ok: findings.length === 0, findings };
|
|
15504
15599
|
}
|
|
15505
15600
|
function checkCommands(docs2, commandPaths) {
|
|
@@ -15544,10 +15639,12 @@ function defaultListDocs(root) {
|
|
|
15544
15639
|
return [...ROOT_DOCS.filter((rel) => (0, import_node_fs18.existsSync)((0, import_node_path16.join)(root, rel))), ...docs2];
|
|
15545
15640
|
}
|
|
15546
15641
|
function defaultIsIgnored(root, relPaths, exec = import_node_child_process9.execFileSync) {
|
|
15642
|
+
const inRepo = relPaths.filter((p) => !p.startsWith(".."));
|
|
15643
|
+
if (inRepo.length === 0) return /* @__PURE__ */ new Set();
|
|
15547
15644
|
try {
|
|
15548
15645
|
const out = exec("git", ["check-ignore", "--stdin"], {
|
|
15549
15646
|
cwd: root,
|
|
15550
|
-
input:
|
|
15647
|
+
input: inRepo.join("\n"),
|
|
15551
15648
|
encoding: "utf8"
|
|
15552
15649
|
});
|
|
15553
15650
|
return new Set(out.split(/\r?\n/).filter(Boolean));
|
|
@@ -15566,7 +15663,7 @@ function runDocRefs(root, deps = {}) {
|
|
|
15566
15663
|
listDocs(root).map((rel) => [rel, readFile6((0, import_node_path16.join)(root, rel))]).filter(([, body]) => body != null)
|
|
15567
15664
|
);
|
|
15568
15665
|
const findings = [
|
|
15569
|
-
...checkPins(root, readFile6).findings,
|
|
15666
|
+
...checkPins(root, readFile6, docs2).findings,
|
|
15570
15667
|
...checkRefs(root, { exists, isIgnored }, docs2).findings
|
|
15571
15668
|
];
|
|
15572
15669
|
if (commandPaths == null) {
|
|
@@ -17033,6 +17130,9 @@ function checkGithubPools(probe) {
|
|
|
17033
17130
|
return lines;
|
|
17034
17131
|
}
|
|
17035
17132
|
|
|
17133
|
+
// src/box-commands.ts
|
|
17134
|
+
var import_node_fs20 = require("node:fs");
|
|
17135
|
+
|
|
17036
17136
|
// src/box.ts
|
|
17037
17137
|
var BOX_KEYS = {
|
|
17038
17138
|
/** Opens every MM box INCLUDING the CI runner (`mmi-runner`). The general-purpose MM key. */
|
|
@@ -17134,6 +17234,12 @@ function formatSshRecipe(box) {
|
|
|
17134
17234
|
`ssh -i "$kf" -o StrictHostKeyChecking=accept-new root@${box.address} 'hostname; uptime'`
|
|
17135
17235
|
].join("\n");
|
|
17136
17236
|
}
|
|
17237
|
+
var SSH_RECIPE_AGENT_NOTE = "# agent callers: this is multi-line, so running it inline is denied (#1473/#2125).\n# Use `mmi-cli runtime box get <name> --ssh --script <path>` to write it to a file, then `bash <path>`.";
|
|
17238
|
+
function sshRecipeScript(box) {
|
|
17239
|
+
return `#!/usr/bin/env bash
|
|
17240
|
+
${formatSshRecipe(box)}
|
|
17241
|
+
`;
|
|
17242
|
+
}
|
|
17137
17243
|
|
|
17138
17244
|
// src/box-commands.ts
|
|
17139
17245
|
var HETZNER_API = "https://api.hetzner.cloud/v1/servers";
|
|
@@ -17206,7 +17312,7 @@ function registerBoxCommands(program3) {
|
|
|
17206
17312
|
await failGraceful(e.message);
|
|
17207
17313
|
}
|
|
17208
17314
|
});
|
|
17209
|
-
box.command("get <name>").description("one box by name \u2014 its address and ssh key path; --ssh prints a ready-to-run connect recipe").option("--json", "machine-readable output").option("--ssh", "print a copy-paste ssh recipe (materializes the key to a temp file; never echoes it)").action(async (name, o) => {
|
|
17315
|
+
box.command("get <name>").description("one box by name \u2014 its address and ssh key path; --ssh prints a ready-to-run connect recipe").option("--json", "machine-readable output").option("--ssh", "print a copy-paste ssh recipe (materializes the key to a temp file; never echoes it)").option("--script <path>", "with --ssh: write the recipe to <path> and print the one line to run \u2014 the runnable form for agent callers, whose transport denies a multi-line inline body (#3385)").action(async (name, o) => {
|
|
17210
17316
|
try {
|
|
17211
17317
|
const { boxes, incomplete } = await fetchInventory();
|
|
17212
17318
|
const result = lookupBox(name, boxes, incomplete);
|
|
@@ -17222,8 +17328,16 @@ function registerBoxCommands(program3) {
|
|
|
17222
17328
|
return;
|
|
17223
17329
|
}
|
|
17224
17330
|
const found = result.box;
|
|
17331
|
+
if (o.script && !o.ssh) {
|
|
17332
|
+
await failGraceful("runtime box get: --script requires --ssh (it writes the ssh connect recipe)");
|
|
17333
|
+
return;
|
|
17334
|
+
}
|
|
17225
17335
|
if (o.json) console.log(JSON.stringify({ box: found, incomplete }, null, 2));
|
|
17226
|
-
else if (o.ssh
|
|
17336
|
+
else if (o.ssh && o.script) {
|
|
17337
|
+
(0, import_node_fs20.writeFileSync)(o.script, sshRecipeScript(found), "utf8");
|
|
17338
|
+
console.log(`wrote ${o.script} \u2014 run: bash "${o.script}"`);
|
|
17339
|
+
} else if (o.ssh) console.log(`${formatSshRecipe(found)}
|
|
17340
|
+
${SSH_RECIPE_AGENT_NOTE}`);
|
|
17227
17341
|
else console.log(formatBoxTable([found]));
|
|
17228
17342
|
if (!o.json) warnIncomplete(incomplete);
|
|
17229
17343
|
if (incomplete.length) process.exitCode = 1;
|
|
@@ -17243,7 +17357,7 @@ var ORG = "mutmutco";
|
|
|
17243
17357
|
var DOC_START_MARKER = "<!-- schedules:inventory:start -->";
|
|
17244
17358
|
var DOC_END_MARKER = "<!-- schedules:inventory:end -->";
|
|
17245
17359
|
function isJervResource(name) {
|
|
17246
|
-
return /^jerv/i.test(name);
|
|
17360
|
+
return /^jerv-memory(?:-|$)/i.test(name);
|
|
17247
17361
|
}
|
|
17248
17362
|
function llmFromHeader(yamlText) {
|
|
17249
17363
|
const m = /^#\s*llm:\s*(.+)$/im.exec(yamlText);
|
|
@@ -17350,6 +17464,11 @@ function awsScheduleRefs(payload) {
|
|
|
17350
17464
|
}
|
|
17351
17465
|
var DECLARED_ENTRIES = [
|
|
17352
17466
|
{ name: "mmi-backup (mm-central, mmi-fofu, zuber, mmi-oguz)", cadence: "17 2 * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-backup \u2192 /opt/mmi-control/nightly-backup.sh (verify: mmi-cli runtime box get <box> --ssh)" },
|
|
17467
|
+
// #3370: the floor under runner-hygiene.yml. That workflow runs ON the runner and needs checkout +
|
|
17468
|
+
// setup-node before it can prune, so at 100% it dies at checkout and never prunes — the cleanup is
|
|
17469
|
+
// unavailable exactly when it is needed. This one is root cron on the box: no checkout, no node, and
|
|
17470
|
+
// a no-op below 90% disk, which is why 15 minutes is affordable.
|
|
17471
|
+
{ name: "mmi-runner-hygiene-oob (mmi-runner)", cadence: "*/15 * * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-runner-hygiene \u2192 /opt/mmi-control/runner-hygiene-oob.sh; acts only at >=90% disk (verify: mmi-cli runtime box get mmi-runner --ssh)" },
|
|
17353
17472
|
{ name: "zuber-bake", cadence: "*:05/30 (every 30 min)", executor: "zuber systemd timer", llm: "no", resolved: "declared", source: "zuber-bake.timer \u2192 rolling overlay bake, metro tier, next ~6h window (verify over ssh)" },
|
|
17354
17473
|
{ name: "zuber-bake-full", cadence: "00:30 UTC (03:30 TRT, daily)", executor: "zuber systemd timer", llm: "no", resolved: "declared", source: "zuber-bake-full.timer \u2192 full 24h overlay rebuild, all 81 il (verify over ssh)" }
|
|
17355
17474
|
];
|
|
@@ -17944,7 +18063,7 @@ function withArmingReport(body) {
|
|
|
17944
18063
|
function registerSchedulesLiftCommand(program3, deps = {}) {
|
|
17945
18064
|
const schedules = program3.commands.find((c) => c.name() === "schedules");
|
|
17946
18065
|
if (!schedules) throw new Error("schedules lift: registerSchedulesCommands must run first \u2014 the lift attaches to the `schedules` command");
|
|
17947
|
-
schedules.command("register").alias("lift").description(`register this repo's eight-field workflow headers as SCHEDULE# registry rows (C2, #3186; renamed from \`lift\`, #3219) \u2014 a per-repo replace; aborts the whole registration on any header violation; exits 75 when the registry is unreachable (#3187). Registering does NOT arm the clock: the fleet-clock reconciler arms the rows within ${FLEET_CLOCK_RECONCILE_WINDOW}, and the success JSON carries an \`arming\` block saying so (#3281)`).option("--repo <name>", "bare repo name for the join key (defaults to the origin remote basename, case-preserved)").option("--dir <path>", "workflows directory to lift (defaults to .github/workflows)").option("--dry-run", "print the {repo, schedules} body that would be POSTed; never write").action(async (o) => {
|
|
18066
|
+
jsonParity(schedules.command("register").alias("lift").description(`register this repo's eight-field workflow headers as SCHEDULE# registry rows (C2, #3186; renamed from \`lift\`, #3219) \u2014 a per-repo replace; aborts the whole registration on any header violation; exits 75 when the registry is unreachable (#3187). Registering does NOT arm the clock: the fleet-clock reconciler arms the rows within ${FLEET_CLOCK_RECONCILE_WINDOW}, and the success JSON carries an \`arming\` block saying so (#3281)`).option("--repo <name>", "bare repo name for the join key (defaults to the origin remote basename, case-preserved)").option("--dir <path>", "workflows directory to lift (defaults to .github/workflows)").option("--dry-run", "print the {repo, schedules} body that would be POSTed; never write")).action(async (o) => {
|
|
17948
18067
|
try {
|
|
17949
18068
|
const result = await runSchedulesLift({ repo: o.repo, dir: o.dir, dryRun: Boolean(o.dryRun) }, deps);
|
|
17950
18069
|
if (o.dryRun) {
|
|
@@ -18423,7 +18542,7 @@ function registerQueryCommands(program3) {
|
|
|
18423
18542
|
}
|
|
18424
18543
|
|
|
18425
18544
|
// src/bootstrap-commands.ts
|
|
18426
|
-
var
|
|
18545
|
+
var import_node_fs21 = require("node:fs");
|
|
18427
18546
|
|
|
18428
18547
|
// src/bootstrap-verify.ts
|
|
18429
18548
|
var TRAIN_BRANCHES2 = ["development", "rc", "main"];
|
|
@@ -18971,7 +19090,7 @@ function registerBootstrapCommands(program3) {
|
|
|
18971
19090
|
client: defaultGitHubClient(),
|
|
18972
19091
|
projectMeta: meta,
|
|
18973
19092
|
deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
|
|
18974
|
-
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0,
|
|
19093
|
+
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs21.existsSync)(path2) ? (0, import_node_fs21.readFileSync)(path2, "utf8") : null,
|
|
18975
19094
|
// requiredGcpApis is stored as an array by a JSON write, but `org project set --var KEY=VALUE` stores a raw
|
|
18976
19095
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
18977
19096
|
requiredGcpApis: (() => {
|
|
@@ -19022,12 +19141,12 @@ function registerBootstrapCommands(program3) {
|
|
|
19022
19141
|
return fail(`bootstrap apply: ${e.message}`);
|
|
19023
19142
|
}
|
|
19024
19143
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
19025
|
-
if (!(0,
|
|
19026
|
-
const manifest = loadBootstrapSeeds((0,
|
|
19144
|
+
if (!(0, import_node_fs21.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
|
|
19145
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs21.readFileSync)(manifestPath, "utf8"));
|
|
19027
19146
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
19028
19147
|
const slug = parsedRepo.slug;
|
|
19029
19148
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
19030
|
-
const readFile6 = (p) => (0,
|
|
19149
|
+
const readFile6 = (p) => (0, import_node_fs21.existsSync)(p) ? (0, import_node_fs21.readFileSync)(p, "utf8") : null;
|
|
19031
19150
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
19032
19151
|
const rawVars = {};
|
|
19033
19152
|
for (const value of cmdOpts.var ?? []) {
|
|
@@ -19223,11 +19342,11 @@ LIVE apply to ${repo}:
|
|
|
19223
19342
|
}
|
|
19224
19343
|
|
|
19225
19344
|
// src/stage-commands.ts
|
|
19226
|
-
var
|
|
19345
|
+
var import_node_fs23 = require("node:fs");
|
|
19227
19346
|
var import_node_path20 = require("node:path");
|
|
19228
19347
|
|
|
19229
19348
|
// src/port-registry.ts
|
|
19230
|
-
var
|
|
19349
|
+
var import_node_fs22 = require("node:fs");
|
|
19231
19350
|
var import_node_path19 = require("node:path");
|
|
19232
19351
|
|
|
19233
19352
|
// ../infra/port-geometry.mjs
|
|
@@ -19242,8 +19361,8 @@ function nextPortBlock(registry2) {
|
|
|
19242
19361
|
return [base, base + PORT_SPAN];
|
|
19243
19362
|
}
|
|
19244
19363
|
function loadPortRegistry(path2) {
|
|
19245
|
-
if (!(0,
|
|
19246
|
-
const raw = JSON.parse((0,
|
|
19364
|
+
if (!(0, import_node_fs22.existsSync)(path2)) return {};
|
|
19365
|
+
const raw = JSON.parse((0, import_node_fs22.readFileSync)(path2, "utf8"));
|
|
19247
19366
|
const out = {};
|
|
19248
19367
|
for (const [key, value] of Object.entries(raw)) {
|
|
19249
19368
|
if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
|
|
@@ -19257,9 +19376,9 @@ function ensurePortRange(repo, path2) {
|
|
|
19257
19376
|
const existing = registry2[repo];
|
|
19258
19377
|
if (existing) return existing;
|
|
19259
19378
|
const range = nextPortBlock(registry2);
|
|
19260
|
-
const raw = (0,
|
|
19379
|
+
const raw = (0, import_node_fs22.existsSync)(path2) ? JSON.parse((0, import_node_fs22.readFileSync)(path2, "utf8")) : {};
|
|
19261
19380
|
raw[repo] = range;
|
|
19262
|
-
(0,
|
|
19381
|
+
(0, import_node_fs22.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
|
|
19263
19382
|
return range;
|
|
19264
19383
|
}
|
|
19265
19384
|
function portCursorSeed(registry2) {
|
|
@@ -19283,7 +19402,7 @@ function existingPortRange(repo, registry2) {
|
|
|
19283
19402
|
function portRangeInfraAt(root, source) {
|
|
19284
19403
|
const registryPath = (0, import_node_path19.join)(root, "infra", "port-ranges.json");
|
|
19285
19404
|
const ddbScriptPath = (0, import_node_path19.join)(root, "infra", "port-ddb.mjs");
|
|
19286
|
-
if (!(0,
|
|
19405
|
+
if (!(0, import_node_fs22.existsSync)(registryPath) || !(0, import_node_fs22.existsSync)(ddbScriptPath)) return null;
|
|
19287
19406
|
return { root, source, registryPath, ddbScriptPath };
|
|
19288
19407
|
}
|
|
19289
19408
|
function resolvePortRangeInfra(cwd, packageDir) {
|
|
@@ -19472,8 +19591,8 @@ function registerStageCommands(program3) {
|
|
|
19472
19591
|
const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
|
|
19473
19592
|
return decideStage({
|
|
19474
19593
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
19475
|
-
hasCompose: (0,
|
|
19476
|
-
hasEnvExample: (0,
|
|
19594
|
+
hasCompose: (0, import_node_fs23.existsSync)((0, import_node_path20.join)(process.cwd(), "docker-compose.yml")),
|
|
19595
|
+
hasEnvExample: (0, import_node_fs23.existsSync)((0, import_node_path20.join)(process.cwd(), ".env.example"))
|
|
19477
19596
|
});
|
|
19478
19597
|
}
|
|
19479
19598
|
async function fetchStageVaultEnvMerge() {
|
|
@@ -19908,7 +20027,7 @@ function registerBoardCommands(program3) {
|
|
|
19908
20027
|
}
|
|
19909
20028
|
|
|
19910
20029
|
// src/merge-cleanup.ts
|
|
19911
|
-
var
|
|
20030
|
+
var import_node_fs24 = require("node:fs");
|
|
19912
20031
|
var import_promises5 = require("node:fs/promises");
|
|
19913
20032
|
var import_node_child_process11 = require("node:child_process");
|
|
19914
20033
|
|
|
@@ -20232,7 +20351,7 @@ async function applyGcPlan(plan, remote) {
|
|
|
20232
20351
|
return cleanupPrMergeLocalBranch(branch.branch, {
|
|
20233
20352
|
beforeWorktrees,
|
|
20234
20353
|
startingPath: branch.worktreePath,
|
|
20235
|
-
pathExists: (p) => (0,
|
|
20354
|
+
pathExists: (p) => (0, import_node_fs24.existsSync)(p),
|
|
20236
20355
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
20237
20356
|
teardownWorktreeStage,
|
|
20238
20357
|
deferredStore,
|
|
@@ -20258,7 +20377,7 @@ async function applyGcPlan(plan, remote) {
|
|
|
20258
20377
|
for (const wt of plan.worktreeDirs) {
|
|
20259
20378
|
try {
|
|
20260
20379
|
const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
|
|
20261
|
-
realpath: (path2) => (0,
|
|
20380
|
+
realpath: (path2) => (0, import_node_fs24.realpathSync)(path2)
|
|
20262
20381
|
});
|
|
20263
20382
|
if (!cleanupTarget.ok) {
|
|
20264
20383
|
result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
|
|
@@ -20385,13 +20504,13 @@ var realWorktreeDirRemover = {
|
|
|
20385
20504
|
probe: (p) => {
|
|
20386
20505
|
let st;
|
|
20387
20506
|
try {
|
|
20388
|
-
st = (0,
|
|
20507
|
+
st = (0, import_node_fs24.lstatSync)(p);
|
|
20389
20508
|
} catch {
|
|
20390
20509
|
return null;
|
|
20391
20510
|
}
|
|
20392
20511
|
if (st.isSymbolicLink()) return "link";
|
|
20393
20512
|
try {
|
|
20394
|
-
(0,
|
|
20513
|
+
(0, import_node_fs24.readlinkSync)(p);
|
|
20395
20514
|
return "link";
|
|
20396
20515
|
} catch {
|
|
20397
20516
|
}
|
|
@@ -20399,7 +20518,7 @@ var realWorktreeDirRemover = {
|
|
|
20399
20518
|
},
|
|
20400
20519
|
readdir: (p) => {
|
|
20401
20520
|
try {
|
|
20402
|
-
return (0,
|
|
20521
|
+
return (0, import_node_fs24.readdirSync)(p);
|
|
20403
20522
|
} catch {
|
|
20404
20523
|
return [];
|
|
20405
20524
|
}
|
|
@@ -20408,9 +20527,9 @@ var realWorktreeDirRemover = {
|
|
|
20408
20527
|
// leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
|
|
20409
20528
|
detachLink: (p) => {
|
|
20410
20529
|
try {
|
|
20411
|
-
(0,
|
|
20530
|
+
(0, import_node_fs24.rmdirSync)(p);
|
|
20412
20531
|
} catch {
|
|
20413
|
-
(0,
|
|
20532
|
+
(0, import_node_fs24.unlinkSync)(p);
|
|
20414
20533
|
}
|
|
20415
20534
|
},
|
|
20416
20535
|
removeTree: (p) => (0, import_promises5.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
|
@@ -20443,9 +20562,9 @@ async function worktreeHasStageState(worktreePath) {
|
|
|
20443
20562
|
}
|
|
20444
20563
|
}
|
|
20445
20564
|
function stageStateFileBelongsToWorktree(statePath, worktreePath) {
|
|
20446
|
-
if (!(0,
|
|
20565
|
+
if (!(0, import_node_fs24.existsSync)(statePath)) return false;
|
|
20447
20566
|
try {
|
|
20448
|
-
const state = JSON.parse((0,
|
|
20567
|
+
const state = JSON.parse((0, import_node_fs24.readFileSync)(statePath, "utf8"));
|
|
20449
20568
|
const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
|
|
20450
20569
|
return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
|
|
20451
20570
|
} catch {
|
|
@@ -20534,12 +20653,15 @@ function parseRestPrSnapshot(json) {
|
|
|
20534
20653
|
async function fetchRestPrSnapshot(prNumber, repo, gh = defaultGhApi) {
|
|
20535
20654
|
return parseRestPrSnapshot(JSON.parse(await gh([`repos/${repo}/pulls/${prNumber}`])));
|
|
20536
20655
|
}
|
|
20656
|
+
async function fetchHeadCheckRuns(headSha, repo, gh) {
|
|
20657
|
+
const runsOut = await gh(["--paginate", `repos/${repo}/commits/${headSha}/check-runs?per_page=100`, "--jq", ".check_runs[] | {id, name, status, conclusion, app_id: .app.id}"]);
|
|
20658
|
+
return dedupeLatestCheckRuns(parseNdjsonLines(runsOut));
|
|
20659
|
+
}
|
|
20537
20660
|
async function fetchHeadCheckBuckets(headSha, repo, gh) {
|
|
20538
|
-
const [
|
|
20539
|
-
|
|
20661
|
+
const [runs, statusesOut] = await Promise.all([
|
|
20662
|
+
fetchHeadCheckRuns(headSha, repo, gh),
|
|
20540
20663
|
gh(["--paginate", `repos/${repo}/commits/${headSha}/status?per_page=100`, "--jq", ".statuses[] | {context, state}"])
|
|
20541
20664
|
]);
|
|
20542
|
-
const runs = dedupeLatestCheckRuns(parseNdjsonLines(runsOut));
|
|
20543
20665
|
const statuses = parseNdjsonLines(statusesOut);
|
|
20544
20666
|
return [...runs.map(classifyCheckRun), ...statuses.map((s) => classifyCommitStatus(s.state))];
|
|
20545
20667
|
}
|
|
@@ -20561,6 +20683,48 @@ async function pollRestPrMerged(prNumber, repo, gh = defaultGhApi) {
|
|
|
20561
20683
|
return false;
|
|
20562
20684
|
}
|
|
20563
20685
|
}
|
|
20686
|
+
var CI_BUDGET_ANNOTATION_TITLE = "CI budget exceeded";
|
|
20687
|
+
function isErrorAnnotation(a) {
|
|
20688
|
+
const level = a.annotation_level?.toLowerCase();
|
|
20689
|
+
return level === "failure" || level === "error";
|
|
20690
|
+
}
|
|
20691
|
+
function classifyFailedChecks(failing) {
|
|
20692
|
+
const budgetKilled = [];
|
|
20693
|
+
const otherFailures = [];
|
|
20694
|
+
for (const check of failing) {
|
|
20695
|
+
const errors = check.annotations.filter(isErrorAnnotation);
|
|
20696
|
+
const allBudget = errors.length > 0 && errors.every((a) => a.title?.trim() === CI_BUDGET_ANNOTATION_TITLE);
|
|
20697
|
+
(allBudget ? budgetKilled : otherFailures).push(check.name);
|
|
20698
|
+
}
|
|
20699
|
+
if (budgetKilled.length && !otherFailures.length) {
|
|
20700
|
+
return {
|
|
20701
|
+
cause: "ci-budget-kill",
|
|
20702
|
+
budgetKilled,
|
|
20703
|
+
otherFailures,
|
|
20704
|
+
reason: `${budgetKilled.length} check(s) were KILLED for wall-clock, not failed: ${budgetKilled.join(", ")}. No test failed \u2014 this is runner contention (concurrent gates sharing one runner). Rerun once the other gates drain (gh run rerun --failed); do not debug the diff.`
|
|
20705
|
+
};
|
|
20706
|
+
}
|
|
20707
|
+
return {
|
|
20708
|
+
cause: "checks-failure",
|
|
20709
|
+
budgetKilled,
|
|
20710
|
+
otherFailures,
|
|
20711
|
+
reason: budgetKilled.length ? `checks failed: ${otherFailures.join(", ")}; separately, ${budgetKilled.join(", ")} was killed for wall-clock, not failed.` : `checks failed: ${otherFailures.join(", ") || "unknown"}.`
|
|
20712
|
+
};
|
|
20713
|
+
}
|
|
20714
|
+
async function diagnoseFailedRestChecks(prNumber, repo, gh = defaultGhApi) {
|
|
20715
|
+
try {
|
|
20716
|
+
const snapshot = await fetchRestPrSnapshot(prNumber, repo, gh);
|
|
20717
|
+
const failing = (await fetchHeadCheckRuns(snapshot.headSha, repo, gh)).filter((run) => classifyCheckRun(run) === "fail" && typeof run.id === "number");
|
|
20718
|
+
if (!failing.length) return null;
|
|
20719
|
+
const annotated = await Promise.all(failing.map(async (run) => ({
|
|
20720
|
+
name: run.name ?? `check-run ${run.id}`,
|
|
20721
|
+
annotations: JSON.parse(await gh([`repos/${repo}/check-runs/${run.id}/annotations`]))
|
|
20722
|
+
})));
|
|
20723
|
+
return classifyFailedChecks(annotated);
|
|
20724
|
+
} catch {
|
|
20725
|
+
return null;
|
|
20726
|
+
}
|
|
20727
|
+
}
|
|
20564
20728
|
async function fetchRestCorePool(gh = defaultGhApi) {
|
|
20565
20729
|
try {
|
|
20566
20730
|
const parsed = JSON.parse(await gh(["rate_limit"]));
|
|
@@ -20573,7 +20737,7 @@ async function fetchRestCorePool(gh = defaultGhApi) {
|
|
|
20573
20737
|
}
|
|
20574
20738
|
|
|
20575
20739
|
// src/worktree-lifecycle-commands.ts
|
|
20576
|
-
var
|
|
20740
|
+
var import_node_fs25 = require("node:fs");
|
|
20577
20741
|
var import_node_path22 = require("node:path");
|
|
20578
20742
|
var GH_TIMEOUT_MS = 2e4;
|
|
20579
20743
|
var DEFAULT_BASE = "origin/development";
|
|
@@ -20659,17 +20823,39 @@ function classifyStaleLeaks(input) {
|
|
|
20659
20823
|
}
|
|
20660
20824
|
const knownPaths = new Set(input.worktrees.map((w) => w.path));
|
|
20661
20825
|
for (const dir of input.orphanDirs) {
|
|
20662
|
-
if (
|
|
20663
|
-
|
|
20664
|
-
|
|
20665
|
-
|
|
20666
|
-
|
|
20667
|
-
|
|
20668
|
-
|
|
20669
|
-
|
|
20826
|
+
if (knownPaths.has(dir.path)) continue;
|
|
20827
|
+
leaks.push({
|
|
20828
|
+
kind: "orphan-dir",
|
|
20829
|
+
ref: dir.path,
|
|
20830
|
+
// Say only what the inspection proved. `orphaned-folder` proves the `.git` entry is gone while this
|
|
20831
|
+
// repo's worktree metadata still points at the directory — NOT that the directory is empty; gc
|
|
20832
|
+
// checks contents separately, at apply time.
|
|
20833
|
+
detail: dir.reason === "dangling-gitdir" ? "this repo's worktree directory whose git metadata is gone" : "this repo's worktree metadata points here, but the .git entry is gone",
|
|
20834
|
+
// gc --apply re-proves ownership before deleting anything; a raw `rm -rf` does not, so it is never
|
|
20835
|
+
// offered here — following it on a mis-classified dir would destroy another repo's live worktree.
|
|
20836
|
+
remediation: "mmi-cli worktree gc --apply"
|
|
20837
|
+
});
|
|
20670
20838
|
}
|
|
20671
20839
|
return leaks;
|
|
20672
20840
|
}
|
|
20841
|
+
var defaultOrphanDirScanDeps = {
|
|
20842
|
+
listDirs: (root) => {
|
|
20843
|
+
try {
|
|
20844
|
+
return (0, import_node_fs25.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path22.join)(root, e.name));
|
|
20845
|
+
} catch {
|
|
20846
|
+
return [];
|
|
20847
|
+
}
|
|
20848
|
+
},
|
|
20849
|
+
inspect: inspectSiblingWorktreeDir
|
|
20850
|
+
};
|
|
20851
|
+
function scanOrphanDirs(worktreesRoot, worktreeGitRoot, deps = defaultOrphanDirScanDeps) {
|
|
20852
|
+
const candidates = [];
|
|
20853
|
+
for (const dir of deps.listDirs(worktreesRoot)) {
|
|
20854
|
+
const { cleanup } = classifySiblingWorktreeDir(deps.inspect(dir, worktreeGitRoot));
|
|
20855
|
+
if (cleanup) candidates.push({ path: dir, reason: cleanup.reason });
|
|
20856
|
+
}
|
|
20857
|
+
return candidates;
|
|
20858
|
+
}
|
|
20673
20859
|
function formatStaleLeaks(leaks) {
|
|
20674
20860
|
if (!leaks.length) return "worktree list --stale: no leaks found";
|
|
20675
20861
|
const lines = [`worktree list --stale: ${leaks.length} leak(s)`];
|
|
@@ -20712,7 +20898,7 @@ function registerWorktreeCommands(program3) {
|
|
|
20712
20898
|
return fail(`worktree land: refusing to land protected branch '${branch}'`, { code: ERROR_CODES.ERR_BAD_ENUM });
|
|
20713
20899
|
}
|
|
20714
20900
|
const gitFile = (0, import_node_path22.join)(wtPath, ".git");
|
|
20715
|
-
const isLinked = (0,
|
|
20901
|
+
const isLinked = (0, import_node_fs25.existsSync)(gitFile) && (0, import_node_fs25.statSync)(gitFile).isFile();
|
|
20716
20902
|
if (apply && !isLinked) {
|
|
20717
20903
|
return fail("worktree land: run from inside the linked worktree you want to land (this is the primary checkout)");
|
|
20718
20904
|
}
|
|
@@ -20801,7 +20987,7 @@ async function gatherWorktreeContext() {
|
|
|
20801
20987
|
const porcelain = (await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout;
|
|
20802
20988
|
const parsed = parseWorktreePorcelain(porcelain);
|
|
20803
20989
|
const primaryPath = parsed[0]?.path ?? repoRoot2;
|
|
20804
|
-
const worktrees = parsed.map((w) => ({ path: w.path, branch: w.branch, primary: w.path === primaryPath }));
|
|
20990
|
+
const worktrees = parsed.map((w) => ({ path: toNativePath(w.path), branch: w.branch, primary: w.path === primaryPath }));
|
|
20805
20991
|
const branchOut = (await execFileP2("git", ["branch", "--format=%(refname:short)"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout;
|
|
20806
20992
|
const localBranches = branchOut.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
20807
20993
|
const currentBranch2 = (await execFileP2("git", ["rev-parse", "--abbrev-ref", "HEAD"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || void 0;
|
|
@@ -20827,18 +21013,10 @@ async function gatherWorktreeContext() {
|
|
|
20827
21013
|
const s = readStageSummary(wt.path);
|
|
20828
21014
|
if (s) stages.push({ path: wt.path, port: s.port });
|
|
20829
21015
|
}
|
|
20830
|
-
const orphanDirs = [];
|
|
20831
21016
|
const wtRoot = siblingMmiWorktreesRoot(repoRoot2);
|
|
20832
|
-
|
|
20833
|
-
|
|
20834
|
-
|
|
20835
|
-
entries = (0, import_node_fs24.readdirSync)(wtRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path22.join)(wtRoot, e.name));
|
|
20836
|
-
} catch {
|
|
20837
|
-
}
|
|
20838
|
-
const known = new Set(worktrees.map((w) => w.path));
|
|
20839
|
-
for (const dir of entries) {
|
|
20840
|
-
if (!known.has(dir)) orphanDirs.push(dir);
|
|
20841
|
-
}
|
|
21017
|
+
let orphanDirs = [];
|
|
21018
|
+
if ((0, import_node_fs25.existsSync)(wtRoot)) {
|
|
21019
|
+
orphanDirs = scanOrphanDirs(wtRoot, await currentRepoWorktreeGitRoot(repoRoot2));
|
|
20842
21020
|
}
|
|
20843
21021
|
return { worktrees, localBranches, currentBranch: currentBranch2, openPrBranches, closedBranches, stages, orphanDirs };
|
|
20844
21022
|
}
|
|
@@ -20857,7 +21035,7 @@ async function bestEffortGit(args, cwd, step, timeoutMs = GIT_TIMEOUT_MS) {
|
|
|
20857
21035
|
}
|
|
20858
21036
|
|
|
20859
21037
|
// src/issue-commands.ts
|
|
20860
|
-
var
|
|
21038
|
+
var import_node_fs26 = require("node:fs");
|
|
20861
21039
|
var import_node_crypto5 = require("node:crypto");
|
|
20862
21040
|
var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
|
|
20863
21041
|
async function editIssue(client, options, deps = {}) {
|
|
@@ -20871,7 +21049,7 @@ async function editIssue(client, options, deps = {}) {
|
|
|
20871
21049
|
if (options.body !== void 0 || options.bodyFile !== void 0) {
|
|
20872
21050
|
let body;
|
|
20873
21051
|
if (options.bodyFile) {
|
|
20874
|
-
const td = deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0,
|
|
21052
|
+
const td = deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs26.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
|
|
20875
21053
|
body = await resolveIssueBody({ body: options.body, bodyFile: options.bodyFile }, td);
|
|
20876
21054
|
} else {
|
|
20877
21055
|
body = options.body ?? "";
|
|
@@ -21136,6 +21314,7 @@ function validateBatchSpecs(specs) {
|
|
|
21136
21314
|
async function createIssuesBatch(specs, options, deps = {}) {
|
|
21137
21315
|
const ensureLabels = deps.ensureLabels ?? ensureLabelsExist;
|
|
21138
21316
|
const client = deps.client ?? defaultGitHubClient();
|
|
21317
|
+
const runCreate = deps.runCreate ?? (async (args) => (await execFileP2("gh", args, { timeout: GH_MUTATION_TIMEOUT_MS })).stdout);
|
|
21139
21318
|
const validation = validateBatchSpecs(specs);
|
|
21140
21319
|
if (!validation.ok) {
|
|
21141
21320
|
const lines = validation.errors.map((e) => ` row ${e.row}: ${e.error}`).join("\n");
|
|
@@ -21172,20 +21351,33 @@ ${lines}`);
|
|
|
21172
21351
|
if (rowKey) {
|
|
21173
21352
|
body = appendIdempotencyMarker(body, rowKey);
|
|
21174
21353
|
}
|
|
21175
|
-
const
|
|
21354
|
+
const rowPriority = spec.priority ? priority : options.priority ?? priority;
|
|
21355
|
+
const args = buildIssueArgs({ type, title: spec.title, body, priority: rowPriority, repo: rowRepo(spec), labels: spec.labels });
|
|
21176
21356
|
const swapped = await bodyArgsViaFile(args);
|
|
21357
|
+
let row;
|
|
21177
21358
|
try {
|
|
21178
|
-
const
|
|
21359
|
+
const stdout = await runCreate(swapped.args);
|
|
21179
21360
|
const result = parseCreatedUrl(stdout);
|
|
21180
|
-
|
|
21361
|
+
row = { row: entry.row, number: result.number, url: result.url };
|
|
21362
|
+
created.push(row);
|
|
21181
21363
|
} finally {
|
|
21182
21364
|
await swapped.cleanup();
|
|
21183
21365
|
}
|
|
21184
|
-
if (
|
|
21366
|
+
if (deps.attach) {
|
|
21367
|
+
const attached = await deps.attach(row.number, rowRepo(spec), rowPriority);
|
|
21368
|
+
row.onBoard = attached.onBoard;
|
|
21369
|
+
if (attached.projectItemId) row.projectItemId = attached.projectItemId;
|
|
21370
|
+
}
|
|
21371
|
+
const parentRef = spec.parent ?? options.parent;
|
|
21372
|
+
if (parentRef) {
|
|
21185
21373
|
try {
|
|
21186
21374
|
const run = deps.runGh ?? ghRunner;
|
|
21187
|
-
await linkSubIssue(run,
|
|
21188
|
-
} catch {
|
|
21375
|
+
row.parent = await linkSubIssue(run, parentRef, row.url, rowRepo(spec));
|
|
21376
|
+
} catch (e) {
|
|
21377
|
+
const err = e;
|
|
21378
|
+
row.parentLinkError = (err.stderr || err.message || String(e)).trim();
|
|
21379
|
+
process.stderr.write(`warning: issue #${row.number} created but NOT linked under ${parentRef}: ${row.parentLinkError}
|
|
21380
|
+
`);
|
|
21189
21381
|
}
|
|
21190
21382
|
}
|
|
21191
21383
|
} catch (e) {
|
|
@@ -21218,7 +21410,7 @@ function parseCloseReason(raw) {
|
|
|
21218
21410
|
if (trimmed === "duplicateof" || trimmed === "duplicate") return { reason: "duplicate-of" };
|
|
21219
21411
|
throw new Error(`unknown close reason "${raw}" \u2014 expected completed, not-planned, or duplicate-of`);
|
|
21220
21412
|
}
|
|
21221
|
-
function registerIssueLifecycleCommands(program3) {
|
|
21413
|
+
function registerIssueLifecycleCommands(program3, deps = {}) {
|
|
21222
21414
|
const issue2 = program3.commands.find((c) => c.name() === "issue");
|
|
21223
21415
|
if (!issue2) return;
|
|
21224
21416
|
mutating(
|
|
@@ -21341,9 +21533,9 @@ function registerIssueLifecycleCommands(program3) {
|
|
|
21341
21533
|
return failGraceful(`issue unlink-child: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
|
|
21342
21534
|
}
|
|
21343
21535
|
});
|
|
21344
|
-
extendCreateCommand(issue2);
|
|
21536
|
+
extendCreateCommand(issue2, deps.attach);
|
|
21345
21537
|
}
|
|
21346
|
-
function extendCreateCommand(issue2) {
|
|
21538
|
+
function extendCreateCommand(issue2, batchAttach) {
|
|
21347
21539
|
const createCmd = issue2.commands.find((c) => c.name() === "create");
|
|
21348
21540
|
if (!createCmd) return;
|
|
21349
21541
|
createCmd.option("--batch <file.json>", "create multiple issues from a JSON array file (pre-validates all rows before creating any)").option("--idempotency-key <key>", "create-if-not-exists: skip creation when an issue with this key already exists (retried loops must not duplicate)");
|
|
@@ -21354,7 +21546,7 @@ function extendCreateCommand(issue2) {
|
|
|
21354
21546
|
if (opts.batch) {
|
|
21355
21547
|
let specs;
|
|
21356
21548
|
try {
|
|
21357
|
-
const raw = (0,
|
|
21549
|
+
const raw = (0, import_node_fs26.readFileSync)(opts.batch, "utf8");
|
|
21358
21550
|
specs = JSON.parse(raw);
|
|
21359
21551
|
if (!Array.isArray(specs)) throw new Error("batch file must contain a JSON array");
|
|
21360
21552
|
} catch (e) {
|
|
@@ -21366,13 +21558,27 @@ function extendCreateCommand(issue2) {
|
|
|
21366
21558
|
return fail(`issue create --batch: validation failed (${validation.errors.length} error(s)):
|
|
21367
21559
|
${lines}`);
|
|
21368
21560
|
}
|
|
21561
|
+
const batchParent = opts.parent;
|
|
21562
|
+
const batchPriority = opts.priority ? normalizePriority(opts.priority) : void 0;
|
|
21369
21563
|
if (opts.dryRun || opts.validateOnly) {
|
|
21370
|
-
const planned = validation.validated.map((v) => ({
|
|
21564
|
+
const planned = validation.validated.map((v) => ({
|
|
21565
|
+
row: v.row,
|
|
21566
|
+
type: v.type,
|
|
21567
|
+
title: v.spec.title,
|
|
21568
|
+
priority: v.spec.priority ? v.priority : batchPriority ?? v.priority,
|
|
21569
|
+
repo: v.spec.repo,
|
|
21570
|
+
...v.spec.parent ?? batchParent ? { parent: v.spec.parent ?? batchParent } : {}
|
|
21571
|
+
}));
|
|
21371
21572
|
console.log(JSON.stringify(opts.validateOnly ? { ok: true, planned } : { dry_run: true, planned }));
|
|
21372
21573
|
return;
|
|
21373
21574
|
}
|
|
21374
21575
|
try {
|
|
21375
|
-
const result = await createIssuesBatch(specs, {
|
|
21576
|
+
const result = await createIssuesBatch(specs, {
|
|
21577
|
+
repo: opts.repo,
|
|
21578
|
+
idempotencyKey: opts.idempotencyKey,
|
|
21579
|
+
parent: batchParent,
|
|
21580
|
+
priority: batchPriority
|
|
21581
|
+
}, { attach: batchAttach });
|
|
21376
21582
|
console.log(JSON.stringify(result));
|
|
21377
21583
|
if (result.failures.length) process.exitCode = 1;
|
|
21378
21584
|
} catch (e) {
|
|
@@ -21398,11 +21604,11 @@ ${lines}`);
|
|
|
21398
21604
|
}
|
|
21399
21605
|
|
|
21400
21606
|
// src/train-commands.ts
|
|
21401
|
-
var
|
|
21607
|
+
var import_node_fs28 = require("node:fs");
|
|
21402
21608
|
var import_node_path24 = require("node:path");
|
|
21403
21609
|
|
|
21404
21610
|
// src/plugin-guard-io.ts
|
|
21405
|
-
var
|
|
21611
|
+
var import_node_fs27 = require("node:fs");
|
|
21406
21612
|
var import_node_path23 = require("node:path");
|
|
21407
21613
|
var import_node_os6 = require("node:os");
|
|
21408
21614
|
|
|
@@ -21537,7 +21743,7 @@ var installedPluginsPath2 = (surface = detectSurface(process.env)) => {
|
|
|
21537
21743
|
};
|
|
21538
21744
|
function readInstalledPlugins(surface = detectSurface(process.env)) {
|
|
21539
21745
|
try {
|
|
21540
|
-
return JSON.parse((0,
|
|
21746
|
+
return JSON.parse((0, import_node_fs27.readFileSync)(installedPluginsPath2(surface), "utf8"));
|
|
21541
21747
|
} catch {
|
|
21542
21748
|
return null;
|
|
21543
21749
|
}
|
|
@@ -21551,7 +21757,7 @@ function marketplaceCloneCandidates(surface, home) {
|
|
|
21551
21757
|
}
|
|
21552
21758
|
return [(0, import_node_path23.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
|
|
21553
21759
|
}
|
|
21554
|
-
function marketplaceClonePresent(surface, home, exists =
|
|
21760
|
+
function marketplaceClonePresent(surface, home, exists = import_node_fs27.existsSync) {
|
|
21555
21761
|
return marketplaceCloneCandidates(surface, home).some(exists);
|
|
21556
21762
|
}
|
|
21557
21763
|
async function fetchNpmReleasedVersion() {
|
|
@@ -21578,7 +21784,7 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
|
|
|
21578
21784
|
isOrgRepo,
|
|
21579
21785
|
installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
|
|
21580
21786
|
marketplaceClonePresent: marketplaceClonePresent(surface, (0, import_node_os6.homedir)()),
|
|
21581
|
-
pluginCachePresent: (0,
|
|
21787
|
+
pluginCachePresent: (0, import_node_fs27.existsSync)((0, import_node_path23.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
|
|
21582
21788
|
};
|
|
21583
21789
|
}
|
|
21584
21790
|
function claudePluginGuardState(isOrgRepo) {
|
|
@@ -21713,7 +21919,7 @@ function formatTrainStatus(r) {
|
|
|
21713
21919
|
// src/train-commands.ts
|
|
21714
21920
|
function readRepoVersion() {
|
|
21715
21921
|
try {
|
|
21716
|
-
return JSON.parse((0,
|
|
21922
|
+
return JSON.parse((0, import_node_fs28.readFileSync)((0, import_node_path24.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
21717
21923
|
} catch {
|
|
21718
21924
|
return void 0;
|
|
21719
21925
|
}
|
|
@@ -21924,12 +22130,14 @@ async function recommendNext(deps) {
|
|
|
21924
22130
|
const load = deps?.loadConfig ?? loadConfig;
|
|
21925
22131
|
const reader = deps?.readBoard ?? readBoard;
|
|
21926
22132
|
const cfg = await load();
|
|
21927
|
-
if (!cfg.sagaApiUrl)
|
|
22133
|
+
if (!cfg.sagaApiUrl) throw new Error("Hub API URL not configured \u2014 the board was NOT read (run `mmi-cli doctor`)");
|
|
21928
22134
|
let report;
|
|
21929
22135
|
try {
|
|
21930
22136
|
report = await reader({ config: cfg });
|
|
21931
|
-
} catch {
|
|
21932
|
-
|
|
22137
|
+
} catch (e) {
|
|
22138
|
+
throw new Error(
|
|
22139
|
+
`board unreachable \u2014 ${e.message}. This is NOT an empty board: confirm the ACTIVE gh account can see this repo (\`gh auth status\`, \`gh repo view <owner/repo>\`; \`gh auth switch --user <other>\` when more than one account is present).`
|
|
22140
|
+
);
|
|
21933
22141
|
}
|
|
21934
22142
|
const claimable = report.primary.claimable;
|
|
21935
22143
|
if (!claimable.length) return null;
|
|
@@ -22003,12 +22211,16 @@ async function collectOnboardStatus() {
|
|
|
22003
22211
|
} else if (!board.ok) {
|
|
22004
22212
|
nextCommand = "mmi-cli board read";
|
|
22005
22213
|
} else {
|
|
22006
|
-
|
|
22007
|
-
|
|
22008
|
-
|
|
22009
|
-
|
|
22010
|
-
|
|
22214
|
+
let top = null;
|
|
22215
|
+
let readError;
|
|
22216
|
+
try {
|
|
22217
|
+
top = await recommendNext();
|
|
22218
|
+
} catch (e) {
|
|
22219
|
+
readError = e.message;
|
|
22011
22220
|
}
|
|
22221
|
+
if (readError) nextCommand = `mmi-cli board read \u2014 ${readError}`;
|
|
22222
|
+
else if (top) nextCommand = `mmi-cli board claim ${top.number} # ${top.title}`;
|
|
22223
|
+
else nextCommand = "mmi-cli board read \u2014 no claimable items found";
|
|
22012
22224
|
}
|
|
22013
22225
|
return { track, board, registry: registry2, secrets, nextCommand };
|
|
22014
22226
|
}
|
|
@@ -22033,7 +22245,7 @@ function registerDiscoveryCommands(program3) {
|
|
|
22033
22245
|
try {
|
|
22034
22246
|
const item = await recommendNext();
|
|
22035
22247
|
if (!item) {
|
|
22036
|
-
console.log("No claimable board items found.");
|
|
22248
|
+
console.log("No claimable board items found (board read OK).");
|
|
22037
22249
|
return;
|
|
22038
22250
|
}
|
|
22039
22251
|
const priorityNote = item.priority ? ` [${item.priority}]` : "";
|
|
@@ -22800,14 +23012,30 @@ function doctorReportExitCode(checks) {
|
|
|
22800
23012
|
// src/doctor-clean.ts
|
|
22801
23013
|
function checkGithubAuth(probe) {
|
|
22802
23014
|
const login = probe.login?.trim();
|
|
22803
|
-
const
|
|
23015
|
+
const authed = Boolean(login);
|
|
23016
|
+
const reach = probe.reach;
|
|
23017
|
+
if (authed && reach?.repo && reach.reachable === false) {
|
|
23018
|
+
const rescuers = reach.otherAccountsThatCanSee ?? [];
|
|
23019
|
+
return {
|
|
23020
|
+
ok: false,
|
|
23021
|
+
label: "github auth",
|
|
23022
|
+
fix: rescuers.length ? `the active gh account (${login}) cannot see ${reach.repo}; another account is logged in \u2014 try: gh auth switch --user ${rescuers[0]}` : `the active gh account (${login}) cannot see ${reach.repo} \u2014 check org membership, or: gh auth login`,
|
|
23023
|
+
verbose: [
|
|
23024
|
+
`gh installed: ${probe.ghInstalled ? "yes" : "no"}`,
|
|
23025
|
+
`authenticated as: ${login}`,
|
|
23026
|
+
`can resolve ${reach.repo}: NO`,
|
|
23027
|
+
rescuers.length ? `other gh accounts logged in: ${rescuers.join(", ")} \u2014 switching is the usual fix` : "no other gh account is logged in, so this is not a wrong-account mix-up"
|
|
23028
|
+
]
|
|
23029
|
+
};
|
|
23030
|
+
}
|
|
22804
23031
|
return {
|
|
22805
|
-
ok,
|
|
23032
|
+
ok: authed,
|
|
22806
23033
|
label: "github auth",
|
|
22807
23034
|
fix: probe.ghInstalled ? 'run: gh auth login --hostname github.com --git-protocol https --web --scopes "project"' : "install GitHub CLI (https://cli.github.com), then: gh auth login",
|
|
22808
23035
|
verbose: [
|
|
22809
23036
|
`gh installed: ${probe.ghInstalled ? "yes" : "no"}`,
|
|
22810
|
-
`authenticated as: ${login || "(none)"}
|
|
23037
|
+
`authenticated as: ${login || "(none)"}`,
|
|
23038
|
+
...reach?.repo ? [`can resolve ${reach.repo}: ${reach.reachable === true ? "yes" : "not probed"}`] : []
|
|
22811
23039
|
]
|
|
22812
23040
|
};
|
|
22813
23041
|
}
|
|
@@ -22980,17 +23208,22 @@ function gcReapable(plan) {
|
|
|
22980
23208
|
}
|
|
22981
23209
|
async function runDoctorClean(opts, io, deps) {
|
|
22982
23210
|
const apply = Boolean(opts.apply);
|
|
22983
|
-
const [login, isOrgRepo, released, callerArn] = await Promise.all([
|
|
23211
|
+
const [login, isOrgRepo, released, callerArn, reach] = await Promise.all([
|
|
22984
23212
|
deps.githubLogin(),
|
|
22985
23213
|
deps.isOrgRepo(),
|
|
22986
23214
|
opts.fast ? Promise.resolve(void 0) : deps.releasedVersion(),
|
|
22987
|
-
opts.fast ? Promise.resolve(void 0) : deps.awsCallerArn()
|
|
23215
|
+
opts.fast ? Promise.resolve(void 0) : deps.awsCallerArn(),
|
|
23216
|
+
// #3365. Skipped on the banner/preflight lanes, which run on every SessionStart and must not pay a
|
|
23217
|
+
// network round-trip — and on a bare `--fast`, whose contract is offline-safe. It DOES run on
|
|
23218
|
+
// `--self`, which rides the fast lane but is the interactive "is my setup actually fine?" diagnostic,
|
|
23219
|
+
// and is exactly where the wrong-active-account trap has to be caught. Fail-soft either way.
|
|
23220
|
+
opts.banner || opts.preflight || opts.fast && !opts.self ? Promise.resolve(void 0) : deps.githubRepoReach?.() ?? Promise.resolve(void 0)
|
|
22988
23221
|
]);
|
|
22989
23222
|
const ghInstalled2 = login ? true : await deps.ghInstalled();
|
|
22990
23223
|
const installed = deps.installedPluginVersion();
|
|
22991
23224
|
const checks = [];
|
|
22992
23225
|
let restartPending = false;
|
|
22993
|
-
checks.push(checkGithubAuth({ login, ghInstalled: ghInstalled2 }));
|
|
23226
|
+
checks.push(checkGithubAuth({ login, ghInstalled: ghInstalled2, reach }));
|
|
22994
23227
|
if (!opts.fast && !opts.banner && !opts.preflight && deps.githubPools) {
|
|
22995
23228
|
checks.push(...checkGithubPools(await deps.githubPools()));
|
|
22996
23229
|
}
|
|
@@ -23164,8 +23397,86 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
23164
23397
|
return exitCode;
|
|
23165
23398
|
}
|
|
23166
23399
|
|
|
23400
|
+
// src/gh-account-visibility.ts
|
|
23401
|
+
function parseGhAuthAccounts(stdout) {
|
|
23402
|
+
const accounts = [];
|
|
23403
|
+
for (const raw of stdout.split(/\r?\n/)) {
|
|
23404
|
+
const match = /Logged in to \S+ (?:account|as)\s+([A-Za-z0-9-]+)/.exec(raw);
|
|
23405
|
+
if (!match?.[1]) continue;
|
|
23406
|
+
accounts.push({ login: match[1], active: /\(active\)/.test(raw) });
|
|
23407
|
+
}
|
|
23408
|
+
return accounts;
|
|
23409
|
+
}
|
|
23410
|
+
function parseOriginRepo(remoteUrl) {
|
|
23411
|
+
const url = remoteUrl.trim();
|
|
23412
|
+
if (!url) return void 0;
|
|
23413
|
+
const match = /github\.com[/:]([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/.exec(url);
|
|
23414
|
+
if (!match) return void 0;
|
|
23415
|
+
return `${match[1]}/${match[2]}`;
|
|
23416
|
+
}
|
|
23417
|
+
function ghHostsConfigPath(env, platform2) {
|
|
23418
|
+
const sep2 = platform2 === "win32" ? "\\" : "/";
|
|
23419
|
+
const join24 = (...parts) => parts.join(sep2);
|
|
23420
|
+
const explicit = env.GH_CONFIG_DIR?.trim();
|
|
23421
|
+
if (explicit) return join24(explicit, "hosts.yml");
|
|
23422
|
+
if (platform2 === "win32") {
|
|
23423
|
+
const appData = (env.AppData ?? env.APPDATA)?.trim();
|
|
23424
|
+
return appData ? join24(appData, "GitHub CLI", "hosts.yml") : void 0;
|
|
23425
|
+
}
|
|
23426
|
+
const xdg = env.XDG_CONFIG_HOME?.trim();
|
|
23427
|
+
if (xdg) return join24(xdg, "gh", "hosts.yml");
|
|
23428
|
+
const home = env.HOME?.trim();
|
|
23429
|
+
return home ? join24(home, ".config", "gh", "hosts.yml") : void 0;
|
|
23430
|
+
}
|
|
23431
|
+
function parseGhHostsAccounts(yaml, host = "github.com") {
|
|
23432
|
+
let hostIndent = null;
|
|
23433
|
+
let usersIndent = null;
|
|
23434
|
+
let loginIndent = null;
|
|
23435
|
+
let activeLogin;
|
|
23436
|
+
const logins = [];
|
|
23437
|
+
for (const raw of yaml.split(/\r?\n/)) {
|
|
23438
|
+
if (!raw.trim() || raw.trimStart().startsWith("#")) continue;
|
|
23439
|
+
const indent = raw.length - raw.trimStart().length;
|
|
23440
|
+
const keyMatch = /^"?([A-Za-z0-9._-]+)"?\s*:(.*)$/.exec(raw.trim());
|
|
23441
|
+
if (!keyMatch) continue;
|
|
23442
|
+
const key = keyMatch[1];
|
|
23443
|
+
if (hostIndent === null) {
|
|
23444
|
+
if (key === host) hostIndent = indent;
|
|
23445
|
+
continue;
|
|
23446
|
+
}
|
|
23447
|
+
if (indent <= hostIndent) break;
|
|
23448
|
+
if (usersIndent !== null && indent > usersIndent) {
|
|
23449
|
+
if (loginIndent === null) loginIndent = indent;
|
|
23450
|
+
if (indent === loginIndent && !/token/i.test(key)) logins.push(key);
|
|
23451
|
+
continue;
|
|
23452
|
+
}
|
|
23453
|
+
usersIndent = null;
|
|
23454
|
+
loginIndent = null;
|
|
23455
|
+
if (key === "users") {
|
|
23456
|
+
usersIndent = indent;
|
|
23457
|
+
continue;
|
|
23458
|
+
}
|
|
23459
|
+
if (key === "user") {
|
|
23460
|
+
const value = keyMatch[2].trim().replace(/^["']|["']$/g, "");
|
|
23461
|
+
if (value) activeLogin = value;
|
|
23462
|
+
}
|
|
23463
|
+
}
|
|
23464
|
+
const unique = [...new Set(logins)];
|
|
23465
|
+
if (!unique.length && activeLogin) unique.push(activeLogin);
|
|
23466
|
+
return unique.map((login) => ({ login, active: login === activeLogin }));
|
|
23467
|
+
}
|
|
23468
|
+
function ghAccountCaveat(announcedLogin, accounts) {
|
|
23469
|
+
if (accounts.length <= 1) return void 0;
|
|
23470
|
+
const active = accounts.find((a) => a.active)?.login;
|
|
23471
|
+
if (active && active !== announcedLogin) {
|
|
23472
|
+
return `NOTE: gh's active account is ${active}, not ${announcedLogin} \u2014 repo reads and pushes use ${active}. A repo that reads as "not found" is that mismatch, not lost access: gh auth switch --user ${announcedLogin}`;
|
|
23473
|
+
}
|
|
23474
|
+
const others = accounts.filter((a) => a.login !== active).map((a) => a.login);
|
|
23475
|
+
return `NOTE: gh has ${accounts.length} accounts (active: ${active ?? "unknown"}; also ${others.join(", ")}) \u2014 a repo that reads as "not found" is usually the wrong active account, not lost access: gh auth status, then gh auth switch`;
|
|
23476
|
+
}
|
|
23477
|
+
|
|
23167
23478
|
// src/doctor-io.ts
|
|
23168
|
-
var
|
|
23479
|
+
var import_node_fs29 = require("node:fs");
|
|
23169
23480
|
var import_node_os7 = require("node:os");
|
|
23170
23481
|
var import_node_path25 = require("node:path");
|
|
23171
23482
|
var import_node_child_process12 = require("node:child_process");
|
|
@@ -23175,7 +23486,7 @@ var MMI_PLUGIN_ID2 = "mmi@mutmutco";
|
|
|
23175
23486
|
function installedClaudePluginVersion() {
|
|
23176
23487
|
try {
|
|
23177
23488
|
const file = JSON.parse(
|
|
23178
|
-
(0,
|
|
23489
|
+
(0, import_node_fs29.readFileSync)((0, import_node_path25.join)((0, import_node_os7.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
|
|
23179
23490
|
);
|
|
23180
23491
|
const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
|
|
23181
23492
|
if (versions.length === 0) return void 0;
|
|
@@ -23202,7 +23513,7 @@ function readGitignore() {
|
|
|
23202
23513
|
const path2 = gitignorePath();
|
|
23203
23514
|
if (path2 === null) return null;
|
|
23204
23515
|
try {
|
|
23205
|
-
return (0,
|
|
23516
|
+
return (0, import_node_fs29.readFileSync)(path2, "utf8");
|
|
23206
23517
|
} catch {
|
|
23207
23518
|
return null;
|
|
23208
23519
|
}
|
|
@@ -23211,7 +23522,7 @@ function writeGitignore(content) {
|
|
|
23211
23522
|
const path2 = gitignorePath();
|
|
23212
23523
|
if (path2 === null) return false;
|
|
23213
23524
|
try {
|
|
23214
|
-
(0,
|
|
23525
|
+
(0, import_node_fs29.writeFileSync)(path2, content, "utf8");
|
|
23215
23526
|
return true;
|
|
23216
23527
|
} catch {
|
|
23217
23528
|
return false;
|
|
@@ -23235,7 +23546,7 @@ async function repoRoot() {
|
|
|
23235
23546
|
}
|
|
23236
23547
|
function hasRepoLocalWorktrees() {
|
|
23237
23548
|
const root = worktreeRootSync();
|
|
23238
|
-
return root !== null && (0,
|
|
23549
|
+
return root !== null && (0, import_node_fs29.existsSync)((0, import_node_path25.join)(root, ".worktrees"));
|
|
23239
23550
|
}
|
|
23240
23551
|
|
|
23241
23552
|
// src/index.ts
|
|
@@ -23256,10 +23567,31 @@ async function readDocsAuditFetch(repo) {
|
|
|
23256
23567
|
verdict: row ? { repo: row.repo, date: row.date, shaRange: row.shaRange, outcome: row.outcome, checkerVendor: row.checkerVendor } : null
|
|
23257
23568
|
};
|
|
23258
23569
|
}
|
|
23570
|
+
async function githubRepoReachProbe() {
|
|
23571
|
+
const remote = await execFileP2("git", ["remote", "get-url", "origin"], { timeout: GIT_TIMEOUT_MS }).then((r) => r.stdout).catch(() => "");
|
|
23572
|
+
const repo = parseOriginRepo(remote);
|
|
23573
|
+
if (!repo) return void 0;
|
|
23574
|
+
const reachable = await execFileP2("gh", ["repo", "view", repo, "--json", "name"], { timeout: GH_MUTATION_TIMEOUT_MS }).then(() => true).catch(() => false);
|
|
23575
|
+
if (reachable) return { repo, reachable: true };
|
|
23576
|
+
const status = await execFileP2("gh", ["auth", "status"], { timeout: GH_MUTATION_TIMEOUT_MS }).then((r) => `${r.stdout}
|
|
23577
|
+
${r.stderr ?? ""}`).catch(() => "");
|
|
23578
|
+
const others = parseGhAuthAccounts(status).filter((a) => !a.active).map((a) => a.login);
|
|
23579
|
+
return { repo, reachable: false, otherAccountsThatCanSee: others };
|
|
23580
|
+
}
|
|
23581
|
+
function ghMultiAccountCaveat(announcedLogin) {
|
|
23582
|
+
try {
|
|
23583
|
+
const hostsPath = ghHostsConfigPath(process.env, process.platform);
|
|
23584
|
+
if (!hostsPath || !(0, import_node_fs30.existsSync)(hostsPath)) return void 0;
|
|
23585
|
+
return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs30.readFileSync)(hostsPath, "utf8")));
|
|
23586
|
+
} catch {
|
|
23587
|
+
return void 0;
|
|
23588
|
+
}
|
|
23589
|
+
}
|
|
23259
23590
|
function mmiDoctorDeps() {
|
|
23260
23591
|
return {
|
|
23261
23592
|
githubLogin,
|
|
23262
23593
|
ghInstalled,
|
|
23594
|
+
githubRepoReach: githubRepoReachProbe,
|
|
23263
23595
|
awsCallerArn,
|
|
23264
23596
|
isOrgRepo: () => isOrgRepoRoot(),
|
|
23265
23597
|
installedPluginVersion: installedClaudePluginVersion,
|
|
@@ -23463,18 +23795,18 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
|
23463
23795
|
var rules = program2.command("rules").description("org-managed .gitignore delivery");
|
|
23464
23796
|
rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").option("--json", "machine-readable output").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
|
|
23465
23797
|
const path2 = (0, import_node_path26.join)(process.cwd(), ".gitignore");
|
|
23466
|
-
const current = (0,
|
|
23798
|
+
const current = (0, import_node_fs30.existsSync)(path2) ? (0, import_node_fs30.readFileSync)(path2, "utf8") : null;
|
|
23467
23799
|
const plan = planManagedGitignore(current);
|
|
23468
23800
|
const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
|
|
23469
23801
|
if (opts.json) {
|
|
23470
|
-
if (opts.write && plan.changed) (0,
|
|
23802
|
+
if (opts.write && plan.changed) (0, import_node_fs30.writeFileSync)(path2, plan.content, "utf8");
|
|
23471
23803
|
console.log(JSON.stringify(plan, null, 2));
|
|
23472
23804
|
if (!opts.write && plan.changed) process.exitCode = 1;
|
|
23473
23805
|
return;
|
|
23474
23806
|
}
|
|
23475
23807
|
if (opts.write) {
|
|
23476
23808
|
if (plan.changed) {
|
|
23477
|
-
(0,
|
|
23809
|
+
(0, import_node_fs30.writeFileSync)(path2, plan.content, "utf8");
|
|
23478
23810
|
console.log(`mmi-cli org rules gitignore: updated .gitignore (${drift})`);
|
|
23479
23811
|
} else {
|
|
23480
23812
|
console.log("mmi-cli org rules gitignore: up to date");
|
|
@@ -23600,7 +23932,7 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
|
|
|
23600
23932
|
if (!Number.isFinite(limit) || limit < 1) return fail("worktree gc: --limit must be a positive integer");
|
|
23601
23933
|
try {
|
|
23602
23934
|
const plan = await gcPlan(o.remote, limit);
|
|
23603
|
-
if (o.apply && !o.json) console.log(formatGcPlan(plan,
|
|
23935
|
+
if (o.apply && !o.json) console.log(formatGcPlan(plan, true));
|
|
23604
23936
|
let applyResult;
|
|
23605
23937
|
if (o.apply) {
|
|
23606
23938
|
const deferredStore = await createDeferredWorktreeStore();
|
|
@@ -23664,26 +23996,26 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
|
|
|
23664
23996
|
function acquireWorktreeSetupLock(worktreeRoot) {
|
|
23665
23997
|
const lockPath = repoRuntimeStatePath(worktreeRoot, "worktree-setup.lock");
|
|
23666
23998
|
const take = () => {
|
|
23667
|
-
const fd = (0,
|
|
23999
|
+
const fd = (0, import_node_fs30.openSync)(lockPath, "wx");
|
|
23668
24000
|
try {
|
|
23669
|
-
(0,
|
|
24001
|
+
(0, import_node_fs30.writeSync)(fd, String(Date.now()));
|
|
23670
24002
|
} finally {
|
|
23671
|
-
(0,
|
|
24003
|
+
(0, import_node_fs30.closeSync)(fd);
|
|
23672
24004
|
}
|
|
23673
24005
|
return () => {
|
|
23674
24006
|
try {
|
|
23675
|
-
(0,
|
|
24007
|
+
(0, import_node_fs30.rmSync)(lockPath, { force: true });
|
|
23676
24008
|
} catch {
|
|
23677
24009
|
}
|
|
23678
24010
|
};
|
|
23679
24011
|
};
|
|
23680
24012
|
try {
|
|
23681
|
-
(0,
|
|
24013
|
+
(0, import_node_fs30.mkdirSync)((0, import_node_path26.dirname)(lockPath), { recursive: true });
|
|
23682
24014
|
return take();
|
|
23683
24015
|
} catch {
|
|
23684
24016
|
try {
|
|
23685
|
-
if (Date.now() - (0,
|
|
23686
|
-
(0,
|
|
24017
|
+
if (Date.now() - (0, import_node_fs30.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
|
|
24018
|
+
(0, import_node_fs30.rmSync)(lockPath, { force: true });
|
|
23687
24019
|
return take();
|
|
23688
24020
|
}
|
|
23689
24021
|
} catch {
|
|
@@ -23693,7 +24025,7 @@ function acquireWorktreeSetupLock(worktreeRoot) {
|
|
|
23693
24025
|
}
|
|
23694
24026
|
var worktree = program2.command("worktree").description("self-provisioning worktrees \u2014 install deps + copy local-only config");
|
|
23695
24027
|
withExamples(mutating(
|
|
23696
|
-
worktree.command("create <branch-or-issue>").description("create a worktree from a base ref and provision it (install deps + copy local-only config); an issue ref derives the <issue>-<slug> branch, and --claim claims its board item (#2996, formerly worktree new)").option("--from <ref>", "base ref to branch from", "origin/development").option("--base <ref>", "alias of --from (git's own word for the base ref)").option("--path <path>", "worktree path (default: ../mmi-worktrees/<branch>)").option("--remote <name>", "remote to fetch when --from is a <remote>/<branch> ref", "origin").option("--slug <slug>", "issue-ref form: override the slug derived from the issue title").option("--claim", "issue-ref form: claim the issue's board item after provisioning").option("--for <login>", "with --claim: assign the board item to this login instead of @me"),
|
|
24028
|
+
worktree.command("create <branch-or-issue>").description("create a worktree from a base ref and provision it (install deps + copy local-only config); an issue ref derives the <issue>-<slug> branch, and --claim claims its board item (#2996, formerly worktree new)").option("--from <ref>", "base ref to branch from", "origin/development").option("--base <ref>", "alias of --from (git's own word for the base ref)").option("--path <path>", "worktree path (default: ../mmi-worktrees/<branch> \u2014 authoritative; worktree gc/list scan only this root)").option("--remote <name>", "remote to fetch when --from is a <remote>/<branch> ref", "origin").option("--slug <slug>", "issue-ref form: override the slug derived from the issue title").option("--claim", "issue-ref form: claim the issue's board item after provisioning").option("--for <login>", "with --claim: assign the board item to this login instead of @me"),
|
|
23697
24029
|
worktreeCreatePlan
|
|
23698
24030
|
).action(async (target, o, cmd) => {
|
|
23699
24031
|
try {
|
|
@@ -24125,7 +24457,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
24125
24457
|
if (dupe) return fail(`org project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
|
|
24126
24458
|
if (o.secretsFile) {
|
|
24127
24459
|
try {
|
|
24128
|
-
vars.push(`secrets=${(0,
|
|
24460
|
+
vars.push(`secrets=${(0, import_node_fs30.readFileSync)(o.secretsFile, "utf8")}`);
|
|
24129
24461
|
} catch (e) {
|
|
24130
24462
|
return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
|
|
24131
24463
|
}
|
|
@@ -24391,11 +24723,14 @@ function resolveCreatePriority(raw, command) {
|
|
|
24391
24723
|
});
|
|
24392
24724
|
}
|
|
24393
24725
|
}
|
|
24394
|
-
function resolveCreateType(raw, command) {
|
|
24726
|
+
function resolveCreateType(raw, command, labels) {
|
|
24395
24727
|
if (raw === void 0 || raw === "") {
|
|
24396
|
-
|
|
24728
|
+
const nearMiss = labels?.find((l) => ISSUE_TYPES.includes(l));
|
|
24729
|
+
const hint = nearMiss ? ` (you passed --label ${nearMiss} \u2014 did you mean --type ${nearMiss}?)` : "";
|
|
24730
|
+
fail(`${command}: --type is required (bug | feature | task) unless --batch supplies it per row${hint}`, {
|
|
24397
24731
|
code: ERROR_CODES.ERR_MISSING_FLAG,
|
|
24398
|
-
offending_flag: "--type"
|
|
24732
|
+
offending_flag: "--type",
|
|
24733
|
+
...nearMiss ? { corrected_command: `${command} --type ${nearMiss} \u2026` } : {}
|
|
24399
24734
|
});
|
|
24400
24735
|
}
|
|
24401
24736
|
if (!ISSUE_TYPES.includes(raw)) {
|
|
@@ -24409,12 +24744,12 @@ function resolveCreateType(raw, command) {
|
|
|
24409
24744
|
}
|
|
24410
24745
|
var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
|
|
24411
24746
|
withExamples(mutating(
|
|
24412
|
-
issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file,
|
|
24747
|
+
issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file. `-` (stdin) needs a heredoc, which the agent inline-body guard denies (#1473/#2125) \u2014 prefer a real path; a title with backticks or newlines needs --title-file for the same reason (#3381)").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment"),
|
|
24413
24748
|
// --dry-run/--validate-only plan: validate --type + --priority (mirrors the action — a bad enum fails
|
|
24414
24749
|
// ERR_BAD_ENUM, a missing priority defaults to medium) then echo the resolved create intent. Refs
|
|
24415
24750
|
// (`--parent`) and title-source are validated by the action on a real run.
|
|
24416
24751
|
(opts) => {
|
|
24417
|
-
const type = resolveCreateType(opts.type, "issue create");
|
|
24752
|
+
const type = resolveCreateType(opts.type, "issue create", opts.label);
|
|
24418
24753
|
const priority = resolveCreatePriority(opts.priority, "issue create");
|
|
24419
24754
|
return { command: "issue create", type, title: opts.title ?? opts.titleFile, priority, repo: opts.repo };
|
|
24420
24755
|
}
|
|
@@ -24427,7 +24762,7 @@ withExamples(mutating(
|
|
|
24427
24762
|
let extraLabels = [];
|
|
24428
24763
|
let targetRepo2;
|
|
24429
24764
|
try {
|
|
24430
|
-
issueType = resolveCreateType(o.type, "issue create");
|
|
24765
|
+
issueType = resolveCreateType(o.type, "issue create", o.label);
|
|
24431
24766
|
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
|
|
24432
24767
|
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
|
|
24433
24768
|
if (o.idempotencyKey) body = appendIdempotencyMarker(body, o.idempotencyKey);
|
|
@@ -24482,6 +24817,16 @@ withExamples(mutating(
|
|
|
24482
24817
|
"--type is one of bug|feature|task.",
|
|
24483
24818
|
"--dry-run and --validate-only pre-validate the create plan before any issue is written."
|
|
24484
24819
|
]);
|
|
24820
|
+
async function readParentField(number, repo) {
|
|
24821
|
+
let payload;
|
|
24822
|
+
try {
|
|
24823
|
+
payload = await ghJson(["api", `repos/${repo}/issues/${number}`]);
|
|
24824
|
+
} catch (e) {
|
|
24825
|
+
const err = e;
|
|
24826
|
+
return { parentReadError: (err.stderr || err.message || String(e)).trim() };
|
|
24827
|
+
}
|
|
24828
|
+
return resolveParentField(payload);
|
|
24829
|
+
}
|
|
24485
24830
|
issue.command("view <number>").description('read an issue as structured JSON \u2014 the mmi-cli path for non-board issue reads (#2347). --comments folds in every comment; --context also adds linkedPrs + children (the one-shot "load the whole item" read, #2894)').option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "number,title,body,url"').option("--comments", 'include every comment (body + comments in one call) \u2014 the agentic "read the whole item before working it" read (#2894)').option("--context", "full working context in one call: implies --comments and also adds linkedPrs and, for an epic, a children summary (#2894)").action(async (number, o) => {
|
|
24486
24831
|
const n = Number(number);
|
|
24487
24832
|
if (!Number.isInteger(n) || n <= 0) return fail("issue view: <number> must be a positive integer");
|
|
@@ -24492,11 +24837,11 @@ issue.command("view <number>").description('read an issue as structured JSON \u2
|
|
|
24492
24837
|
if (o.comments || o.context) {
|
|
24493
24838
|
const boardProjectId = o.context ? await resolveBoardProjectId(o.repo) : void 0;
|
|
24494
24839
|
const data2 = await runIssueViewContext(queryDeps(), { number: n, repo, comments: Boolean(o.comments), context: Boolean(o.context), jsonFields: fields, boardProjectId });
|
|
24495
|
-
console.log(JSON.stringify(data2));
|
|
24840
|
+
console.log(JSON.stringify({ ...data2, ...await readParentField(n, repo) }));
|
|
24496
24841
|
return;
|
|
24497
24842
|
}
|
|
24498
24843
|
const data = await ghJson(["issue", "view", String(n), "--repo", repo, "--json", fields]);
|
|
24499
|
-
console.log(JSON.stringify(data));
|
|
24844
|
+
console.log(JSON.stringify({ ...data, ...await readParentField(n, repo) }));
|
|
24500
24845
|
} catch (e) {
|
|
24501
24846
|
const err = e;
|
|
24502
24847
|
const raw = (err.stderr || err.message || String(e)).trim();
|
|
@@ -24542,7 +24887,7 @@ issue.command("discover-related").description("find related issues for an existi
|
|
|
24542
24887
|
return fail(`issue discover-related: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
24543
24888
|
}
|
|
24544
24889
|
});
|
|
24545
|
-
issue.command("link-child <parent> <child>").description("link an existing issue as a native sub-issue of a parent and print {parentNumber,subIssueNumber,totalCount} JSON").option("--repo <owner/repo>", "repo for bare refs on either side (defaults to the current repo)").action(async (parentRef, childRef, o) => {
|
|
24890
|
+
jsonParity(issue.command("link-child <parent> <child>").description("link an existing issue as a native sub-issue of a parent and print {parentNumber,subIssueNumber,totalCount} JSON").option("--repo <owner/repo>", "repo for bare refs on either side (defaults to the current repo)")).action(async (parentRef, childRef, o) => {
|
|
24546
24891
|
const defaultRepo = await resolveRepo(o.repo);
|
|
24547
24892
|
try {
|
|
24548
24893
|
const result = await linkSubIssue(ghRunner2, parentRef, childRef, defaultRepo);
|
|
@@ -24553,7 +24898,7 @@ issue.command("link-child <parent> <child>").description("link an existing issue
|
|
|
24553
24898
|
return fail(`issue link-child: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
|
|
24554
24899
|
}
|
|
24555
24900
|
});
|
|
24556
|
-
issue.command("comment <ref>").description("post a Markdown comment to an issue and print {number,repo,url,commentUrl} JSON").option("--body <body>", "comment body (markdown; prefer --body-file for multiline Markdown)").option("--body-file <path|->", "read comment body from a UTF-8 file, or from stdin with -").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)").action(async (ref, o) => {
|
|
24901
|
+
jsonParity(issue.command("comment <ref>").description("post a Markdown comment to an issue and print {number,repo,url,commentUrl} JSON").option("--body <body>", "comment body (markdown; prefer --body-file for multiline Markdown)").option("--body-file <path|->", "read comment body from a UTF-8 file, or from stdin with -").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)")).action(async (ref, o) => {
|
|
24557
24902
|
let parsed;
|
|
24558
24903
|
try {
|
|
24559
24904
|
parsed = parseIssueRef(ref);
|
|
@@ -24576,7 +24921,7 @@ issue.command("comment <ref>").description("post a Markdown comment to an issue
|
|
|
24576
24921
|
return fail(`issue comment: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
24577
24922
|
}
|
|
24578
24923
|
});
|
|
24579
|
-
issue.command("check <ref>").description("tick (or with --off untick) a task-list checkbox in an issue/epic body by its item text and print {number,repo,item,checked,changed} JSON").requiredOption("--item <text>", "the checklist item to match \u2014 exact item text, else a unique substring").option("--off", "untick the item ([x] \u2192 [ ]) instead of ticking it").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)").action(async (ref, o) => {
|
|
24924
|
+
jsonParity(issue.command("check <ref>").description("tick (or with --off untick) a task-list checkbox in an issue/epic body by its item text and print {number,repo,item,checked,changed} JSON").requiredOption("--item <text>", "the checklist item to match \u2014 exact item text, else a unique substring").option("--off", "untick the item ([x] \u2192 [ ]) instead of ticking it").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)")).action(async (ref, o) => {
|
|
24580
24925
|
let parsed;
|
|
24581
24926
|
try {
|
|
24582
24927
|
parsed = parseIssueRef(ref);
|
|
@@ -24761,10 +25106,10 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
|
|
|
24761
25106
|
});
|
|
24762
25107
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
24763
25108
|
const wfDir = (0, import_node_path26.join)(cwd, ".github", "workflows");
|
|
24764
|
-
if (!(0,
|
|
24765
|
-
return (0,
|
|
25109
|
+
if (!(0, import_node_fs30.existsSync)(wfDir)) return [];
|
|
25110
|
+
return (0, import_node_fs30.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
|
|
24766
25111
|
try {
|
|
24767
|
-
return workflowReportsPrChecks((0,
|
|
25112
|
+
return workflowReportsPrChecks((0, import_node_fs30.readFileSync)((0, import_node_path26.join)(wfDir, name), "utf8"));
|
|
24768
25113
|
} catch {
|
|
24769
25114
|
return true;
|
|
24770
25115
|
}
|
|
@@ -24792,15 +25137,15 @@ function ciAuditDeps() {
|
|
|
24792
25137
|
readSeedFile: (path2) => {
|
|
24793
25138
|
if (!root) return null;
|
|
24794
25139
|
const fullPath = (0, import_node_path26.join)(root, path2);
|
|
24795
|
-
return (0,
|
|
25140
|
+
return (0, import_node_fs30.existsSync)(fullPath) ? (0, import_node_fs30.readFileSync)(fullPath, "utf8") : null;
|
|
24796
25141
|
}
|
|
24797
25142
|
};
|
|
24798
25143
|
}
|
|
24799
25144
|
function hubRoot() {
|
|
24800
25145
|
const fromPkg = (0, import_node_path26.join)(__dirname, "..", "..");
|
|
24801
25146
|
const marker = "skills/bootstrap/seeds/manifest.json";
|
|
24802
|
-
if ((0,
|
|
24803
|
-
if ((0,
|
|
25147
|
+
if ((0, import_node_fs30.existsSync)((0, import_node_path26.join)(fromPkg, marker))) return fromPkg;
|
|
25148
|
+
if ((0, import_node_fs30.existsSync)((0, import_node_path26.join)(process.cwd(), marker))) return process.cwd();
|
|
24804
25149
|
return null;
|
|
24805
25150
|
}
|
|
24806
25151
|
pr.command("ci-policy").description("report merge CI policy: wait-for-checks vs no-ci (for grind/build agents)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current checkout)").action(async (o) => {
|
|
@@ -24822,6 +25167,9 @@ pr.command("checks-wait <number>").description(`bounded wait for PR checks; skip
|
|
|
24822
25167
|
pollChecks: () => pollRestPrChecks(number, repo),
|
|
24823
25168
|
pollMergeable: () => pollRestPrMergeable(number, repo),
|
|
24824
25169
|
pollRateLimit: () => fetchRestCorePool(),
|
|
25170
|
+
// #3388: on a confirmed red, read the failing runs' annotations so a wall-clock budget kill stops
|
|
25171
|
+
// reading as "your tests failed". One call per failing run, only at the verdict.
|
|
25172
|
+
diagnoseFailure: () => diagnoseFailedRestChecks(number, repo),
|
|
24825
25173
|
baseBranch,
|
|
24826
25174
|
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
|
|
24827
25175
|
log: (message) => console.warn(message),
|
|
@@ -24837,6 +25185,8 @@ pr.command("checks-wait <number>").description(`bounded wait for PR checks; skip
|
|
|
24837
25185
|
printLine(`pr checks-wait: timeout \u2014 waited ${Math.round((result.waitedMs ?? 0) / 6e4)}m, last state: ${result.detail ?? "pending"}. No check failed; re-run to keep waiting, or pass --timeout <minutes>.`);
|
|
24838
25186
|
} else if (result.status === "rate-limited") {
|
|
24839
25187
|
printLine(`pr checks-wait: rate-limited \u2014 ${result.reason ?? "REST pool below floor"}. No check failed; re-run after the pool resets.`);
|
|
25188
|
+
} else if (result.detail === "ci-budget-kill") {
|
|
25189
|
+
printLine(`pr checks-wait: failure (wall-clock budget kill, NOT a test failure) \u2014 ${result.reason}`);
|
|
24840
25190
|
} else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
|
|
24841
25191
|
if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
|
|
24842
25192
|
if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
|
|
@@ -24867,6 +25217,9 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
24867
25217
|
// other base), so the fast-fail message's base branch is always 'development' here.
|
|
24868
25218
|
pollMergeable: () => pollRestPrMergeable(prNumber, repo),
|
|
24869
25219
|
pollRateLimit: () => fetchRestCorePool(),
|
|
25220
|
+
// #3388: `pr land` is the batch path — the one most likely to self-DOS the shared runner and
|
|
25221
|
+
// then read its own wall-clock kill as a broken diff.
|
|
25222
|
+
diagnoseFailure: () => diagnoseFailedRestChecks(prNumber, repo),
|
|
24870
25223
|
baseBranch: "development",
|
|
24871
25224
|
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
|
|
24872
25225
|
log: (message) => console.warn(message),
|
|
@@ -24934,7 +25287,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
24934
25287
|
}
|
|
24935
25288
|
if (result.status === "failed" || result.cleanupError) process.exitCode = 1;
|
|
24936
25289
|
});
|
|
24937
|
-
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) => {
|
|
25290
|
+
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) => {
|
|
24938
25291
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
24939
25292
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
24940
25293
|
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+/);
|
|
@@ -24953,6 +25306,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
24953
25306
|
pollChecks: () => pollRestPrChecks(number, repo),
|
|
24954
25307
|
pollMergeable: () => pollRestPrMergeable(number, repo),
|
|
24955
25308
|
pollRateLimit: () => fetchRestCorePool(),
|
|
25309
|
+
diagnoseFailure: () => diagnoseFailedRestChecks(number, repo),
|
|
24956
25310
|
baseBranch,
|
|
24957
25311
|
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
|
|
24958
25312
|
log: (message) => console.warn(message),
|
|
@@ -25039,7 +25393,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
25039
25393
|
localCleanup = await cleanupPrMergeLocalBranch(headRef, {
|
|
25040
25394
|
beforeWorktrees,
|
|
25041
25395
|
startingPath,
|
|
25042
|
-
pathExists: (p) => (0,
|
|
25396
|
+
pathExists: (p) => (0, import_node_fs30.existsSync)(p),
|
|
25043
25397
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
25044
25398
|
teardownWorktreeStage,
|
|
25045
25399
|
deferredStore,
|
|
@@ -25084,7 +25438,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
25084
25438
|
});
|
|
25085
25439
|
registerQueryCommands(program2);
|
|
25086
25440
|
registerWorktreeCommands(program2);
|
|
25087
|
-
registerIssueLifecycleCommands(program2);
|
|
25441
|
+
registerIssueLifecycleCommands(program2, { attach: attachToProject });
|
|
25088
25442
|
registerTrainCommands(program2);
|
|
25089
25443
|
registerDeployCommands(program2);
|
|
25090
25444
|
registerDiscoveryCommands(program2);
|
|
@@ -25172,7 +25526,8 @@ function renderDeployLine(d) {
|
|
|
25172
25526
|
else if (d.runUrl) parts.push(`run ${d.runUrl}`);
|
|
25173
25527
|
if (d.deployStatus === "success") parts.push("deploy: SUCCEEDED");
|
|
25174
25528
|
else if (d.deployStatus === "failure") parts.push("deploy: FAILED (promotion stands; retry the deploy, do not re-tag)");
|
|
25175
|
-
else if (d.runId != null) parts.push(`deploy: dispatched (watch: gh run watch ${d.runId} --repo mutmutco/MMI-Hub --exit-status)`);
|
|
25529
|
+
else if (d.runId != null) parts.push(`deploy: UNVERIFIED \u2014 dispatched, not resolved (watch: gh run watch ${d.runId} --repo mutmutco/MMI-Hub --exit-status)`);
|
|
25530
|
+
else parts.push("deploy: UNVERIFIED \u2014 no run correlated or watched; resolve every workflow run on the release SHA before calling this release healthy (#3322)");
|
|
25176
25531
|
return parts.join("; ");
|
|
25177
25532
|
}
|
|
25178
25533
|
function renderTrainApply(commandName, r) {
|
|
@@ -25342,7 +25697,7 @@ access.command("role [repo]").description("D14 train authority for a repo (serve
|
|
|
25342
25697
|
const verdict = await fetchTrainAuthority(repo, registryClientDeps(cfg));
|
|
25343
25698
|
if (!verdict.ok) {
|
|
25344
25699
|
if (o.json) {
|
|
25345
|
-
console.log(JSON.stringify({ repo, train: false, error: verdict.error }));
|
|
25700
|
+
console.log(JSON.stringify({ repo, train: false, verified: false, error: verdict.error }));
|
|
25346
25701
|
process.exitCode = 1;
|
|
25347
25702
|
return;
|
|
25348
25703
|
}
|
|
@@ -25350,7 +25705,7 @@ access.command("role [repo]").description("D14 train authority for a repo (serve
|
|
|
25350
25705
|
}
|
|
25351
25706
|
const a = verdict.authority;
|
|
25352
25707
|
if (o.json) {
|
|
25353
|
-
console.log(JSON.stringify(a));
|
|
25708
|
+
console.log(JSON.stringify({ ...a, verified: true }));
|
|
25354
25709
|
if (!a.train) process.exitCode = 1;
|
|
25355
25710
|
return;
|
|
25356
25711
|
}
|
|
@@ -25379,10 +25734,10 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
25379
25734
|
targets = resolution.targets;
|
|
25380
25735
|
}
|
|
25381
25736
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
25382
|
-
const fileMatrix = (0,
|
|
25737
|
+
const fileMatrix = (0, import_node_fs30.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs30.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
25383
25738
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
25384
25739
|
const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
25385
|
-
const fileContracts = (0,
|
|
25740
|
+
const fileContracts = (0, import_node_fs30.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs30.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
|
|
25386
25741
|
const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
|
|
25387
25742
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
|
|
25388
25743
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
@@ -25402,6 +25757,7 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
|
|
|
25402
25757
|
banner: opts.banner,
|
|
25403
25758
|
preflight: opts.preflight,
|
|
25404
25759
|
fast: opts.fast || opts.self,
|
|
25760
|
+
self: opts.self,
|
|
25405
25761
|
json: opts.json,
|
|
25406
25762
|
verbose: opts.verbose
|
|
25407
25763
|
},
|
|
@@ -25415,7 +25771,7 @@ function directoryBytes(path2) {
|
|
|
25415
25771
|
let total = 0;
|
|
25416
25772
|
let entries;
|
|
25417
25773
|
try {
|
|
25418
|
-
entries = (0,
|
|
25774
|
+
entries = (0, import_node_fs30.readdirSync)(path2, { withFileTypes: true });
|
|
25419
25775
|
} catch {
|
|
25420
25776
|
return 0;
|
|
25421
25777
|
}
|
|
@@ -25424,7 +25780,7 @@ function directoryBytes(path2) {
|
|
|
25424
25780
|
if (entry.isDirectory()) total += directoryBytes(child2);
|
|
25425
25781
|
else {
|
|
25426
25782
|
try {
|
|
25427
|
-
total += (0,
|
|
25783
|
+
total += (0, import_node_fs30.statSync)(child2).size;
|
|
25428
25784
|
} catch {
|
|
25429
25785
|
}
|
|
25430
25786
|
}
|
|
@@ -25432,25 +25788,25 @@ function directoryBytes(path2) {
|
|
|
25432
25788
|
return total;
|
|
25433
25789
|
}
|
|
25434
25790
|
function listDirEntries(dir) {
|
|
25435
|
-
return (0,
|
|
25791
|
+
return (0, import_node_fs30.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
|
|
25436
25792
|
}
|
|
25437
25793
|
function readInstalledPluginRefs(home) {
|
|
25438
25794
|
const p = installedPluginsPath(home);
|
|
25439
|
-
if (!(0,
|
|
25795
|
+
if (!(0, import_node_fs30.existsSync)(p)) return [];
|
|
25440
25796
|
try {
|
|
25441
|
-
return installedPluginPaths((0,
|
|
25797
|
+
return installedPluginPaths((0, import_node_fs30.readFileSync)(p, "utf8"));
|
|
25442
25798
|
} catch {
|
|
25443
25799
|
return null;
|
|
25444
25800
|
}
|
|
25445
25801
|
}
|
|
25446
25802
|
function pluginCacheFsDeps(home, dirBytes) {
|
|
25447
25803
|
return {
|
|
25448
|
-
exists: (p) => (0,
|
|
25449
|
-
listVersionDirs: (root) => (0,
|
|
25804
|
+
exists: (p) => (0, import_node_fs30.existsSync)(p),
|
|
25805
|
+
listVersionDirs: (root) => (0, import_node_fs30.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
|
|
25450
25806
|
dirBytes,
|
|
25451
|
-
listStagingDirs: (root) => (0,
|
|
25807
|
+
listStagingDirs: (root) => (0, import_node_fs30.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
|
|
25452
25808
|
try {
|
|
25453
|
-
return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path26.join)(root, d.name), listDirEntries, (p) => (0,
|
|
25809
|
+
return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path26.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs30.statSync)(p).mtimeMs) };
|
|
25454
25810
|
} catch {
|
|
25455
25811
|
return { name: d.name, mtimeMs: Date.now() };
|
|
25456
25812
|
}
|
|
@@ -25465,9 +25821,9 @@ function stagingApplyFsGuard(home) {
|
|
|
25465
25821
|
referencedPaths: () => readInstalledPluginRefs(home),
|
|
25466
25822
|
mtimeMs: (name) => {
|
|
25467
25823
|
const p = (0, import_node_path26.join)(stagingRoot, name);
|
|
25468
|
-
if (!(0,
|
|
25824
|
+
if (!(0, import_node_fs30.existsSync)(p)) return null;
|
|
25469
25825
|
try {
|
|
25470
|
-
return newestMtimeMs(p, listDirEntries, (q) => (0,
|
|
25826
|
+
return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs30.statSync)(q).mtimeMs);
|
|
25471
25827
|
} catch {
|
|
25472
25828
|
return null;
|
|
25473
25829
|
}
|
|
@@ -25483,7 +25839,7 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
|
|
|
25483
25839
|
{ withBytes: true }
|
|
25484
25840
|
);
|
|
25485
25841
|
const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
|
|
25486
|
-
const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0,
|
|
25842
|
+
const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs30.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard((0, import_node_os8.homedir)())) : void 0;
|
|
25487
25843
|
const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
|
|
25488
25844
|
if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
|
|
25489
25845
|
else console.log(renderPluginCachePlan(plan, result));
|
|
@@ -25508,7 +25864,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
25508
25864
|
hubSession: async () => hubAuthSession({ baseUrl: (await loadConfig()).sagaApiUrl ?? defaultHubUrl(), githubToken }),
|
|
25509
25865
|
ghLogin: githubLogin
|
|
25510
25866
|
});
|
|
25511
|
-
const line = whoamiLine(report);
|
|
25867
|
+
const line = whoamiLine(report, report.login ? ghMultiAccountCaveat(report.login) : void 0);
|
|
25512
25868
|
if (line) io.log(line);
|
|
25513
25869
|
},
|
|
25514
25870
|
boardSlice: (io) => runBoardSlice(io, {
|