@mutmutco/claude-plugin 4.6.47 → 4.6.49
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/.claude-plugin/plugin.json +1 -1
- package/cli/dist/main.cjs +961 -389
- package/cli/package.json +1 -1
- package/package.json +1 -1
- package/skills/board/SKILL.md +3 -1
- package/skills/board/reference.md +8 -0
- package/skills/bootstrap/seeds/manifest.json +2 -0
- package/skills/bootstrap/seeds/runbook-unaffected.json +8 -0
- package/skills/hotfix/SKILL.md +1 -1
- package/skills/org-guide/references/design.md +13 -2
- package/skills/release/SKILL.md +1 -1
- package/skills/release/suite.md +1 -1
- package/skills/release/train-common.md +1 -1
package/cli/dist/main.cjs
CHANGED
|
@@ -80,13 +80,13 @@ var init_compat = __esm({
|
|
|
80
80
|
// src/client-version.ts
|
|
81
81
|
function resolveClientVersionManifestCandidates(distDir = __dirname) {
|
|
82
82
|
return [
|
|
83
|
-
(0,
|
|
84
|
-
(0,
|
|
83
|
+
(0, import_node_path2.join)(distDir, "..", "..", ".claude-plugin", "plugin.json"),
|
|
84
|
+
(0, import_node_path2.join)(distDir, "..", "package.json")
|
|
85
85
|
];
|
|
86
86
|
}
|
|
87
87
|
function readVersionFromManifest(path2) {
|
|
88
88
|
try {
|
|
89
|
-
const version = JSON.parse((0,
|
|
89
|
+
const version = JSON.parse((0, import_node_fs2.readFileSync)(path2, "utf8")).version;
|
|
90
90
|
return typeof version === "string" && version.trim() ? version.trim() : null;
|
|
91
91
|
} catch {
|
|
92
92
|
return null;
|
|
@@ -106,12 +106,12 @@ function upgradeRequiredError(res, body) {
|
|
|
106
106
|
const minVersion = body && typeof body === "object" && typeof body.minVersion === "string" ? body.minVersion : "a newer version";
|
|
107
107
|
return `Hub requires mmi-cli >= ${minVersion} \u2014 run mmi-cli doctor (installed ${resolveClientVersion()})`;
|
|
108
108
|
}
|
|
109
|
-
var
|
|
109
|
+
var import_node_fs2, import_node_path2;
|
|
110
110
|
var init_client_version = __esm({
|
|
111
111
|
"src/client-version.ts"() {
|
|
112
112
|
"use strict";
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
import_node_fs2 = require("node:fs");
|
|
114
|
+
import_node_path2 = require("node:path");
|
|
115
115
|
init_compat();
|
|
116
116
|
}
|
|
117
117
|
});
|
|
@@ -470,9 +470,9 @@ function recordGhUsage(via, pool, op) {
|
|
|
470
470
|
if (process.env.MMI_GH_USAGE === "off" || process.env.VITEST && !process.env.MMI_GH_USAGE_DIR) return;
|
|
471
471
|
try {
|
|
472
472
|
const dir = usageDir();
|
|
473
|
-
(0,
|
|
473
|
+
(0, import_node_fs3.mkdirSync)(dir, { recursive: true });
|
|
474
474
|
const row = { t: Date.now(), cmd: commandPath(), via, pool, op: shapeOp(op) };
|
|
475
|
-
(0,
|
|
475
|
+
(0, import_node_fs3.appendFileSync)((0, import_node_path3.join)(dir, `${new Date(row.t).toISOString().slice(0, 13)}.jsonl`), `${JSON.stringify(row)}
|
|
476
476
|
`);
|
|
477
477
|
} catch {
|
|
478
478
|
}
|
|
@@ -483,12 +483,12 @@ function summarizeGhUsage(hours = 1, now = Date.now(), env = process.env) {
|
|
|
483
483
|
const rows = [];
|
|
484
484
|
let files = [];
|
|
485
485
|
try {
|
|
486
|
-
files = (0,
|
|
486
|
+
files = (0, import_node_fs3.readdirSync)(dir).filter((f) => f.endsWith(".jsonl") && Date.parse(`${f.slice(0, 13)}:00:00Z`) >= since - 36e5);
|
|
487
487
|
} catch {
|
|
488
488
|
files = [];
|
|
489
489
|
}
|
|
490
490
|
for (const file of files) {
|
|
491
|
-
for (const line of (0,
|
|
491
|
+
for (const line of (0, import_node_fs3.readFileSync)((0, import_node_path3.join)(dir, file), "utf8").split("\n")) {
|
|
492
492
|
try {
|
|
493
493
|
const row = JSON.parse(line);
|
|
494
494
|
if (row.t >= since) rows.push(row);
|
|
@@ -507,14 +507,14 @@ function summarizeGhUsage(hours = 1, now = Date.now(), env = process.env) {
|
|
|
507
507
|
}
|
|
508
508
|
return { hours, total: rows.length, byPool, top: [...groups.values()].sort((a, b) => b.count - a.count).slice(0, 20) };
|
|
509
509
|
}
|
|
510
|
-
var
|
|
510
|
+
var import_node_fs3, import_node_os, import_node_path3, usageDir, cachedCommand;
|
|
511
511
|
var init_gh_usage = __esm({
|
|
512
512
|
"src/gh-usage.ts"() {
|
|
513
513
|
"use strict";
|
|
514
|
-
|
|
514
|
+
import_node_fs3 = require("node:fs");
|
|
515
515
|
import_node_os = require("node:os");
|
|
516
|
-
|
|
517
|
-
usageDir = (env = process.env) => env.MMI_GH_USAGE_DIR || (0,
|
|
516
|
+
import_node_path3 = require("node:path");
|
|
517
|
+
usageDir = (env = process.env) => env.MMI_GH_USAGE_DIR || (0, import_node_path3.join)((0, import_node_os.homedir)(), ".mmi", "gh-usage");
|
|
518
518
|
}
|
|
519
519
|
});
|
|
520
520
|
|
|
@@ -691,7 +691,7 @@ function createGitHubClient(options = {}) {
|
|
|
691
691
|
if (!res.ok) throw await errorFromResponse(res);
|
|
692
692
|
return res;
|
|
693
693
|
}
|
|
694
|
-
async function
|
|
694
|
+
async function parseJson2(res) {
|
|
695
695
|
if (res.status === 204) return void 0;
|
|
696
696
|
const text = await res.text();
|
|
697
697
|
if (!text) return void 0;
|
|
@@ -700,7 +700,7 @@ function createGitHubClient(options = {}) {
|
|
|
700
700
|
let viewer;
|
|
701
701
|
return {
|
|
702
702
|
async rest(method, path2, init) {
|
|
703
|
-
const read = async () =>
|
|
703
|
+
const read = async () => parseJson2(await request(method, joinUrl(baseUrl, path2), init));
|
|
704
704
|
if (method === "GET" && path2.replace(/^\//, "") === "user" && !init) {
|
|
705
705
|
viewer ??= read().catch((e) => {
|
|
706
706
|
viewer = void 0;
|
|
@@ -712,14 +712,14 @@ function createGitHubClient(options = {}) {
|
|
|
712
712
|
},
|
|
713
713
|
async restWithStatus(method, path2, init) {
|
|
714
714
|
const res = await request(method, joinUrl(baseUrl, path2), init);
|
|
715
|
-
return { status: res.status, body: await
|
|
715
|
+
return { status: res.status, body: await parseJson2(res) };
|
|
716
716
|
},
|
|
717
717
|
async restPaginate(path2, init) {
|
|
718
718
|
const items = [];
|
|
719
719
|
let url = withPerPage(joinUrl(baseUrl, path2));
|
|
720
720
|
while (url) {
|
|
721
721
|
const res = await request("GET", url, init);
|
|
722
|
-
const page = await
|
|
722
|
+
const page = await parseJson2(res);
|
|
723
723
|
if (!Array.isArray(page)) {
|
|
724
724
|
throw new GitHubApiError(
|
|
725
725
|
`pagination page was not a JSON array (got ${page === void 0 ? "an empty body" : typeof page}) \u2014 the list read is PARTIAL and must not be treated as complete: ${url}. Retry the call.`,
|
|
@@ -736,7 +736,7 @@ function createGitHubClient(options = {}) {
|
|
|
736
736
|
...init,
|
|
737
737
|
body: { query, ...variables ? { variables } : {} }
|
|
738
738
|
});
|
|
739
|
-
const parsed = await
|
|
739
|
+
const parsed = await parseJson2(res);
|
|
740
740
|
if (parsed?.errors?.length) {
|
|
741
741
|
const message2 = parsed.errors.map((e) => e.message ?? e.type ?? "unknown GraphQL error").join("; ");
|
|
742
742
|
const rateLimited = parsed.errors.some((e) => e.type === "RATE_LIMITED" || /rate limit/i.test(e.message ?? ""));
|
|
@@ -988,11 +988,11 @@ function tokenFingerprint(token) {
|
|
|
988
988
|
function defaultHubSessionCachePath(env = process.env) {
|
|
989
989
|
if (env.MMI_HUB_SESSION_CACHE) return env.MMI_HUB_SESSION_CACHE;
|
|
990
990
|
if (process.platform === "win32") {
|
|
991
|
-
const base2 = env.LOCALAPPDATA || (0,
|
|
992
|
-
return (0,
|
|
991
|
+
const base2 = env.LOCALAPPDATA || (0, import_node_path4.join)((0, import_node_os2.homedir)(), "AppData", "Local");
|
|
992
|
+
return (0, import_node_path4.join)(base2, "MMI Future", "mmi-cli", "hub-session.json");
|
|
993
993
|
}
|
|
994
|
-
const base = env.XDG_STATE_HOME || (0,
|
|
995
|
-
return (0,
|
|
994
|
+
const base = env.XDG_STATE_HOME || (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".local", "state");
|
|
995
|
+
return (0, import_node_path4.join)(base, "mmi-cli", "hub-session.json");
|
|
996
996
|
}
|
|
997
997
|
function roleFromToken(token) {
|
|
998
998
|
const part = token?.split(".")[1];
|
|
@@ -1006,7 +1006,7 @@ function roleFromToken(token) {
|
|
|
1006
1006
|
}
|
|
1007
1007
|
function readCache(path2, apiUrl, now, githubTokenFingerprint) {
|
|
1008
1008
|
try {
|
|
1009
|
-
const session = JSON.parse((0,
|
|
1009
|
+
const session = JSON.parse((0, import_node_fs4.readFileSync)(path2, "utf8"));
|
|
1010
1010
|
if (!session.token || !session.expiresAt || session.apiUrl !== apiUrl) return { session: null, stale: false };
|
|
1011
1011
|
if (session.githubTokenFingerprint !== githubTokenFingerprint) {
|
|
1012
1012
|
return { session: null, stale: true, staleLogin: typeof session.login === "string" ? session.login : void 0 };
|
|
@@ -1018,16 +1018,16 @@ function readCache(path2, apiUrl, now, githubTokenFingerprint) {
|
|
|
1018
1018
|
}
|
|
1019
1019
|
}
|
|
1020
1020
|
function writeCache(path2, session) {
|
|
1021
|
-
(0,
|
|
1021
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path2), { recursive: true });
|
|
1022
1022
|
const tmp = `${path2}.${process.pid}.${Date.now()}.tmp`;
|
|
1023
|
-
(0,
|
|
1023
|
+
(0, import_node_fs4.writeFileSync)(tmp, JSON.stringify(session, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
1024
1024
|
try {
|
|
1025
|
-
(0,
|
|
1025
|
+
(0, import_node_fs4.chmodSync)(tmp, 384);
|
|
1026
1026
|
} catch {
|
|
1027
1027
|
}
|
|
1028
|
-
(0,
|
|
1028
|
+
(0, import_node_fs4.renameSync)(tmp, path2);
|
|
1029
1029
|
try {
|
|
1030
|
-
(0,
|
|
1030
|
+
(0, import_node_fs4.chmodSync)(path2, 384);
|
|
1031
1031
|
} catch {
|
|
1032
1032
|
}
|
|
1033
1033
|
}
|
|
@@ -1139,13 +1139,13 @@ async function hubAuthSession(deps) {
|
|
|
1139
1139
|
async function hubAuthToken(deps) {
|
|
1140
1140
|
return (await hubAuthSession(deps))?.token;
|
|
1141
1141
|
}
|
|
1142
|
-
var import_node_crypto,
|
|
1142
|
+
var import_node_crypto, import_node_fs4, import_node_path4, import_node_os2, REFRESH_WINDOW_MS, EXCHANGE_TIMEOUT_MS, EXCHANGE_ATTEMPTS, EXIT_HUB_SESSION_STALE, EXIT_HUB_SESSION_REFUSED, lastFailure, MEMBERSHIP_FIX;
|
|
1143
1143
|
var init_hub_auth = __esm({
|
|
1144
1144
|
"src/hub-auth.ts"() {
|
|
1145
1145
|
"use strict";
|
|
1146
1146
|
import_node_crypto = require("node:crypto");
|
|
1147
|
-
|
|
1148
|
-
|
|
1147
|
+
import_node_fs4 = require("node:fs");
|
|
1148
|
+
import_node_path4 = require("node:path");
|
|
1149
1149
|
import_node_os2 = require("node:os");
|
|
1150
1150
|
init_fetch_retry();
|
|
1151
1151
|
init_client_version();
|
|
@@ -1317,7 +1317,7 @@ var init_house_map = __esm({
|
|
|
1317
1317
|
function setInjectedStdin(payload) {
|
|
1318
1318
|
injectedStdin = payload;
|
|
1319
1319
|
}
|
|
1320
|
-
function stdinHasPipedInput(statFd = () => (0,
|
|
1320
|
+
function stdinHasPipedInput(statFd = () => (0, import_node_fs5.fstatSync)(0), getIsTTY = () => process.stdin.isTTY) {
|
|
1321
1321
|
try {
|
|
1322
1322
|
const stat4 = statFd();
|
|
1323
1323
|
if (stat4.isFIFO() || stat4.isFile()) return true;
|
|
@@ -1365,11 +1365,11 @@ async function readStdin(opts = {}) {
|
|
|
1365
1365
|
}
|
|
1366
1366
|
return Buffer.concat(chunks).toString("utf8");
|
|
1367
1367
|
}
|
|
1368
|
-
var
|
|
1368
|
+
var import_node_fs5, injectedStdin, STDIN_MAX_BYTES, STDIN_DRAIN_TIMEOUT_MS;
|
|
1369
1369
|
var init_stdin_inject = __esm({
|
|
1370
1370
|
"src/stdin-inject.ts"() {
|
|
1371
1371
|
"use strict";
|
|
1372
|
-
|
|
1372
|
+
import_node_fs5 = require("node:fs");
|
|
1373
1373
|
STDIN_MAX_BYTES = 8 * 1024 * 1024;
|
|
1374
1374
|
STDIN_DRAIN_TIMEOUT_MS = 5e3;
|
|
1375
1375
|
}
|
|
@@ -1599,15 +1599,15 @@ function rawValue(flag, fallback) {
|
|
|
1599
1599
|
return inline ? inline.slice(flag.length + 1) : fallback;
|
|
1600
1600
|
}
|
|
1601
1601
|
function printLine(value) {
|
|
1602
|
-
(0,
|
|
1602
|
+
(0, import_node_fs6.writeSync)(1, `${value}
|
|
1603
1603
|
`);
|
|
1604
1604
|
}
|
|
1605
|
-
var import_node_child_process,
|
|
1605
|
+
var import_node_child_process, import_node_fs6, import_node_util, rawExecFileP, DEFAULT_EXEC_TIMEOUT_MS, execFileP, ghReadTokenProvider, ghWriteTokenProvider, GIT_TIMEOUT_MS, ExecDeadlineError, cachedGithubLogin, gitOut, consoleIo;
|
|
1606
1606
|
var init_cli_shared = __esm({
|
|
1607
1607
|
"src/cli-shared.ts"() {
|
|
1608
1608
|
"use strict";
|
|
1609
1609
|
import_node_child_process = require("node:child_process");
|
|
1610
|
-
|
|
1610
|
+
import_node_fs6 = require("node:fs");
|
|
1611
1611
|
import_node_util = require("node:util");
|
|
1612
1612
|
init_hub_url();
|
|
1613
1613
|
init_client_version();
|
|
@@ -9186,6 +9186,32 @@ __export(index_exports, {
|
|
|
9186
9186
|
});
|
|
9187
9187
|
module.exports = __toCommonJS(index_exports);
|
|
9188
9188
|
|
|
9189
|
+
// src/utf8-receipt.ts
|
|
9190
|
+
var import_node_fs = require("node:fs");
|
|
9191
|
+
var import_node_path = require("node:path");
|
|
9192
|
+
function writeUtf8Receipt(path2, text) {
|
|
9193
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path2), { recursive: true });
|
|
9194
|
+
const body = text.endsWith("\n") ? text : `${text}
|
|
9195
|
+
`;
|
|
9196
|
+
(0, import_node_fs.writeFileSync)(path2, body, "utf8");
|
|
9197
|
+
return Buffer.byteLength(body, "utf8");
|
|
9198
|
+
}
|
|
9199
|
+
|
|
9200
|
+
// src/tenant-control-output.ts
|
|
9201
|
+
var TENANT_CONTROL_JSON_FAILURE_HINT = " (--json: exits 1 on stderr with nothing on stdout: check the exit code before parsing, never merge stderr into stdout (2>&1); use --out <file> so a failed read leaves no file)";
|
|
9202
|
+
function tenantControlFailure(message2, json) {
|
|
9203
|
+
return json ? `${message2}${TENANT_CONTROL_JSON_FAILURE_HINT}` : message2;
|
|
9204
|
+
}
|
|
9205
|
+
function emitTenantControlOutput(text, o, io = { print: console.log }) {
|
|
9206
|
+
if (o.out === void 0) {
|
|
9207
|
+
io.print(text);
|
|
9208
|
+
return;
|
|
9209
|
+
}
|
|
9210
|
+
if (o.failed) return;
|
|
9211
|
+
const bytes = (io.write ?? writeUtf8Receipt)(o.out, text);
|
|
9212
|
+
io.print(`Wrote tenant control ${o.action} to ${o.out} (UTF-8, ${bytes} bytes)`);
|
|
9213
|
+
}
|
|
9214
|
+
|
|
9189
9215
|
// ../shared/quota-attribution.ts
|
|
9190
9216
|
var ATTRIBUTION_MAX = 120;
|
|
9191
9217
|
function sanitizeAttribution(value, max = ATTRIBUTION_MAX) {
|
|
@@ -9205,9 +9231,9 @@ var import_node_crypto2 = require("node:crypto");
|
|
|
9205
9231
|
var import_node_os5 = require("node:os");
|
|
9206
9232
|
|
|
9207
9233
|
// src/plugin-guard-io.ts
|
|
9208
|
-
var
|
|
9234
|
+
var import_node_fs8 = require("node:fs");
|
|
9209
9235
|
var import_node_child_process2 = require("node:child_process");
|
|
9210
|
-
var
|
|
9236
|
+
var import_node_path5 = require("node:path");
|
|
9211
9237
|
var import_node_os4 = require("node:os");
|
|
9212
9238
|
|
|
9213
9239
|
// src/marketplace-autoupdate.ts
|
|
@@ -9439,7 +9465,7 @@ function buildPluginGuardLine(state, opts = {}) {
|
|
|
9439
9465
|
}
|
|
9440
9466
|
|
|
9441
9467
|
// src/host-doc-lock.ts
|
|
9442
|
-
var
|
|
9468
|
+
var import_node_fs7 = require("node:fs");
|
|
9443
9469
|
var import_node_os3 = require("node:os");
|
|
9444
9470
|
var SHARED_DOC_LOCK_STALE_MS = 3e4;
|
|
9445
9471
|
var SHARED_DOC_LOCK_ATTEMPTS = 50;
|
|
@@ -9453,7 +9479,7 @@ function lockContent(options) {
|
|
|
9453
9479
|
}
|
|
9454
9480
|
function readHolder(lockPath) {
|
|
9455
9481
|
try {
|
|
9456
|
-
const parsed = JSON.parse((0,
|
|
9482
|
+
const parsed = JSON.parse((0, import_node_fs7.readFileSync)(lockPath, "utf8"));
|
|
9457
9483
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
9458
9484
|
return parsed;
|
|
9459
9485
|
} catch {
|
|
@@ -9465,7 +9491,7 @@ function lockAgeMs(lockPath, now) {
|
|
|
9465
9491
|
const declared = typeof holder?.acquiredAt === "string" ? Date.parse(holder.acquiredAt) : Number.NaN;
|
|
9466
9492
|
if (!Number.isNaN(declared)) return Math.max(0, now - declared);
|
|
9467
9493
|
try {
|
|
9468
|
-
return Math.max(0, now - (0,
|
|
9494
|
+
return Math.max(0, now - (0, import_node_fs7.statSync)(lockPath).mtimeMs);
|
|
9469
9495
|
} catch {
|
|
9470
9496
|
return 0;
|
|
9471
9497
|
}
|
|
@@ -9488,9 +9514,9 @@ function acquireSharedDocLock(documentPath, options) {
|
|
|
9488
9514
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
9489
9515
|
let fd;
|
|
9490
9516
|
try {
|
|
9491
|
-
fd = (0,
|
|
9492
|
-
(0,
|
|
9493
|
-
(0,
|
|
9517
|
+
fd = (0, import_node_fs7.openSync)(lockPath, "wx");
|
|
9518
|
+
(0, import_node_fs7.writeSync)(fd, content);
|
|
9519
|
+
(0, import_node_fs7.closeSync)(fd);
|
|
9494
9520
|
return {
|
|
9495
9521
|
documentPath,
|
|
9496
9522
|
lockPath,
|
|
@@ -9503,11 +9529,11 @@ function acquireSharedDocLock(documentPath, options) {
|
|
|
9503
9529
|
} catch (error) {
|
|
9504
9530
|
if (fd !== void 0) {
|
|
9505
9531
|
try {
|
|
9506
|
-
(0,
|
|
9532
|
+
(0, import_node_fs7.closeSync)(fd);
|
|
9507
9533
|
} catch {
|
|
9508
9534
|
}
|
|
9509
9535
|
try {
|
|
9510
|
-
(0,
|
|
9536
|
+
(0, import_node_fs7.unlinkSync)(lockPath);
|
|
9511
9537
|
} catch {
|
|
9512
9538
|
}
|
|
9513
9539
|
}
|
|
@@ -9516,12 +9542,12 @@ function acquireSharedDocLock(documentPath, options) {
|
|
|
9516
9542
|
if (lockAgeMs(lockPath, Date.now()) >= staleAfterMs) {
|
|
9517
9543
|
const aside = `${lockPath}.stale-${Date.now()}`;
|
|
9518
9544
|
try {
|
|
9519
|
-
(0,
|
|
9545
|
+
(0, import_node_fs7.renameSync)(lockPath, aside);
|
|
9520
9546
|
} catch {
|
|
9521
9547
|
continue;
|
|
9522
9548
|
}
|
|
9523
9549
|
try {
|
|
9524
|
-
(0,
|
|
9550
|
+
(0, import_node_fs7.rmSync)(aside, { force: true });
|
|
9525
9551
|
} catch {
|
|
9526
9552
|
}
|
|
9527
9553
|
continue;
|
|
@@ -9534,7 +9560,7 @@ function releaseSharedDocLock(handle) {
|
|
|
9534
9560
|
try {
|
|
9535
9561
|
const holder = readHolder(handle.lockPath);
|
|
9536
9562
|
if (holder && holder.owner === handle.owner && Number(holder.pid) === handle.pid) {
|
|
9537
|
-
(0,
|
|
9563
|
+
(0, import_node_fs7.unlinkSync)(handle.lockPath);
|
|
9538
9564
|
}
|
|
9539
9565
|
} catch {
|
|
9540
9566
|
}
|
|
@@ -9560,8 +9586,8 @@ function insideOpenBotAgentWorkspace(cwd, env = process.env, home = (0, import_n
|
|
|
9560
9586
|
} catch {
|
|
9561
9587
|
return false;
|
|
9562
9588
|
}
|
|
9563
|
-
const rel = (0,
|
|
9564
|
-
return !(0,
|
|
9589
|
+
const rel = (0, import_node_path5.relative)(openbotAgentsRoot(env, home), dir);
|
|
9590
|
+
return !(0, import_node_path5.isAbsolute)(rel) && (rel.split(/[\\/]/)[0] ?? "").toLowerCase().startsWith("agent-");
|
|
9565
9591
|
}
|
|
9566
9592
|
function detectSurface(env, cwd) {
|
|
9567
9593
|
const has = (k) => Boolean(env[k]?.trim());
|
|
@@ -9692,30 +9718,30 @@ function runHostBin(bin, args, opts) {
|
|
|
9692
9718
|
return step ? execFileHard(file, argv, { ...shared, step }) : execFileP(file, argv, shared);
|
|
9693
9719
|
}
|
|
9694
9720
|
function openbotAgentsRoot(env = process.env, home = (0, import_node_os4.homedir)()) {
|
|
9695
|
-
return env.MMI_UPDATER_OPENBOT_AGENTS_DIR?.trim() || (0,
|
|
9721
|
+
return env.MMI_UPDATER_OPENBOT_AGENTS_DIR?.trim() || (0, import_node_path5.join)(home, "OpenBot", "Agents");
|
|
9696
9722
|
}
|
|
9697
9723
|
function openbotAgentWorkspaces(env = process.env, home = (0, import_node_os4.homedir)()) {
|
|
9698
9724
|
const root = openbotAgentsRoot(env, home);
|
|
9699
9725
|
try {
|
|
9700
|
-
return (0,
|
|
9726
|
+
return (0, import_node_fs8.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith("agent-")).map((entry) => (0, import_node_path5.join)(root, entry.name)).sort();
|
|
9701
9727
|
} catch {
|
|
9702
9728
|
return [];
|
|
9703
9729
|
}
|
|
9704
9730
|
}
|
|
9705
9731
|
function surfaceConfigRoot(surface, env = process.env, home = (0, import_node_os4.homedir)()) {
|
|
9706
|
-
if (surface === "codex") return env.CODEX_HOME?.trim() || (0,
|
|
9732
|
+
if (surface === "codex") return env.CODEX_HOME?.trim() || (0, import_node_path5.join)(home, ".codex");
|
|
9707
9733
|
if (surface === "openbot") return openbotAgentsRoot(env, home);
|
|
9708
9734
|
if (surface === "jervcode") {
|
|
9709
|
-
return env.JERVCODE_CODING_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim() || (0,
|
|
9735
|
+
return env.JERVCODE_CODING_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim() || (0, import_node_path5.join)(home, ".jerv", "agent");
|
|
9710
9736
|
}
|
|
9711
|
-
return (0,
|
|
9737
|
+
return (0, import_node_path5.join)(home, ".claude");
|
|
9712
9738
|
}
|
|
9713
9739
|
var installedPluginsPath = (surface = detectSurface(process.env)) => {
|
|
9714
|
-
return (0,
|
|
9740
|
+
return (0, import_node_path5.join)(surfaceConfigRoot(surface), "plugins", "installed_plugins.json");
|
|
9715
9741
|
};
|
|
9716
9742
|
function readInstalledPlugins(surface = detectSurface(process.env)) {
|
|
9717
9743
|
try {
|
|
9718
|
-
return JSON.parse((0,
|
|
9744
|
+
return JSON.parse((0, import_node_fs8.readFileSync)(installedPluginsPath(surface), "utf8"));
|
|
9719
9745
|
} catch {
|
|
9720
9746
|
return null;
|
|
9721
9747
|
}
|
|
@@ -9724,15 +9750,15 @@ function marketplaceCloneCandidates(surface, home, env = process.env) {
|
|
|
9724
9750
|
if (surface === "codex") {
|
|
9725
9751
|
const root = surfaceConfigRoot(surface, env, home);
|
|
9726
9752
|
return [
|
|
9727
|
-
(0,
|
|
9728
|
-
(0,
|
|
9753
|
+
(0, import_node_path5.join)(root, ".tmp", "marketplaces", CODEX_MARKETPLACE),
|
|
9754
|
+
(0, import_node_path5.join)(root, "plugins", "marketplaces", CODEX_MARKETPLACE)
|
|
9729
9755
|
];
|
|
9730
9756
|
}
|
|
9731
9757
|
if (surface === "openbot") return [];
|
|
9732
9758
|
if (surface === "jervcode") return [];
|
|
9733
|
-
return [(0,
|
|
9759
|
+
return [(0, import_node_path5.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
|
|
9734
9760
|
}
|
|
9735
|
-
function marketplaceClonePresent(surface, home, exists =
|
|
9761
|
+
function marketplaceClonePresent(surface, home, exists = import_node_fs8.existsSync, env = process.env) {
|
|
9736
9762
|
return marketplaceCloneCandidates(surface, home, env).some(exists);
|
|
9737
9763
|
}
|
|
9738
9764
|
function runHostBinSync(bin, args) {
|
|
@@ -9776,15 +9802,15 @@ async function fetchNpmReleasedVersion() {
|
|
|
9776
9802
|
var JERVCODE_NPM_PACKAGE = "@mutmutco/pi-plugin";
|
|
9777
9803
|
function jervcodeAgentDirs(env = process.env, home = (0, import_node_os4.homedir)()) {
|
|
9778
9804
|
const primary = surfaceConfigRoot("jervcode", env, home);
|
|
9779
|
-
const legacy = (0,
|
|
9805
|
+
const legacy = (0, import_node_path5.join)(home, ".pi", "agent");
|
|
9780
9806
|
const dirs = [primary];
|
|
9781
9807
|
if (legacy !== primary) dirs.push(legacy);
|
|
9782
|
-
return dirs.filter((dir) => (0,
|
|
9808
|
+
return dirs.filter((dir) => (0, import_node_fs8.existsSync)(dir));
|
|
9783
9809
|
}
|
|
9784
9810
|
function readPiSettings(path2) {
|
|
9785
|
-
if (!(0,
|
|
9811
|
+
if (!(0, import_node_fs8.existsSync)(path2)) return void 0;
|
|
9786
9812
|
try {
|
|
9787
|
-
const parsed = JSON.parse((0,
|
|
9813
|
+
const parsed = JSON.parse((0, import_node_fs8.readFileSync)(path2, "utf8"));
|
|
9788
9814
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
9789
9815
|
return parsed;
|
|
9790
9816
|
} catch {
|
|
@@ -9793,7 +9819,7 @@ function readPiSettings(path2) {
|
|
|
9793
9819
|
}
|
|
9794
9820
|
function mmiPiPackageEntry(env = process.env, home = (0, import_node_os4.homedir)()) {
|
|
9795
9821
|
for (const dir of jervcodeAgentDirs(env, home)) {
|
|
9796
|
-
const settings = readPiSettings((0,
|
|
9822
|
+
const settings = readPiSettings((0, import_node_path5.join)(dir, "settings.json"));
|
|
9797
9823
|
const entries = Array.isArray(settings?.packages) ? settings.packages : [];
|
|
9798
9824
|
for (const entry of entries) {
|
|
9799
9825
|
if (typeof entry !== "string") continue;
|
|
@@ -9806,7 +9832,7 @@ var LEGACY_MMI_PI_PATH = /[/\\]mutmutco[/\\]mmi[/\\]\d+\.\d+\.\d+[/\\]\.pi-plugi
|
|
|
9806
9832
|
function legacyMmiPiPathEntries(env = process.env, home = (0, import_node_os4.homedir)()) {
|
|
9807
9833
|
const found = [];
|
|
9808
9834
|
for (const dir of jervcodeAgentDirs(env, home)) {
|
|
9809
|
-
const settings = readPiSettings((0,
|
|
9835
|
+
const settings = readPiSettings((0, import_node_path5.join)(dir, "settings.json"));
|
|
9810
9836
|
const entries = Array.isArray(settings?.packages) ? settings.packages : [];
|
|
9811
9837
|
for (const entry of entries) {
|
|
9812
9838
|
if (typeof entry === "string" && LEGACY_MMI_PI_PATH.test(entry) && !found.includes(entry)) found.push(entry);
|
|
@@ -9816,8 +9842,8 @@ function legacyMmiPiPathEntries(env = process.env, home = (0, import_node_os4.ho
|
|
|
9816
9842
|
}
|
|
9817
9843
|
function mmiPiPackageRoot(env = process.env, home = (0, import_node_os4.homedir)()) {
|
|
9818
9844
|
for (const dir of jervcodeAgentDirs(env, home)) {
|
|
9819
|
-
const root = (0,
|
|
9820
|
-
if ((0,
|
|
9845
|
+
const root = (0, import_node_path5.join)(dir, "npm", "node_modules", ...JERVCODE_NPM_PACKAGE.split("/"));
|
|
9846
|
+
if ((0, import_node_fs8.existsSync)((0, import_node_path5.join)(root, "package.json"))) return root;
|
|
9821
9847
|
}
|
|
9822
9848
|
return null;
|
|
9823
9849
|
}
|
|
@@ -9825,7 +9851,7 @@ function installedJervCodePackageVersion(env = process.env, home = (0, import_no
|
|
|
9825
9851
|
const root = mmiPiPackageRoot(env, home);
|
|
9826
9852
|
if (!root) return void 0;
|
|
9827
9853
|
try {
|
|
9828
|
-
const manifest = JSON.parse((0,
|
|
9854
|
+
const manifest = JSON.parse((0, import_node_fs8.readFileSync)((0, import_node_path5.join)(root, "package.json"), "utf8"));
|
|
9829
9855
|
return typeof manifest.version === "string" ? manifest.version : void 0;
|
|
9830
9856
|
} catch {
|
|
9831
9857
|
return void 0;
|
|
@@ -9833,17 +9859,17 @@ function installedJervCodePackageVersion(env = process.env, home = (0, import_no
|
|
|
9833
9859
|
}
|
|
9834
9860
|
function mmiPiPackageHealthy(env = process.env, home = (0, import_node_os4.homedir)()) {
|
|
9835
9861
|
const root = mmiPiPackageRoot(env, home);
|
|
9836
|
-
return Boolean(root && (0,
|
|
9862
|
+
return Boolean(root && (0, import_node_fs8.existsSync)((0, import_node_path5.join)(root, "skills", "bootstrap", "SKILL.md")));
|
|
9837
9863
|
}
|
|
9838
9864
|
function piDoctorEnv(agentDir) {
|
|
9839
9865
|
return { ...process.env, PI_CODING_AGENT_DIR: agentDir };
|
|
9840
9866
|
}
|
|
9841
9867
|
function openbotManagedRecord(env = process.env, home = (0, import_node_os4.homedir)()) {
|
|
9842
9868
|
for (const workspace of openbotAgentWorkspaces(env, home)) {
|
|
9843
|
-
const path2 = (0,
|
|
9844
|
-
if (!(0,
|
|
9869
|
+
const path2 = (0, import_node_path5.join)(workspace, ".agents", "skills", ".mmi-managed.json");
|
|
9870
|
+
if (!(0, import_node_fs8.existsSync)(path2)) continue;
|
|
9845
9871
|
try {
|
|
9846
|
-
const record2 = JSON.parse((0,
|
|
9872
|
+
const record2 = JSON.parse((0, import_node_fs8.readFileSync)(path2, "utf8"));
|
|
9847
9873
|
return { workspace, ...typeof record2.version === "string" ? { version: record2.version } : {} };
|
|
9848
9874
|
} catch {
|
|
9849
9875
|
return { workspace };
|
|
@@ -9855,9 +9881,9 @@ function openbotWorkspaceSkillsHealthy(env = process.env, home = (0, import_node
|
|
|
9855
9881
|
const record2 = openbotManagedRecord(env, home);
|
|
9856
9882
|
if (!record2) return false;
|
|
9857
9883
|
try {
|
|
9858
|
-
const recordFile = JSON.parse((0,
|
|
9884
|
+
const recordFile = JSON.parse((0, import_node_fs8.readFileSync)((0, import_node_path5.join)(record2.workspace, ".agents", "skills", ".mmi-managed.json"), "utf8"));
|
|
9859
9885
|
const owned = Array.isArray(recordFile.skills) ? recordFile.skills : [];
|
|
9860
|
-
return owned.length > 0 && owned.every((name) => typeof name === "string" && (0,
|
|
9886
|
+
return owned.length > 0 && owned.every((name) => typeof name === "string" && (0, import_node_fs8.existsSync)((0, import_node_path5.join)(record2.workspace, ".agents", "skills", name, "SKILL.md")));
|
|
9861
9887
|
} catch {
|
|
9862
9888
|
return false;
|
|
9863
9889
|
}
|
|
@@ -9877,8 +9903,8 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
|
|
|
9877
9903
|
// JervCode's settings entry and native local surfaces have no marketplace clone.
|
|
9878
9904
|
marketplaceClonePresent: surface === "jervcode" || surface === "openbot" ? true : marketplaceClonePresent(surface, (0, import_node_os4.homedir)()),
|
|
9879
9905
|
pluginCachePresent: surface === "jervcode" ? mmiPiPackageHealthy() : surface === "openbot" ? openbotWorkspaceSkillsHealthy() : surface === "codex" ? Boolean(
|
|
9880
|
-
codexStatus?.installed && codexStatus.enabled && codexStatus.version && (0,
|
|
9881
|
-
) : (0,
|
|
9906
|
+
codexStatus?.installed && codexStatus.enabled && codexStatus.version && (0, import_node_fs8.existsSync)((0, import_node_path5.join)(root, "plugins", "cache", CODEX_MARKETPLACE, "mmi", codexStatus.version))
|
|
9907
|
+
) : (0, import_node_fs8.existsSync)((0, import_node_path5.join)(root, "plugins", "cache", "mutmutco", "mmi"))
|
|
9882
9908
|
};
|
|
9883
9909
|
}
|
|
9884
9910
|
async function runPluginCli(bin, args, log) {
|
|
@@ -9925,7 +9951,7 @@ async function runHealSteps(host, tableSteps, deps) {
|
|
|
9925
9951
|
const refSupported = needsRefProbe ? await marketplaceAddRefSupported(host) : true;
|
|
9926
9952
|
const { steps } = adaptHealStepsForRefSupport(tableSteps, refSupported);
|
|
9927
9953
|
if (deps.banner) log(deps.banner(refSupported));
|
|
9928
|
-
const pinsPath = (0,
|
|
9954
|
+
const pinsPath = (0, import_node_path5.join)((0, import_node_os4.homedir)(), ...KNOWN_MARKETPLACES_RELATIVE);
|
|
9929
9955
|
const pins = host === "claude" ? captureMarketplacePins(readKnownMarketplacesFile(pinsPath), [MMI_MARKETPLACE_NAME, JERV_MARKETPLACE_NAME]) : /* @__PURE__ */ new Map();
|
|
9930
9956
|
let failure;
|
|
9931
9957
|
try {
|
|
@@ -10036,7 +10062,7 @@ async function healActivePluginForDoctor(surface = detectSurface(process.env), o
|
|
|
10036
10062
|
}
|
|
10037
10063
|
function readKnownMarketplacesFile(path2) {
|
|
10038
10064
|
try {
|
|
10039
|
-
return (0,
|
|
10065
|
+
return (0, import_node_fs8.existsSync)(path2) ? (0, import_node_fs8.readFileSync)(path2, "utf8") : void 0;
|
|
10040
10066
|
} catch {
|
|
10041
10067
|
return void 0;
|
|
10042
10068
|
}
|
|
@@ -10070,11 +10096,11 @@ function writeMarketplacePinsOnDisk(path2, pins, succeeded, failedVerb, declineW
|
|
|
10070
10096
|
if (declined) return declined;
|
|
10071
10097
|
const tmp = `${path2}.tmp-${process.pid}`;
|
|
10072
10098
|
try {
|
|
10073
|
-
(0,
|
|
10074
|
-
(0,
|
|
10099
|
+
(0, import_node_fs8.writeFileSync)(tmp, next, "utf8");
|
|
10100
|
+
(0, import_node_fs8.renameSync)(tmp, path2);
|
|
10075
10101
|
} catch {
|
|
10076
10102
|
try {
|
|
10077
|
-
(0,
|
|
10103
|
+
(0, import_node_fs8.rmSync)(tmp, { force: true });
|
|
10078
10104
|
} catch {
|
|
10079
10105
|
}
|
|
10080
10106
|
return `could NOT ${failedVerb} ${[...pins.keys()].join(", ")} \u2014 set it by hand`;
|
|
@@ -10383,8 +10409,8 @@ function humanReadableArgName(arg2) {
|
|
|
10383
10409
|
// node_modules/commander/lib/command.js
|
|
10384
10410
|
var import_node_events = require("node:events");
|
|
10385
10411
|
var import_node_child_process3 = __toESM(require("node:child_process"), 1);
|
|
10386
|
-
var
|
|
10387
|
-
var
|
|
10412
|
+
var import_node_path6 = __toESM(require("node:path"), 1);
|
|
10413
|
+
var import_node_fs9 = __toESM(require("node:fs"), 1);
|
|
10388
10414
|
var import_node_process = __toESM(require("node:process"), 1);
|
|
10389
10415
|
var import_node_util3 = require("node:util");
|
|
10390
10416
|
|
|
@@ -12352,7 +12378,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
12352
12378
|
* @param {string} subcommandName
|
|
12353
12379
|
*/
|
|
12354
12380
|
_checkForMissingExecutable(executableFile, executableDir, subcommandName) {
|
|
12355
|
-
if (
|
|
12381
|
+
if (import_node_fs9.default.existsSync(executableFile)) return;
|
|
12356
12382
|
const executableDirMessage = executableDir ? `searched for local subcommand relative to directory '${executableDir}'` : "no directory for search for local subcommand, use .executableDir() to supply a custom directory";
|
|
12357
12383
|
const executableMissing = `'${executableFile}' does not exist
|
|
12358
12384
|
- if '${subcommandName}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead
|
|
@@ -12369,11 +12395,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
12369
12395
|
args = args.slice();
|
|
12370
12396
|
const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
|
|
12371
12397
|
function findFile(baseDir, baseName) {
|
|
12372
|
-
const localBin =
|
|
12373
|
-
if (
|
|
12374
|
-
if (sourceExt.includes(
|
|
12398
|
+
const localBin = import_node_path6.default.resolve(baseDir, baseName);
|
|
12399
|
+
if (import_node_fs9.default.existsSync(localBin)) return localBin;
|
|
12400
|
+
if (sourceExt.includes(import_node_path6.default.extname(baseName))) return void 0;
|
|
12375
12401
|
const foundExt = sourceExt.find(
|
|
12376
|
-
(ext) =>
|
|
12402
|
+
(ext) => import_node_fs9.default.existsSync(`${localBin}${ext}`)
|
|
12377
12403
|
);
|
|
12378
12404
|
if (foundExt) return `${localBin}${foundExt}`;
|
|
12379
12405
|
return void 0;
|
|
@@ -12385,21 +12411,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
12385
12411
|
if (this._scriptPath) {
|
|
12386
12412
|
let resolvedScriptPath;
|
|
12387
12413
|
try {
|
|
12388
|
-
resolvedScriptPath =
|
|
12414
|
+
resolvedScriptPath = import_node_fs9.default.realpathSync(this._scriptPath);
|
|
12389
12415
|
} catch {
|
|
12390
12416
|
resolvedScriptPath = this._scriptPath;
|
|
12391
12417
|
}
|
|
12392
|
-
executableDir =
|
|
12393
|
-
|
|
12418
|
+
executableDir = import_node_path6.default.resolve(
|
|
12419
|
+
import_node_path6.default.dirname(resolvedScriptPath),
|
|
12394
12420
|
executableDir
|
|
12395
12421
|
);
|
|
12396
12422
|
}
|
|
12397
12423
|
if (executableDir) {
|
|
12398
12424
|
let localFile = findFile(executableDir, executableFile);
|
|
12399
12425
|
if (!localFile && !subcommand._executableFile && this._scriptPath) {
|
|
12400
|
-
const legacyName =
|
|
12426
|
+
const legacyName = import_node_path6.default.basename(
|
|
12401
12427
|
this._scriptPath,
|
|
12402
|
-
|
|
12428
|
+
import_node_path6.default.extname(this._scriptPath)
|
|
12403
12429
|
);
|
|
12404
12430
|
if (legacyName !== this._name) {
|
|
12405
12431
|
localFile = findFile(
|
|
@@ -12410,7 +12436,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
12410
12436
|
}
|
|
12411
12437
|
executableFile = localFile || executableFile;
|
|
12412
12438
|
}
|
|
12413
|
-
const launchWithNode = sourceExt.includes(
|
|
12439
|
+
const launchWithNode = sourceExt.includes(import_node_path6.default.extname(executableFile));
|
|
12414
12440
|
let proc;
|
|
12415
12441
|
if (import_node_process.default.platform !== "win32") {
|
|
12416
12442
|
if (launchWithNode) {
|
|
@@ -13327,7 +13353,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
13327
13353
|
* @return {Command}
|
|
13328
13354
|
*/
|
|
13329
13355
|
nameFromFilename(filename) {
|
|
13330
|
-
this._name =
|
|
13356
|
+
this._name = import_node_path6.default.basename(filename, import_node_path6.default.extname(filename));
|
|
13331
13357
|
return this;
|
|
13332
13358
|
}
|
|
13333
13359
|
/**
|
|
@@ -13604,27 +13630,27 @@ init_clean_exit();
|
|
|
13604
13630
|
init_cli_shared();
|
|
13605
13631
|
|
|
13606
13632
|
// src/doctor-fleet-drift.ts
|
|
13607
|
-
var
|
|
13633
|
+
var import_node_fs10 = require("node:fs");
|
|
13608
13634
|
var import_node_os6 = require("node:os");
|
|
13609
|
-
var
|
|
13635
|
+
var import_node_path7 = require("node:path");
|
|
13610
13636
|
var ALWAYS_ARMED_SURFACES = ["cli", "updater"];
|
|
13611
13637
|
function updaterJournalPath(env = process.env) {
|
|
13612
|
-
return (0,
|
|
13638
|
+
return (0, import_node_path7.join)(env.MMI_UPDATER_HOME || (0, import_node_path7.join)((0, import_node_os6.homedir)(), ".mmi", "updater"), "journal.jsonl");
|
|
13613
13639
|
}
|
|
13614
13640
|
var JOURNAL_TAIL_BYTES = 256 * 1024;
|
|
13615
13641
|
function readJournalTail(path2, maxBytes) {
|
|
13616
|
-
const fd = (0,
|
|
13642
|
+
const fd = (0, import_node_fs10.openSync)(path2, "r");
|
|
13617
13643
|
try {
|
|
13618
|
-
const size = (0,
|
|
13644
|
+
const size = (0, import_node_fs10.fstatSync)(fd).size;
|
|
13619
13645
|
const length = Math.min(size, maxBytes);
|
|
13620
13646
|
const buffer = Buffer.alloc(length);
|
|
13621
|
-
if (length) (0,
|
|
13647
|
+
if (length) (0, import_node_fs10.readSync)(fd, buffer, 0, length, size - length);
|
|
13622
13648
|
const text = buffer.toString("utf8");
|
|
13623
13649
|
if (length === size) return { text, truncated: false };
|
|
13624
13650
|
const firstBreak = text.indexOf("\n");
|
|
13625
13651
|
return { text: firstBreak === -1 ? "" : text.slice(firstBreak + 1), truncated: true };
|
|
13626
13652
|
} finally {
|
|
13627
|
-
(0,
|
|
13653
|
+
(0, import_node_fs10.closeSync)(fd);
|
|
13628
13654
|
}
|
|
13629
13655
|
}
|
|
13630
13656
|
function parseFleetDrift(journalPath, text, activeSurfaces, truncated = false) {
|
|
@@ -14333,14 +14359,14 @@ function classifyParseError(plain) {
|
|
|
14333
14359
|
}
|
|
14334
14360
|
|
|
14335
14361
|
// src/released-version-cache.ts
|
|
14336
|
-
var
|
|
14337
|
-
var
|
|
14362
|
+
var import_node_fs11 = require("node:fs");
|
|
14363
|
+
var import_node_path8 = require("node:path");
|
|
14338
14364
|
init_client_version();
|
|
14339
14365
|
var RELEASED_VERSION_CACHE_MS = 24 * 36e5;
|
|
14340
14366
|
function releasedVersionCachePath(runtimeRoot) {
|
|
14341
|
-
return (0,
|
|
14367
|
+
return (0, import_node_path8.join)(runtimeRoot, "head-ts", ".released-version");
|
|
14342
14368
|
}
|
|
14343
|
-
function readReleasedVersionCache(cachePath, now = Date.now(), read =
|
|
14369
|
+
function readReleasedVersionCache(cachePath, now = Date.now(), read = import_node_fs11.readFileSync, currentVersion = resolveClientVersion()) {
|
|
14344
14370
|
let parsed;
|
|
14345
14371
|
try {
|
|
14346
14372
|
parsed = JSON.parse(read(cachePath, "utf8"));
|
|
@@ -14358,8 +14384,8 @@ function readReleasedVersionCache(cachePath, now = Date.now(), read = import_nod
|
|
|
14358
14384
|
}
|
|
14359
14385
|
function writeReleasedVersionCache(cachePath, version, now = Date.now()) {
|
|
14360
14386
|
try {
|
|
14361
|
-
(0,
|
|
14362
|
-
(0,
|
|
14387
|
+
(0, import_node_fs11.mkdirSync)((0, import_node_path8.dirname)(cachePath), { recursive: true });
|
|
14388
|
+
(0, import_node_fs11.writeFileSync)(cachePath, JSON.stringify({ version, at: now }), "utf8");
|
|
14363
14389
|
} catch {
|
|
14364
14390
|
}
|
|
14365
14391
|
}
|
|
@@ -14372,47 +14398,47 @@ function cachedReadNote(cachedAt, now = Date.now()) {
|
|
|
14372
14398
|
}
|
|
14373
14399
|
|
|
14374
14400
|
// src/archive-prune.ts
|
|
14375
|
-
var
|
|
14376
|
-
var
|
|
14401
|
+
var import_node_fs14 = require("node:fs");
|
|
14402
|
+
var import_node_path11 = require("node:path");
|
|
14377
14403
|
|
|
14378
14404
|
// src/worktree-evidence-archive.ts
|
|
14379
|
-
var
|
|
14380
|
-
var
|
|
14405
|
+
var import_node_fs13 = require("node:fs");
|
|
14406
|
+
var import_node_path10 = require("node:path");
|
|
14381
14407
|
|
|
14382
14408
|
// src/repo-runtime-state.ts
|
|
14383
14409
|
var import_node_crypto3 = require("node:crypto");
|
|
14384
|
-
var
|
|
14410
|
+
var import_node_fs12 = require("node:fs");
|
|
14385
14411
|
var import_node_os7 = require("node:os");
|
|
14386
|
-
var
|
|
14412
|
+
var import_node_path9 = require("node:path");
|
|
14387
14413
|
var RUNTIME_DIR = "mmi-runtime";
|
|
14388
14414
|
function hashPath(path2) {
|
|
14389
|
-
return (0, import_node_crypto3.createHash)("sha256").update((0,
|
|
14415
|
+
return (0, import_node_crypto3.createHash)("sha256").update((0, import_node_path9.resolve)(path2)).digest("hex").slice(0, 16);
|
|
14390
14416
|
}
|
|
14391
14417
|
function resolveGitDir(cwd) {
|
|
14392
|
-
const dotGit = (0,
|
|
14418
|
+
const dotGit = (0, import_node_path9.join)(cwd, ".git");
|
|
14393
14419
|
try {
|
|
14394
|
-
const st = (0,
|
|
14420
|
+
const st = (0, import_node_fs12.statSync)(dotGit);
|
|
14395
14421
|
if (st.isDirectory()) return dotGit;
|
|
14396
14422
|
if (!st.isFile()) return void 0;
|
|
14397
14423
|
} catch {
|
|
14398
14424
|
return void 0;
|
|
14399
14425
|
}
|
|
14400
14426
|
try {
|
|
14401
|
-
const raw = (0,
|
|
14427
|
+
const raw = (0, import_node_fs12.readFileSync)(dotGit, "utf8").trim();
|
|
14402
14428
|
const match = /^gitdir:\s*(.+)$/i.exec(raw);
|
|
14403
14429
|
if (!match) return void 0;
|
|
14404
14430
|
const gitdir = match[1].trim();
|
|
14405
|
-
return (0,
|
|
14431
|
+
return (0, import_node_path9.isAbsolute)(gitdir) ? gitdir : (0, import_node_path9.resolve)(cwd, gitdir);
|
|
14406
14432
|
} catch {
|
|
14407
14433
|
return void 0;
|
|
14408
14434
|
}
|
|
14409
14435
|
}
|
|
14410
14436
|
function resolveGitDirFrom(cwd) {
|
|
14411
|
-
let dir = (0,
|
|
14437
|
+
let dir = (0, import_node_path9.resolve)(cwd);
|
|
14412
14438
|
for (; ; ) {
|
|
14413
14439
|
const found = resolveGitDir(dir);
|
|
14414
14440
|
if (found) return found;
|
|
14415
|
-
const parent = (0,
|
|
14441
|
+
const parent = (0, import_node_path9.dirname)(dir);
|
|
14416
14442
|
if (parent === dir) return void 0;
|
|
14417
14443
|
dir = parent;
|
|
14418
14444
|
}
|
|
@@ -14426,18 +14452,18 @@ function remoteOriginUrlFromConfig(configText) {
|
|
|
14426
14452
|
function originUrlFromGitConfig(cwd = process.cwd()) {
|
|
14427
14453
|
const gitDir = resolveGitDirFrom(cwd);
|
|
14428
14454
|
if (!gitDir) return void 0;
|
|
14429
|
-
const configs = [(0,
|
|
14455
|
+
const configs = [(0, import_node_path9.join)(gitDir, "config")];
|
|
14430
14456
|
try {
|
|
14431
|
-
const common = (0,
|
|
14457
|
+
const common = (0, import_node_fs12.readFileSync)((0, import_node_path9.join)(gitDir, "commondir"), "utf8").trim();
|
|
14432
14458
|
if (common) {
|
|
14433
|
-
const commonDir = (0,
|
|
14434
|
-
configs.push((0,
|
|
14459
|
+
const commonDir = (0, import_node_path9.isAbsolute)(common) ? common : (0, import_node_path9.resolve)(gitDir, common);
|
|
14460
|
+
configs.push((0, import_node_path9.join)(commonDir, "config"));
|
|
14435
14461
|
}
|
|
14436
14462
|
} catch {
|
|
14437
14463
|
}
|
|
14438
14464
|
for (const path2 of configs) {
|
|
14439
14465
|
try {
|
|
14440
|
-
const url = remoteOriginUrlFromConfig((0,
|
|
14466
|
+
const url = remoteOriginUrlFromConfig((0, import_node_fs12.readFileSync)(path2, "utf8"));
|
|
14441
14467
|
if (url) return url;
|
|
14442
14468
|
} catch {
|
|
14443
14469
|
}
|
|
@@ -14446,8 +14472,8 @@ function originUrlFromGitConfig(cwd = process.cwd()) {
|
|
|
14446
14472
|
}
|
|
14447
14473
|
function repoRuntimeStatePath(cwd, ...parts) {
|
|
14448
14474
|
const gitDir = resolveGitDir(cwd);
|
|
14449
|
-
if (gitDir && (0,
|
|
14450
|
-
return (0,
|
|
14475
|
+
if (gitDir && (0, import_node_fs12.existsSync)(gitDir)) return (0, import_node_path9.join)(gitDir, RUNTIME_DIR, ...parts);
|
|
14476
|
+
return (0, import_node_path9.join)((0, import_node_os7.tmpdir)(), "mmi-cli", hashPath(cwd), ...parts);
|
|
14451
14477
|
}
|
|
14452
14478
|
|
|
14453
14479
|
// src/worktree-evidence-archive.ts
|
|
@@ -14467,24 +14493,24 @@ function resolveArtifactRunScope(env = process.env) {
|
|
|
14467
14493
|
}
|
|
14468
14494
|
function defaultIsDirectory(path2) {
|
|
14469
14495
|
try {
|
|
14470
|
-
return (0,
|
|
14496
|
+
return (0, import_node_fs13.lstatSync)(path2).isDirectory();
|
|
14471
14497
|
} catch {
|
|
14472
14498
|
return false;
|
|
14473
14499
|
}
|
|
14474
14500
|
}
|
|
14475
14501
|
function archiveWorktreeJervArtifacts(args, deps = {}) {
|
|
14476
|
-
const exists = deps.exists ??
|
|
14502
|
+
const exists = deps.exists ?? import_node_fs13.existsSync;
|
|
14477
14503
|
const isDirectory = deps.isDirectory ?? defaultIsDirectory;
|
|
14478
|
-
const copyDir = deps.copyDir ?? ((from, to) => (0,
|
|
14504
|
+
const copyDir = deps.copyDir ?? ((from, to) => (0, import_node_fs13.cpSync)(from, to, { recursive: true, force: true }));
|
|
14479
14505
|
const mkdirp = deps.mkdirp ?? ((path2) => {
|
|
14480
|
-
(0,
|
|
14506
|
+
(0, import_node_fs13.mkdirSync)(path2, { recursive: true });
|
|
14481
14507
|
});
|
|
14482
14508
|
const env = deps.env ?? process.env;
|
|
14483
14509
|
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
14484
14510
|
const resolveRoot = deps.resolveArchiveRoot ?? repoRuntimeStatePath;
|
|
14485
14511
|
if (!args.primaryRoot?.trim()) return { status: "skipped", reason: "missing-primary-root" };
|
|
14486
14512
|
if (!args.worktreePath?.trim()) return { status: "skipped", reason: "missing-worktree-path" };
|
|
14487
|
-
const source = (0,
|
|
14513
|
+
const source = (0, import_node_path10.join)(args.worktreePath, ".jerv");
|
|
14488
14514
|
if (!exists(source)) return { status: "absent" };
|
|
14489
14515
|
if (!isDirectory(source)) return { status: "skipped", reason: "jerv-not-a-directory" };
|
|
14490
14516
|
const runScope = resolveArtifactRunScope(env);
|
|
@@ -14492,7 +14518,7 @@ function archiveWorktreeJervArtifacts(args, deps = {}) {
|
|
|
14492
14518
|
const stamp = now().toISOString().replace(/[:.]/g, "-");
|
|
14493
14519
|
const dest = resolveRoot(args.primaryRoot, "jerv-artifacts", runScope, branchSlug, stamp, ".jerv");
|
|
14494
14520
|
try {
|
|
14495
|
-
mkdirp((0,
|
|
14521
|
+
mkdirp((0, import_node_path10.dirname)(dest));
|
|
14496
14522
|
copyDir(source, dest);
|
|
14497
14523
|
if (!exists(dest)) return { status: "failed", error: `archive write left no directory at ${dest}` };
|
|
14498
14524
|
return { status: "archived", path: dest, runScope };
|
|
@@ -14501,27 +14527,27 @@ function archiveWorktreeJervArtifacts(args, deps = {}) {
|
|
|
14501
14527
|
}
|
|
14502
14528
|
}
|
|
14503
14529
|
function defaultStat(path2) {
|
|
14504
|
-
const st = (0,
|
|
14530
|
+
const st = (0, import_node_fs13.lstatSync)(path2);
|
|
14505
14531
|
return { mtimeMs: st.mtimeMs, size: st.size, isDirectory: () => st.isDirectory(), isSymbolicLink: () => st.isSymbolicLink() };
|
|
14506
14532
|
}
|
|
14507
14533
|
function scanWorktreeTmpEvidenceDetailed(worktreePath, newerThanMs, deps = {}) {
|
|
14508
|
-
const exists = deps.exists ??
|
|
14534
|
+
const exists = deps.exists ?? import_node_fs13.existsSync;
|
|
14509
14535
|
const stat4 = deps.stat ?? defaultStat;
|
|
14510
|
-
const readdir2 = deps.readdir ??
|
|
14536
|
+
const readdir2 = deps.readdir ?? import_node_fs13.readdirSync;
|
|
14511
14537
|
const scan = { entries: [], excluded: [], links: [], unreadable: [] };
|
|
14512
|
-
const tmpRoot = (0,
|
|
14538
|
+
const tmpRoot = (0, import_node_path10.join)(worktreePath, "tmp");
|
|
14513
14539
|
if (!exists(tmpRoot)) return scan;
|
|
14514
14540
|
const walk2 = (dir, preserved) => {
|
|
14515
14541
|
let names;
|
|
14516
14542
|
try {
|
|
14517
14543
|
names = readdir2(dir);
|
|
14518
14544
|
} catch {
|
|
14519
|
-
scan.unreadable.push((0,
|
|
14545
|
+
scan.unreadable.push((0, import_node_path10.relative)(worktreePath, dir).replace(/\\/g, "/"));
|
|
14520
14546
|
return;
|
|
14521
14547
|
}
|
|
14522
14548
|
for (const name of names) {
|
|
14523
|
-
const full = (0,
|
|
14524
|
-
const relPath = (0,
|
|
14549
|
+
const full = (0, import_node_path10.join)(dir, name);
|
|
14550
|
+
const relPath = (0, import_node_path10.relative)(worktreePath, full).replace(/\\/g, "/");
|
|
14525
14551
|
let st;
|
|
14526
14552
|
try {
|
|
14527
14553
|
st = stat4(full);
|
|
@@ -14552,11 +14578,11 @@ function scanWorktreeTmpEvidence(worktreePath, newerThanMs, deps = {}) {
|
|
|
14552
14578
|
return scanWorktreeTmpEvidenceDetailed(worktreePath, newerThanMs, deps).entries;
|
|
14553
14579
|
}
|
|
14554
14580
|
function archiveWorktreeTmpArtifacts(args, deps = {}) {
|
|
14555
|
-
const copyFile = deps.copyFile ??
|
|
14556
|
-
const fileSize = deps.fileSize ?? ((path2) => (0,
|
|
14557
|
-
const writeFile5 = deps.writeFile ?? ((path2, text) => (0,
|
|
14581
|
+
const copyFile = deps.copyFile ?? import_node_fs13.copyFileSync;
|
|
14582
|
+
const fileSize = deps.fileSize ?? ((path2) => (0, import_node_fs13.statSync)(path2).size);
|
|
14583
|
+
const writeFile5 = deps.writeFile ?? ((path2, text) => (0, import_node_fs13.writeFileSync)(path2, text));
|
|
14558
14584
|
const mkdirp = deps.mkdirp ?? ((path2) => {
|
|
14559
|
-
(0,
|
|
14585
|
+
(0, import_node_fs13.mkdirSync)(path2, { recursive: true });
|
|
14560
14586
|
});
|
|
14561
14587
|
const env = deps.env ?? process.env;
|
|
14562
14588
|
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
@@ -14573,14 +14599,14 @@ function archiveWorktreeTmpArtifacts(args, deps = {}) {
|
|
|
14573
14599
|
try {
|
|
14574
14600
|
let bytes = 0;
|
|
14575
14601
|
for (const entry of scan.entries) {
|
|
14576
|
-
const to = (0,
|
|
14577
|
-
mkdirp((0,
|
|
14578
|
-
copyFile((0,
|
|
14602
|
+
const to = (0, import_node_path10.join)(dest, entry.relPath.slice("tmp/".length));
|
|
14603
|
+
mkdirp((0, import_node_path10.dirname)(to));
|
|
14604
|
+
copyFile((0, import_node_path10.join)(args.worktreePath, entry.relPath), to);
|
|
14579
14605
|
const copied = fileSize(to);
|
|
14580
14606
|
if (copied !== entry.bytes) return { status: "failed", error: `${entry.relPath} changed during archive (${entry.bytes} -> ${copied} bytes)` };
|
|
14581
14607
|
bytes += copied;
|
|
14582
14608
|
}
|
|
14583
|
-
const manifest = (0,
|
|
14609
|
+
const manifest = (0, import_node_path10.join)((0, import_node_path10.dirname)(dest), "tmp-manifest.json");
|
|
14584
14610
|
writeFile5(manifest, `${JSON.stringify({ files: scan.entries.map(({ relPath, bytes: b }) => ({ relPath, bytes: b })), bytes, excluded: scan.excluded, skippedLinks: scan.links }, null, 2)}
|
|
14585
14611
|
`);
|
|
14586
14612
|
return { status: "archived", path: dest, runScope, entries: scan.entries.length, bytes, manifest, excluded: scan.excluded, skippedLinks: scan.links };
|
|
@@ -14675,8 +14701,8 @@ function parseSnapshotStamp(stamp) {
|
|
|
14675
14701
|
function listDirs(path2) {
|
|
14676
14702
|
try {
|
|
14677
14703
|
const names = [];
|
|
14678
|
-
for (const name of (0,
|
|
14679
|
-
const st = (0,
|
|
14704
|
+
for (const name of (0, import_node_fs14.readdirSync)(path2)) {
|
|
14705
|
+
const st = (0, import_node_fs14.lstatSync)((0, import_node_path11.join)(path2, name));
|
|
14680
14706
|
if (st.isDirectory() && !st.isSymbolicLink()) names.push(name);
|
|
14681
14707
|
}
|
|
14682
14708
|
return { names, unreadable: false };
|
|
@@ -14689,17 +14715,17 @@ function measureSnapshot(root) {
|
|
|
14689
14715
|
const walk2 = (dir, preserved, inReinstallable) => {
|
|
14690
14716
|
let names;
|
|
14691
14717
|
try {
|
|
14692
|
-
names = (0,
|
|
14718
|
+
names = (0, import_node_fs14.readdirSync)(dir);
|
|
14693
14719
|
} catch {
|
|
14694
14720
|
out.uncertain = true;
|
|
14695
14721
|
return 0;
|
|
14696
14722
|
}
|
|
14697
14723
|
let sum = 0;
|
|
14698
14724
|
for (const name of names) {
|
|
14699
|
-
const full = (0,
|
|
14725
|
+
const full = (0, import_node_path11.join)(dir, name);
|
|
14700
14726
|
let st;
|
|
14701
14727
|
try {
|
|
14702
|
-
st = (0,
|
|
14728
|
+
st = (0, import_node_fs14.lstatSync)(full);
|
|
14703
14729
|
} catch {
|
|
14704
14730
|
out.uncertain = true;
|
|
14705
14731
|
continue;
|
|
@@ -14726,10 +14752,10 @@ function inventoryArchives(archiveRootFor, nowMs = Date.now()) {
|
|
|
14726
14752
|
for (const kind of ARCHIVE_KINDS) {
|
|
14727
14753
|
const root = archiveRootFor(kind);
|
|
14728
14754
|
for (const runScope of listDirs(root).names) {
|
|
14729
|
-
for (const branch of listDirs((0,
|
|
14730
|
-
const branchDir = (0,
|
|
14755
|
+
for (const branch of listDirs((0, import_node_path11.join)(root, runScope)).names) {
|
|
14756
|
+
const branchDir = (0, import_node_path11.join)(root, runScope, branch);
|
|
14731
14757
|
for (const stamp of listDirs(branchDir).names) {
|
|
14732
|
-
const path2 = (0,
|
|
14758
|
+
const path2 = (0, import_node_path11.join)(branchDir, stamp);
|
|
14733
14759
|
const measured = measureSnapshot(path2);
|
|
14734
14760
|
let mtimeMs = nowMs;
|
|
14735
14761
|
let timeKnown = true;
|
|
@@ -14737,14 +14763,14 @@ function inventoryArchives(archiveRootFor, nowMs = Date.now()) {
|
|
|
14737
14763
|
if (stampMs !== void 0) mtimeMs = stampMs;
|
|
14738
14764
|
else {
|
|
14739
14765
|
try {
|
|
14740
|
-
mtimeMs = (0,
|
|
14766
|
+
mtimeMs = (0, import_node_fs14.lstatSync)(path2).mtimeMs;
|
|
14741
14767
|
} catch {
|
|
14742
14768
|
timeKnown = false;
|
|
14743
14769
|
}
|
|
14744
14770
|
}
|
|
14745
14771
|
let hasManifest = false;
|
|
14746
14772
|
try {
|
|
14747
|
-
hasManifest = (0,
|
|
14773
|
+
hasManifest = (0, import_node_fs14.lstatSync)((0, import_node_path11.join)(path2, "tmp-manifest.json")).isFile();
|
|
14748
14774
|
} catch {
|
|
14749
14775
|
hasManifest = false;
|
|
14750
14776
|
}
|
|
@@ -14766,28 +14792,28 @@ function inventoryArchives(archiveRootFor, nowMs = Date.now()) {
|
|
|
14766
14792
|
return rows;
|
|
14767
14793
|
}
|
|
14768
14794
|
function insideRoot(root, target) {
|
|
14769
|
-
const rel = (0,
|
|
14770
|
-
return rel !== "" && !rel.startsWith("..") && !(0,
|
|
14795
|
+
const rel = (0, import_node_path11.relative)(root, target);
|
|
14796
|
+
return rel !== "" && !rel.startsWith("..") && !(0, import_node_path11.isAbsolute)(rel) && !rel.split(import_node_path11.sep).includes("..");
|
|
14771
14797
|
}
|
|
14772
|
-
function applyArchivePrune(plan, archiveRootFor, remove2 = (p) => (0,
|
|
14798
|
+
function applyArchivePrune(plan, archiveRootFor, remove2 = (p) => (0, import_node_fs14.rmSync)(p, { recursive: true, force: true })) {
|
|
14773
14799
|
const result = { removed: [], skipped: [], failed: [] };
|
|
14774
14800
|
for (const snap of plan.snapshots) {
|
|
14775
14801
|
if (snap.proposal !== "reclaim-dependency-trees") continue;
|
|
14776
14802
|
let realRoot;
|
|
14777
14803
|
try {
|
|
14778
|
-
realRoot = (0,
|
|
14804
|
+
realRoot = (0, import_node_fs14.realpathSync)(archiveRootFor(snap.kind));
|
|
14779
14805
|
} catch (e) {
|
|
14780
14806
|
for (const d of snap.reinstallableDirs) result.failed.push({ path: d, error: `archive root unresolvable: ${errMsg(e)}` });
|
|
14781
14807
|
continue;
|
|
14782
14808
|
}
|
|
14783
14809
|
for (const dir of snap.reinstallableDirs) {
|
|
14784
14810
|
try {
|
|
14785
|
-
const st = (0,
|
|
14811
|
+
const st = (0, import_node_fs14.lstatSync)(dir);
|
|
14786
14812
|
if (st.isSymbolicLink() || !st.isDirectory()) {
|
|
14787
14813
|
result.skipped.push({ path: dir, reason: "no longer a real directory" });
|
|
14788
14814
|
continue;
|
|
14789
14815
|
}
|
|
14790
|
-
const real = (0,
|
|
14816
|
+
const real = (0, import_node_fs14.realpathSync)(dir);
|
|
14791
14817
|
if (!insideRoot(realRoot, real)) {
|
|
14792
14818
|
result.skipped.push({ path: dir, reason: "resolves outside the archive root" });
|
|
14793
14819
|
continue;
|
|
@@ -14830,13 +14856,13 @@ function renderArchivePrunePlan(plan, applied) {
|
|
|
14830
14856
|
}
|
|
14831
14857
|
|
|
14832
14858
|
// src/schedules-drift-cache.ts
|
|
14833
|
-
var
|
|
14834
|
-
var
|
|
14859
|
+
var import_node_fs15 = require("node:fs");
|
|
14860
|
+
var import_node_path12 = require("node:path");
|
|
14835
14861
|
var SCHEDULES_DRIFT_CACHE_MS = 6 * 36e5;
|
|
14836
14862
|
function schedulesDriftCachePath(runtimeRoot) {
|
|
14837
|
-
return (0,
|
|
14863
|
+
return (0, import_node_path12.join)(runtimeRoot, "head-ts", ".schedules-drift");
|
|
14838
14864
|
}
|
|
14839
|
-
function readSchedulesDriftCache(cachePath, now = Date.now(), read =
|
|
14865
|
+
function readSchedulesDriftCache(cachePath, now = Date.now(), read = import_node_fs15.readFileSync) {
|
|
14840
14866
|
let parsed;
|
|
14841
14867
|
try {
|
|
14842
14868
|
parsed = JSON.parse(read(cachePath, "utf8"));
|
|
@@ -14853,8 +14879,8 @@ function readSchedulesDriftCache(cachePath, now = Date.now(), read = import_node
|
|
|
14853
14879
|
}
|
|
14854
14880
|
function writeSchedulesDriftCache(cachePath, driftLines, now = Date.now()) {
|
|
14855
14881
|
try {
|
|
14856
|
-
(0,
|
|
14857
|
-
(0,
|
|
14882
|
+
(0, import_node_fs15.mkdirSync)((0, import_node_path12.dirname)(cachePath), { recursive: true });
|
|
14883
|
+
(0, import_node_fs15.writeFileSync)(cachePath, JSON.stringify({ driftLines, at: now }), "utf8");
|
|
14858
14884
|
} catch {
|
|
14859
14885
|
}
|
|
14860
14886
|
}
|
|
@@ -14865,8 +14891,8 @@ function cachedSweepNote(cachedAt, now = Date.now()) {
|
|
|
14865
14891
|
|
|
14866
14892
|
// src/scratch-gc.ts
|
|
14867
14893
|
var import_node_child_process4 = require("node:child_process");
|
|
14868
|
-
var
|
|
14869
|
-
var
|
|
14894
|
+
var import_node_fs16 = require("node:fs");
|
|
14895
|
+
var import_node_path13 = require("node:path");
|
|
14870
14896
|
var PLAN_ADVISORY_AGE_MS = 30 * 24 * 36e5;
|
|
14871
14897
|
var ROOT_SCRATCH_STALE_MS = 24 * 36e5;
|
|
14872
14898
|
function executeScratchGc(repoRoot2, opts, now = Date.now(), deps = {}) {
|
|
@@ -14963,7 +14989,7 @@ function planScratchGc(snap, now = Date.now()) {
|
|
|
14963
14989
|
function isLinkLike(path2, symbolic = false) {
|
|
14964
14990
|
if (symbolic) return true;
|
|
14965
14991
|
try {
|
|
14966
|
-
(0,
|
|
14992
|
+
(0, import_node_fs16.readlinkSync)(path2);
|
|
14967
14993
|
return true;
|
|
14968
14994
|
} catch {
|
|
14969
14995
|
return false;
|
|
@@ -14973,13 +14999,13 @@ function treeOlderThan(root, now, floor) {
|
|
|
14973
14999
|
const stack = [root];
|
|
14974
15000
|
while (stack.length) {
|
|
14975
15001
|
const current = stack.pop();
|
|
14976
|
-
const lst = (0,
|
|
15002
|
+
const lst = (0, import_node_fs16.lstatSync)(current);
|
|
14977
15003
|
if (isLinkLike(current, lst.isSymbolicLink())) return false;
|
|
14978
|
-
const st = (0,
|
|
15004
|
+
const st = (0, import_node_fs16.statSync)(current);
|
|
14979
15005
|
if (now - st.mtimeMs <= floor) return false;
|
|
14980
15006
|
if (!st.isDirectory()) continue;
|
|
14981
|
-
for (const ent of (0,
|
|
14982
|
-
const child2 = (0,
|
|
15007
|
+
for (const ent of (0, import_node_fs16.readdirSync)(current, { withFileTypes: true })) {
|
|
15008
|
+
const child2 = (0, import_node_path13.join)(current, ent.name);
|
|
14983
15009
|
if (isLinkLike(child2, ent.isSymbolicLink())) return false;
|
|
14984
15010
|
stack.push(child2);
|
|
14985
15011
|
}
|
|
@@ -14991,13 +15017,13 @@ function applyScratchGc(plan, repoRoot2, now = Date.now(), deps = {}) {
|
|
|
14991
15017
|
const result = { pruned: [], skipped: 0, bytes: 0 };
|
|
14992
15018
|
let repoAnchor;
|
|
14993
15019
|
try {
|
|
14994
|
-
repoAnchor = (0,
|
|
15020
|
+
repoAnchor = (0, import_node_fs16.realpathSync)(repoRoot2).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
14995
15021
|
} catch {
|
|
14996
15022
|
return result;
|
|
14997
15023
|
}
|
|
14998
15024
|
for (const c of plan.safeAuto) {
|
|
14999
15025
|
try {
|
|
15000
|
-
const real = (0,
|
|
15026
|
+
const real = (0, import_node_fs16.realpathSync)(c.path).replace(/\\/g, "/");
|
|
15001
15027
|
if (!pathContained(real, repoAnchor) || !rootCandidateStillAllowed(c, real, repoAnchor)) {
|
|
15002
15028
|
result.skipped += 1;
|
|
15003
15029
|
continue;
|
|
@@ -15006,12 +15032,12 @@ function applyScratchGc(plan, repoRoot2, now = Date.now(), deps = {}) {
|
|
|
15006
15032
|
result.skipped += 1;
|
|
15007
15033
|
continue;
|
|
15008
15034
|
}
|
|
15009
|
-
const lst = (0,
|
|
15035
|
+
const lst = (0, import_node_fs16.lstatSync)(c.path);
|
|
15010
15036
|
if (isLinkLike(c.path, lst.isSymbolicLink())) {
|
|
15011
15037
|
result.skipped += 1;
|
|
15012
15038
|
continue;
|
|
15013
15039
|
}
|
|
15014
|
-
const st = (0,
|
|
15040
|
+
const st = (0, import_node_fs16.statSync)(real);
|
|
15015
15041
|
if (c.kind === "dir" && !st.isDirectory() || c.kind !== "dir" && !st.isFile()) {
|
|
15016
15042
|
result.skipped += 1;
|
|
15017
15043
|
continue;
|
|
@@ -15025,8 +15051,8 @@ function applyScratchGc(plan, repoRoot2, now = Date.now(), deps = {}) {
|
|
|
15025
15051
|
result.skipped += 1;
|
|
15026
15052
|
continue;
|
|
15027
15053
|
}
|
|
15028
|
-
if (c.kind === "dir") (0,
|
|
15029
|
-
else (0,
|
|
15054
|
+
if (c.kind === "dir") (0, import_node_fs16.rmSync)(real, { recursive: true, force: true });
|
|
15055
|
+
else (0, import_node_fs16.unlinkSync)(real);
|
|
15030
15056
|
result.pruned.push(c.path);
|
|
15031
15057
|
result.bytes += c.bytes;
|
|
15032
15058
|
} catch {
|
|
@@ -15040,19 +15066,19 @@ function pathContained(real, anchor) {
|
|
|
15040
15066
|
}
|
|
15041
15067
|
function rootCandidateStillAllowed(c, realPath, repoAnchor) {
|
|
15042
15068
|
const path2 = realPath.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
15043
|
-
const parent = (0,
|
|
15044
|
-
const name = (0,
|
|
15069
|
+
const parent = (0, import_node_path13.dirname)(path2).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
15070
|
+
const name = (0, import_node_path13.basename)(path2);
|
|
15045
15071
|
if (c.family === "scratch-dir") return c.kind === "dir" && parent === repoAnchor && ROOT_SCRATCH_DIRS.has(name);
|
|
15046
15072
|
if (c.family === "scratch-file") return (c.kind ?? "file") === "file" && parent === repoAnchor && ROOT_SCRATCH_FILE_PREFIXES.some((prefix) => name.startsWith(prefix));
|
|
15047
15073
|
return false;
|
|
15048
15074
|
}
|
|
15049
15075
|
function trackedPathStatus(c, realPath, repoAnchor) {
|
|
15050
|
-
const rel = (0,
|
|
15076
|
+
const rel = (0, import_node_path13.relative)(repoAnchor, realPath).replace(/\\/g, "/");
|
|
15051
15077
|
if (!rel || rel.startsWith("../") || rel === "..") return null;
|
|
15052
15078
|
const gitPath = c.kind === "dir" ? `${rel.replace(/\/+$/, "")}/` : rel;
|
|
15053
15079
|
let hasDotGit = false;
|
|
15054
15080
|
try {
|
|
15055
|
-
(0,
|
|
15081
|
+
(0, import_node_fs16.lstatSync)((0, import_node_path13.join)(repoAnchor, ".git"));
|
|
15056
15082
|
hasDotGit = true;
|
|
15057
15083
|
} catch {
|
|
15058
15084
|
hasDotGit = false;
|
|
@@ -15093,10 +15119,10 @@ function rootScratchDirSnapshot(root, readdir2, stat4) {
|
|
|
15093
15119
|
while (stack.length) {
|
|
15094
15120
|
const current = stack.pop();
|
|
15095
15121
|
for (const ent of readdir2(current, { withFileTypes: true })) {
|
|
15096
|
-
const child2 = (0,
|
|
15122
|
+
const child2 = (0, import_node_path13.join)(current, ent.name);
|
|
15097
15123
|
if (ent.isSymbolicLink?.()) return null;
|
|
15098
15124
|
try {
|
|
15099
|
-
(0,
|
|
15125
|
+
(0, import_node_fs16.readlinkSync)(child2);
|
|
15100
15126
|
return null;
|
|
15101
15127
|
} catch {
|
|
15102
15128
|
}
|
|
@@ -15119,12 +15145,12 @@ function resolveRealPathOrRaw(p, realpath) {
|
|
|
15119
15145
|
}
|
|
15120
15146
|
}
|
|
15121
15147
|
function collectScratchSnapshot(repoRoot2, deps = {}) {
|
|
15122
|
-
const readdir2 = deps.readdir ??
|
|
15123
|
-
const stat4 = deps.stat ??
|
|
15124
|
-
const realpath = deps.realpath ??
|
|
15148
|
+
const readdir2 = deps.readdir ?? import_node_fs16.readdirSync;
|
|
15149
|
+
const stat4 = deps.stat ?? import_node_fs16.statSync;
|
|
15150
|
+
const realpath = deps.realpath ?? import_node_fs16.realpathSync;
|
|
15125
15151
|
const trackedStatus = deps.trackedStatus ?? trackedPathStatus;
|
|
15126
15152
|
const repoAnchor = resolveRealPathOrRaw(repoRoot2, realpath).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
15127
|
-
const plansRoot = (0,
|
|
15153
|
+
const plansRoot = (0, import_node_path13.join)(repoRoot2, "plans");
|
|
15128
15154
|
const rootScratchFiles = [];
|
|
15129
15155
|
try {
|
|
15130
15156
|
for (const ent of readdir2(repoRoot2, { withFileTypes: true })) {
|
|
@@ -15133,7 +15159,7 @@ function collectScratchSnapshot(repoRoot2, deps = {}) {
|
|
|
15133
15159
|
if (!(isDir && ROOT_SCRATCH_DIRS.has(ent.name)) && !(isFile && ROOT_SCRATCH_FILE_PREFIXES.some((prefix) => ent.name.startsWith(prefix)))) {
|
|
15134
15160
|
continue;
|
|
15135
15161
|
}
|
|
15136
|
-
const full = (0,
|
|
15162
|
+
const full = (0, import_node_path13.join)(repoRoot2, ent.name);
|
|
15137
15163
|
const kind = isDir ? "dir" : "file";
|
|
15138
15164
|
try {
|
|
15139
15165
|
const realFull = resolveRealPathOrRaw(full, realpath).replace(/\\/g, "/");
|
|
@@ -15156,7 +15182,7 @@ function collectScratchSnapshot(repoRoot2, deps = {}) {
|
|
|
15156
15182
|
try {
|
|
15157
15183
|
for (const ent of readdir2(plansRoot, { withFileTypes: true })) {
|
|
15158
15184
|
if (!ent.isFile() || !ent.name.endsWith(".md")) continue;
|
|
15159
|
-
const full = (0,
|
|
15185
|
+
const full = (0, import_node_path13.join)(plansRoot, ent.name);
|
|
15160
15186
|
try {
|
|
15161
15187
|
const st = stat4(full);
|
|
15162
15188
|
planMdFiles.push({ path: full, dir: plansRoot, name: ent.name, mtimeMs: st.mtimeMs, bytes: st.size, kind: "file" });
|
|
@@ -15270,9 +15296,9 @@ var import_node_os29 = require("node:os");
|
|
|
15270
15296
|
var import_node_path65 = require("node:path");
|
|
15271
15297
|
|
|
15272
15298
|
// src/board-read.ts
|
|
15273
|
-
var
|
|
15299
|
+
var import_node_fs19 = require("node:fs");
|
|
15274
15300
|
var import_node_os11 = require("node:os");
|
|
15275
|
-
var
|
|
15301
|
+
var import_node_path16 = require("node:path");
|
|
15276
15302
|
init_cli_shared();
|
|
15277
15303
|
init_github_client();
|
|
15278
15304
|
|
|
@@ -15496,6 +15522,9 @@ var APP_WRITE_COMMANDS = /* @__PURE__ */ new Map([
|
|
|
15496
15522
|
// #9587: one named run rerun (all or failed jobs) — same actions:write profile, nothing broader.
|
|
15497
15523
|
["run rerun", { metadata: "read", actions: "write" }],
|
|
15498
15524
|
["run dispatch", { metadata: "read", actions: "write" }],
|
|
15525
|
+
// #10142: one focused-test dispatch plus the run/job/log reads that give it a verdict on the owned
|
|
15526
|
+
// heavy lanes — the same actions:write profile, nothing broader (it reads no repo contents).
|
|
15527
|
+
["run test", { metadata: "read", actions: "write" }],
|
|
15499
15528
|
// #9780: repository Actions variables — get/list mint only Variables:read; create/update/delete
|
|
15500
15529
|
// Variables:write. Repository scope only; no secret store, no rename, no workflow dispatch.
|
|
15501
15530
|
// #9790: the token key is `actions_variables` (as ci-reconcile.yml mints); `variables` was refused 422.
|
|
@@ -16170,26 +16199,26 @@ init_github_quota();
|
|
|
16170
16199
|
|
|
16171
16200
|
// src/rest-etag-cache.ts
|
|
16172
16201
|
var import_node_crypto6 = require("node:crypto");
|
|
16173
|
-
var
|
|
16202
|
+
var import_node_fs17 = require("node:fs");
|
|
16174
16203
|
var import_node_os8 = require("node:os");
|
|
16175
|
-
var
|
|
16204
|
+
var import_node_path14 = require("node:path");
|
|
16176
16205
|
var MAX_AGE_MS = 24 * 60 * 6e4;
|
|
16177
16206
|
var MAX_ENTRIES = 256;
|
|
16178
16207
|
var MAX_BYTES = 16 * 1024 * 1024;
|
|
16179
16208
|
var MAX_ENTRY_BYTES = 1024 * 1024;
|
|
16180
16209
|
function restEtagCacheDir() {
|
|
16181
16210
|
if (process.env.MMI_REST_ETAG_CACHE_DIR) return process.env.MMI_REST_ETAG_CACHE_DIR;
|
|
16182
|
-
return process.platform === "win32" ? (0,
|
|
16211
|
+
return process.platform === "win32" ? (0, import_node_path14.join)(process.env.LOCALAPPDATA || (0, import_node_path14.join)((0, import_node_os8.homedir)(), "AppData", "Local"), "MMI Future", "mmi-cli", "rest-etags") : (0, import_node_path14.join)(process.env.XDG_CACHE_HOME || (0, import_node_path14.join)((0, import_node_os8.homedir)(), ".cache"), "mmi-cli", "rest-etags");
|
|
16183
16212
|
}
|
|
16184
16213
|
function entryPath(dir, key) {
|
|
16185
|
-
return (0,
|
|
16214
|
+
return (0, import_node_path14.join)(dir, `${(0, import_node_crypto6.createHash)("sha256").update(key).digest("hex")}.json`);
|
|
16186
16215
|
}
|
|
16187
16216
|
function readRestEtag(key, dir = restEtagCacheDir()) {
|
|
16188
16217
|
if (dir === "off") return void 0;
|
|
16189
16218
|
try {
|
|
16190
16219
|
const path2 = entryPath(dir, key);
|
|
16191
|
-
if ((0,
|
|
16192
|
-
const entry = JSON.parse((0,
|
|
16220
|
+
if ((0, import_node_fs17.statSync)(path2).size > MAX_ENTRY_BYTES) return void 0;
|
|
16221
|
+
const entry = JSON.parse((0, import_node_fs17.readFileSync)(path2, "utf8"));
|
|
16193
16222
|
const age = Date.now() - entry.storedAt;
|
|
16194
16223
|
if (typeof entry.etag === "string" && entry.etag && !/[\r\n]/.test(entry.etag) && typeof entry.body === "string" && Number.isFinite(entry.storedAt) && age >= 0 && age <= MAX_AGE_MS) return entry;
|
|
16195
16224
|
} catch {
|
|
@@ -16199,18 +16228,18 @@ function readRestEtag(key, dir = restEtagCacheDir()) {
|
|
|
16199
16228
|
function deleteRestEtag(key, dir = restEtagCacheDir()) {
|
|
16200
16229
|
if (dir === "off") return;
|
|
16201
16230
|
try {
|
|
16202
|
-
(0,
|
|
16231
|
+
(0, import_node_fs17.rmSync)(entryPath(dir, key), { force: true });
|
|
16203
16232
|
} catch {
|
|
16204
16233
|
}
|
|
16205
16234
|
}
|
|
16206
16235
|
function prune(dir) {
|
|
16207
16236
|
const now = Date.now();
|
|
16208
|
-
const files = (0,
|
|
16237
|
+
const files = (0, import_node_fs17.readdirSync)(dir).filter((name) => /^[a-f0-9]{64}\.json(?:\.tmp-.+)?$/.test(name)).flatMap((name) => {
|
|
16209
16238
|
try {
|
|
16210
|
-
const path2 = (0,
|
|
16211
|
-
const stat4 = (0,
|
|
16239
|
+
const path2 = (0, import_node_path14.join)(dir, name);
|
|
16240
|
+
const stat4 = (0, import_node_fs17.statSync)(path2);
|
|
16212
16241
|
if (now - stat4.mtimeMs > MAX_AGE_MS) {
|
|
16213
|
-
(0,
|
|
16242
|
+
(0, import_node_fs17.rmSync)(path2, { force: true });
|
|
16214
16243
|
return [];
|
|
16215
16244
|
}
|
|
16216
16245
|
return name.endsWith(".json") ? [{ path: path2, size: stat4.size, at: stat4.mtimeMs }] : [];
|
|
@@ -16223,7 +16252,7 @@ function prune(dir) {
|
|
|
16223
16252
|
for (const file of files) {
|
|
16224
16253
|
if (count <= MAX_ENTRIES && bytes <= MAX_BYTES) break;
|
|
16225
16254
|
try {
|
|
16226
|
-
(0,
|
|
16255
|
+
(0, import_node_fs17.rmSync)(file.path, { force: true });
|
|
16227
16256
|
bytes -= file.size;
|
|
16228
16257
|
count--;
|
|
16229
16258
|
} catch {
|
|
@@ -16240,14 +16269,14 @@ function writeRestEtag(key, entry, dir = restEtagCacheDir()) {
|
|
|
16240
16269
|
deleteRestEtag(key, dir);
|
|
16241
16270
|
return;
|
|
16242
16271
|
}
|
|
16243
|
-
(0,
|
|
16244
|
-
(0,
|
|
16245
|
-
(0,
|
|
16272
|
+
(0, import_node_fs17.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
16273
|
+
(0, import_node_fs17.writeFileSync)(tmp, data, { mode: 384, flag: "wx" });
|
|
16274
|
+
(0, import_node_fs17.renameSync)(tmp, path2);
|
|
16246
16275
|
prune(dir);
|
|
16247
16276
|
} catch {
|
|
16248
16277
|
} finally {
|
|
16249
16278
|
try {
|
|
16250
|
-
(0,
|
|
16279
|
+
(0, import_node_fs17.rmSync)(tmp, { force: true });
|
|
16251
16280
|
} catch {
|
|
16252
16281
|
}
|
|
16253
16282
|
}
|
|
@@ -18712,7 +18741,7 @@ var SCHEDULE_EXECUTORS = /* @__PURE__ */ new Set([
|
|
|
18712
18741
|
function parseScheduleHeader(yamlText) {
|
|
18713
18742
|
const out = {};
|
|
18714
18743
|
for (const rawLine of yamlText.split("\n")) {
|
|
18715
|
-
const m = /^#\s*(schedule|what|owner|cadence|executor|llm|output|breaks|kill|target|model):\s*(.+?)\s*$/.exec(rawLine);
|
|
18744
|
+
const m = /^#\s*(schedule|what|owner|cadence|executor|llm|output|breaks|kill|target|model|fire):\s*(.+?)\s*$/.exec(rawLine);
|
|
18716
18745
|
if (m && out[m[1]] === void 0) out[m[1]] = m[2].trim();
|
|
18717
18746
|
}
|
|
18718
18747
|
return out;
|
|
@@ -18793,6 +18822,10 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
|
|
|
18793
18822
|
if (header.model !== void 0) {
|
|
18794
18823
|
throw new Error(`schedules register: ${expectedId} header \`model: ${header.model}\` is refused \u2014 a model role key is valid only on an LLM lane, and no scheduled-LLM executor is supported (Cursor retired #7779; Codex executor retired #7821). Remove the line.`);
|
|
18795
18824
|
}
|
|
18825
|
+
const fire = header.fire;
|
|
18826
|
+
if (fire !== void 0 && (h.executor !== "github-actions" || fire !== "v1")) {
|
|
18827
|
+
throw new Error(`schedules register: ${expectedId} header \`fire: ${fire}\` is invalid \u2014 the fire contract is \`fire: v1\`, github-actions rows only (#9996). An unknown revision would register a row the dispatcher cannot protect; a non-github-actions row has no workflow to claim it.`);
|
|
18828
|
+
}
|
|
18796
18829
|
return {
|
|
18797
18830
|
id: expectedId,
|
|
18798
18831
|
what: h.what,
|
|
@@ -18805,7 +18838,8 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
|
|
|
18805
18838
|
kill: h.kill,
|
|
18806
18839
|
repo,
|
|
18807
18840
|
sourcePath: workflowPath,
|
|
18808
|
-
...target !== void 0 ? { target } : {}
|
|
18841
|
+
...target !== void 0 ? { target } : {},
|
|
18842
|
+
...fire !== void 0 ? { fire } : {}
|
|
18809
18843
|
};
|
|
18810
18844
|
}
|
|
18811
18845
|
function scheduleRecordsFromWorkflows(repo, files) {
|
|
@@ -20290,9 +20324,9 @@ init_clean_exit();
|
|
|
20290
20324
|
|
|
20291
20325
|
// src/gh-create.ts
|
|
20292
20326
|
var import_promises = require("node:fs/promises");
|
|
20293
|
-
var
|
|
20327
|
+
var import_node_fs18 = require("node:fs");
|
|
20294
20328
|
var import_node_os9 = require("node:os");
|
|
20295
|
-
var
|
|
20329
|
+
var import_node_path15 = require("node:path");
|
|
20296
20330
|
var import_node_crypto7 = require("node:crypto");
|
|
20297
20331
|
|
|
20298
20332
|
// src/board-priority.ts
|
|
@@ -20380,8 +20414,8 @@ async function bodyArgsViaFile(args, deps = {}) {
|
|
|
20380
20414
|
const remove2 = deps.remove ?? import_promises.unlink;
|
|
20381
20415
|
const ensureDir = deps.ensureDir ?? import_promises.mkdir;
|
|
20382
20416
|
const dir = deps.dir ?? (0, import_node_os9.tmpdir)();
|
|
20383
|
-
const file = (0,
|
|
20384
|
-
await ensureDir((0,
|
|
20417
|
+
const file = (0, import_node_path15.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto7.randomBytes)(4).toString("hex")}.md`);
|
|
20418
|
+
await ensureDir((0, import_node_path15.dirname)(file), { recursive: true }).catch(() => {
|
|
20385
20419
|
});
|
|
20386
20420
|
await write(file, args[i + 1], "utf8");
|
|
20387
20421
|
return {
|
|
@@ -20581,8 +20615,8 @@ function retryBodyFile(args) {
|
|
|
20581
20615
|
if (file !== void 0) return file;
|
|
20582
20616
|
const body = flagValue(args, "--body");
|
|
20583
20617
|
if (body === void 0) return void 0;
|
|
20584
|
-
const path2 = (0,
|
|
20585
|
-
(0,
|
|
20618
|
+
const path2 = (0, import_node_path15.join)((0, import_node_fs18.mkdtempSync)((0, import_node_path15.join)((0, import_node_os9.tmpdir)(), "mmi-create-retry-")), "body.md");
|
|
20619
|
+
(0, import_node_fs18.writeFileSync)(path2, body, "utf8");
|
|
20586
20620
|
return path2;
|
|
20587
20621
|
}
|
|
20588
20622
|
function buildPrCreateRetryCommand(args) {
|
|
@@ -20731,7 +20765,7 @@ async function defaultRestCreatePr(exec3, input, deps = {}) {
|
|
|
20731
20765
|
const remove2 = deps.remove ?? import_promises.unlink;
|
|
20732
20766
|
const ensureDir = deps.ensureDir ?? import_promises.mkdir;
|
|
20733
20767
|
const dir = deps.dir ?? (0, import_node_os9.tmpdir)();
|
|
20734
|
-
const file = (0,
|
|
20768
|
+
const file = (0, import_node_path15.join)(dir, `mmi-rest-pr-${process.pid}-${(0, import_node_crypto7.randomBytes)(4).toString("hex")}.json`);
|
|
20735
20769
|
const payload = {
|
|
20736
20770
|
title: input.title,
|
|
20737
20771
|
head: input.head,
|
|
@@ -20739,7 +20773,7 @@ async function defaultRestCreatePr(exec3, input, deps = {}) {
|
|
|
20739
20773
|
};
|
|
20740
20774
|
if (input.base) payload.base = input.base;
|
|
20741
20775
|
if (input.draft) payload.draft = true;
|
|
20742
|
-
await ensureDir((0,
|
|
20776
|
+
await ensureDir((0, import_node_path15.dirname)(file), { recursive: true }).catch(() => {
|
|
20743
20777
|
});
|
|
20744
20778
|
await write(file, JSON.stringify(payload), "utf8");
|
|
20745
20779
|
const args = [
|
|
@@ -24041,11 +24075,11 @@ function scanSessionTranscripts(root, matches, now) {
|
|
|
24041
24075
|
const pending = [root];
|
|
24042
24076
|
while (pending.length) {
|
|
24043
24077
|
const dir = pending.pop();
|
|
24044
|
-
for (const entry of (0,
|
|
24045
|
-
const path2 = (0,
|
|
24078
|
+
for (const entry of (0, import_node_fs19.readdirSync)(dir, { withFileTypes: true })) {
|
|
24079
|
+
const path2 = (0, import_node_path16.join)(dir, entry.name);
|
|
24046
24080
|
if (entry.isDirectory()) pending.push(path2);
|
|
24047
24081
|
else if (entry.isFile() && matches(entry.name.toLowerCase())) {
|
|
24048
|
-
return now - (0,
|
|
24082
|
+
return now - (0, import_node_fs19.statSync)(path2).mtimeMs <= CLAIM_SESSION_ACTIVITY_MS ? "live" : "dead";
|
|
24049
24083
|
}
|
|
24050
24084
|
}
|
|
24051
24085
|
}
|
|
@@ -24065,7 +24099,7 @@ function probeLocalClaimSession(marker, now = Date.now(), roots = {}) {
|
|
|
24065
24099
|
};
|
|
24066
24100
|
try {
|
|
24067
24101
|
const session = marker.session.toLowerCase();
|
|
24068
|
-
const state = kind === "claude" ? scanSessionTranscripts(roots.claude ?? (0,
|
|
24102
|
+
const state = kind === "claude" ? scanSessionTranscripts(roots.claude ?? (0, import_node_path16.join)((0, import_node_os11.homedir)(), ".claude", "projects"), (name) => name === `${session}.jsonl`, now) : scanSessionTranscripts(roots.jerv ?? (0, import_node_path16.join)((0, import_node_os11.homedir)(), ".jerv", "agent", "sessions"), (name) => name.endsWith(`_${session}.jsonl`), now);
|
|
24069
24103
|
return remember(state);
|
|
24070
24104
|
} catch {
|
|
24071
24105
|
return remember("unverifiable");
|
|
@@ -24558,8 +24592,8 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
24558
24592
|
});
|
|
24559
24593
|
if (contest.contested && !ctx.landing && contest.branch) {
|
|
24560
24594
|
const candidateBranch = contest.branch.replace(/ \(head of open PR .*\)$/, "").trim();
|
|
24561
|
-
const
|
|
24562
|
-
if (
|
|
24595
|
+
const originRepo2 = repoFromRemoteUrl(await ctx.git(["remote", "get-url", "origin"]).catch(() => ""));
|
|
24596
|
+
if (originRepo2 && originRepo2.toLowerCase() === item.repository.toLowerCase()) {
|
|
24563
24597
|
const localSha = (await ctx.git(["rev-parse", "--verify", `refs/heads/${candidateBranch}^{commit}`]).catch(() => "")).trim();
|
|
24564
24598
|
const currentBranch2 = (await ctx.git(["symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => "")).trim();
|
|
24565
24599
|
if (localSha && (!contest.branchSha || localSha === contest.branchSha) || currentBranch2 === candidateBranch) {
|
|
@@ -25252,6 +25286,11 @@ function loadBootstrapSeeds(manifestJson) {
|
|
|
25252
25286
|
if (s.ownership !== "org" && s.ownership !== "repo") {
|
|
25253
25287
|
throw new Error(`invalid seed ownership '${s.ownership}' for ${s.target} (must be 'org' or 'repo')`);
|
|
25254
25288
|
}
|
|
25289
|
+
if (s.repos !== void 0) {
|
|
25290
|
+
if (!Array.isArray(s.repos) || s.repos.length === 0 || s.repos.some((r) => typeof r !== "string" || !r.trim() || r.includes("/"))) {
|
|
25291
|
+
throw new Error(`invalid repos scope for ${s.target} (needs a non-empty list of repo slugs, no owner prefix): ${JSON.stringify(s.repos)}`);
|
|
25292
|
+
}
|
|
25293
|
+
}
|
|
25255
25294
|
if (s.managedBlock) {
|
|
25256
25295
|
const block = s.managedBlock;
|
|
25257
25296
|
if (!block.source || !block.begin || !block.end || block.begin === block.end || /[\r\n]/.test(block.begin + block.end)) {
|
|
@@ -25277,6 +25316,11 @@ function seedMatchesReleaseTrack(seed, releaseTrack) {
|
|
|
25277
25316
|
if (!seed.releaseTracks?.length) return true;
|
|
25278
25317
|
return releaseTrack != null && seed.releaseTracks.includes(releaseTrack);
|
|
25279
25318
|
}
|
|
25319
|
+
function seedMatchesRepo(seed, repo) {
|
|
25320
|
+
if (!seed.repos?.length) return true;
|
|
25321
|
+
const slug = (repo ?? "").split("/").pop().trim().toLowerCase();
|
|
25322
|
+
return slug !== "" && seed.repos.some((s) => s.trim().toLowerCase() === slug);
|
|
25323
|
+
}
|
|
25280
25324
|
var TRAIN_ONLY_BEGIN = "mmi-train-only:start";
|
|
25281
25325
|
var TRAIN_ONLY_END = "mmi-train-only:end";
|
|
25282
25326
|
function renderSelfSeedForTrack(content, releaseTrack) {
|
|
@@ -25534,13 +25578,13 @@ function planManagedGitignore(current) {
|
|
|
25534
25578
|
}
|
|
25535
25579
|
|
|
25536
25580
|
// src/docs-index-command.ts
|
|
25537
|
-
var
|
|
25538
|
-
var
|
|
25581
|
+
var import_node_fs21 = require("node:fs");
|
|
25582
|
+
var import_node_path18 = require("node:path");
|
|
25539
25583
|
|
|
25540
25584
|
// src/doc-refs-core.ts
|
|
25541
25585
|
var import_node_child_process6 = require("node:child_process");
|
|
25542
|
-
var
|
|
25543
|
-
var
|
|
25586
|
+
var import_node_fs20 = require("node:fs");
|
|
25587
|
+
var import_node_path17 = require("node:path");
|
|
25544
25588
|
var PIN_RE = /<!--\s*pinned by\s+([^:]+?)\s*:\s*"([^"]+)"[^"]*-->/;
|
|
25545
25589
|
var PIN_MENTION_RE = /<!--\s*pinned by\b/;
|
|
25546
25590
|
var FWD_RE = /<!--\s*forward-ref:\s*(\S+?)\s*-->/;
|
|
@@ -25602,7 +25646,7 @@ function checkPins(root, readFile11, docs) {
|
|
|
25602
25646
|
findings.push({ kind: "malformed-pin", doc, line: pin.line, detail: pin.text });
|
|
25603
25647
|
continue;
|
|
25604
25648
|
}
|
|
25605
|
-
const source = readFile11((0,
|
|
25649
|
+
const source = readFile11((0, import_node_path17.join)(root, pin.file));
|
|
25606
25650
|
if (source == null) {
|
|
25607
25651
|
findings.push({ kind: "missing-test", doc, line: pin.line, detail: pin.file });
|
|
25608
25652
|
continue;
|
|
@@ -25665,11 +25709,11 @@ function checkRefs(root, deps, docs) {
|
|
|
25665
25709
|
for (const { ref } of extractRefs(markdown)) allFirstSegments.add(refFirstSegment(ref));
|
|
25666
25710
|
}
|
|
25667
25711
|
const tracked = allFirstSegments.size ? deps.trackedFirstSegments?.([...allFirstSegments]) ?? null : null;
|
|
25668
|
-
const firstVerifiable = (first) => tracked ? tracked.has(first) : exists((0,
|
|
25712
|
+
const firstVerifiable = (first) => tracked ? tracked.has(first) : exists((0, import_node_path17.join)(root, first));
|
|
25669
25713
|
const candidates = [];
|
|
25670
25714
|
const direct = [];
|
|
25671
25715
|
for (const [doc, markdown] of Object.entries(docs)) {
|
|
25672
|
-
const docDir =
|
|
25716
|
+
const docDir = import_node_path17.posix.dirname(doc);
|
|
25673
25717
|
const base = docDir === "." ? "" : docDir;
|
|
25674
25718
|
const covered = /* @__PURE__ */ new Set();
|
|
25675
25719
|
const markers = [];
|
|
@@ -25678,21 +25722,21 @@ function checkRefs(root, deps, docs) {
|
|
|
25678
25722
|
direct.push({ kind: "malformed-forward-ref", doc, line: fwd.line, detail: fwd.text });
|
|
25679
25723
|
continue;
|
|
25680
25724
|
}
|
|
25681
|
-
const docRel =
|
|
25682
|
-
const rootRel =
|
|
25725
|
+
const docRel = import_node_path17.posix.normalize(import_node_path17.posix.join(base, fwd.target));
|
|
25726
|
+
const rootRel = import_node_path17.posix.normalize(fwd.target.replace(/^\/+/, ""));
|
|
25683
25727
|
markers.push({ target: fwd.target, line: fwd.line, docRel, rootRel });
|
|
25684
25728
|
covered.add(docRel);
|
|
25685
25729
|
covered.add(rootRel);
|
|
25686
25730
|
}
|
|
25687
25731
|
const links = extractLinks(markdown).map(({ target, line }) => {
|
|
25688
|
-
const resolved =
|
|
25689
|
-
return { target, line, resolved, missing: !exists((0,
|
|
25732
|
+
const resolved = import_node_path17.posix.normalize(import_node_path17.posix.join(base, target));
|
|
25733
|
+
return { target, line, resolved, missing: !exists((0, import_node_path17.join)(root, resolved)) };
|
|
25690
25734
|
});
|
|
25691
25735
|
for (const marker of markers) {
|
|
25692
25736
|
const coversMissing = links.some(
|
|
25693
25737
|
(l) => l.missing && (l.resolved === marker.docRel || l.resolved === marker.rootRel)
|
|
25694
25738
|
);
|
|
25695
|
-
if (!coversMissing && (exists((0,
|
|
25739
|
+
if (!coversMissing && (exists((0, import_node_path17.join)(root, marker.docRel)) || exists((0, import_node_path17.join)(root, marker.rootRel)))) {
|
|
25696
25740
|
direct.push({
|
|
25697
25741
|
kind: "stale-forward-ref",
|
|
25698
25742
|
doc,
|
|
@@ -25703,7 +25747,7 @@ function checkRefs(root, deps, docs) {
|
|
|
25703
25747
|
}
|
|
25704
25748
|
for (const { ref, line } of extractRefs(markdown)) {
|
|
25705
25749
|
if (!firstVerifiable(refFirstSegment(ref))) continue;
|
|
25706
|
-
if (!exists((0,
|
|
25750
|
+
if (!exists((0, import_node_path17.join)(root, ref))) candidates.push({ kind: "missing-path", doc, line, detail: ref });
|
|
25707
25751
|
}
|
|
25708
25752
|
for (const { target, line, resolved, missing } of links) {
|
|
25709
25753
|
if (resolved.startsWith("..")) {
|
|
@@ -25826,7 +25870,7 @@ function checkRunbookClaims(root, docs, deps) {
|
|
|
25826
25870
|
const dir = prefix >= 0 ? args[prefix + 1] : ".";
|
|
25827
25871
|
const script = args.slice(run + 1).find((token, i, tail) => !token.startsWith("-") && tail[i - 1] !== "--prefix");
|
|
25828
25872
|
if (!dir || !script || placeholder(dir) || placeholder(script)) continue;
|
|
25829
|
-
const manifest = deps.readFile((0,
|
|
25873
|
+
const manifest = deps.readFile((0, import_node_path17.join)(root, dir, "package.json"));
|
|
25830
25874
|
const scripts = manifest ? JSON.parse(manifest).scripts : void 0;
|
|
25831
25875
|
if (!scripts || !Object.hasOwn(scripts, script)) report("missing-script", `npm run ${script} (${dir}/package.json)`);
|
|
25832
25876
|
} else if ((cmd === "python" || cmd === "python3") && args[0] === "-m") {
|
|
@@ -25836,10 +25880,10 @@ function checkRunbookClaims(root, docs, deps) {
|
|
|
25836
25880
|
const first = rel.split("/")[0];
|
|
25837
25881
|
const tracked = deps.trackedFirstSegments([first, `${first}.py`, `src/${first}`, `src/${first}.py`]);
|
|
25838
25882
|
const roots = ["", "src/"].filter((prefix) => {
|
|
25839
|
-
if (!prefix) return tracked ? tracked.has(first) || tracked.has(`${first}.py`) : deps.exists((0,
|
|
25840
|
-
return tracked ? tracked.has(`src/${first}`) || tracked.has(`src/${first}.py`) : deps.exists((0,
|
|
25883
|
+
if (!prefix) return tracked ? tracked.has(first) || tracked.has(`${first}.py`) : deps.exists((0, import_node_path17.join)(root, first)) || deps.exists((0, import_node_path17.join)(root, `${first}.py`));
|
|
25884
|
+
return tracked ? tracked.has(`src/${first}`) || tracked.has(`src/${first}.py`) : deps.exists((0, import_node_path17.join)(root, "src", first)) || deps.exists((0, import_node_path17.join)(root, "src", `${first}.py`));
|
|
25841
25885
|
});
|
|
25842
|
-
if (roots.length && !roots.some((prefix) => [".py", "/__init__.py", "/__main__.py"].some((suffix) => deps.exists((0,
|
|
25886
|
+
if (roots.length && !roots.some((prefix) => [".py", "/__init__.py", "/__main__.py"].some((suffix) => deps.exists((0, import_node_path17.join)(root, `${prefix}${rel}${suffix}`))))) {
|
|
25843
25887
|
report("missing-module", module2);
|
|
25844
25888
|
}
|
|
25845
25889
|
} else if (["node", "bash", "sh"].includes(cmd)) {
|
|
@@ -25847,7 +25891,7 @@ function checkRunbookClaims(root, docs, deps) {
|
|
|
25847
25891
|
if (!rel || rel.startsWith("-") || placeholder(rel) || /^(\/|[A-Za-z]:|\.\.\/)/.test(rel)) continue;
|
|
25848
25892
|
const first = refFirstSegment(rel);
|
|
25849
25893
|
const tracked = deps.trackedFirstSegments([first]);
|
|
25850
|
-
if ((tracked ? tracked.has(first) : deps.exists((0,
|
|
25894
|
+
if ((tracked ? tracked.has(first) : deps.exists((0, import_node_path17.join)(root, first))) && !deps.exists((0, import_node_path17.join)(root, rel)) && !deps.isIgnored([rel]).has(rel)) report("missing-path", rel);
|
|
25851
25895
|
}
|
|
25852
25896
|
}
|
|
25853
25897
|
}
|
|
@@ -25899,14 +25943,14 @@ function checkRunbookCoverage(docs, base, git3) {
|
|
|
25899
25943
|
return findings;
|
|
25900
25944
|
}
|
|
25901
25945
|
function checkSeedRunbookReview(root, base, readFile11, git3) {
|
|
25902
|
-
const manifestJson = readFile11((0,
|
|
25946
|
+
const manifestJson = readFile11((0, import_node_path17.join)(root, "skills/bootstrap/seeds/manifest.json"));
|
|
25903
25947
|
if (!manifestJson) return [];
|
|
25904
25948
|
const changed = new Set(git3(["diff", "--name-only", `${base}...HEAD`]).split(/\r?\n/).filter(Boolean));
|
|
25905
25949
|
const touched = loadBootstrapSeeds(manifestJson).seeds.filter((seed) => isPropagatableSeed(seed) && changed.has(seedSourcePath(seed)));
|
|
25906
25950
|
if (!touched.length) return [];
|
|
25907
25951
|
let declarations = null;
|
|
25908
25952
|
try {
|
|
25909
|
-
declarations = JSON.parse(readFile11((0,
|
|
25953
|
+
declarations = JSON.parse(readFile11((0, import_node_path17.join)(root, RUNBOOK_UNAFFECTED_PATH)) ?? "null");
|
|
25910
25954
|
} catch {
|
|
25911
25955
|
declarations = null;
|
|
25912
25956
|
}
|
|
@@ -25931,24 +25975,24 @@ function checkSeedRunbookReview(root, base, readFile11, git3) {
|
|
|
25931
25975
|
return findings;
|
|
25932
25976
|
}
|
|
25933
25977
|
function readFileOrNull(path2) {
|
|
25934
|
-
return (0,
|
|
25978
|
+
return (0, import_node_fs20.existsSync)(path2) ? (0, import_node_fs20.readFileSync)(path2, "utf8") : null;
|
|
25935
25979
|
}
|
|
25936
25980
|
function walk(dir, root, out) {
|
|
25937
|
-
for (const entry of (0,
|
|
25938
|
-
const full = (0,
|
|
25939
|
-
if ((0,
|
|
25981
|
+
for (const entry of (0, import_node_fs20.readdirSync)(dir)) {
|
|
25982
|
+
const full = (0, import_node_path17.join)(dir, entry);
|
|
25983
|
+
if ((0, import_node_fs20.statSync)(full).isDirectory()) walk(full, root, out);
|
|
25940
25984
|
else if (entry.endsWith(".md")) out.push(full.slice(root.length + 1).replaceAll("\\", "/"));
|
|
25941
25985
|
}
|
|
25942
25986
|
return out;
|
|
25943
25987
|
}
|
|
25944
25988
|
function defaultListDocs(root) {
|
|
25945
25989
|
const docs = ["docs", "skills/org-guide", "skills/mmi-help"].flatMap((path2) => {
|
|
25946
|
-
const directory = (0,
|
|
25947
|
-
return (0,
|
|
25990
|
+
const directory = (0, import_node_path17.join)(root, path2);
|
|
25991
|
+
return (0, import_node_fs20.existsSync)(directory) ? walk(directory, root, []) : [];
|
|
25948
25992
|
}).filter(
|
|
25949
25993
|
(rel) => !SKIP_WALK.some((skip) => rel.startsWith(skip))
|
|
25950
25994
|
);
|
|
25951
|
-
return [...ROOT_DOCS.filter((rel) => (0,
|
|
25995
|
+
return [...ROOT_DOCS.filter((rel) => (0, import_node_fs20.existsSync)((0, import_node_path17.join)(root, rel))), ...docs];
|
|
25952
25996
|
}
|
|
25953
25997
|
var CHECK_IGNORE_MAX_BUFFER = 32 * 1024 * 1024;
|
|
25954
25998
|
function defaultIsIgnored(root, relPaths, exec3 = import_node_child_process6.execFileSync) {
|
|
@@ -26022,7 +26066,7 @@ function mergeGroupRunbookMessages(root, deps, readFile11, warnings) {
|
|
|
26022
26066
|
}
|
|
26023
26067
|
function runDocRefs(root, deps = {}) {
|
|
26024
26068
|
const readFile11 = deps.readFile ?? readFileOrNull;
|
|
26025
|
-
const exists = deps.exists ??
|
|
26069
|
+
const exists = deps.exists ?? import_node_fs20.existsSync;
|
|
26026
26070
|
const listDocs = deps.listDocs ?? defaultListDocs;
|
|
26027
26071
|
const isIgnored = deps.isIgnored ?? ((paths) => defaultIsIgnored(root, paths));
|
|
26028
26072
|
let inventory;
|
|
@@ -26035,7 +26079,7 @@ function runDocRefs(root, deps = {}) {
|
|
|
26035
26079
|
const walked = listDocs(root);
|
|
26036
26080
|
const ignoredDocs = walked.length ? isIgnored(walked) : /* @__PURE__ */ new Set();
|
|
26037
26081
|
const docs = Object.fromEntries(
|
|
26038
|
-
walked.filter((rel) => !ignoredDocs.has(rel)).map((rel) => [rel, readFile11((0,
|
|
26082
|
+
walked.filter((rel) => !ignoredDocs.has(rel)).map((rel) => [rel, readFile11((0, import_node_path17.join)(root, rel))]).filter(([rel, body]) => body != null && (!deps.runbooksOnly || isRunbook(rel, body)))
|
|
26039
26083
|
);
|
|
26040
26084
|
const refResult = checkRefs(root, { exists, isIgnored, trackedFirstSegments }, docs);
|
|
26041
26085
|
const findings = [
|
|
@@ -26072,14 +26116,14 @@ ${sourceMessages}`;
|
|
|
26072
26116
|
}
|
|
26073
26117
|
for (const finding of findings) {
|
|
26074
26118
|
if (finding.kind !== "missing-path") continue;
|
|
26075
|
-
const directory =
|
|
26119
|
+
const directory = import_node_path17.posix.dirname(import_node_path17.posix.normalize(finding.detail)).split("/");
|
|
26076
26120
|
const distance = (path2) => {
|
|
26077
|
-
const parts =
|
|
26121
|
+
const parts = import_node_path17.posix.dirname(path2).split("/");
|
|
26078
26122
|
let common = 0;
|
|
26079
26123
|
while (common < directory.length && common < parts.length && directory[common] === parts[common]) common += 1;
|
|
26080
26124
|
return directory.length + parts.length - 2 * common;
|
|
26081
26125
|
};
|
|
26082
|
-
const suggest = [...new Set(trackedFiles() ?? [])].filter((path2) =>
|
|
26126
|
+
const suggest = [...new Set(trackedFiles() ?? [])].filter((path2) => import_node_path17.posix.basename(path2) === import_node_path17.posix.basename(finding.detail) && exists((0, import_node_path17.join)(root, path2))).sort((a, b) => distance(a) - distance(b) || a.localeCompare(b)).slice(0, 3);
|
|
26083
26127
|
if (suggest.length) finding.suggest = suggest;
|
|
26084
26128
|
}
|
|
26085
26129
|
return {
|
|
@@ -26172,31 +26216,31 @@ function walkMarkdown(dir) {
|
|
|
26172
26216
|
const stack = [dir];
|
|
26173
26217
|
while (stack.length) {
|
|
26174
26218
|
const current = stack.pop();
|
|
26175
|
-
for (const entry of (0,
|
|
26176
|
-
const full = (0,
|
|
26219
|
+
for (const entry of (0, import_node_fs21.readdirSync)(current, { withFileTypes: true })) {
|
|
26220
|
+
const full = (0, import_node_path18.join)(current, entry.name);
|
|
26177
26221
|
if (entry.isDirectory()) {
|
|
26178
26222
|
stack.push(full);
|
|
26179
26223
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
26180
|
-
out.push((0,
|
|
26224
|
+
out.push((0, import_node_path18.relative)(dir, full).split(import_node_path18.sep).join("/"));
|
|
26181
26225
|
}
|
|
26182
26226
|
}
|
|
26183
26227
|
}
|
|
26184
26228
|
return out;
|
|
26185
26229
|
}
|
|
26186
26230
|
function createDocsIndexDeps(repoRoot2) {
|
|
26187
|
-
const docsDir = (0,
|
|
26188
|
-
const indexPath = (0,
|
|
26231
|
+
const docsDir = (0, import_node_path18.join)(repoRoot2, "docs");
|
|
26232
|
+
const indexPath = (0, import_node_path18.join)(repoRoot2, DOCS_INDEX_PATH);
|
|
26189
26233
|
return {
|
|
26190
26234
|
listDocs: () => {
|
|
26191
|
-
if (!(0,
|
|
26235
|
+
if (!(0, import_node_fs21.existsSync)(docsDir)) return [];
|
|
26192
26236
|
const walked = walkMarkdown(docsDir).filter(isRoutableDocsPath);
|
|
26193
26237
|
if (!walked.length) return [];
|
|
26194
26238
|
const ignored = defaultIsIgnored(repoRoot2, walked.map((rel) => `docs/${rel}`));
|
|
26195
26239
|
return walked.filter((rel) => !ignored.has(`docs/${rel}`)).sort();
|
|
26196
26240
|
},
|
|
26197
|
-
readDoc: (relPath) => (0,
|
|
26198
|
-
readIndex: () => (0,
|
|
26199
|
-
writeIndex: (content) => (0,
|
|
26241
|
+
readDoc: (relPath) => (0, import_node_fs21.readFileSync)((0, import_node_path18.join)(docsDir, relPath), "utf8"),
|
|
26242
|
+
readIndex: () => (0, import_node_fs21.existsSync)(indexPath) ? (0, import_node_fs21.readFileSync)(indexPath, "utf8") : null,
|
|
26243
|
+
writeIndex: (content) => (0, import_node_fs21.writeFileSync)(indexPath, content, "utf8")
|
|
26200
26244
|
};
|
|
26201
26245
|
}
|
|
26202
26246
|
|
|
@@ -27319,8 +27363,8 @@ async function runBankRequest(fullName, options, deps) {
|
|
|
27319
27363
|
}
|
|
27320
27364
|
|
|
27321
27365
|
// src/train-apply-contracts.ts
|
|
27322
|
-
var
|
|
27323
|
-
var
|
|
27366
|
+
var import_node_fs22 = require("node:fs");
|
|
27367
|
+
var import_node_path19 = require("node:path");
|
|
27324
27368
|
function reduceFollowUpOutcomes(outcomes) {
|
|
27325
27369
|
if (outcomes.includes("failure")) return "failure";
|
|
27326
27370
|
if (outcomes.includes("unresolved")) return "unresolved";
|
|
@@ -27400,7 +27444,7 @@ async function resolveFoldPaths(deps, model) {
|
|
|
27400
27444
|
if (model === "vercel") return [];
|
|
27401
27445
|
const helper = "scripts/release-distribution.mjs";
|
|
27402
27446
|
if (model === "hub-serverless" || model === "registry-publish") {
|
|
27403
|
-
if ((0,
|
|
27447
|
+
if ((0, import_node_fs22.existsSync)(helper)) {
|
|
27404
27448
|
let out;
|
|
27405
27449
|
try {
|
|
27406
27450
|
out = await deps.run("node", [helper, "changed-files"]);
|
|
@@ -27549,7 +27593,7 @@ async function restoreMainNpmPackIdentities(deps, version) {
|
|
|
27549
27593
|
if (mainBom.version !== version) return void 0;
|
|
27550
27594
|
let localBom;
|
|
27551
27595
|
try {
|
|
27552
|
-
localBom = JSON.parse((0,
|
|
27596
|
+
localBom = JSON.parse((0, import_node_fs22.readFileSync)(bomPath, "utf8"));
|
|
27553
27597
|
} catch (e) {
|
|
27554
27598
|
throw new Error(
|
|
27555
27599
|
`version fold refused: ${bomPath} written by this fold's prepare is unreadable (${describeReadError(e)}) \u2014 cannot restore the published ${version} npm-pack identities from origin/main (#4503/#4713).`
|
|
@@ -27567,14 +27611,14 @@ async function restoreMainNpmPackIdentities(deps, version) {
|
|
|
27567
27611
|
restored += 1;
|
|
27568
27612
|
}
|
|
27569
27613
|
if (restored === 0) return void 0;
|
|
27570
|
-
(0,
|
|
27614
|
+
(0, import_node_fs22.writeFileSync)(bomPath, `${JSON.stringify(localBom, null, 2)}
|
|
27571
27615
|
`);
|
|
27572
27616
|
return `restored ${restored} npm-pack BOM identit${restored === 1 ? "y" : "ies"} from origin/main (#4503)`;
|
|
27573
27617
|
}
|
|
27574
27618
|
function publishVisibilityFor(surfaceId) {
|
|
27575
27619
|
let raw;
|
|
27576
27620
|
try {
|
|
27577
|
-
raw = (0,
|
|
27621
|
+
raw = (0, import_node_fs22.readFileSync)("surfaces.json", "utf8");
|
|
27578
27622
|
} catch (e) {
|
|
27579
27623
|
if (e.code === "ENOENT") return "unknown";
|
|
27580
27624
|
throw trainReadFailure(
|
|
@@ -27595,7 +27639,7 @@ function publishVisibilityFor(surfaceId) {
|
|
|
27595
27639
|
}
|
|
27596
27640
|
function npmPackArtifactName(packagePath) {
|
|
27597
27641
|
try {
|
|
27598
|
-
const pkg = JSON.parse((0,
|
|
27642
|
+
const pkg = JSON.parse((0, import_node_fs22.readFileSync)((0, import_node_path19.join)(packagePath, "package.json"), "utf8"));
|
|
27599
27643
|
return pkg.name || void 0;
|
|
27600
27644
|
} catch {
|
|
27601
27645
|
return void 0;
|
|
@@ -27604,7 +27648,7 @@ function npmPackArtifactName(packagePath) {
|
|
|
27604
27648
|
async function refuseDivergentPublishedNpmPack(deps, version) {
|
|
27605
27649
|
let localBom;
|
|
27606
27650
|
try {
|
|
27607
|
-
localBom = JSON.parse((0,
|
|
27651
|
+
localBom = JSON.parse((0, import_node_fs22.readFileSync)("distribution-bom.json", "utf8"));
|
|
27608
27652
|
} catch (e) {
|
|
27609
27653
|
throw new Error(
|
|
27610
27654
|
`version fold refused: distribution-bom.json written by this fold's prepare is unreadable (${e instanceof Error ? e.message.split("\n")[0] : String(e)}) \u2014 cannot compare the same-PATCH ${version} npm-pack identities against published npm (#4503/#4662).`
|
|
@@ -28556,10 +28600,10 @@ var rollout_plan_default = {
|
|
|
28556
28600
|
note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
|
|
28557
28601
|
},
|
|
28558
28602
|
baseline: {
|
|
28559
|
-
version: "4.6.
|
|
28560
|
-
tag: "v4.6.
|
|
28561
|
-
commit: "
|
|
28562
|
-
npm: "@mutmutco/cli@4.6.
|
|
28603
|
+
version: "4.6.49",
|
|
28604
|
+
tag: "v4.6.49",
|
|
28605
|
+
commit: "2de561d64225",
|
|
28606
|
+
npm: "@mutmutco/cli@4.6.49"
|
|
28563
28607
|
},
|
|
28564
28608
|
exitCriterion: "fleet-n-of-n",
|
|
28565
28609
|
hubOnlyShortcut: "forbidden",
|
|
@@ -28576,14 +28620,14 @@ var rollout_plan_default = {
|
|
|
28576
28620
|
repo: "mutmutco/mmi-hub",
|
|
28577
28621
|
role: "canary",
|
|
28578
28622
|
schedule: "train",
|
|
28579
|
-
v3Target: "v4.6.
|
|
28623
|
+
v3Target: "v4.6.49"
|
|
28580
28624
|
}
|
|
28581
28625
|
],
|
|
28582
28626
|
rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
|
|
28583
28627
|
rollback: {
|
|
28584
28628
|
independent: true,
|
|
28585
|
-
mechanism: "npm dist-tag latest -> 4.6.
|
|
28586
|
-
v3Target: "v4.6.
|
|
28629
|
+
mechanism: "npm dist-tag latest -> 4.6.49 and redeploy the Hub Lambda from tag v4.6.49 (2de561d64225); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
28630
|
+
v3Target: "v4.6.49 (@mutmutco/cli@4.6.49, tag commit 2de561d64225 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
28587
28631
|
}
|
|
28588
28632
|
},
|
|
28589
28633
|
{
|
|
@@ -28923,17 +28967,6 @@ function resolveIssueViewGhFields(normalized) {
|
|
|
28923
28967
|
return { ghFields: requested.join(","), numberIsTransport: false };
|
|
28924
28968
|
}
|
|
28925
28969
|
|
|
28926
|
-
// src/utf8-receipt.ts
|
|
28927
|
-
var import_node_fs22 = require("node:fs");
|
|
28928
|
-
var import_node_path19 = require("node:path");
|
|
28929
|
-
function writeUtf8Receipt(path2, text) {
|
|
28930
|
-
(0, import_node_fs22.mkdirSync)((0, import_node_path19.dirname)(path2), { recursive: true });
|
|
28931
|
-
const body = text.endsWith("\n") ? text : `${text}
|
|
28932
|
-
`;
|
|
28933
|
-
(0, import_node_fs22.writeFileSync)(path2, body, "utf8");
|
|
28934
|
-
return Buffer.byteLength(body, "utf8");
|
|
28935
|
-
}
|
|
28936
|
-
|
|
28937
28970
|
// src/query-commands.ts
|
|
28938
28971
|
var GH_LIST_TIMEOUT_MS = 2e4;
|
|
28939
28972
|
var DEFAULT_LIMIT = 30;
|
|
@@ -35552,11 +35585,41 @@ function ledgerFailedRemedy(ledger) {
|
|
|
35552
35585
|
function alignmentPrNumbers(note) {
|
|
35553
35586
|
return [...new Set([...(note ?? "").matchAll(/\/pull\/(\d+)/g)].map((m) => Number.parseInt(m[1], 10)))];
|
|
35554
35587
|
}
|
|
35588
|
+
function alignmentHeadBranches(tag) {
|
|
35589
|
+
return [
|
|
35590
|
+
{ target: "development", head: `release-align/development/${tag}` },
|
|
35591
|
+
{ target: "rc", head: `release-align/rc/${tag}` }
|
|
35592
|
+
];
|
|
35593
|
+
}
|
|
35594
|
+
async function alignmentLegClosedByHead(deps, prior) {
|
|
35595
|
+
const found = [];
|
|
35596
|
+
for (const { target, head } of alignmentHeadBranches(prior.tag)) {
|
|
35597
|
+
let rows;
|
|
35598
|
+
try {
|
|
35599
|
+
rows = JSON.parse(await deps.run("gh", ["pr", "list", "--repo", prior.repo, "--head", head, "--state", "all", "--json", "number,state"]));
|
|
35600
|
+
} catch {
|
|
35601
|
+
return void 0;
|
|
35602
|
+
}
|
|
35603
|
+
if (!Array.isArray(rows)) return void 0;
|
|
35604
|
+
for (const row of rows) {
|
|
35605
|
+
if (typeof row?.number !== "number" || !Number.isInteger(row.number) || typeof row.state !== "string") return void 0;
|
|
35606
|
+
if (row.state !== "MERGED") return void 0;
|
|
35607
|
+
found.push({ target, head, number: row.number });
|
|
35608
|
+
}
|
|
35609
|
+
}
|
|
35610
|
+
if (!found.some((f) => f.target === "development")) return void 0;
|
|
35611
|
+
const named = found.map((f) => `#${f.number} (head \`${f.head}\`)`).join(", ");
|
|
35612
|
+
return {
|
|
35613
|
+
state: "complete",
|
|
35614
|
+
note: `alignment: already-merged \u2014 alignment PR ${named} merged on GitHub after the train returned; the leg named no PR, so the PR was found by its head branch; closed live at the next pre-flight (#10212)`,
|
|
35615
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
35616
|
+
};
|
|
35617
|
+
}
|
|
35555
35618
|
async function alignmentLegClosedLive(deps, prior) {
|
|
35556
35619
|
const leg = prior.phases.alignment;
|
|
35557
35620
|
if (leg.state !== "pending") return void 0;
|
|
35558
35621
|
const numbers = alignmentPrNumbers(leg.note);
|
|
35559
|
-
if (numbers.length === 0) return
|
|
35622
|
+
if (numbers.length === 0) return alignmentLegClosedByHead(deps, prior);
|
|
35560
35623
|
for (const n of numbers) {
|
|
35561
35624
|
let state = "";
|
|
35562
35625
|
try {
|
|
@@ -42773,6 +42836,9 @@ async function readGhPrStateWithRetry(fetchState, options) {
|
|
|
42773
42836
|
function isNoFailedCheckQueueEjection(run) {
|
|
42774
42837
|
return !run || run.conclusion === null || run.conclusion === "cancelled" || run.conclusion === "neutral" || run.conclusion === "skipped";
|
|
42775
42838
|
}
|
|
42839
|
+
function isUnchangedHeadFailedRunEjection(run, submittedHead, currentHead) {
|
|
42840
|
+
return !!run && run.conclusion === "failure" && !!submittedHead && !!currentHead && currentHead.toLowerCase() === submittedHead.toLowerCase();
|
|
42841
|
+
}
|
|
42776
42842
|
async function runPrLand(prNumber, options, deps) {
|
|
42777
42843
|
const first = await runPrLandOnce(prNumber, options, deps);
|
|
42778
42844
|
if (options.enqueueOnly || options.expectHead || first.status !== "failed") return first;
|
|
@@ -42964,6 +43030,7 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
|
|
|
42964
43030
|
const deadline = deadlineMs ?? now() + timeoutMs;
|
|
42965
43031
|
let attempt = 0;
|
|
42966
43032
|
let credentialMismatched = false;
|
|
43033
|
+
let submittedHead;
|
|
42967
43034
|
const observe = (read) => {
|
|
42968
43035
|
const seen = deps.observeQueued?.(pr, base.repo, read);
|
|
42969
43036
|
if (seen?.mismatched) {
|
|
@@ -42983,6 +43050,7 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
|
|
|
42983
43050
|
state: "queued",
|
|
42984
43051
|
...read.mergeQueueEntry ? { position: read.mergeQueueEntry.position, entryState: read.mergeQueueEntry.state } : {}
|
|
42985
43052
|
};
|
|
43053
|
+
submittedHead = read.headRefOid ?? submittedHead;
|
|
42986
43054
|
const groupSha = read.mergeQueueEntry?.headCommit?.oid;
|
|
42987
43055
|
if (!options.enqueueOnly && groupSha && deps.findQueueStartupFailure) {
|
|
42988
43056
|
const startup = await deps.findQueueStartupFailure(base.repo, groupSha).catch(() => void 0);
|
|
@@ -43011,11 +43079,13 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
|
|
|
43011
43079
|
continue;
|
|
43012
43080
|
}
|
|
43013
43081
|
const run = await deps.findMergeGroupRun(pr, base.repo, base.base);
|
|
43082
|
+
const withheld = isUnchangedHeadFailedRunEjection(run, submittedHead, again.headRefOid);
|
|
43014
43083
|
const result2 = {
|
|
43015
43084
|
...base,
|
|
43016
43085
|
mergeQueue: { ...base.mergeQueue, state: "ejected", ...run ? { ejectedRun: run } : {} },
|
|
43017
43086
|
...base.pinnedHead ? { pinnedHead: { ...base.pinnedHead, refused: "ejected" } } : {},
|
|
43018
|
-
|
|
43087
|
+
...withheld && submittedHead ? { ejectedOnFailedRun: { runId: run.id, head: submittedHead, ...run.url ? { url: run.url } : {} } } : {},
|
|
43088
|
+
error: withheld && run ? `merge queue ejected PR #${pr} \u2014 merge_group run ${run.id} ${run.conclusion} ${run.url}; the PR head is unchanged since that run, so re-enqueueing it would resubmit the same failing commit and is withheld \u2014 fix the failed check, push the fix commit, then rerun \`mmi-cli devops pr land ${pr} --repo ${base.repo}\` (while a queued entry blocks a fix push, take the PR out of the queue first with \`mmi-cli devops pr land ${pr} --repo ${base.repo} --dequeue\`)` : `merge queue ejected PR #${pr} \u2014 ${run ? `merge_group run ${run.id} ${run.conclusion} ${run.url}` : "merge_group run not found"}; ${prQueueReadHint(pr, base.repo)}${isNoFailedCheckQueueEjection(run) ? `; no check failed \u2014 re-run \`mmi-cli devops pr land ${pr} --repo ${base.repo}\` once to re-enqueue, and if a land is refused again report the exact refusal verbatim (never fall back to a direct/admin merge or an \`MMI_ACTOR=personal\` landing workaround)` : ""}`
|
|
43019
43089
|
};
|
|
43020
43090
|
deps.queueProgress?.(result2);
|
|
43021
43091
|
return result2;
|
|
@@ -43045,7 +43115,7 @@ async function landMany(prs, landOne, cleanup, hooks) {
|
|
|
43045
43115
|
const safe = async (pr) => landOne(pr).catch((e) => ({ status: "failed", repo: "", pr, base: "", error: e.message }));
|
|
43046
43116
|
return Promise.all(prs.map(async (pr) => {
|
|
43047
43117
|
let result = await safe(pr);
|
|
43048
|
-
if (prLandOutcome(result) === "ejected" && !result.pinnedHead && !result.retriedAfterEjection) {
|
|
43118
|
+
if (prLandOutcome(result) === "ejected" && !result.pinnedHead && !result.retriedAfterEjection && !result.ejectedOnFailedRun) {
|
|
43049
43119
|
const baseRed = await hooks?.baseRedCheck?.(pr, result).catch(() => void 0);
|
|
43050
43120
|
if (baseRed) {
|
|
43051
43121
|
result = { ...result, baseRed: { base: result.base, baseRunId: baseRed.baseRunId }, error: `base-red: failures already on ${result.base} (${baseRed.baseRunId})` };
|
|
@@ -43325,10 +43395,17 @@ function readMessageOverride(message2, cwd) {
|
|
|
43325
43395
|
}
|
|
43326
43396
|
function readOverride(base, cwd, head = "HEAD") {
|
|
43327
43397
|
const format = `%H${FLD}%(trailers:key=${TRAILER_KEY},valueonly,unfold)${FLD}%B${REC}`;
|
|
43398
|
+
const records = git(["log", `${base}..${head}`, `--format=${format}`], cwd).split(REC).map((record2) => record2.replace(/^\s+/, "").split(FLD));
|
|
43399
|
+
return readOverrideRecords(records, cwd);
|
|
43400
|
+
}
|
|
43401
|
+
function readMessageOverrides(messages, cwd) {
|
|
43402
|
+
const records = messages.map((message2, index) => [`source-${index + 1}`, readMessageOverride(message2, cwd), message2]).reverse();
|
|
43403
|
+
return readOverrideRecords(records, cwd);
|
|
43404
|
+
}
|
|
43405
|
+
function readOverrideRecords(records, cwd) {
|
|
43328
43406
|
const refusals = [];
|
|
43329
43407
|
let override = null;
|
|
43330
|
-
for (const
|
|
43331
|
-
const [sha, trailer, body] = record2.replace(/^\s+/, "").split(FLD);
|
|
43408
|
+
for (const [sha, trailer, body] of records) {
|
|
43332
43409
|
if (!sha) continue;
|
|
43333
43410
|
if (override) break;
|
|
43334
43411
|
const { value, messageTransform } = readOverrideTrailer(trailer, body, cwd);
|
|
@@ -43354,6 +43431,9 @@ var GO_TEST_RE = GO_TEST_FILE_RE;
|
|
|
43354
43431
|
function readMessageOverride2(message2, cwd) {
|
|
43355
43432
|
return readMessageOverride(message2, cwd);
|
|
43356
43433
|
}
|
|
43434
|
+
function readMessageOverrides2(messages, cwd) {
|
|
43435
|
+
return readMessageOverrides(messages, cwd);
|
|
43436
|
+
}
|
|
43357
43437
|
function translate(glob) {
|
|
43358
43438
|
let out = "";
|
|
43359
43439
|
for (let i = 0; i < glob.length; i++) {
|
|
@@ -44469,6 +44549,38 @@ ${message2.body}`)) || readMessageOverride2(githubQueueSquashMessage(title, pr,
|
|
|
44469
44549
|
error: "queue preparation needed: the multi-commit COMMIT_MESSAGES squash would bury the Test-Policy-Override waiver mid-message, so the queue check would fail malformed-override-trailer"
|
|
44470
44550
|
};
|
|
44471
44551
|
}
|
|
44552
|
+
function queueBodyFile(pr) {
|
|
44553
|
+
return `.jerv/tmp/pr-${pr}-body.md`;
|
|
44554
|
+
}
|
|
44555
|
+
function writeQueueBodyFile(pr, body, targets, cwd = process.cwd()) {
|
|
44556
|
+
try {
|
|
44557
|
+
const alreadyCloses = (number) => new RegExp(`^[ \\t]*(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?):?[ \\t]+\\[?#${number}(?![0-9])`, "im").test(body);
|
|
44558
|
+
const closes = targets.filter((number) => !alreadyCloses(number)).map((number) => `Closes #${number}`).join("\n");
|
|
44559
|
+
const base = body.trimEnd();
|
|
44560
|
+
const separator = [...base.matchAll(/\n[ \t]*\n/g)].at(-1);
|
|
44561
|
+
const head = separator ? base.slice(0, separator.index) : "";
|
|
44562
|
+
const last = separator ? base.slice(separator.index + separator[0].length) : base;
|
|
44563
|
+
const content = !closes ? `${base}
|
|
44564
|
+
` : parseTrailers(base, cwd).length > 0 ? `${head ? `${head}
|
|
44565
|
+
|
|
44566
|
+
` : ""}${closes}
|
|
44567
|
+
|
|
44568
|
+
${last.trimEnd()}
|
|
44569
|
+
` : `${base}${base ? "\n\n" : ""}${closes}
|
|
44570
|
+
`;
|
|
44571
|
+
const directory = (0, import_node_path31.join)(cwd, ".jerv", "tmp");
|
|
44572
|
+
(0, import_node_fs34.mkdirSync)(directory, { recursive: true });
|
|
44573
|
+
(0, import_node_fs34.writeFileSync)((0, import_node_path31.join)(directory, `pr-${pr}-body.md`), content);
|
|
44574
|
+
return queueBodyFile(pr);
|
|
44575
|
+
} catch {
|
|
44576
|
+
return void 0;
|
|
44577
|
+
}
|
|
44578
|
+
}
|
|
44579
|
+
function queueMessageRemedy(pr, repo, targets, bodyFile) {
|
|
44580
|
+
const named = (format, fallback) => targets.length ? targets.map(format).join(", ") : fallback;
|
|
44581
|
+
const issues = named((number) => `#${number}`, "#N");
|
|
44582
|
+
return `PR author: if PR #${pr} is meant to close ${issues}, add ${named((number) => `\`Closes #${number}\``, "`Closes #N`")} to the PR body (\`mmi-cli devops pr edit ${pr} --repo ${repo} --body-file ${queueBodyFile(pr)}\`) and rerun \`mmi-cli devops pr land ${pr} --repo ${repo}\` without pushing a new head.` + (bodyFile ? ` The body file that command reads was just written (\`${bodyFile}\`): the current PR body plus a closing line for any target it lacked, so it runs exactly as printed.` : "") + ` If ${issues} must stay open, remove the closing phrases from the source commit or PR text using ${named((number) => `\`Part of #${number}\``, "`Part of #N`")} / ${named((number) => `\`Refs #${number}\``, "`Refs #N`")}, then re-run checks and \`pr land\` with the prepared head. A queue cannot accept --squash-body-file.`;
|
|
44583
|
+
}
|
|
44472
44584
|
async function inspectQueuePreparation(pr, repo, options) {
|
|
44473
44585
|
const read = options.read ?? readApi;
|
|
44474
44586
|
const queueRead = options.read ?? actorReadApi;
|
|
@@ -44521,7 +44633,7 @@ async function inspectQueuePreparation(pr, repo, options) {
|
|
|
44521
44633
|
status: "preparation-needed",
|
|
44522
44634
|
reason,
|
|
44523
44635
|
issueTargets: targets.map((number) => `${repo}#${number}`),
|
|
44524
|
-
nextAction: nextAction ?? (reason === "queue-message" ?
|
|
44636
|
+
nextAction: nextAction ?? (reason === "queue-message" ? queueMessageRemedy(pr, repo, targets) : "PR author: prepare the source commit messages and required trailers without a body override; re-run checks and pr land. GitHub may append coauthors even to one source commit, so source consolidation alone does not prove the generated message passes test policy. The combined queue candidate must pass with the updated shared trailer reader. Keep repository settings unchanged.")
|
|
44525
44637
|
},
|
|
44526
44638
|
error: `queue preparation needed: ${detail}`
|
|
44527
44639
|
});
|
|
@@ -44580,7 +44692,8 @@ ${pull.body}`;
|
|
|
44580
44692
|
...findNegatedClosings(input.text, closing),
|
|
44581
44693
|
...findCrossRepoAmbiguousClosings(input.text, closing, repo)
|
|
44582
44694
|
])];
|
|
44583
|
-
|
|
44695
|
+
const bodyFile = targets.length ? writeQueueBodyFile(pr, pull.body, targets, process.cwd()) : void 0;
|
|
44696
|
+
return { kind: "refused", result: preparation("queue-message", targets, `GitHub's unchanged ${policy.body} message is unsafe for ${targets.map((number) => `${repo}#${number}`).join(", ")}. ${closingGuardDiagnostic(verdict.message)} See preparation.nextAction for the exact \`mmi-cli devops pr edit ${pr} --repo ${repo} --body-file ${queueBodyFile(pr)}\` remedy.`, queueMessageRemedy(pr, repo, targets, bodyFile)) };
|
|
44584
44697
|
}
|
|
44585
44698
|
if (method === "SQUASH" && policy.body === "COMMIT_MESSAGES" && options.bodyText == null) {
|
|
44586
44699
|
const refusal = checkQueueSquashOverride(pull.title, pr, messages, process.cwd());
|
|
@@ -44588,7 +44701,18 @@ ${pull.body}`;
|
|
|
44588
44701
|
}
|
|
44589
44702
|
const requiredBody = options.bodyText ?? composedBody;
|
|
44590
44703
|
const prBodyPreservesOverrides = requiredBody != null && method === "SQUASH" && policy.body === "PR_BODY" && options.bodyText == null && prBodyPreservesCommitOverrides(messages, headline, pull.body, process.cwd());
|
|
44591
|
-
const
|
|
44704
|
+
const retained = options.bodyText == null && method !== "SQUASH" ? readMessageOverrides2(messages.map((message2) => `${message2.headline}
|
|
44705
|
+
|
|
44706
|
+
${message2.body}`), process.cwd()) : null;
|
|
44707
|
+
if (retained?.refusals.length) {
|
|
44708
|
+
return { kind: "refused", result: preparation(
|
|
44709
|
+
"queue-message-override",
|
|
44710
|
+
[],
|
|
44711
|
+
`retained source commit messages fail the shared waiver reader: ${retained.refusals.map((finding) => finding.detail).join("\n")}`,
|
|
44712
|
+
"PR author: the queue retains each source commit message. Put an authorized Test-Policy-Override with a known scope in a later commit's Git-recognized final trailer block, push without rewriting history, and rerun pr land. The nearest valid waiver supersedes older ones; malformed or unknown-scope waivers newer than it still refuse. The synthetic merge-message preview does not decide this requirement. Keep repository settings unchanged."
|
|
44713
|
+
) };
|
|
44714
|
+
}
|
|
44715
|
+
const queueKeepsWaiver = Boolean(retained?.override && !retained.refusals.length);
|
|
44592
44716
|
if (requiredBody && !prBodyPreservesOverrides && !queueKeepsWaiver && (method !== "SQUASH" || requiredBody.trim() !== body.trim())) {
|
|
44593
44717
|
if (method === "SQUASH" && policy.body === "PR_BODY" && options.bodyText == null) {
|
|
44594
44718
|
return {
|
|
@@ -44675,7 +44799,7 @@ async function abandonQueueAdmission(pr, repo, options = {}) {
|
|
|
44675
44799
|
if (!snapshot.headRefOid || snapshot.headRefOid.toLowerCase() !== receipt.head.toLowerCase()) {
|
|
44676
44800
|
return refuse(`the PR head is ${snapshot.headRefOid ?? "unreadable"}, not the receipt's submitted head ${receipt.head}; the admission is unchanged`);
|
|
44677
44801
|
}
|
|
44678
|
-
if (snapshot.isInMergeQueue) return refuse(
|
|
44802
|
+
if (snapshot.isInMergeQueue) return refuse(`the PR is currently in the merge queue, so the admission is not stuck; the admission is unchanged \u2014 take the PR out of the queue first with \`mmi-cli devops pr land ${pr} --repo ${repo} --dequeue\` if a fix commit must be pushed`);
|
|
44679
44803
|
try {
|
|
44680
44804
|
journal.save("abandoned");
|
|
44681
44805
|
} catch (error) {
|
|
@@ -44683,6 +44807,75 @@ async function abandonQueueAdmission(pr, repo, options = {}) {
|
|
|
44683
44807
|
}
|
|
44684
44808
|
return { status: "abandoned", repo, pr, head: receipt.head, ...receipt.uuid ? { operation: receipt.uuid } : {} };
|
|
44685
44809
|
}
|
|
44810
|
+
async function dequeuePrMergeQueue(pr, repo, options = {}) {
|
|
44811
|
+
if (!/^\d+$/.test(pr)) return { status: "refused", repo, pr, reason: "a bare PR number is required" };
|
|
44812
|
+
const refuse = (reason) => ({ status: "refused", repo, pr, reason });
|
|
44813
|
+
const read = options.read ?? actorReadApi;
|
|
44814
|
+
let entry;
|
|
44815
|
+
try {
|
|
44816
|
+
const result = await queueGraphql(
|
|
44817
|
+
repo,
|
|
44818
|
+
`pullRequest(number:${pr}) { id state isInMergeQueue headRefOid }`,
|
|
44819
|
+
read
|
|
44820
|
+
);
|
|
44821
|
+
if (!result.pullRequest) throw new Error(`merge queue PR #${pr} not found`);
|
|
44822
|
+
entry = result.pullRequest;
|
|
44823
|
+
} catch (error) {
|
|
44824
|
+
return refuse(`the PR and queue state could not be read (${error.message}); nothing was dequeued`);
|
|
44825
|
+
}
|
|
44826
|
+
if (!entry.isInMergeQueue) return refuse(`the PR is '${entry.state ?? "unreadable"}' and not in the merge queue; nothing to dequeue`);
|
|
44827
|
+
if (!entry.id) return refuse("the PR node id is unreadable; nothing was dequeued");
|
|
44828
|
+
let output;
|
|
44829
|
+
try {
|
|
44830
|
+
const mutation = await (options.write ?? writeApi)(["graphql", "-f", "query=mutation($id:ID!){dequeuePullRequest(input:{pullRequestId:$id}){clientMutationId}}", "-f", `id=${entry.id}`]);
|
|
44831
|
+
output = typeof mutation === "string" ? mutation : mutation.stdout;
|
|
44832
|
+
} catch (error) {
|
|
44833
|
+
return refuse(`dequeuePullRequest failed (${error.message}); the queue entry is unchanged`);
|
|
44834
|
+
}
|
|
44835
|
+
let graphqlErrors = "";
|
|
44836
|
+
try {
|
|
44837
|
+
const response = JSON.parse(output);
|
|
44838
|
+
if (response.errors?.length) graphqlErrors = response.errors.map((e) => e.message).join("; ");
|
|
44839
|
+
} catch {
|
|
44840
|
+
graphqlErrors = "the dequeue response is unreadable";
|
|
44841
|
+
}
|
|
44842
|
+
if (graphqlErrors) return refuse(`dequeuePullRequest reported: ${graphqlErrors}; the queue entry is unchanged \u2014 verify with \`mmi-cli devops read pr queue ${pr} --repo ${repo} --json\` before pushing`);
|
|
44843
|
+
const sleep2 = options.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
|
|
44844
|
+
let verified;
|
|
44845
|
+
let lastRead;
|
|
44846
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
44847
|
+
if (attempt) await sleep2(attempt * 1e3);
|
|
44848
|
+
try {
|
|
44849
|
+
const observed = await readPrMergeQueue(pr, repo, read, sleep2);
|
|
44850
|
+
if (!observed.isInMergeQueue) {
|
|
44851
|
+
verified = observed;
|
|
44852
|
+
break;
|
|
44853
|
+
}
|
|
44854
|
+
lastRead = `the PR still reads as in the merge queue (state '${observed.state}')`;
|
|
44855
|
+
} catch (error) {
|
|
44856
|
+
lastRead = error.message;
|
|
44857
|
+
}
|
|
44858
|
+
}
|
|
44859
|
+
if (!verified) {
|
|
44860
|
+
return refuse(`dequeuePullRequest was sent but the read-back could not verify the PR out of the queue (${lastRead ?? "unreadable"}); do not assume the entry is gone \u2014 verify with \`mmi-cli devops read pr queue ${pr} --repo ${repo} --json\`, and rerun --dequeue only if the PR still shows queued`);
|
|
44861
|
+
}
|
|
44862
|
+
let admission = "none";
|
|
44863
|
+
let operation;
|
|
44864
|
+
try {
|
|
44865
|
+
const journal = admissionJournal(repo, pr, options.receiptDirectory);
|
|
44866
|
+
const receipt = journal.receipt;
|
|
44867
|
+
if (receipt && (["pending", "enqueued"].includes(receipt.state) || receipt.state === "dispatching" && !receipt.uuid)) {
|
|
44868
|
+
journal.save("abandoned");
|
|
44869
|
+
admission = "terminal";
|
|
44870
|
+
operation = receipt.uuid;
|
|
44871
|
+
} else if (receipt) {
|
|
44872
|
+
admission = "left-open";
|
|
44873
|
+
}
|
|
44874
|
+
} catch {
|
|
44875
|
+
admission = "unreadable";
|
|
44876
|
+
}
|
|
44877
|
+
return { status: "dequeued", repo, pr, ...entry.headRefOid ? { head: entry.headRefOid } : {}, admission, ...operation ? { operation } : {} };
|
|
44878
|
+
}
|
|
44686
44879
|
function closingGuardDiagnostic(message2) {
|
|
44687
44880
|
if (!message2) return "";
|
|
44688
44881
|
const remedy = message2.search(/GitHub's merge queue composes|If this PR is meant to close/);
|
|
@@ -45173,8 +45366,204 @@ function registerRunDispatch(run, writeToken, projects) {
|
|
|
45173
45366
|
});
|
|
45174
45367
|
}
|
|
45175
45368
|
|
|
45176
|
-
// src/
|
|
45369
|
+
// src/remote-test.ts
|
|
45177
45370
|
var import_node_crypto13 = require("node:crypto");
|
|
45371
|
+
init_cli_shared();
|
|
45372
|
+
var FOCUSED_TEST_REPO = "mutmutco/MMI-Hub";
|
|
45373
|
+
var FOCUSED_TEST_WORKFLOW = "focused-test.yml";
|
|
45374
|
+
var FOCUSED_TEST_REF = "development";
|
|
45375
|
+
var FOCUSED_TEST_OWNER = "mutmutco";
|
|
45376
|
+
var REMOTE_TEST_MAX_FILES = 20;
|
|
45377
|
+
var REMOTE_TEST_POLL_MS = 15e3;
|
|
45378
|
+
var REMOTE_TEST_CORRELATE_MS = 5 * 6e4;
|
|
45379
|
+
var REMOTE_TEST_WATCH_MS = 30 * 6e4;
|
|
45380
|
+
var REMOTE_TEST_LOG_TAIL = 60;
|
|
45381
|
+
var REMOTE_TEST_UNKNOWN = 3;
|
|
45382
|
+
var REMOTE_TEST_FILE_RE = /^[A-Za-z0-9._/@-]+$/;
|
|
45383
|
+
var REMOTE_TEST_NAME_RE = /^[A-Za-z0-9._-]+$/;
|
|
45384
|
+
var REMOTE_TEST_SHA_RE = /^[0-9a-f]{40}$/;
|
|
45385
|
+
function focusedTestRepoName(repo) {
|
|
45386
|
+
const parts = repo.split("/");
|
|
45387
|
+
if (parts.length !== 2 || !parts.every((part) => REMOTE_TEST_NAME_RE.test(part))) {
|
|
45388
|
+
throw new Error(`remote test: --repo must be <owner>/<repo> (letters, digits, dot, underscore, hyphen), received '${repo}'`);
|
|
45389
|
+
}
|
|
45390
|
+
if (parts[0].toLowerCase() !== FOCUSED_TEST_OWNER) {
|
|
45391
|
+
throw new Error(`remote test: --repo must name a ${FOCUSED_TEST_OWNER}/ repository (the central workflow checks out ${FOCUSED_TEST_OWNER}/<name> only), received '${repo}'`);
|
|
45392
|
+
}
|
|
45393
|
+
return parts[1];
|
|
45394
|
+
}
|
|
45395
|
+
function validateRemoteTest(request) {
|
|
45396
|
+
focusedTestRepoName(request.repo);
|
|
45397
|
+
if (!REMOTE_TEST_SHA_RE.test(request.sha)) {
|
|
45398
|
+
throw new Error(`remote test: --sha must be a 40-character lowercase commit sha, received '${request.sha}'`);
|
|
45399
|
+
}
|
|
45400
|
+
if (request.tool !== "node-test" && request.tool !== "vitest") {
|
|
45401
|
+
throw new Error(`remote test: --tool must be node-test or vitest, received '${String(request.tool)}'`);
|
|
45402
|
+
}
|
|
45403
|
+
if (!Array.isArray(request.files) || request.files.length === 0) {
|
|
45404
|
+
throw new Error("remote test: name at least one test file (space-separated literals, max 20)");
|
|
45405
|
+
}
|
|
45406
|
+
if (request.files.length > REMOTE_TEST_MAX_FILES) {
|
|
45407
|
+
throw new Error(`remote test: at most ${REMOTE_TEST_MAX_FILES} test files per run, received ${request.files.length}`);
|
|
45408
|
+
}
|
|
45409
|
+
for (const file of request.files) {
|
|
45410
|
+
if (!REMOTE_TEST_FILE_RE.test(file)) {
|
|
45411
|
+
throw new Error(`remote test: file path uses characters outside A-Za-z0-9._/@- : '${file}'`);
|
|
45412
|
+
}
|
|
45413
|
+
if (file.includes("..")) throw new Error(`remote test: file path must not contain '..': '${file}'`);
|
|
45414
|
+
if (file.startsWith("/")) throw new Error(`remote test: file path must be repo-relative: '${file}'`);
|
|
45415
|
+
if (file.startsWith("-")) throw new Error(`remote test: a file path must never read as a flag: '${file}'`);
|
|
45416
|
+
}
|
|
45417
|
+
if (typeof request.install !== "boolean") throw new Error("remote test: install must be a boolean");
|
|
45418
|
+
}
|
|
45419
|
+
function tailLines(text, count) {
|
|
45420
|
+
return text.replace(/\r\n/g, "\n").split("\n").filter((line, index, all) => !(index === all.length - 1 && line === "")).slice(-count);
|
|
45421
|
+
}
|
|
45422
|
+
function parseJson(path2, text) {
|
|
45423
|
+
try {
|
|
45424
|
+
return JSON.parse(text);
|
|
45425
|
+
} catch {
|
|
45426
|
+
throw new Error(`remote test: unreadable answer from ${path2}`);
|
|
45427
|
+
}
|
|
45428
|
+
}
|
|
45429
|
+
async function runRemoteTest(request, deps) {
|
|
45430
|
+
validateRemoteTest(request);
|
|
45431
|
+
const name = focusedTestRepoName(request.repo);
|
|
45432
|
+
const nonce = deps.nonce();
|
|
45433
|
+
const title = `focused-test ${nonce}`;
|
|
45434
|
+
const uncertain = (message2, runId = null, url = null) => ({ exitCode: REMOTE_TEST_UNKNOWN, title, runId, url, conclusion: null, tail: [], message: message2 });
|
|
45435
|
+
const inputs = [
|
|
45436
|
+
`repo=${name}`,
|
|
45437
|
+
`sha=${request.sha}`,
|
|
45438
|
+
`tool=${request.tool}`,
|
|
45439
|
+
`files=${request.files.join(" ")}`,
|
|
45440
|
+
`install=${request.install}`,
|
|
45441
|
+
`nonce=${nonce}`
|
|
45442
|
+
];
|
|
45443
|
+
try {
|
|
45444
|
+
await deps.dispatch(inputs);
|
|
45445
|
+
} catch (error) {
|
|
45446
|
+
return uncertain(`remote test: dispatch outcome unknown \u2014 ${error.message}; inspect ${FOCUSED_TEST_REPO} Actions before any retry`);
|
|
45447
|
+
}
|
|
45448
|
+
const runsPath = `repos/${FOCUSED_TEST_REPO}/actions/workflows/${FOCUSED_TEST_WORKFLOW}/runs?event=workflow_dispatch&per_page=50`;
|
|
45449
|
+
let run;
|
|
45450
|
+
try {
|
|
45451
|
+
const correlateUntil = deps.now() + REMOTE_TEST_CORRELATE_MS;
|
|
45452
|
+
while (!run) {
|
|
45453
|
+
const rows = parseJson(runsPath, await deps.read(runsPath)).workflow_runs ?? [];
|
|
45454
|
+
run = rows.find((row) => row.display_title === title);
|
|
45455
|
+
if (run) break;
|
|
45456
|
+
if (deps.now() >= correlateUntil) {
|
|
45457
|
+
return uncertain(`remote test: no run named "${title}" appeared within ${REMOTE_TEST_CORRELATE_MS / 6e4}m \u2014 the dispatch was refused or is queued behind a rule; nothing was retried`);
|
|
45458
|
+
}
|
|
45459
|
+
await deps.sleep(REMOTE_TEST_POLL_MS);
|
|
45460
|
+
}
|
|
45461
|
+
const runPath = `repos/${FOCUSED_TEST_REPO}/actions/runs/${run.id}`;
|
|
45462
|
+
const watchUntil = deps.now() + REMOTE_TEST_WATCH_MS;
|
|
45463
|
+
let final;
|
|
45464
|
+
while (!final) {
|
|
45465
|
+
const current = parseJson(runPath, await deps.read(runPath));
|
|
45466
|
+
if (current.status === "completed") {
|
|
45467
|
+
final = current;
|
|
45468
|
+
break;
|
|
45469
|
+
}
|
|
45470
|
+
if (deps.now() >= watchUntil) {
|
|
45471
|
+
return uncertain(`remote test: run ${run.id} still ${current.status} after ${REMOTE_TEST_WATCH_MS / 6e4}m \u2014 no verdict exists`, run.id, current.html_url);
|
|
45472
|
+
}
|
|
45473
|
+
await deps.sleep(REMOTE_TEST_POLL_MS);
|
|
45474
|
+
}
|
|
45475
|
+
const jobsPath = `${runPath}/jobs?per_page=100`;
|
|
45476
|
+
const jobs = parseJson(jobsPath, await deps.read(jobsPath)).jobs ?? [];
|
|
45477
|
+
const testJob = jobs.find((job) => job.name === "test");
|
|
45478
|
+
const conclusion = testJob?.conclusion ?? final.conclusion;
|
|
45479
|
+
let tail = [];
|
|
45480
|
+
if (testJob) {
|
|
45481
|
+
try {
|
|
45482
|
+
tail = tailLines(await deps.read(`repos/${FOCUSED_TEST_REPO}/actions/jobs/${testJob.id}/logs`), REMOTE_TEST_LOG_TAIL);
|
|
45483
|
+
} catch {
|
|
45484
|
+
tail = [];
|
|
45485
|
+
}
|
|
45486
|
+
}
|
|
45487
|
+
const url = final.html_url;
|
|
45488
|
+
if (!testJob && !final.conclusion) return uncertain(`remote test: run ${run.id} completed with no verdict \u2014 no \`test\` job and no run conclusion`, run.id, url);
|
|
45489
|
+
return {
|
|
45490
|
+
exitCode: conclusion === "success" ? 0 : 1,
|
|
45491
|
+
title,
|
|
45492
|
+
runId: run.id,
|
|
45493
|
+
url,
|
|
45494
|
+
conclusion,
|
|
45495
|
+
tail,
|
|
45496
|
+
message: `remote test: ${title} ${conclusion ?? "completed"} on ${request.repo}@${request.sha.slice(0, 12)} \u2014 ${url}`
|
|
45497
|
+
};
|
|
45498
|
+
} catch (error) {
|
|
45499
|
+
return uncertain(`remote test: ${error.message} \u2014 the dispatch already happened; inspect ${FOCUSED_TEST_REPO} Actions for "${title}", never re-dispatch blind`, run?.id ?? null, run?.html_url ?? null);
|
|
45500
|
+
}
|
|
45501
|
+
}
|
|
45502
|
+
function appApiRead(writeToken, exec3 = execFileP) {
|
|
45503
|
+
return async (path2) => {
|
|
45504
|
+
const token = await writeToken();
|
|
45505
|
+
const { stdout } = await exec3("gh", ["api", path2], { timeout: 6e4, env: { ...process.env, GH_TOKEN: token, MMI_GH_POOL: "app" } });
|
|
45506
|
+
return stdout;
|
|
45507
|
+
};
|
|
45508
|
+
}
|
|
45509
|
+
var defaultGit2 = {
|
|
45510
|
+
git: async (args) => {
|
|
45511
|
+
try {
|
|
45512
|
+
return (await execFileP("git", args, { timeout: 15e3 })).stdout.trim();
|
|
45513
|
+
} catch {
|
|
45514
|
+
return "";
|
|
45515
|
+
}
|
|
45516
|
+
}
|
|
45517
|
+
};
|
|
45518
|
+
async function originRepo(git3 = defaultGit2) {
|
|
45519
|
+
const url = await git3.git(["config", "--get", "remote.origin.url"]);
|
|
45520
|
+
const match = /[:/]([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?$/.exec(url);
|
|
45521
|
+
if (!match) throw new Error(`remote test: no usable origin remote (git config --get remote.origin.url returned '${url}'); pass --repo <owner>/<repo>`);
|
|
45522
|
+
return `${match[1]}/${match[2]}`;
|
|
45523
|
+
}
|
|
45524
|
+
async function pushedSha(sha, git3 = defaultGit2) {
|
|
45525
|
+
const resolved = sha ?? await git3.git(["rev-parse", "HEAD"]);
|
|
45526
|
+
if (!REMOTE_TEST_SHA_RE.test(resolved)) {
|
|
45527
|
+
throw new Error(`remote test: --sha must be a 40-character lowercase commit sha (received '${resolved}'); in a checkout, omit --sha to use HEAD`);
|
|
45528
|
+
}
|
|
45529
|
+
if (sha === void 0 && !await git3.git(["branch", "-r", "--contains", resolved])) {
|
|
45530
|
+
throw new Error(`remote test: ${resolved.slice(0, 12)} is not on any origin branch \u2014 push first (the workflow checks the commit out of origin, so a local-only HEAD cannot be tested)`);
|
|
45531
|
+
}
|
|
45532
|
+
return resolved;
|
|
45533
|
+
}
|
|
45534
|
+
function registerRemoteTest(run, deps) {
|
|
45535
|
+
const git3 = deps.git ?? defaultGit2;
|
|
45536
|
+
const write = deps.write ?? printLine;
|
|
45537
|
+
run.command("test [files...]").description("run a focused test on the owned Hetzner heavy lanes (`.github/workflows/focused-test.yml`) instead of the local heavy slot; waits for the verdict and prints the test job log tail (exit 0 pass, 1 fail, 3 uncertain)").option("--repo <owner/repo>", "repository to test (default: this checkout `origin`)").option("--sha <sha>", "commit to test (default: HEAD); it must already be pushed to origin").addOption(new Option("--tool <tool>", "node-test | vitest").default("node-test").choices(["node-test", "vitest"])).option("--no-install", "skip `npm ci` in the checked-out repository").option("--json", "machine-readable receipt").action(async (files, options) => {
|
|
45538
|
+
try {
|
|
45539
|
+
const repo = options.repo ?? await originRepo(git3);
|
|
45540
|
+
const sha = await pushedSha(options.sha, git3);
|
|
45541
|
+
const tool = options.tool;
|
|
45542
|
+
const request = { repo, sha, tool, files, install: options.install };
|
|
45543
|
+
const receipt = await runRemoteTest(request, {
|
|
45544
|
+
dispatch: deps.dispatch,
|
|
45545
|
+
read: deps.read,
|
|
45546
|
+
now: deps.now ?? Date.now,
|
|
45547
|
+
sleep: deps.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms))),
|
|
45548
|
+
nonce: deps.nonce ?? (() => randomNonce())
|
|
45549
|
+
});
|
|
45550
|
+
if (options.json) write(JSON.stringify(receipt));
|
|
45551
|
+
else {
|
|
45552
|
+
write(receipt.message);
|
|
45553
|
+
for (const line of receipt.tail) write(line);
|
|
45554
|
+
}
|
|
45555
|
+
process.exitCode = receipt.exitCode;
|
|
45556
|
+
} catch (error) {
|
|
45557
|
+
return fail(error.message);
|
|
45558
|
+
}
|
|
45559
|
+
});
|
|
45560
|
+
}
|
|
45561
|
+
function randomNonce() {
|
|
45562
|
+
return (0, import_node_crypto13.randomBytes)(8).toString("hex");
|
|
45563
|
+
}
|
|
45564
|
+
|
|
45565
|
+
// src/devops-vars.ts
|
|
45566
|
+
var import_node_crypto14 = require("node:crypto");
|
|
45178
45567
|
init_github_client();
|
|
45179
45568
|
init_cli_shared();
|
|
45180
45569
|
var VARS_RECEIPT_SCHEMA = "mmi-cli.devops-vars/1";
|
|
@@ -45265,7 +45654,7 @@ function receiptBase(deps, operation, repo, ctx, now) {
|
|
|
45265
45654
|
repo,
|
|
45266
45655
|
window: { startedAt: ctx.startedAt.toISOString(), endedAt: now().toISOString(), atomicSnapshot: false },
|
|
45267
45656
|
actor: deps.actor ?? { credential: "app_installation" },
|
|
45268
|
-
correlationId: (deps.correlationId ??
|
|
45657
|
+
correlationId: (deps.correlationId ?? import_node_crypto14.randomUUID)(),
|
|
45269
45658
|
upstream: ctx.log
|
|
45270
45659
|
};
|
|
45271
45660
|
}
|
|
@@ -45676,7 +46065,7 @@ function printMutationText(receipt) {
|
|
|
45676
46065
|
}
|
|
45677
46066
|
|
|
45678
46067
|
// src/repo-archive-status.ts
|
|
45679
|
-
var
|
|
46068
|
+
var import_node_crypto15 = require("node:crypto");
|
|
45680
46069
|
init_github_client();
|
|
45681
46070
|
init_cli_shared();
|
|
45682
46071
|
var ARCHIVE_STATUS_RECEIPT_SCHEMA = "mmi-cli.repo-archive-status/1";
|
|
@@ -45717,7 +46106,7 @@ async function readArchiveStatusReceipt(deps, repo) {
|
|
|
45717
46106
|
sourceTime: { observedAt: "", note: ARCHIVE_SOURCE_TIME_NOTE },
|
|
45718
46107
|
window: { startedAt: startedAt.toISOString(), endedAt: "", atomicSnapshot: false },
|
|
45719
46108
|
actor: deps.actor ?? { credential: "app_installation" },
|
|
45720
|
-
correlationId: (deps.correlationId ??
|
|
46109
|
+
correlationId: (deps.correlationId ?? import_node_crypto15.randomUUID)(),
|
|
45721
46110
|
upstream: log
|
|
45722
46111
|
};
|
|
45723
46112
|
try {
|
|
@@ -45770,7 +46159,7 @@ function registerRepoArchiveStatusCommand(program3, deps) {
|
|
|
45770
46159
|
}
|
|
45771
46160
|
|
|
45772
46161
|
// src/merge-queue-lifecycle.ts
|
|
45773
|
-
var
|
|
46162
|
+
var import_node_crypto16 = require("node:crypto");
|
|
45774
46163
|
var import_node_fs35 = require("node:fs");
|
|
45775
46164
|
var import_node_path32 = require("node:path");
|
|
45776
46165
|
init_github_client();
|
|
@@ -46270,7 +46659,7 @@ async function readMergeQueueLifecycle(deps, req) {
|
|
|
46270
46659
|
observation: { started_at: started.toISOString(), ended_at: now().toISOString() },
|
|
46271
46660
|
source: LIFECYCLE_SOURCE,
|
|
46272
46661
|
actor: deps.actor ?? { credential: "app_installation" },
|
|
46273
|
-
correlation_id: (deps.correlationId ??
|
|
46662
|
+
correlation_id: (deps.correlationId ?? import_node_crypto16.randomUUID)(),
|
|
46274
46663
|
bounds: { max_pages: req.maxPages, max_events: req.maxEvents, deadline_seconds: req.deadlineSeconds },
|
|
46275
46664
|
counts: { pages, events: eventCount, duplicates_dropped: states.reduce((a, s) => a + s.duplicates_dropped, 0) },
|
|
46276
46665
|
complete,
|
|
@@ -46296,7 +46685,7 @@ async function readMergeQueueLifecycle(deps, req) {
|
|
|
46296
46685
|
checkpoint: { prs: states }
|
|
46297
46686
|
};
|
|
46298
46687
|
}
|
|
46299
|
-
var sha256 = (text) => (0,
|
|
46688
|
+
var sha256 = (text) => (0, import_node_crypto16.createHash)("sha256").update(text).digest("hex");
|
|
46300
46689
|
function writeLifecycleReceipt(path2, receipt) {
|
|
46301
46690
|
const text = `${JSON.stringify(receipt, null, 2)}
|
|
46302
46691
|
`;
|
|
@@ -47375,7 +47764,7 @@ var import_node_os20 = require("node:os");
|
|
|
47375
47764
|
var import_node_path36 = require("node:path");
|
|
47376
47765
|
|
|
47377
47766
|
// src/box-control.ts
|
|
47378
|
-
var
|
|
47767
|
+
var import_node_crypto17 = require("node:crypto");
|
|
47379
47768
|
var MANIFEST_PATH = "infra/box-control/manifest.json";
|
|
47380
47769
|
var ALLOWED_POST = /* @__PURE__ */ new Set(["systemctl daemon-reload"]);
|
|
47381
47770
|
var sshdPost = (dest) => `if sshd -t; then systemctl reload ssh; else rm -f ${dest}; echo 'sshd -t rejected ${dest}: removed, not reloaded' >&2; exit 1; fi`;
|
|
@@ -47479,7 +47868,7 @@ function laneCoverageAction(files, box, op) {
|
|
|
47479
47868
|
return { notice: `LANE_COVERAGE: ${mismatch}` };
|
|
47480
47869
|
}
|
|
47481
47870
|
function md5(content) {
|
|
47482
|
-
return (0,
|
|
47871
|
+
return (0, import_node_crypto17.createHash)("md5").update(content).digest("hex");
|
|
47483
47872
|
}
|
|
47484
47873
|
function planRemoteScript(dests) {
|
|
47485
47874
|
const lines2 = ["set -u"];
|
|
@@ -49433,6 +49822,7 @@ ${formatSshRecipe(box, remoteCommand)}
|
|
|
49433
49822
|
var import_node_fs38 = require("node:fs");
|
|
49434
49823
|
var import_node_url = require("node:url");
|
|
49435
49824
|
var import_node_path35 = require("node:path");
|
|
49825
|
+
var import_node_net = require("node:net");
|
|
49436
49826
|
var import_meta = {};
|
|
49437
49827
|
var HZ = "https://api.hetzner.cloud/v1";
|
|
49438
49828
|
var APPLY = process.env.FIREWALL_APPLY === "1";
|
|
@@ -49468,7 +49858,23 @@ function buildRules(cloudflare, extras = []) {
|
|
|
49468
49858
|
if (!["tcp", "udp"].includes(e.protocol) || !/^\d{1,5}(-\d{1,5})?$/.test(String(e.port)) || !e.description) {
|
|
49469
49859
|
throw new Error(`invalid firewall extra ${JSON.stringify(e)}: needs protocol tcp|udp, port N or N-M, description`);
|
|
49470
49860
|
}
|
|
49471
|
-
|
|
49861
|
+
if (e.source_ips !== void 0) {
|
|
49862
|
+
const cidrOk = (entry) => {
|
|
49863
|
+
if (typeof entry !== "string" || !entry.includes("/")) return false;
|
|
49864
|
+
const parts = entry.split("/");
|
|
49865
|
+
if (parts.length !== 2) return false;
|
|
49866
|
+
const [ip, prefixRaw] = parts;
|
|
49867
|
+
if (!/^(0|[1-9]\d{0,2})$/.test(prefixRaw)) return false;
|
|
49868
|
+
const prefix = Number(prefixRaw);
|
|
49869
|
+
if ((0, import_node_net.isIPv4)(ip)) return prefix >= 1 && prefix <= 32;
|
|
49870
|
+
if ((0, import_node_net.isIPv6)(ip)) return prefix >= 1 && prefix <= 128;
|
|
49871
|
+
return false;
|
|
49872
|
+
};
|
|
49873
|
+
if (!Array.isArray(e.source_ips) || !e.source_ips.length || !e.source_ips.every(cidrOk)) {
|
|
49874
|
+
throw new Error(`invalid firewall extra ${JSON.stringify(e)}: source_ips, when declared, is a non-empty list of specific CIDRs (IPv4 a.b.c.d/1-32 or IPv6 x::/1-128; prefix 0 is refused \u2014 omit source_ips if the extra is meant to be world-open)`);
|
|
49875
|
+
}
|
|
49876
|
+
}
|
|
49877
|
+
rules.push({ direction: "in", protocol: e.protocol, port: String(e.port), source_ips: e.source_ips ?? ANY, description: e.description.slice(0, 255) });
|
|
49472
49878
|
}
|
|
49473
49879
|
return rules;
|
|
49474
49880
|
}
|
|
@@ -49478,7 +49884,17 @@ async function cloudflareRanges() {
|
|
|
49478
49884
|
if (!res.ok || !body?.success) throw new Error(`Cloudflare IP list unreadable: ${res.status}`);
|
|
49479
49885
|
return { ipv4: body.result.ipv4_cidrs ?? [], ipv6: body.result.ipv6_cidrs ?? [] };
|
|
49480
49886
|
}
|
|
49481
|
-
|
|
49887
|
+
function exactRules(rules) {
|
|
49888
|
+
const sortKeys = (v) => Array.isArray(v) ? v.map(sortKeys) : v && typeof v === "object" ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, sortKeys(v[k])])) : v;
|
|
49889
|
+
const canon = (r) => {
|
|
49890
|
+
const { destination_ips, port, ...rest } = r;
|
|
49891
|
+
const out = { ...rest, source_ips: [...r.source_ips ?? []].sort() };
|
|
49892
|
+
if (port !== null && port !== void 0) out.port = port;
|
|
49893
|
+
if (Array.isArray(destination_ips) && destination_ips.length > 0) out.destination_ips = [...destination_ips].sort();
|
|
49894
|
+
return JSON.stringify(sortKeys(out));
|
|
49895
|
+
};
|
|
49896
|
+
return JSON.stringify((rules ?? []).map(canon).sort());
|
|
49897
|
+
}
|
|
49482
49898
|
function declaredExtras(box) {
|
|
49483
49899
|
const extrasFile = JSON.parse((0, import_node_fs38.readFileSync)((0, import_node_url.fileURLToPath)(new URL("./firewall-extras.json", import_meta.url)), "utf8"));
|
|
49484
49900
|
return Object.prototype.hasOwnProperty.call(extrasFile, box) ? extrasFile[box] : void 0;
|
|
@@ -49514,7 +49930,7 @@ async function main() {
|
|
|
49514
49930
|
if (extras === void 0) throw new Error(`${box} has no entry in firewall-extras.json; declare it (an empty list is fine) before applying`);
|
|
49515
49931
|
const rules = buildRules(await cloudflareRanges(), extras);
|
|
49516
49932
|
for (const r of rules) console.log(` in ${r.protocol}${r.port ? `/${r.port}` : ""} from ${r.source_ips.length > 2 ? `${r.source_ips.length} Cloudflare ranges` : r.source_ips.join(",")} \u2014 ${r.description}`);
|
|
49517
|
-
const rulesChanged = !existing ||
|
|
49933
|
+
const rulesChanged = !existing || exactRules(existing.rules) !== exactRules(rules);
|
|
49518
49934
|
console.log(`cloud-firewall: rules ${rulesChanged ? "would change" : "unchanged"}; apply-to-server ${applied ? "already done" : "needed"}`);
|
|
49519
49935
|
if (!APPLY) {
|
|
49520
49936
|
console.log("cloud-firewall: dry-run (FIREWALL_APPLY=1 to write)");
|
|
@@ -49542,7 +49958,7 @@ if (process.argv[1] && (0, import_node_path35.basename)(process.argv[1]).toLower
|
|
|
49542
49958
|
|
|
49543
49959
|
// ../infra/hetzner/firewall-extras.json
|
|
49544
49960
|
var firewall_extras_default = {
|
|
49545
|
-
$comment: "#7356: per-box inbound exceptions to the Cloudflare-only Hetzner Cloud Firewall (cloud-firewall.mjs). Every entry names its owner and reason. Web traffic (80/443) is always Cloudflare-only and SSH (22) is always open; list only what must reach the box directly.",
|
|
49961
|
+
$comment: "#7356: per-box inbound exceptions to the Cloudflare-only Hetzner Cloud Firewall (cloud-firewall.mjs). Every entry names its owner and reason; an optional source_ips list (CIDRs) restricts the exception to those sources, absent = world-open. Web traffic (80/443) is always Cloudflare-only and SSH (22) is always open; list only what must reach the box directly.",
|
|
49546
49962
|
"mm-work": [
|
|
49547
49963
|
{ protocol: "tcp", port: "7881", description: "MM-Chat dev huddles LiveKit RTC over TCP (MMI-Hub#7353, owner-approved 2026-09-26)" },
|
|
49548
49964
|
{ protocol: "udp", port: "7882", description: "MM-Chat dev huddles LiveKit RTC over UDP (MMI-Hub#7353, owner-approved 2026-09-26)" },
|
|
@@ -49552,7 +49968,9 @@ var firewall_extras_default = {
|
|
|
49552
49968
|
"mm-central": [],
|
|
49553
49969
|
"mm-recorder": [],
|
|
49554
49970
|
"mmi-runner": [],
|
|
49555
|
-
"mmi-runner2": [
|
|
49971
|
+
"mmi-runner2": [
|
|
49972
|
+
{ protocol: "tcp", port: "8888", source_ips: ["92.44.216.204/32"], description: "Scout's SearXNG gate: the nginx gate on 8888 (Authorization bearer header required, 403 without) in front of the persistent /opt/searxng compose (owner GO for SearXNG-on-Hetzner #2008, 6 Nov 2026). Source is the Scout host's consumer IP - dynamic; if it changes, update the CIDR here or fall back to the ssh tunnel; never open the port to the world" }
|
|
49973
|
+
]
|
|
49556
49974
|
};
|
|
49557
49975
|
|
|
49558
49976
|
// ../shared/runner-hosts.ts
|
|
@@ -49606,17 +50024,72 @@ async function runBoxFirewall(boxName, mode, live, deps) {
|
|
|
49606
50024
|
observedAt
|
|
49607
50025
|
};
|
|
49608
50026
|
if (mode === "detach") {
|
|
49609
|
-
|
|
49610
|
-
|
|
50027
|
+
const saved = deps.loadSnapshot(server.id);
|
|
50028
|
+
if (!saved) {
|
|
50029
|
+
if (!existing || !applied) {
|
|
50030
|
+
return { ...target, ok: true, note: `firewall ${firewallName} is ${existing ? "not applied to" : "absent for"} this server \u2014 nothing to restore` };
|
|
50031
|
+
}
|
|
50032
|
+
throw new FirewallRefusal(`box firewall: refusing to detach ${firewallName} from ${runner}: no prior-policy snapshot is saved for this server, and detaching would leave the edge box with NO firewall. Run one --apply first (it saves the rollback snapshot), or if removing the firewall entirely is really the intent, do it deliberately in the Hetzner console \u2014 never through this rollback route.`);
|
|
50033
|
+
}
|
|
50034
|
+
const savedRules = saved.rules ?? [];
|
|
50035
|
+
target.ruleDiff = rulesDiff(existing?.rules ?? [], savedRules);
|
|
50036
|
+
if (!saved.attached) {
|
|
50037
|
+
if (!existing || !applied) {
|
|
50038
|
+
const fresh = await listAllFirewalls(deps, token);
|
|
50039
|
+
const stillAttached = fresh.some((f) => appliedToServer(f, server.id));
|
|
50040
|
+
target.readback = { observedAt: deps.now(), attached: stillAttached, rulesMatch: null };
|
|
50041
|
+
target.ok = !stillAttached;
|
|
50042
|
+
if (target.ok) target.note = `restored the saved prior policy (saved ${saved.savedAt}): no firewall attached`;
|
|
50043
|
+
if (target.ok) deps.saveSnapshot(server.id, { savedAt: deps.now(), firewall: existing ? { id: existing.id, name: existing.name } : null, rules: existing?.rules ?? [], attached: false });
|
|
50044
|
+
return target;
|
|
50045
|
+
}
|
|
50046
|
+
const action = await actionPost(deps, token, existing.id, "remove_from_resources", { remove_from: [{ type: "server", server: { id: server.id } }] });
|
|
50047
|
+
const waited = await waitForAction(deps, token, action);
|
|
50048
|
+
target.actions.push(actionReceipt("remove_from_resources", waited));
|
|
50049
|
+
target.ambiguous ||= waited.ambiguous;
|
|
50050
|
+
const readback3 = await readbackState(deps, token, existing.id, server.id, null, deps.now());
|
|
50051
|
+
target.readback = readback3;
|
|
50052
|
+
target.ok = !target.ambiguous && readback3.attached === false;
|
|
50053
|
+
if (target.ok) target.note = `restored the saved prior policy (saved ${saved.savedAt}): no firewall was attached`;
|
|
50054
|
+
if (target.ok) deps.saveSnapshot(server.id, { savedAt: deps.now(), firewall: { id: existing.id, name: existing.name }, rules: existing.rules ?? [], attached: false });
|
|
50055
|
+
return target;
|
|
49611
50056
|
}
|
|
49612
|
-
|
|
49613
|
-
|
|
49614
|
-
|
|
49615
|
-
|
|
49616
|
-
|
|
50057
|
+
let firewallId2 = existing?.id;
|
|
50058
|
+
if (!existing) {
|
|
50059
|
+
const created = await deps.hetzner(token, "/firewalls", {
|
|
50060
|
+
method: "POST",
|
|
50061
|
+
body: JSON.stringify({ name: firewallName, labels: { managed_by: "mmi-hub", box: runner }, rules: savedRules })
|
|
50062
|
+
});
|
|
50063
|
+
firewallId2 = created.firewall.id;
|
|
50064
|
+
target.actions.push({ id: firewallId2, kind: "create", status: "created" });
|
|
50065
|
+
} else if (exactRules(existing.rules ?? []) !== exactRules(savedRules)) {
|
|
50066
|
+
const action = await actionPost(deps, token, firewallId2, "set_rules", { rules: savedRules });
|
|
50067
|
+
const waited = await waitForAction(deps, token, action);
|
|
50068
|
+
target.actions.push(actionReceipt("set_rules", waited));
|
|
50069
|
+
target.ambiguous ||= waited.ambiguous;
|
|
50070
|
+
if (waited.ambiguous || waited.action.status === "error") {
|
|
50071
|
+
target.readback = await readbackState(deps, token, firewallId2, server.id, savedRules, deps.now());
|
|
50072
|
+
return target;
|
|
50073
|
+
}
|
|
50074
|
+
}
|
|
50075
|
+
if (!applied) {
|
|
50076
|
+
const action = await actionPost(deps, token, firewallId2, "apply_to_resources", { apply_to: [{ type: "server", server: { id: server.id } }] });
|
|
50077
|
+
const waited = await waitForAction(deps, token, action);
|
|
50078
|
+
target.actions.push(actionReceipt("apply_to_resources", waited));
|
|
50079
|
+
target.ambiguous ||= waited.ambiguous;
|
|
50080
|
+
if (waited.ambiguous || waited.action.status === "error") {
|
|
50081
|
+
target.readback = await readbackState(deps, token, firewallId2, server.id, savedRules, deps.now());
|
|
50082
|
+
return target;
|
|
50083
|
+
}
|
|
50084
|
+
}
|
|
50085
|
+
const readback2 = await readbackState(deps, token, firewallId2, server.id, savedRules, deps.now());
|
|
49617
50086
|
target.readback = readback2;
|
|
49618
|
-
target.
|
|
49619
|
-
|
|
50087
|
+
target.firewall = { id: firewallId2, name: firewallName };
|
|
50088
|
+
target.ok = !target.ambiguous && readback2.attached && readback2.rulesMatch === true;
|
|
50089
|
+
if (target.ok) {
|
|
50090
|
+
target.note = `restored the saved prior policy (saved ${saved.savedAt}), verified by independent readback`;
|
|
50091
|
+
deps.saveSnapshot(server.id, { savedAt: deps.now(), firewall: { id: firewallId2, name: firewallName }, rules: savedRules, attached: true });
|
|
50092
|
+
}
|
|
49620
50093
|
return target;
|
|
49621
50094
|
}
|
|
49622
50095
|
const extras = deps.declaredExtras(runner);
|
|
@@ -49625,13 +50098,18 @@ async function runBoxFirewall(boxName, mode, live, deps) {
|
|
|
49625
50098
|
}
|
|
49626
50099
|
const planned = buildRules(await deps.cloudflareRanges(), extras);
|
|
49627
50100
|
target.plannedRules = planned;
|
|
49628
|
-
const rulesChanged = !existing ||
|
|
50101
|
+
const rulesChanged = !existing || exactRules(existing.rules ?? []) !== exactRules(planned);
|
|
49629
50102
|
target.rulesChanged = rulesChanged;
|
|
50103
|
+
target.ruleDiff = rulesDiff(existing?.rules ?? [], planned);
|
|
49630
50104
|
if (mode === "plan") {
|
|
49631
50105
|
target.ok = !rulesChanged && applied;
|
|
49632
50106
|
if (target.ok) target.note = `firewall ${firewallName}#${existing.id} matches the reviewed policy and is applied`;
|
|
49633
50107
|
return target;
|
|
49634
50108
|
}
|
|
50109
|
+
if (!deps.loadSnapshot(server.id)) {
|
|
50110
|
+
deps.saveSnapshot(server.id, { savedAt: deps.now(), firewall: existing ? { id: existing.id, name: existing.name } : null, rules: existing?.rules ?? [], attached: applied });
|
|
50111
|
+
target.note = "saved the live policy as the rollback snapshot";
|
|
50112
|
+
}
|
|
49635
50113
|
let firewallId = existing?.id;
|
|
49636
50114
|
if (!existing) {
|
|
49637
50115
|
const created = await deps.hetzner(token, "/firewalls", {
|
|
@@ -49666,9 +50144,21 @@ async function runBoxFirewall(boxName, mode, live, deps) {
|
|
|
49666
50144
|
target.firewall = { id: firewallId, name: firewallName };
|
|
49667
50145
|
target.readback = readback;
|
|
49668
50146
|
target.ok = !target.ambiguous && readback.attached && readback.rulesMatch === true;
|
|
49669
|
-
if (target.ok) target.note = `firewall ${firewallName}#${firewallId} applied to ${runner} and read back independently`;
|
|
50147
|
+
if (target.ok) target.note = `firewall ${firewallName}#${firewallId} applied to ${runner} and read back independently${target.note ? `; ${target.note}` : ""}`;
|
|
49670
50148
|
return target;
|
|
49671
50149
|
}
|
|
50150
|
+
async function listAllFirewalls(deps, token) {
|
|
50151
|
+
const out = [];
|
|
50152
|
+
for (let page = 1; page <= 50; page += 1) {
|
|
50153
|
+
const res = await deps.hetzner(token, `/firewalls?page=${page}`);
|
|
50154
|
+
out.push(...res.firewalls ?? []);
|
|
50155
|
+
if (!res.meta?.pagination?.next_page) return out;
|
|
50156
|
+
}
|
|
50157
|
+
throw new FirewallRefusal("box firewall: the project has more firewalls than the listing bound \u2014 cannot prove that no firewall is applied to this server");
|
|
50158
|
+
}
|
|
50159
|
+
function appliedToServer(f, serverId) {
|
|
50160
|
+
return Boolean(f.applied_to?.some((a) => a.server?.id === serverId || a.applied_to_resources?.some((r) => r.server?.id === serverId)));
|
|
50161
|
+
}
|
|
49672
50162
|
async function actionPost(deps, token, firewallId, kind, body) {
|
|
49673
50163
|
const res = await deps.hetzner(token, `/firewalls/${firewallId}/actions/${kind}`, { method: "POST", body: JSON.stringify(body) });
|
|
49674
50164
|
return res.action;
|
|
@@ -49694,10 +50184,20 @@ function actionReceipt(kind, waited) {
|
|
|
49694
50184
|
async function readbackState(deps, token, firewallId, serverId, planned, at) {
|
|
49695
50185
|
const firewall = (await deps.hetzner(token, `/firewalls/${firewallId}`)).firewall;
|
|
49696
50186
|
const server = (await deps.hetzner(token, `/servers/${serverId}`)).server;
|
|
49697
|
-
const attached = server != null && server.id === serverId &&
|
|
49698
|
-
const rulesMatch = planned ?
|
|
50187
|
+
const attached = server != null && server.id === serverId && appliedToServer(firewall, serverId);
|
|
50188
|
+
const rulesMatch = planned ? exactRules(firewall?.rules ?? []) === exactRules(planned) : null;
|
|
49699
50189
|
return { observedAt: at, attached, rulesMatch };
|
|
49700
50190
|
}
|
|
50191
|
+
function rulesDiff(current, target) {
|
|
50192
|
+
const key = (r) => exactRules([r]);
|
|
50193
|
+
const cur = Array.isArray(current) ? current : [];
|
|
50194
|
+
const targetKeys = new Set(target.map(key));
|
|
50195
|
+
const currentKeys = new Set(cur.map(key));
|
|
50196
|
+
return {
|
|
50197
|
+
added: target.filter((r) => !currentKeys.has(key(r))),
|
|
50198
|
+
removed: cur.filter((r) => !targetKeys.has(key(r)))
|
|
50199
|
+
};
|
|
50200
|
+
}
|
|
49701
50201
|
|
|
49702
50202
|
// src/box-commands.ts
|
|
49703
50203
|
var HETZNER_API = "https://api.hetzner.cloud/v1/servers";
|
|
@@ -49920,7 +50420,7 @@ ${SSH_RECIPE_AGENT_NOTE}`);
|
|
|
49920
50420
|
process.exitCode = 2;
|
|
49921
50421
|
}
|
|
49922
50422
|
});
|
|
49923
|
-
box.command("firewall <box>").description("the registered runner boxes' Hetzner Cloud Firewall (#9786): --plan (default, read-only) reports the exact current rules and attachment against the reviewed policy; --apply writes it; --detach --apply is the rollback").option("--plan", "read-only: current firewall, attachment and exact rules, and the diff against the reviewed policy (the default)").option("--apply", "create/update the approved firewall (policy #7359, SSH stays world-open) and attach it to exactly this server; waits for each provider action, then re-reads the firewall and attachment independently").option("--detach", "with --apply:
|
|
50423
|
+
box.command("firewall <box>").description("the registered runner boxes' Hetzner Cloud Firewall (#9786): --plan (default, read-only) reports the exact current rules and attachment against the reviewed policy; --apply writes it; --detach --apply is the rollback").option("--plan", "read-only: current firewall, attachment and exact rules, and the diff against the reviewed policy (the default)").option("--apply", "create/update the approved firewall (policy #7359, SSH stays world-open) and attach it to exactly this server; waits for each provider action, then re-reads the firewall and attachment independently").option("--detach", "with --apply: restore this server's saved prior policy (the snapshot persisted before the first managed apply) \u2014 never a blind detach; refuses when no snapshot exists, because detaching would leave the edge box with no firewall").option("--json", "machine-readable receipt").action(async (name, o) => {
|
|
49924
50424
|
try {
|
|
49925
50425
|
if (o.apply && o.plan) throw new UsageError("box firewall: --plan and --apply are exclusive");
|
|
49926
50426
|
if (o.detach && !o.apply) throw new UsageError("box firewall: --detach is a mutation \u2014 it needs --apply");
|
|
@@ -49969,6 +50469,12 @@ function renderFirewallReceipt(r, json) {
|
|
|
49969
50469
|
lines2.push(` in ${rule.protocol}${rule.port ? `/${rule.port}` : ""} from ${from} \u2014 ${rule.description}`);
|
|
49970
50470
|
}
|
|
49971
50471
|
}
|
|
50472
|
+
if (r.ruleDiff && (r.ruleDiff.added.length > 0 || r.ruleDiff.removed.length > 0)) {
|
|
50473
|
+
lines2.push(` diff vs the provider's current set: ${r.ruleDiff.added.length} added, ${r.ruleDiff.removed.length} removed`);
|
|
50474
|
+
const render = (r2) => `in ${r2.protocol}${r2.port ? `/${r2.port}` : ""} from ${(r2.source_ips ?? []).join(", ")} \u2014 ${r2.description ?? ""}`;
|
|
50475
|
+
for (const rule of r.ruleDiff.added) lines2.push(` + ${render(rule)}`);
|
|
50476
|
+
for (const rule of r.ruleDiff.removed) lines2.push(` - ${render(rule)}`);
|
|
50477
|
+
}
|
|
49972
50478
|
for (const a of r.actions) lines2.push(` action ${a.kind} #${a.id}: ${a.status}${a.error ? ` \u2014 ${a.error.code ?? ""} ${a.error.message ?? ""}` : ""}`);
|
|
49973
50479
|
if (r.readback) {
|
|
49974
50480
|
lines2.push(` readback (independent, ${r.readback.observedAt}): attached=${r.readback.attached}${r.readback.rulesMatch === null ? "" : ` rulesMatch=${r.readback.rulesMatch}`}`);
|
|
@@ -49978,10 +50484,25 @@ function renderFirewallReceipt(r, json) {
|
|
|
49978
50484
|
console.log(lines2.join("\n"));
|
|
49979
50485
|
}
|
|
49980
50486
|
function firewallDeps() {
|
|
50487
|
+
const snapshotDir = (0, import_node_path36.join)((0, import_node_os20.homedir)(), ".mmi-cli", "firewall-rollback");
|
|
50488
|
+
const snapshotPath = (serverId) => (0, import_node_path36.join)(snapshotDir, `${serverId}.json`);
|
|
49981
50489
|
return {
|
|
49982
50490
|
hetzner: (token, path2, init) => hetzner(token, path2, init),
|
|
49983
50491
|
cloudflareRanges: () => cloudflareRanges(),
|
|
49984
50492
|
declaredExtras: (boxName) => declaredExtrasFromBundle(boxName),
|
|
50493
|
+
loadSnapshot: (serverId) => {
|
|
50494
|
+
const file = snapshotPath(serverId);
|
|
50495
|
+
if (!(0, import_node_fs39.existsSync)(file)) return null;
|
|
50496
|
+
const parsed = JSON.parse((0, import_node_fs39.readFileSync)(file, "utf8"));
|
|
50497
|
+
if (typeof parsed.savedAt !== "string" || !Array.isArray(parsed.rules) || typeof parsed.attached !== "boolean") {
|
|
50498
|
+
throw new Error(`box firewall: the rollback snapshot at ${file} is not a valid snapshot \u2014 inspect it before any rollback`);
|
|
50499
|
+
}
|
|
50500
|
+
return parsed;
|
|
50501
|
+
},
|
|
50502
|
+
saveSnapshot: (serverId, snapshot) => {
|
|
50503
|
+
(0, import_node_fs39.mkdirSync)(snapshotDir, { recursive: true });
|
|
50504
|
+
(0, import_node_fs39.writeFileSync)(snapshotPath(serverId), JSON.stringify(snapshot, null, 2));
|
|
50505
|
+
},
|
|
49985
50506
|
fetchToken: async () => {
|
|
49986
50507
|
let token = null;
|
|
49987
50508
|
await withSecrets(async (secretsDeps) => {
|
|
@@ -50147,7 +50668,7 @@ function renderDeployPortDoctor(report) {
|
|
|
50147
50668
|
}
|
|
50148
50669
|
|
|
50149
50670
|
// src/bootstrap-drift.ts
|
|
50150
|
-
var
|
|
50671
|
+
var import_node_crypto18 = require("node:crypto");
|
|
50151
50672
|
function byteComparableSeeds(manifest, cls) {
|
|
50152
50673
|
return manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self" && s.classes.includes(cls));
|
|
50153
50674
|
}
|
|
@@ -50167,7 +50688,7 @@ function compareSeedBytes(hubContent, repoContent) {
|
|
|
50167
50688
|
return normalize(hubContent) === normalize(repoContent) ? "match" : "drift";
|
|
50168
50689
|
}
|
|
50169
50690
|
function seedContentHash(content) {
|
|
50170
|
-
return (0,
|
|
50691
|
+
return (0, import_node_crypto18.createHash)("sha256").update(content.replace(/\r\n/g, "\n"), "utf8").digest("hex");
|
|
50171
50692
|
}
|
|
50172
50693
|
function auditRepoSeedDrift(repo, seeds, hubContents, repoReads) {
|
|
50173
50694
|
const byTarget = new Map(repoReads.map((r) => [r.target, r.content]));
|
|
@@ -51417,7 +51938,7 @@ function renderBootstrapVerifyReport(report) {
|
|
|
51417
51938
|
}
|
|
51418
51939
|
|
|
51419
51940
|
// src/bootstrap-seed-delivery.ts
|
|
51420
|
-
var
|
|
51941
|
+
var import_node_crypto19 = require("node:crypto");
|
|
51421
51942
|
init_github_client();
|
|
51422
51943
|
var SHA = /^[a-f0-9]{40}$/;
|
|
51423
51944
|
async function readSeedFile(repo, target, ref, client) {
|
|
@@ -51450,7 +51971,7 @@ async function targetedSeedPlan(repo, slug, target, baseBranch, sourceSha, clien
|
|
|
51450
51971
|
}
|
|
51451
51972
|
const baseSha = base?.object?.sha;
|
|
51452
51973
|
if (!baseSha || !SHA.test(baseSha) || !SHA.test(sourceSha)) throw new Error("bootstrap apply: seed identity is unreadable");
|
|
51453
|
-
const identity = (0,
|
|
51974
|
+
const identity = (0, import_node_crypto19.createHash)("sha256").update(JSON.stringify([repo.toLowerCase(), target, baseSha, sourceSha])).digest("hex").slice(0, 24);
|
|
51454
51975
|
const delivery = planSeedDelivery(rules, slug, baseBranch, `bootstrap-seed-${identity}`);
|
|
51455
51976
|
const scoped = forcePr && delivery.mode === "direct" ? { mode: "pr", ref: `bootstrap-seed-${identity}-${slug}`, branch: `bootstrap-seed-${identity}-${slug}`, reason: "registry gate render requires review" } : delivery;
|
|
51456
51977
|
return { ...scoped, baseSha, baseBranch };
|
|
@@ -51824,7 +52345,7 @@ function registerBootstrapCommands(program3) {
|
|
|
51824
52345
|
const cls = classOf(repo);
|
|
51825
52346
|
const meta = metaOf(repo);
|
|
51826
52347
|
const track = resolveReleaseTrack(metaWithDeclaredClass(meta, cls), void 0, repo);
|
|
51827
|
-
const seeds = byteComparableSeeds(manifest, cls).filter((s) => seedMatchesReleaseTrack(s, track));
|
|
52348
|
+
const seeds = byteComparableSeeds(manifest, cls).filter((s) => seedMatchesReleaseTrack(s, track) && seedMatchesRepo(s, repo));
|
|
51828
52349
|
const trackHubContents = new Map([...hubContents].map(([t, c]) => [t, c == null ? c : renderSelfSeedForTrack(c, track)]));
|
|
51829
52350
|
seedsPerRepo = Math.max(seedsPerRepo, seeds.length);
|
|
51830
52351
|
const baseBranch = cls === "content" ? "main" : "development";
|
|
@@ -52267,6 +52788,7 @@ function registerBootstrapCommands(program3) {
|
|
|
52267
52788
|
if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
|
|
52268
52789
|
if (!seedMatchesProjectType(seed, applyProjectType)) continue;
|
|
52269
52790
|
if (!seedMatchesReleaseTrack(seed, vars.RELEASE_TRACK)) continue;
|
|
52791
|
+
if (!seedMatchesRepo(seed, repo)) continue;
|
|
52270
52792
|
const waiver = seed.waivers?.[slug];
|
|
52271
52793
|
if (waiver) {
|
|
52272
52794
|
actions.push({ action: "skip", target: seed.target, ownership: seed.ownership, reason: `waived: ${waiver}` });
|
|
@@ -52766,7 +53288,7 @@ LIVE apply to ${repo}:
|
|
|
52766
53288
|
void 0,
|
|
52767
53289
|
repo
|
|
52768
53290
|
);
|
|
52769
|
-
const repos = inClassRepos.filter((repo) => seedMatchesReleaseTrack(seed, trackOf(repo))).map((repo) => {
|
|
53291
|
+
const repos = inClassRepos.filter((repo) => seedMatchesReleaseTrack(seed, trackOf(repo)) && seedMatchesRepo(seed, repo)).map((repo) => {
|
|
52770
53292
|
const slug = repo.split("/").pop().toLowerCase();
|
|
52771
53293
|
const waiver = seed.waivers?.[slug];
|
|
52772
53294
|
return { repo, slug, waiver };
|
|
@@ -53271,7 +53793,7 @@ function registerDeployCommands(program3) {
|
|
|
53271
53793
|
}
|
|
53272
53794
|
|
|
53273
53795
|
// src/intercom-guest.ts
|
|
53274
|
-
var
|
|
53796
|
+
var import_node_crypto20 = require("node:crypto");
|
|
53275
53797
|
var import_promises5 = require("node:fs/promises");
|
|
53276
53798
|
var import_node_os22 = require("node:os");
|
|
53277
53799
|
var import_node_path38 = require("node:path");
|
|
@@ -53522,7 +54044,7 @@ async function runIntercomGuest(request, deps = {}) {
|
|
|
53522
54044
|
};
|
|
53523
54045
|
}
|
|
53524
54046
|
const fetchImpl = deps.fetchImpl ?? ((url, init) => fetch(url, init));
|
|
53525
|
-
const newKey = deps.newIdempotencyKey ?? (() => (0,
|
|
54047
|
+
const newKey = deps.newIdempotencyKey ?? (() => (0, import_node_crypto20.randomUUID)());
|
|
53526
54048
|
const secrets = [token, credential].filter((secret) => Boolean(secret));
|
|
53527
54049
|
const callOne = async (op, body2) => {
|
|
53528
54050
|
const headers = {
|
|
@@ -53648,7 +54170,7 @@ init_github_client();
|
|
|
53648
54170
|
init_cli_shared();
|
|
53649
54171
|
|
|
53650
54172
|
// src/suite-status.ts
|
|
53651
|
-
var
|
|
54173
|
+
var import_node_crypto21 = require("node:crypto");
|
|
53652
54174
|
var SUITE_REPO = "mutmutco/MM-Work";
|
|
53653
54175
|
var REPOS = {
|
|
53654
54176
|
"mmi-scout": "mutmutco/MMI-Scout",
|
|
@@ -53676,7 +54198,7 @@ function parseSuiteMapping(body) {
|
|
|
53676
54198
|
return void 0;
|
|
53677
54199
|
}
|
|
53678
54200
|
}
|
|
53679
|
-
var sha2562 = (b) => (0,
|
|
54201
|
+
var sha2562 = (b) => (0, import_node_crypto21.createHash)("sha256").update(b).digest("hex");
|
|
53680
54202
|
async function proveBundle(url, tag, fetchUrl) {
|
|
53681
54203
|
const get = async (u) => {
|
|
53682
54204
|
try {
|
|
@@ -53935,7 +54457,7 @@ async function waitSuiteDev(deps, opts) {
|
|
|
53935
54457
|
// src/suite-train.ts
|
|
53936
54458
|
var import_node_fs41 = require("node:fs");
|
|
53937
54459
|
var import_node_path39 = require("node:path");
|
|
53938
|
-
var
|
|
54460
|
+
var import_node_crypto22 = require("node:crypto");
|
|
53939
54461
|
|
|
53940
54462
|
// src/kit-version.ts
|
|
53941
54463
|
var parse = (v) => /^(\d+)\.(\d+)\.(\d+)(-.+)?$/.exec(typeof v === "string" ? v.trim() : "");
|
|
@@ -54037,7 +54559,7 @@ function readSuiteCut(path2) {
|
|
|
54037
54559
|
}
|
|
54038
54560
|
function writeSuiteCut(path2, record2) {
|
|
54039
54561
|
(0, import_node_fs41.mkdirSync)((0, import_node_path39.dirname)(path2), { recursive: true });
|
|
54040
|
-
const temp = `${path2}.${(0,
|
|
54562
|
+
const temp = `${path2}.${(0, import_node_crypto22.randomBytes)(8).toString("hex")}.tmp`;
|
|
54041
54563
|
try {
|
|
54042
54564
|
(0, import_node_fs41.writeFileSync)(temp, JSON.stringify(record2, null, 2), { flag: "wx" });
|
|
54043
54565
|
(0, import_node_fs41.renameSync)(temp, path2);
|
|
@@ -57138,7 +57660,7 @@ async function deleteMergedRemoteBranch(options) {
|
|
|
57138
57660
|
init_github_client();
|
|
57139
57661
|
|
|
57140
57662
|
// src/release-custody.ts
|
|
57141
|
-
var
|
|
57663
|
+
var import_node_crypto23 = require("node:crypto");
|
|
57142
57664
|
var import_node_fs43 = require("node:fs");
|
|
57143
57665
|
var import_node_path41 = require("node:path");
|
|
57144
57666
|
init_github_client();
|
|
@@ -57362,7 +57884,7 @@ var ReleaseCustodyError = class extends Error {
|
|
|
57362
57884
|
this.name = "ReleaseCustodyError";
|
|
57363
57885
|
}
|
|
57364
57886
|
};
|
|
57365
|
-
var custodyDigest = (value) => (0,
|
|
57887
|
+
var custodyDigest = (value) => (0, import_node_crypto23.createHash)("sha256").update(value).digest("hex");
|
|
57366
57888
|
function parseCustody(raw, repo) {
|
|
57367
57889
|
const s = JSON.parse(raw);
|
|
57368
57890
|
if (s.schema !== "mmi-release-custody/v1" || s.repo !== repo || !s.precutId || !Number.isSafeInteger(s.revision) || s.revision < 1 || !s.controller || !Number.isSafeInteger(s.controller.runId) || s.controller.runId <= 0 || !Number.isSafeInteger(s.controller.runAttempt) || s.controller.runAttempt <= 0 || !s.controller.token || !(s.ledger === null || typeof s.ledger === "string") || !s.archives || !s.effects) {
|
|
@@ -57463,7 +57985,7 @@ var ReleaseCustodySession = class _ReleaseCustodySession {
|
|
|
57463
57985
|
}
|
|
57464
57986
|
}
|
|
57465
57987
|
let state = {
|
|
57466
|
-
...found.state ?? { schema: "mmi-release-custody/v1", repo, precutId: (0,
|
|
57988
|
+
...found.state ?? { schema: "mmi-release-custody/v1", repo, precutId: (0, import_node_crypto23.randomUUID)(), effects: {}, ...await initialize() },
|
|
57467
57989
|
revision: (found.state?.revision ?? 0) + 1,
|
|
57468
57990
|
controller
|
|
57469
57991
|
};
|
|
@@ -57524,7 +58046,7 @@ var ReleaseCustodySession = class _ReleaseCustodySession {
|
|
|
57524
58046
|
proof: { kind: "ledger-lifecycle", controller: state.controller }
|
|
57525
58047
|
};
|
|
57526
58048
|
}
|
|
57527
|
-
return { ...state, ledger: raw, archives, precutId: state.ledger && !raw ? (0,
|
|
58049
|
+
return { ...state, ledger: raw, archives, precutId: state.ledger && !raw ? (0, import_node_crypto23.randomUUID)() : state.precutId };
|
|
57528
58050
|
});
|
|
57529
58051
|
}
|
|
57530
58052
|
async preserveLocal(raw) {
|
|
@@ -57758,7 +58280,7 @@ function withHubReleaseCustody(deps) {
|
|
|
57758
58280
|
if (!authority.ok || !authority.train || authority.authenticatedDelegation !== true) {
|
|
57759
58281
|
throw new ReleaseCustodyError("repository custody requires the server-verified owner-triggered train session");
|
|
57760
58282
|
}
|
|
57761
|
-
const controller = { ...runIdentity, token: (0,
|
|
58283
|
+
const controller = { ...runIdentity, token: (0, import_node_crypto23.randomUUID)(), ...process.env.GITHUB_JOB ? { job: process.env.GITHUB_JOB } : {} };
|
|
57762
58284
|
const raw = localRaw(path2);
|
|
57763
58285
|
const requiredContexts = () => discoverRequiredCheckContexts(
|
|
57764
58286
|
deps,
|
|
@@ -58470,7 +58992,7 @@ init_github_quota();
|
|
|
58470
58992
|
|
|
58471
58993
|
// src/issue-commands.ts
|
|
58472
58994
|
var import_node_fs46 = require("node:fs");
|
|
58473
|
-
var
|
|
58995
|
+
var import_node_crypto24 = require("node:crypto");
|
|
58474
58996
|
init_cli_shared();
|
|
58475
58997
|
init_clean_exit();
|
|
58476
58998
|
init_error_codes();
|
|
@@ -58947,7 +59469,7 @@ function rowIdempotencyKey(batchKey, spec) {
|
|
|
58947
59469
|
const identity = `${spec.type}
|
|
58948
59470
|
${spec.title.trim()}
|
|
58949
59471
|
${spec.body ?? ""}`;
|
|
58950
|
-
const hash = (0,
|
|
59472
|
+
const hash = (0, import_node_crypto24.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
|
|
58951
59473
|
return `${batchKey}:${hash}`;
|
|
58952
59474
|
}
|
|
58953
59475
|
var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
|
|
@@ -59731,7 +60253,7 @@ function typedReadHint(path2) {
|
|
|
59731
60253
|
}
|
|
59732
60254
|
if (!item) return say(`mmi-cli devops read pr list --repo ${slug} --json number,title,state,headRefName`);
|
|
59733
60255
|
if (item.startsWith("comments/")) return say(`mmi-cli devops read pr view <pr> --repo ${slug} --json comments,reviews`);
|
|
59734
|
-
return say(`mmi-cli devops read pr view ${item} --repo ${slug} --json title,state,comments,reviews,commits,statusCheckRollup,mergedAt,mergeCommit,headRefOid,mergeable,mergeStateStatus`, "; `mergedAt`, `mergeCommit` and `headRefOid` carry the merge and head evidence, `mergeable` and `mergeStateStatus` carry mergeability,
|
|
60256
|
+
return say(`mmi-cli devops read pr view ${item} --repo ${slug} --json title,body,state,comments,reviews,commits,statusCheckRollup,mergedAt,mergeCommit,headRefOid,mergeable,mergeStateStatus`, "; `mergedAt`, `mergeCommit` and `headRefOid` carry the merge and head evidence, `mergeable` and `mergeStateStatus` carry mergeability, `--json` is a field list callers may narrow or extend, and `--jq <expr>` filters that JSON locally (#10154)");
|
|
59735
60257
|
}
|
|
59736
60258
|
function runsReadHint(path2) {
|
|
59737
60259
|
const q = path2.indexOf("?");
|
|
@@ -59875,7 +60397,8 @@ function registerApiReadCommands(program3, overrides) {
|
|
|
59875
60397
|
const shapes = "api get: --out <file> is required for, and only valid with, repos/{owner}/{repo}/actions/artifacts/{id}/zip and repos/{owner}/{repo}/actions/jobs/{id}/logs and repos/{owner}/{repo}/releases/assets/{id}";
|
|
59876
60398
|
if (!zip && o.out !== void 0) {
|
|
59877
60399
|
if (req.kind === "contents") return fail(`${shapes} \u2014 for this file's bytes, drop --out and use \`--accept raw\` with a redirect: \`mmi-cli devops api get "${req.path}" --accept raw > <file>\` (decoded bytes on stdout, no base64 or JSON.parse). This refusal exits 1 on stderr with nothing written: check the exit code and never discard stderr (2>&1 / >/dev/null) before parsing.`);
|
|
59878
|
-
|
|
60400
|
+
const typed = typedReadHint(req.path);
|
|
60401
|
+
return fail(`${shapes} \u2014 drop --out and let the JSON body go to stdout (pipe it into node/jq, or use --jq <expr>)${typed ? `.${typed}` : ", or use the typed read (e.g. `mmi-cli devops read run list --repo <owner/repo> --json databaseId,workflowName,event,status,conclusion`)."} This refusal exits 1 on stderr with nothing written: check the exit code, never discard stderr (2>&1 / >/dev/null) before parsing, and never chain a command after it (a trailing \`; ls <file>\` replaces exit 1 with its own code and hides this reason).`);
|
|
59879
60402
|
}
|
|
59880
60403
|
return fail(`${shapes} \u2014 append \`--out <file>\` to this command (e.g. \`MMI_ACTOR=app mmi-cli devops api get ${req.path} --out .jerv/tmp/job.log\`), or for job logs use the typed read \`MMI_ACTOR=app mmi-cli devops read run log <runId> --repo <owner/repo> --job <name|id>\`. Shell redirection (\`>\` / \`2>&1\`) is not a substitute: this refusal exits 1 on stderr with nothing written to stdout, so \`>\` leaves a zero-byte file \u2014 delete it and check the exit code.`);
|
|
59881
60404
|
}
|
|
@@ -59890,7 +60413,20 @@ function registerApiReadCommands(program3, overrides) {
|
|
|
59890
60413
|
o.accept
|
|
59891
60414
|
);
|
|
59892
60415
|
if (req.workflowRuns) body = sliceWorkflowRunsPage(body, req.workflowRuns);
|
|
59893
|
-
if (o.jq !== void 0)
|
|
60416
|
+
if (o.jq !== void 0) {
|
|
60417
|
+
const upstream = body;
|
|
60418
|
+
body = await jqRun(o.jq, body, localJqEnv(), ...bindings.length ? [bindings] : []);
|
|
60419
|
+
if (body.trim() === "null") {
|
|
60420
|
+
let keys;
|
|
60421
|
+
try {
|
|
60422
|
+
const parsed = JSON.parse(upstream);
|
|
60423
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) keys = Object.keys(parsed);
|
|
60424
|
+
} catch {
|
|
60425
|
+
}
|
|
60426
|
+
if (keys) process.stderr.write(`api get: --jq '${o.jq}' returned null. stdout is the raw upstream JSON with no body/--json wrapper; its top-level keys are: ${keys.join(", ") || "(none)"}.
|
|
60427
|
+
`);
|
|
60428
|
+
}
|
|
60429
|
+
}
|
|
59894
60430
|
if (zip) {
|
|
59895
60431
|
(0, import_node_fs48.writeFileSync)(o.out, body, "latin1");
|
|
59896
60432
|
return;
|
|
@@ -60932,7 +61468,7 @@ function parseCheckJobId(details) {
|
|
|
60932
61468
|
function failingCheckRows(rows) {
|
|
60933
61469
|
return rows.filter((row) => row.status === "fail" || row.status === "failure");
|
|
60934
61470
|
}
|
|
60935
|
-
function
|
|
61471
|
+
function tailLines2(text, max = CHECK_LOG_TAIL_LINES) {
|
|
60936
61472
|
const lines2 = text.split(/\r?\n/).filter((line) => line.trim());
|
|
60937
61473
|
return { tail: lines2.slice(-max).join("\n"), truncated: lines2.length > max };
|
|
60938
61474
|
}
|
|
@@ -60941,7 +61477,7 @@ async function attachFailureLogs(rows, fetchLog) {
|
|
|
60941
61477
|
const jobId = parseCheckJobId(row.details);
|
|
60942
61478
|
if (!jobId) continue;
|
|
60943
61479
|
try {
|
|
60944
|
-
const { tail, truncated } =
|
|
61480
|
+
const { tail, truncated } = tailLines2(await fetchLog(jobId));
|
|
60945
61481
|
if (tail) row.log = { jobId, tail, truncated };
|
|
60946
61482
|
} catch {
|
|
60947
61483
|
}
|
|
@@ -62109,7 +62645,7 @@ init_cli_shared();
|
|
|
62109
62645
|
var import_node_child_process18 = require("node:child_process");
|
|
62110
62646
|
var import_node_fs51 = require("node:fs");
|
|
62111
62647
|
var import_node_path48 = require("node:path");
|
|
62112
|
-
var
|
|
62648
|
+
var import_node_net2 = require("node:net");
|
|
62113
62649
|
var import_node_util5 = require("node:util");
|
|
62114
62650
|
|
|
62115
62651
|
// src/rules-sync.ts
|
|
@@ -62512,7 +63048,7 @@ function pickStagePort(range, isFree) {
|
|
|
62512
63048
|
}
|
|
62513
63049
|
function isPortFree(port) {
|
|
62514
63050
|
return new Promise((resolve8) => {
|
|
62515
|
-
const srv = (0,
|
|
63051
|
+
const srv = (0, import_node_net2.createServer)();
|
|
62516
63052
|
srv.once("error", () => resolve8(false));
|
|
62517
63053
|
srv.once("listening", () => srv.close(() => resolve8(true)));
|
|
62518
63054
|
srv.listen(port, "127.0.0.1");
|
|
@@ -63100,6 +63636,14 @@ function isPathAtOrWithin(path2, worktreePath) {
|
|
|
63100
63636
|
function isWindowsCwdLockResidueError(error) {
|
|
63101
63637
|
return process.platform === "win32" && /\bEPERM\b|access(?: is)? denied/i.test(error);
|
|
63102
63638
|
}
|
|
63639
|
+
function isNodeModulesLockError(error) {
|
|
63640
|
+
return /^(?:EPERM|EACCES|EBUSY)\b/.test(error);
|
|
63641
|
+
}
|
|
63642
|
+
function deferredNodeModulesLockRemediation(wtPath, branch, prNumber, repo) {
|
|
63643
|
+
const quote = (value) => value.replace(/'/g, "''");
|
|
63644
|
+
const rerun = `mmi-cli devops pr merge ${prNumber ?? "<pr>"}${repo ? ` --repo ${repo}` : ""}`;
|
|
63645
|
+
return `Close the handle on the locked file under '${quote((0, import_node_path49.join)(wtPath, "node_modules"))}', then from the primary checkout re-run cleanup for branch '${quote(branch)}': ${rerun}`;
|
|
63646
|
+
}
|
|
63103
63647
|
function deferredCwdLockRemediation(wtPath) {
|
|
63104
63648
|
const quote = (path2) => path2.replace(/'/g, "''");
|
|
63105
63649
|
return `After leaving the directory and closing its handles, remove it only if empty: [System.IO.Directory]::Delete('${quote(wtPath)}', $false)`;
|
|
@@ -63416,7 +63960,7 @@ async function removeWorktreeWithReconcile(wtPath, git3, listWorktrees, pathExis
|
|
|
63416
63960
|
}
|
|
63417
63961
|
function isPrMergeWorktreePartial(cleanup) {
|
|
63418
63962
|
const worktree = cleanup?.worktree;
|
|
63419
|
-
return Boolean(worktree?.path && worktree.status !== "removed" && worktree.status !== "preserved" && worktree.status !== "retained-locked" && worktree.status !== "switched-primary");
|
|
63963
|
+
return Boolean(worktree?.path && worktree.status !== "removed" && worktree.status !== "preserved" && worktree.status !== "retained-locked" && worktree.status !== "switched-primary" && worktree.status !== "deferred");
|
|
63420
63964
|
}
|
|
63421
63965
|
function prMergeLocalCleanupExitCode(cleanup) {
|
|
63422
63966
|
return cleanup?.worktree?.status === "failed" || cleanup?.localBranch?.status === "failed" ? 1 : void 0;
|
|
@@ -63575,18 +64119,19 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
63575
64119
|
if (pathExists((0, import_node_path49.join)(wtPath, "node_modules"))) {
|
|
63576
64120
|
const nmRemoved = await (options.removeRealNodeModules ?? removeWorktreeNodeModules)(wtPath);
|
|
63577
64121
|
if (!nmRemoved.ok) {
|
|
64122
|
+
const deferred = isNodeModulesLockError(nmRemoved.error) && pathExists(wtPath);
|
|
63578
64123
|
report.worktree = {
|
|
63579
64124
|
path: wtPath,
|
|
63580
|
-
status: "failed",
|
|
63581
|
-
reason: "pre-clean-failed",
|
|
64125
|
+
status: deferred ? "deferred" : "failed",
|
|
64126
|
+
reason: deferred ? "node-modules-locked" : "pre-clean-failed",
|
|
63582
64127
|
error: `real-node_modules cleanup failed before worktree removal: ${nmRemoved.error}`,
|
|
63583
64128
|
artifactsArchive,
|
|
63584
64129
|
stageTeardown,
|
|
63585
64130
|
tmpEvidenceCount: tmpEvidence.length,
|
|
63586
64131
|
...unlinkedReparsePoints.length ? { unlinkedReparsePoints } : {},
|
|
63587
|
-
remediation: `Close handles to '${wtPath.replace(/'/g, "''")}' and retry MMI PR landing from the primary checkout`
|
|
64132
|
+
remediation: deferred ? deferredNodeModulesLockRemediation(wtPath, branch, options.prNumber, options.repo) : `Close handles to '${wtPath.replace(/'/g, "''")}' and retry MMI PR landing from the primary checkout`
|
|
63588
64133
|
};
|
|
63589
|
-
report.localBranch = { name: branch, status: "not-attempted", reason: "worktree-pre-clean-failed" };
|
|
64134
|
+
report.localBranch = { name: branch, status: "not-attempted", reason: deferred ? "worktree-cleanup-deferred" : "worktree-pre-clean-failed" };
|
|
63590
64135
|
return report;
|
|
63591
64136
|
}
|
|
63592
64137
|
}
|
|
@@ -63685,6 +64230,8 @@ function renderPrMergeCleanupLines(cleanup) {
|
|
|
63685
64230
|
lines2.push(`pr merge: preserved worktree ${wt.path} (--preserve-worktree)`);
|
|
63686
64231
|
} else if (wt.status === "retained-locked") {
|
|
63687
64232
|
lines2.push(`pr merge: worktree ${wt.path} cleanup deferred \u2014 its Windows parent shell still holds the cwd; ${wt.remediation ?? "exit the shell and remove the residue from the primary checkout"}`);
|
|
64233
|
+
} else if (wt.status === "deferred") {
|
|
64234
|
+
lines2.push(`pr merge: worktree ${wt.path} cleanup deferred \u2014 a node_modules file is locked${wt.error ? ` (${wt.error})` : ""}; ${wt.remediation ?? "close the handle and re-run pr merge for this PR"}`);
|
|
63688
64235
|
} else if (wt.status === "switched-primary") {
|
|
63689
64236
|
const now = wt.switchedTo ? `to ${wt.switchedTo}` : "to the base branch";
|
|
63690
64237
|
lines2.push(wt.error ? `pr merge: primary checkout ${wt.path} switched ${now} but deleting the merged branch failed \u2014 ${wt.error}; remediate: ${wt.remediation ?? "delete the merged branch manually"}` : `pr merge: primary checkout ${wt.path} held the merged branch \u2014 switched it back ${now} and deleted the branch`);
|
|
@@ -65044,7 +65591,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
65044
65591
|
}
|
|
65045
65592
|
class PrHeadBehindBaseError extends Error {
|
|
65046
65593
|
}
|
|
65047
|
-
pr.command("land <numbers...>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only. Several PR numbers land together (#8322): all are enqueued, waited on together, a queue-ejected PR is updated and re-enqueued once, a queue merge is followed by worktree/branch cleanup and board Done, and one table (or --json) reports each outcome").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--enqueue-only", "return once queue membership is confirmed; without a merge queue, land normally").option("--abandon-admission", "one stuck-receipt recovery, one PR per call: journal the saved non-terminal (pending/enqueued, or a dispatching send that saved no operation UUID after its sender died \u2014 #9995) queue-admission receipt terminal ONLY after positively observing the PR OPEN at the receipt head and absent from the queue; any unprovable observation refuses and changes nothing (#9951). The next pr land then submits a new intent with no new head").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "silence the leftover-scratch warning (scratch never blocks, #8785) and acknowledge a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").option("--timeout <minutes>", `merge-queue wait per PR (default ${PR_LAND_QUEUE_TIMEOUT_MS / 6e4} minutes, one window per ${PR_LAND_QUEUE_BATCH_SIZE} PRs \u2014 per-repo ruleset check_response_timeout_minutes is 15 default with 30/45/60 overrides in .github/rulesets/mmi-development-merge-queue.json; agents never edit the ruleset to shorten a queue wait)`).option("--preserve-worktree", "after a queue merge keep the local worktree and branch (skips the post-merge cleanup)").option("--expect-head <sha>", "pin the PR head to this full 40-hex SHA (#8456): checked before landing, after checks and by GitHub at enqueue (--match-head-commit); never updates a BEHIND head and never retries an ejection \u2014 it stops with pinnedHead.refused instead. One PR only").action(async (numbers, opts) => {
|
|
65594
|
+
pr.command("land <numbers...>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only. Several PR numbers land together (#8322): all are enqueued, waited on together, a queue-ejected PR is updated and re-enqueued once, a queue merge is followed by worktree/branch cleanup and board Done, and one table (or --json) reports each outcome").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--enqueue-only", "return once queue membership is confirmed; without a merge queue, land normally").option("--abandon-admission", "one stuck-receipt recovery, one PR per call: journal the saved non-terminal (pending/enqueued, or a dispatching send that saved no operation UUID after its sender died \u2014 #9995) queue-admission receipt terminal ONLY after positively observing the PR OPEN at the receipt head and absent from the queue; any unprovable observation refuses and changes nothing (#9951). The next pr land then submits a new intent with no new head").option("--dequeue", "one queued-PR recovery, one PR per call: take the PR out of GitHub's merge queue (dequeuePullRequest, #10132) so a fix commit can reach a branch whose queue entry blocks pushes \u2014 refuses when the PR is not in the queue, read-back-verifies the entry is gone before reporting, and journals a saved non-terminal admission receipt terminal so the next pr land submits a new intent").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "silence the leftover-scratch warning (scratch never blocks, #8785) and acknowledge a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").option("--timeout <minutes>", `merge-queue wait per PR (default ${PR_LAND_QUEUE_TIMEOUT_MS / 6e4} minutes, one window per ${PR_LAND_QUEUE_BATCH_SIZE} PRs \u2014 per-repo ruleset check_response_timeout_minutes is 15 default with 30/45/60 overrides in .github/rulesets/mmi-development-merge-queue.json; agents never edit the ruleset to shorten a queue wait)`).option("--preserve-worktree", "after a queue merge keep the local worktree and branch (skips the post-merge cleanup)").option("--expect-head <sha>", "pin the PR head to this full 40-hex SHA (#8456): checked before landing, after checks and by GitHub at enqueue (--match-head-commit); never updates a BEHIND head and never retries an ejection \u2014 it stops with pinnedHead.refused instead. One PR only").action(async (numbers, opts) => {
|
|
65048
65595
|
const timeoutMinutes = opts.timeout === void 0 ? void 0 : Number(opts.timeout);
|
|
65049
65596
|
if (timeoutMinutes !== void 0 && !(timeoutMinutes > 0)) return fail("pr land: --timeout must be a positive number of minutes");
|
|
65050
65597
|
if (opts.abandonAdmission) {
|
|
@@ -65057,6 +65604,17 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
65057
65604
|
if (outcome.status !== "abandoned") process.exitCode = 1;
|
|
65058
65605
|
return;
|
|
65059
65606
|
}
|
|
65607
|
+
if (opts.dequeue) {
|
|
65608
|
+
if (opts.abandonAdmission) return fail("pr land: --dequeue and --abandon-admission are separate one-PR recoveries; pass one per call");
|
|
65609
|
+
if (numbers.length !== 1) return fail("pr land: --dequeue dequeues one PR per call");
|
|
65610
|
+
const repo = await resolveRepo(opts.repo).catch(() => void 0) ?? opts.repo;
|
|
65611
|
+
if (!repo) return fail("pr land: --dequeue needs --repo <owner/repo>; the PR repo could not be resolved");
|
|
65612
|
+
const outcome = await dequeuePrMergeQueue(numbers[0], repo);
|
|
65613
|
+
if (opts.json) printLine(JSON.stringify(outcome));
|
|
65614
|
+
else printLine(outcome.status === "dequeued" ? `pr land: dequeued PR #${outcome.pr} from the merge queue on ${repo}${outcome.head ? ` at head ${outcome.head}` : ""} \u2014 push the fix commit, then rerun \`mmi-cli devops pr land ${outcome.pr} --repo ${repo}\`` + (outcome.admission === "terminal" ? `; the saved queue admission was journaled terminal${outcome.operation ? ` (operation ${outcome.operation})` : ""}, so the next pr land submits a new intent` : "") + (outcome.admission === "left-open" ? "; a saved admission receipt was left open \u2014 `--abandon-admission` settles it once the PR is verifiably out of the queue" : "") + (outcome.admission === "unreadable" ? "; the saved queue admission could not be read and was not changed" : "") : `pr land: refused to dequeue PR #${outcome.pr} on ${repo} \u2014 ${outcome.reason}`);
|
|
65615
|
+
if (outcome.status !== "dequeued") process.exitCode = 1;
|
|
65616
|
+
return;
|
|
65617
|
+
}
|
|
65060
65618
|
if (opts.expectHead !== void 0) {
|
|
65061
65619
|
if (!/^[0-9a-f]{40}$/i.test(opts.expectHead)) return fail("pr land: --expect-head needs the full 40-hex head SHA");
|
|
65062
65620
|
if (numbers.length !== 1) return fail("pr land: --expect-head pins one PR head; land one PR per call");
|
|
@@ -65637,6 +66195,8 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
65637
66195
|
preserveWorktree: o.preserveWorktree,
|
|
65638
66196
|
gcAcknowledged: o.gc,
|
|
65639
66197
|
expectedHeadOid: headRefOid,
|
|
66198
|
+
prNumber: number,
|
|
66199
|
+
repo: repoForPostCleanup,
|
|
65640
66200
|
pathExists: (p) => (0, import_node_fs54.existsSync)(p),
|
|
65641
66201
|
// #5899: pin cleanup git calls to the main checkout — the task worktree this process may be
|
|
65642
66202
|
// standing in is removed mid-cleanup, so a cwd-relative invocation fails with
|
|
@@ -72386,14 +72946,14 @@ vercel preview (${preview.branch}): ${d.state}${d.url ? ` \u2014 ${d.url}` : ""}
|
|
|
72386
72946
|
}
|
|
72387
72947
|
|
|
72388
72948
|
// src/tenant-artifact.ts
|
|
72389
|
-
var
|
|
72949
|
+
var import_node_crypto25 = require("node:crypto");
|
|
72390
72950
|
var import_node_fs61 = require("node:fs");
|
|
72391
72951
|
var import_promises12 = require("node:fs/promises");
|
|
72392
72952
|
var import_node_path57 = require("node:path");
|
|
72393
72953
|
var ARTIFACT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
72394
72954
|
var MAX_BYTES2 = 5 * 1024 * 1024 * 1024;
|
|
72395
72955
|
async function sha256File(path2) {
|
|
72396
|
-
const hash = (0,
|
|
72956
|
+
const hash = (0, import_node_crypto25.createHash)("sha256");
|
|
72397
72957
|
for await (const chunk of (0, import_node_fs61.createReadStream)(path2)) hash.update(chunk);
|
|
72398
72958
|
return hash.digest("hex");
|
|
72399
72959
|
}
|
|
@@ -73073,13 +73633,13 @@ async function executeWaveLand(plan, deps) {
|
|
|
73073
73633
|
|
|
73074
73634
|
// src/dist-drift.ts
|
|
73075
73635
|
var import_node_child_process23 = require("node:child_process");
|
|
73076
|
-
var
|
|
73636
|
+
var import_node_crypto27 = require("node:crypto");
|
|
73077
73637
|
var import_node_fs64 = require("node:fs");
|
|
73078
73638
|
var import_node_os28 = require("node:os");
|
|
73079
73639
|
var import_node_path60 = require("node:path");
|
|
73080
73640
|
|
|
73081
73641
|
// ../scripts/distribution-digest.mjs
|
|
73082
|
-
var
|
|
73642
|
+
var import_node_crypto26 = require("node:crypto");
|
|
73083
73643
|
var import_node_fs63 = require("node:fs");
|
|
73084
73644
|
var import_node_path59 = require("node:path");
|
|
73085
73645
|
var slash = (value) => value.replaceAll("\\", "/");
|
|
@@ -73092,7 +73652,7 @@ function repoPath(root, declaredPath, label) {
|
|
|
73092
73652
|
return target;
|
|
73093
73653
|
}
|
|
73094
73654
|
function digestFiles(files) {
|
|
73095
|
-
const hash = (0,
|
|
73655
|
+
const hash = (0, import_node_crypto26.createHash)("sha256");
|
|
73096
73656
|
for (const file of [...files].sort((a, b) => a.relative.localeCompare(b.relative))) {
|
|
73097
73657
|
const content = file.stat.isSymbolicLink() ? Buffer.from((0, import_node_fs63.readlinkSync)(file.absolute), "utf8") : (0, import_node_fs63.readFileSync)(file.absolute);
|
|
73098
73658
|
hash.update(file.relative, "utf8");
|
|
@@ -73123,7 +73683,7 @@ var DIST_ARTIFACTS = [
|
|
|
73123
73683
|
];
|
|
73124
73684
|
var BOM_DIST_TREE_ID = "mmi-cli-dist";
|
|
73125
73685
|
var ABSENT = "absent";
|
|
73126
|
-
var sha2563 = (bytes) => `sha256:${(0,
|
|
73686
|
+
var sha2563 = (bytes) => `sha256:${(0, import_node_crypto27.createHash)("sha256").update(bytes).digest("hex")}`;
|
|
73127
73687
|
function artifactDrift(path2, committedBytes, rebuiltBytes) {
|
|
73128
73688
|
const committed = committedBytes ? sha2563(committedBytes) : ABSENT;
|
|
73129
73689
|
const rebuiltExpected = rebuiltBytes ? sha2563(rebuiltBytes) : ABSENT;
|
|
@@ -74693,6 +75253,7 @@ registerPreAction.call(program2, "preAction", async (_thisCommand, actionCommand
|
|
|
74693
75253
|
"run cancel",
|
|
74694
75254
|
"run rerun",
|
|
74695
75255
|
"run dispatch",
|
|
75256
|
+
"run test",
|
|
74696
75257
|
// #9779/#9780: the vars verbs and archive-status validate --repo/name/value before the first
|
|
74697
75258
|
// dispatch, so their scoped App mint waits for the action handler like the run writes above.
|
|
74698
75259
|
"vars get",
|
|
@@ -74723,7 +75284,7 @@ async function reportWrite(label, res) {
|
|
|
74723
75284
|
return failGraceful(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
74724
75285
|
}
|
|
74725
75286
|
var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
|
|
74726
|
-
tenant.command("control <owner/repo> <stage> <action>").description("bounded tenant control plus value-free vault/broker verification; dev/main retire needs master and exact data-loss confirmation").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("--since <utc>", "logs only: read the journal from this UTC time, e.g. 2026-10-07T13:10Z (includes replaced containers)").option("--until <utc>", "logs only: read the journal up to this UTC time").option("--edge", "logs only: Caddy journal for this stage\u2019s hosts (default window: last 15 minutes)").option("--task <name>", "run-task only: registry-declared task name (never a command)").option("--artifact <id>", "run-task only: opaque id returned by tenant artifact put").option("--input <file>", "run-task only: upload this file as the task artifact (same as tenant artifact put), then run with its id").option("--resume <run-id>", "run-task only: re-attach to an already-dispatched run (read-only, never re-dispatches) and print its receipt").option("--confirm-data-loss <owner/repo/stage>", "dev/main retire only: exact target to decommission; without it show the dry-run plan").option("--json", "machine-readable output").action(async (repo, stage, action, o) => {
|
|
75287
|
+
tenant.command("control <owner/repo> <stage> <action>").description("bounded tenant control plus value-free vault/broker verification; dev/main retire needs master and exact data-loss confirmation").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("--since <utc>", "logs only: read the journal from this UTC time, e.g. 2026-10-07T13:10Z (includes replaced containers)").option("--until <utc>", "logs only: read the journal up to this UTC time").option("--edge", "logs only: Caddy journal for this stage\u2019s hosts (default window: last 15 minutes)").option("--task <name>", "run-task only: registry-declared task name (never a command)").option("--artifact <id>", "run-task only: opaque id returned by tenant artifact put").option("--input <file>", "run-task only: upload this file as the task artifact (same as tenant artifact put), then run with its id").option("--resume <run-id>", "run-task only: re-attach to an already-dispatched run (read-only, never re-dispatches) and print its receipt").option("--confirm-data-loss <owner/repo/stage>", "dev/main retire only: exact target to decommission; without it show the dry-run plan").option("--out <path>", "write the output to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5802); written only after a successful result (#10127)").option("--json", "machine-readable output").action(async (repo, stage, action, o) => {
|
|
74727
75288
|
try {
|
|
74728
75289
|
if (o.resume !== void 0 && (action !== "run-task" || !/^\d+$/.test(o.resume) || o.task || o.artifact || o.input)) return fail("runtime tenant control: --resume <run-id> is run-task-only, takes a numeric run id, and cannot be combined with --task/--artifact/--input");
|
|
74729
75290
|
if (o.input !== void 0 && o.artifact !== void 0) return fail("runtime tenant control: pass --input <file> or --artifact <id>, not both");
|
|
@@ -74732,9 +75293,9 @@ tenant.command("control <owner/repo> <stage> <action>").description("bounded ten
|
|
|
74732
75293
|
if (o.confirmDataLoss !== void 0 && o.confirmDataLoss !== `${repo}/${stage}`) return fail(`runtime tenant control: --confirm-data-loss must match ${repo}/${stage} exactly`);
|
|
74733
75294
|
if (o.confirmDataLoss === void 0) {
|
|
74734
75295
|
const res = await tenantControl({ repo, stage, action, plan: true }, registryClientDeps(await loadConfig()));
|
|
74735
|
-
if (!res.ok) return failGraceful(`runtime tenant control plan: ${res.body?.error ?? res.error ?? `HTTP ${res.status}`}
|
|
74736
|
-
|
|
74737
|
-
Dry run: pass --confirm-data-loss ${repo}/${stage} to decommission
|
|
75296
|
+
if (!res.ok) return failGraceful(tenantControlFailure(`runtime tenant control plan: ${res.body?.error ?? res.error ?? `HTTP ${res.status}`}`, o.json));
|
|
75297
|
+
emitTenantControlOutput(o.json ? JSON.stringify(res.body, null, 2) : `${JSON.stringify(res.body, null, 2)}
|
|
75298
|
+
Dry run: pass --confirm-data-loss ${repo}/${stage} to decommission.`, { out: o.out, action }, { print: printLine });
|
|
74738
75299
|
return;
|
|
74739
75300
|
}
|
|
74740
75301
|
}
|
|
@@ -74760,22 +75321,22 @@ Dry run: pass --confirm-data-loss ${repo}/${stage} to decommission.`);
|
|
|
74760
75321
|
if (!o.json && action === "verify-secrets" && result.secrets) {
|
|
74761
75322
|
const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed", raw: result.secretsRaw };
|
|
74762
75323
|
const { lines: lines3, failure } = renderVerifySecrets(body);
|
|
74763
|
-
|
|
74764
|
-
if (failure) return failGraceful(`runtime tenant control ${stage} verify-secrets: ${failure}
|
|
75324
|
+
emitTenantControlOutput(lines3.join("\n"), { out: o.out, failed: !!failure || result.conclusion === "failure", action }, { print: printLine });
|
|
75325
|
+
if (failure) return failGraceful(tenantControlFailure(`runtime tenant control ${stage} verify-secrets: ${failure}`, o.json));
|
|
74765
75326
|
} else if (!o.json && action === "logs" && result.logs) {
|
|
74766
|
-
|
|
75327
|
+
emitTenantControlOutput(result.logs, { out: o.out, failed: result.conclusion === "failure", action }, { print: printLine });
|
|
74767
75328
|
} else if (!o.json && action === "verify-broker" && result.broker) {
|
|
74768
75329
|
const { lines: lines3, failure } = renderVerifyBroker({ broker: result.broker, raw: result.brokerRaw });
|
|
74769
|
-
|
|
74770
|
-
if (failure) return failGraceful(`runtime tenant control ${stage} verify-broker: ${failure}
|
|
75330
|
+
emitTenantControlOutput(lines3.join("\n"), { out: o.out, failed: !!failure || result.conclusion === "failure", action }, { print: printLine });
|
|
75331
|
+
if (failure) return failGraceful(tenantControlFailure(`runtime tenant control ${stage} verify-broker: ${failure}`, o.json));
|
|
74771
75332
|
} else {
|
|
74772
|
-
|
|
75333
|
+
emitTenantControlOutput(o.json ? JSON.stringify(result, null, 2) : renderTenantControl(result), { out: o.out, failed: result.conclusion === "failure", action }, { print: printLine });
|
|
74773
75334
|
}
|
|
74774
75335
|
if (result.conclusion === "failure") {
|
|
74775
|
-
return failGraceful(`runtime tenant control ${stage} ${action}: ${result.category ?? "failed"} \u2014 ${result.note}
|
|
75336
|
+
return failGraceful(tenantControlFailure(`runtime tenant control ${stage} ${action}: ${result.category ?? "failed"} \u2014 ${result.note}`, o.json));
|
|
74776
75337
|
}
|
|
74777
75338
|
} catch (e) {
|
|
74778
|
-
return failGraceful(`runtime tenant control: ${e.message}
|
|
75339
|
+
return failGraceful(tenantControlFailure(`runtime tenant control: ${e.message}`, o.json));
|
|
74779
75340
|
}
|
|
74780
75341
|
});
|
|
74781
75342
|
tenant.command("undo <owner/repo> <stage>").description("switch a tenant stage back to its previous release (warm standby: seconds; else recreated from the retained release, no rebuild); watches by default").option("--json", "machine-readable output").action(async (repo, stage, o) => {
|
|
@@ -75588,6 +76149,17 @@ registerRunDispatch(
|
|
|
75588
76149
|
async () => (await mintCommandWriteToken(appActorDeps(), "run dispatch")).token,
|
|
75589
76150
|
async () => fetchProjectsList(registryClientDeps(await loadConfig()))
|
|
75590
76151
|
);
|
|
76152
|
+
var remoteTestToken;
|
|
76153
|
+
var remoteTestWriteToken = () => remoteTestToken ??= mintCommandWriteToken(appActorDeps(), "run test").then((t) => t.token);
|
|
76154
|
+
registerRemoteTest(runRoot, {
|
|
76155
|
+
dispatch: async (inputs) => {
|
|
76156
|
+
await dispatchWorkflow(FOCUSED_TEST_REPO, FOCUSED_TEST_WORKFLOW, FOCUSED_TEST_REF, inputs, {
|
|
76157
|
+
projects: async () => fetchProjectsList(registryClientDeps(await loadConfig())),
|
|
76158
|
+
post: appDispatchPost(remoteTestWriteToken)
|
|
76159
|
+
});
|
|
76160
|
+
},
|
|
76161
|
+
read: appApiRead(remoteTestWriteToken)
|
|
76162
|
+
});
|
|
75591
76163
|
var varsAppClient = (commandPath4) => async () => (
|
|
75592
76164
|
// The concrete client always provides restWithStatus (optional on the interface only so hand-built
|
|
75593
76165
|
// test stubs of GitHubClient keep compiling).
|