@mutmutco/cli 2.49.0 → 2.50.1

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.
Files changed (2) hide show
  1. package/dist/main.cjs +376 -86
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -4582,18 +4582,32 @@ async function ffOnlyPull(deps, branch) {
4582
4582
  await deps.run("git", ["pull", "--ff-only", "origin", branch]);
4583
4583
  }
4584
4584
  var RELEASE_TOLERATED_PATHS = [".gitignore"];
4585
- async function predictMergeConflicts(deps, ours, theirs) {
4586
- try {
4587
- await deps.run("git", ["merge-tree", "--write-tree", "--name-only", "--no-messages", ours, theirs]);
4588
- return [];
4589
- } catch (e) {
4590
- const out = String(e.stdout ?? "");
4591
- const files = out.split("\n").map((s) => s.trim()).filter(Boolean).slice(1);
4592
- if (files.length === 0) {
4593
- throw new Error(`could not preflight the ${theirs} -> ${ours} merge (git merge-tree failed: ${e.message ?? e})`);
4585
+ var MERGE_TREE_PREFLIGHT_ATTEMPTS = 3;
4586
+ var MERGE_TREE_PREFLIGHT_DELAY_MS = 250;
4587
+ function mergeTreeConflictFiles(stdout) {
4588
+ return stdout.split("\n").map((s) => s.trim()).filter(Boolean).slice(1);
4589
+ }
4590
+ async function runMergeTreePreflight(deps, ours, theirs) {
4591
+ const sleep = resolveSleep(deps);
4592
+ let lastError;
4593
+ for (let attempt = 0; attempt < MERGE_TREE_PREFLIGHT_ATTEMPTS; attempt++) {
4594
+ try {
4595
+ await deps.run("git", ["merge-tree", "--write-tree", "--name-only", "--no-messages", ours, theirs]);
4596
+ return [];
4597
+ } catch (e) {
4598
+ lastError = e;
4599
+ const files = mergeTreeConflictFiles(String(e.stdout ?? ""));
4600
+ if (files.length > 0) return files;
4601
+ if (attempt + 1 < MERGE_TREE_PREFLIGHT_ATTEMPTS) await sleep(MERGE_TREE_PREFLIGHT_DELAY_MS);
4594
4602
  }
4595
- return files;
4596
4603
  }
4604
+ const msg = lastError?.message ?? String(lastError);
4605
+ throw new Error(
4606
+ `could not preflight the ${theirs} -> ${ours} merge (git merge-tree failed after ${MERGE_TREE_PREFLIGHT_ATTEMPTS} attempts: ${msg})`
4607
+ );
4608
+ }
4609
+ async function predictMergeConflicts(deps, ours, theirs) {
4610
+ return runMergeTreePreflight(deps, ours, theirs);
4597
4611
  }
4598
4612
  async function mergeWithSpineResolution(deps, sourceRef, label, resolve5, extraTolerated = []) {
4599
4613
  try {
@@ -17660,6 +17674,7 @@ async function applyCursorPluginCacheSeed(input) {
17660
17674
 
17661
17675
  // src/doctor.ts
17662
17676
  var GH_PROJECT_LOGIN_FIX = 'run: gh auth login --hostname github.com --git-protocol https --web --scopes "project"';
17677
+ var AWS_CROSS_ACCOUNT_LABEL = "AWS cross-account identity (master-agent audits)";
17663
17678
  var AWS_CROSS_ACCOUNT_FIX = "use a non-root IAM user/session profile for master-agent AWS checks; set AWS_PROFILE or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY (plus AWS_SESSION_TOKEN for temporary credentials), then verify `aws sts get-caller-identity` does not end in :root";
17664
17679
  var MMI_AGENTIC_ONBOARDING_GUIDE = {
17665
17680
  label: "MMI Agentic Onboarding",
@@ -17680,7 +17695,7 @@ function buildAwsCrossAccountCheck(input) {
17680
17695
  const callerArn = input.callerArn?.trim();
17681
17696
  return {
17682
17697
  ok: !callerArn || !callerArn.endsWith(":root"),
17683
- label: "AWS cross-account identity (master-agent audits)",
17698
+ label: AWS_CROSS_ACCOUNT_LABEL,
17684
17699
  fix: AWS_CROSS_ACCOUNT_FIX
17685
17700
  };
17686
17701
  }
@@ -18002,7 +18017,7 @@ function detectSurface(env) {
18002
18017
  if (env.MMI_AGENT_SURFACE === "cursor" || has("CURSOR_TRACE_ID") || has("CURSOR_USER") || has("CURSOR_SESSION_ID") || env.CURSOR_AGENT === "1" || has("CURSOR_EXTENSION_HOST_ROLE")) {
18003
18018
  return "cursor";
18004
18019
  }
18005
- if (env.MMI_AGENT_SURFACE === "opencode") return "opencode";
18020
+ if (env.MMI_AGENT_SURFACE === "opencode" || has("OPENCODE_CLIENT") || has("OPENCODE_SERVER_USERNAME")) return "opencode";
18006
18021
  const isClaude = has("CLAUDECODE") || has("CLAUDE_CODE_ENTRYPOINT") || has("CLAUDE_PLUGIN_ROOT") || env.MMI_AGENT_SURFACE === "claude";
18007
18022
  const isVscode = env.TERM_PROGRAM === "vscode" || has("VSCODE_PID") || has("VSCODE_GIT_ASKPASS_NODE");
18008
18023
  if (isClaude && isVscode) return "claude-vscode";
@@ -18027,7 +18042,10 @@ function reloadAction(surface) {
18027
18042
  }
18028
18043
  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`;
18029
18044
  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`;
18030
- var OPENCODE_RECOVERY = "npm install @mutmutco/opencode-mmi@latest";
18045
+ var OPENCODE_PLUGIN_PACKAGE = "@mutmutco/opencode-mmi";
18046
+ var OPENCODE_PLUGIN_SPEC = `${OPENCODE_PLUGIN_PACKAGE}@latest`;
18047
+ var OPENCODE_PLUGIN_INSTALL_COMMAND = `opencode plugin ${OPENCODE_PLUGIN_SPEC} --global --force`;
18048
+ var OPENCODE_RECOVERY = `${OPENCODE_PLUGIN_INSTALL_COMMAND} # then restart OpenCode to load MMI commands`;
18031
18049
  var PLUGIN_SURFACE_HEAL = {
18032
18050
  claude: {
18033
18051
  delivery: "plugin-cli",
@@ -18067,8 +18085,8 @@ var PLUGIN_SURFACE_HEAL = {
18067
18085
  delivery: "npm",
18068
18086
  recovery: OPENCODE_RECOVERY,
18069
18087
  healSteps: null,
18070
- fix: (surface) => `${OPENCODE_RECOVERY} # then ${reloadAction(surface)} to reload MMI skills`,
18071
- updateRecipe: [OPENCODE_RECOVERY, "npm ls @mutmutco/opencode-mmi # verify the installed version, then restart OpenCode"]
18088
+ fix: () => OPENCODE_RECOVERY,
18089
+ updateRecipe: [OPENCODE_RECOVERY, "restart OpenCode, then run mmi-cli doctor to verify OpenCode plugin reports the released version"]
18072
18090
  }
18073
18091
  };
18074
18092
  var CLAUDE_PLUGIN_RECOVERY = PLUGIN_SURFACE_HEAL.claude.recovery;
@@ -18220,6 +18238,94 @@ function buildOpencodeVersionCheck(input) {
18220
18238
  }
18221
18239
  return { ...base, ok: false, installedVersion: input.installedVersion, releasedVersion: input.releasedVersion };
18222
18240
  }
18241
+ var OPENCODE_CONFIG_PLUGIN_LABEL = "OpenCode MMI adapter config wiring";
18242
+ function opencodePluginEntryMatches(entry) {
18243
+ return entry === OPENCODE_PLUGIN_PACKAGE || entry === OPENCODE_PLUGIN_SPEC || Array.isArray(entry) && (entry[0] === OPENCODE_PLUGIN_PACKAGE || entry[0] === OPENCODE_PLUGIN_SPEC);
18244
+ }
18245
+ function stripJsonc(text) {
18246
+ let out = "";
18247
+ let inString = false;
18248
+ let quote = "";
18249
+ let escaped = false;
18250
+ for (let i = 0; i < text.length; i += 1) {
18251
+ const ch = text[i];
18252
+ const next = text[i + 1];
18253
+ if (inString) {
18254
+ out += ch;
18255
+ if (escaped) escaped = false;
18256
+ else if (ch === "\\") escaped = true;
18257
+ else if (ch === quote) inString = false;
18258
+ continue;
18259
+ }
18260
+ if (ch === '"' || ch === "'") {
18261
+ inString = true;
18262
+ quote = ch;
18263
+ out += ch;
18264
+ continue;
18265
+ }
18266
+ if (ch === "/" && next === "/") {
18267
+ while (i < text.length && text[i] !== "\n") i += 1;
18268
+ out += "\n";
18269
+ continue;
18270
+ }
18271
+ if (ch === "/" && next === "*") {
18272
+ i += 2;
18273
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i += 1;
18274
+ i += 1;
18275
+ out += " ";
18276
+ continue;
18277
+ }
18278
+ out += ch;
18279
+ }
18280
+ return out.replace(/,\s*([}\]])/g, "$1");
18281
+ }
18282
+ function planOpencodeConfigWrite(raw) {
18283
+ if (raw === void 0) {
18284
+ return {
18285
+ action: "create",
18286
+ text: `${JSON.stringify({ $schema: "https://opencode.ai/config.json", plugin: [OPENCODE_PLUGIN_SPEC] }, null, 2)}
18287
+ `
18288
+ };
18289
+ }
18290
+ let parsed;
18291
+ try {
18292
+ parsed = JSON.parse(stripJsonc(raw));
18293
+ } catch {
18294
+ return { action: "unsafe" };
18295
+ }
18296
+ const hasPluginField = Object.prototype.hasOwnProperty.call(parsed, "plugin");
18297
+ if (hasPluginField) {
18298
+ if (!Array.isArray(parsed.plugin)) return { action: "unsafe" };
18299
+ if (parsed.plugin.some(opencodePluginEntryMatches)) return { action: "already" };
18300
+ const next2 = raw.replace(/("plugin"\s*:\s*\[)([\s\S]*?)(\])/m, (_m, start, body, end) => {
18301
+ const prefix = body.trim() ? `${body.replace(/\s*$/, "")}, ` : "";
18302
+ return `${start}${prefix}"${OPENCODE_PLUGIN_SPEC}"${end}`;
18303
+ });
18304
+ if (next2 === raw) return { action: "unsafe" };
18305
+ return { action: "append", text: next2.endsWith("\n") ? next2 : `${next2}
18306
+ ` };
18307
+ }
18308
+ const brace = raw.indexOf("{");
18309
+ if (brace < 0) return { action: "unsafe" };
18310
+ const insertAt = brace + 1;
18311
+ const next = `${raw.slice(0, insertAt)}
18312
+ "plugin": ["${OPENCODE_PLUGIN_SPEC}"],${raw.slice(insertAt)}`;
18313
+ return { action: "insert", text: next.endsWith("\n") ? next : `${next}
18314
+ ` };
18315
+ }
18316
+ function buildOpencodeConfigPluginCheck(input) {
18317
+ const fix = OPENCODE_RECOVERY;
18318
+ const base = { ok: true, label: OPENCODE_CONFIG_PLUGIN_LABEL, fix };
18319
+ if (!input.isOrgRepo) return base;
18320
+ if (!input.hasConfig) return { ...base, ok: false, configPath: input.configPath, reason: "missing-config" };
18321
+ if (!input.parseOk) return { ...base, ok: false, configPath: input.configPath, reason: "unreadable-config" };
18322
+ if (!input.hasPluginField) return { ...base, ok: false, configPath: input.configPath, reason: "missing-plugin-entry" };
18323
+ if (!Array.isArray(input.pluginEntries)) return { ...base, ok: false, configPath: input.configPath, reason: "invalid-plugin-shape" };
18324
+ if (!input.pluginEntries.some(opencodePluginEntryMatches)) {
18325
+ return { ...base, ok: false, configPath: input.configPath, reason: "missing-plugin-entry" };
18326
+ }
18327
+ return { ...base, configPath: input.configPath };
18328
+ }
18223
18329
  var OPENCODE_DESKTOP_BOOTSTRAP_LABEL = "OpenCode Desktop stale project bootstrap";
18224
18330
  var OPENCODE_DESKTOP_BOOTSTRAP_FIX = "OpenCode Desktop is bootstrapping a deleted MMI worktree; open an existing checkout in OpenCode, remove/select away from the stale project entry, then restart OpenCode";
18225
18331
  function decodeLogUrlDirectory(value) {
@@ -18243,6 +18349,38 @@ function buildOpencodeDesktopBootstrapCheck(input) {
18243
18349
  const dirs = input.issues.map((i) => i.directory).join(", ");
18244
18350
  return { ...base, ok: false, fix: `${OPENCODE_DESKTOP_BOOTSTRAP_FIX} (stale: ${dirs})`, issues: [...input.issues] };
18245
18351
  }
18352
+ var OPENCODE_LEGACY_CONFIG_LABEL = "OpenCode legacy ~/.opencode config (stale plugin entries)";
18353
+ var OPENCODE_LEGACY_CONFIG_FIX = 'remove or rename ~/.opencode/opencode.json (legacy path); configure OpenCode at ~/.config/opencode/opencode.jsonc with "plugin": ["@mutmutco/opencode-mmi"] \u2014 see docs/Guides/opencode-saga.md';
18354
+ function parseOpencodeLegacyConfigPlugins(content) {
18355
+ try {
18356
+ const parsed = JSON.parse(content);
18357
+ if (!Array.isArray(parsed.plugin)) return null;
18358
+ return parsed.plugin.filter((entry) => typeof entry === "string");
18359
+ } catch {
18360
+ return null;
18361
+ }
18362
+ }
18363
+ function isStaleOpencodePluginEntry(entry) {
18364
+ const trimmed = entry.trim();
18365
+ if (!trimmed) return false;
18366
+ if (trimmed.startsWith("@") || trimmed.includes("/")) return false;
18367
+ return /^[a-z][a-z0-9-]*$/i.test(trimmed);
18368
+ }
18369
+ function buildOpencodeLegacyConfigCheck(input) {
18370
+ const base = {
18371
+ ok: true,
18372
+ label: OPENCODE_LEGACY_CONFIG_LABEL,
18373
+ fix: OPENCODE_LEGACY_CONFIG_FIX,
18374
+ legacyPath: input.legacyPath
18375
+ };
18376
+ if (!input.isOrgRepo || !input.stalePlugins?.length) return base;
18377
+ return {
18378
+ ...base,
18379
+ ok: false,
18380
+ stalePlugins: [...input.stalePlugins],
18381
+ fix: `${OPENCODE_LEGACY_CONFIG_FIX} \u2014 stale entries: ${input.stalePlugins.join(", ")}`
18382
+ };
18383
+ }
18246
18384
  var CURSOR_PLUGIN_INSTALL_LABEL = "Cursor Team Marketplace plugin install";
18247
18385
  var CURSOR_MARKETPLACE_INSTALL_GUIDE = "https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/cursor-marketplace-install.md";
18248
18386
  var CURSOR_PLUGIN_JSON_REL = ".cursor-plugin/plugin.json";
@@ -18491,6 +18629,35 @@ function doctorPreflightDoneLine(surface) {
18491
18629
  function pluginAutonomousHaltLine(reloadHint) {
18492
18630
  return `\u26A0 PLUGIN RELOAD REQUIRED \u2014 mmi:* skills and agent types are unavailable until you ${reloadHint}. Halt autonomous /grind and /build until then.`;
18493
18631
  }
18632
+ function renderPluginUpdateReportStaleOnly(report) {
18633
+ const v = report.versions;
18634
+ const released = v.released;
18635
+ if (!released) return [];
18636
+ const isStale = (current) => Boolean(current && isSemverVersion2(current) && compareVersions(current, released) < 0);
18637
+ const recipeLines = [];
18638
+ if (isStale(v.cli)) recipeLines.push(` npm CLI: ${report.recipes.cli.join(" ; ")}`);
18639
+ if (isStale(v.claudePlugin)) recipeLines.push(` Claude: ${report.recipes.claude.join(" ; ")}`);
18640
+ if (isStale(v.codexMarketplace) || isStale(v.codexActiveCache)) {
18641
+ recipeLines.push(` Codex: ${report.recipes.codex.join(" ; ")}`);
18642
+ }
18643
+ if (isStale(v.opencodePlugin)) recipeLines.push(` OpenCode: ${report.recipes.opencode.join(" ; ")}`);
18644
+ if (!recipeLines.length) return [];
18645
+ return ["Update recipes (stale surfaces):", ...recipeLines];
18646
+ }
18647
+ function renderTerseDoctorReport(input) {
18648
+ if (!input.gaps.length) return [];
18649
+ const lines = [];
18650
+ for (const c of input.gaps) {
18651
+ lines.push(`\u2717 ${c.label}`);
18652
+ lines.push(` \u2192 ${c.fix}`);
18653
+ }
18654
+ const stale = renderPluginUpdateReportStaleOnly(input.updateReport);
18655
+ if (stale.length) {
18656
+ lines.push("");
18657
+ lines.push(...stale);
18658
+ }
18659
+ return lines;
18660
+ }
18494
18661
 
18495
18662
  // src/kb-drift-report.ts
18496
18663
  var import_node_fs23 = require("node:fs");
@@ -18917,6 +19084,42 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
18917
19084
  return false;
18918
19085
  }
18919
19086
  }
19087
+ function opencodeConfigPath() {
19088
+ return (0, import_node_path22.join)((0, import_node_os5.homedir)(), ".config", "opencode", "opencode.jsonc");
19089
+ }
19090
+ function opencodeConfigSnapshot() {
19091
+ const path2 = opencodeConfigPath();
19092
+ if (!(0, import_node_fs26.existsSync)(path2)) return { path: path2, hasConfig: false, hasPluginField: false, parseOk: true };
19093
+ try {
19094
+ const raw = (0, import_node_fs26.readFileSync)(path2, "utf8");
19095
+ const parsed = JSON.parse(stripJsonc(raw));
19096
+ const hasPluginField = Object.prototype.hasOwnProperty.call(parsed, "plugin");
19097
+ return {
19098
+ path: path2,
19099
+ hasConfig: true,
19100
+ hasPluginField,
19101
+ parseOk: true,
19102
+ raw,
19103
+ ...Array.isArray(parsed.plugin) ? { pluginEntries: parsed.plugin } : parsed.plugin === void 0 ? {} : { pluginEntries: void 0 }
19104
+ };
19105
+ } catch {
19106
+ return { path: path2, hasConfig: true, hasPluginField: false, parseOk: false };
19107
+ }
19108
+ }
19109
+ function writeOpencodeConfigPlugin(snapshot) {
19110
+ try {
19111
+ const path2 = snapshot.path;
19112
+ const plan2 = planOpencodeConfigWrite(snapshot.hasConfig ? snapshot.raw : void 0);
19113
+ if (plan2.action === "already") return true;
19114
+ if (!plan2.text || plan2.action === "unsafe") return false;
19115
+ (0, import_node_fs26.mkdirSync)((0, import_node_path22.dirname)(path2), { recursive: true });
19116
+ if (snapshot.hasConfig) (0, import_node_fs26.copyFileSync)(path2, `${path2}.bak`);
19117
+ (0, import_node_fs26.writeFileSync)(path2, plan2.text, "utf8");
19118
+ return true;
19119
+ } catch {
19120
+ return false;
19121
+ }
19122
+ }
18920
19123
  function opencodeDesktopLogsRoot() {
18921
19124
  if (process.platform === "win32") {
18922
19125
  const base = process.env.APPDATA || (0, import_node_path22.join)((0, import_node_os5.homedir)(), "AppData", "Roaming");
@@ -18940,6 +19143,27 @@ function opencodeDesktopBootstrapSnapshot() {
18940
19143
  return [];
18941
19144
  }
18942
19145
  }
19146
+ function opencodeLegacyConfigSnapshot() {
19147
+ const legacyPath = (0, import_node_path22.join)((0, import_node_os5.homedir)(), ".opencode", "opencode.json");
19148
+ if (!(0, import_node_fs26.existsSync)(legacyPath)) return {};
19149
+ const content = readTextFile(legacyPath);
19150
+ if (content == null) return {};
19151
+ const plugins = parseOpencodeLegacyConfigPlugins(content);
19152
+ if (!plugins) return {};
19153
+ const stalePlugins = plugins.filter(isStaleOpencodePluginEntry);
19154
+ if (!stalePlugins.length) return {};
19155
+ return { legacyPath, stalePlugins };
19156
+ }
19157
+ function quarantineOpencodeLegacyConfig(legacyPath) {
19158
+ try {
19159
+ const backupPath = `${legacyPath}.bak`;
19160
+ if ((0, import_node_fs26.existsSync)(backupPath)) return false;
19161
+ (0, import_node_fs26.renameSync)(legacyPath, backupPath);
19162
+ return true;
19163
+ } catch {
19164
+ return false;
19165
+ }
19166
+ }
18943
19167
  function cursorPluginCacheRoot() {
18944
19168
  return (0, import_node_path22.join)((0, import_node_os5.homedir)(), ".cursor", "plugins", "cache", "mutmutco", "mmi");
18945
19169
  }
@@ -19160,6 +19384,8 @@ async function runDoctor(opts, io = consoleIo) {
19160
19384
  }
19161
19385
  const repairLocal = !opts.json || Boolean(opts.apply) || Boolean(opts.preflight);
19162
19386
  const repoWritesAllowed = !opts.noRepoWrites;
19387
+ const runExtended = Boolean(opts.verbose) || Boolean(opts.json);
19388
+ const terseOutput = !opts.verbose && !opts.json && !opts.banner && !opts.preflight;
19163
19389
  const checks = [];
19164
19390
  const REWRITE_KEY = "url.https://github.com/.insteadOf";
19165
19391
  const CLONE_FIX = 'run: git config --global url."https://github.com/".insteadOf "git@github.com:"';
@@ -19238,15 +19464,19 @@ async function runDoctor(opts, io = consoleIo) {
19238
19464
  installedVersion
19239
19465
  })
19240
19466
  );
19241
- checks.push(
19242
- buildHubDeployFreshnessCheck({
19243
- isOrgRepo: Boolean(cfg.sagaApiUrl),
19244
- deployedHubVersion: hubVersionInfo?.hubVersion,
19245
- installedVersion,
19246
- releasedVersion
19247
- })
19248
- );
19249
- checks.push(buildAwsCrossAccountCheck({ callerArn }));
19467
+ if (runExtended) {
19468
+ checks.push(
19469
+ buildHubDeployFreshnessCheck({
19470
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
19471
+ deployedHubVersion: hubVersionInfo?.hubVersion,
19472
+ installedVersion,
19473
+ releasedVersion
19474
+ })
19475
+ );
19476
+ }
19477
+ if (runExtended) {
19478
+ checks.push(buildAwsCrossAccountCheck({ callerArn }));
19479
+ }
19250
19480
  let cloneOk = cloneProbe;
19251
19481
  if (!cloneOk && repairFull) {
19252
19482
  try {
@@ -19368,7 +19598,35 @@ async function runDoctor(opts, io = consoleIo) {
19368
19598
  }
19369
19599
  }
19370
19600
  checks.push(installedVersionCheck);
19371
- if (surface === "opencode") {
19601
+ const openCodeConfigSnapshot = opencodeConfigSnapshot();
19602
+ const inspectOpenCode = surface === "opencode" || openCodeConfigSnapshot.hasConfig || runExtended;
19603
+ if (inspectOpenCode) {
19604
+ let opencodeConfigCheck = buildOpencodeConfigPluginCheck({
19605
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
19606
+ configPath: openCodeConfigSnapshot.path,
19607
+ hasConfig: openCodeConfigSnapshot.hasConfig,
19608
+ hasPluginField: openCodeConfigSnapshot.hasPluginField,
19609
+ pluginEntries: openCodeConfigSnapshot.pluginEntries,
19610
+ parseOk: openCodeConfigSnapshot.parseOk
19611
+ });
19612
+ if (!opencodeConfigCheck.ok && opencodeConfigCheck.reason !== "unreadable-config" && opencodeConfigCheck.reason !== "invalid-plugin-shape" && repairLocal) {
19613
+ if (writeOpencodeConfigPlugin(openCodeConfigSnapshot)) {
19614
+ const refreshed = opencodeConfigSnapshot();
19615
+ opencodeConfigCheck = buildOpencodeConfigPluginCheck({
19616
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
19617
+ configPath: refreshed.path,
19618
+ hasConfig: refreshed.hasConfig,
19619
+ hasPluginField: refreshed.hasPluginField,
19620
+ pluginEntries: refreshed.pluginEntries,
19621
+ parseOk: refreshed.parseOk
19622
+ });
19623
+ if (opencodeConfigCheck.ok) {
19624
+ markPluginReloadRequired();
19625
+ io.err(` \u21BB repaired: wired ${OPENCODE_PLUGIN_PACKAGE} in OpenCode config \u2014 ${reloadAction("opencode")} to load MMI commands`);
19626
+ }
19627
+ }
19628
+ }
19629
+ checks.push(opencodeConfigCheck);
19372
19630
  checks.push(buildOpencodeVersionCheck({
19373
19631
  isOrgRepo: Boolean(cfg.sagaApiUrl),
19374
19632
  installedVersion: process.env.MMI_OPENCODE_PLUGIN_VERSION,
@@ -19380,6 +19638,21 @@ async function runDoctor(opts, io = consoleIo) {
19380
19638
  issues: opencodeDesktopBootstrapSnapshot()
19381
19639
  }));
19382
19640
  }
19641
+ const legacyOpenCodeConfig = opencodeLegacyConfigSnapshot();
19642
+ if (Boolean(cfg.sagaApiUrl) && legacyOpenCodeConfig.stalePlugins?.length) {
19643
+ let legacyOpenCodeCheck = buildOpencodeLegacyConfigCheck({
19644
+ isOrgRepo: true,
19645
+ legacyPath: legacyOpenCodeConfig.legacyPath,
19646
+ stalePlugins: legacyOpenCodeConfig.stalePlugins
19647
+ });
19648
+ if (!legacyOpenCodeCheck.ok && repairLocal && legacyOpenCodeConfig.legacyPath) {
19649
+ if (quarantineOpencodeLegacyConfig(legacyOpenCodeConfig.legacyPath)) {
19650
+ legacyOpenCodeCheck = buildOpencodeLegacyConfigCheck({ isOrgRepo: true });
19651
+ io.err(` \u21BB quarantined legacy OpenCode config \u2192 ${legacyOpenCodeConfig.legacyPath}.bak \u2014 restart OpenCode`);
19652
+ }
19653
+ }
19654
+ checks.push(legacyOpenCodeCheck);
19655
+ }
19383
19656
  let cacheCleanupCheck = buildMmiPluginCacheCleanupCheck({
19384
19657
  isOrgRepo: Boolean(cfg.sagaApiUrl),
19385
19658
  roots: mmiPluginCacheRootSnapshots(),
@@ -19492,53 +19765,62 @@ async function runDoctor(opts, io = consoleIo) {
19492
19765
  mmiCliOnPath: onPath
19493
19766
  })
19494
19767
  );
19495
- const playwrightMcpConfigs = playwrightMcpConfigSnapshots();
19496
- checks.push(
19497
- buildPlaywrightMcpVisionCapCheck({
19498
- isOrgRepo: Boolean(cfg.sagaApiUrl),
19499
- configs: playwrightMcpConfigs
19500
- })
19501
- );
19502
- checks.push(
19503
- buildPlaywrightMcpOutputDirCheck({
19504
- isOrgRepo: Boolean(cfg.sagaApiUrl),
19505
- configs: playwrightMcpConfigs
19506
- })
19507
- );
19508
- checks.push(
19509
- buildBrowserArtifactsCheck({
19510
- isOrgRepo: Boolean(cfg.sagaApiUrl),
19511
- strayPaths: strayBrowserArtifactPaths()
19512
- })
19513
- );
19514
- if (!opts.banner && Boolean(cfg.sagaApiUrl)) {
19768
+ if (runExtended) {
19769
+ const playwrightMcpConfigs = playwrightMcpConfigSnapshots();
19770
+ checks.push(
19771
+ buildPlaywrightMcpVisionCapCheck({
19772
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
19773
+ configs: playwrightMcpConfigs
19774
+ })
19775
+ );
19776
+ checks.push(
19777
+ buildPlaywrightMcpOutputDirCheck({
19778
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
19779
+ configs: playwrightMcpConfigs
19780
+ })
19781
+ );
19782
+ checks.push(
19783
+ buildBrowserArtifactsCheck({
19784
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
19785
+ strayPaths: strayBrowserArtifactPaths()
19786
+ })
19787
+ );
19788
+ }
19789
+ if (runExtended && !opts.banner && Boolean(cfg.sagaApiUrl)) {
19515
19790
  const repoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
19516
19791
  const driftReport = await fetchLatestKbDriftReport(execFileP2, repoRoot);
19517
19792
  checks.push(buildKbDriftAdvisoryCheck({ isOrgRepo: true, report: driftReport }));
19518
19793
  }
19519
19794
  if (!opts.banner) {
19520
19795
  const repoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
19521
- let scratchRun = executeScratchGc(repoRoot, { apply: false });
19522
- if (opts.apply && repoWritesAllowed && scratchRun.plan.safeAuto.length > 0) {
19523
- scratchRun = executeScratchGc(repoRoot, { apply: true });
19796
+ if (runExtended) {
19797
+ let scratchRun = executeScratchGc(repoRoot, { apply: false });
19798
+ if (opts.apply && repoWritesAllowed && scratchRun.plan.safeAuto.length > 0) {
19799
+ scratchRun = executeScratchGc(repoRoot, { apply: true });
19800
+ if (scratchRun.applied?.pruned.length) {
19801
+ io.err(` \u21BB pruned ${scratchRun.applied.pruned.length} scratch item(s) (${scratchRun.applied.bytes} bytes)`);
19802
+ }
19803
+ scratchRun = executeScratchGc(repoRoot, { apply: false });
19804
+ }
19805
+ checks.push(buildScratchGcCheck(scratchRun.plan));
19806
+ try {
19807
+ checks.push(buildGitGcCheck(await gcPlan("origin", 200)));
19808
+ } catch {
19809
+ }
19810
+ } else if (opts.apply && repoWritesAllowed) {
19811
+ const scratchRun = executeScratchGc(repoRoot, { apply: true });
19524
19812
  if (scratchRun.applied?.pruned.length) {
19525
19813
  io.err(` \u21BB pruned ${scratchRun.applied.pruned.length} scratch item(s) (${scratchRun.applied.bytes} bytes)`);
19526
19814
  }
19527
- scratchRun = executeScratchGc(repoRoot, { apply: false });
19528
- }
19529
- checks.push(buildScratchGcCheck(scratchRun.plan));
19530
- try {
19531
- checks.push(buildGitGcCheck(await gcPlan("origin", 200)));
19532
- } catch {
19533
19815
  }
19534
19816
  }
19535
- if (opts.banner) {
19817
+ if (opts.banner && runExtended) {
19536
19818
  try {
19537
19819
  checks.push(buildGitGcCheck(await gcPlan("origin", 50)));
19538
19820
  } catch {
19539
19821
  }
19540
19822
  }
19541
- if (!opts.banner) {
19823
+ if (runExtended && !opts.banner) {
19542
19824
  const continuity = readContinuityStamp();
19543
19825
  checks.push(
19544
19826
  buildContinuityFreshnessCheck({
@@ -19549,38 +19831,40 @@ async function runDoctor(opts, io = consoleIo) {
19549
19831
  })
19550
19832
  );
19551
19833
  }
19552
- const dashboardConsumer = await resolveDashboardConsumer(cfg);
19553
- const isDashboardConsumer = dashboardConsumer.isConsumer;
19554
- const uiSnapshot = designSystemSnapshot(process.cwd());
19555
- const uiLatestVersion = isDashboardConsumer && uiSnapshot.packageName ? await fetchUiPackageLatestVersion(uiSnapshot.packageName) : void 0;
19556
- let designSystemCheck = dashboardConsumer.registryReadFailed ? buildDesignSystemRegistryReadCheck(dashboardConsumer.registryReadFailed) : buildDesignSystemVersionCheck({
19557
- ...uiSnapshot,
19558
- isConsumerRepo: isDashboardConsumer,
19559
- latestVersion: uiLatestVersion
19560
- });
19561
- if (!designSystemCheck.ok && (repairFull || repairLocal) && designSystemCheck.packageName) {
19562
- designSystemCheck = await applyDesignSystemUpdate(designSystemCheck, (m) => io.err(m));
19563
- if (designSystemCheck.ok) {
19564
- io.err(` \u21BB updated ${designSystemCheck.packageName} \u2192 ${designSystemCheck.installedVersion ?? designSystemCheck.latestVersion ?? "latest"}`);
19834
+ if (runExtended) {
19835
+ const dashboardConsumer = await resolveDashboardConsumer(cfg);
19836
+ const isDashboardConsumer = dashboardConsumer.isConsumer;
19837
+ const uiSnapshot = designSystemSnapshot(process.cwd());
19838
+ const uiLatestVersion = isDashboardConsumer && uiSnapshot.packageName ? await fetchUiPackageLatestVersion(uiSnapshot.packageName) : void 0;
19839
+ let designSystemCheck = dashboardConsumer.registryReadFailed ? buildDesignSystemRegistryReadCheck(dashboardConsumer.registryReadFailed) : buildDesignSystemVersionCheck({
19840
+ ...uiSnapshot,
19841
+ isConsumerRepo: isDashboardConsumer,
19842
+ latestVersion: uiLatestVersion
19843
+ });
19844
+ if (!designSystemCheck.ok && (repairFull || repairLocal) && designSystemCheck.packageName) {
19845
+ designSystemCheck = await applyDesignSystemUpdate(designSystemCheck, (m) => io.err(m));
19846
+ if (designSystemCheck.ok) {
19847
+ io.err(` \u21BB updated ${designSystemCheck.packageName} \u2192 ${designSystemCheck.installedVersion ?? designSystemCheck.latestVersion ?? "latest"}`);
19848
+ }
19565
19849
  }
19566
- }
19567
- checks.push(designSystemCheck);
19568
- const registryTargetVersion = designSystemCheck.latestVersion ?? designSystemCheck.installedVersion ?? uiLatestVersion;
19569
- let registryComponentsCheck = dashboardConsumer.registryReadFailed ? buildRegistryComponentsRegistryReadCheck(dashboardConsumer.registryReadFailed) : buildRegistryComponentsCheck({
19570
- ...await gatherRegistryComponentsState(process.cwd(), registryTargetVersion, { fetch }),
19571
- isConsumerRepo: isDashboardConsumer
19572
- });
19573
- if (!registryComponentsCheck.ok && (repairFull || repairLocal) && repoWritesAllowed && registryComponentsCheck.components?.length) {
19574
- registryComponentsCheck = await applyRegistryComponentsSyncCheck(
19575
- registryComponentsCheck,
19576
- registryTargetVersion,
19577
- (m) => io.err(m)
19578
- );
19579
- if (registryComponentsCheck.ok) {
19580
- io.err(` \u21BB synced ${registryComponentsCheck.components?.length ?? 0} registry component(s) \u2192 .mmi/design-system/components`);
19850
+ checks.push(designSystemCheck);
19851
+ const registryTargetVersion = designSystemCheck.latestVersion ?? designSystemCheck.installedVersion ?? uiLatestVersion;
19852
+ let registryComponentsCheck = dashboardConsumer.registryReadFailed ? buildRegistryComponentsRegistryReadCheck(dashboardConsumer.registryReadFailed) : buildRegistryComponentsCheck({
19853
+ ...await gatherRegistryComponentsState(process.cwd(), registryTargetVersion, { fetch }),
19854
+ isConsumerRepo: isDashboardConsumer
19855
+ });
19856
+ if (!registryComponentsCheck.ok && (repairFull || repairLocal) && repoWritesAllowed && registryComponentsCheck.components?.length) {
19857
+ registryComponentsCheck = await applyRegistryComponentsSyncCheck(
19858
+ registryComponentsCheck,
19859
+ registryTargetVersion,
19860
+ (m) => io.err(m)
19861
+ );
19862
+ if (registryComponentsCheck.ok) {
19863
+ io.err(` \u21BB synced ${registryComponentsCheck.components?.length ?? 0} registry component(s) \u2192 .mmi/design-system/components`);
19864
+ }
19581
19865
  }
19866
+ checks.push(registryComponentsCheck);
19582
19867
  }
19583
- checks.push(registryComponentsCheck);
19584
19868
  const gaps = checks.filter((c) => !c.ok);
19585
19869
  if (opts.preflight) {
19586
19870
  if (healPlan.needsEagerHeal) io.err(doctorPreflightDoneLine(surface));
@@ -19609,6 +19893,12 @@ async function runDoctor(opts, io = consoleIo) {
19609
19893
  io.log(JSON.stringify(buildDoctorJsonPayload({ checks, updateReport, resources }), null, 2));
19610
19894
  return;
19611
19895
  }
19896
+ if (terseOutput) {
19897
+ if (pluginReloadRequired) io.err(pluginAutonomousHaltLine(reloadHint));
19898
+ for (const line of renderTerseDoctorReport({ gaps, updateReport })) io.log(line);
19899
+ for (const r of resources) io.log(`Resource: ${r.label} \u2014 ${r.url}`);
19900
+ return;
19901
+ }
19612
19902
  for (const c of checks) io.log(c.ok ? `\u2713 ${c.label}` : `\u2717 ${c.label}
19613
19903
  \u2192 ${c.fix}`);
19614
19904
  for (const r of resources) io.log(`Resource: ${r.label} \u2014 ${r.url}`);
@@ -22450,7 +22740,7 @@ designSystem.command("registry").description("compare .mmi/design-system/compone
22450
22740
  if (!check.ok) console.log(` fix: ${check.fix}`);
22451
22741
  process.exitCode = check.ok ? 0 : 1;
22452
22742
  });
22453
- program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, Hub API default, plugin git clone, plugin install record, .gitignore managed block, plugin config drift, installed plugin version, Cursor Team Marketplace plugin install, @mutmutco UI npm package freshness, @mutmutco registry component cache, Playwright MCP vision caps, Playwright MCP output dir, browser artifact hygiene, scratch housekeeping, stale git worktrees/branches), print fixes, and report per-surface versions + update recipes").option("--banner", "one-line resume summary; silent when all gates pass").option("--preflight", "eager version/plugin heal with upfront notice when stale; silent when healthy (#1871)").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (read-only inspection \u2014 performs no repairs)").option("--apply", "perform the same auto-repairs as the interactive run (combine with --json for a machine-readable repair run)").option("--no-repo-writes", "env/plugin repairs only \u2014 never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for train preflights)").action((opts) => (
22743
+ program2.command("doctor").description("check onboarding gates and auto-heal CLI/plugin wiring; use --verbose for the full audit checklist").option("--banner", "one-line resume summary; silent when all gates pass").option("--preflight", "eager version/plugin heal with upfront notice when stale; silent when healthy (#1871)").option("--verbose", "run extended audit checks and print the full checklist + version report (#1989)").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (read-only inspection \u2014 performs no repairs)").option("--apply", "perform the same auto-repairs as the interactive run (combine with --json for a machine-readable repair run)").option("--no-repo-writes", "env/plugin repairs only \u2014 never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for train preflights)").action((opts) => (
22454
22744
  // Commander maps `--no-repo-writes` to `repoWrites: false`; translate to the explicit `noRepoWrites` flag.
22455
22745
  runDoctor({ ...opts, noRepoWrites: opts.repoWrites === false })
22456
22746
  ));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "2.49.0",
3
+ "version": "2.50.1",
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",