stim 1.2.0 → 1.3.1

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 (59) hide show
  1. package/README.md +34 -3
  2. package/dist/{android-Bcmev2s2.mjs → android-CT-PkXUh.mjs} +157 -62
  3. package/dist/{android-cas-D6HPZ6UQ.mjs → android-cas-DXorPCi_.mjs} +5 -5
  4. package/dist/android-hMj83VpN.mjs +1374 -0
  5. package/dist/{app-install-NFxUahnH.mjs → app-install-DIp9riNx.mjs} +6 -6
  6. package/dist/{build-slots-D0b6sJyo.mjs → build-slots-C4AouyNz.mjs} +2 -2
  7. package/dist/{cache-manifest-Cu4_Znw9.mjs → cache-manifest-CXR-Y8pH.mjs} +1 -1
  8. package/dist/cache-manifest.mjs +1 -1
  9. package/dist/cli.mjs +13 -13
  10. package/dist/collector-run.d.mts +3 -1
  11. package/dist/collector-run.mjs +33 -8
  12. package/dist/{config-CMtIaTk9.mjs → config-CcefMY2Q.mjs} +93 -12
  13. package/dist/{deps-BLBsTNDS.mjs → deps-0Yt0Inly.mjs} +1 -1
  14. package/dist/{dev-client-RpvXxCXV.mjs → dev-client-VfSZNLT4.mjs} +186 -9
  15. package/dist/{device-CoCPmazS.mjs → device-Bbv45Z9Z.mjs} +19 -11
  16. package/dist/{device-lease-Dpik41RY.mjs → device-lease-CJwK7OZr.mjs} +107 -83
  17. package/dist/{device-pool-B5pEVa3t.mjs → device-pool-CcBNTvM7.mjs} +18 -12
  18. package/dist/{device-remote-E81KAdXf.mjs → device-remote-DAA0Zm6Z.mjs} +7 -7
  19. package/dist/{doctor-CqPeMsSm.mjs → doctor-B4EITgQB.mjs} +11 -12
  20. package/dist/{doctor-D_CX7ib5.mjs → doctor-BueDfLYG.mjs} +4 -4
  21. package/dist/{error-diagnostics-Dw8Gn3yw.mjs → error-diagnostics-DwhNBFDU.mjs} +62 -52
  22. package/dist/{gc-B7KkDinF.mjs → gc-BRu62v4o.mjs} +66 -57
  23. package/dist/{guide-BS2Xc24o.mjs → guide-D95E0u3i.mjs} +307 -74
  24. package/dist/{ios-B7KLgIp5.mjs → ios-DAju3TVy.mjs} +183 -54
  25. package/dist/{ios-D1h1Di39.mjs → ios-Deh5js5V.mjs} +78 -40
  26. package/dist/{ios-device-xNt0Lg6v.mjs → ios-device-CWOM_S44.mjs} +1 -1
  27. package/dist/{logs-DcX9AvRF.mjs → logs-CAdwW6c2.mjs} +34 -6
  28. package/dist/{logs-query-oqn47Ffx.mjs → logs-query-Cdp5aaCH.mjs} +23 -6
  29. package/dist/{metro-COslRK_F.mjs → metro-CORqXyTL.mjs} +2 -2
  30. package/dist/{ndjson-DcAtEx_K.mjs → ndjson-BZwI1NBv.mjs} +5 -2
  31. package/dist/{ownership-CGPart6T.mjs → ownership-8O073GIC.mjs} +3 -3
  32. package/dist/{ownership-CYOyqxUo.mjs → ownership-DXcol4oU.mjs} +3 -3
  33. package/dist/{ownership-claim-7zRxxTfZ.mjs → ownership-claim-BtPtJVmq.mjs} +38 -23
  34. package/dist/{project-BBTVABUz.mjs → project-fKmuIaMI.mjs} +2 -2
  35. package/dist/{reclaim-C5Jmuahx.mjs → reclaim-CUmwucWG.mjs} +80 -74
  36. package/dist/{reload-Z8nKifAk.mjs → reload-CWpXoOgN.mjs} +32 -24
  37. package/dist/{server-bare-DEk37Mz2.mjs → server-bare-DEzPZLvf.mjs} +2 -2
  38. package/dist/server-expo-44em5Pmg.mjs +2 -0
  39. package/dist/{server-expo-C3ktAwUG.mjs → server-expo-DizT4OMk.mjs} +4 -4
  40. package/dist/{settings-DwU1_U_q.mjs → settings-DEJf6_kJ.mjs} +2 -2
  41. package/dist/slot-launch-CZklomj6.mjs +19 -0
  42. package/dist/{start-BrY4EuIg.mjs → start-B5G4NjiA.mjs} +11 -11
  43. package/dist/{state-CZ-ER-8d.mjs → state-B3XA2wp2.mjs} +9 -9
  44. package/dist/{stats-BCWEVLrM.mjs → stats-BR-kwj0B.mjs} +1 -1
  45. package/dist/{stats-BsF15Bti.mjs → stats-CaaQr4yN.mjs} +3 -3
  46. package/dist/{status-DFI1Hfpg.mjs → status-CI9clRSb.mjs} +55 -24
  47. package/dist/{stop-B7DR4awK.mjs → stop-CQhffpmX.mjs} +2 -2
  48. package/dist/{stop-Btg770vV.mjs → stop-CjlgDk0a.mjs} +93 -42
  49. package/dist/supervisor-run.d.mts +1 -2
  50. package/dist/supervisor-run.mjs +5 -5
  51. package/dist/{remote-cache-DDmJ1Roq.mjs → teardown--gESs9ck.mjs} +985 -887
  52. package/dist/{workspace-process-lock-BKfdys1q.mjs → workspace-process-lock-IBj1Towr.mjs} +1 -1
  53. package/dist/{worktree-BPTbI-LN.mjs → worktree-DtQGO9-s.mjs} +121 -111
  54. package/dist/{worktree-C79xx_kM.mjs → worktree-Eq6TLH8N.mjs} +1 -1
  55. package/dist/{xcode-DeKCribl.mjs → xcode-xnxyvJkD.mjs} +159 -96
  56. package/package.json +5 -5
  57. package/dist/android-K63_0UfF.mjs +0 -727
  58. package/dist/build-lock-BsVoLDaq.mjs +0 -645
  59. package/dist/server-expo-Drd7ksHu.mjs +0 -2
