@mutmutco/cli 2.54.2 → 2.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/main.cjs +1760 -516
- package/dist/overlord-controller.cjs +322 -0
- package/package.json +2 -1
package/dist/main.cjs
CHANGED
|
@@ -3408,7 +3408,7 @@ var program = new Command();
|
|
|
3408
3408
|
|
|
3409
3409
|
// src/index.ts
|
|
3410
3410
|
var import_promises8 = require("node:fs/promises");
|
|
3411
|
-
var
|
|
3411
|
+
var import_node_fs29 = require("node:fs");
|
|
3412
3412
|
|
|
3413
3413
|
// src/rules-sync.ts
|
|
3414
3414
|
function normalizeEol(s) {
|
|
@@ -5594,7 +5594,7 @@ async function restoreDirtyOrgSpineToHead(deps, options = {}) {
|
|
|
5594
5594
|
}
|
|
5595
5595
|
|
|
5596
5596
|
// src/index.ts
|
|
5597
|
-
var
|
|
5597
|
+
var import_node_child_process15 = require("node:child_process");
|
|
5598
5598
|
|
|
5599
5599
|
// src/cli-shared.ts
|
|
5600
5600
|
var import_promises = require("node:fs/promises");
|
|
@@ -9862,7 +9862,7 @@ async function runCoopDeliver(coopId, io, quiet = false) {
|
|
|
9862
9862
|
if (!quiet) {
|
|
9863
9863
|
io.log(`[coop deliver] ${coopId} \u2014 issue ${match.meta.issueUrl}`);
|
|
9864
9864
|
for (const m of match.messages) io.log(m.envelope);
|
|
9865
|
-
io.log("[coop deliver]
|
|
9865
|
+
io.log("[coop deliver] Coordinate in `#mmi-agents` via `mmi-cli coop say`; the issue keeps proof/context.");
|
|
9866
9866
|
}
|
|
9867
9867
|
await postJson("/coop/delivered", { coopId });
|
|
9868
9868
|
}
|
|
@@ -9874,7 +9874,7 @@ function registerCoopCommands(program3) {
|
|
|
9874
9874
|
{ file: opts.messageFile },
|
|
9875
9875
|
{ readFile: import_promises4.readFile, readStdin },
|
|
9876
9876
|
{ value: "a message argument", file: "--message-file", noun: "message" }
|
|
9877
|
-
) : "Coop session started
|
|
9877
|
+
) : "Coop session started - coordinate in #mmi-agents; the issue keeps proof/context.";
|
|
9878
9878
|
const payload = {
|
|
9879
9879
|
repo: opts.repo ?? ctx.repo,
|
|
9880
9880
|
branch: ctx.branch,
|
|
@@ -9955,14 +9955,856 @@ function registerCoopCommands(program3) {
|
|
|
9955
9955
|
});
|
|
9956
9956
|
}
|
|
9957
9957
|
|
|
9958
|
-
// src/
|
|
9958
|
+
// src/overlord.ts
|
|
9959
9959
|
var import_node_child_process8 = require("node:child_process");
|
|
9960
|
-
var
|
|
9960
|
+
var import_node_fs15 = require("node:fs");
|
|
9961
|
+
var import_node_os3 = require("node:os");
|
|
9961
9962
|
var import_node_path13 = require("node:path");
|
|
9962
|
-
|
|
9963
|
+
|
|
9964
|
+
// src/atomic-write.ts
|
|
9965
|
+
var import_node_fs14 = require("node:fs");
|
|
9966
|
+
function atomicWriteFileSync(path2, content) {
|
|
9967
|
+
const tmp = `${path2}.${process.pid}.tmp`;
|
|
9968
|
+
(0, import_node_fs14.writeFileSync)(tmp, content, "utf8");
|
|
9969
|
+
(0, import_node_fs14.renameSync)(tmp, path2);
|
|
9970
|
+
}
|
|
9971
|
+
|
|
9972
|
+
// src/overlord.ts
|
|
9973
|
+
var OVERLORD_DEFAULT_COUNT = 3;
|
|
9974
|
+
var OVERLORD_MIN_COUNT = 3;
|
|
9975
|
+
var OVERLORD_MAX_COUNT = 6;
|
|
9976
|
+
var OVERLORD_HANDOFF_TIMEOUT_MS = 12e4;
|
|
9977
|
+
var GENERIC_STOP_NAMES = /* @__PURE__ */ new Set([
|
|
9978
|
+
"windowsterminal",
|
|
9979
|
+
"windows terminal",
|
|
9980
|
+
"pwsh",
|
|
9981
|
+
"powershell",
|
|
9982
|
+
"opencode",
|
|
9983
|
+
"codex",
|
|
9984
|
+
"codex-fugu"
|
|
9985
|
+
]);
|
|
9986
|
+
function numericCountArg(arg) {
|
|
9987
|
+
const match = /^--([0-9]+)$/.exec(arg);
|
|
9988
|
+
return match ? Number(match[1]) : void 0;
|
|
9989
|
+
}
|
|
9990
|
+
function isoNow(now = () => /* @__PURE__ */ new Date()) {
|
|
9991
|
+
return now().toISOString();
|
|
9992
|
+
}
|
|
9993
|
+
function defaultRunId() {
|
|
9994
|
+
return `overlord-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
9995
|
+
}
|
|
9996
|
+
function defaultRunToken() {
|
|
9997
|
+
return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
|
9998
|
+
}
|
|
9999
|
+
function defaultMessageId() {
|
|
10000
|
+
return `msg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
10001
|
+
}
|
|
10002
|
+
function servantSlotId(slot) {
|
|
10003
|
+
return slot.name.replace(/\s+/g, "-").toLowerCase();
|
|
10004
|
+
}
|
|
10005
|
+
function overlordServantPrompt(servant, run) {
|
|
10006
|
+
const roleLine = servant.role === "ultra" ? "You are the single Ultra Fugu: take the hardest, highest-uncertainty questions and report calibrated judgment." : "You are a normal Fugu servant: take one bounded mission at a time and report concise evidence.";
|
|
10007
|
+
return [
|
|
10008
|
+
`You are ${servant.name} in Overlord run ${run.runId}.`,
|
|
10009
|
+
roleLine,
|
|
10010
|
+
"First respond with exactly: ACK " + servant.name + " ready",
|
|
10011
|
+
"After the ACK, wait for the Overlord to assign bounded work.",
|
|
10012
|
+
"Do not start dev servers, browsers, Playwright, PRs, merges, releases, or worktree changes unless the Overlord explicitly assigns that scope.",
|
|
10013
|
+
"When assigned work, gather evidence before editing, verify before claiming done, and escalate blockers instead of looping."
|
|
10014
|
+
].join("\n");
|
|
10015
|
+
}
|
|
10016
|
+
function parseOverlordCount(args) {
|
|
10017
|
+
const counts = args.map(numericCountArg).filter((n) => n !== void 0);
|
|
10018
|
+
if (counts.length === 0) return OVERLORD_DEFAULT_COUNT;
|
|
10019
|
+
if (counts.length > 1) throw new Error("provide only one overlord servant count");
|
|
10020
|
+
const count = counts[0];
|
|
10021
|
+
if (count < OVERLORD_MIN_COUNT || count > OVERLORD_MAX_COUNT) {
|
|
10022
|
+
throw new Error(`overlord servant count must be between ${OVERLORD_MIN_COUNT} and ${OVERLORD_MAX_COUNT}`);
|
|
10023
|
+
}
|
|
10024
|
+
return count;
|
|
10025
|
+
}
|
|
10026
|
+
function buildServantLayout(count) {
|
|
10027
|
+
if (count < OVERLORD_MIN_COUNT || count > OVERLORD_MAX_COUNT) {
|
|
10028
|
+
throw new Error(`overlord servant count must be between ${OVERLORD_MIN_COUNT} and ${OVERLORD_MAX_COUNT}`);
|
|
10029
|
+
}
|
|
10030
|
+
const slots = [{
|
|
10031
|
+
name: "fugu ultra 1",
|
|
10032
|
+
role: "ultra",
|
|
10033
|
+
model: "fugu-ultra",
|
|
10034
|
+
index: 1
|
|
10035
|
+
}];
|
|
10036
|
+
for (let i = 1; i < count; i++) {
|
|
10037
|
+
slots.push({
|
|
10038
|
+
name: `fugu normal ${i}`,
|
|
10039
|
+
role: "normal",
|
|
10040
|
+
model: "fugu",
|
|
10041
|
+
index: i
|
|
10042
|
+
});
|
|
10043
|
+
}
|
|
10044
|
+
return slots;
|
|
10045
|
+
}
|
|
10046
|
+
function validateCodexFuguHelp(helpText) {
|
|
10047
|
+
const problems = [];
|
|
10048
|
+
if (!/(?:^|\s)-a(?:,|\s)|--ask-for-approval/.test(helpText)) problems.push("missing approval flag");
|
|
10049
|
+
if (!/(?:^|\s)-s(?:,|\s)|--sandbox/.test(helpText)) problems.push("missing sandbox flag");
|
|
10050
|
+
if (!/(?:^|\s)-c(?:,|\s)|--config/.test(helpText)) problems.push("missing config override flag");
|
|
10051
|
+
if (!/--no-alt-screen/.test(helpText)) problems.push("missing no-alt-screen flag");
|
|
10052
|
+
if (!/(?:^|\s)-C(?:,|\s)|--cwd|--cd|--workdir/.test(helpText)) problems.push("missing cwd flag");
|
|
10053
|
+
return problems;
|
|
10054
|
+
}
|
|
10055
|
+
function evaluateCodexFuguPreflight(input) {
|
|
10056
|
+
const problems = [];
|
|
10057
|
+
if (!input.codexFound) problems.push("codex is not installed or not on PATH");
|
|
10058
|
+
if (!input.fuguFound) problems.push("codex-fugu is not installed or not on PATH");
|
|
10059
|
+
if (!input.authConfigured && !input.envNames.includes("OPENAI_API_KEY") && !input.envNames.includes("CODEX_API_KEY")) {
|
|
10060
|
+
problems.push("missing API key environment: OPENAI_API_KEY or CODEX_API_KEY");
|
|
10061
|
+
}
|
|
10062
|
+
problems.push(...validateCodexFuguHelp(input.helpText ?? ""));
|
|
10063
|
+
const status = input.statusText ?? "";
|
|
10064
|
+
const modelCatalog = input.modelCatalogText ?? "";
|
|
10065
|
+
if (!/\bfugu-ultra\b/i.test(`${status}
|
|
10066
|
+
${modelCatalog}`)) problems.push("fugu-ultra is not available");
|
|
10067
|
+
if (/native windows codex[^\n]*\/c\/users|\/c\/users[^\n]*native windows codex/i.test(status)) {
|
|
10068
|
+
problems.push("native Windows Codex is configured with a Git Bash /c/Users path");
|
|
10069
|
+
}
|
|
10070
|
+
return { ok: problems.length === 0, problems };
|
|
10071
|
+
}
|
|
10072
|
+
function evaluateOpenCodePreflight(input) {
|
|
10073
|
+
const problems = [];
|
|
10074
|
+
if (!input.found) problems.push("opencode is not installed or not on PATH");
|
|
10075
|
+
if (!/sakana\/fugu\b/i.test(input.modelsText ?? "")) problems.push("sakana/fugu is not available");
|
|
10076
|
+
if (!/sakana\/fugu-ultra\b/i.test(input.modelsText ?? "")) problems.push("sakana/fugu-ultra is not available");
|
|
10077
|
+
const probe = input.jsonProbeText ?? "";
|
|
10078
|
+
if (probe && !/(sessionID|sessionId|step_finish|finish)/i.test(probe)) problems.push("opencode run --format json did not emit session or finish events");
|
|
10079
|
+
return { ok: problems.length === 0, problems };
|
|
10080
|
+
}
|
|
10081
|
+
function buildOpenCodeLaunch(slot, message, sessionId) {
|
|
10082
|
+
const model = slot.role === "ultra" ? "sakana/fugu-ultra" : "sakana/fugu";
|
|
10083
|
+
const args = ["run", "-m", model, "--format", "json"];
|
|
10084
|
+
if (sessionId) args.push("--session", sessionId);
|
|
10085
|
+
args.push(message);
|
|
10086
|
+
return { command: "opencode", args };
|
|
10087
|
+
}
|
|
10088
|
+
function parseOpenCodeEvents(raw) {
|
|
10089
|
+
const events = [];
|
|
10090
|
+
const trimmed = (raw ?? "").trim();
|
|
10091
|
+
if (!trimmed) return { text: "", finished: false, events };
|
|
10092
|
+
const pushParsed = (chunk) => {
|
|
10093
|
+
const t = chunk.trim();
|
|
10094
|
+
if (!t) return;
|
|
10095
|
+
try {
|
|
10096
|
+
events.push(JSON.parse(t));
|
|
10097
|
+
} catch {
|
|
10098
|
+
}
|
|
10099
|
+
};
|
|
10100
|
+
try {
|
|
10101
|
+
const whole = JSON.parse(trimmed);
|
|
10102
|
+
if (Array.isArray(whole)) events.push(...whole);
|
|
10103
|
+
else events.push(whole);
|
|
10104
|
+
} catch {
|
|
10105
|
+
for (const line of trimmed.split(/\r?\n/)) pushParsed(line);
|
|
10106
|
+
}
|
|
10107
|
+
let sessionId;
|
|
10108
|
+
let text = "";
|
|
10109
|
+
let finished = false;
|
|
10110
|
+
for (const ev of events) {
|
|
10111
|
+
if (!ev || typeof ev !== "object") continue;
|
|
10112
|
+
const e = ev;
|
|
10113
|
+
const sid = e.sessionID ?? e.sessionId ?? e.session?.id;
|
|
10114
|
+
if (typeof sid === "string" && sid) sessionId = sid;
|
|
10115
|
+
const type = typeof e.type === "string" ? e.type : void 0;
|
|
10116
|
+
if (typeof e.text === "string") text += e.text;
|
|
10117
|
+
else if (type === "text" && typeof e.content === "string") text += e.content;
|
|
10118
|
+
if (type === "step_finish" || type === "finish" || e.finishReason || e.finish_reason) finished = true;
|
|
10119
|
+
}
|
|
10120
|
+
return { sessionId, text: text.trim(), finished, events };
|
|
10121
|
+
}
|
|
10122
|
+
function appendOverlordLedger(ledgerPath, entry) {
|
|
10123
|
+
try {
|
|
10124
|
+
(0, import_node_fs15.mkdirSync)((0, import_node_path13.dirname)(ledgerPath), { recursive: true });
|
|
10125
|
+
(0, import_node_fs15.appendFileSync)(ledgerPath, `${JSON.stringify(entry)}
|
|
10126
|
+
`, "utf8");
|
|
10127
|
+
} catch {
|
|
10128
|
+
}
|
|
10129
|
+
}
|
|
10130
|
+
function defaultOverlordStatePath(cwd) {
|
|
10131
|
+
return (0, import_node_path13.join)(cwd, "tmp", "overlord", "runs.json");
|
|
10132
|
+
}
|
|
10133
|
+
function readOverlordRegistry(statePath) {
|
|
10134
|
+
if (!(0, import_node_fs15.existsSync)(statePath)) return { runs: {} };
|
|
10135
|
+
try {
|
|
10136
|
+
const parsed = JSON.parse((0, import_node_fs15.readFileSync)(statePath, "utf8"));
|
|
10137
|
+
return { activeRunId: parsed.activeRunId, runs: parsed.runs ?? {} };
|
|
10138
|
+
} catch {
|
|
10139
|
+
return { runs: {} };
|
|
10140
|
+
}
|
|
10141
|
+
}
|
|
10142
|
+
function writeOverlordRegistry(statePath, registry2) {
|
|
10143
|
+
(0, import_node_fs15.mkdirSync)((0, import_node_path13.dirname)(statePath), { recursive: true });
|
|
10144
|
+
atomicWriteFileSync(statePath, `${JSON.stringify(registry2, null, 2)}
|
|
10145
|
+
`);
|
|
10146
|
+
}
|
|
10147
|
+
function buildOverlordRun(options) {
|
|
10148
|
+
const runId = options.runId?.() ?? defaultRunId();
|
|
10149
|
+
const runToken = options.runToken?.() ?? defaultRunToken();
|
|
10150
|
+
const statePath = defaultOverlordStatePath(options.cwd);
|
|
10151
|
+
const journalDir = (0, import_node_path13.join)(options.cwd, "tmp", "overlord", runId);
|
|
10152
|
+
const engine = options.engine ?? "codex-fugu";
|
|
10153
|
+
const timestamp = isoNow(options.now);
|
|
10154
|
+
return {
|
|
10155
|
+
runId,
|
|
10156
|
+
runToken,
|
|
10157
|
+
task: options.task,
|
|
10158
|
+
state: "starting",
|
|
10159
|
+
createdAt: timestamp,
|
|
10160
|
+
updatedAt: timestamp,
|
|
10161
|
+
worktree: options.cwd,
|
|
10162
|
+
statePath,
|
|
10163
|
+
journalDir,
|
|
10164
|
+
ledgerPath: (0, import_node_path13.join)(journalDir, "ledger.jsonl"),
|
|
10165
|
+
engine,
|
|
10166
|
+
provider: engine === "opencode" ? "opencode" : "codex",
|
|
10167
|
+
opencodeVersion: options.opencodeVersion,
|
|
10168
|
+
servants: buildServantLayout(options.count).map((slot) => ({
|
|
10169
|
+
...slot,
|
|
10170
|
+
slotId: servantSlotId(slot),
|
|
10171
|
+
profile: "consultation",
|
|
10172
|
+
state: "planned",
|
|
10173
|
+
journalPath: (0, import_node_path13.join)(journalDir, `${servantSlotId(slot)}.log`),
|
|
10174
|
+
composerSubmitMode: engine === "opencode" ? "surface-api" : "unknown",
|
|
10175
|
+
engine,
|
|
10176
|
+
provider: engine === "opencode" ? "opencode" : "codex",
|
|
10177
|
+
opencodeVersion: options.opencodeVersion,
|
|
10178
|
+
eventJournalPath: engine === "opencode" ? (0, import_node_path13.join)(journalDir, `${servantSlotId(slot)}.events.jsonl`) : void 0
|
|
10179
|
+
})),
|
|
10180
|
+
ownedResources: []
|
|
10181
|
+
};
|
|
10182
|
+
}
|
|
10183
|
+
function recordOverlordHeartbeat(run, options) {
|
|
10184
|
+
const timestamp = isoNow(options.now);
|
|
10185
|
+
const controllerResource = {
|
|
10186
|
+
kind: "process",
|
|
10187
|
+
pid: options.controllerPid,
|
|
10188
|
+
commandName: "mmi-cli overlord controller",
|
|
10189
|
+
runId: run.runId,
|
|
10190
|
+
runToken: run.runToken,
|
|
10191
|
+
fingerprint: options.fingerprint
|
|
10192
|
+
};
|
|
10193
|
+
const others = run.ownedResources.filter((resource) => resource.commandName !== "mmi-cli overlord controller");
|
|
10194
|
+
return {
|
|
10195
|
+
...run,
|
|
10196
|
+
state: run.state === "starting" ? "active" : run.state,
|
|
10197
|
+
updatedAt: timestamp,
|
|
10198
|
+
controllerPid: options.controllerPid,
|
|
10199
|
+
controllerFingerprint: options.fingerprint,
|
|
10200
|
+
lastControllerHeartbeatAt: timestamp,
|
|
10201
|
+
ownedResources: [controllerResource, ...others]
|
|
10202
|
+
};
|
|
10203
|
+
}
|
|
10204
|
+
function buildOverlordStartupPlan(args, cwd) {
|
|
10205
|
+
const count = parseOverlordCount(args);
|
|
10206
|
+
const task = args.filter((arg) => numericCountArg(arg) === void 0).join(" ").trim();
|
|
10207
|
+
return {
|
|
10208
|
+
count,
|
|
10209
|
+
servants: buildServantLayout(count),
|
|
10210
|
+
task,
|
|
10211
|
+
statePath: defaultOverlordStatePath(cwd),
|
|
10212
|
+
nextPhase: "preflight"
|
|
10213
|
+
};
|
|
10214
|
+
}
|
|
10215
|
+
function normalizedCommandName(resource) {
|
|
10216
|
+
return (resource.commandName ?? resource.title ?? "").trim().toLowerCase();
|
|
10217
|
+
}
|
|
10218
|
+
function planStopResource(resource, context) {
|
|
10219
|
+
const exactOwner = resource.runId === context.runId && resource.runToken === context.runToken && Boolean(resource.fingerprint);
|
|
10220
|
+
if (exactOwner) return { action: "stop", reason: "owned" };
|
|
10221
|
+
const name = normalizedCommandName(resource);
|
|
10222
|
+
if (!name || GENERIC_STOP_NAMES.has(name)) return { action: "refuse", reason: "ownership not proven" };
|
|
10223
|
+
return { action: "refuse", reason: "ownership not proven" };
|
|
10224
|
+
}
|
|
10225
|
+
function summarizeOverlordRun(run, probe) {
|
|
10226
|
+
const controller = run.controllerPid == null ? "not-started" : probe.isPidAlive(run.controllerPid) ? "alive" : "lost";
|
|
10227
|
+
const now = probe.now?.() ?? /* @__PURE__ */ new Date();
|
|
10228
|
+
return {
|
|
10229
|
+
active: run.state !== "stopped" && run.state !== "failed",
|
|
10230
|
+
runId: run.runId,
|
|
10231
|
+
state: run.state,
|
|
10232
|
+
controller,
|
|
10233
|
+
servants: run.servants.map((servant) => {
|
|
10234
|
+
if (run.engine === "opencode" || servant.engine === "opencode") return { name: servant.name, state: servantProgress(run, servant, now) };
|
|
10235
|
+
if (servant.pid == null) return { name: servant.name, state: "not-started" };
|
|
10236
|
+
if (!probe.isPidAlive(servant.pid)) return { name: servant.name, state: "lost" };
|
|
10237
|
+
const progress = servantProgress(run, servant, now);
|
|
10238
|
+
if (progress !== servant.state) return { name: servant.name, state: progress };
|
|
10239
|
+
if (servant.state === "ready" && servant.lastAckAt && servant.composerSubmitMode !== "unknown") {
|
|
10240
|
+
return { name: servant.name, state: "ready" };
|
|
10241
|
+
}
|
|
10242
|
+
return { name: servant.name, state: servant.state };
|
|
10243
|
+
})
|
|
10244
|
+
};
|
|
10245
|
+
}
|
|
10246
|
+
function planOverlordRunStop(run) {
|
|
10247
|
+
const killPids = [];
|
|
10248
|
+
const uncertain = [];
|
|
10249
|
+
for (const resource of run.ownedResources) {
|
|
10250
|
+
const plan2 = planStopResource(resource, { runId: run.runId, runToken: run.runToken });
|
|
10251
|
+
if (plan2.action === "stop" && resource.pid != null) killPids.push(resource.pid);
|
|
10252
|
+
else uncertain.push(resource);
|
|
10253
|
+
}
|
|
10254
|
+
return { killPids, uncertain };
|
|
10255
|
+
}
|
|
10256
|
+
function controllerFingerprint(run) {
|
|
10257
|
+
return `mmi-overlord-controller:${run.runId}:${run.worktree}`;
|
|
10258
|
+
}
|
|
10259
|
+
function defaultStartController(run) {
|
|
10260
|
+
const scriptPath = (0, import_node_path13.join)(__dirname, "overlord-controller.cjs");
|
|
10261
|
+
const fingerprint = controllerFingerprint(run);
|
|
10262
|
+
const child = (0, import_node_child_process8.spawn)(process.execPath, [scriptPath, run.runId, run.statePath, fingerprint], {
|
|
10263
|
+
detached: true,
|
|
10264
|
+
stdio: "ignore",
|
|
10265
|
+
windowsHide: true,
|
|
10266
|
+
cwd: run.worktree
|
|
10267
|
+
});
|
|
10268
|
+
child.unref();
|
|
10269
|
+
return { pid: child.pid, fingerprint };
|
|
10270
|
+
}
|
|
10271
|
+
function defaultRunOpenCode(run, servant, message, sessionId) {
|
|
10272
|
+
const launch = buildOpenCodeLaunch(servant, message, sessionId ?? servant.opencodeSessionId);
|
|
10273
|
+
const shell2 = process.platform === "win32";
|
|
10274
|
+
const file = shell2 ? [launch.command, ...launch.args].map(shellQuote2).join(" ") : launch.command;
|
|
10275
|
+
const result = (0, import_node_child_process8.spawnSync)(file, shell2 ? [] : launch.args, {
|
|
10276
|
+
encoding: "utf8",
|
|
10277
|
+
shell: shell2,
|
|
10278
|
+
cwd: run.worktree,
|
|
10279
|
+
timeout: 6e5,
|
|
10280
|
+
windowsHide: true,
|
|
10281
|
+
env: { ...process.env }
|
|
10282
|
+
});
|
|
10283
|
+
if (result.error && result.error.code === "ENOENT") {
|
|
10284
|
+
return { ok: false, error: "opencode is not installed or not on PATH" };
|
|
10285
|
+
}
|
|
10286
|
+
const raw = `${result.stdout ?? ""}
|
|
10287
|
+
${result.stderr ?? ""}`;
|
|
10288
|
+
if (servant.eventJournalPath) {
|
|
10289
|
+
try {
|
|
10290
|
+
(0, import_node_fs15.mkdirSync)((0, import_node_path13.dirname)(servant.eventJournalPath), { recursive: true });
|
|
10291
|
+
(0, import_node_fs15.appendFileSync)(servant.eventJournalPath, `${raw.trim()}
|
|
10292
|
+
`, "utf8");
|
|
10293
|
+
} catch {
|
|
10294
|
+
}
|
|
10295
|
+
}
|
|
10296
|
+
const parsed = parseOpenCodeEvents(raw);
|
|
10297
|
+
if (result.status !== 0 && !parsed.finished) {
|
|
10298
|
+
return { ok: false, sessionId: parsed.sessionId, text: parsed.text, error: `opencode run exited ${result.status ?? "with error"}`, events: parsed.events };
|
|
10299
|
+
}
|
|
10300
|
+
return { ok: true, sessionId: parsed.sessionId, text: parsed.text, events: parsed.events };
|
|
10301
|
+
}
|
|
10302
|
+
function defaultIsPidAlive(pid) {
|
|
10303
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
10304
|
+
try {
|
|
10305
|
+
process.kill(pid, 0);
|
|
10306
|
+
return true;
|
|
10307
|
+
} catch {
|
|
10308
|
+
return false;
|
|
10309
|
+
}
|
|
10310
|
+
}
|
|
10311
|
+
function defaultKillPid(pid) {
|
|
10312
|
+
process.kill(pid);
|
|
10313
|
+
}
|
|
10314
|
+
function shellQuote2(arg) {
|
|
10315
|
+
return /^[A-Za-z0-9_./:-]+$/.test(arg) ? arg : `"${arg.replace(/"/g, '\\"')}"`;
|
|
10316
|
+
}
|
|
10317
|
+
function boundedCommandText(command, args) {
|
|
10318
|
+
const shell2 = process.platform === "win32";
|
|
10319
|
+
const file = shell2 ? [command, ...args].map(shellQuote2).join(" ") : command;
|
|
10320
|
+
const result = (0, import_node_child_process8.spawnSync)(file, shell2 ? [] : args, {
|
|
10321
|
+
encoding: "utf8",
|
|
10322
|
+
shell: shell2,
|
|
10323
|
+
timeout: 5e3,
|
|
10324
|
+
windowsHide: true,
|
|
10325
|
+
env: {
|
|
10326
|
+
...process.env,
|
|
10327
|
+
CODEX_FUGU_NO_NOTICE: "1",
|
|
10328
|
+
CODEX_FUGU_NO_UPDATE: "1"
|
|
10329
|
+
}
|
|
10330
|
+
});
|
|
10331
|
+
const text = `${result.stdout ?? ""}
|
|
10332
|
+
${result.stderr ?? ""}`.trim();
|
|
10333
|
+
if (result.error && result.error.code === "ENOENT") return { found: false, text };
|
|
10334
|
+
return { found: !result.error, text };
|
|
10335
|
+
}
|
|
10336
|
+
function readFuguModelCatalogText() {
|
|
10337
|
+
const codexHome = process.env.CODEX_HOME ?? (0, import_node_path13.join)((0, import_node_os3.homedir)(), ".codex");
|
|
10338
|
+
const catalogPath = (0, import_node_path13.join)(codexHome, "fugu.json");
|
|
10339
|
+
try {
|
|
10340
|
+
return (0, import_node_fs15.existsSync)(catalogPath) ? (0, import_node_fs15.readFileSync)(catalogPath, "utf8") : "";
|
|
10341
|
+
} catch {
|
|
10342
|
+
return "";
|
|
10343
|
+
}
|
|
10344
|
+
}
|
|
10345
|
+
function hasCodexAuthEvidence() {
|
|
10346
|
+
const codexHome = process.env.CODEX_HOME ?? (0, import_node_path13.join)((0, import_node_os3.homedir)(), ".codex");
|
|
10347
|
+
return (0, import_node_fs15.existsSync)((0, import_node_path13.join)(codexHome, "auth.json"));
|
|
10348
|
+
}
|
|
10349
|
+
function collectOpenCodePreflight() {
|
|
10350
|
+
const version = boundedCommandText("opencode", ["--version"]);
|
|
10351
|
+
const models = boundedCommandText("opencode", ["models"]);
|
|
10352
|
+
return evaluateOpenCodePreflight({
|
|
10353
|
+
found: version.found,
|
|
10354
|
+
versionText: version.text,
|
|
10355
|
+
modelsText: models.text
|
|
10356
|
+
});
|
|
10357
|
+
}
|
|
10358
|
+
function collectCodexFuguPreflight() {
|
|
10359
|
+
const codexHelp = boundedCommandText("codex", ["--help"]);
|
|
10360
|
+
const fuguHelp = boundedCommandText("codex-fugu", ["--help"]);
|
|
10361
|
+
const fuguStatus = boundedCommandText("codex-fugu", ["--status"]);
|
|
10362
|
+
return evaluateCodexFuguPreflight({
|
|
10363
|
+
codexFound: codexHelp.found,
|
|
10364
|
+
fuguFound: fuguHelp.found,
|
|
10365
|
+
helpText: fuguHelp.text || codexHelp.text,
|
|
10366
|
+
statusText: fuguStatus.text,
|
|
10367
|
+
modelCatalogText: readFuguModelCatalogText(),
|
|
10368
|
+
authConfigured: hasCodexAuthEvidence(),
|
|
10369
|
+
envNames: Object.keys(process.env)
|
|
10370
|
+
});
|
|
10371
|
+
}
|
|
10372
|
+
function renderCodexFuguPreflightFailure(report) {
|
|
10373
|
+
const lines = [
|
|
10374
|
+
"Overlord setup needed",
|
|
10375
|
+
"The servant pool was not started because Codex/Fugu preflight failed.",
|
|
10376
|
+
"",
|
|
10377
|
+
"Problems:",
|
|
10378
|
+
...report.problems.map((problem) => `- ${problem}`),
|
|
10379
|
+
"",
|
|
10380
|
+
"Fixes:"
|
|
10381
|
+
];
|
|
10382
|
+
if (report.problems.some((problem) => problem.includes("codex is not installed"))) {
|
|
10383
|
+
lines.push("- Install or update Codex, then confirm with `codex --version`.");
|
|
10384
|
+
}
|
|
10385
|
+
if (report.problems.some((problem) => problem.includes("codex-fugu is not installed"))) {
|
|
10386
|
+
lines.push("- Install or repair codex-fugu, then confirm with `codex-fugu --status`.");
|
|
10387
|
+
}
|
|
10388
|
+
if (report.problems.some((problem) => problem.includes("missing API key"))) {
|
|
10389
|
+
lines.push("- Set OPENAI_API_KEY or CODEX_API_KEY in the launching shell; do not paste key values into chat.");
|
|
10390
|
+
}
|
|
10391
|
+
if (report.problems.some((problem) => problem.includes("fugu-ultra"))) {
|
|
10392
|
+
lines.push("- Recheck or update Fugu configs so `fugu-ultra` appears in the Fugu model catalog.");
|
|
10393
|
+
}
|
|
10394
|
+
if (report.problems.some((problem) => problem.includes("missing ") && problem.includes("flag"))) {
|
|
10395
|
+
lines.push("- Update Codex/Fugu until `codex-fugu --help` exposes approval, sandbox, config, cwd, and no-alt-screen flags.");
|
|
10396
|
+
}
|
|
10397
|
+
if (report.problems.some((problem) => problem.includes("/c/Users"))) {
|
|
10398
|
+
lines.push("- Launch from a native shell adapter or repair Fugu path configuration so Windows paths stay native.");
|
|
10399
|
+
}
|
|
10400
|
+
return lines.join("\n");
|
|
10401
|
+
}
|
|
10402
|
+
function updateOpenCodeServant(run, servant, result, timestamp) {
|
|
10403
|
+
return {
|
|
10404
|
+
...servant,
|
|
10405
|
+
state: result.ok ? "ready" : "blocked",
|
|
10406
|
+
opencodeSessionId: result.sessionId ?? servant.opencodeSessionId,
|
|
10407
|
+
lastAckAt: result.ok ? timestamp : servant.lastAckAt,
|
|
10408
|
+
lastUsefulSignalAt: result.ok ? timestamp : servant.lastUsefulSignalAt,
|
|
10409
|
+
lastEventAt: timestamp,
|
|
10410
|
+
lastMessageCompletedAt: result.ok ? timestamp : servant.lastMessageCompletedAt
|
|
10411
|
+
};
|
|
10412
|
+
}
|
|
10413
|
+
function startOpenCodeServants(run, runOpenCode, now) {
|
|
10414
|
+
const timestamp = isoNow(now);
|
|
10415
|
+
const servants = run.servants.map((servant) => {
|
|
10416
|
+
const result = runOpenCode(run, servant, overlordServantPrompt(servant, run));
|
|
10417
|
+
if (run.ledgerPath) appendOverlordLedger(run.ledgerPath, { at: timestamp, kind: "servant-start", ownerSlotId: servant.slotId, ok: result.ok, sessionId: result.sessionId, responseText: result.text, error: result.error });
|
|
10418
|
+
return updateOpenCodeServant(run, servant, result, timestamp);
|
|
10419
|
+
});
|
|
10420
|
+
return { ...run, state: "active", updatedAt: timestamp, servants };
|
|
10421
|
+
}
|
|
10422
|
+
function dispatchOpenCodeMessage(run, message, runOpenCode, now) {
|
|
10423
|
+
const timestamp = isoNow(now);
|
|
10424
|
+
const targets = run.servants.filter((servant) => message.target === "all" || servant.slotId === message.target || normalizeServantTarget(servant.name) === message.target);
|
|
10425
|
+
const responses = [];
|
|
10426
|
+
let ok = targets.length > 0;
|
|
10427
|
+
const nextServants = run.servants.map((servant) => {
|
|
10428
|
+
if (!targets.some((target) => target.slotId === servant.slotId)) return servant;
|
|
10429
|
+
const result = runOpenCode(run, servant, message.text, servant.opencodeSessionId);
|
|
10430
|
+
if (!result.ok) ok = false;
|
|
10431
|
+
if (result.text) responses.push(`${servant.slotId}: ${result.text}`);
|
|
10432
|
+
if (run.ledgerPath) appendOverlordLedger(run.ledgerPath, { at: timestamp, kind: "message-response", messageId: message.id, ownerSlotId: servant.slotId, ok: result.ok, sessionId: result.sessionId, responseText: result.text, error: result.error });
|
|
10433
|
+
return updateOpenCodeServant(run, servant, result, timestamp);
|
|
10434
|
+
});
|
|
10435
|
+
const nextMessage = {
|
|
10436
|
+
...message,
|
|
10437
|
+
state: ok ? "completed" : "failed",
|
|
10438
|
+
startedAt: timestamp,
|
|
10439
|
+
completedAt: ok ? timestamp : void 0,
|
|
10440
|
+
failedAt: ok ? void 0 : timestamp,
|
|
10441
|
+
ackText: ok ? "opencode response captured" : void 0,
|
|
10442
|
+
responseText: responses.join("\n"),
|
|
10443
|
+
failureReason: ok ? void 0 : "one or more OpenCode servant calls failed",
|
|
10444
|
+
eventJournalPath: targets.map((target) => target.eventJournalPath).filter(Boolean).join(",")
|
|
10445
|
+
};
|
|
10446
|
+
return {
|
|
10447
|
+
...run,
|
|
10448
|
+
updatedAt: timestamp,
|
|
10449
|
+
servants: nextServants,
|
|
10450
|
+
messages: [...(run.messages ?? []).filter((m) => m.id !== message.id), nextMessage]
|
|
10451
|
+
};
|
|
10452
|
+
}
|
|
10453
|
+
function findActiveRun(registry2) {
|
|
10454
|
+
const runId = registry2.activeRunId;
|
|
10455
|
+
if (!runId) return void 0;
|
|
10456
|
+
return registry2.runs[runId];
|
|
10457
|
+
}
|
|
10458
|
+
function normalizeServantTarget(target) {
|
|
10459
|
+
return target.trim().toLowerCase().replace(/\s+/g, "-");
|
|
10460
|
+
}
|
|
10461
|
+
function hasServantTarget(run, target) {
|
|
10462
|
+
const normalized = normalizeServantTarget(target);
|
|
10463
|
+
return normalized === "all" || run.servants.some(
|
|
10464
|
+
(servant) => servant.slotId === normalized || normalizeServantTarget(servant.name) === normalized
|
|
10465
|
+
);
|
|
10466
|
+
}
|
|
10467
|
+
function messageProgress(message, now = /* @__PURE__ */ new Date(), timeoutMs = OVERLORD_HANDOFF_TIMEOUT_MS) {
|
|
10468
|
+
if (message.completedAt || message.state === "completed") return "completed";
|
|
10469
|
+
if (message.failedAt || message.state === "failed") return "failed";
|
|
10470
|
+
const startedAt = message.startedAt ?? message.deliveredAt;
|
|
10471
|
+
if (!startedAt) return "queued";
|
|
10472
|
+
const elapsed = now.getTime() - new Date(startedAt).getTime();
|
|
10473
|
+
return Number.isFinite(elapsed) && elapsed >= timeoutMs ? "stalled" : "started";
|
|
10474
|
+
}
|
|
10475
|
+
function servantProgress(run, servant, now = /* @__PURE__ */ new Date()) {
|
|
10476
|
+
const relevant = (run.messages ?? []).filter((message) => message.target === "all" || servant.slotId === message.target || normalizeServantTarget(servant.name) === message.target);
|
|
10477
|
+
return relevant.some((message) => messageProgress(message, now) === "stalled") ? "stalled-after-delivery" : servant.state;
|
|
10478
|
+
}
|
|
10479
|
+
function renderOverlordStatus(summary, run) {
|
|
10480
|
+
const servants = summary.servants.map((servant) => `- ${servant.name}: ${servant.state}`).join("\n");
|
|
10481
|
+
const messages = (run.messages ?? []).map((message) => `- ${message.id}: ${message.target} ${messageProgress(message)}`).join("\n");
|
|
10482
|
+
return [
|
|
10483
|
+
`Overlord run ${summary.runId}`,
|
|
10484
|
+
`State: ${summary.state}`,
|
|
10485
|
+
`Task: ${run.task || "not provided yet"}`,
|
|
10486
|
+
`Controller: ${summary.controller}`,
|
|
10487
|
+
"Servants:",
|
|
10488
|
+
servants,
|
|
10489
|
+
...messages ? ["Messages:", messages] : []
|
|
10490
|
+
].join("\n");
|
|
10491
|
+
}
|
|
10492
|
+
function usefulJournalLines(text) {
|
|
10493
|
+
return text.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !/^fugu\s/i.test(line) && !/^›/.test(line) && !/^\[overlord\] launched/.test(line)).slice(-20);
|
|
10494
|
+
}
|
|
10495
|
+
function servantJournalSummary(servant) {
|
|
10496
|
+
try {
|
|
10497
|
+
const lines = usefulJournalLines((0, import_node_fs15.readFileSync)(servant.journalPath, "utf8"));
|
|
10498
|
+
return { lines, hasHandoff: lines.some((line) => /\b(handoff|evidence|recommended|recommendation)\b/i.test(line)) };
|
|
10499
|
+
} catch {
|
|
10500
|
+
return { lines: [], hasHandoff: false };
|
|
10501
|
+
}
|
|
10502
|
+
}
|
|
10503
|
+
function runJson(run, extra = {}) {
|
|
10504
|
+
return {
|
|
10505
|
+
ok: true,
|
|
10506
|
+
runId: run.runId,
|
|
10507
|
+
state: run.state,
|
|
10508
|
+
task: run.task,
|
|
10509
|
+
count: run.servants.length,
|
|
10510
|
+
engine: run.engine,
|
|
10511
|
+
controllerPid: run.controllerPid,
|
|
10512
|
+
statePath: run.statePath,
|
|
10513
|
+
ledgerPath: run.ledgerPath,
|
|
10514
|
+
servants: run.servants.map((servant) => ({
|
|
10515
|
+
name: servant.name,
|
|
10516
|
+
role: servant.role,
|
|
10517
|
+
model: servant.model,
|
|
10518
|
+
state: servant.state,
|
|
10519
|
+
pid: servant.pid,
|
|
10520
|
+
journalPath: servant.journalPath,
|
|
10521
|
+
engine: servant.engine,
|
|
10522
|
+
opencodeSessionId: servant.opencodeSessionId
|
|
10523
|
+
})),
|
|
10524
|
+
...extra
|
|
10525
|
+
};
|
|
10526
|
+
}
|
|
10527
|
+
function wantsJson(options, command) {
|
|
10528
|
+
return Boolean(options.json || command?.parent?.opts?.().json);
|
|
10529
|
+
}
|
|
10530
|
+
function countArgsFromOptions(options) {
|
|
10531
|
+
return ["3", "4", "5", "6"].filter((key) => options[key]).map((key) => `--${key}`);
|
|
10532
|
+
}
|
|
10533
|
+
function registerOverlordCommands(program3, deps = {}) {
|
|
10534
|
+
const out = deps.out ?? ((text) => process.stdout.write(text));
|
|
10535
|
+
const err = deps.err ?? ((text) => process.stderr.write(text));
|
|
10536
|
+
const cwd = deps.cwd ?? (() => process.cwd());
|
|
10537
|
+
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
10538
|
+
const preflight2 = deps.preflight ?? collectCodexFuguPreflight;
|
|
10539
|
+
const opencodePreflight = deps.opencodePreflight ?? collectOpenCodePreflight;
|
|
10540
|
+
const startController = deps.startController ?? defaultStartController;
|
|
10541
|
+
const runOpenCode = deps.runOpenCode ?? defaultRunOpenCode;
|
|
10542
|
+
const isPidAlive = deps.isPidAlive ?? defaultIsPidAlive;
|
|
10543
|
+
const killPid = deps.killPid ?? defaultKillPid;
|
|
10544
|
+
const overlord = program3.command("overlord").description("coordinate one Ultra and normal Fugu servants for hard org work").allowUnknownOption(true).argument("[task...]", "task for the Overlord system").option("--3", "run one Ultra and two normal Fugus").option("--4", "run one Ultra and three normal Fugus").option("--5", "run one Ultra and four normal Fugus").option("--6", "run one Ultra and five normal Fugus").option("--engine <engine>", "servant engine: opencode or codex-fugu", "codex-fugu").option("--json", "print machine-readable output").action((task = [], options) => {
|
|
10545
|
+
try {
|
|
10546
|
+
const args = [...countArgsFromOptions(options), ...task];
|
|
10547
|
+
const root = cwd();
|
|
10548
|
+
const plan2 = buildOverlordStartupPlan(args, root);
|
|
10549
|
+
const engine = `${options.engine ?? "codex-fugu"}`;
|
|
10550
|
+
if (engine !== "codex-fugu" && engine !== "opencode") throw new Error("--engine must be opencode or codex-fugu");
|
|
10551
|
+
const preflightReport = engine === "opencode" ? opencodePreflight() : preflight2();
|
|
10552
|
+
if (!preflightReport.ok) {
|
|
10553
|
+
err(`${renderCodexFuguPreflightFailure(preflightReport)}
|
|
10554
|
+
`);
|
|
10555
|
+
process.exitCode = 1;
|
|
10556
|
+
return;
|
|
10557
|
+
}
|
|
10558
|
+
const registry2 = readOverlordRegistry(plan2.statePath);
|
|
10559
|
+
const active = findActiveRun(registry2);
|
|
10560
|
+
if (active && active.state !== "stopped" && active.state !== "failed") {
|
|
10561
|
+
throw new Error(`active Overlord run already exists: ${active.runId}`);
|
|
10562
|
+
}
|
|
10563
|
+
let run = buildOverlordRun({
|
|
10564
|
+
task: plan2.task,
|
|
10565
|
+
cwd: root,
|
|
10566
|
+
count: plan2.count,
|
|
10567
|
+
engine,
|
|
10568
|
+
now,
|
|
10569
|
+
runId: deps.runId,
|
|
10570
|
+
runToken: deps.runToken
|
|
10571
|
+
});
|
|
10572
|
+
writeOverlordRegistry(plan2.statePath, { activeRunId: run.runId, runs: { ...registry2.runs, [run.runId]: run } });
|
|
10573
|
+
if (engine === "opencode") {
|
|
10574
|
+
run = startOpenCodeServants(run, runOpenCode, now);
|
|
10575
|
+
writeOverlordRegistry(plan2.statePath, { activeRunId: run.runId, runs: { ...registry2.runs, [run.runId]: run } });
|
|
10576
|
+
} else {
|
|
10577
|
+
const controller = startController(run);
|
|
10578
|
+
if (controller.pid != null) {
|
|
10579
|
+
run = recordOverlordHeartbeat(run, {
|
|
10580
|
+
controllerPid: controller.pid,
|
|
10581
|
+
fingerprint: controller.fingerprint,
|
|
10582
|
+
now
|
|
10583
|
+
});
|
|
10584
|
+
writeOverlordRegistry(plan2.statePath, { activeRunId: run.runId, runs: { ...registry2.runs, [run.runId]: run } });
|
|
10585
|
+
}
|
|
10586
|
+
}
|
|
10587
|
+
if (options.json) out(`${JSON.stringify(runJson(run, { nextPhase: "consult-servants" }), null, 2)}
|
|
10588
|
+
`);
|
|
10589
|
+
else {
|
|
10590
|
+
out(`${[
|
|
10591
|
+
"Overlord startup",
|
|
10592
|
+
`Task: ${run.task || "not provided yet"}`,
|
|
10593
|
+
`Run: ${run.runId}`,
|
|
10594
|
+
`Servants: ${run.servants.length} total`,
|
|
10595
|
+
`Controller: ${run.controllerPid ? `started (pid ${run.controllerPid})` : "launch requested"}`,
|
|
10596
|
+
"Next: consult servants, interview the human, then print an approved todo list."
|
|
10597
|
+
].join("\n")}
|
|
10598
|
+
`);
|
|
10599
|
+
}
|
|
10600
|
+
} catch (e) {
|
|
10601
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
10602
|
+
if (options.json) out(`${JSON.stringify({ ok: false, error: message }, null, 2)}
|
|
10603
|
+
`);
|
|
10604
|
+
else err(`overlord: ${message}
|
|
10605
|
+
`);
|
|
10606
|
+
process.exitCode = 1;
|
|
10607
|
+
}
|
|
10608
|
+
});
|
|
10609
|
+
overlord.command("send").description("queue an assignment or redirect for the active Overlord servant controller").argument("<target>", "servant slot id/name, or all").argument("[message...]", "message to deliver to the servant PTY").option("--json", "print machine-readable output").action((target, message = [], options, command) => {
|
|
10610
|
+
const json = wantsJson(options, command);
|
|
10611
|
+
try {
|
|
10612
|
+
const statePath = defaultOverlordStatePath(cwd());
|
|
10613
|
+
const registry2 = readOverlordRegistry(statePath);
|
|
10614
|
+
const run = findActiveRun(registry2);
|
|
10615
|
+
if (!run) throw new Error("no active Overlord run found");
|
|
10616
|
+
const normalized = normalizeServantTarget(target);
|
|
10617
|
+
if (!hasServantTarget(run, normalized)) throw new Error(`unknown Overlord servant target: ${target}`);
|
|
10618
|
+
const text = message.join(" ").trim();
|
|
10619
|
+
if (!text) throw new Error("message is required");
|
|
10620
|
+
const timestamp = isoNow(now);
|
|
10621
|
+
const queued = {
|
|
10622
|
+
id: deps.runId?.() ?? defaultMessageId(),
|
|
10623
|
+
target: normalized,
|
|
10624
|
+
text,
|
|
10625
|
+
createdAt: timestamp,
|
|
10626
|
+
queuedAt: timestamp,
|
|
10627
|
+
state: "queued"
|
|
10628
|
+
};
|
|
10629
|
+
if (run.engine === "opencode") {
|
|
10630
|
+
const dispatched = dispatchOpenCodeMessage(run, queued, runOpenCode, now);
|
|
10631
|
+
writeOverlordRegistry(statePath, { ...registry2, runs: { ...registry2.runs, [run.runId]: dispatched } });
|
|
10632
|
+
const settled = (dispatched.messages ?? []).find((m) => m.id === queued.id);
|
|
10633
|
+
const ok = settled?.state === "completed";
|
|
10634
|
+
const payload2 = { ok, runId: run.runId, target: normalized, messageId: queued.id, state: settled?.state, responseText: settled?.responseText, statePath };
|
|
10635
|
+
if (json) out(`${JSON.stringify(payload2, null, 2)}
|
|
10636
|
+
`);
|
|
10637
|
+
else out(`Message ${queued.id} ${ok ? "completed" : "failed"} for ${normalized}.
|
|
10638
|
+
State: ${statePath}
|
|
10639
|
+
`);
|
|
10640
|
+
if (!ok) process.exitCode = 1;
|
|
10641
|
+
return;
|
|
10642
|
+
}
|
|
10643
|
+
const next = {
|
|
10644
|
+
...run,
|
|
10645
|
+
updatedAt: timestamp,
|
|
10646
|
+
messages: [...run.messages ?? [], queued]
|
|
10647
|
+
};
|
|
10648
|
+
writeOverlordRegistry(statePath, {
|
|
10649
|
+
...registry2,
|
|
10650
|
+
runs: { ...registry2.runs, [run.runId]: next }
|
|
10651
|
+
});
|
|
10652
|
+
const payload = { queued: 1, runId: run.runId, target: normalized, messageId: queued.id, statePath };
|
|
10653
|
+
if (json) out(`${JSON.stringify(payload, null, 2)}
|
|
10654
|
+
`);
|
|
10655
|
+
else out(`Queued message ${queued.id} for ${normalized}.
|
|
10656
|
+
State: ${statePath}
|
|
10657
|
+
`);
|
|
10658
|
+
} catch (e) {
|
|
10659
|
+
const messageText = e instanceof Error ? e.message : String(e);
|
|
10660
|
+
if (json) out(`${JSON.stringify({ ok: false, error: messageText }, null, 2)}
|
|
10661
|
+
`);
|
|
10662
|
+
else err(`overlord send: ${messageText}
|
|
10663
|
+
`);
|
|
10664
|
+
process.exitCode = 1;
|
|
10665
|
+
}
|
|
10666
|
+
});
|
|
10667
|
+
overlord.command("status").description("show the active Overlord run and servant liveness").option("--json", "print machine-readable output").action((options, command) => {
|
|
10668
|
+
const json = wantsJson(options, command);
|
|
10669
|
+
const statePath = defaultOverlordStatePath(cwd());
|
|
10670
|
+
const registry2 = readOverlordRegistry(statePath);
|
|
10671
|
+
const run = findActiveRun(registry2);
|
|
10672
|
+
if (!run) {
|
|
10673
|
+
const payload2 = { active: false, statePath, message: "no active Overlord run found" };
|
|
10674
|
+
if (json) out(`${JSON.stringify(payload2, null, 2)}
|
|
10675
|
+
`);
|
|
10676
|
+
else out(`No active Overlord run found.
|
|
10677
|
+
State: ${payload2.statePath}
|
|
10678
|
+
`);
|
|
10679
|
+
return;
|
|
10680
|
+
}
|
|
10681
|
+
const summary = summarizeOverlordRun(run, { isPidAlive, now });
|
|
10682
|
+
const current = now();
|
|
10683
|
+
const payload = {
|
|
10684
|
+
...summary,
|
|
10685
|
+
statePath,
|
|
10686
|
+
task: run.task,
|
|
10687
|
+
engine: run.engine,
|
|
10688
|
+
ledgerPath: run.ledgerPath,
|
|
10689
|
+
sessions: run.servants.map((servant) => ({
|
|
10690
|
+
name: servant.name,
|
|
10691
|
+
slotId: servant.slotId,
|
|
10692
|
+
engine: servant.engine,
|
|
10693
|
+
opencodeSessionId: servant.opencodeSessionId,
|
|
10694
|
+
eventJournalPath: servant.eventJournalPath,
|
|
10695
|
+
lastEventAt: servant.lastEventAt,
|
|
10696
|
+
lastMessageCompletedAt: servant.lastMessageCompletedAt
|
|
10697
|
+
})),
|
|
10698
|
+
messages: (run.messages ?? []).map((message) => ({
|
|
10699
|
+
id: message.id,
|
|
10700
|
+
target: message.target,
|
|
10701
|
+
state: messageProgress(message, current),
|
|
10702
|
+
queuedAt: message.queuedAt ?? message.createdAt,
|
|
10703
|
+
startedAt: message.startedAt ?? message.deliveredAt,
|
|
10704
|
+
completedAt: message.completedAt,
|
|
10705
|
+
failedAt: message.failedAt,
|
|
10706
|
+
ackText: message.ackText,
|
|
10707
|
+
responseText: message.responseText,
|
|
10708
|
+
eventJournalPath: message.eventJournalPath,
|
|
10709
|
+
failureReason: message.failureReason
|
|
10710
|
+
}))
|
|
10711
|
+
};
|
|
10712
|
+
if (json) out(`${JSON.stringify(payload, null, 2)}
|
|
10713
|
+
`);
|
|
10714
|
+
else out(`${renderOverlordStatus(summary, run)}
|
|
10715
|
+
State: ${statePath}
|
|
10716
|
+
`);
|
|
10717
|
+
});
|
|
10718
|
+
overlord.command("collect").description("summarize servant handoff/liveness evidence from the active Overlord journals").option("--json", "print machine-readable output").action((options, command) => {
|
|
10719
|
+
const json = wantsJson(options, command);
|
|
10720
|
+
const statePath = defaultOverlordStatePath(cwd());
|
|
10721
|
+
const registry2 = readOverlordRegistry(statePath);
|
|
10722
|
+
const run = findActiveRun(registry2);
|
|
10723
|
+
if (!run) {
|
|
10724
|
+
const payload2 = { active: false, statePath, message: "no active Overlord run found" };
|
|
10725
|
+
if (json) out(`${JSON.stringify(payload2, null, 2)}
|
|
10726
|
+
`);
|
|
10727
|
+
else out(`No active Overlord run found.
|
|
10728
|
+
State: ${payload2.statePath}
|
|
10729
|
+
`);
|
|
10730
|
+
return;
|
|
10731
|
+
}
|
|
10732
|
+
const current = now();
|
|
10733
|
+
const servants = run.servants.map((servant) => {
|
|
10734
|
+
const journal = servantJournalSummary(servant);
|
|
10735
|
+
return {
|
|
10736
|
+
slotId: servant.slotId,
|
|
10737
|
+
name: servant.name,
|
|
10738
|
+
state: servantProgress(run, servant, current),
|
|
10739
|
+
hasHandoff: journal.hasHandoff,
|
|
10740
|
+
journalPath: servant.journalPath,
|
|
10741
|
+
lines: journal.lines
|
|
10742
|
+
};
|
|
10743
|
+
});
|
|
10744
|
+
const payload = { active: true, runId: run.runId, statePath, servants };
|
|
10745
|
+
if (json) out(`${JSON.stringify(payload, null, 2)}
|
|
10746
|
+
`);
|
|
10747
|
+
else out(`${servants.map((servant) => `${servant.name}: ${servant.state}; handoff=${servant.hasHandoff ? "yes" : "no"}; ${servant.journalPath}`).join("\n")}
|
|
10748
|
+
State: ${statePath}
|
|
10749
|
+
`);
|
|
10750
|
+
});
|
|
10751
|
+
overlord.command("stop").description("stop only exact run-owned Overlord resources").option("--json", "print machine-readable output").action((options, command) => {
|
|
10752
|
+
const json = wantsJson(options, command);
|
|
10753
|
+
const statePath = defaultOverlordStatePath(cwd());
|
|
10754
|
+
const registry2 = readOverlordRegistry(statePath);
|
|
10755
|
+
const run = findActiveRun(registry2);
|
|
10756
|
+
if (!run) {
|
|
10757
|
+
const payload2 = { stopped: 0, uncertain: 0, statePath, message: "no active Overlord run found" };
|
|
10758
|
+
if (json) out(`${JSON.stringify(payload2, null, 2)}
|
|
10759
|
+
`);
|
|
10760
|
+
else out(`No active Overlord run found. Nothing stopped.
|
|
10761
|
+
State: ${payload2.statePath}
|
|
10762
|
+
`);
|
|
10763
|
+
return;
|
|
10764
|
+
}
|
|
10765
|
+
const stopPlan = planOverlordRunStop(run);
|
|
10766
|
+
for (const pid of stopPlan.killPids) {
|
|
10767
|
+
try {
|
|
10768
|
+
killPid(pid);
|
|
10769
|
+
} catch {
|
|
10770
|
+
}
|
|
10771
|
+
}
|
|
10772
|
+
const stoppedRun = {
|
|
10773
|
+
...run,
|
|
10774
|
+
state: "stopped",
|
|
10775
|
+
updatedAt: isoNow(now)
|
|
10776
|
+
};
|
|
10777
|
+
writeOverlordRegistry(statePath, {
|
|
10778
|
+
runs: { ...registry2.runs, [run.runId]: stoppedRun }
|
|
10779
|
+
});
|
|
10780
|
+
const payload = {
|
|
10781
|
+
runId: run.runId,
|
|
10782
|
+
stopped: stopPlan.killPids.length,
|
|
10783
|
+
uncertain: stopPlan.uncertain.length,
|
|
10784
|
+
statePath
|
|
10785
|
+
};
|
|
10786
|
+
if (json) out(`${JSON.stringify(payload, null, 2)}
|
|
10787
|
+
`);
|
|
10788
|
+
else {
|
|
10789
|
+
out(`${[
|
|
10790
|
+
`Stopped ${payload.stopped} Overlord-owned process(es).`,
|
|
10791
|
+
payload.uncertain ? `Left ${payload.uncertain} uncertain resource(s) untouched.` : "No uncertain resources.",
|
|
10792
|
+
`State: ${statePath}`
|
|
10793
|
+
].join("\n")}
|
|
10794
|
+
`);
|
|
10795
|
+
}
|
|
10796
|
+
});
|
|
10797
|
+
return overlord;
|
|
10798
|
+
}
|
|
10799
|
+
|
|
10800
|
+
// src/throttle-commands.ts
|
|
10801
|
+
var import_node_child_process9 = require("node:child_process");
|
|
10802
|
+
var import_node_fs16 = require("node:fs");
|
|
10803
|
+
var import_node_path14 = require("node:path");
|
|
10804
|
+
var THROTTLE_TRACE_REL = (0, import_node_path14.join)(".mmi", "throttle", "trace.jsonl");
|
|
9963
10805
|
function resolveRepoGitRoot(cwd = process.cwd()) {
|
|
9964
10806
|
try {
|
|
9965
|
-
const root = (0,
|
|
10807
|
+
const root = (0, import_node_child_process9.execFileSync)("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
|
|
9966
10808
|
encoding: "utf8",
|
|
9967
10809
|
timeout: 5e3
|
|
9968
10810
|
}).trim();
|
|
@@ -9972,7 +10814,7 @@ function resolveRepoGitRoot(cwd = process.cwd()) {
|
|
|
9972
10814
|
}
|
|
9973
10815
|
}
|
|
9974
10816
|
function resolveThrottleTracePath(cwd = process.cwd()) {
|
|
9975
|
-
return (0,
|
|
10817
|
+
return (0, import_node_path14.join)(resolveRepoGitRoot(cwd), THROTTLE_TRACE_REL);
|
|
9976
10818
|
}
|
|
9977
10819
|
function resolveModeFromEnv() {
|
|
9978
10820
|
const v = String(process.env.MMI_THROTTLE_MODE ?? "block").trim().toLowerCase();
|
|
@@ -9992,6 +10834,12 @@ function parseTraceLines(raw) {
|
|
|
9992
10834
|
}
|
|
9993
10835
|
return out;
|
|
9994
10836
|
}
|
|
10837
|
+
function commandSafetyClass(reasonId) {
|
|
10838
|
+
if (reasonId === "shell_dialect_redirect") return "dialect/redirect mismatch";
|
|
10839
|
+
if (reasonId === "shell_log_dump") return "unbounded log/text dump";
|
|
10840
|
+
if (reasonId.startsWith("shell_")) return "shell command safety";
|
|
10841
|
+
return void 0;
|
|
10842
|
+
}
|
|
9995
10843
|
function summarizeTrace(entries) {
|
|
9996
10844
|
let denials = 0;
|
|
9997
10845
|
let readBytesWouldBlock = 0;
|
|
@@ -9999,6 +10847,7 @@ function summarizeTrace(entries) {
|
|
|
9999
10847
|
const byReason = {};
|
|
10000
10848
|
const bySurface = {};
|
|
10001
10849
|
const byMode = {};
|
|
10850
|
+
const commandSafetyBySurface = {};
|
|
10002
10851
|
for (const e of entries) {
|
|
10003
10852
|
denials += 1;
|
|
10004
10853
|
const tool = e.tool ?? "unknown";
|
|
@@ -10009,22 +10858,27 @@ function summarizeTrace(entries) {
|
|
|
10009
10858
|
bySurface[surface] = (bySurface[surface] ?? 0) + 1;
|
|
10010
10859
|
const mode = e.mode ?? "unknown";
|
|
10011
10860
|
byMode[mode] = (byMode[mode] ?? 0) + 1;
|
|
10861
|
+
const safetyClass = commandSafetyClass(reason);
|
|
10862
|
+
if (safetyClass) {
|
|
10863
|
+
commandSafetyBySurface[surface] = commandSafetyBySurface[surface] ?? {};
|
|
10864
|
+
commandSafetyBySurface[surface][safetyClass] = (commandSafetyBySurface[surface][safetyClass] ?? 0) + 1;
|
|
10865
|
+
}
|
|
10012
10866
|
if (reason === "read_unbounded_large") {
|
|
10013
10867
|
readBytesWouldBlock += Number(e.fileBytes) || 0;
|
|
10014
10868
|
}
|
|
10015
10869
|
}
|
|
10016
|
-
return { denials, readBytesWouldBlock, byTool, byReason, bySurface, byMode };
|
|
10870
|
+
return { denials, readBytesWouldBlock, byTool, byReason, bySurface, byMode, commandSafetyBySurface };
|
|
10017
10871
|
}
|
|
10018
10872
|
function runThrottleReport(io, tracePath = resolveThrottleTracePath()) {
|
|
10019
10873
|
const mode = resolveModeFromEnv();
|
|
10020
|
-
if (!(0,
|
|
10874
|
+
if (!(0, import_node_fs16.existsSync)(tracePath)) {
|
|
10021
10875
|
io.log(`Throttle: no trace at ${tracePath} (gates have not denied anything yet).`);
|
|
10022
10876
|
io.log(`Active mode: ${mode} (MMI_THROTTLE_MODE env; default block).`);
|
|
10023
10877
|
return 0;
|
|
10024
10878
|
}
|
|
10025
10879
|
let raw = "";
|
|
10026
10880
|
try {
|
|
10027
|
-
raw = (0,
|
|
10881
|
+
raw = (0, import_node_fs16.readFileSync)(tracePath, "utf8");
|
|
10028
10882
|
} catch (e) {
|
|
10029
10883
|
io.err(`Throttle: could not read trace: ${e.message}`);
|
|
10030
10884
|
return 1;
|
|
@@ -10059,6 +10913,14 @@ function runThrottleReport(io, tracePath = resolveThrottleTracePath()) {
|
|
|
10059
10913
|
io.log(" by surface:");
|
|
10060
10914
|
for (const [name, count] of surfaces) io.log(` ${name}: ${count}`);
|
|
10061
10915
|
}
|
|
10916
|
+
const safetySurfaces = Object.entries(s.commandSafetyBySurface).sort((a, b) => a[0].localeCompare(b[0]));
|
|
10917
|
+
if (safetySurfaces.length) {
|
|
10918
|
+
io.log(" command safety (quoting/dialect/shell) by surface:");
|
|
10919
|
+
for (const [surface, classes] of safetySurfaces) {
|
|
10920
|
+
const detail = Object.entries(classes).sort((a, b) => b[1] - a[1]).map(([cls, count]) => `${cls}: ${count}`).join(", ");
|
|
10921
|
+
io.log(` ${surface}: ${detail}`);
|
|
10922
|
+
}
|
|
10923
|
+
}
|
|
10062
10924
|
return 0;
|
|
10063
10925
|
}
|
|
10064
10926
|
function registerThrottleCommands(program3) {
|
|
@@ -10091,7 +10953,7 @@ async function syncDocs(deps, docs2 = SYNCED_DOCS) {
|
|
|
10091
10953
|
}
|
|
10092
10954
|
|
|
10093
10955
|
// src/board.ts
|
|
10094
|
-
var
|
|
10956
|
+
var import_node_child_process10 = require("node:child_process");
|
|
10095
10957
|
var import_node_util6 = require("node:util");
|
|
10096
10958
|
|
|
10097
10959
|
// src/board-priority.ts
|
|
@@ -10199,7 +11061,7 @@ async function filterDependencyBlockedClaimables(items, client, opts = {}) {
|
|
|
10199
11061
|
var BOARD_STATUSES = ["Todo", "In Progress", "In Review", "Done"];
|
|
10200
11062
|
|
|
10201
11063
|
// src/board.ts
|
|
10202
|
-
var rawExecFileP3 = (0, import_node_util6.promisify)(
|
|
11064
|
+
var rawExecFileP3 = (0, import_node_util6.promisify)(import_node_child_process10.execFile);
|
|
10203
11065
|
var BOARD_GIT_TIMEOUT_MS = 1e4;
|
|
10204
11066
|
var WRITE_PROBE_CONCURRENCY = 8;
|
|
10205
11067
|
var CLAIM_CONCURRENCY = 5;
|
|
@@ -11028,16 +11890,16 @@ function ghError(e) {
|
|
|
11028
11890
|
}
|
|
11029
11891
|
|
|
11030
11892
|
// src/board-slice-cache.ts
|
|
11031
|
-
var
|
|
11032
|
-
var
|
|
11033
|
-
var BOARD_SLICE_CACHE_FILE = (0,
|
|
11893
|
+
var import_node_fs17 = require("node:fs");
|
|
11894
|
+
var import_node_path15 = require("node:path");
|
|
11895
|
+
var BOARD_SLICE_CACHE_FILE = (0, import_node_path15.join)(".mmi", "board-slice.json");
|
|
11034
11896
|
var BOARD_SLICE_CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
11035
11897
|
function boardSliceCachePath(cwd) {
|
|
11036
|
-
return (0,
|
|
11898
|
+
return (0, import_node_path15.join)(cwd, BOARD_SLICE_CACHE_FILE);
|
|
11037
11899
|
}
|
|
11038
11900
|
function readCachedBoardSlice(cwd) {
|
|
11039
11901
|
try {
|
|
11040
|
-
const parsed = JSON.parse((0,
|
|
11902
|
+
const parsed = JSON.parse((0, import_node_fs17.readFileSync)(boardSliceCachePath(cwd), "utf8"));
|
|
11041
11903
|
if (typeof parsed.ts !== "number") return null;
|
|
11042
11904
|
return { block: typeof parsed.block === "string" ? parsed.block : null, ts: parsed.ts };
|
|
11043
11905
|
} catch {
|
|
@@ -11048,12 +11910,12 @@ function writeCachedBoardSlice(cwd, slice) {
|
|
|
11048
11910
|
const path2 = boardSliceCachePath(cwd);
|
|
11049
11911
|
const tmp = `${path2}.${process.pid}.tmp`;
|
|
11050
11912
|
try {
|
|
11051
|
-
(0,
|
|
11052
|
-
(0,
|
|
11053
|
-
(0,
|
|
11913
|
+
(0, import_node_fs17.mkdirSync)((0, import_node_path15.dirname)(path2), { recursive: true });
|
|
11914
|
+
(0, import_node_fs17.writeFileSync)(tmp, JSON.stringify(slice));
|
|
11915
|
+
(0, import_node_fs17.renameSync)(tmp, path2);
|
|
11054
11916
|
} catch {
|
|
11055
11917
|
try {
|
|
11056
|
-
(0,
|
|
11918
|
+
(0, import_node_fs17.rmSync)(tmp, { force: true });
|
|
11057
11919
|
} catch {
|
|
11058
11920
|
}
|
|
11059
11921
|
}
|
|
@@ -11181,8 +12043,8 @@ async function refreshBoardSliceCache(deps) {
|
|
|
11181
12043
|
}
|
|
11182
12044
|
|
|
11183
12045
|
// src/worktree.ts
|
|
11184
|
-
var
|
|
11185
|
-
var
|
|
12046
|
+
var import_node_fs18 = require("node:fs");
|
|
12047
|
+
var import_node_path16 = require("node:path");
|
|
11186
12048
|
var LOCAL_ONLY_FILES = [".claude/settings.local.json"];
|
|
11187
12049
|
var PKG = "package.json";
|
|
11188
12050
|
var LOCKFILE = "package-lock.json";
|
|
@@ -11190,21 +12052,21 @@ var NODE_MODULES = "node_modules";
|
|
|
11190
12052
|
var realFsProbe = {
|
|
11191
12053
|
isDir: (p) => {
|
|
11192
12054
|
try {
|
|
11193
|
-
return (0,
|
|
12055
|
+
return (0, import_node_fs18.statSync)(p).isDirectory();
|
|
11194
12056
|
} catch {
|
|
11195
12057
|
return false;
|
|
11196
12058
|
}
|
|
11197
12059
|
},
|
|
11198
12060
|
isFile: (p) => {
|
|
11199
12061
|
try {
|
|
11200
|
-
return (0,
|
|
12062
|
+
return (0, import_node_fs18.statSync)(p).isFile();
|
|
11201
12063
|
} catch {
|
|
11202
12064
|
return false;
|
|
11203
12065
|
}
|
|
11204
12066
|
},
|
|
11205
12067
|
listDirs: (p) => {
|
|
11206
12068
|
try {
|
|
11207
|
-
return (0,
|
|
12069
|
+
return (0, import_node_fs18.readdirSync)(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
11208
12070
|
} catch {
|
|
11209
12071
|
return [];
|
|
11210
12072
|
}
|
|
@@ -11212,12 +12074,12 @@ var realFsProbe = {
|
|
|
11212
12074
|
};
|
|
11213
12075
|
function scanInstallDirs(root, fs2 = realFsProbe) {
|
|
11214
12076
|
const factsFor = (dir) => {
|
|
11215
|
-
const abs = dir ? (0,
|
|
12077
|
+
const abs = dir ? (0, import_node_path16.join)(root, dir) : root;
|
|
11216
12078
|
return {
|
|
11217
12079
|
dir,
|
|
11218
|
-
hasPackageJson: fs2.isFile((0,
|
|
11219
|
-
hasLockfile: fs2.isFile((0,
|
|
11220
|
-
hasNodeModules: fs2.isDir((0,
|
|
12080
|
+
hasPackageJson: fs2.isFile((0, import_node_path16.join)(abs, PKG)),
|
|
12081
|
+
hasLockfile: fs2.isFile((0, import_node_path16.join)(abs, LOCKFILE)),
|
|
12082
|
+
hasNodeModules: fs2.isDir((0, import_node_path16.join)(abs, NODE_MODULES))
|
|
11221
12083
|
};
|
|
11222
12084
|
};
|
|
11223
12085
|
const children = fs2.listDirs(root).filter((name) => name !== NODE_MODULES && name !== ".git");
|
|
@@ -11227,7 +12089,7 @@ function npmInstallTargets(dirs) {
|
|
|
11227
12089
|
return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install" }));
|
|
11228
12090
|
}
|
|
11229
12091
|
function isLinkedWorktree(root, fs2 = realFsProbe) {
|
|
11230
|
-
return fs2.isFile((0,
|
|
12092
|
+
return fs2.isFile((0, import_node_path16.join)(root, ".git"));
|
|
11231
12093
|
}
|
|
11232
12094
|
function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
|
|
11233
12095
|
if (!isLinkedWorktree(root, fs2)) return null;
|
|
@@ -11237,8 +12099,8 @@ function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
|
|
|
11237
12099
|
return `[worktree] provisioning tooling in the background (deps in ${where} + local config) \u2014 \`mmi-cli worktree setup\` to redo`;
|
|
11238
12100
|
}
|
|
11239
12101
|
function defaultCopyFile(from, to) {
|
|
11240
|
-
(0,
|
|
11241
|
-
(0,
|
|
12102
|
+
(0, import_node_fs18.mkdirSync)((0, import_node_path16.dirname)(to), { recursive: true });
|
|
12103
|
+
(0, import_node_fs18.copyFileSync)(from, to);
|
|
11242
12104
|
}
|
|
11243
12105
|
async function provisionWorktree(worktreeRoot, deps) {
|
|
11244
12106
|
const fs2 = deps.fs ?? realFsProbe;
|
|
@@ -11250,7 +12112,7 @@ async function provisionWorktree(worktreeRoot, deps) {
|
|
|
11250
12112
|
const skippedInstall = allDirs.filter((d) => d.hasPackageJson && d.hasNodeModules).map((d) => d.dir);
|
|
11251
12113
|
const installed = [];
|
|
11252
12114
|
for (const target of targets) {
|
|
11253
|
-
const cwd = target.dir ? (0,
|
|
12115
|
+
const cwd = target.dir ? (0, import_node_path16.join)(worktreeRoot, target.dir) : worktreeRoot;
|
|
11254
12116
|
log(`installing deps: ${target.command} in ${target.dir || "."}`);
|
|
11255
12117
|
await deps.runInstall(target.command, cwd);
|
|
11256
12118
|
installed.push(target);
|
|
@@ -11259,7 +12121,7 @@ async function provisionWorktree(worktreeRoot, deps) {
|
|
|
11259
12121
|
const copySkipped = [];
|
|
11260
12122
|
const primary = await deps.primaryCheckout();
|
|
11261
12123
|
for (const rel of LOCAL_ONLY_FILES) {
|
|
11262
|
-
const dest = (0,
|
|
12124
|
+
const dest = (0, import_node_path16.join)(worktreeRoot, rel);
|
|
11263
12125
|
if (fs2.isFile(dest)) {
|
|
11264
12126
|
copySkipped.push({ file: rel, reason: "already-present" });
|
|
11265
12127
|
continue;
|
|
@@ -11268,11 +12130,11 @@ async function provisionWorktree(worktreeRoot, deps) {
|
|
|
11268
12130
|
copySkipped.push({ file: rel, reason: "no-primary" });
|
|
11269
12131
|
continue;
|
|
11270
12132
|
}
|
|
11271
|
-
if (!fs2.isFile((0,
|
|
12133
|
+
if (!fs2.isFile((0, import_node_path16.join)(primary, rel))) {
|
|
11272
12134
|
copySkipped.push({ file: rel, reason: "absent-in-primary" });
|
|
11273
12135
|
continue;
|
|
11274
12136
|
}
|
|
11275
|
-
copyFile((0,
|
|
12137
|
+
copyFile((0, import_node_path16.join)(primary, rel), dest);
|
|
11276
12138
|
copied.push(rel);
|
|
11277
12139
|
log(`copied local config: ${rel}`);
|
|
11278
12140
|
}
|
|
@@ -11280,7 +12142,7 @@ async function provisionWorktree(worktreeRoot, deps) {
|
|
|
11280
12142
|
}
|
|
11281
12143
|
function defaultWorktreePath(repoRoot, branch) {
|
|
11282
12144
|
const safe = branch.replace(/[/\\]+/g, "-");
|
|
11283
|
-
return (0,
|
|
12145
|
+
return (0, import_node_path16.join)((0, import_node_path16.dirname)(repoRoot), "mmi-worktrees", safe);
|
|
11284
12146
|
}
|
|
11285
12147
|
function resolveWorktreeBase(from, remote) {
|
|
11286
12148
|
const remotePrefix = `${remote}/`;
|
|
@@ -11427,7 +12289,7 @@ function whoamiLine(report) {
|
|
|
11427
12289
|
}
|
|
11428
12290
|
|
|
11429
12291
|
// src/index.ts
|
|
11430
|
-
var
|
|
12292
|
+
var import_node_path26 = require("node:path");
|
|
11431
12293
|
|
|
11432
12294
|
// src/merge-ci-policy.ts
|
|
11433
12295
|
function resolveMergeCiPolicy(input) {
|
|
@@ -12206,14 +13068,14 @@ async function auditRepoCi(repo, deps) {
|
|
|
12206
13068
|
if (deployableGated) {
|
|
12207
13069
|
checks.push({
|
|
12208
13070
|
ok: hasGateWorkflow,
|
|
12209
|
-
label:
|
|
12210
|
-
detail: hasGateWorkflow ?
|
|
13071
|
+
label: `gate workflow committed on ${baseBranch}`,
|
|
13072
|
+
detail: hasGateWorkflow ? `read ${PRODUCT_GATE_PATH} at refs/heads/${baseBranch}` : `missing ${PRODUCT_GATE_PATH} at refs/heads/${baseBranch}`,
|
|
12211
13073
|
remediation: `mmi-cli bootstrap apply ${repo} --class deployable --execute (seeds gate.yml)`
|
|
12212
13074
|
});
|
|
12213
13075
|
checks.push({
|
|
12214
13076
|
ok: await contentExists(deps, repo, baseBranch, PRODUCT_RULESET_REF),
|
|
12215
|
-
label:
|
|
12216
|
-
detail: `
|
|
13077
|
+
label: `product ruleset reference committed on ${baseBranch}`,
|
|
13078
|
+
detail: `read ${PRODUCT_RULESET_REF} at refs/heads/${baseBranch}`,
|
|
12217
13079
|
remediation: `mmi-cli bootstrap apply ${repo} --class deployable --execute`
|
|
12218
13080
|
});
|
|
12219
13081
|
}
|
|
@@ -12609,8 +13471,8 @@ async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn =
|
|
|
12609
13471
|
|
|
12610
13472
|
// src/gh-create.ts
|
|
12611
13473
|
var import_promises5 = require("node:fs/promises");
|
|
12612
|
-
var
|
|
12613
|
-
var
|
|
13474
|
+
var import_node_os4 = require("node:os");
|
|
13475
|
+
var import_node_path17 = require("node:path");
|
|
12614
13476
|
var import_node_crypto5 = require("node:crypto");
|
|
12615
13477
|
var ISSUE_TYPES = ["bug", "feature", "task"];
|
|
12616
13478
|
var GH_MUTATION_TIMEOUT_MS = 12e4;
|
|
@@ -12651,7 +13513,7 @@ async function bodyArgsViaFile(args, deps = {}) {
|
|
|
12651
13513
|
} };
|
|
12652
13514
|
const write = deps.write ?? import_promises5.writeFile;
|
|
12653
13515
|
const remove = deps.remove ?? import_promises5.unlink;
|
|
12654
|
-
const file = (0,
|
|
13516
|
+
const file = (0, import_node_path17.join)(deps.dir ?? (0, import_node_os4.tmpdir)(), `mmi-gh-body-${process.pid}-${(0, import_node_crypto5.randomBytes)(4).toString("hex")}.md`);
|
|
12655
13517
|
await write(file, args[i + 1], "utf8");
|
|
12656
13518
|
return {
|
|
12657
13519
|
args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
|
|
@@ -12696,23 +13558,109 @@ function buildPrArgs({ title, body, base, head, repo }) {
|
|
|
12696
13558
|
return args;
|
|
12697
13559
|
}
|
|
12698
13560
|
|
|
12699
|
-
// src/issue
|
|
12700
|
-
|
|
12701
|
-
|
|
12702
|
-
const
|
|
12703
|
-
|
|
12704
|
-
|
|
12705
|
-
|
|
12706
|
-
|
|
12707
|
-
|
|
12708
|
-
|
|
12709
|
-
|
|
12710
|
-
|
|
12711
|
-
|
|
12712
|
-
|
|
12713
|
-
return
|
|
12714
|
-
}
|
|
12715
|
-
function
|
|
13561
|
+
// src/sub-issue.ts
|
|
13562
|
+
function parseIssueRef(ref) {
|
|
13563
|
+
const trimmed = ref.trim();
|
|
13564
|
+
const url = trimmed.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)$/i);
|
|
13565
|
+
if (url) return { repo: url[1], number: Number(url[2]) };
|
|
13566
|
+
const qualified = trimmed.match(/^([^/\s#]+\/[^/\s#]+)#(\d+)$/);
|
|
13567
|
+
if (qualified) return { repo: qualified[1], number: Number(qualified[2]) };
|
|
13568
|
+
const bare = trimmed.match(/^#?(\d+)$/);
|
|
13569
|
+
if (bare) return { number: Number(bare[1]) };
|
|
13570
|
+
throw new Error(`invalid issue reference "${ref}" \u2014 expected #123, 123, owner/repo#123, or an issue URL`);
|
|
13571
|
+
}
|
|
13572
|
+
function buildResolveIdArgs(ref) {
|
|
13573
|
+
const args = ["issue", "view", String(ref.number), "--json", "id", "--jq", ".id"];
|
|
13574
|
+
if (ref.repo) args.push("--repo", ref.repo);
|
|
13575
|
+
return args;
|
|
13576
|
+
}
|
|
13577
|
+
function buildAddSubIssueArgs(parentId, subIssueId) {
|
|
13578
|
+
if (!parentId) throw new Error("addSubIssue: parentId is required");
|
|
13579
|
+
if (!subIssueId) throw new Error("addSubIssue: subIssueId is required");
|
|
13580
|
+
return [
|
|
13581
|
+
"api",
|
|
13582
|
+
"graphql",
|
|
13583
|
+
"-f",
|
|
13584
|
+
"query=mutation($p:ID!,$c:ID!){addSubIssue(input:{issueId:$p,subIssueId:$c}){issue{number subIssues{totalCount}} subIssue{number}}}",
|
|
13585
|
+
"-f",
|
|
13586
|
+
`p=${parentId}`,
|
|
13587
|
+
"-f",
|
|
13588
|
+
`c=${subIssueId}`
|
|
13589
|
+
];
|
|
13590
|
+
}
|
|
13591
|
+
function parseAddSubIssueResult(stdout) {
|
|
13592
|
+
try {
|
|
13593
|
+
const issue2 = JSON.parse(stdout)?.data?.addSubIssue;
|
|
13594
|
+
const parentNumber = issue2?.issue?.number;
|
|
13595
|
+
const subIssueNumber = issue2?.subIssue?.number;
|
|
13596
|
+
const totalCount = issue2?.issue?.subIssues?.totalCount;
|
|
13597
|
+
if (typeof parentNumber !== "number" || typeof subIssueNumber !== "number") return void 0;
|
|
13598
|
+
return { parentNumber, subIssueNumber, totalCount: typeof totalCount === "number" ? totalCount : 0 };
|
|
13599
|
+
} catch {
|
|
13600
|
+
return void 0;
|
|
13601
|
+
}
|
|
13602
|
+
}
|
|
13603
|
+
var RESOLVE_ID_TIMEOUT_MS = 1e4;
|
|
13604
|
+
async function resolveIssueNodeId(runGh, ref, fallbackRepo) {
|
|
13605
|
+
const resolved = ref.repo ? ref : { ...ref, repo: fallbackRepo };
|
|
13606
|
+
const id = (await runGh(buildResolveIdArgs(resolved), RESOLVE_ID_TIMEOUT_MS)).trim();
|
|
13607
|
+
if (!id) throw new Error(`could not resolve node id for issue #${ref.number}${resolved.repo ? ` in ${resolved.repo}` : ""}`);
|
|
13608
|
+
return id;
|
|
13609
|
+
}
|
|
13610
|
+
async function linkSubIssue(runGh, parentRef, childRef, defaultRepo) {
|
|
13611
|
+
const parent = parseIssueRef(parentRef);
|
|
13612
|
+
const child = parseIssueRef(childRef);
|
|
13613
|
+
const parentId = await resolveIssueNodeId(runGh, parent, defaultRepo);
|
|
13614
|
+
const subIssueId = await resolveIssueNodeId(runGh, child, defaultRepo);
|
|
13615
|
+
const stdout = await runGh(buildAddSubIssueArgs(parentId, subIssueId), GH_MUTATION_TIMEOUT_MS);
|
|
13616
|
+
const result = parseAddSubIssueResult(stdout);
|
|
13617
|
+
if (!result) throw new Error(`addSubIssue returned an unexpected response:
|
|
13618
|
+
${stdout.trim() || "(empty)"}`);
|
|
13619
|
+
return result;
|
|
13620
|
+
}
|
|
13621
|
+
function parentLinkFields(result, error) {
|
|
13622
|
+
if (result) return { parent: result };
|
|
13623
|
+
if (error) return { parentLinkError: error };
|
|
13624
|
+
return {};
|
|
13625
|
+
}
|
|
13626
|
+
|
|
13627
|
+
// src/issue-comment.ts
|
|
13628
|
+
async function postIssueComment(client, input) {
|
|
13629
|
+
const parsed = parseIssueRef(input.ref);
|
|
13630
|
+
const repo = parsed.repo ?? input.defaultRepo;
|
|
13631
|
+
if (!repo) throw new Error("could not resolve repo \u2014 pass --repo owner/repo");
|
|
13632
|
+
if (input.body.trim().length === 0) throw new Error("comment body is empty");
|
|
13633
|
+
const comment = await client.rest(
|
|
13634
|
+
"POST",
|
|
13635
|
+
`repos/${repo}/issues/${parsed.number}/comments`,
|
|
13636
|
+
{ body: { body: input.body } }
|
|
13637
|
+
);
|
|
13638
|
+
if (!comment?.html_url) throw new Error("GitHub did not return a comment URL");
|
|
13639
|
+
return {
|
|
13640
|
+
number: parsed.number,
|
|
13641
|
+
repo,
|
|
13642
|
+
url: `https://github.com/${repo}/issues/${parsed.number}`,
|
|
13643
|
+
commentUrl: comment.html_url
|
|
13644
|
+
};
|
|
13645
|
+
}
|
|
13646
|
+
|
|
13647
|
+
// src/issue-check.ts
|
|
13648
|
+
var CHECKLIST_RE = /^([ \t]*[-*+] \[)([ xX])(\] )(.*)$/gm;
|
|
13649
|
+
function findChecklistItems(body) {
|
|
13650
|
+
const items = [];
|
|
13651
|
+
for (const m of body.matchAll(CHECKLIST_RE)) {
|
|
13652
|
+
const prefix = m[1];
|
|
13653
|
+
const marker = m[2];
|
|
13654
|
+
const text = m[4].replace(/\r$/, "");
|
|
13655
|
+
items.push({
|
|
13656
|
+
markerIndex: (m.index ?? 0) + prefix.length,
|
|
13657
|
+
checked: marker.toLowerCase() === "x",
|
|
13658
|
+
text
|
|
13659
|
+
});
|
|
13660
|
+
}
|
|
13661
|
+
return items;
|
|
13662
|
+
}
|
|
13663
|
+
function selectChecklistItem(items, query) {
|
|
12716
13664
|
const q = query.trim();
|
|
12717
13665
|
if (!q) return { ok: false, reason: "not-found" };
|
|
12718
13666
|
const exact = items.filter((it) => it.text.trim() === q);
|
|
@@ -12978,72 +13926,6 @@ Related work discovered by mmi-cli:
|
|
|
12978
13926
|
${lines.join("\n")}`;
|
|
12979
13927
|
}
|
|
12980
13928
|
|
|
12981
|
-
// src/sub-issue.ts
|
|
12982
|
-
function parseIssueRef(ref) {
|
|
12983
|
-
const trimmed = ref.trim();
|
|
12984
|
-
const url = trimmed.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)$/i);
|
|
12985
|
-
if (url) return { repo: url[1], number: Number(url[2]) };
|
|
12986
|
-
const qualified = trimmed.match(/^([^/\s#]+\/[^/\s#]+)#(\d+)$/);
|
|
12987
|
-
if (qualified) return { repo: qualified[1], number: Number(qualified[2]) };
|
|
12988
|
-
const bare = trimmed.match(/^#?(\d+)$/);
|
|
12989
|
-
if (bare) return { number: Number(bare[1]) };
|
|
12990
|
-
throw new Error(`invalid issue reference "${ref}" \u2014 expected #123, 123, owner/repo#123, or an issue URL`);
|
|
12991
|
-
}
|
|
12992
|
-
function buildResolveIdArgs(ref) {
|
|
12993
|
-
const args = ["issue", "view", String(ref.number), "--json", "id", "--jq", ".id"];
|
|
12994
|
-
if (ref.repo) args.push("--repo", ref.repo);
|
|
12995
|
-
return args;
|
|
12996
|
-
}
|
|
12997
|
-
function buildAddSubIssueArgs(parentId, subIssueId) {
|
|
12998
|
-
if (!parentId) throw new Error("addSubIssue: parentId is required");
|
|
12999
|
-
if (!subIssueId) throw new Error("addSubIssue: subIssueId is required");
|
|
13000
|
-
return [
|
|
13001
|
-
"api",
|
|
13002
|
-
"graphql",
|
|
13003
|
-
"-f",
|
|
13004
|
-
"query=mutation($p:ID!,$c:ID!){addSubIssue(input:{issueId:$p,subIssueId:$c}){issue{number subIssues{totalCount}} subIssue{number}}}",
|
|
13005
|
-
"-f",
|
|
13006
|
-
`p=${parentId}`,
|
|
13007
|
-
"-f",
|
|
13008
|
-
`c=${subIssueId}`
|
|
13009
|
-
];
|
|
13010
|
-
}
|
|
13011
|
-
function parseAddSubIssueResult(stdout) {
|
|
13012
|
-
try {
|
|
13013
|
-
const issue2 = JSON.parse(stdout)?.data?.addSubIssue;
|
|
13014
|
-
const parentNumber = issue2?.issue?.number;
|
|
13015
|
-
const subIssueNumber = issue2?.subIssue?.number;
|
|
13016
|
-
const totalCount = issue2?.issue?.subIssues?.totalCount;
|
|
13017
|
-
if (typeof parentNumber !== "number" || typeof subIssueNumber !== "number") return void 0;
|
|
13018
|
-
return { parentNumber, subIssueNumber, totalCount: typeof totalCount === "number" ? totalCount : 0 };
|
|
13019
|
-
} catch {
|
|
13020
|
-
return void 0;
|
|
13021
|
-
}
|
|
13022
|
-
}
|
|
13023
|
-
var RESOLVE_ID_TIMEOUT_MS = 1e4;
|
|
13024
|
-
async function resolveIssueNodeId(runGh, ref, fallbackRepo) {
|
|
13025
|
-
const resolved = ref.repo ? ref : { ...ref, repo: fallbackRepo };
|
|
13026
|
-
const id = (await runGh(buildResolveIdArgs(resolved), RESOLVE_ID_TIMEOUT_MS)).trim();
|
|
13027
|
-
if (!id) throw new Error(`could not resolve node id for issue #${ref.number}${resolved.repo ? ` in ${resolved.repo}` : ""}`);
|
|
13028
|
-
return id;
|
|
13029
|
-
}
|
|
13030
|
-
async function linkSubIssue(runGh, parentRef, childRef, defaultRepo) {
|
|
13031
|
-
const parent = parseIssueRef(parentRef);
|
|
13032
|
-
const child = parseIssueRef(childRef);
|
|
13033
|
-
const parentId = await resolveIssueNodeId(runGh, parent, defaultRepo);
|
|
13034
|
-
const subIssueId = await resolveIssueNodeId(runGh, child, defaultRepo);
|
|
13035
|
-
const stdout = await runGh(buildAddSubIssueArgs(parentId, subIssueId), GH_MUTATION_TIMEOUT_MS);
|
|
13036
|
-
const result = parseAddSubIssueResult(stdout);
|
|
13037
|
-
if (!result) throw new Error(`addSubIssue returned an unexpected response:
|
|
13038
|
-
${stdout.trim() || "(empty)"}`);
|
|
13039
|
-
return result;
|
|
13040
|
-
}
|
|
13041
|
-
function parentLinkFields(result, error) {
|
|
13042
|
-
if (result) return { parent: result };
|
|
13043
|
-
if (error) return { parentLinkError: error };
|
|
13044
|
-
return {};
|
|
13045
|
-
}
|
|
13046
|
-
|
|
13047
13929
|
// src/report.ts
|
|
13048
13930
|
var HUB_REPO3 = "mutmutco/MMI-Hub";
|
|
13049
13931
|
var REPORT_LABEL = "report";
|
|
@@ -13080,7 +13962,7 @@ ${buildReportBody(body, sourceRepo)}`;
|
|
|
13080
13962
|
|
|
13081
13963
|
// src/skill-lesson.ts
|
|
13082
13964
|
var SKILL_LESSON_LABEL = "skill-lesson";
|
|
13083
|
-
var SKILL_NAMES = ["bootstrap", "browser-automation", "build", "coop", "grind", "handoff", "hotfix", "mmi", "rcand", "release", "secrets", "stage"];
|
|
13965
|
+
var SKILL_NAMES = ["bootstrap", "browser-automation", "build", "coop", "grind", "handoff", "hotfix", "mmi", "overlord", "rcand", "release", "secrets", "stage"];
|
|
13084
13966
|
function assertSkillName(name) {
|
|
13085
13967
|
const match = SKILL_NAMES.find((skill) => skill === name);
|
|
13086
13968
|
if (!match) throw new Error(`unknown skill "${name}" \u2014 expected one of: ${SKILL_NAMES.join(", ")}`);
|
|
@@ -13745,8 +14627,8 @@ async function runStageLiveDown(deps, t) {
|
|
|
13745
14627
|
}
|
|
13746
14628
|
|
|
13747
14629
|
// src/design-system.ts
|
|
13748
|
-
var
|
|
13749
|
-
var
|
|
14630
|
+
var import_node_fs19 = require("node:fs");
|
|
14631
|
+
var import_node_path18 = require("node:path");
|
|
13750
14632
|
var UI_PACKAGE_CANDIDATES = ["@mutmutco/ui-dashboard", "@mutmutco/ui", "@mutmutco/theme"];
|
|
13751
14633
|
var DESIGN_SYSTEM_VERSION_LABEL = "@mutmutco design-system npm package (vs @latest)";
|
|
13752
14634
|
function dashboardConsumerRegistryFix(error) {
|
|
@@ -13795,17 +14677,17 @@ function buildDesignSystemVersionCheck(input) {
|
|
|
13795
14677
|
}
|
|
13796
14678
|
function readJsonFile(path2) {
|
|
13797
14679
|
try {
|
|
13798
|
-
return JSON.parse((0,
|
|
14680
|
+
return JSON.parse((0, import_node_fs19.readFileSync)(path2, "utf8"));
|
|
13799
14681
|
} catch {
|
|
13800
14682
|
return void 0;
|
|
13801
14683
|
}
|
|
13802
14684
|
}
|
|
13803
14685
|
function isUiFactoryCheckout(root) {
|
|
13804
|
-
const pkg = readJsonFile((0,
|
|
14686
|
+
const pkg = readJsonFile((0, import_node_path18.join)(root, "package.json"));
|
|
13805
14687
|
return pkg?.name === "mmd-ui" && pkg?.private === true;
|
|
13806
14688
|
}
|
|
13807
14689
|
function resolveDeclaredUiPackage(root) {
|
|
13808
|
-
const pkg = readJsonFile((0,
|
|
14690
|
+
const pkg = readJsonFile((0, import_node_path18.join)(root, "package.json"));
|
|
13809
14691
|
if (!pkg) return void 0;
|
|
13810
14692
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
13811
14693
|
for (const name of UI_PACKAGE_CANDIDATES) {
|
|
@@ -13815,8 +14697,8 @@ function resolveDeclaredUiPackage(root) {
|
|
|
13815
14697
|
return void 0;
|
|
13816
14698
|
}
|
|
13817
14699
|
function readLockfileInstalledVersion(root, packageName) {
|
|
13818
|
-
const lockPath = (0,
|
|
13819
|
-
if (!(0,
|
|
14700
|
+
const lockPath = (0, import_node_path18.join)(root, "package-lock.json");
|
|
14701
|
+
if (!(0, import_node_fs19.existsSync)(lockPath)) return void 0;
|
|
13820
14702
|
const lock = readJsonFile(lockPath);
|
|
13821
14703
|
const node = lock?.packages?.[`node_modules/${packageName}`];
|
|
13822
14704
|
const version = node?.version?.trim();
|
|
@@ -13845,18 +14727,8 @@ function designSystemSnapshot(root) {
|
|
|
13845
14727
|
|
|
13846
14728
|
// src/design-system-registry.ts
|
|
13847
14729
|
var import_node_crypto6 = require("node:crypto");
|
|
13848
|
-
var
|
|
13849
|
-
var
|
|
13850
|
-
|
|
13851
|
-
// src/atomic-write.ts
|
|
13852
|
-
var import_node_fs18 = require("node:fs");
|
|
13853
|
-
function atomicWriteFileSync(path2, content) {
|
|
13854
|
-
const tmp = `${path2}.${process.pid}.tmp`;
|
|
13855
|
-
(0, import_node_fs18.writeFileSync)(tmp, content, "utf8");
|
|
13856
|
-
(0, import_node_fs18.renameSync)(tmp, path2);
|
|
13857
|
-
}
|
|
13858
|
-
|
|
13859
|
-
// src/design-system-registry.ts
|
|
14730
|
+
var import_node_fs20 = require("node:fs");
|
|
14731
|
+
var import_node_path19 = require("node:path");
|
|
13860
14732
|
var DESIGN_SYSTEM_CACHE_DIR = ".mmi/design-system/components";
|
|
13861
14733
|
var DESIGN_SYSTEM_MANIFEST_PATH = ".mmi/design-system/manifest.json";
|
|
13862
14734
|
var REGISTRY_COMPONENTS_LABEL = "@mutmutco registry components (.mmi cache vs live registry)";
|
|
@@ -13864,13 +14736,13 @@ var REGISTRY_FIX = "run `mmi-cli doctor --apply` to pull registry components int
|
|
|
13864
14736
|
var REGISTRY_UNREACHABLE_FIX = "live @mutmutco registry unreachable \u2014 verify `components.json` `@mutmutco` registry URL and network, then retry `mmi-cli doctor`";
|
|
13865
14737
|
function readJsonFile2(path2) {
|
|
13866
14738
|
try {
|
|
13867
|
-
return JSON.parse((0,
|
|
14739
|
+
return JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
|
|
13868
14740
|
} catch {
|
|
13869
14741
|
return void 0;
|
|
13870
14742
|
}
|
|
13871
14743
|
}
|
|
13872
14744
|
function readComponentsJson(root) {
|
|
13873
|
-
return readJsonFile2((0,
|
|
14745
|
+
return readJsonFile2((0, import_node_path19.join)(root, "components.json"));
|
|
13874
14746
|
}
|
|
13875
14747
|
function hasMutmutcoRegistry(root) {
|
|
13876
14748
|
const url = readComponentsJson(root)?.registries?.["@mutmutco"];
|
|
@@ -13878,7 +14750,7 @@ function hasMutmutcoRegistry(root) {
|
|
|
13878
14750
|
}
|
|
13879
14751
|
function resolveCacheDir(root) {
|
|
13880
14752
|
const custom = readComponentsJson(root)?.mmi?.cacheDir;
|
|
13881
|
-
return (0,
|
|
14753
|
+
return (0, import_node_path19.join)(root, custom ?? DESIGN_SYSTEM_CACHE_DIR);
|
|
13882
14754
|
}
|
|
13883
14755
|
function resolveRegistryUrlTemplate(root) {
|
|
13884
14756
|
return readComponentsJson(root)?.registries?.["@mutmutco"];
|
|
@@ -13887,7 +14759,7 @@ function registryItemUrl(template, name) {
|
|
|
13887
14759
|
return template.replace("{name}", name);
|
|
13888
14760
|
}
|
|
13889
14761
|
function readDesignSystemManifest(root) {
|
|
13890
|
-
const raw = readJsonFile2((0,
|
|
14762
|
+
const raw = readJsonFile2((0, import_node_path19.join)(root, DESIGN_SYSTEM_MANIFEST_PATH));
|
|
13891
14763
|
if (!raw || !Array.isArray(raw.components)) return void 0;
|
|
13892
14764
|
return raw;
|
|
13893
14765
|
}
|
|
@@ -13899,11 +14771,11 @@ function listInstalledRegistryComponents(root) {
|
|
|
13899
14771
|
return scanCachedComponentNames(resolveCacheDir(root));
|
|
13900
14772
|
}
|
|
13901
14773
|
function scanCachedComponentNames(cacheDir) {
|
|
13902
|
-
if (!(0,
|
|
14774
|
+
if (!(0, import_node_fs20.existsSync)(cacheDir)) return [];
|
|
13903
14775
|
const names = /* @__PURE__ */ new Set();
|
|
13904
14776
|
const walk = (dir) => {
|
|
13905
|
-
for (const ent of (0,
|
|
13906
|
-
const p = (0,
|
|
14777
|
+
for (const ent of (0, import_node_fs20.readdirSync)(dir, { withFileTypes: true })) {
|
|
14778
|
+
const p = (0, import_node_path19.join)(dir, ent.name);
|
|
13907
14779
|
if (ent.isDirectory()) walk(p);
|
|
13908
14780
|
else if (ent.isFile() && /\.(tsx?|jsx?)$/.test(ent.name)) {
|
|
13909
14781
|
names.add(ent.name.replace(/\.(tsx|ts|jsx|js)$/, ""));
|
|
@@ -13992,13 +14864,13 @@ async function gatherRegistryComponentsState(root, targetVersion, deps) {
|
|
|
13992
14864
|
let componentStale = false;
|
|
13993
14865
|
for (const file of item.files) {
|
|
13994
14866
|
if (!file.target || file.content == null) continue;
|
|
13995
|
-
const cachePath = (0,
|
|
13996
|
-
if (!(0,
|
|
14867
|
+
const cachePath = (0, import_node_path19.join)(cacheDir, cacheRelativePath(file.target));
|
|
14868
|
+
if (!(0, import_node_fs20.existsSync)(cachePath)) {
|
|
13997
14869
|
componentStale = true;
|
|
13998
14870
|
break;
|
|
13999
14871
|
}
|
|
14000
14872
|
try {
|
|
14001
|
-
if (contentHash((0,
|
|
14873
|
+
if (contentHash((0, import_node_fs20.readFileSync)(cachePath, "utf8")) !== contentHash(file.content)) {
|
|
14002
14874
|
componentStale = true;
|
|
14003
14875
|
break;
|
|
14004
14876
|
}
|
|
@@ -14008,7 +14880,7 @@ async function gatherRegistryComponentsState(root, targetVersion, deps) {
|
|
|
14008
14880
|
}
|
|
14009
14881
|
}
|
|
14010
14882
|
if (componentStale) {
|
|
14011
|
-
if ((0,
|
|
14883
|
+
if ((0, import_node_fs20.existsSync)((0, import_node_path19.join)(cacheDir, "ui", `${name}.tsx`)) || (0, import_node_fs20.existsSync)((0, import_node_path19.join)(cacheDir, `${name}.tsx`))) {
|
|
14012
14884
|
stale.push(name);
|
|
14013
14885
|
} else {
|
|
14014
14886
|
missing.push(name);
|
|
@@ -14036,15 +14908,15 @@ async function applyRegistryComponentsSync(root, components, targetVersion, log,
|
|
|
14036
14908
|
if (!item) return { ok: false };
|
|
14037
14909
|
for (const file of item.files) {
|
|
14038
14910
|
if (!file.target || file.content == null) continue;
|
|
14039
|
-
const outPath = (0,
|
|
14040
|
-
deps.mkdir((0,
|
|
14911
|
+
const outPath = (0, import_node_path19.join)(cacheDir, cacheRelativePath(file.target));
|
|
14912
|
+
deps.mkdir((0, import_node_path19.dirname)(outPath));
|
|
14041
14913
|
const body = file.content.endsWith("\n") ? file.content : `${file.content}
|
|
14042
14914
|
`;
|
|
14043
14915
|
deps.writeFile(outPath, body);
|
|
14044
14916
|
}
|
|
14045
14917
|
}
|
|
14046
|
-
const manifestPath = (0,
|
|
14047
|
-
deps.mkdir((0,
|
|
14918
|
+
const manifestPath = (0, import_node_path19.join)(root, DESIGN_SYSTEM_MANIFEST_PATH);
|
|
14919
|
+
deps.mkdir((0, import_node_path19.dirname)(manifestPath));
|
|
14048
14920
|
const manifest = {
|
|
14049
14921
|
version: targetVersion,
|
|
14050
14922
|
syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -14059,7 +14931,7 @@ function defaultRegistrySyncDeps() {
|
|
|
14059
14931
|
return {
|
|
14060
14932
|
fetch,
|
|
14061
14933
|
writeFile: (path2, content) => atomicWriteFileSync(path2, content),
|
|
14062
|
-
mkdir: (path2) => (0,
|
|
14934
|
+
mkdir: (path2) => (0, import_node_fs20.mkdirSync)(path2, { recursive: true })
|
|
14063
14935
|
};
|
|
14064
14936
|
}
|
|
14065
14937
|
|
|
@@ -14085,12 +14957,12 @@ function renderVerifySecrets(body) {
|
|
|
14085
14957
|
}
|
|
14086
14958
|
|
|
14087
14959
|
// src/hotfix-coverage.ts
|
|
14088
|
-
var
|
|
14960
|
+
var import_node_child_process11 = require("node:child_process");
|
|
14089
14961
|
var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
|
|
14090
14962
|
function checkHotfixCoverage(options = {}) {
|
|
14091
14963
|
const { cwd = process.cwd(), mainRef = "origin/main", rcRef = "origin/rc", manifestPaths = [] } = options;
|
|
14092
14964
|
const ack = (options.ack ?? []).filter(Boolean);
|
|
14093
|
-
const git = options.git ?? ((args, opts) => (0,
|
|
14965
|
+
const git = options.git ?? ((args, opts) => (0, import_node_child_process11.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
|
|
14094
14966
|
const revList = (range) => {
|
|
14095
14967
|
const out = git(["rev-list", "--no-merges", range]).trim();
|
|
14096
14968
|
return out ? out.split("\n") : [];
|
|
@@ -14813,8 +15685,8 @@ async function announceRelease(deps, args) {
|
|
|
14813
15685
|
}
|
|
14814
15686
|
|
|
14815
15687
|
// src/port-registry.ts
|
|
14816
|
-
var
|
|
14817
|
-
var
|
|
15688
|
+
var import_node_fs21 = require("node:fs");
|
|
15689
|
+
var import_node_path20 = require("node:path");
|
|
14818
15690
|
|
|
14819
15691
|
// ../infra/port-geometry.mjs
|
|
14820
15692
|
var PORT_BLOCK = 100;
|
|
@@ -14828,8 +15700,8 @@ function nextPortBlock(registry2) {
|
|
|
14828
15700
|
return [base, base + PORT_SPAN];
|
|
14829
15701
|
}
|
|
14830
15702
|
function loadPortRegistry(path2) {
|
|
14831
|
-
if (!(0,
|
|
14832
|
-
const raw = JSON.parse((0,
|
|
15703
|
+
if (!(0, import_node_fs21.existsSync)(path2)) return {};
|
|
15704
|
+
const raw = JSON.parse((0, import_node_fs21.readFileSync)(path2, "utf8"));
|
|
14833
15705
|
const out = {};
|
|
14834
15706
|
for (const [key, value] of Object.entries(raw)) {
|
|
14835
15707
|
if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
|
|
@@ -14843,9 +15715,9 @@ function ensurePortRange(repo, path2) {
|
|
|
14843
15715
|
const existing = registry2[repo];
|
|
14844
15716
|
if (existing) return existing;
|
|
14845
15717
|
const range = nextPortBlock(registry2);
|
|
14846
|
-
const raw = (0,
|
|
15718
|
+
const raw = (0, import_node_fs21.existsSync)(path2) ? JSON.parse((0, import_node_fs21.readFileSync)(path2, "utf8")) : {};
|
|
14847
15719
|
raw[repo] = range;
|
|
14848
|
-
(0,
|
|
15720
|
+
(0, import_node_fs21.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
|
|
14849
15721
|
return range;
|
|
14850
15722
|
}
|
|
14851
15723
|
function portCursorSeed(registry2) {
|
|
@@ -14867,18 +15739,18 @@ function existingPortRange(repo, registry2) {
|
|
|
14867
15739
|
return registry2[repo] ?? null;
|
|
14868
15740
|
}
|
|
14869
15741
|
function portRangeInfraAt(root, source) {
|
|
14870
|
-
const registryPath = (0,
|
|
14871
|
-
const ddbScriptPath = (0,
|
|
14872
|
-
if (!(0,
|
|
15742
|
+
const registryPath = (0, import_node_path20.join)(root, "infra", "port-ranges.json");
|
|
15743
|
+
const ddbScriptPath = (0, import_node_path20.join)(root, "infra", "port-ddb.mjs");
|
|
15744
|
+
if (!(0, import_node_fs21.existsSync)(registryPath) || !(0, import_node_fs21.existsSync)(ddbScriptPath)) return null;
|
|
14873
15745
|
return { root, source, registryPath, ddbScriptPath };
|
|
14874
15746
|
}
|
|
14875
15747
|
function resolvePortRangeInfra(cwd) {
|
|
14876
15748
|
const direct = portRangeInfraAt(cwd, "cwd");
|
|
14877
15749
|
if (direct) return direct;
|
|
14878
|
-
for (let dir = cwd; ; dir = (0,
|
|
14879
|
-
const sibling = portRangeInfraAt((0,
|
|
15750
|
+
for (let dir = cwd; ; dir = (0, import_node_path20.dirname)(dir)) {
|
|
15751
|
+
const sibling = portRangeInfraAt((0, import_node_path20.join)(dir, "MMI-Hub"), "sibling-hub");
|
|
14880
15752
|
if (sibling) return sibling;
|
|
14881
|
-
const parent = (0,
|
|
15753
|
+
const parent = (0, import_node_path20.dirname)(dir);
|
|
14882
15754
|
if (parent === dir) return null;
|
|
14883
15755
|
}
|
|
14884
15756
|
}
|
|
@@ -14903,7 +15775,8 @@ var OVERGRANT_ROLES = /* @__PURE__ */ new Set(["admin", "maintain"]);
|
|
|
14903
15775
|
var REQUIRED_DATA_ACCESS = {
|
|
14904
15776
|
"mutmutco/MM-Chat": [{ name: "kb-projection-reader", dbRole: "kb_reader", vaultParamNeedle: "KB_READ_DB_URL" }]
|
|
14905
15777
|
};
|
|
14906
|
-
function lockedBranches(repoClass) {
|
|
15778
|
+
function lockedBranches(repoClass, releaseTrack) {
|
|
15779
|
+
if (releaseTrack) return branchesForTrack(releaseTrack);
|
|
14907
15780
|
return repoClass === "content" ? ["main"] : ["development", "rc", "main"];
|
|
14908
15781
|
}
|
|
14909
15782
|
function safeJson(text, fallback) {
|
|
@@ -15030,14 +15903,15 @@ function auditDataAccessContracts(repo, contracts = { consumers: {} }) {
|
|
|
15030
15903
|
}
|
|
15031
15904
|
return findings;
|
|
15032
15905
|
}
|
|
15033
|
-
async function auditRepoAccess(repo, repoClass, owners, deps, projectAdmins = /* @__PURE__ */ new Set(), dataAccess) {
|
|
15906
|
+
async function auditRepoAccess(repo, repoClass, owners, deps, projectAdmins = /* @__PURE__ */ new Set(), dataAccess, releaseTrack) {
|
|
15034
15907
|
const findings = [];
|
|
15035
15908
|
findings.push(...await auditRepoCollaborators(repo, owners, deps));
|
|
15036
15909
|
if (dataAccess) findings.push(...auditDataAccessContracts(repo, dataAccess));
|
|
15037
|
-
|
|
15910
|
+
const track = releaseTrack ?? (repoClass === "content" ? "trunk" : void 0);
|
|
15911
|
+
for (const branch of lockedBranches(repoClass, track)) {
|
|
15038
15912
|
findings.push(...await auditTrainBranch(repo, branch, owners, deps, projectAdmins));
|
|
15039
15913
|
}
|
|
15040
|
-
return { repo, class: repoClass, ok: !findings.some((f) => f.severity === "high"), findings };
|
|
15914
|
+
return { repo, class: repoClass, releaseTrack: track, ok: !findings.some((f) => f.severity === "high"), findings };
|
|
15041
15915
|
}
|
|
15042
15916
|
async function auditOrgBasePermission(deps) {
|
|
15043
15917
|
const org = await restJson2(deps, `orgs/${OWNER}`, {});
|
|
@@ -15058,7 +15932,7 @@ async function auditOrgAccess(targets, deps, matrix = {}, dataAccess) {
|
|
|
15058
15932
|
const orgFindings = await auditOrgBasePermission(deps);
|
|
15059
15933
|
const repos = [];
|
|
15060
15934
|
for (const target of targets) {
|
|
15061
|
-
repos.push(await auditRepoAccess(target.repo, target.class, owners, deps, new Set(entriesValueByCanonicalRepo(matrix, target.repo) ?? []), dataAccess));
|
|
15935
|
+
repos.push(await auditRepoAccess(target.repo, target.class, owners, deps, new Set(entriesValueByCanonicalRepo(matrix, target.repo) ?? []), dataAccess, target.releaseTrack));
|
|
15062
15936
|
}
|
|
15063
15937
|
const ok = orgFindings.every((f) => f.severity !== "high") && repos.every((r) => r.ok);
|
|
15064
15938
|
return { ok, owners: [...owners], orgFindings, repos };
|
|
@@ -15078,7 +15952,8 @@ function loadAccessTargets(projectsJson, fanoutJson) {
|
|
|
15078
15952
|
seen.add(repo);
|
|
15079
15953
|
const repoName = repo.split("/").pop()?.toLowerCase() ?? repo.toLowerCase();
|
|
15080
15954
|
const cls = project2.class === "content" || project2.deployModel === "content" || embeddedContent.has(repo.toLowerCase()) || embeddedContent.has(repoName) || legacyContentNames.has(repo.toLowerCase()) || legacyContentNames.has(repoName) ? "content" : "deployable";
|
|
15081
|
-
|
|
15955
|
+
const releaseTrack = cls === "content" ? "trunk" : resolveReleaseTrack(project2, void 0, repo);
|
|
15956
|
+
targets.push({ repo, class: cls, releaseTrack });
|
|
15082
15957
|
}
|
|
15083
15958
|
}
|
|
15084
15959
|
return targets;
|
|
@@ -15128,7 +16003,7 @@ function renderAccessReport(report) {
|
|
|
15128
16003
|
if (finding.remediation) lines.push(` ${finding.remediation}`);
|
|
15129
16004
|
}
|
|
15130
16005
|
for (const repo of report.repos) {
|
|
15131
|
-
lines.push(`${repo.ok ? "OK" : "FLAG"} ${repo.repo} (${repo.class})`);
|
|
16006
|
+
lines.push(`${repo.ok ? "OK" : "FLAG"} ${repo.repo} (${repo.class}${repo.releaseTrack ? `, ${repo.releaseTrack}` : ""})`);
|
|
15132
16007
|
for (const finding of repo.findings) {
|
|
15133
16008
|
lines.push(` [${finding.severity}] ${finding.kind}${finding.branch ? ` @${finding.branch}` : ""}: ${finding.detail}`);
|
|
15134
16009
|
if (finding.remediation) lines.push(` ${finding.remediation}`);
|
|
@@ -15674,6 +16549,23 @@ async function fetchDeployStatusBySlug(slug, deps) {
|
|
|
15674
16549
|
return null;
|
|
15675
16550
|
}
|
|
15676
16551
|
}
|
|
16552
|
+
async function fetchDeployFactsBySlug(slug, deps) {
|
|
16553
|
+
if (!deps.baseUrl || !slug) return null;
|
|
16554
|
+
const token = await deps.token();
|
|
16555
|
+
if (!token) return null;
|
|
16556
|
+
try {
|
|
16557
|
+
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(slug)}/deploy-facts`, {
|
|
16558
|
+
method: "GET",
|
|
16559
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
16560
|
+
});
|
|
16561
|
+
if (!res.ok) return null;
|
|
16562
|
+
const body = await res.json();
|
|
16563
|
+
if (!body?.stages) return null;
|
|
16564
|
+
return { slug: body.slug ?? slug, stages: body.stages };
|
|
16565
|
+
} catch {
|
|
16566
|
+
return null;
|
|
16567
|
+
}
|
|
16568
|
+
}
|
|
15677
16569
|
async function fetchOrgConfig(deps) {
|
|
15678
16570
|
if (!deps.baseUrl) return null;
|
|
15679
16571
|
const token = await deps.token();
|
|
@@ -15847,7 +16739,7 @@ function attestedLine(att) {
|
|
|
15847
16739
|
return `App-owned readiness attested by @${att.by} on ${att.at.slice(0, 10)} \u2014 the static checklist is cleared (the doctor reads no product repo files); re-run \`mmi-cli project attest\` after app-owned structural changes.`;
|
|
15848
16740
|
}
|
|
15849
16741
|
var CONTRACT_UNDECLARED_LINE = "No runtime secrets declared \u2014 declare requiredRuntimeSecrets (a per-stage name map) in the registry META, or attest the app needs none with an explicit empty stage map ({ dev: [], rc: [], main: [] }).";
|
|
15850
|
-
function appGapsFor(meta, model, slug, projectType) {
|
|
16742
|
+
function appGapsFor(meta, model, slug, projectType, mainDeployFact) {
|
|
15851
16743
|
const attested = appAttestationOf(meta);
|
|
15852
16744
|
const isTenantWeb = !(projectType === "content" || model === "content") && projectType !== "desktop-game" && projectType !== "cli-tool" && !(projectType === "non-deployable" || model === "none") && model !== "hub-serverless" && model !== "serverless" && model !== "registry-publish";
|
|
15853
16745
|
const contractUndeclared = isTenantWeb && Boolean(meta) && !hasRuntimeSecretContract(meta?.requiredRuntimeSecrets);
|
|
@@ -15899,8 +16791,12 @@ function appGapsFor(meta, model, slug, projectType) {
|
|
|
15899
16791
|
if (slug === "mmi-katip") {
|
|
15900
16792
|
gaps.push("Katip-specific app plan: declare Google Workspace service-account requirements, use the service account numeric OAuth2 client ID for DWD, remove prod-hidden impersonation defaults, and make non-critical Google Workspace failures degrade instead of crash-looping.");
|
|
15901
16793
|
}
|
|
15902
|
-
if (
|
|
15903
|
-
|
|
16794
|
+
if (model === "solo-container" || model === "tenant-container") {
|
|
16795
|
+
if (typeof mainDeployFact?.port === "number") {
|
|
16796
|
+
gaps.push(`Seed DEPLOY#main healthUrl to http://127.0.0.1:${mainDeployFact.port}/health during bootstrap (tenant reconcile/control health gate \u2014 #1202; port read from DEPLOY#main).`);
|
|
16797
|
+
} else if (!appAttestationOf(meta)) {
|
|
16798
|
+
gaps.push("Seed DEPLOY#main healthUrl from the DEPLOY#main edgeVhost.port during bootstrap (tenant reconcile/control health gate \u2014 #1202); doctor has no committed DEPLOY port to cite yet.");
|
|
16799
|
+
}
|
|
15904
16800
|
}
|
|
15905
16801
|
if (!meta) gaps.unshift("No app-owned repo changes can be planned precisely until Hub registry META exists.");
|
|
15906
16802
|
return gaps;
|
|
@@ -15945,7 +16841,7 @@ function sameNames(a, b) {
|
|
|
15945
16841
|
function sameStageContract(a, b) {
|
|
15946
16842
|
return STAGES.every((stage2) => sameNames(a[stage2], b[stage2]));
|
|
15947
16843
|
}
|
|
15948
|
-
function buildV2HealPatch(repoOrSlug, meta) {
|
|
16844
|
+
function buildV2HealPatch(repoOrSlug, meta, mainDeployFact) {
|
|
15949
16845
|
const slug = slugOfRepo(repoOrSlug);
|
|
15950
16846
|
const repo = repoFrom(repoOrSlug, slug);
|
|
15951
16847
|
const sub = defaultSubdomain(slug);
|
|
@@ -15983,7 +16879,7 @@ function buildV2HealPatch(repoOrSlug, meta) {
|
|
|
15983
16879
|
patch.requiredRuntimeSecrets = next;
|
|
15984
16880
|
}
|
|
15985
16881
|
}
|
|
15986
|
-
const appOwnedGaps = confidentType ? appGapsFor(meta, model, slug, confidentType) : [`Project type is unset and not derivable \u2014 classify with \`mmi-cli project set ${repo} --project-type <web-app|hub-service|content|desktop-game|non-deployable|cli-tool|worker> --deploy-model <tenant-container|solo-container|hub-serverless|serverless|registry-publish|content|none>\` before heal completes the v2 fields (prevents defaulting a non-web repo to tenant-container).`];
|
|
16882
|
+
const appOwnedGaps = confidentType ? appGapsFor(meta, model, slug, confidentType, mainDeployFact) : [`Project type is unset and not derivable \u2014 classify with \`mmi-cli project set ${repo} --project-type <web-app|hub-service|content|desktop-game|non-deployable|cli-tool|worker> --deploy-model <tenant-container|solo-container|hub-serverless|serverless|registry-publish|content|none>\` before heal completes the v2 fields (prevents defaulting a non-web repo to tenant-container).`];
|
|
15987
16883
|
if (boardRegistryGaps(meta).length) appOwnedGaps.unshift(boardRegistryGapMessage(repo));
|
|
15988
16884
|
return { slug, patch, appOwnedGaps };
|
|
15989
16885
|
}
|
|
@@ -16028,7 +16924,8 @@ async function buildV2Doctor(repoOrSlug, deps) {
|
|
|
16028
16924
|
const meta = read.project;
|
|
16029
16925
|
const projectType = resolveProjectType(meta, repo);
|
|
16030
16926
|
const model = resolveDeployModel(meta, repo);
|
|
16031
|
-
const
|
|
16927
|
+
const mainDeployFact = meta && deps.getDeployFact ? await deps.getDeployFact(slug, "main") : null;
|
|
16928
|
+
const autoHeal = buildV2HealPatch(repo, meta, mainDeployFact);
|
|
16032
16929
|
if (!meta) {
|
|
16033
16930
|
const emptySecrets = {
|
|
16034
16931
|
dev: { required: [], present: [], missing: [] },
|
|
@@ -16142,6 +17039,152 @@ function renderReadinessIssueBody(existingBody, report, opts = {}) {
|
|
|
16142
17039
|
${section}`.trim();
|
|
16143
17040
|
}
|
|
16144
17041
|
|
|
17042
|
+
// src/readiness-audit.ts
|
|
17043
|
+
function publicUrlFromDeployFact(fact) {
|
|
17044
|
+
return fact?.domain ? `https://${fact.domain}` : void 0;
|
|
17045
|
+
}
|
|
17046
|
+
function tenantRuntimeHints(stage2, fact, probe) {
|
|
17047
|
+
const hints = [];
|
|
17048
|
+
if (!fact) {
|
|
17049
|
+
hints.push(`DEPLOY#${stage2} row missing or state-only; seed deploy coords before runtime audit can prove the endpoint.`);
|
|
17050
|
+
return hints;
|
|
17051
|
+
}
|
|
17052
|
+
if (!fact.sshHostPresent && fact.substrate === "hetzner-ssh") hints.push(`DEPLOY#${stage2} has hetzner-ssh substrate but no sshHost presence; tenant-deploy cannot reach the box.`);
|
|
17053
|
+
if (!fact.domain) hints.push(`DEPLOY#${stage2} has no edgeVhost.domain; Cloudflare/Caddy public URL cannot be derived.`);
|
|
17054
|
+
if (typeof fact.port !== "number") hints.push(`DEPLOY#${stage2} has no edgeVhost.port; Caddy upstream/healthUrl hints cannot be checked.`);
|
|
17055
|
+
if (probe?.ok === false || probe?.status != null && probe.status >= 500) hints.push("Public URL probe failed; for Cloudflare 525 check Caddy TLS/origin certificate and Cloudflare SSL mode before changing app code.");
|
|
17056
|
+
if (stage2 === "rc") hints.push("rc runtime is expected to be ephemeral: present between /rcand and /release, then retired after release.");
|
|
17057
|
+
return hints;
|
|
17058
|
+
}
|
|
17059
|
+
function buildTenantRuntimeStatus(input) {
|
|
17060
|
+
const publicUrl = publicUrlFromDeployFact(input.deploy);
|
|
17061
|
+
return {
|
|
17062
|
+
repo: input.repo,
|
|
17063
|
+
slug: input.slug,
|
|
17064
|
+
stage: input.stage,
|
|
17065
|
+
deployRowPresent: Boolean(input.deploy?.present),
|
|
17066
|
+
deploy: input.deploy ?? null,
|
|
17067
|
+
publicUrl,
|
|
17068
|
+
publicProbe: input.publicProbe,
|
|
17069
|
+
lastTenantDeployRun: input.lastTenantDeployRun,
|
|
17070
|
+
expectedEphemeralRc: input.stage === "rc",
|
|
17071
|
+
hints: tenantRuntimeHints(input.stage, input.deploy, input.publicProbe)
|
|
17072
|
+
};
|
|
17073
|
+
}
|
|
17074
|
+
function buildFullTrackReadinessReport(input) {
|
|
17075
|
+
const releaseTrack = resolveReleaseTrack(input.meta, void 0, input.repo);
|
|
17076
|
+
const branches = branchesForTrack(releaseTrack);
|
|
17077
|
+
const reasons = [];
|
|
17078
|
+
if (releaseTrack !== "full") reasons.push(`releaseTrack resolves ${releaseTrack}; set full for development -> rc -> main`);
|
|
17079
|
+
if (!input.trainAuthority?.train) reasons.push("caller does not have train authority");
|
|
17080
|
+
if (!input.deployFacts?.stages.rc?.present) reasons.push("DEPLOY#rc coords missing");
|
|
17081
|
+
const rcRuntime = input.runtime.rc;
|
|
17082
|
+
if (rcRuntime.publicProbe?.ok === false) reasons.push("rc public endpoint probe failed");
|
|
17083
|
+
return {
|
|
17084
|
+
repo: input.repo,
|
|
17085
|
+
slug: input.slug,
|
|
17086
|
+
releaseTrack,
|
|
17087
|
+
branches,
|
|
17088
|
+
trainAuthority: input.trainAuthority,
|
|
17089
|
+
deployFacts: input.deployFacts,
|
|
17090
|
+
rcand: { canApply: reasons.length === 0, reasons },
|
|
17091
|
+
runtime: input.runtime
|
|
17092
|
+
};
|
|
17093
|
+
}
|
|
17094
|
+
|
|
17095
|
+
// src/oauth.ts
|
|
17096
|
+
var DEFAULT_DOMAINS = ["mutatismutandis.co", "mutmut.co"];
|
|
17097
|
+
var DEFAULT_CALLBACK_PATH = "/api/auth/callback";
|
|
17098
|
+
var ENV_PREFIXES = ["", "dev", "rc"];
|
|
17099
|
+
var LOOPBACK = ["http://localhost", "http://127.0.0.1"];
|
|
17100
|
+
var SSM_ENVS = ["dev", "rc", "main"];
|
|
17101
|
+
var SSM_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
17102
|
+
var uniq = (xs) => [...new Set(xs)];
|
|
17103
|
+
function defaultSubdomain2(slug) {
|
|
17104
|
+
const i = slug.indexOf("-");
|
|
17105
|
+
return i === -1 ? slug : slug.slice(i + 1);
|
|
17106
|
+
}
|
|
17107
|
+
function expectedHosts(cfg) {
|
|
17108
|
+
const out = [];
|
|
17109
|
+
for (const sub of cfg.subdomains) {
|
|
17110
|
+
for (const domain of cfg.domains) {
|
|
17111
|
+
const base = sub ? `${sub}.${domain}` : domain;
|
|
17112
|
+
for (const env of ENV_PREFIXES) out.push(env ? `${env}.${base}` : base);
|
|
17113
|
+
}
|
|
17114
|
+
}
|
|
17115
|
+
if (cfg.fofuSubdomain !== void 0) {
|
|
17116
|
+
out.push(cfg.fofuSubdomain ? `${cfg.fofuSubdomain}.fofu.ai` : "fofu.ai");
|
|
17117
|
+
}
|
|
17118
|
+
return uniq(out);
|
|
17119
|
+
}
|
|
17120
|
+
function expectedJsOrigins(cfg) {
|
|
17121
|
+
return uniq([...expectedHosts(cfg).map((h) => `https://${h}`), ...LOOPBACK]);
|
|
17122
|
+
}
|
|
17123
|
+
function expectedRedirectUris(cfg) {
|
|
17124
|
+
const { callbackPath } = cfg;
|
|
17125
|
+
return uniq([
|
|
17126
|
+
...expectedHosts(cfg).map((h) => `https://${h}${callbackPath}`),
|
|
17127
|
+
...LOOPBACK.map((l) => `${l}${callbackPath}`)
|
|
17128
|
+
]);
|
|
17129
|
+
}
|
|
17130
|
+
function oauthSsmKeys() {
|
|
17131
|
+
return SSM_ENVS.flatMap((env) => SSM_NAMES.map((name) => `${env}/${name}`));
|
|
17132
|
+
}
|
|
17133
|
+
function parseOauthClientJson(input) {
|
|
17134
|
+
let parsed;
|
|
17135
|
+
try {
|
|
17136
|
+
parsed = JSON.parse(input);
|
|
17137
|
+
} catch {
|
|
17138
|
+
throw new Error('not valid JSON \u2014 pipe the Google client JSON (the Console "Download JSON" file)');
|
|
17139
|
+
}
|
|
17140
|
+
const root = parsed ?? {};
|
|
17141
|
+
const obj = root.web ?? root.installed ?? parsed;
|
|
17142
|
+
const clientId = typeof obj?.client_id === "string" ? obj.client_id.trim() : "";
|
|
17143
|
+
const clientSecret = typeof obj?.client_secret === "string" ? obj.client_secret.trim() : "";
|
|
17144
|
+
if (!clientId || !clientSecret) {
|
|
17145
|
+
throw new Error("missing client_id or client_secret in the JSON");
|
|
17146
|
+
}
|
|
17147
|
+
return { clientId, clientSecret };
|
|
17148
|
+
}
|
|
17149
|
+
function parseOauthConfig(mmiConfig, slug) {
|
|
17150
|
+
const rawUnknown = mmiConfig?.oauth;
|
|
17151
|
+
if (rawUnknown === void 0) throw new Error(`oauth is not configured for ${slug}`);
|
|
17152
|
+
if (!rawUnknown || typeof rawUnknown !== "object" || Array.isArray(rawUnknown)) {
|
|
17153
|
+
throw new Error("oauth must be an object when configured");
|
|
17154
|
+
}
|
|
17155
|
+
const raw = rawUnknown;
|
|
17156
|
+
const subdomains = Array.isArray(raw.subdomains) && raw.subdomains.length > 0 ? raw.subdomains.map(String) : [defaultSubdomain2(slug)];
|
|
17157
|
+
const domains = Array.isArray(raw.domains) && raw.domains.length > 0 ? raw.domains.map(String) : [...DEFAULT_DOMAINS];
|
|
17158
|
+
const callbackPath = typeof raw.callbackPath === "string" && raw.callbackPath ? raw.callbackPath : DEFAULT_CALLBACK_PATH;
|
|
17159
|
+
if (!callbackPath.startsWith("/")) {
|
|
17160
|
+
throw new Error(`oauth.callbackPath must start with "/" (got ${JSON.stringify(callbackPath)})`);
|
|
17161
|
+
}
|
|
17162
|
+
if (callbackPath !== DEFAULT_CALLBACK_PATH) {
|
|
17163
|
+
throw new Error(`oauth.callbackPath must be "${DEFAULT_CALLBACK_PATH}" (got ${JSON.stringify(callbackPath)})`);
|
|
17164
|
+
}
|
|
17165
|
+
const meta = mmiConfig ?? {};
|
|
17166
|
+
const rawFofuSub = raw.fofuSubdomain;
|
|
17167
|
+
const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub : defaultSubdomain2(slug) : void 0;
|
|
17168
|
+
return { subdomains, domains, callbackPath, fofuSubdomain };
|
|
17169
|
+
}
|
|
17170
|
+
function probeRedirectUri(callbackPath, port = 9123) {
|
|
17171
|
+
return `http://localhost:${port}${callbackPath}`;
|
|
17172
|
+
}
|
|
17173
|
+
function buildAuthorizeProbeUrl(clientId, redirectUri) {
|
|
17174
|
+
const qs = new URLSearchParams({
|
|
17175
|
+
client_id: clientId,
|
|
17176
|
+
redirect_uri: redirectUri,
|
|
17177
|
+
response_type: "code",
|
|
17178
|
+
scope: "openid email",
|
|
17179
|
+
access_type: "offline",
|
|
17180
|
+
prompt: "consent"
|
|
17181
|
+
});
|
|
17182
|
+
return `https://accounts.google.com/o/oauth2/v2/auth?${qs.toString()}`;
|
|
17183
|
+
}
|
|
17184
|
+
function authorizeBodyHasMismatch(body) {
|
|
17185
|
+
return /redirect_uri_mismatch/i.test(body);
|
|
17186
|
+
}
|
|
17187
|
+
|
|
16145
17188
|
// src/project-set.ts
|
|
16146
17189
|
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "dashboard", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate"];
|
|
16147
17190
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
@@ -16263,7 +17306,7 @@ function parseOauthVar(raw) {
|
|
|
16263
17306
|
try {
|
|
16264
17307
|
parsed = JSON.parse(raw);
|
|
16265
17308
|
} catch {
|
|
16266
|
-
throw new Error(
|
|
17309
|
+
throw new Error(`project set: oauth must be JSON, e.g. {"subdomains":["app"],"domains":["example.co"],"callbackPath":"${DEFAULT_CALLBACK_PATH}"}`);
|
|
16267
17310
|
}
|
|
16268
17311
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
16269
17312
|
throw new Error("project set: oauth must be a {subdomains,domains,callbackPath,fofuSubdomain} object");
|
|
@@ -16278,7 +17321,11 @@ function parseOauthVar(raw) {
|
|
|
16278
17321
|
out[key] = value.map((v) => v.trim());
|
|
16279
17322
|
} else if (key === "callbackPath") {
|
|
16280
17323
|
if (typeof value !== "string" || !value.trim()) throw new Error("project set: oauth.callbackPath must be a non-empty string");
|
|
16281
|
-
|
|
17324
|
+
const callbackPath = value.trim();
|
|
17325
|
+
if (callbackPath !== DEFAULT_CALLBACK_PATH) {
|
|
17326
|
+
throw new Error(`project set: oauth.callbackPath must be "${DEFAULT_CALLBACK_PATH}" (got ${JSON.stringify(callbackPath)})`);
|
|
17327
|
+
}
|
|
17328
|
+
out.callbackPath = callbackPath;
|
|
16282
17329
|
} else if (key === "fofuSubdomain") {
|
|
16283
17330
|
if (typeof value !== "string") throw new Error('project set: oauth.fofuSubdomain must be a string ("" selects the apex fofu.ai)');
|
|
16284
17331
|
out.fofuSubdomain = value.trim();
|
|
@@ -16592,8 +17639,12 @@ function resolveKbSource(rawBase) {
|
|
|
16592
17639
|
if (!m) return DEFAULT_KB;
|
|
16593
17640
|
return { owner: m[1], repo: m[2], ref: m[3] };
|
|
16594
17641
|
}
|
|
16595
|
-
function
|
|
17642
|
+
function normalizeKbPath(path2) {
|
|
16596
17643
|
const clean4 = path2.replace(/^\/+/, "");
|
|
17644
|
+
return clean4 === "kb" || clean4.startsWith("kb/") ? clean4 : `kb/${clean4}`;
|
|
17645
|
+
}
|
|
17646
|
+
function buildKbGetArgs(src, path2) {
|
|
17647
|
+
const clean4 = normalizeKbPath(path2);
|
|
16597
17648
|
return ["api", `repos/${src.owner}/${src.repo}/contents/${clean4}?ref=${src.ref}`, "-H", "Accept: application/vnd.github.raw"];
|
|
16598
17649
|
}
|
|
16599
17650
|
function buildKbTreeArgs(src) {
|
|
@@ -16606,20 +17657,20 @@ function parseKbTree(stdout, prefix) {
|
|
|
16606
17657
|
} catch {
|
|
16607
17658
|
return [];
|
|
16608
17659
|
}
|
|
16609
|
-
const pre = prefix ? prefix
|
|
16610
|
-
return tree.filter((t) => t.type === "blob" && typeof t.path === "string" && t.path.startsWith("kb/")).map((t) => t.path).filter((p) => pre ? p.startsWith(pre) : true).sort();
|
|
17660
|
+
const pre = prefix ? normalizeKbPath(prefix) : void 0;
|
|
17661
|
+
return tree.filter((t) => t.type === "blob" && typeof t.path === "string" && t.path.startsWith("kb/") && !t.path.split("/").some((s) => s.startsWith("."))).map((t) => t.path).filter((p) => pre ? p.startsWith(pre) : true).sort();
|
|
16611
17662
|
}
|
|
16612
17663
|
|
|
16613
17664
|
// src/northstar-commands.ts
|
|
16614
|
-
var
|
|
16615
|
-
var
|
|
17665
|
+
var import_node_fs22 = require("node:fs");
|
|
17666
|
+
var import_node_child_process12 = require("node:child_process");
|
|
16616
17667
|
var import_promises6 = require("node:fs/promises");
|
|
16617
17668
|
var planSyncDetached = false;
|
|
16618
17669
|
function detachPlanSync() {
|
|
16619
17670
|
if (planSyncDetached) return;
|
|
16620
17671
|
planSyncDetached = true;
|
|
16621
17672
|
try {
|
|
16622
|
-
(0,
|
|
17673
|
+
(0, import_node_child_process12.spawn)(process.execPath, [process.argv[1], "northstar", "sync", "--quiet"], {
|
|
16623
17674
|
detached: true,
|
|
16624
17675
|
stdio: "ignore",
|
|
16625
17676
|
windowsHide: true,
|
|
@@ -16629,7 +17680,7 @@ function detachPlanSync() {
|
|
|
16629
17680
|
}
|
|
16630
17681
|
}
|
|
16631
17682
|
function makePlanDeps(cfg, io = consoleIo) {
|
|
16632
|
-
const ensureDir = () => (0,
|
|
17683
|
+
const ensureDir = () => (0, import_node_fs22.mkdirSync)(PLANS_DIR, { recursive: true });
|
|
16633
17684
|
return {
|
|
16634
17685
|
apiUrl: cfg.sagaApiUrl,
|
|
16635
17686
|
fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
|
|
@@ -16637,31 +17688,31 @@ function makePlanDeps(cfg, io = consoleIo) {
|
|
|
16637
17688
|
project: async () => (await sagaKey(cfg)).project,
|
|
16638
17689
|
readLocal: (slug) => {
|
|
16639
17690
|
try {
|
|
16640
|
-
return (0,
|
|
17691
|
+
return (0, import_node_fs22.readFileSync)(planPath(slug), "utf8");
|
|
16641
17692
|
} catch {
|
|
16642
17693
|
return null;
|
|
16643
17694
|
}
|
|
16644
17695
|
},
|
|
16645
17696
|
listLocalSlugs: () => {
|
|
16646
17697
|
try {
|
|
16647
|
-
return (0,
|
|
17698
|
+
return (0, import_node_fs22.readdirSync)(PLANS_DIR, { withFileTypes: true }).filter((entry) => entry.isFile() && /^[A-Za-z0-9][A-Za-z0-9_-]*\.md$/.test(entry.name)).map((entry) => entry.name.replace(/\.md$/, ""));
|
|
16648
17699
|
} catch {
|
|
16649
17700
|
return [];
|
|
16650
17701
|
}
|
|
16651
17702
|
},
|
|
16652
17703
|
writeLocal: (slug, content) => {
|
|
16653
17704
|
ensureDir();
|
|
16654
|
-
(0,
|
|
17705
|
+
(0, import_node_fs22.writeFileSync)(planPath(slug), content, "utf8");
|
|
16655
17706
|
},
|
|
16656
17707
|
removeLocal: (slug) => {
|
|
16657
17708
|
try {
|
|
16658
|
-
(0,
|
|
17709
|
+
(0, import_node_fs22.rmSync)(planPath(slug));
|
|
16659
17710
|
} catch {
|
|
16660
17711
|
}
|
|
16661
17712
|
},
|
|
16662
17713
|
readMetaRaw: () => {
|
|
16663
17714
|
try {
|
|
16664
|
-
return (0,
|
|
17715
|
+
return (0, import_node_fs22.readFileSync)(META_FILE, "utf8");
|
|
16665
17716
|
} catch {
|
|
16666
17717
|
return null;
|
|
16667
17718
|
}
|
|
@@ -16672,7 +17723,7 @@ function makePlanDeps(cfg, io = consoleIo) {
|
|
|
16672
17723
|
},
|
|
16673
17724
|
readIndexRaw: () => {
|
|
16674
17725
|
try {
|
|
16675
|
-
return (0,
|
|
17726
|
+
return (0, import_node_fs22.readFileSync)(INDEX_FILE, "utf8");
|
|
16676
17727
|
} catch {
|
|
16677
17728
|
return null;
|
|
16678
17729
|
}
|
|
@@ -16683,7 +17734,7 @@ function makePlanDeps(cfg, io = consoleIo) {
|
|
|
16683
17734
|
},
|
|
16684
17735
|
readQueueRaw: () => {
|
|
16685
17736
|
try {
|
|
16686
|
-
return (0,
|
|
17737
|
+
return (0, import_node_fs22.readFileSync)(QUEUE_FILE, "utf8");
|
|
16687
17738
|
} catch {
|
|
16688
17739
|
return null;
|
|
16689
17740
|
}
|
|
@@ -16705,7 +17756,7 @@ function openInEditor(path2) {
|
|
|
16705
17756
|
return;
|
|
16706
17757
|
}
|
|
16707
17758
|
try {
|
|
16708
|
-
(0,
|
|
17759
|
+
(0, import_node_child_process12.spawn)(editor, [path2], { stdio: "inherit" });
|
|
16709
17760
|
} catch {
|
|
16710
17761
|
console.log(`open ${path2} manually`);
|
|
16711
17762
|
}
|
|
@@ -16743,7 +17794,7 @@ function repoInfoFromRemote(remote) {
|
|
|
16743
17794
|
}
|
|
16744
17795
|
function readStageUrl() {
|
|
16745
17796
|
try {
|
|
16746
|
-
const state = JSON.parse((0,
|
|
17797
|
+
const state = JSON.parse((0, import_node_fs22.readFileSync)("tmp/stage/state.json", "utf8"));
|
|
16747
17798
|
if (typeof state.url === "string" && state.url.trim()) return state.url.trim();
|
|
16748
17799
|
if (typeof state.port === "number" && Number.isFinite(state.port)) return `http://127.0.0.1:${state.port}/`;
|
|
16749
17800
|
if (typeof state.healthUrl === "string" && state.healthUrl.trim()) {
|
|
@@ -17056,6 +18107,11 @@ async function secretsList(deps, opts) {
|
|
|
17056
18107
|
const { secrets } = await res.json();
|
|
17057
18108
|
deps.log(formatSecretList(secrets ?? []));
|
|
17058
18109
|
}
|
|
18110
|
+
var CAPABILITIES_TIMEOUT_MS = 2e4;
|
|
18111
|
+
function isTimeoutError(e) {
|
|
18112
|
+
const name = e?.name;
|
|
18113
|
+
return name === "TimeoutError" || name === "AbortError";
|
|
18114
|
+
}
|
|
17059
18115
|
function formatCapabilities(r) {
|
|
17060
18116
|
const head = `@${r.login} on ${r.repo} \u2014 ${r.role}`;
|
|
17061
18117
|
const items = [...r.capabilities ?? []].sort((a, b) => a.scope.localeCompare(b.scope));
|
|
@@ -17086,10 +18142,15 @@ async function secretsCapabilities(deps, opts) {
|
|
|
17086
18142
|
res = await deps.fetch(`${deps.apiUrl}/secrets/capabilities?${qs}`, {
|
|
17087
18143
|
method: "GET",
|
|
17088
18144
|
headers: await deps.headers(),
|
|
17089
|
-
signal: AbortSignal.timeout(
|
|
18145
|
+
signal: AbortSignal.timeout(CAPABILITIES_TIMEOUT_MS)
|
|
17090
18146
|
});
|
|
17091
18147
|
} catch (e) {
|
|
17092
|
-
|
|
18148
|
+
const message = e.message;
|
|
18149
|
+
if (isTimeoutError(e)) {
|
|
18150
|
+
deps.err(`access capabilities: timed out after ${CAPABILITIES_TIMEOUT_MS}ms while aggregating vault scopes for ${repo}. No access conclusion was made; retry with a warm Hub or run scoped reads such as \`mmi-cli secrets list --repo ${repo}\` while investigating the slow source.`);
|
|
18151
|
+
return;
|
|
18152
|
+
}
|
|
18153
|
+
deps.err(`access capabilities: ${message}`);
|
|
17093
18154
|
return;
|
|
17094
18155
|
}
|
|
17095
18156
|
if (!res.ok) {
|
|
@@ -17483,8 +18544,8 @@ async function secretsUse(deps, key, opts) {
|
|
|
17483
18544
|
}
|
|
17484
18545
|
|
|
17485
18546
|
// src/secrets-commands.ts
|
|
17486
|
-
var
|
|
17487
|
-
var
|
|
18547
|
+
var import_node_fs23 = require("node:fs");
|
|
18548
|
+
var import_node_path21 = require("node:path");
|
|
17488
18549
|
var RAILS_CREDENTIALS_DECRYPT_TIMEOUT_MS = 3e4;
|
|
17489
18550
|
var DEFAULT_RAILS_CREDENTIALS_FILE = "config/credentials.yml.enc";
|
|
17490
18551
|
var DEFAULT_RAILS_MASTER_KEY_FILE = "config/master.key";
|
|
@@ -17492,18 +18553,18 @@ function collectMap(value, previous = []) {
|
|
|
17492
18553
|
return [...previous, value];
|
|
17493
18554
|
}
|
|
17494
18555
|
async function decryptRailsCredentials(input) {
|
|
17495
|
-
const appDir = (0,
|
|
18556
|
+
const appDir = (0, import_node_path21.resolve)(input.appDir ?? process.cwd());
|
|
17496
18557
|
const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
|
|
17497
18558
|
const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
|
|
17498
|
-
const credentialsPath = (0,
|
|
17499
|
-
const masterKeyPath = (0,
|
|
18559
|
+
const credentialsPath = (0, import_node_path21.resolve)(appDir, credentialsFile);
|
|
18560
|
+
const masterKeyPath = (0, import_node_path21.resolve)(appDir, masterKeyFile);
|
|
17500
18561
|
const env = {
|
|
17501
18562
|
...process.env,
|
|
17502
18563
|
MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
|
|
17503
18564
|
MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
|
|
17504
18565
|
};
|
|
17505
|
-
if ((0,
|
|
17506
|
-
env.RAILS_MASTER_KEY = (0,
|
|
18566
|
+
if ((0, import_node_fs23.existsSync)(masterKeyPath)) {
|
|
18567
|
+
env.RAILS_MASTER_KEY = (0, import_node_fs23.readFileSync)(masterKeyPath, "utf8").trim();
|
|
17507
18568
|
}
|
|
17508
18569
|
const script = [
|
|
17509
18570
|
'require "json"',
|
|
@@ -17628,7 +18689,7 @@ function registerSecretsCommands(program3) {
|
|
|
17628
18689
|
{
|
|
17629
18690
|
...d,
|
|
17630
18691
|
decryptRailsCredentials,
|
|
17631
|
-
removeFile: (path2) => (0,
|
|
18692
|
+
removeFile: (path2) => (0, import_node_fs23.unlinkSync)((0, import_node_path21.resolve)(o.appDir ?? process.cwd(), path2))
|
|
17632
18693
|
},
|
|
17633
18694
|
{
|
|
17634
18695
|
repo: o.repo,
|
|
@@ -17713,101 +18774,12 @@ function registerEdgeCommands(program3) {
|
|
|
17713
18774
|
});
|
|
17714
18775
|
}
|
|
17715
18776
|
|
|
17716
|
-
// src/oauth.ts
|
|
17717
|
-
var DEFAULT_DOMAINS = ["mutatismutandis.co", "mutmut.co"];
|
|
17718
|
-
var DEFAULT_CALLBACK_PATH = "/api/auth/callback";
|
|
17719
|
-
var ENV_PREFIXES = ["", "dev", "rc"];
|
|
17720
|
-
var LOOPBACK = ["http://localhost", "http://127.0.0.1"];
|
|
17721
|
-
var SSM_ENVS = ["dev", "rc", "main"];
|
|
17722
|
-
var SSM_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
17723
|
-
var uniq = (xs) => [...new Set(xs)];
|
|
17724
|
-
function defaultSubdomain2(slug) {
|
|
17725
|
-
const i = slug.indexOf("-");
|
|
17726
|
-
return i === -1 ? slug : slug.slice(i + 1);
|
|
17727
|
-
}
|
|
17728
|
-
function expectedHosts(cfg) {
|
|
17729
|
-
const out = [];
|
|
17730
|
-
for (const sub of cfg.subdomains) {
|
|
17731
|
-
for (const domain of cfg.domains) {
|
|
17732
|
-
const base = sub ? `${sub}.${domain}` : domain;
|
|
17733
|
-
for (const env of ENV_PREFIXES) out.push(env ? `${env}.${base}` : base);
|
|
17734
|
-
}
|
|
17735
|
-
}
|
|
17736
|
-
if (cfg.fofuSubdomain !== void 0) {
|
|
17737
|
-
out.push(cfg.fofuSubdomain ? `${cfg.fofuSubdomain}.fofu.ai` : "fofu.ai");
|
|
17738
|
-
}
|
|
17739
|
-
return uniq(out);
|
|
17740
|
-
}
|
|
17741
|
-
function expectedJsOrigins(cfg) {
|
|
17742
|
-
return uniq([...expectedHosts(cfg).map((h) => `https://${h}`), ...LOOPBACK]);
|
|
17743
|
-
}
|
|
17744
|
-
function expectedRedirectUris(cfg) {
|
|
17745
|
-
const { callbackPath } = cfg;
|
|
17746
|
-
return uniq([
|
|
17747
|
-
...expectedHosts(cfg).map((h) => `https://${h}${callbackPath}`),
|
|
17748
|
-
...LOOPBACK.map((l) => `${l}${callbackPath}`)
|
|
17749
|
-
]);
|
|
17750
|
-
}
|
|
17751
|
-
function oauthSsmKeys() {
|
|
17752
|
-
return SSM_ENVS.flatMap((env) => SSM_NAMES.map((name) => `${env}/${name}`));
|
|
17753
|
-
}
|
|
17754
|
-
function parseOauthClientJson(input) {
|
|
17755
|
-
let parsed;
|
|
17756
|
-
try {
|
|
17757
|
-
parsed = JSON.parse(input);
|
|
17758
|
-
} catch {
|
|
17759
|
-
throw new Error('not valid JSON \u2014 pipe the Google client JSON (the Console "Download JSON" file)');
|
|
17760
|
-
}
|
|
17761
|
-
const root = parsed ?? {};
|
|
17762
|
-
const obj = root.web ?? root.installed ?? parsed;
|
|
17763
|
-
const clientId = typeof obj?.client_id === "string" ? obj.client_id.trim() : "";
|
|
17764
|
-
const clientSecret = typeof obj?.client_secret === "string" ? obj.client_secret.trim() : "";
|
|
17765
|
-
if (!clientId || !clientSecret) {
|
|
17766
|
-
throw new Error("missing client_id or client_secret in the JSON");
|
|
17767
|
-
}
|
|
17768
|
-
return { clientId, clientSecret };
|
|
17769
|
-
}
|
|
17770
|
-
function parseOauthConfig(mmiConfig, slug) {
|
|
17771
|
-
const rawUnknown = mmiConfig?.oauth;
|
|
17772
|
-
if (rawUnknown === void 0) throw new Error(`oauth is not configured for ${slug}`);
|
|
17773
|
-
if (!rawUnknown || typeof rawUnknown !== "object" || Array.isArray(rawUnknown)) {
|
|
17774
|
-
throw new Error("oauth must be an object when configured");
|
|
17775
|
-
}
|
|
17776
|
-
const raw = rawUnknown;
|
|
17777
|
-
const subdomains = Array.isArray(raw.subdomains) && raw.subdomains.length > 0 ? raw.subdomains.map(String) : [defaultSubdomain2(slug)];
|
|
17778
|
-
const domains = Array.isArray(raw.domains) && raw.domains.length > 0 ? raw.domains.map(String) : [...DEFAULT_DOMAINS];
|
|
17779
|
-
const callbackPath = typeof raw.callbackPath === "string" && raw.callbackPath ? raw.callbackPath : DEFAULT_CALLBACK_PATH;
|
|
17780
|
-
if (!callbackPath.startsWith("/")) {
|
|
17781
|
-
throw new Error(`oauth.callbackPath must start with "/" (got ${JSON.stringify(callbackPath)})`);
|
|
17782
|
-
}
|
|
17783
|
-
const meta = mmiConfig ?? {};
|
|
17784
|
-
const rawFofuSub = raw.fofuSubdomain;
|
|
17785
|
-
const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub : defaultSubdomain2(slug) : void 0;
|
|
17786
|
-
return { subdomains, domains, callbackPath, fofuSubdomain };
|
|
17787
|
-
}
|
|
17788
|
-
function probeRedirectUri(callbackPath, port = 9123) {
|
|
17789
|
-
return `http://localhost:${port}${callbackPath}`;
|
|
17790
|
-
}
|
|
17791
|
-
function buildAuthorizeProbeUrl(clientId, redirectUri) {
|
|
17792
|
-
const qs = new URLSearchParams({
|
|
17793
|
-
client_id: clientId,
|
|
17794
|
-
redirect_uri: redirectUri,
|
|
17795
|
-
response_type: "code",
|
|
17796
|
-
scope: "openid email",
|
|
17797
|
-
access_type: "offline",
|
|
17798
|
-
prompt: "consent"
|
|
17799
|
-
});
|
|
17800
|
-
return `https://accounts.google.com/o/oauth2/v2/auth?${qs.toString()}`;
|
|
17801
|
-
}
|
|
17802
|
-
function authorizeBodyHasMismatch(body) {
|
|
17803
|
-
return /redirect_uri_mismatch/i.test(body);
|
|
17804
|
-
}
|
|
17805
|
-
|
|
17806
18777
|
// src/doctor-run.ts
|
|
17807
|
-
var
|
|
18778
|
+
var import_node_fs28 = require("node:fs");
|
|
18779
|
+
var import_node_child_process14 = require("node:child_process");
|
|
17808
18780
|
var import_promises7 = require("node:fs/promises");
|
|
17809
|
-
var
|
|
17810
|
-
var
|
|
18781
|
+
var import_node_path25 = require("node:path");
|
|
18782
|
+
var import_node_os6 = require("node:os");
|
|
17811
18783
|
|
|
17812
18784
|
// src/plugin-guard.ts
|
|
17813
18785
|
function buildPluginGuardDecision(i) {
|
|
@@ -17827,10 +18799,10 @@ function buildGuardSessionStartLine(state, opts = {}) {
|
|
|
17827
18799
|
}
|
|
17828
18800
|
|
|
17829
18801
|
// src/cursor-plugin-seed.ts
|
|
17830
|
-
var
|
|
17831
|
-
var
|
|
17832
|
-
var
|
|
17833
|
-
var
|
|
18802
|
+
var import_node_child_process13 = require("node:child_process");
|
|
18803
|
+
var import_node_fs24 = require("node:fs");
|
|
18804
|
+
var import_node_os5 = require("node:os");
|
|
18805
|
+
var import_node_path22 = require("node:path");
|
|
17834
18806
|
var import_node_util7 = require("node:util");
|
|
17835
18807
|
function isSemverVersion(v) {
|
|
17836
18808
|
return typeof v === "string" && /^v?\d+\.\d+\.\d+/.test(v.trim());
|
|
@@ -17838,7 +18810,7 @@ function isSemverVersion(v) {
|
|
|
17838
18810
|
var MMI_HUB_REPO = "mutmutco/MMI-Hub";
|
|
17839
18811
|
var CURSOR_THIRD_PARTY_STATE_KEY = "cursor/thirdPartyExtensibilityEnabled";
|
|
17840
18812
|
var PLUGIN_JSON_REL = ".cursor-plugin/plugin.json";
|
|
17841
|
-
var execFileBuffer = (0, import_node_util7.promisify)(
|
|
18813
|
+
var execFileBuffer = (0, import_node_util7.promisify)(import_node_child_process13.execFile);
|
|
17842
18814
|
function gitFetchReleaseTagArgs(hubCheckout, tag) {
|
|
17843
18815
|
return ["-C", hubCheckout, "fetch", "origin", "tag", tag, "--quiet"];
|
|
17844
18816
|
}
|
|
@@ -17847,17 +18819,17 @@ function ghReleaseTarballApiArgs(tag) {
|
|
|
17847
18819
|
}
|
|
17848
18820
|
function cursorUserGlobalStatePath() {
|
|
17849
18821
|
if (process.platform === "win32") {
|
|
17850
|
-
const base = process.env.APPDATA || (0,
|
|
17851
|
-
return (0,
|
|
18822
|
+
const base = process.env.APPDATA || (0, import_node_path22.join)((0, import_node_os5.homedir)(), "AppData", "Roaming");
|
|
18823
|
+
return (0, import_node_path22.join)(base, "Cursor", "User", "globalStorage", "state.vscdb");
|
|
17852
18824
|
}
|
|
17853
18825
|
if (process.platform === "darwin") {
|
|
17854
|
-
return (0,
|
|
18826
|
+
return (0, import_node_path22.join)((0, import_node_os5.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
17855
18827
|
}
|
|
17856
|
-
return (0,
|
|
18828
|
+
return (0, import_node_path22.join)((0, import_node_os5.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
17857
18829
|
}
|
|
17858
18830
|
async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
|
|
17859
18831
|
const dbPath = cursorUserGlobalStatePath();
|
|
17860
|
-
if (!(0,
|
|
18832
|
+
if (!(0, import_node_fs24.existsSync)(dbPath)) return void 0;
|
|
17861
18833
|
try {
|
|
17862
18834
|
const { stdout } = await execFileP5("sqlite3", [dbPath, `SELECT value FROM ItemTable WHERE key = '${CURSOR_THIRD_PARTY_STATE_KEY}';`], {
|
|
17863
18835
|
timeout: 5e3
|
|
@@ -17871,57 +18843,57 @@ async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
|
|
|
17871
18843
|
}
|
|
17872
18844
|
}
|
|
17873
18845
|
function syncDirContents(src, dest) {
|
|
17874
|
-
(0,
|
|
17875
|
-
for (const name of (0,
|
|
17876
|
-
(0,
|
|
18846
|
+
(0, import_node_fs24.mkdirSync)(dest, { recursive: true });
|
|
18847
|
+
for (const name of (0, import_node_fs24.readdirSync)(dest)) {
|
|
18848
|
+
(0, import_node_fs24.rmSync)((0, import_node_path22.join)(dest, name), { recursive: true, force: true });
|
|
17877
18849
|
}
|
|
17878
|
-
(0,
|
|
18850
|
+
(0, import_node_fs24.cpSync)(src, dest, { recursive: true });
|
|
17879
18851
|
}
|
|
17880
18852
|
function releaseTag(releasedVersion) {
|
|
17881
18853
|
return releasedVersion.startsWith("v") ? releasedVersion : `v${releasedVersion}`;
|
|
17882
18854
|
}
|
|
17883
18855
|
async function extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5) {
|
|
17884
|
-
const tarFile = (0,
|
|
18856
|
+
const tarFile = (0, import_node_path22.join)(tmpRoot, "archive.tar");
|
|
17885
18857
|
try {
|
|
17886
18858
|
await execFileP5("git", gitFetchReleaseTagArgs(hubCheckout, tag), { timeout: 6e4 });
|
|
17887
18859
|
await execFileP5("git", ["-C", hubCheckout, "archive", "--format=tar", `--output=${tarFile}`, tag, "plugins/mmi"], {
|
|
17888
18860
|
timeout: 6e4
|
|
17889
18861
|
});
|
|
17890
18862
|
await execFileP5("tar", ["-xf", tarFile, "-C", tmpRoot], { timeout: 6e4 });
|
|
17891
|
-
const pluginMmi = (0,
|
|
17892
|
-
return (0,
|
|
18863
|
+
const pluginMmi = (0, import_node_path22.join)(tmpRoot, "plugins", "mmi");
|
|
18864
|
+
return (0, import_node_fs24.existsSync)((0, import_node_path22.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
|
|
17893
18865
|
} catch {
|
|
17894
18866
|
return void 0;
|
|
17895
18867
|
}
|
|
17896
18868
|
}
|
|
17897
18869
|
async function downloadPluginMmiViaGh(tag, tmpRoot) {
|
|
17898
|
-
const tarPath = (0,
|
|
18870
|
+
const tarPath = (0, import_node_path22.join)(tmpRoot, "repo.tgz");
|
|
17899
18871
|
try {
|
|
17900
|
-
(0,
|
|
18872
|
+
(0, import_node_fs24.mkdirSync)(tmpRoot, { recursive: true });
|
|
17901
18873
|
const { stdout } = await execFileBuffer("gh", ghReleaseTarballApiArgs(tag), {
|
|
17902
18874
|
timeout: 12e4,
|
|
17903
18875
|
maxBuffer: 100 * 1024 * 1024,
|
|
17904
18876
|
encoding: "buffer",
|
|
17905
18877
|
windowsHide: true
|
|
17906
18878
|
});
|
|
17907
|
-
(0,
|
|
18879
|
+
(0, import_node_fs24.writeFileSync)(tarPath, stdout);
|
|
17908
18880
|
await execFileBuffer("tar", ["-xzf", tarPath, "-C", tmpRoot], { timeout: 12e4, windowsHide: true });
|
|
17909
|
-
const top = (0,
|
|
18881
|
+
const top = (0, import_node_fs24.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
|
|
17910
18882
|
if (!top) return void 0;
|
|
17911
|
-
const pluginMmi = (0,
|
|
17912
|
-
return (0,
|
|
18883
|
+
const pluginMmi = (0, import_node_path22.join)(tmpRoot, top, "plugins", "mmi");
|
|
18884
|
+
return (0, import_node_fs24.existsSync)((0, import_node_path22.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
|
|
17913
18885
|
} catch {
|
|
17914
18886
|
return void 0;
|
|
17915
18887
|
}
|
|
17916
18888
|
}
|
|
17917
18889
|
async function resolvePluginMmiSource(releasedVersion, hubCheckout, tmpRoot, execFileP5) {
|
|
17918
|
-
(0,
|
|
18890
|
+
(0, import_node_fs24.mkdirSync)(tmpRoot, { recursive: true });
|
|
17919
18891
|
const tag = releaseTag(releasedVersion);
|
|
17920
18892
|
if (hubCheckout) {
|
|
17921
18893
|
const fromHub = await extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5);
|
|
17922
18894
|
if (fromHub) return fromHub;
|
|
17923
18895
|
}
|
|
17924
|
-
return downloadPluginMmiViaGh(tag, (0,
|
|
18896
|
+
return downloadPluginMmiViaGh(tag, (0, import_node_path22.join)(tmpRoot, "gh"));
|
|
17925
18897
|
}
|
|
17926
18898
|
function cursorPluginPinsNeedingSeed(pins, releasedVersion) {
|
|
17927
18899
|
if (!isSemverVersion(releasedVersion)) return pins.filter((pin) => !pin.hasPluginJson || !pin.hasHooksJson || pin.isEmpty);
|
|
@@ -17942,7 +18914,7 @@ async function applyCursorPluginCacheSeed(input) {
|
|
|
17942
18914
|
for (const pin of pinsToSeed) {
|
|
17943
18915
|
syncDirContents(source, pin.path);
|
|
17944
18916
|
}
|
|
17945
|
-
(0,
|
|
18917
|
+
(0, import_node_fs24.rmSync)(tmpRoot, { recursive: true, force: true });
|
|
17946
18918
|
return true;
|
|
17947
18919
|
}
|
|
17948
18920
|
|
|
@@ -18269,6 +19241,30 @@ function buildNestedPluginTreeCheck(input) {
|
|
|
18269
19241
|
fix: `${nested.length} self-nested MMI plugin cache tree(s) (#1126) exceed MAX_PATH and can't self-clean \u2014 run: ${nestedPluginTreeCleanupCommand(nested.map((n) => n.path), input.isWindows)}`
|
|
18270
19242
|
};
|
|
18271
19243
|
}
|
|
19244
|
+
var CODEX_ACTIVE_CACHE_LABEL = "Codex active plugin cache (vs latest release)";
|
|
19245
|
+
function buildCodexActiveCacheCheck(input) {
|
|
19246
|
+
const base = {
|
|
19247
|
+
ok: true,
|
|
19248
|
+
label: CODEX_ACTIVE_CACHE_LABEL,
|
|
19249
|
+
fix: CODEX_PLUGIN_RECOVERY
|
|
19250
|
+
};
|
|
19251
|
+
if (!input.isOrgRepo || !isSemverVersion2(input.releasedVersion)) return base;
|
|
19252
|
+
if (isSemverVersion2(input.codexRecordVersion) && compareVersions(input.codexRecordVersion, input.releasedVersion) < 0) {
|
|
19253
|
+
return base;
|
|
19254
|
+
}
|
|
19255
|
+
const activeCacheVersion = highestSemver(input.codexCacheVersions ?? []);
|
|
19256
|
+
const cacheCurrent = isSemverVersion2(activeCacheVersion) && compareVersions(activeCacheVersion, input.releasedVersion) >= 0;
|
|
19257
|
+
if (cacheCurrent) {
|
|
19258
|
+
return { ...base, activeCacheVersion, releasedVersion: input.releasedVersion };
|
|
19259
|
+
}
|
|
19260
|
+
return {
|
|
19261
|
+
...base,
|
|
19262
|
+
ok: false,
|
|
19263
|
+
activeCacheVersion,
|
|
19264
|
+
releasedVersion: input.releasedVersion,
|
|
19265
|
+
fix: `Codex active plugin cache is ${activeCacheVersion ?? "missing the released version"} < ${input.releasedVersion} while the installed record is current \u2014 run \`mmi-cli doctor --apply\` (forces the Codex reinstall when \`codex\` is on PATH; restart Codex after), or manually: ${CODEX_PLUGIN_RECOVERY}`
|
|
19266
|
+
};
|
|
19267
|
+
}
|
|
18272
19268
|
function detectSurface(env) {
|
|
18273
19269
|
const has = (k) => Boolean(env[k]?.trim());
|
|
18274
19270
|
if (env.MMI_AGENT_SURFACE === "codex" || has("CODEX_HOME") || (env.CLAUDE_PLUGIN_ROOT ?? "").includes(".codex")) {
|
|
@@ -18397,6 +19393,25 @@ var CODEX_PLUGIN_HEAL_STEPS = PLUGIN_SURFACE_HEAL.codex.healSteps;
|
|
|
18397
19393
|
function healStepAborts(step, ok) {
|
|
18398
19394
|
return !ok && step.gated;
|
|
18399
19395
|
}
|
|
19396
|
+
function marketplaceAddSupportsRef(helpText) {
|
|
19397
|
+
if (!helpText) return false;
|
|
19398
|
+
return /(^|\s)--ref(\b|=)/.test(helpText);
|
|
19399
|
+
}
|
|
19400
|
+
function adaptHealStepsForRefSupport(steps, refSupported) {
|
|
19401
|
+
if (refSupported) return { steps: [...steps], strippedRef: false };
|
|
19402
|
+
let strippedRef = false;
|
|
19403
|
+
const adapted = steps.map((step) => {
|
|
19404
|
+
const refIdx = step.args.indexOf("--ref");
|
|
19405
|
+
const isAdd = step.args.includes("marketplace") && step.args.includes("add");
|
|
19406
|
+
if (!isAdd || refIdx === -1) return step;
|
|
19407
|
+
strippedRef = true;
|
|
19408
|
+
return { ...step, args: [...step.args.slice(0, refIdx), ...step.args.slice(refIdx + 2)] };
|
|
19409
|
+
});
|
|
19410
|
+
return { steps: adapted, strippedRef };
|
|
19411
|
+
}
|
|
19412
|
+
function recoveryWithoutRef(recovery) {
|
|
19413
|
+
return recovery.replace(/ --ref \S+/g, "");
|
|
19414
|
+
}
|
|
18400
19415
|
function pluginRecoveryFix(surface) {
|
|
18401
19416
|
const token = surfaceToken(surface);
|
|
18402
19417
|
if (token) return PLUGIN_SURFACE_HEAL[token].fix(surface);
|
|
@@ -18737,7 +19752,7 @@ function cursorPluginInstallFix(input) {
|
|
|
18737
19752
|
const cacheDir = joinCachePath(input.cacheRoot, pin);
|
|
18738
19753
|
const autoSeed = "run `mmi-cli doctor --apply` to seed plugins/mmi from the latest release into the active pin";
|
|
18739
19754
|
const localSeed = input.hubCheckout ? `temporary fallback: copy ${joinCachePath(input.hubCheckout, "plugins", "mmi")} to ${cacheDir}, then restart Cursor` : `temporary fallback: copy plugins/mmi from a local MMI-Hub checkout to ${cacheDir}, then restart Cursor`;
|
|
18740
|
-
return `Cursor plugin cache at ${cacheDir} is empty or missing ${CURSOR_PLUGIN_JSON_REL}
|
|
19755
|
+
return `Cursor plugin cache at ${cacheDir} is empty or missing ${CURSOR_PLUGIN_JSON_REL}, ${CURSOR_HOOKS_JSON_REL}, or the shell-dialect guard scripts \u2014 ${autoSeed}; ${marketplaceRefresh}; ${authSteps}; ${localSeed}; ${logHint}; ${guide}`;
|
|
18741
19756
|
}
|
|
18742
19757
|
function buildCursorPluginInstallCheck(input) {
|
|
18743
19758
|
const base = {
|
|
@@ -18759,7 +19774,7 @@ function buildCursorPluginInstallCheck(input) {
|
|
|
18759
19774
|
};
|
|
18760
19775
|
}
|
|
18761
19776
|
for (const pin of input.pins) {
|
|
18762
|
-
if (!pin.hasPluginJson || !pin.hasHooksJson || pin.isEmpty) {
|
|
19777
|
+
if (!pin.hasPluginJson || !pin.hasHooksJson || pin.hasShellDialectGuard === false || pin.isEmpty) {
|
|
18763
19778
|
return {
|
|
18764
19779
|
...base,
|
|
18765
19780
|
ok: false,
|
|
@@ -18976,6 +19991,22 @@ function buildSelfUpdateHaltPayload(input) {
|
|
|
18976
19991
|
checks: input.checks
|
|
18977
19992
|
};
|
|
18978
19993
|
}
|
|
19994
|
+
var DOCTOR_POST_SELF_UPDATE_ENV = "MMI_DOCTOR_POST_SELF_UPDATE";
|
|
19995
|
+
function buildSelfUpdateReexecArgs(opts) {
|
|
19996
|
+
const args = ["doctor"];
|
|
19997
|
+
if (opts.banner) args.push("--banner");
|
|
19998
|
+
if (opts.preflight) args.push("--preflight");
|
|
19999
|
+
if (opts.verbose) args.push("--verbose");
|
|
20000
|
+
if (opts.guide) args.push("--guide");
|
|
20001
|
+
if (opts.json) args.push("--json");
|
|
20002
|
+
if (opts.apply) args.push("--apply");
|
|
20003
|
+
if (opts.noRepoWrites) args.push("--no-repo-writes");
|
|
20004
|
+
return args;
|
|
20005
|
+
}
|
|
20006
|
+
function selfUpdateReexecLine(report) {
|
|
20007
|
+
const to = report.releasedVersion ?? "latest";
|
|
20008
|
+
return `\u21BB mmi-cli updated \u2192 ${to}. Re-running \`mmi-cli doctor\` under the new CLI\u2026`;
|
|
20009
|
+
}
|
|
18979
20010
|
function preflightOutcome(input) {
|
|
18980
20011
|
if (input.gaps.length) {
|
|
18981
20012
|
return { healed: false, line: `\u26A0 MMI preflight: ${input.gaps.length} item(s) still need attention \u2014 ${input.gaps.map((g) => g.fix).join(" \xB7 ")}` };
|
|
@@ -19051,16 +20082,16 @@ function buildPluginResolvabilityCheck(input) {
|
|
|
19051
20082
|
}
|
|
19052
20083
|
|
|
19053
20084
|
// src/kb-drift-report.ts
|
|
19054
|
-
var
|
|
19055
|
-
var
|
|
20085
|
+
var import_node_fs25 = require("node:fs");
|
|
20086
|
+
var import_node_path23 = require("node:path");
|
|
19056
20087
|
function yesterdayIso() {
|
|
19057
20088
|
const d = /* @__PURE__ */ new Date();
|
|
19058
20089
|
d.setUTCDate(d.getUTCDate() - 1);
|
|
19059
20090
|
return d.toISOString().slice(0, 10);
|
|
19060
20091
|
}
|
|
19061
20092
|
async function fetchLatestKbDriftReport(execFileP5, repoRoot) {
|
|
19062
|
-
const sagaIo = (0,
|
|
19063
|
-
if (!(0,
|
|
20093
|
+
const sagaIo = (0, import_node_path23.join)(repoRoot, "infra", "saga-io.mjs");
|
|
20094
|
+
if (!(0, import_node_fs25.existsSync)(sagaIo)) return null;
|
|
19064
20095
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
19065
20096
|
for (const date of [today, yesterdayIso()]) {
|
|
19066
20097
|
try {
|
|
@@ -19076,9 +20107,9 @@ async function fetchLatestKbDriftReport(execFileP5, repoRoot) {
|
|
|
19076
20107
|
}
|
|
19077
20108
|
|
|
19078
20109
|
// src/cli-doctor-shared.ts
|
|
19079
|
-
var import_node_fs25 = require("node:fs");
|
|
19080
|
-
var import_node_path23 = require("node:path");
|
|
19081
20110
|
var import_node_fs26 = require("node:fs");
|
|
20111
|
+
var import_node_path24 = require("node:path");
|
|
20112
|
+
var import_node_fs27 = require("node:fs");
|
|
19082
20113
|
var GC_GH_TIMEOUT_MS = 2e4;
|
|
19083
20114
|
async function awsCallerArn() {
|
|
19084
20115
|
try {
|
|
@@ -19124,7 +20155,7 @@ async function localBranchHeads() {
|
|
|
19124
20155
|
}
|
|
19125
20156
|
async function currentRepoWorktreeGitRoot(repoRoot) {
|
|
19126
20157
|
const gitCommonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
19127
|
-
return gitCommonDir ? (0,
|
|
20158
|
+
return gitCommonDir ? (0, import_node_path24.resolve)(repoRoot, gitCommonDir, "worktrees") : "";
|
|
19128
20159
|
}
|
|
19129
20160
|
async function worktreeBranches() {
|
|
19130
20161
|
const { stdout } = await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
|
|
@@ -19144,18 +20175,18 @@ function resolveGitdirForWorktreeFile(worktreePath, content) {
|
|
|
19144
20175
|
const match = /^gitdir:\s*(.+)\s*$/im.exec(content);
|
|
19145
20176
|
if (!match?.[1]) return void 0;
|
|
19146
20177
|
const raw = match[1].trim();
|
|
19147
|
-
return (0,
|
|
20178
|
+
return (0, import_node_path24.isAbsolute)(raw) ? raw : (0, import_node_path24.resolve)(worktreePath, raw);
|
|
19148
20179
|
}
|
|
19149
20180
|
function metadataOwnsMissingWorktreeDir(worktreePath, worktreeGitRoot) {
|
|
19150
20181
|
if (!worktreeGitRoot) return false;
|
|
19151
20182
|
try {
|
|
19152
|
-
const entries = (0,
|
|
20183
|
+
const entries = (0, import_node_fs27.readdirSync)(worktreeGitRoot, { withFileTypes: true });
|
|
19153
20184
|
for (const ent of entries) {
|
|
19154
20185
|
if (!ent.isDirectory()) continue;
|
|
19155
20186
|
try {
|
|
19156
|
-
const gitdirPath = (0,
|
|
19157
|
-
const resolvedGitdir = (0,
|
|
19158
|
-
if (sameWorktreeMetadataPath((0,
|
|
20187
|
+
const gitdirPath = (0, import_node_fs26.readFileSync)((0, import_node_path24.join)(worktreeGitRoot, ent.name, "gitdir"), "utf8").trim();
|
|
20188
|
+
const resolvedGitdir = (0, import_node_path24.isAbsolute)(gitdirPath) ? gitdirPath : (0, import_node_path24.resolve)(worktreeGitRoot, ent.name, gitdirPath);
|
|
20189
|
+
if (sameWorktreeMetadataPath((0, import_node_path24.dirname)(resolvedGitdir), worktreePath)) return true;
|
|
19159
20190
|
} catch {
|
|
19160
20191
|
}
|
|
19161
20192
|
}
|
|
@@ -19165,7 +20196,7 @@ function metadataOwnsMissingWorktreeDir(worktreePath, worktreeGitRoot) {
|
|
|
19165
20196
|
}
|
|
19166
20197
|
function pathExistsKnown(path2) {
|
|
19167
20198
|
try {
|
|
19168
|
-
(0,
|
|
20199
|
+
(0, import_node_fs27.statSync)(path2);
|
|
19169
20200
|
return true;
|
|
19170
20201
|
} catch (e) {
|
|
19171
20202
|
const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
|
|
@@ -19174,10 +20205,10 @@ function pathExistsKnown(path2) {
|
|
|
19174
20205
|
}
|
|
19175
20206
|
}
|
|
19176
20207
|
function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
|
|
19177
|
-
const gitPath = (0,
|
|
20208
|
+
const gitPath = (0, import_node_path24.join)(path2, ".git");
|
|
19178
20209
|
let st;
|
|
19179
20210
|
try {
|
|
19180
|
-
st = (0,
|
|
20211
|
+
st = (0, import_node_fs27.lstatSync)(gitPath);
|
|
19181
20212
|
} catch (e) {
|
|
19182
20213
|
const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
|
|
19183
20214
|
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
@@ -19194,7 +20225,7 @@ function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
|
|
|
19194
20225
|
if (st.isDirectory()) return { path: path2, gitType: "dir" };
|
|
19195
20226
|
if (!st.isFile()) return { path: path2, gitType: "other" };
|
|
19196
20227
|
try {
|
|
19197
|
-
const gitFileContent = (0,
|
|
20228
|
+
const gitFileContent = (0, import_node_fs26.readFileSync)(gitPath, "utf8");
|
|
19198
20229
|
const gitdir = resolveGitdirForWorktreeFile(path2, gitFileContent);
|
|
19199
20230
|
const gitDirExists = gitdir ? pathExistsKnown(gitdir) : false;
|
|
19200
20231
|
return {
|
|
@@ -19211,7 +20242,7 @@ function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
|
|
|
19211
20242
|
}
|
|
19212
20243
|
function inspectDeadWorktreeDirContent(path2) {
|
|
19213
20244
|
try {
|
|
19214
|
-
return { entries: (0,
|
|
20245
|
+
return { entries: (0, import_node_fs27.readdirSync)(path2) };
|
|
19215
20246
|
} catch (e) {
|
|
19216
20247
|
const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
|
|
19217
20248
|
return { error: code ? `unable to inspect directory contents (${code})` : "unable to inspect directory contents" };
|
|
@@ -19230,8 +20261,8 @@ async function siblingWorktreeDirs() {
|
|
|
19230
20261
|
const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot);
|
|
19231
20262
|
const siblingRoot = siblingMmiWorktreesRoot(repoRoot);
|
|
19232
20263
|
try {
|
|
19233
|
-
const entries = (0,
|
|
19234
|
-
return entries.filter((ent) => ent.isDirectory()).map((ent) => inspectSiblingWorktreeDir((0,
|
|
20264
|
+
const entries = (0, import_node_fs27.readdirSync)(siblingRoot, { withFileTypes: true });
|
|
20265
|
+
return entries.filter((ent) => ent.isDirectory()).map((ent) => inspectSiblingWorktreeDir((0, import_node_path24.join)(siblingRoot, ent.name), worktreeGitRoot)).filter((entry) => Boolean(entry));
|
|
19235
20266
|
} catch {
|
|
19236
20267
|
return [];
|
|
19237
20268
|
}
|
|
@@ -19281,7 +20312,7 @@ async function fetchHubVersionInfo(baseUrl) {
|
|
|
19281
20312
|
}
|
|
19282
20313
|
function readRepoVersion() {
|
|
19283
20314
|
try {
|
|
19284
|
-
return JSON.parse((0,
|
|
20315
|
+
return JSON.parse((0, import_node_fs28.readFileSync)((0, import_node_path25.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
19285
20316
|
} catch {
|
|
19286
20317
|
return void 0;
|
|
19287
20318
|
}
|
|
@@ -19382,6 +20413,21 @@ var CLAUDE_PLUGIN_TIMEOUT_MS = 12e4;
|
|
|
19382
20413
|
function runHostBin(bin, args, opts) {
|
|
19383
20414
|
return isWin ? execFileP2("cmd.exe", ["/c", bin, ...args], opts) : execFileP2(bin, args, opts);
|
|
19384
20415
|
}
|
|
20416
|
+
function reexecMmiCli(args) {
|
|
20417
|
+
return new Promise((resolve6) => {
|
|
20418
|
+
let settled = false;
|
|
20419
|
+
const done = (code) => {
|
|
20420
|
+
if (!settled) {
|
|
20421
|
+
settled = true;
|
|
20422
|
+
resolve6(code);
|
|
20423
|
+
}
|
|
20424
|
+
};
|
|
20425
|
+
const env = { ...process.env, [DOCTOR_POST_SELF_UPDATE_ENV]: "1" };
|
|
20426
|
+
const child = isWin ? (0, import_node_child_process14.spawn)("cmd.exe", ["/c", "mmi-cli", ...args], { stdio: "inherit", env }) : (0, import_node_child_process14.spawn)("mmi-cli", args, { stdio: "inherit", env });
|
|
20427
|
+
child.on("error", () => done(-1));
|
|
20428
|
+
child.on("exit", (code) => done(code ?? 0));
|
|
20429
|
+
});
|
|
20430
|
+
}
|
|
19385
20431
|
function hostBinAvailable(bin) {
|
|
19386
20432
|
return execFileP2(isWin ? "where" : "which", [bin]).then(() => true).catch(() => false);
|
|
19387
20433
|
}
|
|
@@ -19401,15 +20447,32 @@ async function runCodexPlugin(args) {
|
|
|
19401
20447
|
return false;
|
|
19402
20448
|
}
|
|
19403
20449
|
}
|
|
20450
|
+
async function marketplaceAddRefSupported(bin) {
|
|
20451
|
+
try {
|
|
20452
|
+
const { stdout, stderr } = await runHostBin(bin, ["plugin", "marketplace", "add", "--help"], {
|
|
20453
|
+
timeout: CLAUDE_PLUGIN_TIMEOUT_MS
|
|
20454
|
+
});
|
|
20455
|
+
return marketplaceAddSupportsRef(`${stdout}
|
|
20456
|
+
${stderr}`);
|
|
20457
|
+
} catch {
|
|
20458
|
+
return false;
|
|
20459
|
+
}
|
|
20460
|
+
}
|
|
19404
20461
|
async function applyPluginHeal(token, surface, log, opts) {
|
|
19405
20462
|
if (!opts?.force && surfaceToken(surface) !== token) return false;
|
|
19406
20463
|
const descriptor = PLUGIN_SURFACE_HEAL[token];
|
|
19407
|
-
const
|
|
19408
|
-
if (!
|
|
20464
|
+
const tableSteps = descriptor.healSteps;
|
|
20465
|
+
if (!tableSteps) return false;
|
|
20466
|
+
const bin = descriptor.pluginRunner === "codex" ? "codex" : "claude";
|
|
19409
20467
|
const runner = descriptor.pluginRunner === "codex" ? runCodexPlugin : runClaudePlugin;
|
|
20468
|
+
const refSupported = await marketplaceAddRefSupported(bin);
|
|
20469
|
+
const { steps, strippedRef } = adaptHealStepsForRefSupport(tableSteps, refSupported);
|
|
19410
20470
|
log(
|
|
19411
|
-
|
|
20471
|
+
refSupported ? ` \u21BB reinstalling the MMI plugin via \`${bin} plugin\` (marketplace remove \u2192 add --ref main \u2192 ${token === "codex" ? "add" : "install"})\u2026` : ` \u21BB reinstalling the MMI plugin via \`${bin} plugin\` (marketplace remove \u2192 add \u2192 ${token === "codex" ? "add" : "install"})\u2026`
|
|
19412
20472
|
);
|
|
20473
|
+
if (strippedRef) {
|
|
20474
|
+
log(` \u26A0 \`${bin}\` has no \`--ref\` option \u2014 cloning the default branch; update \`${bin}\` to pin the released \`main\` branch (#2080)`);
|
|
20475
|
+
}
|
|
19413
20476
|
for (const step of steps) {
|
|
19414
20477
|
if (healStepAborts(step, await runner([...step.args]))) return false;
|
|
19415
20478
|
}
|
|
@@ -19417,11 +20480,11 @@ async function applyPluginHeal(token, surface, log, opts) {
|
|
|
19417
20480
|
}
|
|
19418
20481
|
var installedPluginsPath = (surface = detectSurface(process.env)) => {
|
|
19419
20482
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
19420
|
-
return (0,
|
|
20483
|
+
return (0, import_node_path25.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
19421
20484
|
};
|
|
19422
20485
|
function readInstalledPlugins(surface = detectSurface(process.env)) {
|
|
19423
20486
|
try {
|
|
19424
|
-
return JSON.parse((0,
|
|
20487
|
+
return JSON.parse((0, import_node_fs28.readFileSync)(installedPluginsPath(surface), "utf8"));
|
|
19425
20488
|
} catch {
|
|
19426
20489
|
return null;
|
|
19427
20490
|
}
|
|
@@ -19432,15 +20495,15 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
|
|
|
19432
20495
|
return {
|
|
19433
20496
|
isOrgRepo,
|
|
19434
20497
|
installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
|
|
19435
|
-
marketplaceClonePresent: (0,
|
|
19436
|
-
pluginCachePresent: (0,
|
|
20498
|
+
marketplaceClonePresent: (0, import_node_fs28.existsSync)((0, import_node_path25.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "marketplaces", "mutmutco")),
|
|
20499
|
+
pluginCachePresent: (0, import_node_fs28.existsSync)((0, import_node_path25.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
|
|
19437
20500
|
};
|
|
19438
20501
|
}
|
|
19439
20502
|
function installedPluginSources() {
|
|
19440
20503
|
return ["claude", "codex"].map((surface) => {
|
|
19441
|
-
const recordPath = (0,
|
|
20504
|
+
const recordPath = (0, import_node_path25.join)((0, import_node_os6.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
|
|
19442
20505
|
try {
|
|
19443
|
-
return { surface, installed: JSON.parse((0,
|
|
20506
|
+
return { surface, installed: JSON.parse((0, import_node_fs28.readFileSync)(recordPath, "utf8")), recordPath };
|
|
19444
20507
|
} catch {
|
|
19445
20508
|
return { surface, installed: null, recordPath };
|
|
19446
20509
|
}
|
|
@@ -19448,7 +20511,7 @@ function installedPluginSources() {
|
|
|
19448
20511
|
}
|
|
19449
20512
|
function readClaudeSettings() {
|
|
19450
20513
|
try {
|
|
19451
|
-
return JSON.parse((0,
|
|
20514
|
+
return JSON.parse((0, import_node_fs28.readFileSync)((0, import_node_path25.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
|
|
19452
20515
|
} catch {
|
|
19453
20516
|
return null;
|
|
19454
20517
|
}
|
|
@@ -19470,7 +20533,7 @@ function writeProjectInstallRecord(record) {
|
|
|
19470
20533
|
const list = file.plugins[MMI_PLUGIN_ID] ?? [];
|
|
19471
20534
|
list.push(record);
|
|
19472
20535
|
file.plugins[MMI_PLUGIN_ID] = list;
|
|
19473
|
-
(0,
|
|
20536
|
+
(0, import_node_fs28.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
|
|
19474
20537
|
`, "utf8");
|
|
19475
20538
|
return true;
|
|
19476
20539
|
} catch {
|
|
@@ -19483,9 +20546,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
19483
20546
|
if (!file) return false;
|
|
19484
20547
|
if (!file.plugins) file.plugins = {};
|
|
19485
20548
|
const path2 = installedPluginsPath();
|
|
19486
|
-
(0,
|
|
20549
|
+
(0, import_node_fs28.copyFileSync)(path2, `${path2}.bak`);
|
|
19487
20550
|
file.plugins[pluginId] = records;
|
|
19488
|
-
(0,
|
|
20551
|
+
(0, import_node_fs28.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
|
|
19489
20552
|
`, "utf8");
|
|
19490
20553
|
return true;
|
|
19491
20554
|
} catch {
|
|
@@ -19493,22 +20556,22 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
19493
20556
|
}
|
|
19494
20557
|
}
|
|
19495
20558
|
function opencodeConfigDir() {
|
|
19496
|
-
return (0,
|
|
20559
|
+
return (0, import_node_path25.join)((0, import_node_os6.homedir)(), ".config", "opencode");
|
|
19497
20560
|
}
|
|
19498
20561
|
function opencodeConfigPath() {
|
|
19499
|
-
return (0,
|
|
20562
|
+
return (0, import_node_path25.join)(opencodeConfigDir(), "opencode.jsonc");
|
|
19500
20563
|
}
|
|
19501
20564
|
function opencodeCommandsDir() {
|
|
19502
|
-
return (0,
|
|
20565
|
+
return (0, import_node_path25.join)(opencodeConfigDir(), "commands");
|
|
19503
20566
|
}
|
|
19504
20567
|
function opencodeSkillsPath() {
|
|
19505
|
-
return (0,
|
|
20568
|
+
return (0, import_node_path25.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "skills");
|
|
19506
20569
|
}
|
|
19507
20570
|
function opencodeConfigSnapshot() {
|
|
19508
20571
|
const path2 = opencodeConfigPath();
|
|
19509
|
-
if (!(0,
|
|
20572
|
+
if (!(0, import_node_fs28.existsSync)(path2)) return { path: path2, hasConfig: false, hasPluginField: false, parseOk: true };
|
|
19510
20573
|
try {
|
|
19511
|
-
const raw = (0,
|
|
20574
|
+
const raw = (0, import_node_fs28.readFileSync)(path2, "utf8");
|
|
19512
20575
|
const parsed = JSON.parse(stripJsonc(raw));
|
|
19513
20576
|
const hasPluginField = Object.prototype.hasOwnProperty.call(parsed, "plugin");
|
|
19514
20577
|
const skillsPaths = Array.isArray(parsed.skills?.paths) ? parsed.skills.paths.filter((p) => typeof p === "string") : void 0;
|
|
@@ -19531,9 +20594,9 @@ function writeOpencodeConfigPlugin(snapshot) {
|
|
|
19531
20594
|
const plan2 = planOpencodeConfigWrite(snapshot.hasConfig ? snapshot.raw : void 0);
|
|
19532
20595
|
if (plan2.action === "already") return true;
|
|
19533
20596
|
if (!plan2.text || plan2.action === "unsafe") return false;
|
|
19534
|
-
(0,
|
|
19535
|
-
if (snapshot.hasConfig) (0,
|
|
19536
|
-
(0,
|
|
20597
|
+
(0, import_node_fs28.mkdirSync)((0, import_node_path25.dirname)(path2), { recursive: true });
|
|
20598
|
+
if (snapshot.hasConfig) (0, import_node_fs28.copyFileSync)(path2, `${path2}.bak`);
|
|
20599
|
+
(0, import_node_fs28.writeFileSync)(path2, plan2.text, "utf8");
|
|
19537
20600
|
return true;
|
|
19538
20601
|
} catch {
|
|
19539
20602
|
return false;
|
|
@@ -19548,9 +20611,9 @@ function writeOpencodeSkillsPath(snapshot, skillsPath) {
|
|
|
19548
20611
|
const normalized = skillsPath.replace(/\\/g, "/");
|
|
19549
20612
|
if (!paths.some((p) => p.replace(/\\/g, "/") === normalized)) paths.push(skillsPath.replace(/\\/g, "/"));
|
|
19550
20613
|
parsed.skills = { ...skills, paths };
|
|
19551
|
-
(0,
|
|
19552
|
-
if (snapshot.hasConfig && (0,
|
|
19553
|
-
(0,
|
|
20614
|
+
(0, import_node_fs28.mkdirSync)((0, import_node_path25.dirname)(snapshot.path), { recursive: true });
|
|
20615
|
+
if (snapshot.hasConfig && (0, import_node_fs28.existsSync)(snapshot.path)) (0, import_node_fs28.copyFileSync)(snapshot.path, `${snapshot.path}.bak`);
|
|
20616
|
+
(0, import_node_fs28.writeFileSync)(snapshot.path, `${JSON.stringify(parsed, null, 2)}
|
|
19554
20617
|
`, "utf8");
|
|
19555
20618
|
return true;
|
|
19556
20619
|
} catch {
|
|
@@ -19559,7 +20622,7 @@ function writeOpencodeSkillsPath(snapshot, skillsPath) {
|
|
|
19559
20622
|
}
|
|
19560
20623
|
function opencodeExistingCommands() {
|
|
19561
20624
|
try {
|
|
19562
|
-
return (0,
|
|
20625
|
+
return (0, import_node_fs28.readdirSync)(opencodeCommandsDir(), { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name.slice(0, -3).toLowerCase());
|
|
19563
20626
|
} catch {
|
|
19564
20627
|
return [];
|
|
19565
20628
|
}
|
|
@@ -19567,9 +20630,9 @@ function opencodeExistingCommands() {
|
|
|
19567
20630
|
function writeOpencodeCommandFiles() {
|
|
19568
20631
|
try {
|
|
19569
20632
|
const dir = opencodeCommandsDir();
|
|
19570
|
-
(0,
|
|
20633
|
+
(0, import_node_fs28.mkdirSync)(dir, { recursive: true });
|
|
19571
20634
|
for (const command of OPENCODE_WORKFLOW_COMMANDS) {
|
|
19572
|
-
(0,
|
|
20635
|
+
(0, import_node_fs28.writeFileSync)((0, import_node_path25.join)(dir, `${command}.md`), opencodeCommandMarkdown(command), "utf8");
|
|
19573
20636
|
}
|
|
19574
20637
|
return true;
|
|
19575
20638
|
} catch {
|
|
@@ -19578,12 +20641,12 @@ function writeOpencodeCommandFiles() {
|
|
|
19578
20641
|
}
|
|
19579
20642
|
function readOpencodeAdapterDiskVersion() {
|
|
19580
20643
|
const candidates = [
|
|
19581
|
-
(0,
|
|
19582
|
-
(0,
|
|
20644
|
+
(0, import_node_path25.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "package.json"),
|
|
20645
|
+
(0, import_node_path25.join)((0, import_node_os6.homedir)(), ".cache", "opencode", "node_modules", "@mutmutco", "opencode-mmi", "package.json")
|
|
19583
20646
|
];
|
|
19584
20647
|
for (const path2 of candidates) {
|
|
19585
20648
|
try {
|
|
19586
|
-
const parsed = JSON.parse((0,
|
|
20649
|
+
const parsed = JSON.parse((0, import_node_fs28.readFileSync)(path2, "utf8"));
|
|
19587
20650
|
if (typeof parsed.version === "string" && parsed.version.trim()) return parsed.version.trim();
|
|
19588
20651
|
} catch {
|
|
19589
20652
|
continue;
|
|
@@ -19599,7 +20662,7 @@ async function forceInstallOpencodeMmiPlugins(snapshot, log) {
|
|
|
19599
20662
|
try {
|
|
19600
20663
|
const specs = opencodeMmiPluginSpecs(snapshot);
|
|
19601
20664
|
log(` \u21BB force-refreshing OpenCode MMI npm plugin(s): ${specs.join(", ")}\u2026`);
|
|
19602
|
-
(0,
|
|
20665
|
+
(0, import_node_fs28.mkdirSync)(opencodeConfigDir(), { recursive: true });
|
|
19603
20666
|
await runHostBin("npm", ["install", "--prefix", opencodeConfigDir(), "--force", ...specs], { timeout: NPM_UPDATE_TIMEOUT_MS });
|
|
19604
20667
|
return true;
|
|
19605
20668
|
} catch {
|
|
@@ -19614,30 +20677,30 @@ function opencodePluginVersionsForReport() {
|
|
|
19614
20677
|
}
|
|
19615
20678
|
function opencodeDesktopLogsRoot() {
|
|
19616
20679
|
if (process.platform === "win32") {
|
|
19617
|
-
const base = process.env.APPDATA || (0,
|
|
19618
|
-
return (0,
|
|
20680
|
+
const base = process.env.APPDATA || (0, import_node_path25.join)((0, import_node_os6.homedir)(), "AppData", "Roaming");
|
|
20681
|
+
return (0, import_node_path25.join)(base, "ai.opencode.desktop", "logs");
|
|
19619
20682
|
}
|
|
19620
20683
|
if (process.platform === "darwin") {
|
|
19621
|
-
return (0,
|
|
20684
|
+
return (0, import_node_path25.join)((0, import_node_os6.homedir)(), "Library", "Application Support", "ai.opencode.desktop", "logs");
|
|
19622
20685
|
}
|
|
19623
|
-
return (0,
|
|
20686
|
+
return (0, import_node_path25.join)((0, import_node_os6.homedir)(), ".config", "ai.opencode.desktop", "logs");
|
|
19624
20687
|
}
|
|
19625
20688
|
function opencodeDesktopBootstrapSnapshot() {
|
|
19626
20689
|
const root = opencodeDesktopLogsRoot();
|
|
19627
20690
|
try {
|
|
19628
|
-
const sessionDirs = (0,
|
|
20691
|
+
const sessionDirs = (0, import_node_fs28.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => (0, import_node_path25.join)(root, entry.name)).sort((a, b) => (0, import_node_fs28.statSync)(b).mtimeMs - (0, import_node_fs28.statSync)(a).mtimeMs);
|
|
19629
20692
|
const newest = sessionDirs[0];
|
|
19630
20693
|
if (!newest) return [];
|
|
19631
|
-
const logPath = (0,
|
|
19632
|
-
const text = (0,
|
|
19633
|
-
return opencodeAgentDirectoriesFromLog(text).filter((directory) => !(0,
|
|
20694
|
+
const logPath = (0, import_node_path25.join)(newest, "renderer.log");
|
|
20695
|
+
const text = (0, import_node_fs28.readFileSync)(logPath, "utf8");
|
|
20696
|
+
return opencodeAgentDirectoriesFromLog(text).filter((directory) => !(0, import_node_fs28.existsSync)(directory)).map((directory) => ({ directory, logPath }));
|
|
19634
20697
|
} catch {
|
|
19635
20698
|
return [];
|
|
19636
20699
|
}
|
|
19637
20700
|
}
|
|
19638
20701
|
function opencodeLegacyConfigSnapshot() {
|
|
19639
|
-
const legacyPath = (0,
|
|
19640
|
-
if (!(0,
|
|
20702
|
+
const legacyPath = (0, import_node_path25.join)((0, import_node_os6.homedir)(), ".opencode", "opencode.json");
|
|
20703
|
+
if (!(0, import_node_fs28.existsSync)(legacyPath)) return {};
|
|
19641
20704
|
const content = readTextFile(legacyPath);
|
|
19642
20705
|
if (content == null) return {};
|
|
19643
20706
|
const plugins = parseOpencodeLegacyConfigPlugins(content);
|
|
@@ -19649,43 +20712,46 @@ function opencodeLegacyConfigSnapshot() {
|
|
|
19649
20712
|
function quarantineOpencodeLegacyConfig(legacyPath) {
|
|
19650
20713
|
try {
|
|
19651
20714
|
const backupPath = `${legacyPath}.bak`;
|
|
19652
|
-
if ((0,
|
|
19653
|
-
(0,
|
|
20715
|
+
if ((0, import_node_fs28.existsSync)(backupPath)) return false;
|
|
20716
|
+
(0, import_node_fs28.renameSync)(legacyPath, backupPath);
|
|
19654
20717
|
return true;
|
|
19655
20718
|
} catch {
|
|
19656
20719
|
return false;
|
|
19657
20720
|
}
|
|
19658
20721
|
}
|
|
19659
20722
|
function cursorPluginCacheRoot() {
|
|
19660
|
-
return (0,
|
|
20723
|
+
return (0, import_node_path25.join)((0, import_node_os6.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
|
|
19661
20724
|
}
|
|
19662
20725
|
function cursorPluginCachePinSnapshots() {
|
|
19663
20726
|
const root = cursorPluginCacheRoot();
|
|
19664
20727
|
try {
|
|
19665
|
-
return (0,
|
|
19666
|
-
const path2 = (0,
|
|
19667
|
-
const pluginJson = (0,
|
|
19668
|
-
const hooksJson = (0,
|
|
19669
|
-
const cliBundle = (0,
|
|
20728
|
+
return (0, import_node_fs28.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
|
|
20729
|
+
const path2 = (0, import_node_path25.join)(root, entry.name);
|
|
20730
|
+
const pluginJson = (0, import_node_path25.join)(path2, ".cursor-plugin", "plugin.json");
|
|
20731
|
+
const hooksJson = (0, import_node_path25.join)(path2, "hooks", "hooks.json");
|
|
20732
|
+
const cliBundle = (0, import_node_path25.join)(path2, "cli", "dist", "index.cjs");
|
|
20733
|
+
const throttleGateCursor = (0, import_node_path25.join)(path2, "scripts", "throttle-gate-cursor.mjs");
|
|
20734
|
+
const throttleCore = (0, import_node_path25.join)(path2, "scripts", "throttle-core.mjs");
|
|
19670
20735
|
let version;
|
|
19671
20736
|
try {
|
|
19672
|
-
const raw = JSON.parse((0,
|
|
20737
|
+
const raw = JSON.parse((0, import_node_fs28.readFileSync)(pluginJson, "utf8"));
|
|
19673
20738
|
version = typeof raw.version === "string" ? raw.version : void 0;
|
|
19674
20739
|
} catch {
|
|
19675
20740
|
version = void 0;
|
|
19676
20741
|
}
|
|
19677
20742
|
let isEmpty = true;
|
|
19678
20743
|
try {
|
|
19679
|
-
isEmpty = (0,
|
|
20744
|
+
isEmpty = (0, import_node_fs28.readdirSync)(path2).length === 0;
|
|
19680
20745
|
} catch {
|
|
19681
20746
|
isEmpty = true;
|
|
19682
20747
|
}
|
|
19683
20748
|
return {
|
|
19684
20749
|
name: entry.name,
|
|
19685
20750
|
path: path2,
|
|
19686
|
-
hasPluginJson: (0,
|
|
19687
|
-
hasHooksJson: (0,
|
|
19688
|
-
hasCliBundle: (0,
|
|
20751
|
+
hasPluginJson: (0, import_node_fs28.existsSync)(pluginJson),
|
|
20752
|
+
hasHooksJson: (0, import_node_fs28.existsSync)(hooksJson),
|
|
20753
|
+
hasCliBundle: (0, import_node_fs28.existsSync)(cliBundle),
|
|
20754
|
+
hasShellDialectGuard: (0, import_node_fs28.existsSync)(throttleGateCursor) && (0, import_node_fs28.existsSync)(throttleCore),
|
|
19689
20755
|
isEmpty,
|
|
19690
20756
|
version
|
|
19691
20757
|
};
|
|
@@ -19695,19 +20761,19 @@ function cursorPluginCachePinSnapshots() {
|
|
|
19695
20761
|
}
|
|
19696
20762
|
}
|
|
19697
20763
|
function hubCheckoutForCursorSeed() {
|
|
19698
|
-
const manifest = (0,
|
|
19699
|
-
return (0,
|
|
20764
|
+
const manifest = (0, import_node_path25.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
|
|
20765
|
+
return (0, import_node_fs28.existsSync)(manifest) ? process.cwd() : void 0;
|
|
19700
20766
|
}
|
|
19701
20767
|
function mmiPluginCacheRootSnapshots() {
|
|
19702
20768
|
const roots = [
|
|
19703
|
-
{ surface: "claude", root: (0,
|
|
19704
|
-
{ surface: "codex", root: (0,
|
|
20769
|
+
{ surface: "claude", root: (0, import_node_path25.join)((0, import_node_os6.homedir)(), ".claude", "plugins", "cache", "mutmutco", "mmi") },
|
|
20770
|
+
{ surface: "codex", root: (0, import_node_path25.join)((0, import_node_os6.homedir)(), ".codex", "plugins", "cache", "mutmutco", "mmi") }
|
|
19705
20771
|
];
|
|
19706
20772
|
return roots.flatMap(({ surface, root }) => {
|
|
19707
20773
|
try {
|
|
19708
|
-
const entries = (0,
|
|
20774
|
+
const entries = (0, import_node_fs28.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
|
|
19709
20775
|
name: entry.name,
|
|
19710
|
-
path: (0,
|
|
20776
|
+
path: (0, import_node_path25.join)(root, entry.name),
|
|
19711
20777
|
isDirectory: entry.isDirectory()
|
|
19712
20778
|
}));
|
|
19713
20779
|
return [{ surface, root, entries }];
|
|
@@ -19718,7 +20784,7 @@ function mmiPluginCacheRootSnapshots() {
|
|
|
19718
20784
|
}
|
|
19719
20785
|
function hasNestedMmiChild(versionDir) {
|
|
19720
20786
|
try {
|
|
19721
|
-
return (0,
|
|
20787
|
+
return (0, import_node_fs28.statSync)((0, import_node_path25.join)(versionDir, "mmi")).isDirectory();
|
|
19722
20788
|
} catch {
|
|
19723
20789
|
return false;
|
|
19724
20790
|
}
|
|
@@ -19729,10 +20795,10 @@ function nestedPluginTreeSnapshot() {
|
|
|
19729
20795
|
);
|
|
19730
20796
|
}
|
|
19731
20797
|
function uniqueQuarantineTarget(path2) {
|
|
19732
|
-
if (!(0,
|
|
20798
|
+
if (!(0, import_node_fs28.existsSync)(path2)) return path2;
|
|
19733
20799
|
for (let i = 1; i < 100; i += 1) {
|
|
19734
20800
|
const candidate = `${path2}-${i}`;
|
|
19735
|
-
if (!(0,
|
|
20801
|
+
if (!(0, import_node_fs28.existsSync)(candidate)) return candidate;
|
|
19736
20802
|
}
|
|
19737
20803
|
return `${path2}-${Date.now()}`;
|
|
19738
20804
|
}
|
|
@@ -19741,10 +20807,10 @@ function quarantinePluginCacheDirs(plan2) {
|
|
|
19741
20807
|
const failed = [];
|
|
19742
20808
|
for (const move of plan2) {
|
|
19743
20809
|
try {
|
|
19744
|
-
if (!(0,
|
|
20810
|
+
if (!(0, import_node_fs28.existsSync)(move.from)) continue;
|
|
19745
20811
|
const target = uniqueQuarantineTarget(move.to);
|
|
19746
|
-
(0,
|
|
19747
|
-
(0,
|
|
20812
|
+
(0, import_node_fs28.mkdirSync)((0, import_node_path25.dirname)(target), { recursive: true });
|
|
20813
|
+
(0, import_node_fs28.renameSync)(move.from, target);
|
|
19748
20814
|
moved += 1;
|
|
19749
20815
|
} catch {
|
|
19750
20816
|
failed.push(move);
|
|
@@ -19763,23 +20829,23 @@ async function robocopyMirrorEmpty(emptyDir, target) {
|
|
|
19763
20829
|
}
|
|
19764
20830
|
async function clearNestedPluginTreeDir(targetPath) {
|
|
19765
20831
|
try {
|
|
19766
|
-
if (!(0,
|
|
20832
|
+
if (!(0, import_node_fs28.existsSync)(targetPath)) return true;
|
|
19767
20833
|
if (isWin) {
|
|
19768
|
-
const emptyDir = (0,
|
|
19769
|
-
(0,
|
|
20834
|
+
const emptyDir = (0, import_node_path25.join)((0, import_node_os6.tmpdir)(), `mmi-empty-${Date.now()}`);
|
|
20835
|
+
(0, import_node_fs28.mkdirSync)(emptyDir, { recursive: true });
|
|
19770
20836
|
try {
|
|
19771
20837
|
await robocopyMirrorEmpty(emptyDir, targetPath);
|
|
19772
|
-
(0,
|
|
20838
|
+
(0, import_node_fs28.rmSync)(targetPath, { recursive: true, force: true });
|
|
19773
20839
|
} finally {
|
|
19774
20840
|
try {
|
|
19775
|
-
(0,
|
|
20841
|
+
(0, import_node_fs28.rmSync)(emptyDir, { recursive: true, force: true });
|
|
19776
20842
|
} catch {
|
|
19777
20843
|
}
|
|
19778
20844
|
}
|
|
19779
|
-
return !(0,
|
|
20845
|
+
return !(0, import_node_fs28.existsSync)(targetPath);
|
|
19780
20846
|
}
|
|
19781
|
-
(0,
|
|
19782
|
-
return !(0,
|
|
20847
|
+
(0, import_node_fs28.rmSync)(targetPath, { recursive: true, force: true });
|
|
20848
|
+
return !(0, import_node_fs28.existsSync)(targetPath);
|
|
19783
20849
|
} catch {
|
|
19784
20850
|
return false;
|
|
19785
20851
|
}
|
|
@@ -19792,23 +20858,23 @@ async function applyNestedPluginTreeCleanup(paths, log) {
|
|
|
19792
20858
|
}
|
|
19793
20859
|
return true;
|
|
19794
20860
|
}
|
|
19795
|
-
var gitignorePath = () => (0,
|
|
20861
|
+
var gitignorePath = () => (0, import_node_path25.join)(process.cwd(), ".gitignore");
|
|
19796
20862
|
function readTextFile(path2) {
|
|
19797
20863
|
try {
|
|
19798
|
-
if (!(0,
|
|
19799
|
-
return (0,
|
|
20864
|
+
if (!(0, import_node_fs28.existsSync)(path2)) return null;
|
|
20865
|
+
return (0, import_node_fs28.readFileSync)(path2, "utf8");
|
|
19800
20866
|
} catch {
|
|
19801
20867
|
return null;
|
|
19802
20868
|
}
|
|
19803
20869
|
}
|
|
19804
20870
|
function playwrightMcpConfigSnapshots() {
|
|
19805
20871
|
const cwd = process.cwd();
|
|
19806
|
-
const home = (0,
|
|
20872
|
+
const home = (0, import_node_os6.homedir)();
|
|
19807
20873
|
const candidates = [
|
|
19808
|
-
(0,
|
|
19809
|
-
(0,
|
|
19810
|
-
(0,
|
|
19811
|
-
(0,
|
|
20874
|
+
(0, import_node_path25.join)(cwd, ".mcp.json"),
|
|
20875
|
+
(0, import_node_path25.join)(cwd, ".cursor", "mcp.json"),
|
|
20876
|
+
(0, import_node_path25.join)(home, ".cursor", "mcp.json"),
|
|
20877
|
+
(0, import_node_path25.join)(home, ".codex", "config.toml")
|
|
19812
20878
|
];
|
|
19813
20879
|
const out = [];
|
|
19814
20880
|
for (const path2 of candidates) {
|
|
@@ -19821,7 +20887,7 @@ function strayBrowserArtifactPaths() {
|
|
|
19821
20887
|
const cwd = process.cwd();
|
|
19822
20888
|
return STRAY_BROWSER_ARTIFACT_DIRS.filter((rel) => {
|
|
19823
20889
|
try {
|
|
19824
|
-
return (0,
|
|
20890
|
+
return (0, import_node_fs28.existsSync)((0, import_node_path25.join)(cwd, rel));
|
|
19825
20891
|
} catch {
|
|
19826
20892
|
return false;
|
|
19827
20893
|
}
|
|
@@ -19840,8 +20906,8 @@ function latestIso(values) {
|
|
|
19840
20906
|
return best;
|
|
19841
20907
|
}
|
|
19842
20908
|
function latestNorthstarContinuityAt() {
|
|
19843
|
-
const meta = parseMeta(readTextFile((0,
|
|
19844
|
-
const queue = parseQueue(readTextFile((0,
|
|
20909
|
+
const meta = parseMeta(readTextFile((0, import_node_path25.join)(process.cwd(), META_FILE)));
|
|
20910
|
+
const queue = parseQueue(readTextFile((0, import_node_path25.join)(process.cwd(), QUEUE_FILE)));
|
|
19845
20911
|
return latestIso([
|
|
19846
20912
|
...Object.values(meta).map((entry) => entry.syncedAt),
|
|
19847
20913
|
...queue.map((entry) => entry.queuedAt)
|
|
@@ -19855,14 +20921,14 @@ async function latestBranchWorkAt() {
|
|
|
19855
20921
|
}
|
|
19856
20922
|
function readGitignore() {
|
|
19857
20923
|
try {
|
|
19858
|
-
return (0,
|
|
20924
|
+
return (0, import_node_fs28.readFileSync)(gitignorePath(), "utf8");
|
|
19859
20925
|
} catch {
|
|
19860
20926
|
return null;
|
|
19861
20927
|
}
|
|
19862
20928
|
}
|
|
19863
20929
|
function writeGitignore(content) {
|
|
19864
20930
|
try {
|
|
19865
|
-
(0,
|
|
20931
|
+
(0, import_node_fs28.writeFileSync)(gitignorePath(), content, "utf8");
|
|
19866
20932
|
return true;
|
|
19867
20933
|
} catch {
|
|
19868
20934
|
return false;
|
|
@@ -19901,7 +20967,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19901
20967
|
const semverPrefix = /^\d+\.\d+\.\d+/;
|
|
19902
20968
|
const isBehind = (installed2, released) => Boolean(installed2 && released && semverPrefix.test(installed2) && semverPrefix.test(released) && compareVersions(installed2, released) < 0);
|
|
19903
20969
|
const opencodeAdapterStale = isBehind(opencodeInstalledVersionForDoctor(), releasedVersion);
|
|
19904
|
-
const cursorCacheStale = (0,
|
|
20970
|
+
const cursorCacheStale = (0, import_node_fs28.existsSync)(cursorPluginCacheRoot()) && (cursorPluginCachePinSnapshots() ?? []).some((p) => isBehind(p.version, releasedVersion));
|
|
19905
20971
|
const healPlan = doctorHealPlan({
|
|
19906
20972
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
19907
20973
|
surface,
|
|
@@ -19936,7 +21002,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19936
21002
|
let onPath = pathProbe;
|
|
19937
21003
|
if (!onPath) {
|
|
19938
21004
|
const root = process.env.CLAUDE_PLUGIN_ROOT;
|
|
19939
|
-
if (root && (0,
|
|
21005
|
+
if (root && (0, import_node_fs28.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
|
|
19940
21006
|
}
|
|
19941
21007
|
checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
|
|
19942
21008
|
const reloadHint = reloadAction(surface);
|
|
@@ -19950,7 +21016,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19950
21016
|
releasedVersion
|
|
19951
21017
|
});
|
|
19952
21018
|
let selfUpdatedCli = false;
|
|
19953
|
-
if (repairFull) {
|
|
21019
|
+
if (repairFull && !process.env[DOCTOR_POST_SELF_UPDATE_ENV]) {
|
|
19954
21020
|
const updated = await applyVersionAutoUpdate(versionReport, (m) => io.err(m));
|
|
19955
21021
|
versionReport = updated.report;
|
|
19956
21022
|
selfUpdatedCli = updated.applied === "npm";
|
|
@@ -19958,6 +21024,12 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19958
21024
|
if (!versionReport.ok) versionReport = { ...versionReport, fix: pluginRecoveryFix(surface) };
|
|
19959
21025
|
checks.push(versionReport);
|
|
19960
21026
|
if (selfUpdatedCli) {
|
|
21027
|
+
if (!opts.json) io.err(selfUpdateReexecLine(versionReport));
|
|
21028
|
+
const code = await reexecMmiCli(buildSelfUpdateReexecArgs(opts));
|
|
21029
|
+
if (code >= 0) {
|
|
21030
|
+
if (code > 0) process.exitCode = code;
|
|
21031
|
+
return;
|
|
21032
|
+
}
|
|
19961
21033
|
if (opts.json) io.log(JSON.stringify(buildSelfUpdateHaltPayload({ checks, updatedTo: versionReport.releasedVersion }), null, 2));
|
|
19962
21034
|
else io.err(selfUpdateHaltLine(versionReport));
|
|
19963
21035
|
return;
|
|
@@ -20209,6 +21281,11 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
20209
21281
|
}
|
|
20210
21282
|
checks.push(legacyOpenCodeCheck);
|
|
20211
21283
|
}
|
|
21284
|
+
const codexRecordVersion = () => {
|
|
21285
|
+
const versions = installedPluginVersions(installedPluginSources().find((src) => src.surface === "codex")?.installed ?? null).filter((v) => /^v?\d+\.\d+\.\d+/.test(v));
|
|
21286
|
+
return versions.sort((a, b) => compareVersions(b, a))[0];
|
|
21287
|
+
};
|
|
21288
|
+
const codexCacheVersions = () => mmiPluginCacheRootSnapshots().filter((r) => r.surface === "codex").flatMap((r) => r.entries.filter((e) => e.isDirectory).map((e) => e.name));
|
|
20212
21289
|
let cacheCleanupCheck = buildMmiPluginCacheCleanupCheck({
|
|
20213
21290
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
20214
21291
|
roots: mmiPluginCacheRootSnapshots(),
|
|
@@ -20240,6 +21317,26 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
20240
21317
|
};
|
|
20241
21318
|
}
|
|
20242
21319
|
checks.push(cacheCleanupCheck);
|
|
21320
|
+
let codexActiveCacheCheck = buildCodexActiveCacheCheck({
|
|
21321
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
21322
|
+
releasedVersion,
|
|
21323
|
+
codexCacheVersions: codexCacheVersions(),
|
|
21324
|
+
codexRecordVersion: codexRecordVersion()
|
|
21325
|
+
});
|
|
21326
|
+
if (!codexActiveCacheCheck.ok && repairFull) {
|
|
21327
|
+
const canDriveCodex = surfaceToken(surface) === "codex" || await hostBinAvailable("codex");
|
|
21328
|
+
if (canDriveCodex && await applyPluginHeal("codex", surface, (m) => io.err(m), { force: true })) {
|
|
21329
|
+
markPluginReloadRequired();
|
|
21330
|
+
io.err(` \u21BB restored Codex MMI plugin cache \u2192 ${releasedVersion ?? "latest"} via codex plugin \u2014 ${reloadAction("codex")} to load the new commands`);
|
|
21331
|
+
codexActiveCacheCheck = buildCodexActiveCacheCheck({
|
|
21332
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
21333
|
+
releasedVersion,
|
|
21334
|
+
codexCacheVersions: codexCacheVersions(),
|
|
21335
|
+
codexRecordVersion: codexRecordVersion()
|
|
21336
|
+
});
|
|
21337
|
+
}
|
|
21338
|
+
}
|
|
21339
|
+
checks.push(codexActiveCacheCheck);
|
|
20243
21340
|
let nestedPluginTreeCheck = buildNestedPluginTreeCheck({
|
|
20244
21341
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
20245
21342
|
isWindows: isWin,
|
|
@@ -20274,7 +21371,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
20274
21371
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
20275
21372
|
surface,
|
|
20276
21373
|
cacheRoot: cursorCacheRoot,
|
|
20277
|
-
cacheRootExists: (0,
|
|
21374
|
+
cacheRootExists: (0, import_node_fs28.existsSync)(cursorCacheRoot),
|
|
20278
21375
|
pins: cursorPins,
|
|
20279
21376
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
20280
21377
|
releasedVersion
|
|
@@ -20285,7 +21382,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
20285
21382
|
releasedVersion,
|
|
20286
21383
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
20287
21384
|
execFileP: execFileP2,
|
|
20288
|
-
mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0,
|
|
21385
|
+
mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0, import_node_path25.join)((0, import_node_os6.tmpdir)(), prefix)),
|
|
20289
21386
|
log: (m) => io.err(m)
|
|
20290
21387
|
});
|
|
20291
21388
|
if (seeded) {
|
|
@@ -20294,7 +21391,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
20294
21391
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
20295
21392
|
surface,
|
|
20296
21393
|
cacheRoot: cursorCacheRoot,
|
|
20297
|
-
cacheRootExists: (0,
|
|
21394
|
+
cacheRootExists: (0, import_node_fs28.existsSync)(cursorCacheRoot),
|
|
20298
21395
|
pins: cursorPins,
|
|
20299
21396
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
20300
21397
|
releasedVersion
|
|
@@ -20483,23 +21580,23 @@ function mergeGuardHook(settings) {
|
|
|
20483
21580
|
next.hooks = hooks;
|
|
20484
21581
|
return next;
|
|
20485
21582
|
}
|
|
20486
|
-
var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0,
|
|
21583
|
+
var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path25.join)((0, import_node_os6.homedir)(), surface === "codex" ? ".codex" : ".claude", "settings.json");
|
|
20487
21584
|
function ensureUserScopeGuardHook(opts = {}) {
|
|
20488
21585
|
const path2 = opts.settingsPath ?? userScopeSettingsPath();
|
|
20489
21586
|
try {
|
|
20490
21587
|
let current = null;
|
|
20491
|
-
if ((0,
|
|
21588
|
+
if ((0, import_node_fs28.existsSync)(path2)) {
|
|
20492
21589
|
try {
|
|
20493
|
-
current = JSON.parse((0,
|
|
21590
|
+
current = JSON.parse((0, import_node_fs28.readFileSync)(path2, "utf8"));
|
|
20494
21591
|
} catch {
|
|
20495
21592
|
return "failed";
|
|
20496
21593
|
}
|
|
20497
21594
|
}
|
|
20498
21595
|
if (settingsHasGuardHook(current)) return "already";
|
|
20499
21596
|
const merged = mergeGuardHook(current);
|
|
20500
|
-
(0,
|
|
20501
|
-
if ((0,
|
|
20502
|
-
(0,
|
|
21597
|
+
(0, import_node_fs28.mkdirSync)((0, import_node_path25.dirname)(path2), { recursive: true });
|
|
21598
|
+
if ((0, import_node_fs28.existsSync)(path2)) (0, import_node_fs28.copyFileSync)(path2, `${path2}.bak`);
|
|
21599
|
+
(0, import_node_fs28.writeFileSync)(path2, `${JSON.stringify(merged, null, 2)}
|
|
20503
21600
|
`, "utf8");
|
|
20504
21601
|
return "written";
|
|
20505
21602
|
} catch {
|
|
@@ -20533,8 +21630,13 @@ async function runPluginHeal(surface = detectSurface(process.env)) {
|
|
|
20533
21630
|
if (healed) {
|
|
20534
21631
|
console.log(` \u2713 MMI plugin reinstalled \u2014 ${reloadAction(surface)} to load MMI commands`);
|
|
20535
21632
|
} else {
|
|
21633
|
+
const bin = descriptor.pluginRunner === "codex" ? "codex" : "claude";
|
|
21634
|
+
const refSupported = await marketplaceAddRefSupported(bin);
|
|
21635
|
+
const recovery = refSupported ? descriptor.recovery : recoveryWithoutRef(descriptor.recovery);
|
|
21636
|
+
const note = refSupported ? "" : `
|
|
21637
|
+
Note: \`${bin}\` has no \`--ref\` option here; update \`${bin}\` to pin the released \`main\` branch.`;
|
|
20536
21638
|
console.log(` \u2717 Auto-heal failed or was skipped. Run manually:
|
|
20537
|
-
${
|
|
21639
|
+
${recovery}${note}`);
|
|
20538
21640
|
}
|
|
20539
21641
|
}
|
|
20540
21642
|
|
|
@@ -20629,7 +21731,7 @@ async function applyGcPlan(plan2, remote) {
|
|
|
20629
21731
|
cleanupBranch: (branch, expectedHeadOid) => cleanupPrMergeLocalBranch(branch.branch, {
|
|
20630
21732
|
beforeWorktrees,
|
|
20631
21733
|
startingPath: branch.worktreePath,
|
|
20632
|
-
pathExists: (p) => (0,
|
|
21734
|
+
pathExists: (p) => (0, import_node_fs29.existsSync)(p),
|
|
20633
21735
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
20634
21736
|
teardownWorktreeStage,
|
|
20635
21737
|
deferredStore,
|
|
@@ -20652,7 +21754,7 @@ async function applyGcPlan(plan2, remote) {
|
|
|
20652
21754
|
for (const wt of plan2.worktreeDirs) {
|
|
20653
21755
|
try {
|
|
20654
21756
|
const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
|
|
20655
|
-
realpath: (path2) => (0,
|
|
21757
|
+
realpath: (path2) => (0, import_node_fs29.realpathSync)(path2)
|
|
20656
21758
|
});
|
|
20657
21759
|
if (!cleanupTarget.ok) {
|
|
20658
21760
|
result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
|
|
@@ -20732,10 +21834,10 @@ async function runRulesSync(opts, io = consoleIo) {
|
|
|
20732
21834
|
for (const entry of fetched) {
|
|
20733
21835
|
if ("error" in entry) continue;
|
|
20734
21836
|
const { file, source } = entry;
|
|
20735
|
-
const current = (0,
|
|
21837
|
+
const current = (0, import_node_fs29.existsSync)(file) ? await (0, import_promises8.readFile)(file, "utf8") : null;
|
|
20736
21838
|
if (needsUpdate(source, current)) {
|
|
20737
21839
|
const slash = file.lastIndexOf("/");
|
|
20738
|
-
if (slash > 0) (0,
|
|
21840
|
+
if (slash > 0) (0, import_node_fs29.mkdirSync)(file.slice(0, slash), { recursive: true });
|
|
20739
21841
|
await (0, import_promises8.writeFile)(file, normalizeEol(source), "utf8");
|
|
20740
21842
|
changed++;
|
|
20741
21843
|
if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
|
|
@@ -20749,13 +21851,13 @@ rules.command("sync").option("--quiet", "stay silent unless something changed or
|
|
|
20749
21851
|
if (!await runRulesSync(opts)) process.exitCode = 1;
|
|
20750
21852
|
});
|
|
20751
21853
|
rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
|
|
20752
|
-
const path2 = (0,
|
|
20753
|
-
const current = (0,
|
|
21854
|
+
const path2 = (0, import_node_path26.join)(process.cwd(), ".gitignore");
|
|
21855
|
+
const current = (0, import_node_fs29.existsSync)(path2) ? (0, import_node_fs29.readFileSync)(path2, "utf8") : null;
|
|
20754
21856
|
const plan2 = planManagedGitignore(current);
|
|
20755
21857
|
const drift = [...plan2.added.map((l) => `+${l}`), ...plan2.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
|
|
20756
21858
|
if (opts.write) {
|
|
20757
21859
|
if (plan2.changed) {
|
|
20758
|
-
(0,
|
|
21860
|
+
(0, import_node_fs29.writeFileSync)(path2, plan2.content, "utf8");
|
|
20759
21861
|
console.log(`mmi-cli rules gitignore: updated .gitignore (${drift})`);
|
|
20760
21862
|
} else {
|
|
20761
21863
|
console.log("mmi-cli rules gitignore: up to date");
|
|
@@ -20782,7 +21884,7 @@ async function runDocsSync(opts, io = consoleIo) {
|
|
|
20782
21884
|
return null;
|
|
20783
21885
|
}
|
|
20784
21886
|
},
|
|
20785
|
-
localContent: async (f) => (0,
|
|
21887
|
+
localContent: async (f) => (0, import_node_fs29.existsSync)(f) ? await (0, import_promises8.readFile)(f, "utf8") : null,
|
|
20786
21888
|
writeDoc: async (f, c) => {
|
|
20787
21889
|
await (0, import_promises8.writeFile)(f, c, "utf8");
|
|
20788
21890
|
}
|
|
@@ -20796,6 +21898,7 @@ docs.command("sync").option("--quiet", "stay silent unless something changed or
|
|
|
20796
21898
|
registerSagaCommands(program2);
|
|
20797
21899
|
registerHandoffCommands(program2);
|
|
20798
21900
|
registerCoopCommands(program2);
|
|
21901
|
+
registerOverlordCommands(program2);
|
|
20799
21902
|
registerThrottleCommands(program2);
|
|
20800
21903
|
program2.command("commands").description("print the command manifest \u2014 every subcommand + its flags (ground against this instead of guessing)").option("--json", "machine-readable JSON: { name, version, tree, index } \u2014 index is a flat list of every leaf command path").action((o) => {
|
|
20801
21904
|
const manifest = buildCommandManifest(program2);
|
|
@@ -20934,7 +22037,7 @@ function runWorktreeInstall(command, cwd, quiet) {
|
|
|
20934
22037
|
const file = isWin2 ? "cmd.exe" : bin;
|
|
20935
22038
|
const spawnArgs = isWin2 ? ["/c", bin, ...args] : args;
|
|
20936
22039
|
return new Promise((resolve6, reject) => {
|
|
20937
|
-
const child = (0,
|
|
22040
|
+
const child = (0, import_node_child_process15.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
|
|
20938
22041
|
const timer = setTimeout(() => {
|
|
20939
22042
|
try {
|
|
20940
22043
|
child.kill();
|
|
@@ -20956,7 +22059,7 @@ function runWorktreeInstall(command, cwd, quiet) {
|
|
|
20956
22059
|
async function primaryCheckoutRoot(worktreeRoot) {
|
|
20957
22060
|
try {
|
|
20958
22061
|
const out = (await execFileP2("git", ["-C", worktreeRoot, "rev-parse", "--path-format=absolute", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
|
|
20959
|
-
return out ? (0,
|
|
22062
|
+
return out ? (0, import_node_path26.dirname)(out) : void 0;
|
|
20960
22063
|
} catch {
|
|
20961
22064
|
return void 0;
|
|
20962
22065
|
}
|
|
@@ -20969,28 +22072,28 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
|
|
|
20969
22072
|
};
|
|
20970
22073
|
}
|
|
20971
22074
|
function acquireWorktreeSetupLock(worktreeRoot) {
|
|
20972
|
-
const lockPath = (0,
|
|
22075
|
+
const lockPath = (0, import_node_path26.join)(worktreeRoot, ".mmi", "worktree-setup.lock");
|
|
20973
22076
|
const take = () => {
|
|
20974
|
-
const fd = (0,
|
|
22077
|
+
const fd = (0, import_node_fs29.openSync)(lockPath, "wx");
|
|
20975
22078
|
try {
|
|
20976
|
-
(0,
|
|
22079
|
+
(0, import_node_fs29.writeSync)(fd, String(Date.now()));
|
|
20977
22080
|
} finally {
|
|
20978
|
-
(0,
|
|
22081
|
+
(0, import_node_fs29.closeSync)(fd);
|
|
20979
22082
|
}
|
|
20980
22083
|
return () => {
|
|
20981
22084
|
try {
|
|
20982
|
-
(0,
|
|
22085
|
+
(0, import_node_fs29.rmSync)(lockPath, { force: true });
|
|
20983
22086
|
} catch {
|
|
20984
22087
|
}
|
|
20985
22088
|
};
|
|
20986
22089
|
};
|
|
20987
22090
|
try {
|
|
20988
|
-
(0,
|
|
22091
|
+
(0, import_node_fs29.mkdirSync)((0, import_node_path26.dirname)(lockPath), { recursive: true });
|
|
20989
22092
|
return take();
|
|
20990
22093
|
} catch {
|
|
20991
22094
|
try {
|
|
20992
|
-
if (Date.now() - (0,
|
|
20993
|
-
(0,
|
|
22095
|
+
if (Date.now() - (0, import_node_fs29.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
|
|
22096
|
+
(0, import_node_fs29.rmSync)(lockPath, { force: true });
|
|
20994
22097
|
return take();
|
|
20995
22098
|
}
|
|
20996
22099
|
} catch {
|
|
@@ -21152,7 +22255,7 @@ function scheduleRelatedDiscovery(o) {
|
|
|
21152
22255
|
try {
|
|
21153
22256
|
const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
|
|
21154
22257
|
if (o.repo) args.push("--repo", o.repo);
|
|
21155
|
-
(0,
|
|
22258
|
+
(0, import_node_child_process15.spawn)(process.execPath, [process.argv[1], ...args], {
|
|
21156
22259
|
detached: true,
|
|
21157
22260
|
stdio: "ignore",
|
|
21158
22261
|
windowsHide: true,
|
|
@@ -21201,6 +22304,20 @@ tenant.command("control <owner/repo> <stage> <action>").description("run bounded
|
|
|
21201
22304
|
return failGraceful(`tenant control: ${e.message}`);
|
|
21202
22305
|
}
|
|
21203
22306
|
});
|
|
22307
|
+
tenant.command("status <owner/repo> <stage>").description("read tenant runtime readiness without dispatching tenant-control: DEPLOY row, last deploy run, public URL probe, and TLS/Caddy/Cloudflare hints").option("--json", "machine-readable output").action(async (repo, stage2, _o) => {
|
|
22308
|
+
if (!["dev", "rc", "main"].includes(stage2)) return fail("tenant status: <stage> must be dev, rc, or main");
|
|
22309
|
+
const cfg = await loadConfig();
|
|
22310
|
+
const result = await buildTenantRuntimeStatusFor(repo, stage2, cfg);
|
|
22311
|
+
console.log(JSON.stringify(result, null, 2));
|
|
22312
|
+
if (result.publicProbe?.ok === false) process.exitCode = 1;
|
|
22313
|
+
});
|
|
22314
|
+
tenant.command("readiness <owner/repo> <stage>").description("alias for tenant status: read-only tenant runtime readiness").option("--json", "machine-readable output").action(async (repo, stage2, _o) => {
|
|
22315
|
+
if (!["dev", "rc", "main"].includes(stage2)) return fail("tenant readiness: <stage> must be dev, rc, or main");
|
|
22316
|
+
const cfg = await loadConfig();
|
|
22317
|
+
const result = await buildTenantRuntimeStatusFor(repo, stage2, cfg);
|
|
22318
|
+
console.log(JSON.stringify(result, null, 2));
|
|
22319
|
+
if (result.publicProbe?.ok === false) process.exitCode = 1;
|
|
22320
|
+
});
|
|
21204
22321
|
tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the central tenant-deploy.yml for an already-promoted ref (no re-tag/merge); train-authority gated").option("--ref <ref>", "ref to deploy (defaults to the stage branch rc/main \u2014 the promoted ref)").option("--watch", "block on the dispatched run and report its outcome (gh run watch --exit-status)").option("--json", "machine-readable output").action(async (repo, stage2, o) => {
|
|
21205
22322
|
if (stage2 !== "rc" && stage2 !== "main") return fail("tenant redeploy: <stage> must be rc or main");
|
|
21206
22323
|
try {
|
|
@@ -21243,6 +22360,44 @@ async function resolveDnsBounded(host, timeoutMs = 3e3) {
|
|
|
21243
22360
|
});
|
|
21244
22361
|
return Promise.race([probe, timeout]);
|
|
21245
22362
|
}
|
|
22363
|
+
async function probeHttpBounded(url, timeoutMs = 5e3) {
|
|
22364
|
+
const controller = new AbortController();
|
|
22365
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
22366
|
+
timeout.unref?.();
|
|
22367
|
+
try {
|
|
22368
|
+
const res = await fetch(url, { method: "GET", signal: controller.signal });
|
|
22369
|
+
return { ok: res.ok, status: res.status };
|
|
22370
|
+
} catch (e) {
|
|
22371
|
+
return { ok: false, error: e.message };
|
|
22372
|
+
} finally {
|
|
22373
|
+
clearTimeout(timeout);
|
|
22374
|
+
}
|
|
22375
|
+
}
|
|
22376
|
+
async function lastWorkflowRun(workflow) {
|
|
22377
|
+
try {
|
|
22378
|
+
const out = await execFileP2("gh", ["run", "list", "--repo", HUB_REPO3, "--workflow", workflow, "--limit", "1", "--json", "databaseId,url,status,conclusion,headBranch,headSha,createdAt"], { timeout: 2e4 });
|
|
22379
|
+
const rows = JSON.parse(out.stdout || "[]");
|
|
22380
|
+
return rows[0];
|
|
22381
|
+
} catch {
|
|
22382
|
+
return void 0;
|
|
22383
|
+
}
|
|
22384
|
+
}
|
|
22385
|
+
async function buildTenantRuntimeStatusFor(target, stage2, cfg) {
|
|
22386
|
+
const slug = slugOf(target);
|
|
22387
|
+
const reg = registryClientDeps(cfg);
|
|
22388
|
+
const facts = await fetchDeployFactsBySlug(slug, reg);
|
|
22389
|
+
const deploy = facts?.stages[stage2] ?? null;
|
|
22390
|
+
const publicUrl = publicUrlFromDeployFact(deploy);
|
|
22391
|
+
const publicProbe = publicUrl ? await probeHttpBounded(publicUrl) : void 0;
|
|
22392
|
+
return buildTenantRuntimeStatus({
|
|
22393
|
+
repo: target,
|
|
22394
|
+
slug,
|
|
22395
|
+
stage: stage2,
|
|
22396
|
+
deploy,
|
|
22397
|
+
publicProbe,
|
|
22398
|
+
lastTenantDeployRun: await lastWorkflowRun("tenant-deploy.yml")
|
|
22399
|
+
});
|
|
22400
|
+
}
|
|
21246
22401
|
async function v2ReadinessDeps(cfg) {
|
|
21247
22402
|
const reg = registryClientDeps(cfg);
|
|
21248
22403
|
return {
|
|
@@ -21257,6 +22412,11 @@ async function v2ReadinessDeps(cfg) {
|
|
|
21257
22412
|
const status = await fetchDeployStatusBySlug(slug, reg);
|
|
21258
22413
|
return Boolean(status?.deployState[stage2]);
|
|
21259
22414
|
},
|
|
22415
|
+
getDeployFact: async (slug, stage2) => {
|
|
22416
|
+
const facts = await fetchDeployFactsBySlug(slug, reg);
|
|
22417
|
+
const fact = facts?.stages[stage2];
|
|
22418
|
+
return fact ? { port: fact.port, domain: fact.domain } : null;
|
|
22419
|
+
},
|
|
21260
22420
|
listSecrets: async (targetRepo2) => {
|
|
21261
22421
|
const apiUrl = cfg.sagaApiUrl;
|
|
21262
22422
|
if (!apiUrl) throw new Error("Hub API URL not configured \u2014 cannot verify secret names (set sagaApiUrl)");
|
|
@@ -21344,7 +22504,7 @@ deploys run centrally (tenant-deploy.yml); product repos carry no deploy files.
|
|
|
21344
22504
|
}
|
|
21345
22505
|
});
|
|
21346
22506
|
project.command("resolve <owner/repo>").description("deploy coords for a stage \u2014 for diagnosis. NOTE: /deploy-coords is OIDC-gated (a deploy job\u2019s id-token), so a gh-token CLI cannot read it from a dev machine").option("--stage <main|rc>", "deploy stage", "main").option("--json", "machine-readable output").action((_repoOrRepo, o) => {
|
|
21347
|
-
const msg = "project resolve: deploy coords are served only to a deploy workflow (GitHub OIDC id-token, repo-scoped). A gh-token CLI on a dev machine cannot read /deploy-coords; inspect
|
|
22507
|
+
const msg = "project resolve: deploy coords are served only to a deploy workflow (GitHub OIDC id-token, repo-scoped). A gh-token CLI on a dev machine cannot read /deploy-coords; inspect nonsecret DEPLOY facts with `mmi-cli project deploy get`. Full coords stay OIDC-gated.";
|
|
21348
22508
|
if (o.json) {
|
|
21349
22509
|
console.log(JSON.stringify({ ok: false, stage: o.stage, error: msg }));
|
|
21350
22510
|
process.exitCode = 1;
|
|
@@ -21352,6 +22512,37 @@ project.command("resolve <owner/repo>").description("deploy coords for a stage \
|
|
|
21352
22512
|
}
|
|
21353
22513
|
fail(msg);
|
|
21354
22514
|
});
|
|
22515
|
+
var projectDeploy = project.command("deploy").description("read nonsecret DEPLOY# facts (domain, port, deploy path, substrate, host presence)");
|
|
22516
|
+
projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# facts for one project; defaults to the current repo").option("--stage <stage>", "dev | rc | main").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
22517
|
+
const cfg = await loadConfig();
|
|
22518
|
+
let target;
|
|
22519
|
+
try {
|
|
22520
|
+
target = await projectTarget("project deploy get", repoOrSlug);
|
|
22521
|
+
} catch (e) {
|
|
22522
|
+
return fail(e.message);
|
|
22523
|
+
}
|
|
22524
|
+
const out = await fetchDeployFactsBySlug(slugOf(target), registryClientDeps(cfg));
|
|
22525
|
+
if (!out) return failGraceful(`project deploy get: Hub deploy facts read failed for ${target}`);
|
|
22526
|
+
const stage2 = o.stage?.trim();
|
|
22527
|
+
if (stage2 && !["dev", "rc", "main"].includes(stage2)) return fail("project deploy get: --stage must be dev, rc, or main");
|
|
22528
|
+
const payload = stage2 ? { slug: out.slug, stage: stage2, deploy: out.stages[stage2] ?? null } : out;
|
|
22529
|
+
console.log(JSON.stringify(payload));
|
|
22530
|
+
});
|
|
22531
|
+
projectDeploy.command("list [owner/repo]").description("alias for project deploy get; lists all stages by default").option("--stage <stage>", "dev | rc | main").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
22532
|
+
const cfg = await loadConfig();
|
|
22533
|
+
let target;
|
|
22534
|
+
try {
|
|
22535
|
+
target = await projectTarget("project deploy list", repoOrSlug);
|
|
22536
|
+
} catch (e) {
|
|
22537
|
+
return fail(e.message);
|
|
22538
|
+
}
|
|
22539
|
+
const out = await fetchDeployFactsBySlug(slugOf(target), registryClientDeps(cfg));
|
|
22540
|
+
if (!out) return failGraceful(`project deploy list: Hub deploy facts read failed for ${target}`);
|
|
22541
|
+
const stage2 = o.stage?.trim();
|
|
22542
|
+
if (stage2 && !["dev", "rc", "main"].includes(stage2)) return fail("project deploy list: --stage must be dev, rc, or main");
|
|
22543
|
+
const payload = stage2 ? { slug: out.slug, stage: stage2, deploy: out.stages[stage2] ?? null } : out;
|
|
22544
|
+
console.log(JSON.stringify(payload));
|
|
22545
|
+
});
|
|
21355
22546
|
project.command("doctor [owner/repo]").description("diagnose Hub v2 readiness for a repo without reading product repo files \u2014 appOwnedGaps are advisory templates; clear them with `project attest`; defaults to the current repo").option("--v2", "compatibility flag; v2 readiness is the default").option("--json", "machine-readable output").action(async (repo, _o) => {
|
|
21356
22547
|
const cfg = await loadConfig();
|
|
21357
22548
|
let target;
|
|
@@ -21444,6 +22635,31 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
21444
22635
|
const res = await upsertProject(slug, { ...patch, repo }, registryClientDeps(cfg));
|
|
21445
22636
|
return reportWrite("project set", res);
|
|
21446
22637
|
});
|
|
22638
|
+
var fullTrack = program2.command("full-track").description("direct-to-full-track readiness audits");
|
|
22639
|
+
fullTrack.command("readiness <owner/repo>").description("aggregate branch topology, train authority, deploy facts, endpoint health, and rcand readiness for direct -> full switches").option("--json", "machine-readable output").action(async (repo, _o) => {
|
|
22640
|
+
const cfg = await loadConfig();
|
|
22641
|
+
const reg = registryClientDeps(cfg);
|
|
22642
|
+
const slug = slugOf(repo);
|
|
22643
|
+
const [metaRead, deployFacts, authority, dev, rc, main] = await Promise.all([
|
|
22644
|
+
fetchProjectBySlugChecked(slug, reg),
|
|
22645
|
+
fetchDeployFactsBySlug(slug, reg),
|
|
22646
|
+
fetchTrainAuthority(repo, reg),
|
|
22647
|
+
buildTenantRuntimeStatusFor(repo, "dev", cfg),
|
|
22648
|
+
buildTenantRuntimeStatusFor(repo, "rc", cfg),
|
|
22649
|
+
buildTenantRuntimeStatusFor(repo, "main", cfg)
|
|
22650
|
+
]);
|
|
22651
|
+
if (!metaRead.ok) return failGraceful(`full-track readiness: Hub registry read failed (${metaRead.error})`);
|
|
22652
|
+
const report = buildFullTrackReadinessReport({
|
|
22653
|
+
repo,
|
|
22654
|
+
slug,
|
|
22655
|
+
meta: metaRead.project,
|
|
22656
|
+
deployFacts,
|
|
22657
|
+
trainAuthority: authority.ok ? authority.authority : void 0,
|
|
22658
|
+
runtime: { dev, rc, main }
|
|
22659
|
+
});
|
|
22660
|
+
console.log(JSON.stringify(report, null, 2));
|
|
22661
|
+
if (!report.rcand.canApply) process.exitCode = 1;
|
|
22662
|
+
});
|
|
21447
22663
|
project.command("set-deploy [owner/repo]").description("write the DEPLOY#<stage> Hetzner deploy coords for a tenant (master-only) \u2014 the explicit-coords path that seeds a freshly-bootstrapped tenant; defaults to the current repo").requiredOption("--stage <stage>", "dev | rc | main").option("--ssh-host <host>", "the box address the deploy ssh-es into (required for hetzner-ssh)").option("--ssh-user <user>", "ssh user (default root)").option("--port <port>", "loopback port the container binds / Caddy upstream (1..65535)").option("--substrate <substrate>", "hetzner-ssh (default)").option("--deploy-path <path>", "on-box per-stage release root (default /opt/mmi/<slug>/<stage>)").option("--service <name>", "systemd/compose service name (default the slug)").option("--domain <domain>", "canonical serving host (default the project edgeDomains[stage])").option("--alias <domain...>", "extra serving hostname the box Caddy answers (repeatable)").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
21448
22664
|
const cfg = await loadConfig();
|
|
21449
22665
|
let target;
|
|
@@ -21680,6 +22896,29 @@ issue.command("link-child <parent> <child>").description("link an existing issue
|
|
|
21680
22896
|
return fail(`issue link-child: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
|
|
21681
22897
|
}
|
|
21682
22898
|
});
|
|
22899
|
+
issue.command("comment <ref>").description("post a Markdown comment to an issue and print {number,repo,url,commentUrl} JSON").option("--body <body>", "comment body (markdown; prefer --body-file for multiline Markdown)").option("--body-file <path|->", "read comment body from a UTF-8 file, or from stdin with -").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)").action(async (ref, o) => {
|
|
22900
|
+
let parsed;
|
|
22901
|
+
try {
|
|
22902
|
+
parsed = parseIssueRef(ref);
|
|
22903
|
+
} catch (e) {
|
|
22904
|
+
return fail(`issue comment: ${e.message}`);
|
|
22905
|
+
}
|
|
22906
|
+
let body;
|
|
22907
|
+
try {
|
|
22908
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
|
|
22909
|
+
} catch (e) {
|
|
22910
|
+
return fail(`issue comment: ${e.message}`);
|
|
22911
|
+
}
|
|
22912
|
+
const repo = await resolveRepo(parsed.repo ?? o.repo);
|
|
22913
|
+
if (!repo) return fail("issue comment: could not resolve repo \u2014 pass --repo owner/repo");
|
|
22914
|
+
try {
|
|
22915
|
+
const result = await postIssueComment(defaultGitHubClient(), { ref, defaultRepo: repo, body });
|
|
22916
|
+
console.log(JSON.stringify(result));
|
|
22917
|
+
} catch (e) {
|
|
22918
|
+
const err = e;
|
|
22919
|
+
return fail(`issue comment: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
22920
|
+
}
|
|
22921
|
+
});
|
|
21683
22922
|
issue.command("check <ref>").description("tick (or with --off untick) a task-list checkbox in an issue/epic body by its item text and print {number,repo,item,checked,changed} JSON").requiredOption("--item <text>", "the checklist item to match \u2014 exact item text, else a unique substring").option("--off", "untick the item ([x] \u2192 [ ]) instead of ticking it").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)").action(async (ref, o) => {
|
|
21684
22923
|
let parsed;
|
|
21685
22924
|
try {
|
|
@@ -21994,9 +23233,9 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
|
|
|
21994
23233
|
console.log(JSON.stringify(created));
|
|
21995
23234
|
});
|
|
21996
23235
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
21997
|
-
const wfDir = (0,
|
|
21998
|
-
if (!(0,
|
|
21999
|
-
return (0,
|
|
23236
|
+
const wfDir = (0, import_node_path26.join)(cwd, ".github", "workflows");
|
|
23237
|
+
if (!(0, import_node_fs29.existsSync)(wfDir)) return [];
|
|
23238
|
+
return (0, import_node_fs29.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
|
|
22000
23239
|
}
|
|
22001
23240
|
async function resolveMergeCiPolicyForCheckout(repoOpt) {
|
|
22002
23241
|
const repo = repoOpt ?? await resolveRepo();
|
|
@@ -22015,7 +23254,7 @@ function ciAuditDeps() {
|
|
|
22015
23254
|
// Continuous CI delivery (#1550): the gate re-seed renders from the Hub's on-disk seed templates. The
|
|
22016
23255
|
// reconcile runs IN the Hub checkout, so this is local-file I/O (no network fetch). Path is relative to
|
|
22017
23256
|
// the repo root (e.g. skills/bootstrap/seeds/gate.template.yml).
|
|
22018
|
-
readSeedFile: (path2) => (0,
|
|
23257
|
+
readSeedFile: (path2) => (0, import_node_fs29.existsSync)(path2) ? (0, import_node_fs29.readFileSync)(path2, "utf8") : null
|
|
22019
23258
|
};
|
|
22020
23259
|
}
|
|
22021
23260
|
pr.command("ci-policy").description("report merge CI policy: wait-for-checks vs no-ci (for grind/build agents)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current checkout)").action(async (o) => {
|
|
@@ -22152,7 +23391,7 @@ async function remoteBranchExists2(branch, options = {}) {
|
|
|
22152
23391
|
}
|
|
22153
23392
|
var COMPOSE_TIMEOUT_MS = 12e4;
|
|
22154
23393
|
function spawnDeferredGcSweep() {
|
|
22155
|
-
spawnDetachedSelf(["gc", "sweep-deferred", "--quiet"], { spawn:
|
|
23394
|
+
spawnDetachedSelf(["gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process15.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
22156
23395
|
}
|
|
22157
23396
|
async function createDeferredWorktreeStore() {
|
|
22158
23397
|
try {
|
|
@@ -22168,7 +23407,7 @@ async function createDeferredWorktreeStore() {
|
|
|
22168
23407
|
},
|
|
22169
23408
|
write: async (entries) => {
|
|
22170
23409
|
try {
|
|
22171
|
-
await (0, import_promises8.mkdir)((0,
|
|
23410
|
+
await (0, import_promises8.mkdir)((0, import_node_path26.dirname)(registryPath), { recursive: true });
|
|
22172
23411
|
await (0, import_promises8.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
|
|
22173
23412
|
} catch {
|
|
22174
23413
|
}
|
|
@@ -22182,13 +23421,13 @@ var realWorktreeDirRemover = {
|
|
|
22182
23421
|
probe: (p) => {
|
|
22183
23422
|
let st;
|
|
22184
23423
|
try {
|
|
22185
|
-
st = (0,
|
|
23424
|
+
st = (0, import_node_fs29.lstatSync)(p);
|
|
22186
23425
|
} catch {
|
|
22187
23426
|
return null;
|
|
22188
23427
|
}
|
|
22189
23428
|
if (st.isSymbolicLink()) return "link";
|
|
22190
23429
|
try {
|
|
22191
|
-
(0,
|
|
23430
|
+
(0, import_node_fs29.readlinkSync)(p);
|
|
22192
23431
|
return "link";
|
|
22193
23432
|
} catch {
|
|
22194
23433
|
}
|
|
@@ -22196,7 +23435,7 @@ var realWorktreeDirRemover = {
|
|
|
22196
23435
|
},
|
|
22197
23436
|
readdir: (p) => {
|
|
22198
23437
|
try {
|
|
22199
|
-
return (0,
|
|
23438
|
+
return (0, import_node_fs29.readdirSync)(p);
|
|
22200
23439
|
} catch {
|
|
22201
23440
|
return [];
|
|
22202
23441
|
}
|
|
@@ -22205,9 +23444,9 @@ var realWorktreeDirRemover = {
|
|
|
22205
23444
|
// leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
|
|
22206
23445
|
detachLink: (p) => {
|
|
22207
23446
|
try {
|
|
22208
|
-
(0,
|
|
23447
|
+
(0, import_node_fs29.rmdirSync)(p);
|
|
22209
23448
|
} catch {
|
|
22210
|
-
(0,
|
|
23449
|
+
(0, import_node_fs29.unlinkSync)(p);
|
|
22211
23450
|
}
|
|
22212
23451
|
},
|
|
22213
23452
|
removeTree: (p) => (0, import_promises8.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
|
@@ -22237,9 +23476,9 @@ async function worktreeHasStageState(worktreePath) {
|
|
|
22237
23476
|
}
|
|
22238
23477
|
}
|
|
22239
23478
|
function stageStateFileBelongsToWorktree(statePath, worktreePath) {
|
|
22240
|
-
if (!(0,
|
|
23479
|
+
if (!(0, import_node_fs29.existsSync)(statePath)) return false;
|
|
22241
23480
|
try {
|
|
22242
|
-
const state = JSON.parse((0,
|
|
23481
|
+
const state = JSON.parse((0, import_node_fs29.readFileSync)(statePath, "utf8"));
|
|
22243
23482
|
const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
|
|
22244
23483
|
return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
|
|
22245
23484
|
} catch {
|
|
@@ -22312,7 +23551,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
22312
23551
|
} : await cleanupPrMergeLocalBranch(headRef, {
|
|
22313
23552
|
beforeWorktrees,
|
|
22314
23553
|
startingPath,
|
|
22315
|
-
pathExists: (p) => (0,
|
|
23554
|
+
pathExists: (p) => (0, import_node_fs29.existsSync)(p),
|
|
22316
23555
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
22317
23556
|
teardownWorktreeStage,
|
|
22318
23557
|
deferredStore,
|
|
@@ -22509,7 +23748,7 @@ function stageScopedRunOpts(o) {
|
|
|
22509
23748
|
};
|
|
22510
23749
|
}
|
|
22511
23750
|
function printLine(value) {
|
|
22512
|
-
(0,
|
|
23751
|
+
(0, import_node_fs29.writeSync)(1, `${value}
|
|
22513
23752
|
`);
|
|
22514
23753
|
}
|
|
22515
23754
|
function stageKeepAlive() {
|
|
@@ -22526,8 +23765,8 @@ async function resolveStage() {
|
|
|
22526
23765
|
local,
|
|
22527
23766
|
shell: shellFor(),
|
|
22528
23767
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
22529
|
-
hasCompose: (0,
|
|
22530
|
-
hasEnvExample: (0,
|
|
23768
|
+
hasCompose: (0, import_node_fs29.existsSync)((0, import_node_path26.join)(process.cwd(), "docker-compose.yml")),
|
|
23769
|
+
hasEnvExample: (0, import_node_fs29.existsSync)((0, import_node_path26.join)(process.cwd(), ".env.example"))
|
|
22531
23770
|
});
|
|
22532
23771
|
}
|
|
22533
23772
|
async function fetchStageVaultEnvMerge() {
|
|
@@ -22851,7 +24090,7 @@ async function resolveRcandPlanTargets() {
|
|
|
22851
24090
|
}
|
|
22852
24091
|
}
|
|
22853
24092
|
for (const commandName of ["rcand", "release"]) {
|
|
22854
|
-
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval").option("--announce-summary-file <path>", "release only: agent-curated summary lines for the Hub Slack announcement (#883)").option("--ack <shas>", "release only: comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)").option("--dev", "release only: full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)").action(async (o) => {
|
|
24093
|
+
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval").option("--announce-summary-file <path>", "release only: agent-curated summary lines for the Hub Slack announcement (#883)").option("--ack <shas>", "release only: comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)").option("--dev", "release only: full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)").option("--repo <owner/repo>", "dry-run plan for a target repo without relying on the current checkout; --apply still uses the current checkout").action(async (o) => {
|
|
22855
24094
|
try {
|
|
22856
24095
|
await requireFreshTrainCli(commandName);
|
|
22857
24096
|
} catch (e) {
|
|
@@ -22863,6 +24102,9 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
22863
24102
|
if (o.dev && commandName !== "release") {
|
|
22864
24103
|
return fail("--dev applies only to release: it ships development -> main skipping rc, which rcand cannot do");
|
|
22865
24104
|
}
|
|
24105
|
+
if (o.apply && o.repo) {
|
|
24106
|
+
return fail(`${commandName}: --repo is read-only for dry-run planning; --apply must run from the target repo checkout`);
|
|
24107
|
+
}
|
|
22866
24108
|
if (o.apply) {
|
|
22867
24109
|
try {
|
|
22868
24110
|
const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -22872,8 +24114,8 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
22872
24114
|
return failGraceful(`${commandName}: ${e.message}`);
|
|
22873
24115
|
}
|
|
22874
24116
|
}
|
|
22875
|
-
const repo = await resolveRepo();
|
|
22876
|
-
const targets = commandName === "rcand" ? await resolveRcandPlanTargets() : void 0;
|
|
24117
|
+
const repo = o.repo ?? await resolveRepo();
|
|
24118
|
+
const targets = commandName === "rcand" && !o.repo ? await resolveRcandPlanTargets() : void 0;
|
|
22877
24119
|
let releaseTrack;
|
|
22878
24120
|
if (repo) {
|
|
22879
24121
|
try {
|
|
@@ -22989,7 +24231,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
22989
24231
|
client: defaultGitHubClient(),
|
|
22990
24232
|
projectMeta: meta,
|
|
22991
24233
|
deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
|
|
22992
|
-
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0,
|
|
24234
|
+
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs29.existsSync)(path2) ? (0, import_node_fs29.readFileSync)(path2, "utf8") : null,
|
|
22993
24235
|
// requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
|
|
22994
24236
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
22995
24237
|
requiredGcpApis: (() => {
|
|
@@ -23033,12 +24275,12 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
23033
24275
|
return fail(`bootstrap apply: ${e.message}`);
|
|
23034
24276
|
}
|
|
23035
24277
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
23036
|
-
if (!(0,
|
|
23037
|
-
const manifest = loadBootstrapSeeds((0,
|
|
24278
|
+
if (!(0, import_node_fs29.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
|
|
24279
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs29.readFileSync)(manifestPath, "utf8"));
|
|
23038
24280
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
23039
24281
|
const slug = parsedRepo.slug;
|
|
23040
24282
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
23041
|
-
const readFile7 = (p) => (0,
|
|
24283
|
+
const readFile7 = (p) => (0, import_node_fs29.existsSync)(p) ? (0, import_node_fs29.readFileSync)(p, "utf8") : null;
|
|
23042
24284
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
23043
24285
|
const rawVars = {};
|
|
23044
24286
|
for (const value of rawValues("--var")) {
|
|
@@ -23287,18 +24529,20 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
23287
24529
|
const registryProjects = await fetchProjectsList(registryClientDeps(cfg));
|
|
23288
24530
|
if (o.repo) {
|
|
23289
24531
|
if (o.class !== "deployable" && o.class !== "content") return failGraceful("access audit: --class must be deployable or content");
|
|
23290
|
-
|
|
24532
|
+
const meta = registryProjects?.find((project2) => (project2.repos ?? []).some((repo) => repo.toLowerCase() === o.repo.toLowerCase())) ?? null;
|
|
24533
|
+
const repoClass = o.class;
|
|
24534
|
+
targets = [{ repo: o.repo, class: repoClass, releaseTrack: repoClass === "content" ? "trunk" : resolveReleaseTrack(meta, void 0, o.repo) }];
|
|
23291
24535
|
} else {
|
|
23292
|
-
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0,
|
|
24536
|
+
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs29.existsSync)("projects.json") ? (0, import_node_fs29.readFileSync)("projects.json", "utf8") : null;
|
|
23293
24537
|
if (!projectsJson) return failGraceful("access audit: no project registry \u2014 Hub API unreachable and projects.json not found; run from the MMI-Hub repo root or pass --repo <owner/repo>");
|
|
23294
|
-
const fanoutJson = (0,
|
|
24538
|
+
const fanoutJson = (0, import_node_fs29.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs29.readFileSync)(".github/fanout-targets.json", "utf8") : null;
|
|
23295
24539
|
targets = loadAccessTargets(projectsJson, fanoutJson);
|
|
23296
24540
|
}
|
|
23297
24541
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
23298
|
-
const fileMatrix = (0,
|
|
24542
|
+
const fileMatrix = (0, import_node_fs29.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs29.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
23299
24543
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
23300
24544
|
const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
23301
|
-
const fileContracts = (0,
|
|
24545
|
+
const fileContracts = (0, import_node_fs29.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs29.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
|
|
23302
24546
|
const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
|
|
23303
24547
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
|
|
23304
24548
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
@@ -23399,7 +24643,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
|
|
|
23399
24643
|
} catch (e) {
|
|
23400
24644
|
console.error(`[mmi-hook] saga session failed: ${e.message}`);
|
|
23401
24645
|
}
|
|
23402
|
-
spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn:
|
|
24646
|
+
spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process15.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
23403
24647
|
spawnDeferredGcSweep();
|
|
23404
24648
|
let northstarInjected = false;
|
|
23405
24649
|
const { parallel, sequential } = buildSessionStartPlan({
|
|
@@ -23441,7 +24685,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
|
|
|
23441
24685
|
readBoard,
|
|
23442
24686
|
// #1813: warm the slice cache out-of-band (detached, like docs sync) so the ~20s live read
|
|
23443
24687
|
// never costs banner time and next session's glance renders instantly within budget.
|
|
23444
|
-
scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn:
|
|
24688
|
+
scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn: import_node_child_process15.spawn, execPath: process.execPath, scriptPath: process.argv[1] })
|
|
23445
24689
|
}),
|
|
23446
24690
|
spineReconcile: async (io) => {
|
|
23447
24691
|
const cfg = await loadConfig();
|
|
@@ -23465,7 +24709,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
|
|
|
23465
24709
|
for (const line of scratchGcLines(process.cwd())) consoleIo.log(line);
|
|
23466
24710
|
const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
|
|
23467
24711
|
if (worktreeBanner) {
|
|
23468
|
-
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn:
|
|
24712
|
+
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process15.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
23469
24713
|
consoleIo.log(worktreeBanner);
|
|
23470
24714
|
}
|
|
23471
24715
|
});
|