@mutmutco/hub 4.0.15 → 4.0.16

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 +460 -243
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -21,6 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  // src/index.ts
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
+ buildWatcherCommand: () => buildWatcherCommand,
25
+ clearPending: () => clearPending,
24
26
  enumerateSurfaces: () => enumerateSurfaces,
25
27
  failConsequence: () => failConsequence,
26
28
  formatReconcileReport: () => formatReconcileReport,
@@ -31,24 +33,32 @@ __export(index_exports, {
31
33
  launcherSh: () => launcherSh,
32
34
  launcherVbs: () => launcherVbs,
33
35
  localStartBoundary: () => localStartBoundary,
36
+ lockShaped: () => lockShaped,
34
37
  main: () => main,
38
+ noteBusyDefer: () => noteBusyDefer,
35
39
  ownVersion: () => ownVersion,
40
+ pendingPath: () => pendingPath,
41
+ pidAlive: () => pidAlive,
36
42
  probeHermesInstallation: () => probeHermesInstallation,
37
43
  progressiveRow: () => progressiveRow,
44
+ readPending: () => readPending,
38
45
  reconcileCursorUserHooks: () => reconcileCursorUserHooks,
39
46
  reportFooterLines: () => reportFooterLines,
40
47
  retryHint: () => retryHint,
48
+ schedulePendingWatcher: () => schedulePendingWatcher,
41
49
  schedulerStatus: () => schedulerStatus,
42
50
  startSpinner: () => startSpinner,
43
51
  systemdServiceUnit: () => systemdServiceUnit,
44
52
  systemdTimerUnit: () => systemdTimerUnit,
45
53
  taskXml: () => taskXml,
46
54
  updateHeader: () => updateHeader,
47
- wrapWords: () => wrapWords
55
+ watcherLockPath: () => watcherLockPath,
56
+ wrapWords: () => wrapWords,
57
+ writePending: () => writePending
48
58
  });
49
59
  module.exports = __toCommonJS(index_exports);
50
- var import_node_fs16 = require("node:fs");
51
- var import_node_path13 = require("node:path");
60
+ var import_node_fs17 = require("node:fs");
61
+ var import_node_path14 = require("node:path");
52
62
 
53
63
  // src/state.ts
54
64
  var import_node_fs = require("node:fs");
@@ -400,16 +410,160 @@ function enumerateSurfaces(env = process.env) {
400
410
  }
401
411
 
402
412
  // src/arm-cli.ts
403
- var import_node_fs5 = require("node:fs");
413
+ var import_node_fs6 = require("node:fs");
414
+ var import_node_child_process4 = require("node:child_process");
415
+ var import_node_path5 = require("node:path");
416
+
417
+ // src/pending.ts
404
418
  var import_node_child_process3 = require("node:child_process");
419
+ var import_node_fs5 = require("node:fs");
405
420
  var import_node_path4 = require("node:path");
421
+ function hubDistPath(env) {
422
+ const root = globalRoot(env);
423
+ return root ? (0, import_node_path4.join)(root, "@mutmutco", "hub", "dist", "index.cjs") : null;
424
+ }
425
+ var WATCHER_MAX_WAIT_MS = 4 * 60 * 60 * 1e3;
426
+ function pendingPath(paths) {
427
+ return (0, import_node_path4.join)(paths.root, "pending.json");
428
+ }
429
+ function watcherLockPath(paths) {
430
+ return (0, import_node_path4.join)(paths.root, "pending-watcher.lock.d");
431
+ }
432
+ function readPending(paths) {
433
+ try {
434
+ const parsed = JSON.parse((0, import_node_fs5.readFileSync)(pendingPath(paths), "utf8"));
435
+ if (!Array.isArray(parsed)) return [];
436
+ 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));
437
+ } catch {
438
+ return [];
439
+ }
440
+ }
441
+ function writePending(paths, records) {
442
+ (0, import_node_fs5.mkdirSync)(paths.root, { recursive: true });
443
+ const path = pendingPath(paths);
444
+ const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
445
+ (0, import_node_fs5.writeFileSync)(tmp, `${JSON.stringify(records, null, 2)}
446
+ `, "utf8");
447
+ (0, import_node_fs5.renameSync)(tmp, path);
448
+ }
449
+ function clearPending(paths, surface) {
450
+ const records = readPending(paths);
451
+ if (!records.some((row) => row.surface === surface)) return;
452
+ writePending(paths, records.filter((row) => row.surface !== surface));
453
+ }
454
+ function pidAlive(pid) {
455
+ try {
456
+ process.kill(pid, 0);
457
+ return true;
458
+ } catch (error) {
459
+ return error?.code === "EPERM";
460
+ }
461
+ }
462
+ function lockShaped(detail) {
463
+ return /EPERM|EBUSY|EACCES|locked|access is denied/i.test(detail);
464
+ }
465
+ function quotePs(value) {
466
+ return `'${value.replace(/'/gu, "''")}'`;
467
+ }
468
+ function quoteSh(value) {
469
+ return `'${value.replace(/'/gu, "'\\''")}'`;
470
+ }
471
+ function escapeEre(value) {
472
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
473
+ }
474
+ function buildWatcherCommand(opts) {
475
+ const platform = opts.platform ?? process.platform;
476
+ const maxWaitMs = opts.maxWaitMs ?? WATCHER_MAX_WAIT_MS;
477
+ const apply = [opts.node, opts.hubDist, "update"];
478
+ if (platform === "win32") {
479
+ const imageChecks2 = opts.images.map((image) => `if (Get-Process -Name ${quotePs(image)} -ErrorAction SilentlyContinue) { return $true }`);
480
+ const dirChecks2 = opts.dirs.map((dir) => `if (Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessId -ne $PID -and $_.CommandLine -and $_.CommandLine.IndexOf(${quotePs(dir)}, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 }) { return $true }`);
481
+ const testBusy2 = `function Test-Busy { ${[...imageChecks2, ...dirChecks2].join("; ")}; return $false }`;
482
+ const wait2 = `$deadline = (Get-Date).AddMilliseconds(${maxWaitMs}); while ((Get-Date) -lt $deadline -and (Test-Busy)) { Start-Sleep -Seconds 5 }; if (Test-Busy) { exit 0 }`;
483
+ const lock2 = `$lock = ${quotePs(opts.lockPath)}; if (Test-Path $lock) { $owner = Get-Content "$lock\\pid" -ErrorAction SilentlyContinue; if ($owner -and -not (Get-Process -Id $owner -ErrorAction SilentlyContinue)) { Remove-Item $lock -Recurse -Force -ErrorAction SilentlyContinue } }`;
484
+ const run2 = `if (New-Item -ItemType Directory -Path $lock -ErrorAction SilentlyContinue) { $PID | Set-Content "$lock\\pid"; try { & ${apply.map(quotePs).join(" ")} *> ${quotePs(opts.logPath)}; Set-Content -Path ${quotePs(opts.codePath)} -Value $LASTEXITCODE } finally { Remove-Item $lock -Recurse -Force -ErrorAction SilentlyContinue } }`;
485
+ return {
486
+ file: "powershell",
487
+ args: ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", `${testBusy2}; ${wait2}; ${lock2}; ${run2}`]
488
+ };
489
+ }
490
+ const imageChecks = opts.images.map((image) => `pgrep -xi ${quoteSh(image)} >/dev/null 2>&1 && return 0`);
491
+ const dirChecks = opts.dirs.map((dir) => `[ -n "$(pgrep -f ${quoteSh(escapeEre(dir))} | grep -vx $$)" ] && return 0`);
492
+ const testBusy = `test_busy() { ${[...imageChecks, ...dirChecks].join("; ") || ":"}; return 1; }`;
493
+ const wait = `deadline=$(( $(date +%s) + ${Math.floor(maxWaitMs / 1e3)} )); while [ "$(date +%s)" -lt "$deadline" ] && test_busy; do sleep 5; done; test_busy && exit 0`;
494
+ const lock = `if [ -d ${quoteSh(opts.lockPath)} ]; then owner=$(cat ${quoteSh(`${opts.lockPath}/pid`)} 2>/dev/null); if [ -n "$owner" ] && ! kill -0 "$owner" 2>/dev/null; then rm -rf ${quoteSh(opts.lockPath)}; fi; fi`;
495
+ const run = `if mkdir ${quoteSh(opts.lockPath)} 2>/dev/null; then echo $$ > ${quoteSh(`${opts.lockPath}/pid`)}; trap "rm -rf ${quoteSh(opts.lockPath)}" EXIT; ${apply.map(quoteSh).join(" ")} > ${quoteSh(opts.logPath)} 2>&1; echo $? > ${quoteSh(opts.codePath)}; fi`;
496
+ return { file: "sh", args: ["-c", `${testBusy}; ${wait}; ${lock}; ${run}`] };
497
+ }
498
+ function defaultDetachSpawn(file, args) {
499
+ const child = (0, import_node_child_process3.spawn)(file, [...args], {
500
+ detached: process.platform !== "win32",
501
+ stdio: "ignore",
502
+ windowsHide: true,
503
+ shell: false
504
+ });
505
+ child.on("error", () => {
506
+ });
507
+ child.unref();
508
+ return child.pid ?? null;
509
+ }
510
+ function schedulePendingWatcher(paths, env = process.env, seam = {}) {
511
+ const records = readPending(paths);
512
+ if (!records.length) return { scheduled: false, reason: "no pending applies" };
513
+ const live = records.find((row) => row.watcherPid && pidAlive(row.watcherPid));
514
+ if (live) return { scheduled: false, reason: `watcher pid ${live.watcherPid} is already waiting` };
515
+ const hubDist = seam.hubDist ?? hubDistPath(env);
516
+ 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" };
517
+ const command = buildWatcherCommand({
518
+ images: [...new Set(records.flatMap((row) => row.images))],
519
+ dirs: [...new Set(records.flatMap((row) => row.dirs))],
520
+ node: process.execPath,
521
+ hubDist,
522
+ codePath: (0, import_node_path4.join)(paths.root, "pending-result.code"),
523
+ logPath: (0, import_node_path4.join)(paths.root, "pending-result.log"),
524
+ lockPath: watcherLockPath(paths)
525
+ });
526
+ try {
527
+ const pid = (seam.detachSpawn ?? defaultDetachSpawn)(command.file, command.args);
528
+ if (!pid) return { scheduled: false, reason: "watcher spawn returned no pid" };
529
+ writePending(paths, records.map((row) => ({ ...row, watcherPid: pid })));
530
+ return { scheduled: true, pid };
531
+ } catch (error) {
532
+ return { scheduled: false, reason: String(error?.message ?? error) };
533
+ }
534
+ }
535
+ function noteBusyDefer(paths, env, entry, seam = {}) {
536
+ try {
537
+ const records = readPending(paths);
538
+ const existing = records.find((row) => row.surface === entry.surface);
539
+ const next = {
540
+ surface: entry.surface,
541
+ target: entry.target,
542
+ since: existing?.since ?? (/* @__PURE__ */ new Date()).toISOString(),
543
+ reason: entry.reason,
544
+ images: entry.images ?? [],
545
+ dirs: entry.dirs ?? [],
546
+ watcherPid: existing?.watcherPid
547
+ };
548
+ writePending(paths, [...records.filter((row) => row.surface !== entry.surface), next]);
549
+ const scheduled = schedulePendingWatcher(paths, env, seam);
550
+ return scheduled.scheduled ? `applies when the host exits (post-exit watcher pid ${scheduled.pid})` : `applies on a quiet tick (${scheduled.reason})`;
551
+ } catch (error) {
552
+ return `applies on a quiet tick (pending record failed: ${String(error?.message ?? error).slice(0, 80)})`;
553
+ }
554
+ }
555
+ function resumePendingWatcher(paths, env = process.env) {
556
+ return schedulePendingWatcher(paths, env);
557
+ }
558
+
559
+ // src/arm-cli.ts
406
560
  function globalCliDist(env) {
407
561
  const root = globalRoot(env);
408
- return root ? (0, import_node_path4.join)(root, "@mutmutco", "cli", "dist", "index.cjs") : null;
562
+ return root ? (0, import_node_path5.join)(root, "@mutmutco", "cli", "dist", "index.cjs") : null;
409
563
  }
410
564
  function probeCliVersion(distPath) {
411
565
  if (!fileExists(distPath)) return null;
412
- const probe = (0, import_node_child_process3.spawnSync)(process.execPath, [distPath, "--version"], { encoding: "utf8", windowsHide: true });
566
+ const probe = (0, import_node_child_process4.spawnSync)(process.execPath, [distPath, "--version"], { encoding: "utf8", windowsHide: true });
413
567
  const version = (probe.stdout || "").trim();
414
568
  return probe.status === 0 && /^\d+\.\d+\.\d+/.test(version) ? version : null;
415
569
  }
