@mutmutco/cli 2.52.1 → 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 +297 -99
- 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;
|
|
@@ -17598,6 +17632,23 @@ var import_promises7 = require("node:fs/promises");
|
|
|
17598
17632
|
var import_node_path23 = require("node:path");
|
|
17599
17633
|
var import_node_os5 = require("node:os");
|
|
17600
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
|
+
|
|
17601
17652
|
// src/cursor-plugin-seed.ts
|
|
17602
17653
|
var import_node_child_process12 = require("node:child_process");
|
|
17603
17654
|
var import_node_fs22 = require("node:fs");
|
|
@@ -17789,20 +17840,6 @@ function pluginInstallManualFix(projectPath, surface = "claude-cli") {
|
|
|
17789
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}\``;
|
|
17790
17841
|
return `run ${register} then ${reloadAction(surface)} to register the install record for ${projectPath}`;
|
|
17791
17842
|
}
|
|
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
17843
|
function hasProjectInstallRecord(file, pluginId, projectPath) {
|
|
17807
17844
|
const records = file?.plugins?.[pluginId];
|
|
17808
17845
|
if (!Array.isArray(records)) return false;
|
|
@@ -17825,7 +17862,7 @@ function buildPluginInstallRecordCheck(input) {
|
|
|
17825
17862
|
fix: pluginInstallManualFix(input.projectPath, input.surface),
|
|
17826
17863
|
pluginId
|
|
17827
17864
|
};
|
|
17828
|
-
if (!input.isOrgRepo
|
|
17865
|
+
if (!input.isOrgRepo) return base;
|
|
17829
17866
|
if (hasAnyPluginRecords(input.installed, LEGACY_MMI_PLUGIN_ID) && !hasAnyPluginRecords(input.installed, pluginId)) {
|
|
17830
17867
|
return {
|
|
17831
17868
|
...base,
|
|
@@ -18086,8 +18123,9 @@ function reloadAction(surface) {
|
|
|
18086
18123
|
return "restart Claude Code (or run /reload-plugins)";
|
|
18087
18124
|
}
|
|
18088
18125
|
}
|
|
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`;
|
|
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`;
|
|
18090
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";
|
|
18091
18129
|
var OPENCODE_PLUGIN_PACKAGE = "@mutmutco/opencode-mmi";
|
|
18092
18130
|
var OPENCODE_PLUGIN_SPEC = `${OPENCODE_PLUGIN_PACKAGE}@latest`;
|
|
18093
18131
|
var OPENCODE_PLUGIN_INSTALL_COMMAND = `mmi-cli doctor --apply`;
|
|
@@ -18136,7 +18174,10 @@ var PLUGIN_SURFACE_HEAL = {
|
|
|
18136
18174
|
healSteps: [
|
|
18137
18175
|
{ args: ["plugin", "marketplace", "remove", LEGACY_MMI_MARKETPLACE], gated: false },
|
|
18138
18176
|
{ args: ["plugin", "marketplace", "remove", "mutmutco"], gated: false },
|
|
18139
|
-
|
|
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 },
|
|
18140
18181
|
{ args: ["plugin", "install", "mmi@mutmutco"], gated: true },
|
|
18141
18182
|
{ args: ["plugin", "enable", "mmi@mutmutco"], gated: false }
|
|
18142
18183
|
],
|
|
@@ -18159,10 +18200,10 @@ var PLUGIN_SURFACE_HEAL = {
|
|
|
18159
18200
|
},
|
|
18160
18201
|
cursor: {
|
|
18161
18202
|
delivery: "cursor-cache",
|
|
18162
|
-
recovery:
|
|
18203
|
+
recovery: CURSOR_RECOVERY,
|
|
18163
18204
|
healSteps: null,
|
|
18164
|
-
fix: (surface) =>
|
|
18165
|
-
updateRecipe: []
|
|
18205
|
+
fix: (surface) => `${CURSOR_RECOVERY}; then ${reloadAction(surface)} to reload MMI skills + hooks`,
|
|
18206
|
+
updateRecipe: [CURSOR_RECOVERY]
|
|
18166
18207
|
},
|
|
18167
18208
|
opencode: {
|
|
18168
18209
|
delivery: "npm",
|
|
@@ -18203,9 +18244,21 @@ function surfaceToken(surface) {
|
|
|
18203
18244
|
var PLUGIN_UPDATE_RECIPES = {
|
|
18204
18245
|
claude: PLUGIN_SURFACE_HEAL.claude.updateRecipe,
|
|
18205
18246
|
codex: PLUGIN_SURFACE_HEAL.codex.updateRecipe,
|
|
18247
|
+
cursor: PLUGIN_SURFACE_HEAL.cursor.updateRecipe,
|
|
18206
18248
|
opencode: PLUGIN_SURFACE_HEAL.opencode.updateRecipe,
|
|
18207
18249
|
cli: ["npm install -g @mutmutco/cli@latest"]
|
|
18208
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
|
+
}
|
|
18209
18262
|
function highestSemver(versions) {
|
|
18210
18263
|
return versions.reduce((best, v) => {
|
|
18211
18264
|
if (!isSemverVersion2(v)) return best;
|
|
@@ -18233,20 +18286,24 @@ function buildPluginUpdateReport(input) {
|
|
|
18233
18286
|
function renderPluginUpdateReport(report) {
|
|
18234
18287
|
const v = report.versions;
|
|
18235
18288
|
const show = (x) => x ?? "unknown";
|
|
18236
|
-
|
|
18237
|
-
"
|
|
18238
|
-
|
|
18239
|
-
|
|
18240
|
-
|
|
18241
|
-
|
|
18242
|
-
|
|
18243
|
-
|
|
18244
|
-
|
|
18245
|
-
`
|
|
18246
|
-
`
|
|
18247
|
-
|
|
18248
|
-
|
|
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"
|
|
18249
18302
|
];
|
|
18303
|
+
for (const surface of PLUGIN_GUIDE_SURFACES) {
|
|
18304
|
+
lines.push(...renderSurfaceGuide(surface.label, report.recipes[surface.key]));
|
|
18305
|
+
}
|
|
18306
|
+
return lines;
|
|
18250
18307
|
}
|
|
18251
18308
|
function buildDoctorJsonPayload(input) {
|
|
18252
18309
|
return {
|
|
@@ -18757,18 +18814,21 @@ function renderPluginUpdateReportStaleOnly(report) {
|
|
|
18757
18814
|
const released = v.released;
|
|
18758
18815
|
if (!released) return [];
|
|
18759
18816
|
const isStale = (current) => Boolean(current && isSemverVersion2(current) && compareVersions(current, released) < 0);
|
|
18760
|
-
const
|
|
18761
|
-
|
|
18762
|
-
|
|
18763
|
-
|
|
18764
|
-
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]));
|
|
18765
18821
|
}
|
|
18766
|
-
if (
|
|
18767
|
-
|
|
18768
|
-
return ["Update recipes (stale surfaces):", ...recipeLines];
|
|
18822
|
+
if (!blocks.length) return [];
|
|
18823
|
+
return ["Update commands (stale surfaces):", ...blocks];
|
|
18769
18824
|
}
|
|
18825
|
+
var DOCTOR_VERBOSE_HINT = "Run mmi-cli doctor --verbose for the full audit checklist + version report.";
|
|
18770
18826
|
function renderTerseDoctorReport(input) {
|
|
18771
|
-
|
|
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
|
+
}
|
|
18772
18832
|
const lines = [];
|
|
18773
18833
|
for (const c of input.gaps) {
|
|
18774
18834
|
lines.push(`\u2717 ${c.label}`);
|
|
@@ -18779,8 +18839,39 @@ function renderTerseDoctorReport(input) {
|
|
|
18779
18839
|
lines.push("");
|
|
18780
18840
|
lines.push(...stale);
|
|
18781
18841
|
}
|
|
18842
|
+
lines.push("");
|
|
18843
|
+
lines.push(`\u26A0 ${input.gaps.length} item(s) need attention \u2014 ${DOCTOR_VERBOSE_HINT}`);
|
|
18782
18844
|
return lines;
|
|
18783
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
|
+
}
|
|
18784
18875
|
|
|
18785
18876
|
// src/kb-drift-report.ts
|
|
18786
18877
|
var import_node_fs23 = require("node:fs");
|
|
@@ -19151,13 +19242,23 @@ var installedPluginsPath = (surface = detectSurface(process.env)) => {
|
|
|
19151
19242
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
19152
19243
|
return (0, import_node_path23.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
19153
19244
|
};
|
|
19154
|
-
function readInstalledPlugins() {
|
|
19245
|
+
function readInstalledPlugins(surface = detectSurface(process.env)) {
|
|
19155
19246
|
try {
|
|
19156
|
-
return JSON.parse((0, import_node_fs26.readFileSync)(installedPluginsPath(), "utf8"));
|
|
19247
|
+
return JSON.parse((0, import_node_fs26.readFileSync)(installedPluginsPath(surface), "utf8"));
|
|
19157
19248
|
} catch {
|
|
19158
19249
|
return null;
|
|
19159
19250
|
}
|
|
19160
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
|
+
}
|
|
19161
19262
|
function installedPluginSources() {
|
|
19162
19263
|
return ["claude", "codex"].map((surface) => {
|
|
19163
19264
|
const recordPath = (0, import_node_path23.join)((0, import_node_os5.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
|
|
@@ -19734,7 +19835,6 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19734
19835
|
}
|
|
19735
19836
|
}
|
|
19736
19837
|
checks.push(pluginCheck);
|
|
19737
|
-
checks.push(buildSettingsPluginDriftCheck({ isOrgRepo: Boolean(cfg.sagaApiUrl), settings: claudeSettings }));
|
|
19738
19838
|
let legacyPluginCheck = buildLegacyPluginInstallCheck({
|
|
19739
19839
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
19740
19840
|
sources: installedPluginSources(),
|
|
@@ -19791,6 +19891,18 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
19791
19891
|
}
|
|
19792
19892
|
}
|
|
19793
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
|
+
}
|
|
19794
19906
|
let installedVersionCheck = buildInstalledPluginVersionCheck({
|
|
19795
19907
|
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
19796
19908
|
sources: installedPluginSources(),
|
|
@@ -20175,6 +20287,79 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
20175
20287
|
io.log(gaps.length ? `
|
|
20176
20288
|
${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
|
|
20177
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
|
+
}
|
|
20178
20363
|
|
|
20179
20364
|
// src/index.ts
|
|
20180
20365
|
var GC_GH_TIMEOUT_MS2 = 2e4;
|
|
@@ -20637,18 +20822,21 @@ function acquireWorktreeSetupLock(worktreeRoot) {
|
|
|
20637
20822
|
}
|
|
20638
20823
|
}
|
|
20639
20824
|
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
|
|
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) => {
|
|
20641
20826
|
try {
|
|
20642
20827
|
const repoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
20643
20828
|
const wtPath = o.path ?? defaultWorktreePath(repoRoot, branch);
|
|
20644
|
-
const
|
|
20645
|
-
if (
|
|
20646
|
-
|
|
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 });
|
|
20647
20835
|
const report = await provisionWorktree(wtPath, makeProvisionDeps(wtPath, Boolean(o.json), (m) => {
|
|
20648
20836
|
if (!o.json) console.error(` ${m}`);
|
|
20649
20837
|
}));
|
|
20650
|
-
if (o.json) return console.log(JSON.stringify({ branch, path: wtPath, base
|
|
20651
|
-
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})`);
|
|
20652
20840
|
console.log(` installed: ${report.installed.map((i) => i.dir || ".").join(", ") || "none"}`);
|
|
20653
20841
|
console.log(` copied: ${report.copied.join(", ") || "none"}`);
|
|
20654
20842
|
} catch (e) {
|
|
@@ -22261,9 +22449,17 @@ async function runStageLiveCommand(o) {
|
|
|
22261
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));
|
|
22262
22450
|
return console.log(renderSteps(`mmi-cli stage --live${o.down ? " --down" : ""}: dry-run plan`, steps));
|
|
22263
22451
|
}
|
|
22452
|
+
const rcDeps = registryClientDeps(await loadConfig());
|
|
22264
22453
|
const deps = {
|
|
22265
22454
|
detectIp: () => detectPublicIp(),
|
|
22266
|
-
|
|
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
|
+
}
|
|
22267
22463
|
};
|
|
22268
22464
|
try {
|
|
22269
22465
|
const result = o.down ? await runStageLiveDown(deps, target) : await runStageLiveUp(deps, target);
|
|
@@ -23012,6 +23208,8 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
|
|
|
23012
23208
|
// Commander maps `--no-repo-writes` to `repoWrites: false`; translate to the explicit `noRepoWrites` flag.
|
|
23013
23209
|
runDoctor({ ...opts, noRepoWrites: opts.repoWrites === false })
|
|
23014
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());
|
|
23015
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 () => {
|
|
23016
23214
|
if (isInsideRepoSubdir(process.cwd())) {
|
|
23017
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",
|