stim 1.2.0 → 1.3.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 (59) hide show
  1. package/README.md +30 -0
  2. package/dist/android-BqVIJoG7.mjs +1368 -0
  3. package/dist/{android-Bcmev2s2.mjs → android-CqcW1hrP.mjs} +148 -56
  4. package/dist/{android-cas-D6HPZ6UQ.mjs → android-cas-BR5YJBYv.mjs} +5 -5
  5. package/dist/{app-install-NFxUahnH.mjs → app-install-DIp9riNx.mjs} +6 -6
  6. package/dist/{build-slots-D0b6sJyo.mjs → build-slots-C3-R4XQr.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-Spz8ozgo.mjs} +1 -1
  14. package/dist/{dev-client-RpvXxCXV.mjs → dev-client-BjisHDB5.mjs} +186 -9
  15. package/dist/{device-CoCPmazS.mjs → device-BkiW-gkk.mjs} +19 -11
  16. package/dist/{device-lease-Dpik41RY.mjs → device-lease-BJYybF-2.mjs} +107 -83
  17. package/dist/{device-pool-B5pEVa3t.mjs → device-pool-CPbLGfrB.mjs} +18 -12
  18. package/dist/{device-remote-E81KAdXf.mjs → device-remote-AkYCKS8U.mjs} +7 -7
  19. package/dist/{doctor-CqPeMsSm.mjs → doctor-Bmv86UUW.mjs} +11 -12
  20. package/dist/{doctor-D_CX7ib5.mjs → doctor-fAMXC4vl.mjs} +4 -4
  21. package/dist/{error-diagnostics-Dw8Gn3yw.mjs → error-diagnostics-DHC31rXA.mjs} +62 -52
  22. package/dist/{gc-B7KkDinF.mjs → gc-GGu-8ULN.mjs} +66 -57
  23. package/dist/{guide-BS2Xc24o.mjs → guide-92CPA21s.mjs} +196 -13
  24. package/dist/{ios-B7KLgIp5.mjs → ios-BSXFeO5H.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-DPNF4C9s.mjs} +9 -6
  28. package/dist/{logs-query-oqn47Ffx.mjs → logs-query-R-Cqc6Qv.mjs} +22 -5
  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-BpCKiRlQ.mjs} +3 -3
  33. package/dist/{ownership-claim-7zRxxTfZ.mjs → ownership-claim-Bf2CAfw3.mjs} +4 -3
  34. package/dist/{project-BBTVABUz.mjs → project-fKmuIaMI.mjs} +2 -2
  35. package/dist/{reclaim-C5Jmuahx.mjs → reclaim-iW9Cad7k.mjs} +80 -74
  36. package/dist/{reload-Z8nKifAk.mjs → reload-DazHDSFA.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-cci0xvmC.mjs +19 -0
  42. package/dist/{start-BrY4EuIg.mjs → start-mObjQH2W.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-BxQNqu9t.mjs} +55 -24
  47. package/dist/{stop-B7DR4awK.mjs → stop-CrHGcjrV.mjs} +2 -2
  48. package/dist/{stop-Btg770vV.mjs → stop-DimGXEKY.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-ClFp1Aax.mjs} +887 -888
  52. package/dist/{workspace-process-lock-BKfdys1q.mjs → workspace-process-lock-3NcQnZ0X.mjs} +1 -1
  53. package/dist/{worktree-BPTbI-LN.mjs → worktree-DxTgN1CJ.mjs} +12 -12
  54. package/dist/{worktree-C79xx_kM.mjs → worktree-Eq6TLH8N.mjs} +1 -1
  55. package/dist/{xcode-DeKCribl.mjs → xcode-D7sAp-Zy.mjs} +82 -56
  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
package/dist/cli.mjs CHANGED
@@ -3,19 +3,19 @@ import { readFileSync } from "node:fs";
3
3
  import { Command } from "commander";
4
4
  //#region bin/cli.ts
5
5
  const commands = /* @__PURE__ */ new Map([
6
- ["doctor", () => import("./doctor-D_CX7ib5.mjs")],
7
- ["worktree", () => import("./worktree-BPTbI-LN.mjs")],
8
- ["start", () => import("./start-BrY4EuIg.mjs")],
9
- ["stop", () => import("./stop-B7DR4awK.mjs")],
10
- ["ios", () => import("./ios-B7KLgIp5.mjs")],
11
- ["android", () => import("./android-Bcmev2s2.mjs")],
12
- ["reload", () => import("./reload-Z8nKifAk.mjs")],
13
- ["device", () => import("./device-CoCPmazS.mjs")],
14
- ["logs", () => import("./logs-DcX9AvRF.mjs")],
15
- ["status", () => import("./status-DFI1Hfpg.mjs")],
16
- ["stats", () => import("./stats-BsF15Bti.mjs")],
17
- ["gc", () => import("./gc-B7KkDinF.mjs")],
18
- ["guide", () => import("./guide-BS2Xc24o.mjs")]
6
+ ["doctor", () => import("./doctor-fAMXC4vl.mjs")],
7
+ ["worktree", () => import("./worktree-DxTgN1CJ.mjs")],
8
+ ["start", () => import("./start-mObjQH2W.mjs")],
9
+ ["stop", () => import("./stop-CrHGcjrV.mjs")],
10
+ ["ios", () => import("./ios-BSXFeO5H.mjs")],
11
+ ["android", () => import("./android-CqcW1hrP.mjs")],
12
+ ["reload", () => import("./reload-DazHDSFA.mjs")],
13
+ ["device", () => import("./device-BkiW-gkk.mjs")],
14
+ ["logs", () => import("./logs-DPNF4C9s.mjs")],
15
+ ["status", () => import("./status-BxQNqu9t.mjs")],
16
+ ["stats", () => import("./stats-CaaQr4yN.mjs")],
17
+ ["gc", () => import("./gc-GGu-8ULN.mjs")],
18
+ ["guide", () => import("./guide-92CPA21s.mjs")]
19
19
  ]);
20
20
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
21
21
  const program = new Command();
@@ -55,6 +55,7 @@ declare function readCollectors(root: string): Record<string, unknown>;
55
55
  //#region src/collector/run.d.ts
56
56
  declare const PLATFORMS: string[];
57
57
  interface ParsedCollectorArgs {
58
+ slot?: string;
58
59
  platform?: string;
59
60
  root?: string;
60
61
  udid?: string;
@@ -69,6 +70,7 @@ interface ParsedCollectorArgs {
69
70
  }
70
71
  declare function parseArgs(argv: string[]): ParsedCollectorArgs;
71
72
  interface RunCollectorOptions {
73
+ slot?: string;
72
74
  platform: string;
73
75
  root: string;
74
76
  udid?: string | null;
@@ -114,7 +116,7 @@ interface RunCollectorHandle {
114
116
  startedAt: string;
115
117
  pid: number | null;
116
118
  }
117
- declare function runCollector({ platform, root, udid, appName, appExecutable, bundleId, serial, packageName, physical, payloadUrl, startStream, resolvePid, pidTimeoutMs, pidOf, pidWatchMs, watchPid, resolveClockOffset, now, onExit, attachSignals, stderr }: RunCollectorOptions): Promise<RunCollectorHandle | null>;
119
+ declare function runCollector({ slot, platform, root, udid, appName, appExecutable, bundleId, serial, packageName, physical, payloadUrl, startStream, resolvePid, pidTimeoutMs, pidOf, pidWatchMs, watchPid, resolveClockOffset, now, onExit, attachSignals, stderr }: RunCollectorOptions): Promise<RunCollectorHandle | null>;
118
120
  declare function main(argv?: string[]): Promise<void>;
119
121
  //#endregion
120
122
  export { PLATFORMS, ParsedCollectorArgs, RunCollectorHandle, RunCollectorOptions, main, parseArgs, readCollectors, registerCollector, runCollector, unregisterCollector };
@@ -1,10 +1,10 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { T as captureProcessToken } from "./config-CMtIaTk9.mjs";
3
- import { r as createNdjsonWriter } from "./ndjson-DcAtEx_K.mjs";
4
- import { D as workspaceLogsDir } from "./state-CZ-ER-8d.mjs";
2
+ import { I as deviceSlotKey, T as captureProcessToken, V as validateDeviceSlot } from "./config-CcefMY2Q.mjs";
3
+ import { r as createNdjsonWriter } from "./ndjson-BZwI1NBv.mjs";
4
+ import { D as workspaceLogsDir } from "./state-B3XA2wp2.mjs";
5
5
  import { t as createLineReader } from "./process-output-C7IlIzd7.mjs";
6
6
  import { i as startIosDeviceConsole, r as parseDeviceConsoleLine } from "./ios-device-D3pzTKQE.mjs";
7
- import { a as unregisterCollector, i as registerCollector, r as readCollectors, t as collectorProcessTitle } from "./ownership-CGPart6T.mjs";
7
+ import { a as unregisterCollector, i as registerCollector, r as readCollectors, t as collectorProcessTitle } from "./ownership-8O073GIC.mjs";
8
8
  import { a as watchAppPid, i as waitForAppPid, n as parseLogcatLine, r as startAndroidLogcat, t as androidClockOffset } from "./android-BW8i_YxU.mjs";
9
9
  import { existsSync } from "node:fs";
10
10
  import { isAbsolute, join, resolve } from "node:path";
@@ -193,6 +193,7 @@ function startIosLogStream({ udid, appName, executableName = null, spawnFn = nul
193
193
  //#region src/collector/run.ts
194
194
  const PLATFORMS = ["ios", "android"];
195
195
  function parseArgs(argv) {
196
+ let slot;
196
197
  let platform;
197
198
  let root;
198
199
  let udid;
@@ -205,6 +206,16 @@ function parseArgs(argv) {
205
206
  let payloadUrl = null;
206
207
  for (let i = 0; i < argv.length; i++) {
207
208
  const arg = argv[i];
209
+ if (arg === "--slot") {
210
+ slot = argv[++i];
211
+ if (!slot) return { error: "--slot needs a name." };
212
+ try {
213
+ validateDeviceSlot(slot);
214
+ } catch (error) {
215
+ return { error: String(error.message) };
216
+ }
217
+ continue;
218
+ }
208
219
  if (arg === "--platform") {
209
220
  platform = argv[++i];
210
221
  continue;
@@ -272,7 +283,8 @@ function parseArgs(argv) {
272
283
  serial,
273
284
  packageName,
274
285
  physical,
275
- payloadUrl
286
+ payloadUrl,
287
+ ...slot ? { slot } : {}
276
288
  };
277
289
  }
278
290
  const noopFlush = () => {};
@@ -281,7 +293,12 @@ function startMessage({ platform, physical, pid, appName, bundleId, udid, packag
281
293
  if (!physical) return `device log collector pid ${pid} streaming ${appName} on ${udid}`;
282
294
  return `device log collector pid ${pid} launching ${bundleId} on device ${udid} and streaming its console; subsystem, category and severity are not carried on hardware -- see \`stim guide logs\``;
283
295
  }
284
- async function runCollector({ platform, root, udid = null, appName = null, appExecutable = null, bundleId = null, serial = null, packageName = null, physical = false, payloadUrl = null, startStream = null, resolvePid = null, pidTimeoutMs = 3e4, pidOf = null, pidWatchMs = null, watchPid = watchAppPid, resolveClockOffset = androidClockOffset, now = Date.now, onExit = (code) => process.exit(code), attachSignals = true, stderr = (line) => console.error(line) }) {
296
+ async function runCollector({ slot = "default", platform, root, udid = null, appName = null, appExecutable = null, bundleId = null, serial = null, packageName = null, physical = false, payloadUrl = null, startStream = null, resolvePid = null, pidTimeoutMs = 3e4, pidOf = null, pidWatchMs = null, watchPid = watchAppPid, resolveClockOffset = androidClockOffset, now = Date.now, onExit = (code) => process.exit(code), attachSignals = true, stderr = (line) => console.error(line) }) {
297
+ const key = deviceSlotKey(platform, slot);
298
+ const attribution = slot === "default" ? {} : {
299
+ slot,
300
+ deviceId: udid ?? serial
301
+ };
285
302
  const writer = createNdjsonWriter(join(workspaceLogsDir(root), "device.ndjson"));
286
303
  const startedAt = new Date(now()).toISOString();
287
304
  const processToken = captureProcessToken(process.pid);
@@ -299,6 +316,7 @@ async function runCollector({ platform, root, udid = null, appName = null, appEx
299
316
  finished = true;
300
317
  watcher?.stop();
301
318
  if (physical && captured === 0 && !signalled) writer.write({
319
+ ...attribution,
302
320
  src: "device",
303
321
  platform,
304
322
  level: "warn",
@@ -306,6 +324,7 @@ async function runCollector({ platform, root, udid = null, appName = null, appEx
306
324
  msg: `the device console produced no output for ${bundleId}; devicectl only connects the app's streams when it starts the app, and os_log below the info level never reaches them`
307
325
  });
308
326
  writer.write({
327
+ ...attribution,
309
328
  src: "device",
310
329
  platform,
311
330
  level,
@@ -313,7 +332,7 @@ async function runCollector({ platform, root, udid = null, appName = null, appEx
313
332
  msg
314
333
  });
315
334
  try {
316
- unregisterCollector(root, platform, process.pid, processToken);
335
+ unregisterCollector(root, key, process.pid, processToken);
317
336
  } catch {}
318
337
  const closed = writer.close();
319
338
  if (closed.dropped > 0) stderr(`Stim collector: dropped ${closed.dropped} record(s); last error: ${describe(closed.lastError)}`);
@@ -327,7 +346,8 @@ async function runCollector({ platform, root, udid = null, appName = null, appEx
327
346
  finish(0, "info", `device log collector received ${signal}; detaching`, "collector_stopped", { signalled: true });
328
347
  });
329
348
  try {
330
- registerCollector(root, platform, {
349
+ registerCollector(root, key, {
350
+ ...attribution,
331
351
  pid: process.pid,
332
352
  processToken,
333
353
  startedAt
@@ -355,6 +375,7 @@ async function runCollector({ platform, root, udid = null, appName = null, appEx
355
375
  pid = resolved.pid ?? null;
356
376
  }
357
377
  writer.write({
378
+ ...attribution,
358
379
  src: "device",
359
380
  platform,
360
381
  level: "info",
@@ -375,6 +396,7 @@ async function runCollector({ platform, root, udid = null, appName = null, appEx
375
396
  const attach = (streamPid) => {
376
397
  const clockOffsetMs = platform === "android" ? resolveClockOffset(serial, { now }) : null;
377
398
  if (platform === "android") writer.write({
399
+ ...attribution,
378
400
  src: "device",
379
401
  platform,
380
402
  level: clockOffsetMs === null ? "warn" : "debug",
@@ -390,6 +412,7 @@ async function runCollector({ platform, root, udid = null, appName = null, appEx
390
412
  if (!record) return;
391
413
  captured++;
392
414
  writer.write({
415
+ ...attribution,
393
416
  ...record,
394
417
  platform
395
418
  });
@@ -420,6 +443,7 @@ async function runCollector({ platform, root, udid = null, appName = null, appEx
420
443
  const errReader = createLineReader(physical ? onLine : (line) => {
421
444
  const text = String(line).trimEnd();
422
445
  if (text.trim()) writer.write({
446
+ ...attribution,
423
447
  src: "device",
424
448
  platform,
425
449
  level: "debug",
@@ -464,6 +488,7 @@ async function runCollector({ platform, root, udid = null, appName = null, appEx
464
488
  killChild(child);
465
489
  child = null;
466
490
  writer.write({
491
+ ...attribution,
467
492
  src: "device",
468
493
  platform,
469
494
  level: "info",
@@ -4,6 +4,85 @@ import { homedir } from "os";
4
4
  import { dirname, isAbsolute, join, normalize } from "path";
5
5
  import { withDirLock } from "@stim-cli/core";
6
6
  import { capture, check, decode } from "unique-pid";
7
+ //#region src/device-slots.ts
8
+ const DEFAULT_DEVICE_SLOT = "default";
9
+ function validateDeviceSlot(slot = "default") {
10
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(slot) || [
11
+ "constructor",
12
+ "prototype",
13
+ "__proto__"
14
+ ].includes(slot)) {
15
+ const error = /* @__PURE__ */ new Error("A device slot must be 1-64 letters, digits, underscores or hyphens, starting with a letter or digit.");
16
+ Object.assign(error, { code: "STIM_BAD_ARG" });
17
+ throw error;
18
+ }
19
+ return slot;
20
+ }
21
+ function deviceSlotPlatforms(project, slot = DEFAULT_DEVICE_SLOT) {
22
+ validateDeviceSlot(slot);
23
+ if (slot === DEFAULT_DEVICE_SLOT) return project?.platforms;
24
+ return project?.deviceSlots && Object.hasOwn(project.deviceSlots, slot) ? project.deviceSlots[slot] : void 0;
25
+ }
26
+ function projectDeviceSlots(project) {
27
+ const slots = [{
28
+ slot: DEFAULT_DEVICE_SLOT,
29
+ platforms: project?.platforms ?? {}
30
+ }];
31
+ for (const [slot, platforms] of Object.entries(project?.deviceSlots ?? {})) {
32
+ validateDeviceSlot(slot);
33
+ if (slot === DEFAULT_DEVICE_SLOT) throw new Error("The default device slot must use the existing platforms record.");
34
+ if (!platforms || typeof platforms !== "object" || Array.isArray(platforms)) throw new Error(`Invalid device slot record: ${slot}`);
35
+ slots.push({
36
+ slot,
37
+ platforms
38
+ });
39
+ }
40
+ return slots;
41
+ }
42
+ function assignSlotDevice(project, platform, device, slot = DEFAULT_DEVICE_SLOT) {
43
+ validateDeviceSlot(slot);
44
+ if (platform !== "ios" && platform !== "android") throw new Error(`Unknown device platform: ${platform}`);
45
+ if (slot === DEFAULT_DEVICE_SLOT) project.platforms = {
46
+ ...project.platforms,
47
+ [platform]: device
48
+ };
49
+ else project.deviceSlots = {
50
+ ...project.deviceSlots,
51
+ [slot]: {
52
+ ...deviceSlotPlatforms(project, slot),
53
+ [platform]: device
54
+ }
55
+ };
56
+ }
57
+ function removeSlotDevice(project, platform, slot = DEFAULT_DEVICE_SLOT) {
58
+ const platforms = deviceSlotPlatforms(project, slot);
59
+ if (!platforms) return;
60
+ delete platforms[platform];
61
+ if (slot !== DEFAULT_DEVICE_SLOT && Object.keys(platforms).length === 0) {
62
+ delete project.deviceSlots[slot];
63
+ if (Object.keys(project.deviceSlots).length === 0) delete project.deviceSlots;
64
+ }
65
+ }
66
+ function deviceSlotKey(platform, slot = "default") {
67
+ validateDeviceSlot(slot);
68
+ if (platform !== "ios" && platform !== "android") throw new Error(`Unknown device platform: ${platform}`);
69
+ return slot === "default" ? platform : `${platform}:${slot}`;
70
+ }
71
+ function parseDeviceSlotKey(key) {
72
+ const [platform, slot = "default", extra] = key.split(":");
73
+ if (platform !== "ios" && platform !== "android" || extra !== void 0) return null;
74
+ try {
75
+ validateDeviceSlot(slot);
76
+ } catch {
77
+ return null;
78
+ }
79
+ if (key !== deviceSlotKey(platform, slot)) return null;
80
+ return {
81
+ platform,
82
+ slot
83
+ };
84
+ }
85
+ //#endregion
7
86
  //#region src/fs-util.ts
8
87
  function volumeRootFor(path) {
9
88
  const m = normalize(String(path)).match(/^\/volumes\/([^/]+)/i);
@@ -266,31 +345,31 @@ function claimMetroPort(projectPath, port) {
266
345
  return port;
267
346
  });
268
347
  }
269
- function setDevice(projectPath, platform, deviceFields) {
348
+ function setDevice(projectPath, platform, deviceFields, slot = "default") {
270
349
  requireAbsoluteProjectPath(projectPath);
271
350
  withConfigLock(() => {
272
351
  const cfg = ensureConfig();
273
352
  if (!cfg.projects[projectPath]) throw new Error(`Project not registered: ${projectPath}`);
274
- cfg.projects[projectPath].platforms = cfg.projects[projectPath].platforms || {};
275
- cfg.projects[projectPath].platforms[platform] = deviceFields;
353
+ assignSlotDevice(cfg.projects[projectPath], platform, deviceFields, slot);
276
354
  saveConfig(cfg);
277
355
  });
278
356
  }
279
- function releaseAndroidConsolePort(projectPath, consolePort) {
357
+ function releaseAndroidConsolePort(projectPath, consolePort, slot = "default") {
280
358
  return withConfigLock(() => {
281
359
  const cfg = loadConfig();
282
- const android = cfg?.projects?.[projectPath]?.platforms?.android;
360
+ const android = deviceSlotPlatforms(cfg?.projects?.[projectPath], slot)?.android;
283
361
  if (!cfg || !android || android.consolePort !== consolePort) return false;
284
362
  delete android.consolePort;
285
363
  saveConfig(cfg);
286
364
  return true;
287
365
  });
288
366
  }
289
- function clearDevice(projectPath, platform) {
367
+ function clearDevice(projectPath, platform, slot = "default") {
290
368
  withConfigLock(() => {
291
369
  const cfg = loadConfig();
292
- if (!cfg?.projects?.[projectPath]?.platforms) return;
293
- delete cfg.projects[projectPath].platforms[platform];
370
+ const project = cfg?.projects?.[projectPath];
371
+ if (!cfg || !project) return;
372
+ removeSlotDevice(project, platform, slot);
294
373
  saveConfig(cfg);
295
374
  });
296
375
  }
@@ -371,11 +450,13 @@ function allConsolePortsAndSerials({ isMounted = isOnMountedVolume } = {}) {
371
450
  if (!cfg) return result;
372
451
  for (const [path, proj] of Object.entries(cfg.projects || {})) {
373
452
  if (!existsSync(path) && isMounted(path)) continue;
374
- const android = proj.platforms?.android;
375
- if (typeof android?.consolePort === "number") result.androidConsolePorts.push(android.consolePort);
376
- if (android?.serial && !android.avdName) result.androidPhysicalSerials.push(android.serial);
453
+ for (const { platforms } of projectDeviceSlots(proj)) {
454
+ const android = platforms.android;
455
+ if (typeof android?.consolePort === "number") result.androidConsolePorts.push(android.consolePort);
456
+ if (android?.serial && !android.avdName) result.androidPhysicalSerials.push(android.serial);
457
+ }
377
458
  }
378
459
  return result;
379
460
  }
380
461
  //#endregion
381
- export { directorySize as A, withConfigLock as C, sameProcessRecord as D, inspectProcessIdentity as E, isOnMountedVolume as M, listMountedVolumes as N, waitForProcessExit as O, volumeRootFor as P, upsertProject as S, captureProcessToken as T, releaseAndroidConsolePort as _, clearSupervisor as a, setDevice as b, findEnclosingWorktreeRoot as c, getConfigPath as d, getProject as f, loadConfig as g, isPathPrefix as h, clearDevice as i, formatBytes as j, withDirLock as k, getConcurrencyLimits as l, getRepoSettings as m, allMetroPorts as n, configCorruptRepair as o, getProjectSettings as p, claimMetroPort as r, ensureConfig as s, allConsolePortsAndSerials as t, getConfigDir as u, removeProject as v, captureProcessIdentity as w, setSupervisor as x, saveConfig as y };
462
+ export { directorySize as A, removeSlotDevice as B, withConfigLock as C, sameProcessRecord as D, inspectProcessIdentity as E, assignSlotDevice as F, deviceSlotKey as I, deviceSlotPlatforms as L, isOnMountedVolume as M, listMountedVolumes as N, waitForProcessExit as O, volumeRootFor as P, parseDeviceSlotKey as R, upsertProject as S, captureProcessToken as T, validateDeviceSlot as V, releaseAndroidConsolePort as _, clearSupervisor as a, setDevice as b, findEnclosingWorktreeRoot as c, getConfigPath as d, getProject as f, loadConfig as g, isPathPrefix as h, clearDevice as i, formatBytes as j, withDirLock as k, getConcurrencyLimits as l, getRepoSettings as m, allMetroPorts as n, configCorruptRepair as o, getProjectSettings as p, claimMetroPort as r, ensureConfig as s, allConsolePortsAndSerials as t, getConfigDir as u, removeProject as v, captureProcessIdentity as w, setSupervisor as x, saveConfig as y, projectDeviceSlots as z };
@@ -1,6 +1,6 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
2
  import { n as stripAnsi, r as waitForChild, t as createLineReader } from "./process-output-C7IlIzd7.mjs";
3
- import { f as startBuildHeartbeat, h as bundlerPin, r as HEARTBEAT_INTERVAL_MS } from "./xcode-DeKCribl.mjs";
3
+ import { f as startBuildHeartbeat, h as bundlerPin, r as HEARTBEAT_INTERVAL_MS } from "./xcode-D7sAp-Zy.mjs";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { dirname, join, relative, resolve, sep } from "node:path";
6
6
  import { homedir } from "node:os";
@@ -1,16 +1,190 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { y as ensureWorkspaceStorage } from "./state-CZ-ER-8d.mjs";
3
- import { C as removeTemporaryEntry, S as makeTemporaryDirectory } from "./worktree-C79xx_kM.mjs";
4
- import { i as detectIsExpo, o as isPackageResolvable } from "./project-BBTVABUz.mjs";
2
+ import { y as ensureWorkspaceStorage } from "./state-B3XA2wp2.mjs";
3
+ import { C as removeTemporaryEntry, S as makeTemporaryDirectory } from "./worktree-Eq6TLH8N.mjs";
4
+ import { i as detectIsExpo, o as isPackageResolvable } from "./project-fKmuIaMI.mjs";
5
5
  import { n as stripAnsi, r as waitForChild, t as createLineReader } from "./process-output-C7IlIzd7.mjs";
6
- import { t as NOT_OURS_FOREIGN_CWD } from "./metro-COslRK_F.mjs";
7
- import { i as LAUNCH_BUNDLING, s as LAUNCH_UNVERIFIED } from "./app-install-NFxUahnH.mjs";
8
- import { f as startBuildHeartbeat, p as tailLines, r as HEARTBEAT_INTERVAL_MS } from "./xcode-DeKCribl.mjs";
9
- import { i as expoBinRefusal, r as expoBinPath, t as cleanLine } from "./server-expo-C3ktAwUG.mjs";
10
- import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
6
+ import { t as NOT_OURS_FOREIGN_CWD } from "./metro-CORqXyTL.mjs";
7
+ import { r as claimFailure } from "./ownership-claim-Bf2CAfw3.mjs";
8
+ import { V as releaseBuildLock, z as acquireBuildLock } from "./android-BqVIJoG7.mjs";
9
+ import { i as expoBinRefusal, r as expoBinPath, t as cleanLine } from "./server-expo-DizT4OMk.mjs";
10
+ import { x as resolveEasCliBin } from "./teardown-ClFp1Aax.mjs";
11
+ import { i as LAUNCH_BUNDLING, s as LAUNCH_UNVERIFIED } from "./app-install-DIp9riNx.mjs";
12
+ import { f as startBuildHeartbeat, p as tailLines, r as HEARTBEAT_INTERVAL_MS } from "./xcode-D7sAp-Zy.mjs";
13
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
14
+ import { createHash } from "node:crypto";
11
15
  import { basename, join } from "node:path";
12
16
  import chalk from "chalk";
13
17
  import { request } from "node:http";
18
+ //#region src/engine/eas-build.ts
19
+ function isEasBuildFailure(result) {
20
+ return result?.ok === false;
21
+ }
22
+ function easOptionRefusal({ profile, isExpo, buildSelector, buildCache }) {
23
+ if (profile === void 0) return null;
24
+ if (profile.trim() && isExpo && buildSelector === void 0 && buildCache !== false) return null;
25
+ return {
26
+ code: "STIM_BAD_ARG",
27
+ message: "--eas-profile requires an Expo project and a non-empty development profile.",
28
+ remedy: "Use --eas-profile <name> without --scheme, --configuration, --variant, or --no-build-cache."
29
+ };
30
+ }
31
+ function object(value) {
32
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
33
+ }
34
+ function shellArg(value) {
35
+ return /^[a-zA-Z0-9_.-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
36
+ }
37
+ function easDeviceBuildRemedy(profile) {
38
+ return `If the device is not registered, run npx eas-cli device:create. With approval for cloud build costs and signing changes, run npx eas-cli build --platform ios --profile ${shellArg(profile)} to produce a build with a valid profile for this device, then retry the same Stim command.`;
39
+ }
40
+ var EasFailure = class extends Error {
41
+ code;
42
+ remedy;
43
+ constructor(code, message, remedy) {
44
+ super(message);
45
+ this.code = code;
46
+ this.remedy = remedy;
47
+ }
48
+ };
49
+ function selectEasBuild(builds, { platform, profile, fingerprint, projectId, physical = false }) {
50
+ if (!Array.isArray(builds)) throw new Error("EAS build:list did not return an array.");
51
+ for (const value of builds) {
52
+ const build = object(value);
53
+ if (typeof build.id === "string" && build.id.length > 0 && build.status === "FINISHED" && build.platform === platform.toUpperCase() && build.buildProfile === profile && object(build.fingerprint).hash === fingerprint && object(build.project).id === projectId && (platform !== "ios" || build.isForIosSimulator === !physical) && build.distribution === "INTERNAL" && typeof object(build.artifacts).applicationArchiveUrl === "string") return build.id;
54
+ }
55
+ return null;
56
+ }
57
+ async function resolveEasDevelopmentBuild({ root, platform, profile, note, isExpo = true, physical = false, selectors = [], buildCache }) {
58
+ if (profile === void 0) return null;
59
+ const refusal = easOptionRefusal({
60
+ profile,
61
+ isExpo,
62
+ buildSelector: selectors.find((value) => value != null) ?? void 0,
63
+ buildCache
64
+ });
65
+ if (refusal) return {
66
+ ok: false,
67
+ ...refusal
68
+ };
69
+ const retry = "the same Stim command";
70
+ const buildCommand = `npx eas-cli build --platform ${platform} --profile ${shellArg(profile)}`;
71
+ let lock = null;
72
+ try {
73
+ const cli = resolveEasCliBin(root);
74
+ if (!cli) throw new EasFailure("STIM_EAS_UNAVAILABLE", "EAS CLI is not installed.", "Install eas-cli, run eas login, then retry.");
75
+ let profileEnv = {};
76
+ const run = (args, timeoutMs = 12e4) => {
77
+ try {
78
+ return JSON.parse(getExecutor().runFile(cli.file, [
79
+ ...args,
80
+ "--json",
81
+ "--non-interactive"
82
+ ], {
83
+ cwd: root,
84
+ timeoutMs,
85
+ env: profileEnv
86
+ }));
87
+ } catch {
88
+ throw new EasFailure("STIM_EAS_UNAVAILABLE", `EAS ${args[0]} failed or returned invalid JSON.`, `Run npx eas-cli ${args.map(shellArg).join(" ")} to inspect the error, then retry ${retry}.`);
89
+ }
90
+ };
91
+ note(`eas resolving profile ${profile}`);
92
+ const config = object(run([
93
+ "config",
94
+ "--platform",
95
+ platform,
96
+ "--profile",
97
+ profile
98
+ ]));
99
+ const buildProfile = object(config.buildProfile);
100
+ profileEnv = object(buildProfile.env);
101
+ const projectId = object(object(object(config.appConfig).extra).eas).projectId;
102
+ if (buildProfile.developmentClient !== true || buildProfile.distribution !== "internal" || platform === "ios" && buildProfile.simulator === true !== !physical) throw new EasFailure("STIM_BAD_ARG", `EAS profile ${profile} is not an internal development build for this ${platform} target.`, `Set developmentClient: true and distribution: internal${platform === "ios" ? `, with ios.simulator: ${!physical}` : ""} in that eas.json profile.`);
103
+ if (typeof projectId !== "string" || !projectId) throw new EasFailure("STIM_BAD_ARG", "The Expo app is not linked to an EAS project.", "Link the intended EAS project with eas init, then retry.");
104
+ if (platform === "ios" ? buildProfile.buildConfiguration !== void 0 && buildProfile.buildConfiguration !== "Debug" : buildProfile.gradleCommand !== void 0 && (typeof buildProfile.gradleCommand !== "string" || !/^:app:assemble\w*Debug$/.test(buildProfile.gradleCommand))) throw new EasFailure("STIM_BAD_ARG", `EAS profile ${profile} overrides the development build configuration.`, "Use ios.buildConfiguration: Debug or an android.gradleCommand that assembles a single Debug APK, or remove the override.");
105
+ note(`eas fingerprinting ${platform} with profile ${profile}`);
106
+ const fingerprint = object(run([
107
+ "fingerprint:generate",
108
+ "--platform",
109
+ platform,
110
+ "--build-profile",
111
+ profile
112
+ ])).hash;
113
+ if (typeof fingerprint !== "string" || !/^[a-f0-9]{40,64}$/.test(fingerprint)) throw new EasFailure("STIM_EAS_UNAVAILABLE", "EAS returned no valid native fingerprint.", `Run npx eas-cli fingerprint:generate --platform ${platform} --build-profile ${shellArg(profile)} to inspect the result.`);
114
+ note(`eas looking for ${fingerprint.slice(0, 8)}`);
115
+ const buildId = selectEasBuild(run([
116
+ "build:list",
117
+ "--platform",
118
+ platform,
119
+ "--build-profile",
120
+ profile,
121
+ "--fingerprint-hash",
122
+ fingerprint,
123
+ "--status",
124
+ "finished",
125
+ ...platform === "ios" && !physical ? ["--simulator"] : ["--distribution", "internal"],
126
+ "--limit",
127
+ "1"
128
+ ]), {
129
+ platform,
130
+ profile,
131
+ fingerprint,
132
+ projectId,
133
+ physical
134
+ });
135
+ if (!buildId) throw new EasFailure("STIM_EAS_BUILD_MISSING", `No compatible EAS ${platform} build exists for profile ${profile} and fingerprint ${fingerprint}.`, `A cloud build may incur charges. With approval to run it, use ${buildCommand}, then retry ${retry}.`);
136
+ const cacheKey = `eas-${createHash("sha256").update(JSON.stringify({
137
+ projectId,
138
+ buildId
139
+ })).digest("hex")}-debug-${physical && platform === "ios" ? "device" : "sim"}`;
140
+ lock = acquireBuildLock({
141
+ platform,
142
+ key: cacheKey,
143
+ root
144
+ });
145
+ if (!lock.acquired) throw new EasFailure("STIM_EAS_UNAVAILABLE", `Another run holds the EAS artifact claim at ${lock.path}.`, `Wait for that run to finish, then retry ${retry}.`);
146
+ note(`eas resolving artifact for build ${buildId}`);
147
+ const result = object(run([
148
+ "build:download",
149
+ "--build-id",
150
+ buildId
151
+ ], 6e5));
152
+ if (typeof result.path !== "string" || !existsSync(result.path)) throw new Error("EAS returned no downloaded artifact.");
153
+ const path = realpathSync(result.path);
154
+ if (platform === "ios" ? !path.endsWith(".app") || !statSync(path).isDirectory() : !path.endsWith(".apk") || !statSync(path).isFile()) throw new Error("EAS returned an unexpected artifact path or type.");
155
+ note(`eas artifact ready ${fingerprint.slice(0, 8)}`);
156
+ return {
157
+ ok: true,
158
+ path,
159
+ fingerprint,
160
+ cacheKey,
161
+ cacheHit: "remote"
162
+ };
163
+ } catch (error) {
164
+ const claim = claimFailure(error, null);
165
+ if (claim) return {
166
+ ok: false,
167
+ code: claim.code,
168
+ message: claim.message,
169
+ remedy: claim.remedy
170
+ };
171
+ if (error instanceof EasFailure) return {
172
+ ok: false,
173
+ code: error.code,
174
+ message: error.message,
175
+ remedy: error.remedy
176
+ };
177
+ return {
178
+ ok: false,
179
+ code: "STIM_EAS_UNAVAILABLE",
180
+ message: `Could not resolve the EAS development build: ${error.message}`,
181
+ remedy: `Retry ${retry}. No cloud build was started.`
182
+ };
183
+ } finally {
184
+ releaseBuildLock(lock);
185
+ }
186
+ }
187
+ //#endregion
14
188
  //#region src/engine/metro-warmup.ts
15
189
  const WARMUP_TIMEOUT_MS = 6e4;
16
190
  async function requestLocal(url, platform, prefetch) {
@@ -135,6 +309,9 @@ function launchOutcomeRecord({ launchState, release, bundleId, configuration, me
135
309
  msg
136
310
  };
137
311
  }
312
+ function isPhysicalDeviceRequest(flag) {
313
+ return flag !== null && flag !== void 0 && flag !== false;
314
+ }
138
315
  //#endregion
139
316
  //#region src/engine/prebuild.ts
140
317
  const PREBUILD_ERROR = "STIM_PREBUILD_FAILED";
@@ -617,4 +794,4 @@ function readJson(file) {
617
794
  }
618
795
  }
619
796
  //#endregion
620
- export { swapJsBundle as a, ensureWorkspaceStorageSafely as c, noMetroMessage as d, noMetroRemedy as f, warmMetro as h, detectEntryFile as i, gateShouldRetry as l, sleep as m, pickDevClientScheme as n, needsPrebuild as o, resolveMetroWithRetry as p, schemesFromInfoPlist as r, runPrebuild as s, devClientScheme as t, launchOutcomeRecord as u };
797
+ export { easDeviceBuildRemedy as _, swapJsBundle as a, ensureWorkspaceStorageSafely as c, launchOutcomeRecord as d, noMetroMessage as f, warmMetro as g, sleep as h, detectEntryFile as i, gateShouldRetry as l, resolveMetroWithRetry as m, pickDevClientScheme as n, needsPrebuild as o, noMetroRemedy as p, schemesFromInfoPlist as r, runPrebuild as s, devClientScheme as t, isPhysicalDeviceRequest as u, isEasBuildFailure as v, resolveEasDevelopmentBuild as y };
@@ -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-BqVIJoG7.mjs";
5
+ import { f as takeLease, l as releaseWorkspaceLeases, n as fileLeaseIo, o as parseLeaseDuration } from "./device-lease-BJYybF-2.mjs";
6
+ import { d as waitForDevice, s as parseDeviceWait, t as selectFromPool } from "./device-pool-CPbLGfrB.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
  });