@mutmutco/cli 3.48.1 → 3.50.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 +528 -96
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -4478,6 +4478,52 @@ async function runSessionStart(parallel, sequential, io) {
|
|
|
4478
4478
|
for (const lines of buffered) flush(lines, io);
|
|
4479
4479
|
for (const step of sequential) flush(await runBufferedStep(step), io);
|
|
4480
4480
|
}
|
|
4481
|
+
var SESSION_PAYLOAD_CAP_CHARS = 1e4;
|
|
4482
|
+
var SESSION_PAYLOAD_BUDGET_CHARS = 8e3;
|
|
4483
|
+
function sessionPayloadStatus(chars) {
|
|
4484
|
+
if (chars > SESSION_PAYLOAD_CAP_CHARS) return "red";
|
|
4485
|
+
if (chars > SESSION_PAYLOAD_BUDGET_CHARS) return "amber";
|
|
4486
|
+
return "ok";
|
|
4487
|
+
}
|
|
4488
|
+
function measuredSessionIo(io) {
|
|
4489
|
+
let chars = 0;
|
|
4490
|
+
return {
|
|
4491
|
+
io: {
|
|
4492
|
+
log: (text) => {
|
|
4493
|
+
chars += text.length + 1;
|
|
4494
|
+
io.log(text);
|
|
4495
|
+
},
|
|
4496
|
+
err: (text) => io.err(text)
|
|
4497
|
+
},
|
|
4498
|
+
chars: () => chars
|
|
4499
|
+
};
|
|
4500
|
+
}
|
|
4501
|
+
function sessionPayloadStatePath(cwd) {
|
|
4502
|
+
return repoRuntimeStatePath(cwd, "session-payload.json");
|
|
4503
|
+
}
|
|
4504
|
+
function recordSessionPayload(cwd, chars, now = /* @__PURE__ */ new Date()) {
|
|
4505
|
+
try {
|
|
4506
|
+
const path2 = sessionPayloadStatePath(cwd);
|
|
4507
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path6.dirname)(path2), { recursive: true });
|
|
4508
|
+
const record = { chars, status: sessionPayloadStatus(chars), at: now.toISOString() };
|
|
4509
|
+
(0, import_node_fs8.writeFileSync)(path2, `${JSON.stringify(record)}
|
|
4510
|
+
`, "utf8");
|
|
4511
|
+
} catch {
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
function readSessionPayload(cwd) {
|
|
4515
|
+
try {
|
|
4516
|
+
const parsed = JSON.parse((0, import_node_fs8.readFileSync)(sessionPayloadStatePath(cwd), "utf8"));
|
|
4517
|
+
if (typeof parsed?.chars !== "number" || !Number.isFinite(parsed.chars)) return void 0;
|
|
4518
|
+
return {
|
|
4519
|
+
chars: parsed.chars,
|
|
4520
|
+
status: sessionPayloadStatus(parsed.chars),
|
|
4521
|
+
at: typeof parsed.at === "string" ? parsed.at : ""
|
|
4522
|
+
};
|
|
4523
|
+
} catch {
|
|
4524
|
+
return void 0;
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4481
4527
|
function buildSessionStartPlan(verbs) {
|
|
4482
4528
|
return {
|
|
4483
4529
|
parallel: [
|
|
@@ -5375,7 +5421,33 @@ function resolveSafeSiblingWorktreeCleanupTarget(worktreePath, siblingRoot, deps
|
|
|
5375
5421
|
}
|
|
5376
5422
|
function siblingMmiWorktreesRoot(repoRoot2) {
|
|
5377
5423
|
const parent = (0, import_node_path7.dirname)(repoRoot2);
|
|
5378
|
-
|
|
5424
|
+
if ((0, import_node_path7.basename)(parent).toLowerCase() === "mmi-worktrees") return parent;
|
|
5425
|
+
const grandparent = (0, import_node_path7.dirname)(parent);
|
|
5426
|
+
if ((0, import_node_path7.basename)(grandparent).toLowerCase() === "mmi-worktrees") return grandparent;
|
|
5427
|
+
return (0, import_node_path7.join)(parent, "mmi-worktrees");
|
|
5428
|
+
}
|
|
5429
|
+
function worktreeScanDirs(root, repoRoot2, listDirs, isRepoCheckout) {
|
|
5430
|
+
const projectsDir = (0, import_node_path7.dirname)(root);
|
|
5431
|
+
const ownName = (0, import_node_path7.basename)(repoRoot2).toLowerCase();
|
|
5432
|
+
const flat = [];
|
|
5433
|
+
let ownContainer = null;
|
|
5434
|
+
for (const dir of listDirs(root)) {
|
|
5435
|
+
const name = (0, import_node_path7.basename)(dir);
|
|
5436
|
+
if (isRepoCheckout((0, import_node_path7.join)(projectsDir, name))) {
|
|
5437
|
+
if (name.toLowerCase() === ownName) ownContainer = dir;
|
|
5438
|
+
continue;
|
|
5439
|
+
}
|
|
5440
|
+
flat.push(dir);
|
|
5441
|
+
}
|
|
5442
|
+
return ownContainer ? [...flat, ...listDirs(ownContainer)] : flat;
|
|
5443
|
+
}
|
|
5444
|
+
function explicitRepoWorktreesRoot(root, repoRoot2, rootDirs) {
|
|
5445
|
+
const repoName = (0, import_node_path7.basename)(repoRoot2);
|
|
5446
|
+
const repoDir = rootDirs.find((name) => name.toLowerCase() === repoName.toLowerCase());
|
|
5447
|
+
return repoDir ? (0, import_node_path7.join)(root, repoDir) : root;
|
|
5448
|
+
}
|
|
5449
|
+
function strayWorktreeRootPaths(container, names, authoritativeRoot) {
|
|
5450
|
+
return names.filter((name) => /worktrees/i.test(name)).map((name) => (0, import_node_path7.join)(container, name)).filter((path2) => !samePath(path2, authoritativeRoot));
|
|
5379
5451
|
}
|
|
5380
5452
|
function classifySiblingWorktreeDir(entry) {
|
|
5381
5453
|
if (!entry.ownedByCurrentRepo) {
|
|
@@ -6235,6 +6307,7 @@ var import_node_child_process5 = require("node:child_process");
|
|
|
6235
6307
|
var OWNER = "mutmutco";
|
|
6236
6308
|
var SSM_ROOT = "/mmi-future";
|
|
6237
6309
|
var PROJECT_TIER_SEGMENT = "dev";
|
|
6310
|
+
var ORG_INFRA_SLUG = "_org";
|
|
6238
6311
|
var KEY_RE = /^(?:[a-z][a-z0-9-]*\/)?[A-Za-z][A-Za-z0-9_]*$/;
|
|
6239
6312
|
function isValidSecretKey(key) {
|
|
6240
6313
|
if (!key || key.length > 256) return false;
|
|
@@ -6445,7 +6518,7 @@ no vault credentials visible`;
|
|
|
6445
6518
|
...lines,
|
|
6446
6519
|
"",
|
|
6447
6520
|
"+ = usable now. Consume keyless: `mmi-cli secrets use <KEY> -- <cmd>` (inject, never printed); validate: `mmi-cli secrets verify <KEY>`. No command prints a raw value (#2844).",
|
|
6448
|
-
|
|
6521
|
+
` Org-infra (#3463): \`--slug\` is ALWAYS \`${ORG_INFRA_SLUG}\`; the provider is the key's FIRST SEGMENT \u2014 \`mmi-cli secrets use cloudflare/GLOBAL_API_KEY --slug ${ORG_INFRA_SLUG} -- <cmd>\` (master-gated).`
|
|
6449
6522
|
].join("\n");
|
|
6450
6523
|
}
|
|
6451
6524
|
async function secretsCapabilities(deps, opts) {
|
|
@@ -6487,6 +6560,81 @@ async function secretsCapabilities(deps, opts) {
|
|
|
6487
6560
|
}
|
|
6488
6561
|
deps.log(formatCapabilities(report));
|
|
6489
6562
|
}
|
|
6563
|
+
var PROBE_TIMEOUT_MS = 4e3;
|
|
6564
|
+
async function probeCapabilities(deps, repo) {
|
|
6565
|
+
try {
|
|
6566
|
+
const qs = new URLSearchParams({ repo }).toString();
|
|
6567
|
+
const res = await deps.fetch(`${deps.apiUrl}/secrets/capabilities?${qs}`, {
|
|
6568
|
+
method: "GET",
|
|
6569
|
+
headers: await deps.headers(),
|
|
6570
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
|
|
6571
|
+
});
|
|
6572
|
+
if (!res.ok) return void 0;
|
|
6573
|
+
return await res.json();
|
|
6574
|
+
} catch {
|
|
6575
|
+
return void 0;
|
|
6576
|
+
}
|
|
6577
|
+
}
|
|
6578
|
+
function secretKeyLeaf(key) {
|
|
6579
|
+
const slash = key.lastIndexOf("/");
|
|
6580
|
+
return slash === -1 ? key : key.slice(slash + 1);
|
|
6581
|
+
}
|
|
6582
|
+
function resolveNotFoundGuidance(input) {
|
|
6583
|
+
const { key, repo, slug, report } = input;
|
|
6584
|
+
const tried = secretParamName(slug ?? "<repo-slug>", key);
|
|
6585
|
+
const isMaster = report?.role === "master";
|
|
6586
|
+
const mnemonic = `Org-infra shape: \`--slug\` is ALWAYS \`${ORG_INFRA_SLUG}\`; the provider is the key's FIRST SEGMENT (e.g. \`cloudflare/API_TOKEN --slug ${ORG_INFRA_SLUG}\`).`;
|
|
6587
|
+
const escalate = (target) => target && target.slug === ORG_INFRA_SLUG ? `Approved escalation: mmi-cli secrets request ${key} --repo ${repo} --reason "org-infra grant for ${target.scope} (key ${target.key}, slug ${ORG_INFRA_SLUG}) \u2014 <why it is needed>"` : `Approved escalation: mmi-cli secrets request ${key} --repo ${repo} --reason "<why it is needed>"`;
|
|
6588
|
+
const leaf = secretKeyLeaf(key);
|
|
6589
|
+
const sameLeaf = (report?.capabilities ?? []).filter(
|
|
6590
|
+
(c) => secretKeyLeaf(c.key) === leaf && !(c.slug === slug && c.key === key)
|
|
6591
|
+
);
|
|
6592
|
+
const exact = sameLeaf.filter((c) => slug ? c.key === `${slug}/${leaf}` : false);
|
|
6593
|
+
const unambiguous = exact.length === 1 ? exact : sameLeaf.length === 1 ? sameLeaf : [];
|
|
6594
|
+
const usable = unambiguous.find((c) => c.accessible);
|
|
6595
|
+
if (usable) {
|
|
6596
|
+
return {
|
|
6597
|
+
reachable: true,
|
|
6598
|
+
lines: [
|
|
6599
|
+
`Secret ${key} was not found at ${tried}.`,
|
|
6600
|
+
`It lives at ${usable.scope} and you can already consume it \u2014 no request needed:`,
|
|
6601
|
+
` mmi-cli secrets use ${usable.key} --slug ${usable.slug} -- <cmd>`,
|
|
6602
|
+
mnemonic
|
|
6603
|
+
]
|
|
6604
|
+
};
|
|
6605
|
+
}
|
|
6606
|
+
if (!unambiguous.length && sameLeaf.length > 1) {
|
|
6607
|
+
const lines2 = [
|
|
6608
|
+
`Secret ${key} was not found at ${tried}.`,
|
|
6609
|
+
`${sameLeaf.length} different keys are named ${leaf} \u2014 pick the one you meant, this command cannot:`,
|
|
6610
|
+
...sameLeaf.map((c) => ` ${c.accessible ? "+" : " "} mmi-cli secrets use ${c.key} --slug ${c.slug} -- <cmd> (${c.scope})`),
|
|
6611
|
+
mnemonic
|
|
6612
|
+
];
|
|
6613
|
+
if (!isMaster) lines2.push(escalate(sameLeaf.find((c) => !c.accessible)));
|
|
6614
|
+
return { reachable: false, lines: lines2 };
|
|
6615
|
+
}
|
|
6616
|
+
if (unambiguous.length) {
|
|
6617
|
+
const other = unambiguous[0];
|
|
6618
|
+
const lines2 = [
|
|
6619
|
+
`Secret ${key} was not found at ${tried}.`,
|
|
6620
|
+
`A key named ${leaf} exists at ${other.scope}, but it is not granted to you (this is an ACCESS gap, not a missing key).`
|
|
6621
|
+
];
|
|
6622
|
+
if (isMaster) lines2.push("You are master \u2014 grant yourself directly: `mmi-cli secrets grant <repo> <login> <provider>/<KEY>`.");
|
|
6623
|
+
else lines2.push(escalate(other));
|
|
6624
|
+
return { reachable: false, lines: lines2 };
|
|
6625
|
+
}
|
|
6626
|
+
const lines = [
|
|
6627
|
+
`Secret ${key} was not found at ${tried}, and no key named ${leaf} is visible to you.`,
|
|
6628
|
+
`Locate it by intent first: \`mmi-cli secrets find "<what it is for>"\` \u2014 it returns the exact keyless-use command.`,
|
|
6629
|
+
mnemonic
|
|
6630
|
+
];
|
|
6631
|
+
if (isMaster) {
|
|
6632
|
+
lines.push("You are master: if it truly does not exist yet, create it with `mmi-cli secrets set` rather than requesting a grant from yourself.");
|
|
6633
|
+
} else {
|
|
6634
|
+
lines.push(escalate());
|
|
6635
|
+
}
|
|
6636
|
+
return { reachable: false, lines };
|
|
6637
|
+
}
|
|
6490
6638
|
async function fetchCatalog(deps, label, repo, extra) {
|
|
6491
6639
|
const qs = new URLSearchParams({ repo, ...extra }).toString();
|
|
6492
6640
|
let res;
|
|
@@ -6754,11 +6902,17 @@ async function secretsVerify(deps, key, opts) {
|
|
|
6754
6902
|
deps.err(`invalid secret key ${JSON.stringify(key)}`);
|
|
6755
6903
|
return false;
|
|
6756
6904
|
}
|
|
6757
|
-
|
|
6758
|
-
deps.err(`no provider verifier configured for ${secretLeafName(key)}`);
|
|
6759
|
-
return false;
|
|
6760
|
-
}
|
|
6905
|
+
const hasProviderVerifier = Boolean(providerForSecretKey(key));
|
|
6761
6906
|
const value = await fetchSecretValue(deps, key, opts);
|
|
6907
|
+
if (!hasProviderVerifier) {
|
|
6908
|
+
if (!value) {
|
|
6909
|
+
deps.err(`${key}: ABSENT or unreachable at this coordinate \u2014 no value could be read`);
|
|
6910
|
+
deps.err(`Locate a key across every vault you can reach: mmi-cli org access capabilities${opts.repo ? ` --repo ${opts.repo}` : ""}`);
|
|
6911
|
+
return false;
|
|
6912
|
+
}
|
|
6913
|
+
deps.log(`${key}: PRESENT (${value.length} chars) \u2014 reachability only; no provider verifier is configured for ${secretLeafName(key)}, so the credential itself was not validated`);
|
|
6914
|
+
return true;
|
|
6915
|
+
}
|
|
6762
6916
|
if (!value) {
|
|
6763
6917
|
deps.err(`secrets verify: could not read ${key}; no value was printed`);
|
|
6764
6918
|
return false;
|
|
@@ -6777,6 +6931,16 @@ async function secretsRequest(deps, key, opts) {
|
|
|
6777
6931
|
return false;
|
|
6778
6932
|
}
|
|
6779
6933
|
const repo = await targetRepo(deps, opts);
|
|
6934
|
+
const requester = await probeCapabilities(deps, repo);
|
|
6935
|
+
if (requester?.role === "master") {
|
|
6936
|
+
deps.err(`secrets request: you are master on ${repo} \u2014 a request would file an issue and notify yourself.`);
|
|
6937
|
+
const candidates = (requester.capabilities ?? []).filter((c) => secretKeyLeaf(c.key) === secretKeyLeaf(key) && c.accessible);
|
|
6938
|
+
const reachable = candidates.length === 1 ? candidates[0] : void 0;
|
|
6939
|
+
if (reachable) deps.err(`${key} is already reachable: \`mmi-cli secrets use ${reachable.key} --slug ${reachable.slug} -- <cmd>\``);
|
|
6940
|
+
else if (candidates.length > 1) deps.err(`${candidates.length} keys are named ${secretKeyLeaf(key)} \u2014 pick one with \`mmi-cli secrets find "<what it is for>"\`.`);
|
|
6941
|
+
else deps.err(`Grant directly with \`mmi-cli secrets grant <repo> <login> <provider>/<KEY>\`, or create it with \`mmi-cli secrets set\` if it does not exist yet.`);
|
|
6942
|
+
return false;
|
|
6943
|
+
}
|
|
6780
6944
|
const res = await deps.fetch(`${deps.apiUrl}/secrets/request`, {
|
|
6781
6945
|
method: "POST",
|
|
6782
6946
|
headers: await deps.headers({ "content-type": "application/json" }),
|
|
@@ -6903,6 +7067,11 @@ async function secretsDeclare(deps, key, opts) {
|
|
|
6903
7067
|
const body = await readJsonBody(res);
|
|
6904
7068
|
if (!res.ok) {
|
|
6905
7069
|
deps.err(await upgradeMessage(res, body) ?? `secrets declare failed: HTTP ${res.status}${errorDetail(body)}`);
|
|
7070
|
+
if (res.status === 404 && !errorDetail(body)) {
|
|
7071
|
+
deps.err(
|
|
7072
|
+
`The Hub did not recognize the declare route (no error body \u2014 an API-gateway 404, not an authority refusal). Fall back to the master-side catalog write: mmi-cli org project set ${repo} --secrets-file <catalog.json> (shape: keyed by env name; each entry {key,purpose,group,owner,stages[],consumers[]} \u2014 see docs/Guides/oauth-provision.md \xA7 Cred catalog shape).`
|
|
7073
|
+
);
|
|
7074
|
+
}
|
|
6906
7075
|
return false;
|
|
6907
7076
|
}
|
|
6908
7077
|
if (opts.json) {
|
|
@@ -7115,7 +7284,12 @@ async function secretsUse(deps, key, opts) {
|
|
|
7115
7284
|
" \u2022 Runtime: the central deploy injects the declared stage env; broker consumers use the workload's scoped runtime token. Never bake a value into an image or commit it.",
|
|
7116
7285
|
` \u2022 CI (GitHub Actions): the workflow assumes its OIDC role and runs \`aws ssm get-parameter --with-decryption --name ${path2}\` \u2014 no GitHub secret.`,
|
|
7117
7286
|
" \u2022 Own project: the stageless canonical and any declared dev/rc/main overrides are project-admin self-service.",
|
|
7118
|
-
" \u2022 Org-infra/cross-slug keys remain master-gated unless exactly granted."
|
|
7287
|
+
" \u2022 Org-infra/cross-slug keys remain master-gated unless exactly granted.",
|
|
7288
|
+
"",
|
|
7289
|
+
"Answer it without consuming it (#3436):",
|
|
7290
|
+
" \u2022 WHERE does a key live, across every vault you can reach: `mmi-cli org access capabilities` (names + tier + scope, never values). `secrets list` is this repo only and cannot see _org.",
|
|
7291
|
+
` \u2022 DOES it exist here: \`mmi-cli secrets verify ${key}${opts.slug ? ` --slug ${opts.slug}` : ""}\` \u2014 PRESENT/ABSENT even with no provider verifier configured.`,
|
|
7292
|
+
" \u2022 PowerShell note: `mmi-cli` resolves to npm's mmi-cli.ps1, whose binder eats the `--` separator. The wrapped command works either way; if a flag still misbinds, call `mmi-cli.cmd` explicitly."
|
|
7119
7293
|
].join("\n")
|
|
7120
7294
|
);
|
|
7121
7295
|
return;
|
|
@@ -7135,8 +7309,8 @@ async function secretsUse(deps, key, opts) {
|
|
|
7135
7309
|
if (!res.ok) {
|
|
7136
7310
|
const body = await readJsonBody(res);
|
|
7137
7311
|
if (res.status === 404 && body.code === "secret_not_found") {
|
|
7138
|
-
|
|
7139
|
-
|
|
7312
|
+
const guidance = resolveNotFoundGuidance({ key, repo, slug, report: await probeCapabilities(deps, repo) });
|
|
7313
|
+
for (const line of guidance.lines) deps.err(line);
|
|
7140
7314
|
return false;
|
|
7141
7315
|
}
|
|
7142
7316
|
deps.err(
|
|
@@ -7361,18 +7535,37 @@ async function preservedBranches() {
|
|
|
7361
7535
|
return [];
|
|
7362
7536
|
}
|
|
7363
7537
|
}
|
|
7364
|
-
async function siblingWorktreeDirs() {
|
|
7538
|
+
async function siblingWorktreeDirs(explicitRoot) {
|
|
7365
7539
|
const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
7366
7540
|
const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
|
|
7367
|
-
const
|
|
7541
|
+
const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path8.dirname)((0, import_node_path8.dirname)(worktreeGitRoot)) : repoRoot2;
|
|
7368
7542
|
try {
|
|
7369
|
-
const
|
|
7370
|
-
return
|
|
7543
|
+
const dirs = explicitRoot ? listDirsIn(resolveExplicitScanRoot(explicitRoot, primaryRepoRoot)) : worktreeScanDirs(siblingMmiWorktreesRoot(primaryRepoRoot), primaryRepoRoot, listDirsIn, isRepoCheckoutDir);
|
|
7544
|
+
return dirs.map((dir) => inspectSiblingWorktreeDir(dir, worktreeGitRoot)).filter((entry) => Boolean(entry));
|
|
7371
7545
|
} catch {
|
|
7372
7546
|
return [];
|
|
7373
7547
|
}
|
|
7374
7548
|
}
|
|
7375
|
-
|
|
7549
|
+
function listDirsIn(dir) {
|
|
7550
|
+
try {
|
|
7551
|
+
return (0, import_node_fs10.readdirSync)(dir, { withFileTypes: true }).filter((ent) => ent.isDirectory()).map((ent) => (0, import_node_path8.join)(dir, ent.name));
|
|
7552
|
+
} catch {
|
|
7553
|
+
return [];
|
|
7554
|
+
}
|
|
7555
|
+
}
|
|
7556
|
+
function isRepoCheckoutDir(dir) {
|
|
7557
|
+
return (0, import_node_fs10.existsSync)((0, import_node_path8.join)(dir, ".git"));
|
|
7558
|
+
}
|
|
7559
|
+
function resolveExplicitScanRoot(explicitRoot, repoRoot2) {
|
|
7560
|
+
let rootDirs;
|
|
7561
|
+
try {
|
|
7562
|
+
rootDirs = (0, import_node_fs10.readdirSync)(explicitRoot, { withFileTypes: true }).filter((ent) => ent.isDirectory()).map((ent) => ent.name);
|
|
7563
|
+
} catch {
|
|
7564
|
+
return explicitRoot;
|
|
7565
|
+
}
|
|
7566
|
+
return explicitRepoWorktreesRoot(explicitRoot, repoRoot2, rootDirs);
|
|
7567
|
+
}
|
|
7568
|
+
async function gcPlan(remote, limit, opts = {}) {
|
|
7376
7569
|
const [branches, heads, current, stale, prs, worktrees, siblingDirs, preserved] = await Promise.all([
|
|
7377
7570
|
gitOut(["branch", "--format=%(refname:short)"]),
|
|
7378
7571
|
localBranchHeads(),
|
|
@@ -7382,7 +7575,7 @@ async function gcPlan(remote, limit) {
|
|
|
7382
7575
|
}),
|
|
7383
7576
|
ghPrs(limit),
|
|
7384
7577
|
worktreeBranches(),
|
|
7385
|
-
siblingWorktreeDirs(),
|
|
7578
|
+
siblingWorktreeDirs(opts.root),
|
|
7386
7579
|
preservedBranches()
|
|
7387
7580
|
]);
|
|
7388
7581
|
return buildGcPlan({
|
|
@@ -7397,6 +7590,62 @@ async function gcPlan(remote, limit) {
|
|
|
7397
7590
|
preservedBranches: preserved
|
|
7398
7591
|
});
|
|
7399
7592
|
}
|
|
7593
|
+
var STRAY_ROOT_WALK_BUDGET_MS = 4e3;
|
|
7594
|
+
function measureWorktreeRoot(root, deadline) {
|
|
7595
|
+
let dirs = 0;
|
|
7596
|
+
let bytes = 0;
|
|
7597
|
+
let partial = false;
|
|
7598
|
+
const stack = [root];
|
|
7599
|
+
let depth0 = true;
|
|
7600
|
+
while (stack.length) {
|
|
7601
|
+
if (Date.now() > deadline) {
|
|
7602
|
+
partial = true;
|
|
7603
|
+
break;
|
|
7604
|
+
}
|
|
7605
|
+
const current = stack.pop();
|
|
7606
|
+
let entries;
|
|
7607
|
+
try {
|
|
7608
|
+
entries = (0, import_node_fs10.readdirSync)(current, { withFileTypes: true });
|
|
7609
|
+
} catch {
|
|
7610
|
+
continue;
|
|
7611
|
+
}
|
|
7612
|
+
for (const ent of entries) {
|
|
7613
|
+
const child2 = (0, import_node_path8.join)(current, ent.name);
|
|
7614
|
+
if (ent.isDirectory()) {
|
|
7615
|
+
if (depth0) dirs++;
|
|
7616
|
+
let isLink = false;
|
|
7617
|
+
try {
|
|
7618
|
+
(0, import_node_fs10.readlinkSync)(child2);
|
|
7619
|
+
isLink = true;
|
|
7620
|
+
} catch {
|
|
7621
|
+
}
|
|
7622
|
+
if (!isLink) stack.push(child2);
|
|
7623
|
+
} else if (ent.isFile()) {
|
|
7624
|
+
try {
|
|
7625
|
+
bytes += (0, import_node_fs10.statSync)(child2).size;
|
|
7626
|
+
} catch {
|
|
7627
|
+
}
|
|
7628
|
+
}
|
|
7629
|
+
}
|
|
7630
|
+
depth0 = false;
|
|
7631
|
+
}
|
|
7632
|
+
return partial ? { dirs, bytes, partial } : { dirs, bytes };
|
|
7633
|
+
}
|
|
7634
|
+
function worktreeRootsProbe(repoRoot2) {
|
|
7635
|
+
const authoritative = siblingMmiWorktreesRoot(repoRoot2);
|
|
7636
|
+
const container = (0, import_node_path8.dirname)(authoritative);
|
|
7637
|
+
let names;
|
|
7638
|
+
try {
|
|
7639
|
+
names = (0, import_node_fs10.readdirSync)(container, { withFileTypes: true }).filter((ent) => ent.isDirectory()).map((ent) => ent.name);
|
|
7640
|
+
} catch {
|
|
7641
|
+
return { authoritative, stray: [] };
|
|
7642
|
+
}
|
|
7643
|
+
const deadline = Date.now() + STRAY_ROOT_WALK_BUDGET_MS;
|
|
7644
|
+
return {
|
|
7645
|
+
authoritative,
|
|
7646
|
+
stray: strayWorktreeRootPaths(container, names, authoritative).map((path2) => ({ path: path2, ...measureWorktreeRoot(path2, deadline) }))
|
|
7647
|
+
};
|
|
7648
|
+
}
|
|
7400
7649
|
|
|
7401
7650
|
// src/repo-resolve.ts
|
|
7402
7651
|
function slugOf(repoOrSlug) {
|
|
@@ -8873,7 +9122,7 @@ async function provisionWorktree(worktreeRoot, deps) {
|
|
|
8873
9122
|
}
|
|
8874
9123
|
function defaultWorktreePath(repoRoot2, branch) {
|
|
8875
9124
|
const safe = branch.replace(/[/\\]+/g, "-");
|
|
8876
|
-
return (0, import_node_path11.join)((0, import_node_path11.dirname)(repoRoot2), "mmi-worktrees", safe);
|
|
9125
|
+
return (0, import_node_path11.join)((0, import_node_path11.dirname)(repoRoot2), "mmi-worktrees", (0, import_node_path11.basename)(repoRoot2), safe);
|
|
8877
9126
|
}
|
|
8878
9127
|
async function primaryCheckoutRootOf(git) {
|
|
8879
9128
|
try {
|
|
@@ -8984,7 +9233,7 @@ function commandLadderHint() {
|
|
|
8984
9233
|
}
|
|
8985
9234
|
|
|
8986
9235
|
// src/index.ts
|
|
8987
|
-
var
|
|
9236
|
+
var import_node_path27 = require("node:path");
|
|
8988
9237
|
|
|
8989
9238
|
// src/merge-ci-policy.ts
|
|
8990
9239
|
function resolveMergeCiPolicy(input) {
|
|
@@ -9179,8 +9428,11 @@ async function waitForPrChecks(deps) {
|
|
|
9179
9428
|
failureStreak += 1;
|
|
9180
9429
|
if (failureStreak >= PR_CHECKS_FAILURE_CONFIRMATIONS) {
|
|
9181
9430
|
const diagnosis = deps.diagnoseFailure ? await deps.diagnoseFailure().catch(() => null) : null;
|
|
9182
|
-
if (diagnosis?.cause === "
|
|
9183
|
-
return { policy, status: "failure", reason: diagnosis.reason, detail: "
|
|
9431
|
+
if (diagnosis?.cause === "stale-head") {
|
|
9432
|
+
return { policy, status: "failure", reason: diagnosis.reason, detail: "stale-head" };
|
|
9433
|
+
}
|
|
9434
|
+
if (diagnosis?.cause === "runner-infra") {
|
|
9435
|
+
return { policy, status: "failure", reason: diagnosis.reason, detail: "runner-infra" };
|
|
9184
9436
|
}
|
|
9185
9437
|
return { policy, status: "failure", reason, detail: "checks-failure" };
|
|
9186
9438
|
}
|
|
@@ -9434,7 +9686,7 @@ var MANAGED_GITIGNORE_LINES = [
|
|
|
9434
9686
|
// and `.cursor/rules/` because a repo may want its rules tracked despite the wall.
|
|
9435
9687
|
".codex/",
|
|
9436
9688
|
".agents/",
|
|
9437
|
-
// #2321 doctrine: the canonical worktree home is the SIBLING `../mmi-worktrees/<branch>` (outside the tree,
|
|
9689
|
+
// #2321 doctrine: the canonical worktree home is the SIBLING `../mmi-worktrees/<RepoName>/<branch>` (outside the tree,
|
|
9438
9690
|
// via `mmi-cli worktree create`), so a repo-local `.worktrees/` is NOT un-ignored org-wide — `mmi-cli
|
|
9439
9691
|
// doctor` flags one explicitly instead (buildRepoLocalWorktreeCheck) rather than baking the fallback path
|
|
9440
9692
|
// into every repo's .gitignore.
|
|
@@ -11774,6 +12026,10 @@ var PATH_OVERRIDES = {
|
|
|
11774
12026
|
"secrets grant": { category: "admin", discovery: "all-only", help_group: "Operations" },
|
|
11775
12027
|
"secrets revoke": { category: "admin", discovery: "all-only", help_group: "Operations" }
|
|
11776
12028
|
};
|
|
12029
|
+
var DEFAULT_ADMIN_MARKER = "(master-only)";
|
|
12030
|
+
var ADMIN_MARKERS = {
|
|
12031
|
+
"secrets org-catalog": "(writes master-only)"
|
|
12032
|
+
};
|
|
11777
12033
|
var HIDDEN_OPTIONS = {
|
|
11778
12034
|
"worktree create": ["--base"],
|
|
11779
12035
|
"org project set": ["--set"]
|
|
@@ -11808,27 +12064,42 @@ function setCommandMetadata(command, metadata) {
|
|
|
11808
12064
|
function commandMetadata(command) {
|
|
11809
12065
|
return command[COMMAND_METADATA];
|
|
11810
12066
|
}
|
|
11811
|
-
|
|
11812
|
-
|
|
12067
|
+
var REQUIRED_OPTION_HELP = {
|
|
12068
|
+
optionDescription(option) {
|
|
12069
|
+
const described = Help.prototype.optionDescription.call(this, option);
|
|
12070
|
+
return option.mandatory ? `(required) ${described}` : described;
|
|
12071
|
+
}
|
|
12072
|
+
};
|
|
12073
|
+
function classifyTree(command, path2, inherited, hideFromParent) {
|
|
12074
|
+
const override = PATH_OVERRIDES[path2];
|
|
12075
|
+
const metadata = override ?? inherited;
|
|
11813
12076
|
setCommandMetadata(command, metadata);
|
|
11814
|
-
|
|
12077
|
+
const hideByOverride = override !== void 0 && metadata.discovery === "hidden";
|
|
12078
|
+
if ((hideByOverride || hideFromParent) && metadata.discovery !== "primary") {
|
|
11815
12079
|
command._hidden = true;
|
|
11816
12080
|
}
|
|
12081
|
+
if (override !== void 0 && metadata.discovery === "all-only" && metadata.category === "admin" && !command.summary()) {
|
|
12082
|
+
const marker = ADMIN_MARKERS[path2] ?? DEFAULT_ADMIN_MARKER;
|
|
12083
|
+
const described = command.description();
|
|
12084
|
+
command.summary(described ? `${marker} ${described}` : marker);
|
|
12085
|
+
}
|
|
11817
12086
|
for (const option of command.options) {
|
|
11818
12087
|
if ((HIDDEN_OPTIONS[path2] ?? []).includes(option.long ?? option.short ?? "")) option.hideHelp();
|
|
11819
12088
|
}
|
|
12089
|
+
command.configureHelp(REQUIRED_OPTION_HELP);
|
|
11820
12090
|
for (const child2 of command.commands) {
|
|
11821
12091
|
const childPath = path2 ? `${path2} ${child2.name()}` : child2.name();
|
|
11822
|
-
classifyTree(child2, childPath,
|
|
12092
|
+
classifyTree(child2, childPath, metadata, false);
|
|
11823
12093
|
}
|
|
11824
12094
|
}
|
|
11825
12095
|
function applyCommandTaxonomy(program3) {
|
|
11826
12096
|
for (const command of program3.commands) {
|
|
11827
12097
|
const metadata = topLevelMetadata(command.name());
|
|
11828
12098
|
command.helpGroup(metadata.help_group);
|
|
11829
|
-
classifyTree(command, command.name(), metadata);
|
|
12099
|
+
classifyTree(command, command.name(), metadata, true);
|
|
11830
12100
|
}
|
|
11831
12101
|
program3.configureHelp({
|
|
12102
|
+
...REQUIRED_OPTION_HELP,
|
|
11832
12103
|
visibleCommands(command) {
|
|
11833
12104
|
const visible = command.commands.filter((child2) => !child2._hidden);
|
|
11834
12105
|
const helpCommand = command._getHelpCommand();
|
|
@@ -16872,7 +17143,7 @@ async function withSecrets(run) {
|
|
|
16872
17143
|
function registerSecretsCommands(program3) {
|
|
16873
17144
|
const secrets = program3.command("secrets").description("project vault \u2014 project-admins self-serve their own repo's full tree (stageless + dev/rc/main); org-infra namespaces are master-gated");
|
|
16874
17145
|
secrets.command("where").description("print where this repo's secrets live \u2014 the two-tier vault layout + well-known keys (no values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsWhere(d, o)));
|
|
16875
|
-
secrets.command("list").description("list secret NAMES + tier for
|
|
17146
|
+
secrets.command("list").description("list secret NAMES + tier for THIS repo (never values). To locate a key you cannot place \u2014 including org-infra (_org) \u2014 use the cross-slug view: `mmi-cli org access capabilities` (#3436)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsList(d, o)));
|
|
16876
17147
|
secrets.command("find <intent>").description("resolve a plain-language intent to canonical secret names + the exact keyless-use command (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((intent, o) => withSecrets(async (d) => {
|
|
16877
17148
|
if (!await secretsFind(d, intent, o)) process.exitCode = 1;
|
|
16878
17149
|
}));
|
|
@@ -16995,7 +17266,7 @@ function registerSecretsCommands(program3) {
|
|
|
16995
17266
|
});
|
|
16996
17267
|
if (!ok) process.exitCode = 1;
|
|
16997
17268
|
}));
|
|
16998
|
-
secrets.command("verify <key>").description("validate a known provider secret without printing its value").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
|
|
17269
|
+
secrets.command("verify <key>").description("validate a known provider secret without printing its value; a key with no configured provider verifier falls back to a value-free PRESENT/ABSENT reachability check (#3436)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for a granted org secret").action((key, o) => withSecrets(async (d) => {
|
|
16999
17270
|
const ok = await secretsVerify(d, key, o);
|
|
17000
17271
|
if (!ok) process.exitCode = 1;
|
|
17001
17272
|
}));
|
|
@@ -17045,7 +17316,7 @@ function registerSecretsCommands(program3) {
|
|
|
17045
17316
|
if (!ok) process.exitCode = 1;
|
|
17046
17317
|
}));
|
|
17047
17318
|
secrets.command("rm <key>").description("remove a secret from your own full project vault; org-infra requires master or an exact grant. --slug _org removes an org-infra <provider>/<KEY> value (#3315)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for the value removal \u2014 master or an exact grant (#3315)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
|
|
17048
|
-
secrets.command("use <key> [command...]").description("consume a secret KEYLESS: own-project full tree or an exactly granted org-infra key; injects into command env and never prints it").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for a granted org secret").option("--name <ENVVAR>", "env var name to inject under (default: the key leaf, UPPER_SNAKE)").action((key, command, o) => withSecrets(async (d) => {
|
|
17319
|
+
secrets.command("use <key> [command...]").description("consume a secret KEYLESS: own-project full tree or an exactly granted org-infra key; injects into command env and never prints it. The wrapped command keeps its OWN flags (`-- node -e \u2026`, `-- curl -H \u2026`) with or without the `--`, because npm's PowerShell shim swallows `--` before the CLI sees it (#3436)").allowUnknownOption().option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for a granted org secret").option("--name <ENVVAR>", "env var name to inject under (default: the key leaf, UPPER_SNAKE)").action((key, command, o) => withSecrets(async (d) => {
|
|
17049
17320
|
const ok = await secretsUse(d, key, { ...o, command });
|
|
17050
17321
|
if (ok === false) process.exitCode = 1;
|
|
17051
17322
|
}));
|
|
@@ -17459,9 +17730,9 @@ function extractWorkflowCrons(yamlText) {
|
|
|
17459
17730
|
function workflowEntry(repo, workflowPath, yamlText) {
|
|
17460
17731
|
const crons = extractWorkflowCrons(yamlText);
|
|
17461
17732
|
if (!crons.length) return null;
|
|
17462
|
-
const
|
|
17733
|
+
const basename4 = workflowPath.split("/").pop() ?? workflowPath;
|
|
17463
17734
|
return {
|
|
17464
|
-
name: `${repo}/${
|
|
17735
|
+
name: `${repo}/${basename4.replace(/\.ya?ml$/, "")}`,
|
|
17465
17736
|
cadence: crons.join(" + "),
|
|
17466
17737
|
executor: "github-actions",
|
|
17467
17738
|
llm: llmFromHeader(yamlText),
|
|
@@ -17493,16 +17764,30 @@ function awsRuleEntries(payload) {
|
|
|
17493
17764
|
}
|
|
17494
17765
|
return entries;
|
|
17495
17766
|
}
|
|
17767
|
+
function lambdaTargetFromArn(arn) {
|
|
17768
|
+
const marker = ":function:";
|
|
17769
|
+
const i = arn.indexOf(marker);
|
|
17770
|
+
if (i < 0) return { functionName: arn.split(":").pop() ?? "" };
|
|
17771
|
+
const rest = arn.slice(i + marker.length);
|
|
17772
|
+
const q = rest.indexOf(":");
|
|
17773
|
+
if (q < 0) return { functionName: rest };
|
|
17774
|
+
return { functionName: rest.slice(0, q), qualifier: rest.slice(q + 1) };
|
|
17775
|
+
}
|
|
17496
17776
|
function awsScheduleEntry(payload) {
|
|
17497
17777
|
if (!payload || typeof payload !== "object") return null;
|
|
17498
17778
|
const { Name, ScheduleExpression, State, Target, Description } = payload;
|
|
17499
17779
|
if (typeof Name !== "string" || typeof ScheduleExpression !== "string") return null;
|
|
17500
17780
|
if (State !== "ENABLED" || isJervResource(Name)) return null;
|
|
17501
17781
|
let target = "";
|
|
17782
|
+
let qualifier;
|
|
17502
17783
|
let scheduleId;
|
|
17503
17784
|
if (Target && typeof Target === "object") {
|
|
17504
17785
|
const { Arn, Input } = Target;
|
|
17505
|
-
if (typeof Arn === "string")
|
|
17786
|
+
if (typeof Arn === "string") {
|
|
17787
|
+
const parsed = lambdaTargetFromArn(Arn);
|
|
17788
|
+
target = parsed.functionName;
|
|
17789
|
+
qualifier = parsed.qualifier;
|
|
17790
|
+
}
|
|
17506
17791
|
if (typeof Input === "string") {
|
|
17507
17792
|
try {
|
|
17508
17793
|
const parsed = JSON.parse(Input);
|
|
@@ -17511,13 +17796,15 @@ function awsScheduleEntry(payload) {
|
|
|
17511
17796
|
}
|
|
17512
17797
|
}
|
|
17513
17798
|
}
|
|
17799
|
+
const sourceBase = `aws scheduler schedule ${Name} (eu-central-1)`;
|
|
17800
|
+
const source = qualifier && target ? `${sourceBase} \u2192 ${target}:${qualifier}` : sourceBase;
|
|
17514
17801
|
return {
|
|
17515
17802
|
name: Name,
|
|
17516
17803
|
cadence: ScheduleExpression,
|
|
17517
17804
|
executor: target ? `aws-scheduler \u2192 ${target}` : "aws-scheduler",
|
|
17518
17805
|
llm: readLlmDeclaration(typeof Description === "string" ? Description : void 0),
|
|
17519
17806
|
resolved: "live",
|
|
17520
|
-
source
|
|
17807
|
+
source,
|
|
17521
17808
|
...scheduleId ? { scheduleId } : {}
|
|
17522
17809
|
};
|
|
17523
17810
|
}
|
|
@@ -17673,7 +17960,7 @@ function strayCronDrifts(workflows) {
|
|
|
17673
17960
|
function unlauncheredLlmDrift(entry) {
|
|
17674
17961
|
if (entry.llm !== "yes") return null;
|
|
17675
17962
|
if (isHarbourLlmLauncher(entry.executor)) return null;
|
|
17676
|
-
if (entry.executor
|
|
17963
|
+
if (entry.executor === "aws-scheduler \u2192 mmi-hub-schedule-dispatcher") return null;
|
|
17677
17964
|
const allowed = [...HARBOUR_LLM_LAUNCHERS].join(", ");
|
|
17678
17965
|
return {
|
|
17679
17966
|
class: "unlaunchered-llm",
|
|
@@ -17766,8 +18053,8 @@ async function repoWorkflowEntries(client, repo) {
|
|
|
17766
18053
|
);
|
|
17767
18054
|
if (typeof contents?.content !== "string" || contents.encoding !== "base64") continue;
|
|
17768
18055
|
const text = Buffer.from(contents.content, "base64").toString("utf8");
|
|
17769
|
-
const
|
|
17770
|
-
const name = `${repo}/${
|
|
18056
|
+
const basename4 = wf.path.split("/").pop() ?? wf.path;
|
|
18057
|
+
const name = `${repo}/${basename4.replace(/\.ya?ml$/, "")}`;
|
|
17771
18058
|
workflows.push({ name, yamlText: text });
|
|
17772
18059
|
const entry = workflowEntry(repo, wf.path, text);
|
|
17773
18060
|
if (entry) entries.push(entry);
|
|
@@ -17990,8 +18277,8 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
|
|
|
17990
18277
|
const crons = extractWorkflowCrons(yamlText);
|
|
17991
18278
|
const header = parseScheduleHeader(yamlText);
|
|
17992
18279
|
if (!crons.length && !header.schedule) return null;
|
|
17993
|
-
const
|
|
17994
|
-
const expectedId = `${repo}/${
|
|
18280
|
+
const basename4 = workflowPath.split("/").pop() ?? workflowPath;
|
|
18281
|
+
const expectedId = `${repo}/${basename4.replace(/\.ya?ml$/, "")}`;
|
|
17995
18282
|
const missing = SCHEDULE_HEADER_FIELDS.filter((f) => !header[f]);
|
|
17996
18283
|
if (missing.length) {
|
|
17997
18284
|
throw new Error(`schedules lift: ${expectedId} (${workflowPath}) is a scheduled workflow but is missing the eight-field entry-template header field(s): ${missing.join(", ")} \u2014 see docs/schedules.md "The entry template".`);
|
|
@@ -18179,7 +18466,9 @@ function planInfraTunnel(hostname, upstream) {
|
|
|
18179
18466
|
].join("\n");
|
|
18180
18467
|
const steps = [
|
|
18181
18468
|
"Install cloudflared on the box (or run locally for dev-only tunnels).",
|
|
18182
|
-
|
|
18469
|
+
// #3463: `--slug <provider> <KEY>` is the retired shape and resolves to nothing. Org-infra is one
|
|
18470
|
+
// slug (`_org`) with the provider as the key's first segment.
|
|
18471
|
+
"Fetch CF account creds (master-gated): mmi-cli secrets use cloudflare/API_TOKEN --slug _org -- <cmd>",
|
|
18183
18472
|
"Create tunnel: cloudflared tunnel create " + tunnelName,
|
|
18184
18473
|
"Route DNS: cloudflared tunnel route dns " + tunnelName + " " + host,
|
|
18185
18474
|
"Write config (below) to /etc/cloudflared/config.yml and run: cloudflared tunnel run " + tunnelName,
|
|
@@ -18204,7 +18493,7 @@ function formatInfraTunnelPlan(plan) {
|
|
|
18204
18493
|
function registerEdgeCommands(program3) {
|
|
18205
18494
|
const edge = program3.command("edge").description("infra-service edge helpers (non-tenant loopback services)");
|
|
18206
18495
|
const tunnel = edge.command("tunnel").description("Cloudflare Tunnel planning for loopback infra services");
|
|
18207
|
-
tunnel.command("plan").description("print a cloudflared tunnel plan (no API calls \u2014
|
|
18496
|
+
tunnel.command("plan").description("print a cloudflared tunnel plan (no API calls \u2014 pair with `secrets use cloudflare/API_TOKEN --slug _org`)").requiredOption("--hostname <host>", "public hostname (e.g. open-meteo.tools.mutatismutandis.co)").requiredOption("--upstream <url>", "loopback origin (e.g. http://127.0.0.1:8080)").option("--json", "machine-readable output").action((o) => {
|
|
18208
18497
|
try {
|
|
18209
18498
|
const plan = planInfraTunnel(o.hostname, o.upstream);
|
|
18210
18499
|
if (o.json) {
|
|
@@ -20101,6 +20390,7 @@ function registerBoardCommands(program3) {
|
|
|
20101
20390
|
// src/merge-cleanup.ts
|
|
20102
20391
|
var import_node_fs24 = require("node:fs");
|
|
20103
20392
|
var import_promises5 = require("node:fs/promises");
|
|
20393
|
+
var import_node_path22 = require("node:path");
|
|
20104
20394
|
var import_node_child_process11 = require("node:child_process");
|
|
20105
20395
|
|
|
20106
20396
|
// src/board-advance.ts
|
|
@@ -20410,7 +20700,7 @@ function assertPrMergeHousekeepingClean(startingPath, context, options = {}) {
|
|
|
20410
20700
|
if (verdict.blocked) throw new Error(verdict.reason);
|
|
20411
20701
|
return housekeeping;
|
|
20412
20702
|
}
|
|
20413
|
-
async function applyGcPlan(plan, remote) {
|
|
20703
|
+
async function applyGcPlan(plan, remote, opts = {}) {
|
|
20414
20704
|
const result = { removedBranches: [], removedRemoteBranches: [], removedTrackingRefs: [], removedWorktreeDirs: [], failed: [], pruned: false };
|
|
20415
20705
|
const beforeWorktrees = parseWorktreePorcelain(
|
|
20416
20706
|
(await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
|
|
@@ -20444,8 +20734,9 @@ async function applyGcPlan(plan, remote) {
|
|
|
20444
20734
|
result.failed.push(...branchTracking.failed);
|
|
20445
20735
|
const removeDeps = worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout);
|
|
20446
20736
|
const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
20447
|
-
const siblingRoot = siblingMmiWorktreesRoot(repoRoot2);
|
|
20448
20737
|
const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
|
|
20738
|
+
const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path22.dirname)((0, import_node_path22.dirname)(worktreeGitRoot)) : repoRoot2;
|
|
20739
|
+
const siblingRoot = opts.root ? resolveExplicitScanRoot(opts.root, primaryRepoRoot) : siblingMmiWorktreesRoot(primaryRepoRoot);
|
|
20449
20740
|
for (const wt of plan.worktreeDirs) {
|
|
20450
20741
|
try {
|
|
20451
20742
|
const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
|
|
@@ -20748,14 +21039,30 @@ function parseRestPrSnapshot(json) {
|
|
|
20748
21039
|
const pr2 = json ?? {};
|
|
20749
21040
|
const headSha = pr2.head?.sha ?? "";
|
|
20750
21041
|
if (!headSha) throw new Error("pulls endpoint returned no head SHA");
|
|
21042
|
+
const headRepo = pr2.head?.repo?.full_name;
|
|
21043
|
+
const baseRepo = pr2.base?.repo?.full_name;
|
|
20751
21044
|
return {
|
|
20752
21045
|
headSha,
|
|
21046
|
+
headRef: pr2.head?.ref ?? "",
|
|
21047
|
+
// Only call it a fork when BOTH names are known and differ — an absent name (private/deleted fork)
|
|
21048
|
+
// must not read as same-repo and send a git-ref probe at a branch the base repo does not have.
|
|
21049
|
+
headIsFork: Boolean(headRepo && baseRepo && headRepo !== baseRepo),
|
|
20753
21050
|
mergeable: pr2.mergeable === true ? "MERGEABLE" : pr2.mergeable === false ? "CONFLICTING" : "UNKNOWN",
|
|
20754
21051
|
baseRef: pr2.base?.ref ?? "development",
|
|
20755
21052
|
merged: pr2.merged === true,
|
|
20756
21053
|
state: pr2.state ?? ""
|
|
20757
21054
|
};
|
|
20758
21055
|
}
|
|
21056
|
+
async function fetchBranchTipSha(repo, ref, gh = defaultGhApi) {
|
|
21057
|
+
if (!ref) return null;
|
|
21058
|
+
try {
|
|
21059
|
+
const parsed = JSON.parse(await gh([`repos/${repo}/git/ref/heads/${ref}`]));
|
|
21060
|
+
const sha = parsed.object?.sha;
|
|
21061
|
+
return typeof sha === "string" && sha ? sha : null;
|
|
21062
|
+
} catch {
|
|
21063
|
+
return null;
|
|
21064
|
+
}
|
|
21065
|
+
}
|
|
20759
21066
|
async function fetchRestPrSnapshot(prNumber, repo, gh = defaultGhApi) {
|
|
20760
21067
|
return parseRestPrSnapshot(JSON.parse(await gh([`repos/${repo}/pulls/${prNumber}`])));
|
|
20761
21068
|
}
|
|
@@ -20790,36 +21097,54 @@ async function pollRestPrMerged(prNumber, repo, gh = defaultGhApi) {
|
|
|
20790
21097
|
}
|
|
20791
21098
|
}
|
|
20792
21099
|
var CI_BUDGET_ANNOTATION_TITLE = "CI budget exceeded";
|
|
21100
|
+
var CI_DISK_ANNOTATION_TITLE = "CI runner disk";
|
|
21101
|
+
var CI_TOOLCHAIN_ANNOTATION_TITLE = "CI runner toolchain";
|
|
21102
|
+
var RUNNER_INFRA_ANNOTATION_TITLES = /* @__PURE__ */ new Set([
|
|
21103
|
+
CI_BUDGET_ANNOTATION_TITLE,
|
|
21104
|
+
CI_DISK_ANNOTATION_TITLE,
|
|
21105
|
+
CI_TOOLCHAIN_ANNOTATION_TITLE
|
|
21106
|
+
]);
|
|
20793
21107
|
function isErrorAnnotation(a) {
|
|
20794
21108
|
const level = a.annotation_level?.toLowerCase();
|
|
20795
21109
|
return level === "failure" || level === "error";
|
|
20796
21110
|
}
|
|
20797
21111
|
function classifyFailedChecks(failing) {
|
|
20798
|
-
const
|
|
21112
|
+
const infraFailures = [];
|
|
20799
21113
|
const otherFailures = [];
|
|
20800
21114
|
for (const check of failing) {
|
|
20801
21115
|
const errors = check.annotations.filter(isErrorAnnotation);
|
|
20802
|
-
const
|
|
20803
|
-
(
|
|
21116
|
+
const allInfra = errors.length > 0 && errors.every((a) => RUNNER_INFRA_ANNOTATION_TITLES.has(a.title?.trim() ?? ""));
|
|
21117
|
+
(allInfra ? infraFailures : otherFailures).push(check.name);
|
|
20804
21118
|
}
|
|
20805
|
-
if (
|
|
21119
|
+
if (infraFailures.length && !otherFailures.length) {
|
|
20806
21120
|
return {
|
|
20807
|
-
cause: "
|
|
20808
|
-
|
|
21121
|
+
cause: "runner-infra",
|
|
21122
|
+
infraFailures,
|
|
20809
21123
|
otherFailures,
|
|
20810
|
-
reason: `${
|
|
21124
|
+
reason: `${infraFailures.length} check(s) failed on RUNNER INFRASTRUCTURE, not your diff: ${infraFailures.join(", ")}. No test failed \u2014 the shared runner hit a wall-clock budget kill, ran out of disk, or was missing a toolchain (the check annotation names which). Re-run once the runner drains/heals (gh run rerun --failed); do not debug the diff.`
|
|
20811
21125
|
};
|
|
20812
21126
|
}
|
|
20813
21127
|
return {
|
|
20814
21128
|
cause: "checks-failure",
|
|
20815
|
-
|
|
21129
|
+
infraFailures,
|
|
20816
21130
|
otherFailures,
|
|
20817
|
-
reason:
|
|
21131
|
+
reason: infraFailures.length ? `checks failed: ${otherFailures.join(", ")}; separately, ${infraFailures.join(", ")} failed on runner infrastructure, not a test.` : `checks failed: ${otherFailures.join(", ") || "unknown"}.`
|
|
20818
21132
|
};
|
|
20819
21133
|
}
|
|
20820
21134
|
async function diagnoseFailedRestChecks(prNumber, repo, gh = defaultGhApi) {
|
|
20821
21135
|
try {
|
|
20822
21136
|
const snapshot = await fetchRestPrSnapshot(prNumber, repo, gh);
|
|
21137
|
+
if (!snapshot.headIsFork) {
|
|
21138
|
+
const branchTip = await fetchBranchTipSha(repo, snapshot.headRef, gh);
|
|
21139
|
+
if (branchTip && branchTip !== snapshot.headSha) {
|
|
21140
|
+
return {
|
|
21141
|
+
cause: "stale-head",
|
|
21142
|
+
infraFailures: [],
|
|
21143
|
+
otherFailures: [],
|
|
21144
|
+
reason: `the PR head (${snapshot.headSha.slice(0, 7)}) is BEHIND the branch tip (${branchTip.slice(0, 7)}) \u2014 GitHub likely dropped a \`synchronize\` event, so the reported red is a superseded commit and NO run exists for your current push. This is not your diff failing. Re-push (an empty commit is enough) or close and reopen the PR to re-sync the head and trigger a fresh run.`
|
|
21145
|
+
};
|
|
21146
|
+
}
|
|
21147
|
+
}
|
|
20823
21148
|
const failing = (await fetchHeadCheckRuns(snapshot.headSha, repo, gh)).filter((run) => classifyCheckRun(run) === "fail" && typeof run.id === "number");
|
|
20824
21149
|
if (!failing.length) return null;
|
|
20825
21150
|
const annotated = await Promise.all(failing.map(async (run) => ({
|
|
@@ -20844,7 +21169,7 @@ async function fetchRestCorePool(gh = defaultGhApi) {
|
|
|
20844
21169
|
|
|
20845
21170
|
// src/worktree-lifecycle-commands.ts
|
|
20846
21171
|
var import_node_fs25 = require("node:fs");
|
|
20847
|
-
var
|
|
21172
|
+
var import_node_path23 = require("node:path");
|
|
20848
21173
|
var GH_TIMEOUT_MS = 2e4;
|
|
20849
21174
|
var DEFAULT_BASE = "origin/development";
|
|
20850
21175
|
var DEFAULT_REMOTE = "origin";
|
|
@@ -20947,7 +21272,7 @@ function classifyStaleLeaks(input) {
|
|
|
20947
21272
|
var defaultOrphanDirScanDeps = {
|
|
20948
21273
|
listDirs: (root) => {
|
|
20949
21274
|
try {
|
|
20950
|
-
return (0, import_node_fs25.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0,
|
|
21275
|
+
return (0, import_node_fs25.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path23.join)(root, e.name));
|
|
20951
21276
|
} catch {
|
|
20952
21277
|
return [];
|
|
20953
21278
|
}
|
|
@@ -21003,13 +21328,13 @@ function registerWorktreeCommands(program3) {
|
|
|
21003
21328
|
if (PROTECTED_BRANCHES2.has(branch)) {
|
|
21004
21329
|
return fail(`worktree land: refusing to land protected branch '${branch}'`, { code: ERROR_CODES.ERR_BAD_ENUM });
|
|
21005
21330
|
}
|
|
21006
|
-
const gitFile = (0,
|
|
21331
|
+
const gitFile = (0, import_node_path23.join)(wtPath, ".git");
|
|
21007
21332
|
const isLinked = (0, import_node_fs25.existsSync)(gitFile) && (0, import_node_fs25.statSync)(gitFile).isFile();
|
|
21008
21333
|
if (apply && !isLinked) {
|
|
21009
21334
|
return fail("worktree land: run from inside the linked worktree you want to land (this is the primary checkout)");
|
|
21010
21335
|
}
|
|
21011
21336
|
const commonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
21012
|
-
const primaryCheckout = commonDir ? (0,
|
|
21337
|
+
const primaryCheckout = commonDir ? (0, import_node_path23.dirname)(commonDir) : wtPath;
|
|
21013
21338
|
const stageSummary = readStageSummary(wtPath);
|
|
21014
21339
|
const hasStage = stageSummary != null;
|
|
21015
21340
|
const steps = landSteps(branch, true, hasStage);
|
|
@@ -21119,10 +21444,15 @@ async function gatherWorktreeContext() {
|
|
|
21119
21444
|
const s = readStageSummary(wt.path);
|
|
21120
21445
|
if (s) stages.push({ path: wt.path, port: s.port });
|
|
21121
21446
|
}
|
|
21122
|
-
const
|
|
21447
|
+
const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
|
|
21448
|
+
const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path23.dirname)((0, import_node_path23.dirname)(worktreeGitRoot)) : repoRoot2;
|
|
21449
|
+
const wtRoot = siblingMmiWorktreesRoot(primaryRepoRoot);
|
|
21123
21450
|
let orphanDirs = [];
|
|
21124
21451
|
if ((0, import_node_fs25.existsSync)(wtRoot)) {
|
|
21125
|
-
orphanDirs = scanOrphanDirs(wtRoot,
|
|
21452
|
+
orphanDirs = scanOrphanDirs(wtRoot, worktreeGitRoot, {
|
|
21453
|
+
...defaultOrphanDirScanDeps,
|
|
21454
|
+
listDirs: (root) => worktreeScanDirs(root, primaryRepoRoot, defaultOrphanDirScanDeps.listDirs, isRepoCheckoutDir)
|
|
21455
|
+
});
|
|
21126
21456
|
}
|
|
21127
21457
|
return { worktrees, localBranches, currentBranch: currentBranch2, openPrBranches, closedBranches, stages, orphanDirs };
|
|
21128
21458
|
}
|
|
@@ -21711,11 +22041,11 @@ ${lines}`);
|
|
|
21711
22041
|
|
|
21712
22042
|
// src/train-commands.ts
|
|
21713
22043
|
var import_node_fs28 = require("node:fs");
|
|
21714
|
-
var
|
|
22044
|
+
var import_node_path25 = require("node:path");
|
|
21715
22045
|
|
|
21716
22046
|
// src/plugin-guard-io.ts
|
|
21717
22047
|
var import_node_fs27 = require("node:fs");
|
|
21718
|
-
var
|
|
22048
|
+
var import_node_path24 = require("node:path");
|
|
21719
22049
|
var import_node_os6 = require("node:os");
|
|
21720
22050
|
|
|
21721
22051
|
// src/plugin-guard.ts
|
|
@@ -21845,7 +22175,7 @@ function runHostBin(bin, args, opts) {
|
|
|
21845
22175
|
}
|
|
21846
22176
|
var installedPluginsPath2 = (surface = detectSurface(process.env)) => {
|
|
21847
22177
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
21848
|
-
return (0,
|
|
22178
|
+
return (0, import_node_path24.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
21849
22179
|
};
|
|
21850
22180
|
function readInstalledPlugins(surface = detectSurface(process.env)) {
|
|
21851
22181
|
try {
|
|
@@ -21857,11 +22187,11 @@ function readInstalledPlugins(surface = detectSurface(process.env)) {
|
|
|
21857
22187
|
function marketplaceCloneCandidates(surface, home) {
|
|
21858
22188
|
if (surface === "codex") {
|
|
21859
22189
|
return [
|
|
21860
|
-
(0,
|
|
21861
|
-
(0,
|
|
22190
|
+
(0, import_node_path24.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
|
|
22191
|
+
(0, import_node_path24.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
|
|
21862
22192
|
];
|
|
21863
22193
|
}
|
|
21864
|
-
return [(0,
|
|
22194
|
+
return [(0, import_node_path24.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
|
|
21865
22195
|
}
|
|
21866
22196
|
function marketplaceClonePresent(surface, home, exists = import_node_fs27.existsSync) {
|
|
21867
22197
|
return marketplaceCloneCandidates(surface, home).some(exists);
|
|
@@ -21891,7 +22221,7 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
|
|
|
21891
22221
|
isOrgRepo,
|
|
21892
22222
|
installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
|
|
21893
22223
|
marketplaceClonePresent: marketplaceClonePresent(surface, (0, import_node_os6.homedir)()),
|
|
21894
|
-
pluginCachePresent: (0, import_node_fs27.existsSync)((0,
|
|
22224
|
+
pluginCachePresent: (0, import_node_fs27.existsSync)((0, import_node_path24.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
|
|
21895
22225
|
};
|
|
21896
22226
|
}
|
|
21897
22227
|
function claudePluginGuardState(isOrgRepo) {
|
|
@@ -22026,7 +22356,7 @@ function formatTrainStatus(r) {
|
|
|
22026
22356
|
// src/train-commands.ts
|
|
22027
22357
|
function readRepoVersion() {
|
|
22028
22358
|
try {
|
|
22029
|
-
return JSON.parse((0, import_node_fs28.readFileSync)((0,
|
|
22359
|
+
return JSON.parse((0, import_node_fs28.readFileSync)((0, import_node_path25.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
22030
22360
|
} catch {
|
|
22031
22361
|
return void 0;
|
|
22032
22362
|
}
|
|
@@ -23169,6 +23499,28 @@ function checkRepoWorktrees(probe) {
|
|
|
23169
23499
|
]
|
|
23170
23500
|
};
|
|
23171
23501
|
}
|
|
23502
|
+
function formatBytes(bytes, partial) {
|
|
23503
|
+
const prefix = partial ? "\u2265" : "";
|
|
23504
|
+
return bytes >= 1e9 ? `${prefix}${(bytes / 1e9).toFixed(2)} GB` : `${prefix}${(bytes / 1e6).toFixed(1)} MB`;
|
|
23505
|
+
}
|
|
23506
|
+
function checkWorktreeRoots(probe) {
|
|
23507
|
+
if (!probe) return null;
|
|
23508
|
+
const evidence = [
|
|
23509
|
+
`scanned by worktree gc/list: ${probe.authoritative}`,
|
|
23510
|
+
...probe.stray.length ? probe.stray.map((s) => `unreachable by any gc: ${s.path} \u2014 ${s.dirs} dir(s), ${formatBytes(s.bytes, s.partial)}`) : ["no other *worktrees* directory beside this repo"]
|
|
23511
|
+
];
|
|
23512
|
+
if (!probe.stray.length) return { ok: true, label: "worktree roots", verbose: evidence };
|
|
23513
|
+
const named = probe.stray.map((s) => `${s.path} (${s.dirs} dir(s), ${formatBytes(s.bytes, s.partial)})`).join("; ");
|
|
23514
|
+
const cutShort = probe.stray.some((s) => s.partial) ? " (a \u2265 figure is a floor \u2014 the size walk hit its time budget, so the real total is larger)" : "";
|
|
23515
|
+
return {
|
|
23516
|
+
ok: false,
|
|
23517
|
+
label: "worktree roots",
|
|
23518
|
+
// The fix is a review, never a delete: `gc --root` still refuses anything it cannot classify as dead,
|
|
23519
|
+
// and doctor itself — with or without --apply — never touches these directories.
|
|
23520
|
+
fix: `${probe.stray.length} worktrees root(s) outside ${probe.authoritative} \u2014 ${named}; no gc scans them. Review, then sweep deliberately from the owning repo with \`mmi-cli worktree gc --root <path> --apply\` (report-only here: doctor never deletes these)${cutShort}`,
|
|
23521
|
+
verbose: evidence
|
|
23522
|
+
};
|
|
23523
|
+
}
|
|
23172
23524
|
function pluginHealTrigger(probe) {
|
|
23173
23525
|
if (probe.guardState === "no-install" || probe.guardState === "unresolved") return "unresolved";
|
|
23174
23526
|
const behind = Boolean(probe.installed && probe.released) && compareVersions(probe.installed, probe.released) < 0;
|
|
@@ -23306,6 +23658,45 @@ function checkDocsAudit(probe) {
|
|
|
23306
23658
|
}
|
|
23307
23659
|
return { ok: true, label: "docs-audit", detail: probe.detail, verbose: [probe.detail] };
|
|
23308
23660
|
}
|
|
23661
|
+
function checkSessionPayload(probe) {
|
|
23662
|
+
if (!probe) return null;
|
|
23663
|
+
const { chars } = probe;
|
|
23664
|
+
const evidence = [
|
|
23665
|
+
`last emitted: ${chars} characters${probe.at ? ` (${probe.at})` : ""}`,
|
|
23666
|
+
`budget ${SESSION_PAYLOAD_BUDGET_CHARS}, Claude Code cap ${SESSION_PAYLOAD_CAP_CHARS}`,
|
|
23667
|
+
// Named in the evidence because the units are the whole trap: the cap is documented in characters
|
|
23668
|
+
// while the runtime's overflow message reports bytes, and they diverge on the non-ASCII the doctrine
|
|
23669
|
+
// blocks preserve. A reader comparing this number to a byte figure is comparing two different things.
|
|
23670
|
+
"measured in CHARACTERS (the documented unit of the cap), never bytes"
|
|
23671
|
+
];
|
|
23672
|
+
const status = sessionPayloadStatus(chars);
|
|
23673
|
+
if (status === "red") {
|
|
23674
|
+
return {
|
|
23675
|
+
ok: false,
|
|
23676
|
+
label: "SessionStart payload",
|
|
23677
|
+
fix: `${chars} characters exceeds the ${SESSION_PAYLOAD_CAP_CHARS}-character additionalContext cap \u2014 Claude Code truncates it to a ~2,000-character preview with no error, so agents are starting on partial doctrine; cut a block from the session-start banner`,
|
|
23678
|
+
verbose: evidence
|
|
23679
|
+
};
|
|
23680
|
+
}
|
|
23681
|
+
if (status === "amber") {
|
|
23682
|
+
return {
|
|
23683
|
+
ok: true,
|
|
23684
|
+
// warn: the banner lane must print this too. Without it the banner's failing-only filter made the
|
|
23685
|
+
// amber tier dead code on the one lane that runs every session — drift past the budget was
|
|
23686
|
+
// invisible until it became a truncation. Still ✓, still never moves the exit code.
|
|
23687
|
+
warn: true,
|
|
23688
|
+
label: "SessionStart payload",
|
|
23689
|
+
detail: `amber \u2014 ${chars} characters, over the ${SESSION_PAYLOAD_BUDGET_CHARS}-character budget and ${SESSION_PAYLOAD_CAP_CHARS - chars} from the hard cap`,
|
|
23690
|
+
verbose: evidence
|
|
23691
|
+
};
|
|
23692
|
+
}
|
|
23693
|
+
return {
|
|
23694
|
+
ok: true,
|
|
23695
|
+
label: "SessionStart payload",
|
|
23696
|
+
detail: `${chars} of ${SESSION_PAYLOAD_BUDGET_CHARS} budgeted characters`,
|
|
23697
|
+
verbose: evidence
|
|
23698
|
+
};
|
|
23699
|
+
}
|
|
23309
23700
|
function planGitignore(current) {
|
|
23310
23701
|
const { content, changed } = upsertManagedGitignoreBlock(current);
|
|
23311
23702
|
return changed ? { ok: false, content } : { ok: true };
|
|
@@ -23401,6 +23792,10 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
23401
23792
|
checks.push(checkCliVersion(cliInput));
|
|
23402
23793
|
}
|
|
23403
23794
|
checks.push(checkPluginCache(deps.pluginCache()));
|
|
23795
|
+
if (deps.sessionPayload) {
|
|
23796
|
+
const payload = checkSessionPayload(deps.sessionPayload());
|
|
23797
|
+
if (payload) checks.push(payload);
|
|
23798
|
+
}
|
|
23404
23799
|
if (!opts.fast && !opts.banner && !opts.preflight) {
|
|
23405
23800
|
const probe = await deps.schedulesNotebook().catch((e) => ({
|
|
23406
23801
|
armed: 0,
|
|
@@ -23421,6 +23816,11 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
23421
23816
|
const docsAudit2 = checkDocsAudit(probe);
|
|
23422
23817
|
if (docsAudit2) checks.push(docsAudit2);
|
|
23423
23818
|
}
|
|
23819
|
+
if (!opts.fast && !opts.banner && !opts.preflight && deps.worktreeRoots) {
|
|
23820
|
+
const probe = await deps.worktreeRoots().catch(() => void 0);
|
|
23821
|
+
const roots = checkWorktreeRoots(probe);
|
|
23822
|
+
if (roots) checks.push(roots);
|
|
23823
|
+
}
|
|
23424
23824
|
if (isOrgRepo && !opts.fast && !opts.banner) {
|
|
23425
23825
|
try {
|
|
23426
23826
|
checks.push(checkTrainSync(await deps.syncTrain()));
|
|
@@ -23492,7 +23892,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
23492
23892
|
return exitCode;
|
|
23493
23893
|
}
|
|
23494
23894
|
if (opts.banner) {
|
|
23495
|
-
const actionable = checks.filter((c) => !c.ok);
|
|
23895
|
+
const actionable = checks.filter((c) => !c.ok || c.warn);
|
|
23496
23896
|
for (const c of actionable) {
|
|
23497
23897
|
io.log(renderReport([c], { restartPending: false }));
|
|
23498
23898
|
if (opts.verbose) for (const evidence of c.verbose ?? []) io.log(`${VERBOSE_INDENT}${evidence}`);
|
|
@@ -23585,7 +23985,7 @@ function ghAccountCaveat(announcedLogin, accounts) {
|
|
|
23585
23985
|
// src/doctor-io.ts
|
|
23586
23986
|
var import_node_fs29 = require("node:fs");
|
|
23587
23987
|
var import_node_os7 = require("node:os");
|
|
23588
|
-
var
|
|
23988
|
+
var import_node_path26 = require("node:path");
|
|
23589
23989
|
var import_node_child_process12 = require("node:child_process");
|
|
23590
23990
|
var import_node_util8 = require("node:util");
|
|
23591
23991
|
var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process12.execFile);
|
|
@@ -23593,7 +23993,7 @@ var MMI_PLUGIN_ID2 = "mmi@mutmutco";
|
|
|
23593
23993
|
function installedClaudePluginVersion() {
|
|
23594
23994
|
try {
|
|
23595
23995
|
const file = JSON.parse(
|
|
23596
|
-
(0, import_node_fs29.readFileSync)((0,
|
|
23996
|
+
(0, import_node_fs29.readFileSync)((0, import_node_path26.join)((0, import_node_os7.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
|
|
23597
23997
|
);
|
|
23598
23998
|
const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
|
|
23599
23999
|
if (versions.length === 0) return void 0;
|
|
@@ -23614,7 +24014,7 @@ function worktreeRootSync() {
|
|
|
23614
24014
|
}
|
|
23615
24015
|
var gitignorePath = () => {
|
|
23616
24016
|
const root = worktreeRootSync();
|
|
23617
|
-
return root === null ? null : (0,
|
|
24017
|
+
return root === null ? null : (0, import_node_path26.join)(root, ".gitignore");
|
|
23618
24018
|
};
|
|
23619
24019
|
function readGitignore() {
|
|
23620
24020
|
const path2 = gitignorePath();
|
|
@@ -23653,7 +24053,7 @@ async function repoRoot() {
|
|
|
23653
24053
|
}
|
|
23654
24054
|
function hasRepoLocalWorktrees() {
|
|
23655
24055
|
const root = worktreeRootSync();
|
|
23656
|
-
return root !== null && (0, import_node_fs29.existsSync)((0,
|
|
24056
|
+
return root !== null && (0, import_node_fs29.existsSync)((0, import_node_path26.join)(root, ".worktrees"));
|
|
23657
24057
|
}
|
|
23658
24058
|
|
|
23659
24059
|
// src/index.ts
|
|
@@ -23776,7 +24176,14 @@ function mmiDoctorDeps() {
|
|
|
23776
24176
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
23777
24177
|
const status = docsAuditStatus(await readDocsAuditFetch(repo), { repo, today });
|
|
23778
24178
|
return { armed: status.state !== "not-armed", ok: status.ok, detail: status.line };
|
|
23779
|
-
}
|
|
24179
|
+
},
|
|
24180
|
+
// #3471: the competing-worktrees-roots probe. Full doctor only (the gather skips it on
|
|
24181
|
+
// fast/banner/preflight), and READ-ONLY by construction — it stats, it never removes. Nothing here is
|
|
24182
|
+
// wired to a reaper, in either mode.
|
|
24183
|
+
worktreeRoots: async () => worktreeRootsProbe(await repoRoot()),
|
|
24184
|
+
// #3470: what the SessionStart hook LAST emitted, measured at emission by the session-start verb below.
|
|
24185
|
+
// A local record read — cheap enough for every lane, including the banner.
|
|
24186
|
+
sessionPayload: () => readSessionPayload(process.cwd())
|
|
23780
24187
|
};
|
|
23781
24188
|
}
|
|
23782
24189
|
async function requireFreshTrainCli(commandName) {
|
|
@@ -23901,7 +24308,7 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
|
23901
24308
|
});
|
|
23902
24309
|
var rules = program2.command("rules").description("org-managed .gitignore delivery");
|
|
23903
24310
|
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) => {
|
|
23904
|
-
const path2 = (0,
|
|
24311
|
+
const path2 = (0, import_node_path27.join)(process.cwd(), ".gitignore");
|
|
23905
24312
|
const current = (0, import_node_fs30.existsSync)(path2) ? (0, import_node_fs30.readFileSync)(path2, "utf8") : null;
|
|
23906
24313
|
const plan = planManagedGitignore(current);
|
|
23907
24314
|
const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
|
|
@@ -24023,12 +24430,12 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
|
|
|
24023
24430
|
process.exit(process.exitCode ?? 0);
|
|
24024
24431
|
});
|
|
24025
24432
|
});
|
|
24026
|
-
gcCmd.option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR local+remote branches, linked worktrees, and stale tracking refs").option("--json", "machine-readable output").option("--scratch", "prune safe local scratch (#1864) instead of git branches/refs; local plans are surfaced advisory-only, never auto-pruned").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to inspect per state", "200").action(async (o) => {
|
|
24433
|
+
gcCmd.option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR local+remote branches, linked worktrees, and stale tracking refs").option("--json", "machine-readable output").option("--scratch", "prune safe local scratch (#1864) instead of git branches/refs; local plans are surfaced advisory-only, never auto-pruned").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to inspect per state", "200").option("--root <path>", "sweep an explicitly named worktrees root instead of the authoritative ../mmi-worktrees (#3471) \u2014 descends into this repo container when present; ownership, dead-dir, and content guards still apply").action(async (o) => {
|
|
24027
24434
|
if (o.apply && o.dryRun) return fail("worktree gc: choose either --dry-run or --apply");
|
|
24028
24435
|
if (o.scratch) {
|
|
24029
24436
|
try {
|
|
24030
|
-
const
|
|
24031
|
-
const run = executeScratchGc(
|
|
24437
|
+
const root2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
24438
|
+
const run = executeScratchGc(root2, { apply: Boolean(o.apply) });
|
|
24032
24439
|
if (o.json) return console.log(JSON.stringify({ dryRun: !o.apply, ...run }, null, 2));
|
|
24033
24440
|
return console.log(formatScratchGcPlan(run.plan, Boolean(o.apply), run.applied));
|
|
24034
24441
|
} catch (e) {
|
|
@@ -24037,8 +24444,19 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
|
|
|
24037
24444
|
}
|
|
24038
24445
|
const limit = Number.parseInt(o.limit, 10);
|
|
24039
24446
|
if (!Number.isFinite(limit) || limit < 1) return fail("worktree gc: --limit must be a positive integer");
|
|
24447
|
+
let root;
|
|
24448
|
+
if (o.root !== void 0) {
|
|
24449
|
+
root = (0, import_node_path27.resolve)(o.root);
|
|
24450
|
+
if (!(0, import_node_fs30.existsSync)(root) || !(0, import_node_fs30.statSync)(root).isDirectory()) return fail(`worktree gc: --root ${o.root} is not a directory`);
|
|
24451
|
+
const gcRepoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
24452
|
+
if (isPathUnderDirectory(gcRepoRoot, root)) {
|
|
24453
|
+
return fail(`worktree gc: --root ${root} contains this checkout \u2014 name a worktrees root, not the repo or an ancestor of it`);
|
|
24454
|
+
}
|
|
24455
|
+
}
|
|
24040
24456
|
try {
|
|
24041
|
-
const plan = await gcPlan(o.remote, limit);
|
|
24457
|
+
const plan = await gcPlan(o.remote, limit, { root });
|
|
24458
|
+
if (root && !o.json) console.log(`worktree gc: scanning the explicitly named root ${root} for worktrees proven to belong to this repo
|
|
24459
|
+
`);
|
|
24042
24460
|
if (o.apply && !o.json) console.log(formatGcPlan(plan, true));
|
|
24043
24461
|
let applyResult;
|
|
24044
24462
|
if (o.apply) {
|
|
@@ -24049,10 +24467,10 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
|
|
|
24049
24467
|
// git fsmonitor daemon from inheriting this sweep's stdio pipe and wedging the git call on Windows.
|
|
24050
24468
|
worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout)
|
|
24051
24469
|
).catch(() => void 0);
|
|
24052
|
-
applyResult = await applyGcPlan(plan, o.remote);
|
|
24470
|
+
applyResult = await applyGcPlan(plan, o.remote, { root });
|
|
24053
24471
|
}
|
|
24054
24472
|
if (o.json) {
|
|
24055
|
-
console.log(JSON.stringify({ dryRun: !o.apply, remote: o.remote, plan, applyResult }, null, 2));
|
|
24473
|
+
console.log(JSON.stringify({ dryRun: !o.apply, remote: o.remote, ...root ? { root } : {}, plan, applyResult }, null, 2));
|
|
24056
24474
|
} else if (!o.apply) {
|
|
24057
24475
|
console.log(formatGcPlan(plan, false));
|
|
24058
24476
|
} else {
|
|
@@ -24117,7 +24535,7 @@ function acquireWorktreeSetupLock(worktreeRoot) {
|
|
|
24117
24535
|
};
|
|
24118
24536
|
};
|
|
24119
24537
|
try {
|
|
24120
|
-
(0, import_node_fs30.mkdirSync)((0,
|
|
24538
|
+
(0, import_node_fs30.mkdirSync)((0, import_node_path27.dirname)(lockPath), { recursive: true });
|
|
24121
24539
|
return take();
|
|
24122
24540
|
} catch {
|
|
24123
24541
|
try {
|
|
@@ -24132,7 +24550,7 @@ function acquireWorktreeSetupLock(worktreeRoot) {
|
|
|
24132
24550
|
}
|
|
24133
24551
|
var worktree = program2.command("worktree").description("self-provisioning worktrees \u2014 install deps + copy local-only config");
|
|
24134
24552
|
withExamples(mutating(
|
|
24135
|
-
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;
|
|
24553
|
+
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/<RepoName>/<branch> \u2014 authoritative; the <RepoName> segment is what makes ownership provable from the path, #3471. gc/list read this root in BOTH the nested and the legacy flat shape)").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"),
|
|
24136
24554
|
worktreeCreatePlan
|
|
24137
24555
|
).action(async (target, o, cmd) => {
|
|
24138
24556
|
try {
|
|
@@ -24548,7 +24966,7 @@ projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# fa
|
|
|
24548
24966
|
const payload = stage ? { slug: out.slug, stage, deploy: out.stages[stage] ?? null } : out;
|
|
24549
24967
|
console.log(JSON.stringify(payload));
|
|
24550
24968
|
});
|
|
24551
|
-
project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>",
|
|
24969
|
+
project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", 'read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (clear all with --unset secrets). SHAPE: a JSON object keyed by env name, each entry {"key":"UPPER_SNAKE" (repeat the env name), "purpose":"<non-empty>", "group":"<e.g. auth|database>", "owner":"<github login>", "stages":[] (empty = the one stageless shared value; else any of dev/rc/main), "consumers":["runtime"|"box"|\u2026], "provider":"<optional>"}').option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets, edgeDomains, requiredGcpApis, publishRequired").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
24552
24970
|
const cfg = await loadConfig();
|
|
24553
24971
|
let target;
|
|
24554
24972
|
try {
|
|
@@ -24735,7 +25153,15 @@ oauth.command("plan", { isDefault: true }).description("print the canonical JS o
|
|
|
24735
25153
|
try {
|
|
24736
25154
|
oc = parseOauthConfig(meta ?? {}, slug);
|
|
24737
25155
|
} catch (e) {
|
|
24738
|
-
|
|
25156
|
+
const message = e.message;
|
|
25157
|
+
if (/^oauth is not configured for /.test(message)) {
|
|
25158
|
+
return failGraceful(
|
|
25159
|
+
`org oauth plan: ${message}. Declare it in the registry META first:
|
|
25160
|
+
mmi-cli org project set ${o.repo ?? `mutmutco/${slug}`} --var 'oauth={"subdomains":["${defaultSubdomain(slug)}"],"callbackPath":"${DEFAULT_CALLBACK_PATH}"}'
|
|
25161
|
+
New repo? The GCP project and its Google Auth Platform consent screen must exist too \u2014 docs/Guides/oauth-provision.md \xA7 New repo.`
|
|
25162
|
+
);
|
|
25163
|
+
}
|
|
25164
|
+
return failGraceful(`org oauth plan: ${message}`);
|
|
24739
25165
|
}
|
|
24740
25166
|
const origins = expectedJsOrigins(oc);
|
|
24741
25167
|
const redirects = expectedRedirectUris(oc);
|
|
@@ -25212,11 +25638,11 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
|
|
|
25212
25638
|
}
|
|
25213
25639
|
});
|
|
25214
25640
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
25215
|
-
const wfDir = (0,
|
|
25641
|
+
const wfDir = (0, import_node_path27.join)(cwd, ".github", "workflows");
|
|
25216
25642
|
if (!(0, import_node_fs30.existsSync)(wfDir)) return [];
|
|
25217
25643
|
return (0, import_node_fs30.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
|
|
25218
25644
|
try {
|
|
25219
|
-
return workflowReportsPrChecks((0, import_node_fs30.readFileSync)((0,
|
|
25645
|
+
return workflowReportsPrChecks((0, import_node_fs30.readFileSync)((0, import_node_path27.join)(wfDir, name), "utf8"));
|
|
25220
25646
|
} catch {
|
|
25221
25647
|
return true;
|
|
25222
25648
|
}
|
|
@@ -25243,16 +25669,16 @@ function ciAuditDeps() {
|
|
|
25243
25669
|
// gate re-seed step is skipped gracefully rather than failing mid-run.
|
|
25244
25670
|
readSeedFile: (path2) => {
|
|
25245
25671
|
if (!root) return null;
|
|
25246
|
-
const fullPath = (0,
|
|
25672
|
+
const fullPath = (0, import_node_path27.join)(root, path2);
|
|
25247
25673
|
return (0, import_node_fs30.existsSync)(fullPath) ? (0, import_node_fs30.readFileSync)(fullPath, "utf8") : null;
|
|
25248
25674
|
}
|
|
25249
25675
|
};
|
|
25250
25676
|
}
|
|
25251
25677
|
function hubRoot() {
|
|
25252
|
-
const fromPkg = (0,
|
|
25678
|
+
const fromPkg = (0, import_node_path27.join)(__dirname, "..", "..");
|
|
25253
25679
|
const marker = "skills/bootstrap/seeds/manifest.json";
|
|
25254
|
-
if ((0, import_node_fs30.existsSync)((0,
|
|
25255
|
-
if ((0, import_node_fs30.existsSync)((0,
|
|
25680
|
+
if ((0, import_node_fs30.existsSync)((0, import_node_path27.join)(fromPkg, marker))) return fromPkg;
|
|
25681
|
+
if ((0, import_node_fs30.existsSync)((0, import_node_path27.join)(process.cwd(), marker))) return process.cwd();
|
|
25256
25682
|
return null;
|
|
25257
25683
|
}
|
|
25258
25684
|
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) => {
|
|
@@ -25292,8 +25718,10 @@ pr.command("checks-wait <number>").description(`bounded wait for PR checks; skip
|
|
|
25292
25718
|
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>.`);
|
|
25293
25719
|
} else if (result.status === "rate-limited") {
|
|
25294
25720
|
printLine(`pr checks-wait: rate-limited \u2014 ${result.reason ?? "REST pool below floor"}. No check failed; re-run after the pool resets.`);
|
|
25295
|
-
} else if (result.detail === "
|
|
25296
|
-
printLine(`pr checks-wait: failure (
|
|
25721
|
+
} else if (result.detail === "runner-infra") {
|
|
25722
|
+
printLine(`pr checks-wait: failure (runner infrastructure, NOT a test failure) \u2014 ${result.reason}`);
|
|
25723
|
+
} else if (result.detail === "stale-head") {
|
|
25724
|
+
printLine(`pr checks-wait: failure (stale PR head, NOT a test failure) \u2014 ${result.reason}`);
|
|
25297
25725
|
} else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
|
|
25298
25726
|
if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
|
|
25299
25727
|
if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
|
|
@@ -25883,7 +26311,7 @@ function directoryBytes(path2) {
|
|
|
25883
26311
|
return 0;
|
|
25884
26312
|
}
|
|
25885
26313
|
for (const entry of entries) {
|
|
25886
|
-
const child2 = (0,
|
|
26314
|
+
const child2 = (0, import_node_path27.join)(path2, entry.name);
|
|
25887
26315
|
if (entry.isDirectory()) total += directoryBytes(child2);
|
|
25888
26316
|
else {
|
|
25889
26317
|
try {
|
|
@@ -25913,7 +26341,7 @@ function pluginCacheFsDeps(home, dirBytes) {
|
|
|
25913
26341
|
dirBytes,
|
|
25914
26342
|
listStagingDirs: (root) => (0, import_node_fs30.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
|
|
25915
26343
|
try {
|
|
25916
|
-
return { name: d.name, mtimeMs: newestMtimeMs((0,
|
|
26344
|
+
return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path27.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs30.statSync)(p).mtimeMs) };
|
|
25917
26345
|
} catch {
|
|
25918
26346
|
return { name: d.name, mtimeMs: Date.now() };
|
|
25919
26347
|
}
|
|
@@ -25927,7 +26355,7 @@ function stagingApplyFsGuard(home) {
|
|
|
25927
26355
|
return {
|
|
25928
26356
|
referencedPaths: () => readInstalledPluginRefs(home),
|
|
25929
26357
|
mtimeMs: (name) => {
|
|
25930
|
-
const p = (0,
|
|
26358
|
+
const p = (0, import_node_path27.join)(stagingRoot, name);
|
|
25931
26359
|
if (!(0, import_node_fs30.existsSync)(p)) return null;
|
|
25932
26360
|
try {
|
|
25933
26361
|
return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs30.statSync)(q).mtimeMs);
|
|
@@ -25963,6 +26391,8 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
25963
26391
|
return;
|
|
25964
26392
|
}
|
|
25965
26393
|
spawnDeferredGcSweep();
|
|
26394
|
+
const measured = measuredSessionIo(consoleIo);
|
|
26395
|
+
const bannerIo = measured.io;
|
|
25966
26396
|
const { parallel, sequential } = buildSessionStartPlan({
|
|
25967
26397
|
// whoami (#879): surface the resolved human so agents act --for them without asking. Silent
|
|
25968
26398
|
// when unknown — a missing gh login must not noise or fail the banner.
|
|
@@ -26006,14 +26436,16 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
26006
26436
|
await runDoctorClean({ banner: true }, io, mmiDoctorDeps());
|
|
26007
26437
|
}
|
|
26008
26438
|
});
|
|
26009
|
-
await runSessionStart(parallel, sequential,
|
|
26010
|
-
for (const line of scratchGcLines(process.cwd()))
|
|
26439
|
+
await runSessionStart(parallel, sequential, bannerIo);
|
|
26440
|
+
for (const line of scratchGcLines(process.cwd())) bannerIo.log(line);
|
|
26011
26441
|
const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
|
|
26012
26442
|
if (worktreeBanner) {
|
|
26013
26443
|
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process13.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
26014
|
-
|
|
26444
|
+
bannerIo.log(worktreeBanner);
|
|
26015
26445
|
}
|
|
26016
|
-
|
|
26446
|
+
const payloadChars = measured.chars();
|
|
26447
|
+
recordSessionPayload(process.cwd(), payloadChars);
|
|
26448
|
+
appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "session-start hook completed", payloadChars });
|
|
26017
26449
|
});
|
|
26018
26450
|
installProcessBackstop();
|
|
26019
26451
|
consolidateCommandNamespaces(program2);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.50.0",
|
|
4
4
|
"description": "MMI Future CLI — the org dev toolbox (board, registry, keyless secrets, release train, bootstrap, doctor) and the cross-IDE engine the plugin's session-start hook drives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|