@@ -436,11 +590,11 @@ function cliArm(options) {
436
590
  if (dryRun) {
437
591
  return { surface: "cli", from: installed, to: target, verdict: "ok", detail: `dry-run: would converge ${installed ?? "absent"} -> ${target}` };
438
592
  }
439
- const stageRoot = (0, import_node_path4.join)(paths.staging, "cli", target);
440
- const stageDist = (0, import_node_path4.join)(stageRoot, "node_modules", "@mutmutco", "cli", "dist", "index.cjs");
593
+ const stageRoot = (0, import_node_path5.join)(paths.staging, "cli", target);
594
+ const stageDist = (0, import_node_path5.join)(stageRoot, "node_modules", "@mutmutco", "cli", "dist", "index.cjs");
441
595
  if (!fileExists(stageDist)) {
442
- (0, import_node_fs5.mkdirSync)(stageRoot, { recursive: true });
443
- (0, import_node_fs5.writeFileSync)((0, import_node_path4.join)(stageRoot, "package.json"), JSON.stringify({ name: "mmi-updater-stage", private: true }) + "\n");
596
+ (0, import_node_fs6.mkdirSync)(stageRoot, { recursive: true });
597
+ (0, import_node_fs6.writeFileSync)((0, import_node_path5.join)(stageRoot, "package.json"), JSON.stringify({ name: "mmi-updater-stage", private: true }) + "\n");
444
598
  const install = runNpm(["install", "--prefix", stageRoot, "--no-audit", "--no-fund", "--no-package-lock", `@mutmutco/cli@${target}`], env);
445
599
  if (install.status !== 0) {
446
600
  return { surface: "cli", from: installed, to: target, verdict: "defer", detail: `staging install failed: ${(install.stderr || install.stdout).split("\n").filter(Boolean).slice(-3).join(" | ")}` };
@@ -448,7 +602,7 @@ function cliArm(options) {
448
602
  }
449
603
  let stagedManifestVersion;
450
604
  try {
451
- stagedManifestVersion = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(stageRoot, "node_modules", "@mutmutco", "cli", "package.json"), "utf8")).version;
605
+ stagedManifestVersion = JSON.parse((0, import_node_fs6.readFileSync)((0, import_node_path5.join)(stageRoot, "node_modules", "@mutmutco", "cli", "package.json"), "utf8")).version;
452
606
  } catch {
453
607
  }
454
608
  if (stagedManifestVersion !== target) {
@@ -460,7 +614,10 @@ function cliArm(options) {
460
614
  }
461
615
  const globalInstall = runNpm(["install", "-g", "--no-audit", "--no-fund", `@mutmutco/cli@${target}`], env);
462
616
  if (globalInstall.status !== 0) {
463
- return { surface: "cli", from: installed, to: target, verdict: "defer", detail: `global install failed: ${(globalInstall.stderr || globalInstall.stdout).split("\n").filter(Boolean).slice(-3).join(" | ")}` };
617
+ const reason = `global install failed: ${(globalInstall.stderr || globalInstall.stdout).split("\n").filter(Boolean).slice(-3).join(" | ")}`;
618
+ const root = globalRoot(env);
619
+ const pending = lockShaped(reason) && root ? ` \u2014 ${noteBusyDefer(paths, env, { surface: "cli", target, reason, dirs: [root] })}` : "";
620
+ return { surface: "cli", from: installed, to: target, verdict: "defer", detail: `${reason}${pending}` };
464
621
  }
465
622
  const switchedDist = globalCliDist(env);
466
623
  const switched = probeCliVersion(switchedDist ?? "");
@@ -471,16 +628,16 @@ function cliArm(options) {
471
628
  }
472
629
 
473
630
  // src/arm-npm-global.ts
474
- var import_node_fs6 = require("node:fs");
475
- var import_node_child_process4 = require("node:child_process");
476
- var import_node_path5 = require("node:path");
631
+ var import_node_fs7 = require("node:fs");
632
+ var import_node_child_process5 = require("node:child_process");
633
+ var import_node_path6 = require("node:path");
477
634
  function globalDist(env, packageName, distRel) {
478
635
  const root = globalRoot(env);
479
- return root ? (0, import_node_path5.join)(root, ...packageName.split("/"), ...distRel.split("/")) : null;
636
+ return root ? (0, import_node_path6.join)(root, ...packageName.split("/"), ...distRel.split("/")) : null;
480
637
  }
481
638
  function probeVersion(distPath) {
482
639
  if (!fileExists(distPath)) return null;
483
- const probe = (0, import_node_child_process4.spawnSync)(process.execPath, [distPath, "--version"], { encoding: "utf8", windowsHide: true });
640
+ const probe = (0, import_node_child_process5.spawnSync)(process.execPath, [distPath, "--version"], { encoding: "utf8", windowsHide: true });
484
641
  const version = (probe.stdout || "").trim();
485
642
  return probe.status === 0 && /^\d+\.\d+\.\d+/.test(version) ? version : null;
486
643
  }
@@ -489,11 +646,11 @@ function tail(output) {
489
646
  }
490
647
  function stagePackage(options) {
491
648
  const { surface, packageName, target, witness, paths, env } = options;
492
- const stageRoot = (0, import_node_path5.join)(paths.staging, surface, target);
493
- const packageRoot = (0, import_node_path5.join)(stageRoot, "node_modules", ...packageName.split("/"));
494
- if (!fileExists((0, import_node_path5.join)(packageRoot, ...witness.split("/")))) {
495
- (0, import_node_fs6.mkdirSync)(stageRoot, { recursive: true });
496
- (0, import_node_fs6.writeFileSync)((0, import_node_path5.join)(stageRoot, "package.json"), JSON.stringify({ name: "mmi-updater-stage", private: true }) + "\n");
649
+ const stageRoot = (0, import_node_path6.join)(paths.staging, surface, target);
650
+ const packageRoot = (0, import_node_path6.join)(stageRoot, "node_modules", ...packageName.split("/"));
651
+ if (!fileExists((0, import_node_path6.join)(packageRoot, ...witness.split("/")))) {
652
+ (0, import_node_fs7.mkdirSync)(stageRoot, { recursive: true });
653
+ (0, import_node_fs7.writeFileSync)((0, import_node_path6.join)(stageRoot, "package.json"), JSON.stringify({ name: "mmi-updater-stage", private: true }) + "\n");
497
654
  const install = runNpm(["install", "--prefix", stageRoot, "--no-audit", "--no-fund", "--no-package-lock", `${packageName}@${target}`], env);
498
655
  if (install.status !== 0) {
499
656
  return { defer: `staging install failed: ${tail(install.stderr || install.stdout)}` };
@@ -501,7 +658,7 @@ function stagePackage(options) {
501
658
  }
502
659
  let stagedManifestVersion;
503
660
  try {
504
- stagedManifestVersion = JSON.parse((0, import_node_fs6.readFileSync)((0, import_node_path5.join)(packageRoot, "package.json"), "utf8")).version;
661
+ stagedManifestVersion = JSON.parse((0, import_node_fs7.readFileSync)((0, import_node_path6.join)(packageRoot, "package.json"), "utf8")).version;
505
662
  } catch {
506
663
  }
507
664
  if (stagedManifestVersion !== target) {
@@ -533,14 +690,17 @@ function npmGlobalArm(options) {
533
690
  if ("defer" in staged) {
534
691
  return result(installed, target, "defer", staged.defer);
535
692
  }
536
- const stageDist = (0, import_node_path5.join)(staged.packageRoot, ...distRel.split("/"));
693
+ const stageDist = (0, import_node_path6.join)(staged.packageRoot, ...distRel.split("/"));
537
694
  const stagedVersion = probeVersion(stageDist);
538
695
  if (stagedVersion !== target) {
539
696
  return result(installed, target, "defer", `staged binary probed ${stagedVersion ?? "dead"}, expected ${target}`);
540
697
  }
541
698
  const globalInstall = runNpm(["install", "-g", "--no-audit", "--no-fund", spec], env);
542
699
  if (globalInstall.status !== 0) {
543
- return result(installed, target, "defer", `global install failed: ${tail(globalInstall.stderr || globalInstall.stdout)}`);
700
+ const reason = `global install failed: ${tail(globalInstall.stderr || globalInstall.stdout)}`;
701
+ const root = globalRoot(env);
702
+ const pending = lockShaped(reason) && root ? ` \u2014 ${noteBusyDefer(paths, env, { surface, target, reason, dirs: [root] })}` : "";
703
+ return result(installed, target, "defer", `${reason}${pending}`);
544
704
  }
545
705
  const switchedDist = globalDist(env, packageName, distRel);
546
706
  const switched = probeVersion(switchedDist ?? "");
@@ -561,10 +721,10 @@ function selfArm(options) {
561
721
  }
562
722
 
563
723
  // src/arm-host-plugins.ts
564
- var import_node_child_process5 = require("node:child_process");
565
- var import_node_fs7 = require("node:fs");
724
+ var import_node_child_process6 = require("node:child_process");
725
+ var import_node_fs8 = require("node:fs");
566
726
  var import_node_os3 = require("node:os");
567
- var import_node_path6 = require("node:path");
727
+ var import_node_path7 = require("node:path");
568
728
  var REPO_URL_DEFAULT = "https://github.com/mutmutco/MMI-Hub.git";
569
729
  var MMI_MARKETPLACE_REPO = "mutmutco/MMI-Hub";
570
730
  function convergedAtOrAbove(installed, target, healthy) {
@@ -574,13 +734,13 @@ function cmdArg(value) {
574
734
  return /^[A-Za-z0-9_@./:\\=+-]+$/.test(value) ? value : `"${value.replaceAll('"', '""')}"`;
575
735
  }
576
736
  var runHostCommand = (command, args, env) => {
577
- const result = process.platform === "win32" ? (0, import_node_child_process5.spawnSync)("cmd.exe", ["/d", "/s", "/c", [command, ...args].map(cmdArg).join(" ")], {
737
+ const result = process.platform === "win32" ? (0, import_node_child_process6.spawnSync)("cmd.exe", ["/d", "/s", "/c", [command, ...args].map(cmdArg).join(" ")], {
578
738
  encoding: "utf8",
579
739
  env,
580
740
  input: "",
581
741
  timeout: 12e4,
582
742
  windowsHide: true
583
- }) : (0, import_node_child_process5.spawnSync)(command, args, {
743
+ }) : (0, import_node_child_process6.spawnSync)(command, args, {
584
744
  encoding: "utf8",
585
745
  env,
586
746
  input: "",
@@ -607,7 +767,7 @@ function claudeMmi(runner, env) {
607
767
  return { row: parsed.find((row) => row.id === "mmi@mutmutco") ?? null };
608
768
  }
609
769
  function claudeMarketplacesPath(env) {
610
- return env.MMI_UPDATER_CLAUDE_MARKETPLACES || (0, import_node_path6.join)((0, import_node_os3.homedir)(), ".claude", "plugins", "known_marketplaces.json");
770
+ return env.MMI_UPDATER_CLAUDE_MARKETPLACES || (0, import_node_path7.join)((0, import_node_os3.homedir)(), ".claude", "plugins", "known_marketplaces.json");
611
771
  }
612
772
  var CLAUDE_PROCESS_SEGMENT = /^claude(-code|-cli)?(\.(exe|cmd|bat|ps1|js|mjs|cjs|py))?$/;
613
773
  function claudeProcessSegment(line) {
@@ -621,7 +781,7 @@ function claudeProcessSegment(line) {
621
781
  function claudeHostRunning(env) {
622
782
  const marker = env.CLAUDE_CODE_SESSION_ID?.trim() ? "CLAUDE_CODE_SESSION_ID" : env.CLAUDECODE?.trim() ? "CLAUDECODE" : env.CLAUDE_PLUGIN_ROOT?.trim() ? "CLAUDE_PLUGIN_ROOT" : null;
623
783
  if (marker) return { running: true, evidence: `${marker} is set \u2014 this process is itself inside a Claude hook` };
624
- 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 });
784
+ const result = process.platform === "win32" ? (0, import_node_child_process6.spawnSync)("powershell.exe", ["-NoProfile", "-Command", "Get-CimInstance Win32_Process | ForEach-Object { $_.Name + ' ' + $_.CommandLine }"], { encoding: "utf8", windowsHide: true, timeout: 15e3 }) : (0, import_node_child_process6.spawnSync)("ps", ["-eo", "args="], { encoding: "utf8", windowsHide: true, timeout: 15e3 });
625
785
  if (result.status !== 0 || !(result.stdout ?? "").trim()) return { running: true, evidence: "the process list is unreadable \u2014 unsafe to write" };
626
786
  for (const line of result.stdout.split(/\r?\n/)) {
627
787
  const segment = claudeProcessSegment(line);
@@ -633,7 +793,7 @@ function ensureClaudeSingleWriter(env, dryRun) {
633
793
  const path = claudeMarketplacesPath(env);
634
794
  let body;
635
795
  try {
636
- body = JSON.parse((0, import_node_fs7.readFileSync)(path, "utf8"));
796
+ body = JSON.parse((0, import_node_fs8.readFileSync)(path, "utf8"));
637
797
  } catch (error) {
638
798
  if (error?.code === "ENOENT") return { ok: false, detail: `${path} does not exist \u2014 mutmutco marketplace is not registered`, unregistered: true };
639
799
  return { ok: false, detail: `cannot read ${path}` };
@@ -645,7 +805,10 @@ function ensureClaudeSingleWriter(env, dryRun) {
645
805
  if (registeredRepo !== MMI_MARKETPLACE_REPO) {
646
806
  return {
647
807
  ok: false,
648
- detail: `mutmutco marketplace is registered to ${registeredRepo ?? "a non-github source"}, not ${MMI_MARKETPLACE_REPO} \u2014 a retired or foreign repo is squatting the name; re-register it (claude plugin marketplace remove mutmutco && claude plugin marketplace add ${REPO_URL_DEFAULT}) before the updater can trust it`
808
+ // Remedy must use the github shorthand (#5369): `marketplace add <url>` registers
809
+ // source:'git', which fails this same github-provenance check forever; `add owner/repo`
810
+ // registers source:'github' and passes.
811
+ detail: `mutmutco marketplace is registered to ${registeredRepo ?? "a non-github source"}, not ${MMI_MARKETPLACE_REPO} \u2014 a retired or foreign repo is squatting the name; re-register it (claude plugin marketplace remove mutmutco && claude plugin marketplace add ${MMI_MARKETPLACE_REPO}) before the updater can trust it`
649
812
  };
650
813
  }
651
814
  if (registration.autoUpdate !== true) return { ok: true };
@@ -657,9 +820,9 @@ function ensureClaudeSingleWriter(env, dryRun) {
657
820
  registration.autoUpdate = false;
658
821
  const tmp = `${path}.tmp-${process.pid}`;
659
822
  try {
660
- (0, import_node_fs7.writeFileSync)(tmp, JSON.stringify(body, null, 2) + "\n", "utf8");
661
- (0, import_node_fs7.renameSync)(tmp, path);
662
- const reread = JSON.parse((0, import_node_fs7.readFileSync)(path, "utf8"));
823
+ (0, import_node_fs8.writeFileSync)(tmp, JSON.stringify(body, null, 2) + "\n", "utf8");
824
+ (0, import_node_fs8.renameSync)(tmp, path);
825
+ const reread = JSON.parse((0, import_node_fs8.readFileSync)(path, "utf8"));
663
826
  return reread?.mutmutco?.autoUpdate === false ? { ok: true, detail: "disabled Claude background autoUpdate (updater is the single writer)" } : { ok: false, detail: "Claude background autoUpdate write did not persist" };
664
827
  } catch (error) {
665
828
  return { ok: false, detail: `cannot disable Claude background autoUpdate: ${error?.message ?? error}` };
@@ -773,11 +936,11 @@ function codexArm(options) {
773
936
  }
774
937
  var KILO_PACKAGE = "@mutmutco/kilo-plugin";
775
938
  function kiloConfigPath(env) {
776
- return env.MMI_UPDATER_KILO_CONFIG || (0, import_node_path6.join)((0, import_node_os3.homedir)(), ".config", "kilo", "opencode.json");
939
+ return env.MMI_UPDATER_KILO_CONFIG || (0, import_node_path7.join)((0, import_node_os3.homedir)(), ".config", "kilo", "opencode.json");
777
940
  }
778
941
  function readKiloConfig(env) {
779
942
  try {
780
- const config = JSON.parse((0, import_node_fs7.readFileSync)(kiloConfigPath(env), "utf8"));
943
+ const config = JSON.parse((0, import_node_fs8.readFileSync)(kiloConfigPath(env), "utf8"));
781
944
  const specs = Array.isArray(config.plugin) && config.plugin.every((entry) => typeof entry === "string") ? config.plugin : [];
782
945
  return { config, specs };
783
946
  } catch {
@@ -797,8 +960,8 @@ function normalizeKiloConfig(env, exact) {
797
960
  const path = kiloConfigPath(env);
798
961
  const tmp = `${path}.tmp-${process.pid}`;
799
962
  try {
800
- (0, import_node_fs7.writeFileSync)(tmp, JSON.stringify({ ...current.config, plugin: next }, null, 2) + "\n", "utf8");
801
- (0, import_node_fs7.renameSync)(tmp, path);
963
+ (0, import_node_fs8.writeFileSync)(tmp, JSON.stringify({ ...current.config, plugin: next }, null, 2) + "\n", "utf8");
964
+ (0, import_node_fs8.renameSync)(tmp, path);
802
965
  return null;
803
966
  } catch (error) {
804
967
  return `cannot retire legacy Kilo registrations: ${error?.message ?? error}`;
@@ -861,15 +1024,15 @@ function kiloArm(options) {
861
1024
  }
862
1025
 
863
1026
  // src/arm-kimi.ts
864
- var import_node_child_process6 = require("node:child_process");
865
- var import_node_fs9 = require("node:fs");
866
- var import_node_path7 = require("node:path");
1027
+ var import_node_child_process7 = require("node:child_process");
1028
+ var import_node_fs10 = require("node:fs");
1029
+ var import_node_path8 = require("node:path");
867
1030
 
868
1031
  // src/compat.ts
869
- var import_node_fs8 = require("node:fs");
1032
+ var import_node_fs9 = require("node:fs");
870
1033
  function readManifestCompat(manifestPath) {
871
1034
  try {
872
- const parsed = JSON.parse((0, import_node_fs8.readFileSync)(manifestPath, "utf8"));
1035
+ const parsed = JSON.parse((0, import_node_fs9.readFileSync)(manifestPath, "utf8"));
873
1036
  return typeof parsed.mmiCompat === "string" ? parsed.mmiCompat : void 0;
874
1037
  } catch {
875
1038
  return void 0;
@@ -880,11 +1043,11 @@ function readManifestCompat(manifestPath) {
880
1043
  var KIMI_PACKAGE = "@mutmutco/kimi-plugin";
881
1044
  var TREE_MARKERS = [".kimi-plugin/plugin.json", "skills/mmi/SKILL.md", "scripts/hook-run.mjs"];
882
1045
  function treeHealthy(root) {
883
- return TREE_MARKERS.every((rel) => fileExists((0, import_node_path7.join)(root, ...rel.split("/"))));
1046
+ return TREE_MARKERS.every((rel) => fileExists((0, import_node_path8.join)(root, ...rel.split("/"))));
884
1047
  }
885
1048
  function markerVersion(root) {
886
1049
  try {
887
- const version = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path7.join)(root, ".kimi-plugin", "plugin.json"), "utf8")).version;
1050
+ const version = JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path8.join)(root, ".kimi-plugin", "plugin.json"), "utf8")).version;
888
1051
  return typeof version === "string" ? version : null;
889
1052
  } catch {
890
1053
  return null;
@@ -892,14 +1055,14 @@ function markerVersion(root) {
892
1055
  }
893
1056
  function packageVersion(root) {
894
1057
  try {
895
- const version = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path7.join)(root, "package.json"), "utf8")).version;
1058
+ const version = JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path8.join)(root, "package.json"), "utf8")).version;
896
1059
  return typeof version === "string" ? version : null;
897
1060
  } catch {
898
1061
  return null;
899
1062
  }
900
1063
  }
901
1064
  function probeKimiInstallation(env = process.env) {
902
- const location = (0, import_node_path7.join)(kimiHome(env), "plugins", "managed", "mmi");
1065
+ const location = (0, import_node_path8.join)(kimiHome(env), "plugins", "managed", "mmi");
903
1066
  const version = markerVersion(location);
904
1067
  return { version, location, detail: version ? "installed Kimi plugin manifest" : "installed Kimi plugin manifest was unreadable" };
905
1068
  }
@@ -907,14 +1070,14 @@ function tail2(output) {
907
1070
  return output.split("\n").filter(Boolean).slice(-3).join(" | ");
908
1071
  }
909
1072
  function scratchRoot(env) {
910
- return (0, import_node_path7.join)(kimiHome(env), ".mmi-updater");
1073
+ return (0, import_node_path8.join)(kimiHome(env), ".mmi-updater");
911
1074
  }
912
1075
  function pruneScratchDir(root, keep) {
913
1076
  try {
914
- for (const name of (0, import_node_fs9.readdirSync)(root)) {
1077
+ for (const name of (0, import_node_fs10.readdirSync)(root)) {
915
1078
  if (name === keep) continue;
916
1079
  try {
917
- (0, import_node_fs9.rmSync)((0, import_node_path7.join)(root, name), { recursive: true, force: true });
1080
+ (0, import_node_fs10.rmSync)((0, import_node_path8.join)(root, name), { recursive: true, force: true });
918
1081
  } catch {
919
1082
  }
920
1083
  }
@@ -930,7 +1093,7 @@ function kimiProcessToken(line) {
930
1093
  }
931
1094
  function kimiHostRunning(env) {
932
1095
  if (env.KIMI_PLUGIN_ROOT?.trim()) return { running: true, evidence: "KIMI_PLUGIN_ROOT is set \u2014 this process is itself inside a Kimi hook" };
933
- const result = process.platform === "win32" ? (0, import_node_child_process6.spawnSync)("powershell.exe", ["-NoProfile", "-Command", "Get-CimInstance Win32_Process | ForEach-Object { $_.Name + ' ' + $_.CommandLine }"], { encoding: "utf8", windowsHide: true, timeout: 15e3 }) : (0, import_node_child_process6.spawnSync)("ps", ["-eo", "args="], { encoding: "utf8", windowsHide: true, timeout: 15e3 });
1096
+ const result = process.platform === "win32" ? (0, import_node_child_process7.spawnSync)("powershell.exe", ["-NoProfile", "-Command", "Get-CimInstance Win32_Process | ForEach-Object { $_.Name + ' ' + $_.CommandLine }"], { encoding: "utf8", windowsHide: true, timeout: 15e3 }) : (0, import_node_child_process7.spawnSync)("ps", ["-eo", "args="], { encoding: "utf8", windowsHide: true, timeout: 15e3 });
934
1097
  if (result.status !== 0 || !(result.stdout ?? "").trim()) return { running: true, evidence: "the process list is unreadable \u2014 unsafe to write" };
935
1098
  for (const line of result.stdout.split(/\r?\n/)) {
936
1099
  const token = kimiProcessToken(line);
@@ -940,11 +1103,11 @@ function kimiHostRunning(env) {
940
1103
  }
941
1104
  function restoreQuarantine(quarantine, live) {
942
1105
  try {
943
- (0, import_node_fs9.renameSync)(quarantine, live);
1106
+ (0, import_node_fs10.renameSync)(quarantine, live);
944
1107
  } catch (error) {
945
1108
  return { restored: false, detail: `${error?.message ?? error}` };
946
1109
  }
947
- if (!(0, import_node_fs9.existsSync)(live)) return { restored: false, detail: `${live} is still absent after the restore rename` };
1110
+ if (!(0, import_node_fs10.existsSync)(live)) return { restored: false, detail: `${live} is still absent after the restore rename` };
948
1111
  return { restored: true, detail: `marker ${markerVersion(live) ?? "unreadable"}` };
949
1112
  }
950
1113
  function kimiArm(options) {
@@ -958,10 +1121,10 @@ function kimiArm(options) {
958
1121
  detail,
959
1122
  // C11: the constraint of the tree LEFT at `live` — read at result time, so ok carries the new
960
1123
  // manifest's declaration and defer/fail carry the previous tree's (#4973 reconsult ruling).
961
- compat: readManifestCompat((0, import_node_path7.join)(live, ".kimi-plugin", "plugin.json"))
1124
+ compat: readManifestCompat((0, import_node_path8.join)(live, ".kimi-plugin", "plugin.json"))
962
1125
  });
963
- const managedParent = (0, import_node_path7.join)(kimiHome(env), "plugins", "managed");
964
- const live = (0, import_node_path7.join)(managedParent, "mmi");
1126
+ const managedParent = (0, import_node_path8.join)(kimiHome(env), "plugins", "managed");
1127
+ const live = (0, import_node_path8.join)(managedParent, "mmi");
965
1128
  const installed = markerVersion(live);
966
1129
  if (installed && compareSemver(installed, target) > 0) {
967
1130
  return result(installed, installed, "skip", `installed ${installed} is above candidate ${target} (monotonic)`);
@@ -976,11 +1139,11 @@ function kimiArm(options) {
976
1139
  if (dryRun) {
977
1140
  return result(installed, target, "ok", `dry-run: would converge ${installed ?? "absent"} -> ${target} (tarball generation switch while Kimi is absent)`);
978
1141
  }
979
- const stageRoot = (0, import_node_path7.join)(paths.staging, "kimi", target);
980
- const staged = (0, import_node_path7.join)(stageRoot, "node_modules", ...KIMI_PACKAGE.split("/"));
1142
+ const stageRoot = (0, import_node_path8.join)(paths.staging, "kimi", target);
1143
+ const staged = (0, import_node_path8.join)(stageRoot, "node_modules", ...KIMI_PACKAGE.split("/"));
981
1144
  if (!treeHealthy(staged)) {
982
- (0, import_node_fs9.mkdirSync)(stageRoot, { recursive: true });
983
- (0, import_node_fs9.writeFileSync)((0, import_node_path7.join)(stageRoot, "package.json"), JSON.stringify({ name: "mmi-updater-stage", private: true }) + "\n");
1145
+ (0, import_node_fs10.mkdirSync)(stageRoot, { recursive: true });
1146
+ (0, import_node_fs10.writeFileSync)((0, import_node_path8.join)(stageRoot, "package.json"), JSON.stringify({ name: "mmi-updater-stage", private: true }) + "\n");
984
1147
  const install = runNpm(["install", "--prefix", stageRoot, "--no-audit", "--no-fund", "--no-package-lock", `${KIMI_PACKAGE}@${target}`], env);
985
1148
  if (install.status !== 0) {
986
1149
  return result(installed, target, "defer", `staging install failed: ${tail2(install.stderr || install.stdout)}`);
@@ -999,75 +1162,79 @@ function kimiArm(options) {
999
1162
  }
1000
1163
  const quiescence = kimiHostRunning(env);
1001
1164
  if (quiescence.running) {
1002
- return result(installed, target, "defer", `Kimi may be running \u2014 the managed tree is switched only while the host is absent: ${quiescence.evidence}`);
1165
+ const pending = options.dryRun ? "" : ` ${noteBusyDefer(options.paths, env, { surface: "kimi", target, reason: quiescence.evidence, dirs: ["kimi"] })}`;
1166
+ return result(installed, target, "defer", `Kimi may be running \u2014 the managed tree is switched only while the host is absent: ${quiescence.evidence}${pending ? ` \u2014${pending}` : ""}`);
1003
1167
  }
1004
1168
  const scratch = scratchRoot(env);
1005
- const incomingRoot = (0, import_node_path7.join)(scratch, "incoming");
1006
- const incoming = (0, import_node_path7.join)(incomingRoot, target);
1007
- const quarantineRoot = (0, import_node_path7.join)(scratch, "quarantine");
1008
- const rejectedRoot = (0, import_node_path7.join)(scratch, "rejected");
1169
+ const incomingRoot = (0, import_node_path8.join)(scratch, "incoming");
1170
+ const incoming = (0, import_node_path8.join)(incomingRoot, target);
1171
+ const quarantineRoot = (0, import_node_path8.join)(scratch, "quarantine");
1172
+ const rejectedRoot = (0, import_node_path8.join)(scratch, "rejected");
1009
1173
  try {
1010
- (0, import_node_fs9.mkdirSync)(managedParent, { recursive: true });
1011
- (0, import_node_fs9.mkdirSync)(quarantineRoot, { recursive: true });
1012
- (0, import_node_fs9.rmSync)(incomingRoot, { recursive: true, force: true });
1013
- (0, import_node_fs9.cpSync)(staged, incoming, { recursive: true });
1174
+ (0, import_node_fs10.mkdirSync)(managedParent, { recursive: true });
1175
+ (0, import_node_fs10.mkdirSync)(quarantineRoot, { recursive: true });
1176
+ (0, import_node_fs10.rmSync)(incomingRoot, { recursive: true, force: true });
1177
+ (0, import_node_fs10.cpSync)(staged, incoming, { recursive: true });
1014
1178
  } catch (error) {
1015
1179
  return result(installed, target, "defer", `cannot place the incoming generation under ${scratch}: ${error?.message ?? error}`);
1016
1180
  }
1017
1181
  if (markerVersion(incoming) !== target || !treeHealthy(incoming)) {
1018
1182
  return result(installed, target, "defer", `incoming generation verifies as ${markerVersion(incoming) ?? "unreadable"}, expected ${target}`);
1019
1183
  }
1020
- const hadLive = (0, import_node_fs9.existsSync)(live);
1184
+ const hadLive = (0, import_node_fs10.existsSync)(live);
1021
1185
  const generation = `${installed ?? "unknown"}-${Date.now()}`;
1022
- const quarantine = (0, import_node_path7.join)(quarantineRoot, generation);
1186
+ const quarantine = (0, import_node_path8.join)(quarantineRoot, generation);
1023
1187
  if (hadLive) {
1024
1188
  try {
1025
- (0, import_node_fs9.renameSync)(live, quarantine);
1189
+ (0, import_node_fs10.renameSync)(live, quarantine);
1026
1190
  } catch (error) {
1027
- return result(installed, target, "defer", `cannot quarantine the live generation: ${error?.message ?? error}`);
1191
+ const reason = String(error?.message ?? error);
1192
+ const pending = lockShaped(reason) ? ` \u2014 ${noteBusyDefer(options.paths, env, { surface: "kimi", target, reason, dirs: ["kimi"] })}` : "";
1193
+ return result(installed, target, "defer", `cannot quarantine the live generation: ${reason}${pending}`);
1028
1194
  }
1029
1195
  }
1030
1196
  try {
1031
- (0, import_node_fs9.renameSync)(incoming, live);
1197
+ (0, import_node_fs10.renameSync)(incoming, live);
1032
1198
  } catch (error) {
1033
1199
  const failed = `cannot switch the new generation in: ${error?.message ?? error}`;
1034
1200
  if (!hadLive) {
1035
1201
  return result(installed, target, "defer", `${failed} \u2014 no generation was displaced; ${live} is still absent`);
1036
1202
  }
1037
1203
  const restore = restoreQuarantine(quarantine, live);
1038
- return restore.restored ? result(installed, installed ?? "unknown", "defer", `${failed} \u2014 the previous generation is back in place (${restore.detail})`) : result(installed, "none", "fail", `${failed}; ROLLBACK FAILED (${restore.detail}) \u2014 ${live} now has NO plugin and Kimi enforces nothing until this is repaired; the known-good generation is intact at ${quarantine}`);
1204
+ const pending = restore.restored && lockShaped(failed) ? ` \u2014 ${noteBusyDefer(options.paths, env, { surface: "kimi", target, reason: failed, dirs: ["kimi"] })}` : "";
1205
+ return restore.restored ? result(installed, installed ?? "unknown", "defer", `${failed} \u2014 the previous generation is back in place (${restore.detail})${pending}`) : result(installed, "none", "fail", `${failed}; ROLLBACK FAILED (${restore.detail}) \u2014 ${live} now has NO plugin and Kimi enforces nothing until this is repaired; the known-good generation is intact at ${quarantine}`);
1039
1206
  }
1040
1207
  const switched = markerVersion(live);
1041
1208
  if (switched !== target || !treeHealthy(live)) {
1042
1209
  const observed = `post-switch marker is ${switched ?? "unreadable"}, expected ${target}`;
1043
1210
  const rejectedName = `${target}-${Date.now()}`;
1044
1211
  try {
1045
- (0, import_node_fs9.mkdirSync)(rejectedRoot, { recursive: true });
1046
- (0, import_node_fs9.renameSync)(live, (0, import_node_path7.join)(rejectedRoot, rejectedName));
1212
+ (0, import_node_fs10.mkdirSync)(rejectedRoot, { recursive: true });
1213
+ (0, import_node_fs10.renameSync)(live, (0, import_node_path8.join)(rejectedRoot, rejectedName));
1047
1214
  } catch (error) {
1048
1215
  return result(installed, switched ?? "unknown", "fail", `${observed} \u2014 the unverified tree could NOT be moved aside (${error?.message ?? error}) and is STILL LIVE at ${live}${hadLive ? `; the previous generation is intact at ${quarantine}` : ""}`);
1049
1216
  }
1050
1217
  pruneScratchDir(rejectedRoot, rejectedName);
1051
1218
  if (!hadLive) {
1052
- return result(installed, "none", "fail", `${observed} \u2014 the unverified tree is parked at ${(0, import_node_path7.join)(rejectedRoot, rejectedName)}; ${live} is absent (there was no previous generation to restore)`);
1219
+ return result(installed, "none", "fail", `${observed} \u2014 the unverified tree is parked at ${(0, import_node_path8.join)(rejectedRoot, rejectedName)}; ${live} is absent (there was no previous generation to restore)`);
1053
1220
  }
1054
1221
  const restore = restoreQuarantine(quarantine, live);
1055
- return restore.restored ? result(installed, installed ?? "unknown", "fail", `${observed} \u2014 the unverified tree is parked at ${(0, import_node_path7.join)(rejectedRoot, rejectedName)}; rolled back to ${installed ?? "the previous generation"} and read back (${restore.detail})`) : result(installed, "none", "fail", `${observed}; ROLLBACK FAILED (${restore.detail}) \u2014 ${live} now has NO plugin and Kimi enforces nothing until this is repaired; the known-good generation is intact at ${quarantine}`);
1222
+ return restore.restored ? result(installed, installed ?? "unknown", "fail", `${observed} \u2014 the unverified tree is parked at ${(0, import_node_path8.join)(rejectedRoot, rejectedName)}; rolled back to ${installed ?? "the previous generation"} and read back (${restore.detail})`) : result(installed, "none", "fail", `${observed}; ROLLBACK FAILED (${restore.detail}) \u2014 ${live} now has NO plugin and Kimi enforces nothing until this is repaired; the known-good generation is intact at ${quarantine}`);
1056
1223
  }
1057
1224
  pruneScratchDir(quarantineRoot, hadLive ? generation : null);
1058
1225
  pruneScratchDir(rejectedRoot, null);
1059
1226
  try {
1060
- (0, import_node_fs9.rmSync)(incomingRoot, { recursive: true, force: true });
1227
+ (0, import_node_fs10.rmSync)(incomingRoot, { recursive: true, force: true });
1061
1228
  } catch {
1062
1229
  }
1063
1230
  return result(installed, target, "ok", `converged ${installed ?? "absent"} -> ${target} (staged, verified, switched${hadLive ? `; previous generation quarantined at ${quarantine}` : ""})`);
1064
1231
  }
1065
1232
 
1066
1233
  // src/arm-cursor.ts
1067
- var import_node_fs10 = require("node:fs");
1068
- var import_node_child_process7 = require("node:child_process");
1234
+ var import_node_fs11 = require("node:fs");
1235
+ var import_node_child_process8 = require("node:child_process");
1069
1236
  var import_node_os4 = require("node:os");
1070
- var import_node_path8 = require("node:path");
1237
+ var import_node_path9 = require("node:path");
1071
1238
  var PACKAGE = "@mutmutco/cursor-plugin";
1072
1239
  var MANIFEST = ".cursor-plugin/plugin.json";
1073
1240
  var USER_HOOKS_FILE = "hooks.json";
@@ -1076,14 +1243,14 @@ var KEEP_GENERATIONS = 2;
1076
1243
  var STRANDED_TMP_AGE_MS = 24 * 60 * 6e4;
1077
1244
  var TREE_FILES = [MANIFEST, "skills/mmi/SKILL.md", "hooks/cursor-hooks.json", "scripts/hook-run.mjs", "scripts/hook-policy.mjs"];
1078
1245
  function cursorPluginsRoot() {
1079
- return (0, import_node_path8.join)((0, import_node_os4.homedir)(), ".cursor", "plugins");
1246
+ return (0, import_node_path9.join)((0, import_node_os4.homedir)(), ".cursor", "plugins");
1080
1247
  }
1081
1248
  function treeHealthy2(root) {
1082
- return TREE_FILES.every((rel) => (0, import_node_fs10.existsSync)((0, import_node_path8.join)(root, ...rel.split("/"))));
1249
+ return TREE_FILES.every((rel) => (0, import_node_fs11.existsSync)((0, import_node_path9.join)(root, ...rel.split("/"))));
1083
1250
  }
1084
1251
  function readJson(root, rel) {
1085
1252
  try {
1086
- return JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path8.join)(root, ...rel.split("/")), "utf8"));
1253
+ return JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path9.join)(root, ...rel.split("/")), "utf8"));
1087
1254
  } catch {
1088
1255
  return null;
1089
1256
  }
@@ -1092,7 +1259,7 @@ function manifest(root) {
1092
1259
  return readJson(root, MANIFEST);
1093
1260
  }
1094
1261
  function probeCursorInstallation() {
1095
- const location = (0, import_node_path8.join)(cursorPluginsRoot(), "local", "mmi");
1262
+ const location = (0, import_node_path9.join)(cursorPluginsRoot(), "local", "mmi");
1096
1263
  const version = manifest(location)?.version ?? null;
1097
1264
  return { version, location, detail: version ? "installed Cursor plugin manifest" : "installed Cursor plugin manifest was unreadable" };
1098
1265
  }
@@ -1107,7 +1274,7 @@ function message(error) {
1107
1274
  }
1108
1275
  function discard(path) {
1109
1276
  try {
1110
- (0, import_node_fs10.rmSync)(path, { recursive: true, force: true });
1277
+ (0, import_node_fs11.rmSync)(path, { recursive: true, force: true });
1111
1278
  } catch {
1112
1279
  }
1113
1280
  }
@@ -1126,7 +1293,7 @@ function cursorHostEvidence(env) {
1126
1293
  if (env[marker]?.trim()) return `env marker ${marker}`;
1127
1294
  }
1128
1295
  if (env.CURSOR_AGENT === "1") return "env marker CURSOR_AGENT=1";
1129
- const result = process.platform === "win32" ? (0, import_node_child_process7.spawnSync)("powershell.exe", ["-NoProfile", "-Command", "Get-CimInstance Win32_Process | ForEach-Object { $_.CommandLine }"], { encoding: "utf8", windowsHide: true, timeout: 15e3 }) : (0, import_node_child_process7.spawnSync)("ps", ["-eo", "args="], { encoding: "utf8", windowsHide: true, timeout: 15e3 });
1296
+ const result = process.platform === "win32" ? (0, import_node_child_process8.spawnSync)("powershell.exe", ["-NoProfile", "-Command", "Get-CimInstance Win32_Process | ForEach-Object { $_.CommandLine }"], { encoding: "utf8", windowsHide: true, timeout: 15e3 }) : (0, import_node_child_process8.spawnSync)("ps", ["-eo", "args="], { encoding: "utf8", windowsHide: true, timeout: 15e3 });
1130
1297
  if (result.status !== 0 || !(result.stdout ?? "").trim()) return "the process table is unreadable (unsafe to write)";
1131
1298
  for (const line of result.stdout.split(/\r?\n/)) {
1132
1299
  const executable = cursorExecutable(line);
@@ -1135,7 +1302,7 @@ function cursorHostEvidence(env) {
1135
1302
  return null;
1136
1303
  }
1137
1304
  function resolvedPluginRoot(root) {
1138
- return (0, import_node_path8.join)(root).replace(/\\/g, "/");
1305
+ return (0, import_node_path9.join)(root).replace(/\\/g, "/");
1139
1306
  }
1140
1307
  function isMmiEntry(entry, pluginRoot) {
1141
1308
  return typeof entry === "object" && entry !== null && typeof entry.command === "string" && entry.command.includes(resolvedPluginRoot(pluginRoot));
@@ -1145,7 +1312,7 @@ function resolveCommand(command, pluginRoot) {
1145
1312
  }
1146
1313
  function desiredUserHooks(pluginRoot) {
1147
1314
  try {
1148
- const doc = JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path8.join)(pluginRoot, ...PLUGIN_HOOKS_REL), "utf8"));
1315
+ const doc = JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path9.join)(pluginRoot, ...PLUGIN_HOOKS_REL), "utf8"));
1149
1316
  const hooks = doc.hooks;
1150
1317
  if (typeof hooks !== "object" || hooks === null || Array.isArray(hooks)) return null;
1151
1318
  const desired = /* @__PURE__ */ new Map();
@@ -1181,14 +1348,14 @@ function mergeUserHooks(existing, desired, pluginRoot) {
1181
1348
  function reconcileCursorUserHooks(pluginRoot, homeDir) {
1182
1349
  const desired = desiredUserHooks(pluginRoot);
1183
1350
  if (!desired) return "source-unreadable";
1184
- const path = (0, import_node_path8.join)(homeDir, ".cursor", USER_HOOKS_FILE);
1185
- if (!(0, import_node_fs10.existsSync)(path)) {
1186
- (0, import_node_fs10.writeFileSync)(path, JSON.stringify({ version: 1, hooks: Object.fromEntries(desired) }, null, 2) + "\n", "utf8");
1351
+ const path = (0, import_node_path9.join)(homeDir, ".cursor", USER_HOOKS_FILE);
1352
+ if (!(0, import_node_fs11.existsSync)(path)) {
1353
+ (0, import_node_fs11.writeFileSync)(path, JSON.stringify({ version: 1, hooks: Object.fromEntries(desired) }, null, 2) + "\n", "utf8");
1187
1354
  return "updated";
1188
1355
  }
1189
1356
  let existing;
1190
1357
  try {
1191
- existing = JSON.parse((0, import_node_fs10.readFileSync)(path, "utf8"));
1358
+ existing = JSON.parse((0, import_node_fs11.readFileSync)(path, "utf8"));
1192
1359
  } catch {
1193
1360
  return "user-conflict";
1194
1361
  }
@@ -1196,8 +1363,8 @@ function reconcileCursorUserHooks(pluginRoot, homeDir) {
1196
1363
  if (!merged) return "user-conflict";
1197
1364
  if (JSON.stringify(merged) === JSON.stringify(existing)) return "current";
1198
1365
  const tmp = `${path}.tmp-${process.pid}`;
1199
- (0, import_node_fs10.writeFileSync)(tmp, JSON.stringify(merged, null, 2) + "\n", "utf8");
1200
- (0, import_node_fs10.renameSync)(tmp, path);
1366
+ (0, import_node_fs11.writeFileSync)(tmp, JSON.stringify(merged, null, 2) + "\n", "utf8");
1367
+ (0, import_node_fs11.renameSync)(tmp, path);
1201
1368
  return "updated";
1202
1369
  }
1203
1370
  function userHooksDetail(pluginRoot, homeDir) {
@@ -1205,7 +1372,7 @@ function userHooksDetail(pluginRoot, homeDir) {
1205
1372
  case "updated":
1206
1373
  return "; user hooks bridged into ~/.cursor/hooks.json";
1207
1374
  case "user-conflict":
1208
- return `; USER HOOKS CONFLICT at ${(0, import_node_path8.join)(homeDir, ".cursor", USER_HOOKS_FILE)} \u2014 Cursor gates are NOT bridged; fix or remove that file and reconcile`;
1375
+ return `; USER HOOKS CONFLICT at ${(0, import_node_path9.join)(homeDir, ".cursor", USER_HOOKS_FILE)} \u2014 Cursor gates are NOT bridged; fix or remove that file and reconcile`;
1209
1376
  case "source-unreadable":
1210
1377
  return "; user hooks source unreadable \u2014 Cursor gates are NOT bridged";
1211
1378
  default:
@@ -1213,35 +1380,35 @@ function userHooksDetail(pluginRoot, homeDir) {
1213
1380
  }
1214
1381
  }
1215
1382
  function writeRollbackPointer(root, entry) {
1216
- const path = (0, import_node_path8.join)(root, "quarantine", "rollback.json");
1383
+ const path = (0, import_node_path9.join)(root, "quarantine", "rollback.json");
1217
1384
  const tmp = `${path}.tmp-${process.pid}`;
1218
1385
  try {
1219
- (0, import_node_fs10.writeFileSync)(tmp, JSON.stringify({ surface: "cursor", ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry }, null, 2) + "\n", "utf8");
1220
- (0, import_node_fs10.renameSync)(tmp, path);
1386
+ (0, import_node_fs11.writeFileSync)(tmp, JSON.stringify({ surface: "cursor", ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry }, null, 2) + "\n", "utf8");
1387
+ (0, import_node_fs11.renameSync)(tmp, path);
1221
1388
  } catch {
1222
1389
  discard(tmp);
1223
1390
  }
1224
1391
  }
1225
1392
  function mtimeMs(path) {
1226
1393
  try {
1227
- return (0, import_node_fs10.statSync)(path).mtimeMs;
1394
+ return (0, import_node_fs11.statSync)(path).mtimeMs;
1228
1395
  } catch {
1229
1396
  return 0;
1230
1397
  }
1231
1398
  }
1232
1399
  function pruneQuarantine(root, keep) {
1233
- const dir = (0, import_node_path8.join)(root, "quarantine");
1400
+ const dir = (0, import_node_path9.join)(root, "quarantine");
1234
1401
  let generations;
1235
1402
  try {
1236
- generations = (0, import_node_fs10.readdirSync)(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith("mmi-")).map((entry) => (0, import_node_path8.join)(dir, entry.name)).sort((a, b) => mtimeMs(b) - mtimeMs(a));
1403
+ generations = (0, import_node_fs11.readdirSync)(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith("mmi-")).map((entry) => (0, import_node_path9.join)(dir, entry.name)).sort((a, b) => mtimeMs(b) - mtimeMs(a));
1237
1404
  } catch {
1238
1405
  return 0;
1239
1406
  }
1240
1407
  let stranded = 0;
1241
1408
  try {
1242
- for (const entry of (0, import_node_fs10.readdirSync)(dir, { withFileTypes: true })) {
1409
+ for (const entry of (0, import_node_fs11.readdirSync)(dir, { withFileTypes: true })) {
1243
1410
  if (!entry.isFile() || !/^rollback\.json\.tmp-\d+$/.test(entry.name)) continue;
1244
- const path = (0, import_node_path8.join)(dir, entry.name);
1411
+ const path = (0, import_node_path9.join)(dir, entry.name);
1245
1412
  if (Date.now() - mtimeMs(path) < STRANDED_TMP_AGE_MS) continue;
1246
1413
  discard(path);
1247
1414
  stranded += 1;
@@ -1259,16 +1426,16 @@ function pruneQuarantine(root, keep) {
1259
1426
  }
1260
1427
  function restorePrevious(live, quarantined, broken) {
1261
1428
  try {
1262
- (0, import_node_fs10.renameSync)(live, broken);
1429
+ (0, import_node_fs11.renameSync)(live, broken);
1263
1430
  } catch {
1264
1431
  return "kept-broken";
1265
1432
  }
1266
1433
  try {
1267
- (0, import_node_fs10.renameSync)(quarantined, live);
1434
+ (0, import_node_fs11.renameSync)(quarantined, live);
1268
1435
  return "restored";
1269
1436
  } catch {
1270
1437
  try {
1271
- (0, import_node_fs10.renameSync)(broken, live);
1438
+ (0, import_node_fs11.renameSync)(broken, live);
1272
1439
  return "kept-broken";
1273
1440
  } catch {
1274
1441
  return "lost";
@@ -1279,7 +1446,7 @@ function cursorArm(options) {
1279
1446
  const env = options.env ?? process.env;
1280
1447
  const { target, dryRun, paths } = options;
1281
1448
  const root = cursorPluginsRoot();
1282
- const live = (0, import_node_path8.join)(root, "local", "mmi");
1449
+ const live = (0, import_node_path9.join)(root, "local", "mmi");
1283
1450
  const result = (from, to, verdict, detail) => ({
1284
1451
  surface: "cursor",
1285
1452
  from,
@@ -1288,7 +1455,7 @@ function cursorArm(options) {
1288
1455
  detail,
1289
1456
  // C11: the constraint of the generation LEFT at `live` — read at result time, so a failed
1290
1457
  // switch reports the restored generation's declaration, not the rejected one's (#4973).
1291
- compat: readManifestCompat((0, import_node_path8.join)(live, MANIFEST))
1458
+ compat: readManifestCompat((0, import_node_path9.join)(live, MANIFEST))
1292
1459
  });
1293
1460
  const current = manifest(live);
1294
1461
  const installed = current?.version ?? null;
@@ -1302,7 +1469,7 @@ function cursorArm(options) {
1302
1469
  if (installed === target && treeHealthy2(live)) {
1303
1470
  return result(installed, target, "ok", `already at target ${target}${userHooksDetail(live, (0, import_node_os4.homedir)())}`);
1304
1471
  }
1305
- if ((0, import_node_fs10.existsSync)(live) && !mmiOwned(live)) {
1472
+ if ((0, import_node_fs11.existsSync)(live) && !mmiOwned(live)) {
1306
1473
  return result(installed, target, "defer", `${live} carries no MMI ownership marker (plugin manifest name, or a ${PACKAGE} package.json) \u2014 refusing to displace an unmanaged directory (mmi-cli plugin heal adopts it)`);
1307
1474
  }
1308
1475
  if (dryRun) {
@@ -1318,15 +1485,16 @@ function cursorArm(options) {
1318
1485
  }
1319
1486
  const busy = cursorHostEvidence(env);
1320
1487
  if (busy) {
1321
- return result(installed, target, "defer", `Cursor is running (${busy}) \u2014 the plugin tree is only swapped while the host is absent`);
1488
+ const pending = dryRun ? "" : ` \u2014 ${noteBusyDefer(paths, env, { surface: "cursor", target, reason: busy, images: ["Cursor"] })}`;
1489
+ return result(installed, target, "defer", `Cursor is running (${busy}) \u2014 the plugin tree is only swapped while the host is absent${pending}`);
1322
1490
  }
1323
1491
  const stamp = Date.now();
1324
- const incoming = (0, import_node_path8.join)(root, "staging", `mmi-${safeSegment(target)}-${stamp}`);
1325
- const quarantined = (0, import_node_path8.join)(root, "quarantine", `mmi-${safeSegment(installed ?? "unknown")}-${stamp}`);
1326
- for (const dir of ["local", "staging", "quarantine"]) (0, import_node_fs10.mkdirSync)((0, import_node_path8.join)(root, dir), { recursive: true });
1492
+ const incoming = (0, import_node_path9.join)(root, "staging", `mmi-${safeSegment(target)}-${stamp}`);
1493
+ const quarantined = (0, import_node_path9.join)(root, "quarantine", `mmi-${safeSegment(installed ?? "unknown")}-${stamp}`);
1494
+ for (const dir of ["local", "staging", "quarantine"]) (0, import_node_fs11.mkdirSync)((0, import_node_path9.join)(root, dir), { recursive: true });
1327
1495
  try {
1328
1496
  discard(incoming);
1329
- (0, import_node_fs10.cpSync)(staged.packageRoot, incoming, { recursive: true });
1497
+ (0, import_node_fs11.cpSync)(staged.packageRoot, incoming, { recursive: true });
1330
1498
  } catch (error) {
1331
1499
  discard(incoming);
1332
1500
  return result(installed, target, "defer", `copy into ${root} failed: ${message(error)}`);
@@ -1338,28 +1506,31 @@ function cursorArm(options) {
1338
1506
  const started = cursorHostEvidence(env);
1339
1507
  if (started) {
1340
1508
  discard(incoming);
1341
- return result(installed, target, "defer", `Cursor started during staging (${started}) \u2014 the plugin tree is only swapped while the host is absent`);
1509
+ const pending = noteBusyDefer(paths, env, { surface: "cursor", target, reason: started, images: ["Cursor"] });
1510
+ return result(installed, target, "defer", `Cursor started during staging (${started}) \u2014 the plugin tree is only swapped while the host is absent \u2014 ${pending}`);
1342
1511
  }
1343
1512
  let displaced = false;
1344
1513
  try {
1345
- if ((0, import_node_fs10.existsSync)(live)) {
1346
- (0, import_node_fs10.renameSync)(live, quarantined);
1514
+ if ((0, import_node_fs11.existsSync)(live)) {
1515
+ (0, import_node_fs11.renameSync)(live, quarantined);
1347
1516
  displaced = true;
1348
1517
  }
1349
- (0, import_node_fs10.renameSync)(incoming, live);
1518
+ (0, import_node_fs11.renameSync)(incoming, live);
1350
1519
  } catch (error) {
1351
- if (displaced && !(0, import_node_fs10.existsSync)(live)) {
1520
+ if (displaced && !(0, import_node_fs11.existsSync)(live)) {
1352
1521
  try {
1353
- (0, import_node_fs10.renameSync)(quarantined, live);
1522
+ (0, import_node_fs11.renameSync)(quarantined, live);
1354
1523
  } catch {
1355
1524
  }
1356
1525
  }
1357
1526
  discard(incoming);
1358
- return result(installed, target, "defer", `generation switch failed, previous generation retained: ${message(error)}`);
1527
+ const reason = message(error);
1528
+ const pending = lockShaped(reason) ? ` \u2014 ${noteBusyDefer(paths, env, { surface: "cursor", target, reason, images: ["Cursor"] })}` : "";
1529
+ return result(installed, target, "defer", `generation switch failed, previous generation retained: ${reason}${pending}`);
1359
1530
  }
1360
1531
  const evidence = manifest(live)?.version;
1361
1532
  if (evidence !== target || !treeHealthy2(live)) {
1362
- const broken = `${(0, import_node_path8.join)(root, "quarantine", `mmi-${safeSegment(evidence ?? "broken")}-${stamp}`)}-failed`;
1533
+ const broken = `${(0, import_node_path9.join)(root, "quarantine", `mmi-${safeSegment(evidence ?? "broken")}-${stamp}`)}-failed`;
1363
1534
  const outcome = displaced ? restorePrevious(live, quarantined, broken) : "none";
1364
1535
  const detail = outcome === "restored" ? `restored the previous generation ${installed ?? "unknown"}, unverified tree kept at ${broken}` : outcome === "lost" ? `restore failed and ${live} is missing \u2014 the previous generation is at ${quarantined}` : displaced ? `restore failed, the unverified tree is still live and the previous generation is at ${quarantined}` : "nothing was displaced \u2014 there is no previous generation to restore";
1365
1536
  writeRollbackPointer(root, outcome === "restored" ? { installed: installed ?? "unknown", previous: null, generation: null } : { installed: evidence ?? "unknown", previous: installed, generation: displaced ? quarantined : null });
@@ -1372,8 +1543,8 @@ function cursorArm(options) {
1372
1543
  }
1373
1544
 
1374
1545
  // src/arm-jervcode.ts
1375
- var import_node_fs11 = require("node:fs");
1376
- var import_node_path9 = require("node:path");
1546
+ var import_node_fs12 = require("node:fs");
1547
+ var import_node_path10 = require("node:path");
1377
1548
  var PI_PACKAGE = "@mutmutco/pi-plugin";
1378
1549
  function hostEnv(env) {
1379
1550
  const dir = jervcodeAgentDir(env);
@@ -1384,7 +1555,7 @@ function hostCommands(_env) {
1384
1555
  }
1385
1556
  function readPackageSpecs(env) {
1386
1557
  try {
1387
- const settings = JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path9.join)(jervcodeAgentDir(env), "settings.json"), "utf8"));
1558
+ const settings = JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path10.join)(jervcodeAgentDir(env), "settings.json"), "utf8"));
1388
1559
  return Array.isArray(settings.packages) ? settings.packages.filter((entry) => typeof entry === "string") : [];
1389
1560
  } catch {
1390
1561
  return null;
@@ -1392,14 +1563,14 @@ function readPackageSpecs(env) {
1392
1563
  }
1393
1564
  function installedVersion(env) {
1394
1565
  try {
1395
- const manifest2 = JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path9.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json"), "utf8"));
1566
+ const manifest2 = JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path10.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json"), "utf8"));
1396
1567
  return typeof manifest2.version === "string" ? manifest2.version : null;
1397
1568
  } catch {
1398
1569
  return null;
1399
1570
  }
1400
1571
  }
1401
1572
  function probeJervcodeInstallation(env = process.env) {
1402
- const location = (0, import_node_path9.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"));
1573
+ const location = (0, import_node_path10.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"));
1403
1574
  const version = installedVersion(env);
1404
1575
  return { version, location, detail: version ? "materialized pi package manifest" : "materialized pi package manifest was unreadable" };
1405
1576
  }
@@ -1421,32 +1592,32 @@ function jervcodeArm(options) {
1421
1592
  detail,
1422
1593
  // C11: the constraint of the package pi LEFT materialised — read at result time, so defer/fail
1423
1594
  // carry the previous payload's declaration (#4973 reconsult ruling).
1424
- compat: readManifestCompat((0, import_node_path9.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json"))
1595
+ compat: readManifestCompat((0, import_node_path10.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json"))
1425
1596
  });
1426
1597
  const before = readPackageSpecs(env);
1427
- if (!before) return result(null, options.target, "defer", `cannot read ${(0, import_node_path9.join)(jervcodeAgentDir(env), "settings.json")}`);
1598
+ if (!before) return result(null, options.target, "defer", `cannot read ${(0, import_node_path10.join)(jervcodeAgentDir(env), "settings.json")}`);
1428
1599
  const from = installedVersion(env);
1429
1600
  const legacy = before.filter(isLegacyMmiPiPath);
1430
1601
  const highWater = journalHighWater(options.paths.journalPath, "jervcode");
1431
1602
  const floor = from && compareSemver(from, options.target) > 0 ? `installed ${from} is above candidate ${options.target} (monotonic)` : highWater && compareSemver(highWater, options.target) > 0 ? `journal high-water ${highWater} is above candidate ${options.target} (monotonic)` : null;
1432
1603
  if (options.dryRun) {
1433
1604
  const action = floor ? `hold ${from ?? "the install"} and skip the install (${floor})` : before.includes(exact) && from === options.target ? "keep exact pin" : `install ${exact}`;
1434
- return { surface: "jervcode", from, to: options.target, verdict: "ok", detail: `dry-run: would ${action}${legacy.length ? ` and retire ${legacy.length} legacy claude-cache path registration${legacy.length === 1 ? "" : "s"}` : ""}`, compat: readManifestCompat((0, import_node_path9.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1605
+ return { surface: "jervcode", from, to: options.target, verdict: "ok", detail: `dry-run: would ${action}${legacy.length ? ` and retire ${legacy.length} legacy claude-cache path registration${legacy.length === 1 ? "" : "s"}` : ""}`, compat: readManifestCompat((0, import_node_path10.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1435
1606
  }
1436
1607
  const hosted = hostEnv(env);
1437
1608
  const commands = hostCommands(env);
1438
1609
  const command = commands.find((bin) => runner(bin, ["--version"], hosted).status === 0);
1439
- if (!command) return { surface: "jervcode", from, to: options.target, verdict: "defer", detail: "neither `jervcode` nor `pi` answered --version \u2014 the host CLI is not on PATH", compat: readManifestCompat((0, import_node_path9.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1610
+ if (!command) return { surface: "jervcode", from, to: options.target, verdict: "defer", detail: "neither `jervcode` nor `pi` answered --version \u2014 the host CLI is not on PATH", compat: readManifestCompat((0, import_node_path10.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1440
1611
  if (!floor && (!before.includes(exact) || from !== options.target)) {
1441
1612
  const install = runner(command, ["install", exact], hosted);
1442
1613
  if (install.status !== 0) {
1443
- return { surface: "jervcode", from, to: options.target, verdict: "defer", detail: `${command} install ${exact} failed: ${failure2(install)}`, compat: readManifestCompat((0, import_node_path9.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1614
+ return { surface: "jervcode", from, to: options.target, verdict: "defer", detail: `${command} install ${exact} failed: ${failure2(install)}`, compat: readManifestCompat((0, import_node_path10.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1444
1615
  }
1445
1616
  }
1446
1617
  for (const stale of legacy) {
1447
1618
  const removed = runner(command, ["remove", stale], hosted);
1448
1619
  if (removed.status !== 0) {
1449
- return { surface: "jervcode", from, to: options.target, verdict: "defer", detail: `${command} remove of the legacy registration failed: ${failure2(removed)}`, compat: readManifestCompat((0, import_node_path9.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1620
+ return { surface: "jervcode", from, to: options.target, verdict: "defer", detail: `${command} remove of the legacy registration failed: ${failure2(removed)}`, compat: readManifestCompat((0, import_node_path10.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1450
1621
  }
1451
1622
  }
1452
1623
  const after = readPackageSpecs(env);
@@ -1454,7 +1625,7 @@ function jervcodeArm(options) {
1454
1625
  const legacyAfter = after?.filter(isLegacyMmiPiPath) ?? [];
1455
1626
  const retired = legacy.length ? `; retired ${legacy.length} legacy claude-cache path registration${legacy.length === 1 ? "" : "s"}` : "";
1456
1627
  if (floor && !legacyAfter.length) {
1457
- return { surface: "jervcode", from, to: converged ?? highWater ?? options.target, verdict: "skip", detail: `${floor}${retired}`, compat: readManifestCompat((0, import_node_path9.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1628
+ return { surface: "jervcode", from, to: converged ?? highWater ?? options.target, verdict: "skip", detail: `${floor}${retired}`, compat: readManifestCompat((0, import_node_path10.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1458
1629
  }
1459
1630
  if (!after?.includes(exact) || converged !== options.target || legacyAfter.length) {
1460
1631
  return {
@@ -1475,14 +1646,15 @@ function jervcodeArm(options) {
1475
1646
  }
1476
1647
 
1477
1648
  // src/arm-hermes.ts
1478
- var import_node_fs12 = require("node:fs");
1479
- var import_node_path10 = require("node:path");
1649
+ var import_node_fs13 = require("node:fs");
1650
+ var import_node_child_process9 = require("node:child_process");
1651
+ var import_node_path11 = require("node:path");
1480
1652
  var PACKAGE2 = "@mutmutco/hermes-plugin";
1481
1653
  var MANIFEST2 = "plugin.yaml";
1482
1654
  var TREE_MARKERS2 = [MANIFEST2, "__init__.py", "skills"];
1483
1655
  function yamlVersion(root) {
1484
1656
  try {
1485
- const text = (0, import_node_fs12.readFileSync)((0, import_node_path10.join)(root, MANIFEST2), "utf8");
1657
+ const text = (0, import_node_fs13.readFileSync)((0, import_node_path11.join)(root, MANIFEST2), "utf8");
1486
1658
  try {
1487
1659
  const version = JSON.parse(text).version;
1488
1660
  if (typeof version === "string" && version.trim()) return version.trim();
@@ -1495,9 +1667,9 @@ function yamlVersion(root) {
1495
1667
  }
1496
1668
  }
1497
1669
  function treeHealthy3(root) {
1498
- return TREE_MARKERS2.slice(0, 2).every((file) => (0, import_node_fs12.existsSync)((0, import_node_path10.join)(root, file))) && (() => {
1670
+ return TREE_MARKERS2.slice(0, 2).every((file) => (0, import_node_fs13.existsSync)((0, import_node_path11.join)(root, file))) && (() => {
1499
1671
  try {
1500
- return (0, import_node_fs12.statSync)((0, import_node_path10.join)(root, "skills")).isDirectory();
1672
+ return (0, import_node_fs13.statSync)((0, import_node_path11.join)(root, "skills")).isDirectory();
1501
1673
  } catch {
1502
1674
  return false;
1503
1675
  }
@@ -1505,13 +1677,13 @@ function treeHealthy3(root) {
1505
1677
  }
1506
1678
  function packageIdentity(root) {
1507
1679
  try {
1508
- return JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path10.join)(root, "package.json"), "utf8")).name === PACKAGE2;
1680
+ return JSON.parse((0, import_node_fs13.readFileSync)((0, import_node_path11.join)(root, "package.json"), "utf8")).name === PACKAGE2;
1509
1681
  } catch {
1510
1682
  return false;
1511
1683
  }
1512
1684
  }
1513
1685
  function probeHermesInstallation(env = process.env) {
1514
- const location = (0, import_node_path10.join)(hermesHome(env), "plugins", "mmi");
1686
+ const location = (0, import_node_path11.join)(hermesHome(env), "plugins", "mmi");
1515
1687
  const version = yamlVersion(location);
1516
1688
  return { version, location, detail: version ? "installed Hermes plugin manifest" : "installed Hermes plugin manifest was unreadable" };
1517
1689
  }
@@ -1520,27 +1692,45 @@ function mmiOwned2(root) {
1520
1692
  }
1521
1693
  function discard2(path) {
1522
1694
  try {
1523
- (0, import_node_fs12.rmSync)(path, { recursive: true, force: true });
1695
+ (0, import_node_fs13.rmSync)(path, { recursive: true, force: true });
1524
1696
  } catch {
1525
1697
  }
1526
1698
  }
1527
1699
  function message2(error) {
1528
1700
  return String(error?.message ?? error).replace(/\s+/g, " ").slice(0, 200);
1529
1701
  }
1702
+ var HERMES_PROCESS_SEGMENT = /^hermes(-cli|-code)?(\.(exe|cmd|bat|ps1|js|mjs|cjs|py))?$/i;
1703
+ function hermesProcessToken(line, home) {
1704
+ const normalized = line.replace(/["']/g, " ");
1705
+ if (home && normalized.toLowerCase().includes(home.toLowerCase())) return home;
1706
+ for (const token of normalized.split(/\s+/)) {
1707
+ if (token && token.split(/[/\\]/).some((segment) => HERMES_PROCESS_SEGMENT.test(segment))) return token;
1708
+ }
1709
+ return null;
1710
+ }
1711
+ function hermesHostEvidence(env, home) {
1712
+ const result = process.platform === "win32" ? (0, import_node_child_process9.spawnSync)("powershell.exe", ["-NoProfile", "-Command", "Get-CimInstance Win32_Process | ForEach-Object { $_.Name + ' ' + $_.CommandLine }"], { encoding: "utf8", windowsHide: true, timeout: 15e3 }) : (0, import_node_child_process9.spawnSync)("ps", ["-eo", "args="], { encoding: "utf8", windowsHide: true, timeout: 15e3 });
1713
+ if (result.status !== 0 || !(result.stdout ?? "").trim()) return "the process list is unreadable (unsafe to write)";
1714
+ for (const line of result.stdout.split(/\r?\n/)) {
1715
+ const token = hermesProcessToken(line, home);
1716
+ if (token) return `a live process names hermes (${token})`;
1717
+ }
1718
+ return null;
1719
+ }
1530
1720
  function materializeHermesSkills(home) {
1531
- const pluginSkills = (0, import_node_path10.join)(home, "plugins", "mmi", "skills");
1532
- const skillsRoot = (0, import_node_path10.join)(home, "skills");
1533
- const targetRoot = (0, import_node_path10.join)(skillsRoot, "mmi");
1721
+ const pluginSkills = (0, import_node_path11.join)(home, "plugins", "mmi", "skills");
1722
+ const skillsRoot = (0, import_node_path11.join)(home, "skills");
1723
+ const targetRoot = (0, import_node_path11.join)(skillsRoot, "mmi");
1534
1724
  try {
1535
- if (!(0, import_node_fs12.existsSync)(pluginSkills)) return { ok: false, detail: `plugin skills tree missing at ${pluginSkills}` };
1536
- const names = (0, import_node_fs12.readdirSync)(pluginSkills, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_")).map((entry) => entry.name);
1725
+ if (!(0, import_node_fs13.existsSync)(pluginSkills)) return { ok: false, detail: `plugin skills tree missing at ${pluginSkills}` };
1726
+ const names = (0, import_node_fs13.readdirSync)(pluginSkills, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_")).map((entry) => entry.name);
1537
1727
  if (!names.length) return { ok: false, detail: `plugin skills tree is empty at ${pluginSkills}` };
1538
- (0, import_node_fs12.mkdirSync)(skillsRoot, { recursive: true });
1539
- const incoming = (0, import_node_path10.join)(skillsRoot, ".mmi-incoming");
1728
+ (0, import_node_fs13.mkdirSync)(skillsRoot, { recursive: true });
1729
+ const incoming = (0, import_node_path11.join)(skillsRoot, ".mmi-incoming");
1540
1730
  discard2(incoming);
1541
- (0, import_node_fs12.cpSync)(pluginSkills, incoming, { recursive: true });
1731
+ (0, import_node_fs13.cpSync)(pluginSkills, incoming, { recursive: true });
1542
1732
  discard2(targetRoot);
1543
- (0, import_node_fs12.renameSync)(incoming, targetRoot);
1733
+ (0, import_node_fs13.renameSync)(incoming, targetRoot);
1544
1734
  return { ok: true, detail: `provisioned ${names.length} skills to ${targetRoot}` };
1545
1735
  } catch (error) {
1546
1736
  return { ok: false, detail: message2(error) };
@@ -1549,8 +1739,8 @@ function materializeHermesSkills(home) {
1549
1739
  function hermesArm(options) {
1550
1740
  const env = options.env ?? process.env;
1551
1741
  const home = hermesHome(env);
1552
- const parent = (0, import_node_path10.join)(home, "plugins");
1553
- const live = (0, import_node_path10.join)(parent, "mmi");
1742
+ const parent = (0, import_node_path11.join)(home, "plugins");
1743
+ const live = (0, import_node_path11.join)(parent, "mmi");
1554
1744
  const installed = yamlVersion(live);
1555
1745
  const result = (from, to, verdict, detail) => ({
1556
1746
  surface: "hermes",
@@ -1559,8 +1749,8 @@ function hermesArm(options) {
1559
1749
  verdict,
1560
1750
  detail
1561
1751
  });
1562
- if (!(0, import_node_fs12.existsSync)(home)) return result(null, options.target, "skip", `absent (${home}) \u2014 skipped without writes`);
1563
- if ((0, import_node_fs12.existsSync)(live) && !mmiOwned2(live)) {
1752
+ if (!(0, import_node_fs13.existsSync)(home)) return result(null, options.target, "skip", `absent (${home}) \u2014 skipped without writes`);
1753
+ if ((0, import_node_fs13.existsSync)(live) && !mmiOwned2(live)) {
1564
1754
  return result(installed, options.target, "defer", `${live} carries no ${PACKAGE2} package identity \u2014 refusing to displace an unmanaged Hermes mmi plugin`);
1565
1755
  }
1566
1756
  if (installed && compareSemver(installed, options.target) > 0) return result(installed, installed, "skip", `installed ${installed} is above candidate ${options.target} (monotonic)`);
@@ -1571,21 +1761,21 @@ function hermesArm(options) {
1571
1761
  if (!skills2.ok) return result(installed, options.target, "fail", `plugin at target ${options.target}, but skills provisioning failed: ${skills2.detail}`);
1572
1762
  return result(installed, options.target, "ok", `already at target ${options.target}; ${skills2.detail}`);
1573
1763
  }
1574
- 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_path10.join)(home, "skills", "mmi")}`);
1764
+ 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_path11.join)(home, "skills", "mmi")}`);
1575
1765
  const staged = stagePackage({ surface: "hermes", packageName: PACKAGE2, target: options.target, witness: MANIFEST2, paths: options.paths, env });
1576
1766
  if ("defer" in staged) return result(installed, options.target, "defer", staged.defer);
1577
1767
  if (yamlVersion(staged.packageRoot) !== options.target || !treeHealthy3(staged.packageRoot)) {
1578
1768
  return result(installed, options.target, "defer", `staged Hermes manifest is ${yamlVersion(staged.packageRoot) ?? "unreadable"} or its tree is incomplete (expected ${options.target})`);
1579
1769
  }
1580
- const scratch = (0, import_node_path10.join)(home, ".mmi-updater");
1581
- const incomingRoot = (0, import_node_path10.join)(scratch, "incoming");
1582
- const incoming = (0, import_node_path10.join)(incomingRoot, options.target);
1583
- const quarantine = (0, import_node_path10.join)(scratch, "quarantine", `${installed ?? "unknown"}-${Date.now()}`);
1770
+ const scratch = (0, import_node_path11.join)(home, ".mmi-updater");
1771
+ const incomingRoot = (0, import_node_path11.join)(scratch, "incoming");
1772
+ const incoming = (0, import_node_path11.join)(incomingRoot, options.target);
1773
+ const quarantine = (0, import_node_path11.join)(scratch, "quarantine", `${installed ?? "unknown"}-${Date.now()}`);
1584
1774
  try {
1585
- (0, import_node_fs12.mkdirSync)(parent, { recursive: true });
1586
- (0, import_node_fs12.mkdirSync)((0, import_node_path10.join)(scratch, "quarantine"), { recursive: true });
1775
+ (0, import_node_fs13.mkdirSync)(parent, { recursive: true });
1776
+ (0, import_node_fs13.mkdirSync)((0, import_node_path11.join)(scratch, "quarantine"), { recursive: true });
1587
1777
  discard2(incomingRoot);
1588
- (0, import_node_fs12.cpSync)(staged.packageRoot, incoming, { recursive: true });
1778
+ (0, import_node_fs13.cpSync)(staged.packageRoot, incoming, { recursive: true });
1589
1779
  } catch (error) {
1590
1780
  return result(installed, options.target, "defer", `cannot place incoming Hermes generation: ${message2(error)}`);
1591
1781
  }
@@ -1593,24 +1783,32 @@ function hermesArm(options) {
1593
1783
  discard2(incomingRoot);
1594
1784
  return result(installed, options.target, "defer", "incoming Hermes generation did not reproduce the verified payload");
1595
1785
  }
1596
- const displaced = (0, import_node_fs12.existsSync)(live);
1786
+ const busy = hermesHostEvidence(env, home);
1787
+ if (busy) {
1788
+ discard2(incomingRoot);
1789
+ const pending = noteBusyDefer(options.paths, env, { surface: "hermes", target: options.target, reason: busy, images: ["hermes"], dirs: [home] });
1790
+ return result(installed, options.target, "defer", `Hermes may be running \u2014 the plugin tree is only swapped while the host is absent: ${busy} \u2014 ${pending}`);
1791
+ }
1792
+ const displaced = (0, import_node_fs13.existsSync)(live);
1597
1793
  try {
1598
- if (displaced) (0, import_node_fs12.renameSync)(live, quarantine);
1599
- (0, import_node_fs12.renameSync)(incoming, live);
1794
+ if (displaced) (0, import_node_fs13.renameSync)(live, quarantine);
1795
+ (0, import_node_fs13.renameSync)(incoming, live);
1600
1796
  } catch (error) {
1601
- if (displaced && !(0, import_node_fs12.existsSync)(live)) try {
1602
- (0, import_node_fs12.renameSync)(quarantine, live);
1797
+ if (displaced && !(0, import_node_fs13.existsSync)(live)) try {
1798
+ (0, import_node_fs13.renameSync)(quarantine, live);
1603
1799
  } catch {
1604
1800
  }
1605
1801
  discard2(incomingRoot);
1606
- return result(installed, options.target, "defer", `Hermes generation switch failed; previous generation retained: ${message2(error)}`);
1802
+ const reason = message2(error);
1803
+ const pending = lockShaped(reason) ? ` \u2014 ${noteBusyDefer(options.paths, env, { surface: "hermes", target: options.target, reason, images: ["hermes"], dirs: [home] })}` : "";
1804
+ return result(installed, options.target, "defer", `Hermes generation switch failed; previous generation retained: ${reason}${pending}`);
1607
1805
  }
1608
1806
  if (yamlVersion(live) !== options.target || !treeHealthy3(live)) {
1609
- const broken = (0, import_node_path10.join)(scratch, "rejected", `${options.target}-${Date.now()}`);
1807
+ const broken = (0, import_node_path11.join)(scratch, "rejected", `${options.target}-${Date.now()}`);
1610
1808
  try {
1611
- (0, import_node_fs12.mkdirSync)((0, import_node_path10.join)(scratch, "rejected"), { recursive: true });
1612
- (0, import_node_fs12.renameSync)(live, broken);
1613
- if (displaced) (0, import_node_fs12.renameSync)(quarantine, live);
1809
+ (0, import_node_fs13.mkdirSync)((0, import_node_path11.join)(scratch, "rejected"), { recursive: true });
1810
+ (0, import_node_fs13.renameSync)(live, broken);
1811
+ if (displaced) (0, import_node_fs13.renameSync)(quarantine, live);
1614
1812
  } catch (error) {
1615
1813
  return result(installed, yamlVersion(live) ?? "unknown", "fail", `post-switch Hermes verification failed and recovery failed: ${message2(error)}`);
1616
1814
  }
@@ -1623,9 +1821,9 @@ function hermesArm(options) {
1623
1821
  }
1624
1822
 
1625
1823
  // src/reap.ts
1626
- var import_node_child_process8 = require("node:child_process");
1627
- var import_node_fs13 = require("node:fs");
1628
- var import_node_path11 = require("node:path");
1824
+ var import_node_child_process10 = require("node:child_process");
1825
+ var import_node_fs14 = require("node:fs");
1826
+ var import_node_path12 = require("node:path");
1629
1827
  var KEEP_GENERATIONS2 = 2;
1630
1828
  var SCRATCH_AGE_MS = 24 * 60 * 6e4;
1631
1829
  var ORPHAN_MIN_AGE_MS = 10 * 6e4;
@@ -1633,7 +1831,7 @@ var SNAPSHOT_TIMEOUT_MS = 2e4;
1633
1831
  var MAX_UNWIND_ROUNDS = 4;
1634
1832
  function listDirs(path) {
1635
1833
  try {
1636
- return (0, import_node_fs13.readdirSync)(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
1834
+ return (0, import_node_fs14.readdirSync)(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
1637
1835
  } catch {
1638
1836
  return [];
1639
1837
  }
@@ -1641,7 +1839,7 @@ function listDirs(path) {
1641
1839
  function generationSets(root) {
1642
1840
  const children = listDirs(root);
1643
1841
  if (children.some((name) => parseSemver(name))) return [root];
1644
- return children.map((name) => (0, import_node_path11.join)(root, name)).filter((path) => listDirs(path).some((name) => parseSemver(name)));
1842
+ return children.map((name) => (0, import_node_path12.join)(root, name)).filter((path) => listDirs(path).some((name) => parseSemver(name)));
1645
1843
  }
1646
1844
  function pruneGenerations(root, category, dryRun) {
1647
1845
  let reaped = 0;
@@ -1654,7 +1852,7 @@ function pruneGenerations(root, category, dryRun) {
1654
1852
  continue;
1655
1853
  }
1656
1854
  try {
1657
- (0, import_node_fs13.rmSync)((0, import_node_path11.join)(set, version), { recursive: true, force: true });
1855
+ (0, import_node_fs14.rmSync)((0, import_node_path12.join)(set, version), { recursive: true, force: true });
1658
1856
  reaped += 1;
1659
1857
  } catch (error) {
1660
1858
  failures.push(`${version}: ${error?.code ?? error?.message ?? error}`);
@@ -1668,8 +1866,8 @@ function pruneGenerations(root, category, dryRun) {
1668
1866
  }
1669
1867
  var LEASE_STEAL_SCRATCH = /^lease\.lock\.stale-\d+$/;
1670
1868
  function atomicWriteScratch(path) {
1671
- const literal = (0, import_node_path11.basename)(path).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1672
- return { dir: (0, import_node_path11.dirname)(path), pattern: new RegExp(`^${literal}\\.tmp-\\d+$`) };
1869
+ const literal = (0, import_node_path12.basename)(path).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1870
+ return { dir: (0, import_node_path12.dirname)(path), pattern: new RegExp(`^${literal}\\.tmp-\\d+$`) };
1673
1871
  }
1674
1872
  function reapScratch(context, dryRun) {
1675
1873
  const category = "scratch";
@@ -1687,7 +1885,7 @@ function reapScratch(context, dryRun) {
1687
1885
  for (const [dir, patterns] of scanned) {
1688
1886
  let entries;
1689
1887
  try {
1690
- entries = (0, import_node_fs13.readdirSync)(dir, { withFileTypes: true });
1888
+ entries = (0, import_node_fs14.readdirSync)(dir, { withFileTypes: true });
1691
1889
  } catch {
1692
1890
  unreadable.push(dir);
1693
1891
  continue;
@@ -1695,10 +1893,10 @@ function reapScratch(context, dryRun) {
1695
1893
  for (const entry of entries) {
1696
1894
  if (!patterns.some((pattern) => pattern.test(entry.name))) continue;
1697
1895
  if (!entry.isFile()) continue;
1698
- const path = (0, import_node_path11.join)(dir, entry.name);
1896
+ const path = (0, import_node_path12.join)(dir, entry.name);
1699
1897
  let ageMs;
1700
1898
  try {
1701
- ageMs = now - (0, import_node_fs13.statSync)(path).mtimeMs;
1899
+ ageMs = now - (0, import_node_fs14.statSync)(path).mtimeMs;
1702
1900
  } catch {
1703
1901
  continue;
1704
1902
  }
@@ -1708,7 +1906,7 @@ function reapScratch(context, dryRun) {
1708
1906
  continue;
1709
1907
  }
1710
1908
  try {
1711
- (0, import_node_fs13.rmSync)(path, { force: true });
1909
+ (0, import_node_fs14.rmSync)(path, { force: true });
1712
1910
  reaped += 1;
1713
1911
  } catch (error) {
1714
1912
  failures.push(`${entry.name}: ${error?.code ?? error?.message ?? error}`);
@@ -1824,7 +2022,7 @@ function stillTheProvenProcess(rows, kill) {
1824
2022
  }
1825
2023
  var SNAPSHOT_COMMAND = "Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine,@{n='Started';e={if ($_.CreationDate) { $_.CreationDate.ToUniversalTime().ToString('o') }}} | ConvertTo-Json -Compress";
1826
2024
  function processSnapshot() {
1827
- const result = (0, import_node_child_process8.spawnSync)("powershell.exe", ["-NoProfile", "-Command", SNAPSHOT_COMMAND], { encoding: "utf8", windowsHide: true, timeout: SNAPSHOT_TIMEOUT_MS });
2025
+ const result = (0, import_node_child_process10.spawnSync)("powershell.exe", ["-NoProfile", "-Command", SNAPSHOT_COMMAND], { encoding: "utf8", windowsHide: true, timeout: SNAPSHOT_TIMEOUT_MS });
1828
2026
  if (result.status !== 0 || !(result.stdout ?? "").trim()) return null;
1829
2027
  try {
1830
2028
  const parsed = JSON.parse(result.stdout);
@@ -1948,12 +2146,20 @@ function reconcile(options) {
1948
2146
  if (arm.verdict === "fail") arm.repeatFailure = journalSurfaceRepeatedFail(paths.journalPath, arm.surface, run);
1949
2147
  journal({ kind: "arm", surface: arm.surface, from: arm.from, to: arm.to, verdict: arm.verdict, detail: arm.detail, compat: arm.compat });
1950
2148
  options.onArm?.(arm);
2149
+ if (arm.verdict === "ok" && !options.dryRun) clearPending(paths, arm.surface);
1951
2150
  if (arm.verdict === "defer") summary.exit = Math.max(summary.exit, 2);
1952
2151
  if (arm.verdict === "fail") summary.exit = Math.max(summary.exit, 3);
1953
2152
  };
1954
2153
  try {
1955
2154
  say("acquiring single-writer lease");
1956
2155
  return withLease(paths.leasePath, () => {
2156
+ if (!options.dryRun) {
2157
+ const pendingBefore = readPending(paths);
2158
+ if (pendingBefore.length) {
2159
+ const resumed = resumePendingWatcher(paths, env);
2160
+ journal({ kind: "pending", verdict: "ok", count: pendingBefore.length, detail: resumed.scheduled ? `${pendingBefore.length} pending appl${pendingBefore.length === 1 ? "y" : "ies"}; post-exit watcher pid ${resumed.pid} waiting` : `${pendingBefore.length} pending appl${pendingBefore.length === 1 ? "y" : "ies"}; ${resumed.reason}` });
2161
+ }
2162
+ }
1957
2163
  summary.reap = reap({ paths, dryRun: options.dryRun, env });
1958
2164
  for (const row of summary.reap) journal({ kind: "reap", surface: row.category, verdict: row.verdict, count: row.reaped, detail: row.detail });
1959
2165
  for (const row of summary.reap.filter((entry) => entry.verdict !== "skip")) say(`reap ${row.category}: ${row.verdict}${row.detail ? " \u2014 " + row.detail : ""}`);
@@ -2047,10 +2253,10 @@ function reconcile(options) {
2047
2253
  }
2048
2254
 
2049
2255
  // src/scheduler.ts
2050
- var import_node_child_process9 = require("node:child_process");
2051
- var import_node_fs14 = require("node:fs");
2256
+ var import_node_child_process11 = require("node:child_process");
2257
+ var import_node_fs15 = require("node:fs");
2052
2258
  var import_node_os5 = require("node:os");
2053
- var import_node_path12 = require("node:path");
2259
+ var import_node_path13 = require("node:path");
2054
2260
  var TASK_NAME = "MMI Fleet Updater";
2055
2261
  var LAUNCHER_FILE = "reconcile-hidden.vbs";
2056
2262
  var RUN_BOUND = "PT15M";
@@ -2066,15 +2272,15 @@ var SYSTEMD_UNIT = "mmi-hub-updater";
2066
2272
  var SYSTEMD_SERVICE_NAME = `${SYSTEMD_UNIT}.service`;
2067
2273
  var SYSTEMD_TIMER_NAME = `${SYSTEMD_UNIT}.timer`;
2068
2274
  function schtasks(args) {
2069
- const result = (0, import_node_child_process9.spawnSync)("schtasks.exe", args, { encoding: "utf8", windowsHide: true });
2275
+ const result = (0, import_node_child_process11.spawnSync)("schtasks.exe", args, { encoding: "utf8", windowsHide: true });
2070
2276
  return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
2071
2277
  }
2072
2278
  function launchctl(args) {
2073
- const result = (0, import_node_child_process9.spawnSync)("launchctl", args, { encoding: "utf8", windowsHide: true });
2279
+ const result = (0, import_node_child_process11.spawnSync)("launchctl", args, { encoding: "utf8", windowsHide: true });
2074
2280
  return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
2075
2281
  }
2076
2282
  function systemctlUser(args) {
2077
- const result = (0, import_node_child_process9.spawnSync)("systemctl", ["--user", ...args], { encoding: "utf8", windowsHide: true });
2283
+ const result = (0, import_node_child_process11.spawnSync)("systemctl", ["--user", ...args], { encoding: "utf8", windowsHide: true });
2078
2284
  return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
2079
2285
  }
2080
2286
  function uid() {
@@ -2099,7 +2305,7 @@ WScript.Quit code
2099
2305
  `;
2100
2306
  }
2101
2307
  function loginPath(fallback = process.env.PATH ?? "") {
2102
- const probe = (0, import_node_child_process9.spawnSync)("/bin/sh", ["-lc", 'printf %s "$PATH"'], { encoding: "utf8", windowsHide: true, timeout: 1e4 });
2308
+ const probe = (0, import_node_child_process11.spawnSync)("/bin/sh", ["-lc", 'printf %s "$PATH"'], { encoding: "utf8", windowsHide: true, timeout: 1e4 });
2103
2309
  const captured = (probe.status === 0 ? probe.stdout ?? "" : "").trim();
2104
2310
  const source = captured || fallback;
2105
2311
  const durable = source.split(":").filter((entry) => entry && !/^\/(?:private\/)?(?:tmp|var\/folders)\//.test(entry));
@@ -2208,7 +2414,7 @@ WantedBy=timers.target
2208
2414
  }
2209
2415
  function readFileOrNull(path) {
2210
2416
  try {
2211
- return (0, import_node_fs14.readFileSync)(path, "utf8");
2417
+ return (0, import_node_fs15.readFileSync)(path, "utf8");
2212
2418
  } catch {
2213
2419
  return null;
2214
2420
  }
@@ -2220,7 +2426,7 @@ function elevatedRelaunch(mode, env) {
2220
2426
  if (env.MMI_UPDATER_ELEVATED_RETRY === "1") return false;
2221
2427
  const self = process.argv[1];
2222
2428
  const command = self ? `"${process.execPath}" "${self}" autoupdate ${mode}` : `mmi-hub autoupdate ${mode}`;
2223
- const relaunch = (0, import_node_child_process9.spawnSync)(
2429
+ const relaunch = (0, import_node_child_process11.spawnSync)(
2224
2430
  "powershell.exe",
2225
2431
  ["-NoProfile", "-Command", `Start-Process -Verb RunAs -Wait -WindowStyle Hidden -FilePath 'cmd.exe' -ArgumentList '/c',${psQuote(`set MMI_UPDATER_ELEVATED_RETRY=1&& ${command}`)}`],
2226
2432
  { encoding: "utf8", windowsHide: true }
@@ -2231,7 +2437,7 @@ var NEVER_RUN_RESULT = 267011;
2231
2437
  var NEVER_RUN_YEAR = 1999;
2232
2438
  var EMPTY_RUN_INFO = { lastRunTime: null, lastTaskResult: null, nextRunTime: null };
2233
2439
  function taskRunInfo() {
2234
- const result = (0, import_node_child_process9.spawnSync)(
2440
+ const result = (0, import_node_child_process11.spawnSync)(
2235
2441
  "powershell.exe",
2236
2442
  ["-NoProfile", "-Command", `$stamp = { param($t) if ($t) { $t.ToString('yyyy-MM-dd HH:mm:ss') } }; Get-ScheduledTaskInfo -TaskName '${TASK_NAME}' | Select-Object @{n='LastRunTime';e={& $stamp $_.LastRunTime}},LastTaskResult,@{n='NextRunTime';e={& $stamp $_.NextRunTime}} | ConvertTo-Json -Compress`],
2237
2443
  { encoding: "utf8", windowsHide: true, timeout: 3e4 }
@@ -2257,7 +2463,7 @@ function windowsSchedulerStatus(env) {
2257
2463
  return { ok: false, supported: true, enabled: false, ...EMPTY_RUN_INFO, detail: `"${TASK_NAME}" is not registered \u2014 run \`mmi-hub autoupdate on\`` };
2258
2464
  }
2259
2465
  const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
2260
- const launcherPath = (0, import_node_path12.join)(statePaths(env).root, LAUNCHER_FILE);
2466
+ const launcherPath = (0, import_node_path13.join)(statePaths(env).root, LAUNCHER_FILE);
2261
2467
  const info = taskRunInfo();
2262
2468
  const runInfo = info ?? EMPTY_RUN_INFO;
2263
2469
  const fail = (detail) => ({ ok: false, supported: true, enabled: true, ...runInfo, detail });
@@ -2282,13 +2488,13 @@ function windowsSchedulerStatus(env) {
2282
2488
  return { ok: true, supported: true, enabled: true, ...runInfo, detail: `"${TASK_NAME}" registered ${CADENCE}; ${points}; ${ran}; next ${runInfo.nextRunTime ?? "unscheduled"}` };
2283
2489
  }
2284
2490
  function darwinSchedulerStatus(env) {
2285
- const plistPath = (0, import_node_path12.join)((0, import_node_os5.homedir)(), "Library", "LaunchAgents", LAUNCHD_PLIST_NAME);
2491
+ const plistPath = (0, import_node_path13.join)((0, import_node_os5.homedir)(), "Library", "LaunchAgents", LAUNCHD_PLIST_NAME);
2286
2492
  const plist = readFileOrNull(plistPath);
2287
2493
  if (!plist) {
2288
2494
  return { ok: false, supported: true, enabled: false, ...EMPTY_RUN_INFO, detail: `"${LAUNCHD_LABEL}" is not registered \u2014 run \`mmi-hub autoupdate on\`` };
2289
2495
  }
2290
2496
  const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
2291
- const launcherPath = (0, import_node_path12.join)(statePaths(env).root, POSIX_LAUNCHER_FILE);
2497
+ const launcherPath = (0, import_node_path13.join)(statePaths(env).root, POSIX_LAUNCHER_FILE);
2292
2498
  const fail = (detail) => ({ ok: false, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail });
2293
2499
  if (!plist.includes(launcherPath)) {
2294
2500
  return fail(`"${LAUNCHD_LABEL}" does not run the reconcile launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
@@ -2308,15 +2514,15 @@ function darwinSchedulerStatus(env) {
2308
2514
  return { ok: true, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail: `"${LAUNCHD_LABEL}" registered ${POSIX_CADENCE}; ${points}` };
2309
2515
  }
2310
2516
  function linuxSchedulerStatus(env) {
2311
- const unitDir = (0, import_node_path12.join)((0, import_node_os5.homedir)(), ".config", "systemd", "user");
2312
- const timerPath = (0, import_node_path12.join)(unitDir, SYSTEMD_TIMER_NAME);
2517
+ const unitDir = (0, import_node_path13.join)((0, import_node_os5.homedir)(), ".config", "systemd", "user");
2518
+ const timerPath = (0, import_node_path13.join)(unitDir, SYSTEMD_TIMER_NAME);
2313
2519
  const timer = readFileOrNull(timerPath);
2314
- const service = readFileOrNull((0, import_node_path12.join)(unitDir, SYSTEMD_SERVICE_NAME));
2520
+ const service = readFileOrNull((0, import_node_path13.join)(unitDir, SYSTEMD_SERVICE_NAME));
2315
2521
  if (!timer || !service) {
2316
2522
  return { ok: false, supported: true, enabled: false, ...EMPTY_RUN_INFO, detail: `"${SYSTEMD_TIMER_NAME}" is not registered \u2014 run \`mmi-hub autoupdate on\`` };
2317
2523
  }
2318
2524
  const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
2319
- const launcherPath = (0, import_node_path12.join)(statePaths(env).root, POSIX_LAUNCHER_FILE);
2525
+ const launcherPath = (0, import_node_path13.join)(statePaths(env).root, POSIX_LAUNCHER_FILE);
2320
2526
  const fail = (detail) => ({ ok: false, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail });
2321
2527
  if (!service.includes(launcherPath)) {
2322
2528
  return fail(`"${SYSTEMD_SERVICE_NAME}" does not run the reconcile launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
@@ -2355,10 +2561,10 @@ function installWindowsTask(env) {
2355
2561
  const paths = ensureState(env);
2356
2562
  const userId = `${env.USERDOMAIN || env.COMPUTERNAME || ""}\\${env.USERNAME || ""}`.replace(/^\\/, "");
2357
2563
  if (!env.USERNAME) return { ok: false, detail: "cannot resolve the registering user (USERNAME unset) \u2014 the task principal must be explicit" };
2358
- const launcherPath = (0, import_node_path12.join)(paths.root, LAUNCHER_FILE);
2359
- (0, import_node_fs14.writeFileSync)(launcherPath, launcherVbs(process.execPath, hubDist), "utf8");
2360
- const xmlPath = (0, import_node_path12.join)(paths.root, "task.xml");
2361
- (0, import_node_fs14.writeFileSync)(xmlPath, "\uFEFF" + taskXml(launcherPath, paths.root, userId, localStartBoundary(/* @__PURE__ */ new Date())), "utf16le");
2564
+ const launcherPath = (0, import_node_path13.join)(paths.root, LAUNCHER_FILE);
2565
+ (0, import_node_fs15.writeFileSync)(launcherPath, launcherVbs(process.execPath, hubDist), "utf8");
2566
+ const xmlPath = (0, import_node_path13.join)(paths.root, "task.xml");
2567
+ (0, import_node_fs15.writeFileSync)(xmlPath, "\uFEFF" + taskXml(launcherPath, paths.root, userId, localStartBoundary(/* @__PURE__ */ new Date())), "utf16le");
2362
2568
  const create = schtasks(["/create", "/tn", TASK_NAME, "/xml", xmlPath, "/f"]);
2363
2569
  if (create.status !== 0) {
2364
2570
  const denied = /access is denied/i.test(create.stderr + create.stdout);
@@ -2374,11 +2580,11 @@ function refreshPosixLauncher(env) {
2374
2580
  const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
2375
2581
  if (!hubDist) return null;
2376
2582
  const paths = ensureState(env);
2377
- const launcherPath = (0, import_node_path12.join)(paths.root, POSIX_LAUNCHER_FILE);
2583
+ const launcherPath = (0, import_node_path13.join)(paths.root, POSIX_LAUNCHER_FILE);
2378
2584
  const desired = launcherSh(process.execPath, hubDist);
2379
2585
  if (readFileOrNull(launcherPath) === desired) return null;
2380
- (0, import_node_fs14.writeFileSync)(launcherPath, desired, "utf8");
2381
- (0, import_node_fs14.chmodSync)(launcherPath, 493);
2586
+ (0, import_node_fs15.writeFileSync)(launcherPath, desired, "utf8");
2587
+ (0, import_node_fs15.chmodSync)(launcherPath, 493);
2382
2588
  return launcherPath;
2383
2589
  }
2384
2590
  function installLaunchd(env) {
@@ -2390,13 +2596,13 @@ function installLaunchd(env) {
2390
2596
  const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
2391
2597
  if (!hubDist) return { ok: false, detail: "cannot resolve the global @mutmutco/hub dist \u2014 run `npm install -g @mutmutco/hub` first" };
2392
2598
  const paths = ensureState(env);
2393
- const launcherPath = (0, import_node_path12.join)(paths.root, POSIX_LAUNCHER_FILE);
2394
- (0, import_node_fs14.writeFileSync)(launcherPath, launcherSh(process.execPath, hubDist), "utf8");
2395
- (0, import_node_fs14.chmodSync)(launcherPath, 493);
2396
- const agentsDir = (0, import_node_path12.join)((0, import_node_os5.homedir)(), "Library", "LaunchAgents");
2397
- (0, import_node_fs14.mkdirSync)(agentsDir, { recursive: true });
2398
- const plistPath = (0, import_node_path12.join)(agentsDir, LAUNCHD_PLIST_NAME);
2399
- (0, import_node_fs14.writeFileSync)(plistPath, launchdPlist(launcherPath, paths.root), "utf8");
2599
+ const launcherPath = (0, import_node_path13.join)(paths.root, POSIX_LAUNCHER_FILE);
2600
+ (0, import_node_fs15.writeFileSync)(launcherPath, launcherSh(process.execPath, hubDist), "utf8");
2601
+ (0, import_node_fs15.chmodSync)(launcherPath, 493);
2602
+ const agentsDir = (0, import_node_path13.join)((0, import_node_os5.homedir)(), "Library", "LaunchAgents");
2603
+ (0, import_node_fs15.mkdirSync)(agentsDir, { recursive: true });
2604
+ const plistPath = (0, import_node_path13.join)(agentsDir, LAUNCHD_PLIST_NAME);
2605
+ (0, import_node_fs15.writeFileSync)(plistPath, launchdPlist(launcherPath, paths.root), "utf8");
2400
2606
  launchctl(["bootout", `gui/${uid()}/${LAUNCHD_LABEL}`]);
2401
2607
  const load = launchctl(["bootstrap", `gui/${uid()}`, plistPath]);
2402
2608
  if (load.status !== 0) {
@@ -2414,13 +2620,13 @@ function installSystemdTimer(env) {
2414
2620
  const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
2415
2621
  if (!hubDist) return { ok: false, detail: "cannot resolve the global @mutmutco/hub dist \u2014 run `npm install -g @mutmutco/hub` first" };
2416
2622
  const paths = ensureState(env);
2417
- const launcherPath = (0, import_node_path12.join)(paths.root, POSIX_LAUNCHER_FILE);
2418
- (0, import_node_fs14.writeFileSync)(launcherPath, launcherSh(process.execPath, hubDist), "utf8");
2419
- (0, import_node_fs14.chmodSync)(launcherPath, 493);
2420
- const unitDir = (0, import_node_path12.join)((0, import_node_os5.homedir)(), ".config", "systemd", "user");
2421
- (0, import_node_fs14.mkdirSync)(unitDir, { recursive: true });
2422
- (0, import_node_fs14.writeFileSync)((0, import_node_path12.join)(unitDir, SYSTEMD_SERVICE_NAME), systemdServiceUnit(launcherPath, paths.root), "utf8");
2423
- (0, import_node_fs14.writeFileSync)((0, import_node_path12.join)(unitDir, SYSTEMD_TIMER_NAME), systemdTimerUnit(), "utf8");
2623
+ const launcherPath = (0, import_node_path13.join)(paths.root, POSIX_LAUNCHER_FILE);
2624
+ (0, import_node_fs15.writeFileSync)(launcherPath, launcherSh(process.execPath, hubDist), "utf8");
2625
+ (0, import_node_fs15.chmodSync)(launcherPath, 493);
2626
+ const unitDir = (0, import_node_path13.join)((0, import_node_os5.homedir)(), ".config", "systemd", "user");
2627
+ (0, import_node_fs15.mkdirSync)(unitDir, { recursive: true });
2628
+ (0, import_node_fs15.writeFileSync)((0, import_node_path13.join)(unitDir, SYSTEMD_SERVICE_NAME), systemdServiceUnit(launcherPath, paths.root), "utf8");
2629
+ (0, import_node_fs15.writeFileSync)((0, import_node_path13.join)(unitDir, SYSTEMD_TIMER_NAME), systemdTimerUnit(), "utf8");
2424
2630
  const reload = systemctlUser(["daemon-reload"]);
2425
2631
  if (reload.status !== 0) {
2426
2632
  return { ok: false, detail: `systemctl --user daemon-reload failed: ${(reload.stderr || reload.stdout).trim() || "unknown error \u2014 is a user systemd instance running?"}` };
@@ -2458,28 +2664,28 @@ function uninstallWindowsTask(env) {
2458
2664
  return { ok: true, detail: `automatic updates off \u2014 removed "${TASK_NAME}"; installed tooling was kept` };
2459
2665
  }
2460
2666
  function uninstallLaunchd(env) {
2461
- const plistPath = (0, import_node_path12.join)((0, import_node_os5.homedir)(), "Library", "LaunchAgents", LAUNCHD_PLIST_NAME);
2667
+ const plistPath = (0, import_node_path13.join)((0, import_node_os5.homedir)(), "Library", "LaunchAgents", LAUNCHD_PLIST_NAME);
2462
2668
  const boot = launchctl(["bootout", `gui/${uid()}/${LAUNCHD_LABEL}`]);
2463
- if (boot.status !== 0 && (0, import_node_fs14.existsSync)(plistPath)) {
2669
+ if (boot.status !== 0 && (0, import_node_fs15.existsSync)(plistPath)) {
2464
2670
  return { ok: false, detail: `launchctl bootout failed: ${(boot.stderr || boot.stdout).trim() || "unknown error"}` };
2465
2671
  }
2466
2672
  try {
2467
- (0, import_node_fs14.rmSync)(plistPath, { force: true });
2673
+ (0, import_node_fs15.rmSync)(plistPath, { force: true });
2468
2674
  } catch {
2469
2675
  }
2470
2676
  dropLauncher(env, POSIX_LAUNCHER_FILE);
2471
2677
  return { ok: true, detail: `automatic updates off \u2014 removed "${LAUNCHD_LABEL}"; installed tooling was kept` };
2472
2678
  }
2473
2679
  function uninstallSystemdTimer(env) {
2474
- const unitDir = (0, import_node_path12.join)((0, import_node_os5.homedir)(), ".config", "systemd", "user");
2475
- const timerPath = (0, import_node_path12.join)(unitDir, SYSTEMD_TIMER_NAME);
2680
+ const unitDir = (0, import_node_path13.join)((0, import_node_os5.homedir)(), ".config", "systemd", "user");
2681
+ const timerPath = (0, import_node_path13.join)(unitDir, SYSTEMD_TIMER_NAME);
2476
2682
  const disable = systemctlUser(["disable", "--now", SYSTEMD_TIMER_NAME]);
2477
- if (disable.status !== 0 && (0, import_node_fs14.existsSync)(timerPath)) {
2683
+ if (disable.status !== 0 && (0, import_node_fs15.existsSync)(timerPath)) {
2478
2684
  return { ok: false, detail: `systemctl --user disable --now ${SYSTEMD_TIMER_NAME} failed: ${(disable.stderr || disable.stdout).trim() || "unknown error"}` };
2479
2685
  }
2480
2686
  try {
2481
- (0, import_node_fs14.rmSync)(timerPath, { force: true });
2482
- (0, import_node_fs14.rmSync)((0, import_node_path12.join)(unitDir, SYSTEMD_SERVICE_NAME), { force: true });
2687
+ (0, import_node_fs15.rmSync)(timerPath, { force: true });
2688
+ (0, import_node_fs15.rmSync)((0, import_node_path13.join)(unitDir, SYSTEMD_SERVICE_NAME), { force: true });
2483
2689
  } catch {
2484
2690
  }
2485
2691
  systemctlUser(["daemon-reload"]);
@@ -2494,16 +2700,16 @@ function uninstallTask(env = process.env) {
2494
2700
  }
2495
2701
  function dropLauncher(env, file) {
2496
2702
  try {
2497
- (0, import_node_fs14.rmSync)((0, import_node_path12.join)(statePaths(env).root, file), { force: true });
2703
+ (0, import_node_fs15.rmSync)((0, import_node_path13.join)(statePaths(env).root, file), { force: true });
2498
2704
  } catch {
2499
2705
  }
2500
2706
  }
2501
2707
 
2502
2708
  // src/status.ts
2503
- var import_node_fs15 = require("node:fs");
2709
+ var import_node_fs16 = require("node:fs");
2504
2710
  function readJournal(path) {
2505
2711
  try {
2506
- const events = (0, import_node_fs15.readFileSync)(path, "utf8").split("\n").filter((line) => line.trim()).map((line) => {
2712
+ const events = (0, import_node_fs16.readFileSync)(path, "utf8").split("\n").filter((line) => line.trim()).map((line) => {
2507
2713
  try {
2508
2714
  return JSON.parse(line);
2509
2715
  } catch {
@@ -2622,6 +2828,7 @@ var YELLOW = "\x1B[33m";
2622
2828
  var RED = "\x1B[31m";
2623
2829
  var RESET = "\x1B[0m";
2624
2830
  var RETRY_HINTS = [
2831
+ [/applies when the host exits|applies on a quiet tick/, "installs when the app closes"],
2625
2832
  [/in flight|changed while/, "rerun update \u2014 host race"],
2626
2833
  [/is running|live process|inside a .* hook/, "close app, rerun update"],
2627
2834
  [/staging install failed|etarget|e404|network|fetch failed/, "check network/login, rerun"],
@@ -2880,7 +3087,7 @@ function startSpinner(enabled) {
2880
3087
  // src/index.ts
2881
3088
  function ownVersion() {
2882
3089
  try {
2883
- return JSON.parse((0, import_node_fs16.readFileSync)((0, import_node_path13.join)(__dirname, "..", "package.json"), "utf8")).version;
3090
+ return JSON.parse((0, import_node_fs17.readFileSync)((0, import_node_path14.join)(__dirname, "..", "package.json"), "utf8")).version;
2884
3091
  } catch {
2885
3092
  return "0.0.0";
2886
3093
  }
@@ -3006,6 +3213,8 @@ if (typeof require !== "undefined" && require.main === module) {
3006
3213
  }
3007
3214
  // Annotate the CommonJS export names for ESM import in node:
3008
3215
  0 && (module.exports = {
3216
+ buildWatcherCommand,
3217
+ clearPending,
3009
3218
  enumerateSurfaces,
3010
3219
  failConsequence,
3011
3220
  formatReconcileReport,
@@ -3016,18 +3225,26 @@ if (typeof require !== "undefined" && require.main === module) {
3016
3225
  launcherSh,
3017
3226
  launcherVbs,
3018
3227
  localStartBoundary,
3228
+ lockShaped,
3019
3229
  main,
3230
+ noteBusyDefer,
3020
3231
  ownVersion,
3232
+ pendingPath,
3233
+ pidAlive,
3021
3234
  probeHermesInstallation,
3022
3235
  progressiveRow,
3236
+ readPending,
3023
3237
  reconcileCursorUserHooks,
3024
3238
  reportFooterLines,
3025
3239
  retryHint,
3240
+ schedulePendingWatcher,
3026
3241
  schedulerStatus,
3027
3242
  startSpinner,
3028
3243
  systemdServiceUnit,
3029
3244
  systemdTimerUnit,
3030
3245
  taskXml,
3031
3246
  updateHeader,
3032
- wrapWords
3247
+ watcherLockPath,
3248
+ wrapWords,
3249
+ writePending
3033
3250
  });