@@ -1,9 +1,10 @@
1
- import { a as findProjectRoot } from "./project-BBTVABUz.mjs";
1
+ import { V as validateDeviceSlot } from "./config-CcefMY2Q.mjs";
2
+ import { a as findProjectRoot } from "./project-fKmuIaMI.mjs";
2
3
  import { r as clockTime } from "./command-output-8wHMEde0.mjs";
3
- import { f as takeLease, l as releaseWorkspaceLeases, n as fileLeaseIo, o as parseLeaseDuration } from "./device-lease-Dpik41RY.mjs";
4
- import { d as waitForDevice, s as parseDeviceWait, t as selectFromPool } from "./device-pool-B5pEVa3t.mjs";
5
- import { C as memoizeEmulatorProbe, P as resolvePhysicalDevice, i as androidPoolNoCandidatesRefusal, j as probeEmulatorSerial, k as physicalDeviceModel, r as androidPoolCandidates, y as listAdbDevices } from "./android-K63_0UfF.mjs";
6
- import { c as listIosDevices, o as iosPoolCandidates, s as iosPoolNoCandidatesRefusal, u as resolveIosPhysicalDevice } from "./ios-device-xNt0Lg6v.mjs";
4
+ import { C as memoizeEmulatorProbe, P as resolvePhysicalDevice, i as androidPoolNoCandidatesRefusal, j as probeEmulatorSerial, k as physicalDeviceModel, r as androidPoolCandidates, y as listAdbDevices } from "./android-hMj83VpN.mjs";
5
+ import { f as takeLease, l as releaseWorkspaceLeases, n as fileLeaseIo, o as parseLeaseDuration } from "./device-lease-CJwK7OZr.mjs";
6
+ import { d as waitForDevice, s as parseDeviceWait, t as selectFromPool } from "./device-pool-CcBNTvM7.mjs";
7
+ import { c as listIosDevices, o as iosPoolCandidates, s as iosPoolNoCandidatesRefusal, u as resolveIosPhysicalDevice } from "./ios-device-CWOM_S44.mjs";
7
8
  import chalk from "chalk";
8
9
  //#region src/commands/device.ts
9
10
  const DEFAULT_FOR = "5m";
@@ -24,7 +25,7 @@ const DEFAULT_DEPS = {
24
25
  note: (line) => console.error(line)
25
26
  };
26
27
  function grantLine(facts, durationText) {
27
- return `locked ${facts.deviceName ? `${facts.deviceName} (${facts.id})` : facts.id} for ${facts.holder} until ${clockTime(facts.expiresAt)} (${durationText}). Renew: stim device lock ${facts.platform} --for ${durationText}. Release: stim device unlock.`;
28
+ return `locked ${facts.deviceName ? `${facts.deviceName} (${facts.id})` : facts.id} for ${facts.holder} until ${clockTime(facts.expiresAt)} (${durationText}). Renew: stim device lock ${facts.platform}${facts.slot ? ` --slot ${facts.slot}` : ""} --for ${durationText}. Release: stim device unlock${facts.slot ? ` --slot ${facts.slot}` : ""}.`;
28
29
  }
