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,18 +1,18 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { C as withConfigLock, E as inspectProcessIdentity, _ as releaseAndroidConsolePort, b as setDevice, g as loadConfig, i as clearDevice, t as allConsolePortsAndSerials, u as getConfigDir, w as captureProcessIdentity, y as saveConfig } from "./config-CMtIaTk9.mjs";
3
- import { r as register } from "./cache-manifest-Cu4_Znw9.mjs";
4
- import { E as workspaceId, S as sharedCompilationCache, w as workspaceDerivedData } from "./state-CZ-ER-8d.mjs";
5
- import { s as ownedDeviceLabel } from "./project-BBTVABUz.mjs";
2
+ import { C as withConfigLock, E as inspectProcessIdentity, L as deviceSlotPlatforms, _ as releaseAndroidConsolePort, b as setDevice, g as loadConfig, i as clearDevice, t as allConsolePortsAndSerials, u as getConfigDir, w as captureProcessIdentity, y as saveConfig, z as projectDeviceSlots } from "./config-CcefMY2Q.mjs";
3
+ import { r as register } from "./cache-manifest-CXR-Y8pH.mjs";
4
+ import { E as workspaceId, S as sharedCompilationCache, w as workspaceDerivedData } from "./state-B3XA2wp2.mjs";
5
+ import { s as ownedDeviceLabel } from "./project-fKmuIaMI.mjs";
6
6
  import { n as stripAnsi, r as waitForChild, t as createLineReader } from "./process-output-C7IlIzd7.mjs";
7
- import { r as pidExists } from "./metro-COslRK_F.mjs";
8
- import { C as selectParked, S as readParked, _ as teardownParkedIosSim, b as dropParked, g as teardownParkedAvd, m as teardownOwnedAvd, x as parkedMaxSetting, y as adoptParked } from "./remote-cache-DDmJ1Roq.mjs";
9
- import { U as resolveOptimizations, l as androidDataPartitionSizeGbSetting, o as androidAvdConfigSetting, v as iosSimSlimProfileSetting } from "./settings-DwU1_U_q.mjs";
7
+ import { r as pidExists } from "./metro-CORqXyTL.mjs";
10
8
  import { a as formatElapsed, l as phaseLine } from "./command-output-8wHMEde0.mjs";
11
- import { E as readHostMemoryPressure, T as hostMemoryPressureAdvice, _ as renameIosSim, b as resolveIosCreation, c as iosRuntimeMatches, d as listIosDeviceTypes, g as parseRuntimeVersion, i as createOwnedIosSim, l as iosSimulatorFailureAdvice, m as ownedSimName, n as bootIosSim, u as listAllIosSims, v as resetIosKeychain, x as resolveOwnedIosSim, y as resetIosPrivacy } from "./ios-D1h1Di39.mjs";
12
- import { f as releaseClaim, h as tryAcquireClaim, i as claimRemoveCommand, l as markClaimChildPending, n as ClaimRefusedError, p as setClaimChild, u as processGroupAlive } from "./ownership-claim-7zRxxTfZ.mjs";
13
- import { A as pickDefaultSystemImage, D as ownedAvdName, E as ownedAvdMatchesConfiguration, L as waitForBoot, N as resolveOwnedAvdSerial, O as ownedAvdSystemImage, b as listAvds, c as avdPoolConfiguration, d as configureNewOwnedAvd, f as createOwnedAvd, o as assertOwnedAvdStopped, u as bootAndroidEmulator, v as getAvdNameForSerial, w as nextConsolePort, x as listInstalledSystemImages, y as listAdbDevices } from "./android-K63_0UfF.mjs";
14
- import { t as withWorkspaceProcessLock } from "./workspace-process-lock-BKfdys1q.mjs";
15
- import { t as cleanLine } from "./server-expo-C3ktAwUG.mjs";
9
+ import { c as markClaimChildPending, d as releaseClaim, f as setClaimChild, i as claimRemoveCommand, l as processGroupAlive, m as tryAcquireClaim, t as ClaimRefusedError } from "./ownership-claim-BtPtJVmq.mjs";
10
+ import { A as pickDefaultSystemImage, D as ownedAvdName, E as ownedAvdMatchesConfiguration, L as waitForBoot, N as resolveOwnedAvdSerial, O as ownedAvdSystemImage, b as listAvds, c as avdPoolConfiguration, d as configureNewOwnedAvd, f as createOwnedAvd, o as assertOwnedAvdStopped, u as bootAndroidEmulator, v as getAvdNameForSerial, w as nextConsolePort, x as listInstalledSystemImages, y as listAdbDevices } from "./android-hMj83VpN.mjs";
11
+ import { U as resolveOptimizations, l as androidDataPartitionSizeGbSetting, o as androidAvdConfigSetting, v as iosSimSlimProfileSetting } from "./settings-DEJf6_kJ.mjs";
12
+ import { t as cleanLine } from "./server-expo-DizT4OMk.mjs";
13
+ import { c as parkedMaxSetting, i as teardownParkedIosSim, l as readParked, o as adoptParked, r as teardownParkedAvd, s as dropParked, t as teardownOwnedAvd, u as selectParked } from "./teardown--gESs9ck.mjs";
14
+ import { t as withWorkspaceProcessLock } from "./workspace-process-lock-IBj1Towr.mjs";
15
+ import { E as readHostMemoryPressure, T as hostMemoryPressureAdvice, _ as renameIosSim, b as resolveIosCreation, c as iosRuntimeMatches, d as listIosDeviceTypes, g as parseRuntimeVersion, i as createOwnedIosSim, l as iosSimulatorFailureAdvice, m as ownedSimName, n as bootIosSim, u as listAllIosSims, v as resetIosKeychain, x as resolveOwnedIosSim, y as resetIosPrivacy } from "./ios-Deh5js5V.mjs";
16
16
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
17
17
  import { createHash, randomUUID } from "node:crypto";
