@mutmutco/cli 3.89.0 → 3.91.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 +359 -46
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -4040,6 +4040,56 @@ var execFileP2 = (file, args, options = {}) => (
|
|
|
4040
4040
|
rawExecFileP2(file, args, { encoding: "utf8", windowsHide: true, timeout: DEFAULT_EXEC_TIMEOUT_MS, killSignal: "SIGTERM", ...options })
|
|
4041
4041
|
);
|
|
4042
4042
|
var GIT_TIMEOUT_MS = DEFAULT_EXEC_TIMEOUT_MS;
|
|
4043
|
+
var ExecDeadlineError = class extends Error {
|
|
4044
|
+
constructor(step, timeoutMs, elapsedMs, killed) {
|
|
4045
|
+
super(
|
|
4046
|
+
`\`${step}\` did not finish within ${Math.round(timeoutMs / 1e3)}s (gave up after ${Math.round(elapsedMs / 1e3)}s). It stopped responding and did not exit when asked to, so the timeout alone could not end it; ${killed ? "its process tree was force-terminated" : "its process tree could NOT be terminated and may still be running"}.`
|
|
4047
|
+
);
|
|
4048
|
+
this.step = step;
|
|
4049
|
+
this.timeoutMs = timeoutMs;
|
|
4050
|
+
this.elapsedMs = elapsedMs;
|
|
4051
|
+
this.killed = killed;
|
|
4052
|
+
this.name = "ExecDeadlineError";
|
|
4053
|
+
}
|
|
4054
|
+
step;
|
|
4055
|
+
timeoutMs;
|
|
4056
|
+
elapsedMs;
|
|
4057
|
+
killed;
|
|
4058
|
+
};
|
|
4059
|
+
function killProcessTree(pid) {
|
|
4060
|
+
try {
|
|
4061
|
+
if (process.platform === "win32") {
|
|
4062
|
+
(0, import_node_child_process3.execFileSync)("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
4063
|
+
} else {
|
|
4064
|
+
process.kill(pid, "SIGKILL");
|
|
4065
|
+
}
|
|
4066
|
+
return true;
|
|
4067
|
+
} catch {
|
|
4068
|
+
return false;
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
function execFileHard(file, args, options) {
|
|
4072
|
+
const { timeout, step, ...rest } = options;
|
|
4073
|
+
const started = Date.now();
|
|
4074
|
+
return new Promise((resolve5, reject) => {
|
|
4075
|
+
const child2 = (0, import_node_child_process3.execFile)(file, args, { encoding: "utf8", windowsHide: true, ...rest, timeout: 0 }, (error, stdout, stderr) => {
|
|
4076
|
+
clearTimeout(timer);
|
|
4077
|
+
if (expired) return;
|
|
4078
|
+
if (error) reject(error);
|
|
4079
|
+
else resolve5({ stdout: String(stdout), stderr: String(stderr) });
|
|
4080
|
+
});
|
|
4081
|
+
let expired = false;
|
|
4082
|
+
const timer = setTimeout(() => {
|
|
4083
|
+
expired = true;
|
|
4084
|
+
const killed = child2.pid ? killProcessTree(child2.pid) : false;
|
|
4085
|
+
child2.stdout?.destroy();
|
|
4086
|
+
child2.stderr?.destroy();
|
|
4087
|
+
child2.unref();
|
|
4088
|
+
reject(new ExecDeadlineError(step, timeout, Date.now() - started, killed));
|
|
4089
|
+
}, timeout);
|
|
4090
|
+
timer.unref?.();
|
|
4091
|
+
});
|
|
4092
|
+
}
|
|
4043
4093
|
var cachedGithubLogin;
|
|
4044
4094
|
async function githubLogin() {
|
|
4045
4095
|
cachedGithubLogin ??= execFileP2("gh", ["api", "user", "--jq", ".login"]).then(({ stdout }) => stdout.trim() || void 0).catch(() => void 0);
|
|
@@ -8872,7 +8922,11 @@ function hasUserInstallRecord(file, pluginId) {
|
|
|
8872
8922
|
var CLAUDE_PLUGIN_TIMEOUT_MS = 12e4;
|
|
8873
8923
|
var NPM_VIEW_TIMEOUT_MS = 15e3;
|
|
8874
8924
|
function runHostBin(bin, args, opts) {
|
|
8875
|
-
|
|
8925
|
+
const { step, timeout } = opts;
|
|
8926
|
+
const shared = { timeout, stdio: ["ignore", "pipe", "pipe"], maxBuffer: 16 * 1024 * 1024 };
|
|
8927
|
+
const file = isWin ? "cmd.exe" : bin;
|
|
8928
|
+
const argv = isWin ? ["/c", bin, ...args] : args;
|
|
8929
|
+
return step ? execFileHard(file, argv, { ...shared, step }) : execFileP2(file, argv, shared);
|
|
8876
8930
|
}
|
|
8877
8931
|
function surfaceConfigRoot(surface, env = process.env, home = (0, import_node_os4.homedir)()) {
|
|
8878
8932
|
if (surface === "codex") return env.CODEX_HOME?.trim() || (0, import_node_path14.join)(home, ".codex");
|
|
@@ -9050,27 +9104,14 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
|
|
|
9050
9104
|
) : (0, import_node_fs15.existsSync)((0, import_node_path14.join)(root, "plugins", "cache", "mutmutco", "mmi"))
|
|
9051
9105
|
};
|
|
9052
9106
|
}
|
|
9053
|
-
async function
|
|
9107
|
+
async function runPluginCli(bin, args, log) {
|
|
9054
9108
|
try {
|
|
9055
|
-
await runHostBin(
|
|
9109
|
+
await runHostBin(bin, args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS, step: `${bin} ${args.join(" ")}` });
|
|
9056
9110
|
return true;
|
|
9057
|
-
} catch {
|
|
9058
|
-
|
|
9059
|
-
}
|
|
9060
|
-
}
|
|
9061
|
-
async function runCodexPlugin(args) {
|
|
9062
|
-
try {
|
|
9063
|
-
await runHostBin("codex", args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
|
|
9064
|
-
return true;
|
|
9065
|
-
} catch {
|
|
9066
|
-
return false;
|
|
9067
|
-
}
|
|
9068
|
-
}
|
|
9069
|
-
async function runKiloPlugin(args) {
|
|
9070
|
-
try {
|
|
9071
|
-
await runHostBin("kilo", args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
|
|
9072
|
-
return true;
|
|
9073
|
-
} catch {
|
|
9111
|
+
} catch (error) {
|
|
9112
|
+
if (error instanceof ExecDeadlineError) {
|
|
9113
|
+
log(` \u2717 ${error.message} Recover with: mmi-cli plugin heal`);
|
|
9114
|
+
}
|
|
9074
9115
|
return false;
|
|
9075
9116
|
}
|
|
9076
9117
|
}
|
|
@@ -9131,7 +9172,8 @@ async function installCursorPluginCheckout(env = process.env) {
|
|
|
9131
9172
|
});
|
|
9132
9173
|
} else {
|
|
9133
9174
|
await runHostBin("gh", ["repo", "clone", "mutmutco/MMI-Hub", staged, "--", "--branch", "main", "--depth", "1"], {
|
|
9134
|
-
timeout: CLAUDE_PLUGIN_TIMEOUT_MS
|
|
9175
|
+
timeout: CLAUDE_PLUGIN_TIMEOUT_MS,
|
|
9176
|
+
step: "gh repo clone mutmutco/MMI-Hub"
|
|
9135
9177
|
});
|
|
9136
9178
|
}
|
|
9137
9179
|
if (!cursorPluginTreeHealthy(staged)) {
|
|
@@ -9161,7 +9203,8 @@ async function installCursorPluginCheckout(env = process.env) {
|
|
|
9161
9203
|
async function marketplaceAddRefSupported(bin) {
|
|
9162
9204
|
try {
|
|
9163
9205
|
const { stdout, stderr } = await runHostBin(bin, ["plugin", "marketplace", "add", "--help"], {
|
|
9164
|
-
timeout: CLAUDE_PLUGIN_TIMEOUT_MS
|
|
9206
|
+
timeout: CLAUDE_PLUGIN_TIMEOUT_MS,
|
|
9207
|
+
step: `${bin} plugin marketplace add --help`
|
|
9165
9208
|
});
|
|
9166
9209
|
return marketplaceAddSupportsRef(`${stdout}
|
|
9167
9210
|
${stderr}`);
|
|
@@ -9191,7 +9234,7 @@ async function applyPluginHeal(surface, log, opts) {
|
|
|
9191
9234
|
if (token === "kilo") {
|
|
9192
9235
|
log(" \u21BB reinstalling the MMI plugin via `kilo plugin` (install \u2192 server() provisions the skills)\u2026");
|
|
9193
9236
|
for (const step of descriptor.healSteps) {
|
|
9194
|
-
const ok = await
|
|
9237
|
+
const ok = await runPluginCli("kilo", [...step.args], log);
|
|
9195
9238
|
if (healStepAborts(step, ok)) return false;
|
|
9196
9239
|
}
|
|
9197
9240
|
return true;
|
|
@@ -9207,7 +9250,7 @@ async function applyPluginHeal(surface, log, opts) {
|
|
|
9207
9250
|
const pins = token === "claude" ? captureMarketplacePins(readKnownMarketplacesFile(pinsPath), [MMI_MARKETPLACE_NAME, JERV_MARKETPLACE_NAME]) : /* @__PURE__ */ new Map();
|
|
9208
9251
|
try {
|
|
9209
9252
|
for (const step of steps) {
|
|
9210
|
-
const ok =
|
|
9253
|
+
const ok = await runPluginCli(token, [...step.args], log);
|
|
9211
9254
|
if (healStepAborts(step, ok)) return false;
|
|
9212
9255
|
}
|
|
9213
9256
|
} finally {
|
|
@@ -9270,11 +9313,27 @@ function readKnownMarketplacesFile(path2) {
|
|
|
9270
9313
|
return void 0;
|
|
9271
9314
|
}
|
|
9272
9315
|
}
|
|
9273
|
-
function
|
|
9316
|
+
function claudeCodeIsRunning(env = process.env, listProcesses = defaultProcessList) {
|
|
9317
|
+
if (env.CLAUDE_CODE_SESSION_ID?.trim() || env.CLAUDECODE?.trim() || env.CLAUDE_PLUGIN_ROOT?.trim()) return true;
|
|
9318
|
+
let table;
|
|
9319
|
+
try {
|
|
9320
|
+
table = listProcesses();
|
|
9321
|
+
} catch {
|
|
9322
|
+
return true;
|
|
9323
|
+
}
|
|
9324
|
+
if (!table.trim()) return true;
|
|
9325
|
+
return table.split(/\r?\n/).some((line) => /(^|[/\\])claude(\.(exe|cmd|ps1))?\s/.test(`${line.trim()} `));
|
|
9326
|
+
}
|
|
9327
|
+
function defaultProcessList() {
|
|
9328
|
+
return isWin ? (0, import_node_child_process6.execFileSync)("powershell.exe", ["-NoProfile", "-Command", "Get-CimInstance Win32_Process | ForEach-Object { $_.CommandLine }"], { encoding: "utf8", windowsHide: true, maxBuffer: 32 * 1024 * 1024, timeout: 15e3 }) : (0, import_node_child_process6.execFileSync)("ps", ["-eo", "args="], { encoding: "utf8", windowsHide: true, timeout: 15e3 });
|
|
9329
|
+
}
|
|
9330
|
+
function writeMarketplacePinsOnDisk(path2, pins, succeeded, failedVerb, declineWhileHostLive) {
|
|
9274
9331
|
if (pins.size === 0) return void 0;
|
|
9275
9332
|
const after = readKnownMarketplacesFile(path2);
|
|
9276
9333
|
const next = restoreMarketplacePins(after, pins);
|
|
9277
9334
|
if (next === null) return void 0;
|
|
9335
|
+
const declined = declineWhileHostLive?.();
|
|
9336
|
+
if (declined) return declined;
|
|
9278
9337
|
try {
|
|
9279
9338
|
(0, import_node_fs15.writeFileSync)(path2, next, "utf8");
|
|
9280
9339
|
} catch {
|
|
@@ -9287,16 +9346,27 @@ function writeMarketplacePinsOnDisk(path2, pins, succeeded, failedVerb) {
|
|
|
9287
9346
|
});
|
|
9288
9347
|
return failed.length ? `${failedVerb} did NOT take for ${failed.map(([n]) => n).join(", ")} \u2014 set it by hand` : succeeded(pins);
|
|
9289
9348
|
}
|
|
9290
|
-
function restoreMarketplacePinsOnDisk(path2, pins) {
|
|
9291
|
-
return writeMarketplacePinsOnDisk(path2, pins, restoredPinsLine, "restore");
|
|
9292
|
-
}
|
|
9293
|
-
function applyOrgMarketplacePins(path2, names) {
|
|
9349
|
+
function restoreMarketplacePinsOnDisk(path2, pins, hostIsRunning = claudeCodeIsRunning) {
|
|
9294
9350
|
return writeMarketplacePinsOnDisk(
|
|
9351
|
+
path2,
|
|
9352
|
+
pins,
|
|
9353
|
+
(restored) => hostIsRunning() ? `${restoredPinsLine(restored)}, but Claude Code is running and can rewrite this file from its own copy \u2014 restart it, then \`mmi-cli doctor --apply\` if the pins did not survive` : restoredPinsLine(restored),
|
|
9354
|
+
"restore"
|
|
9355
|
+
);
|
|
9356
|
+
}
|
|
9357
|
+
function applyOrgMarketplacePins(path2, names, hostIsRunning = claudeCodeIsRunning) {
|
|
9358
|
+
let landed = false;
|
|
9359
|
+
const detail = writeMarketplacePinsOnDisk(
|
|
9295
9360
|
path2,
|
|
9296
9361
|
new Map(names.map((name) => [name, ORG_MARKETPLACE_PINS])),
|
|
9297
|
-
(pins) =>
|
|
9298
|
-
|
|
9362
|
+
(pins) => {
|
|
9363
|
+
landed = true;
|
|
9364
|
+
return `pinned ${[...pins.keys()].join(", ")} to ${ORG_MARKETPLACE_PINS.ref} with auto-update on`;
|
|
9365
|
+
},
|
|
9366
|
+
"pin",
|
|
9367
|
+
() => hostIsRunning() ? "not pinned \u2014 Claude Code is running and rewrites this registration from its own copy; quit it, then run `mmi-cli doctor --apply`" : void 0
|
|
9299
9368
|
);
|
|
9369
|
+
return detail === void 0 ? void 0 : { detail, wrote: landed };
|
|
9300
9370
|
}
|
|
9301
9371
|
function writeMarketplacePinPending(path2, names, now = Date.now()) {
|
|
9302
9372
|
try {
|
|
@@ -22906,12 +22976,42 @@ var canonicalPriorityColors = { Urgent: "RED", High: "ORANGE", Medium: "YELLOW",
|
|
|
22906
22976
|
function miscoloredOptions(options, canon) {
|
|
22907
22977
|
return (options ?? []).filter((o) => canon[o.name] != null && o.color != null && o.color !== canon[o.name]).map((o) => `${o.name}=${o.color} (want ${canon[o.name]})`);
|
|
22908
22978
|
}
|
|
22979
|
+
function missingBoardViews(views, required) {
|
|
22980
|
+
return required.filter((req) => !views.some((v) => v.name === req.name && v.layout === req.layout));
|
|
22981
|
+
}
|
|
22982
|
+
function boardGroupingDrift(board, wantColumn, wantSwimlane) {
|
|
22983
|
+
if (!board) return ["no Board view found"];
|
|
22984
|
+
const problems = [];
|
|
22985
|
+
if (!board.verticalGroupByFields.includes(wantColumn)) {
|
|
22986
|
+
problems.push(`columns: ${board.verticalGroupByFields.join(", ") || "none"} (want ${wantColumn})`);
|
|
22987
|
+
}
|
|
22988
|
+
if (!board.groupByFields.includes(wantSwimlane)) {
|
|
22989
|
+
problems.push(`swimlanes: ${board.groupByFields.join(", ") || "none"} (want ${wantSwimlane})`);
|
|
22990
|
+
}
|
|
22991
|
+
return problems;
|
|
22992
|
+
}
|
|
22993
|
+
function boardCardFieldDrift(visibleFields, canonical) {
|
|
22994
|
+
const have = new Set(visibleFields);
|
|
22995
|
+
const want = new Set(canonical);
|
|
22996
|
+
return {
|
|
22997
|
+
missing: canonical.filter((f) => !have.has(f)),
|
|
22998
|
+
extra: visibleFields.filter((f) => !want.has(f))
|
|
22999
|
+
};
|
|
23000
|
+
}
|
|
22909
23001
|
var requiredProjectWorkflows = [
|
|
22910
23002
|
"Auto-add sub-issues to project",
|
|
22911
23003
|
"Auto-archive items",
|
|
22912
23004
|
"Item added to project",
|
|
22913
23005
|
"Item closed"
|
|
22914
23006
|
];
|
|
23007
|
+
var requiredBoardViews = [
|
|
23008
|
+
{ name: "List", layout: "TABLE_LAYOUT" },
|
|
23009
|
+
{ name: "Board", layout: "BOARD_LAYOUT" },
|
|
23010
|
+
{ name: "Roadmap", layout: "ROADMAP_LAYOUT" }
|
|
23011
|
+
];
|
|
23012
|
+
var requiredBoardColumnField = "Status";
|
|
23013
|
+
var requiredBoardSwimlaneField = "Repository";
|
|
23014
|
+
var requiredBoardCardFields = ["Title", "Assignees", "Status", "Labels", "Linked pull requests", "Parent issue", "Sub-issues progress", "Priority"];
|
|
22915
23015
|
var requiredOrgRulesetTypes = ["pull_request", "non_fast_forward", "deletion"];
|
|
22916
23016
|
var requiredHubStatusChecks = ["cli", "infra", "docs"];
|
|
22917
23017
|
var requiredProductStatusChecks = ["gate"];
|
|
@@ -23260,6 +23360,48 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
23260
23360
|
label: `Project workflow enabled: ${workflowName}`
|
|
23261
23361
|
});
|
|
23262
23362
|
}
|
|
23363
|
+
const viewsQuery = `query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { views(first: 10) { nodes { name layout groupByFields(first: 5) { nodes { ... on ProjectV2FieldCommon { name } } } verticalGroupByFields(first: 5) { nodes { ... on ProjectV2FieldCommon { name } } } visibleFields(first: 20) { nodes { ... on ProjectV2FieldCommon { name } } } } } } } }`;
|
|
23364
|
+
const boardViews = await (async () => {
|
|
23365
|
+
try {
|
|
23366
|
+
const data = await deps.client.graphql(viewsQuery, {
|
|
23367
|
+
login: config.projectOwner,
|
|
23368
|
+
number: config.projectNumber
|
|
23369
|
+
});
|
|
23370
|
+
const nodes = data.organization?.projectV2?.views?.nodes ?? [];
|
|
23371
|
+
const names = (conn) => (conn?.nodes ?? []).filter((f) => Boolean(f?.name)).map((f) => f.name);
|
|
23372
|
+
return nodes.filter((v) => Boolean(v)).map((v) => ({
|
|
23373
|
+
name: v.name,
|
|
23374
|
+
layout: v.layout,
|
|
23375
|
+
groupByFields: names(v.groupByFields),
|
|
23376
|
+
verticalGroupByFields: names(v.verticalGroupByFields),
|
|
23377
|
+
visibleFields: names(v.visibleFields)
|
|
23378
|
+
}));
|
|
23379
|
+
} catch {
|
|
23380
|
+
return [];
|
|
23381
|
+
}
|
|
23382
|
+
})();
|
|
23383
|
+
const missingViews = missingBoardViews(boardViews, requiredBoardViews);
|
|
23384
|
+
checks.push({
|
|
23385
|
+
ok: missingViews.length === 0,
|
|
23386
|
+
label: "Project view triple present: List/Board/Roadmap (#4093)",
|
|
23387
|
+
detail: missingViews.length ? `missing: ${missingViews.map((v) => `${v.name} (${v.layout})`).join(", ")} \u2014 createProjectV2View/updateProjectV2View is API-writable (name + layout)` : void 0
|
|
23388
|
+
});
|
|
23389
|
+
const boardView = boardViews.find((v) => v.layout === "BOARD_LAYOUT");
|
|
23390
|
+
const groupingDrift = boardGroupingDrift(boardView, requiredBoardColumnField, requiredBoardSwimlaneField);
|
|
23391
|
+
checks.push({
|
|
23392
|
+
ok: groupingDrift.length === 0,
|
|
23393
|
+
label: `Board view grouped by ${requiredBoardColumnField} columns / ${requiredBoardSwimlaneField} swimlanes (#4093)`,
|
|
23394
|
+
detail: groupingDrift.length ? `${groupingDrift.join("; ")} \u2014 GitHub exposes no create/update mutation for view grouping (ProjectV2ViewConfigurationInput carries only visibleFieldIds); fix in the UI: Board view \u2192 \u2699 (top-right) \u2192 Group by \u2192 ${requiredBoardColumnField}, Swimlanes \u2192 ${requiredBoardSwimlaneField} \u2192 Save view` : void 0
|
|
23395
|
+
});
|
|
23396
|
+
const cardDrift = boardCardFieldDrift(boardView?.visibleFields ?? [], requiredBoardCardFields);
|
|
23397
|
+
checks.push({
|
|
23398
|
+
ok: cardDrift.missing.length === 0 && cardDrift.extra.length === 0,
|
|
23399
|
+
label: "Board view card fields match the org standard (#4093)",
|
|
23400
|
+
detail: cardDrift.missing.length || cardDrift.extra.length ? `${[
|
|
23401
|
+
cardDrift.missing.length ? `missing: ${cardDrift.missing.join(", ")}` : null,
|
|
23402
|
+
cardDrift.extra.length ? `extra: ${cardDrift.extra.join(", ")}` : null
|
|
23403
|
+
].filter(Boolean).join("; ")} \u2014 API-writable via updateProjectV2View(configuration:{visibleFieldIds:[...]}); fix in the UI (Board view \u2192 Fields) or that mutation` : void 0
|
|
23404
|
+
});
|
|
23263
23405
|
}
|
|
23264
23406
|
const projectRegistry = localRegistryCheck(deps, "projects.json", (json) => Array.isArray(json?.projects) && projectRegistryIncludesRepo(json.projects, repo));
|
|
23265
23407
|
if (projectRegistry != null) checks.push({ ok: projectRegistry, label: "project registry includes repo" });
|
|
@@ -24037,27 +24179,41 @@ function decideStage(inputs) {
|
|
|
24037
24179
|
var import_node_net2 = require("node:net");
|
|
24038
24180
|
var STAGE_LIVE_HUB_REPO = "mutmutco/MMI-Hub";
|
|
24039
24181
|
var IP_ECHO_URL = "https://api.ipify.org";
|
|
24182
|
+
var IP6_ECHO_URL = "https://api6.ipify.org";
|
|
24040
24183
|
var IP_DETECT_TIMEOUT_MS = 1e4;
|
|
24041
24184
|
function validStageLiveIp(ip) {
|
|
24042
24185
|
return (0, import_node_net2.isIP)(ip.trim()) !== 0;
|
|
24043
24186
|
}
|
|
24044
24187
|
async function detectPublicIp(fetchImpl = fetch) {
|
|
24188
|
+
return detectPublicIpFrom(IP_ECHO_URL, fetchImpl);
|
|
24189
|
+
}
|
|
24190
|
+
async function detectCallerIps(fetchImpl = fetch) {
|
|
24191
|
+
const [v4, v6] = await Promise.allSettled([
|
|
24192
|
+
detectPublicIp(fetchImpl),
|
|
24193
|
+
detectPublicIpFrom(IP6_ECHO_URL, fetchImpl)
|
|
24194
|
+
]);
|
|
24195
|
+
if (v4.status === "rejected") throw v4.reason;
|
|
24196
|
+
const ip = v4.value;
|
|
24197
|
+
const candidate = v6.status === "fulfilled" ? v6.value : void 0;
|
|
24198
|
+
return { ip, ip6: candidate && (0, import_node_net2.isIP)(candidate) === 6 ? candidate : void 0 };
|
|
24199
|
+
}
|
|
24200
|
+
async function detectPublicIpFrom(url, fetchImpl) {
|
|
24045
24201
|
let res;
|
|
24046
24202
|
try {
|
|
24047
|
-
res = await fetchImpl(
|
|
24203
|
+
res = await fetchImpl(url, { signal: AbortSignal.timeout(IP_DETECT_TIMEOUT_MS) });
|
|
24048
24204
|
} catch (e) {
|
|
24049
|
-
throw new Error(`public IP detection failed (${
|
|
24205
|
+
throw new Error(`public IP detection failed (${url}): ${e.message}`);
|
|
24050
24206
|
}
|
|
24051
|
-
if (!res.ok) throw new Error(`public IP detection failed: HTTP ${res.status} from ${
|
|
24207
|
+
if (!res.ok) throw new Error(`public IP detection failed: HTTP ${res.status} from ${url}`);
|
|
24052
24208
|
const ip = (await res.text()).trim();
|
|
24053
|
-
if (!validStageLiveIp(ip)) throw new Error(`public IP detection returned a non-IP body from ${
|
|
24209
|
+
if (!validStageLiveIp(ip)) throw new Error(`public IP detection returned a non-IP body from ${url}: "${ip.slice(0, 80)}"`);
|
|
24054
24210
|
return ip;
|
|
24055
24211
|
}
|
|
24056
24212
|
function stageLiveUpSteps(t) {
|
|
24057
24213
|
return [
|
|
24058
|
-
{ label: `detect your public
|
|
24214
|
+
{ label: `detect your public IPv4 and IPv6 (${IP_ECHO_URL} + ${IP6_ECHO_URL}, bounded, in parallel)` },
|
|
24059
24215
|
{ label: `deploy ${t.ref ?? "<current branch>"} to the ${t.slug} dev stage via the Hub backend (tenant-deploy)` },
|
|
24060
|
-
{ label: `gate ${t.host} to your
|
|
24216
|
+
{ label: `gate ${t.host} to your IPv4 and your IPv6 /64 at the Cloudflare edge via the Hub backend (tenant-control cf-gate-allow)` },
|
|
24061
24217
|
{ label: "tear down when done", command: "mmi-cli stage --live --down --apply" }
|
|
24062
24218
|
];
|
|
24063
24219
|
}
|
|
@@ -24069,11 +24225,14 @@ function stageLiveDownSteps(t) {
|
|
|
24069
24225
|
}
|
|
24070
24226
|
async function runStageLiveUp(deps, t) {
|
|
24071
24227
|
if (!t.ref?.trim()) throw new Error("stage --live: cannot resolve the current branch to deploy");
|
|
24072
|
-
const
|
|
24228
|
+
const detected = await deps.detectIp();
|
|
24229
|
+
const ip = detected.ip.trim();
|
|
24073
24230
|
if (!validStageLiveIp(ip)) throw new Error(`stage --live: detected public IP is not a literal IPv4/IPv6 address: "${ip.slice(0, 80)}"`);
|
|
24231
|
+
const ip6 = detected.ip6?.trim() || void 0;
|
|
24074
24232
|
if (!t.host?.trim()) throw new Error("stage --live: cannot resolve the dev edge host (registry edgeDomains.dev)");
|
|
24075
24233
|
await deps.deployDev({ repo: t.repo, ref: t.ref });
|
|
24076
|
-
await deps.control({ repo: t.repo, action: "cf-gate-allow", host: t.host, ip });
|
|
24234
|
+
await deps.control({ repo: t.repo, action: "cf-gate-allow", host: t.host, ip, ip6 });
|
|
24235
|
+
const allowed = ip6 ? `${ip} and your IPv6 /64 prefix (from ${ip6})` : `${ip} (IPv4 only \u2014 no public IPv6 detected)`;
|
|
24077
24236
|
return {
|
|
24078
24237
|
command: "stage --live",
|
|
24079
24238
|
mode: "up",
|
|
@@ -24081,10 +24240,11 @@ async function runStageLiveUp(deps, t) {
|
|
|
24081
24240
|
repo: t.repo,
|
|
24082
24241
|
ref: t.ref,
|
|
24083
24242
|
ip,
|
|
24243
|
+
ip6,
|
|
24084
24244
|
dispatched: ["tenant-deploy.yml", "tenant-control.yml"],
|
|
24085
24245
|
// #2656: the gate now writes a skip+block pair and self-verifies from the runner (a non-allowed vantage),
|
|
24086
24246
|
// so a gate that failed to close fails the run RED. The stage is private only once that gate run is green.
|
|
24087
|
-
message: `dispatched the dev deploy of ${t.ref} and the Cloudflare edge gate for ${t.host} \u2192 ${
|
|
24247
|
+
message: `dispatched the dev deploy of ${t.ref} and the Cloudflare edge gate for ${t.host} \u2192 ${allowed}; watch the runs in ${STAGE_LIVE_HUB_REPO} Actions and treat the stage as private ONLY once the cf-gate-allow run is green (it self-verifies the host is blocked) \u2014 tear down with: mmi-cli stage --live --down --apply`
|
|
24088
24248
|
};
|
|
24089
24249
|
}
|
|
24090
24250
|
async function runStageLiveDown(deps, t) {
|
|
@@ -24221,13 +24381,13 @@ function registerStageCommands(program3) {
|
|
|
24221
24381
|
}
|
|
24222
24382
|
const rcDeps = registryClientDeps(await loadConfig());
|
|
24223
24383
|
const deps = {
|
|
24224
|
-
detectIp: () =>
|
|
24384
|
+
detectIp: () => detectCallerIps(),
|
|
24225
24385
|
deployDev: async ({ repo, ref }) => {
|
|
24226
24386
|
const res = await tenantDeploy({ repo, stage: "dev", ref }, rcDeps);
|
|
24227
24387
|
if (!res.ok) throw new Error(`dev deploy dispatch failed: ${res.body?.error ?? res.error ?? `HTTP ${res.status}`}`);
|
|
24228
24388
|
},
|
|
24229
|
-
control: async ({ repo, action, host, ip }) => {
|
|
24230
|
-
const res = await tenantControl({ repo, stage: "dev", action, host, ip }, rcDeps);
|
|
24389
|
+
control: async ({ repo, action, host, ip, ip6 }) => {
|
|
24390
|
+
const res = await tenantControl({ repo, stage: "dev", action, host, ip, ip6 }, rcDeps);
|
|
24231
24391
|
if (!res.ok) throw new Error(`runtime tenant control ${action} dispatch failed: ${res.body?.error ?? res.error ?? `HTTP ${res.status}`}`);
|
|
24232
24392
|
}
|
|
24233
24393
|
};
|
|
@@ -25419,6 +25579,88 @@ async function fetchRestCorePool(gh = defaultGhApi) {
|
|
|
25419
25579
|
}
|
|
25420
25580
|
}
|
|
25421
25581
|
|
|
25582
|
+
// src/pr-create-docs-check.ts
|
|
25583
|
+
var GIT_TIMEOUT_MS2 = 15e3;
|
|
25584
|
+
function createPrCreateDocsIndexDeps() {
|
|
25585
|
+
return {
|
|
25586
|
+
worktreeRoot: async () => {
|
|
25587
|
+
try {
|
|
25588
|
+
const { stdout } = await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS2 });
|
|
25589
|
+
return stdout.trim() || void 0;
|
|
25590
|
+
} catch {
|
|
25591
|
+
return void 0;
|
|
25592
|
+
}
|
|
25593
|
+
},
|
|
25594
|
+
originRepo: async (root) => {
|
|
25595
|
+
try {
|
|
25596
|
+
const { stdout } = await execFileP2("git", ["-C", root, "remote", "get-url", "origin"], { timeout: GIT_TIMEOUT_MS2 });
|
|
25597
|
+
return repoFromRemoteUrl(stdout.trim());
|
|
25598
|
+
} catch {
|
|
25599
|
+
return void 0;
|
|
25600
|
+
}
|
|
25601
|
+
},
|
|
25602
|
+
refResolves: async (root, ref) => {
|
|
25603
|
+
try {
|
|
25604
|
+
await execFileP2("git", ["-C", root, "rev-parse", "--verify", "--quiet", `${ref}^{commit}`], { timeout: GIT_TIMEOUT_MS2 });
|
|
25605
|
+
return true;
|
|
25606
|
+
} catch {
|
|
25607
|
+
return false;
|
|
25608
|
+
}
|
|
25609
|
+
},
|
|
25610
|
+
listDocsAtRef: async (root, ref) => {
|
|
25611
|
+
try {
|
|
25612
|
+
const { stdout } = await execFileP2("git", ["-C", root, "ls-tree", "-r", "--name-only", ref, "--", "docs"], { timeout: GIT_TIMEOUT_MS2 });
|
|
25613
|
+
return stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
25614
|
+
} catch {
|
|
25615
|
+
return [];
|
|
25616
|
+
}
|
|
25617
|
+
},
|
|
25618
|
+
readAtRef: async (root, ref, path2) => {
|
|
25619
|
+
try {
|
|
25620
|
+
const { stdout } = await execFileP2("git", ["-C", root, "show", `${ref}:${path2}`], { timeout: GIT_TIMEOUT_MS2 });
|
|
25621
|
+
return stdout;
|
|
25622
|
+
} catch {
|
|
25623
|
+
return void 0;
|
|
25624
|
+
}
|
|
25625
|
+
}
|
|
25626
|
+
};
|
|
25627
|
+
}
|
|
25628
|
+
async function checkDocsIndexAtHead(opts, deps) {
|
|
25629
|
+
const root = await deps.worktreeRoot();
|
|
25630
|
+
if (!root) return void 0;
|
|
25631
|
+
if (opts.repo) {
|
|
25632
|
+
const origin = await deps.originRepo(root);
|
|
25633
|
+
if (!origin || origin.toLowerCase() !== opts.repo.toLowerCase()) return void 0;
|
|
25634
|
+
}
|
|
25635
|
+
const ref = opts.head || "HEAD";
|
|
25636
|
+
if (!await deps.refResolves(root, ref)) return void 0;
|
|
25637
|
+
const paths = await deps.listDocsAtRef(root, ref);
|
|
25638
|
+
if (!paths.includes(DOCS_INDEX_PATH)) return void 0;
|
|
25639
|
+
const relDocs = paths.filter((p) => p.startsWith("docs/")).map((p) => p.slice("docs/".length)).filter(isRoutableDocsPath);
|
|
25640
|
+
const [indexContent, docContents] = await Promise.all([
|
|
25641
|
+
deps.readAtRef(root, ref, DOCS_INDEX_PATH),
|
|
25642
|
+
Promise.all(relDocs.map((rel) => deps.readAtRef(root, ref, `docs/${rel}`)))
|
|
25643
|
+
]);
|
|
25644
|
+
const contentByPath = new Map(relDocs.map((rel, i) => [rel, docContents[i] ?? ""]));
|
|
25645
|
+
const atRefDeps = {
|
|
25646
|
+
listDocs: () => relDocs,
|
|
25647
|
+
readDoc: (rel) => contentByPath.get(rel) ?? "",
|
|
25648
|
+
readIndex: () => indexContent ?? null,
|
|
25649
|
+
writeIndex: () => {
|
|
25650
|
+
throw new Error("checkDocsIndexAtHead: read-only \u2014 a commit ref is never written to");
|
|
25651
|
+
}
|
|
25652
|
+
};
|
|
25653
|
+
const result = docsIndex(atRefDeps, { check: true });
|
|
25654
|
+
if (!result.drift) {
|
|
25655
|
+
return { ok: true, detail: `${DOCS_INDEX_PATH} matches the docs/ tree at ${ref === "HEAD" ? "HEAD" : ref}` };
|
|
25656
|
+
}
|
|
25657
|
+
return {
|
|
25658
|
+
ok: false,
|
|
25659
|
+
detail: `${DOCS_INDEX_PATH} is stale at the commit this PR would open from \u2014 it no longer matches the docs/ tree there`,
|
|
25660
|
+
fix: "run `mmi-cli docs index --write`, commit docs/index.md, and push again before retrying `pr create`"
|
|
25661
|
+
};
|
|
25662
|
+
}
|
|
25663
|
+
|
|
25422
25664
|
// src/worktree-lifecycle-commands.ts
|
|
25423
25665
|
var import_node_fs31 = require("node:fs");
|
|
25424
25666
|
var import_promises9 = require("node:fs/promises");
|
|
@@ -28911,6 +29153,32 @@ function checkSessionPayload(probe) {
|
|
|
28911
29153
|
verbose: evidence
|
|
28912
29154
|
};
|
|
28913
29155
|
}
|
|
29156
|
+
function checkDocsIndex(probe) {
|
|
29157
|
+
if (!probe) return null;
|
|
29158
|
+
const evidence = [
|
|
29159
|
+
// The WORKING TREE, said out loud. This row reads disk and never asks git what is staged or committed,
|
|
29160
|
+
// so it must not claim to have measured the commit: a developer who ran `--write` and has not committed
|
|
29161
|
+
// has a current tree and a stale HEAD, and a row that said "committed: current" there would be the one
|
|
29162
|
+
// failure this check exists to prevent, one layer down. Closing that gap belongs to the surface that
|
|
29163
|
+
// knows about commits (`pr create`, #4092), not to a local hygiene reading.
|
|
29164
|
+
`${DOCS_INDEX_PATH}: read from the working tree, not from HEAD`,
|
|
29165
|
+
`records indexed: ${probe.docCount}`,
|
|
29166
|
+
// Named because it is the one thing a Windows operator reproducing a CI failure needs to trust the
|
|
29167
|
+
// verdict: the comparison normalizes CRLF, so this row and the Linux gate agree on the same content (#3411).
|
|
29168
|
+
"comparison: eol-insensitive, against the index the generator renders now"
|
|
29169
|
+
];
|
|
29170
|
+
if (!probe.drift) {
|
|
29171
|
+
return { ok: true, id: "docs-index", label: "docs index", detail: "current", verbose: evidence };
|
|
29172
|
+
}
|
|
29173
|
+
return {
|
|
29174
|
+
ok: false,
|
|
29175
|
+
id: "docs-index",
|
|
29176
|
+
label: "docs index",
|
|
29177
|
+
detail: "stale \u2014 docs/index.md no longer matches the docs/ tree",
|
|
29178
|
+
fix: "run `mmi-cli docs index --write` and commit docs/index.md",
|
|
29179
|
+
verbose: evidence
|
|
29180
|
+
};
|
|
29181
|
+
}
|
|
28914
29182
|
function planGitignore(current) {
|
|
28915
29183
|
const { content, changed } = upsertManagedGitignoreBlock(current);
|
|
28916
29184
|
return changed ? { ok: false, content } : { ok: true };
|
|
@@ -29058,8 +29326,8 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
29058
29326
|
if (applyEnv) {
|
|
29059
29327
|
const healed = deps.healMarketplacePins();
|
|
29060
29328
|
if (healed) {
|
|
29061
|
-
healIntent(`marketplace pins \u2014 ${healed}`);
|
|
29062
|
-
restartPending = true;
|
|
29329
|
+
healIntent(`marketplace pins \u2014 ${healed.detail}`);
|
|
29330
|
+
if (healed.wrote) restartPending = true;
|
|
29063
29331
|
}
|
|
29064
29332
|
}
|
|
29065
29333
|
for (const row of deps.marketplaceRows()) emitNow(row);
|
|
@@ -29078,6 +29346,28 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
29078
29346
|
});
|
|
29079
29347
|
}
|
|
29080
29348
|
}
|
|
29349
|
+
async function runDocsIndexRow() {
|
|
29350
|
+
let probe;
|
|
29351
|
+
try {
|
|
29352
|
+
probe = deps.docsIndexState(await deps.repoRoot());
|
|
29353
|
+
} catch (e) {
|
|
29354
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
29355
|
+
emitNow({
|
|
29356
|
+
ok: false,
|
|
29357
|
+
id: "docs-index",
|
|
29358
|
+
label: "docs index",
|
|
29359
|
+
// The measurement goes in `detail` and the one next action in `fix`, per docs/doctor-contract.md
|
|
29360
|
+
// § check model — never the error text stapled to the front of the advice, and never "re-run the
|
|
29361
|
+
// read that just failed" as the action.
|
|
29362
|
+
detail: `could not be read \u2014 ${message}`,
|
|
29363
|
+
fix: "repair the docs/ tree this repo cannot walk, then re-run doctor",
|
|
29364
|
+
verbose: [`probe threw: ${message}`]
|
|
29365
|
+
});
|
|
29366
|
+
return;
|
|
29367
|
+
}
|
|
29368
|
+
const docs2 = checkDocsIndex(probe);
|
|
29369
|
+
if (docs2) emitNow(docs2);
|
|
29370
|
+
}
|
|
29081
29371
|
async function runHousekeeperRows() {
|
|
29082
29372
|
const repoRoot2 = await deps.repoRoot();
|
|
29083
29373
|
try {
|
|
@@ -29148,6 +29438,9 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
29148
29438
|
{ id: "plugin-cache", when: true, run: runPluginCacheRow },
|
|
29149
29439
|
{ id: "sessionstart-payload", when: true, run: runSessionPayloadRow },
|
|
29150
29440
|
{ id: "marketplace", when: true, run: runMarketplaceRows },
|
|
29441
|
+
// A `docs/` tree walk plus one batched `git check-ignore` — cheap next to the two rows below it, but a
|
|
29442
|
+
// disk walk and a subprocess all the same, so full lane only (#4091).
|
|
29443
|
+
{ id: "docs-index", when: isOrgRepo && lane.full, run: runDocsIndexRow },
|
|
29151
29444
|
// The two most expensive things in this file — a real `git fetch` plus train-branch fast-forward,
|
|
29152
29445
|
// and a `gh`-backed gc sweep with a 20s timeout — so org repos on the full lane only (#3485).
|
|
29153
29446
|
{ id: "train-branches", when: isOrgRepo && lane.full, run: runTrainSyncRow },
|
|
@@ -29589,13 +29882,29 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
29589
29882
|
const home = (0, import_node_os14.homedir)();
|
|
29590
29883
|
const names = [MMI_MARKETPLACE_NAME];
|
|
29591
29884
|
const result = applyOrgMarketplacePins((0, import_node_path35.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), names);
|
|
29592
|
-
if (result?.
|
|
29885
|
+
if (result?.wrote) {
|
|
29593
29886
|
writeMarketplacePinPending((0, import_node_path35.join)(home, ".claude", "plugins", ".mmi-marketplace-pin-pending.json"), names);
|
|
29594
29887
|
}
|
|
29595
29888
|
return result;
|
|
29596
29889
|
} catch {
|
|
29597
29890
|
return void 0;
|
|
29598
29891
|
}
|
|
29892
|
+
},
|
|
29893
|
+
// #4091: the same `docs index --check` comparison the required CI gate runs, in-process. Rooted at the
|
|
29894
|
+
// repo root doctor already resolved — never `process.cwd()`, which would have `mmi-cli doctor` run from
|
|
29895
|
+
// `cli/` measure a `cli/docs/` tree that does not exist.
|
|
29896
|
+
//
|
|
29897
|
+
// `existsSync` on `docs/index.md` is the adoption gate, and it sits UNDER the table's org-repo gate
|
|
29898
|
+
// rather than replacing it: a missing index is drift by construction, so without this a repo that never
|
|
29899
|
+
// adopted the generated routing index (MMG-Unlive: `docs/Archive/**` only, no index, no gate.yml) would
|
|
29900
|
+
// get a permanent ✗ demanding an artifact it never asked for.
|
|
29901
|
+
docsIndexState: (root) => {
|
|
29902
|
+
if (!(0, import_node_fs36.existsSync)((0, import_node_path35.join)(root, DOCS_INDEX_PATH))) return void 0;
|
|
29903
|
+
const real = createDocsIndexDeps(root);
|
|
29904
|
+
let docs2;
|
|
29905
|
+
const listDocs = () => docs2 ??= real.listDocs();
|
|
29906
|
+
const drift = docsIndex({ ...real, listDocs }, { check: true }).drift;
|
|
29907
|
+
return { drift, docCount: listDocs().length };
|
|
29599
29908
|
}
|
|
29600
29909
|
};
|
|
29601
29910
|
}
|
|
@@ -31440,6 +31749,10 @@ withExamples(pr.command("create").description("create a PR and print {number,url
|
|
|
31440
31749
|
return fail(`pr create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
|
|
31441
31750
|
}
|
|
31442
31751
|
body = normalizeClosingDirectives(body);
|
|
31752
|
+
const docsCheck = await checkDocsIndexAtHead({ repo: o.repo, head: o.head }, createPrCreateDocsIndexDeps());
|
|
31753
|
+
if (docsCheck && !docsCheck.ok) {
|
|
31754
|
+
return fail(`pr create: ${docsCheck.detail} \u2014 ${docsCheck.fix}`);
|
|
31755
|
+
}
|
|
31443
31756
|
const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo, draft: o.draft }));
|
|
31444
31757
|
console.log(JSON.stringify(created));
|
|
31445
31758
|
}), [
|
package/package.json
CHANGED