stim 1.0.0 → 1.2.0

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 (66) hide show
  1. package/README.md +29 -10
  2. package/dist/android-BW8i_YxU.mjs +204 -0
  3. package/dist/android-Bcmev2s2.mjs +2334 -0
  4. package/dist/android-K63_0UfF.mjs +727 -0
  5. package/dist/android-cas-D6HPZ6UQ.mjs +1078 -0
  6. package/dist/android-cas-compiler.mjs +2 -2
  7. package/dist/app-install-NFxUahnH.mjs +1149 -0
  8. package/dist/build-lock-BsVoLDaq.mjs +645 -0
  9. package/dist/build-slots-D0b6sJyo.mjs +152 -0
  10. package/dist/{cache-manifest-4tH64LQ9.mjs → cache-manifest-Cu4_Znw9.mjs} +2 -2
  11. package/dist/cache-manifest.mjs +1 -1
  12. package/dist/cli.mjs +24 -27166
  13. package/dist/collector-run.d.mts +1 -0
  14. package/dist/collector-run.mjs +7 -5
  15. package/dist/command-output-8wHMEde0.mjs +289 -0
  16. package/dist/{config-7kmtuhO2.mjs → config-CMtIaTk9.mjs} +44 -13
  17. package/dist/deps-BLBsTNDS.mjs +435 -0
  18. package/dist/dev-client-RpvXxCXV.mjs +620 -0
  19. package/dist/device-CoCPmazS.mjs +250 -0
  20. package/dist/device-lease-Dpik41RY.mjs +335 -0
  21. package/dist/device-pool-B5pEVa3t.mjs +408 -0
  22. package/dist/device-remote-E81KAdXf.mjs +1209 -0
  23. package/dist/doctor-CqPeMsSm.mjs +1130 -0
  24. package/dist/doctor-D_CX7ib5.mjs +530 -0
  25. package/dist/error-diagnostics-Dw8Gn3yw.mjs +636 -0
  26. package/dist/{exec-bsN9MJXb.mjs → exec-CyylIdq9.mjs} +4 -2
  27. package/dist/gc-B7KkDinF.mjs +1665 -0
  28. package/dist/guide-BS2Xc24o.mjs +4503 -0
  29. package/dist/ios-B7KLgIp5.mjs +2733 -0
  30. package/dist/ios-D1h1Di39.mjs +451 -0
  31. package/dist/ios-device-D3pzTKQE.mjs +81 -0
  32. package/dist/ios-device-xNt0Lg6v.mjs +413 -0
  33. package/dist/logs-DcX9AvRF.mjs +176 -0
  34. package/dist/logs-query-oqn47Ffx.mjs +273 -0
  35. package/dist/metro-COslRK_F.mjs +184 -0
  36. package/dist/metro-store-CDDZHtPl.mjs +92 -0
  37. package/dist/ownership-CGPart6T.mjs +60 -0
  38. package/dist/ownership-CYOyqxUo.mjs +475 -0
  39. package/dist/ownership-claim-7zRxxTfZ.mjs +569 -0
  40. package/dist/{project-Dit1CckW.mjs → project-BBTVABUz.mjs} +12 -3
  41. package/dist/reclaim-C5Jmuahx.mjs +348 -0
  42. package/dist/reload-Z8nKifAk.mjs +332 -0
  43. package/dist/remote-cache-DDmJ1Roq.mjs +994 -0
  44. package/dist/{server-bare-BmTVVmBB.mjs → server-bare-DEk37Mz2.mjs} +8 -2
  45. package/dist/{server-expo-CHJe1r6j.mjs → server-expo-C3ktAwUG.mjs} +6 -5
  46. package/dist/server-expo-Drd7ksHu.mjs +2 -0
  47. package/dist/settings-DwU1_U_q.mjs +708 -0
  48. package/dist/spawn-entry-nCE6m770.mjs +14 -0
  49. package/dist/start-BrY4EuIg.mjs +822 -0
  50. package/dist/{state-2nush3MK.mjs → state-CZ-ER-8d.mjs} +21 -22
  51. package/dist/stats-BCWEVLrM.mjs +307 -0
  52. package/dist/stats-BsF15Bti.mjs +62 -0
  53. package/dist/status-DFI1Hfpg.mjs +393 -0
  54. package/dist/stop-B7DR4awK.mjs +3 -0
  55. package/dist/stop-Btg770vV.mjs +591 -0
  56. package/dist/supervisor-run.d.mts +0 -1
  57. package/dist/supervisor-run.mjs +4 -4
  58. package/dist/workspace-process-lock-BKfdys1q.mjs +47 -0
  59. package/dist/worktree-BPTbI-LN.mjs +1322 -0
  60. package/dist/worktree-C79xx_kM.mjs +543 -0
  61. package/dist/xcode-DeKCribl.mjs +1832 -0
  62. package/package.json +5 -5
  63. package/shim/bundle-response.cjs +5 -0
  64. package/dist/android-jc8MnUyL.mjs +0 -514
  65. package/dist/metro-store-COLl1pOk.mjs +0 -1249
  66. package/dist/server-expo-BZJm--wA.mjs +0 -2
