@mutmutco/cli 3.94.0 → 3.96.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 +2009 -572
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -3416,8 +3416,8 @@ var program = new Command();
|
|
|
3416
3416
|
|
|
3417
3417
|
// src/index.ts
|
|
3418
3418
|
var import_promises11 = require("node:fs/promises");
|
|
3419
|
-
var
|
|
3420
|
-
var
|
|
3419
|
+
var import_node_fs39 = require("node:fs");
|
|
3420
|
+
var import_node_child_process18 = require("node:child_process");
|
|
3421
3421
|
|
|
3422
3422
|
// src/cli-shared.ts
|
|
3423
3423
|
var import_node_child_process3 = require("node:child_process");
|
|
@@ -6993,11 +6993,13 @@ function commandLadderHint() {
|
|
|
6993
6993
|
}
|
|
6994
6994
|
lines.push("[mmi] Schedules doctrine (#3228): scheduled jobs + LLM crons go through the Hub registry \u2014 eight-field header \u2192 PR \u2192 `mmi-cli org schedules register`; LLM crons ONLY via the harbour (llm: yes \u21D2 executor cursor-agent). Never hand-build an unregistered cron. Opt out per repo: `mmi-cli org schedules mode self-managed`.");
|
|
6995
6995
|
lines.push('[mmi] Registering \u2260 armed (#3281): `org schedules register` writes SCHEDULE# rows; the fleet-clock reconciler arms them within ~15 min. Report "registered successfully \u2014 arms on the next fleet-clock tick", never "armed"/"live". The success JSON carries an `arming` block. Before that window an absent clock means "not reconciled yet", NOT "will never fire" \u2014 verify with `mmi-cli org schedules` after it.');
|
|
6996
|
+
lines.push("[mmi] Current-state doors (#4120): prefer `org schedules`, `runtime box list`, `org project get`, `board`/`status`/`next`, `docs index` over markdown inventories or janitor prose \u2014 see docs/Architecture/compute-at-read.md.");
|
|
6997
|
+
lines.push("[mmi] Estate structure search (#4133): `mmi-cli repo-index search <q>` hits Hub cloud (lexical/hybrid; `--semantic` for meaning). Pointer hits only \u2014 never invent wiki prose or trust living `docs/**` current-state. Local rebuild is optional (`--local`); laptops do not index the estate.");
|
|
6996
6998
|
return lines.join("\n");
|
|
6997
6999
|
}
|
|
6998
7000
|
|
|
6999
7001
|
// src/index.ts
|
|
7000
|
-
var
|
|
7002
|
+
var import_node_path38 = require("node:path");
|
|
7001
7003
|
|
|
7002
7004
|
// src/merge-ci-policy.ts
|
|
7003
7005
|
function resolveMergeCiPolicy(input) {
|
|
@@ -13319,7 +13321,7 @@ async function executeWaveLand(plan, deps, opts = { preserveWorktree: true }) {
|
|
|
13319
13321
|
}
|
|
13320
13322
|
|
|
13321
13323
|
// src/index.ts
|
|
13322
|
-
var
|
|
13324
|
+
var import_node_os15 = require("node:os");
|
|
13323
13325
|
|
|
13324
13326
|
// src/board.ts
|
|
13325
13327
|
var import_node_child_process9 = require("node:child_process");
|
|
@@ -15105,9 +15107,9 @@ function parseSecretsUseArgv(tail) {
|
|
|
15105
15107
|
const flags = {};
|
|
15106
15108
|
const keys = [];
|
|
15107
15109
|
const firstSep = tail.indexOf("--");
|
|
15108
|
-
const
|
|
15109
|
-
const head =
|
|
15110
|
-
let command =
|
|
15110
|
+
const sep3 = firstSep !== -1 && separatorIsOurs(tail.slice(0, firstSep)) ? firstSep : -1;
|
|
15111
|
+
const head = sep3 === -1 ? tail : tail.slice(0, sep3);
|
|
15112
|
+
let command = sep3 === -1 ? [] : tail.slice(sep3 + 1).slice();
|
|
15111
15113
|
for (let i = 0; i < head.length; ) {
|
|
15112
15114
|
const tok = head[i];
|
|
15113
15115
|
const eq = tok.indexOf("=");
|
|
@@ -16988,7 +16990,7 @@ async function boardDoctor(options, deps = {}) {
|
|
|
16988
16990
|
};
|
|
16989
16991
|
await Promise.all(Array.from({ length: Math.min(concurrency, suspicious.length) }, () => ghostWorker()));
|
|
16990
16992
|
}
|
|
16991
|
-
const claimedItems = collected.items.filter(
|
|
16993
|
+
const claimedItems = options.skipClaimLiveness ? [] : collected.items.filter(
|
|
16992
16994
|
(item) => item.contentType === "Issue" && item.assignees.length > 0 && item.status === "In Progress"
|
|
16993
16995
|
);
|
|
16994
16996
|
const liveClaims = [];
|
|
@@ -17231,11 +17233,11 @@ var PRIMARY_GROUPS = [
|
|
|
17231
17233
|
// `tests` sits beside `docs` deliberately: both are deterministic, repo-local gates a workflow
|
|
17232
17234
|
// step invokes (`docs refs`, `tests policy`), not org-plane operations (#3605). `spawn policy`
|
|
17233
17235
|
// joins them on the same footing (#3979).
|
|
17234
|
-
["Setup and support", ["bootstrap", "secrets", "docs", "tests", "spawn"]],
|
|
17236
|
+
["Setup and support", ["bootstrap", "secrets", "docs", "repo-index", "tests", "spawn"]],
|
|
17235
17237
|
["Coordinate and improve", ["wave", "report", "skill-lesson"]]
|
|
17236
17238
|
];
|
|
17237
17239
|
var OPERATIONAL_TOP_LEVEL = /* @__PURE__ */ new Set(["org", "runtime", "plugin"]);
|
|
17238
|
-
var SUPPORT_PRIMARY = /* @__PURE__ */ new Set(["doctor", "whoami", "commands", "explain", "docs", "tests", "spawn", "wave", "report", "skill-lesson"]);
|
|
17240
|
+
var SUPPORT_PRIMARY = /* @__PURE__ */ new Set(["doctor", "whoami", "commands", "explain", "docs", "repo-index", "tests", "spawn", "wave", "report", "skill-lesson"]);
|
|
17239
17241
|
var TOP_LEVEL_ORDER = /* @__PURE__ */ new Map();
|
|
17240
17242
|
var HELP_GROUP_ORDER = /* @__PURE__ */ new Map();
|
|
17241
17243
|
var topLevelPosition = 0;
|
|
@@ -17274,6 +17276,7 @@ var COMMAND_OWNERSHIP = {
|
|
|
17274
17276
|
bootstrap: { module_owner: "cli/src/bootstrap-commands.ts", consumer: "repo-bootstrap" },
|
|
17275
17277
|
secrets: { module_owner: "cli/src/secrets-commands.ts", consumer: "authenticated-operator" },
|
|
17276
17278
|
docs: { module_owner: "cli/src/docs-index-command.ts", consumer: "repo-gates" },
|
|
17279
|
+
"repo-index": { module_owner: "cli/src/repo-index.ts", consumer: "agent-session" },
|
|
17277
17280
|
tests: { module_owner: "cli/src/test-policy-core.ts", consumer: "repo-gates" },
|
|
17278
17281
|
spawn: { module_owner: "cli/src/spawn-policy-core.ts", consumer: "repo-gates" },
|
|
17279
17282
|
wave: { module_owner: "cli/src/wave-land.ts", consumer: "campaign-orchestrator" },
|
|
@@ -19298,216 +19301,884 @@ function renderAccessReport(report) {
|
|
|
19298
19301
|
return lines.join("\n");
|
|
19299
19302
|
}
|
|
19300
19303
|
|
|
19301
|
-
// src/
|
|
19304
|
+
// src/repo-index.ts
|
|
19305
|
+
var import_node_crypto4 = require("node:crypto");
|
|
19302
19306
|
var import_node_child_process11 = require("node:child_process");
|
|
19303
19307
|
var import_node_fs22 = require("node:fs");
|
|
19304
19308
|
var import_node_path20 = require("node:path");
|
|
19305
|
-
var
|
|
19306
|
-
var
|
|
19307
|
-
|
|
19308
|
-
|
|
19309
|
-
/
|
|
19310
|
-
/
|
|
19311
|
-
|
|
19312
|
-
/\.
|
|
19313
|
-
/\.
|
|
19309
|
+
var REPO_INDEX_SCHEMA = 1;
|
|
19310
|
+
var HARD_DENY = [
|
|
19311
|
+
/(^|\/)\.env(\.|$)/i,
|
|
19312
|
+
/(^|\/)\.env\./i,
|
|
19313
|
+
/credentials/i,
|
|
19314
|
+
/secrets?\.json$/i,
|
|
19315
|
+
/\.pem$/i,
|
|
19316
|
+
/\.p12$/i,
|
|
19317
|
+
/\.key$/i,
|
|
19318
|
+
/(^|\/)id_rsa/i,
|
|
19319
|
+
/(^|\/)id_ed25519/i,
|
|
19320
|
+
/\.keystore$/i
|
|
19314
19321
|
];
|
|
19315
|
-
|
|
19316
|
-
|
|
19322
|
+
var INDEXABLE_EXT = /* @__PURE__ */ new Set([
|
|
19323
|
+
".ts",
|
|
19324
|
+
".tsx",
|
|
19325
|
+
".js",
|
|
19326
|
+
".jsx",
|
|
19327
|
+
".mjs",
|
|
19328
|
+
".cjs",
|
|
19329
|
+
".py",
|
|
19330
|
+
".go",
|
|
19331
|
+
".rs",
|
|
19332
|
+
".java",
|
|
19333
|
+
".kt",
|
|
19334
|
+
".md",
|
|
19335
|
+
".yml",
|
|
19336
|
+
".yaml",
|
|
19337
|
+
".json",
|
|
19338
|
+
".toml",
|
|
19339
|
+
".sh",
|
|
19340
|
+
".ps1",
|
|
19341
|
+
".css",
|
|
19342
|
+
".html"
|
|
19343
|
+
]);
|
|
19344
|
+
var SYMBOL_RE = /^(?:export\s+)?(?:async\s+)?(?:function|class|const|let|var|type|interface|enum)\s+([A-Za-z_$][\w$]*)/gm;
|
|
19345
|
+
function repoIndexPathTieBreak(path2) {
|
|
19346
|
+
const p = path2.replace(/\\/g, "/");
|
|
19347
|
+
if (/(^|\/)__tests__\//.test(p) || /(^|\/)tests?\//.test(p)) return 1;
|
|
19348
|
+
if (/\.(test|spec)\.[^.]+$/i.test(p)) return 1;
|
|
19349
|
+
return 0;
|
|
19317
19350
|
}
|
|
19318
|
-
function
|
|
19319
|
-
|
|
19320
|
-
for (let i = open2; i < src.length; i++) {
|
|
19321
|
-
const c = src[i];
|
|
19322
|
-
if (c === "(") depth++;
|
|
19323
|
-
else if (c === ")") {
|
|
19324
|
-
depth--;
|
|
19325
|
-
if (depth === 0) return src.slice(open2, i + 1);
|
|
19326
|
-
}
|
|
19327
|
-
}
|
|
19328
|
-
return "";
|
|
19351
|
+
function repoIndexStorePath(cwd) {
|
|
19352
|
+
return repoRuntimeStatePath(cwd, "repo-index", "index.json");
|
|
19329
19353
|
}
|
|
19330
|
-
function
|
|
19331
|
-
|
|
19332
|
-
|
|
19333
|
-
|
|
19334
|
-
|
|
19335
|
-
);
|
|
19336
|
-
if (
|
|
19337
|
-
|
|
19338
|
-
const
|
|
19339
|
-
|
|
19340
|
-
|
|
19354
|
+
function isHardDeniedPath(relPosix) {
|
|
19355
|
+
return HARD_DENY.some((re) => re.test(relPosix));
|
|
19356
|
+
}
|
|
19357
|
+
function isIndexablePath(relPosix) {
|
|
19358
|
+
if (isHardDeniedPath(relPosix)) return false;
|
|
19359
|
+
if (relPosix.startsWith(".git/")) return false;
|
|
19360
|
+
if (relPosix.includes("node_modules/")) return false;
|
|
19361
|
+
if (relPosix.includes("dist/")) return false;
|
|
19362
|
+
const dot = relPosix.lastIndexOf(".");
|
|
19363
|
+
if (dot < 0) return false;
|
|
19364
|
+
return INDEXABLE_EXT.has(relPosix.slice(dot).toLowerCase());
|
|
19365
|
+
}
|
|
19366
|
+
function extractSymbols(source) {
|
|
19367
|
+
const out = /* @__PURE__ */ new Set();
|
|
19368
|
+
SYMBOL_RE.lastIndex = 0;
|
|
19341
19369
|
let m;
|
|
19342
|
-
while (m =
|
|
19343
|
-
|
|
19344
|
-
|
|
19345
|
-
|
|
19346
|
-
|
|
19347
|
-
|
|
19348
|
-
|
|
19349
|
-
|
|
19350
|
-
|
|
19351
|
-
|
|
19352
|
-
|
|
19353
|
-
|
|
19354
|
-
|
|
19355
|
-
|
|
19356
|
-
if (SPAWNERS.includes(id)) continue;
|
|
19357
|
-
const decl = new RegExp(String.raw`\b(?:const|let|var)\s+${id}\b[^;]*`, "s").exec(src);
|
|
19358
|
-
if (decl && decl[0].includes("windowsHide")) {
|
|
19359
|
-
satisfied = true;
|
|
19360
|
-
break;
|
|
19361
|
-
}
|
|
19362
|
-
if (new RegExp(String.raw`\b${id}\s*:\s*\{[^}]*windowsHide`, "s").test(src)) {
|
|
19363
|
-
satisfied = true;
|
|
19364
|
-
break;
|
|
19365
|
-
}
|
|
19370
|
+
while ((m = SYMBOL_RE.exec(source)) !== null) {
|
|
19371
|
+
if (m[1]) out.add(m[1]);
|
|
19372
|
+
}
|
|
19373
|
+
return [...out].sort((a, b) => a.localeCompare(b));
|
|
19374
|
+
}
|
|
19375
|
+
var BLURB_CAP = 200;
|
|
19376
|
+
function extractModuleBlurb(source) {
|
|
19377
|
+
const lines = source.split(/\r?\n/);
|
|
19378
|
+
let i = 0;
|
|
19379
|
+
while (i < lines.length) {
|
|
19380
|
+
const t = lines[i].trim();
|
|
19381
|
+
if (!t || t.startsWith("#!") || /^['"]use (strict|client|server)['"]/.test(t)) {
|
|
19382
|
+
i++;
|
|
19383
|
+
continue;
|
|
19366
19384
|
}
|
|
19367
|
-
|
|
19368
|
-
found.push({ line, callee });
|
|
19385
|
+
break;
|
|
19369
19386
|
}
|
|
19370
|
-
return
|
|
19387
|
+
if (i >= lines.length) return void 0;
|
|
19388
|
+
const first = lines[i].trim();
|
|
19389
|
+
if (first.startsWith("/**")) {
|
|
19390
|
+
const same = first.replace(/^\/\*\*\s*/, "").replace(/\*\/\s*$/, "").replace(/^\*\s?/, "").trim();
|
|
19391
|
+
if (same && !same.startsWith("*") && same !== "/") return same.slice(0, BLURB_CAP);
|
|
19392
|
+
for (let j = i + 1; j < Math.min(i + 12, lines.length); j++) {
|
|
19393
|
+
const raw = lines[j].trim();
|
|
19394
|
+
if (raw.startsWith("*/")) break;
|
|
19395
|
+
const content = raw.replace(/^\*\s?/, "").trim();
|
|
19396
|
+
if (!content || content.startsWith("@")) continue;
|
|
19397
|
+
return content.slice(0, BLURB_CAP);
|
|
19398
|
+
}
|
|
19399
|
+
return void 0;
|
|
19400
|
+
}
|
|
19401
|
+
if (first.startsWith("//")) {
|
|
19402
|
+
const content = first.replace(/^\/\/\s?/, "").trim();
|
|
19403
|
+
return content ? content.slice(0, BLURB_CAP) : void 0;
|
|
19404
|
+
}
|
|
19405
|
+
if (first.startsWith("#")) {
|
|
19406
|
+
const content = first.replace(/^#+\s?/, "").trim();
|
|
19407
|
+
return content ? content.slice(0, BLURB_CAP) : void 0;
|
|
19408
|
+
}
|
|
19409
|
+
return void 0;
|
|
19371
19410
|
}
|
|
19372
|
-
function
|
|
19373
|
-
const
|
|
19374
|
-
|
|
19375
|
-
|
|
19376
|
-
|
|
19377
|
-
|
|
19378
|
-
|
|
19379
|
-
|
|
19380
|
-
|
|
19411
|
+
function extractReadmeSectionHeaders(markdown, limit = 6) {
|
|
19412
|
+
const out = [];
|
|
19413
|
+
for (const line of markdown.split(/\r?\n/)) {
|
|
19414
|
+
const m = /^(#{1,3})\s+(.+?)\s*$/.exec(line.trim());
|
|
19415
|
+
if (!m?.[2]) continue;
|
|
19416
|
+
const title = m[2].replace(/#+\s*$/, "").trim();
|
|
19417
|
+
if (!title) continue;
|
|
19418
|
+
out.push(title);
|
|
19419
|
+
if (out.length >= limit) break;
|
|
19381
19420
|
}
|
|
19382
|
-
return
|
|
19421
|
+
return out;
|
|
19383
19422
|
}
|
|
19384
|
-
function
|
|
19385
|
-
const
|
|
19386
|
-
const
|
|
19387
|
-
|
|
19388
|
-
|
|
19423
|
+
function loadReadmeHints(cwd, candidatePaths) {
|
|
19424
|
+
const hints = /* @__PURE__ */ new Map();
|
|
19425
|
+
const readmes = /* @__PURE__ */ new Set();
|
|
19426
|
+
if (candidatePaths.includes("README.md")) readmes.add("README.md");
|
|
19427
|
+
for (const p of candidatePaths) {
|
|
19428
|
+
const parts = p.split("/");
|
|
19429
|
+
if (parts.length === 2 && parts[1] === "README.md" && parts[0]) readmes.add(p);
|
|
19430
|
+
}
|
|
19431
|
+
for (const rel of readmes) {
|
|
19432
|
+
if (isHardDeniedPath(rel)) continue;
|
|
19433
|
+
const abs = (0, import_node_path20.join)(cwd, ...rel.split("/"));
|
|
19434
|
+
if (!(0, import_node_fs22.existsSync)(abs)) continue;
|
|
19435
|
+
let text;
|
|
19389
19436
|
try {
|
|
19390
|
-
|
|
19437
|
+
text = (0, import_node_fs22.readFileSync)(abs, "utf8");
|
|
19391
19438
|
} catch {
|
|
19392
19439
|
continue;
|
|
19393
19440
|
}
|
|
19394
|
-
|
|
19395
|
-
|
|
19396
|
-
|
|
19397
|
-
|
|
19398
|
-
|
|
19399
|
-
detail: `${file}:${v.line} ${v.callee}(...) has no windowsHide`
|
|
19400
|
-
});
|
|
19401
|
-
}
|
|
19441
|
+
if (text.length > 2e5) continue;
|
|
19442
|
+
const headers = extractReadmeSectionHeaders(text);
|
|
19443
|
+
if (!headers.length) continue;
|
|
19444
|
+
const key = rel.includes("/") ? rel.split("/")[0] : "";
|
|
19445
|
+
hints.set(key, headers.join("; ").slice(0, BLURB_CAP));
|
|
19402
19446
|
}
|
|
19403
|
-
return
|
|
19447
|
+
return hints;
|
|
19404
19448
|
}
|
|
19405
|
-
|
|
19406
|
-
|
|
19407
|
-
|
|
19408
|
-
|
|
19409
|
-
|
|
19410
|
-
|
|
19411
|
-
|
|
19412
|
-
|
|
19413
|
-
|
|
19414
|
-
|
|
19415
|
-
|
|
19416
|
-
|
|
19417
|
-
|
|
19418
|
-
|
|
19419
|
-
|
|
19420
|
-
|
|
19421
|
-
|
|
19422
|
-
|
|
19423
|
-
|
|
19424
|
-
|
|
19425
|
-
|
|
19426
|
-
|
|
19427
|
-
|
|
19428
|
-
|
|
19429
|
-
|
|
19430
|
-
|
|
19431
|
-
|
|
19432
|
-
|
|
19433
|
-
|
|
19434
|
-
out += ".*";
|
|
19435
|
-
i += 1;
|
|
19436
|
-
}
|
|
19437
|
-
} else {
|
|
19438
|
-
out += "[^/]*";
|
|
19439
|
-
}
|
|
19449
|
+
function toPosix(p) {
|
|
19450
|
+
return p.split(import_node_path20.sep).join("/");
|
|
19451
|
+
}
|
|
19452
|
+
function listCandidatePaths(cwd, exec = import_node_child_process11.execFileSync) {
|
|
19453
|
+
try {
|
|
19454
|
+
const out = exec("git", ["ls-files", "-z", "-c", "-o", "--exclude-standard"], {
|
|
19455
|
+
cwd,
|
|
19456
|
+
encoding: "utf8",
|
|
19457
|
+
maxBuffer: 64 * 1024 * 1024
|
|
19458
|
+
});
|
|
19459
|
+
return out.split("\0").filter(Boolean).map(toPosix);
|
|
19460
|
+
} catch {
|
|
19461
|
+
return [];
|
|
19462
|
+
}
|
|
19463
|
+
}
|
|
19464
|
+
function rebuildRepoIndex(cwd, repoSlug2) {
|
|
19465
|
+
const candidates = listCandidatePaths(cwd).filter(isIndexablePath);
|
|
19466
|
+
const ignored = defaultIsIgnored(cwd, candidates);
|
|
19467
|
+
const readmeHints = loadReadmeHints(cwd, candidates.filter((p) => !ignored.has(p)));
|
|
19468
|
+
const entries = [];
|
|
19469
|
+
for (const rel of candidates) {
|
|
19470
|
+
if (ignored.has(rel)) continue;
|
|
19471
|
+
if (isHardDeniedPath(rel)) continue;
|
|
19472
|
+
const abs = (0, import_node_path20.join)(cwd, ...rel.split("/"));
|
|
19473
|
+
if (!(0, import_node_fs22.existsSync)(abs)) continue;
|
|
19474
|
+
let text;
|
|
19475
|
+
try {
|
|
19476
|
+
text = (0, import_node_fs22.readFileSync)(abs, "utf8");
|
|
19477
|
+
} catch {
|
|
19440
19478
|
continue;
|
|
19441
19479
|
}
|
|
19442
|
-
if (
|
|
19443
|
-
|
|
19444
|
-
|
|
19445
|
-
|
|
19446
|
-
|
|
19447
|
-
|
|
19448
|
-
|
|
19449
|
-
|
|
19450
|
-
|
|
19451
|
-
|
|
19480
|
+
if (text.length > 15e5) continue;
|
|
19481
|
+
const hash = (0, import_node_crypto4.createHash)("sha256").update(text).digest("hex").slice(0, 16);
|
|
19482
|
+
const symbols = extractSymbols(text);
|
|
19483
|
+
const docBlurb = extractModuleBlurb(text);
|
|
19484
|
+
const top = rel.includes("/") ? rel.split("/")[0] : "";
|
|
19485
|
+
const hint = docBlurb ?? (top ? readmeHints.get(top) : void 0) ?? readmeHints.get("");
|
|
19486
|
+
entries.push({
|
|
19487
|
+
path: rel,
|
|
19488
|
+
hash,
|
|
19489
|
+
symbols,
|
|
19490
|
+
...hint ? { blurb: hint.slice(0, BLURB_CAP) } : {}
|
|
19491
|
+
});
|
|
19452
19492
|
}
|
|
19453
|
-
|
|
19493
|
+
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
19494
|
+
const projection = {
|
|
19495
|
+
schema: REPO_INDEX_SCHEMA,
|
|
19496
|
+
repo: repoSlug2,
|
|
19497
|
+
builtAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19498
|
+
entries
|
|
19499
|
+
};
|
|
19500
|
+
const store = repoIndexStorePath(cwd);
|
|
19501
|
+
(0, import_node_fs22.mkdirSync)((0, import_node_path20.dirname)(store), { recursive: true });
|
|
19502
|
+
(0, import_node_fs22.writeFileSync)(store, `${JSON.stringify(projection, null, 2)}
|
|
19503
|
+
`, "utf8");
|
|
19504
|
+
return projection;
|
|
19454
19505
|
}
|
|
19455
|
-
function
|
|
19456
|
-
|
|
19506
|
+
function loadRepoIndex(cwd) {
|
|
19507
|
+
const store = repoIndexStorePath(cwd);
|
|
19508
|
+
if (!(0, import_node_fs22.existsSync)(store)) return null;
|
|
19509
|
+
try {
|
|
19510
|
+
const raw = JSON.parse((0, import_node_fs22.readFileSync)(store, "utf8"));
|
|
19511
|
+
if (raw?.schema !== REPO_INDEX_SCHEMA || !Array.isArray(raw.entries)) return null;
|
|
19512
|
+
return raw;
|
|
19513
|
+
} catch {
|
|
19514
|
+
return null;
|
|
19515
|
+
}
|
|
19457
19516
|
}
|
|
19458
|
-
function
|
|
19459
|
-
|
|
19517
|
+
function repoIndexStatus(cwd) {
|
|
19518
|
+
const path2 = repoIndexStorePath(cwd);
|
|
19519
|
+
const idx = loadRepoIndex(cwd);
|
|
19520
|
+
if (!idx) return { present: false, path: path2 };
|
|
19521
|
+
const symbolCount = idx.entries.reduce((n, e) => n + e.symbols.length, 0);
|
|
19522
|
+
return {
|
|
19523
|
+
present: true,
|
|
19524
|
+
path: path2,
|
|
19525
|
+
schema: idx.schema,
|
|
19526
|
+
repo: idx.repo,
|
|
19527
|
+
builtAt: idx.builtAt,
|
|
19528
|
+
fileCount: idx.entries.length,
|
|
19529
|
+
symbolCount
|
|
19530
|
+
};
|
|
19460
19531
|
}
|
|
19461
|
-
function
|
|
19462
|
-
const
|
|
19463
|
-
if (
|
|
19464
|
-
|
|
19465
|
-
|
|
19466
|
-
|
|
19467
|
-
|
|
19532
|
+
function searchRepoIndex(idx, query, limit = 20) {
|
|
19533
|
+
const q = query.trim().toLowerCase();
|
|
19534
|
+
if (!q) return [];
|
|
19535
|
+
const hits = [];
|
|
19536
|
+
for (const e of idx.entries) {
|
|
19537
|
+
const pathLower = e.path.toLowerCase();
|
|
19538
|
+
if (pathLower.includes(q) || pathLower.endsWith(`/${q}`) || pathLower === q) {
|
|
19539
|
+
hits.push({
|
|
19540
|
+
repo: idx.repo,
|
|
19541
|
+
path: e.path,
|
|
19542
|
+
kind: "path",
|
|
19543
|
+
why: pathLower === q || pathLower.endsWith(`/${q}`) ? "exact path segment" : "path substring",
|
|
19544
|
+
score: pathLower === q ? 1 : pathLower.endsWith(`/${q}`) ? 0.95 : 0.7
|
|
19545
|
+
});
|
|
19546
|
+
}
|
|
19547
|
+
for (const sym of e.symbols) {
|
|
19548
|
+
const sl = sym.toLowerCase();
|
|
19549
|
+
if (sl === q || sl.startsWith(q)) {
|
|
19550
|
+
hits.push({
|
|
19551
|
+
repo: idx.repo,
|
|
19552
|
+
path: e.path,
|
|
19553
|
+
kind: "symbol",
|
|
19554
|
+
symbol: sym,
|
|
19555
|
+
why: sl === q ? "exact symbol" : "symbol prefix",
|
|
19556
|
+
score: sl === q ? 1 : 0.85
|
|
19557
|
+
});
|
|
19558
|
+
}
|
|
19559
|
+
}
|
|
19468
19560
|
}
|
|
19561
|
+
hits.sort(
|
|
19562
|
+
(a, b) => b.score - a.score || repoIndexPathTieBreak(a.path) - repoIndexPathTieBreak(b.path) || a.path.localeCompare(b.path)
|
|
19563
|
+
);
|
|
19564
|
+
const seen = /* @__PURE__ */ new Set();
|
|
19565
|
+
const out = [];
|
|
19566
|
+
for (const h of hits) {
|
|
19567
|
+
const key = `${h.kind}:${h.path}:${h.symbol ?? ""}`;
|
|
19568
|
+
if (seen.has(key)) continue;
|
|
19569
|
+
seen.add(key);
|
|
19570
|
+
out.push(h);
|
|
19571
|
+
if (out.length >= limit) break;
|
|
19572
|
+
}
|
|
19573
|
+
return out;
|
|
19469
19574
|
}
|
|
19470
|
-
function
|
|
19575
|
+
function inferRepoSlug(cwd, exec = import_node_child_process11.execFileSync) {
|
|
19471
19576
|
try {
|
|
19472
|
-
|
|
19577
|
+
const url = String(exec("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" })).trim();
|
|
19578
|
+
const m = /[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(url);
|
|
19579
|
+
if (m?.[1]) return m[1].toLowerCase();
|
|
19473
19580
|
} catch {
|
|
19474
|
-
return null;
|
|
19475
19581
|
}
|
|
19582
|
+
return ((0, import_node_path20.basename)(cwd) || "local").toLowerCase();
|
|
19476
19583
|
}
|
|
19477
|
-
|
|
19478
|
-
|
|
19479
|
-
|
|
19480
|
-
|
|
19481
|
-
}
|
|
19482
|
-
|
|
19483
|
-
|
|
19484
|
-
|
|
19485
|
-
|
|
19486
|
-
|
|
19487
|
-
|
|
19488
|
-
|
|
19489
|
-
|
|
19490
|
-
|
|
19491
|
-
|
|
19492
|
-
|
|
19493
|
-
|
|
19584
|
+
|
|
19585
|
+
// src/repo-index-cloud-client.ts
|
|
19586
|
+
var RETRY_ATTEMPTS2 = 3;
|
|
19587
|
+
async function publishRepoIndexCloud(payload, deps) {
|
|
19588
|
+
if (!deps.baseUrl) return { ok: false, error: "Hub API URL not configured" };
|
|
19589
|
+
const token = await deps.token();
|
|
19590
|
+
if (!token) return { ok: false, error: "no Hub session token (run `gh auth login`)" };
|
|
19591
|
+
try {
|
|
19592
|
+
const res = await fetchWithRetry(
|
|
19593
|
+
deps.fetch ?? fetch,
|
|
19594
|
+
`${deps.baseUrl.replace(/\/$/, "")}/repo-index/publish`,
|
|
19595
|
+
{
|
|
19596
|
+
method: "POST",
|
|
19597
|
+
headers: {
|
|
19598
|
+
...clientVersionHeaders(),
|
|
19599
|
+
Authorization: `Bearer ${token}`,
|
|
19600
|
+
"content-type": "application/json"
|
|
19601
|
+
},
|
|
19602
|
+
body: JSON.stringify(payload)
|
|
19603
|
+
},
|
|
19604
|
+
{
|
|
19605
|
+
attempts: RETRY_ATTEMPTS2,
|
|
19606
|
+
// Embed publish can take longer than a registry read.
|
|
19607
|
+
timeoutMs: payload.embed ? 12e4 : deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS,
|
|
19608
|
+
sleep: deps.retrySleep
|
|
19609
|
+
}
|
|
19610
|
+
);
|
|
19611
|
+
if (res.status === 426) return { ok: false, error: upgradeRequiredError(res, await res.json().catch(() => null)), status: 426 };
|
|
19612
|
+
const body = await res.json().catch(() => ({}));
|
|
19613
|
+
if (!res.ok) {
|
|
19614
|
+
return { ok: false, error: typeof body.error === "string" ? body.error : `publish HTTP ${res.status}`, status: res.status };
|
|
19494
19615
|
}
|
|
19616
|
+
return { ok: true, body };
|
|
19617
|
+
} catch (e) {
|
|
19618
|
+
return { ok: false, error: e.message };
|
|
19495
19619
|
}
|
|
19496
|
-
const removedProtected = [...removed].filter((p) => protectedBy.has(p)).map((p) => ({ path: p, why: protectedBy.get(p) ?? "" }));
|
|
19497
|
-
return { mandatoryHits, untestedHits, testChanges, addedTests, removedProtected };
|
|
19498
19620
|
}
|
|
19499
|
-
function
|
|
19500
|
-
|
|
19621
|
+
async function searchRepoIndexCloud(query, opts, deps) {
|
|
19622
|
+
if (!deps.baseUrl) return { ok: false, error: "Hub API URL not configured" };
|
|
19623
|
+
const token = await deps.token();
|
|
19624
|
+
if (!token) return { ok: false, error: "no Hub session token (run `gh auth login`)" };
|
|
19625
|
+
const params = new URLSearchParams({
|
|
19626
|
+
q: query,
|
|
19627
|
+
mode: opts.mode ?? "hybrid",
|
|
19628
|
+
limit: String(opts.limit ?? 20)
|
|
19629
|
+
});
|
|
19630
|
+
if (opts.repo) params.set("repo", opts.repo);
|
|
19631
|
+
try {
|
|
19632
|
+
const res = await fetchWithRetry(
|
|
19633
|
+
deps.fetch ?? fetch,
|
|
19634
|
+
`${deps.baseUrl.replace(/\/$/, "")}/repo-index/search?${params}`,
|
|
19635
|
+
{ method: "GET", headers: { ...clientVersionHeaders(), Authorization: `Bearer ${token}` } },
|
|
19636
|
+
{ attempts: RETRY_ATTEMPTS2, timeoutMs: opts.mode === "semantic" || opts.mode === "hybrid" ? 6e4 : deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS, sleep: deps.retrySleep }
|
|
19637
|
+
);
|
|
19638
|
+
if (res.status === 426) return { ok: false, error: upgradeRequiredError(res, await res.json().catch(() => null)), status: 426 };
|
|
19639
|
+
const body = await res.json().catch(() => ({}));
|
|
19640
|
+
if (!res.ok) return { ok: false, error: body.error ?? `search HTTP ${res.status}`, status: res.status };
|
|
19641
|
+
return {
|
|
19642
|
+
ok: true,
|
|
19643
|
+
query: body.query ?? query,
|
|
19644
|
+
mode: body.mode ?? opts.mode ?? "hybrid",
|
|
19645
|
+
hits: Array.isArray(body.hits) ? body.hits : [],
|
|
19646
|
+
note: body.note
|
|
19647
|
+
};
|
|
19648
|
+
} catch (e) {
|
|
19649
|
+
return { ok: false, error: e.message };
|
|
19650
|
+
}
|
|
19501
19651
|
}
|
|
19502
|
-
function
|
|
19503
|
-
|
|
19504
|
-
return [...new Set(declared)].filter((p) => !exists((0, import_node_path21.join)(root, p)));
|
|
19652
|
+
function isRepoIndexStatusError(st) {
|
|
19653
|
+
return typeof st === "object" && st !== null && st.ok === false && typeof st.error === "string" && typeof st.code === "string";
|
|
19505
19654
|
}
|
|
19506
|
-
function
|
|
19507
|
-
|
|
19508
|
-
|
|
19509
|
-
|
|
19510
|
-
|
|
19655
|
+
function explainRepoIndexStatusError(err) {
|
|
19656
|
+
switch (err.code) {
|
|
19657
|
+
case "deploy-lag":
|
|
19658
|
+
return "repo-index status: the Hub API did not recognize /repo-index/status (HTTP 404). The deployed Hub predates the estate index routes \u2014 not an auth error. Ship a Hub release from development that includes /repo-index/*, then run `repo-index-reconcile` (or `mmi-cli repo-index sync-estate`) so projections exist.";
|
|
19659
|
+
case "auth":
|
|
19660
|
+
return `repo-index status: ${err.error} \u2014 run \`gh auth login\` / refresh the Hub session.`;
|
|
19661
|
+
case "config":
|
|
19662
|
+
return `repo-index status: ${err.error}`;
|
|
19663
|
+
default:
|
|
19664
|
+
return `repo-index status: ${err.error}`;
|
|
19665
|
+
}
|
|
19666
|
+
}
|
|
19667
|
+
async function statusRepoIndexCloud(repo, deps) {
|
|
19668
|
+
if (!deps.baseUrl) return { ok: false, error: "Hub API URL not configured", code: "config" };
|
|
19669
|
+
const token = await deps.token();
|
|
19670
|
+
if (!token) return { ok: false, error: "no Hub session token (run `gh auth login`)", code: "auth" };
|
|
19671
|
+
const params = new URLSearchParams();
|
|
19672
|
+
if (repo) params.set("repo", repo);
|
|
19673
|
+
const q = params.toString();
|
|
19674
|
+
try {
|
|
19675
|
+
const res = await fetchWithRetry(
|
|
19676
|
+
deps.fetch ?? fetch,
|
|
19677
|
+
`${deps.baseUrl.replace(/\/$/, "")}/repo-index/status${q ? `?${q}` : ""}`,
|
|
19678
|
+
{ method: "GET", headers: { ...clientVersionHeaders(), Authorization: `Bearer ${token}` } },
|
|
19679
|
+
{ attempts: RETRY_ATTEMPTS2, timeoutMs: deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS, sleep: deps.retrySleep }
|
|
19680
|
+
);
|
|
19681
|
+
if (res.status === 401 || res.status === 403) {
|
|
19682
|
+
return { ok: false, error: `status HTTP ${res.status}`, code: "auth", status: res.status };
|
|
19683
|
+
}
|
|
19684
|
+
if (res.status === 404) {
|
|
19685
|
+
return {
|
|
19686
|
+
ok: false,
|
|
19687
|
+
error: "the Hub API did not recognize /repo-index/status (HTTP 404) \u2014 deployed Hub predates estate index",
|
|
19688
|
+
code: "deploy-lag",
|
|
19689
|
+
status: 404
|
|
19690
|
+
};
|
|
19691
|
+
}
|
|
19692
|
+
if (!res.ok) return { ok: false, error: `status HTTP ${res.status}`, code: "http", status: res.status };
|
|
19693
|
+
return await res.json();
|
|
19694
|
+
} catch (e) {
|
|
19695
|
+
return { ok: false, error: e.message, code: "network" };
|
|
19696
|
+
}
|
|
19697
|
+
}
|
|
19698
|
+
async function gcRepoIndexCloud(deps) {
|
|
19699
|
+
if (!deps.baseUrl) return { ok: false, error: "Hub API URL not configured" };
|
|
19700
|
+
const token = await deps.token();
|
|
19701
|
+
if (!token) return { ok: false, error: "no Hub session token (run `gh auth login`)" };
|
|
19702
|
+
try {
|
|
19703
|
+
const res = await fetchWithRetry(
|
|
19704
|
+
deps.fetch ?? fetch,
|
|
19705
|
+
`${deps.baseUrl.replace(/\/$/, "")}/repo-index/gc`,
|
|
19706
|
+
{
|
|
19707
|
+
method: "POST",
|
|
19708
|
+
headers: { ...clientVersionHeaders(), Authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
19709
|
+
body: "{}"
|
|
19710
|
+
},
|
|
19711
|
+
{ attempts: RETRY_ATTEMPTS2, timeoutMs: deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS, sleep: deps.retrySleep }
|
|
19712
|
+
);
|
|
19713
|
+
const body = await res.json().catch(() => ({}));
|
|
19714
|
+
if (!res.ok) return { ok: false, error: body.error ?? `gc HTTP ${res.status}` };
|
|
19715
|
+
return { ok: true, removed: body.removed ?? [] };
|
|
19716
|
+
} catch (e) {
|
|
19717
|
+
return { ok: false, error: e.message };
|
|
19718
|
+
}
|
|
19719
|
+
}
|
|
19720
|
+
|
|
19721
|
+
// src/repo-index-sync.ts
|
|
19722
|
+
var import_node_fs23 = require("node:fs");
|
|
19723
|
+
var import_node_os9 = require("node:os");
|
|
19724
|
+
var import_node_path21 = require("node:path");
|
|
19725
|
+
var import_node_child_process12 = require("node:child_process");
|
|
19726
|
+
var MAX_EMBED_BACKFILL_ROUNDS = 40;
|
|
19727
|
+
function normalizeRepo(raw) {
|
|
19728
|
+
const t = raw.trim().replace(/\.git$/, "");
|
|
19729
|
+
if (t.includes("/")) {
|
|
19730
|
+
const [owner, name] = t.split("/");
|
|
19731
|
+
return `${(owner || "").toLowerCase()}/${(name || "").toLowerCase()}`;
|
|
19732
|
+
}
|
|
19733
|
+
return `mutmutco/${t.toLowerCase()}`;
|
|
19734
|
+
}
|
|
19735
|
+
function rosterRepos(projects) {
|
|
19736
|
+
const set = /* @__PURE__ */ new Set();
|
|
19737
|
+
for (const p of projects) {
|
|
19738
|
+
if (p.class === "content") continue;
|
|
19739
|
+
for (const r of p.repos ?? []) set.add(normalizeRepo(r));
|
|
19740
|
+
}
|
|
19741
|
+
return [...set].sort((a, b) => a.localeCompare(b));
|
|
19742
|
+
}
|
|
19743
|
+
function shallowClone(repo, dest, token) {
|
|
19744
|
+
const basic = Buffer.from(`x-access-token:${token}`, "utf8").toString("base64");
|
|
19745
|
+
(0, import_node_child_process12.execFileSync)(
|
|
19746
|
+
"git",
|
|
19747
|
+
["-c", `http.extraHeader=Authorization: Basic ${basic}`, "clone", "--depth", "1", "--single-branch", `https://github.com/${repo}.git`, dest],
|
|
19748
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }
|
|
19749
|
+
);
|
|
19750
|
+
}
|
|
19751
|
+
async function syncEstateRepoIndex(opts) {
|
|
19752
|
+
const projects = await fetchProjectsList(opts.deps);
|
|
19753
|
+
if (!projects) {
|
|
19754
|
+
return { ok: false, published: [], failed: [{ repo: "*", error: "could not read /projects/list" }], skipped: [] };
|
|
19755
|
+
}
|
|
19756
|
+
let repos = rosterRepos(projects);
|
|
19757
|
+
if (opts.repo) {
|
|
19758
|
+
const want = normalizeRepo(opts.repo);
|
|
19759
|
+
repos = repos.filter((r) => r.toLowerCase() === want.toLowerCase());
|
|
19760
|
+
if (repos.length === 0) {
|
|
19761
|
+
return { ok: false, published: [], failed: [{ repo: want, error: "not on registry code roster" }], skipped: [] };
|
|
19762
|
+
}
|
|
19763
|
+
}
|
|
19764
|
+
const published = [];
|
|
19765
|
+
const failed = [];
|
|
19766
|
+
const skipped = [];
|
|
19767
|
+
for (const repo of repos) {
|
|
19768
|
+
const dir = (0, import_node_fs23.mkdtempSync)((0, import_node_path21.join)((0, import_node_os9.tmpdir)(), "mmi-repo-index-"));
|
|
19769
|
+
try {
|
|
19770
|
+
shallowClone(repo, dir, opts.githubToken);
|
|
19771
|
+
const built = rebuildRepoIndex(dir, repo);
|
|
19772
|
+
let pub = await publishRepoIndexCloud(
|
|
19773
|
+
{
|
|
19774
|
+
repo,
|
|
19775
|
+
builtAt: built.builtAt,
|
|
19776
|
+
entries: built.entries,
|
|
19777
|
+
embed: opts.embed === true
|
|
19778
|
+
},
|
|
19779
|
+
opts.deps
|
|
19780
|
+
);
|
|
19781
|
+
if (!pub.ok) {
|
|
19782
|
+
failed.push({ repo, error: pub.error });
|
|
19783
|
+
continue;
|
|
19784
|
+
}
|
|
19785
|
+
let rounds = 1;
|
|
19786
|
+
while (opts.embed === true && pub.ok && pub.body.embTruncated === true && rounds < MAX_EMBED_BACKFILL_ROUNDS) {
|
|
19787
|
+
pub = await publishRepoIndexCloud(
|
|
19788
|
+
{
|
|
19789
|
+
repo,
|
|
19790
|
+
builtAt: built.builtAt,
|
|
19791
|
+
entries: built.entries,
|
|
19792
|
+
embed: true
|
|
19793
|
+
},
|
|
19794
|
+
opts.deps
|
|
19795
|
+
);
|
|
19796
|
+
if (!pub.ok) {
|
|
19797
|
+
failed.push({ repo, error: pub.error });
|
|
19798
|
+
break;
|
|
19799
|
+
}
|
|
19800
|
+
rounds++;
|
|
19801
|
+
}
|
|
19802
|
+
if (!pub.ok) continue;
|
|
19803
|
+
const fileCount = Number(pub.body.fileCount ?? built.entries.length);
|
|
19804
|
+
const embCount = typeof pub.body.embCount === "number" ? pub.body.embCount : void 0;
|
|
19805
|
+
const embGap = typeof pub.body.embGap === "number" ? pub.body.embGap : typeof embCount === "number" ? Math.max(0, fileCount - embCount) : void 0;
|
|
19806
|
+
published.push({
|
|
19807
|
+
repo,
|
|
19808
|
+
fileCount,
|
|
19809
|
+
embCount,
|
|
19810
|
+
embGap,
|
|
19811
|
+
embTruncated: pub.body.embTruncated === true,
|
|
19812
|
+
embedRounds: opts.embed ? rounds : void 0
|
|
19813
|
+
});
|
|
19814
|
+
} catch (e) {
|
|
19815
|
+
failed.push({ repo, error: e.message });
|
|
19816
|
+
} finally {
|
|
19817
|
+
try {
|
|
19818
|
+
(0, import_node_fs23.rmSync)(dir, { recursive: true, force: true });
|
|
19819
|
+
} catch {
|
|
19820
|
+
}
|
|
19821
|
+
}
|
|
19822
|
+
}
|
|
19823
|
+
return { ok: failed.length === 0, published, failed, skipped };
|
|
19824
|
+
}
|
|
19825
|
+
|
|
19826
|
+
// src/repo-index-health.ts
|
|
19827
|
+
var import_node_fs24 = require("node:fs");
|
|
19828
|
+
var import_node_path22 = require("node:path");
|
|
19829
|
+
function defaultGoldenPath(repoRoot2) {
|
|
19830
|
+
return (0, import_node_path22.join)(repoRoot2, "cli", "testdata", "repo-index-golden-queries.json");
|
|
19831
|
+
}
|
|
19832
|
+
function loadGoldenSuite(path2) {
|
|
19833
|
+
const raw = JSON.parse((0, import_node_fs24.readFileSync)(path2, "utf8"));
|
|
19834
|
+
if (!raw || raw.schema !== 1 || !Array.isArray(raw.queries)) {
|
|
19835
|
+
throw new Error(`invalid golden suite at ${path2}`);
|
|
19836
|
+
}
|
|
19837
|
+
return raw;
|
|
19838
|
+
}
|
|
19839
|
+
function evaluateQueryHits(query, hits) {
|
|
19840
|
+
if (!hits.length) {
|
|
19841
|
+
return { ok: false, code: "empty-hits", detail: `${query.id}: no hits for ${JSON.stringify(query.q)}` };
|
|
19842
|
+
}
|
|
19843
|
+
const top = hits[0];
|
|
19844
|
+
const minScore = query.expect.minScore ?? 0;
|
|
19845
|
+
if (typeof top.score === "number" && top.score < minScore) {
|
|
19846
|
+
return {
|
|
19847
|
+
ok: false,
|
|
19848
|
+
code: "low-score",
|
|
19849
|
+
detail: `${query.id}: top score ${top.score} < ${minScore}`
|
|
19850
|
+
};
|
|
19851
|
+
}
|
|
19852
|
+
if (query.expect.topRepos?.length) {
|
|
19853
|
+
const repo = String(top.repo ?? "");
|
|
19854
|
+
if (!query.expect.topRepos.some((r) => r.toLowerCase() === repo.toLowerCase())) {
|
|
19855
|
+
return {
|
|
19856
|
+
ok: false,
|
|
19857
|
+
code: "wrong-repo",
|
|
19858
|
+
detail: `${query.id}: top repo ${JSON.stringify(repo)} not in ${JSON.stringify(query.expect.topRepos)}`
|
|
19859
|
+
};
|
|
19860
|
+
}
|
|
19861
|
+
}
|
|
19862
|
+
if (query.expect.topPaths?.length) {
|
|
19863
|
+
const path2 = String(top.path ?? "");
|
|
19864
|
+
if (!query.expect.topPaths.some((p) => path2 === p || path2.endsWith(p) || path2.includes(p))) {
|
|
19865
|
+
return {
|
|
19866
|
+
ok: false,
|
|
19867
|
+
code: "wrong-path",
|
|
19868
|
+
detail: `${query.id}: top path ${JSON.stringify(path2)} not in ${JSON.stringify(query.expect.topPaths)}`
|
|
19869
|
+
};
|
|
19870
|
+
}
|
|
19871
|
+
}
|
|
19872
|
+
return { ok: true, code: "ok", detail: `${query.id}: top hit accepted` };
|
|
19873
|
+
}
|
|
19874
|
+
function evaluateCloudStatus(status, golden) {
|
|
19875
|
+
const findings = [];
|
|
19876
|
+
const count = typeof status.count === "number" ? status.count : Array.isArray(status.repos) ? status.repos.length : null;
|
|
19877
|
+
if (count === 0 || Array.isArray(status.repos) && status.repos.length === 0 && status.present !== true) {
|
|
19878
|
+
findings.push({ ok: false, code: "projections-empty", detail: "cloud repo-index has zero projections" });
|
|
19879
|
+
}
|
|
19880
|
+
const repos = [];
|
|
19881
|
+
if (Array.isArray(status.repos)) {
|
|
19882
|
+
for (const r of status.repos) {
|
|
19883
|
+
if (r && typeof r === "object") repos.push(r);
|
|
19884
|
+
}
|
|
19885
|
+
} else if (status.present === true && typeof status.repo === "string") {
|
|
19886
|
+
repos.push(status);
|
|
19887
|
+
}
|
|
19888
|
+
const stale = repos.filter((r) => r.stale === true);
|
|
19889
|
+
if (stale.length) {
|
|
19890
|
+
findings.push({
|
|
19891
|
+
ok: false,
|
|
19892
|
+
code: "stale-projections",
|
|
19893
|
+
detail: `${stale.length} stale projection(s) (threshold ${golden.staleThresholdHours}h): ${stale.slice(0, 5).map((r) => r.repo).join(", ")}`
|
|
19894
|
+
});
|
|
19895
|
+
}
|
|
19896
|
+
const hub = repos.find((r) => r.repo?.toLowerCase() === golden.hubRepo.toLowerCase()) ?? (typeof status.repo === "string" && status.repo.toLowerCase() === golden.hubRepo.toLowerCase() ? status : void 0);
|
|
19897
|
+
if (!hub || hub.present === false) {
|
|
19898
|
+
findings.push({ ok: false, code: "hub-missing", detail: `${golden.hubRepo} projection missing` });
|
|
19899
|
+
} else {
|
|
19900
|
+
const fileCount = Number(hub.fileCount ?? 0);
|
|
19901
|
+
const embCount = Number(hub.embCount ?? 0);
|
|
19902
|
+
if (fileCount <= 0) {
|
|
19903
|
+
findings.push({ ok: false, code: "hub-empty", detail: `${golden.hubRepo} has fileCount=${fileCount}` });
|
|
19904
|
+
}
|
|
19905
|
+
const denom = Math.min(fileCount, golden.maxSyncEmbeds);
|
|
19906
|
+
const coverage = denom > 0 ? embCount / denom : 0;
|
|
19907
|
+
if (fileCount > 0 && coverage < golden.hubEmbMinCoverage) {
|
|
19908
|
+
findings.push({
|
|
19909
|
+
ok: false,
|
|
19910
|
+
code: "hub-emb-gap",
|
|
19911
|
+
detail: `${golden.hubRepo} embCoverage=${coverage.toFixed(2)} (emb=${embCount} files=${fileCount} cap=${golden.maxSyncEmbeds}) < ${golden.hubEmbMinCoverage}`
|
|
19912
|
+
});
|
|
19913
|
+
}
|
|
19914
|
+
}
|
|
19915
|
+
if (!findings.length) {
|
|
19916
|
+
findings.push({ ok: true, code: "status-ok", detail: `status healthy (${repos.length || count || 0} repo projection(s))` });
|
|
19917
|
+
}
|
|
19918
|
+
return findings;
|
|
19919
|
+
}
|
|
19920
|
+
async function runRepoIndexHealth(opts) {
|
|
19921
|
+
const findings = [];
|
|
19922
|
+
if (opts.live) {
|
|
19923
|
+
const st = await opts.live.status();
|
|
19924
|
+
if (st.ok === false && typeof st.error === "string") {
|
|
19925
|
+
findings.push({ ok: false, code: "status-error", detail: st.error });
|
|
19926
|
+
} else {
|
|
19927
|
+
findings.push(...evaluateCloudStatus(st, opts.golden));
|
|
19928
|
+
}
|
|
19929
|
+
for (const q of opts.golden.queries) {
|
|
19930
|
+
const res = await opts.live.search(q.q, q.mode);
|
|
19931
|
+
if (!res.ok) {
|
|
19932
|
+
const semantic503 = q.mode === "semantic" && (res.status === 503 || /semantic unavailable|503/i.test(res.error));
|
|
19933
|
+
findings.push({
|
|
19934
|
+
ok: false,
|
|
19935
|
+
code: semantic503 ? "semantic-503" : "search-error",
|
|
19936
|
+
detail: `${q.id}: ${res.error}`
|
|
19937
|
+
});
|
|
19938
|
+
continue;
|
|
19939
|
+
}
|
|
19940
|
+
findings.push(evaluateQueryHits(q, res.hits));
|
|
19941
|
+
}
|
|
19942
|
+
} else if (opts.fixtures) {
|
|
19943
|
+
for (const q of opts.golden.queries) {
|
|
19944
|
+
const fx = opts.fixtures[q.id];
|
|
19945
|
+
if (!fx) {
|
|
19946
|
+
findings.push({ ok: false, code: "fixture-missing", detail: `${q.id}: no fixture` });
|
|
19947
|
+
continue;
|
|
19948
|
+
}
|
|
19949
|
+
if (fx.error) {
|
|
19950
|
+
const semantic503 = q.mode === "semantic" && (fx.status === 503 || /semantic unavailable|503/i.test(fx.error));
|
|
19951
|
+
findings.push({
|
|
19952
|
+
ok: false,
|
|
19953
|
+
code: semantic503 ? "semantic-503" : "search-error",
|
|
19954
|
+
detail: `${q.id}: ${fx.error}`
|
|
19955
|
+
});
|
|
19956
|
+
continue;
|
|
19957
|
+
}
|
|
19958
|
+
findings.push(evaluateQueryHits(q, fx.hits ?? []));
|
|
19959
|
+
}
|
|
19960
|
+
} else {
|
|
19961
|
+
for (const q of opts.golden.queries) {
|
|
19962
|
+
if (!q.id || !q.q || !q.mode) {
|
|
19963
|
+
findings.push({ ok: false, code: "golden-invalid", detail: `query missing id/q/mode` });
|
|
19964
|
+
} else {
|
|
19965
|
+
findings.push({ ok: true, code: "golden-ok", detail: `${q.id}: shape ok` });
|
|
19966
|
+
}
|
|
19967
|
+
}
|
|
19968
|
+
}
|
|
19969
|
+
return { ok: findings.every((f) => f.ok), findings };
|
|
19970
|
+
}
|
|
19971
|
+
|
|
19972
|
+
// src/spawn-policy-core.ts
|
|
19973
|
+
var import_node_child_process13 = require("node:child_process");
|
|
19974
|
+
var import_node_fs25 = require("node:fs");
|
|
19975
|
+
var import_node_path23 = require("node:path");
|
|
19976
|
+
var SPAWNERS = ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync"];
|
|
19977
|
+
var CALL_SOURCE = String.raw`(^|[^.\w$])(${SPAWNERS.join("|")})\s*\(`;
|
|
19978
|
+
var SOURCE_EXT = /\.(ts|mts|cts|js|mjs|cjs)$/;
|
|
19979
|
+
var EXCLUDED = [
|
|
19980
|
+
/(^|\/)node_modules\//,
|
|
19981
|
+
/(^|\/)(dist|build|coverage)\//,
|
|
19982
|
+
/(^|\/)(test|tests|__tests__|fixtures|__fixtures__)\//,
|
|
19983
|
+
/\.(test|spec)\.[cm]?[jt]s$/,
|
|
19984
|
+
/\.d\.[cm]?ts$/
|
|
19985
|
+
];
|
|
19986
|
+
function stripComments(src) {
|
|
19987
|
+
return src.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " ")).replace(/(^|[^:])\/\/[^\n]*/g, (m, p) => p + " ".repeat(m.length - p.length));
|
|
19988
|
+
}
|
|
19989
|
+
function callArgs(src, open2) {
|
|
19990
|
+
let depth = 0;
|
|
19991
|
+
for (let i = open2; i < src.length; i++) {
|
|
19992
|
+
const c = src[i];
|
|
19993
|
+
if (c === "(") depth++;
|
|
19994
|
+
else if (c === ")") {
|
|
19995
|
+
depth--;
|
|
19996
|
+
if (depth === 0) return src.slice(open2, i + 1);
|
|
19997
|
+
}
|
|
19998
|
+
}
|
|
19999
|
+
return "";
|
|
20000
|
+
}
|
|
20001
|
+
function findViolationsInSource(raw) {
|
|
20002
|
+
if (!/from\s+["']node:child_process["']/.test(raw)) return [];
|
|
20003
|
+
const importBlock = /import\s*(?:type\s*)?\{([^}]*)\}\s*from\s+["']node:child_process["']/.exec(raw);
|
|
20004
|
+
const imported = new Set(
|
|
20005
|
+
(importBlock?.[1] ?? "").split(",").map((s) => s.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean)
|
|
20006
|
+
);
|
|
20007
|
+
if (imported.size === 0) return [];
|
|
20008
|
+
const rawLines = raw.split("\n");
|
|
20009
|
+
const src = stripComments(raw);
|
|
20010
|
+
const call = new RegExp(CALL_SOURCE, "g");
|
|
20011
|
+
const found = [];
|
|
20012
|
+
let m;
|
|
20013
|
+
while (m = call.exec(src)) {
|
|
20014
|
+
const callee = m[2];
|
|
20015
|
+
if (!imported.has(callee)) continue;
|
|
20016
|
+
const open2 = m.index + m[0].length - 1;
|
|
20017
|
+
const args = callArgs(src, open2);
|
|
20018
|
+
if (!args) continue;
|
|
20019
|
+
if (args.includes("windowsHide")) continue;
|
|
20020
|
+
const line = src.slice(0, m.index + m[1].length).split("\n").length;
|
|
20021
|
+
if (/windows-hide-exempt:/.test(rawLines[line - 2] ?? "")) continue;
|
|
20022
|
+
if (/\.{3}\s*\w+/.test(args)) continue;
|
|
20023
|
+
if (/\b(opts|options|spawnOptions)\b/.test(args)) continue;
|
|
20024
|
+
const identifiers = new Set(args.match(/\b[A-Za-z_$][\w$]*\b/g) ?? []);
|
|
20025
|
+
let satisfied = false;
|
|
20026
|
+
for (const id of identifiers) {
|
|
20027
|
+
if (SPAWNERS.includes(id)) continue;
|
|
20028
|
+
const decl = new RegExp(String.raw`\b(?:const|let|var)\s+${id}\b[^;]*`, "s").exec(src);
|
|
20029
|
+
if (decl && decl[0].includes("windowsHide")) {
|
|
20030
|
+
satisfied = true;
|
|
20031
|
+
break;
|
|
20032
|
+
}
|
|
20033
|
+
if (new RegExp(String.raw`\b${id}\s*:\s*\{[^}]*windowsHide`, "s").test(src)) {
|
|
20034
|
+
satisfied = true;
|
|
20035
|
+
break;
|
|
20036
|
+
}
|
|
20037
|
+
}
|
|
20038
|
+
if (satisfied) continue;
|
|
20039
|
+
found.push({ line, callee });
|
|
20040
|
+
}
|
|
20041
|
+
return found;
|
|
20042
|
+
}
|
|
20043
|
+
function policedFiles(root) {
|
|
20044
|
+
const r = (0, import_node_child_process13.spawnSync)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
20045
|
+
cwd: root,
|
|
20046
|
+
encoding: "utf8",
|
|
20047
|
+
windowsHide: true,
|
|
20048
|
+
maxBuffer: 32 * 1024 * 1024
|
|
20049
|
+
});
|
|
20050
|
+
if (r.status !== 0) {
|
|
20051
|
+
throw new Error(`spawn policy: git ls-files failed: ${(r.stderr || "").trim() || `exit ${r.status}`}`);
|
|
20052
|
+
}
|
|
20053
|
+
return (r.stdout ?? "").split("\0").filter(Boolean).filter((f) => SOURCE_EXT.test(f)).filter((f) => !EXCLUDED.some((re) => re.test(f)));
|
|
20054
|
+
}
|
|
20055
|
+
function runSpawnPolicy(root) {
|
|
20056
|
+
const files = policedFiles(root);
|
|
20057
|
+
const findings = [];
|
|
20058
|
+
for (const file of files) {
|
|
20059
|
+
let raw;
|
|
20060
|
+
try {
|
|
20061
|
+
raw = (0, import_node_fs25.readFileSync)((0, import_node_path23.join)(root, file), "utf8");
|
|
20062
|
+
} catch {
|
|
20063
|
+
continue;
|
|
20064
|
+
}
|
|
20065
|
+
for (const v of findViolationsInSource(raw)) {
|
|
20066
|
+
findings.push({
|
|
20067
|
+
file,
|
|
20068
|
+
line: v.line,
|
|
20069
|
+
callee: v.callee,
|
|
20070
|
+
detail: `${file}:${v.line} ${v.callee}(...) has no windowsHide`
|
|
20071
|
+
});
|
|
20072
|
+
}
|
|
20073
|
+
}
|
|
20074
|
+
return { ok: findings.length === 0, scannedCount: files.length, findings };
|
|
20075
|
+
}
|
|
20076
|
+
|
|
20077
|
+
// src/test-policy-core.ts
|
|
20078
|
+
var import_node_child_process14 = require("node:child_process");
|
|
20079
|
+
var import_node_fs26 = require("node:fs");
|
|
20080
|
+
var import_node_path24 = require("node:path");
|
|
20081
|
+
var POLICY_FILE = "test-policy.json";
|
|
20082
|
+
var TEST_RE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
20083
|
+
var PY_TEST_RE = /(?:^|\/)test_[^/]*\.py$|_test\.py$/;
|
|
20084
|
+
var TRAILER_KEY = "Test-Policy-Override";
|
|
20085
|
+
var OVERRIDE_RE = /^Test-Policy-Override:\s*(.+)$/im;
|
|
20086
|
+
var REC = "";
|
|
20087
|
+
var FLD = "";
|
|
20088
|
+
var WAIVABLE_KINDS = [
|
|
20089
|
+
"mandatory-zone-untested",
|
|
20090
|
+
"unrequested-test-file",
|
|
20091
|
+
"protected-removed",
|
|
20092
|
+
"stale-protected-entry",
|
|
20093
|
+
"stale-satisfied-by"
|
|
20094
|
+
];
|
|
20095
|
+
function translate(glob) {
|
|
20096
|
+
let out = "";
|
|
20097
|
+
for (let i = 0; i < glob.length; i++) {
|
|
20098
|
+
const c = glob[i];
|
|
20099
|
+
if (c === "*") {
|
|
20100
|
+
if (glob[i + 1] === "*") {
|
|
20101
|
+
if (glob[i + 2] === "/") {
|
|
20102
|
+
out += "(?:.*/)?";
|
|
20103
|
+
i += 2;
|
|
20104
|
+
} else {
|
|
20105
|
+
out += ".*";
|
|
20106
|
+
i += 1;
|
|
20107
|
+
}
|
|
20108
|
+
} else {
|
|
20109
|
+
out += "[^/]*";
|
|
20110
|
+
}
|
|
20111
|
+
continue;
|
|
20112
|
+
}
|
|
20113
|
+
if (c === "{") {
|
|
20114
|
+
const close = glob.indexOf("}", i);
|
|
20115
|
+
if (close !== -1) {
|
|
20116
|
+
const alts = glob.slice(i + 1, close).split(",").map((a) => translate(a));
|
|
20117
|
+
out += `(?:${alts.join("|")})`;
|
|
20118
|
+
i = close;
|
|
20119
|
+
continue;
|
|
20120
|
+
}
|
|
20121
|
+
}
|
|
20122
|
+
out += /[.+?^${}()|[\]\\]/.test(c) ? `\\${c}` : c;
|
|
20123
|
+
}
|
|
20124
|
+
return out;
|
|
20125
|
+
}
|
|
20126
|
+
function globToRegExp(glob) {
|
|
20127
|
+
return new RegExp(`^${translate(glob)}$`);
|
|
20128
|
+
}
|
|
20129
|
+
function isTestPath(path2) {
|
|
20130
|
+
return TEST_RE.test(path2) || PY_TEST_RE.test(path2);
|
|
20131
|
+
}
|
|
20132
|
+
function loadPolicy(root, readFile9 = readFileOrNull2) {
|
|
20133
|
+
const raw = readFile9((0, import_node_path24.join)(root, POLICY_FILE));
|
|
20134
|
+
if (raw == null) return { mandatory: [], declared: false };
|
|
20135
|
+
try {
|
|
20136
|
+
return { ...JSON.parse(raw), declared: true };
|
|
20137
|
+
} catch (error) {
|
|
20138
|
+
throw new Error(`${POLICY_FILE} is not valid JSON: ${error.message}`);
|
|
20139
|
+
}
|
|
20140
|
+
}
|
|
20141
|
+
function readFileOrNull2(path2) {
|
|
20142
|
+
try {
|
|
20143
|
+
return (0, import_node_fs26.readFileSync)(path2, "utf8");
|
|
20144
|
+
} catch {
|
|
20145
|
+
return null;
|
|
20146
|
+
}
|
|
20147
|
+
}
|
|
20148
|
+
function removedPaths(changed) {
|
|
20149
|
+
return new Set(
|
|
20150
|
+
changed.map((f) => f.status === "D" ? f.path : f.status === "R" || f.status === "C" ? f.from : void 0).filter((p) => typeof p === "string")
|
|
20151
|
+
);
|
|
20152
|
+
}
|
|
20153
|
+
function classify(changed, policy, present = () => false) {
|
|
20154
|
+
const matchers = (policy.mandatory ?? []).map((m) => ({ ...m, re: globToRegExp(m.glob) }));
|
|
20155
|
+
const mandatoryHits = changed.filter((f) => matchers.some((m) => m.re.test(f.path)));
|
|
20156
|
+
const testChanges = changed.filter((f) => isTestPath(f.path));
|
|
20157
|
+
const addedTests = testChanges.filter((f) => f.status === "A");
|
|
20158
|
+
const removed = removedPaths(changed);
|
|
20159
|
+
const discharged = (m) => (m.satisfiedBy?.length ?? 0) > 0 && m.satisfiedBy.every((p) => present(p) && !removed.has(p));
|
|
20160
|
+
const untestedHits = changed.filter((f) => matchers.some((m) => m.re.test(f.path) && !discharged(m)));
|
|
20161
|
+
const protectedBy = new Map((policy.protected ?? []).map((p) => [p.path, p.why ?? ""]));
|
|
20162
|
+
for (const m of policy.mandatory ?? []) {
|
|
20163
|
+
for (const p of m.satisfiedBy ?? []) {
|
|
20164
|
+
if (!protectedBy.has(p)) protectedBy.set(p, `the standing coverage \`${m.glob}\` is discharged by. Removing it silently empties that glob.`);
|
|
20165
|
+
}
|
|
20166
|
+
}
|
|
20167
|
+
const removedProtected = [...removed].filter((p) => protectedBy.has(p)).map((p) => ({ path: p, why: protectedBy.get(p) ?? "" }));
|
|
20168
|
+
return { mandatoryHits, untestedHits, testChanges, addedTests, removedProtected };
|
|
20169
|
+
}
|
|
20170
|
+
function unresolvedProtectedEntries(policy, root, exists = (path2) => (0, import_node_fs26.existsSync)(path2)) {
|
|
20171
|
+
return (policy.protected ?? []).map((p) => p.path).filter((p) => !exists((0, import_node_path24.join)(root, p)));
|
|
20172
|
+
}
|
|
20173
|
+
function unresolvedSatisfiers(policy, root, exists = (path2) => (0, import_node_fs26.existsSync)(path2)) {
|
|
20174
|
+
const declared = (policy.mandatory ?? []).flatMap((m) => m.satisfiedBy ?? []);
|
|
20175
|
+
return [...new Set(declared)].filter((p) => !exists((0, import_node_path24.join)(root, p)));
|
|
20176
|
+
}
|
|
20177
|
+
function evaluate(changed, policy, present = () => false) {
|
|
20178
|
+
const { mandatoryHits, untestedHits, testChanges, addedTests, removedProtected } = classify(changed, policy, present);
|
|
20179
|
+
const findings = [];
|
|
20180
|
+
if (removedProtected.length > 0) {
|
|
20181
|
+
findings.push({
|
|
19511
20182
|
kind: "protected-removed",
|
|
19512
20183
|
paths: removedProtected.map((f) => f.path),
|
|
19513
20184
|
detail: `PROTECTED TEST FILE(S) REMOVED \u2014 this change deletes or renames away ${removedProtected.length} file(s) test-policy.json protects:
|
|
@@ -19534,14 +20205,14 @@ function evaluate(changed, policy, present = () => false) {
|
|
|
19534
20205
|
return findings;
|
|
19535
20206
|
}
|
|
19536
20207
|
function git(args, cwd) {
|
|
19537
|
-
return (0,
|
|
20208
|
+
return (0, import_node_child_process14.execFileSync)("git", args, { windowsHide: true, cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
|
|
19538
20209
|
}
|
|
19539
20210
|
var COAUTHOR_KEY = "Co-authored-by";
|
|
19540
20211
|
var GH_MESSAGE_SEPARATOR = /^-{5,}$/;
|
|
19541
20212
|
var LIFTED_KEYS = new RegExp(`^(?:${TRAILER_KEY}|${COAUTHOR_KEY}):`, "i");
|
|
19542
20213
|
function parseTrailers(message, cwd) {
|
|
19543
20214
|
try {
|
|
19544
|
-
return (0,
|
|
20215
|
+
return (0, import_node_child_process14.execFileSync)("git", ["interpret-trailers", "--parse", "--unfold"], {
|
|
19545
20216
|
windowsHide: true,
|
|
19546
20217
|
cwd,
|
|
19547
20218
|
input: message,
|
|
@@ -19683,13 +20354,13 @@ function changedFilesSince(base, cwd) {
|
|
|
19683
20354
|
}
|
|
19684
20355
|
function runTestPolicy(root, deps = {}) {
|
|
19685
20356
|
const policy = deps.policy ?? loadPolicy(root);
|
|
19686
|
-
const exists = deps.exists ?? ((path2) => (0,
|
|
20357
|
+
const exists = deps.exists ?? ((path2) => (0, import_node_fs26.existsSync)(path2));
|
|
19687
20358
|
const counts = { mandatoryCount: (policy.mandatory ?? []).length, protectedCount: (policy.protected ?? []).length };
|
|
19688
20359
|
const base = deps.changed ? "(injected)" : resolveBase(root, deps.base);
|
|
19689
20360
|
const refusal = deps.changed ? null : untrustworthyRange(root, base);
|
|
19690
20361
|
const changed = deps.changed ?? (refusal ? [] : changedFilesSince(base, root));
|
|
19691
20362
|
const lookup = deps.override !== void 0 ? { override: deps.override, refusals: [] } : !deps.changed && !refusal ? readOverride(base, root) : { override: null, refusals: [] };
|
|
19692
|
-
const present = (path2) => exists((0,
|
|
20363
|
+
const present = (path2) => exists((0, import_node_path24.join)(root, path2));
|
|
19693
20364
|
const removedByThisDiff = removedPaths(changed);
|
|
19694
20365
|
const staleFindings = [];
|
|
19695
20366
|
const unresolved = unresolvedProtectedEntries(policy, root, exists).filter((p) => !removedByThisDiff.has(p));
|
|
@@ -19845,8 +20516,8 @@ function docsAuditStatus(fetch2, opts) {
|
|
|
19845
20516
|
}
|
|
19846
20517
|
|
|
19847
20518
|
// src/project-info-sync.ts
|
|
19848
|
-
var
|
|
19849
|
-
var
|
|
20519
|
+
var import_node_fs27 = require("node:fs");
|
|
20520
|
+
var import_node_path25 = require("node:path");
|
|
19850
20521
|
var UPDATE_PROJECT_INFO = `mutation($projectId: ID!, $shortDescription: String!, $readme: String!) {
|
|
19851
20522
|
updateProjectV2(input: { projectId: $projectId, shortDescription: $shortDescription, readme: $readme }) {
|
|
19852
20523
|
projectV2 { id }
|
|
@@ -19891,14 +20562,14 @@ function sharedName(entries, fallback) {
|
|
|
19891
20562
|
}
|
|
19892
20563
|
function buildProjectInfoSyncPlan(targetRepo2, project2, projects, repoRoot2) {
|
|
19893
20564
|
if (!project2.projectId) throw new Error(`org project sync-info: ${targetRepo2} registry META has no projectId`);
|
|
19894
|
-
const readmePath = (0,
|
|
19895
|
-
if (!(0,
|
|
20565
|
+
const readmePath = (0, import_node_path25.join)(repoRoot2, "README.md");
|
|
20566
|
+
if (!(0, import_node_fs27.existsSync)(readmePath)) throw new Error(`org project sync-info: ${targetRepo2} has no README.md`);
|
|
19896
20567
|
const entries = entriesFor(project2, projects);
|
|
19897
20568
|
const memberRepos = [...new Set(entries.flatMap((entry) => entry.repos ?? []))].filter((repo) => /^[^/]+\/[^/]+$/.test(repo)).sort((a, b) => a.localeCompare(b));
|
|
19898
20569
|
const projectName = sharedName(entries, project2.name?.trim() || targetRepo2.split("/").pop() || targetRepo2);
|
|
19899
20570
|
if (!memberRepos.length) throw new Error(`org project sync-info: project ${projectName} has no registered member repos`);
|
|
19900
20571
|
const entryNames = entries.map((entry) => entry.name?.trim()).filter((name) => Boolean(name));
|
|
19901
|
-
const shortDescription = memberRepos.length === 1 ? shortDescriptionFromReadme((0,
|
|
20572
|
+
const shortDescription = memberRepos.length === 1 ? shortDescriptionFromReadme((0, import_node_fs27.readFileSync)(readmePath, "utf8")) : `Shared work across ${new Intl.ListFormat("en", { type: "conjunction" }).format(entryNames)}.`;
|
|
19902
20573
|
const lines = [
|
|
19903
20574
|
`# ${projectName}`,
|
|
19904
20575
|
"",
|
|
@@ -19917,8 +20588,8 @@ function buildProjectInfoSyncPlan(targetRepo2, project2, projects, repoRoot2) {
|
|
|
19917
20588
|
const targetBase = `https://github.com/${targetRepo2}`;
|
|
19918
20589
|
const targetBranch = branchFor(targetRepo2, projects);
|
|
19919
20590
|
const orgDocs = [
|
|
19920
|
-
(0,
|
|
19921
|
-
(0,
|
|
20591
|
+
(0, import_node_fs27.existsSync)((0, import_node_path25.join)(repoRoot2, "docs", "org-readme.md")) ? `- [Org identity](${targetBase}/blob/${targetBranch}/docs/org-readme.md)` : "",
|
|
20592
|
+
(0, import_node_fs27.existsSync)((0, import_node_path25.join)(repoRoot2, "docs", "org-architecture.md")) ? `- [Org architecture](${targetBase}/blob/${targetBranch}/docs/org-architecture.md)` : ""
|
|
19922
20593
|
].filter(Boolean);
|
|
19923
20594
|
if (orgDocs.length) lines.push("", "## Organisation docs", "", ...orgDocs);
|
|
19924
20595
|
return { projectId: project2.projectId, projectName, targetRepo: targetRepo2, memberRepos, shortDescription, readme: `${lines.join("\n")}
|
|
@@ -20777,9 +21448,9 @@ function writeError(res) {
|
|
|
20777
21448
|
}
|
|
20778
21449
|
|
|
20779
21450
|
// src/secrets-commands.ts
|
|
20780
|
-
var
|
|
20781
|
-
var
|
|
20782
|
-
var
|
|
21451
|
+
var import_node_fs28 = require("node:fs");
|
|
21452
|
+
var import_node_path26 = require("node:path");
|
|
21453
|
+
var import_node_os10 = require("node:os");
|
|
20783
21454
|
|
|
20784
21455
|
// src/project-runtime.ts
|
|
20785
21456
|
function hasRuntimeSecretContract(contract) {
|
|
@@ -20902,18 +21573,18 @@ function collectMap(value, previous = []) {
|
|
|
20902
21573
|
return [...previous, value];
|
|
20903
21574
|
}
|
|
20904
21575
|
async function decryptRailsCredentials(input) {
|
|
20905
|
-
const appDir = (0,
|
|
21576
|
+
const appDir = (0, import_node_path26.resolve)(input.appDir ?? process.cwd());
|
|
20906
21577
|
const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
|
|
20907
21578
|
const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
|
|
20908
|
-
const credentialsPath = (0,
|
|
20909
|
-
const masterKeyPath = (0,
|
|
21579
|
+
const credentialsPath = (0, import_node_path26.resolve)(appDir, credentialsFile);
|
|
21580
|
+
const masterKeyPath = (0, import_node_path26.resolve)(appDir, masterKeyFile);
|
|
20910
21581
|
const env = {
|
|
20911
21582
|
...process.env,
|
|
20912
21583
|
MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
|
|
20913
21584
|
MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
|
|
20914
21585
|
};
|
|
20915
|
-
if ((0,
|
|
20916
|
-
env.RAILS_MASTER_KEY = (0,
|
|
21586
|
+
if ((0, import_node_fs28.existsSync)(masterKeyPath)) {
|
|
21587
|
+
env.RAILS_MASTER_KEY = (0, import_node_fs28.readFileSync)(masterKeyPath, "utf8").trim();
|
|
20917
21588
|
}
|
|
20918
21589
|
const script = [
|
|
20919
21590
|
'require "json"',
|
|
@@ -20923,9 +21594,9 @@ async function decryptRailsCredentials(input) {
|
|
|
20923
21594
|
'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
|
|
20924
21595
|
"puts JSON.generate(config.config)"
|
|
20925
21596
|
].join("\n");
|
|
20926
|
-
const scriptDir = (0,
|
|
20927
|
-
const scriptPath = (0,
|
|
20928
|
-
(0,
|
|
21597
|
+
const scriptDir = (0, import_node_fs28.mkdtempSync)((0, import_node_path26.join)((0, import_node_os10.tmpdir)(), "mmi-rails-decrypt-"));
|
|
21598
|
+
const scriptPath = (0, import_node_path26.join)(scriptDir, "decrypt.rb");
|
|
21599
|
+
(0, import_node_fs28.writeFileSync)(scriptPath, script, "utf8");
|
|
20929
21600
|
try {
|
|
20930
21601
|
const args = ["exec", "ruby", scriptPath];
|
|
20931
21602
|
const cmd = process.platform === "win32" ? "cmd.exe" : "bundle";
|
|
@@ -20937,7 +21608,7 @@ async function decryptRailsCredentials(input) {
|
|
|
20937
21608
|
});
|
|
20938
21609
|
return JSON.parse(stdout);
|
|
20939
21610
|
} finally {
|
|
20940
|
-
(0,
|
|
21611
|
+
(0, import_node_fs28.rmSync)(scriptDir, { recursive: true, force: true });
|
|
20941
21612
|
}
|
|
20942
21613
|
}
|
|
20943
21614
|
async function readSecretStdin() {
|
|
@@ -21027,7 +21698,7 @@ function registerSecretsCommands(program3) {
|
|
|
21027
21698
|
let body;
|
|
21028
21699
|
if (o.file) {
|
|
21029
21700
|
try {
|
|
21030
|
-
body = (0,
|
|
21701
|
+
body = (0, import_node_fs28.readFileSync)((0, import_node_path26.resolve)(o.file), "utf8");
|
|
21031
21702
|
} catch (e) {
|
|
21032
21703
|
return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
|
|
21033
21704
|
}
|
|
@@ -21132,7 +21803,7 @@ function registerSecretsCommands(program3) {
|
|
|
21132
21803
|
{
|
|
21133
21804
|
...d,
|
|
21134
21805
|
decryptRailsCredentials,
|
|
21135
|
-
removeFile: (path2) => (0,
|
|
21806
|
+
removeFile: (path2) => (0, import_node_fs28.unlinkSync)((0, import_node_path26.resolve)(o.appDir ?? process.cwd(), path2))
|
|
21136
21807
|
},
|
|
21137
21808
|
{
|
|
21138
21809
|
repo: o.repo,
|
|
@@ -21177,7 +21848,7 @@ function registerSecretsCommands(program3) {
|
|
|
21177
21848
|
}
|
|
21178
21849
|
|
|
21179
21850
|
// src/app-actor.ts
|
|
21180
|
-
var
|
|
21851
|
+
var import_node_crypto5 = require("node:crypto");
|
|
21181
21852
|
var APP_ACTOR_ENV = "MMI_ACTOR";
|
|
21182
21853
|
var APP_VAULT_REPO = "mutmutco/MMI-Hub";
|
|
21183
21854
|
var APP_VAULT_KEYS = ["GITHUB_APP_ID", "GITHUB_APP_INSTALLATION_ID", "GITHUB_APP_PRIVATE_KEY"];
|
|
@@ -21221,7 +21892,7 @@ function mintAppJwt(appId, privateKeyPem, nowSec) {
|
|
|
21221
21892
|
exp: now + APP_JWT_TTL_S,
|
|
21222
21893
|
iss: appId
|
|
21223
21894
|
}));
|
|
21224
|
-
const signer = (0,
|
|
21895
|
+
const signer = (0, import_node_crypto5.createSign)("RSA-SHA256");
|
|
21225
21896
|
signer.update(`${header}.${payload}`);
|
|
21226
21897
|
return `${header}.${payload}.${signer.sign(privateKeyPem, "base64url")}`;
|
|
21227
21898
|
}
|
|
@@ -21296,7 +21967,7 @@ async function activateAppActor(commandPath3, env, mint) {
|
|
|
21296
21967
|
}
|
|
21297
21968
|
|
|
21298
21969
|
// src/box-commands.ts
|
|
21299
|
-
var
|
|
21970
|
+
var import_node_fs29 = require("node:fs");
|
|
21300
21971
|
|
|
21301
21972
|
// src/box.ts
|
|
21302
21973
|
var BOX_KEYS = {
|
|
@@ -21499,7 +22170,7 @@ function registerBoxCommands(program3) {
|
|
|
21499
22170
|
}
|
|
21500
22171
|
if (o.json) console.log(JSON.stringify({ box: found, incomplete }, null, 2));
|
|
21501
22172
|
else if (o.ssh && o.script) {
|
|
21502
|
-
(0,
|
|
22173
|
+
(0, import_node_fs29.writeFileSync)(o.script, sshRecipeScript(found), "utf8");
|
|
21503
22174
|
console.log(`wrote ${o.script} \u2014 run: bash "${o.script}"`);
|
|
21504
22175
|
} else if (o.ssh) console.log(`${formatSshRecipe(found)}
|
|
21505
22176
|
${SSH_RECIPE_AGENT_NOTE}`);
|
|
@@ -21514,7 +22185,7 @@ ${SSH_RECIPE_AGENT_NOTE}`);
|
|
|
21514
22185
|
|
|
21515
22186
|
// src/schedules-commands.ts
|
|
21516
22187
|
var import_promises4 = require("node:fs/promises");
|
|
21517
|
-
var
|
|
22188
|
+
var import_node_child_process15 = require("node:child_process");
|
|
21518
22189
|
var import_node_util7 = require("node:util");
|
|
21519
22190
|
|
|
21520
22191
|
// src/schedules.ts
|
|
@@ -21638,9 +22309,9 @@ function extractWorkflowCrons(yamlText) {
|
|
|
21638
22309
|
function workflowEntry(repo, workflowPath, yamlText) {
|
|
21639
22310
|
const crons = extractWorkflowCrons(yamlText);
|
|
21640
22311
|
if (!crons.length) return null;
|
|
21641
|
-
const
|
|
22312
|
+
const basename5 = workflowPath.split("/").pop() ?? workflowPath;
|
|
21642
22313
|
return {
|
|
21643
|
-
name: `${repo}/${
|
|
22314
|
+
name: `${repo}/${basename5.replace(/\.ya?ml$/, "")}`,
|
|
21644
22315
|
cadence: crons.join(" + "),
|
|
21645
22316
|
executor: "github-actions",
|
|
21646
22317
|
llm: llmFromHeader(yamlText),
|
|
@@ -21753,6 +22424,9 @@ var DECLARED_ENTRIES = [
|
|
|
21753
22424
|
// unavailable exactly when it is needed. This one is root cron on the box: no checkout, no node, and
|
|
21754
22425
|
// a no-op below 90% disk, which is why 15 minutes is affordable.
|
|
21755
22426
|
{ 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)" },
|
|
22427
|
+
// #4118: Listening + systemd-active ≠ GitHub online after acquirejob 503. restart.conf cannot see
|
|
22428
|
+
// it; an Actions workflow on mmi-live cannot heal it when half the fleet is offline. Root cron.
|
|
22429
|
+
{ name: "mmi-runner-online-reconcile (mmi-runner)", cadence: "*/5 * * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-runner-online \u2192 /opt/mmi-control/runner-online-reconcile.sh; API-offline + _diag acquirejob, max 2 restarts/tick (verify: mmi-cli runtime box get mmi-runner --ssh)" },
|
|
21756
22430
|
{ 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)" },
|
|
21757
22431
|
{ 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)" }
|
|
21758
22432
|
];
|
|
@@ -21948,7 +22622,7 @@ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWork
|
|
|
21948
22622
|
}
|
|
21949
22623
|
|
|
21950
22624
|
// src/schedules-commands.ts
|
|
21951
|
-
var execFileP5 = (0, import_node_util7.promisify)(
|
|
22625
|
+
var execFileP5 = (0, import_node_util7.promisify)(import_node_child_process15.execFile);
|
|
21952
22626
|
var AWS_REGION = "eu-central-1";
|
|
21953
22627
|
var AWS_TIMEOUT_MS = 3e4;
|
|
21954
22628
|
var AWS_RETRY_DELAY_MS = 1500;
|
|
@@ -21982,8 +22656,8 @@ async function repoWorkflowEntries(client, repo) {
|
|
|
21982
22656
|
);
|
|
21983
22657
|
if (typeof contents?.content !== "string" || contents.encoding !== "base64") continue;
|
|
21984
22658
|
const text = Buffer.from(contents.content, "base64").toString("utf8");
|
|
21985
|
-
const
|
|
21986
|
-
const name = `${repo}/${
|
|
22659
|
+
const basename5 = wf.path.split("/").pop() ?? wf.path;
|
|
22660
|
+
const name = `${repo}/${basename5.replace(/\.ya?ml$/, "")}`;
|
|
21987
22661
|
workflows.push({ name, yamlText: text });
|
|
21988
22662
|
const entry = workflowEntry(repo, wf.path, text);
|
|
21989
22663
|
if (entry) entries.push(entry);
|
|
@@ -22114,7 +22788,7 @@ function reportDrift(drift) {
|
|
|
22114
22788
|
for (const d of drift) console.error(`org schedules: DRIFT \u2014 ${d}`);
|
|
22115
22789
|
}
|
|
22116
22790
|
function registerSchedulesCommands(program3) {
|
|
22117
|
-
const schedules = program3.command("schedules").description("the org schedules notebook \u2014 every armed cron, timer, and scheduled LLM lane, resolved live (jerv side lives in jerv-cli schedules)").option("--json", "machine-readable output (consumed by jerv-cli schedules --all)").option("--doc <path>", "
|
|
22791
|
+
const schedules = program3.command("schedules").description("the org schedules notebook \u2014 every armed cron, timer, and scheduled LLM lane, resolved live (jerv side lives in jerv-cli schedules)").option("--json", "machine-readable output (consumed by jerv-cli schedules --all)").option("--doc <path>", "optional snapshot only \u2014 splice inventory between schedules:inventory markers; live `org schedules` / --json remains SSOT (Hub#4120)").action(async (o) => {
|
|
22118
22792
|
try {
|
|
22119
22793
|
const { entries, incomplete, drift, reconciliation } = await fetchNotebook();
|
|
22120
22794
|
if (o.doc) {
|
|
@@ -22185,7 +22859,7 @@ function registerSchedulesCommands(program3) {
|
|
|
22185
22859
|
|
|
22186
22860
|
// src/file-lock.ts
|
|
22187
22861
|
var import_promises5 = require("node:fs/promises");
|
|
22188
|
-
var
|
|
22862
|
+
var import_node_path27 = require("node:path");
|
|
22189
22863
|
var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
22190
22864
|
var IMMEDIATE_RETRY_BUDGET = 3;
|
|
22191
22865
|
var FileLockBusyError = class extends Error {
|
|
@@ -22270,7 +22944,7 @@ async function releaseFileLock(lockPath, guard) {
|
|
|
22270
22944
|
}
|
|
22271
22945
|
async function withFileLock(lockPath, opts, fn) {
|
|
22272
22946
|
const resolved = resolveFileLockOpts(opts);
|
|
22273
|
-
await (0, import_promises5.mkdir)((0,
|
|
22947
|
+
await (0, import_promises5.mkdir)((0, import_node_path27.dirname)(lockPath), { recursive: true }).catch(() => void 0);
|
|
22274
22948
|
const guard = await acquireFileLock(lockPath, resolved, Date.now() + resolved.maxWaitMs);
|
|
22275
22949
|
try {
|
|
22276
22950
|
return await fn();
|
|
@@ -22281,7 +22955,7 @@ async function withFileLock(lockPath, opts, fn) {
|
|
|
22281
22955
|
|
|
22282
22956
|
// src/schedules-lift-command.ts
|
|
22283
22957
|
var import_promises6 = require("node:fs/promises");
|
|
22284
|
-
var
|
|
22958
|
+
var import_node_path28 = require("node:path");
|
|
22285
22959
|
|
|
22286
22960
|
// src/schedules-lift.ts
|
|
22287
22961
|
var SCHEDULE_HEADER_FIELDS = ["schedule", "what", "owner", "cadence", "executor", "llm", "output", "breaks", "kill"];
|
|
@@ -22303,8 +22977,8 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
|
|
|
22303
22977
|
const crons = extractWorkflowCrons(yamlText);
|
|
22304
22978
|
const header = parseScheduleHeader(yamlText);
|
|
22305
22979
|
if (!crons.length && !header.schedule) return null;
|
|
22306
|
-
const
|
|
22307
|
-
const expectedId = `${repo}/${
|
|
22980
|
+
const basename5 = workflowPath.split("/").pop() ?? workflowPath;
|
|
22981
|
+
const expectedId = `${repo}/${basename5.replace(/\.ya?ml$/, "")}`;
|
|
22308
22982
|
const missing = SCHEDULE_HEADER_FIELDS.filter((f) => !header[f]);
|
|
22309
22983
|
if (missing.length) {
|
|
22310
22984
|
throw new Error(`schedules register: ${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".`);
|
|
@@ -22387,7 +23061,7 @@ async function readWorkflowFiles(dir) {
|
|
|
22387
23061
|
const files = [];
|
|
22388
23062
|
for (const name of names.sort()) {
|
|
22389
23063
|
if (!/\.ya?ml$/.test(name)) continue;
|
|
22390
|
-
files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises6.readFile)((0,
|
|
23064
|
+
files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises6.readFile)((0, import_node_path28.join)(dir, name), "utf8") });
|
|
22391
23065
|
}
|
|
22392
23066
|
return files;
|
|
22393
23067
|
}
|
|
@@ -22929,9 +23603,9 @@ function registerQueryCommands(program3) {
|
|
|
22929
23603
|
}
|
|
22930
23604
|
|
|
22931
23605
|
// src/bootstrap-commands.ts
|
|
22932
|
-
var
|
|
22933
|
-
var
|
|
22934
|
-
var
|
|
23606
|
+
var import_node_fs30 = require("node:fs");
|
|
23607
|
+
var import_node_os11 = require("node:os");
|
|
23608
|
+
var import_node_path29 = require("node:path");
|
|
22935
23609
|
|
|
22936
23610
|
// src/bootstrap-drift.ts
|
|
22937
23611
|
function byteComparableSeeds(manifest, cls) {
|
|
@@ -23644,13 +24318,13 @@ function registerBootstrapCommands(program3) {
|
|
|
23644
24318
|
client: defaultGitHubClient(),
|
|
23645
24319
|
projectMeta: meta,
|
|
23646
24320
|
deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
|
|
23647
|
-
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0,
|
|
24321
|
+
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs30.existsSync)(path2) ? (0, import_node_fs30.readFileSync)(path2, "utf8") : null,
|
|
23648
24322
|
// requiredGcpApis is stored as an array by a JSON write, but `org project set --var KEY=VALUE` stores a raw
|
|
23649
24323
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
23650
24324
|
// #3689: the same committed map the org access audit reads (#3664), so a sanctioned admin is not a
|
|
23651
24325
|
// permanent bootstrap failure on one surface and an intended state on the other. Absent file → no
|
|
23652
24326
|
// sanction, which is the pre-#3664 behaviour.
|
|
23653
|
-
sanctionedAdmins: (0,
|
|
24327
|
+
sanctionedAdmins: (0, import_node_fs30.existsSync)("access-matrix.json") ? entriesValueByCanonicalRepo(loadSanctionedAdmins((0, import_node_fs30.readFileSync)("access-matrix.json", "utf8")), repo) : void 0,
|
|
23654
24328
|
requiredGcpApis: (() => {
|
|
23655
24329
|
const v = meta?.requiredGcpApis;
|
|
23656
24330
|
if (Array.isArray(v)) return v;
|
|
@@ -23703,12 +24377,12 @@ function registerBootstrapCommands(program3) {
|
|
|
23703
24377
|
bootstrap.command("drift").description("#3818: compare every org-owned whole-file seed against MMI-Hub's copy across the registry roster; read-only").option("--repo <owner/repo>", "audit one repo instead of the roster (never a fleet verdict)").option("--json", "machine-readable output").action(async () => {
|
|
23704
24378
|
const o = { repo: rawValue("--repo", ""), json: rawFlag("--json") };
|
|
23705
24379
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
23706
|
-
if (!(0,
|
|
23707
|
-
const manifest = loadBootstrapSeeds((0,
|
|
24380
|
+
if (!(0, import_node_fs30.existsSync)(manifestPath)) return fail(`bootstrap drift: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the reference this compares against`);
|
|
24381
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs30.readFileSync)(manifestPath, "utf8"));
|
|
23708
24382
|
const hubContents = /* @__PURE__ */ new Map();
|
|
23709
24383
|
for (const s of manifest.seeds) {
|
|
23710
24384
|
if (s.ownership !== "org" || s.source !== "self") continue;
|
|
23711
|
-
hubContents.set(s.target, (0,
|
|
24385
|
+
hubContents.set(s.target, (0, import_node_fs30.existsSync)(s.target) ? (0, import_node_fs30.readFileSync)(s.target, "utf8") : null);
|
|
23712
24386
|
}
|
|
23713
24387
|
let targets;
|
|
23714
24388
|
let classOf = (_repo) => "deployable";
|
|
@@ -23785,8 +24459,8 @@ function registerBootstrapCommands(program3) {
|
|
|
23785
24459
|
return fail(`bootstrap apply: ${e.message}`);
|
|
23786
24460
|
}
|
|
23787
24461
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
23788
|
-
if (!(0,
|
|
23789
|
-
const manifest = loadBootstrapSeeds((0,
|
|
24462
|
+
if (!(0, import_node_fs30.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`);
|
|
24463
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs30.readFileSync)(manifestPath, "utf8"));
|
|
23790
24464
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
23791
24465
|
const slug = parsedRepo.slug;
|
|
23792
24466
|
const onlyTarget = o.only.trim();
|
|
@@ -23797,16 +24471,16 @@ function registerBootstrapCommands(program3) {
|
|
|
23797
24471
|
${known}`);
|
|
23798
24472
|
}
|
|
23799
24473
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
23800
|
-
const readFile9 = (p) => (0,
|
|
24474
|
+
const readFile9 = (p) => (0, import_node_fs30.existsSync)(p) ? (0, import_node_fs30.readFileSync)(p, "utf8") : null;
|
|
23801
24475
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
23802
24476
|
const putSeed = async (target, content, ref, sha) => {
|
|
23803
|
-
const tmp = (0,
|
|
23804
|
-
(0,
|
|
24477
|
+
const tmp = (0, import_node_path29.join)((0, import_node_os11.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
|
|
24478
|
+
(0, import_node_fs30.writeFileSync)(tmp, JSON.stringify(contentPutBody(target, content, ref, sha)), "utf8");
|
|
23805
24479
|
try {
|
|
23806
24480
|
await gh(contentPutInputArgs(repo, target, tmp));
|
|
23807
24481
|
} finally {
|
|
23808
24482
|
try {
|
|
23809
|
-
(0,
|
|
24483
|
+
(0, import_node_fs30.unlinkSync)(tmp);
|
|
23810
24484
|
} catch {
|
|
23811
24485
|
}
|
|
23812
24486
|
}
|
|
@@ -24063,12 +24737,12 @@ LIVE apply to ${repo}:
|
|
|
24063
24737
|
}
|
|
24064
24738
|
|
|
24065
24739
|
// src/stage-commands.ts
|
|
24066
|
-
var
|
|
24067
|
-
var
|
|
24740
|
+
var import_node_fs32 = require("node:fs");
|
|
24741
|
+
var import_node_path31 = require("node:path");
|
|
24068
24742
|
|
|
24069
24743
|
// src/port-registry.ts
|
|
24070
|
-
var
|
|
24071
|
-
var
|
|
24744
|
+
var import_node_fs31 = require("node:fs");
|
|
24745
|
+
var import_node_path30 = require("node:path");
|
|
24072
24746
|
|
|
24073
24747
|
// ../infra/port-geometry.mjs
|
|
24074
24748
|
var PORT_BLOCK = 100;
|
|
@@ -24082,8 +24756,8 @@ function nextPortBlock(registry2) {
|
|
|
24082
24756
|
return [base, base + PORT_SPAN];
|
|
24083
24757
|
}
|
|
24084
24758
|
function loadPortRegistry(path2) {
|
|
24085
|
-
if (!(0,
|
|
24086
|
-
const raw = JSON.parse((0,
|
|
24759
|
+
if (!(0, import_node_fs31.existsSync)(path2)) return {};
|
|
24760
|
+
const raw = JSON.parse((0, import_node_fs31.readFileSync)(path2, "utf8"));
|
|
24087
24761
|
const out = {};
|
|
24088
24762
|
for (const [key, value] of Object.entries(raw)) {
|
|
24089
24763
|
if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
|
|
@@ -24097,9 +24771,9 @@ function ensurePortRange(repo, path2) {
|
|
|
24097
24771
|
const existing = registry2[repo];
|
|
24098
24772
|
if (existing) return existing;
|
|
24099
24773
|
const range = nextPortBlock(registry2);
|
|
24100
|
-
const raw = (0,
|
|
24774
|
+
const raw = (0, import_node_fs31.existsSync)(path2) ? JSON.parse((0, import_node_fs31.readFileSync)(path2, "utf8")) : {};
|
|
24101
24775
|
raw[repo] = range;
|
|
24102
|
-
(0,
|
|
24776
|
+
(0, import_node_fs31.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
|
|
24103
24777
|
return range;
|
|
24104
24778
|
}
|
|
24105
24779
|
function portCursorSeed(registry2) {
|
|
@@ -24121,22 +24795,22 @@ function existingPortRange(repo, registry2) {
|
|
|
24121
24795
|
return registry2[repo] ?? null;
|
|
24122
24796
|
}
|
|
24123
24797
|
function portRangeInfraAt(root, source) {
|
|
24124
|
-
const registryPath = (0,
|
|
24125
|
-
const ddbScriptPath = (0,
|
|
24126
|
-
if (!(0,
|
|
24798
|
+
const registryPath = (0, import_node_path30.join)(root, "infra", "port-ranges.json");
|
|
24799
|
+
const ddbScriptPath = (0, import_node_path30.join)(root, "infra", "port-ddb.mjs");
|
|
24800
|
+
if (!(0, import_node_fs31.existsSync)(registryPath) || !(0, import_node_fs31.existsSync)(ddbScriptPath)) return null;
|
|
24127
24801
|
return { root, source, registryPath, ddbScriptPath };
|
|
24128
24802
|
}
|
|
24129
24803
|
function resolvePortRangeInfra(cwd, packageDir) {
|
|
24130
24804
|
const direct = portRangeInfraAt(cwd, "cwd");
|
|
24131
24805
|
if (direct) return direct;
|
|
24132
|
-
for (let dir = cwd; ; dir = (0,
|
|
24133
|
-
const sibling = portRangeInfraAt((0,
|
|
24806
|
+
for (let dir = cwd; ; dir = (0, import_node_path30.dirname)(dir)) {
|
|
24807
|
+
const sibling = portRangeInfraAt((0, import_node_path30.join)(dir, "MMI-Hub"), "sibling-hub");
|
|
24134
24808
|
if (sibling) return sibling;
|
|
24135
|
-
const parent = (0,
|
|
24809
|
+
const parent = (0, import_node_path30.dirname)(dir);
|
|
24136
24810
|
if (parent === dir) break;
|
|
24137
24811
|
}
|
|
24138
24812
|
if (packageDir) {
|
|
24139
|
-
const pkgRoot = (0,
|
|
24813
|
+
const pkgRoot = (0, import_node_path30.join)(packageDir, "..", "..");
|
|
24140
24814
|
const pkgFrom = portRangeInfraAt(pkgRoot, "pkg-root");
|
|
24141
24815
|
if (pkgFrom) return pkgFrom;
|
|
24142
24816
|
}
|
|
@@ -24330,8 +25004,8 @@ function registerStageCommands(program3) {
|
|
|
24330
25004
|
const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
|
|
24331
25005
|
return decideStage({
|
|
24332
25006
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
24333
|
-
hasCompose: (0,
|
|
24334
|
-
hasEnvExample: (0,
|
|
25007
|
+
hasCompose: (0, import_node_fs32.existsSync)((0, import_node_path31.join)(process.cwd(), "docker-compose.yml")),
|
|
25008
|
+
hasEnvExample: (0, import_node_fs32.existsSync)((0, import_node_path31.join)(process.cwd(), ".env.example"))
|
|
24335
25009
|
});
|
|
24336
25010
|
}
|
|
24337
25011
|
async function fetchStageVaultEnvMerge() {
|
|
@@ -24769,11 +25443,11 @@ function registerBoardCommands(program3) {
|
|
|
24769
25443
|
}
|
|
24770
25444
|
|
|
24771
25445
|
// src/merge-cleanup.ts
|
|
24772
|
-
var
|
|
25446
|
+
var import_node_fs33 = require("node:fs");
|
|
24773
25447
|
var import_promises8 = require("node:fs/promises");
|
|
24774
|
-
var
|
|
24775
|
-
var
|
|
24776
|
-
var
|
|
25448
|
+
var import_node_path33 = require("node:path");
|
|
25449
|
+
var import_node_os12 = require("node:os");
|
|
25450
|
+
var import_node_child_process16 = require("node:child_process");
|
|
24777
25451
|
|
|
24778
25452
|
// src/board-advance.ts
|
|
24779
25453
|
function repoOf2(ref) {
|
|
@@ -24859,7 +25533,7 @@ function boardAdvanceFailureMessage(result) {
|
|
|
24859
25533
|
|
|
24860
25534
|
// src/deferred-registry-store.ts
|
|
24861
25535
|
var import_promises7 = require("node:fs/promises");
|
|
24862
|
-
var
|
|
25536
|
+
var import_node_path32 = require("node:path");
|
|
24863
25537
|
var sleep2 = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
24864
25538
|
async function atomicWrite(target, contents) {
|
|
24865
25539
|
const tmp = `${target}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
@@ -24910,12 +25584,12 @@ function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
|
|
|
24910
25584
|
},
|
|
24911
25585
|
// Standalone atomic write — THROWS on failure (no best-effort swallow, #2846).
|
|
24912
25586
|
write: async (entries) => {
|
|
24913
|
-
await (0, import_promises7.mkdir)((0,
|
|
25587
|
+
await (0, import_promises7.mkdir)((0, import_node_path32.dirname)(registryPath), { recursive: true });
|
|
24914
25588
|
await atomicWrite(registryPath, serializeDeferredWorktrees(entries));
|
|
24915
25589
|
},
|
|
24916
25590
|
// Serialized read-modify-write under the repo-wide lock (#2846).
|
|
24917
25591
|
update: async (mutate) => {
|
|
24918
|
-
await (0, import_promises7.mkdir)((0,
|
|
25592
|
+
await (0, import_promises7.mkdir)((0, import_node_path32.dirname)(registryPath), { recursive: true });
|
|
24919
25593
|
const deadline = Date.now() + opts.maxWaitMs;
|
|
24920
25594
|
for (; ; ) {
|
|
24921
25595
|
const guard = await acquireLock(lockPath, opts, deadline);
|
|
@@ -25063,7 +25737,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
|
|
|
25063
25737
|
);
|
|
25064
25738
|
const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
25065
25739
|
const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
|
|
25066
|
-
const primaryRepoRoot = worktreeGitRoot ? (0,
|
|
25740
|
+
const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path33.dirname)((0, import_node_path33.dirname)(worktreeGitRoot)) : repoRoot2;
|
|
25067
25741
|
const gcActor = describeActor({ env: process.env, surface: detectSurface(process.env), cwd: process.cwd() });
|
|
25068
25742
|
const owners = readWorktreeOwners(primaryRepoRoot);
|
|
25069
25743
|
const removalNow = Date.now();
|
|
@@ -25094,7 +25768,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
|
|
|
25094
25768
|
const cleanup = await cleanupPrMergeLocalBranch(branch.branch, {
|
|
25095
25769
|
beforeWorktrees,
|
|
25096
25770
|
startingPath: branch.worktreePath,
|
|
25097
|
-
pathExists: (p) => (0,
|
|
25771
|
+
pathExists: (p) => (0, import_node_fs33.existsSync)(p),
|
|
25098
25772
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
25099
25773
|
teardownWorktreeStage,
|
|
25100
25774
|
deferredStore,
|
|
@@ -25122,7 +25796,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
|
|
|
25122
25796
|
let removalAttempted = false;
|
|
25123
25797
|
try {
|
|
25124
25798
|
const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
|
|
25125
|
-
realpath: (path2) => (0,
|
|
25799
|
+
realpath: (path2) => (0, import_node_fs33.realpathSync)(path2)
|
|
25126
25800
|
});
|
|
25127
25801
|
if (!cleanupTarget.ok) {
|
|
25128
25802
|
result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
|
|
@@ -25208,13 +25882,13 @@ async function composeOverrideBodyFile(prNumber, repoArgs, gh) {
|
|
|
25208
25882
|
const commits = JSON.parse(raw).commits ?? [];
|
|
25209
25883
|
const body = squashBodyWithOverride(commits.map((c) => ({ headline: c.messageHeadline ?? "", body: c.messageBody ?? "" })), process.cwd());
|
|
25210
25884
|
if (!body) return void 0;
|
|
25211
|
-
const dir = (0,
|
|
25212
|
-
const path2 = (0,
|
|
25213
|
-
(0,
|
|
25885
|
+
const dir = (0, import_node_fs33.mkdtempSync)((0, import_node_path33.join)((0, import_node_os12.tmpdir)(), "mmi-squash-body-"));
|
|
25886
|
+
const path2 = (0, import_node_path33.join)(dir, "body.txt");
|
|
25887
|
+
(0, import_node_fs33.writeFileSync)(path2, `${body}
|
|
25214
25888
|
`, "utf8");
|
|
25215
25889
|
return { path: path2, cleanup: () => {
|
|
25216
25890
|
try {
|
|
25217
|
-
(0,
|
|
25891
|
+
(0, import_node_fs33.rmSync)(dir, { recursive: true, force: true });
|
|
25218
25892
|
} catch {
|
|
25219
25893
|
}
|
|
25220
25894
|
} };
|
|
@@ -25322,7 +25996,7 @@ async function remoteBranchExists2(branch, options = {}) {
|
|
|
25322
25996
|
}
|
|
25323
25997
|
var COMPOSE_TIMEOUT_MS = 12e4;
|
|
25324
25998
|
function spawnDeferredGcSweep() {
|
|
25325
|
-
spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn:
|
|
25999
|
+
spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process16.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
25326
26000
|
}
|
|
25327
26001
|
async function createDeferredWorktreeStore() {
|
|
25328
26002
|
try {
|
|
@@ -25336,13 +26010,13 @@ var realWorktreeDirRemover = {
|
|
|
25336
26010
|
probe: (p) => {
|
|
25337
26011
|
let st;
|
|
25338
26012
|
try {
|
|
25339
|
-
st = (0,
|
|
26013
|
+
st = (0, import_node_fs33.lstatSync)(p);
|
|
25340
26014
|
} catch {
|
|
25341
26015
|
return null;
|
|
25342
26016
|
}
|
|
25343
26017
|
if (st.isSymbolicLink()) return "link";
|
|
25344
26018
|
try {
|
|
25345
|
-
(0,
|
|
26019
|
+
(0, import_node_fs33.readlinkSync)(p);
|
|
25346
26020
|
return "link";
|
|
25347
26021
|
} catch {
|
|
25348
26022
|
}
|
|
@@ -25350,7 +26024,7 @@ var realWorktreeDirRemover = {
|
|
|
25350
26024
|
},
|
|
25351
26025
|
readdir: (p) => {
|
|
25352
26026
|
try {
|
|
25353
|
-
return (0,
|
|
26027
|
+
return (0, import_node_fs33.readdirSync)(p);
|
|
25354
26028
|
} catch {
|
|
25355
26029
|
return [];
|
|
25356
26030
|
}
|
|
@@ -25359,9 +26033,9 @@ var realWorktreeDirRemover = {
|
|
|
25359
26033
|
// leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
|
|
25360
26034
|
detachLink: (p) => {
|
|
25361
26035
|
try {
|
|
25362
|
-
(0,
|
|
26036
|
+
(0, import_node_fs33.rmdirSync)(p);
|
|
25363
26037
|
} catch {
|
|
25364
|
-
(0,
|
|
26038
|
+
(0, import_node_fs33.unlinkSync)(p);
|
|
25365
26039
|
}
|
|
25366
26040
|
},
|
|
25367
26041
|
removeTree: (p) => (0, import_promises8.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
|
@@ -25394,9 +26068,9 @@ async function worktreeHasStageState(worktreePath) {
|
|
|
25394
26068
|
}
|
|
25395
26069
|
}
|
|
25396
26070
|
function stageStateFileBelongsToWorktree(statePath, worktreePath) {
|
|
25397
|
-
if (!(0,
|
|
26071
|
+
if (!(0, import_node_fs33.existsSync)(statePath)) return false;
|
|
25398
26072
|
try {
|
|
25399
|
-
const state = JSON.parse((0,
|
|
26073
|
+
const state = JSON.parse((0, import_node_fs33.readFileSync)(statePath, "utf8"));
|
|
25400
26074
|
const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
|
|
25401
26075
|
return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
|
|
25402
26076
|
} catch {
|
|
@@ -25556,24 +26230,48 @@ var RUNNER_INFRA_ANNOTATION_TITLES = /* @__PURE__ */ new Set([
|
|
|
25556
26230
|
CI_DISK_ANNOTATION_TITLE,
|
|
25557
26231
|
CI_TOOLCHAIN_ANNOTATION_TITLE
|
|
25558
26232
|
]);
|
|
26233
|
+
var RUNNER_INFRA_ANNOTATION_MESSAGE_PATTERNS = [
|
|
26234
|
+
/not acquired by Runner/i,
|
|
26235
|
+
/Service Unavailable/i,
|
|
26236
|
+
/Failed to resolve action download info/i
|
|
26237
|
+
];
|
|
26238
|
+
var RUNNER_INFRA_PRESTART_CONCLUSIONS = /* @__PURE__ */ new Set([
|
|
26239
|
+
"cancelled",
|
|
26240
|
+
"startup_failure"
|
|
26241
|
+
]);
|
|
25559
26242
|
function isErrorAnnotation(a) {
|
|
25560
26243
|
const level = a.annotation_level?.toLowerCase();
|
|
25561
26244
|
return level === "failure" || level === "error";
|
|
25562
26245
|
}
|
|
26246
|
+
function isRunnerInfraAnnotation(a) {
|
|
26247
|
+
const title = a.title?.trim() ?? "";
|
|
26248
|
+
if (RUNNER_INFRA_ANNOTATION_TITLES.has(title)) return true;
|
|
26249
|
+
const text = `${title}
|
|
26250
|
+
${a.message?.trim() ?? ""}`;
|
|
26251
|
+
return RUNNER_INFRA_ANNOTATION_MESSAGE_PATTERNS.some((re) => re.test(text));
|
|
26252
|
+
}
|
|
25563
26253
|
function classifyFailedChecks(failing) {
|
|
25564
26254
|
const infraFailures = [];
|
|
25565
26255
|
const otherFailures = [];
|
|
25566
26256
|
for (const check of failing) {
|
|
25567
26257
|
const errors = check.annotations.filter(isErrorAnnotation);
|
|
25568
|
-
|
|
25569
|
-
|
|
26258
|
+
if (errors.length > 0) {
|
|
26259
|
+
const allInfra = errors.every((a) => isRunnerInfraAnnotation(a));
|
|
26260
|
+
(allInfra ? infraFailures : otherFailures).push(check.name);
|
|
26261
|
+
continue;
|
|
26262
|
+
}
|
|
26263
|
+
if (RUNNER_INFRA_PRESTART_CONCLUSIONS.has(check.conclusion?.trim() ?? "")) {
|
|
26264
|
+
infraFailures.push(check.name);
|
|
26265
|
+
continue;
|
|
26266
|
+
}
|
|
26267
|
+
otherFailures.push(check.name);
|
|
25570
26268
|
}
|
|
25571
26269
|
if (infraFailures.length && !otherFailures.length) {
|
|
25572
26270
|
return {
|
|
25573
26271
|
cause: "runner-infra",
|
|
25574
26272
|
infraFailures,
|
|
25575
26273
|
otherFailures,
|
|
25576
|
-
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,
|
|
26274
|
+
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, was missing a toolchain, could not acquire an mmi-live lane, or Actions returned Service Unavailable before checkout (the check annotation / conclusion names which). Re-run once the runner drains/heals (gh run rerun --failed); do not debug the diff.`
|
|
25577
26275
|
};
|
|
25578
26276
|
}
|
|
25579
26277
|
return {
|
|
@@ -25601,6 +26299,7 @@ async function diagnoseFailedRestChecks(prNumber, repo, gh = defaultGhApi) {
|
|
|
25601
26299
|
if (!failing.length) return null;
|
|
25602
26300
|
const annotated = await Promise.all(failing.map(async (run) => ({
|
|
25603
26301
|
name: run.name ?? `check-run ${run.id}`,
|
|
26302
|
+
conclusion: run.conclusion ?? null,
|
|
25604
26303
|
annotations: JSON.parse(await gh([`repos/${repo}/check-runs/${run.id}/annotations`]))
|
|
25605
26304
|
})));
|
|
25606
26305
|
return classifyFailedChecks(annotated);
|
|
@@ -25702,9 +26401,9 @@ async function checkDocsIndexAtHead(opts, deps) {
|
|
|
25702
26401
|
}
|
|
25703
26402
|
|
|
25704
26403
|
// src/worktree-lifecycle-commands.ts
|
|
25705
|
-
var
|
|
26404
|
+
var import_node_fs34 = require("node:fs");
|
|
25706
26405
|
var import_promises9 = require("node:fs/promises");
|
|
25707
|
-
var
|
|
26406
|
+
var import_node_path34 = require("node:path");
|
|
25708
26407
|
var GH_TIMEOUT_MS = 2e4;
|
|
25709
26408
|
var DEFAULT_BASE = "origin/development";
|
|
25710
26409
|
var DEFAULT_REMOTE = "origin";
|
|
@@ -25850,7 +26549,7 @@ function classifyStaleLeaks(input) {
|
|
|
25850
26549
|
var defaultOrphanDirScanDeps = {
|
|
25851
26550
|
listDirs: (root) => {
|
|
25852
26551
|
try {
|
|
25853
|
-
return (0,
|
|
26552
|
+
return (0, import_node_fs34.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path34.join)(root, e.name));
|
|
25854
26553
|
} catch {
|
|
25855
26554
|
return [];
|
|
25856
26555
|
}
|
|
@@ -26003,13 +26702,13 @@ function registerWorktreeCommands(program3) {
|
|
|
26003
26702
|
const detached = headBorn && !symbolicBranch;
|
|
26004
26703
|
const branch = symbolicBranch || (detached ? "HEAD" : "");
|
|
26005
26704
|
if (!wtPath || !branch) return fail("worktree land: not inside a git worktree");
|
|
26006
|
-
const gitFile = (0,
|
|
26007
|
-
const isLinked = (0,
|
|
26705
|
+
const gitFile = (0, import_node_path34.join)(wtPath, ".git");
|
|
26706
|
+
const isLinked = (0, import_node_fs34.existsSync)(gitFile) && (0, import_node_fs34.statSync)(gitFile).isFile();
|
|
26008
26707
|
if (apply && !isLinked) {
|
|
26009
26708
|
return fail("worktree land: run from inside the linked worktree you want to land (this is the primary checkout)");
|
|
26010
26709
|
}
|
|
26011
26710
|
const commonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
26012
|
-
const primaryCheckout = commonDir ? (0,
|
|
26711
|
+
const primaryCheckout = commonDir ? (0, import_node_path34.dirname)(commonDir) : wtPath;
|
|
26013
26712
|
const localBranchNames = await execFileP2("git", ["-C", primaryCheckout, "for-each-ref", "--format=%(refname:short)", "refs/heads"], { timeout: GIT_TIMEOUT_MS }).then(({ stdout }) => new Set((stdout || "").split("\n").map((l) => l.trim()).filter(Boolean))).catch(() => void 0);
|
|
26014
26713
|
const orphan = classifyOrphanedWorktree({
|
|
26015
26714
|
branch,
|
|
@@ -26220,10 +26919,10 @@ async function gatherWorktreeContext() {
|
|
|
26220
26919
|
if (s) stages.push({ path: wt.path, port: s.port });
|
|
26221
26920
|
}
|
|
26222
26921
|
const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
|
|
26223
|
-
const primaryRepoRoot = worktreeGitRoot ? (0,
|
|
26922
|
+
const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path34.dirname)((0, import_node_path34.dirname)(worktreeGitRoot)) : repoRoot2;
|
|
26224
26923
|
const wtRoot = siblingMmiWorktreesRoot(primaryRepoRoot);
|
|
26225
26924
|
let orphanDirs = [];
|
|
26226
|
-
if ((0,
|
|
26925
|
+
if ((0, import_node_fs34.existsSync)(wtRoot)) {
|
|
26227
26926
|
orphanDirs = scanOrphanDirs(wtRoot, worktreeGitRoot, {
|
|
26228
26927
|
...defaultOrphanDirScanDeps,
|
|
26229
26928
|
listDirs: (root) => worktreeScanDirs(root, primaryRepoRoot, defaultOrphanDirScanDeps.listDirs, isRepoCheckoutDir)
|
|
@@ -26249,8 +26948,8 @@ ${err.stderr ?? ""}`;
|
|
|
26249
26948
|
}
|
|
26250
26949
|
|
|
26251
26950
|
// src/issue-commands.ts
|
|
26252
|
-
var
|
|
26253
|
-
var
|
|
26951
|
+
var import_node_fs35 = require("node:fs");
|
|
26952
|
+
var import_node_crypto6 = require("node:crypto");
|
|
26254
26953
|
var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
|
|
26255
26954
|
var ReparentConflictError = class extends Error {
|
|
26256
26955
|
constructor(message, payload) {
|
|
@@ -26267,7 +26966,7 @@ async function editIssue(client, options, deps = {}) {
|
|
|
26267
26966
|
const url = `https://github.com/${repo}/issues/${parsed.number}`;
|
|
26268
26967
|
const patch = {};
|
|
26269
26968
|
let bodyChanged = false;
|
|
26270
|
-
const textDeps = () => deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0,
|
|
26969
|
+
const textDeps = () => deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs35.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
|
|
26271
26970
|
if (options.titleFile !== void 0) {
|
|
26272
26971
|
patch.title = await resolveIssueTitle({ title: options.title, titleFile: options.titleFile }, textDeps());
|
|
26273
26972
|
} else if (options.title !== void 0) {
|
|
@@ -26500,7 +27199,7 @@ function rowIdempotencyKey(batchKey, spec) {
|
|
|
26500
27199
|
const identity = `${spec.type}
|
|
26501
27200
|
${spec.title.trim()}
|
|
26502
27201
|
${spec.body ?? ""}`;
|
|
26503
|
-
const hash = (0,
|
|
27202
|
+
const hash = (0, import_node_crypto6.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
|
|
26504
27203
|
return `${batchKey}:${hash}`;
|
|
26505
27204
|
}
|
|
26506
27205
|
var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
|
|
@@ -26830,7 +27529,7 @@ function extendCreateCommand(issue2, batchAttach) {
|
|
|
26830
27529
|
if (opts.batch) {
|
|
26831
27530
|
let specs;
|
|
26832
27531
|
try {
|
|
26833
|
-
const raw = (0,
|
|
27532
|
+
const raw = (0, import_node_fs35.readFileSync)(opts.batch, "utf8");
|
|
26834
27533
|
specs = JSON.parse(raw);
|
|
26835
27534
|
if (!Array.isArray(specs)) throw new Error("batch file must contain a JSON array");
|
|
26836
27535
|
} catch (e) {
|
|
@@ -26904,8 +27603,8 @@ ${lines}`, {
|
|
|
26904
27603
|
}
|
|
26905
27604
|
|
|
26906
27605
|
// src/train-commands.ts
|
|
26907
|
-
var
|
|
26908
|
-
var
|
|
27606
|
+
var import_node_fs36 = require("node:fs");
|
|
27607
|
+
var import_node_path35 = require("node:path");
|
|
26909
27608
|
|
|
26910
27609
|
// src/train-status.ts
|
|
26911
27610
|
function buildTrainStatusReport(input) {
|
|
@@ -26945,7 +27644,7 @@ function formatTrainStatus(r) {
|
|
|
26945
27644
|
// src/train-commands.ts
|
|
26946
27645
|
function readRepoVersion() {
|
|
26947
27646
|
try {
|
|
26948
|
-
return JSON.parse((0,
|
|
27647
|
+
return JSON.parse((0, import_node_fs36.readFileSync)((0, import_node_path35.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
26949
27648
|
} catch {
|
|
26950
27649
|
return void 0;
|
|
26951
27650
|
}
|
|
@@ -27091,9 +27790,9 @@ function registerDeployCommands(program3) {
|
|
|
27091
27790
|
}
|
|
27092
27791
|
|
|
27093
27792
|
// src/discovery-commands.ts
|
|
27094
|
-
var
|
|
27095
|
-
var
|
|
27096
|
-
var
|
|
27793
|
+
var import_node_fs37 = require("node:fs");
|
|
27794
|
+
var import_node_os13 = require("node:os");
|
|
27795
|
+
var import_node_path36 = require("node:path");
|
|
27097
27796
|
var GC_GH_TIMEOUT_MS3 = 2e4;
|
|
27098
27797
|
async function collectStatus() {
|
|
27099
27798
|
const repo = await resolveRepo();
|
|
@@ -27179,6 +27878,11 @@ async function recommendNext(repo, deps) {
|
|
|
27179
27878
|
const top = sorted[0];
|
|
27180
27879
|
return { number: top.number, title: top.title, url: top.url, priority: top.priority, repo: top.repository };
|
|
27181
27880
|
}
|
|
27881
|
+
function requiredEstateCommandsPresent(commandNames) {
|
|
27882
|
+
const required = ["repo-index"];
|
|
27883
|
+
const present = new Set(commandNames);
|
|
27884
|
+
return required.filter((name) => !present.has(name));
|
|
27885
|
+
}
|
|
27182
27886
|
function onboardPluginGate(deps) {
|
|
27183
27887
|
const { registered, declared, ref } = readKnownMarketplace(deps.readKnown(), MMI_MARKETPLACE_NAME);
|
|
27184
27888
|
if (!registered) {
|
|
@@ -27196,8 +27900,13 @@ function onboardPluginGate(deps) {
|
|
|
27196
27900
|
if (catalog && !catalog.agrees) gaps.push(`the catalog is ${catalog.unpinned ? "unpinned" : `pinned to ${catalog.ref}`} \u2014 ${CATALOG_REF_PIN_STEPS}`);
|
|
27197
27901
|
return gaps.length === 0 ? { ok: true, detail: `auto-update on, catalog pinned to ${catalog?.ref}` } : { ok: false, detail: gaps.join("; ") };
|
|
27198
27902
|
}
|
|
27199
|
-
async function collectOnboardStatus() {
|
|
27903
|
+
async function collectOnboardStatus(opts = {}) {
|
|
27200
27904
|
const cfg = await loadConfig();
|
|
27905
|
+
const missingEstate = requiredEstateCommandsPresent(opts.commandNames ?? []);
|
|
27906
|
+
const estateCli = missingEstate.length === 0 ? { ok: true, detail: "estate commands present (repo-index)" } : {
|
|
27907
|
+
ok: false,
|
|
27908
|
+
detail: `global CLI missing ${missingEstate.join(", ")} \u2014 run mmi-cli doctor to heal`
|
|
27909
|
+
};
|
|
27201
27910
|
let track = "unknown";
|
|
27202
27911
|
try {
|
|
27203
27912
|
const revParse2 = (await execFileP2("git", ["rev-parse", "--abbrev-ref", "HEAD"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
@@ -27251,7 +27960,9 @@ async function collectOnboardStatus() {
|
|
|
27251
27960
|
}
|
|
27252
27961
|
}
|
|
27253
27962
|
let nextCommand = "mmi-cli doctor";
|
|
27254
|
-
if (!
|
|
27963
|
+
if (!estateCli.ok) {
|
|
27964
|
+
nextCommand = "mmi-cli doctor \u2014 heal global CLI (missing repo-index)";
|
|
27965
|
+
} else if (!cfg.sagaApiUrl) {
|
|
27255
27966
|
nextCommand = "mmi-cli doctor \u2014 fix Hub API URL configuration first";
|
|
27256
27967
|
} else if (!registry2.ok) {
|
|
27257
27968
|
nextCommand = "mmi-cli org project set <owner/repo>";
|
|
@@ -27269,12 +27980,12 @@ async function collectOnboardStatus() {
|
|
|
27269
27980
|
else if (top) nextCommand = `mmi-cli board claim ${top.number} # ${top.title}`;
|
|
27270
27981
|
else nextCommand = "mmi-cli board read \u2014 no claimable items found";
|
|
27271
27982
|
}
|
|
27272
|
-
const home = (0,
|
|
27983
|
+
const home = (0, import_node_os13.homedir)();
|
|
27273
27984
|
const plugin = onboardPluginGate({
|
|
27274
|
-
readKnown: () => readFileSyncSafe((0,
|
|
27275
|
-
readSettings: () => readFileSyncSafe((0,
|
|
27985
|
+
readKnown: () => readFileSyncSafe((0, import_node_path36.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs37.readFileSync),
|
|
27986
|
+
readSettings: () => readFileSyncSafe((0, import_node_path36.join)(home, ".claude", "settings.json"), import_node_fs37.readFileSync)
|
|
27276
27987
|
});
|
|
27277
|
-
return { track, board, registry: registry2, secrets, plugin, nextCommand };
|
|
27988
|
+
return { track, board, registry: registry2, secrets, plugin, estateCli, nextCommand };
|
|
27278
27989
|
}
|
|
27279
27990
|
function registerDiscoveryCommands(program3) {
|
|
27280
27991
|
program3.command("status").description("unified repo snapshot: branch, worktrees, open PRs (yours), claimed board items, stage state").option("--json", "machine-readable output").action(async (o) => {
|
|
@@ -27311,7 +28022,8 @@ function registerDiscoveryCommands(program3) {
|
|
|
27311
28022
|
});
|
|
27312
28023
|
program3.command("onboard").description("report repo track/board/secrets/registry readiness and the concrete next command").option("--json", "machine-readable output").action(async (o) => {
|
|
27313
28024
|
try {
|
|
27314
|
-
const
|
|
28025
|
+
const commandNames = program3.commands.map((c) => c.name());
|
|
28026
|
+
const report = await collectOnboardStatus({ commandNames });
|
|
27315
28027
|
if (o.json) {
|
|
27316
28028
|
console.log(JSON.stringify(report, null, 2));
|
|
27317
28029
|
} else {
|
|
@@ -27320,6 +28032,7 @@ function registerDiscoveryCommands(program3) {
|
|
|
27320
28032
|
console.log(`Registry: ${report.registry.ok ? "\u2713" : "\u2717"} ${report.registry.detail}`);
|
|
27321
28033
|
console.log(`Secrets: ${report.secrets.ok ? "\u2713" : "\u2717"} ${report.secrets.detail}`);
|
|
27322
28034
|
console.log(`Plugin: ${report.plugin.ok ? "\u2713" : "\u2717"} ${report.plugin.detail}`);
|
|
28035
|
+
console.log(`Estate CLI: ${report.estateCli.ok ? "\u2713" : "\u2717"} ${report.estateCli.detail}`);
|
|
27323
28036
|
console.log(`
|
|
27324
28037
|
Next command: ${report.nextCommand}`);
|
|
27325
28038
|
}
|
|
@@ -27335,6 +28048,7 @@ var LOOP_PLAYBOOKS = {
|
|
|
27335
28048
|
title: "Agent",
|
|
27336
28049
|
steps: [
|
|
27337
28050
|
{ label: "Orient in the current repository", command: "mmi-cli onboard" },
|
|
28051
|
+
{ label: "Structure search via Hub cloud (paths/symbols/meaning) before grepping docs/", command: "mmi-cli repo-index search <q>" },
|
|
27338
28052
|
{ label: "Read the next board item", command: "mmi-cli board read" },
|
|
27339
28053
|
{ label: "Claim it and create an isolated worktree", command: "mmi-cli worktree create <issue-number> --claim --from origin/development" },
|
|
27340
28054
|
{ label: "Build and test in the touched package", command: "npm test && npm run build" },
|
|
@@ -29215,14 +29929,110 @@ function checkDocsIndex(probe) {
|
|
|
29215
29929
|
if (!probe.drift) {
|
|
29216
29930
|
return { ok: true, id: "docs-index", label: "docs index", detail: "current", verbose: evidence };
|
|
29217
29931
|
}
|
|
29218
|
-
return {
|
|
29219
|
-
ok: false,
|
|
29220
|
-
id: "docs-index",
|
|
29221
|
-
label: "docs index",
|
|
29222
|
-
detail: "stale \u2014 docs/index.md no longer matches the docs/ tree",
|
|
29223
|
-
fix: "run `mmi-cli docs index --write` and commit docs/index.md",
|
|
29224
|
-
verbose: evidence
|
|
29225
|
-
};
|
|
29932
|
+
return {
|
|
29933
|
+
ok: false,
|
|
29934
|
+
id: "docs-index",
|
|
29935
|
+
label: "docs index",
|
|
29936
|
+
detail: "stale \u2014 docs/index.md no longer matches the docs/ tree",
|
|
29937
|
+
fix: "run `mmi-cli docs index --write` and commit docs/index.md",
|
|
29938
|
+
verbose: evidence
|
|
29939
|
+
};
|
|
29940
|
+
}
|
|
29941
|
+
function checkRepoIndexCloud(probe) {
|
|
29942
|
+
if (!probe) return null;
|
|
29943
|
+
const evidence = [
|
|
29944
|
+
probe.repo ? `repo: ${probe.repo}` : "repo: (estate)",
|
|
29945
|
+
probe.builtAt ? `builtAt: ${probe.builtAt}` : "builtAt: n/a",
|
|
29946
|
+
typeof probe.fileCount === "number" ? `fileCount: ${probe.fileCount}` : "fileCount: n/a",
|
|
29947
|
+
typeof probe.embCount === "number" ? `embCount: ${probe.embCount}` : "embCount: n/a",
|
|
29948
|
+
typeof probe.localPresent === "boolean" ? `local cache: ${probe.localPresent ? "present" : "absent"}` : "local cache: n/a"
|
|
29949
|
+
];
|
|
29950
|
+
switch (probe.kind) {
|
|
29951
|
+
case "healthy":
|
|
29952
|
+
return {
|
|
29953
|
+
ok: true,
|
|
29954
|
+
id: "repo-index",
|
|
29955
|
+
label: "repo-index",
|
|
29956
|
+
detail: probe.detail ?? `cloud ok${typeof probe.fileCount === "number" ? ` \u2014 ${probe.fileCount} files` : ""}`,
|
|
29957
|
+
verbose: evidence
|
|
29958
|
+
};
|
|
29959
|
+
case "command-absent":
|
|
29960
|
+
return {
|
|
29961
|
+
ok: false,
|
|
29962
|
+
id: "repo-index",
|
|
29963
|
+
label: "repo-index",
|
|
29964
|
+
detail: "this mmi-cli build has no repo-index command",
|
|
29965
|
+
fix: "run `mmi-cli doctor` to heal the global CLI, or `npm install -g @mutmutco/cli`",
|
|
29966
|
+
verbose: evidence
|
|
29967
|
+
};
|
|
29968
|
+
case "deploy-lag":
|
|
29969
|
+
return {
|
|
29970
|
+
ok: false,
|
|
29971
|
+
id: "repo-index",
|
|
29972
|
+
label: "repo-index",
|
|
29973
|
+
detail: "Hub /repo-index/* not deployed yet (HTTP 404)",
|
|
29974
|
+
fix: "ship a Hub release carrying estate index routes, then run repo-index-reconcile",
|
|
29975
|
+
reportOnly: true,
|
|
29976
|
+
verbose: evidence
|
|
29977
|
+
};
|
|
29978
|
+
case "missing-projection":
|
|
29979
|
+
return {
|
|
29980
|
+
ok: false,
|
|
29981
|
+
id: "repo-index",
|
|
29982
|
+
label: "repo-index",
|
|
29983
|
+
detail: probe.detail ?? "no cloud projection for this repo",
|
|
29984
|
+
fix: "run harbour repo-index-reconcile or `mmi-cli repo-index sync-estate --repo <owner/name>`",
|
|
29985
|
+
verbose: evidence
|
|
29986
|
+
};
|
|
29987
|
+
case "stale":
|
|
29988
|
+
return {
|
|
29989
|
+
ok: false,
|
|
29990
|
+
id: "repo-index",
|
|
29991
|
+
label: "repo-index",
|
|
29992
|
+
detail: probe.detail ?? "cloud projection looks stale vs recent pushes",
|
|
29993
|
+
fix: "trigger `repo-index-reconcile` (workflow_dispatch) or wait for the 6h harbour tick",
|
|
29994
|
+
verbose: evidence
|
|
29995
|
+
};
|
|
29996
|
+
case "auth":
|
|
29997
|
+
return {
|
|
29998
|
+
ok: false,
|
|
29999
|
+
id: "repo-index",
|
|
30000
|
+
label: "repo-index",
|
|
30001
|
+
detail: "Hub session missing or unauthorized for repo-index",
|
|
30002
|
+
fix: "run `gh auth login` and ensure a Hub session exists (`mmi-cli whoami`)",
|
|
30003
|
+
verbose: evidence
|
|
30004
|
+
};
|
|
30005
|
+
case "config":
|
|
30006
|
+
return {
|
|
30007
|
+
ok: false,
|
|
30008
|
+
id: "repo-index",
|
|
30009
|
+
label: "repo-index",
|
|
30010
|
+
detail: probe.detail ?? "Hub API URL not configured",
|
|
30011
|
+
fix: "run `mmi-cli doctor` to repair Hub wiring",
|
|
30012
|
+
verbose: evidence
|
|
30013
|
+
};
|
|
30014
|
+
case "network":
|
|
30015
|
+
return {
|
|
30016
|
+
ok: false,
|
|
30017
|
+
id: "repo-index",
|
|
30018
|
+
label: "repo-index",
|
|
30019
|
+
detail: probe.detail ?? "could not reach Hub repo-index",
|
|
30020
|
+
fix: "retry when the network is up; structure search stays fail-soft",
|
|
30021
|
+
reportOnly: true,
|
|
30022
|
+
verbose: evidence
|
|
30023
|
+
};
|
|
30024
|
+
case "local-advisory":
|
|
30025
|
+
return {
|
|
30026
|
+
ok: true,
|
|
30027
|
+
id: "repo-index",
|
|
30028
|
+
label: "repo-index",
|
|
30029
|
+
detail: probe.detail ?? "local cache absent (cloud search does not need it)",
|
|
30030
|
+
warn: true,
|
|
30031
|
+
verbose: evidence
|
|
30032
|
+
};
|
|
30033
|
+
default:
|
|
30034
|
+
return null;
|
|
30035
|
+
}
|
|
29226
30036
|
}
|
|
29227
30037
|
function planGitignore(current) {
|
|
29228
30038
|
const { content, changed } = upsertManagedGitignoreBlock(current);
|
|
@@ -29272,7 +30082,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
29272
30082
|
if (!opts.json) io.log(`\u21BB ${line}`);
|
|
29273
30083
|
};
|
|
29274
30084
|
const healStep = (message) => {
|
|
29275
|
-
if (!opts.json) io.log(`${VERBOSE_INDENT}${message.trim()}`);
|
|
30085
|
+
if (!opts.json && opts.verbose) io.log(`${VERBOSE_INDENT}${message.trim()}`);
|
|
29276
30086
|
};
|
|
29277
30087
|
const releasedNote = deps.releasedVersionNote?.();
|
|
29278
30088
|
let pluginHealed = false;
|
|
@@ -29307,30 +30117,63 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
29307
30117
|
}
|
|
29308
30118
|
}
|
|
29309
30119
|
async function runCliRow() {
|
|
30120
|
+
const missing = deps.missingCliCommands?.() ?? [];
|
|
29310
30121
|
const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
|
|
29311
30122
|
const cliReport = buildVersionLagReport(cliInput);
|
|
29312
|
-
|
|
30123
|
+
const capabilityGap = missing.length > 0 && Boolean(cliReport.releasedVersion);
|
|
30124
|
+
const shouldUpdate = Boolean(
|
|
30125
|
+
applyEnv && deps.updateCli && (versionAutoUpdateAction(cliReport) === "npm" || capabilityGap)
|
|
30126
|
+
);
|
|
30127
|
+
if (!shouldUpdate) {
|
|
29313
30128
|
const cli = checkCliVersion(cliInput, releasedNote);
|
|
29314
|
-
if (cli)
|
|
30129
|
+
if (cli) {
|
|
30130
|
+
if (missing.length) {
|
|
30131
|
+
emitNow({
|
|
30132
|
+
...cli,
|
|
30133
|
+
ok: false,
|
|
30134
|
+
detail: cli.ok ? `missing commands: ${missing.join(", ")}` : `${cli.detail ?? ""}`.trim() || `missing commands: ${missing.join(", ")}`,
|
|
30135
|
+
fix: `run \`mmi-cli doctor\` to heal the global CLI (missing: ${missing.join(", ")}), or \`${cliUpdateCommand(cliReport.releasedVersion)}\``,
|
|
30136
|
+
verbose: [...cli.verbose ?? [], `missing commands: ${missing.join(", ")}`]
|
|
30137
|
+
});
|
|
30138
|
+
} else {
|
|
30139
|
+
emitNow(cli);
|
|
30140
|
+
}
|
|
30141
|
+
} else if (missing.length) {
|
|
30142
|
+
emitNow({
|
|
30143
|
+
id: "cli-version",
|
|
30144
|
+
ok: false,
|
|
30145
|
+
label: "mmi-cli",
|
|
30146
|
+
detail: `missing commands: ${missing.join(", ")}`,
|
|
30147
|
+
fix: `run \`mmi-cli doctor\` to heal the global CLI, or \`${cliUpdateCommand(cliReport.releasedVersion)}\``,
|
|
30148
|
+
verbose: [`missing commands: ${missing.join(", ")}`]
|
|
30149
|
+
});
|
|
30150
|
+
}
|
|
29315
30151
|
return;
|
|
29316
30152
|
}
|
|
29317
|
-
|
|
29318
|
-
const
|
|
29319
|
-
|
|
30153
|
+
const target = cliReport.releasedVersion;
|
|
30154
|
+
const intent = capabilityGap && cliReport.ok ? `mmi-cli \u2014 self-updating to ${target} via npm install -g (missing commands: ${missing.join(", ")})` : `mmi-cli \u2014 self-updating ${cliReport.currentVersion} \u2192 ${target} via npm install -g`;
|
|
30155
|
+
healIntent(intent);
|
|
30156
|
+
const heal = await deps.updateCli(target, healStep);
|
|
30157
|
+
const healEvidence = [
|
|
30158
|
+
`running: ${cliReport.currentVersion}`,
|
|
30159
|
+
`published: ${target ?? "(unknown)"}`,
|
|
30160
|
+
`heal: ${heal.detail}`,
|
|
30161
|
+
...missing.length ? [`missing commands before heal: ${missing.join(", ")}`] : []
|
|
30162
|
+
];
|
|
29320
30163
|
emitNow(heal.ok ? {
|
|
29321
30164
|
id: "cli-version",
|
|
29322
30165
|
ok: true,
|
|
29323
30166
|
label: "mmi-cli",
|
|
29324
|
-
detail: `${cliReport.currentVersion} \u2192 ${cliReport.
|
|
30167
|
+
detail: capabilityGap && cliReport.ok ? `${cliReport.currentVersion} \u2192 ${target} \u2014 self-updated via npm install -g (was missing: ${missing.join(", ")}); the next mmi-cli invocation runs the new version` : `${cliReport.currentVersion} \u2192 ${target} \u2014 self-updated via npm install -g; the next mmi-cli invocation runs the new version`,
|
|
29325
30168
|
verbose: healEvidence
|
|
29326
30169
|
} : {
|
|
29327
30170
|
id: "cli-version",
|
|
29328
30171
|
ok: false,
|
|
29329
30172
|
label: "mmi-cli",
|
|
29330
|
-
detail: `${cliReport.currentVersion} \u2192 ${
|
|
30173
|
+
detail: capabilityGap && cliReport.ok ? `missing commands: ${missing.join(", ")}` : `${cliReport.currentVersion} \u2192 ${target}`,
|
|
29331
30174
|
// #3489: same split as the plugin heal above — a lock-contention skip is not this run's gap.
|
|
29332
30175
|
...heal.skipped ? { reportOnly: true } : {},
|
|
29333
|
-
fix: heal.skipped ? `${heal.detail} \u2014 another doctor on this machine is updating it now; re-run once it finishes` : `self-update failed (${heal.detail}) \u2014 run \`${cliUpdateCommand(
|
|
30176
|
+
fix: heal.skipped ? `${heal.detail} \u2014 another doctor on this machine is updating it now; re-run once it finishes` : `self-update failed (${heal.detail}) \u2014 run \`${cliUpdateCommand(target)}\``,
|
|
29334
30177
|
verbose: healEvidence
|
|
29335
30178
|
});
|
|
29336
30179
|
}
|
|
@@ -29393,8 +30236,10 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
29393
30236
|
}
|
|
29394
30237
|
async function runDocsIndexRow() {
|
|
29395
30238
|
let probe;
|
|
30239
|
+
let root;
|
|
29396
30240
|
try {
|
|
29397
|
-
|
|
30241
|
+
root = await deps.repoRoot();
|
|
30242
|
+
probe = deps.docsIndexState(root);
|
|
29398
30243
|
} catch (e) {
|
|
29399
30244
|
const message = e instanceof Error ? e.message : String(e);
|
|
29400
30245
|
emitNow({
|
|
@@ -29410,11 +30255,243 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
29410
30255
|
});
|
|
29411
30256
|
return;
|
|
29412
30257
|
}
|
|
30258
|
+
let healedWrite = false;
|
|
30259
|
+
if (applyRepo && probe?.drift && deps.healDocsIndex) {
|
|
30260
|
+
healIntent("docs index \u2014 regenerating docs/index.md");
|
|
30261
|
+
try {
|
|
30262
|
+
probe = deps.healDocsIndex(root);
|
|
30263
|
+
healedWrite = !probe.drift;
|
|
30264
|
+
} catch (e) {
|
|
30265
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
30266
|
+
emitNow({
|
|
30267
|
+
ok: false,
|
|
30268
|
+
id: "docs-index",
|
|
30269
|
+
label: "docs index",
|
|
30270
|
+
detail: `heal failed \u2014 ${message}`,
|
|
30271
|
+
fix: "run `mmi-cli docs index --write` and commit docs/index.md",
|
|
30272
|
+
verbose: [`healDocsIndex threw: ${message}`]
|
|
30273
|
+
});
|
|
30274
|
+
return;
|
|
30275
|
+
}
|
|
30276
|
+
}
|
|
29413
30277
|
const docs2 = checkDocsIndex(probe);
|
|
29414
|
-
if (docs2)
|
|
30278
|
+
if (!docs2) return;
|
|
30279
|
+
if (healedWrite) {
|
|
30280
|
+
emitNow({
|
|
30281
|
+
...docs2,
|
|
30282
|
+
detail: "rewrote docs/index.md (working-tree heal \u2014 commit still needed)"
|
|
30283
|
+
});
|
|
30284
|
+
restartPending = true;
|
|
30285
|
+
return;
|
|
30286
|
+
}
|
|
30287
|
+
emitNow(docs2);
|
|
30288
|
+
}
|
|
30289
|
+
async function runRepoIndexRow() {
|
|
30290
|
+
const missing = deps.missingCliCommands?.() ?? [];
|
|
30291
|
+
if (missing.includes("repo-index")) {
|
|
30292
|
+
const row2 = checkRepoIndexCloud({ kind: "command-absent" });
|
|
30293
|
+
if (row2) emitNow(row2);
|
|
30294
|
+
return;
|
|
30295
|
+
}
|
|
30296
|
+
if (lane.preflight && !opts.self && !lane.full) {
|
|
30297
|
+
emitNow({
|
|
30298
|
+
ok: true,
|
|
30299
|
+
id: "repo-index",
|
|
30300
|
+
label: "repo-index",
|
|
30301
|
+
detail: "command present on this CLI (cloud probe skipped on --preflight)",
|
|
30302
|
+
verbose: ["preflight lane: no Hub network"]
|
|
30303
|
+
});
|
|
30304
|
+
return;
|
|
30305
|
+
}
|
|
30306
|
+
let probe;
|
|
30307
|
+
try {
|
|
30308
|
+
probe = await deps.repoIndexCloudState(await deps.repoRoot());
|
|
30309
|
+
} catch (e) {
|
|
30310
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
30311
|
+
emitNow({
|
|
30312
|
+
ok: false,
|
|
30313
|
+
id: "repo-index",
|
|
30314
|
+
label: "repo-index",
|
|
30315
|
+
detail: `could not be read \u2014 ${message}`,
|
|
30316
|
+
fix: "repair Hub session / network, then re-run doctor",
|
|
30317
|
+
reportOnly: true,
|
|
30318
|
+
verbose: [`probe threw: ${message}`]
|
|
30319
|
+
});
|
|
30320
|
+
return;
|
|
30321
|
+
}
|
|
30322
|
+
const row = checkRepoIndexCloud(probe);
|
|
30323
|
+
if (row) emitNow(row);
|
|
30324
|
+
}
|
|
30325
|
+
async function runBoardDoctorRow() {
|
|
30326
|
+
if (!deps.boardDoctorFix) return;
|
|
30327
|
+
try {
|
|
30328
|
+
const result = await deps.boardDoctorFix({ fix: applyRepo });
|
|
30329
|
+
if (!result) {
|
|
30330
|
+
emitNow({
|
|
30331
|
+
ok: true,
|
|
30332
|
+
warn: true,
|
|
30333
|
+
reportOnly: true,
|
|
30334
|
+
id: "board-doctor",
|
|
30335
|
+
label: "board doctor",
|
|
30336
|
+
detail: "skipped",
|
|
30337
|
+
verbose: ["boardDoctorFix returned undefined"]
|
|
30338
|
+
});
|
|
30339
|
+
return;
|
|
30340
|
+
}
|
|
30341
|
+
if (result.timedOut) {
|
|
30342
|
+
emitNow({
|
|
30343
|
+
ok: false,
|
|
30344
|
+
warn: true,
|
|
30345
|
+
reportOnly: true,
|
|
30346
|
+
id: "board-doctor",
|
|
30347
|
+
label: "board doctor",
|
|
30348
|
+
detail: "timed out",
|
|
30349
|
+
fix: "run `mmi-cli board doctor --fix`",
|
|
30350
|
+
verbose: [`scanned before timeout: ${result.scanned}`, `findings: ${result.findings}`]
|
|
30351
|
+
});
|
|
30352
|
+
return;
|
|
30353
|
+
}
|
|
30354
|
+
const allFixed = applyRepo && result.findings > 0 && result.failed === 0 && result.fixed >= result.findings;
|
|
30355
|
+
const ok = result.findings === 0 || Boolean(allFixed);
|
|
30356
|
+
emitNow(ok ? {
|
|
30357
|
+
ok: true,
|
|
30358
|
+
id: "board-doctor",
|
|
30359
|
+
label: "board doctor",
|
|
30360
|
+
detail: `scanned ${result.scanned}; fixed ${result.fixed}`,
|
|
30361
|
+
verbose: [`findings: ${result.findings}`, `fixed: ${result.fixed}`, `failed: ${result.failed}`]
|
|
30362
|
+
} : {
|
|
30363
|
+
ok: false,
|
|
30364
|
+
// Gate when fix was attempted and mechanical findings remain; detect-only stays visible but
|
|
30365
|
+
// report-only would hide a red that `board doctor --fix` clears — prefer gate after a failed fix.
|
|
30366
|
+
reportOnly: !applyRepo,
|
|
30367
|
+
id: "board-doctor",
|
|
30368
|
+
label: "board doctor",
|
|
30369
|
+
detail: `scanned ${result.scanned}; ${result.findings} findings; fixed ${result.fixed}; failed ${result.failed}`,
|
|
30370
|
+
fix: "run `mmi-cli board doctor --fix`",
|
|
30371
|
+
verbose: [`findings: ${result.findings}`, `fixed: ${result.fixed}`, `failed: ${result.failed}`]
|
|
30372
|
+
});
|
|
30373
|
+
} catch (e) {
|
|
30374
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
30375
|
+
emitNow({
|
|
30376
|
+
ok: true,
|
|
30377
|
+
warn: true,
|
|
30378
|
+
reportOnly: true,
|
|
30379
|
+
id: "board-doctor",
|
|
30380
|
+
label: "board doctor",
|
|
30381
|
+
detail: `skipped \u2014 ${message}`,
|
|
30382
|
+
verbose: [`boardDoctorFix threw: ${message}`]
|
|
30383
|
+
});
|
|
30384
|
+
}
|
|
30385
|
+
}
|
|
30386
|
+
async function runSchedulesDriftRow() {
|
|
30387
|
+
if (!deps.schedulesDriftState) return;
|
|
30388
|
+
try {
|
|
30389
|
+
const state = await deps.schedulesDriftState();
|
|
30390
|
+
if (!state) {
|
|
30391
|
+
emitNow({
|
|
30392
|
+
ok: true,
|
|
30393
|
+
warn: true,
|
|
30394
|
+
reportOnly: true,
|
|
30395
|
+
id: "schedules-drift",
|
|
30396
|
+
label: "schedules drift",
|
|
30397
|
+
detail: "skipped",
|
|
30398
|
+
verbose: ["schedulesDriftState returned undefined"]
|
|
30399
|
+
});
|
|
30400
|
+
return;
|
|
30401
|
+
}
|
|
30402
|
+
if (state.timedOut) {
|
|
30403
|
+
emitNow({
|
|
30404
|
+
ok: false,
|
|
30405
|
+
warn: true,
|
|
30406
|
+
reportOnly: true,
|
|
30407
|
+
id: "schedules-drift",
|
|
30408
|
+
label: "schedules drift",
|
|
30409
|
+
detail: "timed out",
|
|
30410
|
+
fix: "run `mmi-cli org schedules` / `org schedules register`",
|
|
30411
|
+
verbose: ["schedulesDriftState timed out"]
|
|
30412
|
+
});
|
|
30413
|
+
return;
|
|
30414
|
+
}
|
|
30415
|
+
if (state.incomplete.length) {
|
|
30416
|
+
emitNow({
|
|
30417
|
+
ok: true,
|
|
30418
|
+
warn: true,
|
|
30419
|
+
reportOnly: true,
|
|
30420
|
+
id: "schedules-drift",
|
|
30421
|
+
label: "schedules drift",
|
|
30422
|
+
detail: `incomplete \u2014 ${state.incomplete.join("; ")}`,
|
|
30423
|
+
fix: "run `mmi-cli org schedules` / `org schedules register`",
|
|
30424
|
+
verbose: state.incomplete
|
|
30425
|
+
});
|
|
30426
|
+
return;
|
|
30427
|
+
}
|
|
30428
|
+
if (state.driftLines.length === 0) {
|
|
30429
|
+
emitNow({
|
|
30430
|
+
ok: true,
|
|
30431
|
+
reportOnly: true,
|
|
30432
|
+
id: "schedules-drift",
|
|
30433
|
+
label: "schedules drift",
|
|
30434
|
+
detail: "no drift",
|
|
30435
|
+
verbose: ["no drift lines"]
|
|
30436
|
+
});
|
|
30437
|
+
return;
|
|
30438
|
+
}
|
|
30439
|
+
emitNow({
|
|
30440
|
+
ok: false,
|
|
30441
|
+
reportOnly: true,
|
|
30442
|
+
id: "schedules-drift",
|
|
30443
|
+
label: "schedules drift",
|
|
30444
|
+
detail: `${state.driftLines.length} drift line(s)`,
|
|
30445
|
+
fix: "run `mmi-cli org schedules` / `org schedules register`",
|
|
30446
|
+
verbose: state.driftLines
|
|
30447
|
+
});
|
|
30448
|
+
} catch (e) {
|
|
30449
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
30450
|
+
emitNow({
|
|
30451
|
+
ok: true,
|
|
30452
|
+
warn: true,
|
|
30453
|
+
reportOnly: true,
|
|
30454
|
+
id: "schedules-drift",
|
|
30455
|
+
label: "schedules drift",
|
|
30456
|
+
detail: `skipped \u2014 ${message}`,
|
|
30457
|
+
verbose: [`schedulesDriftState threw: ${message}`]
|
|
30458
|
+
});
|
|
30459
|
+
}
|
|
29415
30460
|
}
|
|
29416
30461
|
async function runHousekeeperRows() {
|
|
29417
30462
|
const repoRoot2 = await deps.repoRoot();
|
|
30463
|
+
if (applyRepo && deps.sweepDeferred) {
|
|
30464
|
+
try {
|
|
30465
|
+
const sweep = await deps.sweepDeferred();
|
|
30466
|
+
const detailParts = [];
|
|
30467
|
+
if (sweep.removed.length) detailParts.push(`removed ${sweep.removed.length}`);
|
|
30468
|
+
if (sweep.stillQueued) detailParts.push(`${sweep.stillQueued} still queued`);
|
|
30469
|
+
if (sweep.skipped) detailParts.push(`${sweep.skipped} skipped`);
|
|
30470
|
+
emitNow({
|
|
30471
|
+
id: "deferred-worktrees",
|
|
30472
|
+
ok: true,
|
|
30473
|
+
label: "deferred worktrees",
|
|
30474
|
+
detail: detailParts.length ? detailParts.join("; ") : "nothing queued",
|
|
30475
|
+
verbose: [
|
|
30476
|
+
...sweep.removed.map((p) => `removed: ${p}`),
|
|
30477
|
+
`stillQueued: ${sweep.stillQueued}`,
|
|
30478
|
+
`skipped: ${sweep.skipped}`
|
|
30479
|
+
]
|
|
30480
|
+
});
|
|
30481
|
+
if (sweep.removed.length) restartPending = true;
|
|
30482
|
+
} catch (e) {
|
|
30483
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
30484
|
+
emitNow({
|
|
30485
|
+
id: "deferred-worktrees",
|
|
30486
|
+
ok: true,
|
|
30487
|
+
warn: true,
|
|
30488
|
+
reportOnly: true,
|
|
30489
|
+
label: "deferred worktrees",
|
|
30490
|
+
detail: `sweep failed (fail-soft) \u2014 ${message}`,
|
|
30491
|
+
verbose: [`sweepDeferred threw: ${message}`]
|
|
30492
|
+
});
|
|
30493
|
+
}
|
|
30494
|
+
}
|
|
29418
30495
|
try {
|
|
29419
30496
|
const plan = await deps.gcPlan("origin", 200);
|
|
29420
30497
|
if (applyRepo) {
|
|
@@ -29471,9 +30548,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
29471
30548
|
emitNow(scratch.plan.safeAuto.length === 0 ? { id: "scratch", ok: true, label: "scratch", verbose: scratchEvidence } : { id: "scratch", ok: false, label: "scratch", detail: `${scratch.plan.safeAuto.length} aged item(s)`, fix: "run `mmi-cli doctor` (without --no-repo-writes)", verbose: scratchEvidence });
|
|
29472
30549
|
}
|
|
29473
30550
|
}
|
|
29474
|
-
const
|
|
29475
|
-
// Heals first: the slowest work starts at second zero, and a stale mmi-cli is the binary every row
|
|
29476
|
-
// below is being measured with (#3954).
|
|
30551
|
+
const prefix = [
|
|
29477
30552
|
{ id: "plugin", when: true, run: runPluginRow },
|
|
29478
30553
|
{ id: "cli-version", when: true, run: runCliRow },
|
|
29479
30554
|
{ id: "github-auth", when: true, run: runGithubAuthRow },
|
|
@@ -29482,16 +30557,22 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
29482
30557
|
{ id: "gitignore-block", when: isOrgRepo, run: runGitignoreRow },
|
|
29483
30558
|
{ id: "plugin-cache", when: true, run: runPluginCacheRow },
|
|
29484
30559
|
{ id: "sessionstart-payload", when: true, run: runSessionPayloadRow },
|
|
29485
|
-
{ id: "marketplace", when: true, run: runMarketplaceRows }
|
|
29486
|
-
|
|
29487
|
-
|
|
30560
|
+
{ id: "marketplace", when: true, run: runMarketplaceRows }
|
|
30561
|
+
];
|
|
30562
|
+
for (const entry of prefix) if (entry.when) await entry.run();
|
|
30563
|
+
const parallel = [
|
|
29488
30564
|
{ id: "docs-index", when: isOrgRepo && lane.full, run: runDocsIndexRow },
|
|
29489
|
-
//
|
|
29490
|
-
|
|
30565
|
+
// Hub cloud probe (short timeout). Full lane + `--self`; `--preflight` may emit command-absent only (#4156).
|
|
30566
|
+
{ id: "repo-index", when: isOrgRepo && (lane.full || Boolean(opts.self) || lane.preflight), run: runRepoIndexRow },
|
|
30567
|
+
{ id: "board-doctor", when: isOrgRepo && lane.full, run: runBoardDoctorRow },
|
|
30568
|
+
{ id: "schedules-drift", when: isOrgRepo && lane.full, run: runSchedulesDriftRow }
|
|
30569
|
+
];
|
|
30570
|
+
await Promise.all(parallel.filter((entry) => entry.when).map((entry) => entry.run()));
|
|
30571
|
+
const suffix = [
|
|
29491
30572
|
{ id: "train-branches", when: isOrgRepo && lane.full, run: runTrainSyncRow },
|
|
29492
30573
|
{ id: "housekeeper", when: isOrgRepo && lane.full, run: runHousekeeperRows }
|
|
29493
30574
|
];
|
|
29494
|
-
for (const entry of
|
|
30575
|
+
for (const entry of suffix) if (entry.when) await entry.run();
|
|
29495
30576
|
const exitCode = doctorReportExitCode(checks);
|
|
29496
30577
|
if (opts.json) {
|
|
29497
30578
|
const payload = opts.verbose ? checks : checks.map(({ verbose: _evidence, ...check }) => check);
|
|
@@ -29543,18 +30624,18 @@ function parseOriginRepo(remoteUrl) {
|
|
|
29543
30624
|
return `${match[1]}/${match[2]}`;
|
|
29544
30625
|
}
|
|
29545
30626
|
function ghHostsConfigPath(env, platform2) {
|
|
29546
|
-
const
|
|
29547
|
-
const
|
|
30627
|
+
const sep3 = platform2 === "win32" ? "\\" : "/";
|
|
30628
|
+
const join34 = (...parts) => parts.join(sep3);
|
|
29548
30629
|
const explicit = env.GH_CONFIG_DIR?.trim();
|
|
29549
|
-
if (explicit) return
|
|
30630
|
+
if (explicit) return join34(explicit, "hosts.yml");
|
|
29550
30631
|
if (platform2 === "win32") {
|
|
29551
30632
|
const appData = (env.AppData ?? env.APPDATA)?.trim();
|
|
29552
|
-
return appData ?
|
|
30633
|
+
return appData ? join34(appData, "GitHub CLI", "hosts.yml") : void 0;
|
|
29553
30634
|
}
|
|
29554
30635
|
const xdg = env.XDG_CONFIG_HOME?.trim();
|
|
29555
|
-
if (xdg) return
|
|
30636
|
+
if (xdg) return join34(xdg, "gh", "hosts.yml");
|
|
29556
30637
|
const home = env.HOME?.trim();
|
|
29557
|
-
return home ?
|
|
30638
|
+
return home ? join34(home, ".config", "gh", "hosts.yml") : void 0;
|
|
29558
30639
|
}
|
|
29559
30640
|
function parseGhHostsAccounts(yaml, host = "github.com") {
|
|
29560
30641
|
let hostIndent = null;
|
|
@@ -29604,17 +30685,17 @@ function ghAccountCaveat(announcedLogin, accounts) {
|
|
|
29604
30685
|
}
|
|
29605
30686
|
|
|
29606
30687
|
// src/doctor-io.ts
|
|
29607
|
-
var
|
|
29608
|
-
var
|
|
29609
|
-
var
|
|
29610
|
-
var
|
|
30688
|
+
var import_node_fs38 = require("node:fs");
|
|
30689
|
+
var import_node_os14 = require("node:os");
|
|
30690
|
+
var import_node_path37 = require("node:path");
|
|
30691
|
+
var import_node_child_process17 = require("node:child_process");
|
|
29611
30692
|
var import_node_util8 = require("node:util");
|
|
29612
|
-
var execFileP6 = (0, import_node_util8.promisify)(
|
|
30693
|
+
var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process17.execFile);
|
|
29613
30694
|
var MMI_PLUGIN_ID2 = "mmi@mutmutco";
|
|
29614
30695
|
function installedClaudePluginVersion() {
|
|
29615
30696
|
try {
|
|
29616
30697
|
const file = JSON.parse(
|
|
29617
|
-
(0,
|
|
30698
|
+
(0, import_node_fs38.readFileSync)((0, import_node_path37.join)((0, import_node_os14.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
|
|
29618
30699
|
);
|
|
29619
30700
|
const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
|
|
29620
30701
|
if (versions.length === 0) return void 0;
|
|
@@ -29625,7 +30706,7 @@ function installedClaudePluginVersion() {
|
|
|
29625
30706
|
}
|
|
29626
30707
|
function manifestVersion(path2) {
|
|
29627
30708
|
try {
|
|
29628
|
-
const manifest = JSON.parse((0,
|
|
30709
|
+
const manifest = JSON.parse((0, import_node_fs38.readFileSync)(path2, "utf8"));
|
|
29629
30710
|
return typeof manifest.version === "string" && manifest.version.trim() ? manifest.version.trim() : void 0;
|
|
29630
30711
|
} catch {
|
|
29631
30712
|
return void 0;
|
|
@@ -29635,27 +30716,27 @@ function installedSurfacePluginVersion(surface) {
|
|
|
29635
30716
|
const token = surfaceToken(surface);
|
|
29636
30717
|
if (token === "kilo") {
|
|
29637
30718
|
try {
|
|
29638
|
-
const stamp = (0,
|
|
30719
|
+
const stamp = (0, import_node_fs38.readFileSync)((0, import_node_path37.join)((0, import_node_os14.homedir)(), ".kilo", ".mmi-kilo-version"), "utf8").trim();
|
|
29639
30720
|
return stamp || void 0;
|
|
29640
30721
|
} catch {
|
|
29641
30722
|
return void 0;
|
|
29642
30723
|
}
|
|
29643
30724
|
}
|
|
29644
30725
|
if (token === "cursor") {
|
|
29645
|
-
return manifestVersion((0,
|
|
30726
|
+
return manifestVersion((0, import_node_path37.join)(cursorLocalPluginRoot(), ".cursor-plugin", "plugin.json"));
|
|
29646
30727
|
}
|
|
29647
30728
|
if (token === "kimi") {
|
|
29648
|
-
return manifestVersion((0,
|
|
30729
|
+
return manifestVersion((0, import_node_path37.join)(surfaceConfigRoot(surface), "plugins", "managed", "mmi", ".kimi-plugin", "plugin.json"));
|
|
29649
30730
|
}
|
|
29650
30731
|
if (token === "claude") return installedClaudePluginVersion();
|
|
29651
30732
|
if (token !== "codex") return void 0;
|
|
29652
30733
|
try {
|
|
29653
|
-
const raw = process.platform === "win32" ? (0,
|
|
30734
|
+
const raw = process.platform === "win32" ? (0, import_node_child_process17.execFileSync)("cmd.exe", ["/c", "codex", "plugin", "list", "--json"], {
|
|
29654
30735
|
encoding: "utf8",
|
|
29655
30736
|
stdio: ["ignore", "pipe", "ignore"],
|
|
29656
30737
|
timeout: 15e3,
|
|
29657
30738
|
windowsHide: true
|
|
29658
|
-
}) : (0,
|
|
30739
|
+
}) : (0, import_node_child_process17.execFileSync)("codex", ["plugin", "list", "--json"], {
|
|
29659
30740
|
encoding: "utf8",
|
|
29660
30741
|
stdio: ["ignore", "pipe", "ignore"],
|
|
29661
30742
|
timeout: 15e3,
|
|
@@ -29673,7 +30754,7 @@ function installedActivePluginVersion(surface = detectSurface(process.env)) {
|
|
|
29673
30754
|
}
|
|
29674
30755
|
function worktreeRootSync() {
|
|
29675
30756
|
try {
|
|
29676
|
-
const out = (0,
|
|
30757
|
+
const out = (0, import_node_child_process17.execFileSync)("git", ["rev-parse", "--show-toplevel"], { windowsHide: true, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
29677
30758
|
let root = out.endsWith("\n") ? out.slice(0, -1) : out;
|
|
29678
30759
|
if (process.platform === "win32" && root.endsWith("\r")) root = root.slice(0, -1);
|
|
29679
30760
|
return root || null;
|
|
@@ -29683,13 +30764,13 @@ function worktreeRootSync() {
|
|
|
29683
30764
|
}
|
|
29684
30765
|
var gitignorePath = () => {
|
|
29685
30766
|
const root = worktreeRootSync();
|
|
29686
|
-
return root === null ? null : (0,
|
|
30767
|
+
return root === null ? null : (0, import_node_path37.join)(root, ".gitignore");
|
|
29687
30768
|
};
|
|
29688
30769
|
function readGitignore() {
|
|
29689
30770
|
const path2 = gitignorePath();
|
|
29690
30771
|
if (path2 === null) return null;
|
|
29691
30772
|
try {
|
|
29692
|
-
return (0,
|
|
30773
|
+
return (0, import_node_fs38.readFileSync)(path2, "utf8");
|
|
29693
30774
|
} catch {
|
|
29694
30775
|
return null;
|
|
29695
30776
|
}
|
|
@@ -29698,7 +30779,7 @@ function writeGitignore(content) {
|
|
|
29698
30779
|
const path2 = gitignorePath();
|
|
29699
30780
|
if (path2 === null) return false;
|
|
29700
30781
|
try {
|
|
29701
|
-
(0,
|
|
30782
|
+
(0, import_node_fs38.writeFileSync)(path2, content, "utf8");
|
|
29702
30783
|
return true;
|
|
29703
30784
|
} catch {
|
|
29704
30785
|
return false;
|
|
@@ -29722,7 +30803,7 @@ async function repoRoot() {
|
|
|
29722
30803
|
}
|
|
29723
30804
|
function hasRepoLocalWorktrees() {
|
|
29724
30805
|
const root = worktreeRootSync();
|
|
29725
|
-
return root !== null && (0,
|
|
30806
|
+
return root !== null && (0, import_node_fs38.existsSync)((0, import_node_path37.join)(root, ".worktrees"));
|
|
29726
30807
|
}
|
|
29727
30808
|
|
|
29728
30809
|
// src/index.ts
|
|
@@ -29758,8 +30839,8 @@ ${r.stderr ?? ""}`).catch(() => "");
|
|
|
29758
30839
|
function ghMultiAccountCaveat(announcedLogin) {
|
|
29759
30840
|
try {
|
|
29760
30841
|
const hostsPath = ghHostsConfigPath(process.env, process.platform);
|
|
29761
|
-
if (!hostsPath || !(0,
|
|
29762
|
-
return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0,
|
|
30842
|
+
if (!hostsPath || !(0, import_node_fs39.existsSync)(hostsPath)) return void 0;
|
|
30843
|
+
return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs39.readFileSync)(hostsPath, "utf8")));
|
|
29763
30844
|
} catch {
|
|
29764
30845
|
return void 0;
|
|
29765
30846
|
}
|
|
@@ -29767,20 +30848,20 @@ function ghMultiAccountCaveat(announcedLogin) {
|
|
|
29767
30848
|
var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
|
|
29768
30849
|
var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
|
|
29769
30850
|
function envHealLockPath(home) {
|
|
29770
|
-
return (0,
|
|
30851
|
+
return (0, import_node_path38.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
|
|
29771
30852
|
}
|
|
29772
30853
|
async function withEnvHealLock(what, run) {
|
|
29773
30854
|
try {
|
|
29774
30855
|
return await withFileLock(
|
|
29775
|
-
envHealLockPath((0,
|
|
30856
|
+
envHealLockPath((0, import_node_os15.homedir)()),
|
|
29776
30857
|
{ staleMs: ENV_HEAL_LOCK_STALE_MS, maxWaitMs: ENV_HEAL_LOCK_MAX_WAIT_MS, label: "mmi env-heal lock" },
|
|
29777
30858
|
run
|
|
29778
30859
|
);
|
|
29779
30860
|
} catch (e) {
|
|
29780
30861
|
if (e instanceof FileLockBusyError) {
|
|
29781
|
-
return { ok: false, skipped: true, detail: `${what} skipped
|
|
30862
|
+
return { ok: false, skipped: true, detail: `${what} skipped ? ${e.message}` };
|
|
29782
30863
|
}
|
|
29783
|
-
return { ok: false, detail: `${what} could not take the env-heal lock
|
|
30864
|
+
return { ok: false, detail: `${what} could not take the env-heal lock ? ${e.message}` };
|
|
29784
30865
|
}
|
|
29785
30866
|
}
|
|
29786
30867
|
function throttledReleasedVersion() {
|
|
@@ -29836,13 +30917,13 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
29836
30917
|
pluginTrustState: () => codexHookTrustState(),
|
|
29837
30918
|
releasedVersion: throttled ? throttled.read : fetchNpmReleasedVersion,
|
|
29838
30919
|
releasedVersionNote: throttled ? throttled.note : void 0,
|
|
29839
|
-
// #3272: the --apply self-heal for a stale running CLI
|
|
30920
|
+
// #3272: the --apply self-heal for a stale running CLI ? npm global, shadows any plugin shim (#2879).
|
|
29840
30921
|
// #3489: serialised. Both heals mutate machine-global state (the npm global prefix; the Claude
|
|
29841
30922
|
// marketplace clone + plugin cache) and neither is idempotent under concurrency. They share ONE lock
|
|
29842
30923
|
// rather than one each, because they are not independent: the plugin heal reinstalls a bundle that
|
|
29843
30924
|
// carries a CLI shim, so a concurrent npm global install races the same PATH surface.
|
|
29844
30925
|
updateCli: (target, onStep) => withEnvHealLock("npm global CLI self-update", () => npmSelfUpdateCli(target, onStep)),
|
|
29845
|
-
// #3282: the --apply self-heal for a stale/unresolved Claude plugin
|
|
30926
|
+
// #3282: the --apply self-heal for a stale/unresolved Claude plugin ? the same marketplace reinstall
|
|
29846
30927
|
// `mmi-cli plugin heal` drives. Takes effect on the next Claude reload, so the row asks for a restart.
|
|
29847
30928
|
healPlugin: (onStep) => {
|
|
29848
30929
|
const surface = detectSurface(process.env);
|
|
@@ -29860,7 +30941,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
29860
30941
|
// #2759: same fetch+ff-only sync SessionStart runs, wired here too so an on-demand `mmi-cli doctor`
|
|
29861
30942
|
// self-heals a checkout a long-running session left stale.
|
|
29862
30943
|
syncTrain: () => syncLocalTrainBranches(execFileGitRun),
|
|
29863
|
-
// #2903: doctor DETECTS stale cached plugin versions and defers the delete to `plugin-prune`
|
|
30944
|
+
// #2903: doctor DETECTS stale cached plugin versions and defers the delete to `plugin-prune` ? it never
|
|
29864
30945
|
// writes to the harness-owned cache itself. Same plan builder the verb uses, so the two never disagree.
|
|
29865
30946
|
// No `withBytes` here: doctor runs on EVERY SessionStart, and sizing means recursively stat'ing every
|
|
29866
30947
|
// stale version tree. The count comes from one cheap readdir; `plugin-prune` reports the MB.
|
|
@@ -29869,7 +30950,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
29869
30950
|
const configRoot = surfaceConfigRoot(surface);
|
|
29870
30951
|
const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
|
|
29871
30952
|
const plan = buildPluginCachePlan(
|
|
29872
|
-
(0,
|
|
30953
|
+
(0, import_node_os15.homedir)(),
|
|
29873
30954
|
running,
|
|
29874
30955
|
pluginCacheFsDeps(configRoot, () => 0),
|
|
29875
30956
|
{ configRoot, includeStaging: surface !== "codex" }
|
|
@@ -29883,24 +30964,24 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
29883
30964
|
};
|
|
29884
30965
|
},
|
|
29885
30966
|
// #3470: what the SessionStart hook LAST emitted, measured at emission by the session-start verb below.
|
|
29886
|
-
// A local record read
|
|
30967
|
+
// A local record read ? cheap enough for every lane, including the banner.
|
|
29887
30968
|
sessionPayload: () => readSessionPayload(process.cwd()),
|
|
29888
|
-
// #3485 items 9 and 8: why the MMI plugin has never printed an "updated
|
|
30969
|
+
// #3485 items 9 and 8: why the MMI plugin has never printed an "updated ? please restart" notice, and
|
|
29889
30970
|
// which branch it would pick one up from. Two local file reads, no network, fail-soft to no rows.
|
|
29890
30971
|
marketplaceRows: () => {
|
|
29891
30972
|
try {
|
|
29892
30973
|
if (detectSurface(process.env) === "codex") return [];
|
|
29893
|
-
const home = (0,
|
|
30974
|
+
const home = (0, import_node_os15.homedir)();
|
|
29894
30975
|
const rows = marketplaceRows(
|
|
29895
30976
|
MMI_MARKETPLACE_NAME,
|
|
29896
|
-
readFileSyncSafe((0,
|
|
29897
|
-
readFileSyncSafe((0,
|
|
30977
|
+
readFileSyncSafe((0, import_node_path38.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs39.readFileSync),
|
|
30978
|
+
readFileSyncSafe((0, import_node_path38.join)(home, ".claude", "settings.json"), import_node_fs39.readFileSync),
|
|
29898
30979
|
// #3974: this CLI heals these rows, so report mode names the doctor run rather than the hand
|
|
29899
|
-
// edit. Unconditional
|
|
30980
|
+
// edit. Unconditional ? it is a fact about mmi-cli, not about the lane this run is on.
|
|
29900
30981
|
true
|
|
29901
30982
|
);
|
|
29902
30983
|
const pending = readMarketplacePinPending(
|
|
29903
|
-
(0,
|
|
30984
|
+
(0, import_node_path38.join)(home, ".claude", "plugins", ".mmi-marketplace-pin-pending.json"),
|
|
29904
30985
|
MMI_MARKETPLACE_NAME
|
|
29905
30986
|
);
|
|
29906
30987
|
if (!pending) return rows;
|
|
@@ -29911,7 +30992,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
29911
30992
|
...rest,
|
|
29912
30993
|
ok: true,
|
|
29913
30994
|
warn: true,
|
|
29914
|
-
detail: `pending restart
|
|
30995
|
+
detail: `pending restart ? doctor pinned main at ${pending.at}; restart Claude Code to load it`,
|
|
29915
30996
|
verbose: [...row.verbose ?? [], "known_marketplaces.json was rewritten by the running Claude host after the verified doctor write"]
|
|
29916
30997
|
};
|
|
29917
30998
|
});
|
|
@@ -29919,16 +31000,16 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
29919
31000
|
return [];
|
|
29920
31001
|
}
|
|
29921
31002
|
},
|
|
29922
|
-
// #3974: the heal behind those rows, under --apply only. Same Codex carve-out as the rows above
|
|
31003
|
+
// #3974: the heal behind those rows, under --apply only. Same Codex carve-out as the rows above ?
|
|
29923
31004
|
// Codex keeps its registration in config.toml, so there is no ~/.claude registration to pin.
|
|
29924
31005
|
healMarketplacePins: () => {
|
|
29925
31006
|
try {
|
|
29926
31007
|
if (detectSurface(process.env) === "codex") return void 0;
|
|
29927
|
-
const home = (0,
|
|
31008
|
+
const home = (0, import_node_os15.homedir)();
|
|
29928
31009
|
const names = [MMI_MARKETPLACE_NAME];
|
|
29929
|
-
const result = applyOrgMarketplacePins((0,
|
|
31010
|
+
const result = applyOrgMarketplacePins((0, import_node_path38.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), names);
|
|
29930
31011
|
if (result?.wrote) {
|
|
29931
|
-
writeMarketplacePinPending((0,
|
|
31012
|
+
writeMarketplacePinPending((0, import_node_path38.join)(home, ".claude", "plugins", ".mmi-marketplace-pin-pending.json"), names);
|
|
29932
31013
|
}
|
|
29933
31014
|
return result;
|
|
29934
31015
|
} catch {
|
|
@@ -29936,20 +31017,151 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
29936
31017
|
}
|
|
29937
31018
|
},
|
|
29938
31019
|
// #4091: the same `docs index --check` comparison the required CI gate runs, in-process. Rooted at the
|
|
29939
|
-
// repo root doctor already resolved
|
|
31020
|
+
// repo root doctor already resolved ? never `process.cwd()`, which would have `mmi-cli doctor` run from
|
|
29940
31021
|
// `cli/` measure a `cli/docs/` tree that does not exist.
|
|
29941
31022
|
//
|
|
29942
31023
|
// `existsSync` on `docs/index.md` is the adoption gate, and it sits UNDER the table's org-repo gate
|
|
29943
31024
|
// rather than replacing it: a missing index is drift by construction, so without this a repo that never
|
|
29944
31025
|
// adopted the generated routing index (MMG-Unlive: `docs/Archive/**` only, no index, no gate.yml) would
|
|
29945
|
-
// get a permanent
|
|
31026
|
+
// get a permanent ? demanding an artifact it never asked for.
|
|
29946
31027
|
docsIndexState: (root) => {
|
|
29947
|
-
if (!(0,
|
|
31028
|
+
if (!(0, import_node_fs39.existsSync)((0, import_node_path38.join)(root, DOCS_INDEX_PATH))) return void 0;
|
|
31029
|
+
const real = createDocsIndexDeps(root);
|
|
31030
|
+
let docs2;
|
|
31031
|
+
const listDocs = () => docs2 ??= real.listDocs();
|
|
31032
|
+
const drift = docsIndex({ ...real, listDocs }, { check: true }).drift;
|
|
31033
|
+
return { drift, docCount: listDocs().length };
|
|
31034
|
+
},
|
|
31035
|
+
// #4168: working-tree heal — write if drifted, then re-check. Commit remains the operator's step.
|
|
31036
|
+
healDocsIndex: (root) => {
|
|
31037
|
+
if (!(0, import_node_fs39.existsSync)((0, import_node_path38.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
|
|
29948
31038
|
const real = createDocsIndexDeps(root);
|
|
29949
31039
|
let docs2;
|
|
29950
31040
|
const listDocs = () => docs2 ??= real.listDocs();
|
|
31041
|
+
docsIndex({ ...real, listDocs }, { check: false });
|
|
29951
31042
|
const drift = docsIndex({ ...real, listDocs }, { check: true }).drift;
|
|
29952
31043
|
return { drift, docCount: listDocs().length };
|
|
31044
|
+
},
|
|
31045
|
+
// #4169: mirror `worktree gc --apply` — sweep deferred removals before applyGcPlan.
|
|
31046
|
+
sweepDeferred: async () => {
|
|
31047
|
+
const deferredStore = await createDeferredWorktreeStore();
|
|
31048
|
+
const removalContext = await currentWorktreeRemovalContext("doctor deferred sweep");
|
|
31049
|
+
const result = await sweepDeferredWorktrees(
|
|
31050
|
+
deferredStore,
|
|
31051
|
+
// #2841: same `-c core.fsmonitor=false` guard as `worktree gc --apply` / sweep-deferred.
|
|
31052
|
+
worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
|
|
31053
|
+
removalContext
|
|
31054
|
+
);
|
|
31055
|
+
return {
|
|
31056
|
+
removed: result.removed,
|
|
31057
|
+
stillQueued: result.stillDeferred.length,
|
|
31058
|
+
skipped: result.skipped.length
|
|
31059
|
+
};
|
|
31060
|
+
},
|
|
31061
|
+
// #4170: light board doctor with 15s ceiling. skipClaimLiveness keeps Phase 3 off.
|
|
31062
|
+
boardDoctorFix: async ({ fix } = {}) => {
|
|
31063
|
+
const BOARD_DOCTOR_TIMEOUT_MS = 15e3;
|
|
31064
|
+
try {
|
|
31065
|
+
const cfg = await loadConfigForRepo();
|
|
31066
|
+
const work = boardDoctor({
|
|
31067
|
+
config: cfg,
|
|
31068
|
+
fix: Boolean(fix),
|
|
31069
|
+
skipClaimLiveness: true
|
|
31070
|
+
});
|
|
31071
|
+
const raced = await Promise.race([
|
|
31072
|
+
work.then((r) => ({ ...r, timedOut: false })),
|
|
31073
|
+
new Promise((resolve5) => {
|
|
31074
|
+
setTimeout(() => resolve5({ timedOut: true, scanned: 0, findings: 0, fixed: 0, failed: 0 }), BOARD_DOCTOR_TIMEOUT_MS);
|
|
31075
|
+
})
|
|
31076
|
+
]);
|
|
31077
|
+
if (raced.timedOut) return { scanned: 0, findings: 0, fixed: 0, failed: 0, timedOut: true };
|
|
31078
|
+
return {
|
|
31079
|
+
scanned: raced.scanned,
|
|
31080
|
+
findings: raced.findings,
|
|
31081
|
+
fixed: raced.fixed,
|
|
31082
|
+
failed: raced.failed
|
|
31083
|
+
};
|
|
31084
|
+
} catch {
|
|
31085
|
+
return void 0;
|
|
31086
|
+
}
|
|
31087
|
+
},
|
|
31088
|
+
// #4171: schedules drift notebook — report-only, ~20s ceiling.
|
|
31089
|
+
schedulesDriftState: async () => {
|
|
31090
|
+
const SCHEDULES_DRIFT_TIMEOUT_MS = 2e4;
|
|
31091
|
+
try {
|
|
31092
|
+
const raced = await Promise.race([
|
|
31093
|
+
fetchNotebook().then((nb) => ({
|
|
31094
|
+
driftLines: nb.drift,
|
|
31095
|
+
incomplete: nb.incomplete,
|
|
31096
|
+
timedOut: false
|
|
31097
|
+
})),
|
|
31098
|
+
new Promise((resolve5) => {
|
|
31099
|
+
setTimeout(() => resolve5({ driftLines: [], incomplete: [], timedOut: true }), SCHEDULES_DRIFT_TIMEOUT_MS);
|
|
31100
|
+
})
|
|
31101
|
+
]);
|
|
31102
|
+
return raced;
|
|
31103
|
+
} catch {
|
|
31104
|
+
return void 0;
|
|
31105
|
+
}
|
|
31106
|
+
},
|
|
31107
|
+
// #4173: estate commands absent from the running Commander program (stale global CLI).
|
|
31108
|
+
missingCliCommands: () => {
|
|
31109
|
+
const required = ["repo-index"];
|
|
31110
|
+
const names = new Set((opts.program?.commands ?? []).map((c) => c.name()));
|
|
31111
|
+
return required.filter((n) => !names.has(n));
|
|
31112
|
+
},
|
|
31113
|
+
// #4156: short-timeout Hub status for the current repo. Fail-soft ? never throws to doctor.
|
|
31114
|
+
repoIndexCloudState: async (root) => {
|
|
31115
|
+
const local = repoIndexStatus(root);
|
|
31116
|
+
try {
|
|
31117
|
+
const cfg = await loadConfig();
|
|
31118
|
+
const repo = inferRepoSlug(root);
|
|
31119
|
+
const st = await statusRepoIndexCloud(repo, {
|
|
31120
|
+
...registryClientDeps(cfg),
|
|
31121
|
+
timeoutMs: 4e3
|
|
31122
|
+
});
|
|
31123
|
+
if (isRepoIndexStatusError(st)) {
|
|
31124
|
+
if (st.code === "deploy-lag") return { kind: "deploy-lag", repo, localPresent: local.present };
|
|
31125
|
+
if (st.code === "auth") return { kind: "auth", repo, localPresent: local.present };
|
|
31126
|
+
if (st.code === "config") return { kind: "config", repo, detail: st.error, localPresent: local.present };
|
|
31127
|
+
return { kind: "network", repo, detail: st.error, localPresent: local.present };
|
|
31128
|
+
}
|
|
31129
|
+
const present = st.present;
|
|
31130
|
+
const builtAt = typeof st.builtAt === "string" ? st.builtAt : void 0;
|
|
31131
|
+
const fileCount = typeof st.fileCount === "number" ? st.fileCount : void 0;
|
|
31132
|
+
const embCount = typeof st.embCount === "number" ? st.embCount : void 0;
|
|
31133
|
+
if (present === false) {
|
|
31134
|
+
return { kind: "missing-projection", repo, localPresent: local.present, fileCount: 0, embCount: 0 };
|
|
31135
|
+
}
|
|
31136
|
+
if (builtAt) {
|
|
31137
|
+
const ageMs = Date.now() - Date.parse(builtAt);
|
|
31138
|
+
if (Number.isFinite(ageMs) && ageMs > 48 * 60 * 60 * 1e3) {
|
|
31139
|
+
return {
|
|
31140
|
+
kind: "stale",
|
|
31141
|
+
repo,
|
|
31142
|
+
builtAt,
|
|
31143
|
+
fileCount,
|
|
31144
|
+
embCount,
|
|
31145
|
+
localPresent: local.present,
|
|
31146
|
+
detail: `projection builtAt ${builtAt} (>48h old)`
|
|
31147
|
+
};
|
|
31148
|
+
}
|
|
31149
|
+
}
|
|
31150
|
+
return {
|
|
31151
|
+
kind: "healthy",
|
|
31152
|
+
repo: typeof st.repo === "string" ? st.repo : repo,
|
|
31153
|
+
builtAt,
|
|
31154
|
+
fileCount,
|
|
31155
|
+
embCount,
|
|
31156
|
+
localPresent: local.present
|
|
31157
|
+
};
|
|
31158
|
+
} catch (e) {
|
|
31159
|
+
return {
|
|
31160
|
+
kind: "network",
|
|
31161
|
+
detail: e instanceof Error ? e.message : String(e),
|
|
31162
|
+
localPresent: local.present
|
|
31163
|
+
};
|
|
31164
|
+
}
|
|
29953
31165
|
}
|
|
29954
31166
|
};
|
|
29955
31167
|
}
|
|
@@ -29997,7 +31209,7 @@ function argvWantsJson2() {
|
|
|
29997
31209
|
var unknownFlagJsonHandled = false;
|
|
29998
31210
|
var PARSE_HINT_SENTINEL = "@@mmi-parse-hint@@";
|
|
29999
31211
|
var DISCOVERY_HINT = "run `mmi-cli commands` for the agentic-coding map, `mmi-cli explain <command>` for detail, or `mmi-cli commands --json --primary` for machine discovery";
|
|
30000
|
-
var STALE_HINT = `(${DISCOVERY_HINT}). If you expected this command to exist, your installed mmi-cli may be stale
|
|
31212
|
+
var STALE_HINT = `(${DISCOVERY_HINT}). If you expected this command to exist, your installed mmi-cli may be stale ? update it (see: mmi-cli doctor)`;
|
|
30001
31213
|
var lastParseErrorKind = "other";
|
|
30002
31214
|
var lastUnknownCommand;
|
|
30003
31215
|
function classifyParseError(plain) {
|
|
@@ -30063,7 +31275,7 @@ function resolveParseHint() {
|
|
|
30063
31275
|
const path2 = commandPath2(cmd);
|
|
30064
31276
|
return [
|
|
30065
31277
|
`Usage: mmi-cli ${path2} ${cmd.usage()}`,
|
|
30066
|
-
"This command exists and your mmi-cli is not the problem
|
|
31278
|
+
"This command exists and your mmi-cli is not the problem ? the ARGUMENTS did not parse.",
|
|
30067
31279
|
`Run \`mmi-cli ${path2} --help\` for its signature, \`mmi-cli explain ${path2}\` for detail, or \`mmi-cli commands\` for the full map.`
|
|
30068
31280
|
].join("\n");
|
|
30069
31281
|
}
|
|
@@ -30124,7 +31336,7 @@ function envelopeAwareWriteErr(str) {
|
|
|
30124
31336
|
process.stderr.write(str);
|
|
30125
31337
|
}
|
|
30126
31338
|
var program2 = new Command();
|
|
30127
|
-
program2.name("mmi-cli").description("MMI Future Hub CLI
|
|
31339
|
+
program2.name("mmi-cli").description("MMI Future Hub CLI ? the org control plane for agentic coding.").version(resolveClientVersion()).configureOutput({ writeErr: envelopeAwareWriteErr }).showHelpAfterError(PARSE_HINT_SENTINEL);
|
|
30128
31340
|
function appActorDeps() {
|
|
30129
31341
|
return {
|
|
30130
31342
|
fetchSecret: async (key) => fetchSecretValue(makeSecretsDeps(await loadConfig()), key, { repo: APP_VAULT_REPO }),
|
|
@@ -30156,19 +31368,19 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
|
30156
31368
|
});
|
|
30157
31369
|
var rules = program2.command("rules").description("org-managed .gitignore delivery");
|
|
30158
31370
|
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) => {
|
|
30159
|
-
const path2 = (0,
|
|
30160
|
-
const current = (0,
|
|
31371
|
+
const path2 = (0, import_node_path38.join)(process.cwd(), ".gitignore");
|
|
31372
|
+
const current = (0, import_node_fs39.existsSync)(path2) ? (0, import_node_fs39.readFileSync)(path2, "utf8") : null;
|
|
30161
31373
|
const plan = planManagedGitignore(current);
|
|
30162
31374
|
const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
|
|
30163
31375
|
if (opts.json) {
|
|
30164
|
-
if (opts.write && plan.changed) (0,
|
|
31376
|
+
if (opts.write && plan.changed) (0, import_node_fs39.writeFileSync)(path2, plan.content, "utf8");
|
|
30165
31377
|
console.log(JSON.stringify(plan, null, 2));
|
|
30166
31378
|
if (!opts.write && plan.changed) process.exitCode = 1;
|
|
30167
31379
|
return;
|
|
30168
31380
|
}
|
|
30169
31381
|
if (opts.write) {
|
|
30170
31382
|
if (plan.changed) {
|
|
30171
|
-
(0,
|
|
31383
|
+
(0, import_node_fs39.writeFileSync)(path2, plan.content, "utf8");
|
|
30172
31384
|
console.log(`mmi-cli org rules gitignore: updated .gitignore (${drift})`);
|
|
30173
31385
|
} else {
|
|
30174
31386
|
console.log("mmi-cli org rules gitignore: up to date");
|
|
@@ -30176,7 +31388,7 @@ rules.command("gitignore").option("--write", "upsert the managed block into .git
|
|
|
30176
31388
|
return;
|
|
30177
31389
|
}
|
|
30178
31390
|
if (plan.changed) {
|
|
30179
|
-
console.error(`mmi-cli org rules gitignore: managed block drift (${drift})
|
|
31391
|
+
console.error(`mmi-cli org rules gitignore: managed block drift (${drift}) ? run \`mmi-cli org rules gitignore --write\` and commit`);
|
|
30180
31392
|
process.exitCode = 1;
|
|
30181
31393
|
} else {
|
|
30182
31394
|
console.log("mmi-cli org rules gitignore: up to date");
|
|
@@ -30282,7 +31494,7 @@ wave.command("land").description("serial merge open PRs to development with reba
|
|
|
30282
31494
|
});
|
|
30283
31495
|
var gcCmd = program2.command("gc").description("dry-run cleanup for merged/closed PR local+remote branches, linked worktrees, stale tracking refs, and worktree metadata (--scratch: safe local scratch + confirmed-synced plans)");
|
|
30284
31496
|
var DEFERRED_SWEEP_HARD_TIMEOUT_MS = 12e4;
|
|
30285
|
-
gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree removals with backoff (#1932)
|
|
31497
|
+
gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree removals with backoff (#1932) ? SessionStart + pr land spawn this detached").option("--quiet", "silent when registry empty or all cleared").option("--json", "machine-readable output").action(async (o) => {
|
|
30286
31498
|
await runWithSweepWatchdog(async () => {
|
|
30287
31499
|
try {
|
|
30288
31500
|
const deferredStore = await createDeferredWorktreeStore();
|
|
@@ -30290,7 +31502,7 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
|
|
|
30290
31502
|
const result = await sweepDeferredWorktreesWithRetry(
|
|
30291
31503
|
deferredStore,
|
|
30292
31504
|
// #2841: `-c core.fsmonitor=false` stops a git fsmonitor daemon from inheriting this detached
|
|
30293
|
-
// worker's stdio pipe (the likely Windows hang
|
|
31505
|
+
// worker's stdio pipe (the likely Windows hang ? execFile then never sees EOF and never resolves).
|
|
30294
31506
|
worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
|
|
30295
31507
|
{ removalContext }
|
|
30296
31508
|
);
|
|
@@ -30305,11 +31517,11 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
|
|
|
30305
31517
|
fail(`worktree gc sweep-deferred: ${e.message}`);
|
|
30306
31518
|
}
|
|
30307
31519
|
}, DEFERRED_SWEEP_HARD_TIMEOUT_MS, () => {
|
|
30308
|
-
console.error("worktree gc sweep-deferred: exceeded hard timeout
|
|
31520
|
+
console.error("worktree gc sweep-deferred: exceeded hard timeout ? exiting so the detached worker cannot leak (#2841)");
|
|
30309
31521
|
process.exit(process.exitCode ?? 0);
|
|
30310
31522
|
});
|
|
30311
31523
|
});
|
|
30312
|
-
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("--force", "remove worktrees even when the ownership registry shows another session created or worked in them recently (#3580)").option("--root <path>", "sweep an explicitly named worktrees root instead of the authoritative ../mmi-worktrees (#3471)
|
|
31524
|
+
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("--force", "remove worktrees even when the ownership registry shows another session created or worked in them recently (#3580)").option("--root <path>", "sweep an explicitly named worktrees root instead of the authoritative ../mmi-worktrees (#3471) ? descends into this repo container when present; ownership, dead-dir, and content guards still apply").action(async (o) => {
|
|
30313
31525
|
if (o.apply && o.dryRun) return fail("worktree gc: choose either --dry-run or --apply");
|
|
30314
31526
|
if (o.scratch) {
|
|
30315
31527
|
try {
|
|
@@ -30325,11 +31537,11 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
|
|
|
30325
31537
|
if (!Number.isFinite(limit) || limit < 1) return fail("worktree gc: --limit must be a positive integer");
|
|
30326
31538
|
let root;
|
|
30327
31539
|
if (o.root !== void 0) {
|
|
30328
|
-
root = (0,
|
|
30329
|
-
if (!(0,
|
|
31540
|
+
root = (0, import_node_path38.resolve)(o.root);
|
|
31541
|
+
if (!(0, import_node_fs39.existsSync)(root) || !(0, import_node_fs39.statSync)(root).isDirectory()) return fail(`worktree gc: --root ${o.root} is not a directory`);
|
|
30330
31542
|
const gcRepoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
30331
31543
|
if (isPathUnderDirectory(gcRepoRoot, root)) {
|
|
30332
|
-
return fail(`worktree gc: --root ${root} contains this checkout
|
|
31544
|
+
return fail(`worktree gc: --root ${root} contains this checkout ? name a worktrees root, not the repo or an ancestor of it`);
|
|
30333
31545
|
}
|
|
30334
31546
|
}
|
|
30335
31547
|
try {
|
|
@@ -30343,7 +31555,7 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
|
|
|
30343
31555
|
const removalContext = await currentWorktreeRemovalContext("worktree gc", o.force);
|
|
30344
31556
|
await sweepDeferredWorktrees(
|
|
30345
31557
|
deferredStore,
|
|
30346
|
-
// #2841: same `-c core.fsmonitor=false` guard as the detached `gc sweep-deferred` worker
|
|
31558
|
+
// #2841: same `-c core.fsmonitor=false` guard as the detached `gc sweep-deferred` worker ? keep a
|
|
30347
31559
|
// git fsmonitor daemon from inheriting this sweep's stdio pipe and wedging the git call on Windows.
|
|
30348
31560
|
worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
|
|
30349
31561
|
removalContext
|
|
@@ -30368,11 +31580,11 @@ var WORKTREE_SETUP_LOCK_TTL_MS = 10 * 6e4;
|
|
|
30368
31580
|
function runWorktreeInstall(command, cwd, quiet, opts) {
|
|
30369
31581
|
const stdio = quiet ? "ignore" : "inherit";
|
|
30370
31582
|
return new Promise((resolve5, reject) => {
|
|
30371
|
-
const child2 = opts?.shell ? (0,
|
|
31583
|
+
const child2 = opts?.shell ? (0, import_node_child_process18.spawn)(command, { cwd, stdio, windowsHide: true, shell: true }) : (() => {
|
|
30372
31584
|
const [bin, ...args] = command.split(" ");
|
|
30373
31585
|
const file = isWin2 ? "cmd.exe" : bin;
|
|
30374
31586
|
const spawnArgs = isWin2 ? ["/c", bin, ...args] : args;
|
|
30375
|
-
return (0,
|
|
31587
|
+
return (0, import_node_child_process18.spawn)(file, spawnArgs, { cwd, stdio, windowsHide: true });
|
|
30376
31588
|
})();
|
|
30377
31589
|
const timer = setTimeout(() => {
|
|
30378
31590
|
try {
|
|
@@ -30405,14 +31617,14 @@ async function currentWorktreeRemovalContext(command, force) {
|
|
|
30405
31617
|
};
|
|
30406
31618
|
}
|
|
30407
31619
|
async function unprovenWorktreeReason(wtPath, repoRoot2) {
|
|
30408
|
-
if (!(0,
|
|
31620
|
+
if (!(0, import_node_fs39.existsSync)(wtPath)) return `${wtPath} does not exist on disk`;
|
|
30409
31621
|
const porcelain = (await execFileP2("git", ["-C", repoRoot2, "worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout;
|
|
30410
31622
|
const registered = parseWorktreePorcelainEntries(porcelain);
|
|
30411
31623
|
if (!registered.length) {
|
|
30412
31624
|
return `'git worktree list' could not be read from ${repoRoot2}, so ${wtPath} could not be proven registered`;
|
|
30413
31625
|
}
|
|
30414
31626
|
if (!registered.some((w) => samePath(w.path, wtPath))) {
|
|
30415
|
-
return `${wtPath} exists on disk but 'git worktree list' does not name it
|
|
31627
|
+
return `${wtPath} exists on disk but 'git worktree list' does not name it ? its git metadata is gone, so it is not a usable worktree`;
|
|
30416
31628
|
}
|
|
30417
31629
|
return void 0;
|
|
30418
31630
|
}
|
|
@@ -30427,26 +31639,26 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
|
|
|
30427
31639
|
function acquireWorktreeSetupLock(worktreeRoot) {
|
|
30428
31640
|
const lockPath = repoRuntimeStatePath(worktreeRoot, "worktree-setup.lock");
|
|
30429
31641
|
const take = () => {
|
|
30430
|
-
const fd = (0,
|
|
31642
|
+
const fd = (0, import_node_fs39.openSync)(lockPath, "wx");
|
|
30431
31643
|
try {
|
|
30432
|
-
(0,
|
|
31644
|
+
(0, import_node_fs39.writeSync)(fd, String(Date.now()));
|
|
30433
31645
|
} finally {
|
|
30434
|
-
(0,
|
|
31646
|
+
(0, import_node_fs39.closeSync)(fd);
|
|
30435
31647
|
}
|
|
30436
31648
|
return () => {
|
|
30437
31649
|
try {
|
|
30438
|
-
(0,
|
|
31650
|
+
(0, import_node_fs39.rmSync)(lockPath, { force: true });
|
|
30439
31651
|
} catch {
|
|
30440
31652
|
}
|
|
30441
31653
|
};
|
|
30442
31654
|
};
|
|
30443
31655
|
try {
|
|
30444
|
-
(0,
|
|
31656
|
+
(0, import_node_fs39.mkdirSync)((0, import_node_path38.dirname)(lockPath), { recursive: true });
|
|
30445
31657
|
return take();
|
|
30446
31658
|
} catch {
|
|
30447
31659
|
try {
|
|
30448
|
-
if (Date.now() - (0,
|
|
30449
|
-
(0,
|
|
31660
|
+
if (Date.now() - (0, import_node_fs39.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
|
|
31661
|
+
(0, import_node_fs39.rmSync)(lockPath, { force: true });
|
|
30450
31662
|
return take();
|
|
30451
31663
|
}
|
|
30452
31664
|
} catch {
|
|
@@ -30454,15 +31666,15 @@ function acquireWorktreeSetupLock(worktreeRoot) {
|
|
|
30454
31666
|
return null;
|
|
30455
31667
|
}
|
|
30456
31668
|
}
|
|
30457
|
-
var worktree = program2.command("worktree").description("self-provisioning worktrees
|
|
31669
|
+
var worktree = program2.command("worktree").description("self-provisioning worktrees ? install deps + copy local-only config");
|
|
30458
31670
|
withExamples(mutating(
|
|
30459
|
-
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>
|
|
31671
|
+
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> ? 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"),
|
|
30460
31672
|
worktreeCreatePlan
|
|
30461
31673
|
).action(async (target, o, cmd) => {
|
|
30462
31674
|
let step = "resolve the branch name";
|
|
30463
31675
|
try {
|
|
30464
31676
|
if (o.base !== void 0 && cmd.getOptionValueSource("from") === "cli" && o.base !== o.from) {
|
|
30465
|
-
return fail(`worktree create: --from and --base are the same option (--base is an alias); got --from ${o.from} and --base ${o.base}
|
|
31677
|
+
return fail(`worktree create: --from and --base are the same option (--base is an alias); got --from ${o.from} and --base ${o.base} ? pass one`);
|
|
30466
31678
|
}
|
|
30467
31679
|
const fromRef = o.base ?? o.from;
|
|
30468
31680
|
const issueForm = isIssueRef(target);
|
|
@@ -30475,7 +31687,7 @@ withExamples(mutating(
|
|
|
30475
31687
|
const slug = o.slug ?? await fetchIssueTitle(selector.repo, selector.number).then((t) => t ? slugifyIssueTitle(t) : "");
|
|
30476
31688
|
branch = buildNewBranchName(selector.number, slug ?? "");
|
|
30477
31689
|
if (PROTECTED_BRANCHES2.has(branch)) {
|
|
30478
|
-
return fail(`worktree create: generated branch name '${branch}' collides with a protected train branch
|
|
31690
|
+
return fail(`worktree create: generated branch name '${branch}' collides with a protected train branch ? pass a branch name instead`);
|
|
30479
31691
|
}
|
|
30480
31692
|
} else if (o.claim || o.for) {
|
|
30481
31693
|
return fail("worktree create: --claim/--for need an issue-ref argument (e.g. worktree create 2687 --claim)");
|
|
@@ -30499,7 +31711,7 @@ withExamples(mutating(
|
|
|
30499
31711
|
if (preferRemote && await revParseRef(preferRemote)) base = preferRemote;
|
|
30500
31712
|
if (!o.json) {
|
|
30501
31713
|
const baseSha = (await revParseRef(base))?.slice(0, 12) ?? "unresolved";
|
|
30502
|
-
const localOnly = preferRemote && base !== preferRemote ? ` (no ${preferRemote}
|
|
31714
|
+
const localOnly = preferRemote && base !== preferRemote ? ` (no ${preferRemote} ? local ref)` : "";
|
|
30503
31715
|
console.error(` base ${base} ${baseSha}${localOnly}`);
|
|
30504
31716
|
}
|
|
30505
31717
|
const registered = parseWorktreePorcelainEntries((await execFileP2(
|
|
@@ -30515,7 +31727,7 @@ withExamples(mutating(
|
|
|
30515
31727
|
return fail(`worktree create: ${wtPath} is already registered for '${exact.branch ?? "detached HEAD"}', not '${branch}'`);
|
|
30516
31728
|
}
|
|
30517
31729
|
const status = (await execFileP2("git", ["-C", wtPath, "status", "--porcelain"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
|
|
30518
|
-
if (status) return fail(`worktree create: refusing to resume ${wtPath}
|
|
31730
|
+
if (status) return fail(`worktree create: refusing to resume ${wtPath} ? it has uncommitted changes`);
|
|
30519
31731
|
const head = await revParseRef(`refs/heads/${branch}`);
|
|
30520
31732
|
const baseOid = await revParseRef(base);
|
|
30521
31733
|
if (!head || !baseOid) return fail(`worktree create: could not prove '${branch}' and '${base}' before resume`);
|
|
@@ -30525,7 +31737,7 @@ withExamples(mutating(
|
|
|
30525
31737
|
{ timeout: GIT_TIMEOUT_MS }
|
|
30526
31738
|
).then(() => true).catch(() => false);
|
|
30527
31739
|
if (!canFastForward) {
|
|
30528
|
-
return fail(`worktree create: refusing to resume '${branch}'
|
|
31740
|
+
return fail(`worktree create: refusing to resume '${branch}' ? it has local commits or diverges from ${base}`);
|
|
30529
31741
|
}
|
|
30530
31742
|
await execFileP2("git", ["-C", wtPath, "merge", "--ff-only", base], { timeout: GIT_TIMEOUT_MS });
|
|
30531
31743
|
resumed = true;
|
|
@@ -30598,11 +31810,11 @@ withExamples(mutating(
|
|
|
30598
31810
|
console.log(` installed: ${report.installed.map((i) => i.dir || ".").join(", ") || "none"}`);
|
|
30599
31811
|
console.log(` copied: ${report.copied.join(", ") || "none"}`);
|
|
30600
31812
|
if (issueForm && o.claim && selector) {
|
|
30601
|
-
if (claimError) console.error(` warning: board claim failed (${claimError})
|
|
31813
|
+
if (claimError) console.error(` warning: board claim failed (${claimError}) ? worktree is ready, finish the claim manually`);
|
|
30602
31814
|
else console.log(` board item ${selector.repo}#${selector.number} claimed`);
|
|
30603
31815
|
}
|
|
30604
31816
|
} catch (e) {
|
|
30605
|
-
fail(`worktree create: failed at step '${step}'
|
|
31817
|
+
fail(`worktree create: failed at step '${step}' ? ${e.message}`);
|
|
30606
31818
|
}
|
|
30607
31819
|
}), [
|
|
30608
31820
|
"mmi-cli worktree create fix/gc-sweep-2687",
|
|
@@ -30613,7 +31825,7 @@ worktree.command("setup [path]").description("provision an existing worktree (in
|
|
|
30613
31825
|
const root = path2 ?? process.cwd();
|
|
30614
31826
|
const release = acquireWorktreeSetupLock(root);
|
|
30615
31827
|
if (!release) {
|
|
30616
|
-
if (!o.quiet && !o.json) console.log("worktree setup: another provision is in progress
|
|
31828
|
+
if (!o.quiet && !o.json) console.log("worktree setup: another provision is in progress ? skipping");
|
|
30617
31829
|
return;
|
|
30618
31830
|
}
|
|
30619
31831
|
try {
|
|
@@ -30633,7 +31845,7 @@ worktree.command("setup [path]").description("provision an existing worktree (in
|
|
|
30633
31845
|
release();
|
|
30634
31846
|
}
|
|
30635
31847
|
});
|
|
30636
|
-
worktree.command("events").description("who created and who removed this repo's worktrees
|
|
31848
|
+
worktree.command("events").description("who created and who removed this repo's worktrees ? the append-only attribution log (#3580)").option("--limit <n>", "most recent events to show", "50").option("--json", "machine-readable output").action(async (o) => {
|
|
30637
31849
|
const limit = Number.parseInt(o.limit, 10);
|
|
30638
31850
|
if (!Number.isFinite(limit) || limit < 1) return fail("worktree events: --limit must be a positive integer");
|
|
30639
31851
|
const primaryRoot = await primaryCheckoutRoot(process.cwd()) ?? process.cwd();
|
|
@@ -30651,11 +31863,11 @@ async function attachToProject(issueNumber, repo, priority) {
|
|
|
30651
31863
|
try {
|
|
30652
31864
|
cfg = await loadConfigForRepo(targetRepo2);
|
|
30653
31865
|
} catch (e) {
|
|
30654
|
-
console.error(`issue create: board attach skipped
|
|
31866
|
+
console.error(`issue create: board attach skipped ? ${e.message}`);
|
|
30655
31867
|
return { onBoard: false };
|
|
30656
31868
|
}
|
|
30657
31869
|
if (!cfg.projectId) {
|
|
30658
|
-
console.error(`issue create: board attach skipped
|
|
31870
|
+
console.error(`issue create: board attach skipped ? no Hub registry board META for ${targetRepo2 ?? "current repo"}; run \`mmi-cli org project get ${targetRepo2 ?? "<owner/repo>"}\` and backfill board coords`);
|
|
30659
31871
|
return { onBoard: false };
|
|
30660
31872
|
}
|
|
30661
31873
|
try {
|
|
@@ -30701,7 +31913,7 @@ function scheduleRelatedDiscovery(o) {
|
|
|
30701
31913
|
try {
|
|
30702
31914
|
const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body, "--fail-soft"];
|
|
30703
31915
|
if (o.repo) args.push("--repo", o.repo);
|
|
30704
|
-
spawnDetachedSelf(args, { spawn:
|
|
31916
|
+
spawnDetachedSelf(args, { spawn: import_node_child_process18.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
|
|
30705
31917
|
} catch {
|
|
30706
31918
|
}
|
|
30707
31919
|
}
|
|
@@ -30710,14 +31922,236 @@ registerEdgeCommands(program2);
|
|
|
30710
31922
|
registerBoxCommands(program2);
|
|
30711
31923
|
registerSchedulesCommands(program2);
|
|
30712
31924
|
registerSchedulesLiftCommand(program2);
|
|
30713
|
-
var
|
|
30714
|
-
|
|
31925
|
+
var repoIndex = program2.command("repo-index").description("Hub cloud + local pointer index ? paths/symbols only, never wiki prose (Hub#4133)");
|
|
31926
|
+
repoIndex.command("rebuild").description("walk the checkout (git ls-files + gitignore/secrets walls), rebuild the local pointer index, drop orphans").option("--json", "machine-readable projection summary").action(async (o) => {
|
|
31927
|
+
try {
|
|
31928
|
+
const root = await repoRoot();
|
|
31929
|
+
const repo = inferRepoSlug(root);
|
|
31930
|
+
const built = rebuildRepoIndex(root, repo);
|
|
31931
|
+
const symbolCount = built.entries.reduce((n, e) => n + e.symbols.length, 0);
|
|
31932
|
+
if (o.json) {
|
|
31933
|
+
consoleIo.log(JSON.stringify({
|
|
31934
|
+
ok: true,
|
|
31935
|
+
repo: built.repo,
|
|
31936
|
+
builtAt: built.builtAt,
|
|
31937
|
+
fileCount: built.entries.length,
|
|
31938
|
+
symbolCount,
|
|
31939
|
+
store: repoIndexStatus(root).path
|
|
31940
|
+
}, null, 2));
|
|
31941
|
+
return;
|
|
31942
|
+
}
|
|
31943
|
+
console.log(`repo-index: rebuilt ${built.repo} ? ${built.entries.length} files, ${symbolCount} symbols`);
|
|
31944
|
+
} catch (e) {
|
|
31945
|
+
await failGraceful(e.message);
|
|
31946
|
+
}
|
|
31947
|
+
});
|
|
31948
|
+
repoIndex.command("publish").description("publish this checkout's projection to Hub cloud (rebuilds locally first; estate indexer also runs on a schedule)").option("--embed", "request Titan file embeddings on the Hub (capped per publish)").option("--json", "machine-readable result").action(async (o) => {
|
|
31949
|
+
try {
|
|
31950
|
+
const root = await repoRoot();
|
|
31951
|
+
const repo = inferRepoSlug(root);
|
|
31952
|
+
const built = rebuildRepoIndex(root, repo);
|
|
31953
|
+
const cfg = await loadConfig();
|
|
31954
|
+
const res = await publishRepoIndexCloud(
|
|
31955
|
+
{ repo, builtAt: built.builtAt, entries: built.entries, embed: o.embed === true },
|
|
31956
|
+
registryClientDeps(cfg)
|
|
31957
|
+
);
|
|
31958
|
+
if (!res.ok) return await failGraceful(res.error);
|
|
31959
|
+
if (o.json) {
|
|
31960
|
+
consoleIo.log(JSON.stringify(res.body, null, 2));
|
|
31961
|
+
return;
|
|
31962
|
+
}
|
|
31963
|
+
console.log(`repo-index: published ${res.body.repo} ? ${res.body.fileCount} files, emb=${res.body.embCount ?? 0}`);
|
|
31964
|
+
} catch (e) {
|
|
31965
|
+
return await failGraceful(e.message);
|
|
31966
|
+
}
|
|
31967
|
+
});
|
|
31968
|
+
repoIndex.command("search").description("search Hub cloud by default (lexical/hybrid/semantic); use --local for checkout-only").argument("<query>", "path fragment, symbol, or meaning phrase").option("--limit <n>", "max hits", "20").option("--json", "machine-readable hits").option("--local", "search only the local projection (rebuild if missing)").option("--cloud", "force Hub cloud search (default when not --local)").option("--semantic", "semantic mode (Titan cosine over path+symbols embeddings)").option("--lexical", "lexical-only mode (paths/symbols)").option("--repo <owner/name>", "limit cloud search to one repo").action(async (query, o) => {
|
|
31969
|
+
try {
|
|
31970
|
+
const limit = Math.max(1, Math.min(100, Number(o.limit ?? 20) || 20));
|
|
31971
|
+
const useLocal = o.local === true && o.cloud !== true;
|
|
31972
|
+
if (useLocal) {
|
|
31973
|
+
const root = await repoRoot();
|
|
31974
|
+
let idx = loadRepoIndex(root);
|
|
31975
|
+
if (!idx) idx = rebuildRepoIndex(root, inferRepoSlug(root));
|
|
31976
|
+
const hits = searchRepoIndex(idx, query, limit);
|
|
31977
|
+
if (o.json) {
|
|
31978
|
+
consoleIo.log(JSON.stringify({ ok: true, source: "local", repo: idx.repo, query, hits }, null, 2));
|
|
31979
|
+
return;
|
|
31980
|
+
}
|
|
31981
|
+
if (hits.length === 0) {
|
|
31982
|
+
console.log(`repo-index: no local hits for ${JSON.stringify(query)}`);
|
|
31983
|
+
return;
|
|
31984
|
+
}
|
|
31985
|
+
for (const h of hits) {
|
|
31986
|
+
const sym = h.symbol ? ` ${h.symbol}` : "";
|
|
31987
|
+
console.log(`${h.score.toFixed(2)} ${h.kind.padEnd(8)} ${h.path}${sym} (${h.why})`);
|
|
31988
|
+
}
|
|
31989
|
+
return;
|
|
31990
|
+
}
|
|
31991
|
+
const mode = o.semantic ? "semantic" : o.lexical ? "lexical" : "hybrid";
|
|
31992
|
+
const cfg = await loadConfig();
|
|
31993
|
+
const res = await searchRepoIndexCloud(query, { mode, limit, repo: o.repo }, registryClientDeps(cfg));
|
|
31994
|
+
if (!res.ok) return await failGraceful(res.error);
|
|
31995
|
+
if (o.json) {
|
|
31996
|
+
consoleIo.log(JSON.stringify({ source: "cloud", ...res }, null, 2));
|
|
31997
|
+
return;
|
|
31998
|
+
}
|
|
31999
|
+
if (res.hits.length === 0) {
|
|
32000
|
+
console.log(`repo-index: no cloud hits for ${JSON.stringify(query)} (${res.mode})`);
|
|
32001
|
+
return;
|
|
32002
|
+
}
|
|
32003
|
+
for (const h of res.hits) {
|
|
32004
|
+
const sym = h.symbol ? ` ${h.symbol}` : "";
|
|
32005
|
+
console.log(`${h.score.toFixed(2)} ${h.kind.padEnd(8)} ${h.repo} ${h.path}${sym} (${h.why})`);
|
|
32006
|
+
}
|
|
32007
|
+
} catch (e) {
|
|
32008
|
+
return await failGraceful(e.message);
|
|
32009
|
+
}
|
|
32010
|
+
});
|
|
32011
|
+
repoIndex.command("status").description("show local and/or cloud repo-index status").option("--json", "machine-readable status").option("--cloud", "query Hub cloud status (estate or --repo)").option("--repo <owner/name>", "cloud status for one repo").action(async (o) => {
|
|
32012
|
+
try {
|
|
32013
|
+
if (o.cloud || o.repo) {
|
|
32014
|
+
const cfg = await loadConfig();
|
|
32015
|
+
const st2 = await statusRepoIndexCloud(o.repo, registryClientDeps(cfg));
|
|
32016
|
+
if (isRepoIndexStatusError(st2)) {
|
|
32017
|
+
if (o.json) {
|
|
32018
|
+
consoleIo.log(JSON.stringify(st2, null, 2));
|
|
32019
|
+
return await failGraceful(st2.error);
|
|
32020
|
+
}
|
|
32021
|
+
return await failGraceful(explainRepoIndexStatusError(st2));
|
|
32022
|
+
}
|
|
32023
|
+
if (o.json) {
|
|
32024
|
+
consoleIo.log(JSON.stringify(st2, null, 2));
|
|
32025
|
+
return;
|
|
32026
|
+
}
|
|
32027
|
+
if (st2.present === true && typeof st2.repo === "string") {
|
|
32028
|
+
const fileCount = Number(st2.fileCount ?? 0);
|
|
32029
|
+
const embCount = Number(st2.embCount ?? 0);
|
|
32030
|
+
const embGap = Number(st2.embGap ?? Math.max(0, fileCount - embCount));
|
|
32031
|
+
const stale = st2.staleBuiltAt === true ? " stale-builtAt" : "";
|
|
32032
|
+
console.log(
|
|
32033
|
+
`repo-index: cloud ${st2.repo} built ${st2.builtAt} ? ${fileCount} files, emb=${embCount}/${fileCount} gap=${embGap}${stale}`
|
|
32034
|
+
);
|
|
32035
|
+
return;
|
|
32036
|
+
}
|
|
32037
|
+
consoleIo.log(JSON.stringify(st2, null, 2));
|
|
32038
|
+
const count = typeof st2.count === "number" ? st2.count : void 0;
|
|
32039
|
+
const present = st2.present;
|
|
32040
|
+
if (count === 0 || present === false) {
|
|
32041
|
+
console.error(
|
|
32042
|
+
"repo-index: no cloud projection yet ? after Hub deploy, run harbour `repo-index-reconcile` or `mmi-cli repo-index sync-estate` (see docs/Guides/repo-index-runbook.md)."
|
|
32043
|
+
);
|
|
32044
|
+
}
|
|
32045
|
+
return;
|
|
32046
|
+
}
|
|
32047
|
+
const root = await repoRoot();
|
|
32048
|
+
const st = repoIndexStatus(root);
|
|
32049
|
+
if (o.json) {
|
|
32050
|
+
consoleIo.log(JSON.stringify(st, null, 2));
|
|
32051
|
+
return;
|
|
32052
|
+
}
|
|
32053
|
+
if (!st.present) {
|
|
32054
|
+
console.log(`repo-index: local missing ? cloud search needs no rebuild; optional \`repo-index rebuild\` for --local`);
|
|
32055
|
+
return;
|
|
32056
|
+
}
|
|
32057
|
+
console.log(`repo-index: local ${st.repo} built ${st.builtAt} ? ${st.fileCount} files, ${st.symbolCount} symbols`);
|
|
32058
|
+
} catch (e) {
|
|
32059
|
+
return await failGraceful(e.message);
|
|
32060
|
+
}
|
|
32061
|
+
});
|
|
32062
|
+
repoIndex.command("health").description("post-deploy health gate: status + golden lexical/semantic queries (Hub#4149)").option("--live", "hit Hub cloud (status + searches); omit for offline golden-shape check").option("--golden <path>", "golden queries JSON (default: cli/testdata/repo-index-golden-queries.json)").option("--json", "machine-readable findings").action(async (o) => {
|
|
32063
|
+
try {
|
|
32064
|
+
const root = await repoRoot();
|
|
32065
|
+
const goldenPath = o.golden || defaultGoldenPath(root);
|
|
32066
|
+
const golden = loadGoldenSuite(goldenPath);
|
|
32067
|
+
const result = o.live ? await runRepoIndexHealth({
|
|
32068
|
+
golden,
|
|
32069
|
+
live: {
|
|
32070
|
+
status: async () => {
|
|
32071
|
+
const cfg = await loadConfig();
|
|
32072
|
+
const st = await statusRepoIndexCloud(void 0, registryClientDeps(cfg));
|
|
32073
|
+
if ("error" in st && st.ok === false) return { ok: false, error: st.error };
|
|
32074
|
+
return st;
|
|
32075
|
+
},
|
|
32076
|
+
search: async (q, mode) => {
|
|
32077
|
+
const cfg = await loadConfig();
|
|
32078
|
+
const res = await searchRepoIndexCloud(q, { mode, limit: 10 }, registryClientDeps(cfg));
|
|
32079
|
+
if (!res.ok) {
|
|
32080
|
+
return {
|
|
32081
|
+
ok: false,
|
|
32082
|
+
error: res.error,
|
|
32083
|
+
status: res.status
|
|
32084
|
+
};
|
|
32085
|
+
}
|
|
32086
|
+
return { ok: true, hits: res.hits };
|
|
32087
|
+
}
|
|
32088
|
+
}
|
|
32089
|
+
}) : await runRepoIndexHealth({ golden });
|
|
32090
|
+
if (o.json) {
|
|
32091
|
+
consoleIo.log(JSON.stringify({ ok: result.ok, findings: result.findings, golden: goldenPath }, null, 2));
|
|
32092
|
+
} else {
|
|
32093
|
+
for (const f of result.findings) {
|
|
32094
|
+
console.log(`${f.ok ? "ok" : "FAIL"} ${f.code} ${f.detail}`);
|
|
32095
|
+
}
|
|
32096
|
+
console.log(result.ok ? "repo-index health: PASS" : "repo-index health: FAIL");
|
|
32097
|
+
}
|
|
32098
|
+
if (!result.ok) process.exitCode = 1;
|
|
32099
|
+
} catch (e) {
|
|
32100
|
+
return await failGraceful(e.message);
|
|
32101
|
+
}
|
|
32102
|
+
});
|
|
32103
|
+
repoIndex.command("gc").description("remove cloud projections for repos no longer on the registry roster").option("--cloud", "required ? GC only applies to Hub cloud").option("--json", "machine-readable result").action(async (o) => {
|
|
32104
|
+
try {
|
|
32105
|
+
if (!o.cloud) return await failGraceful("repo-index gc requires --cloud");
|
|
32106
|
+
const cfg = await loadConfig();
|
|
32107
|
+
const res = await gcRepoIndexCloud(registryClientDeps(cfg));
|
|
32108
|
+
if (!res.ok) return await failGraceful(res.error);
|
|
32109
|
+
if (o.json) {
|
|
32110
|
+
consoleIo.log(JSON.stringify(res, null, 2));
|
|
32111
|
+
return;
|
|
32112
|
+
}
|
|
32113
|
+
console.log(`repo-index: gc removed ${res.removed.length} orphan projection(s)`);
|
|
32114
|
+
} catch (e) {
|
|
32115
|
+
return await failGraceful(e.message);
|
|
32116
|
+
}
|
|
32117
|
+
});
|
|
32118
|
+
repoIndex.command("sync-estate").description("Hub indexer: shallow-clone registry repos, rebuild, publish to cloud (CI / operator)").option("--repo <owner/name>", "limit to one registered repo").option("--embed", "request Titan embeddings on publish").option("--json", "machine-readable result").action(async (o) => {
|
|
32119
|
+
try {
|
|
32120
|
+
const cfg = await loadConfig();
|
|
32121
|
+
const gh = process.env.GH_TOKEN || process.env.GITHUB_TOKEN || "";
|
|
32122
|
+
if (!gh) await failGraceful("sync-estate needs GH_TOKEN or GITHUB_TOKEN with contents:read on target repos");
|
|
32123
|
+
const res = await syncEstateRepoIndex({
|
|
32124
|
+
deps: registryClientDeps(cfg),
|
|
32125
|
+
repo: o.repo,
|
|
32126
|
+
embed: o.embed === true,
|
|
32127
|
+
githubToken: gh
|
|
32128
|
+
});
|
|
32129
|
+
if (o.json) {
|
|
32130
|
+
consoleIo.log(JSON.stringify(res, null, 2));
|
|
32131
|
+
if (!res.ok) process.exitCode = 1;
|
|
32132
|
+
return;
|
|
32133
|
+
}
|
|
32134
|
+
for (const p of res.published) {
|
|
32135
|
+
const gap = p.embGap ?? Math.max(0, p.fileCount - (p.embCount ?? 0));
|
|
32136
|
+
const trunc = p.embTruncated ? " truncated" : "";
|
|
32137
|
+
console.log(`repo-index: published ${p.repo} ? ${p.fileCount} files emb=${p.embCount ?? 0}/${p.fileCount} gap=${gap}${trunc}`);
|
|
32138
|
+
}
|
|
32139
|
+
for (const f of res.failed) {
|
|
32140
|
+
console.error(`repo-index: FAILED ${f.repo}: ${f.error}`);
|
|
32141
|
+
}
|
|
32142
|
+
if (!res.ok) await failGraceful(`sync-estate incomplete (${res.failed.length} failed)`);
|
|
32143
|
+
} catch (e) {
|
|
32144
|
+
await failGraceful(e.message);
|
|
32145
|
+
}
|
|
32146
|
+
});
|
|
32147
|
+
var docs = program2.command("docs").description("generated docs surfaces ? the routing index (org knowledge layer)");
|
|
32148
|
+
docs.command("index").description("regenerate docs/index.md from the docs/ tree (--write, the default) or fail on drift (--check) ? the generated routing index, never hand-maintained").option("--check", "compare against the committed docs/index.md and exit 1 on drift; never write").option("--write", "regenerate docs/index.md when it has drifted (the default)").action(async (o) => {
|
|
30715
32149
|
try {
|
|
30716
32150
|
const root = await repoRoot();
|
|
30717
32151
|
const result = docsIndex(createDocsIndexDeps(root), { check: Boolean(o.check) });
|
|
30718
32152
|
if (o.check) {
|
|
30719
32153
|
if (result.drift) {
|
|
30720
|
-
return failGraceful(`docs index: ${DOCS_INDEX_PATH} is stale
|
|
32154
|
+
return failGraceful(`docs index: ${DOCS_INDEX_PATH} is stale ? run \`mmi-cli docs index --write\` and commit the result`);
|
|
30721
32155
|
}
|
|
30722
32156
|
console.log(`docs index: ${DOCS_INDEX_PATH} is current`);
|
|
30723
32157
|
return;
|
|
@@ -30727,7 +32161,7 @@ docs.command("index").description("regenerate docs/index.md from the docs/ tree
|
|
|
30727
32161
|
await failGraceful(e.message);
|
|
30728
32162
|
}
|
|
30729
32163
|
});
|
|
30730
|
-
docs.command("refs").description("deterministic doc reference gate: every backticked repo path, relative .md link, `mmi-cli` command ref, and `<!-- pinned by
|
|
32164
|
+
docs.command("refs").description("deterministic doc reference gate: every backticked repo path, relative .md link, `mmi-cli` command ref, and `<!-- pinned by ? -->` comment across docs/** + README.md + architecture.md must resolve, or exit 1 (fleet-portable port of the Hub gate scripts/check-doc-refs.mjs, #3339)").option("--json", "machine-readable findings list: { ok, docCount, findings[], warnings[] }").action(async (o) => {
|
|
30731
32165
|
try {
|
|
30732
32166
|
const root = await repoRoot();
|
|
30733
32167
|
const commandPaths = new Set(buildCommandManifest(program2).index.map((entry) => entry.path));
|
|
@@ -30751,7 +32185,7 @@ docs.command("refs").description("deterministic doc reference gate: every backti
|
|
|
30751
32185
|
await failGraceful(e.message);
|
|
30752
32186
|
}
|
|
30753
32187
|
});
|
|
30754
|
-
var spawnCmd = program2.command("spawn").description("this repo's process-spawn contract
|
|
32188
|
+
var spawnCmd = program2.command("spawn").description("this repo's process-spawn contract ? every child process must be unable to pop a console window");
|
|
30755
32189
|
spawnCmd.command("policy").description("enforce the windowsHide contract across this repo's tracked source: a child_process call must set windowsHide, or take its options from a named constant, a type annotation, or a forwarded caller bag that does. Waive a call the scan cannot classify with a `// windows-hide-exempt: <reason>` comment on the line above it (#3979)").option("--json", "machine-readable result: { ok, scannedCount, findings[] }").action(async (o) => {
|
|
30756
32190
|
try {
|
|
30757
32191
|
const root = await repoRoot();
|
|
@@ -30767,14 +32201,14 @@ spawnCmd.command("policy").description("enforce the windowsHide contract across
|
|
|
30767
32201
|
}
|
|
30768
32202
|
for (const f of result.findings) console.error(`spawn policy: ${f.detail}`);
|
|
30769
32203
|
console.error(
|
|
30770
|
-
"\nA process spawned without windowsHide allocates a console when it has none to inherit.\nWhere Windows Terminal is the default terminal, that console becomes a desktop window the\nhost never reaps
|
|
32204
|
+
"\nA process spawned without windowsHide allocates a console when it has none to inherit.\nWhere Windows Terminal is the default terminal, that console becomes a desktop window the\nhost never reaps ? it outlives the process as an empty frame. Set windowsHide: true, or\nroute the call through a helper that does."
|
|
30771
32205
|
);
|
|
30772
32206
|
process.exitCode = 1;
|
|
30773
32207
|
} catch (e) {
|
|
30774
32208
|
await failGraceful(e.message);
|
|
30775
32209
|
}
|
|
30776
32210
|
});
|
|
30777
|
-
var tests = program2.command("tests").description("a repo's test-policy.json
|
|
32211
|
+
var tests = program2.command("tests").description("a repo's test-policy.json ? the opt-in test contract and its enforcement");
|
|
30778
32212
|
tests.command("policy").description("enforce this repo's test-policy.json against the diff: a mandatory-zone change must carry a test, an unrequested new test file is refused, a `protected` test file may not be deleted or renamed away, and a `protected` entry naming a missing file is refused. Override any of them with a `Test-Policy-Override: <reason>` commit trailer (#3605)").option("--json", "machine-readable result: { ok, base, changedCount, findings[] }").option("--base <ref>", "comparison base (default: TEST_POLICY_BASE, then origin/development, then origin/main)").action(async (o) => {
|
|
30779
32213
|
try {
|
|
30780
32214
|
const root = await repoRoot();
|
|
@@ -30787,7 +32221,7 @@ tests.command("policy").description("enforce this repo's test-policy.json agains
|
|
|
30787
32221
|
if (result.overriddenBy && result.ok) {
|
|
30788
32222
|
const { sha, reason, kinds } = result.overriddenBy;
|
|
30789
32223
|
const scope = (result.waived ?? []).map((f) => f.kind).join(", ") || "nothing";
|
|
30790
|
-
console.log(`tests policy: overridden by ${sha.slice(0, 8)} (waiving ${scope}; scope ${kinds.join(", ")})
|
|
32224
|
+
console.log(`tests policy: overridden by ${sha.slice(0, 8)} (waiving ${scope}; scope ${kinds.join(", ")}) ? ${reason}`);
|
|
30791
32225
|
return;
|
|
30792
32226
|
}
|
|
30793
32227
|
if (result.ok) {
|
|
@@ -30802,7 +32236,7 @@ tests.command("policy").description("enforce this repo's test-policy.json agains
|
|
|
30802
32236
|
await failGraceful(e.message);
|
|
30803
32237
|
}
|
|
30804
32238
|
});
|
|
30805
|
-
var docsAudit = program2.command("docs-audit").description("the docs janitor verdict ledger
|
|
32239
|
+
var docsAudit = program2.command("docs-audit").description("the docs janitor verdict ledger ? record a dated run verdict, or read the dead-man status back");
|
|
30806
32240
|
docsAudit.command("record").description("write a dated janitor verdict for one repo to the registry ledger (master-only server-side)").option("--repo <owner/name>", "the repo the verdict is for (default: the current repo)").option("--date <YYYY-MM-DD>", "the ISO day the run examined (default: today)").requiredOption("--sha-range <a..b>", "the git range the janitor read (e.g. <lastVerdictSha>..HEAD)").addOption(new Option("--outcome <kind>", "clean | refreshed | failed").makeOptionMandatory().choices(["clean", "refreshed", "failed"])).option("--count <n>", "docs refreshed (required when --outcome refreshed)").option("--reason <text>", "why the run failed (required when --outcome failed)").requiredOption("--checker-vendor <vendor>", "which vendor's model actually ran the check").action(async (o) => {
|
|
30807
32241
|
try {
|
|
30808
32242
|
const repo = o.repo ?? await currentRepoFullName();
|
|
@@ -30818,7 +32252,7 @@ docsAudit.command("record").description("write a dated janitor verdict for one r
|
|
|
30818
32252
|
await failGraceful(e.message);
|
|
30819
32253
|
}
|
|
30820
32254
|
});
|
|
30821
|
-
docsAudit.command("status").description("read the janitor dead-man verdict back for one repo
|
|
32255
|
+
docsAudit.command("status").description("read the janitor dead-man verdict back for one repo ? missing/stale/failed is RED; a not-yet-armed registry route is an informational exit 0").option("--repo <owner/name>", "the repo to check (default: the current repo)").option("--json", "machine-readable output ? the discrete state rather than the sentence").action(async (o) => {
|
|
30822
32256
|
try {
|
|
30823
32257
|
const repo = o.repo ?? await currentRepoFullName();
|
|
30824
32258
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
@@ -30851,7 +32285,7 @@ async function reportWrite(label, res) {
|
|
|
30851
32285
|
}
|
|
30852
32286
|
if (res.error) return failGraceful(`${label}: ${res.error}`);
|
|
30853
32287
|
const detail = res.body?.error ?? "";
|
|
30854
|
-
return failGraceful(`${label}: HTTP ${res.status}${detail ? `
|
|
32288
|
+
return failGraceful(`${label}: HTTP ${res.status}${detail ? ` ? ${detail}` : ""}`);
|
|
30855
32289
|
}
|
|
30856
32290
|
var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
|
|
30857
32291
|
tenant.command("control <owner/repo> <stage> <action>").description("bounded tenant control plus value-free vault/broker verification; project-admin own dev/rc, master main").option("--watch", "block on the dispatched run and report its conclusion (status/retire/verify-secrets/verify-broker/logs watch by default)").option("--lines <n>", "logs only: trailing lines of the tenant service to return (1-2000, default 200)").option("--json", "machine-readable output").action(async (repo, stage, action, o) => {
|
|
@@ -30878,7 +32312,7 @@ tenant.command("control <owner/repo> <stage> <action>").description("bounded ten
|
|
|
30878
32312
|
printLine(o.json ? JSON.stringify(result, null, 2) : renderTenantControl(result));
|
|
30879
32313
|
}
|
|
30880
32314
|
if (result.conclusion === "failure") {
|
|
30881
|
-
return failGraceful(`runtime tenant control ${stage} ${action}: ${result.category ?? "failed"}
|
|
32315
|
+
return failGraceful(`runtime tenant control ${stage} ${action}: ${result.category ?? "failed"} ? ${result.note}`);
|
|
30882
32316
|
}
|
|
30883
32317
|
} catch (e) {
|
|
30884
32318
|
return failGraceful(`runtime tenant control: ${e.message}`);
|
|
@@ -30887,7 +32321,7 @@ tenant.command("control <owner/repo> <stage> <action>").description("bounded ten
|
|
|
30887
32321
|
tenant.command("reconcile <owner/repo> <stage>").description("re-render this tenant stage's generated box assets from registry truth; project-admin own dev/rc, master main; watches by default").option("--watch", "wait for the tenant-reconcile.yml run (default)").option("--no-watch", "return after dispatch; do not redeploy until the reconcile run succeeds").option("--json", "machine-readable output").action(async (repo, stage, o) => {
|
|
30888
32322
|
try {
|
|
30889
32323
|
const result = await runTenantReconcile(trainApplyDeps(), { repo, stage, watch: o.watch });
|
|
30890
|
-
printLine(o.json ? JSON.stringify(result, null, 2) : `${result.note}${result.runUrl ? `
|
|
32324
|
+
printLine(o.json ? JSON.stringify(result, null, 2) : `${result.note}${result.runUrl ? ` ? ${result.runUrl}` : ""}`);
|
|
30891
32325
|
if (result.conclusion === "failure" || !result.dispatched) process.exitCode = 1;
|
|
30892
32326
|
} catch (e) {
|
|
30893
32327
|
return failGraceful(`runtime tenant reconcile: ${e.message}`);
|
|
@@ -30900,7 +32334,7 @@ tenant.command("status <owner/repo> <stage>").description("read tenant runtime r
|
|
|
30900
32334
|
console.log(JSON.stringify(result, null, 2));
|
|
30901
32335
|
if (result.publicProbe?.ok === false) process.exitCode = 1;
|
|
30902
32336
|
});
|
|
30903
|
-
tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the central tenant-deploy.yml for an already-promoted ref (no re-tag/merge); train-authority gated").option("--ref <ref>", "ref to deploy (defaults to the stage branch rc/main, or `development` for dev
|
|
32337
|
+
tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the central tenant-deploy.yml for an already-promoted ref (no re-tag/merge); train-authority gated").option("--ref <ref>", "ref to deploy (defaults to the stage branch rc/main, or `development` for dev ? the promoted/staging ref)").option("--watch", "block on the dispatched run and report its outcome (gh run watch --exit-status)").option("--json", "machine-readable output").action(async (repo, stage, o) => {
|
|
30904
32338
|
if (stage !== "dev" && stage !== "rc" && stage !== "main") return fail("runtime tenant redeploy: <stage> must be dev, rc, or main");
|
|
30905
32339
|
try {
|
|
30906
32340
|
const result = await runTenantRedeploy(trainApplyDeps(), { repo, stage, ref: o.ref, watch: o.watch });
|
|
@@ -30909,9 +32343,9 @@ tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the cen
|
|
|
30909
32343
|
return failGraceful(`runtime tenant redeploy: ${e.message}`);
|
|
30910
32344
|
}
|
|
30911
32345
|
});
|
|
30912
|
-
tenant.command("sweep-rc").description("discover (and optionally retire) running rc tenant runtimes across tenant-containers
|
|
32346
|
+
tenant.command("sweep-rc").description("discover (and optionally retire) running rc tenant runtimes across tenant-containers ? orphan cleanup after a failed post-release retire (#942)").option("--retire", "retire every running rc runtime found (requires --yes) ? WARNING: tears down a legitimately-staged rc too").option("--yes", "confirm the destructive --retire").option("--json", "machine-readable output").action(async (o) => {
|
|
30913
32347
|
if (o.retire && !o.yes) {
|
|
30914
|
-
return fail("runtime tenant sweep-rc --retire is destructive (it tears down EVERY running rc, including one legitimately staged between /rcand and /release)
|
|
32348
|
+
return fail("runtime tenant sweep-rc --retire is destructive (it tears down EVERY running rc, including one legitimately staged between /rcand and /release) ? re-run with --yes to confirm");
|
|
30915
32349
|
}
|
|
30916
32350
|
const cfg = await loadConfig();
|
|
30917
32351
|
const cdeps = registryClientDeps(cfg);
|
|
@@ -30964,7 +32398,7 @@ async function buildTenantRuntimeStatusFor(target, stage, cfg) {
|
|
|
30964
32398
|
lastTenantDeployRun: (await fetchLastTenantDeployRun(slug, stage)).run
|
|
30965
32399
|
});
|
|
30966
32400
|
}
|
|
30967
|
-
var project = program2.command("project").description("the DDB org registry
|
|
32401
|
+
var project = program2.command("project").description("the DDB org registry ? list/get projects (any member); set is master-only");
|
|
30968
32402
|
async function projectTarget(commandName, explicitTarget) {
|
|
30969
32403
|
return requireProjectTarget(commandName, explicitTarget, explicitTarget ? void 0 : await resolveRepo());
|
|
30970
32404
|
}
|
|
@@ -31008,7 +32442,7 @@ project.command("list").description("list all projects (identity + board, never
|
|
|
31008
32442
|
console.log(`${p.slug ?? "?"} - ${p.name ?? ""}${p.division ? ` [${p.division}]` : ""}${p.class ? ` (${p.class})` : ""}${p.projectType ? ` <${p.projectType}>` : ""}${p.deployModel ? ` {${p.deployModel}}` : ""}`);
|
|
31009
32443
|
}
|
|
31010
32444
|
});
|
|
31011
|
-
project.command("get [owner/repo]").description("a project's META (board ids + pointers) by repo or slug; defaults to the current repo
|
|
32445
|
+
project.command("get [owner/repo]").description("a project's META (board ids + pointers) by repo or slug; defaults to the current repo ? identity, NOT deploy coords").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
31012
32446
|
const cfg = await loadConfig();
|
|
31013
32447
|
let target;
|
|
31014
32448
|
try {
|
|
@@ -31018,7 +32452,7 @@ project.command("get [owner/repo]").description("a project's META (board ids + p
|
|
|
31018
32452
|
}
|
|
31019
32453
|
const read = await fetchProjectBySlugChecked(slugOf(target), registryClientDeps(cfg));
|
|
31020
32454
|
if (!read.ok) {
|
|
31021
|
-
return failGraceful(`org project get: Hub registry read failed (${read.error})
|
|
32455
|
+
return failGraceful(`org project get: Hub registry read failed (${read.error}) ? likely transient (cold start, network, or auth blip); retry shortly`);
|
|
31022
32456
|
}
|
|
31023
32457
|
if (!read.project) {
|
|
31024
32458
|
return failGraceful(`org project get: no registry META for ${target} (unknown or unbootstrapped)`);
|
|
@@ -31028,10 +32462,10 @@ project.command("get [owner/repo]").description("a project's META (board ids + p
|
|
|
31028
32462
|
const m = read.project;
|
|
31029
32463
|
const track = resolveReleaseTrack(m, void 0, target);
|
|
31030
32464
|
const stages = branchesForTrack(track).join(" -> ");
|
|
31031
|
-
const note = track === "direct" ? " (direct
|
|
32465
|
+
const note = track === "direct" ? " (direct ? no rc; /rcand refuses, /release ships development -> main)" : track === "trunk" ? " (trunk ? main only)" : "";
|
|
31032
32466
|
console.error(
|
|
31033
|
-
`${m.name ?? target}
|
|
31034
|
-
release track: ${track}
|
|
32467
|
+
`${m.name ?? target} ? class ${m.class ?? "?"} ? deploy ${m.deployModel ?? "?"}
|
|
32468
|
+
release track: ${track} ? stages: ${stages}${note}
|
|
31035
32469
|
deploys run centrally (tenant-deploy.yml); product repos carry no deploy files. Inspect nonsecret DEPLOY facts with \`mmi-cli org project deploy get\`; full coords remain OIDC-gated.`
|
|
31036
32470
|
);
|
|
31037
32471
|
}
|
|
@@ -31064,7 +32498,7 @@ projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# fa
|
|
|
31064
32498
|
const payload = stage ? { slug: out.slug, stage, deploy: out.stages[stage] ?? null } : out;
|
|
31065
32499
|
console.log(JSON.stringify(payload));
|
|
31066
32500
|
});
|
|
31067
|
-
project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").addOption(new Option("--class <class>", "deployable | content").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).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"
|
|
32501
|
+
project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").addOption(new Option("--class <class>", "deployable | content").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).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"|?], "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) => {
|
|
31068
32502
|
const cfg = await loadConfig();
|
|
31069
32503
|
let target;
|
|
31070
32504
|
try {
|
|
@@ -31080,7 +32514,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
31080
32514
|
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`);
|
|
31081
32515
|
if (o.secretsFile) {
|
|
31082
32516
|
try {
|
|
31083
|
-
vars.push(`secrets=${(0,
|
|
32517
|
+
vars.push(`secrets=${(0, import_node_fs39.readFileSync)(o.secretsFile, "utf8")}`);
|
|
31084
32518
|
} catch (e) {
|
|
31085
32519
|
return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
|
|
31086
32520
|
}
|
|
@@ -31105,7 +32539,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
31105
32539
|
const res = await upsertProject(slug, { ...patch, repo }, registryClientDeps(cfg));
|
|
31106
32540
|
return reportWrite("org project set", res);
|
|
31107
32541
|
});
|
|
31108
|
-
project.command("retire [owner/repo]").description("retire an orphaned registry slug (master-only): soft-delete the PROJECT# META row + drop the repo's board items; secrets/vault left alone. DRY-RUN by default
|
|
32542
|
+
project.command("retire [owner/repo]").description("retire an orphaned registry slug (master-only): soft-delete the PROJECT# META row + drop the repo's board items; secrets/vault left alone. DRY-RUN by default ? pass --apply to actually delete; defaults to the current repo").option("--apply", "actually delete (default is a dry-run preview of what would be removed)").option("--skip-board", "retire the registry META only ? leave board items in place").option("--json", "machine-readable {slug, removedMeta, removedBoardItem} output").action(async (repoOrSlug, o) => {
|
|
31109
32543
|
const cfg = await loadConfig();
|
|
31110
32544
|
let target;
|
|
31111
32545
|
try {
|
|
@@ -31137,9 +32571,9 @@ project.command("retire [owner/repo]").description("retire an orphaned registry
|
|
|
31137
32571
|
const verb = result.applied ? "retired" : "WOULD retire (dry-run; pass --apply to delete)";
|
|
31138
32572
|
printLine(`org project retire: ${verb} ${result.slug} (${result.repo})`);
|
|
31139
32573
|
if (result.applied) {
|
|
31140
|
-
printLine(` META: ${result.removedMeta.ok ? `removed=${result.removedMeta.removed ?? "unknown"}` : `FAILED
|
|
32574
|
+
printLine(` META: ${result.removedMeta.ok ? `removed=${result.removedMeta.removed ?? "unknown"}` : `FAILED ? ${result.removedMeta.error}`}`);
|
|
31141
32575
|
} else {
|
|
31142
|
-
printLine(` META: ${result.removedMeta.ok ? result.removedMeta.existing ? "live row present
|
|
32576
|
+
printLine(` META: ${result.removedMeta.ok ? result.removedMeta.existing ? "live row present ? would be tombstoned" : "no live row (already absent/tombstoned)" : `read failed ? ${result.removedMeta.error}`}`);
|
|
31143
32577
|
}
|
|
31144
32578
|
if (result.removedBoardItem) {
|
|
31145
32579
|
const b = result.removedBoardItem;
|
|
@@ -31150,7 +32584,7 @@ project.command("retire [owner/repo]").description("retire an orphaned registry
|
|
|
31150
32584
|
printLine(` ${result.vaultNote}`);
|
|
31151
32585
|
}
|
|
31152
32586
|
if (result.applied && !result.removedMeta.ok) {
|
|
31153
|
-
return failGraceful(`org project retire: META delete failed
|
|
32587
|
+
return failGraceful(`org project retire: META delete failed ? ${result.removedMeta.error}`);
|
|
31154
32588
|
}
|
|
31155
32589
|
});
|
|
31156
32590
|
var fullTrack = program2.command("full-track").description("direct-to-train readiness audits");
|
|
@@ -31178,7 +32612,7 @@ fullTrack.command("readiness <owner/repo>").description("aggregate branch topolo
|
|
|
31178
32612
|
console.log(JSON.stringify(report, null, 2));
|
|
31179
32613
|
if (!report.rcand.canApply) process.exitCode = 1;
|
|
31180
32614
|
});
|
|
31181
|
-
project.command("set-deploy [owner/repo]").description("patch a tenant DEPLOY row
|
|
32615
|
+
project.command("set-deploy [owner/repo]").description("patch a tenant DEPLOY row ? project-admin may set only --no-env-file on their own existing dev/rc row; master may seed/change all coords and main; defaults to the current repo").addOption(new Option("--stage <stage>", "dev | rc | main").makeOptionMandatory().choices(["dev", "rc", "main"])).option("--ssh-host <host>", "the box address the deploy ssh-es into; omit to keep the stored value (required only for a NEW hetzner-ssh row ? `mmi-cli runtime box list` finds it)").option("--ssh-user <user>", "ssh user; omit to leave the row unchanged (default root on a new row)").option("--port <port>", "loopback port the container binds / Caddy upstream (1..65535); omit to leave the row unchanged").addOption(new Option("--substrate <substrate>", "hetzner-ssh | s3-cloudfront; omit to leave the row unchanged").choices([...DEPLOY_SUBSTRATES])).option("--deploy-role-arn <arn>", "s3-cloudfront: the scoped OIDC deployer role tenant-deploy.yml assumes for this stage").option("--app-bucket <bucket>", "s3-cloudfront: the S3 bucket the built dist/ is synced into").option("--distribution-id <id>", "s3-cloudfront: the CloudFront distribution invalidated after the sync").option("--distribution-domain <domain>", "s3-cloudfront: the host the post-deploy security-header proof is measured against").option("--deploy-path <path>", "on-box per-stage release root; omit to leave the row unchanged (default /opt/mmi/<slug>/<stage> on a new row)").option("--service <name>", "systemd/compose service name; omit to leave the row unchanged (default the slug on a new row)").option("--domain <domain>", "canonical serving host; omit to leave the row unchanged").option("--alias <domain...>", "extra serving hostname the box Caddy answers (repeatable); omit to leave the stored aliases unchanged").option("--clear-aliases", "remove EVERY serving alias from the row (#2986) ? omitting --alias preserves them, so clearing needs saying out loud").option("--no-env-file <bool>", "set the DEPLOY# fileless flag (true|false) ? own-repo project-admin dev/rc; master main; true = env passthrough with no .env symlink").option("--force", "explicit recovery override: skip fileless-compose verification only after independent proof").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
31182
32616
|
const cfg = await loadConfig();
|
|
31183
32617
|
let target;
|
|
31184
32618
|
try {
|
|
@@ -31204,7 +32638,7 @@ ${filelessTransitionGuide(target, o.stage)}`);
|
|
|
31204
32638
|
if (verdict.warn) console.error(`org project set-deploy: ${verdict.warn}`);
|
|
31205
32639
|
} else if (!o.force) {
|
|
31206
32640
|
return fail(
|
|
31207
|
-
`org project set-deploy: cannot verify ${target} ${branch} from this checkout
|
|
32641
|
+
`org project set-deploy: cannot verify ${target} ${branch} from this checkout ? refusing to change noEnvFile. Run from the target repo with origin/${branch} fetched.
|
|
31208
32642
|
${filelessTransitionGuide(target, o.stage)}`
|
|
31209
32643
|
);
|
|
31210
32644
|
} else {
|
|
@@ -31239,14 +32673,14 @@ ${filelessTransitionGuide(target, o.stage)}`
|
|
|
31239
32673
|
const res = await setDeployCoords(slug, body, registryClientDeps(cfg));
|
|
31240
32674
|
return reportWrite("org project set-deploy", res);
|
|
31241
32675
|
});
|
|
31242
|
-
var registry = program2.command("registry").description("the DDB org registry
|
|
32676
|
+
var registry = program2.command("registry").description("the DDB org registry ? org-level constants");
|
|
31243
32677
|
registry.command("org").description("the org config (account id, region, orgProjectId, sagaApiUrl)").option("--json", "machine-readable output").action(async (_o) => {
|
|
31244
32678
|
const cfg = await loadConfig();
|
|
31245
32679
|
const org = await fetchOrgConfig(registryClientDeps(cfg));
|
|
31246
32680
|
if (!org) return failGraceful("org config get: Hub API unreachable, unseeded, or this repo is not bootstrapped");
|
|
31247
32681
|
console.log(JSON.stringify(org));
|
|
31248
32682
|
});
|
|
31249
|
-
var oauth = program2.command("oauth").description("per-repo Google OAuth
|
|
32683
|
+
var oauth = program2.command("oauth").description("per-repo Google OAuth ? plan the canonical URI set, verify the client is port-agnostic");
|
|
31250
32684
|
oauth.command("plan", { isDefault: true }).description("print the canonical JS origins + redirect URIs + SSM cred param names for this repo").option("--repo <owner/repo>", "slug source (defaults to the current repo)").option("--json", "machine-readable output").action(async (o) => {
|
|
31251
32685
|
const cfg = await loadConfig();
|
|
31252
32686
|
const slug = (o.repo ? o.repo.split("/").pop() : cfg.project ?? await repoSlug()).toLowerCase();
|
|
@@ -31260,7 +32694,7 @@ oauth.command("plan", { isDefault: true }).description("print the canonical JS o
|
|
|
31260
32694
|
return failGraceful(
|
|
31261
32695
|
`org oauth plan: ${message}. Declare it in the registry META first:
|
|
31262
32696
|
mmi-cli org project set ${o.repo ?? `mutmutco/${slug}`} --var 'oauth={"subdomains":["${defaultSubdomain(slug)}"],"callbackPath":"${DEFAULT_CALLBACK_PATH}"}'
|
|
31263
|
-
New repo? The GCP project and its Google Auth Platform consent screen must exist too
|
|
32697
|
+
New repo? The GCP project and its Google Auth Platform consent screen must exist too ? docs/Guides/oauth-provision.md ? New repo.`
|
|
31264
32698
|
);
|
|
31265
32699
|
}
|
|
31266
32700
|
return failGraceful(`org oauth plan: ${message}`);
|
|
@@ -31286,7 +32720,7 @@ SSM cred params (under /mmi-future/${slug}/):`);
|
|
|
31286
32720
|
oauth.command("set-creds").description('store the OAuth client into the canonical stageless GOOGLE_CLIENT_ID/SECRET pair (pipe the Console "Download JSON" on stdin)').option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (o) => {
|
|
31287
32721
|
const raw = await readStdin();
|
|
31288
32722
|
if (!raw.trim()) {
|
|
31289
|
-
return fail("org oauth set-creds: pipe the Google client JSON on stdin
|
|
32723
|
+
return fail("org oauth set-creds: pipe the Google client JSON on stdin ? e.g.\n mmi-cli org oauth set-creds --repo <owner/repo> < client.json");
|
|
31290
32724
|
}
|
|
31291
32725
|
let creds;
|
|
31292
32726
|
try {
|
|
@@ -31336,9 +32770,9 @@ oauth.command("verify").description("probe Google authorize with an arbitrary po
|
|
|
31336
32770
|
if (o.json) {
|
|
31337
32771
|
console.log(JSON.stringify({ slug, redirectUri, portAgnostic: !mismatch }));
|
|
31338
32772
|
} else if (mismatch) {
|
|
31339
|
-
console.error(`FAIL ${slug}: redirect_uri_mismatch for ${redirectUri}
|
|
32773
|
+
console.error(`FAIL ${slug}: redirect_uri_mismatch for ${redirectUri} ? client is not port-agnostic (run /oauth-provision)`);
|
|
31340
32774
|
} else {
|
|
31341
|
-
console.log(`PASS ${slug}: ${redirectUri} accepted
|
|
32775
|
+
console.log(`PASS ${slug}: ${redirectUri} accepted ? port-agnostic OAuth is live`);
|
|
31342
32776
|
}
|
|
31343
32777
|
if (mismatch) process.exitCode = 1;
|
|
31344
32778
|
});
|
|
@@ -31351,7 +32785,7 @@ function resolveCreatePriority(raw, command) {
|
|
|
31351
32785
|
try {
|
|
31352
32786
|
return normalizePriority(raw);
|
|
31353
32787
|
} catch {
|
|
31354
|
-
fail(`${command}: unknown priority "${raw}"
|
|
32788
|
+
fail(`${command}: unknown priority "${raw}" ? expected one of: ${CLI_PRIORITIES.join(", ")}`, {
|
|
31355
32789
|
code: ERROR_CODES.ERR_BAD_ENUM,
|
|
31356
32790
|
offending_flag: "--priority",
|
|
31357
32791
|
expected: [...CLI_PRIORITIES]
|
|
@@ -31361,15 +32795,15 @@ function resolveCreatePriority(raw, command) {
|
|
|
31361
32795
|
function resolveCreateType(raw, command, labels) {
|
|
31362
32796
|
if (raw === void 0 || raw === "") {
|
|
31363
32797
|
const nearMiss = labels?.find((l) => ISSUE_TYPES.includes(l));
|
|
31364
|
-
const hint = nearMiss ? ` (you passed --label ${nearMiss}
|
|
32798
|
+
const hint = nearMiss ? ` (you passed --label ${nearMiss} ? did you mean --type ${nearMiss}?)` : "";
|
|
31365
32799
|
fail(`${command}: --type is required (bug | feature | task) unless --batch supplies it per row${hint}`, {
|
|
31366
32800
|
code: ERROR_CODES.ERR_MISSING_FLAG,
|
|
31367
32801
|
offending_flag: "--type",
|
|
31368
|
-
...nearMiss ? { corrected_command: `${command} --type ${nearMiss}
|
|
32802
|
+
...nearMiss ? { corrected_command: `${command} --type ${nearMiss} ?` } : {}
|
|
31369
32803
|
});
|
|
31370
32804
|
}
|
|
31371
32805
|
if (!ISSUE_TYPES.includes(raw)) {
|
|
31372
|
-
fail(`${command}: unknown type "${raw}"
|
|
32806
|
+
fail(`${command}: unknown type "${raw}" ? expected one of: ${ISSUE_TYPES.join(", ")}`, {
|
|
31373
32807
|
code: ERROR_CODES.ERR_BAD_ENUM,
|
|
31374
32808
|
offending_flag: "--type",
|
|
31375
32809
|
expected: [...ISSUE_TYPES]
|
|
@@ -31383,9 +32817,9 @@ function resolveCreateSurface(opts) {
|
|
|
31383
32817
|
function surfaceWaived() {
|
|
31384
32818
|
return rawFlag("--no-surface");
|
|
31385
32819
|
}
|
|
31386
|
-
var issue = program2.command("issue").description("issues
|
|
32820
|
+
var issue = program2.command("issue").description("issues ? reliable create with structured output");
|
|
31387
32821
|
withExamples(mutating(
|
|
31388
|
-
issue.command("create").description("create an issue (type
|
|
32822
|
+
issue.command("create").description("create an issue (type ? label) and print {number,url,label} JSON").addOption(new Option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").choices([...ISSUE_TYPES])).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) ? prefer a real path; a title with backticks needs --title-file for the same reason (#3381)").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only ? 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("--surface <surface>", "issue surface, with or without the surface: prefix (#3789). Required when the target repo runs the one-surface-label board rule; any value satisfies it, so this is not a closed enum").option("--no-surface", "file without a surface label on a repo that requires one ? for a genuinely exempt filing (e.g. a coop proof issue that spans every surface)").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"),
|
|
31389
32823
|
// --dry-run/--validate-only plan: resolve the same title source and validate the same type, priority,
|
|
31390
32824
|
// and surface contract as the real action. A plan that echoes the title-file PATH instead of its value
|
|
31391
32825
|
// is not a plan of the mutation that will run (#3914).
|
|
@@ -31452,7 +32886,7 @@ withExamples(mutating(
|
|
|
31452
32886
|
}
|
|
31453
32887
|
targetRepo2 = await resolveRepo(o.repo);
|
|
31454
32888
|
if (!targetRepo2) {
|
|
31455
|
-
return fail("issue create: could not resolve the target repo
|
|
32889
|
+
return fail("issue create: could not resolve the target repo ? run inside a git checkout or pass --repo <owner/repo>");
|
|
31456
32890
|
}
|
|
31457
32891
|
args = buildIssueArgs({
|
|
31458
32892
|
type: issueType,
|
|
@@ -31481,7 +32915,7 @@ withExamples(mutating(
|
|
|
31481
32915
|
labels: extraLabels.length ? extraLabels : void 0
|
|
31482
32916
|
});
|
|
31483
32917
|
process.stderr.write(
|
|
31484
|
-
`warning: --surface ${surfaceFlagLabel} was dropped
|
|
32918
|
+
`warning: --surface ${surfaceFlagLabel} was dropped ? ${targetRepo2} defines no surface:* labels, and creating one here would switch the one-surface-label rule on for every later filing in it. Use --label ${surfaceFlagLabel} if you really mean to start that taxonomy.
|
|
31485
32919
|
`
|
|
31486
32920
|
);
|
|
31487
32921
|
}
|
|
@@ -31530,7 +32964,7 @@ async function readParentField(number, repo) {
|
|
|
31530
32964
|
}
|
|
31531
32965
|
return resolveParentField(payload);
|
|
31532
32966
|
}
|
|
31533
|
-
issue.command("view <number>").description('read an issue as structured JSON
|
|
32967
|
+
issue.command("view <number>").description('read an issue as structured JSON ? 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 ? 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) ? 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) => {
|
|
31534
32968
|
const n = Number(number);
|
|
31535
32969
|
if (!Number.isInteger(n) || n <= 0) return fail("issue view: <number> must be a positive integer");
|
|
31536
32970
|
const repo = await resolveRepo(o.repo);
|
|
@@ -31624,7 +33058,7 @@ jsonParity(issue.command("comment <ref>").description("post a Markdown comment t
|
|
|
31624
33058
|
return fail(`issue comment: ${e.message}`);
|
|
31625
33059
|
}
|
|
31626
33060
|
const repo = await resolveRepo(parsed.repo ?? o.repo);
|
|
31627
|
-
if (!repo) return fail("issue comment: could not resolve repo
|
|
33061
|
+
if (!repo) return fail("issue comment: could not resolve repo ? pass --repo owner/repo");
|
|
31628
33062
|
try {
|
|
31629
33063
|
const result = await postIssueComment(defaultGitHubClient(), { ref, defaultRepo: repo, body });
|
|
31630
33064
|
console.log(JSON.stringify(result));
|
|
@@ -31633,7 +33067,7 @@ jsonParity(issue.command("comment <ref>").description("post a Markdown comment t
|
|
|
31633
33067
|
return failGraceful(`issue comment: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
31634
33068
|
}
|
|
31635
33069
|
});
|
|
31636
|
-
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
|
|
33070
|
+
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 ? exact item text, else a unique substring").option("--off", "untick the item ([x] ? [ ]) instead of ticking it").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)")).action(async (ref, o) => {
|
|
31637
33071
|
let parsed;
|
|
31638
33072
|
try {
|
|
31639
33073
|
parsed = parseIssueRef(ref);
|
|
@@ -31641,7 +33075,7 @@ jsonParity(issue.command("check <ref>").description("tick (or with --off untick)
|
|
|
31641
33075
|
return fail(`issue check: ${e.message}`);
|
|
31642
33076
|
}
|
|
31643
33077
|
const repo = await resolveRepo(parsed.repo ?? o.repo);
|
|
31644
|
-
if (!repo) return fail("issue check: could not resolve repo
|
|
33078
|
+
if (!repo) return fail("issue check: could not resolve repo ? pass --repo owner/repo");
|
|
31645
33079
|
const checked = o.off !== true;
|
|
31646
33080
|
let body;
|
|
31647
33081
|
try {
|
|
@@ -31654,7 +33088,7 @@ jsonParity(issue.command("check <ref>").description("tick (or with --off untick)
|
|
|
31654
33088
|
if (!result.ok) {
|
|
31655
33089
|
if (result.reason === "ambiguous") {
|
|
31656
33090
|
const list = result.matches.map((m) => ` - ${m.text}`).join("\n");
|
|
31657
|
-
return fail(`issue check: "${o.item}" matches ${result.matches.length} checklist items in ${repo}#${parsed.number}
|
|
33091
|
+
return fail(`issue check: "${o.item}" matches ${result.matches.length} checklist items in ${repo}#${parsed.number} ? narrow the text:
|
|
31658
33092
|
${list}`);
|
|
31659
33093
|
}
|
|
31660
33094
|
return fail(`issue check: no checklist item matching "${o.item}" in ${repo}#${parsed.number}`);
|
|
@@ -31673,7 +33107,7 @@ ${list}`);
|
|
|
31673
33107
|
}
|
|
31674
33108
|
console.log(JSON.stringify({ number: parsed.number, repo, item: result.item.text, checked, changed: true }));
|
|
31675
33109
|
});
|
|
31676
|
-
program2.command("report").description("file a friction report on the Hub board (Hub session auth, dedups open reports) and print {number,url} JSON").option("--title <title>", "one-line friction summary").option("--title-file <path|->", "read the friction summary from a UTF-8 file, or from stdin with -").option("--body <body>", "report body (markdown)").option("--body-file <path|->", "read report body from a UTF-8 file, or from stdin with -").addOption(new Option("--type <type>", "bug | feature | task (sets the matching label)").default("task").choices([...ISSUE_TYPES])).option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only, #416)").option("--repo <owner/repo>", 'attribute the report to a different source repo than the current checkout for the "Filed via..." footer (rare
|
|
33110
|
+
program2.command("report").description("file a friction report on the Hub board (Hub session auth, dedups open reports) and print {number,url} JSON").option("--title <title>", "one-line friction summary").option("--title-file <path|->", "read the friction summary from a UTF-8 file, or from stdin with -").option("--body <body>", "report body (markdown)").option("--body-file <path|->", "read report body from a UTF-8 file, or from stdin with -").addOption(new Option("--type <type>", "bug | feature | task (sets the matching label)").default("task").choices([...ISSUE_TYPES])).option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only, #416)").option("--repo <owner/repo>", 'attribute the report to a different source repo than the current checkout for the "Filed via..." footer (rare ? usually auto-detected; every report always lands on the org Hub, never an alternate target, #263)').option("--force", "file a new issue even when an open report looks like a duplicate").option("--json", "machine-readable output (already the default ? report always prints JSON; #682)").action(async (o) => {
|
|
31677
33111
|
let body;
|
|
31678
33112
|
let priority;
|
|
31679
33113
|
let title;
|
|
@@ -31683,7 +33117,7 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
31683
33117
|
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises11.readFile, readStdin });
|
|
31684
33118
|
priority = resolveCreatePriority(o.priority, "report");
|
|
31685
33119
|
if (!ISSUE_TYPES.includes(o.type)) {
|
|
31686
|
-
throw new Error(`unknown issue type "${o.type}"
|
|
33120
|
+
throw new Error(`unknown issue type "${o.type}" ? expected one of: ${ISSUE_TYPES.join(", ")}`);
|
|
31687
33121
|
}
|
|
31688
33122
|
} catch (e) {
|
|
31689
33123
|
const m = e.message;
|
|
@@ -31720,7 +33154,7 @@ async function resolvePluginSha() {
|
|
|
31720
33154
|
return void 0;
|
|
31721
33155
|
}
|
|
31722
33156
|
}
|
|
31723
|
-
program2.command("skill-lesson").description("file a skill-lesson on the Hub board (GitHub auth, dedups open lessons) and print {number,url} JSON").addOption(new Option("--skill <name>", `which skill misfired (${SKILL_NAMES.join(" | ")})`).makeOptionMandatory().choices([...SKILL_NAMES])).option("--title <title>", "one-line summary of what misfired").option("--title-file <path|->", "read the one-line summary from a UTF-8 file, or from stdin with -").option("--body <body>", "lesson body: what misfired, the evidence, and the proposed amendment (markdown)").option("--body-file <path|->", "read the lesson body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only, #416)").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO3})`).option("--force", "file a new issue even when an open lesson looks like a duplicate").option("--json", "machine-readable output (already the default
|
|
33157
|
+
program2.command("skill-lesson").description("file a skill-lesson on the Hub board (GitHub auth, dedups open lessons) and print {number,url} JSON").addOption(new Option("--skill <name>", `which skill misfired (${SKILL_NAMES.join(" | ")})`).makeOptionMandatory().choices([...SKILL_NAMES])).option("--title <title>", "one-line summary of what misfired").option("--title-file <path|->", "read the one-line summary from a UTF-8 file, or from stdin with -").option("--body <body>", "lesson body: what misfired, the evidence, and the proposed amendment (markdown)").option("--body-file <path|->", "read the lesson body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only, #416)").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO3})`).option("--force", "file a new issue even when an open lesson looks like a duplicate").option("--json", "machine-readable output (already the default ? skill-lesson always prints JSON)").action(async (o) => {
|
|
31724
33158
|
const targetRepo2 = o.repo ?? HUB_REPO3;
|
|
31725
33159
|
const sourceRepo = await resolveRepo(void 0);
|
|
31726
33160
|
const pluginSha = await resolvePluginSha();
|
|
@@ -31775,7 +33209,7 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
|
|
|
31775
33209
|
repo: targetRepo2,
|
|
31776
33210
|
labels: [SKILL_LESSON_LABEL],
|
|
31777
33211
|
command: "skill-lesson",
|
|
31778
|
-
waiver: { reason: "tooling lesson spans product surfaces
|
|
33212
|
+
waiver: { reason: "tooling lesson spans product surfaces ? coop-proof class (#3789)" }
|
|
31779
33213
|
});
|
|
31780
33214
|
if (surfaceWarn) process.stderr.write(`${surfaceWarn}
|
|
31781
33215
|
`);
|
|
@@ -31787,7 +33221,7 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
|
|
|
31787
33221
|
const { projectItemId, onBoard } = await attachToProject(created.number, targetRepo2, priority);
|
|
31788
33222
|
console.log(JSON.stringify({ ...created, deduped: false, label: SKILL_LESSON_LABEL, skill, priority, projectItemId, onBoard }));
|
|
31789
33223
|
});
|
|
31790
|
-
var pr = program2.command("pr").description("pull requests
|
|
33224
|
+
var pr = program2.command("pr").description("pull requests ? reliable create with structured output");
|
|
31791
33225
|
withExamples(pr.command("create").description("create a PR and print {number,url} JSON").option("--title <title>", "PR title").option("--title-file <path|->", "read the PR title from a UTF-8 file, or from stdin with -").option("--body <body>", "PR body (markdown)").option("--body-file <path|->", "read PR body from a UTF-8 file, or from stdin with -").option("--base <branch>", "base branch (defaults to the repo default)").option("--head <branch>", "head branch (defaults to the current branch)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--draft", "open the PR in draft state (#2667)").option("--json", "machine-readable output (default; accepted for parity)").action(async (o) => {
|
|
31792
33226
|
let body;
|
|
31793
33227
|
let title;
|
|
@@ -31800,7 +33234,7 @@ withExamples(pr.command("create").description("create a PR and print {number,url
|
|
|
31800
33234
|
body = normalizeClosingDirectives(body);
|
|
31801
33235
|
const docsCheck = await checkDocsIndexAtHead({ repo: o.repo, head: o.head }, createPrCreateDocsIndexDeps());
|
|
31802
33236
|
if (docsCheck && !docsCheck.ok) {
|
|
31803
|
-
return fail(`pr create: ${docsCheck.detail}
|
|
33237
|
+
return fail(`pr create: ${docsCheck.detail} ? ${docsCheck.fix}`);
|
|
31804
33238
|
}
|
|
31805
33239
|
const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo, draft: o.draft }));
|
|
31806
33240
|
console.log(JSON.stringify(created));
|
|
@@ -31811,7 +33245,7 @@ withExamples(pr.command("create").description("create a PR and print {number,url
|
|
|
31811
33245
|
"--head and --base default to the current branch and the repo default; only pass them to override.",
|
|
31812
33246
|
"Use --body-file for multiline PR bodies instead of shell-escaped inline markdown."
|
|
31813
33247
|
]);
|
|
31814
|
-
pr.command("view <number>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit)
|
|
33248
|
+
pr.command("view <number>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit) ? the mmi-cli read path (#2347). --comments folds in every comment; --context also adds linkedIssues (the issues it closes/references, #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 ? in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "state,baseRefName,mergeCommit"').option("--comments", "include every comment (body + comments in one call) ? read the whole PR before landing it (#2894)").option("--context", "full working context in one call: implies --comments and also adds linkedIssues (the issues the PR closes/references) (#2894)").action(async (number, o) => {
|
|
31815
33249
|
const n = Number(number);
|
|
31816
33250
|
if (!Number.isInteger(n) || n <= 0) return fail("pr view: <number> must be a positive integer");
|
|
31817
33251
|
const repo = await resolveRepo(o.repo);
|
|
@@ -31832,11 +33266,11 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
|
|
|
31832
33266
|
}
|
|
31833
33267
|
});
|
|
31834
33268
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
31835
|
-
const wfDir = (0,
|
|
31836
|
-
if (!(0,
|
|
31837
|
-
return (0,
|
|
33269
|
+
const wfDir = (0, import_node_path38.join)(cwd, ".github", "workflows");
|
|
33270
|
+
if (!(0, import_node_fs39.existsSync)(wfDir)) return [];
|
|
33271
|
+
return (0, import_node_fs39.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
|
|
31838
33272
|
try {
|
|
31839
|
-
return workflowReportsPrChecks((0,
|
|
33273
|
+
return workflowReportsPrChecks((0, import_node_fs39.readFileSync)((0, import_node_path38.join)(wfDir, name), "utf8"));
|
|
31840
33274
|
} catch {
|
|
31841
33275
|
return true;
|
|
31842
33276
|
}
|
|
@@ -31868,16 +33302,16 @@ function ciAuditDeps() {
|
|
|
31868
33302
|
// gate re-seed step is skipped gracefully rather than failing mid-run.
|
|
31869
33303
|
readSeedFile: (path2) => {
|
|
31870
33304
|
if (!root) return null;
|
|
31871
|
-
const fullPath = (0,
|
|
31872
|
-
return (0,
|
|
33305
|
+
const fullPath = (0, import_node_path38.join)(root, path2);
|
|
33306
|
+
return (0, import_node_fs39.existsSync)(fullPath) ? (0, import_node_fs39.readFileSync)(fullPath, "utf8") : null;
|
|
31873
33307
|
}
|
|
31874
33308
|
};
|
|
31875
33309
|
}
|
|
31876
33310
|
function hubRoot() {
|
|
31877
|
-
const fromPkg = (0,
|
|
33311
|
+
const fromPkg = (0, import_node_path38.join)(__dirname, "..", "..");
|
|
31878
33312
|
const marker = "skills/bootstrap/seeds/manifest.json";
|
|
31879
|
-
if ((0,
|
|
31880
|
-
if ((0,
|
|
33313
|
+
if ((0, import_node_fs39.existsSync)((0, import_node_path38.join)(fromPkg, marker))) return fromPkg;
|
|
33314
|
+
if ((0, import_node_fs39.existsSync)((0, import_node_path38.join)(process.cwd(), marker))) return process.cwd();
|
|
31881
33315
|
return null;
|
|
31882
33316
|
}
|
|
31883
33317
|
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) => {
|
|
@@ -31885,7 +33319,7 @@ pr.command("ci-policy").description("report merge CI policy: wait-for-checks vs
|
|
|
31885
33319
|
if (o.json) return printLine(JSON.stringify(result));
|
|
31886
33320
|
printLine(`merge CI policy: ${result.policy} (${result.reason})`);
|
|
31887
33321
|
});
|
|
31888
|
-
pr.command("checks-wait <number>").description(`bounded wait for PR checks; skips immediately on no-ci repos (#1432), fails immediately on a CONFLICTING PR
|
|
33322
|
+
pr.command("checks-wait <number>").description(`bounded wait for PR checks; skips immediately on no-ci repos (#1432), fails immediately on a CONFLICTING PR ? GitHub never queues checks for one (#2970). REST-only polling with a pool floor (#3024). Default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m; --timeout raises it. Exit 1 = a check FAILED or the PR is CONFLICTING, exit ${PR_CHECKS_TIMEOUT_EXIT_CODE} = the wait window expired or the API pool ran dry (re-arm)`).option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--timeout <minutes>", `wait budget in minutes (default ${PR_CHECKS_TIMEOUT_MS / 6e4}) ? raise it for serial self-hosted e2e queues`).action(async (number, o) => {
|
|
31889
33323
|
let timeoutMs;
|
|
31890
33324
|
if (o.timeout !== void 0) {
|
|
31891
33325
|
const minutes = Number(o.timeout);
|
|
@@ -31910,24 +33344,27 @@ pr.command("checks-wait <number>").description(`bounded wait for PR checks; skip
|
|
|
31910
33344
|
timeoutMs,
|
|
31911
33345
|
// Liveness on stderr, one line per poll. A silent bounded wait is indistinguishable from a hang, and
|
|
31912
33346
|
// an agent harness kills it on its own (shorter) deadline before the verdict ever prints (#2940).
|
|
31913
|
-
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr checks-wait: ${state}
|
|
33347
|
+
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr checks-wait: ${state} ? ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
|
|
31914
33348
|
});
|
|
31915
33349
|
if (o.json) printLine(JSON.stringify(result));
|
|
31916
33350
|
else if (result.status === "conflicting") {
|
|
31917
|
-
printLine(`pr checks-wait: conflicting
|
|
33351
|
+
printLine(`pr checks-wait: conflicting ? ${result.reason}`);
|
|
31918
33352
|
} else if (result.status === "timeout") {
|
|
31919
|
-
|
|
33353
|
+
const stuckQueuing = /no-checks-reported/i.test(result.detail ?? "");
|
|
33354
|
+
printLine(
|
|
33355
|
+
`pr checks-wait: timeout ? 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>.` + (stuckQueuing ? " Tip: jobs never appeared ? mmi-live may be saturated or Actions may be failing before checkout; check runner health (docs/Guides/gh-runner-runbook.md) and re-run the workflow." : "")
|
|
33356
|
+
);
|
|
31920
33357
|
} else if (result.status === "rate-limited") {
|
|
31921
|
-
printLine(`pr checks-wait: rate-limited
|
|
33358
|
+
printLine(`pr checks-wait: rate-limited ? ${result.reason ?? "REST pool below floor"}. No check failed; re-run after the pool resets.`);
|
|
31922
33359
|
} else if (result.detail === "runner-infra") {
|
|
31923
|
-
printLine(`pr checks-wait: failure (runner infrastructure, NOT a test failure)
|
|
33360
|
+
printLine(`pr checks-wait: failure (runner infrastructure, NOT a test failure) ? ${result.reason}`);
|
|
31924
33361
|
} else if (result.detail === "stale-head") {
|
|
31925
|
-
printLine(`pr checks-wait: failure (stale PR head, NOT a test failure)
|
|
31926
|
-
} else printLine(`pr checks-wait: ${result.status}${result.reason ? `
|
|
33362
|
+
printLine(`pr checks-wait: failure (stale PR head, NOT a test failure) ? ${result.reason}`);
|
|
33363
|
+
} else printLine(`pr checks-wait: ${result.status}${result.reason ? ` ? ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
|
|
31927
33364
|
if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
|
|
31928
33365
|
if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
|
|
31929
33366
|
});
|
|
31930
|
-
pr.command("land <number>").description("agent merge path (#1440): train probe
|
|
33367
|
+
pr.command("land <number>").description("agent merge path (#1440): train probe ? checks-wait ? merge --auto ? poll enqueued ? development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--preserve-worktree", "after merge, keep the local PR worktree/stage/branch for an active batch (#1888)").option("--force", "acknowledge and land past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword refusal (#3718)").action(async (number, o) => {
|
|
31931
33368
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
31932
33369
|
const startingPath = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
31933
33370
|
assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr land", { force: o.force });
|
|
@@ -31953,8 +33390,8 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
31953
33390
|
} catch {
|
|
31954
33391
|
}
|
|
31955
33392
|
if (isPromotionBase(base, track)) {
|
|
31956
|
-
const shape = track ? `${track} track` : "unresolved track
|
|
31957
|
-
throw new Error(`pr land: base branch ${base} is a promotion target (${shape})
|
|
33393
|
+
const shape = track ? `${track} track` : "unresolved track ? strict reading";
|
|
33394
|
+
throw new Error(`pr land: base branch ${base} is a promotion target (${shape}) ? promotion merges stay human-only`);
|
|
31958
33395
|
}
|
|
31959
33396
|
return repo;
|
|
31960
33397
|
},
|
|
@@ -31962,23 +33399,23 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
31962
33399
|
resolveCiPolicy: (repo) => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
|
|
31963
33400
|
waitForChecks: (prNumber, repo) => waitForPrChecks({
|
|
31964
33401
|
resolvePolicy: () => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
|
|
31965
|
-
// #3024: REST-only wait loop
|
|
33402
|
+
// #3024: REST-only wait loop ? runPrLand's resolveRepo already guarantees `repo` is set.
|
|
31966
33403
|
pollChecks: () => pollRestPrChecks(prNumber, repo),
|
|
31967
33404
|
// #2970: `pr land` only ever lands PRs based on development (resolveRepo above already rejects any
|
|
31968
33405
|
// other base), so the fast-fail message's base branch is always 'development' here.
|
|
31969
33406
|
pollMergeable: () => pollRestPrMergeable(prNumber, repo),
|
|
31970
33407
|
pollRateLimit: () => fetchRestCorePool(),
|
|
31971
|
-
// #3388: `pr land` is the batch path
|
|
33408
|
+
// #3388: `pr land` is the batch path ? the one most likely to self-DOS the shared runner and
|
|
31972
33409
|
// then read its own wall-clock kill as a broken diff.
|
|
31973
33410
|
diagnoseFailure: () => diagnoseFailedRestChecks(prNumber, repo),
|
|
31974
33411
|
baseBranch: "development",
|
|
31975
33412
|
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
|
|
31976
33413
|
log: (message) => console.warn(message),
|
|
31977
|
-
// `pr land` inherits the same (raised) checks budget, so it needs the same liveness
|
|
33414
|
+
// `pr land` inherits the same (raised) checks budget, so it needs the same liveness ? otherwise the
|
|
31978
33415
|
// 30m wait is SILENT and reads exactly like the hang #2940 was filed about, only three times longer.
|
|
31979
|
-
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr land: waiting on checks
|
|
33416
|
+
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr land: waiting on checks ? ${state}, ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
|
|
31980
33417
|
}),
|
|
31981
|
-
// #2425: re-probe used ONLY to decide whether a failed `gh pr merge --auto` is worth retrying
|
|
33418
|
+
// #2425: re-probe used ONLY to decide whether a failed `gh pr merge --auto` is worth retrying ? a
|
|
31982
33419
|
// fresh read of state/mergeable/checks, independent of the merge call's own (possibly stale) error.
|
|
31983
33420
|
probeMergeReady: async (prNumber, repo) => {
|
|
31984
33421
|
const [snapshot, checks] = await Promise.all([
|
|
@@ -32008,7 +33445,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
32008
33445
|
});
|
|
32009
33446
|
if (result.status !== "failed") {
|
|
32010
33447
|
const repoFlag = result.repo ? ` --repo ${result.repo}` : "";
|
|
32011
|
-
console.warn(`pr land: merge confirmed; cleanup pending
|
|
33448
|
+
console.warn(`pr land: merge confirmed; cleanup pending ? if this process is interrupted, resume with: mmi-cli pr merge ${number}${repoFlag} --squash`);
|
|
32012
33449
|
}
|
|
32013
33450
|
if (result.status !== "failed") {
|
|
32014
33451
|
try {
|
|
@@ -32037,13 +33474,13 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
32037
33474
|
}
|
|
32038
33475
|
if (o.json) printLine(JSON.stringify(result));
|
|
32039
33476
|
else {
|
|
32040
|
-
printLine(`pr land: ${result.status}${result.error ? `
|
|
33477
|
+
printLine(`pr land: ${result.status}${result.error ? ` ? ${result.error}` : ""}`);
|
|
32041
33478
|
for (const line of renderPrLandCleanupLines(result.cleanup)) printLine(line);
|
|
32042
33479
|
if (result.cleanupError) printLine(`pr land cleanup: ${result.cleanupError}`);
|
|
32043
33480
|
}
|
|
32044
33481
|
if (result.status === "failed" || result.cleanupError) process.exitCode = 1;
|
|
32045
33482
|
});
|
|
32046
|
-
jsonParity(pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree
|
|
33483
|
+
jsonParity(pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree ? 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 ? 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) or a negated-closing-keyword refusal (#3718)")).action(async (number, o) => {
|
|
32047
33484
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
32048
33485
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
32049
33486
|
const repoForPostCleanup = await resolveRepo(o.repo) ?? o.repo;
|
|
@@ -32076,10 +33513,10 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
32076
33513
|
baseBranch,
|
|
32077
33514
|
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
|
|
32078
33515
|
log: (message) => console.warn(message),
|
|
32079
|
-
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr merge: --wait checks
|
|
33516
|
+
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr merge: --wait checks ? ${state}, ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
|
|
32080
33517
|
});
|
|
32081
33518
|
if (wait.status !== "success" && wait.status !== "skipped") {
|
|
32082
|
-
console.warn(`pr merge: --wait stopped before merge
|
|
33519
|
+
console.warn(`pr merge: --wait stopped before merge ? ${wait.status}${wait.reason ? `: ${wait.reason}` : ""}${wait.detail ? ` (${wait.detail})` : ""}`);
|
|
32083
33520
|
process.exitCode = wait.status === "timeout" || wait.status === "rate-limited" ? PR_CHECKS_TIMEOUT_EXIT_CODE : 1;
|
|
32084
33521
|
return;
|
|
32085
33522
|
}
|
|
@@ -32118,7 +33555,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
32118
33555
|
return;
|
|
32119
33556
|
}
|
|
32120
33557
|
if (!o.auto && basePolicyBlocksImmediateMerge(message)) {
|
|
32121
|
-
console.warn(`pr merge: the base-branch policy blocks an immediate merge
|
|
33558
|
+
console.warn(`pr merge: the base-branch policy blocks an immediate merge ? upgrading to --auto (merges once required checks pass).`);
|
|
32122
33559
|
upgradedToAuto = true;
|
|
32123
33560
|
await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: true, deleteBranch: !headIsProtected, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
|
|
32124
33561
|
const m2 = String(e2.message || "");
|
|
@@ -32149,7 +33586,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
32149
33586
|
const state = stateRead.ok ? stateRead.state : "";
|
|
32150
33587
|
const enqueued = describePrMergeEnqueuedReason(await pollGhPrChecks(number, repoArgs).catch(() => void 0));
|
|
32151
33588
|
console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", enqueuedReason: enqueued.reason, pr: number, branch: headRef, state: state || "unknown", upgradedToAuto: upgradedToAuto || void 0 }));
|
|
32152
|
-
console.warn(`pr merge: PR #${number} is ENQUEUED, not merged
|
|
33589
|
+
console.warn(`pr merge: PR #${number} is ENQUEUED, not merged ? ${enqueued.message}.`);
|
|
32153
33590
|
if (upgradedToAuto && !o.auto) {
|
|
32154
33591
|
console.warn(`pr merge: exiting ${PR_MERGE_ENQUEUED_EXIT_CODE} so a chained command does not treat this as a completed merge.`);
|
|
32155
33592
|
process.exitCode = PR_MERGE_ENQUEUED_EXIT_CODE;
|
|
@@ -32165,7 +33602,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
32165
33602
|
state: stateRead.ok ? stateRead.state : "unknown",
|
|
32166
33603
|
cleanupStatus: "skipped"
|
|
32167
33604
|
}));
|
|
32168
|
-
console.error(`pr merge: ${gate.message}. Nothing was deleted
|
|
33605
|
+
console.error(`pr merge: ${gate.message}. Nothing was deleted ? re-check the PR and retry.`);
|
|
32169
33606
|
process.exitCode = 1;
|
|
32170
33607
|
return;
|
|
32171
33608
|
}
|
|
@@ -32185,7 +33622,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
32185
33622
|
localCleanup = await cleanupPrMergeLocalBranch(headRef, {
|
|
32186
33623
|
beforeWorktrees,
|
|
32187
33624
|
startingPath,
|
|
32188
|
-
pathExists: (p) => (0,
|
|
33625
|
+
pathExists: (p) => (0, import_node_fs39.existsSync)(p),
|
|
32189
33626
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
32190
33627
|
teardownWorktreeStage,
|
|
32191
33628
|
deferredStore,
|
|
@@ -32259,7 +33696,7 @@ function trainApplyDeps() {
|
|
|
32259
33696
|
const verdict = await fetchTrainAuthority(repo, registryClientDeps(await loadConfig()));
|
|
32260
33697
|
return verdict.ok ? { ok: true, role: verdict.authority.role, train: verdict.authority.train } : verdict;
|
|
32261
33698
|
},
|
|
32262
|
-
// Hub-App-authority dispatch of the central tenant deploy (#953)
|
|
33699
|
+
// Hub-App-authority dispatch of the central tenant deploy (#953) ? the Hub fires the
|
|
32263
33700
|
// workflow_dispatch with its App token, so the caller needs no MMI-Hub Actions write.
|
|
32264
33701
|
dispatchTenantDeploy: async ({ repo, stage, ref }) => {
|
|
32265
33702
|
const res = await tenantDeploy({ repo, stage, ref }, registryClientDeps(await loadConfig()));
|
|
@@ -32268,7 +33705,7 @@ function trainApplyDeps() {
|
|
|
32268
33705
|
throw new Error(`tenant deploy dispatch failed: ${detail}`);
|
|
32269
33706
|
}
|
|
32270
33707
|
},
|
|
32271
|
-
// Hub-App-authority dispatch of the central tenant-control.yml (#1717)
|
|
33708
|
+
// Hub-App-authority dispatch of the central tenant-control.yml (#1717) ? the Hub fires the
|
|
32272
33709
|
// workflow_dispatch with its App token. Never throws for an expected rejection: it returns the dispatch
|
|
32273
33710
|
// outcome so runTenantControl can map a 5xx (transport-failed, retryable) vs a 4xx (rejected) vs ok.
|
|
32274
33711
|
dispatchTenantControl: async ({ repo, stage, action, lines }) => {
|
|
@@ -32284,7 +33721,7 @@ function trainApplyDeps() {
|
|
|
32284
33721
|
return { ok: false, category: body?.category, error: body?.error ?? res.error };
|
|
32285
33722
|
},
|
|
32286
33723
|
// Hotfix-coverage guard (#958): runs against the local clone via real git. manifestPaths exempts the
|
|
32287
|
-
// release version fold (#976)
|
|
33724
|
+
// release version fold (#976) ? a main-only commit touching ONLY the root package manifest is the
|
|
32288
33725
|
// fold's version metadata, which the candidate replaces with its own. (The Hub's wider distribution
|
|
32289
33726
|
// set never reaches this guard: the Hub is direct-track.)
|
|
32290
33727
|
hotfixCoverage: (input) => checkHotfixCoverage({ ...input, manifestPaths: ["package.json", "package-lock.json"] }),
|
|
@@ -32319,13 +33756,13 @@ function renderDeployLine(d) {
|
|
|
32319
33756
|
else if (d.runUrl) parts.push(`run ${d.runUrl}`);
|
|
32320
33757
|
if (d.deployStatus === "success") parts.push("deploy: SUCCEEDED");
|
|
32321
33758
|
else if (d.deployStatus === "failure") parts.push("deploy: FAILED (promotion stands; retry the deploy, do not re-tag)");
|
|
32322
|
-
else if (d.runId != null) parts.push(`deploy: UNVERIFIED
|
|
32323
|
-
else if (d.workflowRuns?.length) parts.push("deploy: UNVERIFIED
|
|
32324
|
-
else parts.push("deploy: UNVERIFIED
|
|
33759
|
+
else if (d.runId != null) parts.push(`deploy: UNVERIFIED ? dispatched, not resolved (watch: gh run watch ${d.runId} --repo mutmutco/MMI-Hub --exit-status)`);
|
|
33760
|
+
else if (d.workflowRuns?.length) parts.push("deploy: UNVERIFIED ? the runs above are enumerated, not resolved; watch each to conclusion before calling this release healthy (#3322)");
|
|
33761
|
+
else parts.push("deploy: UNVERIFIED ? no run correlated or watched; resolve every workflow run on the release SHA before calling this release healthy (#3322)");
|
|
32325
33762
|
return parts.join("; ");
|
|
32326
33763
|
}
|
|
32327
33764
|
function renderReleaseResume(r) {
|
|
32328
|
-
const lines = [`mmi-cli release --resume: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) [${r.deployModel}]
|
|
33765
|
+
const lines = [`mmi-cli release --resume: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) [${r.deployModel}] ? ${r.note}`];
|
|
32329
33766
|
for (const step of r.steps) lines.push(` - ${step}`);
|
|
32330
33767
|
if (r.releaseUrl) lines.push(` release: ${r.releaseUrl}`);
|
|
32331
33768
|
if (r.announceNote) lines.push(` announce: ${r.announceNote}`);
|
|
@@ -32335,23 +33772,23 @@ function renderReleaseResume(r) {
|
|
|
32335
33772
|
return lines.join("\n");
|
|
32336
33773
|
}
|
|
32337
33774
|
function renderReleaseAbort(r) {
|
|
32338
|
-
return `mmi-cli release --abort --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)})
|
|
33775
|
+
return `mmi-cli release --abort --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) ? ${r.note}`;
|
|
32339
33776
|
}
|
|
32340
33777
|
function renderReleasePublishRetry(r) {
|
|
32341
|
-
return `mmi-cli release --retry-publish: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)})
|
|
33778
|
+
return `mmi-cli release --retry-publish: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) ? ${r.note}; ${r.runUrl}`;
|
|
32342
33779
|
}
|
|
32343
33780
|
function renderRcandResume(r) {
|
|
32344
|
-
return `mmi-cli rcand --resume: promoted ${r.repo}
|
|
33781
|
+
return `mmi-cli rcand --resume: promoted ${r.repo} ? rc at ${r.tag} (${r.tagSha.slice(0, 12)}) [${r.deployModel}]; ${renderDeployLine(r)}; ${r.note}`;
|
|
32345
33782
|
}
|
|
32346
33783
|
function renderAlignment(label, alignment) {
|
|
32347
33784
|
if (alignment.status !== "pr-pending") return `${label}: ${alignment.note}`;
|
|
32348
33785
|
if (alignment.autoMergeEnqueued) {
|
|
32349
33786
|
return `${label}: alignment PR #${alignment.prNumber ?? "?"} AUTO-MERGE ENQUEUED (merges when checks pass)${alignment.prUrl ? ` (${alignment.prUrl})` : ""}`;
|
|
32350
33787
|
}
|
|
32351
|
-
return `${label}: ALIGNMENT PR PENDING
|
|
33788
|
+
return `${label}: ALIGNMENT PR PENDING ? land it with \`mmi-cli pr merge ${alignment.prNumber ?? "<number>"} --auto --merge\`${alignment.prUrl ? ` (${alignment.prUrl})` : ""}`;
|
|
32352
33789
|
}
|
|
32353
33790
|
function renderTrainApply(commandName, r) {
|
|
32354
|
-
let base = `mmi-cli ${commandName}: promoted ${r.repo}
|
|
33791
|
+
let base = `mmi-cli ${commandName}: promoted ${r.repo} ? ${r.stage} at ${r.tag} [${r.deployModel}]; ${renderDeployLine(r)}`;
|
|
32355
33792
|
if (r.versionFold) base = `${base}; ${r.versionFold}`;
|
|
32356
33793
|
if (r.resumeNote) base = `${base}; ${r.resumeNote}`;
|
|
32357
33794
|
if (r.devNote) base = `${base}; ${r.devNote}`;
|
|
@@ -32409,16 +33846,16 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
32409
33846
|
return fail(`${commandName}: ${e.message}`);
|
|
32410
33847
|
}
|
|
32411
33848
|
if (o.ack && commandName !== "release") {
|
|
32412
|
-
return fail(`${commandName}: --ack applies only to release
|
|
33849
|
+
return fail(`${commandName}: --ack applies only to release ? it overrides the rc -> main hotfix-coverage guard, which rcand does not run. Run: mmi-cli release --ack <shas>`);
|
|
32413
33850
|
}
|
|
32414
33851
|
if (o.dev && commandName !== "release") {
|
|
32415
|
-
return fail(`${commandName}: --dev applies only to release
|
|
33852
|
+
return fail(`${commandName}: --dev applies only to release ? it ships development -> main skipping rc, which rcand cannot do. Run: mmi-cli release --dev`);
|
|
32416
33853
|
}
|
|
32417
33854
|
if (o.announceSummaryFile && commandName !== "release") {
|
|
32418
|
-
return fail(`${commandName}: --announce-summary-file applies only to release
|
|
33855
|
+
return fail(`${commandName}: --announce-summary-file applies only to release ? rcand posts no Hub Slack announcement. Run: mmi-cli release --announce-summary-file <path>`);
|
|
32419
33856
|
}
|
|
32420
33857
|
if (o.abort && commandName !== "release") {
|
|
32421
|
-
return fail(`${commandName}: --abort applies only to release
|
|
33858
|
+
return fail(`${commandName}: --abort applies only to release ? it rolls back a proven unpublished Hub release tag. Run: mmi-cli release --abort --apply`);
|
|
32422
33859
|
}
|
|
32423
33860
|
if (o.retryPublish && commandName !== "release") {
|
|
32424
33861
|
return fail(`${commandName}: --retry-publish applies only to release. Run: mmi-cli release --retry-publish <run-id> --apply --watch`);
|
|
@@ -32440,7 +33877,7 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
32440
33877
|
}
|
|
32441
33878
|
}
|
|
32442
33879
|
if (o.abort) {
|
|
32443
|
-
if (o.resume) return fail("release: --abort and --resume are mutually exclusive
|
|
33880
|
+
if (o.resume) return fail("release: --abort and --resume are mutually exclusive ? abort removes an unpublished tag while resume preserves and promotes it");
|
|
32444
33881
|
if (!o.apply) return fail("release: --abort requires --apply after explicit approval; nothing was written");
|
|
32445
33882
|
if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
|
|
32446
33883
|
return fail("release: --abort accepts only --apply, --repo and --json; promotion flags cannot be combined with rollback");
|
|
@@ -32457,7 +33894,7 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
32457
33894
|
}
|
|
32458
33895
|
}
|
|
32459
33896
|
if (o.resume) {
|
|
32460
|
-
if (o.apply) return fail(`${commandName}: --resume and --apply are mutually exclusive
|
|
33897
|
+
if (o.apply) return fail(`${commandName}: --resume and --apply are mutually exclusive ? --apply cuts the NEXT version, --resume finishes the immutable tag already on origin`);
|
|
32461
33898
|
try {
|
|
32462
33899
|
if (commandName === "rcand") {
|
|
32463
33900
|
const result2 = await runRcandResume(trainApplyDeps(), { watch: o.watch });
|
|
@@ -32505,7 +33942,7 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
32505
33942
|
projectInfoSync = await runProjectInfoSync(result.repo, true);
|
|
32506
33943
|
} catch (e) {
|
|
32507
33944
|
const error = e.message;
|
|
32508
|
-
projectInfoSync = { applied: false, note: `FAILED
|
|
33945
|
+
projectInfoSync = { applied: false, note: `FAILED ? ${error}`, error };
|
|
32509
33946
|
}
|
|
32510
33947
|
}
|
|
32511
33948
|
const alignmentPending = result.devRollForward?.status === "pr-pending" || result.rcAlignment?.status === "pr-pending";
|
|
@@ -32560,13 +33997,13 @@ function renderHotfixRelease(r) {
|
|
|
32560
33997
|
...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
|
|
32561
33998
|
` - ${r.verifyNote}`,
|
|
32562
33999
|
...r.announceNote ? [` - announce: ${r.announceNote}`] : [],
|
|
32563
|
-
` - next: mmi-cli hotfix status ${r.tag} (no back-merge
|
|
34000
|
+
` - next: mmi-cli hotfix status ${r.tag} (no back-merge ? development already has the fix; the next /release back-merge aligns the version manifests)`
|
|
32564
34001
|
].join("\n");
|
|
32565
34002
|
}
|
|
32566
34003
|
function renderHotfixStatus(r) {
|
|
32567
34004
|
return [
|
|
32568
|
-
`mmi-cli hotfix status: ${r.tag} on ${r.repo}
|
|
32569
|
-
` - branch: ${r.branchExists ? "pushed" : "absent"}
|
|
34005
|
+
`mmi-cli hotfix status: ${r.tag} on ${r.repo} ? ${r.state}`,
|
|
34006
|
+
` - branch: ${r.branchExists ? "pushed" : "absent"} ? PR: ${r.pr ? `#${r.pr.number} ${r.pr.state}` : "none"} ? tag: ${r.tagPushed ? "pushed" : "absent"} ? Release: ${r.releaseExists ? "exists" : "absent"}`,
|
|
32570
34007
|
...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
|
|
32571
34008
|
` - npm @mutmutco/cli: ${r.npmVersion}`,
|
|
32572
34009
|
` - next: ${r.next}`,
|
|
@@ -32582,13 +34019,13 @@ async function runHotfixSub(sub, body, json, render) {
|
|
|
32582
34019
|
return failGraceful(`hotfix ${sub}: ${e.message}`);
|
|
32583
34020
|
}
|
|
32584
34021
|
}
|
|
32585
|
-
var hotfixCmd = program2.command("hotfix").description("stepwise hotfix orchestrator: start
|
|
34022
|
+
var hotfixCmd = program2.command("hotfix").description("stepwise hotfix orchestrator: start ? release, with status (bare command prints the dry-run plan; no back-merge ? #839)").option("--json", "machine-readable output").option("--apply", "not a verb; use the hotfix subcommands (start/release/status)").action(async (o) => {
|
|
32586
34023
|
try {
|
|
32587
34024
|
await requireFreshTrainCli("hotfix");
|
|
32588
34025
|
} catch (e) {
|
|
32589
34026
|
return fail(`hotfix: ${e.message}`);
|
|
32590
34027
|
}
|
|
32591
|
-
if (o.apply) return fail("hotfix: use the stepwise subcommands
|
|
34028
|
+
if (o.apply) return fail("hotfix: use the stepwise subcommands ? mmi-cli hotfix start --from <pr#|sha> ? release <vX.Y.Z> ? status [vX.Y.Z]");
|
|
32592
34029
|
const steps = trainPlan("hotfix");
|
|
32593
34030
|
console.log(o.json ? JSON.stringify({ command: "hotfix", steps }, null, 2) : renderSteps("mmi-cli hotfix: dry-run plan", steps));
|
|
32594
34031
|
});
|
|
@@ -32603,7 +34040,7 @@ ci.command("audit").description("read-only fleet scan: gate workflow, ruleset co
|
|
|
32603
34040
|
else console.log(renderCiAuditText(report));
|
|
32604
34041
|
if (!report.ok) process.exitCode = 1;
|
|
32605
34042
|
});
|
|
32606
|
-
ci.command("reconcile").description("audit + optionally apply merge settings and product ruleset activation (master-admin)").option("--json", "machine-readable output").option("--repo <owner/repo>", "reconcile one repo instead of the full registry").option("--apply", "PATCH merge settings + activate product ruleset when missing (master role required); activation is skipped while the repo's gate has never passed (#3694)").option("--park-ruleset", "stop the product ruleset enforcing without deleting it
|
|
34043
|
+
ci.command("reconcile").description("audit + optionally apply merge settings and product ruleset activation (master-admin)").option("--json", "machine-readable output").option("--repo <owner/repo>", "reconcile one repo instead of the full registry").option("--apply", "PATCH merge settings + activate product ruleset when missing (master role required); activation is skipped while the repo's gate has never passed (#3694)").option("--park-ruleset", "stop the product ruleset enforcing without deleting it ? keeps its required contexts (master role required)").action(async (o) => {
|
|
32607
34044
|
if (o.apply && o.parkRuleset) {
|
|
32608
34045
|
return fail("ci reconcile: --apply and --park-ruleset ask for opposite things; pass one");
|
|
32609
34046
|
}
|
|
@@ -32626,7 +34063,7 @@ ci.command("reconcile").description("audit + optionally apply merge settings and
|
|
|
32626
34063
|
${r.repo}: applied=[${r.applied.join("; ")}] skipped=[${r.skipped.join("; ")}]${r.errors.length ? ` errors=[${r.errors.join("; ")}]` : ""}`);
|
|
32627
34064
|
}
|
|
32628
34065
|
} else {
|
|
32629
|
-
console.log("\nDry-run
|
|
34066
|
+
console.log("\nDry-run ? re-run with --apply to patch merge settings and activate product rulesets (master-admin).");
|
|
32630
34067
|
}
|
|
32631
34068
|
}
|
|
32632
34069
|
const applyFailed = applyResults.some((result) => result.errors.length > 0 || result.postApply?.state === "failed");
|
|
@@ -32654,7 +34091,7 @@ access.command("role [repo]").description("D14 train authority for a repo (serve
|
|
|
32654
34091
|
if (!a.train) process.exitCode = 1;
|
|
32655
34092
|
return;
|
|
32656
34093
|
}
|
|
32657
|
-
console.log(`${a.repo}: @${a.login} is ${a.role}
|
|
34094
|
+
console.log(`${a.repo}: @${a.login} is ${a.role} ? train ${a.train ? "AUTHORIZED" : "not authorized"}${a.hubTrainMasterOnly ? " (Hub train is master-only)" : ""}`);
|
|
32658
34095
|
if (!a.train) process.exitCode = 1;
|
|
32659
34096
|
});
|
|
32660
34097
|
access.command("audit").description("audit collaborator roles + train-branch push allowlists vs the locked state; read-only, emits gh remediation, never applies").option("--json", "machine-readable output").option("--repo <owner/repo>", "audit a single repo instead of the whole org").option("--class <class>", "repo class for --repo (deployable | content)", "deployable").action(async () => {
|
|
@@ -32679,19 +34116,19 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
32679
34116
|
targets = resolution.targets;
|
|
32680
34117
|
}
|
|
32681
34118
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
32682
|
-
const fileMatrix = (0,
|
|
34119
|
+
const fileMatrix = (0, import_node_fs39.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs39.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
32683
34120
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
32684
34121
|
const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
32685
|
-
const fileContracts = (0,
|
|
34122
|
+
const fileContracts = (0, import_node_fs39.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs39.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
|
|
32686
34123
|
const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
|
|
32687
|
-
const sanctioned = (0,
|
|
34124
|
+
const sanctioned = (0, import_node_fs39.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs39.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
32688
34125
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess, sanctioned);
|
|
32689
34126
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
32690
34127
|
if (!report.ok) process.exitCode = 1;
|
|
32691
34128
|
});
|
|
32692
|
-
access.command("capabilities").description("enumerate your effective vault reach
|
|
34129
|
+
access.command("capabilities").description("enumerate your effective vault reach ? every credential NAME + tier + scope you can read/use across project + org/master tiers (names only, no values) (#1615)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsCapabilities(d, o)));
|
|
32693
34130
|
var isWin2 = process.platform === "win32";
|
|
32694
|
-
program2.command("doctor").description("heal CLI/plugin wiring and clean up repo cruft
|
|
34131
|
+
program2.command("doctor").description("heal CLI/plugin wiring and clean up repo cruft ? repairs run by default (#3975); use --verbose for the full audit checklist").option("--banner", "one-line resume summary; silent when all gates pass").option("--fast", "offline-safe fast lane; read-only, skips slow checks and network probes").option("--preflight", "eager version/plugin-heal (env repairs only ? never the repo working tree) with upfront notice when stale; silent when healthy (#1871)").option("--verbose", "print the evidence behind every check ? probes, resolved paths, versions compared, and the names behind each count (#2977)").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (an output format ? repairs still run by lane)").option("--apply", "deprecated no-op: repairs run by default now (#3975); kept so older instructions still parse").option("--no-repo-writes", "env/plugin repairs only ? never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for train preflights)").option("--self", "verify CLI/plugin version parity and gh auth reach; suggests plugin-heal on a hard gap (reads the published version, so not offline-safe) (#2689)").addHelpText("after", "\nExit codes:\n 0 no hard gaps remain; advisory gaps may exist\n 1 one or more included hard checks failed\n\nA plain run heals env drift (CLI, plugin, marketplace pins) and cleans repo cruft (gitignore block,\nmerged branches, dead worktrees, aged scratch) automatically (#3975). --no-repo-writes keeps the\nworking tree untouched for train preflights; --banner/--fast/--self are read-only lanes.\n\n--banner keeps the legacy SessionStart contract and returns 0 unless the process crashes.\n").action(async (opts) => {
|
|
32695
34132
|
if (opts.guide) {
|
|
32696
34133
|
consoleIo.log("MMI Agentic Onboarding: docs/Architecture/agentic-dev-environment.md");
|
|
32697
34134
|
return;
|
|
@@ -32707,7 +34144,7 @@ program2.command("doctor").description("heal CLI/plugin wiring and clean up repo
|
|
|
32707
34144
|
verbose: opts.verbose
|
|
32708
34145
|
},
|
|
32709
34146
|
consoleIo,
|
|
32710
|
-
mmiDoctorDeps()
|
|
34147
|
+
mmiDoctorDeps({ program: program2 })
|
|
32711
34148
|
);
|
|
32712
34149
|
});
|
|
32713
34150
|
program2.command("guard").description("detect a pruned/unresolved MMI plugin on disk").action(() => runGuard());
|
|
@@ -32716,16 +34153,16 @@ function directoryBytes(path2) {
|
|
|
32716
34153
|
let total = 0;
|
|
32717
34154
|
let entries;
|
|
32718
34155
|
try {
|
|
32719
|
-
entries = (0,
|
|
34156
|
+
entries = (0, import_node_fs39.readdirSync)(path2, { withFileTypes: true });
|
|
32720
34157
|
} catch {
|
|
32721
34158
|
return 0;
|
|
32722
34159
|
}
|
|
32723
34160
|
for (const entry of entries) {
|
|
32724
|
-
const child2 = (0,
|
|
34161
|
+
const child2 = (0, import_node_path38.join)(path2, entry.name);
|
|
32725
34162
|
if (entry.isDirectory()) total += directoryBytes(child2);
|
|
32726
34163
|
else {
|
|
32727
34164
|
try {
|
|
32728
|
-
total += (0,
|
|
34165
|
+
total += (0, import_node_fs39.statSync)(child2).size;
|
|
32729
34166
|
} catch {
|
|
32730
34167
|
}
|
|
32731
34168
|
}
|
|
@@ -32733,25 +34170,25 @@ function directoryBytes(path2) {
|
|
|
32733
34170
|
return total;
|
|
32734
34171
|
}
|
|
32735
34172
|
function listDirEntries(dir) {
|
|
32736
|
-
return (0,
|
|
34173
|
+
return (0, import_node_fs39.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
|
|
32737
34174
|
}
|
|
32738
34175
|
function readInstalledPluginRefs(configRoot) {
|
|
32739
34176
|
const p = installedPluginsPathForConfig(configRoot);
|
|
32740
|
-
if (!(0,
|
|
34177
|
+
if (!(0, import_node_fs39.existsSync)(p)) return [];
|
|
32741
34178
|
try {
|
|
32742
|
-
return installedPluginPaths((0,
|
|
34179
|
+
return installedPluginPaths((0, import_node_fs39.readFileSync)(p, "utf8"));
|
|
32743
34180
|
} catch {
|
|
32744
34181
|
return null;
|
|
32745
34182
|
}
|
|
32746
34183
|
}
|
|
32747
34184
|
function pluginCacheFsDeps(configRoot, dirBytes) {
|
|
32748
34185
|
return {
|
|
32749
|
-
exists: (p) => (0,
|
|
32750
|
-
listVersionDirs: (root) => (0,
|
|
34186
|
+
exists: (p) => (0, import_node_fs39.existsSync)(p),
|
|
34187
|
+
listVersionDirs: (root) => (0, import_node_fs39.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
|
|
32751
34188
|
dirBytes,
|
|
32752
|
-
listStagingDirs: (root) => (0,
|
|
34189
|
+
listStagingDirs: (root) => (0, import_node_fs39.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
|
|
32753
34190
|
try {
|
|
32754
|
-
return { name: d.name, mtimeMs: newestMtimeMs((0,
|
|
34191
|
+
return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path38.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs39.statSync)(p).mtimeMs) };
|
|
32755
34192
|
} catch {
|
|
32756
34193
|
return { name: d.name, mtimeMs: Date.now() };
|
|
32757
34194
|
}
|
|
@@ -32765,10 +34202,10 @@ function stagingApplyFsGuard(configRoot) {
|
|
|
32765
34202
|
return {
|
|
32766
34203
|
referencedPaths: () => readInstalledPluginRefs(configRoot),
|
|
32767
34204
|
mtimeMs: (name) => {
|
|
32768
|
-
const p = (0,
|
|
32769
|
-
if (!(0,
|
|
34205
|
+
const p = (0, import_node_path38.join)(stagingRoot, name);
|
|
34206
|
+
if (!(0, import_node_fs39.existsSync)(p)) return null;
|
|
32770
34207
|
try {
|
|
32771
|
-
return newestMtimeMs(p, listDirEntries, (q) => (0,
|
|
34208
|
+
return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs39.statSync)(q).mtimeMs);
|
|
32772
34209
|
} catch {
|
|
32773
34210
|
return null;
|
|
32774
34211
|
}
|
|
@@ -32788,13 +34225,13 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
|
|
|
32788
34225
|
return;
|
|
32789
34226
|
}
|
|
32790
34227
|
const plan = buildPluginCachePlan(
|
|
32791
|
-
(0,
|
|
34228
|
+
(0, import_node_os15.homedir)(),
|
|
32792
34229
|
running,
|
|
32793
34230
|
pluginCacheFsDeps(configRoot, directoryBytes),
|
|
32794
34231
|
{ withBytes: true, configRoot, includeStaging: surface !== "codex" }
|
|
32795
34232
|
);
|
|
32796
34233
|
const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
|
|
32797
|
-
const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0,
|
|
34234
|
+
const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs39.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
|
|
32798
34235
|
const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
|
|
32799
34236
|
if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
|
|
32800
34237
|
else console.log(renderPluginCachePlan(plan, result));
|
|
@@ -32802,7 +34239,7 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
|
|
|
32802
34239
|
});
|
|
32803
34240
|
program2.command("session-start").description("run the SessionStart verbs (whoami, board slice, doctor) in one process").action(async () => {
|
|
32804
34241
|
if (isInsideRepoSubdir(process.cwd())) {
|
|
32805
|
-
console.error("[mmi-hook] plugin session-start: cwd is a repository SUBDIRECTORY
|
|
34242
|
+
console.error("[mmi-hook] plugin session-start: cwd is a repository SUBDIRECTORY ? skipping the SessionStart hook (spine/docs/plan/saga delivery); run it from the repo root.");
|
|
32806
34243
|
appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "skip (repo subdirectory)" });
|
|
32807
34244
|
return;
|
|
32808
34245
|
}
|
|
@@ -32815,7 +34252,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
32815
34252
|
const bannerIo = measured.io;
|
|
32816
34253
|
const { parallel, sequential } = buildSessionStartPlan({
|
|
32817
34254
|
// whoami (#879): surface the resolved human so agents act --for them without asking. Silent
|
|
32818
|
-
// when unknown
|
|
34255
|
+
// when unknown ? a missing gh login must not noise or fail the banner.
|
|
32819
34256
|
whoami: async (io) => {
|
|
32820
34257
|
const report = await resolveWhoami({
|
|
32821
34258
|
hubSession: async () => hubAuthSession({ baseUrl: (await loadConfig()).sagaApiUrl ?? defaultHubUrl(), githubToken }),
|
|
@@ -32826,15 +34263,15 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
32826
34263
|
const authority = authorityLine(report);
|
|
32827
34264
|
if (authority) io.log(authority);
|
|
32828
34265
|
},
|
|
32829
|
-
// command-ladder hint (#2609): compact gh
|
|
32830
|
-
// not after a denied gh call. <1KB, pure in-memory
|
|
34266
|
+
// command-ladder hint (#2609): compact gh?mmi-cli cheat sheet so agents reach for mmi-cli first,
|
|
34267
|
+
// not after a denied gh call. <1KB, pure in-memory ? no network, no fs, fail-soft.
|
|
32831
34268
|
commandLadderHint: async (io) => {
|
|
32832
34269
|
const hint = commandLadderHint();
|
|
32833
34270
|
if (hint) io.log(hint);
|
|
32834
34271
|
},
|
|
32835
34272
|
// stale worktrees: fast LOCAL-only git check (worktree list + branch -vv gone status). Flags leaked
|
|
32836
34273
|
// worktrees / merged-but-undeleted branches so the session never starts atop leftover state. No
|
|
32837
|
-
// network
|
|
34274
|
+
// network ? never the ~20s `gh pr list` path of `worktree list --stale`. Fail-soft.
|
|
32838
34275
|
staleWorktrees: async (io) => {
|
|
32839
34276
|
const line = await gatherStaleWorktreeWarning();
|
|
32840
34277
|
if (line) io.log(line);
|
|
@@ -32847,17 +34284,17 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
32847
34284
|
const line = localTrainSyncBannerLine(result);
|
|
32848
34285
|
if (line) io.log(line);
|
|
32849
34286
|
},
|
|
32850
|
-
// #3485 item 7: the ONLY lane that throttles the npm read
|
|
34287
|
+
// #3485 item 7: the ONLY lane that throttles the npm read ? it runs on every session start, on a
|
|
32851
34288
|
// blocking hook. Every interactive lane still reads live.
|
|
32852
34289
|
doctor: async (io) => {
|
|
32853
|
-
await runDoctorClean({ banner: true }, io, mmiDoctorDeps({ throttleReleasedRead: true }));
|
|
34290
|
+
await runDoctorClean({ banner: true }, io, mmiDoctorDeps({ throttleReleasedRead: true, program: program2 }));
|
|
32854
34291
|
}
|
|
32855
34292
|
});
|
|
32856
34293
|
await runSessionStart(parallel, sequential, bannerIo);
|
|
32857
34294
|
for (const line of scratchGcLines(process.cwd())) bannerIo.log(line);
|
|
32858
34295
|
const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
|
|
32859
34296
|
if (worktreeBanner) {
|
|
32860
|
-
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn:
|
|
34297
|
+
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process18.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
32861
34298
|
bannerIo.log(worktreeBanner);
|
|
32862
34299
|
}
|
|
32863
34300
|
if (isLinkedWorktree(process.cwd())) {
|