@mutmutco/hub 4.6.2 → 4.6.4

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 +94 -17
  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);
@@ -3323,28 +3355,65 @@ function generationSets(root) {
3323
3355
  if (children.some((name) => parseSemver(name))) return [root];
3324
3356
  return children.map((name) => (0, import_node_path14.join)(root, name)).filter((path) => listDirs(path).some((name) => parseSemver(name)));
3325
3357
  }
3326
- function pruneGenerations(root, category, dryRun) {
3327
- let reaped = 0;
3358
+ function pruneGenerations(root, category, dryRun, options = {}) {
3359
+ const protectedPaths = options.protectedPaths ?? /* @__PURE__ */ new Set();
3360
+ const isProtected = (path) => {
3361
+ const dir = path.replaceAll("\\", "/");
3362
+ for (const ref of protectedPaths) {
3363
+ if (ref === dir || ref.startsWith(`${dir}/`)) return true;
3364
+ }
3365
+ return false;
3366
+ };
3367
+ const prunablePaths = (set, versions, incoming) => {
3368
+ const tree = incoming && parseSemver(incoming) && !versions.includes(incoming) ? [...versions, incoming] : versions;
3369
+ return [...tree].sort((a, b) => compareSemver(b, a)).slice(KEEP_GENERATIONS).map((version) => (0, import_node_path14.join)(set, version)).filter((path) => !isProtected(path));
3370
+ };
3371
+ const relName = (path) => (0, import_node_path14.relative)(root, path).replaceAll("\\", "/");
3372
+ const collect = (incoming) => {
3373
+ const candidates = [];
3374
+ for (const set of generationSets(root)) {
3375
+ candidates.push(...prunablePaths(set, listDirs(set).filter((name) => parseSemver(name)), incoming));
3376
+ }
3377
+ return candidates;
3378
+ };
3328
3379
  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
- }
3380
+ const removed = [];
3381
+ if (!dryRun) {
3382
+ for (const path of collect()) {
3336
3383
  try {
3337
- (0, import_node_fs20.rmSync)((0, import_node_path14.join)(set, version), { recursive: true, force: true });
3338
- reaped += 1;
3384
+ (0, import_node_fs20.rmSync)(path, { recursive: true, force: true });
3385
+ removed.push(path);
3339
3386
  } catch (error) {
3340
- failures.push(`${version}: ${error?.code ?? error?.message ?? error}`);
3387
+ failures.push(`${(0, import_node_path14.basename)(path)}: ${error?.code ?? error?.message ?? error}`);
3341
3388
  }
3342
3389
  }
3343
3390
  }
3344
- if (!reaped && !failures.length) return { category, reaped: 0, verdict: "skip", detail: `${root}: within keep-${KEEP_GENERATIONS}` };
3391
+ const pruned = dryRun ? collect() : removed;
3392
+ const preview = dryRun && options.incomingTarget ? collect(options.incomingTarget) : [];
3393
+ if (!pruned.length && !failures.length && !preview.length) return { category, reaped: 0, verdict: "skip", detail: `${root}: within keep-${KEEP_GENERATIONS}` };
3345
3394
  const verb = dryRun ? "dry-run: would prune" : "pruned";
3346
3395
  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}` };
3396
+ const segments = [`${verb} ${pruned.length} generation(s) below keep-${KEEP_GENERATIONS} under ${root}${pruned.length ? `: ${pruned.map(relName).join(", ")}` : ""}`];
3397
+ 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(", ")}`);
3398
+ return { category, reaped: pruned.length, verdict: failures.length ? "defer" : "ok", detail: `${segments.join("; ")}${stuck}` };
3399
+ }
3400
+ function liveGenerationRefs(env, paths) {
3401
+ const refs = /* @__PURE__ */ new Set();
3402
+ const launch = readLaunchTarget(claudeLaunchTargetPath(env));
3403
+ if (launch?.incoming) refs.add(launch.incoming.replaceAll("\\", "/"));
3404
+ for (const pending of readPending(paths)) {
3405
+ for (const dir of pending.dirs) refs.add(dir.replaceAll("\\", "/"));
3406
+ }
3407
+ return refs;
3408
+ }
3409
+ function stagingPrunePreview(options) {
3410
+ const env = options.env ?? process.env;
3411
+ const startedAt = import_node_perf_hooks.performance.now();
3412
+ try {
3413
+ return { ...pruneGenerations(options.paths.staging, "staging-prune-preview", true, { incomingTarget: options.incomingTarget, protectedPaths: liveGenerationRefs(env, options.paths) }), durationMs: elapsedMs(startedAt) };
3414
+ } catch (error) {
3415
+ return { category: "staging-prune-preview", reaped: 0, verdict: "fail", detail: `reap failed: ${error?.message ?? error}`, durationMs: elapsedMs(startedAt) };
3416
+ }
3348
3417
  }
3349
3418
  var LEASE_STEAL_SCRATCH = /^lease\.lock\.stale-\d+$/;
3350
3419
  function atomicWriteScratch(path) {
@@ -3413,8 +3482,9 @@ function reap(options) {
3413
3482
  }
3414
3483
  };
3415
3484
  const atomicWritePaths = [claudeMarketplacesPath(env)];
3485
+ const protectedPaths = liveGenerationRefs(env, paths);
3416
3486
  return [
3417
- guarded("staging-prune", () => pruneGenerations(paths.staging, "staging-prune", dryRun)),
3487
+ guarded("staging-prune", () => pruneGenerations(paths.staging, "staging-prune", dryRun, { protectedPaths })),
3418
3488
  guarded("scratch", () => reapScratch({ root: paths.root, atomicWritePaths }, dryRun)),
3419
3489
  guarded("cursor-retire", () => retireCursor({ paths, dryRun, env }))
3420
3490
  ];
@@ -3533,6 +3603,11 @@ async function reconcile(options) {
3533
3603
  say(`target v${summary.target} (${summary.gate})`);
3534
3604
  phase("resolve", "ok");
3535
3605
  options.onTargetResolved?.(summary.target, summary.gate);
3606
+ if (options.dryRun && summary.target) {
3607
+ const preview = stagingPrunePreview({ paths, env, incomingTarget: summary.target });
3608
+ summary.reap.push(preview);
3609
+ journal({ kind: "reap", surface: preview.category, verdict: preview.verdict, count: preview.reaped, detail: preview.detail, durationMs: preview.durationMs });
3610
+ }
3536
3611
  phase("check", "running");
3537
3612
  say("updater: self-bootstrap");
3538
3613
  const selfStartedAt = import_node_perf_hooks2.performance.now();
@@ -4457,6 +4532,7 @@ if (typeof require !== "undefined" && require.main === module) {
4457
4532
  buildWatcherCommand,
4458
4533
  checkAtomicity,
4459
4534
  claudeArm,
4535
+ claudeCatalogMmiVersion,
4460
4536
  clearLaunchTarget,
4461
4537
  clearPending,
4462
4538
  codexArm,
@@ -4510,6 +4586,7 @@ if (typeof require !== "undefined" && require.main === module) {
4510
4586
  sharedDocLockTimeoutReceipt,
4511
4587
  shippedTargetFrom,
4512
4588
  shouldHandOff,
4589
+ stagingPrunePreview,
4513
4590
  statusScreen,
4514
4591
  surfaceArmPlan,
4515
4592
  systemdServiceUnit,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/hub",
3
- "version": "4.6.2",
3
+ "version": "4.6.4",
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",