@@ -0,0 +1,569 @@
1
+ import { E as inspectProcessIdentity, w as captureProcessIdentity } from "./config-CMtIaTk9.mjs";
2
+ import { d as quotedPath } from "./command-output-8wHMEde0.mjs";
3
+ import { lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { randomUUID } from "node:crypto";
5
+ import { dirname, join } from "node:path";
6
+ //#region src/ownership-claim.ts
7
+ const CLAIM_REFUSED = "STIM_CLAIM_REFUSED";
8
+ const CLAIM_UNAVAILABLE = "STIM_CLAIM_UNAVAILABLE";
9
+ /**
10
+ * The refusal reason for a claim store whose own path is occupied by a file. It names a filesystem
11
+ * state that predates any claim, so a caller that only reads can tell it apart from an unresolvable
12
+ * claim record and fall back to what it did before claims existed.
13
+ */
14
+ const CLAIM_PATH_NOT_A_DIRECTORY = "the claim path is a file, not a claim directory";
15
+ const EXCLUSIVE_DIR = "exclusive";
16
+ const SHARED_DIR = "shared";
17
+ const CLAIM_SUFFIX = ".claim";
18
+ const CHILD_SUFFIX = ".child";
19
+ const STAGING_PREFIX = ".staging-";
20
+ const PUBLISH_ATTEMPTS = 64;
21
+ var ClaimRefusedError = class extends Error {
22
+ code = CLAIM_REFUSED;
23
+ claimPath;
24
+ reason;
25
+ removeCommand;
26
+ constructor({ claimPath, root, reason, label }) {
27
+ const removeCommand = claimRemoveCommand(claimPath.startsWith(root) ? claimPath : root);
28
+ super(`Stim cannot tell whether the ${label} claim at ${claimPath} is still held: ${reason}. It will not remove a claim it cannot prove is dead, and it will not wait on one either. If nothing is using it, remove the claim and run the command again:\n ${removeCommand}`);
29
+ this.claimPath = claimPath;
30
+ this.reason = reason;
31
+ this.removeCommand = removeCommand;
32
+ }
33
+ };
34
+ var ClaimUnavailableError = class extends Error {
35
+ code = CLAIM_UNAVAILABLE;
36
+ reason;
37
+ constructor(reason) {
38
+ super(`Stim could not record a process identity, so it cannot take an ownership claim: ${reason}.`);
39
+ this.reason = reason;
40
+ }
41
+ };
42
+ /**
43
+ * The command that clears one surveyed claim: targeted at the claim's own file when the path names one,
44
+ * and quoted, because this text is printed for a human to copy into a shell and a path with a space in
45
+ * it would otherwise name several targets.
46
+ */
47
+ function claimRemoveCommand(path) {
48
+ return `rm ${path.endsWith(CLAIM_SUFFIX) || path.endsWith(CHILD_SUFFIX) ? "-f" : "-rf"} ${quotedPath(path)}`;
49
+ }
50
+ function isClaimRefusal(err) {
51
+ return err?.code === CLAIM_REFUSED;
52
+ }
53
+ function isClaimUnavailable(err) {
54
+ return err?.code === CLAIM_UNAVAILABLE;
55
+ }
56
+ /**
57
+ * The refusal a command reports for either error the claim primitive raises, and null for anything else.
58
+ * Both are refusals: a claim Stim cannot resolve and a process identity it cannot record are the two
59
+ * states in which it holds no claim, and running the operation a claim serializes without one gives a
60
+ * competing run no protection at all.
61
+ */
62
+ function claimFailure(err, retryCommand) {
63
+ if (isClaimRefusal(err)) return {
64
+ code: CLAIM_REFUSED,
65
+ message: err.message,
66
+ remedy: `Run \`${err.removeCommand}\`, then run \`${retryCommand}\` again.`
67
+ };
68
+ if (isClaimUnavailable(err)) return {
69
+ code: CLAIM_UNAVAILABLE,
70
+ message: err.message,
71
+ remedy: `Reinstall Stim so the unique-pid native module for this platform is present, then run \`${retryCommand}\` again.`
72
+ };
73
+ return null;
74
+ }
75
+ function exclusiveClaimDir(root) {
76
+ return join(root, EXCLUSIVE_DIR);
77
+ }
78
+ function sharedClaimDir(root) {
79
+ return join(root, SHARED_DIR);
80
+ }
81
+ function childPath(claimPath) {
82
+ return `${claimPath.slice(0, -6)}${CHILD_SUFFIX}`;
83
+ }
84
+ function asOwner(record) {
85
+ const candidate = record;
86
+ if (!candidate || typeof candidate !== "object") return null;
87
+ if (typeof candidate.pid !== "number" || !Number.isSafeInteger(candidate.pid) || candidate.pid <= 0) return null;
88
+ if (typeof candidate.processToken !== "string" || !candidate.processToken) return null;
89
+ return {
90
+ pid: candidate.pid,
91
+ processToken: candidate.processToken
92
+ };
93
+ }
94
+ function readChild(claimPath) {
95
+ let text;
96
+ try {
97
+ text = readFileSync(childPath(claimPath), "utf-8");
98
+ } catch (err) {
99
+ if (err?.code === "ENOENT") return null;
100
+ return "unreadable";
101
+ }
102
+ try {
103
+ return { record: asOwner(JSON.parse(text)?.record) };
104
+ } catch {
105
+ return "unreadable";
106
+ }
107
+ }
108
+ function readClaim(path, mode) {
109
+ let text;
110
+ try {
111
+ text = readFileSync(path, "utf-8");
112
+ } catch (err) {
113
+ if (err?.code === "ENOENT") return null;
114
+ return "unreadable";
115
+ }
116
+ let parsed;
117
+ try {
118
+ parsed = JSON.parse(text);
119
+ } catch {
120
+ return "unreadable";
121
+ }
122
+ const owner = asOwner(parsed?.owner);
123
+ if (!owner || typeof parsed.claimId !== "string" || !parsed.claimId) return "unreadable";
124
+ const child = readChild(path);
125
+ if (child === "unreadable") return "unreadable";
126
+ return {
127
+ path,
128
+ claimId: parsed.claimId,
129
+ mode,
130
+ owner,
131
+ startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : "",
132
+ details: parsed.details && typeof parsed.details === "object" ? parsed.details : {},
133
+ child: child?.record ?? null,
134
+ childDeclared: child !== null
135
+ };
136
+ }
137
+ /**
138
+ * Whether any member of the process group `pid` leads is alive. A recorded child is resolved through
139
+ * its group rather than through the one process, so a package manager's postinstall descendant keeps
140
+ * the claim after the manager exits. A pgid, unlike a ProcessRecord, carries no identity: a recycled
141
+ * one reads as alive, which holds a claim longer than it needs to be held and never frees a live one.
142
+ */
143
+ function processGroupAlive(pid) {
144
+ if (!Number.isSafeInteger(pid) || pid <= 1) return false;
145
+ try {
146
+ process.kill(-pid, 0);
147
+ return true;
148
+ } catch (err) {
149
+ return err?.code === "EPERM";
150
+ }
151
+ }
152
+ function childLiveness(claimPath) {
153
+ const child = readChild(claimPath);
154
+ if (child === null) return "none";
155
+ if (child === "unreadable" || !child.record) return "unknown";
156
+ const status = inspectProcessIdentity(child.record);
157
+ if (status === "same") return "live";
158
+ if (status === "gone") return processGroupAlive(child.record.pid) ? "live" : "dead";
159
+ return "unknown";
160
+ }
161
+ function claimLiveness(holder) {
162
+ const status = inspectProcessIdentity(holder.owner);
163
+ if (status === "same") return "live";
164
+ if (status === "unknown") return "unknown";
165
+ const child = childLiveness(holder.path);
166
+ return child === "none" ? "dead" : child;
167
+ }
168
+ function refuse(root, claimPath, label, reason) {
169
+ throw new ClaimRefusedError({
170
+ claimPath,
171
+ root,
172
+ label,
173
+ reason
174
+ });
175
+ }
176
+ function names(dir) {
177
+ try {
178
+ return readdirSync(dir).toSorted();
179
+ } catch (err) {
180
+ if (err?.code === "ENOENT") return [];
181
+ return "unreadable";
182
+ }
183
+ }
184
+ function survey(dir, mode, into) {
185
+ const listed = names(dir);
186
+ if (listed === "unreadable") {
187
+ into.unresolved.push({
188
+ path: dir,
189
+ reason: "its directory could not be read"
190
+ });
191
+ return;
192
+ }
193
+ const claims = new Set(listed.filter((name) => name.endsWith(CLAIM_SUFFIX)));
194
+ const orphans = listed.filter((name) => name.endsWith(CHILD_SUFFIX) && !claims.has(`${name.slice(0, -6)}${CLAIM_SUFFIX}`));
195
+ for (const name of orphans) into.orphans.push(join(dir, name));
196
+ for (const name of listed) {
197
+ if (!name.endsWith(CLAIM_SUFFIX)) continue;
198
+ const path = join(dir, name);
199
+ const holder = readClaim(path, mode);
200
+ if (holder === null) continue;
201
+ if (holder === "unreadable") {
202
+ into.unresolved.push({
203
+ path,
204
+ reason: "its record is missing, truncated or not valid JSON"
205
+ });
206
+ continue;
207
+ }
208
+ const liveness = claimLiveness(holder);
209
+ if (liveness === "live") into.live.push(holder);
210
+ else if (liveness === "dead") into.dead.push(holder);
211
+ else into.unresolved.push({
212
+ path,
213
+ reason: holder.childDeclared && !holder.child ? "it spawned the process that holds it and was killed before recording which one" : "its process identity token does not decode, so the holder cannot be identified"
214
+ });
215
+ }
216
+ if (listed.filter((name) => !orphans.includes(name)).length > 0 && claims.size === 0) into.unresolved.push({
217
+ path: dir,
218
+ reason: "it holds files Stim did not write"
219
+ });
220
+ }
221
+ /** Classify every claim in a claim set without removing anything and without refusing. */
222
+ function readClaimSet(root) {
223
+ const state = {
224
+ live: [],
225
+ dead: [],
226
+ unresolved: [],
227
+ orphans: []
228
+ };
229
+ survey(exclusiveClaimDir(root), "exclusive", state);
230
+ survey(sharedClaimDir(root), "shared", state);
231
+ return state;
232
+ }
233
+ function removeOrRefuse(path, root, claimPath, label) {
234
+ try {
235
+ unlinkSync(path);
236
+ } catch (err) {
237
+ const code = err?.code;
238
+ if (code === "ENOENT") return;
239
+ refuse(root, claimPath, label, `the claim left behind could not be removed (${code || err?.message})`);
240
+ }
241
+ }
242
+ function reap(holder, root, label) {
243
+ const again = readClaim(holder.path, holder.mode);
244
+ if (again === null) return true;
245
+ if (again === "unreadable" || again.claimId !== holder.claimId) return false;
246
+ removeOrRefuse(childPath(holder.path), root, holder.path, label);
247
+ removeOrRefuse(holder.path, root, holder.path, label);
248
+ return true;
249
+ }
250
+ function tidy(dir) {
251
+ try {
252
+ rmdirSync(dir);
253
+ } catch {}
254
+ }
255
+ function inspectClaimSet(root, { label = "ownership" } = {}) {
256
+ const state = readClaimSet(root);
257
+ const unresolved = state.unresolved[0];
258
+ if (unresolved) refuse(root, unresolved.path, label, unresolved.reason);
259
+ const reaped = [];
260
+ for (const orphan of state.orphans) removeOrRefuse(orphan, root, orphan, label);
261
+ for (const holder of state.dead) if (reap(holder, root, label)) reaped.push(holder);
262
+ if (!state.live.some((holder) => holder.mode === "exclusive")) tidy(exclusiveClaimDir(root));
263
+ if (!state.live.some((holder) => holder.mode === "shared")) tidy(sharedClaimDir(root));
264
+ return {
265
+ exclusive: state.live.find((holder) => holder.mode === "exclusive") ?? null,
266
+ shared: state.live.filter((holder) => holder.mode === "shared"),
267
+ reaped
268
+ };
269
+ }
270
+ function tidySet(root) {
271
+ tidy(exclusiveClaimDir(root));
272
+ tidy(sharedClaimDir(root));
273
+ tidy(root);
274
+ }
275
+ function removeAbandonedStaging(root) {
276
+ const listed = names(root);
277
+ if (listed === "unreadable") return;
278
+ for (const name of listed) if (name.startsWith(STAGING_PREFIX)) rmSync(join(root, name), {
279
+ recursive: true,
280
+ force: true
281
+ });
282
+ }
283
+ /**
284
+ * Remove a claim set that holds nothing. Takes the set's own exclusive claim first and removes the set
285
+ * only through the token-verified unlink and empty-directory removals that releasing a claim performs,
286
+ * so a claim published between the survey that reported the set stale and this call is kept, not
287
+ * deleted; a set whose state cannot be established is refused rather than removed.
288
+ */
289
+ function clearFreeClaimSet({ root, label = "ownership" }) {
290
+ let attempt;
291
+ try {
292
+ attempt = tryAcquireClaim({
293
+ root,
294
+ mode: "exclusive",
295
+ label
296
+ });
297
+ } catch (err) {
298
+ if (isClaimRefusal(err) || isClaimUnavailable(err)) return {
299
+ status: "refused",
300
+ reason: err.reason
301
+ };
302
+ return {
303
+ status: "failed",
304
+ reason: err?.message ?? String(err)
305
+ };
306
+ }
307
+ if (!attempt.acquired) {
308
+ if (attempt.pending) releaseClaim(attempt.pending);
309
+ return {
310
+ status: "held",
311
+ holder: attempt.held ?? attempt.waitingFor?.[0] ?? null
312
+ };
313
+ }
314
+ try {
315
+ removeAbandonedStaging(root);
316
+ } catch (err) {
317
+ return {
318
+ status: "failed",
319
+ reason: err?.message ?? String(err)
320
+ };
321
+ } finally {
322
+ releaseClaim(attempt.acquired);
323
+ }
324
+ tidySet(root);
325
+ const remaining = names(root);
326
+ if (remaining === "unreadable") return {
327
+ status: "failed",
328
+ reason: "its directory could not be read"
329
+ };
330
+ const after = readClaimSet(root);
331
+ if (after.live.length > 0) return {
332
+ status: "held",
333
+ holder: after.live[0]
334
+ };
335
+ if (after.unresolved.length > 0) return {
336
+ status: "refused",
337
+ reason: after.unresolved[0].reason
338
+ };
339
+ if (remaining.length > 0) return {
340
+ status: "failed",
341
+ reason: `it still holds ${remaining.join(", ")}`
342
+ };
343
+ return { status: "cleared" };
344
+ }
345
+ function selfOwner() {
346
+ const captured = captureProcessIdentity(process.pid);
347
+ if (!captured.ok) throw new ClaimUnavailableError(captured.reason);
348
+ return {
349
+ pid: process.pid,
350
+ processToken: captured.token
351
+ };
352
+ }
353
+ function contended(error) {
354
+ if (error instanceof MissingClaimStoreError) return false;
355
+ const code = error?.code;
356
+ return code === "EEXIST" || code === "ENOTEMPTY" || code === "ENOENT" || code === "EINVAL";
357
+ }
358
+ var MissingClaimStoreError = class extends Error {
359
+ code = "ENOENT";
360
+ };
361
+ function createClaimDir(path) {
362
+ try {
363
+ mkdirSync(path, { recursive: true });
364
+ return;
365
+ } catch (error) {
366
+ if (error?.code !== "ENOENT") throw error;
367
+ }
368
+ const missing = [];
369
+ for (let level = path;; level = dirname(level)) {
370
+ let entry;
371
+ try {
372
+ entry = lstatSync(level);
373
+ } catch (error) {
374
+ if (error?.code !== "ENOENT") throw error;
375
+ missing.push(level);
376
+ continue;
377
+ }
378
+ if (entry.isSymbolicLink()) try {
379
+ statSync(level);
380
+ } catch (error) {
381
+ if (error?.code === "ENOENT") throw new MissingClaimStoreError(error.message, { cause: error });
382
+ throw error;
383
+ }
384
+ break;
385
+ }
386
+ for (const level of missing.toReversed()) try {
387
+ mkdirSync(level);
388
+ } catch (error) {
389
+ if (error?.code !== "EEXIST") throw error;
390
+ }
391
+ }
392
+ function publishExclusive(root, payload, claimId, label) {
393
+ const staging = join(root, `${STAGING_PREFIX}${claimId}`);
394
+ const target = exclusiveClaimDir(root);
395
+ try {
396
+ createClaimDir(staging);
397
+ writeFileSync(join(staging, `${claimId}${CLAIM_SUFFIX}`), payload);
398
+ renameSync(staging, target);
399
+ return join(target, `${claimId}${CLAIM_SUFFIX}`);
400
+ } catch (err) {
401
+ rmSync(staging, {
402
+ recursive: true,
403
+ force: true
404
+ });
405
+ const code = err?.code;
406
+ if (contended(err)) return null;
407
+ if (code === "ENOTDIR") refuse(root, target, label, CLAIM_PATH_NOT_A_DIRECTORY);
408
+ throw err;
409
+ }
410
+ }
411
+ function publishShared(root, payload, claimId) {
412
+ const staging = join(root, `${STAGING_PREFIX}${claimId}`);
413
+ const target = join(sharedClaimDir(root), `${claimId}${CLAIM_SUFFIX}`);
414
+ try {
415
+ createClaimDir(sharedClaimDir(root));
416
+ writeFileSync(staging, payload);
417
+ renameSync(staging, target);
418
+ return target;
419
+ } catch (err) {
420
+ rmSync(staging, { force: true });
421
+ if (contended(err)) return null;
422
+ throw err;
423
+ }
424
+ }
425
+ function settleOrRelease(claim) {
426
+ try {
427
+ return inspectClaimSet(claim.root, { label: claim.label });
428
+ } catch (err) {
429
+ releaseClaim(claim);
430
+ throw err;
431
+ }
432
+ }
433
+ function tryAcquireClaim({ root, mode, details = {}, label = "ownership" }) {
434
+ const owner = selfOwner();
435
+ const reaped = [];
436
+ for (let attempt = 0; attempt < PUBLISH_ATTEMPTS; attempt++) {
437
+ try {
438
+ createClaimDir(root);
439
+ } catch (err) {
440
+ const code = err?.code;
441
+ if (code === "EEXIST" || code === "ENOTDIR") refuse(root, root, label, CLAIM_PATH_NOT_A_DIRECTORY);
442
+ if (contended(err)) continue;
443
+ throw err;
444
+ }
445
+ const state = inspectClaimSet(root, { label });
446
+ reaped.push(...state.reaped);
447
+ if (state.exclusive) return {
448
+ held: state.exclusive,
449
+ reaped
450
+ };
451
+ const claimId = randomUUID();
452
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
453
+ const payload = JSON.stringify({
454
+ claimId,
455
+ mode,
456
+ owner,
457
+ startedAt,
458
+ details
459
+ });
460
+ const handle = (path) => ({
461
+ root,
462
+ path,
463
+ claimId,
464
+ mode,
465
+ owner,
466
+ startedAt,
467
+ details,
468
+ label
469
+ });
470
+ if (mode === "shared") {
471
+ const published = publishShared(root, payload, claimId);
472
+ if (!published) continue;
473
+ const claim = handle(published);
474
+ const settled = settleOrRelease(claim);
475
+ reaped.push(...settled.reaped);
476
+ if (!settled.exclusive) return {
477
+ acquired: claim,
478
+ reaped
479
+ };
480
+ releaseClaim(claim);
481
+ return {
482
+ held: settled.exclusive,
483
+ reaped
484
+ };
485
+ }
486
+ const path = publishExclusive(root, payload, claimId, label);
487
+ if (!path) continue;
488
+ const claim = handle(path);
489
+ const settled = settleOrRelease(claim);
490
+ reaped.push(...settled.reaped);
491
+ if (settled.shared.length === 0) return {
492
+ acquired: claim,
493
+ reaped
494
+ };
495
+ return {
496
+ waitingFor: settled.shared,
497
+ pending: claim,
498
+ reaped
499
+ };
500
+ }
501
+ const contender = inspectClaimSet(root, { label });
502
+ reaped.push(...contender.reaped);
503
+ const holder = contender.exclusive ?? (mode === "exclusive" ? contender.shared[0] : void 0);
504
+ if (holder) return {
505
+ held: holder,
506
+ reaped
507
+ };
508
+ refuse(root, root, label, `another process took and gave up the claim on every one of ${PUBLISH_ATTEMPTS} attempts to take it`);
509
+ }
510
+ function settleClaim(pending) {
511
+ const state = inspectClaimSet(pending.root, { label: pending.label });
512
+ if (state.shared.length === 0) return {
513
+ acquired: pending,
514
+ reaped: state.reaped
515
+ };
516
+ return {
517
+ waitingFor: state.shared,
518
+ pending,
519
+ reaped: state.reaped
520
+ };
521
+ }
522
+ function releaseClaim(handle) {
523
+ if (!handle) return false;
524
+ const current = readClaim(handle.path, handle.mode);
525
+ if (current === null) return true;
526
+ if (current === "unreadable" || current.claimId !== handle.claimId) return false;
527
+ try {
528
+ rmSync(childPath(handle.path), { force: true });
529
+ rmSync(handle.path, { force: true });
530
+ } catch {
531
+ return false;
532
+ }
533
+ tidy(handle.mode === "exclusive" ? exclusiveClaimDir(handle.root) : sharedClaimDir(handle.root));
534
+ tidy(handle.root);
535
+ return true;
536
+ }
537
+ /**
538
+ * Declare that this claim is about to spawn the process that does the real work, before the spawn.
539
+ * A claim marked this way and left without a child record reads as unresolvable rather than as free,
540
+ * so a parent killed between the spawn and the record cannot release a claim its child still needs.
541
+ */
542
+ function markClaimChildPending(handle) {
543
+ writeChild(handle, { record: null });
544
+ }
545
+ /**
546
+ * Record the spawned process that holds this claim. Spawn it with `detached: true` so it leads its own
547
+ * process group: the claim then reads as held while any member of that group is alive, which is what
548
+ * keeps a package manager's postinstall child covered after the manager itself exits.
549
+ */
550
+ function setClaimChild(handle, record) {
551
+ const child = asOwner(record);
552
+ if (!child) throw new ClaimUnavailableError("the child process identity was not a pid and token pair");
553
+ writeChild(handle, { record: child });
554
+ }
555
+ function clearClaimChild(handle) {
556
+ rmSync(childPath(handle.path), { force: true });
557
+ }
558
+ function writeChild(handle, body) {
559
+ const staging = join(handle.root, `${STAGING_PREFIX}child-${handle.claimId}`);
560
+ try {
561
+ writeFileSync(staging, JSON.stringify(body));
562
+ renameSync(staging, childPath(handle.path));
563
+ } catch (err) {
564
+ rmSync(staging, { force: true });
565
+ throw err;
566
+ }
567
+ }
568
+ //#endregion
569
+ export { clearClaimChild as a, isClaimUnavailable as c, readClaimSet as d, releaseClaim as f, tryAcquireClaim as h, claimRemoveCommand as i, markClaimChildPending as l, settleClaim as m, ClaimRefusedError as n, clearFreeClaimSet as o, setClaimChild as p, claimFailure as r, isClaimRefusal as s, CLAIM_PATH_NOT_A_DIRECTORY as t, processGroupAlive as u };
@@ -1,7 +1,8 @@
1
- import { s as findEnclosingWorktreeRoot, u as getProject } from "./config-7kmtuhO2.mjs";
1
+ import { c as findEnclosingWorktreeRoot, f as getProject } from "./config-CMtIaTk9.mjs";
2
+ import { g as repoRoot } from "./worktree-C79xx_kM.mjs";
2
3
  import { existsSync, readFileSync, readdirSync, realpathSync } from "fs";
4
+ import { basename, dirname, join, resolve } from "path";
3
5
  import { createRequire } from "module";
4
- import { dirname, join, resolve } from "path";
5
6
  //#region src/project.ts
6
7
  const requireFromHere = createRequire(import.meta.url);
7
8
  function resolvePackageJson(projectRoot, name) {
@@ -29,6 +30,14 @@ function projectShortcut(path, proj) {
29
30
  if (rootPath && rootPath !== path) return `${projectShortcut(rootPath, getProject(rootPath))}/${path.split("/").pop() || path}`;
30
31
  return path.split("/").pop() || path;
31
32
  }
33
+ function ownedDeviceLabel(projectPath) {
34
+ const app = realpathSync(projectPath);
35
+ const root = repoRoot(app);
36
+ const appLabel = basename(app);
37
+ if (!root) return appLabel;
38
+ const worktreeLabel = basename(realpathSync(root));
39
+ return worktreeLabel === appLabel ? appLabel : `${worktreeLabel}-${appLabel}`;
40
+ }
32
41
  function findProjectRoot(startDir) {
33
42
  let dir;
34
43
  try {
@@ -231,4 +240,4 @@ function detectAndroidPackageFromGradle(projectRoot) {
231
240
  return null;
232
241
  }
233
242
  //#endregion
234
- export { findProjectRoot as a, resolvePackageJson as c, detectIsExpo as i, detectAndroidPackage as n, isPackageResolvable as o, detectBundleId as r, projectShortcut as s, appProjectProblem as t };
243
+ export { findProjectRoot as a, projectShortcut as c, detectIsExpo as i, resolvePackageJson as l, detectAndroidPackage as n, isPackageResolvable as o, detectBundleId as r, ownedDeviceLabel as s, appProjectProblem as t };