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
package/README.md CHANGED
@@ -44,8 +44,9 @@ stim stop
44
44
 
45
45
  For `stim logs --errors`, a clean check requires exit code 0 and no matching
46
46
  errors in the captured logs. Exit code 0 alone means the query succeeded, even
47
- when it prints errors; an empty result does not prove launch or log capture
48
- succeeded.
47
+ when it prints errors. A workspace that has never produced a log timeline
48
+ refuses with `STIM_NO_PROJECT`; in a monorepo, the error names the nearest
49
+ registered descendant app with logs when one exists.
49
50
 
50
51
  Use `stim start --reset-cache` to recover from stale Metro transforms or file-map
51
52
  state. It restarts only this app's verified owned Metro, keeping its port and
@@ -77,6 +78,17 @@ Stim builds or restores the app, installs it, launches it, and checks launch
77
78
  readiness. Plain output streams progress and reports the complete result. Use
78
79
  `--json` when a script needs structured data.
79
80
 
81
+ To use an existing EAS development build, run `stim ios --eas-profile ios-simulator`
82
+ or `stim android --eas-profile development` with your project's profile name.
83
+ The profile must be an internal development build. Add `--device` for a connected
84
+ phone; iOS requires `ios.simulator: true` for a simulator and false for a phone.
85
+ Stim downloads a matching native build and connects it to the workspace's Metro.
86
+ EAS CLI manages the downloaded artifact cache. An iOS provisioning failure
87
+ points to EAS device registration and rebuild commands.
88
+ On a miss it stops and prints the EAS build command, which may incur charges;
89
+ it never triggers the build automatically. See `stim guide lifecycle eas` for
90
+ setup, environment handling, and cache behavior.
91
+
80
92
  Use `stim ios --scheme "App Staging"` when a project has several shared Xcode
81
93
  app schemes. Combine it with `--configuration Release` if needed. Without the
82
94
  flag, Stim keeps its automatic scheme selection. Explicit schemes use separate
@@ -117,7 +129,7 @@ eligible `.env` and local configuration files. Existing entries are preserved;
117
129
  existing ignored directories such as `node_modules` are skipped whole.
118
130
 
119
131
  `stim worktree warm --refresh` brings the source checkout up to date first: it
120
- fetches, fast-forwards whatever branch is checked out there, and installs what
132
+ checks the upstream, fetches changes when needed, fast-forwards whatever branch is checked out there, and installs what
121
133
  the new commits moved before copying. It refuses a source checkout it cannot
122
134
  move (uncommitted tracked changes, a rebase or merge in progress, a detached
123
135
  HEAD, a diverged branch) and never switches branches. One lock per repository
@@ -167,3 +179,22 @@ Update programmatic imports from `stim-cli/cache-manifest` to
167
179
  `stim/cache-manifest`. The `@stim-cli/*` packages keep their names.
168
180
 
169
181
  MIT License.
182
+
183
+ ### Multiple devices in one workspace
184
+
185
+ Use a stable slot name for each simulator, emulator, or physical device. Slots
186
+ can use identical models and all share the workspace's Metro server and build
187
+ cache. Omit `--slot` to reuse your existing default device.
188
+
189
+ ```sh
190
+ stim ios --slot phone
191
+ stim ios --slot tablet --device-type "iPad Pro 13-inch (M4)"
192
+ stim ios --slot hardware --device <udid>
193
+ stim logs --slot tablet --source device
194
+ stim stop --slot tablet
195
+ ```
196
+
197
+ `stim status` shows every slot. Plain `stim stop` stops the whole workspace;
198
+ `--slot` preserves the shared server and other devices. `stim reload ios` or
199
+ `stim reload android` can reload multiple connected devices. Read
200
+ `stim guide lifecycle options` for launch verification and recycling behavior.
@@ -1,34 +1,35 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { C as withConfigLock, S as upsertProject, b as setDevice, f as getProject, g as loadConfig, l as getConcurrencyLimits, y as saveConfig } from "./config-CMtIaTk9.mjs";
3
- import { r as createNdjsonWriter } from "./ndjson-DcAtEx_K.mjs";
4
- import { D as workspaceLogsDir, T as workspaceDir, _ as writeWorkspaceState, g as writeWorkspaceLaunch, n as MODE_EXPO, p as readWorkspaceState, t as MODE_BARE, v as emulatorLogFile } from "./state-CZ-ER-8d.mjs";
5
- import { C as removeTemporaryEntry, S as makeTemporaryDirectory, g as repoRoot, s as gitCommonDir } from "./worktree-C79xx_kM.mjs";
6
- import { a as findProjectRoot, c as projectShortcut, i as detectIsExpo, n as detectAndroidPackage, r as detectBundleId, t as appProjectProblem } from "./project-BBTVABUz.mjs";
2
+ import { C as withConfigLock, I as deviceSlotKey, L as deviceSlotPlatforms, S as upsertProject, V as validateDeviceSlot, b as setDevice, f as getProject, g as loadConfig, l as getConcurrencyLimits, y as saveConfig } from "./config-CcefMY2Q.mjs";
3
+ import { r as createNdjsonWriter } from "./ndjson-BZwI1NBv.mjs";
4
+ import { D as workspaceLogsDir, T as workspaceDir, _ as writeWorkspaceState, g as writeWorkspaceLaunch, n as MODE_EXPO, p as readWorkspaceState, t as MODE_BARE, v as emulatorLogFile } from "./state-B3XA2wp2.mjs";
5
+ import { C as removeTemporaryEntry, S as makeTemporaryDirectory, g as repoRoot, s as gitCommonDir } from "./worktree-Eq6TLH8N.mjs";
6
+ import { a as findProjectRoot, c as projectShortcut, i as detectIsExpo, n as detectAndroidPackage, r as detectBundleId, t as appProjectProblem } from "./project-fKmuIaMI.mjs";
7
7
  import { r as waitForChild, t as createLineReader } from "./process-output-C7IlIzd7.mjs";
8
- import { i as resolveProjectMetro, r as pidExists } from "./metro-COslRK_F.mjs";
9
- import { n as verifyCollectorOwnership, r as readCollectors } from "./ownership-CGPart6T.mjs";
10
- import { a as easAuthNote, c as loadProjectProvider, d as resolveEasCliBin, f as resolveRemote, i as checkEasAuth, n as UPLOAD_TIMEOUT_MS, o as exitAfterFlush, p as uploadRemote, r as cacheLevel, s as isEasAuthFailureText, t as RESOLVE_TIMEOUT_MS, x as parkedMaxSetting } from "./remote-cache-DDmJ1Roq.mjs";
11
- import { A as resolveSettings, B as artifactCachePolicy, D as remoteDeviceSettingError, E as remoteAndroidSetting, F as tunnelModeSetting, H as optimizationBuildProfile, I as unknownSettingKeys, N as settingShapeErrors, R as detectProviders, T as publicUrlSetting, U as resolveOptimizations, V as compilerCacheFallbackMessage, a as SETTING_SHAPE_REMEDY, d as cacheProviderSettingError, j as settingFile, k as resolveCacheProviderConfig, r as REMOTE_DEVICE_BACKENDS, s as androidAvdConfigSettingError, u as androidDataPartitionSizeGbSettingError, x as metroWarmupUrlSetting } from "./settings-DwU1_U_q.mjs";
8
+ import { i as resolveProjectMetro, r as pidExists } from "./metro-CORqXyTL.mjs";
9
+ import { n as verifyCollectorOwnership, r as readCollectors } from "./ownership-8O073GIC.mjs";
12
10
  import { _ as launchErrorPreview, c as launchErrorReport, g as stepTimer, h as stepClock, i as formatDuration, l as phaseLine, n as appReadinessMessage, p as shortHash } from "./command-output-8wHMEde0.mjs";
