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,8 +1,8 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { o as parseNdjsonText } from "./ndjson-DcAtEx_K.mjs";
2
+ import { o as parseNdjsonText } from "./ndjson-BZwI1NBv.mjs";
3
3
  import { t as DEV_MENU_LAUNCH_ARGS } from "./ios-device-D3pzTKQE.mjs";
4
4
  import { s as isAppLaunchError } from "./command-output-8wHMEde0.mjs";
5
- import { l as iosSimulatorFailureAdvice, p as listUserApps, w as uninstallIosApp } from "./ios-D1h1Di39.mjs";
5
+ import { l as iosSimulatorFailureAdvice, p as listUserApps, w as uninstallIosApp } from "./ios-Deh5js5V.mjs";
6
6
  import { closeSync, lstatSync, openSync, readFileSync, readSync, readdirSync } from "node:fs";
7
7
  import { createHash } from "node:crypto";
8
8
  import { join } from "node:path";
@@ -900,7 +900,7 @@ function nativeLaunchFailure({ readCrashes, since, platform, processAlive, mode,
900
900
  waitedMs
901
901
  } : null;
902
902
  }
903
- async function verifyLaunch({ logsDir, since, metroPort = null, platform = null, mode = null, requireBundleResponse, timeoutMs = VERIFY_TIMEOUT_MS, stabilityMs = STABILITY_WINDOW_MS, pollMs = VERIFY_POLL_MS, readRecords = null, readDeviceRecords = null, readClientRecords = null, readNativeCrashes = null, processAlive = null, onReadinessPending = null, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) } = {}) {
903
+ async function verifyLaunch({ slot, logsDir, since, metroPort = null, platform = null, mode = null, requireBundleResponse, timeoutMs = VERIFY_TIMEOUT_MS, stabilityMs = STABILITY_WINDOW_MS, pollMs = VERIFY_POLL_MS, readRecords = null, readDeviceRecords = null, readClientRecords = null, readNativeCrashes = null, processAlive = null, onReadinessPending = null, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) } = {}) {
904
904
  const read = readRecords || (() => readMetroRecords(logsDir));
905
905
  const readDevice = readDeviceRecords || (() => readNdjson(logsDir, "device.ndjson"));
906
906
  const readClient = readClientRecords || (() => readNdjson(logsDir, "client.ndjson"));
@@ -915,10 +915,10 @@ async function verifyLaunch({ logsDir, since, metroPort = null, platform = null,
915
915
  let runtimeStartedAt = null;
916
916
  let nextCrashCheck = startedAt + 1e3;
917
917
  while (true) {
918
- const metroRecords = read().filter((record) => after(record, since));
918
+ const metroRecords = read().filter((record) => after(record, since) && (slot === void 0 || record.slot === slot));
919
919
  const bundleRecords = metroRecords.filter((record) => !requireBundleResponse || String(record.event).startsWith("bundle_response_"));
920
- const deviceRecords = readDevice().filter((record) => after(record, since));
921
- const clientRecords = readClient().filter((record) => after(record, since));
920
+ const deviceRecords = readDevice().filter((record) => after(record, since) && (slot === void 0 || (record.slot ?? "default") === slot));
921
+ const clientRecords = readClient().filter((record) => after(record, since) && (slot === void 0 || record.slot === slot));
922
922
  if (now() >= nextCrashCheck) {
923
923
  nextCrashCheck = now() + 2e3;
924
924
  const crash = nativeLaunchFailure({
@@ -1,6 +1,6 @@
1
- import { u as getConfigDir } from "./config-CMtIaTk9.mjs";
1
+ import { u as getConfigDir } from "./config-CcefMY2Q.mjs";
2
2
  import { a as formatElapsed } from "./command-output-8wHMEde0.mjs";
3
- import { d as readClaimSet, f as releaseClaim, h as tryAcquireClaim, s as isClaimRefusal } from "./ownership-claim-7zRxxTfZ.mjs";
3
+ import { d as releaseClaim, m as tryAcquireClaim, s as isClaimRefusal, u as readClaimSet } from "./ownership-claim-BtPtJVmq.mjs";
4
4
  import { readdirSync, statSync } from "node:fs";
5
5
  import { join } from "node:path";
6
6
  //#region src/engine/build-slots.ts
@@ -1,4 +1,4 @@
1
- import { u as getConfigDir } from "./config-CMtIaTk9.mjs";
1
+ import { u as getConfigDir } from "./config-CcefMY2Q.mjs";
2
2
  import { existsSync } from "fs";
3
3
  import { homedir } from "os";
4
4
  import { join, resolve } from "path";
@@ -1,2 +1,2 @@
1
- import { a as unregister, i as registeredCaches, n as readManifest, r as register, t as manifestPath } from "./cache-manifest-Cu4_Znw9.mjs";
1
+ import { a as unregister, i as registeredCaches, n as readManifest, r as register, t as manifestPath } from "./cache-manifest-CXR-Y8pH.mjs";
2
2
  export { manifestPath, readManifest, register, registeredCaches, unregister };
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-BueDfLYG.mjs")],
7
+ ["worktree", () => import("./worktree-DtQGO9-s.mjs")],
8
+ ["start", () => import("./start-B5G4NjiA.mjs")],
9
+ ["stop", () => import("./stop-CQhffpmX.mjs")],
10
+ ["ios", () => import("./ios-DAju3TVy.mjs")],
11
+ ["android", () => import("./android-CT-PkXUh.mjs")],
12
+ ["reload", () => import("./reload-CWpXoOgN.mjs")],
13
+ ["device", () => import("./device-Bbv45Z9Z.mjs")],
14
+ ["logs", () => import("./logs-CAdwW6c2.mjs")],
15
+ ["status", () => import("./status-CI9clRSb.mjs")],
16
+ ["stats", () => import("./stats-CaaQr4yN.mjs")],
17
+ ["gc", () => import("./gc-BRu62v4o.mjs")],
18
+ ["guide", () => import("./guide-D95E0u3i.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-xnxyvJkD.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-BtPtJVmq.mjs";
8
+ import { V as releaseBuildLock, z as acquireBuildLock } from "./android-hMj83VpN.mjs";
9
+ import { i as expoBinRefusal, r as expoBinPath, t as cleanLine } from "./server-expo-DizT4OMk.mjs";
10
+ import { x as resolveEasCliBin } from "./teardown--gESs9ck.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-xnxyvJkD.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 };