29
30
  function releasedLine(lease) {
30
31
  return `unlocked ${lease.deviceName ? `${lease.deviceName} (${lease.id})` : lease.id} -- the ${lease.platform} lease ran until ${clockTime(lease.expiresAt)}.`;
@@ -59,9 +60,10 @@ function resolveDevice(platform, requested, d) {
59
60
  function isFailure(value) {
60
61
  return "code" in value;
61
62
  }
62
- async function poolDevice(platform, idLabel, waitSeconds, deadline, root, d) {
63
+ async function poolDevice(platform, idLabel, waitSeconds, deadline, root, d, slot) {
63
64
  const isEmulator = memoizeEmulatorProbe(d.probeEmulatorSerial);
64
65
  const pooled = await d.selectFromPool({
66
+ ...slot ? { slot } : {},
65
67
  root,
66
68
  platform,
67
69
  idLabel,
@@ -140,10 +142,11 @@ async function runLock(platformArg, idArg, opts = {}, overrides = {}) {
140
142
  const idLabel = platform === "ios" ? "udid" : "serial";
141
143
  const deadline = d.now() + wait.seconds * 1e3;
142
144
  const lastLine = { at: null };
143
- const device = idArg ? resolveDevice(platform, idArg, d) : await poolDevice(platform, idLabel, wait.seconds, deadline, root, d);
145
+ const device = idArg ? resolveDevice(platform, idArg, d) : await poolDevice(platform, idLabel, wait.seconds, deadline, root, d, opts.slot);
144
146
  if (isFailure(device)) return report(device);
145
147
  for (;;) {
146
148
  const outcome = await d.waitForDevice({
149
+ ...opts.slot ? { slot: opts.slot } : {},
147
150
  root,
148
151
  platform,
149
152
  id: device.id,
@@ -161,6 +164,7 @@ async function runLock(platformArg, idArg, opts = {}, overrides = {}) {
161
164
  lease: outcome.refusal.lease
162
165
  });
163
166
  const taken = d.takeLease({
167
+ ...opts.slot ? { slot: opts.slot } : {},
164
168
  root,
165
169
  platform,
166
170
  id: device.id,
@@ -182,6 +186,7 @@ async function runLock(platformArg, idArg, opts = {}, overrides = {}) {
182
186
  });
183
187
  if (taken.status === "held") continue;
184
188
  const facts = {
189
+ ...taken.lease.slot ? { slot: taken.lease.slot } : {},
185
190
  platform,
186
191
  id: taken.lease.id,
187
192
  deviceName: taken.lease.deviceName,
@@ -224,7 +229,10 @@ async function runUnlock(platformArg, opts = {}, overrides = {}) {
224
229
  message: "Not in a React Native project (no package.json found).",
225
230
  remedy: "Run this from the app directory -- the one holding package.json."
226
231
  });
227
- const released = d.releaseLeases(root, { platform }, d.io);
232
+ const released = d.releaseLeases(root, {
233
+ platform,
234
+ ...opts.slot ? { slot: opts.slot } : {}
235
+ }, d.io);
228
236
  if (released.length === 0) d.note(chalk.dim(`No ${platform ? `${platform} ` : ""}device lease to release for ${root}. \`stim status\` lists every lease on this machine.`));
229
237
  if (json) d.out(JSON.stringify(released.map((lease) => ({
230
238
  ...lease,
@@ -235,10 +243,10 @@ async function runUnlock(platformArg, opts = {}, overrides = {}) {
235
243
  }
236
244
  function registerDevice(program, deps = {}) {
237
245
  const device = program.command("device").description("Lease a connected physical device to this workspace, and give it back");
238
- device.command("lock").argument("<platform>", "ios or android").argument("[id]", "the UDID or serial to lease; without one, the first free connected device is used").description("Lease a connected physical device to this workspace for a declared time, so another workspace's `--device` run waits instead of installing over it. Nothing but this command and a `--device` run moves the expiry.").option("--for <duration>", `How long to hold it: a whole number of seconds or minutes from 10s to 30m (default ${DEFAULT_FOR})`).option("--wait <seconds>", "How long to wait for a device another workspace holds, before refusing with STIM_DEVICE_BUSY (default 60, 0 refuses at once)").option("--json", "print the lease as JSON on stdout").action(async (platform, id, opts) => {
246
+ device.command("lock").argument("<platform>", "ios or android").argument("[id]", "the UDID or serial to lease; without one, the first free connected device is used").description("Lease a connected physical device to this workspace for a declared time, so another workspace's `--device` run waits instead of installing over it. Nothing but this command and a `--device` run moves the expiry.").option("--for <duration>", `How long to hold it: a whole number of seconds or minutes from 10s to 30m (default ${DEFAULT_FOR})`).option("--wait <seconds>", "How long to wait for a device another workspace holds, before refusing with STIM_DEVICE_BUSY (default 60, 0 refuses at once)").option("--slot <name>", "Use this workspace device slot", validateDeviceSlot).option("--json", "print the lease as JSON on stdout").action(async (platform, id, opts) => {
239
247
  if ("code" in await runLock(platform, id, opts, deps)) process.exit(1);
240
248
  });
241
- device.command("unlock").argument("[platform]", "ios or android; without one, every lease this workspace holds is released").description("Release the device lease or leases this workspace holds. Releasing nothing is not an error.").option("--json", "print the released leases as JSON on stdout").action(async (platform, opts) => {
249
+ device.command("unlock").argument("[platform]", "ios or android; without one, every lease this workspace holds is released").description("Release the device lease or leases this workspace holds. Releasing nothing is not an error.").option("--slot <name>", "Use this workspace device slot", validateDeviceSlot).option("--json", "print the released leases as JSON on stdout").action(async (platform, opts) => {
242
250
  const result = await runUnlock(platform, opts, deps);
243
251
  if (!Array.isArray(result)) process.exit(1);
244
252
  });
@@ -1,6 +1,6 @@
1
- import { k as withDirLock, u as getConfigDir } from "./config-CMtIaTk9.mjs";
2
- import { _ as writeWorkspaceState, o as clearWorkspaceStateKeys, p as readWorkspaceState } from "./state-CZ-ER-8d.mjs";
3
- import { a as segment } from "./build-lock-BsVoLDaq.mjs";
1
+ import { I as deviceSlotKey, R as parseDeviceSlotKey, V as validateDeviceSlot, k as withDirLock, u as getConfigDir } from "./config-CcefMY2Q.mjs";
2
+ import { _ as writeWorkspaceState, m as withWorkspaceStateLock, o as clearWorkspaceStateKeys, p as readWorkspaceState } from "./state-B3XA2wp2.mjs";
3
+ import { H as segment } from "./android-hMj83VpN.mjs";
4
4
  import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { randomBytes } from "node:crypto";
6
6
  import { join } from "node:path";
@@ -21,6 +21,7 @@ function deviceLeaseLockPath(platform, id) {
21
21
  return join(deviceLocksDir(), `${leaseStem(platform, id)}${LOCK_SUFFIX}`);
22
22
  }
23
23
  const fileLeaseIo = {
24
+ withHolderLock: withWorkspaceStateLock,
24
25
  now: () => Date.now(),
25
26
  readLease(path) {
26
27
  try {
@@ -83,8 +84,17 @@ function parseLease(raw) {
83
84
  const token = text(entry.token);
84
85
  const expiresAt = text(entry.expiresAt);
85
86
  if (!id || !holder || !token || !expiresAt || !Number.isFinite(Date.parse(expiresAt))) return null;
87
+ if (entry.slot !== void 0) {
88
+ if (typeof entry.slot !== "string") return null;
89
+ try {
90
+ validateDeviceSlot(entry.slot);
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
86
95
  return {
87
96
  version: 1,
97
+ ...typeof entry.slot === "string" ? { slot: entry.slot } : {},
88
98
  platform: entry.platform,
89
99
  id,
90
100
  deviceName: text(entry.deviceName),
@@ -98,7 +108,7 @@ function parseWorkspaceLeases(value) {
98
108
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
99
109
  const leases = {};
100
110
  for (const [platform, entry] of Object.entries(value)) {
101
- if (!isPlatform(platform) || !entry || typeof entry !== "object") continue;
111
+ if (!parseDeviceSlotKey(platform) || !entry || typeof entry !== "object") continue;
102
112
  const record = entry;
103
113
  const id = text(record.id);
104
114
  const token = text(record.token);
@@ -145,70 +155,74 @@ function writeLease(lease, io) {
145
155
  io.writeLease(deviceLeasePath(lease.platform, lease.id), `${JSON.stringify(lease, null, 2)}\n`);
146
156
  return lease;
147
157
  }
148
- function takeLease({ root, platform, id, deviceName = null, kind, durationMs = DEFAULT_LEASE_MS }, io = fileLeaseIo) {
149
- const previous = io.readHolder(root)[platform];
150
- if (previous && previous.id !== id) releaseHeld(root, platform, previous, io);
151
- for (const entry of listLeaseFiles(io)) {
152
- const other = entry.lease;
153
- if (!other || other.platform !== platform || other.holder !== root || other.id === id) continue;
154
- releaseByRoot(root, platform, other.id, io);
155
- }
156
- const declared = previous?.kind === "declared" && previous.id === id;
157
- const path = deviceLeasePath(platform, id);
158
- const result = io.withLeaseLock(deviceLeaseLockPath(platform, id), () => {
159
- const now = io.now();
160
- const raw = io.readLease(path);
161
- const current = parseLease(raw);
162
- if (raw !== null && !current) return {
163
- status: "unreadable",
164
- path
165
- };
166
- const asked = now + durationMs;
167
- if (current && !leaseIsExpired(current, now)) {
168
- if (current.holder !== root) return {
169
- status: "held",
170
- lease: current
158
+ function takeLease({ root, platform, slot = "default", id, deviceName = null, kind, durationMs = DEFAULT_LEASE_MS }, io = fileLeaseIo) {
159
+ return withHolderLock(io, root, () => {
160
+ const key = deviceSlotKey(platform, slot);
161
+ const previous = io.readHolder(root)[key];
162
+ if (previous && previous.id !== id) releaseHeld(root, platform, previous, io, slot);
163
+ for (const entry of listLeaseFiles(io)) {
164
+ const other = entry.lease;
165
+ if (!other || other.platform !== platform || other.holder !== root || (other.slot ?? "default") !== slot || other.id === id) continue;
166
+ releaseByRoot(root, platform, other.id, io);
167
+ }
168
+ const declared = previous?.kind === "declared" && previous.id === id;
169
+ const path = deviceLeasePath(platform, id);
170
+ const result = io.withLeaseLock(deviceLeaseLockPath(platform, id), () => {
171
+ const now = io.now();
172
+ const raw = io.readLease(path);
173
+ const current = parseLease(raw);
174
+ if (raw !== null && !current) return {
175
+ status: "unreadable",
176
+ path
171
177
  };
172
- const floor = declared && kind === "run" ? Math.max(asked, Date.parse(current.expiresAt)) : asked;
178
+ const asked = now + durationMs;
179
+ if (current && !leaseIsExpired(current, now)) {
180
+ if (current.holder !== root || (current.slot ?? "default") !== slot) return {
181
+ status: "held",
182
+ lease: current
183
+ };
184
+ const floor = declared && kind === "run" ? Math.max(asked, Date.parse(current.expiresAt)) : asked;
185
+ return {
186
+ status: "set",
187
+ lease: writeLease({
188
+ ...current,
189
+ deviceName: deviceName ?? current.deviceName,
190
+ expiresAt: new Date(floor).toISOString()
191
+ }, io)
192
+ };
193
+ }
194
+ const expiresAt = new Date(asked).toISOString();
173
195
  return {
174
- status: "set",
196
+ status: "taken",
175
197
  lease: writeLease({
176
- ...current,
177
- deviceName: deviceName ?? current.deviceName,
178
- expiresAt: new Date(floor).toISOString()
198
+ version: 1,
199
+ ...slot === "default" ? {} : { slot },
200
+ platform,
201
+ id,
202
+ deviceName,
203
+ holder: root,
204
+ token: randomBytes(12).toString("hex"),
205
+ grantedAt: new Date(now).toISOString(),
206
+ expiresAt
179
207
  }, io)
180
208
  };
209
+ });
210
+ if (result.status === "taken" || result.status === "set") {
211
+ const recorded = result.status === "set" && declared ? "declared" : kind;
212
+ io.writeHolder(root, {
213
+ ...io.readHolder(root),
214
+ [key]: {
215
+ id,
216
+ token: result.lease.token,
217
+ kind: recorded
218
+ }
219
+ });
181
220
  }
182
- const expiresAt = new Date(asked).toISOString();
183
- return {
184
- status: "taken",
185
- lease: writeLease({
186
- version: 1,
187
- platform,
188
- id,
189
- deviceName,
190
- holder: root,
191
- token: randomBytes(12).toString("hex"),
192
- grantedAt: new Date(now).toISOString(),
193
- expiresAt
194
- }, io)
195
- };
221
+ return result;
196
222
  });
197
- if (result.status === "taken" || result.status === "set") {
198
- const recorded = result.status === "set" && declared ? "declared" : kind;
199
- io.writeHolder(root, {
200
- ...io.readHolder(root),
201
- [platform]: {
202
- id,
203
- token: result.lease.token,
204
- kind: recorded
205
- }
206
- });
207
- }
208
- return result;
209
223
  }
210
- function raiseLease({ root, platform, minMs }, io = fileLeaseIo) {
211
- const record = io.readHolder(root)[platform];
224
+ function raiseLease({ root, platform, slot = "default", minMs }, io = fileLeaseIo) {
225
+ const record = io.readHolder(root)[deviceSlotKey(platform, slot)];
212
226
  if (!record) return { status: "none" };
213
227
  const path = deviceLeasePath(platform, record.id);
214
228
  return io.withLeaseLock(deviceLeaseLockPath(platform, record.id), () => {
@@ -239,7 +253,8 @@ function summarize(lease) {
239
253
  expiresAt: lease.expiresAt
240
254
  };
241
255
  }
242
- function releaseHeld(root, platform, record, io) {
256
+ function releaseHeld(root, platform, record, io, slot = "default") {
257
+ const key = deviceSlotKey(platform, slot);
243
258
  const path = deviceLeasePath(platform, record.id);
244
259
  const released = io.withLeaseLock(deviceLeaseLockPath(platform, record.id), () => {
245
260
  const current = parseLease(io.readLease(path));
@@ -248,8 +263,8 @@ function releaseHeld(root, platform, record, io) {
248
263
  return current;
249
264
  });
250
265
  const held = io.readHolder(root);
251
- if (held[platform]?.token === record.token) {
252
- delete held[platform];
266
+ if (held[key]?.token === record.token) {
267
+ delete held[key];
253
268
  io.writeHolder(root, held);
254
269
  }
255
270
  return released;
@@ -263,27 +278,33 @@ function releaseByRoot(root, platform, id, io) {
263
278
  return current;
264
279
  });
265
280
  }
266
- function releaseRunLease({ root, platform }, io = fileLeaseIo) {
267
- const record = io.readHolder(root)[platform];
268
- if (!record || record.kind !== "run") return null;
269
- const released = releaseHeld(root, platform, record, io);
270
- return released ? summarize(released) : null;
281
+ function releaseRunLease({ root, platform, slot = "default" }, io = fileLeaseIo) {
282
+ return withHolderLock(io, root, () => {
283
+ const record = io.readHolder(root)[deviceSlotKey(platform, slot)];
284
+ if (!record || record.kind !== "run") return null;
285
+ const released = releaseHeld(root, platform, record, io, slot);
286
+ return released ? summarize(released) : null;
287
+ });
271
288
  }
272
- function releaseWorkspaceLeases(root, { platform = null } = {}, io = fileLeaseIo) {
273
- const released = [];
274
- for (const [name, record] of Object.entries(io.readHolder(root))) {
275
- if (!isPlatform(name) || platform && name !== platform) continue;
276
- const lease = releaseHeld(root, name, record, io);
277
- if (lease) released.push(summarize(lease));
278
- }
279
- for (const entry of listLeaseFiles(io)) {
280
- const lease = entry.lease;
281
- if (!lease || lease.holder !== root) continue;
282
- if (platform && lease.platform !== platform) continue;
283
- const byRoot = releaseByRoot(root, lease.platform, lease.id, io);
284
- if (byRoot) released.push(summarize(byRoot));
285
- }
286
- return released;
289
+ function releaseWorkspaceLeases(root, { platform = null, slot = null } = {}, io = fileLeaseIo) {
290
+ return withHolderLock(io, root, () => {
291
+ const released = [];
292
+ for (const [name, record] of Object.entries(io.readHolder(root))) {
293
+ const parsed = parseDeviceSlotKey(name);
294
+ if (!parsed || platform && parsed.platform !== platform || slot !== null && parsed.slot !== slot) continue;
295
+ const lease = releaseHeld(root, parsed.platform, record, io, parsed.slot);
296
+ if (lease) released.push(summarize(lease));
297
+ }
298
+ for (const entry of listLeaseFiles(io)) {
299
+ const lease = entry.lease;
300
+ if (!lease || lease.holder !== root) continue;
301
+ if (platform && lease.platform !== platform) continue;
302
+ if (slot !== null && (lease.slot ?? "default") !== slot) continue;
303
+ const byRoot = releaseByRoot(root, lease.platform, lease.id, io);
304
+ if (byRoot) released.push(summarize(byRoot));
305
+ }
306
+ return released;
307
+ });
287
308
  }
288
309
  function removeExpiredLease(entry, io = fileLeaseIo) {
289
310
  const lease = entry.lease;
@@ -331,5 +352,8 @@ function selectPoolDevice({ candidates, leases, held = null, now }) {
331
352
  holders
332
353
  };
333
354
  }
355
+ function withHolderLock(io, root, fn) {
356
+ return io.withHolderLock ? io.withHolderLock(root, fn) : fn();
357
+ }
334
358
  //#endregion
335
359
  export { parseLease as a, releaseRunLease as c, selectPoolDevice as d, takeLease as f, listLeaseFiles as i, releaseWorkspaceLeases as l, fileLeaseIo as n, parseLeaseDuration as o, leaseIsExpired as r, raiseLease as s, deviceLeasePath as t, removeExpiredLease as u };
@@ -1,6 +1,7 @@
1
+ import { I as deviceSlotKey } from "./config-CcefMY2Q.mjs";
1
2
  import { a as formatElapsed, r as clockTime } from "./command-output-8wHMEde0.mjs";
2
- import { A as APP_READINESS_TIMEOUT_MS, l as VERIFY_TIMEOUT_MS } from "./app-install-NFxUahnH.mjs";
3
- import { a as parseLease, c as releaseRunLease, d as selectPoolDevice, f as takeLease, n as fileLeaseIo, r as leaseIsExpired, s as raiseLease, t as deviceLeasePath } from "./device-lease-Dpik41RY.mjs";
3
+ import { A as APP_READINESS_TIMEOUT_MS, l as VERIFY_TIMEOUT_MS } from "./app-install-DIp9riNx.mjs";
4
+ import { a as parseLease, c as releaseRunLease, d as selectPoolDevice, f as takeLease, n as fileLeaseIo, r as leaseIsExpired, s as raiseLease, t as deviceLeasePath } from "./device-lease-CJwK7OZr.mjs";
4
5
  //#region src/engine/device-lease-run.ts
5
6
  const LEASE_STEP_FLOOR_MS = 6e4;
6
7
  const DEBUG_VERIFY_STEP_MS = VERIFY_TIMEOUT_MS + APP_READINESS_TIMEOUT_MS;
@@ -101,7 +102,7 @@ function lostRefusal(holder, expiresAt, now) {
101
102
  lease: null
102
103
  };
103
104
  }
104
- function runLease({ root, platform, kind, expiresAt, io = fileLeaseIo }) {
105
+ function runLease({ root, platform, slot = "default", kind, expiresAt, io = fileLeaseIo }) {
105
106
  const handle = {
106
107
  kind,
107
108
  expiresAt,
@@ -115,6 +116,7 @@ function runLease({ root, platform, kind, expiresAt, io = fileLeaseIo }) {
115
116
  const result = raiseLease({
116
117
  root,
117
118
  platform,
119
+ slot,
118
120
  minMs: leaseStepMs(boundMs)
119
121
  }, io);
120
122
  if (result.status === "raised") {
@@ -137,7 +139,8 @@ function runLease({ root, platform, kind, expiresAt, io = fileLeaseIo }) {
137
139
  if (handle.kind === null) return;
138
140
  releaseRunLease({
139
141
  root,
140
- platform
142
+ platform,
143
+ slot
141
144
  }, io);
142
145
  },
143
146
  facts() {
@@ -173,7 +176,7 @@ function releaseLeaseOnSignal(release, { on = (signal, handler) => {
173
176
  installed.length = 0;
174
177
  };
175
178
  }
176
- async function waitForDevice({ root, platform, id, idLabel, waitSeconds, noWait = false, appId = null, holderAppId = () => null, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), warn = () => {}, io = fileLeaseIo, deadline, lastLine }) {
179
+ async function waitForDevice({ root, platform, slot = "default", id, idLabel, waitSeconds, noWait = false, appId = null, holderAppId = () => null, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), warn = () => {}, io = fileLeaseIo, deadline, lastLine }) {
177
180
  const path = deviceLeasePath(platform, id);
178
181
  for (;;) {
179
182
  const raw = io.readLease(path);
@@ -183,7 +186,7 @@ async function waitForDevice({ root, platform, id, idLabel, waitSeconds, noWait
183
186
  refusal: unreadableRefusal(path)
184
187
  };
185
188
  if (!current || leaseIsExpired(current, now())) return { status: "free" };
186
- if (current.holder === root) return {
189
+ if (current.holder === root && (current.slot ?? "default") === slot) return {
187
190
  status: "ours",
188
191
  lease: current
189
192
  };
@@ -210,8 +213,8 @@ async function waitForDevice({ root, platform, id, idLabel, waitSeconds, noWait
210
213
  await sleep(DEVICE_WAIT_POLL_MS);
211
214
  }
212
215
  }
213
- async function acquireRunLease({ root, platform, id, deviceName = null, idLabel, waitSeconds, noWait, installBoundMs, appId = null, holderAppId = () => null, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), warn = () => {}, io = fileLeaseIo }) {
214
- const recorded = io.readHolder(root)[platform];
216
+ async function acquireRunLease({ root, platform, slot = "default", id, deviceName = null, idLabel, waitSeconds, noWait, installBoundMs, appId = null, holderAppId = () => null, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), warn = () => {}, io = fileLeaseIo }) {
217
+ const recorded = io.readHolder(root)[deviceSlotKey(platform, slot)];
215
218
  let held = recorded;
216
219
  if (recorded && recorded.id !== id) {
217
220
  const other = parseLease(io.readLease(deviceLeasePath(platform, recorded.id)));
@@ -234,6 +237,7 @@ async function acquireRunLease({ root, platform, id, deviceName = null, idLabel,
234
237
  const raised = raiseLease({
235
238
  root,
236
239
  platform,
240
+ slot,
237
241
  minMs: leaseStepMs(installBoundMs)
238
242
  }, io);
239
243
  if (raised.status === "raised") return {
@@ -245,6 +249,7 @@ async function acquireRunLease({ root, platform, id, deviceName = null, idLabel,
245
249
  const outcome = await waitForDevice({
246
250
  root,
247
251
  platform,
252
+ slot,
248
253
  id,
249
254
  idLabel,
250
255
  waitSeconds,
@@ -267,6 +272,7 @@ async function acquireRunLease({ root, platform, id, deviceName = null, idLabel,
267
272
  const taken = takeLease({
268
273
  root,
269
274
  platform,
275
+ slot,
270
276
  id,
271
277
  deviceName,
272
278
  kind: "run",
@@ -285,8 +291,8 @@ async function acquireRunLease({ root, platform, id, deviceName = null, idLabel,
285
291
  }
286
292
  //#endregion
287
293
  //#region src/engine/device-pool.ts
288
- function heldPoolId(root, platform, now, io = fileLeaseIo) {
289
- const record = io.readHolder(root)[platform];
294
+ function heldPoolId(root, platform, now, io = fileLeaseIo, slot = "default") {
295
+ const record = io.readHolder(root)[deviceSlotKey(platform, slot)];
290
296
  if (!record) return null;
291
297
  const lease = parseLease(io.readLease(deviceLeasePath(platform, record.id)));
292
298
  if (!lease || lease.token !== record.token || leaseIsExpired(lease, now)) return null;
@@ -352,8 +358,8 @@ function poolBusyRefusal(holders, leases, unreadable, now, waitSeconds) {
352
358
  }
353
359
  };
354
360
  }
355
- async function selectFromPool({ root, platform, idLabel, list, noCandidates, waitSeconds, deadline, noWait = false, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), warn = () => {}, io = fileLeaseIo }) {
356
- const held = heldPoolId(root, platform, now(), io);
361
+ async function selectFromPool({ root, platform, slot = "default", idLabel, list, noCandidates, waitSeconds, deadline, noWait = false, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), warn = () => {}, io = fileLeaseIo }) {
362
+ const held = heldPoolId(root, platform, now(), io, slot);
357
363
  const until = deadline ?? now() + waitSeconds * 1e3;
358
364
  let lastLine = null;
359
365
  for (;;) {
@@ -1,11 +1,11 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { k as withDirLock, u as getConfigDir } from "./config-CMtIaTk9.mjs";
3
- import { D as workspaceLogsDir, O as workspaceName, T as workspaceDir, a as clearRemoteSession, c as readMetroTunnel, k as workspaceStateFile, u as readRemoteSession } from "./state-CZ-ER-8d.mjs";
4
- import { r as pidExists } from "./metro-COslRK_F.mjs";
5
- import { z as planMetroReach } from "./settings-DwU1_U_q.mjs";
6
- import { T as readMetroRecords, a as LAUNCH_ERROR, b as isBundleProof, p as devClientDeepLink, r as INSTALL_ERROR } from "./app-install-NFxUahnH.mjs";
7
- import { S as sanitizeDeviceLabel } from "./ios-D1h1Di39.mjs";
8
- import { t as withWorkspaceProcessLock } from "./workspace-process-lock-BKfdys1q.mjs";
2
+ import { k as withDirLock, u as getConfigDir } from "./config-CcefMY2Q.mjs";
3
+ import { D as workspaceLogsDir, O as workspaceName, T as workspaceDir, a as clearRemoteSession, c as readMetroTunnel, k as workspaceStateFile, u as readRemoteSession } from "./state-B3XA2wp2.mjs";
4
+ import { r as pidExists } from "./metro-CORqXyTL.mjs";
5
+ import { z as planMetroReach } from "./settings-DEJf6_kJ.mjs";
6
+ import { t as withWorkspaceProcessLock } from "./workspace-process-lock-IBj1Towr.mjs";
7
+ import { T as readMetroRecords, a as LAUNCH_ERROR, b as isBundleProof, p as devClientDeepLink, r as INSTALL_ERROR } from "./app-install-DIp9riNx.mjs";
8
+ import { S as sanitizeDeviceLabel } from "./ios-Deh5js5V.mjs";
9
9
  import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
10
10
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
11
11
  import { homedir } from "node:os";
@@ -1,17 +1,16 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { g as loadConfig, l as getConcurrencyLimits, o as configCorruptRepair } from "./config-CMtIaTk9.mjs";
3
- import { b as sharedBuildCache, w as workspaceDerivedData } from "./state-CZ-ER-8d.mjs";
4
- import { S as makeTemporaryDirectory, a as dirtyFingerprintFiles, f as listWorktrees, g as repoRoot, s as gitCommonDir, w as temporaryRoot, x as filesystemDevice } from "./worktree-C79xx_kM.mjs";
5
- import { i as detectIsExpo, t as appProjectProblem } from "./project-BBTVABUz.mjs";
6
- import { d as resolveEasCliBin, i as checkEasAuth$1, l as ownerFromConfig, u as providerFromConfig, v as POOL_SETTING_REMEDY, x as parkedMaxSetting } from "./remote-cache-DDmJ1Roq.mjs";
7
- import { A as resolveSettings, E as remoteAndroidSetting, I as unknownSettingKeys, M as settingOrigin, N as settingShapeErrors, O as remoteIosSetting, P as settingsLayers, U as resolveOptimizations, V as compilerCacheFallbackMessage, a as SETTING_SHAPE_REMEDY, i as SETTINGS_WITH_RESOLVE_TIME_FALLBACK, n as PATH_SETTINGS, v as iosSimSlimProfileSetting, w as projectOptimizations, y as mergeSettingsLayers } from "./settings-DwU1_U_q.mjs";
2
+ import { g as loadConfig, l as getConcurrencyLimits, o as configCorruptRepair } from "./config-CcefMY2Q.mjs";
3
+ import { b as sharedBuildCache, w as workspaceDerivedData } from "./state-B3XA2wp2.mjs";
4
+ import { S as makeTemporaryDirectory, a as dirtyFingerprintFiles, f as listWorktrees, g as repoRoot, s as gitCommonDir, w as temporaryRoot, x as filesystemDevice } from "./worktree-Eq6TLH8N.mjs";
5
+ import { i as detectIsExpo, t as appProjectProblem } from "./project-fKmuIaMI.mjs";
8
6
  import { d as quotedPath, u as plural } from "./command-output-8wHMEde0.mjs";
9
- import { E as readHostMemoryPressure, T as hostMemoryPressureAdvice, u as listAllIosSims } from "./ios-D1h1Di39.mjs";
10
- import { m as fingerprintProject, r as listBuildLocks, u as diffFingerprintSources } from "./build-lock-BsVoLDaq.mjs";
11
- import { n as listBuildSlots } from "./build-slots-D0b6sJyo.mjs";
12
- import { y as listAdbDevices } from "./android-K63_0UfF.mjs";
13
- import { E as simslimIsOnPath, S as liveOwnedDeviceCount, a as ccacheEnabled, g as podInstallCommand } from "./xcode-DeKCribl.mjs";
14
- import { d as projectCmakeLauncher, i as apkOutputsDir, r as resolveAndroidCompilerCache, t as readAndroidCasToolchain } from "./android-cas-D6HPZ6UQ.mjs";
7
+ import { B as listBuildLocks, Z as fingerprintProject, q as diffFingerprintSources, y as listAdbDevices } from "./android-hMj83VpN.mjs";
8
+ import { A as resolveSettings, E as remoteAndroidSetting, I as unknownSettingKeys, M as settingOrigin, N as settingShapeErrors, O as remoteIosSetting, P as settingsLayers, U as resolveOptimizations, V as compilerCacheFallbackMessage, a as SETTING_SHAPE_REMEDY, i as SETTINGS_WITH_RESOLVE_TIME_FALLBACK, n as PATH_SETTINGS, v as iosSimSlimProfileSetting, w as projectOptimizations, y as mergeSettingsLayers } from "./settings-DEJf6_kJ.mjs";
9
+ import { a as POOL_SETTING_REMEDY, b as providerFromConfig, c as parkedMaxSetting, m as checkEasAuth$1, x as resolveEasCliBin, y as ownerFromConfig } from "./teardown--gESs9ck.mjs";
10
+ import { E as readHostMemoryPressure, T as hostMemoryPressureAdvice, u as listAllIosSims } from "./ios-Deh5js5V.mjs";
11
+ import { n as listBuildSlots } from "./build-slots-C4AouyNz.mjs";
12
+ import { C as liveOwnedDeviceCount, D as simslimIsOnPath, a as ccacheEnabled, g as podInstallCommand } from "./xcode-xnxyvJkD.mjs";
13
+ import { d as projectCmakeLauncher, i as apkOutputsDir, r as resolveAndroidCompilerCache, t as readAndroidCasToolchain } from "./android-cas-DXorPCi_.mjs";
15
14
  import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, rmSync } from "node:fs";
16
15
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
17
16
  import { existsSync as existsSync$1, lstatSync as lstatSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, rmSync as rmSync$1 } from "fs";
@@ -1,9 +1,9 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { g as repoRoot } from "./worktree-C79xx_kM.mjs";
3
- import { a as findProjectRoot } from "./project-BBTVABUz.mjs";
2
+ import { g as repoRoot } from "./worktree-Eq6TLH8N.mjs";
3
+ import { a as findProjectRoot } from "./project-fKmuIaMI.mjs";
4
4
  import { l as phaseLine } from "./command-output-8wHMEde0.mjs";
5
- import { s as detectXcodeMajor } from "./xcode-DeKCribl.mjs";
6
- import { c as repairCxxLauncherState, n as detectFingerprintParity, r as detectLinkedLibraryGitMetadata, s as runDoctor } from "./doctor-CqPeMsSm.mjs";
5
+ import { s as detectXcodeMajor } from "./xcode-xnxyvJkD.mjs";
6
+ import { c as repairCxxLauncherState, n as detectFingerprintParity, r as detectLinkedLibraryGitMetadata, s as runDoctor } from "./doctor-B4EITgQB.mjs";
7
7
  import { accessSync, constants, realpathSync, statSync } from "node:fs";
8
8
  import { InvalidArgumentError } from "commander";
9
9
  import { delimiter, join, resolve } from "node:path";