mcproc 0.14.1 → 0.14.3

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.
package/dist/cli.js CHANGED
@@ -2631,7 +2631,7 @@ async function unpack(target, opts, deps2) {
2631
2631
  }
2632
2632
 
2633
2633
  // src/core/leases/view.ts
2634
- import fs8 from "fs";
2634
+ import fs9 from "fs";
2635
2635
 
2636
2636
  // src/core/memory.ts
2637
2637
  var KB = 1024;
@@ -2688,8 +2688,8 @@ function stateDir(env = process.env) {
2688
2688
  }
2689
2689
 
2690
2690
  // src/core/leases/ensure.ts
2691
- import fs7 from "fs";
2692
- import path11 from "path";
2691
+ import fs8 from "fs";
2692
+ import path12 from "path";
2693
2693
 
2694
2694
  // src/core/ecosystem.ts
2695
2695
  import path9 from "path";
@@ -2731,11 +2731,95 @@ function buildEcosystem(m, host) {
2731
2731
  return { apps };
2732
2732
  }
2733
2733
 
2734
- // src/core/leases/store.ts
2734
+ // src/core/slots.ts
2735
2735
  import crypto from "crypto";
2736
2736
  import fs6 from "fs";
2737
- import os5 from "os";
2738
2737
  import path10 from "path";