13
- import { D as verifyAndroidReleaseLaunch, E as unverifiedLaunchLines, O as verifyLaunch, S as launchAndroidReleaseApp, _ as installConflictKind, c as RELEASE_VERIFY_WAIT_MS, d as androidDevClientUrl, g as installAndroidApp, h as deviceShellArg, i as LAUNCH_BUNDLING, l as VERIFY_TIMEOUT_MS, n as DEFAULT_METRO_PORT, o as LAUNCH_FATAL, s as LAUNCH_UNVERIFIED, t as ADB_INSTALL_TIMEOUT_MS, u as androidAppProcess, x as launchAndroidApp } from "./app-install-NFxUahnH.mjs";
14
- import { C as untrackedNativeFiles, D as readAssetManifest, E as compareAssetManifests, S as untrackedMissLine, T as captureAssetManifest, _ as providerUploadOutcome, b as storeBuild, c as buildCacheKey, d as filesystemBuildCapability, f as fingerprintDiffRecord, g as providerDownloadPath, h as prepareProviderDownloadDir, i as releaseBuildLock, l as describeFingerprintMiss, m as fingerprintProject, n as acquireBuildLock, o as takeoverLine, p as fingerprintDiffSuffix, s as waitForBuild, t as WAIT_CEILING_MS, v as refingerprintAfterMutation, w as assetDiffReason, x as storedAssetManifest, y as resolveBuild } from "./build-lock-BsVoLDaq.mjs";
15
- import { r as claimFailure } from "./ownership-claim-7zRxxTfZ.mjs";
16
- import { a as lostLine, c as releaseLeaseOnSignal, i as leaseExpiryText, l as runLease, n as DEBUG_VERIFY_STEP_MS, o as lostRefusal, r as acquireRunLease, s as parseDeviceWait, t as selectFromPool, u as waitFlagConflict } from "./device-pool-B5pEVa3t.mjs";
17
- import { r as releaseBuildSlot, t as acquireBuildSlot } from "./build-slots-D0b6sJyo.mjs";
18
- import { c as ensureWorkspaceStorageSafely, d as noMetroMessage, f as noMetroRemedy, h as warmMetro, i as detectEntryFile, n as pickDevClientScheme, o as needsPrebuild, p as resolveMetroWithRetry, s as runPrebuild, t as devClientScheme, u as launchOutcomeRecord } from "./dev-client-RpvXxCXV.mjs";
19
- import { a as statsProjectKey, i as recordRunStats, n as readRunEstimates, t as createRunRecorder } from "./stats-BCWEVLrM.mjs";
20
- import { C as memoizeEmulatorProbe, L as waitForBoot, M as resetAdoptedAvd, N as resolveOwnedAvdSerial, P as resolvePhysicalDevice, _ as findBuildTool, a as androidSystemImageAbi, g as extractEmulatorFailure, h as emulatorFailureRemedy, i as androidPoolNoCandidatesRefusal, j as probeEmulatorSerial, k as physicalDeviceModel, m as emulatorDiskSpaceRemedy, n as androidHome, r as androidPoolCandidates, t as androidDeviceAbi, x as listInstalledSystemImages, y as listAdbDevices } from "./android-K63_0UfF.mjs";
21
- import { C as unknownAndroidSystemImageRefusal, _ as AvdRecoveryError, b as ensureBooted, f as startBuildHeartbeat, p as tailLines, r as HEARTBEAT_INTERVAL_MS, v as checkDeviceCapacity, x as ensureOwnedDevice } from "./xcode-DeKCribl.mjs";
22
- import { a as ensureMetroReachable, b as ownedSessionName, l as resolveRemoteContext, n as binOnPath, o as ensureRemoteBootOwned, s as remoteAndroidDeps } from "./device-remote-E81KAdXf.mjs";
23
- import { t as cleanLine } from "./server-expo-C3ktAwUG.mjs";
24
- import { a as buildAndroid, c as CCACHE_NOT_RUN, f as resolveCcache, l as CCACHE_UNAVAILABLE, n as resolveAndroidCas, o as productFlavorRefusal, p as formatDiagnostic, r as resolveAndroidCompilerCache, s as readProductFlavors, u as ccacheActivityLine } from "./android-cas-D6HPZ6UQ.mjs";
11
+ import { r as claimFailure } from "./ownership-claim-BtPtJVmq.mjs";
12
+ import { $ as providerDownloadPath, C as memoizeEmulatorProbe, G as buildCacheKey, J as filesystemBuildCapability, K as describeFingerprintMiss, L as waitForBoot, M as resetAdoptedAvd, N as resolveOwnedAvdSerial, P as resolvePhysicalDevice, Q as prepareProviderDownloadDir, R as WAIT_CEILING_MS, U as takeoverLine, V as releaseBuildLock, W as waitForBuild, X as fingerprintDiffSuffix, Y as fingerprintDiffRecord, Z as fingerprintProject, _ as findBuildTool, a as androidSystemImageAbi, at as untrackedMissLine, ct as captureAssetManifest, et as providerUploadOutcome, g as extractEmulatorFailure, h as emulatorFailureRemedy, i as androidPoolNoCandidatesRefusal, it as storedAssetManifest, j as probeEmulatorSerial, k as physicalDeviceModel, lt as compareAssetManifests, m as emulatorDiskSpaceRemedy, n as androidHome, nt as resolveBuild, ot as untrackedNativeFiles, r as androidPoolCandidates, rt as storeBuild, st as assetDiffReason, t as androidDeviceAbi, tt as refingerprintAfterMutation, ut as readAssetManifest, x as listInstalledSystemImages, y as listAdbDevices, z as acquireBuildLock } from "./android-hMj83VpN.mjs";
13
+ import { A as resolveSettings, B as artifactCachePolicy, D as remoteDeviceSettingError, E as remoteAndroidSetting, F as tunnelModeSetting, H as optimizationBuildProfile, I as unknownSettingKeys, N as settingShapeErrors, R as detectProviders, T as publicUrlSetting, U as resolveOptimizations, V as compilerCacheFallbackMessage, a as SETTING_SHAPE_REMEDY, d as cacheProviderSettingError, j as settingFile, k as resolveCacheProviderConfig, r as REMOTE_DEVICE_BACKENDS, s as androidAvdConfigSettingError, u as androidDataPartitionSizeGbSettingError, x as metroWarmupUrlSetting } from "./settings-DEJf6_kJ.mjs";
14
+ import { t as cleanLine } from "./server-expo-DizT4OMk.mjs";
15
+ import { C as uploadRemote, S as resolveRemote, _ as isEasAuthFailureText, c as parkedMaxSetting, d as RESOLVE_TIMEOUT_MS, f as UPLOAD_TIMEOUT_MS, g as exitAfterFlush, h as easAuthNote, m as checkEasAuth, p as cacheLevel, v as loadProjectProvider, x as resolveEasCliBin } from "./teardown--gESs9ck.mjs";
16
+ import { c as ensureWorkspaceStorageSafely, d as launchOutcomeRecord, f as noMetroMessage, g as warmMetro, i as detectEntryFile, m as resolveMetroWithRetry, n as pickDevClientScheme, o as needsPrebuild, p as noMetroRemedy, s as runPrebuild, t as devClientScheme, u as isPhysicalDeviceRequest, v as isEasBuildFailure, y as resolveEasDevelopmentBuild } from "./dev-client-VfSZNLT4.mjs";
17
+ import { t as withWorkspaceProcessLock } from "./workspace-process-lock-IBj1Towr.mjs";
18
+ import { D as verifyAndroidReleaseLaunch, E as unverifiedLaunchLines, O as verifyLaunch, S as launchAndroidReleaseApp, _ as installConflictKind, c as RELEASE_VERIFY_WAIT_MS, d as androidDevClientUrl, g as installAndroidApp, h as deviceShellArg, i as LAUNCH_BUNDLING, l as VERIFY_TIMEOUT_MS, n as DEFAULT_METRO_PORT, o as LAUNCH_FATAL, s as LAUNCH_UNVERIFIED, t as ADB_INSTALL_TIMEOUT_MS, u as androidAppProcess, x as launchAndroidApp } from "./app-install-DIp9riNx.mjs";
19
+ import { a as lostLine, c as releaseLeaseOnSignal, i as leaseExpiryText, l as runLease, n as DEBUG_VERIFY_STEP_MS, o as lostRefusal, r as acquireRunLease, s as parseDeviceWait, t as selectFromPool, u as waitFlagConflict } from "./device-pool-CcBNTvM7.mjs";
20
+ import { r as releaseBuildSlot, t as acquireBuildSlot } from "./build-slots-C4AouyNz.mjs";
21
+ import { a as statsProjectKey, i as recordRunStats, n as readRunEstimates, t as createRunRecorder } from "./stats-BR-kwj0B.mjs";
22
+ import { S as ensureOwnedDevice, _ as AvdBootError, f as startBuildHeartbeat, p as tailLines, r as HEARTBEAT_INTERVAL_MS, v as AvdRecoveryError, w as unknownAndroidSystemImageRefusal, x as ensureBooted, y as checkDeviceCapacity } from "./xcode-xnxyvJkD.mjs";
23
+ import { a as ensureMetroReachable, b as ownedSessionName, l as resolveRemoteContext, n as binOnPath, o as ensureRemoteBootOwned, s as remoteAndroidDeps } from "./device-remote-DAA0Zm6Z.mjs";
24
+ import { a as buildAndroid, c as CCACHE_NOT_RUN, f as resolveCcache, l as CCACHE_UNAVAILABLE, n as resolveAndroidCas, o as productFlavorRefusal, p as formatDiagnostic, r as resolveAndroidCompilerCache, s as readProductFlavors, u as ccacheActivityLine } from "./android-cas-DXorPCi_.mjs";
25
+ import { n as nativeRunCommand, t as launchSlotScope } from "./slot-launch-CZklomj6.mjs";
25
26
  import { t as spawnEntry } from "./spawn-entry-nCE6m770.mjs";
