@mutmutco/cli 2.52.1 → 2.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.cjs +684 -309
- package/dist/saga.cjs +16 -8
- package/package.json +1 -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_fs28 = require("node:fs");
|
|
3412
3412
|
|
|
3413
3413
|
// src/rules-sync.ts
|
|
3414
3414
|
function normalizeEol(s) {
|
|
@@ -3601,7 +3601,7 @@ async function sweepDeferredWorktreesWithRetry(store, deps, opts = {}) {
|
|
|
3601
3601
|
}
|
|
3602
3602
|
return last;
|
|
3603
3603
|
}
|
|
3604
|
-
var defaultSleep = (ms) => new Promise((
|
|
3604
|
+
var defaultSleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
3605
3605
|
async function removeWorktreeWithRecovery(wtPath, deps) {
|
|
3606
3606
|
const maxAttempts = deps.maxAttempts ?? 3;
|
|
3607
3607
|
const backoff = deps.backoffMs ?? [250, 1e3];
|
|
@@ -4609,7 +4609,7 @@ async function runMergeTreePreflight(deps, ours, theirs) {
|
|
|
4609
4609
|
async function predictMergeConflicts(deps, ours, theirs) {
|
|
4610
4610
|
return runMergeTreePreflight(deps, ours, theirs);
|
|
4611
4611
|
}
|
|
4612
|
-
async function mergeWithSpineResolution(deps, sourceRef, label,
|
|
4612
|
+
async function mergeWithSpineResolution(deps, sourceRef, label, resolve6, extraTolerated = []) {
|
|
4613
4613
|
try {
|
|
4614
4614
|
await deps.run("git", ["merge", sourceRef, "--no-edit"]);
|
|
4615
4615
|
return;
|
|
@@ -4623,7 +4623,7 @@ async function mergeWithSpineResolution(deps, sourceRef, label, resolve5, extraT
|
|
|
4623
4623
|
unmerged.length === 0 ? `${label} merge failed without conflicted paths \u2014 merge aborted; inspect the repo state and rerun` : `${label} merge conflicts on non-spine path(s): ${blocking.join(", ")} \u2014 merge aborted (the train is misaligned; reconcile the branches via an approved alignment PR, then rerun)`
|
|
4624
4624
|
);
|
|
4625
4625
|
}
|
|
4626
|
-
await deps.run("git", ["checkout", `--${
|
|
4626
|
+
await deps.run("git", ["checkout", `--${resolve6}`, "--", ...unmerged]);
|
|
4627
4627
|
await deps.run("git", ["add", "--", ...unmerged]);
|
|
4628
4628
|
await deps.run("git", ["commit", "--no-edit"]);
|
|
4629
4629
|
}
|
|
@@ -4740,7 +4740,7 @@ function requireProjectMetaForTrain(load, repo) {
|
|
|
4740
4740
|
var CORRELATE_ATTEMPTS = 5;
|
|
4741
4741
|
var CORRELATE_DELAY_MS = 1500;
|
|
4742
4742
|
var CORRELATE_SKEW_SLACK_MS = 1e4;
|
|
4743
|
-
var defaultSleep2 = (ms) => new Promise((
|
|
4743
|
+
var defaultSleep2 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
4744
4744
|
function resolveSleep(deps) {
|
|
4745
4745
|
return deps.sleep ?? defaultSleep2;
|
|
4746
4746
|
}
|
|
@@ -4959,6 +4959,23 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
|
|
|
4959
4959
|
`timed out waiting for required train checks on ${sha}: ${lastError ? `last error: ${lastError}` : lastStatus}`
|
|
4960
4960
|
);
|
|
4961
4961
|
}
|
|
4962
|
+
function partialTrainRecoveryError(cause, input) {
|
|
4963
|
+
const causeMessage = cause instanceof Error ? cause.message : String(cause);
|
|
4964
|
+
const branch = input.stage;
|
|
4965
|
+
const releaseState = input.stage === "rc" ? "GitHub Release n/a" : "GitHub Release not created";
|
|
4966
|
+
const releaseCommand = input.stage === "main" ? `
|
|
4967
|
+
2. gh release create ${input.tag} --target main --generate-notes --latest --repo ${input.repo}` : "";
|
|
4968
|
+
const deployStep = input.stage === "main" ? "3" : "2";
|
|
4969
|
+
return new Error(
|
|
4970
|
+
`${causeMessage}
|
|
4971
|
+
|
|
4972
|
+
partial train state: tag ${input.tag} is already pushed; origin/${branch} has not been pushed; ${releaseState}; deploy not dispatched.
|
|
4973
|
+
Recovery sequence:
|
|
4974
|
+
1. git push origin ${branch}` + releaseCommand + `
|
|
4975
|
+
${deployStep}. mmi-cli tenant redeploy ${input.repo} ${input.stage} --watch
|
|
4976
|
+
Do not delete or force-move the pushed tag; rerun the train only after confirming the branch, release, and deploy states above.`
|
|
4977
|
+
);
|
|
4978
|
+
}
|
|
4962
4979
|
async function ensureTagPushed(deps, tag, sha) {
|
|
4963
4980
|
const remoteOut = await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]);
|
|
4964
4981
|
const remoteSha = clean(remoteOut).split(/\s+/)[0] || "";
|
|
@@ -5167,19 +5184,25 @@ async function mergeSourceToMain(deps, deployModel, args) {
|
|
|
5167
5184
|
async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha) {
|
|
5168
5185
|
await ensureTagPushed(deps, tag, releaseSha);
|
|
5169
5186
|
const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
5170
|
-
|
|
5187
|
+
let checks;
|
|
5188
|
+
try {
|
|
5189
|
+
checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
|
|
5190
|
+
} catch (e) {
|
|
5191
|
+
throw partialTrainRecoveryError(e, { repo: ctx.repo, tag, stage: "main" });
|
|
5192
|
+
}
|
|
5171
5193
|
await deps.run("git", ["push", "origin", "main"]);
|
|
5172
5194
|
const releaseUrl = clean(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
5173
5195
|
await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
|
|
5174
5196
|
const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
5175
5197
|
const autoRunSince = (deps.now ?? Date.now)();
|
|
5176
5198
|
const deployDispatch = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report", meta.publishDir);
|
|
5177
|
-
const publishDispatch = deployDispatch.deployStatus === "
|
|
5199
|
+
const publishDispatch = deployDispatch.deployStatus === "success" ? await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, watch, "report") : null;
|
|
5178
5200
|
let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
|
|
5179
|
-
if (!publishDispatch && deployDispatch.deployStatus
|
|
5201
|
+
if (!publishDispatch && deployDispatch.deployStatus !== "success" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
|
|
5202
|
+
const reason = deployDispatch.deployStatus === "failure" ? "box deploy failed \u2014 redeploy the box before publishing" : "box deploy not confirmed (run with --watch) \u2014 publish after the box deploy lands";
|
|
5180
5203
|
dispatch = {
|
|
5181
5204
|
...dispatch,
|
|
5182
|
-
note: `${dispatch.note}; tenant-publish.yml skipped (
|
|
5205
|
+
note: `${dispatch.note}; tenant-publish.yml skipped (${reason})`
|
|
5183
5206
|
};
|
|
5184
5207
|
}
|
|
5185
5208
|
return { checks, releaseUrl, announceNote, dispatch };
|
|
@@ -5217,7 +5240,12 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
5217
5240
|
const resumeNote = resume.tag ? resume.note : void 0;
|
|
5218
5241
|
await ensureTagPushed(deps, tag2, rcSha);
|
|
5219
5242
|
const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "rc");
|
|
5220
|
-
|
|
5243
|
+
let checks2;
|
|
5244
|
+
try {
|
|
5245
|
+
checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks);
|
|
5246
|
+
} catch (e) {
|
|
5247
|
+
throw partialTrainRecoveryError(e, { repo: ctx.repo, tag: tag2, stage: "rc" });
|
|
5248
|
+
}
|
|
5221
5249
|
const autoRunSince = (deps.now ?? Date.now)();
|
|
5222
5250
|
await deps.run("git", ["push", "origin", "rc"]);
|
|
5223
5251
|
const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince, rcSha);
|
|
@@ -5653,7 +5681,7 @@ async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
|
|
|
5653
5681
|
const attempts = opts.attempts ?? 3;
|
|
5654
5682
|
const baseDelayMs = opts.baseDelayMs ?? 250;
|
|
5655
5683
|
const retryOn = opts.retryOn ?? ((res) => res.status >= 500);
|
|
5656
|
-
const sleep = opts.sleep ?? ((ms) => new Promise((
|
|
5684
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
|
|
5657
5685
|
let lastErr;
|
|
5658
5686
|
for (let i = 0; i < attempts; i++) {
|
|
5659
5687
|
const isLast = i === attempts - 1;
|
|
@@ -5712,7 +5740,7 @@ function hardExit(code) {
|
|
|
5712
5740
|
async function cleanExit(code) {
|
|
5713
5741
|
process.exitCode = code;
|
|
5714
5742
|
await closeHttpPool();
|
|
5715
|
-
await new Promise((
|
|
5743
|
+
await new Promise((resolve6) => setImmediate(resolve6));
|
|
5716
5744
|
process.exit(code);
|
|
5717
5745
|
}
|
|
5718
5746
|
async function failGraceful(msg) {
|
|
@@ -6084,18 +6112,23 @@ function headPrompt(state) {
|
|
|
6084
6112
|
const decisions = shownDecisions(state.decisions);
|
|
6085
6113
|
const actions = (state.actionLog ?? []).slice(-HEAD_PROMPT_ACTION_LIMIT);
|
|
6086
6114
|
return [
|
|
6087
|
-
"You maintain
|
|
6088
|
-
"
|
|
6089
|
-
"
|
|
6090
|
-
"
|
|
6091
|
-
"
|
|
6092
|
-
"
|
|
6115
|
+
"You maintain two durable slots of a work-session: PINNED (things worth remembering) and a NEXT",
|
|
6116
|
+
"SUGGESTION (a best-effort one-line hint for the next useful step). Given the CURRENT HEAD and the",
|
|
6117
|
+
"recent TRANSCRIPT + DECISIONS, return an updated PINNED and, optionally, a next suggestion. Keep",
|
|
6118
|
+
"PINNED tight and concrete; keep anything the user pinned; never invent; preserve Turkish characters",
|
|
6119
|
+
"(\xE7 \u011F \u0131 \u0130 \xF6 \u015F \xFC) exactly. Do NOT manage the checklist \u2014 the note path owns that. The ANCHOR is the",
|
|
6120
|
+
"read-only North-Star \u2014 NEVER change it. Never restate an unverified artifact-claim (a named file,",
|
|
6121
|
+
"PR, flag, or board state) as settled fact \u2014 keep it as the belief it was recorded as.",
|
|
6093
6122
|
"You MAY also propose supersessions: each DECISION is shown with its original stable 0-based index. Propose a",
|
|
6094
6123
|
"supersession ONLY for a NEWER decision that directly contradicts/replaces an OLDER one where neither",
|
|
6095
6124
|
"already carries a supersededBy. HIGH PRECISION \u2014 propose ONLY when you are confident the older claim",
|
|
6096
6125
|
"is now false or obsolete; the newer decision's timestamp MUST be later than the older's (newer-supersedes-",
|
|
6097
|
-
"older only, never the reverse). When unsure, propose nothing. NEVER touch the anchor
|
|
6098
|
-
'
|
|
6126
|
+
"older only, never the reverse). When unsure, propose nothing. NEVER touch the anchor or checklist.",
|
|
6127
|
+
'For "next": propose a single concise actionable line (\u2264140 chars) describing the most useful next step',
|
|
6128
|
+
"for a future session, derived from the transcript. This is a best-effort suggestion shown only when",
|
|
6129
|
+
"the user has not set their own NEXT. Use an empty string if nothing concrete emerges \u2014 never invent.",
|
|
6130
|
+
"Preserve Turkish characters exactly.",
|
|
6131
|
+
'Output ONLY a JSON object: {"pinned":[string],"next":string,"supersede":[{"older":int,"newer":int,"reason":string}]}.',
|
|
6099
6132
|
"",
|
|
6100
6133
|
"CURRENT HEAD:",
|
|
6101
6134
|
JSON.stringify(state.head ?? {}, null, 2),
|
|
@@ -6119,6 +6152,9 @@ function parseHeadUpdate(raw) {
|
|
|
6119
6152
|
if (!obj || typeof obj !== "object") return null;
|
|
6120
6153
|
const u = {};
|
|
6121
6154
|
if (Array.isArray(obj.pinned)) u.pinned = obj.pinned.filter((x) => typeof x === "string");
|
|
6155
|
+
if (typeof obj.next === "string" && obj.next.trim()) {
|
|
6156
|
+
u.nextAuto = obj.next.trim().slice(0, 280);
|
|
6157
|
+
}
|
|
6122
6158
|
if (Array.isArray(obj.supersede)) {
|
|
6123
6159
|
const supersede = obj.supersede.filter((e) => {
|
|
6124
6160
|
if (!e || typeof e !== "object") return false;
|
|
@@ -6131,12 +6167,12 @@ function parseHeadUpdate(raw) {
|
|
|
6131
6167
|
}
|
|
6132
6168
|
async function runHeadEngine(prompt, timeoutMs = HEAD_ENGINE_TIMEOUT_MS) {
|
|
6133
6169
|
const { cmd, args, shell: shell2 } = resolveEngine(process.platform, process.env.SAGA_HEAD_ENGINE);
|
|
6134
|
-
return await new Promise((
|
|
6170
|
+
return await new Promise((resolve6) => {
|
|
6135
6171
|
let child;
|
|
6136
6172
|
try {
|
|
6137
6173
|
child = (0, import_node_child_process3.spawn)(cmd, args, { shell: shell2, windowsHide: true });
|
|
6138
6174
|
} catch {
|
|
6139
|
-
return
|
|
6175
|
+
return resolve6("");
|
|
6140
6176
|
}
|
|
6141
6177
|
let out = "";
|
|
6142
6178
|
let done = false;
|
|
@@ -6144,7 +6180,7 @@ async function runHeadEngine(prompt, timeoutMs = HEAD_ENGINE_TIMEOUT_MS) {
|
|
|
6144
6180
|
if (done) return;
|
|
6145
6181
|
done = true;
|
|
6146
6182
|
clearTimeout(timer);
|
|
6147
|
-
|
|
6183
|
+
resolve6(v);
|
|
6148
6184
|
};
|
|
6149
6185
|
const timer = setTimeout(() => {
|
|
6150
6186
|
try {
|
|
@@ -6292,8 +6328,8 @@ async function readStdin(opts = {}) {
|
|
|
6292
6328
|
})().catch(() => {
|
|
6293
6329
|
});
|
|
6294
6330
|
let timer;
|
|
6295
|
-
const timeout = new Promise((
|
|
6296
|
-
timer = setTimeout(
|
|
6331
|
+
const timeout = new Promise((resolve6) => {
|
|
6332
|
+
timer = setTimeout(resolve6, timeoutMs);
|
|
6297
6333
|
});
|
|
6298
6334
|
try {
|
|
6299
6335
|
await Promise.race([drain, timeout]);
|
|
@@ -6966,7 +7002,7 @@ var execFileP3 = (0, import_node_util5.promisify)(import_node_child_process5.exe
|
|
|
6966
7002
|
var DOCKER_TIMEOUT_MS = 15e3;
|
|
6967
7003
|
var EARLY_EXIT_GRACE_MS = 2e3;
|
|
6968
7004
|
function waitForProcessStability(child, graceMs = EARLY_EXIT_GRACE_MS) {
|
|
6969
|
-
return new Promise((
|
|
7005
|
+
return new Promise((resolve6, reject) => {
|
|
6970
7006
|
let settled = false;
|
|
6971
7007
|
const finish = (fn) => {
|
|
6972
7008
|
if (settled) return;
|
|
@@ -6976,7 +7012,7 @@ function waitForProcessStability(child, graceMs = EARLY_EXIT_GRACE_MS) {
|
|
|
6976
7012
|
child.removeAllListeners("exit");
|
|
6977
7013
|
fn();
|
|
6978
7014
|
};
|
|
6979
|
-
const timer = setTimeout(() => finish(
|
|
7015
|
+
const timer = setTimeout(() => finish(resolve6), graceMs);
|
|
6980
7016
|
child.on("error", (err) => finish(() => reject(new Error(`stage process failed to start: ${err.message}`))));
|
|
6981
7017
|
child.on("exit", (code, signal) => {
|
|
6982
7018
|
const detail = code != null ? `code ${code}` : signal ? `signal ${signal}` : "unknown reason";
|
|
@@ -7186,10 +7222,10 @@ function pickStagePort(range, isFree) {
|
|
|
7186
7222
|
throw new Error(`no free stage port in range ${start}-${end} \u2014 every port is in use`);
|
|
7187
7223
|
}
|
|
7188
7224
|
function isPortFree(port) {
|
|
7189
|
-
return new Promise((
|
|
7225
|
+
return new Promise((resolve6) => {
|
|
7190
7226
|
const srv = (0, import_node_net.createServer)();
|
|
7191
|
-
srv.once("error", () =>
|
|
7192
|
-
srv.once("listening", () => srv.close(() =>
|
|
7227
|
+
srv.once("error", () => resolve6(false));
|
|
7228
|
+
srv.once("listening", () => srv.close(() => resolve6(true)));
|
|
7193
7229
|
srv.listen(port, "127.0.0.1");
|
|
7194
7230
|
});
|
|
7195
7231
|
}
|
|
@@ -7365,6 +7401,18 @@ function writeState(path2, state) {
|
|
|
7365
7401
|
mkdirFor(path2);
|
|
7366
7402
|
(0, import_node_fs9.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
|
|
7367
7403
|
}
|
|
7404
|
+
function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
|
|
7405
|
+
const reservation = {
|
|
7406
|
+
pid: 0,
|
|
7407
|
+
command: "",
|
|
7408
|
+
cwd,
|
|
7409
|
+
statePath,
|
|
7410
|
+
startedAt: now().toISOString(),
|
|
7411
|
+
port
|
|
7412
|
+
};
|
|
7413
|
+
writeState(statePath, reservation);
|
|
7414
|
+
if (globalStatePath && globalStatePath !== statePath) writeState(globalStatePath, reservation);
|
|
7415
|
+
}
|
|
7368
7416
|
async function cleanupStageState(state, paths, timeoutMs, fallbackCwd) {
|
|
7369
7417
|
await killTree(state.pid);
|
|
7370
7418
|
if (state.teardown?.command.trim()) {
|
|
@@ -7388,7 +7436,7 @@ async function killTree(pid) {
|
|
|
7388
7436
|
} catch {
|
|
7389
7437
|
}
|
|
7390
7438
|
}
|
|
7391
|
-
await new Promise((
|
|
7439
|
+
await new Promise((resolve6) => setTimeout(resolve6, 500));
|
|
7392
7440
|
try {
|
|
7393
7441
|
process.kill(-pid, "SIGKILL");
|
|
7394
7442
|
} catch {
|
|
@@ -7409,7 +7457,7 @@ async function waitForHealth(url, timeoutMs, anyStatus = false) {
|
|
|
7409
7457
|
} catch (e) {
|
|
7410
7458
|
last = e.message;
|
|
7411
7459
|
}
|
|
7412
|
-
await new Promise((
|
|
7460
|
+
await new Promise((resolve6) => setTimeout(resolve6, 1e3));
|
|
7413
7461
|
}
|
|
7414
7462
|
throw new Error(`stage health check timed out for ${url}${last ? ` (${last})` : ""}`);
|
|
7415
7463
|
}
|
|
@@ -7505,6 +7553,8 @@ async function runStage(config = {}, opts = {}) {
|
|
|
7505
7553
|
if (problems.length) throw new Error(problems.join("; "));
|
|
7506
7554
|
const cwd = opts.cwd ?? process.cwd();
|
|
7507
7555
|
const timeoutMs = opts.timeoutMs ?? 6e4;
|
|
7556
|
+
const statePath = opts.statePath ?? stageStatePath(cwd);
|
|
7557
|
+
const globalStatePath = await resolveGlobalStatePath(cwd, opts.globalStatePath);
|
|
7508
7558
|
const portGuard = resolveStagePortGuard(opts);
|
|
7509
7559
|
await stopStage({ ...opts, cwd, requiredIdentityCwd: opts.requiredIdentityCwd ?? cwd });
|
|
7510
7560
|
const reserved = await reservedPortsForWorktree(cwd);
|
|
@@ -7515,11 +7565,20 @@ async function runStage(config = {}, opts = {}) {
|
|
|
7515
7565
|
stagePort = await resolveStagePort(config, portGuard, reserved);
|
|
7516
7566
|
}
|
|
7517
7567
|
const sub = (s) => substituteStagePort(s, stagePort);
|
|
7518
|
-
|
|
7568
|
+
if (stagePort != null) {
|
|
7569
|
+
writeStagePortReservation(stagePort, cwd, statePath, globalStatePath, opts.now ?? (() => /* @__PURE__ */ new Date()));
|
|
7570
|
+
}
|
|
7519
7571
|
const extraEnv = stageExtraEnv(config, stagePort);
|
|
7520
7572
|
const build2 = config.build?.trim();
|
|
7521
7573
|
const ranBuild = Boolean(build2);
|
|
7522
|
-
|
|
7574
|
+
try {
|
|
7575
|
+
await ensureStageRuntimeEnv(config, opts, cwd);
|
|
7576
|
+
if (build2) await shell(sub(build2), cwd, timeoutMs, stageProcessEnv(stagePort, extraEnv));
|
|
7577
|
+
} catch (e) {
|
|
7578
|
+
(0, import_node_fs9.rmSync)(statePath, { force: true });
|
|
7579
|
+
if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs9.rmSync)(globalStatePath, { force: true });
|
|
7580
|
+
throw e;
|
|
7581
|
+
}
|
|
7523
7582
|
const started = await startStage(config, {
|
|
7524
7583
|
...opts,
|
|
7525
7584
|
cwd,
|
|
@@ -8519,15 +8578,28 @@ function planDirtyForAutosave(localHash, meta, project2, slug, queue) {
|
|
|
8519
8578
|
if (metaEntry?.hash === localHash) return false;
|
|
8520
8579
|
return true;
|
|
8521
8580
|
}
|
|
8522
|
-
function
|
|
8581
|
+
function resolveAutosaveProject(meta, queue, defaultProject, slug) {
|
|
8582
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
8583
|
+
const suffix = `/${slug}`;
|
|
8584
|
+
for (const key of Object.keys(meta)) {
|
|
8585
|
+
if (key.endsWith(suffix)) candidates.add(key.slice(0, -suffix.length));
|
|
8586
|
+
}
|
|
8587
|
+
for (const entry of queue) {
|
|
8588
|
+
if (entry.slug === slug) candidates.add(entry.project);
|
|
8589
|
+
}
|
|
8590
|
+
if (candidates.size === 1) return [...candidates][0];
|
|
8591
|
+
return defaultProject;
|
|
8592
|
+
}
|
|
8593
|
+
function plansNeedingAutosave(deps, project2, slugs) {
|
|
8523
8594
|
const meta = parseMeta(deps.readMetaRaw());
|
|
8524
8595
|
const queue = parseQueue(deps.readQueueRaw());
|
|
8525
8596
|
const out = [];
|
|
8526
8597
|
for (const slug of slugs) {
|
|
8527
8598
|
const raw = deps.readLocal(slug);
|
|
8528
8599
|
if (raw == null) continue;
|
|
8600
|
+
const autosaveProject = resolveAutosaveProject(meta, queue, project2, slug);
|
|
8529
8601
|
const hash = hashContent(normalizeEol(raw));
|
|
8530
|
-
if (planDirtyForAutosave(hash, meta,
|
|
8602
|
+
if (planDirtyForAutosave(hash, meta, autosaveProject, slug, queue)) out.push({ project: autosaveProject, slug });
|
|
8531
8603
|
}
|
|
8532
8604
|
return out;
|
|
8533
8605
|
}
|
|
@@ -8555,11 +8627,11 @@ async function planAutoEnqueueDirty(deps, opts = {}) {
|
|
|
8555
8627
|
}
|
|
8556
8628
|
}
|
|
8557
8629
|
const enqueued = [];
|
|
8558
|
-
for (const
|
|
8559
|
-
const raw = deps.readLocal(slug);
|
|
8630
|
+
for (const entry of plansNeedingAutosave(deps, project2, slugs)) {
|
|
8631
|
+
const raw = deps.readLocal(entry.slug);
|
|
8560
8632
|
if (raw == null) continue;
|
|
8561
|
-
enqueuePlanPush(deps, { project:
|
|
8562
|
-
enqueued.push(slug);
|
|
8633
|
+
enqueuePlanPush(deps, { project: entry.project, slug: entry.slug, hash: hashContent(normalizeEol(raw)) }, { detach: false });
|
|
8634
|
+
enqueued.push(entry.slug);
|
|
8563
8635
|
}
|
|
8564
8636
|
if (enqueued.length) deps.detachSync();
|
|
8565
8637
|
return enqueued;
|
|
@@ -11210,6 +11282,11 @@ function defaultWorktreePath(repoRoot, branch) {
|
|
|
11210
11282
|
const safe = branch.replace(/[/\\]+/g, "-");
|
|
11211
11283
|
return (0, import_node_path15.join)((0, import_node_path15.dirname)(repoRoot), "mmi-worktrees", safe);
|
|
11212
11284
|
}
|
|
11285
|
+
function resolveWorktreeBase(from, remote) {
|
|
11286
|
+
const remotePrefix = `${remote}/`;
|
|
11287
|
+
const fetchBranch = from.startsWith(remotePrefix) ? from.slice(remotePrefix.length) : void 0;
|
|
11288
|
+
return { base: from, fetchBranch };
|
|
11289
|
+
}
|
|
11213
11290
|
|
|
11214
11291
|
// src/northstar-context.ts
|
|
11215
11292
|
var SESSION_START_NORTHSTAR_TIMEOUT_MS = 3e3;
|
|
@@ -11350,7 +11427,7 @@ function whoamiLine(report) {
|
|
|
11350
11427
|
}
|
|
11351
11428
|
|
|
11352
11429
|
// src/index.ts
|
|
11353
|
-
var
|
|
11430
|
+
var import_node_path25 = require("node:path");
|
|
11354
11431
|
|
|
11355
11432
|
// src/merge-ci-policy.ts
|
|
11356
11433
|
function resolveMergeCiPolicy(input) {
|
|
@@ -12414,7 +12491,7 @@ var PR_LAND_STATE_READ_DELAY_MS = 2e3;
|
|
|
12414
12491
|
async function readGhPrStateWithRetry(fetchState2, options) {
|
|
12415
12492
|
const retries = options?.retries ?? PR_LAND_STATE_READ_RETRIES;
|
|
12416
12493
|
const delayMs = options?.delayMs ?? PR_LAND_STATE_READ_DELAY_MS;
|
|
12417
|
-
const sleep = options?.sleep ?? ((ms) => new Promise((
|
|
12494
|
+
const sleep = options?.sleep ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
|
|
12418
12495
|
let lastError = "empty state";
|
|
12419
12496
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
12420
12497
|
try {
|
|
@@ -13621,35 +13698,18 @@ async function detectPublicIp(fetchImpl = fetch) {
|
|
|
13621
13698
|
if (!validStageLiveIp(ip)) throw new Error(`public IP detection returned a non-IP body from ${IP_ECHO_URL}: "${ip.slice(0, 80)}"`);
|
|
13622
13699
|
return ip;
|
|
13623
13700
|
}
|
|
13624
|
-
function ghDispatchArgs(workflow, inputs) {
|
|
13625
|
-
const args = ["workflow", "run", workflow, "--repo", STAGE_LIVE_HUB_REPO];
|
|
13626
|
-
for (const [key, value] of Object.entries(inputs)) args.push("-f", `${key}=${value}`);
|
|
13627
|
-
return args;
|
|
13628
|
-
}
|
|
13629
13701
|
function stageLiveUpSteps(t) {
|
|
13630
13702
|
return [
|
|
13631
13703
|
{ label: `detect your public IP (${IP_ECHO_URL}, bounded)` },
|
|
13632
|
-
{
|
|
13633
|
-
|
|
13634
|
-
command: `gh ${ghDispatchArgs("tenant-deploy.yml", { slug: t.slug, repo: t.repo, ref: t.ref ?? "<branch>", stage: "dev" }).join(" ")}`
|
|
13635
|
-
},
|
|
13636
|
-
{
|
|
13637
|
-
label: `gate ${t.host} to your IP at the Cloudflare edge (ephemeral firewall_custom skip rule)`,
|
|
13638
|
-
command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "cf-gate-allow", host: t.host, ip: "<your ip>" }).join(" ")}`
|
|
13639
|
-
},
|
|
13704
|
+
{ label: `deploy ${t.ref ?? "<current branch>"} to the ${t.slug} dev stage via the Hub backend (tenant-deploy)` },
|
|
13705
|
+
{ label: `gate ${t.host} to your IP at the Cloudflare edge via the Hub backend (tenant-control cf-gate-allow)` },
|
|
13640
13706
|
{ label: "tear down when done", command: "mmi-cli stage --live --down --apply" }
|
|
13641
13707
|
];
|
|
13642
13708
|
}
|
|
13643
13709
|
function stageLiveDownSteps(t) {
|
|
13644
13710
|
return [
|
|
13645
|
-
{
|
|
13646
|
-
|
|
13647
|
-
command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "stop" }).join(" ")}`
|
|
13648
|
-
},
|
|
13649
|
-
{
|
|
13650
|
-
label: "remove the Cloudflare edge gate (the stage goes dark even if restarted)",
|
|
13651
|
-
command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "cf-gate-clear", host: t.host }).join(" ")}`
|
|
13652
|
-
}
|
|
13711
|
+
{ label: `stop the ${t.slug} dev runtime via the Hub backend (tenant-control stop)` },
|
|
13712
|
+
{ label: `remove the Cloudflare edge gate for ${t.host} via the Hub backend (tenant-control cf-gate-clear)` }
|
|
13653
13713
|
];
|
|
13654
13714
|
}
|
|
13655
13715
|
async function runStageLiveUp(deps, t) {
|
|
@@ -13657,8 +13717,8 @@ async function runStageLiveUp(deps, t) {
|
|
|
13657
13717
|
const ip = (await deps.detectIp()).trim();
|
|
13658
13718
|
if (!validStageLiveIp(ip)) throw new Error(`stage --live: detected public IP is not a literal IPv4/IPv6 address: "${ip.slice(0, 80)}"`);
|
|
13659
13719
|
if (!t.host?.trim()) throw new Error("stage --live: cannot resolve the dev edge host (registry edgeDomains.dev)");
|
|
13660
|
-
await deps.
|
|
13661
|
-
await deps.
|
|
13720
|
+
await deps.deployDev({ repo: t.repo, ref: t.ref });
|
|
13721
|
+
await deps.control({ repo: t.repo, action: "cf-gate-allow", host: t.host, ip });
|
|
13662
13722
|
return {
|
|
13663
13723
|
command: "stage --live",
|
|
13664
13724
|
mode: "up",
|
|
@@ -13672,8 +13732,8 @@ async function runStageLiveUp(deps, t) {
|
|
|
13672
13732
|
}
|
|
13673
13733
|
async function runStageLiveDown(deps, t) {
|
|
13674
13734
|
if (!t.host?.trim()) throw new Error("stage --live: cannot resolve the dev edge host (registry edgeDomains.dev)");
|
|
13675
|
-
await deps.
|
|
13676
|
-
await deps.
|
|
13735
|
+
await deps.control({ repo: t.repo, action: "stop" });
|
|
13736
|
+
await deps.control({ repo: t.repo, action: "cf-gate-clear", host: t.host });
|
|
13677
13737
|
return {
|
|
13678
13738
|
command: "stage --live",
|
|
13679
13739
|
mode: "down",
|
|
@@ -14174,7 +14234,7 @@ function clean3(out) {
|
|
|
14174
14234
|
return out.trim();
|
|
14175
14235
|
}
|
|
14176
14236
|
function sleeper(deps) {
|
|
14177
|
-
return deps.sleep ?? ((ms) => new Promise((
|
|
14237
|
+
return deps.sleep ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
|
|
14178
14238
|
}
|
|
14179
14239
|
function normalizeHotfixVersion(input) {
|
|
14180
14240
|
const m = /^v?(\d+\.\d+\.\d+)$/.exec(input.trim());
|
|
@@ -14400,12 +14460,13 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
14400
14460
|
"report",
|
|
14401
14461
|
meta.publishDir
|
|
14402
14462
|
);
|
|
14403
|
-
const publish = deploy.deployStatus === "
|
|
14463
|
+
const publish = deploy.deployStatus === "success" ? await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, true, "report") : null;
|
|
14404
14464
|
let dispatch = appendPublishDispatch(deploy, publish);
|
|
14405
|
-
if (!publish && deploy.deployStatus
|
|
14465
|
+
if (!publish && deploy.deployStatus !== "success" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
|
|
14466
|
+
const reason = deploy.deployStatus === "failure" ? "box deploy failed \u2014 redeploy the box before publishing" : "box deploy not confirmed (run with --watch) \u2014 publish after the box deploy lands";
|
|
14406
14467
|
dispatch = {
|
|
14407
14468
|
...dispatch,
|
|
14408
|
-
note: `${dispatch.note}; tenant-publish.yml skipped (
|
|
14469
|
+
note: `${dispatch.note}; tenant-publish.yml skipped (${reason})`
|
|
14409
14470
|
};
|
|
14410
14471
|
}
|
|
14411
14472
|
deployNote = dispatch.note;
|
|
@@ -16847,8 +16908,8 @@ function vaultPointer(slug) {
|
|
|
16847
16908
|
slug,
|
|
16848
16909
|
root,
|
|
16849
16910
|
tiers: {
|
|
16850
|
-
project: `${root}
|
|
16851
|
-
org: [
|
|
16911
|
+
project: `${root}/{dev,rc,main}/* (project-admin self-serve for this repo)`,
|
|
16912
|
+
org: [`/mmi-future/{shared,cloudflare,mmi-hub,...}/* (org-infra, master-gated)`]
|
|
16852
16913
|
},
|
|
16853
16914
|
stages: ["dev", "rc", "main"],
|
|
16854
16915
|
// Google OAuth is one client per repo; creds live at every stage under the standard key names
|
|
@@ -16861,15 +16922,16 @@ function vaultPointer(slug) {
|
|
|
16861
16922
|
function formatVaultPointer(p) {
|
|
16862
16923
|
const lines = [
|
|
16863
16924
|
`vault root: ${p.root}`,
|
|
16864
|
-
` project
|
|
16865
|
-
` org
|
|
16925
|
+
` project repo tree: ${p.tiers.project}`,
|
|
16926
|
+
` org-infra tree: ${p.tiers.org.join(" \xB7 ")}`,
|
|
16866
16927
|
`stages: ${p.stages.join(", ")} (local is port-agnostic, reuses dev)`,
|
|
16867
16928
|
`well-known keys:`,
|
|
16868
16929
|
...Object.entries(p.wellKnown).map(([k, keys]) => ` ${k}: ${keys.join(", ")}`),
|
|
16869
16930
|
``,
|
|
16870
16931
|
`enumerate actual keys: mmi-cli secrets list`,
|
|
16871
16932
|
`read one: mmi-cli secrets get <stage>/<KEY> (e.g. main/GOOGLE_CLIENT_ID)`,
|
|
16872
|
-
`set a
|
|
16933
|
+
`set a key: mmi-cli secrets set <stage>/<KEY> (value via stdin; project-admin self-serves own repo)`,
|
|
16934
|
+
`import Rails creds: mmi-cli secrets import-rails-credentials --stage main --map secret_key_base=SECRET_KEY_BASE`,
|
|
16873
16935
|
`copy provider keys: mmi-cli secrets copy --from rc --to dev --keys RECALL_API_KEY,GEMINI_API_KEY`
|
|
16874
16936
|
];
|
|
16875
16937
|
return lines.join("\n");
|
|
@@ -17222,6 +17284,89 @@ async function secretsSet(deps, key, opts) {
|
|
|
17222
17284
|
}
|
|
17223
17285
|
return putSecret(deps, key, value, opts);
|
|
17224
17286
|
}
|
|
17287
|
+
function parseRailsCredentialMapping(raw, stage2) {
|
|
17288
|
+
const eq = raw.indexOf("=");
|
|
17289
|
+
if (eq <= 0 || eq === raw.length - 1) return null;
|
|
17290
|
+
const credentialPath = raw.slice(0, eq).trim();
|
|
17291
|
+
const envKey = raw.slice(eq + 1).trim();
|
|
17292
|
+
const vaultKey = stageKey2(stage2, envKey);
|
|
17293
|
+
if (!credentialPath || !envKey || !isValidSecretKey(vaultKey)) return null;
|
|
17294
|
+
return { credentialPath, envKey, vaultKey };
|
|
17295
|
+
}
|
|
17296
|
+
function credentialValueAt(root, path2) {
|
|
17297
|
+
let cur = root;
|
|
17298
|
+
for (const part of path2.split(".").filter(Boolean)) {
|
|
17299
|
+
if (!cur || typeof cur !== "object" || !(part in cur)) return void 0;
|
|
17300
|
+
cur = cur[part];
|
|
17301
|
+
}
|
|
17302
|
+
return cur;
|
|
17303
|
+
}
|
|
17304
|
+
function credentialValueToSecret(value) {
|
|
17305
|
+
if (value === void 0 || value === null) return null;
|
|
17306
|
+
if (typeof value === "string") return value || null;
|
|
17307
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
17308
|
+
return JSON.stringify(value);
|
|
17309
|
+
}
|
|
17310
|
+
async function secretsImportRailsCredentials(deps, opts) {
|
|
17311
|
+
const mappings = opts.mappings.map((m) => parseRailsCredentialMapping(m, opts.stage));
|
|
17312
|
+
const bad = mappings.findIndex((m) => !m);
|
|
17313
|
+
if (bad !== -1) {
|
|
17314
|
+
deps.err(`invalid Rails credential mapping ${JSON.stringify(opts.mappings[bad])}; use credential.path=ENV_KEY`);
|
|
17315
|
+
return false;
|
|
17316
|
+
}
|
|
17317
|
+
const parsed = mappings;
|
|
17318
|
+
if (!parsed.length) {
|
|
17319
|
+
deps.err("secrets import-rails-credentials: at least one --map credential.path=ENV_KEY is required");
|
|
17320
|
+
return false;
|
|
17321
|
+
}
|
|
17322
|
+
if (opts.removeFiles && !deps.removeFile) {
|
|
17323
|
+
deps.err("secrets import-rails-credentials: --remove-files is unavailable in this execution context");
|
|
17324
|
+
return false;
|
|
17325
|
+
}
|
|
17326
|
+
let credentials;
|
|
17327
|
+
try {
|
|
17328
|
+
credentials = await deps.decryptRailsCredentials({
|
|
17329
|
+
appDir: opts.appDir,
|
|
17330
|
+
credentialsFile: opts.credentialsFile,
|
|
17331
|
+
masterKeyFile: opts.masterKeyFile
|
|
17332
|
+
});
|
|
17333
|
+
} catch (e) {
|
|
17334
|
+
deps.err(`secrets import-rails-credentials: could not decrypt Rails credentials: ${e.message}`);
|
|
17335
|
+
return false;
|
|
17336
|
+
}
|
|
17337
|
+
const imports = [];
|
|
17338
|
+
for (const mapping of parsed) {
|
|
17339
|
+
const value = credentialValueToSecret(credentialValueAt(credentials, mapping.credentialPath));
|
|
17340
|
+
if (!value) {
|
|
17341
|
+
deps.err(`credential path ${mapping.credentialPath} not found or empty; nothing written`);
|
|
17342
|
+
return false;
|
|
17343
|
+
}
|
|
17344
|
+
imports.push({ ...mapping, value });
|
|
17345
|
+
}
|
|
17346
|
+
if (opts.dryRun) {
|
|
17347
|
+
for (const item of imports) deps.log(`would import ${item.credentialPath} -> ${item.vaultKey}`);
|
|
17348
|
+
deps.log(`${imports.length} Rails credential mapping(s) checked; no values printed and nothing written`);
|
|
17349
|
+
return true;
|
|
17350
|
+
}
|
|
17351
|
+
let written = 0;
|
|
17352
|
+
for (const item of imports) {
|
|
17353
|
+
const ok = await putSecret(deps, item.vaultKey, item.value, opts);
|
|
17354
|
+
if (!ok) {
|
|
17355
|
+
deps.err(
|
|
17356
|
+
`Rails credentials import stopped after a partial write (${written}/${imports.length}); local encrypted files left untouched`
|
|
17357
|
+
);
|
|
17358
|
+
return false;
|
|
17359
|
+
}
|
|
17360
|
+
written += 1;
|
|
17361
|
+
deps.log(`imported ${item.credentialPath} -> ${item.vaultKey}`);
|
|
17362
|
+
}
|
|
17363
|
+
if (opts.removeFiles) {
|
|
17364
|
+
const files = [...new Set([opts.credentialsFile, opts.masterKeyFile].filter((p) => Boolean(p)))];
|
|
17365
|
+
for (const file of files) await deps.removeFile(file);
|
|
17366
|
+
if (files.length) deps.log(`removed ${files.length} local Rails encrypted credential file(s) after successful import`);
|
|
17367
|
+
}
|
|
17368
|
+
return true;
|
|
17369
|
+
}
|
|
17225
17370
|
async function secretsEdit(deps, key, opts) {
|
|
17226
17371
|
return secretsSet(deps, key, opts);
|
|
17227
17372
|
}
|
|
@@ -17332,12 +17477,52 @@ async function secretsUse(deps, key, opts) {
|
|
|
17332
17477
|
` \u2022 Runtime / agents: read it keylessly at runtime via the box's OIDC role (it can read its own ${tier} tier). Never bake it into an image or commit it.`,
|
|
17333
17478
|
` \u2022 CI (GitHub Actions): the workflow assumes its OIDC role and runs \`aws ssm get-parameter --with-decryption --name ${path2}\` \u2014 no GitHub secret.`,
|
|
17334
17479
|
" \u2022 Local dev: pull it into a gitignored .env from the vault. To confirm access without printing in PowerShell: `$null = mmi-cli secrets get " + key + "`; in POSIX shells: `mmi-cli secrets get " + key + " >/dev/null`. Never paste it into tracked files or chat.",
|
|
17335
|
-
tier === "project" ? " \u2022
|
|
17480
|
+
tier === "project" ? " \u2022 Bare keys default to dev/. Use an explicit rc/<KEY> or main/<KEY> when the stage needs its own value." : " \u2022 For your own product repo, project-admins self-serve this stage key. Org-infra/cross-slug keys remain master-gated."
|
|
17336
17481
|
].join("\n")
|
|
17337
17482
|
);
|
|
17338
17483
|
}
|
|
17339
17484
|
|
|
17340
17485
|
// src/secrets-commands.ts
|
|
17486
|
+
var import_node_fs22 = require("node:fs");
|
|
17487
|
+
var import_node_path20 = require("node:path");
|
|
17488
|
+
var RAILS_CREDENTIALS_DECRYPT_TIMEOUT_MS = 3e4;
|
|
17489
|
+
var DEFAULT_RAILS_CREDENTIALS_FILE = "config/credentials.yml.enc";
|
|
17490
|
+
var DEFAULT_RAILS_MASTER_KEY_FILE = "config/master.key";
|
|
17491
|
+
function collectMap(value, previous = []) {
|
|
17492
|
+
return [...previous, value];
|
|
17493
|
+
}
|
|
17494
|
+
async function decryptRailsCredentials(input) {
|
|
17495
|
+
const appDir = (0, import_node_path20.resolve)(input.appDir ?? process.cwd());
|
|
17496
|
+
const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
|
|
17497
|
+
const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
|
|
17498
|
+
const credentialsPath = (0, import_node_path20.resolve)(appDir, credentialsFile);
|
|
17499
|
+
const masterKeyPath = (0, import_node_path20.resolve)(appDir, masterKeyFile);
|
|
17500
|
+
const env = {
|
|
17501
|
+
...process.env,
|
|
17502
|
+
MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
|
|
17503
|
+
MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
|
|
17504
|
+
};
|
|
17505
|
+
if ((0, import_node_fs22.existsSync)(masterKeyPath)) {
|
|
17506
|
+
env.RAILS_MASTER_KEY = (0, import_node_fs22.readFileSync)(masterKeyPath, "utf8").trim();
|
|
17507
|
+
}
|
|
17508
|
+
const script = [
|
|
17509
|
+
'require "json"',
|
|
17510
|
+
'require "active_support/encrypted_configuration"',
|
|
17511
|
+
'config_path = ENV.fetch("MMI_RAILS_CREDENTIALS_FILE")',
|
|
17512
|
+
'key_path = ENV.fetch("MMI_RAILS_MASTER_KEY_FILE")',
|
|
17513
|
+
'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
|
|
17514
|
+
"puts JSON.generate(config.config)"
|
|
17515
|
+
].join("; ");
|
|
17516
|
+
const args = ["exec", "ruby", "-e", script];
|
|
17517
|
+
const cmd = process.platform === "win32" ? "cmd.exe" : "bundle";
|
|
17518
|
+
const cmdArgs = process.platform === "win32" ? ["/c", "bundle", ...args] : args;
|
|
17519
|
+
const { stdout } = await execFileP2(cmd, cmdArgs, {
|
|
17520
|
+
cwd: appDir,
|
|
17521
|
+
env,
|
|
17522
|
+
timeout: RAILS_CREDENTIALS_DECRYPT_TIMEOUT_MS
|
|
17523
|
+
});
|
|
17524
|
+
return JSON.parse(stdout);
|
|
17525
|
+
}
|
|
17341
17526
|
async function readSecretStdin() {
|
|
17342
17527
|
if (process.stdin.isTTY) {
|
|
17343
17528
|
process.stderr.write(
|
|
@@ -17432,6 +17617,32 @@ function registerSecretsCommands(program3) {
|
|
|
17432
17617
|
});
|
|
17433
17618
|
if (!ok) process.exitCode = 1;
|
|
17434
17619
|
}));
|
|
17620
|
+
secrets.command("import-rails-credentials").description("decrypt Rails credentials and import explicit mappings into the vault (values never printed)").requiredOption("--stage <dev|rc|main>", "target vault stage").option("--map <credential.path=ENV_KEY>", "explicit credential-to-env mapping; repeat for each key", collectMap, []).option("--app-dir <path>", "Rails app directory (defaults to cwd)").option("--credentials-file <path>", `encrypted credentials path relative to --app-dir (default: ${DEFAULT_RAILS_CREDENTIALS_FILE})`).option("--master-key-file <path>", `master key path relative to --app-dir (default: ${DEFAULT_RAILS_MASTER_KEY_FILE})`).option("--dry-run", "decrypt and show mapped key names without writing values").option("--remove-files", "delete credentials/master key files after every mapped value imports successfully").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets(async (d) => {
|
|
17621
|
+
const stages = ["dev", "rc", "main"];
|
|
17622
|
+
if (!stages.includes(o.stage)) {
|
|
17623
|
+
return fail("secrets import-rails-credentials: --stage must be dev, rc, or main");
|
|
17624
|
+
}
|
|
17625
|
+
const credentialsFile = o.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
|
|
17626
|
+
const masterKeyFile = o.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
|
|
17627
|
+
const ok = await secretsImportRailsCredentials(
|
|
17628
|
+
{
|
|
17629
|
+
...d,
|
|
17630
|
+
decryptRailsCredentials,
|
|
17631
|
+
removeFile: (path2) => (0, import_node_fs22.unlinkSync)((0, import_node_path20.resolve)(o.appDir ?? process.cwd(), path2))
|
|
17632
|
+
},
|
|
17633
|
+
{
|
|
17634
|
+
repo: o.repo,
|
|
17635
|
+
stage: o.stage,
|
|
17636
|
+
mappings: o.map,
|
|
17637
|
+
appDir: o.appDir,
|
|
17638
|
+
credentialsFile,
|
|
17639
|
+
masterKeyFile,
|
|
17640
|
+
dryRun: o.dryRun,
|
|
17641
|
+
removeFiles: o.removeFiles
|
|
17642
|
+
}
|
|
17643
|
+
);
|
|
17644
|
+
if (!ok) process.exitCode = 1;
|
|
17645
|
+
}));
|
|
17435
17646
|
secrets.command("rm <key>").description("remove a secret (project tier self-serve; org tier needs a grant)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
|
|
17436
17647
|
secrets.command("use <key>").description("print guidance on consuming a secret without committing it (no value)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsUse(d, key, o)));
|
|
17437
17648
|
secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to a specific org-tier secret").action((repo, login, key) => withSecrets((d) => secretsGrant(d, repo, login, key, {})));
|
|
@@ -17593,16 +17804,33 @@ function authorizeBodyHasMismatch(body) {
|
|
|
17593
17804
|
}
|
|
17594
17805
|
|
|
17595
17806
|
// src/doctor-run.ts
|
|
17596
|
-
var
|
|
17807
|
+
var import_node_fs27 = require("node:fs");
|
|
17597
17808
|
var import_promises7 = require("node:fs/promises");
|
|
17598
|
-
var
|
|
17809
|
+
var import_node_path24 = require("node:path");
|
|
17599
17810
|
var import_node_os5 = require("node:os");
|
|
17600
17811
|
|
|
17812
|
+
// src/plugin-guard.ts
|
|
17813
|
+
function buildPluginGuardDecision(i) {
|
|
17814
|
+
if (!i.isOrgRepo) return { state: "not-org" };
|
|
17815
|
+
if (!i.installRecordPresent) return { state: "no-install" };
|
|
17816
|
+
if (!i.marketplaceClonePresent || !i.pluginCachePresent) return { state: "unresolved" };
|
|
17817
|
+
return { state: "healthy" };
|
|
17818
|
+
}
|
|
17819
|
+
function buildGuardSessionStartLine(state, opts = {}) {
|
|
17820
|
+
if (state === "healthy" || state === "not-org") return { exitCode: 0 };
|
|
17821
|
+
const recovery = opts.recovery ?? "mmi-cli plugin-heal";
|
|
17822
|
+
const reason = state === "no-install" ? "MMI plugin is not installed for this user/session" : "MMI plugin is installed but its marketplace/cache is unresolved";
|
|
17823
|
+
return {
|
|
17824
|
+
line: `[mmi-guard] ${reason}; run ${recovery} and restart Claude Code / reload plugins.`,
|
|
17825
|
+
exitCode: 1
|
|
17826
|
+
};
|
|
17827
|
+
}
|
|
17828
|
+
|
|
17601
17829
|
// src/cursor-plugin-seed.ts
|
|
17602
17830
|
var import_node_child_process12 = require("node:child_process");
|
|
17603
|
-
var
|
|
17831
|
+
var import_node_fs23 = require("node:fs");
|
|
17604
17832
|
var import_node_os4 = require("node:os");
|
|
17605
|
-
var
|
|
17833
|
+
var import_node_path21 = require("node:path");
|
|
17606
17834
|
var import_node_util7 = require("node:util");
|
|
17607
17835
|
function isSemverVersion(v) {
|
|
17608
17836
|
return typeof v === "string" && /^v?\d+\.\d+\.\d+/.test(v.trim());
|
|
@@ -17619,17 +17847,17 @@ function ghReleaseTarballApiArgs(tag) {
|
|
|
17619
17847
|
}
|
|
17620
17848
|
function cursorUserGlobalStatePath() {
|
|
17621
17849
|
if (process.platform === "win32") {
|
|
17622
|
-
const base = process.env.APPDATA || (0,
|
|
17623
|
-
return (0,
|
|
17850
|
+
const base = process.env.APPDATA || (0, import_node_path21.join)((0, import_node_os4.homedir)(), "AppData", "Roaming");
|
|
17851
|
+
return (0, import_node_path21.join)(base, "Cursor", "User", "globalStorage", "state.vscdb");
|
|
17624
17852
|
}
|
|
17625
17853
|
if (process.platform === "darwin") {
|
|
17626
|
-
return (0,
|
|
17854
|
+
return (0, import_node_path21.join)((0, import_node_os4.homedir)(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
17627
17855
|
}
|
|
17628
|
-
return (0,
|
|
17856
|
+
return (0, import_node_path21.join)((0, import_node_os4.homedir)(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
17629
17857
|
}
|
|
17630
17858
|
async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
|
|
17631
17859
|
const dbPath = cursorUserGlobalStatePath();
|
|
17632
|
-
if (!(0,
|
|
17860
|
+
if (!(0, import_node_fs23.existsSync)(dbPath)) return void 0;
|
|
17633
17861
|
try {
|
|
17634
17862
|
const { stdout } = await execFileP5("sqlite3", [dbPath, `SELECT value FROM ItemTable WHERE key = '${CURSOR_THIRD_PARTY_STATE_KEY}';`], {
|
|
17635
17863
|
timeout: 5e3
|
|
@@ -17643,57 +17871,57 @@ async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
|
|
|
17643
17871
|
}
|
|
17644
17872
|
}
|
|
17645
17873
|
function syncDirContents(src, dest) {
|
|
17646
|
-
(0,
|
|
17647
|
-
for (const name of (0,
|
|
17648
|
-
(0,
|
|
17874
|
+
(0, import_node_fs23.mkdirSync)(dest, { recursive: true });
|
|
17875
|
+
for (const name of (0, import_node_fs23.readdirSync)(dest)) {
|
|
17876
|
+
(0, import_node_fs23.rmSync)((0, import_node_path21.join)(dest, name), { recursive: true, force: true });
|
|
17649
17877
|
}
|
|
17650
|
-
(0,
|
|
17878
|
+
(0, import_node_fs23.cpSync)(src, dest, { recursive: true });
|
|
17651
17879
|
}
|
|
17652
17880
|
function releaseTag(releasedVersion) {
|
|
17653
17881
|
return releasedVersion.startsWith("v") ? releasedVersion : `v${releasedVersion}`;
|
|
17654
17882
|
}
|
|
17655
17883
|
async function extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5) {
|
|
17656
|
-
const tarFile = (0,
|
|
17884
|
+
const tarFile = (0, import_node_path21.join)(tmpRoot, "archive.tar");
|
|
17657
17885
|
try {
|
|
17658
17886
|
await execFileP5("git", gitFetchReleaseTagArgs(hubCheckout, tag), { timeout: 6e4 });
|
|
17659
17887
|
await execFileP5("git", ["-C", hubCheckout, "archive", "--format=tar", `--output=${tarFile}`, tag, "plugins/mmi"], {
|
|
17660
17888
|
timeout: 6e4
|
|
17661
17889
|
});
|
|
17662
17890
|
await execFileP5("tar", ["-xf", tarFile, "-C", tmpRoot], { timeout: 6e4 });
|
|
17663
|
-
const pluginMmi = (0,
|
|
17664
|
-
return (0,
|
|
17891
|
+
const pluginMmi = (0, import_node_path21.join)(tmpRoot, "plugins", "mmi");
|
|
17892
|
+
return (0, import_node_fs23.existsSync)((0, import_node_path21.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
|
|
17665
17893
|
} catch {
|
|
17666
17894
|
return void 0;
|
|
17667
17895
|
}
|
|
17668
17896
|
}
|
|
17669
17897
|
async function downloadPluginMmiViaGh(tag, tmpRoot) {
|
|
17670
|
-
const tarPath = (0,
|
|
17898
|
+
const tarPath = (0, import_node_path21.join)(tmpRoot, "repo.tgz");
|
|
17671
17899
|
try {
|
|
17672
|
-
(0,
|
|
17900
|
+
(0, import_node_fs23.mkdirSync)(tmpRoot, { recursive: true });
|
|
17673
17901
|
const { stdout } = await execFileBuffer("gh", ghReleaseTarballApiArgs(tag), {
|
|
17674
17902
|
timeout: 12e4,
|
|
17675
17903
|
maxBuffer: 100 * 1024 * 1024,
|
|
17676
17904
|
encoding: "buffer",
|
|
17677
17905
|
windowsHide: true
|
|
17678
17906
|
});
|
|
17679
|
-
(0,
|
|
17907
|
+
(0, import_node_fs23.writeFileSync)(tarPath, stdout);
|
|
17680
17908
|
await execFileBuffer("tar", ["-xzf", tarPath, "-C", tmpRoot], { timeout: 12e4, windowsHide: true });
|
|
17681
|
-
const top = (0,
|
|
17909
|
+
const top = (0, import_node_fs23.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
|
|
17682
17910
|
if (!top) return void 0;
|
|
17683
|
-
const pluginMmi = (0,
|
|
17684
|
-
return (0,
|
|
17911
|
+
const pluginMmi = (0, import_node_path21.join)(tmpRoot, top, "plugins", "mmi");
|
|
17912
|
+
return (0, import_node_fs23.existsSync)((0, import_node_path21.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
|
|
17685
17913
|
} catch {
|
|
17686
17914
|
return void 0;
|
|
17687
17915
|
}
|
|
17688
17916
|
}
|
|
17689
17917
|
async function resolvePluginMmiSource(releasedVersion, hubCheckout, tmpRoot, execFileP5) {
|
|
17690
|
-
(0,
|
|
17918
|
+
(0, import_node_fs23.mkdirSync)(tmpRoot, { recursive: true });
|
|
17691
17919
|
const tag = releaseTag(releasedVersion);
|
|
17692
17920
|
if (hubCheckout) {
|
|
17693
17921
|
const fromHub = await extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5);
|
|
17694
17922
|
if (fromHub) return fromHub;
|
|
17695
17923
|
}
|
|
17696
|
-
return downloadPluginMmiViaGh(tag, (0,
|
|
17924
|
+
return downloadPluginMmiViaGh(tag, (0, import_node_path21.join)(tmpRoot, "gh"));
|
|
17697
17925
|
}
|
|
17698
17926
|
function cursorPluginPinsNeedingSeed(pins, releasedVersion) {
|
|
17699
17927
|
if (!isSemverVersion(releasedVersion)) return pins.filter((pin) => !pin.hasPluginJson || !pin.hasHooksJson || pin.isEmpty);
|
|
@@ -17714,7 +17942,7 @@ async function applyCursorPluginCacheSeed(input) {
|
|
|
17714
17942
|
for (const pin of pinsToSeed) {
|
|
17715
17943
|
syncDirContents(source, pin.path);
|
|
17716
17944
|
}
|
|
17717
|
-
(0,
|
|
17945
|
+
(0, import_node_fs23.rmSync)(tmpRoot, { recursive: true, force: true });
|
|
17718
17946
|
return true;
|
|
17719
17947
|
}
|
|
17720
17948
|
|
|
@@ -17789,20 +18017,6 @@ function pluginInstallManualFix(projectPath, surface = "claude-cli") {
|
|
|
17789
18017
|
const register = surface === "codex" ? `\`codex plugin add ${MMI_PLUGIN_ID}\`` : surface === "cursor" ? `import the MMI Team Marketplace in Cursor Dashboard \u2192 Settings \u2192 Plugins (or enable the MMI plugin from the marketplace panel)` : surface === "shell" ? `enable the MMI plugin in your client` : surface === "claude-vscode" ? `\`claude plugin enable ${MMI_PLUGIN_ID}\`` : `\`/plugin install ${MMI_PLUGIN_ID}\``;
|
|
17790
18018
|
return `run ${register} then ${reloadAction(surface)} to register the install record for ${projectPath}`;
|
|
17791
18019
|
}
|
|
17792
|
-
function isMmiPluginEnabled(settings) {
|
|
17793
|
-
return Boolean(settings?.enabledPlugins?.[MMI_PLUGIN_ID]);
|
|
17794
|
-
}
|
|
17795
|
-
function buildSettingsPluginDriftCheck(input) {
|
|
17796
|
-
const base = {
|
|
17797
|
-
ok: true,
|
|
17798
|
-
label: "org plugin wiring in .claude/settings.json (mmi@mutmutco)",
|
|
17799
|
-
fix: "the Claude Code app pruned mmi@mutmutco from the tracked .claude/settings.json (it does this at session start when the mutmutco marketplace source does not resolve, #1805); restore it with `git checkout -- .claude/settings.json` before committing, or the whole org skill set is disabled for the branch"
|
|
17800
|
-
};
|
|
17801
|
-
const enabled = input.settings?.enabledPlugins;
|
|
17802
|
-
if (!input.isOrgRepo || !enabled || Object.keys(enabled).length === 0) return base;
|
|
17803
|
-
if (!enabled[MMI_PLUGIN_ID]) return { ...base, ok: false };
|
|
17804
|
-
return base;
|
|
17805
|
-
}
|
|
17806
18020
|
function hasProjectInstallRecord(file, pluginId, projectPath) {
|
|
17807
18021
|
const records = file?.plugins?.[pluginId];
|
|
17808
18022
|
if (!Array.isArray(records)) return false;
|
|
@@ -17825,7 +18039,7 @@ function buildPluginInstallRecordCheck(input) {
|
|
|
17825
18039
|
fix: pluginInstallManualFix(input.projectPath, input.surface),
|
|
17826
18040
|
pluginId
|
|
17827
18041
|
};
|
|
17828
|
-
if (!input.isOrgRepo
|
|
18042
|
+
if (!input.isOrgRepo) return base;
|
|
17829
18043
|
if (hasAnyPluginRecords(input.installed, LEGACY_MMI_PLUGIN_ID) && !hasAnyPluginRecords(input.installed, pluginId)) {
|
|
17830
18044
|
return {
|
|
17831
18045
|
...base,
|
|
@@ -18086,8 +18300,9 @@ function reloadAction(surface) {
|
|
|
18086
18300
|
return "restart Claude Code (or run /reload-plugins)";
|
|
18087
18301
|
}
|
|
18088
18302
|
}
|
|
18089
|
-
var CLAUDE_RECOVERY = `claude plugin marketplace remove ${LEGACY_MMI_MARKETPLACE} && claude plugin marketplace remove mutmutco && claude plugin marketplace add mutmutco/MMI-Hub && claude plugin install mmi@mutmutco`;
|
|
18303
|
+
var CLAUDE_RECOVERY = `claude plugin marketplace remove ${LEGACY_MMI_MARKETPLACE} && claude plugin marketplace remove mutmutco && claude plugin marketplace add mutmutco/MMI-Hub --ref main && claude plugin install mmi@mutmutco`;
|
|
18090
18304
|
var CODEX_RECOVERY = `codex plugin marketplace remove ${LEGACY_MMI_MARKETPLACE} && codex plugin marketplace remove mutmutco && codex plugin marketplace add mutmutco/MMI-Hub --ref main && codex plugin add mmi@mutmutco`;
|
|
18305
|
+
var CURSOR_RECOVERY = "in Cursor Dashboard \u2192 Settings \u2192 Plugins, click Update next to the MMI Team Marketplace";
|
|
18091
18306
|
var OPENCODE_PLUGIN_PACKAGE = "@mutmutco/opencode-mmi";
|
|
18092
18307
|
var OPENCODE_PLUGIN_SPEC = `${OPENCODE_PLUGIN_PACKAGE}@latest`;
|
|
18093
18308
|
var OPENCODE_PLUGIN_INSTALL_COMMAND = `mmi-cli doctor --apply`;
|
|
@@ -18136,7 +18351,10 @@ var PLUGIN_SURFACE_HEAL = {
|
|
|
18136
18351
|
healSteps: [
|
|
18137
18352
|
{ args: ["plugin", "marketplace", "remove", LEGACY_MMI_MARKETPLACE], gated: false },
|
|
18138
18353
|
{ args: ["plugin", "marketplace", "remove", "mutmutco"], gated: false },
|
|
18139
|
-
|
|
18354
|
+
// Pin --ref main: the marketplace clone must track `main` (the released branch the plugin pins in
|
|
18355
|
+
// .claude-plugin/marketplace.json source.ref), removing the dev-vs-main skew that triggers the prune
|
|
18356
|
+
// and doubles fetch exposure. Mirrors the existing codex entry (#2038).
|
|
18357
|
+
{ args: ["plugin", "marketplace", "add", "mutmutco/MMI-Hub", "--ref", "main"], gated: true },
|
|
18140
18358
|
{ args: ["plugin", "install", "mmi@mutmutco"], gated: true },
|
|
18141
18359
|
{ args: ["plugin", "enable", "mmi@mutmutco"], gated: false }
|
|
18142
18360
|
],
|
|
@@ -18159,10 +18377,10 @@ var PLUGIN_SURFACE_HEAL = {
|
|
|
18159
18377
|
},
|
|
18160
18378
|
cursor: {
|
|
18161
18379
|
delivery: "cursor-cache",
|
|
18162
|
-
recovery:
|
|
18380
|
+
recovery: CURSOR_RECOVERY,
|
|
18163
18381
|
healSteps: null,
|
|
18164
|
-
fix: (surface) =>
|
|
18165
|
-
updateRecipe: []
|
|
18382
|
+
fix: (surface) => `${CURSOR_RECOVERY}; then ${reloadAction(surface)} to reload MMI skills + hooks`,
|
|
18383
|
+
updateRecipe: [CURSOR_RECOVERY]
|
|
18166
18384
|
},
|
|
18167
18385
|
opencode: {
|
|
18168
18386
|
delivery: "npm",
|
|
@@ -18203,9 +18421,21 @@ function surfaceToken(surface) {
|
|
|
18203
18421
|
var PLUGIN_UPDATE_RECIPES = {
|
|
18204
18422
|
claude: PLUGIN_SURFACE_HEAL.claude.updateRecipe,
|
|
18205
18423
|
codex: PLUGIN_SURFACE_HEAL.codex.updateRecipe,
|
|
18424
|
+
cursor: PLUGIN_SURFACE_HEAL.cursor.updateRecipe,
|
|
18206
18425
|
opencode: PLUGIN_SURFACE_HEAL.opencode.updateRecipe,
|
|
18207
18426
|
cli: ["npm install -g @mutmutco/cli@latest"]
|
|
18208
18427
|
};
|
|
18428
|
+
var PLUGIN_GUIDE_SURFACES = [
|
|
18429
|
+
{ key: "cli", label: "npm CLI", versionKeys: ["cli"] },
|
|
18430
|
+
{ key: "claude", label: "Claude Code", versionKeys: ["claudePlugin"] },
|
|
18431
|
+
{ key: "codex", label: "Codex", versionKeys: ["codexMarketplace", "codexActiveCache"] },
|
|
18432
|
+
{ key: "cursor", label: "Cursor", versionKeys: [] },
|
|
18433
|
+
{ key: "opencode", label: "OpenCode", versionKeys: ["opencodePlugin"] }
|
|
18434
|
+
];
|
|
18435
|
+
function renderSurfaceGuide(label, steps) {
|
|
18436
|
+
if (!steps.length) return [];
|
|
18437
|
+
return [` ${label}`, ...steps.map((step) => ` ${step}`)];
|
|
18438
|
+
}
|
|
18209
18439
|
function highestSemver(versions) {
|
|
18210
18440
|
return versions.reduce((best, v) => {
|
|
18211
18441
|
if (!isSemverVersion2(v)) return best;
|
|
@@ -18233,20 +18463,24 @@ function buildPluginUpdateReport(input) {
|
|
|
18233
18463
|
function renderPluginUpdateReport(report) {
|
|
18234
18464
|
const v = report.versions;
|
|
18235
18465
|
const show = (x) => x ?? "unknown";
|
|
18236
|
-
|
|
18237
|
-
"
|
|
18238
|
-
|
|
18239
|
-
|
|
18240
|
-
|
|
18241
|
-
|
|
18242
|
-
` OpenCode plugin: ${show(v.opencodePlugin)}`,
|
|
18243
|
-
` latest release: ${show(v.released)}`,
|
|
18244
|
-
"Update recipes (per surface):",
|
|
18245
|
-
` Claude: ${report.recipes.claude.join(" ; ")}`,
|
|
18246
|
-
` Codex: ${report.recipes.codex.join(" ; ")}`,
|
|
18247
|
-
` OpenCode: ${report.recipes.opencode.join(" ; ")}`,
|
|
18248
|
-
` npm CLI: ${report.recipes.cli.join(" ; ")}`
|
|
18466
|
+
const versionRows = [
|
|
18467
|
+
["mmi-cli", show(v.cli)],
|
|
18468
|
+
["Claude plugin", show(v.claudePlugin)],
|
|
18469
|
+
["Codex marketplace", show(v.codexMarketplace)],
|
|
18470
|
+
["Codex active cache", show(v.codexActiveCache)],
|
|
18471
|
+
["OpenCode plugin", show(v.opencodePlugin)]
|
|
18249
18472
|
];
|
|
18473
|
+
const pad = Math.max(...versionRows.map(([label]) => label.length));
|
|
18474
|
+
const lines = [
|
|
18475
|
+
`MMI versions (target release: ${show(v.released)})`,
|
|
18476
|
+
...versionRows.map(([label, value]) => ` ${label.padEnd(pad)} ${value}`),
|
|
18477
|
+
"",
|
|
18478
|
+
"Update commands by surface"
|
|
18479
|
+
];
|
|
18480
|
+
for (const surface of PLUGIN_GUIDE_SURFACES) {
|
|
18481
|
+
lines.push(...renderSurfaceGuide(surface.label, report.recipes[surface.key]));
|
|
18482
|
+
}
|
|
18483
|
+
return lines;
|
|
18250
18484
|
}
|
|
18251
18485
|
function buildDoctorJsonPayload(input) {
|
|
18252
18486
|
return {
|
|
@@ -18757,18 +18991,21 @@ function renderPluginUpdateReportStaleOnly(report) {
|
|
|
18757
18991
|
const released = v.released;
|
|
18758
18992
|
if (!released) return [];
|
|
18759
18993
|
const isStale = (current) => Boolean(current && isSemverVersion2(current) && compareVersions(current, released) < 0);
|
|
18760
|
-
const
|
|
18761
|
-
|
|
18762
|
-
|
|
18763
|
-
|
|
18764
|
-
recipeLines.push(` Codex: ${report.recipes.codex.join(" ; ")}`);
|
|
18994
|
+
const blocks = [];
|
|
18995
|
+
for (const surface of PLUGIN_GUIDE_SURFACES) {
|
|
18996
|
+
if (!surface.versionKeys.some((k) => isStale(v[k]))) continue;
|
|
18997
|
+
blocks.push(...renderSurfaceGuide(surface.label, report.recipes[surface.key]));
|
|
18765
18998
|
}
|
|
18766
|
-
if (
|
|
18767
|
-
|
|
18768
|
-
return ["Update recipes (stale surfaces):", ...recipeLines];
|
|
18999
|
+
if (!blocks.length) return [];
|
|
19000
|
+
return ["Update commands (stale surfaces):", ...blocks];
|
|
18769
19001
|
}
|
|
19002
|
+
var DOCTOR_VERBOSE_HINT = "Run mmi-cli doctor --verbose for the full audit checklist + version report.";
|
|
18770
19003
|
function renderTerseDoctorReport(input) {
|
|
18771
|
-
|
|
19004
|
+
const cliVersion = input.updateReport.versions.cli;
|
|
19005
|
+
const versionSuffix = cliVersion ? ` (mmi-cli ${cliVersion})` : "";
|
|
19006
|
+
if (!input.gaps.length) {
|
|
19007
|
+
return [`\u2713 MMI doctor: all checks passed${versionSuffix}.`, DOCTOR_VERBOSE_HINT];
|
|
19008
|
+
}
|
|
18772
19009
|
const lines = [];
|
|
18773
19010
|
for (const c of input.gaps) {
|
|
18774
19011
|
lines.push(`\u2717 ${c.label}`);
|
|
@@ -18779,20 +19016,51 @@ function renderTerseDoctorReport(input) {
|
|
|
18779
19016
|
lines.push("");
|
|
18780
19017
|
lines.push(...stale);
|
|
18781
19018
|
}
|
|
19019
|
+
lines.push("");
|
|
19020
|
+
lines.push(`\u26A0 ${input.gaps.length} item(s) need attention \u2014 ${DOCTOR_VERBOSE_HINT}`);
|
|
18782
19021
|
return lines;
|
|
18783
19022
|
}
|
|
19023
|
+
var PLUGIN_RESOLVABILITY_LABEL = "MMI plugin resolvability (marketplace + cache present)";
|
|
19024
|
+
function buildPluginResolvabilityCheck(input) {
|
|
19025
|
+
const { state } = buildPluginGuardDecision(input);
|
|
19026
|
+
const surface = input.surface ?? "shell";
|
|
19027
|
+
switch (state) {
|
|
19028
|
+
case "not-org":
|
|
19029
|
+
return { ok: true, label: PLUGIN_RESOLVABILITY_LABEL, fix: "", state };
|
|
19030
|
+
case "healthy":
|
|
19031
|
+
return { ok: true, label: PLUGIN_RESOLVABILITY_LABEL, fix: "", state };
|
|
19032
|
+
case "no-install":
|
|
19033
|
+
return {
|
|
19034
|
+
ok: false,
|
|
19035
|
+
label: PLUGIN_RESOLVABILITY_LABEL,
|
|
19036
|
+
fix: `run: ${PLUGIN_SURFACE_HEAL[surfaceToken(surface) ?? "claude"]?.recovery ?? PLUGIN_SURFACE_HEAL.claude.recovery}`,
|
|
19037
|
+
state
|
|
19038
|
+
};
|
|
19039
|
+
case "unresolved":
|
|
19040
|
+
return {
|
|
19041
|
+
ok: false,
|
|
19042
|
+
label: PLUGIN_RESOLVABILITY_LABEL,
|
|
19043
|
+
fix: "run: mmi-cli plugin-heal",
|
|
19044
|
+
state
|
|
19045
|
+
};
|
|
19046
|
+
default: {
|
|
19047
|
+
const _exhaustive = state;
|
|
19048
|
+
return _exhaustive;
|
|
19049
|
+
}
|
|
19050
|
+
}
|
|
19051
|
+
}
|
|
18784
19052
|
|
|
18785
19053
|
// src/kb-drift-report.ts
|
|
18786
|
-
var
|
|
18787
|
-
var
|
|
19054
|
+
var import_node_fs24 = require("node:fs");
|
|
19055
|
+
var import_node_path22 = require("node:path");
|
|
18788
19056
|
function yesterdayIso() {
|
|
18789
19057
|
const d = /* @__PURE__ */ new Date();
|
|
18790
19058
|
d.setUTCDate(d.getUTCDate() - 1);
|
|
18791
19059
|
return d.toISOString().slice(0, 10);
|
|
18792
19060
|
}
|
|
18793
19061
|
async function fetchLatestKbDriftReport(execFileP5, repoRoot) {
|
|
18794
|
-
const sagaIo = (0,
|
|
18795
|
-
if (!(0,
|
|
19062
|
+
const sagaIo = (0, import_node_path22.join)(repoRoot, "infra", "saga-io.mjs");
|
|
19063
|
+
if (!(0, import_node_fs24.existsSync)(sagaIo)) return null;
|
|
18796
19064
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
18797
19065
|
for (const date of [today, yesterdayIso()]) {
|
|
18798
19066
|
try {
|
|
@@ -18808,9 +19076,9 @@ async function fetchLatestKbDriftReport(execFileP5, repoRoot) {
|
|
|
18808
19076
|
}
|
|
18809
19077
|
|
|
18810
19078
|
// src/cli-doctor-shared.ts
|
|
18811
|
-
var import_node_fs24 = require("node:fs");
|
|
18812
|
-
var import_node_path22 = require("node:path");
|
|
18813
19079
|
var import_node_fs25 = require("node:fs");
|
|
19080
|
+
var import_node_path23 = require("node:path");
|
|
19081
|
+
var import_node_fs26 = require("node:fs");
|
|
18814
19082
|
var GC_GH_TIMEOUT_MS = 2e4;
|
|
18815
19083
|
async function awsCallerArn() {
|
|
18816
19084
|
try {
|
|
@@ -18856,7 +19124,7 @@ async function localBranchHeads() {
|
|
|
18856
19124
|
}
|
|
18857
19125
|
async function currentRepoWorktreeGitRoot(repoRoot) {
|
|
18858
19126
|
const gitCommonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
18859
|
-
return gitCommonDir ? (0,
|
|
19127
|
+
return gitCommonDir ? (0, import_node_path23.resolve)(repoRoot, gitCommonDir, "worktrees") : "";
|
|
18860
19128
|
}
|
|
18861
19129
|
async function worktreeBranches() {
|
|
18862
19130
|
const { stdout } = await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
|
|
@@ -18876,18 +19144,18 @@ function resolveGitdirForWorktreeFile(worktreePath, content) {
|
|
|
18876
19144
|
const match = /^gitdir:\s*(.+)\s*$/im.exec(content);
|
|
18877
19145
|
if (!match?.[1]) return void 0;
|
|
18878
19146
|
const raw = match[1].trim();
|
|
18879
|
-
return (0,
|
|
19147
|
+
return (0, import_node_path23.isAbsolute)(raw) ? raw : (0, import_node_path23.resolve)(worktreePath, raw);
|
|
18880
19148
|
}
|
|
18881
19149
|
function metadataOwnsMissingWorktreeDir(worktreePath, worktreeGitRoot) {
|
|
18882
19150
|
if (!worktreeGitRoot) return false;
|
|
18883
19151
|
try {
|
|
18884
|
-
const entries = (0,
|
|
19152
|
+
const entries = (0, import_node_fs26.readdirSync)(worktreeGitRoot, { withFileTypes: true });
|
|
18885
19153
|
for (const ent of entries) {
|
|
18886
19154
|
if (!ent.isDirectory()) continue;
|
|
18887
19155
|
try {
|
|
18888
|
-
const gitdirPath = (0,
|
|
18889
|
-
const resolvedGitdir = (0,
|
|
18890
|
-
if (sameWorktreeMetadataPath((0,
|
|
19156
|
+
const gitdirPath = (0, import_node_fs25.readFileSync)((0, import_node_path23.join)(worktreeGitRoot, ent.name, "gitdir"), "utf8").trim();
|
|
19157
|
+
const resolvedGitdir = (0, import_node_path23.isAbsolute)(gitdirPath) ? gitdirPath : (0, import_node_path23.resolve)(worktreeGitRoot, ent.name, gitdirPath);
|
|
19158
|
+
if (sameWorktreeMetadataPath((0, import_node_path23.dirname)(resolvedGitdir), worktreePath)) return true;
|
|
18891
19159
|
} catch {
|
|
18892
19160
|
}
|
|
18893
19161
|
}
|
|
@@ -18897,7 +19165,7 @@ function metadataOwnsMissingWorktreeDir(worktreePath, worktreeGitRoot) {
|
|
|
18897
19165
|
}
|
|
18898
19166
|
function pathExistsKnown(path2) {
|
|
18899
19167
|
try {
|
|
18900
|
-
(0,
|
|
19168
|
+
(0, import_node_fs26.statSync)(path2);
|
|
18901
19169
|
return true;
|
|
18902
19170
|
} catch (e) {
|
|
18903
19171
|
const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
|
|
@@ -18906,10 +19174,10 @@ function pathExistsKnown(path2) {
|
|
|
18906
19174
|
}
|
|
18907
19175
|
}
|
|
18908
19176
|
function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
|
|
18909
|
-
const gitPath = (0,
|
|
19177
|
+
const gitPath = (0, import_node_path23.join)(path2, ".git");
|
|
18910
19178
|
let st;
|
|
18911
19179
|
try {
|
|
18912
|
-
st = (0,
|
|
19180
|
+
st = (0, import_node_fs26.lstatSync)(gitPath);
|
|
18913
19181
|
} catch (e) {
|
|
18914
19182
|
const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
|
|
18915
19183
|
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
@@ -18926,7 +19194,7 @@ function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
|
|
|
18926
19194
|
if (st.isDirectory()) return { path: path2, gitType: "dir" };
|
|
18927
19195
|
if (!st.isFile()) return { path: path2, gitType: "other" };
|
|
18928
19196
|
try {
|
|
18929
|
-
const gitFileContent = (0,
|
|
19197
|
+
const gitFileContent = (0, import_node_fs25.readFileSync)(gitPath, "utf8");
|
|
18930
19198
|
const gitdir = resolveGitdirForWorktreeFile(path2, gitFileContent);
|
|
18931
19199
|
const gitDirExists = gitdir ? pathExistsKnown(gitdir) : false;
|
|
18932
19200
|
return {
|
|
@@ -18943,7 +19211,7 @@ function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
|
|
|
18943
19211
|
}
|
|
18944
19212
|
function inspectDeadWorktreeDirContent(path2) {
|
|
18945
19213
|
try {
|
|
18946
|
-
return { entries: (0,
|
|
19214
|
+
return { entries: (0, import_node_fs26.readdirSync)(path2) };
|
|
18947
19215
|
} catch (e) {
|
|
18948
19216
|
const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
|
|
18949
19217
|
return { error: code ? `unable to inspect directory contents (${code})` : "unable to inspect directory contents" };
|
|
@@ -18962,8 +19230,8 @@ async function siblingWorktreeDirs() {
|
|
|
18962
19230
|
const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot);
|
|
18963
19231
|
const siblingRoot = siblingMmiWorktreesRoot(repoRoot);
|
|
18964
19232
|
try {
|
|
18965
|
-
const entries = (0,
|
|
18966
|
-
return entries.filter((ent) => ent.isDirectory()).map((ent) => inspectSiblingWorktreeDir((0,
|
|
19233
|
+
const entries = (0, import_node_fs26.readdirSync)(siblingRoot, { withFileTypes: true });
|
|
19234
|
+
return entries.filter((ent) => ent.isDirectory()).map((ent) => inspectSiblingWorktreeDir((0, import_node_path23.join)(siblingRoot, ent.name), worktreeGitRoot)).filter((entry) => Boolean(entry));
|
|
18967
19235
|
} catch {
|
|
18968
19236
|
return [];
|
|
18969
19237
|
}
|
|
@@ -19013,7 +19281,7 @@ async function fetchHubVersionInfo(baseUrl) {
|
|
|
19013
19281
|
}
|
|
19014
19282
|
function readRepoVersion() {
|
|
19015
19283
|
try {
|
|
19016
|
-
return JSON.parse((0,
|
|
19284
|
+
return JSON.parse((0, import_node_fs27.readFileSync)((0, import_node_path24.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
19017
19285
|
} catch {
|
|
19018
19286
|
return void 0;
|
|
19019
19287
|
}
|
|
@@ -19149,20 +19417,30 @@ async function applyPluginHeal(token, surface, log, opts) {
|
|
|
19149
19417
|
}
|
|
19150
19418
|
var installedPluginsPath = (surface = detectSurface(process.env)) => {
|
|
19151
19419
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
19152
|
-
return (0,
|
|
19420
|
+
return (0, import_node_path24.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
19153
19421
|
};
|
|
19154
|
-
function readInstalledPlugins() {
|
|
19422
|
+
function readInstalledPlugins(surface = detectSurface(process.env)) {
|
|
19155
19423
|
try {
|
|
19156
|
-
return JSON.parse((0,
|
|
19424
|
+
return JSON.parse((0, import_node_fs27.readFileSync)(installedPluginsPath(surface), "utf8"));
|
|
19157
19425
|
} catch {
|
|
19158
19426
|
return null;
|
|
19159
19427
|
}
|
|
19160
19428
|
}
|
|
19429
|
+
function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRepo = false) {
|
|
19430
|
+
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
19431
|
+
const installed = readInstalledPlugins(surface);
|
|
19432
|
+
return {
|
|
19433
|
+
isOrgRepo,
|
|
19434
|
+
installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
|
|
19435
|
+
marketplaceClonePresent: (0, import_node_fs27.existsSync)((0, import_node_path24.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "marketplaces", "mutmutco")),
|
|
19436
|
+
pluginCachePresent: (0, import_node_fs27.existsSync)((0, import_node_path24.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
|
|
19437
|
+
};
|
|
19438
|
+
}
|
|
19161
19439
|
function installedPluginSources() {
|
|
19162
19440
|
return ["claude", "codex"].map((surface) => {
|
|
19163
|
-
const recordPath = (0,
|
|
19441
|
+
const recordPath = (0, import_node_path24.join)((0, import_node_os5.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
|
|
19164
19442
|
try {
|
|
19165
|
-
return { surface, installed: JSON.parse((0,
|
|
19443
|
+
return { surface, installed: JSON.parse((0, import_node_fs27.readFileSync)(recordPath, "utf8")), recordPath };
|
|
19166
19444
|
} catch {
|
|
19167
19445
|
return { surface, installed: null, recordPath };
|
|
19168
19446
|
}
|
|
@@ -19170,7 +19448,7 @@ function installedPluginSources() {
|
|
|
19170
19448
|
}
|
|
19171
19449
|
function readClaudeSettings() {
|
|
19172
19450
|
try {
|
|
19173
|
-
return JSON.parse((0,
|
|
19451
|
+
return JSON.parse((0, import_node_fs27.readFileSync)((0, import_node_path24.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
|
|
19174
19452
|
} catch {
|
|
19175
19453
|
return null;
|
|
19176
19454
|
}
|
|
@@ -19192,7 +19470,7 @@ function writeProjectInstallRecord(record) {
|
|
|
19192
19470
|
const list = file.plugins[MMI_PLUGIN_ID] ?? [];
|
|
19193
19471
|
list.push(record);
|
|
19194
19472
|
file.plugins[MMI_PLUGIN_ID] = list;
|
|
19195
|
-
(0,
|
|
19473
|
+
(0, import_node_fs27.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
|
|
19196
19474
|
`, "utf8");
|
|
19197
19475
|
return true;
|
|
19198
19476
|
} catch {
|
|
@@ -19205,9 +19483,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
19205
19483
|
if (!file) return false;
|
|
19206
19484
|
if (!file.plugins) file.plugins = {};
|
|
19207
19485
|
const path2 = installedPluginsPath();
|
|
19208
|
-
(0,
|
|
19486
|
+
(0, import_node_fs27.copyFileSync)(path2, `${path2}.bak`);
|
|
19209
19487
|
file.plugins[pluginId] = records;
|
|
19210
|
-
(0,
|
|
19488
|
+
(0, import_node_fs27.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
|
|
19211
19489
|
`, "utf8");
|
|
19212
19490
|
return true;
|
|
19213
19491
|
} catch {
|
|
@@ -19215,22 +19493,22 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
|
|
|
19215
19493
|
}
|
|
19216
19494
|
}
|
|
19217
19495
|
function opencodeConfigDir() {
|
|
19218
|
-
return (0,
|
|
19496
|
+
return (0, import_node_path24.join)((0, import_node_os5.homedir)(), ".config", "opencode");
|
|
19219
19497
|
}
|
|
19220
19498
|
function opencodeConfigPath() {
|
|
19221
|
-
return (0,
|
|
19499
|
+
return (0, import_node_path24.join)(opencodeConfigDir(), "opencode.jsonc");
|
|
19222
19500
|
}
|
|
19223
19501
|
function opencodeCommandsDir() {
|
|
19224
|
-
return (0,
|
|
19502
|
+
return (0, import_node_path24.join)(opencodeConfigDir(), "commands");
|
|
19225
19503
|
}
|
|
19226
19504
|
function opencodeSkillsPath() {
|
|
19227
|
-
return (0,
|
|
19505
|
+
return (0, import_node_path24.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "skills");
|
|
19228
19506
|
}
|
|
19229
19507
|
function opencodeConfigSnapshot() {
|
|
19230
19508
|
const path2 = opencodeConfigPath();
|
|
19231
|
-
if (!(0,
|
|
19509
|
+
if (!(0, import_node_fs27.existsSync)(path2)) return { path: path2, hasConfig: false, hasPluginField: false, parseOk: true };
|
|
19232
19510
|
try {
|
|
19233
|
-
const raw = (0,
|
|
19511
|
+
const raw = (0, import_node_fs27.readFileSync)(path2, "utf8");
|
|
19234
19512
|
const parsed = JSON.parse(stripJsonc(raw));
|
|
19235
19513
|
const hasPluginField = Object.prototype.hasOwnProperty.call(parsed, "plugin");
|
|
19236
19514
|
const skillsPaths = Array.isArray(parsed.skills?.paths) ? parsed.skills.paths.filter((p) => typeof p === "string") : void 0;
|
|
@@ -19253,9 +19531,9 @@ function writeOpencodeConfigPlugin(snapshot) {
|
|
|
19253
19531
|
const plan2 = planOpencodeConfigWrite(snapshot.hasConfig ? snapshot.raw : void 0);
|
|
19254
19532
|
if (plan2.action === "already") return true;
|
|
19255
19533
|
if (!plan2.text || plan2.action === "unsafe") return false;
|
|
19256
|
-
(0,
|
|
19257
|
-
if (snapshot.hasConfig) (0,
|
|
19258
|
-
(0,
|
|
19534
|
+
(0, import_node_fs27.mkdirSync)((0, import_node_path24.dirname)(path2), { recursive: true });
|
|
19535
|
+
if (snapshot.hasConfig) (0, import_node_fs27.copyFileSync)(path2, `${path2}.bak`);
|
|
19536
|
+
(0, import_node_fs27.writeFileSync)(path2, plan2.text, "utf8");
|
|
19259
19537
|
return true;
|
|
19260
19538
|
} catch {
|
|
19261
19539
|
return false;
|
|
@@ -19270,9 +19548,9 @@ function writeOpencodeSkillsPath(snapshot, skillsPath) {
|
|
|
19270
19548
|
const normalized = skillsPath.replace(/\\/g, "/");
|
|
19271
19549
|
if (!paths.some((p) => p.replace(/\\/g, "/") === normalized)) paths.push(skillsPath.replace(/\\/g, "/"));
|
|
19272
19550
|
parsed.skills = { ...skills, paths };
|
|
19273
|
-
(0,
|
|
19274
|
-
if (snapshot.hasConfig && (0,
|
|
19275
|
-
(0,
|
|
19551
|
+
(0, import_node_fs27.mkdirSync)((0, import_node_path24.dirname)(snapshot.path), { recursive: true });
|
|
19552
|
+
if (snapshot.hasConfig && (0, import_node_fs27.existsSync)(snapshot.path)) (0, import_node_fs27.copyFileSync)(snapshot.path, `${snapshot.path}.bak`);
|
|
19553
|
+
(0, import_node_fs27.writeFileSync)(snapshot.path, `${JSON.stringify(parsed, null, 2)}
|
|
19276
19554
|
`, "utf8");
|
|
19277
19555
|
return true;
|
|
19278
19556
|
} catch {
|
|
@@ -19281,7 +19559,7 @@ function writeOpencodeSkillsPath(snapshot, skillsPath) {
|
|
|
19281
19559
|
}
|
|
19282
19560
|
function opencodeExistingCommands() {
|
|
19283
19561
|
try {
|
|
19284
|
-
return (0,
|
|
19562
|
+
return (0, import_node_fs27.readdirSync)(opencodeCommandsDir(), { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name.slice(0, -3).toLowerCase());
|
|
19285
19563
|
} catch {
|
|
19286
19564
|
return [];
|
|
19287
19565
|
}
|
|
@@ -19289,9 +19567,9 @@ function opencodeExistingCommands() {
|
|
|
19289
19567
|
function writeOpencodeCommandFiles() {
|
|
19290
19568
|
try {
|
|
19291
19569
|
const dir = opencodeCommandsDir();
|
|
19292
|
-
(0,
|
|
19570
|
+
(0, import_node_fs27.mkdirSync)(dir, { recursive: true });
|
|
19293
19571
|
for (const command of OPENCODE_WORKFLOW_COMMANDS) {
|
|
19294
|
-
(0,
|
|
19572
|
+
(0, import_node_fs27.writeFileSync)((0, import_node_path24.join)(dir, `${command}.md`), opencodeCommandMarkdown(command), "utf8");
|
|
19295
19573
|
}
|
|
19296
19574
|
return true;
|
|
19297
19575
|
} catch {
|
|
@@ -19300,12 +19578,12 @@ function writeOpencodeCommandFiles() {
|
|
|
19300
19578
|
}
|
|
19301
19579
|
function readOpencodeAdapterDiskVersion() {
|
|
19302
19580
|
const candidates = [
|
|
19303
|
-
(0,
|
|
19304
|
-
(0,
|
|
19581
|
+
(0, import_node_path24.join)(opencodeConfigDir(), "node_modules", "@mutmutco", "opencode-mmi", "package.json"),
|
|
19582
|
+
(0, import_node_path24.join)((0, import_node_os5.homedir)(), ".cache", "opencode", "node_modules", "@mutmutco", "opencode-mmi", "package.json")
|
|
19305
19583
|
];
|
|
19306
19584
|
for (const path2 of candidates) {
|
|
19307
19585
|
try {
|
|
19308
|
-
const parsed = JSON.parse((0,
|
|
19586
|
+
const parsed = JSON.parse((0, import_node_fs27.readFileSync)(path2, "utf8"));
|
|
19309
19587
|
if (typeof parsed.version === "string" && parsed.version.trim()) return parsed.version.trim();
|
|
19310
19588
|
} catch {
|
|
19311
19589
|
continue;
|
|
@@ -19321,7 +19599,7 @@ async function forceInstallOpencodeMmiPlugins(snapshot, log) {
|
|
|
19321
19599
|
try {
|
|
19322
19600
|
const specs = opencodeMmiPluginSpecs(snapshot);
|
|
19323
19601
|
log(` \u21BB force-refreshing OpenCode MMI npm plugin(s): ${specs.join(", ")}\u2026`);
|
|
19324
|
-
(0,
|
|
19602
|
+
(0, import_node_fs27.mkdirSync)(opencodeConfigDir(), { recursive: true });
|
|
19325
19603
|
await runHostBin("npm", ["install", "--prefix", opencodeConfigDir(), "--force", ...specs], { timeout: NPM_UPDATE_TIMEOUT_MS });
|
|
19326
19604
|
return true;
|
|
19327
19605
|
} catch {
|
|
@@ -19336,30 +19614,30 @@ function opencodePluginVersionsForReport() {
|
|
|
19336
19614
|
}
|
|
19337
19615
|
function opencodeDesktopLogsRoot() {
|
|
19338
19616
|
if (process.platform === "win32") {
|
|
19339
|
-
const base = process.env.APPDATA || (0,
|
|
19340
|
-
return (0,
|
|
19617
|
+
const base = process.env.APPDATA || (0, import_node_path24.join)((0, import_node_os5.homedir)(), "AppData", "Roaming");
|
|
19618
|
+
return (0, import_node_path24.join)(base, "ai.opencode.desktop", "logs");
|
|
19341
19619
|
}
|
|
19342
19620
|
if (process.platform === "darwin") {
|
|
19343
|
-
return (0,
|
|
19621
|
+
return (0, import_node_path24.join)((0, import_node_os5.homedir)(), "Library", "Application Support", "ai.opencode.desktop", "logs");
|
|
19344
19622
|
}
|
|
19345
|
-
return (0,
|
|
19623
|
+
return (0, import_node_path24.join)((0, import_node_os5.homedir)(), ".config", "ai.opencode.desktop", "logs");
|
|
19346
19624
|
}
|
|
19347
19625
|
function opencodeDesktopBootstrapSnapshot() {
|
|
19348
19626
|
const root = opencodeDesktopLogsRoot();
|
|
19349
19627
|
try {
|
|
19350
|
-
const sessionDirs = (0,
|
|
19628
|
+
const sessionDirs = (0, import_node_fs27.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => (0, import_node_path24.join)(root, entry.name)).sort((a, b) => (0, import_node_fs27.statSync)(b).mtimeMs - (0, import_node_fs27.statSync)(a).mtimeMs);
|
|
19351
19629
|
const newest = sessionDirs[0];
|
|
19352
19630
|
if (!newest) return [];
|
|
19353
|
-
const logPath = (0,
|
|
19354
|
-
const text = (0,
|
|
19355
|
-
return opencodeAgentDirectoriesFromLog(text).filter((directory) => !(0,
|
|
19631
|
+
const logPath = (0, import_node_path24.join)(newest, "renderer.log");
|
|
19632
|
+
const text = (0, import_node_fs27.readFileSync)(logPath, "utf8");
|
|
19633
|
+
return opencodeAgentDirectoriesFromLog(text).filter((directory) => !(0, import_node_fs27.existsSync)(directory)).map((directory) => ({ directory, logPath }));
|
|
19356
19634
|
} catch {
|
|
19357
19635
|
return [];
|
|
19358
19636
|
}
|
|
19359
19637
|
}
|
|
19360
19638
|
function opencodeLegacyConfigSnapshot() {
|
|
19361
|
-
const legacyPath = (0,
|
|
19362
|
-
if (!(0,
|
|
19639
|
+
const legacyPath = (0, import_node_path24.join)((0, import_node_os5.homedir)(), ".opencode", "opencode.json");
|
|
19640
|
+
if (!(0, import_node_fs27.existsSync)(legacyPath)) return {};
|
|
19363
19641
|
const content = readTextFile(legacyPath);
|
|
19364
19642
|
if (content == null) return {};
|
|
19365
19643
|
const plugins = parseOpencodeLegacyConfigPlugins(content);
|
|
@@ -19371,43 +19649,43 @@ function opencodeLegacyConfigSnapshot() {
|
|
|
19371
19649
|
function quarantineOpencodeLegacyConfig(legacyPath) {
|
|
19372
19650
|
try {
|
|
19373
19651
|
const backupPath = `${legacyPath}.bak`;
|
|
19374
|
-
if ((0,
|
|
19375
|
-
(0,
|
|
19652
|
+
if ((0, import_node_fs27.existsSync)(backupPath)) return false;
|
|
19653
|
+
(0, import_node_fs27.renameSync)(legacyPath, backupPath);
|
|
19376
19654
|
return true;
|
|
19377
19655
|
} catch {
|
|
19378
19656
|
return false;
|
|
19379
19657
|
}
|
|
19380
19658
|
}
|
|
19381
19659
|
function cursorPluginCacheRoot() {
|
|
19382
|
-
return (0,
|
|
19660
|
+
return (0, import_node_path24.join)((0, import_node_os5.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
|
|
19383
19661
|
}
|
|
19384
19662
|
function cursorPluginCachePinSnapshots() {
|
|
19385
19663
|
const root = cursorPluginCacheRoot();
|
|
19386
19664
|
try {
|
|
19387
|
-
return (0,
|
|
19388
|
-
const path2 = (0,
|
|
19389
|
-
const pluginJson = (0,
|
|
19390
|
-
const hooksJson = (0,
|
|
19391
|
-
const cliBundle = (0,
|
|
19665
|
+
return (0, import_node_fs27.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
|
|
19666
|
+
const path2 = (0, import_node_path24.join)(root, entry.name);
|
|
19667
|
+
const pluginJson = (0, import_node_path24.join)(path2, ".cursor-plugin", "plugin.json");
|
|
19668
|
+
const hooksJson = (0, import_node_path24.join)(path2, "hooks", "hooks.json");
|
|
19669
|
+
const cliBundle = (0, import_node_path24.join)(path2, "cli", "dist", "index.cjs");
|
|
19392
19670
|
let version;
|
|
19393
19671
|
try {
|
|
19394
|
-
const raw = JSON.parse((0,
|
|
19672
|
+
const raw = JSON.parse((0, import_node_fs27.readFileSync)(pluginJson, "utf8"));
|
|
19395
19673
|
version = typeof raw.version === "string" ? raw.version : void 0;
|
|
19396
19674
|
} catch {
|
|
19397
19675
|
version = void 0;
|
|
19398
19676
|
}
|
|
19399
19677
|
let isEmpty = true;
|
|
19400
19678
|
try {
|
|
19401
|
-
isEmpty = (0,
|
|
19679
|
+
isEmpty = (0, import_node_fs27.readdirSync)(path2).length === 0;
|
|
19402
19680
|
} catch {
|
|
19403
19681
|
isEmpty = true;
|
|
19404
19682
|
}
|
|
19405
19683
|
return {
|
|
19406
19684
|
name: entry.name,
|
|
19407
19685
|
path: path2,
|
|
19408
|
-
hasPluginJson: (0,
|
|
19409
|
-
hasHooksJson: (0,
|
|
19410
|
-
hasCliBundle: (0,
|
|
19686
|
+
hasPluginJson: (0, import_node_fs27.existsSync)(pluginJson),
|
|
19687
|
+
hasHooksJson: (0, import_node_fs27.existsSync)(hooksJson),
|
|
19688
|
+
hasCliBundle: (0, import_node_fs27.existsSync)(cliBundle),
|
|
19411
19689
|
isEmpty,
|
|
19412
19690
|
version
|
|
19413
19691
|
};
|
|
@@ -19417,19 +19695,19 @@ function cursorPluginCachePinSnapshots() {
|
|
|
19417
19695
|
}
|
|
19418
19696
|
}
|
|
19419
19697
|
function hubCheckoutForCursorSeed() {
|
|
19420
|
-
const manifest = (0,
|
|
19421
|
-
return (0,
|
|
19698
|
+
const manifest = (0, import_node_path24.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
|
|
19699
|
+
return (0, import_node_fs27.existsSync)(manifest) ? process.cwd() : void 0;
|
|
19422
19700
|
}
|
|
19423
19701
|
function mmiPluginCacheRootSnapshots() {
|
|
19424
19702
|
const roots = [
|
|
19425
|
-
{ surface: "claude", root: (0,
|
|
19426
|
-
{ surface: "codex", root: (0,
|
|
19703
|
+
{ surface: "claude", root: (0, import_node_path24.join)((0, import_node_os5.homedir)(), ".claude", "plugins", "cache", "mutmutco", "mmi") },
|
|
19704
|
+
{ surface: "codex", root: (0, import_node_path24.join)((0, import_node_os5.homedir)(), ".codex", "plugins", "cache", "mutmutco", "mmi") }
|
|
19427
19705
|
];
|
|
19428
19706
|
return roots.flatMap(({ surface, root }) => {
|
|
19429
19707
|
try {
|
|
19430
|
-
const entries = (0,
|
|
19708
|
+
const entries = (0, import_node_fs27.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
|
|
19431
19709
|
name: entry.name,
|
|
19432
|
-
path: (0,
|
|
19710
|
+
path: (0, import_node_path24.join)(root, entry.name),
|
|
19433
19711
|
isDirectory: entry.isDirectory()
|
|
19434
19712
|
}));
|
|
19435
19713
|
return [{ surface, root, entries }];
|
|
@@ -19440,7 +19718,7 @@ function mmiPluginCacheRootSnapshots() {
|
|
|
19440
19718
|
}
|
|
19441
19719
|
function hasNestedMmiChild(versionDir) {
|
|
19442
19720
|
try {
|
|
19443
|
-
return (0,
|
|
19721
|
+
return (0, import_node_fs27.statSync)((0, import_node_path24.join)(versionDir, "mmi")).isDirectory();
|
|
19444
19722
|
} catch {
|
|
19445
19723
|
return false;
|
|
19446
19724
|
}
|
|
@@ -19451,10 +19729,10 @@ function nestedPluginTreeSnapshot() {
|
|
|
19451
19729
|
);
|
|
19452
19730
|
}
|
|
19453
19731
|
function uniqueQuarantineTarget(path2) {
|
|
19454
|
-
if (!(0,
|
|
19732
|
+
if (!(0, import_node_fs27.existsSync)(path2)) return path2;
|
|
19455
19733
|
for (let i = 1; i < 100; i += 1) {
|
|
19456
19734
|
const candidate = `${path2}-${i}`;
|
|
19457
|
-
if (!(0,
|
|
19735
|
+
if (!(0, import_node_fs27.existsSync)(candidate)) return candidate;
|
|
19458
19736
|
}
|
|
19459
19737
|
return `${path2}-${Date.now()}`;
|
|
19460
19738
|
}
|
|
@@ -19463,10 +19741,10 @@ function quarantinePluginCacheDirs(plan2) {
|
|
|
19463
19741
|
const failed = [];
|
|
19464
19742
|
for (const move of plan2) {
|
|
19465
19743
|
try {
|
|
19466
|
-
if (!(0,
|
|
19744
|
+
if (!(0, import_node_fs27.existsSync)(move.from)) continue;
|
|
19467
19745
|
const target = uniqueQuarantineTarget(move.to);
|
|
19468
|
-
(0,
|
|
19469
|
-
(0,
|
|
19746
|
+
(0, import_node_fs27.mkdirSync)((0, import_node_path24.dirname)(target), { recursive: true });
|
|
19747
|
+
(0, import_node_fs27.renameSync)(move.from, target);
|
|
19470
19748
|
moved += 1;
|
|
19471
19749
|
} catch {
|
|
19472
19750
|
failed.push(move);
|
|
@@ -19485,23 +19763,23 @@ async function robocopyMirrorEmpty(emptyDir, target) {
|
|
|
19485
19763
|
}
|
|
19486
19764
|
async function clearNestedPluginTreeDir(targetPath) {
|
|
19487
19765
|
try {
|
|
19488
|
-
if (!(0,
|
|
19766
|
+
if (!(0, import_node_fs27.existsSync)(targetPath)) return true;
|
|
19489
19767
|
if (isWin) {
|
|
19490
|
-
const emptyDir = (0,
|
|
19491
|
-
(0,
|
|
19768
|
+
const emptyDir = (0, import_node_path24.join)((0, import_node_os5.tmpdir)(), `mmi-empty-${Date.now()}`);
|
|
19769
|
+
(0, import_node_fs27.mkdirSync)(emptyDir, { recursive: true });
|
|
19492
19770
|
try {
|
|
19493
19771
|
await robocopyMirrorEmpty(emptyDir, targetPath);
|
|
19494
|
-
(0,
|
|
19772
|
+
(0, import_node_fs27.rmSync)(targetPath, { recursive: true, force: true });
|
|
19495
19773
|
} finally {
|
|
19496
19774
|
try {
|
|
19497
|
-
(0,
|
|
19775
|
+
(0, import_node_fs27.rmSync)(emptyDir, { recursive: true, force: true });
|
|
19498
19776
|
} catch {
|
|
19499
19777
|
}
|
|
19500
19778
|
}
|
|
19501
|
-
return !(0,
|
|
19779
|
+
return !(0, import_node_fs27.existsSync)(targetPath);
|
|
19502
19780
|
}
|
|
19503
|
-
(0,
|
|
19504
|
-
return !(0,
|
|
19781
|
+
(0, import_node_fs27.rmSync)(targetPath, { recursive: true, force: true });
|
|
19782
|
+
return !(0, import_node_fs27.existsSync)(targetPath);
|
|
19505
19783
|
} catch {
|
|
19506
19784
|
return false;
|
|
19507
19785
|
}
|
|
@@ -19514,11 +19792,11 @@ async function applyNestedPluginTreeCleanup(paths, log) {
|
|
|
19514
19792
|
}
|
|
19515
19793
|
return true;
|
|
19516
19794
|
}
|
|
19517
|
-
var gitignorePath = () => (0,
|
|
19795
|
+
var gitignorePath = () => (0, import_node_path24.join)(process.cwd(), ".gitignore");
|
|
19518
19796
|
function readTextFile(path2) {
|
|
19519
19797
|
try {
|
|
19520
|
-
if (!(0,
|
|
19521
|
-
return (0,
|
|
19798
|
+
if (!(0, import_node_fs27.existsSync)(path2)) return null;
|
|
19799
|
+
return (0, import_node_fs27.readFileSync)(path2, "utf8");
|
|
19522
19800
|
} catch {
|
|
19523
19801
|
return null;
|
|
19524
19802
|
}
|
|
@@ -19527,10 +19805,10 @@ function playwrightMcpConfigSnapshots() {
|
|
|
19527
19805
|
const cwd = process.cwd();
|
|
19528
19806
|
const home = (0, import_node_os5.homedir)();
|
|
19529
19807
|
const candidates = [
|
|
19530
|
-
(0,
|
|
19531
|
-
(0,
|
|
19532
|
-
(0,
|
|
19533
|
-
(0,
|
|
19808
|
+
(0, import_node_path24.join)(cwd, ".mcp.json"),
|
|
19809
|
+
(0, import_node_path24.join)(cwd, ".cursor", "mcp.json"),
|
|
19810
|
+
(0, import_node_path24.join)(home, ".cursor", "mcp.json"),
|
|
19811
|
+
(0, import_node_path24.join)(home, ".codex", "config.toml")
|
|
19534
19812
|
];
|
|
19535
19813
|
const out = [];
|
|
19536
19814
|
for (const path2 of candidates) {
|
|
@@ -19543,7 +19821,7 @@ function strayBrowserArtifactPaths() {
|
|
|
19543
19821
|
const cwd = process.cwd();
|
|
19544
19822
|
return STRAY_BROWSER_ARTIFACT_DIRS.filter((rel) => {
|
|
19545
19823
|
try {
|
|
19546
|
-
return (0,
|
|
19824
|
+
return (0, import_node_fs27.existsSync)((0, import_node_path24.join)(cwd, rel));
|
|
19547
19825
|
} catch {
|
|
19548
19826
|
return false;
|
|
19549
19827
|
}
|
|
@@ -19562,8 +19840,8 @@ function latestIso(values) {
|
|
|
19562
19840
|
return best;
|
|
19563
19841
|
}
|
|
19564
19842
|
function latestNorthstarContinuityAt() {
|
|
19565
|
-
const meta = parseMeta(readTextFile((0,
|
|
19566
|
-
const queue = parseQueue(readTextFile((0,
|
|
19843
|
+
const meta = parseMeta(readTextFile((0, import_node_path24.join)(process.cwd(), META_FILE)));
|
|
19844
|
+
const queue = parseQueue(readTextFile((0, import_node_path24.join)(process.cwd(), QUEUE_FILE)));
|
|
19567
19845
|
return latestIso([
|
|
19568
19846
|
...Object.values(meta).map((entry) => entry.syncedAt),
|
|
19569
19847
|
...queue.map((entry) => entry.queuedAt)
|
|
@@ -19577,14 +19855,14 @@ async function latestBranchWorkAt() {
|
|
|
19577
19855
|
}
|
|
19578
19856
|
function readGitignore() {
|
|
19579
19857
|
try {
|
|
19580
|
-
return (0,
|
|
19858
|
+
return (0, import_node_fs27.readFileSync)(gitignorePath(), "utf8");
|
|
19581
19859
|
} catch {
|
|
19582
19860
|
return null;
|
|
19583
19861
|
}
|
|
19584
19862
|
}
|
|
19585
19863
|
function writeGitignore(content) {
|
|
19586
19864
|
try {
|
|
19587
|
-
(0,
|
|
19865
|
+
(0, import_node_fs27.writeFileSync)(gitignorePath(), content, "utf8");
|
|
19588
19866
|
return true;
|
|
19589
19867
|
} catch {
|
|
19590
19868
|
return false;
|
|
@@ -19623,7 +19901,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19623
19901
|
const semverPrefix = /^\d+\.\d+\.\d+/;
|
|
19624
19902
|
const isBehind = (installed2, released) => Boolean(installed2 && released && semverPrefix.test(installed2) && semverPrefix.test(released) && compareVersions(installed2, released) < 0);
|
|
19625
19903
|
const opencodeAdapterStale = isBehind(opencodeInstalledVersionForDoctor(), releasedVersion);
|
|
19626
|
-
const cursorCacheStale = (0,
|
|
19904
|
+
const cursorCacheStale = (0, import_node_fs27.existsSync)(cursorPluginCacheRoot()) && (cursorPluginCachePinSnapshots() ?? []).some((p) => isBehind(p.version, releasedVersion));
|
|
19627
19905
|
const healPlan = doctorHealPlan({
|
|
19628
19906
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
19629
19907
|
surface,
|
|
@@ -19658,7 +19936,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19658
19936
|
let onPath = pathProbe;
|
|
19659
19937
|
if (!onPath) {
|
|
19660
19938
|
const root = process.env.CLAUDE_PLUGIN_ROOT;
|
|
19661
|
-
if (root && (0,
|
|
19939
|
+
if (root && (0, import_node_fs27.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
|
|
19662
19940
|
}
|
|
19663
19941
|
checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
|
|
19664
19942
|
const reloadHint = reloadAction(surface);
|
|
@@ -19734,7 +20012,6 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19734
20012
|
}
|
|
19735
20013
|
}
|
|
19736
20014
|
checks.push(pluginCheck);
|
|
19737
|
-
checks.push(buildSettingsPluginDriftCheck({ isOrgRepo: Boolean(cfg.sagaApiUrl), settings: claudeSettings }));
|
|
19738
20015
|
let legacyPluginCheck = buildLegacyPluginInstallCheck({
|
|
19739
20016
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
19740
20017
|
sources: installedPluginSources(),
|
|
@@ -19791,6 +20068,18 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19791
20068
|
}
|
|
19792
20069
|
}
|
|
19793
20070
|
checks.push(driftCheck);
|
|
20071
|
+
checks.push(
|
|
20072
|
+
buildPluginResolvabilityCheck({
|
|
20073
|
+
...snapshotPluginGuardInput(surface, Boolean(cfg.sagaApiUrl)),
|
|
20074
|
+
surface
|
|
20075
|
+
})
|
|
20076
|
+
);
|
|
20077
|
+
if (repairFull && Boolean(cfg.sagaApiUrl) && (surfaceToken(surface) === "claude" || surfaceToken(surface) === "codex")) {
|
|
20078
|
+
const guardResult = ensureUserScopeGuardHook();
|
|
20079
|
+
if (guardResult === "written") {
|
|
20080
|
+
io.err(` \u21BB installed user-scope MMI guard hook (${surface === "codex" ? "~/.codex" : "~/.claude"}/settings.json) \u2014 survives a plugin prune`);
|
|
20081
|
+
}
|
|
20082
|
+
}
|
|
19794
20083
|
let installedVersionCheck = buildInstalledPluginVersionCheck({
|
|
19795
20084
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
19796
20085
|
sources: installedPluginSources(),
|
|
@@ -19985,7 +20274,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19985
20274
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
19986
20275
|
surface,
|
|
19987
20276
|
cacheRoot: cursorCacheRoot,
|
|
19988
|
-
cacheRootExists: (0,
|
|
20277
|
+
cacheRootExists: (0, import_node_fs27.existsSync)(cursorCacheRoot),
|
|
19989
20278
|
pins: cursorPins,
|
|
19990
20279
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
19991
20280
|
releasedVersion
|
|
@@ -19996,7 +20285,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19996
20285
|
releasedVersion,
|
|
19997
20286
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
19998
20287
|
execFileP: execFileP2,
|
|
19999
|
-
mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0,
|
|
20288
|
+
mkdtemp: (prefix) => (0, import_promises7.mkdtemp)((0, import_node_path24.join)((0, import_node_os5.tmpdir)(), prefix)),
|
|
20000
20289
|
log: (m) => io.err(m)
|
|
20001
20290
|
});
|
|
20002
20291
|
if (seeded) {
|
|
@@ -20005,7 +20294,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
20005
20294
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
20006
20295
|
surface,
|
|
20007
20296
|
cacheRoot: cursorCacheRoot,
|
|
20008
|
-
cacheRootExists: (0,
|
|
20297
|
+
cacheRootExists: (0, import_node_fs27.existsSync)(cursorCacheRoot),
|
|
20009
20298
|
pins: cursorPins,
|
|
20010
20299
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
20011
20300
|
releasedVersion
|
|
@@ -20175,6 +20464,79 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
20175
20464
|
io.log(gaps.length ? `
|
|
20176
20465
|
${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
|
|
20177
20466
|
}
|
|
20467
|
+
var USER_SCOPE_GUARD_MARKER = "mmi-guard:v1";
|
|
20468
|
+
var USER_SCOPE_GUARD_COMMAND = `mmi-cli guard --session-start || true # ${USER_SCOPE_GUARD_MARKER}`;
|
|
20469
|
+
function settingsHasGuardHook(settings) {
|
|
20470
|
+
const groups = settings?.hooks?.SessionStart;
|
|
20471
|
+
if (!Array.isArray(groups)) return false;
|
|
20472
|
+
return groups.some(
|
|
20473
|
+
(g) => Array.isArray(g?.hooks) && g.hooks.some((h) => typeof h?.command === "string" && h.command.includes(USER_SCOPE_GUARD_MARKER))
|
|
20474
|
+
);
|
|
20475
|
+
}
|
|
20476
|
+
function mergeGuardHook(settings) {
|
|
20477
|
+
const next = settings && typeof settings === "object" ? { ...settings } : {};
|
|
20478
|
+
if (settingsHasGuardHook(next)) return next;
|
|
20479
|
+
const hooks = { ...next.hooks ?? {} };
|
|
20480
|
+
const sessionStart = Array.isArray(hooks.SessionStart) ? [...hooks.SessionStart] : [];
|
|
20481
|
+
sessionStart.push({ hooks: [{ type: "command", command: USER_SCOPE_GUARD_COMMAND, timeout: 10 }] });
|
|
20482
|
+
hooks.SessionStart = sessionStart;
|
|
20483
|
+
next.hooks = hooks;
|
|
20484
|
+
return next;
|
|
20485
|
+
}
|
|
20486
|
+
var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path24.join)((0, import_node_os5.homedir)(), surface === "codex" ? ".codex" : ".claude", "settings.json");
|
|
20487
|
+
function ensureUserScopeGuardHook(opts = {}) {
|
|
20488
|
+
const path2 = opts.settingsPath ?? userScopeSettingsPath();
|
|
20489
|
+
try {
|
|
20490
|
+
let current = null;
|
|
20491
|
+
if ((0, import_node_fs27.existsSync)(path2)) {
|
|
20492
|
+
try {
|
|
20493
|
+
current = JSON.parse((0, import_node_fs27.readFileSync)(path2, "utf8"));
|
|
20494
|
+
} catch {
|
|
20495
|
+
return "failed";
|
|
20496
|
+
}
|
|
20497
|
+
}
|
|
20498
|
+
if (settingsHasGuardHook(current)) return "already";
|
|
20499
|
+
const merged = mergeGuardHook(current);
|
|
20500
|
+
(0, import_node_fs27.mkdirSync)((0, import_node_path24.dirname)(path2), { recursive: true });
|
|
20501
|
+
if ((0, import_node_fs27.existsSync)(path2)) (0, import_node_fs27.copyFileSync)(path2, `${path2}.bak`);
|
|
20502
|
+
(0, import_node_fs27.writeFileSync)(path2, `${JSON.stringify(merged, null, 2)}
|
|
20503
|
+
`, "utf8");
|
|
20504
|
+
return "written";
|
|
20505
|
+
} catch {
|
|
20506
|
+
return "failed";
|
|
20507
|
+
}
|
|
20508
|
+
}
|
|
20509
|
+
async function runGuard(opts = {}) {
|
|
20510
|
+
void opts;
|
|
20511
|
+
try {
|
|
20512
|
+
const surface = detectSurface(process.env);
|
|
20513
|
+
const cfg = await loadConfig();
|
|
20514
|
+
const input = snapshotPluginGuardInput(surface, Boolean(cfg.sagaApiUrl));
|
|
20515
|
+
const { state } = buildPluginGuardDecision(input);
|
|
20516
|
+
const { line, exitCode } = buildGuardSessionStartLine(state);
|
|
20517
|
+
if (line) console.error(line);
|
|
20518
|
+
process.exitCode = exitCode;
|
|
20519
|
+
} catch {
|
|
20520
|
+
process.exitCode = 0;
|
|
20521
|
+
}
|
|
20522
|
+
}
|
|
20523
|
+
async function runPluginHeal(surface = detectSurface(process.env)) {
|
|
20524
|
+
const token = surface === "codex" ? "codex" : "claude";
|
|
20525
|
+
const tableKey = surfaceToken(surface) ?? "claude";
|
|
20526
|
+
const descriptor = PLUGIN_SURFACE_HEAL[tableKey];
|
|
20527
|
+
if (!descriptor.healSteps) {
|
|
20528
|
+
console.log(descriptor.recovery);
|
|
20529
|
+
console.log(`Then: ${reloadAction(surface)}`);
|
|
20530
|
+
return;
|
|
20531
|
+
}
|
|
20532
|
+
const healed = await applyPluginHeal(token, surface, console.log, { force: true });
|
|
20533
|
+
if (healed) {
|
|
20534
|
+
console.log(` \u2713 MMI plugin reinstalled \u2014 ${reloadAction(surface)} to load MMI commands`);
|
|
20535
|
+
} else {
|
|
20536
|
+
console.log(` \u2717 Auto-heal failed or was skipped. Run manually:
|
|
20537
|
+
${descriptor.recovery}`);
|
|
20538
|
+
}
|
|
20539
|
+
}
|
|
20178
20540
|
|
|
20179
20541
|
// src/index.ts
|
|
20180
20542
|
var GC_GH_TIMEOUT_MS2 = 2e4;
|
|
@@ -20267,7 +20629,7 @@ async function applyGcPlan(plan2, remote) {
|
|
|
20267
20629
|
cleanupBranch: (branch, expectedHeadOid) => cleanupPrMergeLocalBranch(branch.branch, {
|
|
20268
20630
|
beforeWorktrees,
|
|
20269
20631
|
startingPath: branch.worktreePath,
|
|
20270
|
-
pathExists: (p) => (0,
|
|
20632
|
+
pathExists: (p) => (0, import_node_fs28.existsSync)(p),
|
|
20271
20633
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
20272
20634
|
teardownWorktreeStage,
|
|
20273
20635
|
deferredStore,
|
|
@@ -20290,7 +20652,7 @@ async function applyGcPlan(plan2, remote) {
|
|
|
20290
20652
|
for (const wt of plan2.worktreeDirs) {
|
|
20291
20653
|
try {
|
|
20292
20654
|
const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
|
|
20293
|
-
realpath: (path2) => (0,
|
|
20655
|
+
realpath: (path2) => (0, import_node_fs28.realpathSync)(path2)
|
|
20294
20656
|
});
|
|
20295
20657
|
if (!cleanupTarget.ok) {
|
|
20296
20658
|
result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
|
|
@@ -20370,10 +20732,10 @@ async function runRulesSync(opts, io = consoleIo) {
|
|
|
20370
20732
|
for (const entry of fetched) {
|
|
20371
20733
|
if ("error" in entry) continue;
|
|
20372
20734
|
const { file, source } = entry;
|
|
20373
|
-
const current = (0,
|
|
20735
|
+
const current = (0, import_node_fs28.existsSync)(file) ? await (0, import_promises8.readFile)(file, "utf8") : null;
|
|
20374
20736
|
if (needsUpdate(source, current)) {
|
|
20375
20737
|
const slash = file.lastIndexOf("/");
|
|
20376
|
-
if (slash > 0) (0,
|
|
20738
|
+
if (slash > 0) (0, import_node_fs28.mkdirSync)(file.slice(0, slash), { recursive: true });
|
|
20377
20739
|
await (0, import_promises8.writeFile)(file, normalizeEol(source), "utf8");
|
|
20378
20740
|
changed++;
|
|
20379
20741
|
if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
|
|
@@ -20387,13 +20749,13 @@ rules.command("sync").option("--quiet", "stay silent unless something changed or
|
|
|
20387
20749
|
if (!await runRulesSync(opts)) process.exitCode = 1;
|
|
20388
20750
|
});
|
|
20389
20751
|
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) => {
|
|
20390
|
-
const path2 = (0,
|
|
20391
|
-
const current = (0,
|
|
20752
|
+
const path2 = (0, import_node_path25.join)(process.cwd(), ".gitignore");
|
|
20753
|
+
const current = (0, import_node_fs28.existsSync)(path2) ? (0, import_node_fs28.readFileSync)(path2, "utf8") : null;
|
|
20392
20754
|
const plan2 = planManagedGitignore(current);
|
|
20393
20755
|
const drift = [...plan2.added.map((l) => `+${l}`), ...plan2.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
|
|
20394
20756
|
if (opts.write) {
|
|
20395
20757
|
if (plan2.changed) {
|
|
20396
|
-
(0,
|
|
20758
|
+
(0, import_node_fs28.writeFileSync)(path2, plan2.content, "utf8");
|
|
20397
20759
|
console.log(`mmi-cli rules gitignore: updated .gitignore (${drift})`);
|
|
20398
20760
|
} else {
|
|
20399
20761
|
console.log("mmi-cli rules gitignore: up to date");
|
|
@@ -20420,7 +20782,7 @@ async function runDocsSync(opts, io = consoleIo) {
|
|
|
20420
20782
|
return null;
|
|
20421
20783
|
}
|
|
20422
20784
|
},
|
|
20423
|
-
localContent: async (f) => (0,
|
|
20785
|
+
localContent: async (f) => (0, import_node_fs28.existsSync)(f) ? await (0, import_promises8.readFile)(f, "utf8") : null,
|
|
20424
20786
|
writeDoc: async (f, c) => {
|
|
20425
20787
|
await (0, import_promises8.writeFile)(f, c, "utf8");
|
|
20426
20788
|
}
|
|
@@ -20571,7 +20933,7 @@ function runWorktreeInstall(command, cwd, quiet) {
|
|
|
20571
20933
|
const [bin, ...args] = command.split(" ");
|
|
20572
20934
|
const file = isWin2 ? "cmd.exe" : bin;
|
|
20573
20935
|
const spawnArgs = isWin2 ? ["/c", bin, ...args] : args;
|
|
20574
|
-
return new Promise((
|
|
20936
|
+
return new Promise((resolve6, reject) => {
|
|
20575
20937
|
const child = (0, import_node_child_process13.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
|
|
20576
20938
|
const timer = setTimeout(() => {
|
|
20577
20939
|
try {
|
|
@@ -20586,7 +20948,7 @@ function runWorktreeInstall(command, cwd, quiet) {
|
|
|
20586
20948
|
});
|
|
20587
20949
|
child.on("exit", (code) => {
|
|
20588
20950
|
clearTimeout(timer);
|
|
20589
|
-
if (code === 0)
|
|
20951
|
+
if (code === 0) resolve6();
|
|
20590
20952
|
else reject(new Error(`${command} exited ${code} in ${cwd}`));
|
|
20591
20953
|
});
|
|
20592
20954
|
});
|
|
@@ -20594,7 +20956,7 @@ function runWorktreeInstall(command, cwd, quiet) {
|
|
|
20594
20956
|
async function primaryCheckoutRoot(worktreeRoot) {
|
|
20595
20957
|
try {
|
|
20596
20958
|
const out = (await execFileP2("git", ["-C", worktreeRoot, "rev-parse", "--path-format=absolute", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
|
|
20597
|
-
return out ? (0,
|
|
20959
|
+
return out ? (0, import_node_path25.dirname)(out) : void 0;
|
|
20598
20960
|
} catch {
|
|
20599
20961
|
return void 0;
|
|
20600
20962
|
}
|
|
@@ -20607,28 +20969,28 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
|
|
|
20607
20969
|
};
|
|
20608
20970
|
}
|
|
20609
20971
|
function acquireWorktreeSetupLock(worktreeRoot) {
|
|
20610
|
-
const lockPath = (0,
|
|
20972
|
+
const lockPath = (0, import_node_path25.join)(worktreeRoot, ".mmi", "worktree-setup.lock");
|
|
20611
20973
|
const take = () => {
|
|
20612
|
-
const fd = (0,
|
|
20974
|
+
const fd = (0, import_node_fs28.openSync)(lockPath, "wx");
|
|
20613
20975
|
try {
|
|
20614
|
-
(0,
|
|
20976
|
+
(0, import_node_fs28.writeSync)(fd, String(Date.now()));
|
|
20615
20977
|
} finally {
|
|
20616
|
-
(0,
|
|
20978
|
+
(0, import_node_fs28.closeSync)(fd);
|
|
20617
20979
|
}
|
|
20618
20980
|
return () => {
|
|
20619
20981
|
try {
|
|
20620
|
-
(0,
|
|
20982
|
+
(0, import_node_fs28.rmSync)(lockPath, { force: true });
|
|
20621
20983
|
} catch {
|
|
20622
20984
|
}
|
|
20623
20985
|
};
|
|
20624
20986
|
};
|
|
20625
20987
|
try {
|
|
20626
|
-
(0,
|
|
20988
|
+
(0, import_node_fs28.mkdirSync)((0, import_node_path25.dirname)(lockPath), { recursive: true });
|
|
20627
20989
|
return take();
|
|
20628
20990
|
} catch {
|
|
20629
20991
|
try {
|
|
20630
|
-
if (Date.now() - (0,
|
|
20631
|
-
(0,
|
|
20992
|
+
if (Date.now() - (0, import_node_fs28.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
|
|
20993
|
+
(0, import_node_fs28.rmSync)(lockPath, { force: true });
|
|
20632
20994
|
return take();
|
|
20633
20995
|
}
|
|
20634
20996
|
} catch {
|
|
@@ -20637,18 +20999,21 @@ function acquireWorktreeSetupLock(worktreeRoot) {
|
|
|
20637
20999
|
}
|
|
20638
21000
|
}
|
|
20639
21001
|
var worktree = program2.command("worktree").description("self-provisioning worktrees \u2014 install deps + copy local-only config");
|
|
20640
|
-
worktree.command("create <branch>").description("create a worktree from a base ref and provision it (install deps + copy local-only config)").option("--from <ref>", "base ref to branch from", "origin/development").option("--path <path>", "worktree path (default: ../mmi-worktrees/<branch>)").option("--remote <name>", "remote to fetch
|
|
21002
|
+
worktree.command("create <branch>").description("create a worktree from a base ref and provision it (install deps + copy local-only config)").option("--from <ref>", "base ref to branch from", "origin/development").option("--path <path>", "worktree path (default: ../mmi-worktrees/<branch>)").option("--remote <name>", "remote to fetch when --from is a <remote>/<branch> ref", "origin").option("--json", "machine-readable output").action(async (branch, o) => {
|
|
20641
21003
|
try {
|
|
20642
21004
|
const repoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
20643
21005
|
const wtPath = o.path ?? defaultWorktreePath(repoRoot, branch);
|
|
20644
|
-
const
|
|
20645
|
-
if (
|
|
20646
|
-
|
|
21006
|
+
const { base, fetchBranch } = resolveWorktreeBase(o.from, o.remote);
|
|
21007
|
+
if (fetchBranch) {
|
|
21008
|
+
const fetchErr = await execFileP2("git", ["fetch", o.remote, fetchBranch], { timeout: GH_MUTATION_TIMEOUT_MS }).then(() => void 0).catch((e) => (e instanceof Error ? e.message : String(e)).split("\n")[0]);
|
|
21009
|
+
if (fetchErr) console.error(` warning: could not fetch ${o.remote}/${fetchBranch} (${fetchErr}); base ${base} may be stale`);
|
|
21010
|
+
}
|
|
21011
|
+
await execFileP2("git", ["worktree", "add", wtPath, "-b", branch, base], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
20647
21012
|
const report = await provisionWorktree(wtPath, makeProvisionDeps(wtPath, Boolean(o.json), (m) => {
|
|
20648
21013
|
if (!o.json) console.error(` ${m}`);
|
|
20649
21014
|
}));
|
|
20650
|
-
if (o.json) return console.log(JSON.stringify({ branch, path: wtPath, base
|
|
20651
|
-
console.log(`worktree ready: ${wtPath} (branch ${branch} from ${
|
|
21015
|
+
if (o.json) return console.log(JSON.stringify({ branch, path: wtPath, base, ...report }, null, 2));
|
|
21016
|
+
console.log(`worktree ready: ${wtPath} (branch ${branch} from ${base})`);
|
|
20652
21017
|
console.log(` installed: ${report.installed.map((i) => i.dir || ".").join(", ") || "none"}`);
|
|
20653
21018
|
console.log(` copied: ${report.copied.join(", ") || "none"}`);
|
|
20654
21019
|
} catch (e) {
|
|
@@ -20873,8 +21238,8 @@ tenant.command("sweep-rc").description("discover (and optionally retire) running
|
|
|
20873
21238
|
async function resolveDnsBounded(host, timeoutMs = 3e3) {
|
|
20874
21239
|
const { lookup } = await import("node:dns/promises");
|
|
20875
21240
|
const probe = lookup(host).then(() => true).catch((e) => dnsErrorToResolution(e?.code));
|
|
20876
|
-
const timeout = new Promise((
|
|
20877
|
-
setTimeout(() =>
|
|
21241
|
+
const timeout = new Promise((resolve6) => {
|
|
21242
|
+
setTimeout(() => resolve6(void 0), timeoutMs).unref?.();
|
|
20878
21243
|
});
|
|
20879
21244
|
return Promise.race([probe, timeout]);
|
|
20880
21245
|
}
|
|
@@ -21629,9 +21994,9 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
|
|
|
21629
21994
|
console.log(JSON.stringify(created));
|
|
21630
21995
|
});
|
|
21631
21996
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
21632
|
-
const wfDir = (0,
|
|
21633
|
-
if (!(0,
|
|
21634
|
-
return (0,
|
|
21997
|
+
const wfDir = (0, import_node_path25.join)(cwd, ".github", "workflows");
|
|
21998
|
+
if (!(0, import_node_fs28.existsSync)(wfDir)) return [];
|
|
21999
|
+
return (0, import_node_fs28.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
|
|
21635
22000
|
}
|
|
21636
22001
|
async function resolveMergeCiPolicyForCheckout(repoOpt) {
|
|
21637
22002
|
const repo = repoOpt ?? await resolveRepo();
|
|
@@ -21650,7 +22015,7 @@ function ciAuditDeps() {
|
|
|
21650
22015
|
// Continuous CI delivery (#1550): the gate re-seed renders from the Hub's on-disk seed templates. The
|
|
21651
22016
|
// reconcile runs IN the Hub checkout, so this is local-file I/O (no network fetch). Path is relative to
|
|
21652
22017
|
// the repo root (e.g. skills/bootstrap/seeds/gate.template.yml).
|
|
21653
|
-
readSeedFile: (path2) => (0,
|
|
22018
|
+
readSeedFile: (path2) => (0, import_node_fs28.existsSync)(path2) ? (0, import_node_fs28.readFileSync)(path2, "utf8") : null
|
|
21654
22019
|
};
|
|
21655
22020
|
}
|
|
21656
22021
|
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) => {
|
|
@@ -21680,7 +22045,7 @@ pr.command("checks-wait <number>").description("bounded wait for PR checks; skip
|
|
|
21680
22045
|
const result = await waitForPrChecks({
|
|
21681
22046
|
resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
|
|
21682
22047
|
pollChecks: () => pollGhPrChecks(number, repoArgs),
|
|
21683
|
-
sleep: (ms) => new Promise((
|
|
22048
|
+
sleep: (ms) => new Promise((resolve6) => setTimeout(resolve6, ms))
|
|
21684
22049
|
});
|
|
21685
22050
|
if (o.json) printLine(JSON.stringify(result));
|
|
21686
22051
|
else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
|
|
@@ -21707,7 +22072,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
21707
22072
|
waitForChecks: (prNumber, repo) => waitForPrChecks({
|
|
21708
22073
|
resolvePolicy: () => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
|
|
21709
22074
|
pollChecks: () => pollGhPrChecks(prNumber, repo ? ["--repo", repo] : []),
|
|
21710
|
-
sleep: (ms) => new Promise((
|
|
22075
|
+
sleep: (ms) => new Promise((resolve6) => setTimeout(resolve6, ms))
|
|
21711
22076
|
}),
|
|
21712
22077
|
mergeAuto: async (prNumber, repo) => {
|
|
21713
22078
|
const args = repo ? ["--repo", repo] : [];
|
|
@@ -21743,7 +22108,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
21743
22108
|
while (Date.now() < deadlineMs) {
|
|
21744
22109
|
const stateRead = await readGhPrStateWithRetry(async () => (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS2 })).stdout, { retries: 2, delayMs: 1e3 });
|
|
21745
22110
|
if (stateRead.ok && stateRead.state === "MERGED") return true;
|
|
21746
|
-
await new Promise((
|
|
22111
|
+
await new Promise((resolve6) => setTimeout(resolve6, PR_LAND_POLL_MS));
|
|
21747
22112
|
}
|
|
21748
22113
|
return false;
|
|
21749
22114
|
}
|
|
@@ -21803,7 +22168,7 @@ async function createDeferredWorktreeStore() {
|
|
|
21803
22168
|
},
|
|
21804
22169
|
write: async (entries) => {
|
|
21805
22170
|
try {
|
|
21806
|
-
await (0, import_promises8.mkdir)((0,
|
|
22171
|
+
await (0, import_promises8.mkdir)((0, import_node_path25.dirname)(registryPath), { recursive: true });
|
|
21807
22172
|
await (0, import_promises8.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
|
|
21808
22173
|
} catch {
|
|
21809
22174
|
}
|
|
@@ -21817,13 +22182,13 @@ var realWorktreeDirRemover = {
|
|
|
21817
22182
|
probe: (p) => {
|
|
21818
22183
|
let st;
|
|
21819
22184
|
try {
|
|
21820
|
-
st = (0,
|
|
22185
|
+
st = (0, import_node_fs28.lstatSync)(p);
|
|
21821
22186
|
} catch {
|
|
21822
22187
|
return null;
|
|
21823
22188
|
}
|
|
21824
22189
|
if (st.isSymbolicLink()) return "link";
|
|
21825
22190
|
try {
|
|
21826
|
-
(0,
|
|
22191
|
+
(0, import_node_fs28.readlinkSync)(p);
|
|
21827
22192
|
return "link";
|
|
21828
22193
|
} catch {
|
|
21829
22194
|
}
|
|
@@ -21831,7 +22196,7 @@ var realWorktreeDirRemover = {
|
|
|
21831
22196
|
},
|
|
21832
22197
|
readdir: (p) => {
|
|
21833
22198
|
try {
|
|
21834
|
-
return (0,
|
|
22199
|
+
return (0, import_node_fs28.readdirSync)(p);
|
|
21835
22200
|
} catch {
|
|
21836
22201
|
return [];
|
|
21837
22202
|
}
|
|
@@ -21840,9 +22205,9 @@ var realWorktreeDirRemover = {
|
|
|
21840
22205
|
// leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
|
|
21841
22206
|
detachLink: (p) => {
|
|
21842
22207
|
try {
|
|
21843
|
-
(0,
|
|
22208
|
+
(0, import_node_fs28.rmdirSync)(p);
|
|
21844
22209
|
} catch {
|
|
21845
|
-
(0,
|
|
22210
|
+
(0, import_node_fs28.unlinkSync)(p);
|
|
21846
22211
|
}
|
|
21847
22212
|
},
|
|
21848
22213
|
removeTree: (p) => (0, import_promises8.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
|
@@ -21857,7 +22222,7 @@ async function resolvePrimaryCheckout(execGit) {
|
|
|
21857
22222
|
function worktreeRemoveDeps(execGit) {
|
|
21858
22223
|
return {
|
|
21859
22224
|
git: execGit,
|
|
21860
|
-
sleep: (ms) => new Promise((
|
|
22225
|
+
sleep: (ms) => new Promise((resolve6) => setTimeout(resolve6, ms)),
|
|
21861
22226
|
removeWorktreeDir: async (worktreePath) => removeWorktreeTree(worktreePath, await resolvePrimaryCheckout(execGit), realWorktreeDirRemover)
|
|
21862
22227
|
};
|
|
21863
22228
|
}
|
|
@@ -21872,9 +22237,9 @@ async function worktreeHasStageState(worktreePath) {
|
|
|
21872
22237
|
}
|
|
21873
22238
|
}
|
|
21874
22239
|
function stageStateFileBelongsToWorktree(statePath, worktreePath) {
|
|
21875
|
-
if (!(0,
|
|
22240
|
+
if (!(0, import_node_fs28.existsSync)(statePath)) return false;
|
|
21876
22241
|
try {
|
|
21877
|
-
const state = JSON.parse((0,
|
|
22242
|
+
const state = JSON.parse((0, import_node_fs28.readFileSync)(statePath, "utf8"));
|
|
21878
22243
|
const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
|
|
21879
22244
|
return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
|
|
21880
22245
|
} catch {
|
|
@@ -21947,7 +22312,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
21947
22312
|
} : await cleanupPrMergeLocalBranch(headRef, {
|
|
21948
22313
|
beforeWorktrees,
|
|
21949
22314
|
startingPath,
|
|
21950
|
-
pathExists: (p) => (0,
|
|
22315
|
+
pathExists: (p) => (0, import_node_fs28.existsSync)(p),
|
|
21951
22316
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
21952
22317
|
teardownWorktreeStage,
|
|
21953
22318
|
deferredStore,
|
|
@@ -22144,7 +22509,7 @@ function stageScopedRunOpts(o) {
|
|
|
22144
22509
|
};
|
|
22145
22510
|
}
|
|
22146
22511
|
function printLine(value) {
|
|
22147
|
-
(0,
|
|
22512
|
+
(0, import_node_fs28.writeSync)(1, `${value}
|
|
22148
22513
|
`);
|
|
22149
22514
|
}
|
|
22150
22515
|
function stageKeepAlive() {
|
|
@@ -22161,8 +22526,8 @@ async function resolveStage() {
|
|
|
22161
22526
|
local,
|
|
22162
22527
|
shell: shellFor(),
|
|
22163
22528
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
22164
|
-
hasCompose: (0,
|
|
22165
|
-
hasEnvExample: (0,
|
|
22529
|
+
hasCompose: (0, import_node_fs28.existsSync)((0, import_node_path25.join)(process.cwd(), "docker-compose.yml")),
|
|
22530
|
+
hasEnvExample: (0, import_node_fs28.existsSync)((0, import_node_path25.join)(process.cwd(), ".env.example"))
|
|
22166
22531
|
});
|
|
22167
22532
|
}
|
|
22168
22533
|
async function fetchStageVaultEnvMerge() {
|
|
@@ -22261,9 +22626,17 @@ async function runStageLiveCommand(o) {
|
|
|
22261
22626
|
if (o.json) return console.log(JSON.stringify({ command: "stage --live", mode, slug: target.slug, repo: target.repo, ref: o.down ? void 0 : target.ref, steps }, null, 2));
|
|
22262
22627
|
return console.log(renderSteps(`mmi-cli stage --live${o.down ? " --down" : ""}: dry-run plan`, steps));
|
|
22263
22628
|
}
|
|
22629
|
+
const rcDeps = registryClientDeps(await loadConfig());
|
|
22264
22630
|
const deps = {
|
|
22265
22631
|
detectIp: () => detectPublicIp(),
|
|
22266
|
-
|
|
22632
|
+
deployDev: async ({ repo, ref }) => {
|
|
22633
|
+
const res = await tenantDeploy({ repo, stage: "dev", ref }, rcDeps);
|
|
22634
|
+
if (!res.ok) throw new Error(`dev deploy dispatch failed: ${res.body?.error ?? res.error ?? `HTTP ${res.status}`}`);
|
|
22635
|
+
},
|
|
22636
|
+
control: async ({ repo, action, host, ip }) => {
|
|
22637
|
+
const res = await tenantControl({ repo, stage: "dev", action, host, ip }, rcDeps);
|
|
22638
|
+
if (!res.ok) throw new Error(`tenant control ${action} dispatch failed: ${res.body?.error ?? res.error ?? `HTTP ${res.status}`}`);
|
|
22639
|
+
}
|
|
22267
22640
|
};
|
|
22268
22641
|
try {
|
|
22269
22642
|
const result = o.down ? await runStageLiveDown(deps, target) : await runStageLiveUp(deps, target);
|
|
@@ -22616,7 +22989,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
22616
22989
|
client: defaultGitHubClient(),
|
|
22617
22990
|
projectMeta: meta,
|
|
22618
22991
|
deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
|
|
22619
|
-
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0,
|
|
22992
|
+
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs28.existsSync)(path2) ? (0, import_node_fs28.readFileSync)(path2, "utf8") : null,
|
|
22620
22993
|
// requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
|
|
22621
22994
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
22622
22995
|
requiredGcpApis: (() => {
|
|
@@ -22660,12 +23033,12 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
22660
23033
|
return fail(`bootstrap apply: ${e.message}`);
|
|
22661
23034
|
}
|
|
22662
23035
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
22663
|
-
if (!(0,
|
|
22664
|
-
const manifest = loadBootstrapSeeds((0,
|
|
23036
|
+
if (!(0, import_node_fs28.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
|
|
23037
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs28.readFileSync)(manifestPath, "utf8"));
|
|
22665
23038
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
22666
23039
|
const slug = parsedRepo.slug;
|
|
22667
23040
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
22668
|
-
const readFile7 = (p) => (0,
|
|
23041
|
+
const readFile7 = (p) => (0, import_node_fs28.existsSync)(p) ? (0, import_node_fs28.readFileSync)(p, "utf8") : null;
|
|
22669
23042
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
22670
23043
|
const rawVars = {};
|
|
22671
23044
|
for (const value of rawValues("--var")) {
|
|
@@ -22916,16 +23289,16 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
22916
23289
|
if (o.class !== "deployable" && o.class !== "content") return failGraceful("access audit: --class must be deployable or content");
|
|
22917
23290
|
targets = [{ repo: o.repo, class: o.class }];
|
|
22918
23291
|
} else {
|
|
22919
|
-
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0,
|
|
23292
|
+
const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs28.existsSync)("projects.json") ? (0, import_node_fs28.readFileSync)("projects.json", "utf8") : null;
|
|
22920
23293
|
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>");
|
|
22921
|
-
const fanoutJson = (0,
|
|
23294
|
+
const fanoutJson = (0, import_node_fs28.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs28.readFileSync)(".github/fanout-targets.json", "utf8") : null;
|
|
22922
23295
|
targets = loadAccessTargets(projectsJson, fanoutJson);
|
|
22923
23296
|
}
|
|
22924
23297
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
22925
|
-
const fileMatrix = (0,
|
|
23298
|
+
const fileMatrix = (0, import_node_fs28.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs28.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
22926
23299
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
22927
23300
|
const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
22928
|
-
const fileContracts = (0,
|
|
23301
|
+
const fileContracts = (0, import_node_fs28.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs28.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
|
|
22929
23302
|
const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
|
|
22930
23303
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
|
|
22931
23304
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
@@ -23012,6 +23385,8 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
|
|
|
23012
23385
|
// Commander maps `--no-repo-writes` to `repoWrites: false`; translate to the explicit `noRepoWrites` flag.
|
|
23013
23386
|
runDoctor({ ...opts, noRepoWrites: opts.repoWrites === false })
|
|
23014
23387
|
));
|
|
23388
|
+
program2.command("guard").description("detect a pruned/unresolved MMI plugin on disk; loud one-line stderr at session start").option("--session-start", "run in user-scope SessionStart mode").action((opts) => runGuard({ sessionStart: opts.sessionStart }));
|
|
23389
|
+
program2.command("plugin-heal").description("reinstall + re-enable the MMI plugin (recover from a marketplace prune)").action(() => runPluginHeal());
|
|
23015
23390
|
program2.command("session-start").description("run the SessionStart verbs (rules sync, saga session+show, saga health, whoami, doctor, plan-store check) in one process; docs sync runs detached").action(async () => {
|
|
23016
23391
|
if (isInsideRepoSubdir(process.cwd())) {
|
|
23017
23392
|
console.error("[mmi-hook] session-start: cwd is a repository SUBDIRECTORY \u2014 skipping the SessionStart hook (spine/docs/plan/saga delivery); run it from the repo root.");
|