@mutmutco/hub 4.6.3 → 4.6.5

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 +100 -22
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -26,6 +26,7 @@ __export(index_exports, {
26
26
  buildWatcherCommand: () => buildWatcherCommand,
27
27
  checkAtomicity: () => checkAtomicity,
28
28
  claudeArm: () => claudeArm,
29
+ claudeCatalogMmiVersion: () => claudeCatalogMmiVersion,
29
30
  clearLaunchTarget: () => clearLaunchTarget,
30
31
  clearPending: () => clearPending,
31
32
  codexArm: () => codexArm,
@@ -79,6 +80,7 @@ __export(index_exports, {
79
80
  sharedDocLockTimeoutReceipt: () => sharedDocLockTimeoutReceipt,
80
81
  shippedTargetFrom: () => shippedTargetFrom,
81
82
  shouldHandOff: () => shouldHandOff,
83
+ stagingPrunePreview: () => stagingPrunePreview,
82
84
  statusScreen: () => statusScreen,
83
85
  surfaceArmPlan: () => surfaceArmPlan,
84
86
  systemdServiceUnit: () => systemdServiceUnit,
@@ -2510,6 +2512,9 @@ function claudeHomeDir(env) {
2510
2512
  function claudeMarketplacesPath(env) {
2511
2513
  return env.MMI_UPDATER_CLAUDE_MARKETPLACES || (0, import_node_path10.join)(claudeHomeDir(env), "plugins", "known_marketplaces.json");
2512
2514
  }
2515
+ function claudeLaunchTargetPath(env) {
2516
+ return (0, import_node_path10.join)(claudeHomeDir(env), "plugins", "staging", "launch-target.json");
2517
+ }
2513
2518
  function ensureClaudeMarketplace(env) {
2514
2519
  const path = claudeMarketplacesPath(env);
2515
2520
  let body;
@@ -2555,7 +2560,30 @@ function claudeNpmEnv(env) {
2555
2560
  return { ...env, npm_config_allow_remote: "all" };
2556
2561
  }
2557
2562
  function probeClaudeLaunchStaging(env = process.env) {
2558
- return probeLaunchTargetVersion((0, import_node_path10.join)(claudePluginsRoot(env), "staging", "launch-target.json"), claudeMarkerVersion);
2563
+ return probeLaunchTargetVersion(claudeLaunchTargetPath(env), claudeMarkerVersion);
2564
+ }
2565
+ function catalogVersionAt(path, pick) {
2566
+ try {
2567
+ const version = pick(JSON.parse((0, import_node_fs14.readFileSync)(path, "utf8")));
2568
+ return typeof version === "string" && parseSemver(version) ? version : null;
2569
+ } catch {
2570
+ return null;
2571
+ }
2572
+ }
2573
+ function claudeCatalogMmiVersion(env) {
2574
+ let clone = null;
2575
+ try {
2576
+ const location = JSON.parse((0, import_node_fs14.readFileSync)(claudeMarketplacesPath(env), "utf8"))?.mutmutco?.installLocation;
2577
+ clone = typeof location === "string" && location.trim() ? location : null;
2578
+ } catch {
2579
+ clone = null;
2580
+ }
2581
+ const root = clone ?? (0, import_node_path10.join)(claudeHomeDir(env), "plugins", "marketplaces", "mutmutco");
2582
+ const fromCatalog = catalogVersionAt(
2583
+ (0, import_node_path10.join)(root, ".claude-plugin", "marketplace.json"),
2584
+ (body) => Array.isArray(body?.plugins) ? body.plugins.find((row) => row?.name === "mmi")?.version : null
2585
+ );
2586
+ return fromCatalog ?? catalogVersionAt((0, import_node_path10.join)(root, ".claude-plugin", "plugin.json"), (body) => body?.version);
2559
2587
  }
2560
2588
  function claudeArm(options) {
2561
2589
  const env = options.env ?? process.env;
@@ -2588,7 +2616,7 @@ function claudeArm(options) {
2588
2616
  }
2589
2617
  if (!marketplaceTrust.ok) return result(from, target, "defer", marketplaceTrust.detail ?? "cannot trust the mutmutco marketplace registration");
2590
2618
  const pluginsRoot = claudePluginsRoot(env);
2591
- const launchTargetPath = (0, import_node_path10.join)(pluginsRoot, "staging", "launch-target.json");
2619
+ const launchTargetPath = claudeLaunchTargetPath(env);
2592
2620
  if (options.dryRun) {
2593
2621
  return result(from, target, "ok", `dry-run: would refresh mutmutco, stage ${target} on the Claude volume, and converge mmi@mutmutco ${from ?? "absent"} -> ${target}`);
2594
2622
  }
@@ -2602,6 +2630,10 @@ function claudeArm(options) {
2602
2630
  clearLaunchTarget(launchTargetPath);
2603
2631
  return result(from, target, "ok", `catalog refreshed; installed+enabled at target ${target}`);
2604
2632
  }
2633
+ const catalogVersion = claudeCatalogMmiVersion(env);
2634
+ if (catalogVersion && compareSemver(catalogVersion, target) < 0) {
2635
+ return result(from, target, "defer", `catalog not yet at target (catalog ${catalogVersion} < target ${target}; release-align pending)`);
2636
+ }
2605
2637
  const incomingRoot = (0, import_node_path10.join)(pluginsRoot, "staging", "incoming");
2606
2638
  const incoming = (0, import_node_path10.join)(incomingRoot, target);
2607
2639
  const priorLaunch = readLaunchTarget(launchTargetPath);
@@ -2687,12 +2719,13 @@ function codexArm(options) {
2687
2719
  if (options.dryRun) {
2688
2720
  return { surface: "codex", from, to: options.target, verdict: "ok", detail: `dry-run: would upgrade mutmutco and converge mmi@mutmutco ${from ?? "absent"} -> ${options.target}` };
2689
2721
  }
2690
- const upgrade = runner("codex", ["plugin", "marketplace", "upgrade", "mutmutco", "--json"], env);
2722
+ const npmEnv = claudeNpmEnv(env);
2723
+ const upgrade = runner("codex", ["plugin", "marketplace", "upgrade", "mutmutco", "--json"], npmEnv);
2691
2724
  let upgradeJson = parseJson(upgrade);
2692
2725
  if ((upgrade.status !== 0 || !upgradeJson || (upgradeJson.errors?.length ?? 0) > 0) && NOT_CONFIGURED.test(failure(upgrade))) {
2693
- const add = runner("codex", ["plugin", "marketplace", "add", repoUrl], env);
2726
+ const add = runner("codex", ["plugin", "marketplace", "add", repoUrl], npmEnv);
2694
2727
  if (add.status !== 0) return { surface: "codex", from, to: options.target, verdict: "defer", detail: `mutmutco marketplace was not configured and marketplace add failed: ${failure(add)}` };
2695
- const retry = runner("codex", ["plugin", "marketplace", "upgrade", "mutmutco", "--json"], env);
2728
+ const retry = runner("codex", ["plugin", "marketplace", "upgrade", "mutmutco", "--json"], npmEnv);
2696
2729
  upgradeJson = parseJson(retry);
2697
2730
  if (retry.status !== 0 || !upgradeJson || (upgradeJson.errors?.length ?? 0) > 0) {
2698
2731
  return { surface: "codex", from, to: options.target, verdict: "defer", detail: `marketplace upgrade failed after registration: ${failure(retry)}` };
@@ -2702,8 +2735,8 @@ function codexArm(options) {
2702
2735
  }
2703
2736
  let observed = codexMmi(runner, env);
2704
2737
  if (observed.error) return { surface: "codex", from, to: options.target, verdict: "defer", detail: observed.error };
2705
- if (!observed.row || observed.row.installed !== true || observed.row.enabled !== true) {
2706
- const install = runner("codex", ["plugin", "add", "mmi@mutmutco", "--json"], env);
2738
+ if (!observed.row || !convergedAtOrAbove(observed.row.version, options.target, observed.row.installed === true && observed.row.enabled === true)) {
2739
+ const install = runner("codex", ["plugin", "add", "mmi@mutmutco", "--json"], npmEnv);
2707
2740
  if (install.status !== 0) {
2708
2741
  return { surface: "codex", from, to: options.target, verdict: "defer", detail: `plugin add failed: ${failure(install)}` };
2709
2742
  }
@@ -3323,28 +3356,65 @@ function generationSets(root) {
3323
3356
  if (children.some((name) => parseSemver(name))) return [root];
3324
3357
  return children.map((name) => (0, import_node_path14.join)(root, name)).filter((path) => listDirs(path).some((name) => parseSemver(name)));
3325
3358
  }
3326
- function pruneGenerations(root, category, dryRun) {
3327
- let reaped = 0;
3359
+ function pruneGenerations(root, category, dryRun, options = {}) {
3360
+ const protectedPaths = options.protectedPaths ?? /* @__PURE__ */ new Set();
3361
+ const isProtected = (path) => {
3362
+ const dir = path.replaceAll("\\", "/");
3363
+ for (const ref of protectedPaths) {
3364
+ if (ref === dir || ref.startsWith(`${dir}/`)) return true;
3365
+ }
3366
+ return false;
3367
+ };
3368
+ const prunablePaths = (set, versions, incoming) => {
3369
+ const tree = incoming && parseSemver(incoming) && !versions.includes(incoming) ? [...versions, incoming] : versions;
3370
+ return [...tree].sort((a, b) => compareSemver(b, a)).slice(KEEP_GENERATIONS).map((version) => (0, import_node_path14.join)(set, version)).filter((path) => !isProtected(path));
3371
+ };
3372
+ const relName = (path) => (0, import_node_path14.relative)(root, path).replaceAll("\\", "/");
3373
+ const collect = (incoming) => {
3374
+ const candidates = [];
3375
+ for (const set of generationSets(root)) {
3376
+ candidates.push(...prunablePaths(set, listDirs(set).filter((name) => parseSemver(name)), incoming));
3377
+ }
3378
+ return candidates;
3379
+ };
3328
3380
  const failures = [];
3329
- for (const set of generationSets(root)) {
3330
- const versions = listDirs(set).filter((name) => parseSemver(name)).sort((a, b) => compareSemver(b, a));
3331
- for (const version of versions.slice(KEEP_GENERATIONS)) {
3332
- if (dryRun) {
3333
- reaped += 1;
3334
- continue;
3335
- }
3381
+ const removed = [];
3382
+ if (!dryRun) {
3383
+ for (const path of collect()) {
3336
3384
  try {
3337
- (0, import_node_fs20.rmSync)((0, import_node_path14.join)(set, version), { recursive: true, force: true });
3338
- reaped += 1;
3385
+ (0, import_node_fs20.rmSync)(path, { recursive: true, force: true });
3386
+ removed.push(path);
3339
3387
  } catch (error) {
3340
- failures.push(`${version}: ${error?.code ?? error?.message ?? error}`);
3388
+ failures.push(`${(0, import_node_path14.basename)(path)}: ${error?.code ?? error?.message ?? error}`);
3341
3389
  }
3342
3390
  }
3343
3391
  }
3344
- if (!reaped && !failures.length) return { category, reaped: 0, verdict: "skip", detail: `${root}: within keep-${KEEP_GENERATIONS}` };
3392
+ const pruned = dryRun ? collect() : removed;
3393
+ const preview = dryRun && options.incomingTarget ? collect(options.incomingTarget) : [];
3394
+ if (!pruned.length && !failures.length && !preview.length) return { category, reaped: 0, verdict: "skip", detail: `${root}: within keep-${KEEP_GENERATIONS}` };
3345
3395
  const verb = dryRun ? "dry-run: would prune" : "pruned";
3346
3396
  const stuck = failures.length ? `; ${failures.length} locked (${failures.join(", ")})` : "";
3347
- return { category, reaped, verdict: failures.length ? "defer" : "ok", detail: `${verb} ${reaped} generation(s) below keep-${KEEP_GENERATIONS} under ${root}${stuck}` };
3397
+ const segments = [`${verb} ${pruned.length} generation(s) below keep-${KEEP_GENERATIONS} under ${root}${pruned.length ? `: ${pruned.map(relName).join(", ")}` : ""}`];
3398
+ if (preview.length) segments.push(`after staging ${options.incomingTarget}: would prune ${preview.length} generation(s) below keep-${KEEP_GENERATIONS} under ${root}: ${preview.map(relName).join(", ")}`);
3399
+ return { category, reaped: pruned.length, verdict: failures.length ? "defer" : "ok", detail: `${segments.join("; ")}${stuck}` };
3400
+ }
3401
+ function liveGenerationRefs(env, paths) {
3402
+ const refs = /* @__PURE__ */ new Set();
3403
+ const launch = readLaunchTarget(claudeLaunchTargetPath(env));
3404
+ if (launch?.incoming) refs.add(launch.incoming.replaceAll("\\", "/"));
3405
+ for (const pending of readPending(paths)) {
3406
+ for (const dir of pending.dirs) refs.add(dir.replaceAll("\\", "/"));
3407
+ }
3408
+ return refs;
3409
+ }
3410
+ function stagingPrunePreview(options) {
3411
+ const env = options.env ?? process.env;
3412
+ const startedAt = import_node_perf_hooks.performance.now();
3413
+ try {
3414
+ return { ...pruneGenerations(options.paths.staging, "staging-prune-preview", true, { incomingTarget: options.incomingTarget, protectedPaths: liveGenerationRefs(env, options.paths) }), durationMs: elapsedMs(startedAt) };
3415
+ } catch (error) {
3416
+ return { category: "staging-prune-preview", reaped: 0, verdict: "fail", detail: `reap failed: ${error?.message ?? error}`, durationMs: elapsedMs(startedAt) };
3417
+ }
3348
3418
  }
3349
3419
  var LEASE_STEAL_SCRATCH = /^lease\.lock\.stale-\d+$/;
3350
3420
  function atomicWriteScratch(path) {
@@ -3413,8 +3483,9 @@ function reap(options) {
3413
3483
  }
3414
3484
  };
3415
3485
  const atomicWritePaths = [claudeMarketplacesPath(env)];
3486
+ const protectedPaths = liveGenerationRefs(env, paths);
3416
3487
  return [
3417
- guarded("staging-prune", () => pruneGenerations(paths.staging, "staging-prune", dryRun)),
3488
+ guarded("staging-prune", () => pruneGenerations(paths.staging, "staging-prune", dryRun, { protectedPaths })),
3418
3489
  guarded("scratch", () => reapScratch({ root: paths.root, atomicWritePaths }, dryRun)),
3419
3490
  guarded("cursor-retire", () => retireCursor({ paths, dryRun, env }))
3420
3491
  ];
@@ -3533,6 +3604,11 @@ async function reconcile(options) {
3533
3604
  say(`target v${summary.target} (${summary.gate})`);
3534
3605
  phase("resolve", "ok");
3535
3606
  options.onTargetResolved?.(summary.target, summary.gate);
3607
+ if (options.dryRun && summary.target) {
3608
+ const preview = stagingPrunePreview({ paths, env, incomingTarget: summary.target });
3609
+ summary.reap.push(preview);
3610
+ journal({ kind: "reap", surface: preview.category, verdict: preview.verdict, count: preview.reaped, detail: preview.detail, durationMs: preview.durationMs });
3611
+ }
3536
3612
  phase("check", "running");
3537
3613
  say("updater: self-bootstrap");
3538
3614
  const selfStartedAt = import_node_perf_hooks2.performance.now();
@@ -4457,6 +4533,7 @@ if (typeof require !== "undefined" && require.main === module) {
4457
4533
  buildWatcherCommand,
4458
4534
  checkAtomicity,
4459
4535
  claudeArm,
4536
+ claudeCatalogMmiVersion,
4460
4537
  clearLaunchTarget,
4461
4538
  clearPending,
4462
4539
  codexArm,
@@ -4510,6 +4587,7 @@ if (typeof require !== "undefined" && require.main === module) {
4510
4587
  sharedDocLockTimeoutReceipt,
4511
4588
  shippedTargetFrom,
4512
4589
  shouldHandOff,
4590
+ stagingPrunePreview,
4513
4591
  statusScreen,
4514
4592
  surfaceArmPlan,
4515
4593
  systemdServiceUnit,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/hub",
3
- "version": "4.6.3",
3
+ "version": "4.6.5",
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",