26
- import { a as printNativeCrashReport, r as captureNativeCrashes, t as errorDiagnostics } from "./error-diagnostics-Dw8Gn3yw.mjs";
27
+ import { a as printNativeCrashReport, r as captureNativeCrashes, t as errorDiagnostics } from "./error-diagnostics-DwhNBFDU.mjs";
27
28
  import { existsSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync } from "node:fs";
28
29
  import { InvalidArgumentError } from "commander";
29
30
  import { basename, dirname, join, relative } from "node:path";
30
- import chalk from "chalk";
31
31
  import { createWarnOnce, loadCacheProvider, resolveTieredBuild, storeTieredBuild } from "@stim-cli/cache";
32
+ import chalk from "chalk";
32
33
  //#region src/engine/apk-swap.ts
33
34
  const ANDROID_BUNDLE_NAME = "index.android.bundle";
34
35
  const ANDROID_BUNDLE_ENTRY = `assets/${ANDROID_BUNDLE_NAME}`;
@@ -399,7 +400,12 @@ function androidSystemImageSetting(settings) {
399
400
  function resolveSystemImage(flag, settings) {
400
401
  return (typeof flag === "string" && flag.trim() !== "" ? flag.trim() : null) || androidSystemImageSetting(settings);
401
402
  }
402
- function systemImageRefusal({ flag, resolved, physical, remoteBackend, listImages }) {
403
+ function systemImageRefusal({ slot, flag, resolved, physical, remoteBackend, listImages }) {
404
+ if (slot && slot !== "default" && remoteBackend) return {
405
+ code: "STIM_BAD_ARG",
406
+ message: "Named slots currently support local simulators and physical devices.",
407
+ remedy: "Use the default slot for a remote session."
408
+ };
403
409
  if (typeof flag === "string" && flag.trim() === "") return {
404
410
  code: "STIM_BAD_ARG",
405
411
  message: "--system-image was given an empty id.",
@@ -624,9 +630,10 @@ function androidBuildOptions({ release, physical, device, variant, deviceAbi, co
624
630
  }
625
631
  //#endregion
626
632
  //#region src/commands/android/result.ts
627
- function androidFacts({ serial, avdName = null, deviceName = null, systemImage = null, fingerprint, cacheKey = null, variant = null, metroPort = null, cacheHit, cacheSkipped = false, waitedForBuild = null, appPath, bundleId, installSkipped = false, launched, ccache = CCACHE_UNAVAILABLE, logs, debugHttpHost = null, debugHttpHostNote = null, devClientUrl = null, durationMs, lease }) {
633
+ function androidFacts({ slot, serial, avdName = null, deviceName = null, systemImage = null, fingerprint, cacheKey = null, variant = null, metroPort = null, cacheHit, cacheSkipped = false, waitedForBuild = null, appPath, bundleId, installSkipped = false, launched, ccache = CCACHE_UNAVAILABLE, logs, debugHttpHost = null, debugHttpHostNote = null, devClientUrl = null, durationMs, lease }) {
628
634
  return {
629
635
  platform: PLATFORM,
636
+ ...slot && slot !== "default" ? { slot } : {},
630
637
  serial: serial ?? null,
631
638
  avdName: avdName ?? null,
632
639
  deviceName: deviceName ?? avdName ?? null,
@@ -689,7 +696,7 @@ async function finishAndroidUpload(uploadPending, remote, phase) {
689
696
  }
690
697
  return false;
691
698
  }
692
- function reportAndroidResult({ json, useBuildCache, variant, release, metroCheck, metroPort, logsDir, serial, apkPath, androidPackage, installSkipped, record, waitedForBuild, remote, providerName, launchState, launchWarning, launched, ccache, durationMs, writer, emit, lease, recordRun }) {
699
+ function reportAndroidResult({ slot, json, useBuildCache, variant, release, metroCheck, metroPort, logsDir, serial, apkPath, androidPackage, installSkipped, record, waitedForBuild, remote, providerName, launchState, launchWarning, launched, ccache, durationMs, writer, emit, lease, recordRun }) {
693
700
  recordRun({
694
701
  failed: false,
695
702
  cacheHit: cacheLevel(record.cacheHit),
@@ -697,6 +704,7 @@ function reportAndroidResult({ json, useBuildCache, variant, release, metroCheck
697
704
  durationMs
698
705
  });
699
706
  const facts = androidFacts({
707
+ slot,
700
708
  serial,
701
709
  avdName: record.avdName,
702
710
  deviceName: record.deviceName,
@@ -765,18 +773,18 @@ function persistLastBuild({ writeState, root, record, startedAt, durationMs, sta
765
773
  function collectorEntry() {
766
774
  return spawnEntry("collector-run");
767
775
  }
768
- function collectorLogFile(root) {
769
- return join(workspaceLogsDir(root), `collector-${PLATFORM}.log`);
776
+ function collectorLogFile(root, slot = "default") {
777
+ return join(workspaceLogsDir(root), `collector-${deviceSlotKey(PLATFORM, slot)}.log`);
770
778
  }
771
779
  const COLLECTOR_EXIT_WAIT_MS = 2e3;
772
780
  const COLLECTOR_POLL_MS = 25;
773
- function killPreviousCollector(root, { platform = PLATFORM, kill = (pid, signal) => process.kill(pid, signal), collectors = null, verify = verifyCollectorOwnership, isAlive = pidExists, note = (_line) => {} } = {}) {
774
- const record = (collectors ?? readCollectors(root))?.[platform];
781
+ function killPreviousCollector(root, { platform = PLATFORM, slot = "default", kill = (pid, signal) => process.kill(pid, signal), collectors = null, verify = verifyCollectorOwnership, isAlive = pidExists, note = (_line) => {} } = {}) {
782
+ const record = (collectors ?? readCollectors(root))?.[deviceSlotKey(platform, slot)];
775
783
  const pid = Number(record?.pid);
776
784
  if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid) return null;
777
785
  const ownership = verify({
778
786
  pid,
779
- platform,
787
+ platform: deviceSlotKey(platform, slot),
780
788
  root,
781
789
  isAlive,
782
790
  expected: record
@@ -793,8 +801,9 @@ function killPreviousCollector(root, { platform = PLATFORM, kill = (pid, signal)
793
801
  return null;
794
802
  }
795
803
  }
796
- async function startCollector({ root, serial, packageName, spawn, kill, alive = pidExists, verify = verifyCollectorOwnership, waitMs = COLLECTOR_EXIT_WAIT_MS, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), out }) {
804
+ async function startCollector({ root, slot = "default", serial, packageName, spawn, kill, alive = pidExists, verify = verifyCollectorOwnership, waitMs = COLLECTOR_EXIT_WAIT_MS, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), out }) {
797
805
  const previousPid = killPreviousCollector(root, {
806
+ slot,
798
807
  kill,
799
808
  isAlive: alive,
800
809
  verify,
@@ -807,7 +816,7 @@ async function startCollector({ root, serial, packageName, spawn, kill, alive =
807
816
  let stdio = "ignore";
808
817
  try {
809
818
  mkdirSync(workspaceLogsDir(root), { recursive: true });
810
- const fd = openSync(collectorLogFile(root), "a");
819
+ const fd = openSync(collectorLogFile(root, slot), "a");
811
820
  stdio = [
812
821
  "ignore",
813
822
  fd,
@@ -821,6 +830,7 @@ async function startCollector({ root, serial, packageName, spawn, kill, alive =
821
830
  PLATFORM,
822
831
  "--root",
823
832
  root,
833
+ ...slot === "default" ? [] : ["--slot", slot],
824
834
  "--serial",
825
835
  serial,
826
836
  "--package",
@@ -840,9 +850,14 @@ async function startCollector({ root, serial, packageName, spawn, kill, alive =
840
850
  }
841
851
  //#endregion
842
852
  //#region src/commands/android/launch.ts
843
- async function verifyAndroidRun({ root, release, remoteRelease, remoteDevice, verifyReleaseLaunched, verifyLaunched, serial, androidPackage, variant, metroCheck, logsDir, launchedAt, metroPort, isExpo, physical, device, scheme, component = null, phase }) {
853
+ async function verifyAndroidRun({ root, slot, release, remoteRelease, remoteDevice, verifyReleaseLaunched, verifyLaunched, serial, androidPackage, variant, metroCheck, logsDir, launchedAt, metroPort, isExpo, physical, device, scheme, component = null, phase }) {
854
+ const runCommand = nativeRunCommand("android", slot, {
855
+ physical,
856
+ deviceId: serial
857
+ });
844
858
  const readNativeCrashes = () => remoteDevice ? [] : captureNativeCrashes({
845
859
  root,
860
+ slot,
846
861
  platform: "android",
847
862
  deviceId: serial,
848
863
  appId: androidPackage,
@@ -878,6 +893,7 @@ async function verifyAndroidRun({ root, release, remoteRelease, remoteDevice, ve
878
893
  const verification = metroCheck ? await verifyLaunched({
879
894
  timeoutMs,
880
895
  requireBundleResponse: true,
896
+ slot: launchSlotScope(root, slot),
881
897
  onReadinessPending: () => phase("readiness", "waiting for app readiness (up to 30s after bundle load)"),
882
898
  logsDir,
883
899
  since: launchedAt,
@@ -909,10 +925,10 @@ async function verifyAndroidRun({ root, release, remoteRelease, remoteDevice, ve
909
925
  phase("verify", chalk.red(`FATAL after ${formatDuration(verification.waitedMs ?? 0)}: ${reason}`));
910
926
  for (const line of launchErrorPreview(verification.errors ?? [], root)) phase("", chalk.red(line));
911
927
  if (verification.processAlive === false && !verification.errors?.some((record) => record.event === "native_crash")) phase("logs", "No attributable native crash report captured yet. Run `stim logs --errors` again for delayed reports, or `stim logs --source device` for available output.");
912
- if (nativeFatal || verification.processAlive === false) phase("remedy", chalk.yellow(`Fix the crash, then restart the app: \`adb -s ${deviceShellArg(serial)} shell am force-stop ${deviceShellArg(androidPackage)}\`, then run \`stim android\` again. A crashed Android process can remain alive behind the system crash dialog; Metro reload cannot recover it.`));
928
+ if (nativeFatal || verification.processAlive === false) phase("remedy", chalk.yellow(`Fix the crash, then restart the app: \`adb -s ${deviceShellArg(serial)} shell am force-stop ${deviceShellArg(androidPackage)}\`, then run \`${runCommand}\` again. A crashed Android process can remain alive behind the system crash dialog; Metro reload cannot recover it.`));
913
929
  else if (verification.processAlive === true && metroPort !== null) {
914
930
  const reloadRemedy = physical ? `run \`agent-device metro reload --metro-port ${metroPort}\`.` : "run `stim reload android`.";
915
- phase("remedy", chalk.yellow(`The native app is still running. ${deliveryFailed ? "Check the Metro logs and device connection, then" : "Fix the JavaScript or TypeScript error, then"} ${reloadRemedy} Do not run \`stim android\` unless native inputs changed or the app process exits.`));
931
+ phase("remedy", chalk.yellow(`The native app is still running. ${deliveryFailed ? "Check the Metro logs and device connection, then" : "Fix the JavaScript or TypeScript error, then"} ${reloadRemedy} Do not run \`${runCommand}\` unless native inputs changed or the app process exits.`));
916
932
  }
917
933
  return { state: LAUNCH_FATAL };
918
934
  }
@@ -923,7 +939,7 @@ async function verifyAndroidRun({ root, release, remoteRelease, remoteDevice, ve
923
939
  for (const line of report.lines) phase("launch", chalk.yellow(line));
924
940
  if (report.lines.length > 0 && verification.processAlive === true && metroPort !== null) {
925
941
  const reloadRemedy = physical ? `run \`agent-device metro reload --metro-port ${metroPort}\`.` : "run `stim reload android`.";
926
- phase("remedy", chalk.yellow(`The native app is still running. Fix the JavaScript or TypeScript error; Fast Refresh should apply the edit. If the error screen remains, ${reloadRemedy} Do not run \`stim android\` unless native inputs changed or the app process exits.`));
942
+ phase("remedy", chalk.yellow(`The native app is still running. Fix the JavaScript or TypeScript error; Fast Refresh should apply the edit. If the error screen remains, ${reloadRemedy} Do not run \`${runCommand}\` unless native inputs changed or the app process exits.`));
927
943
  }
928
944
  return {
929
945
  state: true,
@@ -952,7 +968,7 @@ async function verifyAndroidRun({ root, release, remoteRelease, remoteDevice, ve
952
968
  })) phase("", chalk.yellow(line));
953
969
  return { state: LAUNCH_UNVERIFIED };
954
970
  }
955
- async function resolveInstallSerial({ root, device, physical, remoteDevice, serial, resolveAvdSerial, waitForDeviceBoot, phase }) {
971
+ async function resolveInstallSerial({ root, slot, device, physical, remoteDevice, serial, resolveAvdSerial, waitForDeviceBoot, phase }) {
956
972
  if (physical || remoteDevice || !device.owned || !device.avdName) return serial;
957
973
  const resolved = resolveAvdSerial(device.avdName, { timeoutMs: 5e3 });
958
974
  if (!resolved.serial) throw new Error(`Could not verify a running owned AVD ${device.avdName} before installation.`);
@@ -965,16 +981,16 @@ async function resolveInstallSerial({ root, device, physical, remoteDevice, seri
965
981
  }
966
982
  const consolePort = Number(resolved.serial.replace(/^emulator-/, ""));
967
983
  withConfigLock(() => {
968
- const current = loadConfig()?.projects?.[root]?.platforms?.android;
984
+ const current = deviceSlotPlatforms(loadConfig()?.projects?.[root], slot)?.android;
969
985
  if (!current?.owned || current.avdName !== device.avdName) throw new Error("The owned emulator assignment changed before installation.");
970
986
  if (current.consolePort !== consolePort) setDevice(root, PLATFORM, {
971
987
  ...current,
972
988
  consolePort
973
- });
989
+ }, slot);
974
990
  });
975
991
  return resolved.serial;
976
992
  }
977
- async function finishAndroidRun({ lease, releaseLease, root, json, metroCheck, useBuildCache, variant, release, isExpo, metroPort, logsDir, emuLog, device, physical, remoteDevice, bootPromise, resolveAvdSerial, waitForDeviceBoot, bootDuration, apkPath, androidPackage: initialPackage, swapDir, record, waitedForBuild, ccache, uploadPending, providerUpload, providerName, remote, abandonedRemote: remoteWasAbandoned, started, startedAt, writer, phase, fail, readApkPackage, install, launch, launchRelease, resolveDevClientScheme, verifyLaunched, verifyReleaseLaunched, spawn, kill, pidAlive, verifyCollector, writeLaunch, writeState, now, out, emit, recordRun }) {
993
+ async function finishAndroidRun({ lease, releaseLease, root, slot, json, metroCheck, useBuildCache, variant, release, isExpo, metroPort, logsDir, emuLog, device, physical, remoteDevice, bootPromise, resolveAvdSerial, waitForDeviceBoot, bootDuration, apkPath, androidPackage: initialPackage, swapDir, record, waitedForBuild, ccache, uploadPending, providerUpload, providerName, remote, abandonedRemote: remoteWasAbandoned, started, startedAt, writer, phase, fail, readApkPackage, install, launch, launchRelease, resolveDevClientScheme, verifyLaunched, verifyReleaseLaunched, spawn, kill, pidAlive, verifyCollector, writeLaunch, writeState, now, out, emit, recordRun }) {
978
994
  let androidPackage = initialPackage;
979
995
  let leaseWarned = false;
980
996
  const raiseLeaseFor = (boundMs, beforeInstall) => {
@@ -991,13 +1007,12 @@ async function finishAndroidRun({ lease, releaseLease, root, json, metroCheck, u
991
1007
  return null;
992
1008
  };
993
1009
  const booted = await bootPromise;
1010
+ const runCommand = nativeRunCommand("android", slot, {
1011
+ physical,
1012
+ deviceId: booted.serial
1013
+ });
994
1014
  if (booted.failed) {
995
- const diag = noDeviceDiagnostic({
996
- reason: booted.reason ?? "The emulator did not boot.",
997
- logFile: emuLog,
998
- remedy: "Run `stim status` to see what Stim thinks it owns; re-running `stim android` creates a fresh owned AVD.",
999
- localEmulator: !physical
1000
- });
1015
+ const diag = diagnoseBootFailure(booted, emuLog, runCommand, physical);
1001
1016
  return fail(NO_DEVICE, diag.message, diag.remedy, {
1002
1017
  lines: diag.lines,
1003
1018
  logPath: diag.logPath ? displayPath(root, diag.logPath) : null
@@ -1007,6 +1022,7 @@ async function finishAndroidRun({ lease, releaseLease, root, json, metroCheck, u
1007
1022
  try {
1008
1023
  serial = await resolveInstallSerial({
1009
1024
  root,
1025
+ slot,
1010
1026
  device,
1011
1027
  physical,
1012
1028
  remoteDevice,
@@ -1016,7 +1032,7 @@ async function finishAndroidRun({ lease, releaseLease, root, json, metroCheck, u
1016
1032
  phase
1017
1033
  });
1018
1034
  } catch (error) {
1019
- return fail(NO_DEVICE, error instanceof Error ? error.message : String(error), "Run `stim status` to inspect the owned AVD, then retry `stim android`; the APK was not installed.");
1035
+ return fail(NO_DEVICE, error instanceof Error ? error.message : String(error), `Run \`stim status\` to inspect the owned AVD, then retry \`${runCommand}\`; the APK was not installed.`);
1020
1036
  }
1021
1037
  phase("device", physical ? `${device.deviceName || serial} (${serial}) connected, not owned by Stim` : `${device.avdName || serial} (${serial}) ${device.adopted || device.adoptionPending ? "adopted" : "booted"} ${bootDuration()}`);
1022
1038
  const packageFromApk = readApkPackage(apkPath);
@@ -1049,7 +1065,7 @@ async function finishAndroidRun({ lease, releaseLease, root, json, metroCheck, u
1049
1065
  if (adopting) try {
1050
1066
  withConfigLock(() => {
1051
1067
  const config = loadConfig();
1052
- const current = config?.projects?.[root]?.platforms?.android;
1068
+ const current = deviceSlotPlatforms(config?.projects?.[root], slot)?.android;
1053
1069
  if (!config || !current || current.avdName !== device.avdName) throw new Error("The adopted emulator assignment changed during installation.");
1054
1070
  delete current.adoptionPending;
1055
1071
  saveConfig(config);
@@ -1111,6 +1127,7 @@ async function finishAndroidRun({ lease, releaseLease, root, json, metroCheck, u
1111
1127
  if (launched.failed) {
1112
1128
  printNativeCrashReport({
1113
1129
  root,
1130
+ slot,
1114
1131
  platform: "android",
1115
1132
  deviceId: serial,
1116
1133
  appId: androidPackage,
@@ -1173,6 +1190,7 @@ async function finishAndroidRun({ lease, releaseLease, root, json, metroCheck, u
1173
1190
  if (physical) raiseLeaseFor(0, false);
1174
1191
  const collectorPid = await startCollector({
1175
1192
  root,
1193
+ slot,
1176
1194
  serial,
1177
1195
  packageName: androidPackage,
1178
1196
  spawn,
@@ -1186,6 +1204,7 @@ async function finishAndroidRun({ lease, releaseLease, root, json, metroCheck, u
1186
1204
  if (physical) raiseLeaseFor(release ? RELEASE_VERIFY_WAIT_MS : DEBUG_VERIFY_STEP_MS, false);
1187
1205
  const { state: launchState, warning: launchWarning } = await verifyAndroidRun({
1188
1206
  root,
1207
+ slot,
1189
1208
  release,
1190
1209
  remoteRelease,
1191
1210
  remoteDevice: Boolean(remoteDevice),
@@ -1219,6 +1238,7 @@ async function finishAndroidRun({ lease, releaseLease, root, json, metroCheck, u
1219
1238
  const leaseFacts = lease?.facts() ?? null;
1220
1239
  releaseLease();
1221
1240
  const facts = reportAndroidResult({
1241
+ slot,
1222
1242
  json,
1223
1243
  useBuildCache,
1224
1244
  variant,
@@ -1250,6 +1270,14 @@ async function finishAndroidRun({ lease, releaseLease, root, json, metroCheck, u
1250
1270
  facts
1251
1271
  };
1252
1272
  }
1273
+ function diagnoseBootFailure(booted, logFile, runCommand, physical) {
1274
+ return noDeviceDiagnostic({
1275
+ reason: booted.reason ?? "The emulator did not boot.",
1276
+ logFile,
1277
+ remedy: booted.remedy ?? `Run \`stim status\` to see what Stim thinks it owns; re-running \`${runCommand}\` creates a fresh owned AVD.`,
1278
+ localEmulator: !physical
1279
+ });
1280
+ }
1253
1281
  //#endregion
1254
1282
  //#region src/commands/android.ts
1255
1283
  const FALLBACK_LINES = 5;
@@ -1281,7 +1309,7 @@ function androidCommand(program) {
1281
1309
  registerAndroid(program);
1282
1310
  }
1283
1311
  function registerAndroid(program) {
1284
- program.command("android").description("Build (or install from the shared cache), install and launch this workspace's Android app on its owned emulator, wired to the reserved Metro port. Never starts the bundler -- run `stim start` first.").option("--json", "Emit the facts as a single JSON line on stdout; every other line goes to stderr").option("--no-metro-check", "Skip the reserved-port Metro health check (the app will load no bundle unless something else serves it)").option("--no-build-cache", "Build fresh, ignoring cached artifacts (local and the project's build-cache provider); the fresh build still replaces the cache entry").option("--variant <name>", "Gradle variant to assemble and install (e.g. productionDebug on a flavored project); overrides the android.variant setting. A variant ending in Release embeds the JS bundle and skips Metro entirely. Default: debug").option("--system-image <id>", "Android system image to create this workspace's owned AVD from, as the sdkmanager package id (e.g. \"system-images;android-36;google_apis;arm64-v8a\"). Overrides the android.systemImage setting for this invocation. An unknown id refuses with STIM_BAD_ARG and prints the installed images.").option("--device [serial]", "Install and launch on a connected physical device instead of this workspace's owned emulator. With no serial, the first connected device this workspace can lease is used. Stim never creates, boots, or deletes a physical device.").option("--remote <backend>", "Install and launch on a remote device with proxy or EAS. The build still happens here.", (value) => {
1312
+ program.command("android").description("Build (or install from the shared cache), install and launch this workspace's Android app on its owned emulator, wired to the reserved Metro port. Never starts the bundler -- run `stim start` first.").option("--eas-profile <name>", "Download a matching EAS development build; on a miss, print the build command without running it").option("--slot <name>", "Reusable device slot within this workspace (default: default)", validateDeviceSlot).option("--json", "Emit the facts as a single JSON line on stdout; every other line goes to stderr").option("--no-metro-check", "Skip the reserved-port Metro health check (the app will load no bundle unless something else serves it)").option("--no-build-cache", "Build fresh, ignoring cached artifacts (local and the project's build-cache provider); the fresh build still replaces the cache entry").option("--variant <name>", "Gradle variant to assemble and install (e.g. productionDebug on a flavored project); overrides the android.variant setting. A variant ending in Release embeds the JS bundle and skips Metro entirely. Default: debug").option("--system-image <id>", "Android system image to create this workspace's owned AVD from, as the sdkmanager package id (e.g. \"system-images;android-36;google_apis;arm64-v8a\"). Overrides the android.systemImage setting for this invocation. An unknown id refuses with STIM_BAD_ARG and prints the installed images.").option("--device [serial]", "Install and launch on a connected physical device instead of this workspace's owned emulator. With no serial, the first connected device this workspace can lease is used. Stim never creates, boots, or deletes a physical device.").option("--remote <backend>", "Install and launch on a remote device with proxy or EAS. Builds are local unless --eas-profile selects an existing EAS build.", (value) => {
1285
1313
  if (REMOTE_DEVICE_BACKENDS.includes(value)) return value;
1286
1314
  throw new InvalidArgumentError(`expected one of: ${REMOTE_DEVICE_BACKENDS.join(", ")}`);
1287
1315
  }).option("--wait <seconds>", "How long to wait for another workspace to release the device it leases, before refusing with STIM_DEVICE_BUSY (default 60, 0 refuses at once). Only with --device.").option("--no-wait", "Install on a device another workspace leases instead of waiting: this run takes no lease and, when both workspaces build the same app id, the install terminates the holder's running app. Only with --device.").action(async (opts) => {
@@ -1291,8 +1319,10 @@ function registerAndroid(program) {
1291
1319
  process.exit(1);
1292
1320
  return;
1293
1321
  }
1294
- if (!(await runAndroid({
1322
+ if (!(await withWorkspaceProcessLock(workspaceDir(root), "native-run", () => runAndroid({
1295
1323
  root,
1324
+ slot: opts.slot,
1325
+ easProfile: opts.easProfile,
1296
1326
  json: Boolean(opts.json),
1297
1327
  metroCheck: opts.metroCheck !== false,
1298
1328
  useBuildCache: opts.buildCache !== false,
@@ -1302,10 +1332,13 @@ function registerAndroid(program) {
1302
1332
  device: opts.device ?? null,
1303
1333
  wait: opts.wait,
1304
1334
  waitConflict: waitFlagConflict(process.argv)
1335
+ }), {
1336
+ external: true,
1337
+ waitMs: 18e5
1305
1338
  })).ok) process.exit(1);
1306
1339
  });
1307
1340
  }
1308
- function resolveRunAndroidOptions({ root, json = false, remoteDevice: commandRemoteBackend = null, resolveSettingsFor = resolveSettings, resolveRemoteDeviceContext = resolveRemoteContext, remoteDeviceDeps: makeRemoteDeviceDeps = remoteAndroidDeps, resolveEasBin = resolveEasCliBin, ensureMetroReachable: ensureRemoteMetro = ensureMetroReachable, ensureRemoteBootOwned: ensureRemoteOwned = ensureRemoteBootOwned, detectRemoteProviders = detectProviders, metroCheck = true, useBuildCache = true, variant: variantFlag = null, systemImage: systemImageFlag = null, device: deviceFlag = null, wait: waitFlag = void 0, waitConflict = false, acquireLease = acquireRunLease, makeRunLease = runLease, selectPool = selectFromPool, onLeaseSignal = releaseLeaseOnSignal, listDevices = listAdbDevices, deviceModel = physicalDeviceModel, deviceAbi = androidDeviceAbi, isEmulatorDevice = probeEmulatorSerial, readApkPackage = (apkPath) => apkPackage(dumpApkManifest(apkPath)), getLimits = getConcurrencyLimits, checkCapacity = checkDeviceCapacity, acquireSlot = acquireBuildSlot, releaseSlot = releaseBuildSlot, ensureDevice = ensureOwnedDevice, listSystemImages = listInstalledSystemImages, ensureDeviceBooted = ensureBooted, resolveAvdSerial = resolveOwnedAvdSerial, waitForDeviceBoot = waitForBoot, resolveMetro = resolveProjectMetro, warmMetro: prewarmMetro = warmMetro, resolveMetroRetrying = resolveMetroWithRetry, readState = readWorkspaceState, pidAlive = pidExists, verifyCollector = verifyCollectorOwnership, verifyLaunched = verifyLaunch, ensureStorage = ensureWorkspaceStorageSafely, fingerprint = fingerprintProject, untracked = untrackedNativeFiles, resolveCached = resolveBuild, storeCached = storeBuild, storedAssets = storedAssetManifest, captureAssets = captureAssetManifest, acquireLock = acquireBuildLock, releaseLock = releaseBuildLock, waitForBuild: waitForBuild$1 = waitForBuild, loadProvider = loadProjectProvider, easAuth = checkEasAuth, resolveRemoteBuild = resolveRemote, uploadRemoteBuild = uploadRemote, resolveCacheProvider = resolveCacheProviderConfig, loadCacheProviderModule = loadCacheProvider, needsPrebuildFor = needsPrebuild, prebuild = runPrebuild, build = buildAndroid, ccacheFor = resolveCcache, install = installAndroidApp, launch = launchAndroidApp, launchRelease = launchAndroidReleaseApp, verifyReleaseLaunched = verifyAndroidReleaseLaunch, swapApk = swapApkBundle, resolveDevClientScheme = androidDevClientScheme, spawn = (cmd, args, opts) => getExecutor().spawn(cmd, args, opts), kill = (pid, signal) => process.kill(pid, signal), createWriter = createNdjsonWriter, writeLaunch = writeWorkspaceLaunch, writeState = writeWorkspaceState, recordStats = recordRunStats, readEstimates = readRunEstimates, now = Date.now, out = (line) => console.error(line), emit = (line) => console.log(line) } = {}) {
1341
+ function resolveRunAndroidOptions({ root, json = false, remoteDevice: commandRemoteBackend = null, resolveSettingsFor = resolveSettings, resolveRemoteDeviceContext = resolveRemoteContext, remoteDeviceDeps: makeRemoteDeviceDeps = remoteAndroidDeps, resolveEasBin = resolveEasCliBin, ensureMetroReachable: ensureRemoteMetro = ensureMetroReachable, ensureRemoteBootOwned: ensureRemoteOwned = ensureRemoteBootOwned, detectRemoteProviders = detectProviders, metroCheck = true, useBuildCache = true, easProfile, resolveEasDevelopmentBuild: resolveEasBuild = resolveEasDevelopmentBuild, variant: variantFlag = null, systemImage: systemImageFlag = null, device: deviceFlag = null, wait: waitFlag = void 0, waitConflict = false, acquireLease = acquireRunLease, makeRunLease = runLease, selectPool = selectFromPool, onLeaseSignal = releaseLeaseOnSignal, listDevices = listAdbDevices, deviceModel = physicalDeviceModel, deviceAbi = androidDeviceAbi, isEmulatorDevice = probeEmulatorSerial, readApkPackage = (apkPath) => apkPackage(dumpApkManifest(apkPath)), getLimits = getConcurrencyLimits, checkCapacity = checkDeviceCapacity, acquireSlot = acquireBuildSlot, releaseSlot = releaseBuildSlot, ensureDevice = ensureOwnedDevice, listSystemImages = listInstalledSystemImages, ensureDeviceBooted = ensureBooted, resolveAvdSerial = resolveOwnedAvdSerial, waitForDeviceBoot = waitForBoot, resolveMetro = resolveProjectMetro, warmMetro: prewarmMetro = warmMetro, resolveMetroRetrying = resolveMetroWithRetry, readState = readWorkspaceState, pidAlive = pidExists, verifyCollector = verifyCollectorOwnership, verifyLaunched = verifyLaunch, ensureStorage = ensureWorkspaceStorageSafely, fingerprint = fingerprintProject, untracked = untrackedNativeFiles, resolveCached = resolveBuild, storeCached = storeBuild, storedAssets = storedAssetManifest, captureAssets = captureAssetManifest, acquireLock = acquireBuildLock, releaseLock = releaseBuildLock, waitForBuild: waitForBuild$1 = waitForBuild, loadProvider = loadProjectProvider, easAuth = checkEasAuth, resolveRemoteBuild = resolveRemote, uploadRemoteBuild = uploadRemote, resolveCacheProvider = resolveCacheProviderConfig, loadCacheProviderModule = loadCacheProvider, needsPrebuildFor = needsPrebuild, prebuild = runPrebuild, build = buildAndroid, ccacheFor = resolveCcache, install = installAndroidApp, launch = launchAndroidApp, launchRelease = launchAndroidReleaseApp, verifyReleaseLaunched = verifyAndroidReleaseLaunch, swapApk = swapApkBundle, resolveDevClientScheme = androidDevClientScheme, spawn = (cmd, args, opts) => getExecutor().spawn(cmd, args, opts), kill = (pid, signal) => process.kill(pid, signal), createWriter = createNdjsonWriter, writeLaunch = writeWorkspaceLaunch, writeState = writeWorkspaceState, recordStats = recordRunStats, readEstimates = readRunEstimates, now = Date.now, out = (line) => console.error(line), emit = (line) => console.log(line) } = {}) {
1309
1342
  return {
1310
1343
  root,
1311
1344
  json,
@@ -1319,6 +1352,8 @@ function resolveRunAndroidOptions({ root, json = false, remoteDevice: commandRem
1319
1352
  detectRemoteProviders,
1320
1353
  metroCheck,
1321
1354
  useBuildCache,
1355
+ easProfile,
1356
+ resolveEasBuild,
1322
1357
  variantFlag,
1323
1358
  systemImageFlag,
1324
1359
  deviceFlag,
@@ -1387,8 +1422,38 @@ function resolveRunAndroidOptions({ root, json = false, remoteDevice: commandRem
1387
1422
  emit
1388
1423
  };
1389
1424
  }
1425
+ function androidSlotOptions(options) {
1426
+ const base = resolveRunAndroidOptions(options);
1427
+ const slot = validateDeviceSlot(options.slot);
1428
+ if (slot === "default") return base;
1429
+ return {
1430
+ ...base,
1431
+ ensureDevice: (args) => base.ensureDevice({
1432
+ ...args,
1433
+ slot
1434
+ }),
1435
+ checkCapacity: (args) => base.checkCapacity({
1436
+ ...args,
1437
+ slot
1438
+ }),
1439
+ selectPool: (args) => base.selectPool({
1440
+ ...args,
1441
+ slot
1442
+ }),
1443
+ acquireLease: (args) => base.acquireLease({
1444
+ ...args,
1445
+ slot
1446
+ }),
1447
+ makeRunLease: (args) => base.makeRunLease({
1448
+ ...args,
1449
+ slot
1450
+ }),
1451
+ writeLaunch: ((projectRoot, platform, record) => base.writeLaunch(projectRoot, platform, record, slot))
1452
+ };
1453
+ }
1390
1454
  async function runAndroid(options = {}) {
1391
- let { root, json, commandRemoteBackend, resolveSettingsFor, resolveRemoteDeviceContext, makeRemoteDeviceDeps, resolveEasBin, ensureRemoteMetro, ensureRemoteOwned, detectRemoteProviders, metroCheck, useBuildCache: requestedBuildCache, variantFlag, systemImageFlag, deviceFlag, waitFlag, waitConflict, acquireLease, makeRunLease, selectPool, onLeaseSignal, listDevices, deviceModel, deviceAbi, isEmulatorDevice, readApkPackage, getLimits, checkCapacity, acquireSlot, releaseSlot, ensureDevice, listSystemImages, ensureDeviceBooted, resolveAvdSerial, waitForDeviceBoot, resolveMetro, prewarmMetro, resolveMetroRetrying, readState, pidAlive, verifyCollector, verifyLaunched, ensureStorage, fingerprint, untracked, resolveCached, storeCached, storedAssets, captureAssets, acquireLock, releaseLock, waitForBuild, loadProvider, easAuth, resolveRemoteBuild, uploadRemoteBuild, resolveCacheProvider, loadCacheProviderModule, needsPrebuildFor, prebuild, build, ccacheFor, install, launch, launchRelease, verifyReleaseLaunched, swapApk, resolveDevClientScheme, spawn, kill, createWriter, writeLaunch, writeState, recordStats, readEstimates, now, out, emit } = resolveRunAndroidOptions(options);
1455
+ let { root, json, commandRemoteBackend, resolveSettingsFor, resolveRemoteDeviceContext, makeRemoteDeviceDeps, resolveEasBin, ensureRemoteMetro, ensureRemoteOwned, detectRemoteProviders, metroCheck, useBuildCache: requestedBuildCache, easProfile, resolveEasBuild, variantFlag, systemImageFlag, deviceFlag, waitFlag, waitConflict, acquireLease, makeRunLease, selectPool, onLeaseSignal, listDevices, deviceModel, deviceAbi, isEmulatorDevice, readApkPackage, getLimits, checkCapacity, acquireSlot, releaseSlot, ensureDevice, listSystemImages, ensureDeviceBooted, resolveAvdSerial, waitForDeviceBoot, resolveMetro, prewarmMetro, resolveMetroRetrying, readState, pidAlive, verifyCollector, verifyLaunched, ensureStorage, fingerprint, untracked, resolveCached, storeCached, storedAssets, captureAssets, acquireLock, releaseLock, waitForBuild, loadProvider, easAuth, resolveRemoteBuild, uploadRemoteBuild, resolveCacheProvider, loadCacheProviderModule, needsPrebuildFor, prebuild, build, ccacheFor, install, launch, launchRelease, verifyReleaseLaunched, swapApk, resolveDevClientScheme, spawn, kill, createWriter, writeLaunch, writeState, recordStats, readEstimates, now, out, emit } = androidSlotOptions(options);
1456
+ const slot = validateDeviceSlot(options.slot);
1392
1457
  const started = now();
1393
1458
  const startedAt = new Date(started).toISOString();
1394
1459
  const projectProblem = appProjectProblem(root);
@@ -1433,8 +1498,11 @@ async function runAndroid(options = {}) {
1433
1498
  };
1434
1499
  }
1435
1500
  const logsDir = workspaceLogsDir(root);
1436
- const buildLog = join(logsDir, "build-android.ndjson");
1437
- const writer = createWriter(buildLog, { truncate: true });
1501
+ const buildLog = join(logsDir, `build-${deviceSlotKey("android", slot)}.ndjson`);
1502
+ const writer = createWriter(buildLog, {
1503
+ truncate: true,
1504
+ fields: { slot }
1505
+ });
1438
1506
  const record = {
1439
1507
  fingerprint: null,
1440
1508
  cacheKey: null,
@@ -1559,7 +1627,7 @@ async function runAndroid(options = {}) {
1559
1627
  const remoteSettingError = remoteDeviceSettingError(settings);
1560
1628
  if (remoteSettingError) return fail("STIM_BAD_ARG", remoteSettingError, `Set ios.remote and android.remote to one of: ${REMOTE_DEVICE_BACKENDS.join(", ")}.`);
1561
1629
  const systemImage = resolveSystemImage(systemImageFlag, settings);
1562
- const variant = resolveVariant(variantFlag, settings);
1630
+ const variant = easProfile !== void 0 ? "debug" : resolveVariant(variantFlag, settings);
1563
1631
  const flavorRefusal = productFlavorRefusal({
1564
1632
  flavors: readProductFlavors(root),
1565
1633
  variant
@@ -1569,7 +1637,7 @@ async function runAndroid(options = {}) {
1569
1637
  const cachePolicy = artifactCachePolicy(optimizations, requestedBuildCache, release);
1570
1638
  const useBuildCache = cachePolicy.read;
1571
1639
  const isExpo = detectIsExpo(root);
1572
- const physical = deviceFlag !== null && deviceFlag !== void 0 && deviceFlag !== false;
1640
+ const physical = isPhysicalDeviceRequest(deviceFlag);
1573
1641
  if (physical && deviceFlag === "") return fail("STIM_BAD_ARG", "--device was given an empty serial.", "Pass `--device` on its own to take the first connected device this workspace can lease, or `--device <serial>` to name one.");
1574
1642
  if (physical && commandRemoteBackend) return fail("STIM_BAD_ARG", "--device installs on a device connected to this machine, and --remote installs on a remote one.", "Pass only one of --device and --remote.");
1575
1643
  const noWait = waitFlag === false;
@@ -1581,6 +1649,7 @@ async function runAndroid(options = {}) {
1581
1649
  const waitSeconds = waitParsed.seconds;
1582
1650
  const remoteBackend = physical ? null : commandRemoteBackend ?? remoteAndroidSetting(settings);
1583
1651
  const imageRefusal = systemImageRefusal({
1652
+ slot,
1584
1653
  flag: systemImageFlag,
1585
1654
  resolved: systemImage,
1586
1655
  physical,
@@ -1588,6 +1657,17 @@ async function runAndroid(options = {}) {
1588
1657
  listImages: listSystemImages
1589
1658
  });
1590
1659
  if (imageRefusal) return fail(imageRefusal.code, imageRefusal.message, imageRefusal.remedy);
1660
+ const easBuild = await resolveEasBuild({
1661
+ root,
1662
+ platform: PLATFORM,
1663
+ profile: easProfile,
1664
+ note: out,
1665
+ isExpo,
1666
+ physical,
1667
+ selectors: [variantFlag],
1668
+ buildCache: requestedBuildCache
1669
+ });
1670
+ if (isEasBuildFailure(easBuild)) return fail(easBuild.code, easBuild.message, easBuild.remedy);
1591
1671
  const requestedSerial = typeof deviceFlag === "string" ? deviceFlag : null;
1592
1672
  let androidPackage = detectAndroidPackage(root);
1593
1673
  record.bundleId = androidPackage;
@@ -1719,7 +1799,7 @@ async function runAndroid(options = {}) {
1719
1799
  reason: `Could not ensure an owned Android emulator: ${err?.message || err}`,
1720
1800
  logFile: emuLog,
1721
1801
  localEmulator: !(err instanceof AvdRecoveryError),
1722
- remedy: err instanceof AvdRecoveryError ? err.remedy : "Check that JAVA_HOME and ANDROID_HOME are set correctly, and that an arm64 system image is installed (`sdkmanager \"system-images;android-36;google_apis;arm64-v8a\"`)."
1802
+ remedy: err instanceof AvdBootError ? err.remedy : "Check that JAVA_HOME and ANDROID_HOME are set correctly, and that an arm64 system image is installed (`sdkmanager \"system-images;android-36;google_apis;arm64-v8a\"`)."
1723
1803
  });
1724
1804
  return fail(NO_DEVICE, diag.message, diag.remedy, {
1725
1805
  lines: diag.lines,
@@ -1799,6 +1879,18 @@ async function runAndroid(options = {}) {
1799
1879
  let storeSources = [];
1800
1880
  let apkPath = null;
1801
1881
  async function resolveInitialFingerprint() {
1882
+ if (easBuild?.ok) {
1883
+ hash = storeHash = easBuild.fingerprint;
1884
+ cacheKey = storeKey = easBuild.cacheKey;
1885
+ apkPath = easBuild.path;
1886
+ record.fingerprint = hash;
1887
+ record.cacheKey = cacheKey;
1888
+ record.cacheHit = easBuild.cacheHit;
1889
+ record.cacheSkipped = !useBuildCache;
1890
+ providerName = "eas";
1891
+ stats.setCacheKey(cacheKey);
1892
+ return true;
1893
+ }
1802
1894
  const fingerprintTimer = stepTimer(now);
1803
1895
  try {
1804
1896
  const computed = await fingerprint(root, { platform: PLATFORM });
@@ -1930,7 +2022,7 @@ async function runAndroid(options = {}) {
1930
2022
  } else phase("cache", `remote miss (${remote.name}) ${remoteTimer()}`);
1931
2023
  }
1932
2024
  }
1933
- await resolveRemoteArtifact();
2025
+ if (!easBuild) await resolveRemoteArtifact();
1934
2026
  let waitedForBuild = null;
1935
2027
  let releasedWait = null;
1936
2028
  async function waitForSharedBuild() {
@@ -2014,7 +2106,7 @@ async function runAndroid(options = {}) {
2014
2106
  }
2015
2107
  return true;
2016
2108
  }
2017
- if (!await waitForSharedBuild()) return phaseFailure;
2109
+ if (!easBuild && !await waitForSharedBuild()) return phaseFailure;
2018
2110
  let swapDir = null;
2019
2111
  let swapFellBack = false;
2020
2112
  const installableCachedApk = async (key, cachedPath) => {
@@ -2214,8 +2306,10 @@ async function runAndroid(options = {}) {
2214
2306
  }
2215
2307
  return true;
2216
2308
  }
2217
- await prepareCachedArtifact();
2218
- if (!await buildArtifact()) return phaseFailure;
2309
+ if (!easBuild) {
2310
+ await prepareCachedArtifact();
2311
+ if (!await buildArtifact()) return phaseFailure;
2312
+ }
2219
2313
  record.appPath = apkPath;
2220
2314
  let leaseHandle = null;
2221
2315
  let stopLeaseSignals = null;
@@ -2260,6 +2354,7 @@ async function runAndroid(options = {}) {
2260
2354
  }
2261
2355
  try {
2262
2356
  return await finishAndroidRun({
2357
+ slot,
2263
2358
  lease: leaseHandle,
2264
2359
  releaseLease,
2265
2360
  root,
@@ -1,11 +1,11 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { k as withDirLock, u as getConfigDir } from "./config-CMtIaTk9.mjs";
3
- import { r as register } from "./cache-manifest-Cu4_Znw9.mjs";
4
- import { D as workspaceLogsDir, x as sharedCcache, y as ensureWorkspaceStorage } from "./state-CZ-ER-8d.mjs";
2
+ import { k as withDirLock, u as getConfigDir } from "./config-CcefMY2Q.mjs";
3
+ import { r as register } from "./cache-manifest-CXR-Y8pH.mjs";
4
+ import { D as workspaceLogsDir, x as sharedCcache, y as ensureWorkspaceStorage } from "./state-B3XA2wp2.mjs";
5
5
  import { n as stripAnsi, r as waitForChild, t as createLineReader } from "./process-output-C7IlIzd7.mjs";
6
6
  import { l as phaseLine } from "./command-output-8wHMEde0.mjs";
7
- import { n as androidHome } from "./android-K63_0UfF.mjs";
8
- import { f as startBuildHeartbeat, r as HEARTBEAT_INTERVAL_MS } from "./xcode-DeKCribl.mjs";
7
+ import { n as androidHome } from "./android-hMj83VpN.mjs";
8
+ import { f as startBuildHeartbeat, r as HEARTBEAT_INTERVAL_MS } from "./xcode-xnxyvJkD.mjs";
9
9
  import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
10
10
  import { createHash, randomUUID } from "node:crypto";
11
11
  import { dirname, isAbsolute, join, relative } from "node:path";