18
18
  import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
@@ -161,9 +161,12 @@ function simslimIsOnPath() {
161
161
  }
162
162
  //#endregion
163
163
  //#region src/engine/device.ts
164
- function startIosBoot(udid, configure) {
164
+ function startIosBoot(udid, configure, label, out) {
165
165
  const done = (async () => {
166
- await bootIosSim(udid);
166
+ await bootIosSim(udid, {
167
+ label,
168
+ out
169
+ });
167
170
  await configure();
168
171
  })();
169
172
  done.catch(() => {});
@@ -172,11 +175,13 @@ function startIosBoot(udid, configure) {
172
175
  done
173
176
  };
174
177
  }
175
- async function ensureOwnedDevice({ platform, project, projectPath, settingsRoot = projectPath, label = ownedDeviceLabel(projectPath), settings, flags = {}, note = () => {}, out = () => {}, logFile = null, alive = pidExists, configureAvd = configureNewOwnedAvd, teardownAvd = teardownOwnedAvd, reconcileIosSimulator = reconcileSimSlim }) {
176
- const record = project?.platforms?.[platform] ?? null;
178
+ async function ensureOwnedDevice({ platform, project, projectPath, slot, settingsRoot = projectPath, label = ownedDeviceLabel(projectPath), settings, flags = {}, note = () => {}, out = () => {}, logFile = null, alive = pidExists, configureAvd = configureNewOwnedAvd, teardownAvd = teardownOwnedAvd, reconcileIosSimulator = reconcileSimSlim }) {
179
+ if (slot && slot !== "default") label = `${label}-${slot}`;
180
+ const record = deviceSlotPlatforms(project, slot)?.[platform] ?? null;
177
181
  if (platform === "ios") return ensureOwnedIosDevice({
178
182
  record,
179
183
  projectPath,
184
+ slot,
180
185
  settingsRoot,
181
186
  label,
182
187
  settings,
@@ -188,6 +193,7 @@ async function ensureOwnedDevice({ platform, project, projectPath, settingsRoot
188
193
  return ensureOwnedAndroidDevice({
189
194
  record,
190
195
  projectPath,
196
+ slot,
191
197
  settingsRoot,
192
198
  label,
193
199
  settings,
@@ -200,7 +206,7 @@ async function ensureOwnedDevice({ platform, project, projectPath, settingsRoot
200
206
  teardownAvd
201
207
  });
202
208
  }
203
- async function ensureOwnedIosDevice({ record, projectPath, settingsRoot, label, settings, flags, note, out, reconcileIosSimulator }) {
209
+ async function ensureOwnedIosDevice({ record, projectPath, slot, settingsRoot, label, settings, flags, note, out, reconcileIosSimulator }) {
204
210
  const memoryAdvice = hostMemoryPressureAdvice(readHostMemoryPressure());
205
211
  if (memoryAdvice) out(chalk.yellow(phaseLine("memory", memoryAdvice)));
206
212
  const simslimProfile = iosSimSlimProfileSetting(settings, settingsRoot);
@@ -233,6 +239,7 @@ async function ensureOwnedIosDevice({ record, projectPath, settingsRoot, label,
233
239
  return configureOwnedIosSim({
234
240
  record: updated,
235
241
  projectPath,
242
+ slot,
236
243
  profile: simslimProfile,
237
244
  out,
238
245
  reconcileIosSimulator
@@ -246,7 +253,7 @@ async function ensureOwnedIosDevice({ record, projectPath, settingsRoot, label,
246
253
  out(chalk.dim(phaseLine("device", `booting ${name} (${sim.udid})`)));
247
254
  return {
248
255
  ...updated,
249
- booting: startIosBoot(sim.udid, configure),
256
+ booting: startIosBoot(sim.udid, configure, name, out),
250
257
  ...facts
251
258
  };
252
259
  }
@@ -265,6 +272,12 @@ async function ensureOwnedIosDevice({ record, projectPath, settingsRoot, label,
265
272
  return record;
266
273
  }
267
274
  }
275
+ withConfigLock(() => {
276
+ const current = deviceSlotPlatforms(loadConfig()?.projects?.[projectPath], slot)?.ios;
277
+ if (!current) return;
278
+ if (current.deviceUdid !== record.deviceUdid || current.owned !== record.owned) throw new Error("The simulator assignment changed during recovery. Run `stim ios` again.");
279
+ clearDevice(projectPath, "ios", slot);
280
+ });
268
281
  }
269
282
  const choice = resolveIosCreation({
270
283
  deviceType: flags.deviceType || settings.ios?.deviceType,
@@ -272,6 +285,7 @@ async function ensureOwnedIosDevice({ record, projectPath, settingsRoot, label,
272
285
  });
273
286
  const adopted = parkedMaxSetting("ios").max > 0 ? await withIosDeviceNameLock(() => takeParkedIosSim({
274
287
  projectPath,
288
+ slot,
275
289
  label,
276
290
  choice,
277
291
  out
@@ -283,11 +297,12 @@ async function ensureOwnedIosDevice({ record, projectPath, settingsRoot, label,
283
297
  await configureOwnedIosSim({
284
298
  record: adopted,
285
299
  projectPath,
300
+ slot,
286
301
  profile: simslimProfile,
287
302
  out,
288
303
  reconcileIosSimulator
289
304
  });
290
- });
305
+ }, adopted.deviceName, out);
291
306
  return {
292
307
  ...adopted,
293
308
  booting,
@@ -305,7 +320,7 @@ async function ensureOwnedIosDevice({ record, projectPath, settingsRoot, label,
305
320
  deviceUdid: result.udid,
306
321
  owned: true,
307
322
  deviceName: result.name
308
- });
323
+ }, slot);
309
324
  return result;
310
325
  });
311
326
  const newRecord = {
@@ -316,10 +331,11 @@ async function ensureOwnedIosDevice({ record, projectPath, settingsRoot, label,
316
331
  const booting = startIosBoot(created.udid, () => configureOwnedIosSim({
317
332
  record: newRecord,
318
333
  projectPath,
334
+ slot,
319
335
  profile: simslimProfile,
320
336
  out,
321
337
  reconcileIosSimulator
322
- }));
338
+ }), created.name, out);
323
339
  return {
324
340
  ...newRecord,
325
341
  created: true,
@@ -333,8 +349,8 @@ function withIosDeviceNameLock(fn) {
333
349
  }
334
350
  function ownedIosNameSuffix(label, model, projectPath, udid) {
335
351
  const names = new Set(listAllIosSims({ includeUnavailable: true }).filter((sim) => sim.udid !== udid).map((sim) => sim.name));
336
- for (const project of Object.values(loadConfig()?.projects ?? {})) {
337
- const record = project.platforms?.ios;
352
+ for (const project of Object.values(loadConfig()?.projects ?? {})) for (const { platforms } of projectDeviceSlots(project)) {
353
+ const record = platforms.ios;
338
354
  if (record?.deviceUdid !== udid && record?.deviceName) names.add(record.deviceName);
339
355
  }
340
356
  for (const parked of readParked("ios")) if (parked.udid !== udid) names.add(parked.name);
@@ -357,7 +373,7 @@ function resetAdoptedSim(udid, out) {
357
373
  resetIosPrivacy(udid);
358
374
  resetIosKeychain(udid);
359
375
  }
360
- function takeParkedIosSim({ projectPath, label, choice, out }) {
376
+ function takeParkedIosSim({ projectPath, slot, label, choice, out }) {
361
377
  const candidates = selectParked(readParked("ios"), {
362
378
  deviceTypeIdentifier: choice.deviceTypeId,
363
379
  runtimeIdentifier: choice.runtimeId
@@ -393,6 +409,7 @@ function takeParkedIosSim({ projectPath, label, choice, out }) {
393
409
  if (!adoptParked({
394
410
  platform: "ios",
395
411
  projectPath,
412
+ slot,
396
413
  udid: parked.udid,
397
414
  device
398
415
  })) continue;
@@ -403,10 +420,10 @@ function takeParkedIosSim({ projectPath, label, choice, out }) {
403
420
  }
404
421
  return null;
405
422
  }
406
- function clearIosAdoptionPending(projectPath) {
423
+ function clearIosAdoptionPending(projectPath, slot = "default") {
407
424
  withConfigLock(() => {
408
425
  const cfg = loadConfig();
409
- const ios = cfg?.projects?.[projectPath]?.platforms?.ios;
426
+ const ios = deviceSlotPlatforms(cfg?.projects?.[projectPath], slot)?.ios;
410
427
  if (!cfg || !ios?.adoptionPending) return;
411
428
  delete ios.adopted;
412
429
  delete ios.adoptionPending;
@@ -414,7 +431,7 @@ function clearIosAdoptionPending(projectPath) {
414
431
  saveConfig(cfg);
415
432
  });
416
433
  }
417
- async function configureOwnedIosSim({ record, projectPath, profile, out, reconcileIosSimulator }) {
434
+ async function configureOwnedIosSim({ record, projectPath, slot, profile, out, reconcileIosSimulator }) {
418
435
  if (profile) out(chalk.dim(phaseLine("device", `applying the SimSlim profile to ${record.deviceUdid}`)));
419
436
  else if (record.simslimManaged) out(chalk.dim(phaseLine("device", `restoring stock simulator services on ${record.deviceUdid}`)));
420
437
  const previouslyManaged = Boolean(record.simslimManaged);
@@ -423,7 +440,7 @@ async function configureOwnedIosSim({ record, projectPath, profile, out, reconci
423
440
  ...record,
424
441
  simslimManaged: true
425
442
  };
426
- setDevice(projectPath, "ios", pending);
443
+ setDevice(projectPath, "ios", pending, slot);
427
444
  record = pending;
428
445
  }
429
446
  const result = await reconcileIosSimulator({
@@ -435,25 +452,23 @@ async function configureOwnedIosSim({ record, projectPath, profile, out, reconci
435
452
  const updated = { ...record };
436
453
  if (result.managed) updated.simslimManaged = true;
437
454
  else delete updated.simslimManaged;
438
- setDevice(projectPath, "ios", updated);
455
+ setDevice(projectPath, "ios", updated, slot);
439
456
  return updated;
440
457
  }
441
- function findOtherProjectOwningAvd(avdName, projectPath) {
458
+ function findOtherProjectOwningAvd(avdName, projectPath, selectedSlot = "default") {
442
459
  const cfg = loadConfig();
443
- for (const [path, proj] of Object.entries(cfg?.projects || {})) {
444
- if (path === projectPath) continue;
445
- if (proj?.platforms?.android?.avdName === avdName) return path;
446
- }
460
+ for (const [path, proj] of Object.entries(cfg?.projects || {})) if (projectDeviceSlots(proj).some(({ slot, platforms }) => (path !== projectPath || slot !== selectedSlot) && platforms.android?.avdName?.toLowerCase() === avdName.toLowerCase())) return path;
447
461
  return null;
448
462
  }
449
- var AvdRecoveryError = class extends Error {
463
+ var AvdBootError = class extends Error {
450
464
  remedy;
451
465
  constructor(message, remedy, cause) {
452
466
  super(message, { cause });
453
467
  this.remedy = remedy;
454
468
  }
455
469
  };
456
- async function ensureOwnedAndroidDevice({ record, projectPath, settingsRoot, label, settings, flags, note, out, logFile, alive, configureAvd, teardownAvd }) {
470
+ var AvdRecoveryError = class extends AvdBootError {};
471
+ async function ensureOwnedAndroidDevice({ record, projectPath, slot, settingsRoot, label, settings, flags, note, out, logFile, alive, configureAvd, teardownAvd }) {
457
472
  const avdConfig = androidAvdConfigSetting(settings, settingsRoot);
458
473
  const configuration = avdPoolConfiguration(androidDataPartitionSizeGbSetting(settings), avdConfig);
459
474
  if (record?.setupIncomplete && record.avdName) {
@@ -462,7 +477,7 @@ async function ensureOwnedAndroidDevice({ record, projectPath, settingsRoot, lab
462
477
  owner: { projectPath }
463
478
  });
464
479
  if (cleanup.status === "failed" || cleanup.status === "skipped") throw new Error(`Owned AVD ${record.avdName} has incomplete setup and could not be deleted (${cleanup.reason || cleanup.status}). Fix the cause, then retry; Stim kept the device record for cleanup.`);
465
- clearDevice(projectPath, "android");
480
+ clearDevice(projectPath, "android", slot);
466
481
  record = null;
467
482
  }
468
483
  if (record?.avdName) {
@@ -479,7 +494,7 @@ async function ensureOwnedAndroidDevice({ record, projectPath, settingsRoot, lab
479
494
  owned: true,
480
495
  deviceName: record.deviceName ?? record.avdName
481
496
  };
482
- setDevice(projectPath, "android", updated);
497
+ setDevice(projectPath, "android", updated, slot);
483
498
  return {
484
499
  ...updated,
485
500
  systemImage: ownedAvdSystemImage(record.avdName)
@@ -491,6 +506,7 @@ async function ensureOwnedAndroidDevice({ record, projectPath, settingsRoot, lab
491
506
  avdName: record.avdName,
492
507
  metadata: record,
493
508
  projectPath,
509
+ slot,
494
510
  deviceName: record.deviceName,
495
511
  out,
496
512
  logFile,
@@ -543,6 +559,7 @@ async function ensureOwnedAndroidDevice({ record, projectPath, settingsRoot, lab
543
559
  if (!adoptParked({
544
560
  platform: "android",
545
561
  projectPath,
562
+ slot,
546
563
  udid: parked.udid,
547
564
  device: adopted
548
565
  })) continue;
@@ -550,6 +567,7 @@ async function ensureOwnedAndroidDevice({ record, projectPath, settingsRoot, lab
550
567
  ...await bootOwnedAvdOnFreshPort({
551
568
  avdName: parked.name,
552
569
  projectPath,
570
+ slot,
553
571
  metadata: adopted,
554
572
  out,
555
573
  logFile,
@@ -564,9 +582,9 @@ async function ensureOwnedAndroidDevice({ record, projectPath, settingsRoot, lab
564
582
  let fresh = false;
565
583
  try {
566
584
  created = withConfigLock(() => {
567
- const current = loadConfig()?.projects?.[projectPath]?.platforms?.android;
585
+ const current = deviceSlotPlatforms(loadConfig()?.projects?.[projectPath], slot)?.android;
568
586
  if (current?.avdName && current.avdName !== record?.avdName) throw new Error(`Another Stim run assigned AVD ${current.avdName} to this workspace. Retry to use it.`);
569
- if (readParked("android").some((entry) => entry.name === ownedAvdName(label)) || findOtherProjectOwningAvd(ownedAvdName(label), projectPath)) label = `${label}-${randomUUID().slice(0, 8)}`;
587
+ if (readParked("android").some((entry) => entry.name === ownedAvdName(label)) || findOtherProjectOwningAvd(ownedAvdName(label), projectPath, slot)) label = `${label}-${randomUUID().slice(0, 8)}`;
570
588
  const result = createOwnedAvd(label, { systemImage: flags.systemImage || settings.android?.systemImage });
571
589
  setDevice(projectPath, "android", {
572
590
  avdName: result.avdName,
@@ -574,7 +592,7 @@ async function ensureOwnedAndroidDevice({ record, projectPath, settingsRoot, lab
574
592
  deviceName: result.avdName,
575
593
  setupIncomplete: true,
576
594
  poolConfiguration: configuration
577
- });
595
+ }, slot);
578
596
  return result;
579
597
  });
580
598
  fresh = true;
@@ -586,9 +604,9 @@ async function ensureOwnedAndroidDevice({ record, projectPath, settingsRoot, lab
586
604
  if (!listAvds().includes(avdName)) throw new AvdRecoveryError(`AVD ${avdName} already exists on disk but is not listed by the emulator. ${message}`, "Run `npx stim gc` to inspect orphaned owned AVDs, then `npx stim gc --delete` to reclaim those safe to delete. Retry `stim android` after cleanup; keep any AVD that GC cannot verify.", e);
587
605
  created = withConfigLock(() => {
588
606
  if (readParked("android").some((entry) => entry.name === avdName)) throw new Error(`AVD ${avdName} was parked by another Stim run. Retry to adopt it safely.`, { cause: e });
589
- const owner = findOtherProjectOwningAvd(avdName, projectPath);
607
+ const owner = findOtherProjectOwningAvd(avdName, projectPath, slot);
590
608
  if (owner) throw new Error(`AVD ${avdName} already exists and is owned by another project (${owner}). Retry to allocate a distinct owned emulator.`, { cause: e });
591
- const current = loadConfig()?.projects?.[projectPath]?.platforms?.android;
609
+ const current = deviceSlotPlatforms(loadConfig()?.projects?.[projectPath], slot)?.android;
592
610
  if (current?.avdName) {
593
611
  const state = current.setupIncomplete ? "has incomplete setup" : "was registered";
594
612
  throw new Error(`AVD ${current.avdName} ${state} by another concurrent Stim run. Retry after that run finishes so the recorded device is resolved safely.`, { cause: e });
@@ -602,7 +620,7 @@ async function ensureOwnedAndroidDevice({ record, projectPath, settingsRoot, lab
602
620
  deviceName: avdName,
603
621
  ...resolved.serial ? { consolePort: Number(resolved.serial.replace(/^emulator-/, "")) } : {}
604
622
  };
605
- setDevice(projectPath, "android", recovered);
623
+ setDevice(projectPath, "android", recovered, slot);
606
624
  return {
607
625
  ...recovered,
608
626
  systemImage: ownedAvdSystemImage(avdName),
@@ -633,7 +651,7 @@ async function ensureOwnedAndroidDevice({ record, projectPath, settingsRoot, lab
633
651
  owner: { projectPath }
634
652
  });
635
653
  const kept = cleanup.status === "failed" || cleanup.status === "skipped";
636
- if (!kept) clearDevice(projectPath, "android");
654
+ if (!kept) clearDevice(projectPath, "android", slot);
637
655
  const orphan = kept ? ` The owned AVD remains tracked for cleanup (${cleanup.reason || cleanup.status}); fix the cause, then retry or run \`stim gc --delete\`.` : "";
638
656
  throw new Error(`Created owned AVD ${created.avdName}, but could not configure its AVD settings: ${String(error?.message || error)}${orphan}`, { cause: error });
639
657
  }
@@ -642,6 +660,7 @@ async function ensureOwnedAndroidDevice({ record, projectPath, settingsRoot, lab
642
660
  avdName: created.avdName,
643
661
  metadata: fresh ? { poolConfiguration: configuration } : void 0,
644
662
  projectPath,
663
+ slot,
645
664
  deviceName: created.avdName,
646
665
  out,
647
666
  logFile,
@@ -651,7 +670,7 @@ async function ensureOwnedAndroidDevice({ record, projectPath, settingsRoot, lab
651
670
  systemImage: created.systemImage
652
671
  };
653
672
  }
654
- function claimAndroidConsolePort({ projectPath, avdName, deviceName, livePorts = [], metadata }, { lock = withConfigLock, recordedPorts = () => allConsolePortsAndSerials().androidConsolePorts, record = setDevice } = {}) {
673
+ function claimAndroidConsolePort({ projectPath, slot, avdName, deviceName, livePorts = [], metadata }, { lock = withConfigLock, recordedPorts = () => allConsolePortsAndSerials().androidConsolePorts, record = setDevice } = {}) {
655
674
  return lock(() => {
656
675
  const consolePort = nextConsolePort([...recordedPorts(), ...livePorts]);
657
676
  const claim = {
@@ -662,7 +681,7 @@ function claimAndroidConsolePort({ projectPath, avdName, deviceName, livePorts =
662
681
  owned: true,
663
682
  deviceName: deviceName ?? avdName
664
683
  };
665
- record(projectPath, "android", claim);
684
+ record(projectPath, "android", claim, slot);
666
685
  return claim;
667
686
  });
668
687
  }
@@ -670,9 +689,10 @@ function liveAndroidConsolePorts() {
670
689
  const adbLive = listAdbDevices();
671
690
  return [...adbLive.emulators.map((e) => e.consolePort), ...adbLive.unhealthy.map((u) => u.consolePort).filter((p) => p != null)];
672
691
  }
673
- async function bootOwnedAvdOnFreshPort({ avdName, metadata, projectPath, deviceName, out, logFile = null, alive = pidExists }) {
692
+ async function bootOwnedAvdOnFreshPort({ avdName, metadata, projectPath, slot, deviceName, out, logFile = null, alive = pidExists }) {
674
693
  const claim = claimAndroidConsolePort({
675
694
  projectPath,
695
+ slot,
676
696
  avdName,
677
697
  deviceName,
678
698
  livePorts: liveAndroidConsolePorts(),
@@ -680,10 +700,17 @@ async function bootOwnedAvdOnFreshPort({ avdName, metadata, projectPath, deviceN
680
700
  });
681
701
  const serial = `emulator-${claim.consolePort}`;
682
702
  try {
703
+ reportAndroidMemoryPressure(out);
683
704
  const pid = bootAndroidEmulator(avdName, claim.consolePort, { logFile });
684
705
  out(chalk.dim(phaseLine("device", `waiting for ${serial} to finish booting`)));
685
- const result = await waitForBoot(serial, 12e4, { aborted: emulatorGone(pid, alive) });
686
- if (!result.ok) throw new Error(`${bootFailurePrefix(serial, result.exited, 12e4)} Diagnostic: ${JSON.stringify(result.diagnostic)}`);
706
+ const result = await waitForAndroidBoot({
707
+ serial,
708
+ timeoutMs: 12e4,
709
+ pid,
710
+ alive,
711
+ out
712
+ });
713
+ if (result.failed) throw new AvdBootError(result.reason, result.remedy);
687
714
  const running = getAvdNameForSerial(serial);
688
715
  if (running && running !== avdName) throw new Error(`${serial} is running AVD ${running}, not this workspace's owned AVD ${avdName}; refusing to use it.`);
689
716
  return {
@@ -691,40 +718,83 @@ async function bootOwnedAvdOnFreshPort({ avdName, metadata, projectPath, deviceN
691
718
  serial
692
719
  };
693
720
  } catch (error) {
694
- releaseAndroidConsolePort(projectPath, claim.consolePort);
721
+ releaseAndroidConsolePort(projectPath, claim.consolePort, slot);
695
722
  throw error;
696
723
  }
697
724
  }
698
- function emulatorGone(pid, alive) {
699
- if (!pid) return () => false;
700
- return () => !alive(pid);
725
+ function reportAndroidMemoryPressure(out) {
726
+ const pressure = readHostMemoryPressure();
727
+ if (pressure === "warning" || pressure === "critical") out(chalk.dim(phaseLine("memory", `macOS reports ${pressure} host memory pressure; emulator boot may be slow. Free memory with \`stim stop\` only in workspaces you own; ask before closing other apps or devices.`)));
701
728
  }
702
- function bootFailurePrefix(serial, exited, timeoutMs) {
703
- return exited ? `The emulator process for ${serial} exited before the device finished booting.` : `Emulator ${serial} did not finish booting within ${Math.round(timeoutMs / 1e3)}s.`;
729
+ function androidBootRemedy(pressure) {
730
+ let count = null;
731
+ try {
732
+ count = liveOwnedDeviceCount({
733
+ sims: process.platform === "darwin" ? listAllIosSims({ timeoutMs: 2e3 }) : [],
734
+ adbEmulators: listAdbDevices({ timeoutMs: 2e3 }).emulators,
735
+ config: loadConfig()
736
+ });
737
+ } catch {}
738
+ const observation = pressure === "warning" || pressure === "critical" ? `macOS reports ${pressure} host memory pressure. ` : "";
739
+ const devices = count ? `${count} Stim-owned ${count === 1 ? "device is" : "devices are"} running. ` : "";
740
+ if (observation || devices) return `${observation}${devices}Stop an unneeded device with \`stim stop\` only in a workspace you own, then run \`stim android\` again; ask before closing other apps or devices. These observations do not establish the cause of the timeout or an OOM crash.`;
741
+ return "Inspect the emulator log and run `stim doctor` to check JAVA_HOME, ANDROID_HOME, and installed system images, then run `stim android` again.";
742
+ }
743
+ async function waitForAndroidBoot({ serial, timeoutMs, pid = null, alive = pidExists, out }) {
744
+ const aborted = () => pid !== null && !alive(pid);
745
+ const wait = (windowMs) => waitForBoot(serial, windowMs, {
746
+ aborted,
747
+ commandTimeoutMs: 5e3
748
+ });
749
+ let result = await wait(timeoutMs);
750
+ if (result.ok) return {
751
+ ok: true,
752
+ serial
753
+ };
754
+ let pressure = readHostMemoryPressure();
755
+ let elapsedMs = timeoutMs;
756
+ if (!result.exited && pid !== null && !aborted() && (pressure === "warning" || pressure === "critical")) {
757
+ const extensionMs = 24e4;
758
+ out(chalk.dim(phaseLine("memory", `macOS reports ${pressure} host memory pressure; retrying the boot wait once for the same live emulator ${serial} (up to ${extensionMs / 1e3}s more).`)));
759
+ result = await wait(extensionMs);
760
+ if (result.ok) return {
761
+ ok: true,
762
+ serial
763
+ };
764
+ elapsedMs += extensionMs;
765
+ pressure = readHostMemoryPressure();
766
+ }
767
+ const exited = result.exited || aborted();
768
+ return {
769
+ failed: true,
770
+ reason: `${exited ? `The emulator process for ${serial} exited before the device finished booting.` : `Emulator ${serial} did not finish booting within ${Math.round(elapsedMs / 1e3)}s.`} Diagnostic: ${JSON.stringify(result.diagnostic)}`,
771
+ remedy: exited ? "Inspect the emulator log for the process exit, fix what it reports, then run `stim android` again." : androidBootRemedy(pressure)
772
+ };
704
773
  }
705
774
  function liveOwnedDeviceCount({ sims = [], adbEmulators = [], config = null } = {}) {
706
775
  let count = 0;
707
776
  for (const sim of sims) if (sim?.state === "Booted" && sim.name?.startsWith("stim-")) count++;
708
777
  const livePorts = new Set(adbEmulators.map((e) => e.consolePort));
709
- for (const proj of Object.values(config?.projects || {})) {
710
- const android = proj?.platforms?.android;
778
+ for (const proj of Object.values(config?.projects || {})) for (const { platforms } of projectDeviceSlots(proj)) {
779
+ const android = platforms.android;
711
780
  if (android?.owned && android.avdName && typeof android.consolePort === "number" && livePorts.has(android.consolePort)) count++;
712
781
  }
713
782
  return count;
714
783
  }
715
- function workspaceHasLiveDevice({ platform, project, sims = [], adbEmulators = [] } = {}) {
784
+ function workspaceHasLiveDevice({ platform, project, slot = "default", sims = [], adbEmulators = [] } = {}) {
716
785
  if (!platform) return false;
717
- const record = project?.platforms?.[platform];
786
+ const record = deviceSlotPlatforms(project, slot)?.[platform];
718
787
  if (!record) return false;
719
788
  if (platform === "ios") return sims.some((s) => s.udid === record.deviceUdid && s.state === "Booted");
720
789
  return typeof record.consolePort === "number" && adbEmulators.some((e) => e.consolePort === record.consolePort);
721
790
  }
722
- function deviceCapacityRefusal({ platform, project, max, sims = [], adb = null, config = null } = {}) {
791
+ function deviceCapacityRefusal({ platform, project, slot = "default", max, sims = [], adb = null, config = null } = {}) {
723
792
  if (!max || max <= 0) return null;
724
793
  const adbEmulators = adb?.emulators || [];
725
794
  if (workspaceHasLiveDevice({
726
795
  platform,
727
796
  project,
797
+ slot,
728
798
  sims,
729
799
  adbEmulators
730
800
  })) return null;
@@ -740,7 +810,7 @@ function deviceCapacityRefusal({ platform, project, max, sims = [], adb = null,
740
810
  remedy: "stop an environment (stim stop) or raise concurrency.maxDevices"
741
811
  };
742
812
  }
743
- function checkDeviceCapacity({ platform, project, max, sims = listAllIosSims, adb = listAdbDevices, config = loadConfig } = {}) {
813
+ function checkDeviceCapacity({ platform, project, slot = "default", max, sims = listAllIosSims, adb = listAdbDevices, config = loadConfig } = {}) {
744
814
  if (!max || max <= 0) return null;
745
815
  let simList = [];
746
816
  let adbRes = {
@@ -761,6 +831,7 @@ function checkDeviceCapacity({ platform, project, max, sims = listAllIosSims, ad
761
831
  return deviceCapacityRefusal({
762
832
  platform,
763
833
  project,
834
+ slot,
764
835
  max,
765
836
  sims: simList,
766
837
  adb: adbRes,
@@ -839,7 +910,8 @@ async function ensureIosBooted({ device, timeoutMs, pollMs, out }) {
839
910
  "simctl",
840
911
  "spawn",
841
912
  udid,
842
- "/usr/bin/true"
913
+ "launchctl",
914
+ "list"
843
915
  ], { timeoutMs: 3e4 });
844
916
  return {
845
917
  ok: true,
@@ -886,7 +958,11 @@ async function ensureIosBooted({ device, timeoutMs, pollMs, out }) {
886
958
  out(chalk.dim(phaseLine("device", `booting ${sim.name} (${udid})`)));
887
959
  const bootDeadline = Date.now() + timeoutMs;
888
960
  try {
889
- await bootIosSim(udid, { timeoutMs });
961
+ await bootIosSim(udid, {
962
+ timeoutMs,
963
+ label: sim.name,
964
+ out
965
+ });
890
966
  } catch (e) {
891
967
  return {
892
968
  failed: true,
@@ -929,30 +1005,19 @@ async function ensureAndroidBooted({ device, timeoutMs, out, logFile = null, ali
929
1005
  failed: true,
930
1006
  reason: `AVD ${device.avdName} is not Stim-owned by name; refusing to boot it.`
931
1007
  };
932
- if (resolved.serial) {
933
- const ready = await waitForBoot(resolved.serial, timeoutMs);
934
- if (!ready.ok) return {
935
- failed: true,
936
- reason: `Emulator ${resolved.serial} never reported boot completion. Diagnostic: ${JSON.stringify(ready.diagnostic)}`
937
- };
938
- return {
939
- ok: true,
940
- serial: resolved.serial
941
- };
942
- }
1008
+ if (resolved.serial) return waitForAndroidBoot({
1009
+ serial: resolved.serial,
1010
+ timeoutMs,
1011
+ out
1012
+ });
943
1013
  const freshSerial = `emulator-${device.consolePort}`;
944
- if (device.owned && device.serial === freshSerial) {
945
- const ready = await waitForBoot(freshSerial, timeoutMs);
946
- if (ready.ok) return {
947
- ok: true,
948
- serial: freshSerial
949
- };
950
- return {
951
- failed: true,
952
- reason: `Emulator ${freshSerial} never reported boot completion. Diagnostic: ${JSON.stringify(ready.diagnostic)}`
953
- };
954
- }
1014
+ if (device.owned && device.serial === freshSerial) return waitForAndroidBoot({
1015
+ serial: freshSerial,
1016
+ timeoutMs,
1017
+ out
1018
+ });
955
1019
  const serial = `emulator-${pickConsolePort(device.consolePort)}`;
1020
+ reportAndroidMemoryPressure(out);
956
1021
  out(chalk.dim(phaseLine("device", `booting ${device.avdName} as ${serial}`)));
957
1022
  let pid = null;
958
1023
  try {
@@ -963,15 +1028,13 @@ async function ensureAndroidBooted({ device, timeoutMs, out, logFile = null, ali
963
1028
  reason: `Could not start emulator for AVD ${device.avdName}: ${e?.message || e}`
964
1029
  };
965
1030
  }
966
- const ready = await waitForBoot(serial, timeoutMs, { aborted: emulatorGone(pid, alive) });
967
- if (!ready.ok) return {
968
- failed: true,
969
- reason: `${bootFailurePrefix(serial, ready.exited, timeoutMs)} Diagnostic: ${JSON.stringify(ready.diagnostic)}`
970
- };
971
- return {
972
- ok: true,
973
- serial
974
- };
1031
+ return waitForAndroidBoot({
1032
+ serial,
1033
+ timeoutMs,
1034
+ pid,
1035
+ alive,
1036
+ out
1037
+ });
975
1038
  }
976
1039
  function pickConsolePort(recorded) {
977
1040
  const live = liveAndroidConsolePorts();
@@ -1829,4 +1892,4 @@ async function buildIos({ root, udid = null, logWriter, project = null, scheme =
1829
1892
  };
1830
1893
  }
1831
1894
  //#endregion
1832
- export { unknownAndroidSystemImageRefusal as C, simslimIsOnPath as E, liveOwnedDeviceCount as S, unknownIosRuntimeRefusal as T, AvdRecoveryError as _, ccacheEnabled as a, ensureBooted as b, discoverXcodeProject as c, resolveScheme as d, startBuildHeartbeat as f, podInstallCommand as g, bundlerPin as h, buildIos as i, readBundleExecutable as l, describeDiagnostic as m, COMPILATION_CACHE_UNAVAILABLE as n, compilationCacheActivityLine as o, tailLines as p, HEARTBEAT_INTERVAL_MS as r, detectXcodeMajor as s, COMPILATION_CACHE_NOT_RUN as t, readBundleId as u, checkDeviceCapacity as v, unknownIosDeviceTypeRefusal as w, ensureOwnedDevice as x, clearIosAdoptionPending as y };
1895
+ export { liveOwnedDeviceCount as C, simslimIsOnPath as D, unknownIosRuntimeRefusal as E, ensureOwnedDevice as S, unknownIosDeviceTypeRefusal as T, AvdBootError as _, ccacheEnabled as a, clearIosAdoptionPending as b, discoverXcodeProject as c, resolveScheme as d, startBuildHeartbeat as f, podInstallCommand as g, bundlerPin as h, buildIos as i, readBundleExecutable as l, describeDiagnostic as m, COMPILATION_CACHE_UNAVAILABLE as n, compilationCacheActivityLine as o, tailLines as p, HEARTBEAT_INTERVAL_MS as r, detectXcodeMajor as s, COMPILATION_CACHE_NOT_RUN as t, readBundleId as u, AvdRecoveryError as v, unknownAndroidSystemImageRefusal as w, ensureBooted as x, checkDeviceCapacity as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stim",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Stim: fast, isolated React Native environments for coding agents",
5
5
  "keywords": [
6
6
  "agent",
@@ -42,16 +42,16 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@expo/fingerprint": "^0.20.10",
45
- "@stim-cli/cache": "^1.2.0",
46
- "@stim-cli/core": "^1.2.0",
47
- "@stim-cli/metro": "^1.2.0",
45
+ "@stim-cli/cache": "^1.3.1",
46
+ "@stim-cli/core": "^1.3.1",
47
+ "@stim-cli/metro": "^1.3.1",
48
48
  "chalk": "^5.4.1",
49
49
  "commander": "^13.1.0",
50
50
  "unique-pid": "0.1.1",
51
51
  "ws": "^8.21.3"
52
52
  },
53
53
  "devDependencies": {
54
- "@stim-cli/expo-build-cache": "1.2.0",
54
+ "@stim-cli/expo-build-cache": "1.3.1",
55
55
  "@types/ws": "^8.18.1"
56
56
  },
57
57
  "engines": {