2738
+ function registryKey(baseProject, repo) {
2739
+ return `${baseProject}-${crypto.createHash("sha256").update(realOf(repo)).digest("hex").slice(0, 8)}`;
2740
+ }
2741
+ function registryPath(stateDir2, baseProject, repo) {
2742
+ return path10.join(stateDir2, "slots", `${registryKey(baseProject, repo)}.json`);
2743
+ }
2744
+ function emptyRegistry(repo) {
2745
+ return { repo, slots: {} };
2746
+ }
2747
+ function readRegistry(file) {
2748
+ if (!fs6.existsSync(file)) return null;
2749
+ return JSON.parse(fs6.readFileSync(file, "utf8"));
2750
+ }
2751
+ function writeRegistry(file, r) {
2752
+ fs6.mkdirSync(path10.dirname(file), { recursive: true });
2753
+ const tmp = `${file}.${process.pid}.tmp`;
2754
+ fs6.writeFileSync(tmp, JSON.stringify(r, null, 2));
2755
+ fs6.renameSync(tmp, file);
2756
+ }
2757
+ function withMain(r, entry) {
2758
+ return { ...r, slots: { ...r.slots, "0": entry } };
2759
+ }
2760
+ function claimSlot(r, opts) {
2761
+ if (opts.want !== void 0) {
2762
+ if (opts.want < 1 || opts.want > opts.max)
2763
+ throw new McprocError(
2764
+ "BAD_ARGUMENT",
2765
+ `slot ${opts.want} is outside 1..${opts.max} (slot 0 is the main checkout)`
2766
+ );
2767
+ if (Object.hasOwn(r.slots, String(opts.want)))
2768
+ throw new McprocError(
2769
+ "SLOT_TAKEN",
2770
+ `slot ${opts.want} is taken by ${r.slots[String(opts.want)].path}`,
2771
+ { hint: "Pick another slot, or remove that worktree first." }
2772
+ );
2773
+ return {
2774
+ registry: { ...r, slots: { ...r.slots, [opts.want]: opts.entry } },
2775
+ slot: opts.want
2776
+ };
2777
+ }
2778
+ for (let s = 1; s <= opts.max; s++)
2779
+ if (!Object.hasOwn(r.slots, String(s)))
2780
+ return {
2781
+ registry: { ...r, slots: { ...r.slots, [s]: opts.entry } },
2782
+ slot: s
2783
+ };
2784
+ throw new McprocError(
2785
+ "NO_FREE_SLOT",
2786
+ `all ${opts.max} worktree slots of ${r.repo} are taken`,
2787
+ {
2788
+ hint: "Remove a worktree (worktree remove), or raise worktree.slots in the manifest."
2789
+ }
2790
+ );
2791
+ }
2792
+ function releaseSlot(r, slot) {
2793
+ const slots = { ...r.slots };
2794
+ delete slots[String(slot)];
2795
+ return { ...r, slots };
2796
+ }
2797
+ function realOf(p) {
2798
+ try {
2799
+ return fs6.realpathSync(p);
2800
+ } catch {
2801
+ return path10.resolve(p);
2802
+ }
2803
+ }
2804
+ function reclaimGone(r, exists) {
2805
+ const freed = [];
2806
+ let out = r;
2807
+ for (const [k, e] of Object.entries(r.slots)) {
2808
+ if (k === "0" || exists(realOf(e.path))) continue;
2809
+ out = releaseSlot(out, Number(k));
2810
+ freed.push(Number(k));
2811
+ }
2812
+ return { registry: out, freed: freed.sort((a, b) => a - b) };
2813
+ }
2814
+ function siblingsOf(r, root) {
2815
+ return Object.entries(r.slots).filter(([, e]) => realOf(e.path) !== realOf(root)).map(([k, entry]) => ({ slot: Number(k), entry })).sort((a, b) => a.slot - b.slot);
2816
+ }
2817
+
2818
+ // src/core/leases/store.ts
2819
+ import crypto2 from "crypto";
2820
+ import fs7 from "fs";
2821
+ import os5 from "os";
2822
+ import path11 from "path";
2739
2823
  var ALLOWED = {
2740
2824
  pending: ["starting", "running", "cancelled"],
2741
2825
  starting: ["running", "failed", "cancelled"],
@@ -2776,7 +2860,7 @@ function parseHeld(raw) {
2776
2860
  }
2777
2861
  function readHeld(file) {
2778
2862
  try {
2779
- return parseHeld(fs6.readFileSync(file, "utf8"));
2863
+ return parseHeld(fs7.readFileSync(file, "utf8"));
2780
2864
  } catch {
2781
2865
  return null;
2782
2866
  }
@@ -2791,13 +2875,13 @@ var LeaseStore = class {
2791
2875
  * `beforeUnlink` (tests only) runs while a breaker holds the break guard, before the re-read that decides the unlink.
2792
2876
  */
2793
2877
  constructor(stateDir2, deps2 = {}) {
2794
- this.dir = path10.join(stateDir2, "leases");
2878
+ this.dir = path11.join(stateDir2, "leases");
2795
2879
  this.now = deps2.now ?? (() => Date.now());
2796
2880
  this.pid = deps2.pid ?? process.pid;
2797
2881
  this.beforeUnlink = deps2.beforeUnlink;
2798
2882
  }
2799
2883
  file(l) {
2800
- return path10.join(
2884
+ return path11.join(
2801
2885
  this.dir,
2802
2886
  `${String(l.seq).padStart(6, "0")}-${l.id}.json`
2803
2887
  );
@@ -2806,11 +2890,11 @@ var LeaseStore = class {
2806
2890
  return new Date(this.now()).toISOString();
2807
2891
  }
2808
2892
  write(l) {
2809
- fs6.mkdirSync(this.dir, { recursive: true });
2893
+ fs7.mkdirSync(this.dir, { recursive: true });
2810
2894
  const f = this.file(l);
2811
2895
  const tmp = `${f}.${this.pid}.tmp`;
2812
- fs6.writeFileSync(tmp, JSON.stringify(l, null, 2));
2813
- fs6.renameSync(tmp, f);
2896
+ fs7.writeFileSync(tmp, JSON.stringify(l, null, 2));
2897
+ fs7.renameSync(tmp, f);
2814
2898
  return l;
2815
2899
  }
2816
2900
  /**
@@ -2820,13 +2904,13 @@ var LeaseStore = class {
2820
2904
  * breakIfStale), never by moving it aside, so the lock path is never left free while a live holder has it.
2821
2905
  */
2822
2906
  async withLock(fn) {
2823
- fs6.mkdirSync(this.dir, { recursive: true });
2824
- const lock = path10.join(this.dir, ".lock");
2825
- const token = `${this.pid}-${crypto.randomBytes(6).toString("hex")}`;
2826
- const tmp = path10.join(this.dir, `.lock.${token}.tmp`);
2907
+ fs7.mkdirSync(this.dir, { recursive: true });
2908
+ const lock = path11.join(this.dir, ".lock");
2909
+ const token = `${this.pid}-${crypto2.randomBytes(6).toString("hex")}`;
2910
+ const tmp = path11.join(this.dir, `.lock.${token}.tmp`);
2827
2911
  const writeToken = () => {
2828
- fs6.mkdirSync(this.dir, { recursive: true });
2829
- fs6.writeFileSync(
2912
+ fs7.mkdirSync(this.dir, { recursive: true });
2913
+ fs7.writeFileSync(
2830
2914
  tmp,
2831
2915
  JSON.stringify({ pid: this.pid, at: this.now(), token })
2832
2916
  );
@@ -2857,7 +2941,7 @@ var LeaseStore = class {
2857
2941
  await new Promise((r2) => setTimeout(r2, 50));
2858
2942
  }
2859
2943
  } finally {
2860
- fs6.rmSync(tmp, { force: true });
2944
+ fs7.rmSync(tmp, { force: true });
2861
2945
  }
2862
2946
  LIVE_TOKENS.add(token);
2863
2947
  try {
@@ -2883,15 +2967,15 @@ var LeaseStore = class {
2883
2967
  sweepStrays() {
2884
2968
  let names;
2885
2969
  try {
2886
- names = fs6.readdirSync(this.dir);
2970
+ names = fs7.readdirSync(this.dir);
2887
2971
  } catch {
2888
2972
  return;
2889
2973
  }
2890
2974
  for (const n of names) {
2891
2975
  if (!STRAY.test(n)) continue;
2892
- const f = path10.join(this.dir, n);
2976
+ const f = path11.join(this.dir, n);
2893
2977
  if (this.mtimeAgeMs(f) > LOCK_STALE_MS)
2894
- fs6.rmSync(f, { recursive: true, force: true });
2978
+ fs7.rmSync(f, { recursive: true, force: true });
2895
2979
  }
2896
2980
  }
2897
2981
  /**
@@ -2902,12 +2986,12 @@ var LeaseStore = class {
2902
2986
  tryLink(tmp, lock, rewrite) {
2903
2987
  for (let attempt = 0; ; attempt++) {
2904
2988
  try {
2905
- fs6.linkSync(tmp, lock);
2989
+ fs7.linkSync(tmp, lock);
2906
2990
  return true;
2907
2991
  } catch (e) {
2908
2992
  const code = e.code;
2909
2993
  if (code === "EEXIST") return false;
2910
- if (code === "ENOENT" && attempt === 0 && !fs6.existsSync(tmp)) {
2994
+ if (code === "ENOENT" && attempt === 0 && !fs7.existsSync(tmp)) {
2911
2995
  rewrite();
2912
2996
  continue;
2913
2997
  }
@@ -2919,7 +3003,7 @@ var LeaseStore = class {
2919
3003
  stillStale(lock, judged) {
2920
3004
  let raw;
2921
3005
  try {
2922
- raw = fs6.readFileSync(lock, "utf8");
3006
+ raw = fs7.readFileSync(lock, "utf8");
2923
3007
  } catch (e) {
2924
3008
  if (e.code === "ENOENT") return false;
2925
3009
  throw e;
@@ -2932,7 +3016,7 @@ var LeaseStore = class {
2932
3016
  * or, with no readable holder, the directory's mtime is older than LOCK_STALE_MS.
2933
3017
  */
2934
3018
  guardStale(guard) {
2935
- const held = readHeld(path10.join(guard, "holder"));
3019
+ const held = readHeld(path11.join(guard, "holder"));
2936
3020
  if (held)
2937
3021
  return this.now() - held.at > LOCK_STALE_MS && !pidAlive(held.pid);
2938
3022
  return this.mtimeAgeMs(guard) > LOCK_STALE_MS;
@@ -2950,7 +3034,7 @@ var LeaseStore = class {
2950
3034
  breakIfStale(lock, token, take) {
2951
3035
  let raw;
2952
3036
  try {
2953
- raw = fs6.readFileSync(lock, "utf8");
3037
+ raw = fs7.readFileSync(lock, "utf8");
2954
3038
  } catch (e) {
2955
3039
  if (e.code === "ENOENT") return "retry";
2956
3040
  throw e;
@@ -2960,57 +3044,57 @@ var LeaseStore = class {
2960
3044
  if (!stale) return "wait";
2961
3045
  const guard = `${lock}.break`;
2962
3046
  try {
2963
- fs6.mkdirSync(guard);
3047
+ fs7.mkdirSync(guard);
2964
3048
  } catch (e) {
2965
3049
  if (e.code !== "EEXIST") throw e;
2966
3050
  if (!this.guardStale(guard)) return "wait";
2967
3051
  const moved = `${guard}.${token}.stale`;
2968
3052
  try {
2969
- fs6.renameSync(guard, moved);
3053
+ fs7.renameSync(guard, moved);
2970
3054
  } catch (err) {
2971
3055
  if (err.code === "ENOENT") return "retry";
2972
3056
  throw err;
2973
3057
  }
2974
- fs6.rmSync(moved, { recursive: true, force: true });
3058
+ fs7.rmSync(moved, { recursive: true, force: true });
2975
3059
  return "retry";
2976
3060
  }
2977
3061
  try {
2978
- fs6.writeFileSync(
2979
- path10.join(guard, "holder"),
3062
+ fs7.writeFileSync(
3063
+ path11.join(guard, "holder"),
2980
3064
  JSON.stringify({ pid: this.pid, at: this.now(), token })
2981
3065
  );
2982
3066
  this.beforeUnlink?.();
2983
3067
  if (!this.stillStale(lock, held)) return "retry";
2984
3068
  try {
2985
- fs6.unlinkSync(lock);
3069
+ fs7.unlinkSync(lock);
2986
3070
  } catch (e) {
2987
3071
  if (e.code !== "ENOENT") throw e;
2988
3072
  }
2989
3073
  return take() ? "taken" : "retry";
2990
3074
  } finally {
2991
- fs6.rmSync(guard, { recursive: true, force: true });
3075
+ fs7.rmSync(guard, { recursive: true, force: true });
2992
3076
  }
2993
3077
  }
2994
3078
  /** Age of a lock file by its modification time on the file system's clock; 0 when it is gone. */
2995
3079
  mtimeAgeMs(lock) {
2996
3080
  try {
2997
- return Date.now() - fs6.statSync(lock).mtimeMs;
3081
+ return Date.now() - fs7.statSync(lock).mtimeMs;
2998
3082
  } catch {
2999
3083
  return 0;
3000
3084
  }
3001
3085
  }
3002
3086
  /** Unlinks the lock only when it still carries this holder's token; a lock it does not own is never removed. */
3003
3087
  release(lock, token) {
3004
- if (readHeld(lock)?.token === token) fs6.rmSync(lock, { force: true });
3088
+ if (readHeld(lock)?.token === token) fs7.rmSync(lock, { force: true });
3005
3089
  }
3006
3090
  /** Every readable lease, in sequence order; a half-written file is skipped. */
3007
3091
  list() {
3008
- if (!fs6.existsSync(this.dir)) return [];
3009
- return fs6.readdirSync(this.dir).filter((f) => f.endsWith(".json")).sort().flatMap((f) => {
3092
+ if (!fs7.existsSync(this.dir)) return [];
3093
+ return fs7.readdirSync(this.dir).filter((f) => f.endsWith(".json")).sort().flatMap((f) => {
3010
3094
  try {
3011
3095
  return [
3012
3096
  JSON.parse(
3013
- fs6.readFileSync(path10.join(this.dir, f), "utf8")
3097
+ fs7.readFileSync(path11.join(this.dir, f), "utf8")
3014
3098
  )
3015
3099
  ];
3016
3100
  } catch {
@@ -3023,25 +3107,25 @@ var LeaseStore = class {
3023
3107
  return this.list().find((l) => l.id === id) ?? null;
3024
3108
  }
3025
3109
  nextSeq() {
3026
- const f = path10.join(this.dir, ".seq");
3110
+ const f = path11.join(this.dir, ".seq");
3027
3111
  const onDisk = Math.max(
3028
3112
  0,
3029
- ...fs6.readdirSync(this.dir).filter((name2) => name2.endsWith(".json")).map((name2) => Number(name2.split("-")[0])).filter(Number.isFinite)
3113
+ ...fs7.readdirSync(this.dir).filter((name2) => name2.endsWith(".json")).map((name2) => Number(name2.split("-")[0])).filter(Number.isFinite)
3030
3114
  );
3031
- const parsed = fs6.existsSync(f) ? Number(fs6.readFileSync(f, "utf8")) : 0;
3115
+ const parsed = fs7.existsSync(f) ? Number(fs7.readFileSync(f, "utf8")) : 0;
3032
3116
  const n = Math.max(Number.isFinite(parsed) ? parsed : 0, onDisk);
3033
3117
  const tmp = `${f}.${this.pid}.tmp`;
3034
- fs6.writeFileSync(tmp, String(n + 1));
3035
- fs6.renameSync(tmp, f);
3118
+ fs7.writeFileSync(tmp, String(n + 1));
3119
+ fs7.renameSync(tmp, f);
3036
3120
  return n + 1;
3037
3121
  }
3038
3122
  /** A new lease, pending (queued) or running (admitted at once), with `child_pid` the process running its start; call it under the lock. */
3039
3123
  async create(input, state, opts = {}) {
3040
- fs6.mkdirSync(this.dir, { recursive: true });
3124
+ fs7.mkdirSync(this.dir, { recursive: true });
3041
3125
  const at = this.stamp();
3042
3126
  const l = {
3043
3127
  ...input,
3044
- id: `l_${crypto.randomBytes(8).toString("base64url").replace(/[^a-z0-9]/gi, "").toLowerCase().padEnd(10, "0").slice(0, 10)}`,
3128
+ id: `l_${crypto2.randomBytes(8).toString("base64url").replace(/[^a-z0-9]/gi, "").toLowerCase().padEnd(10, "0").slice(0, 10)}`,
3045
3129
  seq: this.nextSeq(),
3046
3130
  state,
3047
3131
  child_pid: opts.child_pid ?? null,
@@ -3119,7 +3203,7 @@ var LeaseStore = class {
3119
3203
  for (const l of this.list()) {
3120
3204
  if (!ENDED.has(l.state) || !l.ended_at) continue;
3121
3205
  if (this.now() - Date.parse(l.ended_at) > maxAgeMs) {
3122
- fs6.rmSync(this.file(l), { force: true });
3206
+ fs7.rmSync(this.file(l), { force: true });
3123
3207
  n++;
3124
3208
  }
3125
3209
  }
@@ -3137,7 +3221,7 @@ var EXIT_POLL_MS = 250;
3137
3221
  function exitingAt(dir) {
3138
3222
  try {
3139
3223
  const v = JSON.parse(
3140
- fs7.readFileSync(path11.join(dir, WATCHER_NOTE), "utf8")
3224
+ fs8.readFileSync(path12.join(dir, WATCHER_NOTE), "utf8")
3141
3225
  );
3142
3226
  const at = typeof v.exiting_at === "string" ? Date.parse(v.exiting_at) : Number.NaN;
3143
3227
  return Number.isFinite(at) ? at : null;
@@ -3146,9 +3230,10 @@ function exitingAt(dir) {
3146
3230
  }
3147
3231
  }
3148
3232
  function watcherEntry(env = process.env, argv = process.argv) {
3149
- if (env.MCPROC_CLI_ENTRY) return path11.resolve(env.MCPROC_CLI_ENTRY);
3233
+ if (env.MCPROC_CLI_ENTRY) return path12.resolve(env.MCPROC_CLI_ENTRY);
3150
3234
  const own2 = argv[1] ?? "";
3151
- if (own2.endsWith("cli.js")) return path11.resolve(own2);
3235
+ const real = own2 ? realOf(own2) : "";
3236
+ if (real.endsWith("cli.js")) return real;
3152
3237
  throw new McprocError(
3153
3238
  "NOT_STARTED",
3154
3239
  `cannot launch the lease watcher: this mcproc runs from ${own2 || "an unknown entry"}, not from a built cli.js`,
@@ -3180,10 +3265,10 @@ async function lookAndLaunch(ctx, dir, leaving) {
3180
3265
  if (await watcherOnline(ctx) && (leaving === null || exitingAt(dir) !== leaving))
3181
3266
  return "online";
3182
3267
  const entry = watcherEntry();
3183
- const file = path11.join(dir, "watcher.ecosystem.json");
3184
- fs7.mkdirSync(dir, { recursive: true });
3185
- fs7.rmSync(path11.join(dir, WATCHER_NOTE), { force: true });
3186
- fs7.writeFileSync(
3268
+ const file = path12.join(dir, "watcher.ecosystem.json");
3269
+ fs8.mkdirSync(dir, { recursive: true });
3270
+ fs8.rmSync(path12.join(dir, WATCHER_NOTE), { force: true });
3271
+ fs8.writeFileSync(
3187
3272
  file,
3188
3273
  JSON.stringify(
3189
3274
  {
@@ -3219,7 +3304,7 @@ async function lookAndLaunch(ctx, dir, leaving) {
3219
3304
  // src/core/leases/view.ts
3220
3305
  async function leasesView(dir = stateDir(), ctx) {
3221
3306
  const store = new LeaseStore(dir);
3222
- if (fs8.existsSync(store.dir))
3307
+ if (fs9.existsSync(store.dir))
3223
3308
  await store.withLock(() => store.prune()).catch(() => {
3224
3309
  });
3225
3310
  const leases = store.list().map((l) => ({ ...l, position: store.positionOf(l.id) }));
@@ -3280,11 +3365,11 @@ import fs12 from "fs";
3280
3365
  import path16 from "path";
3281
3366
 
3282
3367
  // src/core/actions.ts
3283
- import path14 from "path";
3368
+ import path15 from "path";
3284
3369
 
3285
3370
  // src/core/list.ts
3286
- import fs9 from "fs";
3287
- import path12 from "path";
3371
+ import fs10 from "fs";
3372
+ import path13 from "path";
3288
3373
 
3289
3374
  // src/core/metrics.ts
3290
3375
  function cpuPercent(pids, before, after, clkTck, intervalSec) {
@@ -4345,8 +4430,8 @@ var depView = (d) => ({
4345
4430
  timeout_s: d.timeoutMs / 1e3
4346
4431
  });
4347
4432
  function describeProject(m) {
4348
- const readmePath = path12.join(m.root, ".mcproc", "README.md");
4349
- const readme = fs9.existsSync(readmePath) ? fs9.readFileSync(readmePath, "utf8") : null;
4433
+ const readmePath = path13.join(m.root, ".mcproc", "README.md");
4434
+ const readme = fs10.existsSync(readmePath) ? fs10.readFileSync(readmePath, "utf8") : null;
4350
4435
  return {
4351
4436
  project: m.project,
4352
4437
  root: m.root,
@@ -4379,7 +4464,7 @@ function describeProject(m) {
4379
4464
 
4380
4465
  // src/core/logfollow.ts
4381
4466
  import { open } from "fs/promises";
4382
- import path13 from "path";
4467
+ import path14 from "path";
4383
4468
 
4384
4469
  // src/core/logwatch.ts
4385
4470
  import { watch as fsWatch } from "fs";
@@ -4873,8 +4958,8 @@ var LogFollower = class {
4873
4958
  }
4874
4959
  const byDir = /* @__PURE__ */ new Map();
4875
4960
  for (const p of [this.paths.out, this.paths.err]) {
4876
- const dir = path13.dirname(p);
4877
- byDir.set(dir, (byDir.get(dir) ?? /* @__PURE__ */ new Set()).add(path13.basename(p)));
4961
+ const dir = path14.dirname(p);
4962
+ byDir.set(dir, (byDir.get(dir) ?? /* @__PURE__ */ new Set()).add(path14.basename(p)));
4878
4963
  }
4879
4964
  for (const [dir, names] of byDir)
4880
4965
  this.unwatch.push(
@@ -6331,7 +6416,7 @@ function assertNoCollision(ctx, procs, names) {
6331
6416
  const name2 = pm2Name(ctx.manifest.project, n);
6332
6417
  const expectedCwd = appCwd(ctx.host, ctx.manifest.apps[n]);
6333
6418
  const other = procs.find(
6334
- (p) => p.name === name2 && (p.namespace !== ctx.manifest.project || p.cwd !== "" && path14.posix.resolve(p.cwd) !== expectedCwd)
6419
+ (p) => p.name === name2 && (p.namespace !== ctx.manifest.project || p.cwd !== "" && path15.posix.resolve(p.cwd) !== expectedCwd)
6335
6420
  );
6336
6421
  if (other && other.namespace !== ctx.manifest.project) {
6337
6422
  throw new McprocError(
@@ -6358,7 +6443,7 @@ async function shipEcosystem(ctx) {
6358
6443
  const eco = buildEcosystem(ctx.manifest, ctx.host);
6359
6444
  const file = ecosystemPath(ctx.host);
6360
6445
  const dir = generatedDir(ctx.host);
6361
- const gi = path14.posix.join(path14.posix.dirname(dir), ".gitignore");
6446
+ const gi = path15.posix.join(path15.posix.dirname(dir), ".gitignore");
6362
6447
  const cmd = `mkdir -p ${shq(dir)} && { [ -e ${shq(gi)} ] || printf '*\\n' > ${shq(gi)}; } && cat > ${shq(file)}`;
6363
6448
  const res = ensureOk(
6364
6449
  await ctx.transport.exec(cmd, { stdin: JSON.stringify(eco, null, 2) }),
@@ -6536,92 +6621,6 @@ function admit(opts) {
6536
6621
 
6537
6622
  // src/core/leases/siblings.ts
6538
6623
  import fs11 from "fs";
6539
-
6540
- // src/core/slots.ts
6541
- import crypto2 from "crypto";
6542
- import fs10 from "fs";
6543
- import path15 from "path";
6544
- function registryKey(baseProject, repo) {
6545
- return `${baseProject}-${crypto2.createHash("sha256").update(realOf(repo)).digest("hex").slice(0, 8)}`;
6546
- }
6547
- function registryPath(stateDir2, baseProject, repo) {
6548
- return path15.join(stateDir2, "slots", `${registryKey(baseProject, repo)}.json`);
6549
- }
6550
- function emptyRegistry(repo) {
6551
- return { repo, slots: {} };
6552
- }
6553
- function readRegistry(file) {
6554
- if (!fs10.existsSync(file)) return null;
6555
- return JSON.parse(fs10.readFileSync(file, "utf8"));
6556
- }
6557
- function writeRegistry(file, r) {
6558
- fs10.mkdirSync(path15.dirname(file), { recursive: true });
6559
- const tmp = `${file}.${process.pid}.tmp`;
6560
- fs10.writeFileSync(tmp, JSON.stringify(r, null, 2));
6561
- fs10.renameSync(tmp, file);
6562
- }
6563
- function withMain(r, entry) {
6564
- return { ...r, slots: { ...r.slots, "0": entry } };
6565
- }
6566
- function claimSlot(r, opts) {
6567
- if (opts.want !== void 0) {
6568
- if (opts.want < 1 || opts.want > opts.max)
6569
- throw new McprocError(
6570
- "BAD_ARGUMENT",
6571
- `slot ${opts.want} is outside 1..${opts.max} (slot 0 is the main checkout)`
6572
- );
6573
- if (Object.hasOwn(r.slots, String(opts.want)))
6574
- throw new McprocError(
6575
- "SLOT_TAKEN",
6576
- `slot ${opts.want} is taken by ${r.slots[String(opts.want)].path}`,
6577
- { hint: "Pick another slot, or remove that worktree first." }
6578
- );
6579
- return {
6580
- registry: { ...r, slots: { ...r.slots, [opts.want]: opts.entry } },
6581
- slot: opts.want
6582
- };
6583
- }
6584
- for (let s = 1; s <= opts.max; s++)
6585
- if (!Object.hasOwn(r.slots, String(s)))
6586
- return {
6587
- registry: { ...r, slots: { ...r.slots, [s]: opts.entry } },
6588
- slot: s
6589
- };
6590
- throw new McprocError(
6591
- "NO_FREE_SLOT",
6592
- `all ${opts.max} worktree slots of ${r.repo} are taken`,
6593
- {
6594
- hint: "Remove a worktree (worktree remove), or raise worktree.slots in the manifest."
6595
- }
6596
- );
6597
- }
6598
- function releaseSlot(r, slot) {
6599
- const slots = { ...r.slots };
6600
- delete slots[String(slot)];
6601
- return { ...r, slots };
6602
- }
6603
- function realOf(p) {
6604
- try {
6605
- return fs10.realpathSync(p);
6606
- } catch {
6607
- return path15.resolve(p);
6608
- }
6609
- }
6610
- function reclaimGone(r, exists) {
6611
- const freed = [];
6612
- let out = r;
6613
- for (const [k, e] of Object.entries(r.slots)) {
6614
- if (k === "0" || exists(realOf(e.path))) continue;
6615
- out = releaseSlot(out, Number(k));
6616
- freed.push(Number(k));
6617
- }
6618
- return { registry: out, freed: freed.sort((a, b) => a - b) };
6619
- }
6620
- function siblingsOf(r, root) {
6621
- return Object.entries(r.slots).filter(([, e]) => realOf(e.path) !== realOf(root)).map(([k, entry]) => ({ slot: Number(k), entry })).sort((a, b) => a.slot - b.slot);
6622
- }
6623
-
6624
- // src/core/leases/siblings.ts
6625
6624
  var MIB = 1024 * 1024;
6626
6625
  var repoOf = (m) => m.repo;
6627
6626
  function registryOf(m, dir = stateDir()) {