@mutmutco/hub 4.1.9 → 4.1.11

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/index.cjs +495 -245
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -23,6 +23,8 @@ var index_exports = {};
23
23
  __export(index_exports, {
24
24
  acquireSharedDocLock: () => acquireSharedDocLock,
25
25
  buildWatcherCommand: () => buildWatcherCommand,
26
+ claudeArm: () => claudeArm,
27
+ claudeHostEvidence: () => claudeHostEvidence,
26
28
  clearLaunchTarget: () => clearLaunchTarget,
27
29
  clearPending: () => clearPending,
28
30
  enumerateSurfaces: () => enumerateSurfaces,
@@ -30,7 +32,9 @@ __export(index_exports, {
30
32
  formatReconcileReport: () => formatReconcileReport,
31
33
  hermesArm: () => hermesArm,
32
34
  hermesHome: () => hermesHome,
35
+ hermesHostCommand: () => hermesHostCommand,
33
36
  hermesHostEvidence: () => hermesHostEvidence,
37
+ hermesHostPresent: () => hermesHostPresent,
34
38
  hubStatus: () => hubStatus,
35
39
  launchdPlist: () => launchdPlist,
36
40
  launcherSh: () => launcherSh,
@@ -45,9 +49,11 @@ __export(index_exports, {
45
49
  parseNpmDistIntegrity: () => parseNpmDistIntegrity,
46
50
  pendingPath: () => pendingPath,
47
51
  pidAlive: () => pidAlive,
52
+ probeClaudeLaunchStaging: () => probeClaudeLaunchStaging,
48
53
  probeCursorLaunchStaging: () => probeCursorLaunchStaging,
49
54
  probeHermesInstallation: () => probeHermesInstallation,
50
55
  probeHermesLaunchStaging: () => probeHermesLaunchStaging,
56
+ probeJervcodeInstallation: () => probeJervcodeInstallation,
51
57
  probeKimiLaunchStaging: () => probeKimiLaunchStaging,
52
58
  probeLaunchTargetVersion: () => probeLaunchTargetVersion,
53
59
  progressiveRow: () => progressiveRow,
@@ -400,66 +406,39 @@ function checkAtomicity(cachePath, tagCommit, bom, candidate, npm) {
400
406
  }
401
407
 
402
408
  // src/surfaces.ts
403
- var import_node_fs4 = require("node:fs");
409
+ var import_node_fs8 = require("node:fs");
410
+ var import_node_os3 = require("node:os");
411
+ var import_node_path7 = require("node:path");
412
+
413
+ // src/arm-host-plugins.ts
414
+ var import_node_child_process5 = require("node:child_process");
415
+ var import_node_fs7 = require("node:fs");
404
416
  var import_node_os2 = require("node:os");
405
- var import_node_path3 = require("node:path");
406
- function kimiHome(env = process.env) {
407
- return env.KIMI_CODE_HOME?.trim() || (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".kimi-code");
408
- }
409
- function jervcodeAgentDir(env = process.env) {
410
- return env.MMI_UPDATER_JERVCODE_AGENT_DIR?.trim() || env.JERVCODE_CODING_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim() || (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".jerv", "agent");
411
- }
412
- function hermesHome(env = process.env, platform = process.platform) {
413
- if (env.HERMES_HOME?.trim()) return env.HERMES_HOME.trim();
414
- if (platform === "win32") return (0, import_node_path3.join)(env.LOCALAPPDATA?.trim() || (0, import_node_path3.join)((0, import_node_os2.homedir)(), "AppData", "Local"), "hermes");
415
- return (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".hermes");
416
- }
417
- function enumerateSurfaces(env = process.env) {
418
- const home = (0, import_node_os2.homedir)();
419
- const codexHome = env.CODEX_HOME || (0, import_node_path3.join)(home, ".codex");
420
- return [
421
- { id: "claude", marker: (0, import_node_path3.join)(home, ".claude", "plugins", "installed_plugins.json") },
422
- { id: "codex", marker: (0, import_node_path3.join)(codexHome, "plugins", "installed_plugins.json") },
423
- // The host, not the payload (#4951, #5356): this marker used to be plugins/local/mmi — the MMI
424
- // plugin tree itself — so a machine running Cursor WITHOUT the plugin enumerated as "no Cursor"
425
- // and the arm could never make the first install. The plugin's absence is a converge action, not
426
- // a skip. The host root mirrors surfaceConfigRoot('cursor') in cli/src/plugin-guard-io.ts.
427
- { id: "cursor", marker: (0, import_node_path3.join)(home, ".cursor") },
428
- { id: "kilo", marker: (0, import_node_path3.join)(home, ".config", "kilo") },
429
- // The host, not the payload (#4951): this marker used to be plugins/managed/mmi — the MMI plugin
430
- // itself — so a machine running Kimi WITHOUT the plugin enumerated as "no Kimi" and the arm
431
- // could never make the first install. The plugin's absence is a converge action, not a skip.
432
- { id: "kimi", marker: kimiHome(env) },
433
- { id: "jervcode", marker: (0, import_node_path3.join)(jervcodeAgentDir(env), "settings.json") },
434
- // The host root is the marker, not plugins/mmi: an installed Hermes without MMI must be
435
- // discovered so its native plugin tree can be converged for the first time.
436
- { id: "hermes", marker: hermesHome(env) }
437
- ].map((probe) => ({ ...probe, present: (0, import_node_fs4.existsSync)(probe.marker) }));
438
- }
417
+ var import_node_path6 = require("node:path");
439
418
 
440
- // src/arm-cli.ts
441
- var import_node_fs6 = require("node:fs");
419
+ // src/arm-npm-global.ts
420
+ var import_node_fs5 = require("node:fs");
442
421
  var import_node_child_process4 = require("node:child_process");
443
- var import_node_path5 = require("node:path");
422
+ var import_node_path4 = require("node:path");
444
423
 
445
424
  // src/pending.ts
446
425
  var import_node_child_process3 = require("node:child_process");
447
- var import_node_fs5 = require("node:fs");
448
- var import_node_path4 = require("node:path");
426
+ var import_node_fs4 = require("node:fs");
427
+ var import_node_path3 = require("node:path");
449
428
  function hubDistPath(env) {
450
429
  const root = globalRoot(env);
451
- return root ? (0, import_node_path4.join)(root, "@mutmutco", "hub", "dist", "index.cjs") : null;
430
+ return root ? (0, import_node_path3.join)(root, "@mutmutco", "hub", "dist", "index.cjs") : null;
452
431
  }
453
432
  var WATCHER_MAX_WAIT_MS = 4 * 60 * 60 * 1e3;
454
433
  function pendingPath(paths) {
455
- return (0, import_node_path4.join)(paths.root, "pending.json");
434
+ return (0, import_node_path3.join)(paths.root, "pending.json");
456
435
  }
457
436
  function watcherLockPath(paths) {
458
- return (0, import_node_path4.join)(paths.root, "pending-watcher.lock.d");
437
+ return (0, import_node_path3.join)(paths.root, "pending-watcher.lock.d");
459
438
  }
460
439
  function readPending(paths) {
461
440
  try {
462
- const parsed = JSON.parse((0, import_node_fs5.readFileSync)(pendingPath(paths), "utf8"));
441
+ const parsed = JSON.parse((0, import_node_fs4.readFileSync)(pendingPath(paths), "utf8"));
463
442
  if (!Array.isArray(parsed)) return [];
464
443
  return parsed.filter((row) => Boolean(row) && typeof row === "object" && typeof row.surface === "string" && typeof row.target === "string" && Array.isArray(row.images) && Array.isArray(row.dirs));
465
444
  } catch {
@@ -467,12 +446,12 @@ function readPending(paths) {
467
446
  }
468
447
  }
469
448
  function writePending(paths, records) {
470
- (0, import_node_fs5.mkdirSync)(paths.root, { recursive: true });
449
+ (0, import_node_fs4.mkdirSync)(paths.root, { recursive: true });
471
450
  const path = pendingPath(paths);
472
451
  const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
473
- (0, import_node_fs5.writeFileSync)(tmp, `${JSON.stringify(records, null, 2)}
452
+ (0, import_node_fs4.writeFileSync)(tmp, `${JSON.stringify(records, null, 2)}
474
453
  `, "utf8");
475
- (0, import_node_fs5.renameSync)(tmp, path);
454
+ (0, import_node_fs4.renameSync)(tmp, path);
476
455
  }
477
456
  function clearPending(paths, surface) {
478
457
  const records = readPending(paths);
@@ -541,14 +520,14 @@ function schedulePendingWatcher(paths, env = process.env, seam = {}) {
541
520
  const live = records.find((row) => row.watcherPid && pidAlive(row.watcherPid));
542
521
  if (live) return { scheduled: false, reason: `watcher pid ${live.watcherPid} is already waiting` };
543
522
  const hubDist = seam.hubDist ?? hubDistPath(env);
544
- if (!hubDist || !(0, import_node_fs5.existsSync)(hubDist)) return { scheduled: false, reason: "the installed hub bundle is unresolvable \u2014 the hourly tick remains the retry" };
523
+ if (!hubDist || !(0, import_node_fs4.existsSync)(hubDist)) return { scheduled: false, reason: "the installed hub bundle is unresolvable \u2014 the hourly tick remains the retry" };
545
524
  const command = buildWatcherCommand({
546
525
  images: [...new Set(records.flatMap((row) => row.images))],
547
526
  dirs: [...new Set(records.flatMap((row) => row.dirs))],
548
527
  node: process.execPath,
549
528
  hubDist,
550
- codePath: (0, import_node_path4.join)(paths.root, "pending-result.code"),
551
- logPath: (0, import_node_path4.join)(paths.root, "pending-result.log"),
529
+ codePath: (0, import_node_path3.join)(paths.root, "pending-result.code"),
530
+ logPath: (0, import_node_path3.join)(paths.root, "pending-result.log"),
552
531
  lockPath: watcherLockPath(paths)
553
532
  });
554
533
  try {
@@ -584,88 +563,14 @@ function resumePendingWatcher(paths, env = process.env) {
584
563
  return schedulePendingWatcher(paths, env);
585
564
  }
586
565
 
587
- // src/arm-cli.ts
588
- function globalCliDist(env) {
589
- const root = globalRoot(env);
590
- return root ? (0, import_node_path5.join)(root, "@mutmutco", "cli", "dist", "index.cjs") : null;
591
- }
592
- function probeCliVersion(distPath) {
593
- if (!fileExists(distPath)) return null;
594
- const probe = (0, import_node_child_process4.spawnSync)(process.execPath, [distPath, "--version"], { encoding: "utf8", windowsHide: true });
595
- const version = (probe.stdout || "").trim();
596
- return probe.status === 0 && /^\d+\.\d+\.\d+/.test(version) ? version : null;
597
- }
598
- function probeCliInstallation(env = process.env) {
599
- const location = globalCliDist(env);
600
- const version = location ? probeCliVersion(location) : null;
601
- return { version, location, detail: version ? `absolute binary reported ${version}` : "global @mutmutco/cli binary was not readable" };
602
- }
603
- function cliArm(options) {
604
- const env = options.env ?? process.env;
605
- const { target, dryRun, paths } = options;
606
- const dist = globalCliDist(env);
607
- const installed = dist ? probeCliVersion(dist) : null;
608
- if (installed && compareSemver(installed, target) > 0) {
609
- return { surface: "cli", from: installed, to: installed, verdict: "skip", detail: `installed ${installed} is above candidate ${target} (monotonic)` };
610
- }
611
- const highWater = journalHighWater(paths.journalPath, "cli");
612
- if (highWater && compareSemver(highWater, target) > 0) {
613
- return { surface: "cli", from: installed, to: highWater, verdict: "skip", detail: `journal high-water ${highWater} is above candidate ${target} (monotonic)` };
614
- }
615
- if (installed === target) {
616
- return { surface: "cli", from: installed, to: target, verdict: "ok", detail: `already at target ${target}` };
617
- }
618
- if (dryRun) {
619
- return { surface: "cli", from: installed, to: target, verdict: "ok", detail: `dry-run: would converge ${installed ?? "absent"} -> ${target}` };
620
- }
621
- const stageRoot = (0, import_node_path5.join)(paths.staging, "cli", target);
622
- const stageDist = (0, import_node_path5.join)(stageRoot, "node_modules", "@mutmutco", "cli", "dist", "index.cjs");
623
- if (!fileExists(stageDist)) {
624
- (0, import_node_fs6.mkdirSync)(stageRoot, { recursive: true });
625
- (0, import_node_fs6.writeFileSync)((0, import_node_path5.join)(stageRoot, "package.json"), JSON.stringify({ name: "mmi-updater-stage", private: true }) + "\n");
626
- const install = runNpm(["install", "--prefix", stageRoot, "--no-audit", "--no-fund", "--no-package-lock", `@mutmutco/cli@${target}`], env);
627
- if (install.status !== 0) {
628
- return { surface: "cli", from: installed, to: target, verdict: "defer", detail: `staging install failed: ${(install.stderr || install.stdout).split("\n").filter(Boolean).slice(-3).join(" | ")}` };
629
- }
630
- }
631
- let stagedManifestVersion;
632
- try {
633
- stagedManifestVersion = JSON.parse((0, import_node_fs6.readFileSync)((0, import_node_path5.join)(stageRoot, "node_modules", "@mutmutco", "cli", "package.json"), "utf8")).version;
634
- } catch {
635
- }
636
- if (stagedManifestVersion !== target) {
637
- return { surface: "cli", from: installed, to: target, verdict: "defer", detail: `staged manifest is ${stagedManifestVersion ?? "unreadable"}, expected ${target}` };
638
- }
639
- const stagedVersion = probeCliVersion(stageDist);
640
- if (stagedVersion !== target) {
641
- return { surface: "cli", from: installed, to: target, verdict: "defer", detail: `staged binary probed ${stagedVersion ?? "dead"}, expected ${target}` };
642
- }
643
- const globalInstall = runNpm(["install", "-g", "--no-audit", "--no-fund", `@mutmutco/cli@${target}`], env);
644
- if (globalInstall.status !== 0) {
645
- const reason = `global install failed: ${(globalInstall.stderr || globalInstall.stdout).split("\n").filter(Boolean).slice(-3).join(" | ")}`;
646
- const root = globalRoot(env);
647
- const pending = lockShaped(reason) && root ? ` \u2014 ${noteBusyDefer(paths, env, { surface: "cli", target, reason, dirs: [root] })}` : "";
648
- return { surface: "cli", from: installed, to: target, verdict: "defer", detail: `${reason}${pending}` };
649
- }
650
- const switchedDist = globalCliDist(env);
651
- const switched = probeCliVersion(switchedDist ?? "");
652
- if (switched !== target) {
653
- return { surface: "cli", from: installed, to: switched ?? installed ?? "unknown", verdict: "fail", detail: `post-switch probe returned ${switched ?? "dead"} at ${switchedDist ?? "an unresolvable path"}, expected ${target}; the global install already ran \u2014 nothing was rolled back` };
654
- }
655
- return { surface: "cli", from: installed, to: target, verdict: "ok", detail: `converged ${installed ?? "absent"} -> ${target} (staged, verified, switched)` };
656
- }
657
-
658
566
  // src/arm-npm-global.ts
659
- var import_node_fs7 = require("node:fs");
660
- var import_node_child_process5 = require("node:child_process");
661
- var import_node_path6 = require("node:path");
662
567
  function globalDist(env, packageName, distRel) {
663
568
  const root = globalRoot(env);
664
- return root ? (0, import_node_path6.join)(root, ...packageName.split("/"), ...distRel.split("/")) : null;
569
+ return root ? (0, import_node_path4.join)(root, ...packageName.split("/"), ...distRel.split("/")) : null;
665
570
  }
666
571
  function probeVersion(distPath) {
667
572
  if (!fileExists(distPath)) return null;
668
- const probe = (0, import_node_child_process5.spawnSync)(process.execPath, [distPath, "--version"], { encoding: "utf8", windowsHide: true });
573
+ const probe = (0, import_node_child_process4.spawnSync)(process.execPath, [distPath, "--version"], { encoding: "utf8", windowsHide: true });
669
574
  const version = (probe.stdout || "").trim();
670
575
  return probe.status === 0 && /^\d+\.\d+\.\d+/.test(version) ? version : null;
671
576
  }
@@ -674,11 +579,11 @@ function tail(output) {
674
579
  }
675
580
  function stagePackage(options) {
676
581
  const { surface, packageName, target, witness, paths, env } = options;
677
- const stageRoot = (0, import_node_path6.join)(paths.staging, surface, target);
678
- const packageRoot = (0, import_node_path6.join)(stageRoot, "node_modules", ...packageName.split("/"));
679
- if (!fileExists((0, import_node_path6.join)(packageRoot, ...witness.split("/")))) {
680
- (0, import_node_fs7.mkdirSync)(stageRoot, { recursive: true });
681
- (0, import_node_fs7.writeFileSync)((0, import_node_path6.join)(stageRoot, "package.json"), JSON.stringify({ name: "mmi-updater-stage", private: true }) + "\n");
582
+ const stageRoot = (0, import_node_path4.join)(paths.staging, surface, target);
583
+ const packageRoot = (0, import_node_path4.join)(stageRoot, "node_modules", ...packageName.split("/"));
584
+ if (!fileExists((0, import_node_path4.join)(packageRoot, ...witness.split("/")))) {
585
+ (0, import_node_fs5.mkdirSync)(stageRoot, { recursive: true });
586
+ (0, import_node_fs5.writeFileSync)((0, import_node_path4.join)(stageRoot, "package.json"), JSON.stringify({ name: "mmi-updater-stage", private: true }) + "\n");
682
587
  const install = runNpm(["install", "--prefix", stageRoot, "--no-audit", "--no-fund", "--no-package-lock", `${packageName}@${target}`], env);
683
588
  if (install.status !== 0) {
684
589
  return { defer: `staging install failed: ${tail(install.stderr || install.stdout)}` };
@@ -686,7 +591,7 @@ function stagePackage(options) {
686
591
  }
687
592
  let stagedManifestVersion;
688
593
  try {
689
- stagedManifestVersion = JSON.parse((0, import_node_fs7.readFileSync)((0, import_node_path6.join)(packageRoot, "package.json"), "utf8")).version;
594
+ stagedManifestVersion = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(packageRoot, "package.json"), "utf8")).version;
690
595
  } catch {
691
596
  }
692
597
  if (stagedManifestVersion !== target) {
@@ -718,7 +623,7 @@ function npmGlobalArm(options) {
718
623
  if ("defer" in staged) {
719
624
  return result(installed, target, "defer", staged.defer);
720
625
  }
721
- const stageDist = (0, import_node_path6.join)(staged.packageRoot, ...distRel.split("/"));
626
+ const stageDist = (0, import_node_path4.join)(staged.packageRoot, ...distRel.split("/"));
722
627
  const stagedVersion = probeVersion(stageDist);
723
628
  if (stagedVersion !== target) {
724
629
  return result(installed, target, "defer", `staged binary probed ${stagedVersion ?? "dead"}, expected ${target}`);
@@ -738,21 +643,56 @@ function npmGlobalArm(options) {
738
643
  return result(installed, target, "ok", `converged ${installed ?? "absent"} -> ${target} (staged, verified, switched)`);
739
644
  }
740
645
 
741
- // src/arm-self.ts
742
- function selfArm(options) {
743
- return npmGlobalArm({
744
- surface: "updater",
745
- packageName: "@mutmutco/hub",
746
- distRel: "dist/index.cjs",
747
- ...options
646
+ // src/launch-staging.ts
647
+ var import_node_fs6 = require("node:fs");
648
+ var import_node_path5 = require("node:path");
649
+ function readLaunchTarget(path) {
650
+ try {
651
+ const parsed = JSON.parse((0, import_node_fs6.readFileSync)(path, "utf8"));
652
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
653
+ const row = parsed;
654
+ if (typeof row.surface !== "string" || typeof row.target !== "string" || typeof row.since !== "string" || typeof row.incoming !== "string") return null;
655
+ return row;
656
+ } catch {
657
+ return null;
658
+ }
659
+ }
660
+ function writeLaunchTarget(path, record) {
661
+ (0, import_node_fs6.mkdirSync)((0, import_node_path5.join)(path, ".."), { recursive: true });
662
+ const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
663
+ (0, import_node_fs6.writeFileSync)(tmp, `${JSON.stringify(record, null, 2)}
664
+ `, "utf8");
665
+ (0, import_node_fs6.renameSync)(tmp, path);
666
+ }
667
+ function clearLaunchTarget(path) {
668
+ try {
669
+ (0, import_node_fs6.rmSync)(path, { force: true });
670
+ } catch {
671
+ }
672
+ }
673
+ function noteLaunchStaged(paths, env, options, seam = {}) {
674
+ writeLaunchTarget(options.launchTargetPath, {
675
+ surface: options.surface,
676
+ target: options.target,
677
+ since: (/* @__PURE__ */ new Date()).toISOString(),
678
+ incoming: options.incoming
748
679
  });
680
+ return noteBusyDefer(paths, env, {
681
+ surface: options.surface,
682
+ target: options.target,
683
+ reason: options.reason,
684
+ images: options.images,
685
+ dirs: options.dirs
686
+ }, seam);
687
+ }
688
+ function probeLaunchTargetVersion(launchTargetPath, versionAt) {
689
+ const record = readLaunchTarget(launchTargetPath);
690
+ if (!record || !(0, import_node_fs6.existsSync)(record.incoming)) return null;
691
+ const version = versionAt(record.incoming);
692
+ return version === record.target ? record.target : null;
749
693
  }
750
694
 
751
695
  // src/arm-host-plugins.ts
752
- var import_node_child_process6 = require("node:child_process");
753
- var import_node_fs8 = require("node:fs");
754
- var import_node_os3 = require("node:os");
755
- var import_node_path7 = require("node:path");
756
696
  var REPO_URL_DEFAULT = "https://github.com/mutmutco/MMI-Hub.git";
757
697
  var MMI_MARKETPLACE_REPO = "mutmutco/MMI-Hub";
758
698
  function convergedAtOrAbove(installed, target, healthy) {
@@ -762,13 +702,13 @@ function cmdArg(value) {
762
702
  return /^[A-Za-z0-9_@./:\\=+-]+$/.test(value) ? value : `"${value.replaceAll('"', '""')}"`;
763
703
  }
764
704
  var runHostCommand = (command, args, env) => {
765
- const result = process.platform === "win32" ? (0, import_node_child_process6.spawnSync)("cmd.exe", ["/d", "/s", "/c", [command, ...args].map(cmdArg).join(" ")], {
705
+ const result = process.platform === "win32" ? (0, import_node_child_process5.spawnSync)("cmd.exe", ["/d", "/s", "/c", [command, ...args].map(cmdArg).join(" ")], {
766
706
  encoding: "utf8",
767
707
  env,
768
708
  input: "",
769
709
  timeout: 12e4,
770
710
  windowsHide: true
771
- }) : (0, import_node_child_process6.spawnSync)(command, args, {
711
+ }) : (0, import_node_child_process5.spawnSync)(command, args, {
772
712
  encoding: "utf8",
773
713
  env,
774
714
  input: "",
@@ -794,14 +734,18 @@ function claudeMmi(runner, env) {
794
734
  if (!Array.isArray(parsed)) return { row: null, error: `claude plugin list --json failed: ${failure(result)}` };
795
735
  return { row: parsed.find((row) => row.id === "mmi@mutmutco") ?? null };
796
736
  }
737
+ function claudeHomeDir(env) {
738
+ const home = env.HOME?.trim() || env.USERPROFILE?.trim() || (0, import_node_os2.homedir)();
739
+ return (0, import_node_path6.join)(home, ".claude");
740
+ }
797
741
  function claudeMarketplacesPath(env) {
798
- return env.MMI_UPDATER_CLAUDE_MARKETPLACES || (0, import_node_path7.join)((0, import_node_os3.homedir)(), ".claude", "plugins", "known_marketplaces.json");
742
+ return env.MMI_UPDATER_CLAUDE_MARKETPLACES || (0, import_node_path6.join)(claudeHomeDir(env), "plugins", "known_marketplaces.json");
799
743
  }
800
744
  function ensureClaudeMarketplace(env) {
801
745
  const path = claudeMarketplacesPath(env);
802
746
  let body;
803
747
  try {
804
- body = JSON.parse((0, import_node_fs8.readFileSync)(path, "utf8"));
748
+ body = JSON.parse((0, import_node_fs7.readFileSync)(path, "utf8"));
805
749
  } catch (error) {
806
750
  if (error?.code === "ENOENT") return { ok: false, detail: `${path} does not exist \u2014 mutmutco marketplace is not registered`, unregistered: true };
807
751
  return { ok: false, detail: `cannot read ${path}` };
@@ -821,54 +765,185 @@ function ensureClaudeMarketplace(env) {
821
765
  }
822
766
  return { ok: true };
823
767
  }
768
+ var CLAUDE_PACKAGE = "@mutmutco/claude-plugin";
769
+ var CLAUDE_MANIFEST = ".claude-plugin/plugin.json";
770
+ var CLAUDE_TREE_MARKERS = [".claude-plugin/plugin.json", "hooks/hooks.json", "bin/mmi-hook"];
771
+ function claudePluginsRoot(env) {
772
+ return (0, import_node_path6.join)(claudeHomeDir(env), "plugins");
773
+ }
774
+ function claudeMarkerVersion(root) {
775
+ try {
776
+ const version = JSON.parse((0, import_node_fs7.readFileSync)((0, import_node_path6.join)(root, ...CLAUDE_MANIFEST.split("/")), "utf8")).version;
777
+ return typeof version === "string" ? version : null;
778
+ } catch {
779
+ return null;
780
+ }
781
+ }
782
+ function claudeTreeHealthy(root) {
783
+ return CLAUDE_TREE_MARKERS.every((rel) => fileExists((0, import_node_path6.join)(root, ...rel.split("/"))));
784
+ }
785
+ var CLAUDE_PROCESS_SEGMENT = /^claude(-code|-cli)?(\.(exe|cmd|bat|ps1|js|mjs|cjs|py))?$/i;
786
+ var HUB_UPDATE_COMMAND = /(?:\bmmi-hub(?:\.(?:cmd|exe|bat|ps1))?\b|@mutmutco[/\\]hub[/\\]dist[/\\]index\.(?:cjs|mjs|js))[\s'"]+update\b/i;
787
+ function claudeProcessToken(line) {
788
+ if (HUB_UPDATE_COMMAND.test(line)) return null;
789
+ for (const token of line.replace(/["']/g, " ").split(/\s+/)) {
790
+ for (const segment of token.split(/[/\\]/)) {
791
+ if (CLAUDE_PROCESS_SEGMENT.test(segment)) return segment;
792
+ }
793
+ }
794
+ return null;
795
+ }
796
+ function realClaudeProcessListing() {
797
+ const result = process.platform === "win32" ? (0, import_node_child_process5.spawnSync)("powershell.exe", ["-NoProfile", "-Command", "Get-CimInstance Win32_Process | ForEach-Object { $_.Name + ' ' + $_.CommandLine }"], { encoding: "utf8", windowsHide: true, timeout: 15e3 }) : (0, import_node_child_process5.spawnSync)("ps", ["-eo", "args="], { encoding: "utf8", windowsHide: true, timeout: 15e3 });
798
+ return result.status === 0 ? result.stdout ?? "" : null;
799
+ }
800
+ function claudeHostEvidence(env, processListProvider) {
801
+ for (const marker of ["CLAUDE_CODE_SESSION_ID", "CLAUDECODE", "CLAUDE_PLUGIN_ROOT"]) {
802
+ if (env[marker]?.trim()) return `env marker ${marker}`;
803
+ }
804
+ const listing = processListProvider ? processListProvider() : realClaudeProcessListing();
805
+ if (listing === null || !listing.trim()) return "the process list is unreadable (unsafe to write)";
806
+ for (const line of listing.split(/\r?\n/)) {
807
+ const token = claudeProcessToken(line);
808
+ if (token) return `a live process names claude (${token})`;
809
+ }
810
+ return null;
811
+ }
812
+ function probeClaudeLaunchStaging(env = process.env) {
813
+ return probeLaunchTargetVersion((0, import_node_path6.join)(claudePluginsRoot(env), "staging", "launch-target.json"), claudeMarkerVersion);
814
+ }
824
815
  function claudeArm(options) {
825
816
  const env = options.env ?? process.env;
826
817
  const runner = options.runner ?? runHostCommand;
818
+ const paths = options.paths ?? ensureState(env);
819
+ const target = options.target;
827
820
  const repoUrl = options.repoUrl ?? (env.MMI_UPDATER_REPO || REPO_URL_DEFAULT);
821
+ const home = claudeHomeDir(env);
822
+ const result = (from2, to, verdict, detail, launchStaged = false) => ({
823
+ surface: "claude",
824
+ from: from2,
825
+ to,
826
+ verdict,
827
+ detail,
828
+ launchStaged: launchStaged || void 0
829
+ });
828
830
  const before = claudeMmi(runner, env);
829
- if (before.error) return { surface: "claude", from: null, to: options.target, verdict: "defer", detail: before.error };
831
+ if (before.error) return result(null, target, "defer", before.error);
830
832
  const from = before.row?.version ?? null;
833
+ if (from && compareSemver(from, target) > 0) {
834
+ return result(from, from, "skip", `installed ${from} is above candidate ${target} (monotonic)`);
835
+ }
836
+ const highWater = journalHighWater(paths.journalPath, "claude");
837
+ if (highWater && compareSemver(highWater, target) > 0) {
838
+ return result(from, highWater, "skip", `journal high-water ${highWater} is above candidate ${target} (monotonic)`);
839
+ }
831
840
  let marketplaceTrust = ensureClaudeMarketplace(env);
832
841
  if (!marketplaceTrust.ok && marketplaceTrust.unregistered) {
833
842
  const add = runner("claude", ["plugin", "marketplace", "add", repoUrl], env);
834
- if (add.status !== 0) return { surface: "claude", from, to: options.target, verdict: "defer", detail: `mutmutco marketplace was not registered and marketplace add failed: ${failure(add)}` };
843
+ if (add.status !== 0) return result(from, target, "defer", `mutmutco marketplace was not registered and marketplace add failed: ${failure(add)}`);
835
844
  marketplaceTrust = ensureClaudeMarketplace(env);
836
845
  }
837
- if (!marketplaceTrust.ok) return { surface: "claude", from, to: options.target, verdict: "defer", detail: marketplaceTrust.detail ?? "cannot trust the mutmutco marketplace registration" };
846
+ if (!marketplaceTrust.ok) return result(from, target, "defer", marketplaceTrust.detail ?? "cannot trust the mutmutco marketplace registration");
847
+ const pluginsRoot = claudePluginsRoot(env);
848
+ const launchTargetPath = (0, import_node_path6.join)(pluginsRoot, "staging", "launch-target.json");
838
849
  if (options.dryRun) {
839
- return { surface: "claude", from, to: options.target, verdict: "ok", detail: `dry-run: would refresh mutmutco and converge mmi@mutmutco ${from ?? "absent"} -> ${options.target}` };
850
+ return result(from, target, "ok", `dry-run: would refresh mutmutco, stage ${target} on the Claude volume, and converge mmi@mutmutco ${from ?? "absent"} -> ${target}`);
840
851
  }
841
852
  const marketplace = runner("claude", ["plugin", "marketplace", "update", "mutmutco"], env);
842
853
  if (marketplace.status !== 0) {
843
- return { surface: "claude", from, to: options.target, verdict: "defer", detail: `marketplace update failed: ${failure(marketplace)}` };
854
+ return result(from, target, "defer", `marketplace update failed: ${failure(marketplace)}`);
844
855
  }
845
856
  let observed = claudeMmi(runner, env);
846
- if (observed.error) return { surface: "claude", from, to: options.target, verdict: "defer", detail: observed.error };
847
- if (observed.row?.version === options.target && observed.row.enabled === true) {
848
- return { surface: "claude", from, to: options.target, verdict: "ok", detail: `catalog refreshed; installed+enabled at target ${options.target}` };
857
+ if (observed.error) return result(from, target, "defer", observed.error);
858
+ if (observed.row?.version === target && observed.row.enabled === true) {
859
+ clearLaunchTarget(launchTargetPath);
860
+ return result(from, target, "ok", `catalog refreshed; installed+enabled at target ${target}`);
861
+ }
862
+ const incomingRoot = (0, import_node_path6.join)(pluginsRoot, "staging", "incoming");
863
+ const incoming = (0, import_node_path6.join)(incomingRoot, target);
864
+ const priorLaunch = readLaunchTarget(launchTargetPath);
865
+ const reuseIncoming = priorLaunch?.target === target && priorLaunch.incoming === incoming && claudeMarkerVersion(incoming) === target && claudeTreeHealthy(incoming);
866
+ if (!reuseIncoming) {
867
+ const staged = stagePackage({ surface: "claude", packageName: CLAUDE_PACKAGE, target, witness: CLAUDE_MANIFEST, paths, env });
868
+ if ("defer" in staged) return result(from, target, "defer", staged.defer);
869
+ if (claudeMarkerVersion(staged.packageRoot) !== target || !claudeTreeHealthy(staged.packageRoot)) {
870
+ return result(from, target, "defer", `staged Claude manifest is ${claudeMarkerVersion(staged.packageRoot) ?? "unreadable"} or its tree is incomplete (expected ${target})`);
871
+ }
872
+ try {
873
+ (0, import_node_fs7.mkdirSync)((0, import_node_path6.join)(pluginsRoot, "staging"), { recursive: true });
874
+ (0, import_node_fs7.rmSync)(incomingRoot, { recursive: true, force: true });
875
+ (0, import_node_fs7.cpSync)(staged.packageRoot, incoming, { recursive: true });
876
+ } catch (error) {
877
+ return result(from, target, "defer", `cannot place incoming Claude generation: ${error?.message ?? error}`);
878
+ }
879
+ if (claudeMarkerVersion(incoming) !== target || !claudeTreeHealthy(incoming)) {
880
+ (0, import_node_fs7.rmSync)(incomingRoot, { recursive: true, force: true });
881
+ return result(from, target, "defer", "incoming Claude generation did not reproduce the verified payload");
882
+ }
883
+ }
884
+ const busy = claudeHostEvidence(env, options.processListProvider);
885
+ if (busy) {
886
+ const pending = noteLaunchStaged(paths, env, {
887
+ surface: "claude",
888
+ target,
889
+ reason: busy,
890
+ launchTargetPath,
891
+ incoming,
892
+ images: ["claude"],
893
+ dirs: [home]
894
+ });
895
+ return result(from, target, "ok", `staged ${target} for next launch while Claude Code is running (${busy}) \u2014 ${pending}`, true);
896
+ }
897
+ const started = claudeHostEvidence(env, options.processListProvider);
898
+ if (started) {
899
+ const pending = noteLaunchStaged(paths, env, {
900
+ surface: "claude",
901
+ target,
902
+ reason: started,
903
+ launchTargetPath,
904
+ incoming,
905
+ images: ["claude"],
906
+ dirs: [home]
907
+ });
908
+ return result(from, target, "ok", `staged ${target} for next launch \u2014 Claude Code started during staging (${started}) \u2014 ${pending}`, true);
909
+ }
910
+ const cacheTarget = (0, import_node_path6.join)(pluginsRoot, "cache", "mutmutco", "mmi", target);
911
+ try {
912
+ if (!(0, import_node_fs7.existsSync)(cacheTarget) || claudeMarkerVersion(cacheTarget) !== target || !claudeTreeHealthy(cacheTarget)) {
913
+ (0, import_node_fs7.rmSync)(cacheTarget, { recursive: true, force: true });
914
+ (0, import_node_fs7.mkdirSync)((0, import_node_path6.join)(cacheTarget, ".."), { recursive: true });
915
+ (0, import_node_fs7.cpSync)(incoming, cacheTarget, { recursive: true });
916
+ }
917
+ } catch (error) {
918
+ return result(from, target, "defer", `cannot populate Claude plugin cache: ${error?.message ?? error}`);
849
919
  }
850
920
  const scope = observed.row?.scope || before.row?.scope || "user";
851
921
  const command = observed.row ? ["plugin", "update", "mmi@mutmutco", "--scope", scope] : ["plugin", "install", "mmi@mutmutco", "--scope", scope];
852
922
  const converge = runner("claude", command, env);
853
923
  if (converge.status !== 0) {
854
- return { surface: "claude", from, to: options.target, verdict: "defer", detail: `${observed.row ? "plugin update" : "plugin install"} failed: ${failure(converge)}` };
924
+ return result(from, target, "defer", `${observed.row ? "plugin update" : "plugin install"} failed: ${failure(converge)}`);
855
925
  }
856
926
  observed = claudeMmi(runner, env);
857
- if (observed.error) return { surface: "claude", from, to: options.target, verdict: "defer", detail: observed.error };
858
- if (observed.row && observed.row.version === options.target && observed.row.enabled !== true) {
927
+ if (observed.error) return result(from, target, "defer", observed.error);
928
+ if (observed.row && observed.row.version === target && observed.row.enabled !== true) {
859
929
  const enable = runner("claude", ["plugin", "enable", "mmi@mutmutco", "--scope", observed.row.scope || scope], env);
860
930
  if (enable.status !== 0) {
861
- return { surface: "claude", from, to: options.target, verdict: "defer", detail: `plugin enable failed: ${failure(enable)}` };
931
+ return result(from, target, "defer", `plugin enable failed: ${failure(enable)}`);
862
932
  }
863
933
  observed = claudeMmi(runner, env);
864
934
  }
865
- if (!convergedAtOrAbove(observed.row?.version, options.target, observed.row?.enabled === true)) {
866
- return { surface: "claude", from, to: observed.row?.version ?? "unknown", verdict: "fail", detail: `post-update evidence is version ${observed.row?.version ?? "missing"}, enabled=${String(observed.row?.enabled)}, expected ${options.target}/true` };
935
+ if (!convergedAtOrAbove(observed.row?.version, target, observed.row?.enabled === true)) {
936
+ return result(from, observed.row?.version ?? "unknown", "fail", `post-update evidence is version ${observed.row?.version ?? "missing"}, enabled=${String(observed.row?.enabled)}, expected ${target}/true`);
867
937
  }
868
- if (observed.row.version !== options.target) {
869
- return { surface: "claude", from, to: observed.row.version, verdict: "skip", detail: `installed ${observed.row.version} is above candidate ${options.target} (monotonic)` };
938
+ if (observed.row.version !== target) {
939
+ return result(from, observed.row.version, "skip", `installed ${observed.row.version} is above candidate ${target} (monotonic)`);
870
940
  }
871
- return { surface: "claude", from, to: options.target, verdict: "ok", detail: `catalog refreshed; converged and verified ${from ?? "absent"} -> ${options.target}` };
941
+ clearLaunchTarget(launchTargetPath);
942
+ try {
943
+ (0, import_node_fs7.rmSync)(incomingRoot, { recursive: true, force: true });
944
+ } catch {
945
+ }
946
+ return result(from, target, "ok", `catalog refreshed; converged and verified ${from ?? "absent"} -> ${target}`);
872
947
  }
873
948
  var SNAPSHOT_MISSING = /marketplace snapshot|marketplace root does not contain/i;
874
949
  function codexMmi(runner, env) {
@@ -927,11 +1002,11 @@ function codexArm(options) {
927
1002
  }
928
1003
  var KILO_PACKAGE = "@mutmutco/kilo-plugin";
929
1004
  function kiloConfigPath(env) {
930
- return env.MMI_UPDATER_KILO_CONFIG || (0, import_node_path7.join)((0, import_node_os3.homedir)(), ".config", "kilo", "opencode.json");
1005
+ return env.MMI_UPDATER_KILO_CONFIG || (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".config", "kilo", "opencode.json");
931
1006
  }
932
1007
  function readKiloConfig(env) {
933
1008
  try {
934
- const config = JSON.parse((0, import_node_fs8.readFileSync)(kiloConfigPath(env), "utf8"));
1009
+ const config = JSON.parse((0, import_node_fs7.readFileSync)(kiloConfigPath(env), "utf8"));
935
1010
  const specs = Array.isArray(config.plugin) && config.plugin.every((entry) => typeof entry === "string") ? config.plugin : [];
936
1011
  return { config, specs };
937
1012
  } catch {
@@ -951,8 +1026,8 @@ function normalizeKiloConfig(env, exact) {
951
1026
  const path = kiloConfigPath(env);
952
1027
  const tmp = `${path}.tmp-${process.pid}`;
953
1028
  try {
954
- (0, import_node_fs8.writeFileSync)(tmp, JSON.stringify({ ...current.config, plugin: next }, null, 2) + "\n", "utf8");
955
- (0, import_node_fs8.renameSync)(tmp, path);
1029
+ (0, import_node_fs7.writeFileSync)(tmp, JSON.stringify({ ...current.config, plugin: next }, null, 2) + "\n", "utf8");
1030
+ (0, import_node_fs7.renameSync)(tmp, path);
956
1031
  return null;
957
1032
  } catch (error) {
958
1033
  return `cannot retire legacy Kilo registrations: ${error?.message ?? error}`;
@@ -1014,71 +1089,150 @@ function kiloArm(options) {
1014
1089
  return { surface: "kilo", from, to: options.target, verdict: "ok", detail: `global config verified at ${exact}; ${legacyCount ? `retired ${legacyCount} legacy MMI path${legacyCount === 1 ? "" : "s"}; ` : ""}payload provisions at next session start` };
1015
1090
  }
1016
1091
 
1092
+ // src/surfaces.ts
1093
+ function kimiHome(env = process.env) {
1094
+ return env.KIMI_CODE_HOME?.trim() || (0, import_node_path7.join)((0, import_node_os3.homedir)(), ".kimi-code");
1095
+ }
1096
+ function jervcodeAgentDir(env = process.env) {
1097
+ return env.MMI_UPDATER_JERVCODE_AGENT_DIR?.trim() || env.JERVCODE_CODING_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim() || (0, import_node_path7.join)((0, import_node_os3.homedir)(), ".jerv", "agent");
1098
+ }
1099
+ function hermesHome(env = process.env, platform = process.platform) {
1100
+ if (env.HERMES_HOME?.trim()) return env.HERMES_HOME.trim();
1101
+ if (platform === "win32") return (0, import_node_path7.join)(env.LOCALAPPDATA?.trim() || (0, import_node_path7.join)((0, import_node_os3.homedir)(), "AppData", "Local"), "hermes");
1102
+ return (0, import_node_path7.join)((0, import_node_os3.homedir)(), ".hermes");
1103
+ }
1104
+ function hermesHostCommand(env = process.env) {
1105
+ return env.MMI_UPDATER_HERMES?.trim() || "hermes";
1106
+ }
1107
+ function hermesHostPresent(env = process.env, runner = runHostCommand) {
1108
+ return runner(hermesHostCommand(env), ["--version"], env).status === 0;
1109
+ }
1110
+ function enumerateSurfaces(env = process.env) {
1111
+ const home = env.HOME?.trim() || env.USERPROFILE?.trim() || (0, import_node_os3.homedir)();
1112
+ const codexHome = env.CODEX_HOME || (0, import_node_path7.join)(home, ".codex");
1113
+ return [
1114
+ { id: "claude", marker: (0, import_node_path7.join)(home, ".claude", "plugins", "installed_plugins.json") },
1115
+ { id: "codex", marker: (0, import_node_path7.join)(codexHome, "plugins", "installed_plugins.json") },
1116
+ // The host, not the payload (#4951, #5356): this marker used to be plugins/local/mmi — the MMI
1117
+ // plugin tree itself — so a machine running Cursor WITHOUT the plugin enumerated as "no Cursor"
1118
+ // and the arm could never make the first install. The plugin's absence is a converge action, not
1119
+ // a skip. The host root mirrors surfaceConfigRoot('cursor') in cli/src/plugin-guard-io.ts.
1120
+ { id: "cursor", marker: (0, import_node_path7.join)(home, ".cursor") },
1121
+ { id: "kilo", marker: (0, import_node_path7.join)(home, ".config", "kilo") },
1122
+ // The host, not the payload (#4951): this marker used to be plugins/managed/mmi — the MMI plugin
1123
+ // itself — so a machine running Kimi WITHOUT the plugin enumerated as "no Kimi" and the arm
1124
+ // could never make the first install. The plugin's absence is a converge action, not a skip.
1125
+ { id: "kimi", marker: kimiHome(env) },
1126
+ { id: "jervcode", marker: (0, import_node_path7.join)(jervcodeAgentDir(env), "settings.json") },
1127
+ // Host CLI, not HERMES_HOME (#5673): the config tree can survive an uninstall and still carry
1128
+ // leftover plugin manifests — that residue must not enumerate as an installed host.
1129
+ { id: "hermes", marker: `${hermesHostCommand(env)} --version` }
1130
+ ].map((probe) => ({
1131
+ ...probe,
1132
+ present: probe.id === "hermes" ? hermesHostPresent(env) : (0, import_node_fs8.existsSync)(probe.marker)
1133
+ }));
1134
+ }
1135
+
1136
+ // src/arm-cli.ts
1137
+ var import_node_fs9 = require("node:fs");
1138
+ var import_node_child_process6 = require("node:child_process");
1139
+ var import_node_path8 = require("node:path");
1140
+ function globalCliDist(env) {
1141
+ const root = globalRoot(env);
1142
+ return root ? (0, import_node_path8.join)(root, "@mutmutco", "cli", "dist", "index.cjs") : null;
1143
+ }
1144
+ function probeCliVersion(distPath) {
1145
+ if (!fileExists(distPath)) return null;
1146
+ const probe = (0, import_node_child_process6.spawnSync)(process.execPath, [distPath, "--version"], { encoding: "utf8", windowsHide: true });
1147
+ const version = (probe.stdout || "").trim();
1148
+ return probe.status === 0 && /^\d+\.\d+\.\d+/.test(version) ? version : null;
1149
+ }
1150
+ function probeCliInstallation(env = process.env) {
1151
+ const location = globalCliDist(env);
1152
+ const version = location ? probeCliVersion(location) : null;
1153
+ return { version, location, detail: version ? `absolute binary reported ${version}` : "global @mutmutco/cli binary was not readable" };
1154
+ }
1155
+ function cliArm(options) {
1156
+ const env = options.env ?? process.env;
1157
+ const { target, dryRun, paths } = options;
1158
+ const dist = globalCliDist(env);
1159
+ const installed = dist ? probeCliVersion(dist) : null;
1160
+ if (installed && compareSemver(installed, target) > 0) {
1161
+ return { surface: "cli", from: installed, to: installed, verdict: "skip", detail: `installed ${installed} is above candidate ${target} (monotonic)` };
1162
+ }
1163
+ const highWater = journalHighWater(paths.journalPath, "cli");
1164
+ if (highWater && compareSemver(highWater, target) > 0) {
1165
+ return { surface: "cli", from: installed, to: highWater, verdict: "skip", detail: `journal high-water ${highWater} is above candidate ${target} (monotonic)` };
1166
+ }
1167
+ if (installed === target) {
1168
+ return { surface: "cli", from: installed, to: target, verdict: "ok", detail: `already at target ${target}` };
1169
+ }
1170
+ if (dryRun) {
1171
+ return { surface: "cli", from: installed, to: target, verdict: "ok", detail: `dry-run: would converge ${installed ?? "absent"} -> ${target}` };
1172
+ }
1173
+ const stageRoot = (0, import_node_path8.join)(paths.staging, "cli", target);
1174
+ const stageDist = (0, import_node_path8.join)(stageRoot, "node_modules", "@mutmutco", "cli", "dist", "index.cjs");
1175
+ if (!fileExists(stageDist)) {
1176
+ (0, import_node_fs9.mkdirSync)(stageRoot, { recursive: true });
1177
+ (0, import_node_fs9.writeFileSync)((0, import_node_path8.join)(stageRoot, "package.json"), JSON.stringify({ name: "mmi-updater-stage", private: true }) + "\n");
1178
+ const install = runNpm(["install", "--prefix", stageRoot, "--no-audit", "--no-fund", "--no-package-lock", `@mutmutco/cli@${target}`], env);
1179
+ if (install.status !== 0) {
1180
+ return { surface: "cli", from: installed, to: target, verdict: "defer", detail: `staging install failed: ${(install.stderr || install.stdout).split("\n").filter(Boolean).slice(-3).join(" | ")}` };
1181
+ }
1182
+ }
1183
+ let stagedManifestVersion;
1184
+ try {
1185
+ stagedManifestVersion = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path8.join)(stageRoot, "node_modules", "@mutmutco", "cli", "package.json"), "utf8")).version;
1186
+ } catch {
1187
+ }
1188
+ if (stagedManifestVersion !== target) {
1189
+ return { surface: "cli", from: installed, to: target, verdict: "defer", detail: `staged manifest is ${stagedManifestVersion ?? "unreadable"}, expected ${target}` };
1190
+ }
1191
+ const stagedVersion = probeCliVersion(stageDist);
1192
+ if (stagedVersion !== target) {
1193
+ return { surface: "cli", from: installed, to: target, verdict: "defer", detail: `staged binary probed ${stagedVersion ?? "dead"}, expected ${target}` };
1194
+ }
1195
+ const globalInstall = runNpm(["install", "-g", "--no-audit", "--no-fund", `@mutmutco/cli@${target}`], env);
1196
+ if (globalInstall.status !== 0) {
1197
+ const reason = `global install failed: ${(globalInstall.stderr || globalInstall.stdout).split("\n").filter(Boolean).slice(-3).join(" | ")}`;
1198
+ const root = globalRoot(env);
1199
+ const pending = lockShaped(reason) && root ? ` \u2014 ${noteBusyDefer(paths, env, { surface: "cli", target, reason, dirs: [root] })}` : "";
1200
+ return { surface: "cli", from: installed, to: target, verdict: "defer", detail: `${reason}${pending}` };
1201
+ }
1202
+ const switchedDist = globalCliDist(env);
1203
+ const switched = probeCliVersion(switchedDist ?? "");
1204
+ if (switched !== target) {
1205
+ return { surface: "cli", from: installed, to: switched ?? installed ?? "unknown", verdict: "fail", detail: `post-switch probe returned ${switched ?? "dead"} at ${switchedDist ?? "an unresolvable path"}, expected ${target}; the global install already ran \u2014 nothing was rolled back` };
1206
+ }
1207
+ return { surface: "cli", from: installed, to: target, verdict: "ok", detail: `converged ${installed ?? "absent"} -> ${target} (staged, verified, switched)` };
1208
+ }
1209
+
1210
+ // src/arm-self.ts
1211
+ function selfArm(options) {
1212
+ return npmGlobalArm({
1213
+ surface: "updater",
1214
+ packageName: "@mutmutco/hub",
1215
+ distRel: "dist/index.cjs",
1216
+ ...options
1217
+ });
1218
+ }
1219
+
1017
1220
  // src/arm-kimi.ts
1018
1221
  var import_node_child_process7 = require("node:child_process");
1019
1222
  var import_node_fs12 = require("node:fs");
1020
1223
  var import_node_path9 = require("node:path");
1021
1224
 
1022
1225
  // src/compat.ts
1023
- var import_node_fs9 = require("node:fs");
1226
+ var import_node_fs10 = require("node:fs");
1024
1227
  function readManifestCompat(manifestPath) {
1025
1228
  try {
1026
- const parsed = JSON.parse((0, import_node_fs9.readFileSync)(manifestPath, "utf8"));
1229
+ const parsed = JSON.parse((0, import_node_fs10.readFileSync)(manifestPath, "utf8"));
1027
1230
  return typeof parsed.mmiCompat === "string" ? parsed.mmiCompat : void 0;
1028
1231
  } catch {
1029
1232
  return void 0;
1030
1233
  }
1031
1234
  }
1032
1235
 
1033
- // src/launch-staging.ts
1034
- var import_node_fs10 = require("node:fs");
1035
- var import_node_path8 = require("node:path");
1036
- function readLaunchTarget(path) {
1037
- try {
1038
- const parsed = JSON.parse((0, import_node_fs10.readFileSync)(path, "utf8"));
1039
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
1040
- const row = parsed;
1041
- if (typeof row.surface !== "string" || typeof row.target !== "string" || typeof row.since !== "string" || typeof row.incoming !== "string") return null;
1042
- return row;
1043
- } catch {
1044
- return null;
1045
- }
1046
- }
1047
- function writeLaunchTarget(path, record) {
1048
- (0, import_node_fs10.mkdirSync)((0, import_node_path8.join)(path, ".."), { recursive: true });
1049
- const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
1050
- (0, import_node_fs10.writeFileSync)(tmp, `${JSON.stringify(record, null, 2)}
1051
- `, "utf8");
1052
- (0, import_node_fs10.renameSync)(tmp, path);
1053
- }
1054
- function clearLaunchTarget(path) {
1055
- try {
1056
- (0, import_node_fs10.rmSync)(path, { force: true });
1057
- } catch {
1058
- }
1059
- }
1060
- function noteLaunchStaged(paths, env, options, seam = {}) {
1061
- writeLaunchTarget(options.launchTargetPath, {
1062
- surface: options.surface,
1063
- target: options.target,
1064
- since: (/* @__PURE__ */ new Date()).toISOString(),
1065
- incoming: options.incoming
1066
- });
1067
- return noteBusyDefer(paths, env, {
1068
- surface: options.surface,
1069
- target: options.target,
1070
- reason: options.reason,
1071
- images: options.images,
1072
- dirs: options.dirs
1073
- }, seam);
1074
- }
1075
- function probeLaunchTargetVersion(launchTargetPath, versionAt) {
1076
- const record = readLaunchTarget(launchTargetPath);
1077
- if (!record || !(0, import_node_fs10.existsSync)(record.incoming)) return null;
1078
- const version = versionAt(record.incoming);
1079
- return version === record.target ? record.target : null;
1080
- }
1081
-
1082
1236
  // src/kimi-registration.ts
1083
1237
  function validateKimiInstalledPlugin(document, pluginRoot, id) {
1084
1238
  if (!document || typeof document !== "object" || Array.isArray(document)) return "malformed-document";
@@ -1863,10 +2017,47 @@ function installedVersion(env) {
1863
2017
  return null;
1864
2018
  }
1865
2019
  }
1866
- function probeJervcodeInstallation(env = process.env) {
1867
- const location = (0, import_node_path11.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"));
1868
- const version = installedVersion(env);
1869
- return { version, location, detail: version ? "materialized pi package manifest" : "materialized pi package manifest was unreadable" };
2020
+ function materializedManifestLocation(env) {
2021
+ return (0, import_node_path11.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"));
2022
+ }
2023
+ var PI_LIST_SPEC = new RegExp(`npm:${PI_PACKAGE.replace("/", "\\/")}@(\\S+)`);
2024
+ function parseListText(stdout) {
2025
+ for (const line of stdout.split("\n")) {
2026
+ const match = line.match(PI_LIST_SPEC);
2027
+ if (match) return match[1];
2028
+ }
2029
+ return null;
2030
+ }
2031
+ function probeJervcodeInstallation(env = process.env, runner = runHostCommand) {
2032
+ const manifestLocation = materializedManifestLocation(env);
2033
+ const manifestVersion = installedVersion(env);
2034
+ const manifestEvidence = () => ({
2035
+ version: manifestVersion,
2036
+ location: manifestLocation,
2037
+ detail: manifestVersion ? "materialized pi package manifest" : "materialized pi package manifest was unreadable"
2038
+ });
2039
+ const hosted = hostEnv(env);
2040
+ const command = hostCommands(env).find((bin) => runner(bin, ["--version"], hosted).status === 0);
2041
+ if (!command) return manifestEvidence();
2042
+ const hostLocation = `${command} list`;
2043
+ const result = runner(command, ["list"], hosted);
2044
+ const version = result.status === 0 ? parseListText(result.stdout) : null;
2045
+ if (version) {
2046
+ return {
2047
+ version,
2048
+ location: hostLocation,
2049
+ detail: "native host user package listing"
2050
+ };
2051
+ }
2052
+ const hostError = result.status !== 0 ? `${hostLocation} failed: ${failure2(result)}` : `${hostLocation} did not list npm:${PI_PACKAGE}`;
2053
+ if (manifestVersion) {
2054
+ return {
2055
+ version: manifestVersion,
2056
+ location: manifestLocation,
2057
+ detail: `${hostError}; using materialized pi package manifest`
2058
+ };
2059
+ }
2060
+ return { version: null, location: hostLocation, detail: hostError };
1870
2061
  }
1871
2062
  function isLegacyMmiPiPath(spec) {
1872
2063
  return /[/\\]mutmutco[/\\]mmi[/\\]\d+\.\d+\.\d+[/\\]\.pi-plugin[/\\]?$/i.test(spec);
@@ -1945,7 +2136,7 @@ var import_node_child_process9 = require("node:child_process");
1945
2136
  var import_node_path12 = require("node:path");
1946
2137
  var PACKAGE2 = "@mutmutco/hermes-plugin";
1947
2138
  var MANIFEST2 = "plugin.yaml";
1948
- var TREE_MARKERS2 = [MANIFEST2, "__init__.py", "skills"];
2139
+ var TREE_MARKERS_FALLBACK = [MANIFEST2, "__init__.py", "skills"];
1949
2140
  function yamlVersion(root) {
1950
2141
  try {
1951
2142
  const text = (0, import_node_fs15.readFileSync)((0, import_node_path12.join)(root, MANIFEST2), "utf8");
@@ -1960,14 +2151,36 @@ function yamlVersion(root) {
1960
2151
  return null;
1961
2152
  }
1962
2153
  }
1963
- function treeHealthy3(root) {
1964
- return TREE_MARKERS2.slice(0, 2).every((file) => (0, import_node_fs15.existsSync)((0, import_node_path12.join)(root, file))) && (() => {
1965
- try {
1966
- return (0, import_node_fs15.statSync)((0, import_node_path12.join)(root, "skills")).isDirectory();
1967
- } catch {
1968
- return false;
2154
+ function packageTreeMarkers(root) {
2155
+ try {
2156
+ const files = JSON.parse((0, import_node_fs15.readFileSync)((0, import_node_path12.join)(root, "package.json"), "utf8")).files;
2157
+ if (Array.isArray(files) && files.length && files.every((entry) => typeof entry === "string" && entry.trim())) {
2158
+ return files.map((entry) => entry.trim());
1969
2159
  }
1970
- })();
2160
+ } catch {
2161
+ }
2162
+ return TREE_MARKERS_FALLBACK;
2163
+ }
2164
+ function markerPresent(root, rel) {
2165
+ const path = (0, import_node_path12.join)(root, ...rel.split("/"));
2166
+ if (!(0, import_node_fs15.existsSync)(path)) return false;
2167
+ try {
2168
+ const stat = (0, import_node_fs15.statSync)(path);
2169
+ return stat.isFile() || stat.isDirectory();
2170
+ } catch {
2171
+ return false;
2172
+ }
2173
+ }
2174
+ function treeHealth(root) {
2175
+ const missing = packageTreeMarkers(root).filter((rel) => !markerPresent(root, rel));
2176
+ return { healthy: missing.length === 0, missing };
2177
+ }
2178
+ function treeHealthy3(root) {
2179
+ return treeHealth(root).healthy;
2180
+ }
2181
+ function treeIncompleteDetail(root, label) {
2182
+ const { healthy, missing } = treeHealth(root);
2183
+ return healthy ? null : `${label} tree is missing ${missing.join(", ")}`;
1971
2184
  }
1972
2185
  function packageIdentity(root) {
1973
2186
  try {
@@ -1994,14 +2207,14 @@ function message2(error) {
1994
2207
  return String(error?.message ?? error).replace(/\s+/g, " ").slice(0, 200);
1995
2208
  }
1996
2209
  var HERMES_PROCESS_SEGMENT = /^hermes(-cli|-code)?(\.(exe|cmd|bat|ps1|js|mjs|cjs|py))?$/i;
1997
- var HUB_UPDATE_COMMAND = /(?:\bmmi-hub(?:\.(?:cmd|exe|bat|ps1))?\b|@mutmutco[/\\]hub[/\\]dist[/\\]index\.(?:cjs|mjs|js))[\s'"]+update\b/i;
2210
+ var HUB_UPDATE_COMMAND2 = /(?:\bmmi-hub(?:\.(?:cmd|exe|bat|ps1))?\b|@mutmutco[/\\]hub[/\\]dist[/\\]index\.(?:cjs|mjs|js))[\s'"]+update\b/i;
1998
2211
  function hermesProcessToken(line, home) {
1999
2212
  const normalized = line.replace(/["']/g, " ");
2000
2213
  const tokens = normalized.split(/\s+/);
2001
2214
  const hermesToken = (candidates) => candidates.find((token) => token && token.split(/[/\\]/).some((segment) => HERMES_PROCESS_SEGMENT.test(segment))) ?? null;
2002
2215
  const direct = hermesToken(tokens.slice(0, 2));
2003
2216
  if (direct) return direct;
2004
- if (HUB_UPDATE_COMMAND.test(line)) return null;
2217
+ if (HUB_UPDATE_COMMAND2.test(line)) return null;
2005
2218
  return hermesToken(tokens) ?? (home && normalized.toLowerCase().includes(home.toLowerCase()) ? home : null);
2006
2219
  }
2007
2220
  function realProcessListing() {
@@ -2050,7 +2263,9 @@ function hermesArm(options) {
2050
2263
  detail,
2051
2264
  launchStaged: launchStaged || void 0
2052
2265
  });
2053
- if (!(0, import_node_fs15.existsSync)(home)) return result(null, options.target, "skip", `absent (${home}) \u2014 skipped without writes`);
2266
+ if (!hermesHostPresent(env, options.hostCommandRunner)) {
2267
+ return result(null, options.target, "skip", `host absent (${hermesHostCommand(env)} CLI not on PATH) \u2014 skipped without writes`);
2268
+ }
2054
2269
  if ((0, import_node_fs15.existsSync)(live) && !mmiOwned2(live)) {
2055
2270
  return result(installed, options.target, "defer", `${live} carries no ${PACKAGE2} package identity \u2014 refusing to displace an unmanaged Hermes mmi plugin`);
2056
2271
  }
@@ -2065,9 +2280,12 @@ function hermesArm(options) {
2065
2280
  if (options.dryRun) return result(installed, options.target, "ok", `dry-run: would stage ${PACKAGE2}@${options.target}, atomically switch ${installed ?? "absent"} -> ${options.target}, then provision skills into ${(0, import_node_path12.join)(home, "skills", "mmi")}`);
2066
2281
  const staged = stagePackage({ surface: "hermes", packageName: PACKAGE2, target: options.target, witness: MANIFEST2, paths: options.paths, env });
2067
2282
  if ("defer" in staged) return result(installed, options.target, "defer", staged.defer);
2068
- if (yamlVersion(staged.packageRoot) !== options.target || !treeHealthy3(staged.packageRoot)) {
2069
- return result(installed, options.target, "defer", `staged Hermes manifest is ${yamlVersion(staged.packageRoot) ?? "unreadable"} or its tree is incomplete (expected ${options.target})`);
2283
+ const stagedVersion = yamlVersion(staged.packageRoot);
2284
+ if (stagedVersion !== options.target) {
2285
+ return result(installed, options.target, "defer", `staged Hermes manifest is ${stagedVersion ?? "unreadable"}, expected ${options.target}`);
2070
2286
  }
2287
+ const stagedTree = treeIncompleteDetail(staged.packageRoot, "staged Hermes");
2288
+ if (stagedTree) return result(installed, options.target, "defer", stagedTree);
2071
2289
  const scratch = (0, import_node_path12.join)(home, ".mmi-updater");
2072
2290
  const launchTargetPath = (0, import_node_path12.join)(scratch, "launch-target.json");
2073
2291
  const incomingRoot = (0, import_node_path12.join)(scratch, "incoming");
@@ -2084,9 +2302,15 @@ function hermesArm(options) {
2084
2302
  } catch (error) {
2085
2303
  return result(installed, options.target, "defer", `cannot place incoming Hermes generation: ${message2(error)}`);
2086
2304
  }
2087
- if (yamlVersion(incoming) !== options.target || !treeHealthy3(incoming)) {
2305
+ const incomingVersion = yamlVersion(incoming);
2306
+ if (incomingVersion !== options.target) {
2088
2307
  discard2(incomingRoot);
2089
- return result(installed, options.target, "defer", "incoming Hermes generation did not reproduce the verified payload");
2308
+ return result(installed, options.target, "defer", `incoming Hermes manifest is ${incomingVersion ?? "unreadable"}, expected ${options.target}`);
2309
+ }
2310
+ const incomingTree = treeIncompleteDetail(incoming, "incoming Hermes");
2311
+ if (incomingTree) {
2312
+ discard2(incomingRoot);
2313
+ return result(installed, options.target, "defer", incomingTree);
2090
2314
  }
2091
2315
  }
2092
2316
  const busy = hermesHostEvidence(env, home, options.processListProvider);
@@ -2116,16 +2340,19 @@ function hermesArm(options) {
2116
2340
  const pending = lockShaped(reason) ? ` \u2014 ${noteBusyDefer(options.paths, env, { surface: "hermes", target: options.target, reason, images: ["hermes"], dirs: [home] })}` : "";
2117
2341
  return result(installed, options.target, "defer", `Hermes generation switch failed; previous generation retained: ${reason}${pending}`);
2118
2342
  }
2119
- if (yamlVersion(live) !== options.target || !treeHealthy3(live)) {
2343
+ const liveVersion = yamlVersion(live);
2344
+ const liveTree = treeIncompleteDetail(live, "live Hermes");
2345
+ if (liveVersion !== options.target || liveTree) {
2120
2346
  const broken = (0, import_node_path12.join)(scratch, "rejected", `${options.target}-${Date.now()}`);
2347
+ const verifyDetail = liveVersion !== options.target ? `manifest is ${liveVersion ?? "unreadable"}, expected ${options.target}` : liveTree;
2121
2348
  try {
2122
2349
  (0, import_node_fs15.mkdirSync)((0, import_node_path12.join)(scratch, "rejected"), { recursive: true });
2123
2350
  (0, import_node_fs15.renameSync)(live, broken);
2124
2351
  if (displaced) (0, import_node_fs15.renameSync)(quarantine, live);
2125
2352
  } catch (error) {
2126
- return result(installed, yamlVersion(live) ?? "unknown", "fail", `post-switch Hermes verification failed and recovery failed: ${message2(error)}`);
2353
+ return result(installed, liveVersion ?? "unknown", "fail", `post-switch Hermes verification failed and recovery failed: ${message2(error)}`);
2127
2354
  }
2128
- return result(installed, displaced ? installed ?? "unknown" : "none", "fail", `post-switch Hermes verification failed; rejected generation parked at ${broken}${displaced ? "; previous generation restored" : ""}`);
2355
+ return result(installed, displaced ? installed ?? "unknown" : "none", "fail", `post-switch Hermes verification failed (${verifyDetail}); rejected generation parked at ${broken}${displaced ? "; previous generation restored" : ""}`);
2129
2356
  }
2130
2357
  discard2(incomingRoot);
2131
2358
  clearLaunchTarget(launchTargetPath);
@@ -2535,7 +2762,7 @@ function reconcile(options) {
2535
2762
  continue;
2536
2763
  }
2537
2764
  say(`${probe.id}: converging to v${summary.target}`);
2538
- const arm = probe.id === "claude" ? claudeArm({ target: summary.target, dryRun: options.dryRun, env, repoUrl }) : probe.id === "codex" ? codexArm({ target: summary.target, dryRun: options.dryRun, env, repoUrl }) : probe.id === "kilo" ? kiloArm({ target: summary.target, dryRun: options.dryRun, env }) : probe.id === "kimi" ? kimiArm({ target: summary.target, dryRun: options.dryRun, paths, env }) : probe.id === "cursor" ? cursorArm({ target: summary.target, dryRun: options.dryRun, paths, env }) : probe.id === "jervcode" ? jervcodeArm({ target: summary.target, dryRun: options.dryRun, paths, env }) : probe.id === "hermes" ? hermesArm({ target: summary.target, dryRun: options.dryRun, paths, env }) : null;
2765
+ const arm = probe.id === "claude" ? claudeArm({ target: summary.target, dryRun: options.dryRun, paths, env, repoUrl }) : probe.id === "codex" ? codexArm({ target: summary.target, dryRun: options.dryRun, env, repoUrl }) : probe.id === "kilo" ? kiloArm({ target: summary.target, dryRun: options.dryRun, env }) : probe.id === "kimi" ? kimiArm({ target: summary.target, dryRun: options.dryRun, paths, env }) : probe.id === "cursor" ? cursorArm({ target: summary.target, dryRun: options.dryRun, paths, env }) : probe.id === "jervcode" ? jervcodeArm({ target: summary.target, dryRun: options.dryRun, paths, env }) : probe.id === "hermes" ? hermesArm({ target: summary.target, dryRun: options.dryRun, paths, env }) : null;
2539
2766
  if (arm) {
2540
2767
  summary.plugins.push(arm);
2541
2768
  summary.surfaces.push({ id: probe.id, present: true, action: `arm-${arm.verdict}` });
@@ -3048,14 +3275,19 @@ function hubStatus(hubVersion, env = process.env, invokedPath = process.argv[1])
3048
3275
  const expectedVersion = [...realEvents].reverse().find((event) => event.kind === "target" && event.verdict === "ok" && event.to)?.to ?? null;
3049
3276
  const latestEvent = realEvents.at(-1) ?? null;
3050
3277
  const latestRunEvents = latestEvent ? realEvents.filter((event) => event.run === latestEvent.run) : [];
3278
+ const probes = enumerateSurfaces(env);
3279
+ const presentSurfaces = new Set(probes.filter((probe) => probe.present).map((probe) => probe.id));
3051
3280
  const actual = /* @__PURE__ */ new Map();
3052
3281
  const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
3053
3282
  const globalHubVersion = hubDist ? probeVersion(hubDist) : null;
3054
3283
  actual.set("hub", globalHubVersion ? { version: globalHubVersion, staged: null, location: hubDist, detail: "absolute global Hub binary" } : { version: hubVersion, staged: null, location: invokedPath ?? null, detail: "currently executing Hub bundle (global package location unavailable)" });
3055
3284
  const cliProbe = probeCliInstallation(env);
3056
3285
  actual.set("cli", { version: cliProbe.version, staged: null, location: cliProbe.location, detail: cliProbe.detail });
3057
- for (const surface of enumerateSurfaces(env).filter((probe) => probe.present)) {
3058
- if (surface.id === "claude" || surface.id === "codex" || surface.id === "kilo") {
3286
+ for (const surface of probes.filter((probe) => probe.present)) {
3287
+ if (surface.id === "claude") {
3288
+ const probe = probeNativeHostInstallation("claude", env);
3289
+ actual.set(surface.id, { version: probe.version, staged: probeClaudeLaunchStaging(env), location: probe.location, detail: probe.detail });
3290
+ } else if (surface.id === "codex" || surface.id === "kilo") {
3059
3291
  const probe = probeNativeHostInstallation(surface.id, env);
3060
3292
  actual.set(surface.id, { version: probe.version, staged: null, location: probe.location, detail: probe.detail });
3061
3293
  } else if (surface.id === "kimi") {
@@ -3095,6 +3327,9 @@ function hubStatus(hubVersion, env = process.env, invokedPath = process.argv[1])
3095
3327
  }
3096
3328
  for (const event of latestRunEvents) {
3097
3329
  if ((event.kind === "arm" || event.kind === "reconcile") && (event.verdict === "fail" || event.verdict === "defer")) {
3330
+ if (event.kind === "arm" && event.surface && event.surface !== "cli" && event.surface !== "updater" && !presentSurfaces.has(event.surface)) {
3331
+ continue;
3332
+ }
3098
3333
  const surface = event.surface === "updater" ? "hub" : event.surface ?? event.kind;
3099
3334
  const repeat = event.kind === "arm" && event.verdict === "fail" && event.surface && journalSurfaceRepeatedFail(paths.journalPath, event.surface, latestEvent.run);
3100
3335
  failures.push(repeat ? `last run ${surface}: ${event.detail ?? event.verdict} \u2014 already failed the previous run too; \`mmi-hub update\` will not clear it, inspect the journal` : `last run ${surface}: ${event.detail ?? event.verdict} \u2014 run \`mmi-hub update\``);
@@ -3303,6 +3538,15 @@ function reportFooterLines(summary, options = {}) {
3303
3538
  };
3304
3539
  const footer = dryRun ? `${counts.updated}/${counts.total} would update` : `${counts.updated}/${counts.total} updated` + (counts.retry ? ` \xB7 ${counts.retry} will retry` : "") + (counts.failed ? ` \xB7 ${counts.failed} failed` : "");
3305
3540
  const out = ["", " " + footer];
3541
+ const self = summary.updater;
3542
+ if (!dryRun && self && self.from && self.to && self.from !== self.to && self.verdict !== "fail") {
3543
+ out.push("");
3544
+ out.push(...noteLines(
3545
+ " note: ",
3546
+ `hub moved ${self.from} \u2192 ${self.to} after the surfaces above ran, so they used ${self.from} logic \u2014 rerun \`mmi-hub update\` to bring them to ${self.to}`,
3547
+ width
3548
+ ));
3549
+ }
3306
3550
  if (counts.failed) {
3307
3551
  out.push("");
3308
3552
  for (const row of rows.filter((row2) => row2.kind === "failed")) {
@@ -3539,6 +3783,8 @@ if (typeof require !== "undefined" && require.main === module) {
3539
3783
  0 && (module.exports = {
3540
3784
  acquireSharedDocLock,
3541
3785
  buildWatcherCommand,
3786
+ claudeArm,
3787
+ claudeHostEvidence,
3542
3788
  clearLaunchTarget,
3543
3789
  clearPending,
3544
3790
  enumerateSurfaces,
@@ -3546,7 +3792,9 @@ if (typeof require !== "undefined" && require.main === module) {
3546
3792
  formatReconcileReport,
3547
3793
  hermesArm,
3548
3794
  hermesHome,
3795
+ hermesHostCommand,
3549
3796
  hermesHostEvidence,
3797
+ hermesHostPresent,
3550
3798
  hubStatus,
3551
3799
  launchdPlist,
3552
3800
  launcherSh,
@@ -3561,9 +3809,11 @@ if (typeof require !== "undefined" && require.main === module) {
3561
3809
  parseNpmDistIntegrity,
3562
3810
  pendingPath,
3563
3811
  pidAlive,
3812
+ probeClaudeLaunchStaging,
3564
3813
  probeCursorLaunchStaging,
3565
3814
  probeHermesInstallation,
3566
3815
  probeHermesLaunchStaging,
3816
+ probeJervcodeInstallation,
3567
3817
  probeKimiLaunchStaging,
3568
3818
  probeLaunchTargetVersion,
3569
3819
  progressiveRow,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/hub",
3
- "version": "4.1.9",
3
+ "version": "4.1.11",
4
4
  "description": "Install and maintain the MMI CLI and every present host surface from one release-gated Hub command.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",