@mutmutco/cli 2.52.0 → 2.53.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 +312 -100
- package/dist/saga.cjs +16 -8
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -5174,12 +5174,13 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
|
|
|
5174
5174
|
const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
5175
5175
|
const autoRunSince = (deps.now ?? Date.now)();
|
|
5176
5176
|
const deployDispatch = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report", meta.publishDir);
|
|
5177
|
-
const publishDispatch = deployDispatch.deployStatus === "
|
|
5177
|
+
const publishDispatch = deployDispatch.deployStatus === "success" ? await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, watch, "report") : null;
|
|
5178
5178
|
let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
|
|
5179
|
-
if (!publishDispatch && deployDispatch.deployStatus
|
|
5179
|
+
if (!publishDispatch && deployDispatch.deployStatus !== "success" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
|
|
5180
|
+
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
5181
|
dispatch = {
|
|
5181
5182
|
...dispatch,
|
|
5182
|
-
note: `${dispatch.note}; tenant-publish.yml skipped (
|
|
5183
|
+
note: `${dispatch.note}; tenant-publish.yml skipped (${reason})`
|
|
5183
5184
|
};
|
|
5184
5185
|
}
|
|
5185
5186
|
return { checks, releaseUrl, announceNote, dispatch };
|
|
@@ -6084,18 +6085,23 @@ function headPrompt(state) {
|
|
|
6084
6085
|
const decisions = shownDecisions(state.decisions);
|
|
6085
6086
|
const actions = (state.actionLog ?? []).slice(-HEAD_PROMPT_ACTION_LIMIT);
|
|
6086
6087
|
return [
|
|
6087
|
-
"You maintain
|
|
6088
|
-
"
|
|
6089
|
-
"
|
|
6090
|
-
"
|
|
6091
|
-
"
|
|
6092
|
-
"
|
|
6088
|
+
"You maintain two durable slots of a work-session: PINNED (things worth remembering) and a NEXT",
|
|
6089
|
+
"SUGGESTION (a best-effort one-line hint for the next useful step). Given the CURRENT HEAD and the",
|
|
6090
|
+
"recent TRANSCRIPT + DECISIONS, return an updated PINNED and, optionally, a next suggestion. Keep",
|
|
6091
|
+
"PINNED tight and concrete; keep anything the user pinned; never invent; preserve Turkish characters",
|
|
6092
|
+
"(\xE7 \u011F \u0131 \u0130 \xF6 \u015F \xFC) exactly. Do NOT manage the checklist \u2014 the note path owns that. The ANCHOR is the",
|
|
6093
|
+
"read-only North-Star \u2014 NEVER change it. Never restate an unverified artifact-claim (a named file,",
|
|
6094
|
+
"PR, flag, or board state) as settled fact \u2014 keep it as the belief it was recorded as.",
|
|
6093
6095
|
"You MAY also propose supersessions: each DECISION is shown with its original stable 0-based index. Propose a",
|
|
6094
6096
|
"supersession ONLY for a NEWER decision that directly contradicts/replaces an OLDER one where neither",
|
|
6095
6097
|
"already carries a supersededBy. HIGH PRECISION \u2014 propose ONLY when you are confident the older claim",
|
|
6096
6098
|
"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
|
-
'
|
|
6099
|
+
"older only, never the reverse). When unsure, propose nothing. NEVER touch the anchor or checklist.",
|
|
6100
|
+
'For "next": propose a single concise actionable line (\u2264140 chars) describing the most useful next step',
|
|
6101
|
+
"for a future session, derived from the transcript. This is a best-effort suggestion shown only when",
|
|
6102
|
+
"the user has not set their own NEXT. Use an empty string if nothing concrete emerges \u2014 never invent.",
|
|
6103
|
+
"Preserve Turkish characters exactly.",
|
|
6104
|
+
'Output ONLY a JSON object: {"pinned":[string],"next":string,"supersede":[{"older":int,"newer":int,"reason":string}]}.',
|
|
6099
6105
|
"",
|
|
6100
6106
|
"CURRENT HEAD:",
|
|
6101
6107
|
JSON.stringify(state.head ?? {}, null, 2),
|
|
@@ -6119,6 +6125,9 @@ function parseHeadUpdate(raw) {
|
|
|
6119
6125
|
if (!obj || typeof obj !== "object") return null;
|
|
6120
6126
|
const u = {};
|
|
6121
6127
|
if (Array.isArray(obj.pinned)) u.pinned = obj.pinned.filter((x) => typeof x === "string");
|
|
6128
|
+
if (typeof obj.next === "string" && obj.next.trim()) {
|
|
6129
|
+
u.nextAuto = obj.next.trim().slice(0, 280);
|
|
6130
|
+
}
|
|
6122
6131
|
if (Array.isArray(obj.supersede)) {
|
|
6123
6132
|
const supersede = obj.supersede.filter((e) => {
|
|
6124
6133
|
if (!e || typeof e !== "object") return false;
|
|
@@ -7365,6 +7374,18 @@ function writeState(path2, state) {
|
|
|
7365
7374
|
mkdirFor(path2);
|
|
7366
7375
|
(0, import_node_fs9.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
|
|
7367
7376
|
}
|
|
7377
|
+
function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
|
|
7378
|
+
const reservation = {
|
|
7379
|
+
pid: 0,
|
|
7380
|
+
command: "",
|
|
7381
|
+
cwd,
|
|
7382
|
+
statePath,
|
|
7383
|
+
startedAt: now().toISOString(),
|
|
7384
|
+
port
|
|
7385
|
+
};
|
|
7386
|
+
writeState(statePath, reservation);
|
|
7387
|
+
if (globalStatePath && globalStatePath !== statePath) writeState(globalStatePath, reservation);
|
|
7388
|
+
}
|
|
7368
7389
|
async function cleanupStageState(state, paths, timeoutMs, fallbackCwd) {
|
|
7369
7390
|
await killTree(state.pid);
|
|
7370
7391
|
if (state.teardown?.command.trim()) {
|
|
@@ -7505,6 +7526,8 @@ async function runStage(config = {}, opts = {}) {
|
|
|
7505
7526
|
if (problems.length) throw new Error(problems.join("; "));
|
|
7506
7527
|
const cwd = opts.cwd ?? process.cwd();
|
|
7507
7528
|
const timeoutMs = opts.timeoutMs ?? 6e4;
|
|
7529
|
+
const statePath = opts.statePath ?? stageStatePath(cwd);
|
|
7530
|
+
const globalStatePath = await resolveGlobalStatePath(cwd, opts.globalStatePath);
|
|
7508
7531
|
const portGuard = resolveStagePortGuard(opts);
|
|
7509
7532
|
await stopStage({ ...opts, cwd, requiredIdentityCwd: opts.requiredIdentityCwd ?? cwd });
|
|
7510
7533
|
const reserved = await reservedPortsForWorktree(cwd);
|
|
@@ -7515,11 +7538,20 @@ async function runStage(config = {}, opts = {}) {
|
|
|
7515
7538
|
stagePort = await resolveStagePort(config, portGuard, reserved);
|
|
7516
7539
|
}
|
|
7517
7540
|
const sub = (s) => substituteStagePort(s, stagePort);
|
|
7518
|
-
|
|
7541
|
+
if (stagePort != null) {
|
|
7542
|
+
writeStagePortReservation(stagePort, cwd, statePath, globalStatePath, opts.now ?? (() => /* @__PURE__ */ new Date()));
|
|
7543
|
+
}
|
|
7519
7544
|
const extraEnv = stageExtraEnv(config, stagePort);
|
|
7520
7545
|
const build2 = config.build?.trim();
|
|
7521
7546
|
const ranBuild = Boolean(build2);
|
|
7522
|
-
|
|
7547
|
+
try {
|
|
7548
|
+
await ensureStageRuntimeEnv(config, opts, cwd);
|
|
7549
|
+
if (build2) await shell(sub(build2), cwd, timeoutMs, stageProcessEnv(stagePort, extraEnv));
|
|
7550
|
+
} catch (e) {
|
|
7551
|
+
(0, import_node_fs9.rmSync)(statePath, { force: true });
|
|
7552
|
+
if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs9.rmSync)(globalStatePath, { force: true });
|
|
7553
|
+
throw e;
|
|
7554
|
+
}
|
|
7523
7555
|
const started = await startStage(config, {
|
|
7524
7556
|
...opts,
|
|
7525
7557
|
cwd,
|
|
@@ -8519,15 +8551,28 @@ function planDirtyForAutosave(localHash, meta, project2, slug, queue) {
|
|
|
8519
8551
|
if (metaEntry?.hash === localHash) return false;
|
|
8520
8552
|
return true;
|
|
8521
8553
|
}
|
|
8522
|
-
function
|
|
8554
|
+
function resolveAutosaveProject(meta, queue, defaultProject, slug) {
|
|
8555
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
8556
|
+
const suffix = `/${slug}`;
|
|
8557
|
+
for (const key of Object.keys(meta)) {
|
|
8558
|
+
if (key.endsWith(suffix)) candidates.add(key.slice(0, -suffix.length));
|
|
8559
|
+
}
|
|
8560
|
+
for (const entry of queue) {
|
|
8561
|
+
if (entry.slug === slug) candidates.add(entry.project);
|
|
8562
|
+
}
|
|
8563
|
+
if (candidates.size === 1) return [...candidates][0];
|
|
8564
|
+
return defaultProject;
|
|
8565
|
+
}
|
|
8566
|
+
function plansNeedingAutosave(deps, project2, slugs) {
|
|
8523
8567
|
const meta = parseMeta(deps.readMetaRaw());
|
|
8524
8568
|
const queue = parseQueue(deps.readQueueRaw());
|
|
8525
8569
|
const out = [];
|
|
8526
8570
|
for (const slug of slugs) {
|
|
8527
8571
|
const raw = deps.readLocal(slug);
|
|
8528
8572
|
if (raw == null) continue;
|
|
8573
|
+
const autosaveProject = resolveAutosaveProject(meta, queue, project2, slug);
|
|
8529
8574
|
const hash = hashContent(normalizeEol(raw));
|
|
8530
|
-
if (planDirtyForAutosave(hash, meta,
|
|
8575
|
+
if (planDirtyForAutosave(hash, meta, autosaveProject, slug, queue)) out.push({ project: autosaveProject, slug });
|
|
8531
8576
|
}
|
|
8532
8577
|
return out;
|
|
8533
8578
|
}
|
|
@@ -8555,11 +8600,11 @@ async function planAutoEnqueueDirty(deps, opts = {}) {
|
|
|
8555
8600
|
}
|
|
8556
8601
|
}
|
|
8557
8602
|
const enqueued = [];
|
|
8558
|
-
for (const
|
|
8559
|
-
const raw = deps.readLocal(slug);
|
|
8603
|
+
for (const entry of plansNeedingAutosave(deps, project2, slugs)) {
|
|
8604
|
+
const raw = deps.readLocal(entry.slug);
|
|
8560
8605
|
if (raw == null) continue;
|
|
8561
|
-
enqueuePlanPush(deps, { project:
|
|
8562
|
-
enqueued.push(slug);
|
|
8606
|
+
enqueuePlanPush(deps, { project: entry.project, slug: entry.slug, hash: hashContent(normalizeEol(raw)) }, { detach: false });
|
|
8607
|
+
enqueued.push(entry.slug);
|
|
8563
8608
|
}
|
|
8564
8609
|
if (enqueued.length) deps.detachSync();
|
|
8565
8610
|
return enqueued;
|
|
@@ -11210,6 +11255,11 @@ function defaultWorktreePath(repoRoot, branch) {
|
|
|
11210
11255
|
const safe = branch.replace(/[/\\]+/g, "-");
|
|
11211
11256
|
return (0, import_node_path15.join)((0, import_node_path15.dirname)(repoRoot), "mmi-worktrees", safe);
|
|
11212
11257
|
}
|
|
11258
|
+
function resolveWorktreeBase(from, remote) {
|
|
11259
|
+
const remotePrefix = `${remote}/`;
|
|
11260
|
+
const fetchBranch = from.startsWith(remotePrefix) ? from.slice(remotePrefix.length) : void 0;
|
|
11261
|
+
return { base: from, fetchBranch };
|
|
11262
|
+
}
|
|
11213
11263
|
|
|
11214
11264
|
// src/northstar-context.ts
|
|
11215
11265
|
var SESSION_START_NORTHSTAR_TIMEOUT_MS = 3e3;
|
|
@@ -13621,35 +13671,18 @@ async function detectPublicIp(fetchImpl = fetch) {
|
|
|
13621
13671
|
if (!validStageLiveIp(ip)) throw new Error(`public IP detection returned a non-IP body from ${IP_ECHO_URL}: "${ip.slice(0, 80)}"`);
|
|
13622
13672
|
return ip;
|
|
13623
13673
|
}
|
|
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
13674
|
function stageLiveUpSteps(t) {
|
|
13630
13675
|
return [
|
|
13631
13676
|
{ 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
|
-
},
|
|
13677
|
+
{ label: `deploy ${t.ref ?? "<current branch>"} to the ${t.slug} dev stage via the Hub backend (tenant-deploy)` },
|
|
13678
|
+
{ label: `gate ${t.host} to your IP at the Cloudflare edge via the Hub backend (tenant-control cf-gate-allow)` },
|
|
13640
13679
|
{ label: "tear down when done", command: "mmi-cli stage --live --down --apply" }
|
|
13641
13680
|
];
|
|
13642
13681
|
}
|
|
13643
13682
|
function stageLiveDownSteps(t) {
|
|
13644
13683
|
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
|
-
}
|
|
13684
|
+
{ label: `stop the ${t.slug} dev runtime via the Hub backend (tenant-control stop)` },
|
|
13685
|
+
{ label: `remove the Cloudflare edge gate for ${t.host} via the Hub backend (tenant-control cf-gate-clear)` }
|
|
13653
13686
|
];
|
|
13654
13687
|
}
|
|
13655
13688
|
async function runStageLiveUp(deps, t) {
|
|
@@ -13657,8 +13690,8 @@ async function runStageLiveUp(deps, t) {
|
|
|
13657
13690
|
const ip = (await deps.detectIp()).trim();
|
|
13658
13691
|
if (!validStageLiveIp(ip)) throw new Error(`stage --live: detected public IP is not a literal IPv4/IPv6 address: "${ip.slice(0, 80)}"`);
|
|
13659
13692
|
if (!t.host?.trim()) throw new Error("stage --live: cannot resolve the dev edge host (registry edgeDomains.dev)");
|
|
13660
|
-
await deps.
|
|
13661
|
-
await deps.
|
|
13693
|
+
await deps.deployDev({ repo: t.repo, ref: t.ref });
|
|
13694
|
+
await deps.control({ repo: t.repo, action: "cf-gate-allow", host: t.host, ip });
|
|
13662
13695
|
return {
|
|
13663
13696
|
command: "stage --live",
|
|
13664
13697
|
mode: "up",
|
|
@@ -13672,8 +13705,8 @@ async function runStageLiveUp(deps, t) {
|
|
|
13672
13705
|
}
|
|
13673
13706
|
async function runStageLiveDown(deps, t) {
|
|
13674
13707
|
if (!t.host?.trim()) throw new Error("stage --live: cannot resolve the dev edge host (registry edgeDomains.dev)");
|
|
13675
|
-
await deps.
|
|
13676
|
-
await deps.
|
|
13708
|
+
await deps.control({ repo: t.repo, action: "stop" });
|
|
13709
|
+
await deps.control({ repo: t.repo, action: "cf-gate-clear", host: t.host });
|
|
13677
13710
|
return {
|
|
13678
13711
|
command: "stage --live",
|
|
13679
13712
|
mode: "down",
|
|
@@ -14400,12 +14433,13 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
14400
14433
|
"report",
|
|
14401
14434
|
meta.publishDir
|
|
14402
14435
|
);
|
|
14403
|
-
const publish = deploy.deployStatus === "
|
|
14436
|
+
const publish = deploy.deployStatus === "success" ? await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, true, "report") : null;
|
|
14404
14437
|
let dispatch = appendPublishDispatch(deploy, publish);
|
|
14405
|
-
if (!publish && deploy.deployStatus
|
|
14438
|
+
if (!publish && deploy.deployStatus !== "success" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
|
|
14439
|
+
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
14440
|
dispatch = {
|
|
14407
14441
|
...dispatch,
|
|
14408
|
-
note: `${dispatch.note}; tenant-publish.yml skipped (
|
|
14442
|
+
note: `${dispatch.note}; tenant-publish.yml skipped (${reason})`
|
|
14409
14443
|
};
|
|
14410
14444
|
}
|
|
14411
14445
|
deployNote = dispatch.note;
|
|
@@ -16082,7 +16116,7 @@ ${section}`.trim();
|
|
|
16082
16116
|
}
|
|
16083
16117
|
|
|
16084
16118
|
// src/project-set.ts
|
|
16085
|
-
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dashboard", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate"];
|
|
16119
|
+
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "dashboard", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate"];
|
|
16086
16120
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
16087
16121
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
16088
16122
|
function parseRuntimeSecretsVar(raw) {
|
|
@@ -16268,6 +16302,16 @@ function parsePublishDirVar(raw) {
|
|
|
16268
16302
|
}
|
|
16269
16303
|
return v;
|
|
16270
16304
|
}
|
|
16305
|
+
function parseDsManifestPathVar(raw) {
|
|
16306
|
+
const v = raw.trim();
|
|
16307
|
+
if (v === "" || !/^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/.test(v) || /(^|\/)\.\.(\/|$)/.test(v)) {
|
|
16308
|
+
throw new Error('project set: dsManifestPath must be a safe relative path \u2014 no leading slash, no ".." segment');
|
|
16309
|
+
}
|
|
16310
|
+
if (v !== "package.json" && !v.endsWith("/package.json")) {
|
|
16311
|
+
throw new Error("project set: dsManifestPath must point to a package.json, e.g. web/package.json");
|
|
16312
|
+
}
|
|
16313
|
+
return v;
|
|
16314
|
+
}
|
|
16271
16315
|
function parseRequiredChecksVar(raw) {
|
|
16272
16316
|
let parsed;
|
|
16273
16317
|
try {
|
|
@@ -16319,6 +16363,7 @@ var SETTABLE_VAR_KEYS = [
|
|
|
16319
16363
|
"oauth",
|
|
16320
16364
|
"publishRequired",
|
|
16321
16365
|
"publishDir",
|
|
16366
|
+
"dsManifestPath",
|
|
16322
16367
|
"dashboard",
|
|
16323
16368
|
"fofuEnabled",
|
|
16324
16369
|
"consumesDesignSystem",
|
|
@@ -16339,6 +16384,7 @@ var SETTABLE_VAR_HINTS = {
|
|
|
16339
16384
|
projectNumber: "numeric",
|
|
16340
16385
|
publishRequired: "true|false",
|
|
16341
16386
|
publishDir: "relative subpath, e.g. packages/ui",
|
|
16387
|
+
dsManifestPath: "relative path to a package.json, e.g. web/package.json",
|
|
16342
16388
|
dashboard: "true|false",
|
|
16343
16389
|
fofuEnabled: "true|false",
|
|
16344
16390
|
consumesDesignSystem: '"fofu"',
|
|
@@ -16423,6 +16469,8 @@ function buildProjectSetPatch(input) {
|
|
|
16423
16469
|
patch[key] = parseConsumesDesignSystemVar(raw);
|
|
16424
16470
|
} else if (key === "publishDir") {
|
|
16425
16471
|
patch[key] = parsePublishDirVar(raw);
|
|
16472
|
+
} else if (key === "dsManifestPath") {
|
|
16473
|
+
patch[key] = parseDsManifestPathVar(raw);
|
|
16426
16474
|
} else if (key === "ci") {
|
|
16427
16475
|
if (raw !== "none") throw new Error('project set: ci must be "none" (or use --unset ci to require checks)');
|
|
16428
16476
|
patch[key] = raw;
|
|
@@ -17584,6 +17632,23 @@ var import_promises7 = require("node:fs/promises");
|
|
|
17584
17632
|
var import_node_path23 = require("node:path");
|
|
17585
17633
|
var import_node_os5 = require("node:os");
|
|
17586
17634
|
|
|
17635
|
+
// src/plugin-guard.ts
|
|
17636
|
+
function buildPluginGuardDecision(i) {
|
|
17637
|
+
if (!i.isOrgRepo) return { state: "not-org" };
|
|
17638
|
+
if (!i.installRecordPresent) return { state: "no-install" };
|
|
17639
|
+
if (!i.marketplaceClonePresent || !i.pluginCachePresent) return { state: "unresolved" };
|
|
17640
|
+
return { state: "healthy" };
|
|
17641
|
+
}
|
|
17642
|
+
function buildGuardSessionStartLine(state, opts = {}) {
|
|
17643
|
+
if (state === "healthy" || state === "not-org") return { exitCode: 0 };
|
|
17644
|
+
const recovery = opts.recovery ?? "mmi-cli plugin-heal";
|
|
17645
|
+
const reason = state === "no-install" ? "MMI plugin is not installed for this user/session" : "MMI plugin is installed but its marketplace/cache is unresolved";
|
|
17646
|
+
return {
|
|
17647
|
+
line: `[mmi-guard] ${reason}; run ${recovery} and restart Claude Code / reload plugins.`,
|
|
17648
|
+
exitCode: 1
|
|
17649
|
+
};
|
|
17650
|
+
}
|
|
17651
|
+
|
|
17587
17652
|
// src/cursor-plugin-seed.ts
|
|
17588
17653
|
var import_node_child_process12 = require("node:child_process");
|
|
17589
17654
|
var import_node_fs22 = require("node:fs");
|
|
@@ -17775,20 +17840,6 @@ function pluginInstallManualFix(projectPath, surface = "claude-cli") {
|
|
|
17775
17840
|
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}\``;
|
|
17776
17841
|
return `run ${register} then ${reloadAction(surface)} to register the install record for ${projectPath}`;
|
|
17777
17842
|
}
|
|
17778
|
-
function isMmiPluginEnabled(settings) {
|
|
17779
|
-
return Boolean(settings?.enabledPlugins?.[MMI_PLUGIN_ID]);
|
|
17780
|
-
}
|
|
17781
|
-
function buildSettingsPluginDriftCheck(input) {
|
|
17782
|
-
const base = {
|
|
17783
|
-
ok: true,
|
|
17784
|
-
label: "org plugin wiring in .claude/settings.json (mmi@mutmutco)",
|
|
17785
|
-
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"
|
|
17786
|
-
};
|
|
17787
|
-
const enabled = input.settings?.enabledPlugins;
|
|
17788
|
-
if (!input.isOrgRepo || !enabled || Object.keys(enabled).length === 0) return base;
|
|
17789
|
-
if (!enabled[MMI_PLUGIN_ID]) return { ...base, ok: false };
|
|
17790
|
-
return base;
|
|
17791
|
-
}
|
|
17792
17843
|
function hasProjectInstallRecord(file, pluginId, projectPath) {
|
|
17793
17844
|
const records = file?.plugins?.[pluginId];
|
|
17794
17845
|
if (!Array.isArray(records)) return false;
|
|
@@ -17811,7 +17862,7 @@ function buildPluginInstallRecordCheck(input) {
|
|
|
17811
17862
|
fix: pluginInstallManualFix(input.projectPath, input.surface),
|
|
17812
17863
|
pluginId
|
|
17813
17864
|
};
|
|
17814
|
-
if (!input.isOrgRepo
|
|
17865
|
+
if (!input.isOrgRepo) return base;
|
|
17815
17866
|
if (hasAnyPluginRecords(input.installed, LEGACY_MMI_PLUGIN_ID) && !hasAnyPluginRecords(input.installed, pluginId)) {
|
|
17816
17867
|
return {
|
|
17817
17868
|
...base,
|
|
@@ -18072,8 +18123,9 @@ function reloadAction(surface) {
|
|
|
18072
18123
|
return "restart Claude Code (or run /reload-plugins)";
|
|
18073
18124
|
}
|
|
18074
18125
|
}
|
|
18075
|
-
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`;
|
|
18126
|
+
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`;
|
|
18076
18127
|
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`;
|
|
18128
|
+
var CURSOR_RECOVERY = "in Cursor Dashboard \u2192 Settings \u2192 Plugins, click Update next to the MMI Team Marketplace";
|
|
18077
18129
|
var OPENCODE_PLUGIN_PACKAGE = "@mutmutco/opencode-mmi";
|
|
18078
18130
|
var OPENCODE_PLUGIN_SPEC = `${OPENCODE_PLUGIN_PACKAGE}@latest`;
|
|
18079
18131
|
var OPENCODE_PLUGIN_INSTALL_COMMAND = `mmi-cli doctor --apply`;
|
|
@@ -18122,7 +18174,10 @@ var PLUGIN_SURFACE_HEAL = {
|
|
|
18122
18174
|
healSteps: [
|
|
18123
18175
|
{ args: ["plugin", "marketplace", "remove", LEGACY_MMI_MARKETPLACE], gated: false },
|
|
18124
18176
|
{ args: ["plugin", "marketplace", "remove", "mutmutco"], gated: false },
|
|
18125
|
-
|
|
18177
|
+
// Pin --ref main: the marketplace clone must track `main` (the released branch the plugin pins in
|
|
18178
|
+
// .claude-plugin/marketplace.json source.ref), removing the dev-vs-main skew that triggers the prune
|
|
18179
|
+
// and doubles fetch exposure. Mirrors the existing codex entry (#2038).
|
|
18180
|
+
{ args: ["plugin", "marketplace", "add", "mutmutco/MMI-Hub", "--ref", "main"], gated: true },
|
|
18126
18181
|
{ args: ["plugin", "install", "mmi@mutmutco"], gated: true },
|
|
18127
18182
|
{ args: ["plugin", "enable", "mmi@mutmutco"], gated: false }
|
|
18128
18183
|
],
|
|
@@ -18145,10 +18200,10 @@ var PLUGIN_SURFACE_HEAL = {
|
|
|
18145
18200
|
},
|
|
18146
18201
|
cursor: {
|
|
18147
18202
|
delivery: "cursor-cache",
|
|
18148
|
-
recovery:
|
|
18203
|
+
recovery: CURSOR_RECOVERY,
|
|
18149
18204
|
healSteps: null,
|
|
18150
|
-
fix: (surface) =>
|
|
18151
|
-
updateRecipe: []
|
|
18205
|
+
fix: (surface) => `${CURSOR_RECOVERY}; then ${reloadAction(surface)} to reload MMI skills + hooks`,
|
|
18206
|
+
updateRecipe: [CURSOR_RECOVERY]
|
|
18152
18207
|
},
|
|
18153
18208
|
opencode: {
|
|
18154
18209
|
delivery: "npm",
|
|
@@ -18189,9 +18244,21 @@ function surfaceToken(surface) {
|
|
|
18189
18244
|
var PLUGIN_UPDATE_RECIPES = {
|
|
18190
18245
|
claude: PLUGIN_SURFACE_HEAL.claude.updateRecipe,
|
|
18191
18246
|
codex: PLUGIN_SURFACE_HEAL.codex.updateRecipe,
|
|
18247
|
+
cursor: PLUGIN_SURFACE_HEAL.cursor.updateRecipe,
|
|
18192
18248
|
opencode: PLUGIN_SURFACE_HEAL.opencode.updateRecipe,
|
|
18193
18249
|
cli: ["npm install -g @mutmutco/cli@latest"]
|
|
18194
18250
|
};
|
|
18251
|
+
var PLUGIN_GUIDE_SURFACES = [
|
|
18252
|
+
{ key: "cli", label: "npm CLI", versionKeys: ["cli"] },
|
|
18253
|
+
{ key: "claude", label: "Claude Code", versionKeys: ["claudePlugin"] },
|
|
18254
|
+
{ key: "codex", label: "Codex", versionKeys: ["codexMarketplace", "codexActiveCache"] },
|
|
18255
|
+
{ key: "cursor", label: "Cursor", versionKeys: [] },
|
|
18256
|
+
{ key: "opencode", label: "OpenCode", versionKeys: ["opencodePlugin"] }
|
|
18257
|
+
];
|
|
18258
|
+
function renderSurfaceGuide(label, steps) {
|
|
18259
|
+
if (!steps.length) return [];
|
|
18260
|
+
return [` ${label}`, ...steps.map((step) => ` ${step}`)];
|
|
18261
|
+
}
|
|
18195
18262
|
function highestSemver(versions) {
|
|
18196
18263
|
return versions.reduce((best, v) => {
|
|
18197
18264
|
if (!isSemverVersion2(v)) return best;
|
|
@@ -18219,20 +18286,24 @@ function buildPluginUpdateReport(input) {
|
|
|
18219
18286
|
function renderPluginUpdateReport(report) {
|
|
18220
18287
|
const v = report.versions;
|
|
18221
18288
|
const show = (x) => x ?? "unknown";
|
|
18222
|
-
|
|
18223
|
-
"
|
|
18224
|
-
|
|
18225
|
-
|
|
18226
|
-
|
|
18227
|
-
|
|
18228
|
-
|
|
18229
|
-
|
|
18230
|
-
|
|
18231
|
-
`
|
|
18232
|
-
`
|
|
18233
|
-
|
|
18234
|
-
|
|
18289
|
+
const versionRows = [
|
|
18290
|
+
["mmi-cli", show(v.cli)],
|
|
18291
|
+
["Claude plugin", show(v.claudePlugin)],
|
|
18292
|
+
["Codex marketplace", show(v.codexMarketplace)],
|
|
18293
|
+
["Codex active cache", show(v.codexActiveCache)],
|
|
18294
|
+
["OpenCode plugin", show(v.opencodePlugin)]
|
|
18295
|
+
];
|
|
18296
|
+
const pad = Math.max(...versionRows.map(([label]) => label.length));
|
|
18297
|
+
const lines = [
|
|
18298
|
+
`MMI versions (target release: ${show(v.released)})`,
|
|
18299
|
+
...versionRows.map(([label, value]) => ` ${label.padEnd(pad)} ${value}`),
|
|
18300
|
+
"",
|
|
18301
|
+
"Update commands by surface"
|
|
18235
18302
|
];
|
|
18303
|
+
for (const surface of PLUGIN_GUIDE_SURFACES) {
|
|
18304
|
+
lines.push(...renderSurfaceGuide(surface.label, report.recipes[surface.key]));
|
|
18305
|
+
}
|
|
18306
|
+
return lines;
|
|
18236
18307
|
}
|
|
18237
18308
|
function buildDoctorJsonPayload(input) {
|
|
18238
18309
|
return {
|
|
@@ -18743,18 +18814,21 @@ function renderPluginUpdateReportStaleOnly(report) {
|
|
|
18743
18814
|
const released = v.released;
|
|
18744
18815
|
if (!released) return [];
|
|
18745
18816
|
const isStale = (current) => Boolean(current && isSemverVersion2(current) && compareVersions(current, released) < 0);
|
|
18746
|
-
const
|
|
18747
|
-
|
|
18748
|
-
|
|
18749
|
-
|
|
18750
|
-
recipeLines.push(` Codex: ${report.recipes.codex.join(" ; ")}`);
|
|
18817
|
+
const blocks = [];
|
|
18818
|
+
for (const surface of PLUGIN_GUIDE_SURFACES) {
|
|
18819
|
+
if (!surface.versionKeys.some((k) => isStale(v[k]))) continue;
|
|
18820
|
+
blocks.push(...renderSurfaceGuide(surface.label, report.recipes[surface.key]));
|
|
18751
18821
|
}
|
|
18752
|
-
if (
|
|
18753
|
-
|
|
18754
|
-
return ["Update recipes (stale surfaces):", ...recipeLines];
|
|
18822
|
+
if (!blocks.length) return [];
|
|
18823
|
+
return ["Update commands (stale surfaces):", ...blocks];
|
|
18755
18824
|
}
|
|
18825
|
+
var DOCTOR_VERBOSE_HINT = "Run mmi-cli doctor --verbose for the full audit checklist + version report.";
|
|
18756
18826
|
function renderTerseDoctorReport(input) {
|
|
18757
|
-
|
|
18827
|
+
const cliVersion = input.updateReport.versions.cli;
|
|
18828
|
+
const versionSuffix = cliVersion ? ` (mmi-cli ${cliVersion})` : "";
|
|
18829
|
+
if (!input.gaps.length) {
|
|
18830
|
+
return [`\u2713 MMI doctor: all checks passed${versionSuffix}.`, DOCTOR_VERBOSE_HINT];
|
|
18831
|
+
}
|
|
18758
18832
|
const lines = [];
|
|
18759
18833
|
for (const c of input.gaps) {
|
|
18760
18834
|
lines.push(`\u2717 ${c.label}`);
|
|
@@ -18765,8 +18839,39 @@ function renderTerseDoctorReport(input) {
|
|
|
18765
18839
|
lines.push("");
|
|
18766
18840
|
lines.push(...stale);
|
|
18767
18841
|
}
|
|
18842
|
+
lines.push("");
|
|
18843
|
+
lines.push(`\u26A0 ${input.gaps.length} item(s) need attention \u2014 ${DOCTOR_VERBOSE_HINT}`);
|
|
18768
18844
|
return lines;
|
|
18769
18845
|
}
|
|
18846
|
+
var PLUGIN_RESOLVABILITY_LABEL = "MMI plugin resolvability (marketplace + cache present)";
|
|
18847
|
+
function buildPluginResolvabilityCheck(input) {
|
|
18848
|
+
const { state } = buildPluginGuardDecision(input);
|
|
18849
|
+
const surface = input.surface ?? "shell";
|
|
18850
|
+
switch (state) {
|
|
18851
|
+
case "not-org":
|
|
18852
|
+
return { ok: true, label: PLUGIN_RESOLVABILITY_LABEL, fix: "", state };
|
|
18853
|
+
case "healthy":
|
|
18854
|
+
return { ok: true, label: PLUGIN_RESOLVABILITY_LABEL, fix: "", state };
|
|
18855
|
+
case "no-install":
|
|
18856
|
+
return {
|
|
18857
|
+
ok: false,
|
|
18858
|
+
label: PLUGIN_RESOLVABILITY_LABEL,
|
|
18859
|
+
fix: `run: ${PLUGIN_SURFACE_HEAL[surfaceToken(surface) ?? "claude"]?.recovery ?? PLUGIN_SURFACE_HEAL.claude.recovery}`,
|
|
18860
|
+
state
|
|
18861
|
+
};
|
|
18862
|
+
case "unresolved":
|
|
18863
|
+
return {
|
|
18864
|
+
ok: false,
|
|
18865
|
+
label: PLUGIN_RESOLVABILITY_LABEL,
|
|
18866
|
+
fix: "run: mmi-cli plugin-heal",
|
|
18867
|
+
state
|
|
18868
|
+
};
|
|
18869
|
+
default: {
|
|
18870
|
+
const _exhaustive = state;
|
|
18871
|
+
return _exhaustive;
|
|
18872
|
+
}
|
|
18873
|
+
}
|
|
18874
|
+
}
|
|
18770
18875
|
|
|
18771
18876
|
// src/kb-drift-report.ts
|
|
18772
18877
|
var import_node_fs23 = require("node:fs");
|
|
@@ -19137,13 +19242,23 @@ var installedPluginsPath = (surface = detectSurface(process.env)) => {
|
|
|
19137
19242
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
19138
19243
|
return (0, import_node_path23.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
19139
19244
|
};
|
|
19140
|
-
function readInstalledPlugins() {
|
|
19245
|
+
function readInstalledPlugins(surface = detectSurface(process.env)) {
|
|
19141
19246
|
try {
|
|
19142
|
-
return JSON.parse((0, import_node_fs26.readFileSync)(installedPluginsPath(), "utf8"));
|
|
19247
|
+
return JSON.parse((0, import_node_fs26.readFileSync)(installedPluginsPath(surface), "utf8"));
|
|
19143
19248
|
} catch {
|
|
19144
19249
|
return null;
|
|
19145
19250
|
}
|
|
19146
19251
|
}
|
|
19252
|
+
function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRepo = false) {
|
|
19253
|
+
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
19254
|
+
const installed = readInstalledPlugins(surface);
|
|
19255
|
+
return {
|
|
19256
|
+
isOrgRepo,
|
|
19257
|
+
installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
|
|
19258
|
+
marketplaceClonePresent: (0, import_node_fs26.existsSync)((0, import_node_path23.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "marketplaces", "mutmutco")),
|
|
19259
|
+
pluginCachePresent: (0, import_node_fs26.existsSync)((0, import_node_path23.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
|
|
19260
|
+
};
|
|
19261
|
+
}
|
|
19147
19262
|
function installedPluginSources() {
|
|
19148
19263
|
return ["claude", "codex"].map((surface) => {
|
|
19149
19264
|
const recordPath = (0, import_node_path23.join)((0, import_node_os5.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
|
|
@@ -19720,7 +19835,6 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19720
19835
|
}
|
|
19721
19836
|
}
|
|
19722
19837
|
checks.push(pluginCheck);
|
|
19723
|
-
checks.push(buildSettingsPluginDriftCheck({ isOrgRepo: Boolean(cfg.sagaApiUrl), settings: claudeSettings }));
|
|
19724
19838
|
let legacyPluginCheck = buildLegacyPluginInstallCheck({
|
|
19725
19839
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
19726
19840
|
sources: installedPluginSources(),
|
|
@@ -19777,6 +19891,18 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19777
19891
|
}
|
|
19778
19892
|
}
|
|
19779
19893
|
checks.push(driftCheck);
|
|
19894
|
+
checks.push(
|
|
19895
|
+
buildPluginResolvabilityCheck({
|
|
19896
|
+
...snapshotPluginGuardInput(surface, Boolean(cfg.sagaApiUrl)),
|
|
19897
|
+
surface
|
|
19898
|
+
})
|
|
19899
|
+
);
|
|
19900
|
+
if (repairFull && Boolean(cfg.sagaApiUrl) && (surfaceToken(surface) === "claude" || surfaceToken(surface) === "codex")) {
|
|
19901
|
+
const guardResult = ensureUserScopeGuardHook();
|
|
19902
|
+
if (guardResult === "written") {
|
|
19903
|
+
io.err(` \u21BB installed user-scope MMI guard hook (${surface === "codex" ? "~/.codex" : "~/.claude"}/settings.json) \u2014 survives a plugin prune`);
|
|
19904
|
+
}
|
|
19905
|
+
}
|
|
19780
19906
|
let installedVersionCheck = buildInstalledPluginVersionCheck({
|
|
19781
19907
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
19782
19908
|
sources: installedPluginSources(),
|
|
@@ -20161,6 +20287,79 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
20161
20287
|
io.log(gaps.length ? `
|
|
20162
20288
|
${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
|
|
20163
20289
|
}
|
|
20290
|
+
var USER_SCOPE_GUARD_MARKER = "mmi-guard:v1";
|
|
20291
|
+
var USER_SCOPE_GUARD_COMMAND = `mmi-cli guard --session-start || true # ${USER_SCOPE_GUARD_MARKER}`;
|
|
20292
|
+
function settingsHasGuardHook(settings) {
|
|
20293
|
+
const groups = settings?.hooks?.SessionStart;
|
|
20294
|
+
if (!Array.isArray(groups)) return false;
|
|
20295
|
+
return groups.some(
|
|
20296
|
+
(g) => Array.isArray(g?.hooks) && g.hooks.some((h) => typeof h?.command === "string" && h.command.includes(USER_SCOPE_GUARD_MARKER))
|
|
20297
|
+
);
|
|
20298
|
+
}
|
|
20299
|
+
function mergeGuardHook(settings) {
|
|
20300
|
+
const next = settings && typeof settings === "object" ? { ...settings } : {};
|
|
20301
|
+
if (settingsHasGuardHook(next)) return next;
|
|
20302
|
+
const hooks = { ...next.hooks ?? {} };
|
|
20303
|
+
const sessionStart = Array.isArray(hooks.SessionStart) ? [...hooks.SessionStart] : [];
|
|
20304
|
+
sessionStart.push({ hooks: [{ type: "command", command: USER_SCOPE_GUARD_COMMAND, timeout: 10 }] });
|
|
20305
|
+
hooks.SessionStart = sessionStart;
|
|
20306
|
+
next.hooks = hooks;
|
|
20307
|
+
return next;
|
|
20308
|
+
}
|
|
20309
|
+
var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path23.join)((0, import_node_os5.homedir)(), surface === "codex" ? ".codex" : ".claude", "settings.json");
|
|
20310
|
+
function ensureUserScopeGuardHook(opts = {}) {
|
|
20311
|
+
const path2 = opts.settingsPath ?? userScopeSettingsPath();
|
|
20312
|
+
try {
|
|
20313
|
+
let current = null;
|
|
20314
|
+
if ((0, import_node_fs26.existsSync)(path2)) {
|
|
20315
|
+
try {
|
|
20316
|
+
current = JSON.parse((0, import_node_fs26.readFileSync)(path2, "utf8"));
|
|
20317
|
+
} catch {
|
|
20318
|
+
return "failed";
|
|
20319
|
+
}
|
|
20320
|
+
}
|
|
20321
|
+
if (settingsHasGuardHook(current)) return "already";
|
|
20322
|
+
const merged = mergeGuardHook(current);
|
|
20323
|
+
(0, import_node_fs26.mkdirSync)((0, import_node_path23.dirname)(path2), { recursive: true });
|
|
20324
|
+
if ((0, import_node_fs26.existsSync)(path2)) (0, import_node_fs26.copyFileSync)(path2, `${path2}.bak`);
|
|
20325
|
+
(0, import_node_fs26.writeFileSync)(path2, `${JSON.stringify(merged, null, 2)}
|
|
20326
|
+
`, "utf8");
|
|
20327
|
+
return "written";
|
|
20328
|
+
} catch {
|
|
20329
|
+
return "failed";
|
|
20330
|
+
}
|
|
20331
|
+
}
|
|
20332
|
+
async function runGuard(opts = {}) {
|
|
20333
|
+
void opts;
|
|
20334
|
+
try {
|
|
20335
|
+
const surface = detectSurface(process.env);
|
|
20336
|
+
const cfg = await loadConfig();
|
|
20337
|
+
const input = snapshotPluginGuardInput(surface, Boolean(cfg.sagaApiUrl));
|
|
20338
|
+
const { state } = buildPluginGuardDecision(input);
|
|
20339
|
+
const { line, exitCode } = buildGuardSessionStartLine(state);
|
|
20340
|
+
if (line) console.error(line);
|
|
20341
|
+
process.exitCode = exitCode;
|
|
20342
|
+
} catch {
|
|
20343
|
+
process.exitCode = 0;
|
|
20344
|
+
}
|
|
20345
|
+
}
|
|
20346
|
+
async function runPluginHeal(surface = detectSurface(process.env)) {
|
|
20347
|
+
const token = surface === "codex" ? "codex" : "claude";
|
|
20348
|
+
const tableKey = surfaceToken(surface) ?? "claude";
|
|
20349
|
+
const descriptor = PLUGIN_SURFACE_HEAL[tableKey];
|
|
20350
|
+
if (!descriptor.healSteps) {
|
|
20351
|
+
console.log(descriptor.recovery);
|
|
20352
|
+
console.log(`Then: ${reloadAction(surface)}`);
|
|
20353
|
+
return;
|
|
20354
|
+
}
|
|
20355
|
+
const healed = await applyPluginHeal(token, surface, console.log, { force: true });
|
|
20356
|
+
if (healed) {
|
|
20357
|
+
console.log(` \u2713 MMI plugin reinstalled \u2014 ${reloadAction(surface)} to load MMI commands`);
|
|
20358
|
+
} else {
|
|
20359
|
+
console.log(` \u2717 Auto-heal failed or was skipped. Run manually:
|
|
20360
|
+
${descriptor.recovery}`);
|
|
20361
|
+
}
|
|
20362
|
+
}
|
|
20164
20363
|
|
|
20165
20364
|
// src/index.ts
|
|
20166
20365
|
var GC_GH_TIMEOUT_MS2 = 2e4;
|
|
@@ -20623,18 +20822,21 @@ function acquireWorktreeSetupLock(worktreeRoot) {
|
|
|
20623
20822
|
}
|
|
20624
20823
|
}
|
|
20625
20824
|
var worktree = program2.command("worktree").description("self-provisioning worktrees \u2014 install deps + copy local-only config");
|
|
20626
|
-
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
|
|
20825
|
+
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) => {
|
|
20627
20826
|
try {
|
|
20628
20827
|
const repoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
20629
20828
|
const wtPath = o.path ?? defaultWorktreePath(repoRoot, branch);
|
|
20630
|
-
const
|
|
20631
|
-
if (
|
|
20632
|
-
|
|
20829
|
+
const { base, fetchBranch } = resolveWorktreeBase(o.from, o.remote);
|
|
20830
|
+
if (fetchBranch) {
|
|
20831
|
+
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]);
|
|
20832
|
+
if (fetchErr) console.error(` warning: could not fetch ${o.remote}/${fetchBranch} (${fetchErr}); base ${base} may be stale`);
|
|
20833
|
+
}
|
|
20834
|
+
await execFileP2("git", ["worktree", "add", wtPath, "-b", branch, base], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
20633
20835
|
const report = await provisionWorktree(wtPath, makeProvisionDeps(wtPath, Boolean(o.json), (m) => {
|
|
20634
20836
|
if (!o.json) console.error(` ${m}`);
|
|
20635
20837
|
}));
|
|
20636
|
-
if (o.json) return console.log(JSON.stringify({ branch, path: wtPath, base
|
|
20637
|
-
console.log(`worktree ready: ${wtPath} (branch ${branch} from ${
|
|
20838
|
+
if (o.json) return console.log(JSON.stringify({ branch, path: wtPath, base, ...report }, null, 2));
|
|
20839
|
+
console.log(`worktree ready: ${wtPath} (branch ${branch} from ${base})`);
|
|
20638
20840
|
console.log(` installed: ${report.installed.map((i) => i.dir || ".").join(", ") || "none"}`);
|
|
20639
20841
|
console.log(` copied: ${report.copied.join(", ") || "none"}`);
|
|
20640
20842
|
} catch (e) {
|
|
@@ -22247,9 +22449,17 @@ async function runStageLiveCommand(o) {
|
|
|
22247
22449
|
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));
|
|
22248
22450
|
return console.log(renderSteps(`mmi-cli stage --live${o.down ? " --down" : ""}: dry-run plan`, steps));
|
|
22249
22451
|
}
|
|
22452
|
+
const rcDeps = registryClientDeps(await loadConfig());
|
|
22250
22453
|
const deps = {
|
|
22251
22454
|
detectIp: () => detectPublicIp(),
|
|
22252
|
-
|
|
22455
|
+
deployDev: async ({ repo, ref }) => {
|
|
22456
|
+
const res = await tenantDeploy({ repo, stage: "dev", ref }, rcDeps);
|
|
22457
|
+
if (!res.ok) throw new Error(`dev deploy dispatch failed: ${res.body?.error ?? res.error ?? `HTTP ${res.status}`}`);
|
|
22458
|
+
},
|
|
22459
|
+
control: async ({ repo, action, host, ip }) => {
|
|
22460
|
+
const res = await tenantControl({ repo, stage: "dev", action, host, ip }, rcDeps);
|
|
22461
|
+
if (!res.ok) throw new Error(`tenant control ${action} dispatch failed: ${res.body?.error ?? res.error ?? `HTTP ${res.status}`}`);
|
|
22462
|
+
}
|
|
22253
22463
|
};
|
|
22254
22464
|
try {
|
|
22255
22465
|
const result = o.down ? await runStageLiveDown(deps, target) : await runStageLiveUp(deps, target);
|
|
@@ -22998,6 +23208,8 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
|
|
|
22998
23208
|
// Commander maps `--no-repo-writes` to `repoWrites: false`; translate to the explicit `noRepoWrites` flag.
|
|
22999
23209
|
runDoctor({ ...opts, noRepoWrites: opts.repoWrites === false })
|
|
23000
23210
|
));
|
|
23211
|
+
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 }));
|
|
23212
|
+
program2.command("plugin-heal").description("reinstall + re-enable the MMI plugin (recover from a marketplace prune)").action(() => runPluginHeal());
|
|
23001
23213
|
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 () => {
|
|
23002
23214
|
if (isInsideRepoSubdir(process.cwd())) {
|
|
23003
23215
|
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.");
|
package/dist/saga.cjs
CHANGED
|
@@ -3522,18 +3522,23 @@ function headPrompt(state) {
|
|
|
3522
3522
|
const decisions = shownDecisions(state.decisions);
|
|
3523
3523
|
const actions = (state.actionLog ?? []).slice(-HEAD_PROMPT_ACTION_LIMIT);
|
|
3524
3524
|
return [
|
|
3525
|
-
"You maintain
|
|
3526
|
-
"
|
|
3527
|
-
"
|
|
3528
|
-
"
|
|
3529
|
-
"
|
|
3530
|
-
"
|
|
3525
|
+
"You maintain two durable slots of a work-session: PINNED (things worth remembering) and a NEXT",
|
|
3526
|
+
"SUGGESTION (a best-effort one-line hint for the next useful step). Given the CURRENT HEAD and the",
|
|
3527
|
+
"recent TRANSCRIPT + DECISIONS, return an updated PINNED and, optionally, a next suggestion. Keep",
|
|
3528
|
+
"PINNED tight and concrete; keep anything the user pinned; never invent; preserve Turkish characters",
|
|
3529
|
+
"(\xE7 \u011F \u0131 \u0130 \xF6 \u015F \xFC) exactly. Do NOT manage the checklist \u2014 the note path owns that. The ANCHOR is the",
|
|
3530
|
+
"read-only North-Star \u2014 NEVER change it. Never restate an unverified artifact-claim (a named file,",
|
|
3531
|
+
"PR, flag, or board state) as settled fact \u2014 keep it as the belief it was recorded as.",
|
|
3531
3532
|
"You MAY also propose supersessions: each DECISION is shown with its original stable 0-based index. Propose a",
|
|
3532
3533
|
"supersession ONLY for a NEWER decision that directly contradicts/replaces an OLDER one where neither",
|
|
3533
3534
|
"already carries a supersededBy. HIGH PRECISION \u2014 propose ONLY when you are confident the older claim",
|
|
3534
3535
|
"is now false or obsolete; the newer decision's timestamp MUST be later than the older's (newer-supersedes-",
|
|
3535
|
-
"older only, never the reverse). When unsure, propose nothing. NEVER touch the anchor
|
|
3536
|
-
'
|
|
3536
|
+
"older only, never the reverse). When unsure, propose nothing. NEVER touch the anchor or checklist.",
|
|
3537
|
+
'For "next": propose a single concise actionable line (\u2264140 chars) describing the most useful next step',
|
|
3538
|
+
"for a future session, derived from the transcript. This is a best-effort suggestion shown only when",
|
|
3539
|
+
"the user has not set their own NEXT. Use an empty string if nothing concrete emerges \u2014 never invent.",
|
|
3540
|
+
"Preserve Turkish characters exactly.",
|
|
3541
|
+
'Output ONLY a JSON object: {"pinned":[string],"next":string,"supersede":[{"older":int,"newer":int,"reason":string}]}.',
|
|
3537
3542
|
"",
|
|
3538
3543
|
"CURRENT HEAD:",
|
|
3539
3544
|
JSON.stringify(state.head ?? {}, null, 2),
|
|
@@ -3557,6 +3562,9 @@ function parseHeadUpdate(raw) {
|
|
|
3557
3562
|
if (!obj || typeof obj !== "object") return null;
|
|
3558
3563
|
const u = {};
|
|
3559
3564
|
if (Array.isArray(obj.pinned)) u.pinned = obj.pinned.filter((x) => typeof x === "string");
|
|
3565
|
+
if (typeof obj.next === "string" && obj.next.trim()) {
|
|
3566
|
+
u.nextAuto = obj.next.trim().slice(0, 280);
|
|
3567
|
+
}
|
|
3560
3568
|
if (Array.isArray(obj.supersede)) {
|
|
3561
3569
|
const supersede = obj.supersede.filter((e) => {
|
|
3562
3570
|
if (!e || typeof e !== "object") return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.53.0",
|
|
4
4
|
"description": "MMI Future CLI — delivers the org rules (whole-file), plus saga and KB access. The cross-IDE engine the plugin's SessionStart hook drives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|