stim 1.16.0 → 1.17.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 (108) hide show
  1. package/dist/{activity-BxwdD81S.mjs → activity-IRvJY_ru.mjs} +23 -3
  2. package/dist/{agent-device-usage-output-Dpih1Tfa.mjs → agent-device-usage-output-BK41vnzN.mjs} +3 -3
  3. package/dist/{android-Ift4kVvt.mjs → android-B6rDWrTm.mjs} +681 -319
  4. package/dist/{android-B_NumVyQ.mjs → android-B_YNZy61.mjs} +7 -43
  5. package/dist/{android-r2rBL8kV.mjs → android-DE4v59Mb.mjs} +12 -3
  6. package/dist/{android-cas-8U4DPqeR.mjs → android-cas-ZFpjJBL_.mjs} +6 -5
  7. package/dist/{app-install-CKWdbXIX.mjs → app-install-VPkI5WEg.mjs} +6 -5
  8. package/dist/{budget-DjrURfQn.mjs → budget-CUDz870h.mjs} +62 -25
  9. package/dist/{build-plan-DjeSb2Xl.mjs → build-plan-BJSXzsCL.mjs} +345 -86
  10. package/dist/{idle-DYVymG92.mjs → build-progress-D0WkjCPd.mjs} +170 -233
  11. package/dist/{build-slots-rUk77xqh.mjs → build-slots-41v_d2x9.mjs} +35 -25
  12. package/dist/{cli-BkYZ_6hy.mjs → cli-BLQqDGlX.mjs} +17 -17
  13. package/dist/cli.mjs +1 -1
  14. package/dist/{client-BTY686R6.mjs → client-CxNotugx.mjs} +313 -44
  15. package/dist/collector-run.mjs +3 -3
  16. package/dist/{command-output-DnJMdEY-.mjs → command-output-CDAIFD-8.mjs} +3 -3
  17. package/dist/{created-devices-D5owjN4C.mjs → created-devices-CpvzBL25.mjs} +1 -1
  18. package/dist/{dependency-state-B6cMXZab.mjs → dependency-state-CLcBHH2M.mjs} +1 -1
  19. package/dist/{deps-urbmfXig.mjs → deps-CVxlsRFn.mjs} +1 -1
  20. package/dist/{dev-client-CroAIjjj.mjs → dev-client-B-XC4uRH.mjs} +2 -2
  21. package/dist/{device-DZQjF7H5.mjs → device-C8IOoJPv.mjs} +7 -7
  22. package/dist/device-capacity-ZC4XSZ4R.mjs +580 -0
  23. package/dist/device-host-worker.mjs +436 -154
  24. package/dist/{device-ios-BadmeL2V.mjs → device-ios-C2Um-XUR.mjs} +75 -44
  25. package/dist/{device-lease-D-hLqx1b.mjs → device-lease-DpoS36_u.mjs} +2 -2
  26. package/dist/{device-lease-run-CqjShqWW.mjs → device-lease-run-B8YltIH_.mjs} +3 -3
  27. package/dist/{device-pool-CUaEWMMm.mjs → device-pool-3o7s5lo4.mjs} +2 -2
  28. package/dist/{device-remote-D5WVWqOq.mjs → device-remote-hFSkpfQu.mjs} +12 -12
  29. package/dist/{doctor-Cimf-gGB.mjs → doctor-BF_435OC.mjs} +40 -58
  30. package/dist/{error-diagnostics-B9L4Ae0x.mjs → error-diagnostics-C-NMnhrk.mjs} +7 -7
  31. package/dist/{gc-Bqy4bjEu.mjs → gc-BsWKa_-d.mjs} +313 -30
  32. package/dist/{gradle-BsO1MdsH.mjs → gradle-bsBRy6i9.mjs} +3 -3
  33. package/dist/{guide-B_uyKTxW.mjs → guide-DC3n6-NH.mjs} +1776 -655
  34. package/dist/{guide-status-CjEZMAD4.mjs → guide-status-Du-ZQ3FD.mjs} +1 -1
  35. package/dist/hosted-android-DgU31wY3.mjs +32 -0
  36. package/dist/{hosted-client-a-WmqE2L.mjs → hosted-client-CdSoH-G6.mjs} +5 -6
  37. package/dist/hosted-ios-BTaVj4Jb.mjs +15 -0
  38. package/dist/{hosted-logs-D5g041Q3.mjs → hosted-logs-SxfBvSvc.mjs} +16 -16
  39. package/dist/{hosted-macos-BSZyIb3y.mjs → hosted-macos-CT9E3meL.mjs} +4 -4
  40. package/dist/{hosted-ios-CIWelIqf.mjs → hosted-native-CZLOrMrN.mjs} +77 -42
  41. package/dist/idle-BXG__1BA.mjs +176 -0
  42. package/dist/{idle-shutdown-CTsDnAde.mjs → idle-shutdown-IGyNV8Cr.mjs} +25 -17
  43. package/dist/{in-use-DvBw0Kxo.mjs → in-use-CvGOUn-x.mjs} +11 -11
  44. package/dist/{ios-aq3QoGI3.mjs → ios-C7JuT1bI.mjs} +173 -91
  45. package/dist/{ios-BaInn3sW.mjs → ios-DSfnPfse.mjs} +4 -4
  46. package/dist/{ios-device-FfCN_C8p.mjs → ios-device-pAN4t54-.mjs} +1 -1
  47. package/dist/ios-state-8L3lgum6.mjs +89 -0
  48. package/dist/{launch-verify-2DBRizjo.mjs → launch-verify-lTnWZikR.mjs} +2 -2
  49. package/dist/{logs-CHEVK8zD.mjs → logs-DNxKcok9.mjs} +36 -23
  50. package/dist/{macos-CSYEAFmF.mjs → macos-BEs8cwKx.mjs} +75 -34
  51. package/dist/macos-run.mjs +1 -1
  52. package/dist/maintenance-run.mjs +2 -2
  53. package/dist/{metro-BeK1ZPz1.mjs → metro-g8NqiJp-.mjs} +1 -1
  54. package/dist/{metro-gateway-BJ-rxQAB.mjs → metro-gateway-CHqK0p-a.mjs} +22 -3
  55. package/dist/{named-ports-Bk_YXEmE.mjs → named-ports-C8m8EQs2.mjs} +2 -2
  56. package/dist/{native-run-NaBrRFtQ.mjs → native-run-D1Q56rAD.mjs} +17 -6
  57. package/dist/{native-runtime-DBzRnEKS.mjs → native-runtime-YhaSxP4v.mjs} +6 -6
  58. package/dist/offload-worker.mjs +9 -9
  59. package/dist/{teardown-CL6vDs2Y.mjs → ownership-BMLqyBgJ.mjs} +603 -26
  60. package/dist/{ownership-DiA16AsN.mjs → ownership-DyRQru0q.mjs} +2 -2
  61. package/dist/{ports-R2k4Knz3.mjs → ports-4nYqxPWC.mjs} +2 -2
  62. package/dist/{prebuild-D2fsca_Z.mjs → prebuild-D7RUMkwN.mjs} +3 -3
  63. package/dist/{preview-DhVbvPAZ.mjs → preview-C_JjkEy4.mjs} +7 -7
  64. package/dist/{project-Du91Ilvc.mjs → project-D47Rh0ly.mjs} +7 -2
  65. package/dist/{recordings-BIXOzVnr.mjs → recordings-BNgsD1Tr.mjs} +1 -1
  66. package/dist/{reload-rM9jV1fz.mjs → reload-byOjE3P4.mjs} +62 -35
  67. package/dist/{remote-cache-C8-V13XN.mjs → remote-cache-Ci4DQYYj.mjs} +3 -3
  68. package/dist/{run-f4vI6EnK.mjs → run-CkE9y0nc.mjs} +1 -1
  69. package/dist/{server-bare-CpPGmnmO.mjs → server-bare-BkrSxSsZ.mjs} +1 -1
  70. package/dist/{server-expo-MC0rHlOC.mjs → server-expo-CHTyXD0G.mjs} +2 -2
  71. package/dist/server-expo-CeakhI8P.mjs +2 -0
  72. package/dist/{settings-oTSkkNrM.mjs → settings-Cfd-0r2d.mjs} +10 -9
  73. package/dist/{settings-ZkDzkzvB.mjs → settings-SR9OmW9h.mjs} +3 -3
  74. package/dist/settings.schema.json +123 -76
  75. package/dist/{simslim-B8-7AoTI.mjs → simslim-BWNrc3M3.mjs} +3 -3
  76. package/dist/{slot-launch-B8VrHxn1.mjs → slot-launch-DmjBmpQV.mjs} +8 -8
  77. package/dist/{start-CX8Ig-U2.mjs → start-BEP-XmEA.mjs} +1 -1
  78. package/dist/{start-ClDfwXa4.mjs → start-BHIersSm.mjs} +11 -11
  79. package/dist/{state-DcW6vOd_.mjs → state-BKa8wNN5.mjs} +1 -1
  80. package/dist/{state-3DwSQIBK.mjs → state-DyaNTgfR.mjs} +1 -1
  81. package/dist/{state-C1jB7Q8q.mjs → state-PeQiJuIu.mjs} +2 -2
  82. package/dist/{stats-CEZ_i5je.mjs → stats-Bq4fJOdB.mjs} +7 -4
  83. package/dist/{status-Cs7VLg9D.mjs → status-CqeWbIWW.mjs} +128 -38
  84. package/dist/{status-BuvBFeII.mjs → status-CzS4bPO9.mjs} +4 -4
  85. package/dist/{stop-Bffozsac.mjs → stop-Cj01je8D.mjs} +48 -27
  86. package/dist/{stop-B0Yxtmiy.mjs → stop-D03prQnW.mjs} +4 -4
  87. package/dist/{stop-Dt_dM16A.mjs → stop-DOxwkH4j.mjs} +2 -2
  88. package/dist/{stop-cause-CidxIqup.mjs → stop-cause-rhmGfwvt.mjs} +1 -1
  89. package/dist/supervisor-run.d.mts +7 -4
  90. package/dist/supervisor-run.mjs +26 -28
  91. package/dist/{support-D4aYJniO.mjs → support-BE-tbOP9.mjs} +45 -20
  92. package/dist/{support-CAP8qDUF.mjs → support-WvD__tSV.mjs} +7 -7
  93. package/dist/{toolchain-DkUflduf.mjs → toolchain-CdrxgUeA.mjs} +8 -8
  94. package/dist/{warm-progress-CpGv3Wb6.mjs → warm-progress-DF4JRvYI.mjs} +3 -3
  95. package/dist/watchman-APYjXSQa.mjs +48 -0
  96. package/dist/{web-DWUaMp8P.mjs → web-DddUdJzr.mjs} +12 -13
  97. package/dist/web-run.mjs +4 -4
  98. package/dist/{workspace-process-lock-BL7Q0HCO.mjs → workspace-process-lock-2mNzOlaB.mjs} +3 -2
  99. package/dist/{workspace-state-B8-aJ1_5.mjs → workspace-state-DqcCbpE7.mjs} +2 -2
  100. package/dist/{workspaces-BV4pdHFz.mjs → workspaces-Bcib97-6.mjs} +3 -3
  101. package/dist/{worktree-K-xj9Db0.mjs → worktree-AK_m-d_m.mjs} +141 -40
  102. package/dist/{worktree-jnG8Iph5.mjs → worktree-CsAjSmEo.mjs} +1 -1
  103. package/package.json +5 -5
  104. package/dist/device-capacity-BHwxNBlu.mjs +0 -136
  105. package/dist/ios-state-DS5UpYTH.mjs +0 -48
  106. package/dist/machines-BIBUdhpj.mjs +0 -265
  107. package/dist/ownership-hCkPzGmr.mjs +0 -571
  108. package/dist/server-expo-CVqO2c35.mjs +0 -2
@@ -2,49 +2,433 @@ import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
2
  import { C as saveConfig, D as withConfigLock$1, E as upsertProject, L as deviceSlotFileKey, R as deviceSlotKey, V as parseDeviceSlotOption, W as validateDeviceSlot, _ as loadConfig$1, j as formatBytes, p as getProject, u as getConcurrencyLimits, w as setDevice, z as deviceSlotPlatforms } from "./config-D0DbpZtM.mjs";
3
3
  import { n as ownership_claim_exports } from "./process-identity-BmRm1qV0.mjs";
4
4
  import { r as waitForChild, t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
5
- import { _ as stepTimer, c as launchErrorReport, f as refuseNoProject, g as stepClock, i as formatDuration, l as phaseLine, m as shortHash, n as appReadinessMessage, v as launchErrorPreview } from "./command-output-DnJMdEY-.mjs";
6
- import { B as settingsLayers, F as settingFile, G as detectProviders, H as unknownSettingKeys, J as artifactCachePolicy, L as settingOriginScope, N as resolveCacheProviderConfig, P as resolveSettings, R as settingShapeErrors, S as metroPortSetting, Tt as removeTemporaryEntry, V as tunnelModeSetting, X as optimizationBuildProfile, Y as compilerCacheFallbackMessage, Z as resolveOptimizations, a as SETTING_SHAPE_REMEDY, d as cacheProviderSettingError, gt as repoRoot, it as gitCommonDir, j as remoteAndroidSetting, k as publicUrlSetting, r as REMOTE_DEVICE_BACKENDS, s as androidAvdConfigSettingError, u as androidDataPartitionSizeGbSettingError, w as metroWarmupUrlSetting, wt as makeTemporaryDirectory } from "./settings-oTSkkNrM.mjs";
7
- import { n as appProjectProblem, o as findProjectRoot$1, r as detectIsExpo, t as NO_PROJECT_REFUSAL, u as projectShortcut } from "./project-Du91Ilvc.mjs";
8
- import { l as resolveProjectMetro, o as pidExists, u as signalProcessTree } from "./metro-BeK1ZPz1.mjs";
9
- import { c as writeWorkspaceState, g as workspaceAgentDeviceDir, i as recordWorkspaceUse, l as emulatorLogFile, r as readWorkspaceState$1, v as workspaceDir, x as workspaceLogsDir$1 } from "./workspace-state-B8-aJ1_5.mjs";
10
- import { t as budgetGate } from "./budget-DjrURfQn.mjs";
11
- import { A as providerDownloadPath, B as captureAssetManifest, D as fingerprintProject, E as fingerprintDiffRecord, F as storedAssetManifest, H as readAssetManifest, L as untrackedMissLine, M as refingerprintAfterMutation, N as resolveBuild, O as inputsChangedDuringBuild, P as storeBuild, R as untrackedNativeFiles, S as configInputsChanged, T as filesystemBuildCapability, V as compareAssetManifests, _ as waitForBuild, b as buildCacheKey, g as releaseBuildLock, j as providerUploadOutcome, k as prepareProviderDownloadDir, m as acquireBuildLock, v as waitForSharedBuild, x as changedDuringBuildLine, z as assetDiffReason } from "./device-lease-D-hLqx1b.mjs";
12
- import { r as releaseBuildSlot, t as acquireBuildSlot } from "./build-slots-rUk77xqh.mjs";
13
- import { m as writeWorkspaceLaunch, n as MODE_EXPO, t as MODE_BARE } from "./state-DcW6vOd_.mjs";
14
- import { S as statsProjectKey$1, _ as tapBuildLog, g as startBuildProgress, h as recordFinishedBuild, l as NO_BUILD_PROGRESS, v as createRunRecorder, x as recordRunStats, y as readRunEstimates } from "./idle-DYVymG92.mjs";
15
- import { $ as trimAndroidCaches, A as listAvds, B as ownedAvdSystemImage, D as hostSystemImageArch, F as ownedAvdDeviceProfile, G as probeEmulatorSerial, I as ownedAvdDirectory, J as resolveOwnedAvdSerial, K as profileNeedsFoldFeature, O as listAdbDevices, Q as systemImageSupportsFold, T as findBuildTool, U as physicalDeviceModel, W as pickDefaultSystemImage, Y as resolvePhysicalDevice, a as androidDeviceAbi, ft as parkedMaxSetting, i as androidDataFreeBytes, j as listInstalledSystemImages, k as listAvdDeviceProfiles, q as resetAdoptedAvd, r as androidBuildToolName, rt as configuredAndroidEmulatorApp, t as DEFAULT_AVD_DEVICE_PROFILE, tt as waitForBoot, u as androidToolCwd } from "./android-B_NumVyQ.mjs";
5
+ import { _ as stepTimer, c as launchErrorReport, f as refuseNoProject, g as stepClock, i as formatDuration, l as phaseLine, m as shortHash, n as appReadinessMessage, v as launchErrorPreview } from "./command-output-CDAIFD-8.mjs";
6
+ import { B as settingsLayers, C as metroWarmupUrlSetting, F as settingFile, G as detectProviders, H as unknownSettingKeys, J as artifactCachePolicy, L as settingOriginScope, N as resolveCacheProviderConfig, P as resolveSettings, R as settingShapeErrors, T as parseIosRemote, Tt as removeTemporaryEntry, V as tunnelModeSetting, X as optimizationBuildProfile, Y as compilerCacheFallbackMessage, Z as resolveOptimizations, gt as repoRoot, i as SETTING_SHAPE_REMEDY, it as gitCommonDir, j as remoteAndroidSetting, k as publicUrlSetting, l as androidDataPartitionSizeGbSettingError, o as androidAvdConfigSettingError, u as cacheProviderSettingError, wt as makeTemporaryDirectory, x as metroPortSetting } from "./settings-Cfd-0r2d.mjs";
7
+ import { d as projectShortcut, n as appProjectProblem, r as detectIsExpo, s as findProjectRoot$1, t as NO_PROJECT_REFUSAL } from "./project-D47Rh0ly.mjs";
8
+ import { l as resolveProjectMetro, o as pidExists, u as signalProcessTree } from "./metro-g8NqiJp-.mjs";
9
+ import { c as writeWorkspaceState, g as workspaceAgentDeviceDir, i as recordWorkspaceUse, l as emulatorLogFile, r as readWorkspaceState$1, v as workspaceDir, x as workspaceLogsDir$1 } from "./workspace-state-DqcCbpE7.mjs";
10
+ import { t as budgetGate } from "./budget-CUDz870h.mjs";
11
+ import { A as providerDownloadPath, B as captureAssetManifest, D as fingerprintProject, E as fingerprintDiffRecord, F as storedAssetManifest, H as readAssetManifest, L as untrackedMissLine, M as refingerprintAfterMutation, N as resolveBuild, O as inputsChangedDuringBuild, P as storeBuild, R as untrackedNativeFiles, S as configInputsChanged, T as filesystemBuildCapability, V as compareAssetManifests, _ as waitForBuild, b as buildCacheKey, g as releaseBuildLock, j as providerUploadOutcome, k as prepareProviderDownloadDir, m as acquireBuildLock, v as waitForSharedBuild, x as changedDuringBuildLine, z as assetDiffReason } from "./device-lease-DpoS36_u.mjs";
12
+ import { r as releaseBuildSlot, t as acquireBuildSlot } from "./build-slots-41v_d2x9.mjs";
13
+ import { m as writeWorkspaceLaunch, n as MODE_EXPO, t as MODE_BARE } from "./state-BKa8wNN5.mjs";
14
+ import { $ as trimAndroidCaches, A as listAvds, B as ownedAvdSystemImage, D as hostSystemImageArch, F as ownedAvdDeviceProfile, G as probeEmulatorSerial, I as ownedAvdDirectory, J as resolveOwnedAvdSerial, K as profileNeedsFoldFeature, O as listAdbDevices, Q as systemImageSupportsFold, T as findBuildTool, U as physicalDeviceModel, W as pickDefaultSystemImage, Y as resolvePhysicalDevice, a as androidDeviceAbi, ft as parkedMaxSetting, i as androidDataFreeBytes, j as listInstalledSystemImages, k as listAvdDeviceProfiles, q as resetAdoptedAvd, r as androidBuildToolName, rt as configuredAndroidEmulatorApp, t as DEFAULT_AVD_DEVICE_PROFILE, tt as waitForBoot, u as androidToolCwd } from "./android-B_YNZy61.mjs";
16
15
  import { a as workspaceLinks, i as workspaceLinkLine } from "./stim-desktop-Cnz8CPnl.mjs";
17
- import { i as teardownOwnedAvd } from "./teardown-CL6vDs2Y.mjs";
16
+ import { m as teardownOwnedAvd } from "./ownership-BMLqyBgJ.mjs";
17
+ import { _ as statsProjectKey$1, c as recordFinishedBuild, d as createRunRecorder, f as readRunEstimates, g as recordRunStats, l as startBuildProgress, m as recordCapacityRefusal, n as NO_BUILD_PROGRESS, u as tapBuildLog } from "./build-progress-D0WkjCPd.mjs";
18
18
  import { r as createNdjsonWriter } from "./ndjson-PclOIwVY.mjs";
19
- import { E as ccacheActivityLine, T as CCACHE_UNAVAILABLE, b as tailLines, k as resolveCcache, n as androidToolchain, t as androidRequirements, u as HEARTBEAT_INTERVAL_MS, w as CCACHE_NOT_RUN, y as startBuildHeartbeat } from "./toolchain-DkUflduf.mjs";
20
- import { t as cleanLine } from "./server-expo-MC0rHlOC.mjs";
21
- import { a as offloadBuild, c as placementLoad, d as OffloadRefusal, f as namedBuildMachine, i as closeOffload, l as remotePhaseText, m as resolveBuildPlacement, n as buildPlacementCandidates, p as parseBuildMachineOption, r as chooseBuildMachine, s as offloadPlacement } from "./client-BTY686R6.mjs";
22
- import { n as verifyCollectorOwnership, r as readCollectors } from "./ownership-DiA16AsN.mjs";
23
- import { S as restartedAppNote, b as launchAndroidReleaseApp, c as androidDevClientUrl, g as installConflictKind, h as installAndroidApp, r as DEFAULT_METRO_PORT, s as androidAppProcess, t as ADB_INSTALL_TIMEOUT_MS, y as launchAndroidApp } from "./app-install-CKWdbXIX.mjs";
24
- import { t as checkDeviceCapacity } from "./device-capacity-BHwxNBlu.mjs";
25
- import { C as resolveSystemImage, S as resolveDeviceProfile, T as systemImageRefusal, _ as isReleaseVariant, a as PLATFORM, b as pooledAndroidDevice, c as androidSystemImageSetting, d as apkPackage, f as deviceProfileRefusal, g as foldableImageRefusal, h as findAapt, i as NO_FINGERPRINT, l as androidVariantSetting, m as dumpApkManifest, n as LAUNCH_FAILED, o as androidBuildOptions, p as displayPath, r as NO_DEVICE, s as androidDevClientScheme, t as INSTALL_FAILED, u as apkDevClientFacts, v as noDeviceDiagnostic, w as resolveVariant, x as remoteAvdFlagRefusal, y as parseXmltree } from "./support-CAP8qDUF.mjs";
19
+ import { E as ccacheActivityLine, T as CCACHE_UNAVAILABLE, b as tailLines, k as resolveCcache, n as androidToolchain, t as androidRequirements, u as HEARTBEAT_INTERVAL_MS, w as CCACHE_NOT_RUN, y as startBuildHeartbeat } from "./toolchain-CdrxgUeA.mjs";
20
+ import { t as cleanLine } from "./server-expo-CHTyXD0G.mjs";
21
+ import { a as offloadBuild, c as placementLoad, d as OffloadRefusal, f as namedBuildMachine, h as resolveBuildPlacement, i as closeOffload, l as remotePhaseText, n as buildPlacementCandidates, p as parseBuildMachineOption, r as chooseBuildMachine, s as offloadPlacement } from "./client-CxNotugx.mjs";
22
+ import { r as hostedMetroSettings } from "./metro-gateway-CHqK0p-a.mjs";
23
+ import { n as verifyCollectorOwnership, r as readCollectors } from "./ownership-DyRQru0q.mjs";
24
+ import { n as DeviceAdmissionRefusal, r as checkDeviceCapacity, t as DEFAULT_DEVICE_SLOT_WAIT_MS } from "./device-capacity-ZC4XSZ4R.mjs";
25
+ import { S as restartedAppNote, b as launchAndroidReleaseApp, c as androidDevClientUrl, g as installConflictKind, h as installAndroidApp, r as DEFAULT_METRO_PORT, s as androidAppProcess, t as ADB_INSTALL_TIMEOUT_MS, y as launchAndroidApp } from "./app-install-VPkI5WEg.mjs";
26
+ import { C as resolveSystemImage, S as resolveDeviceProfile, T as systemImageRefusal, _ as isReleaseVariant, a as PLATFORM, b as pooledAndroidDevice, c as androidSystemImageSetting, d as apkPackage, f as deviceProfileRefusal, g as foldableImageRefusal, h as findAapt, i as NO_FINGERPRINT, l as androidVariantSetting, m as dumpApkManifest, n as LAUNCH_FAILED, o as androidBuildOptions, p as displayPath, r as NO_DEVICE, s as androidDevClientScheme, t as INSTALL_FAILED, u as apkDevClientFacts, v as noDeviceDiagnostic, w as resolveVariant, x as remoteAvdFlagRefusal, y as parseXmltree } from "./support-WvD__tSV.mjs";
26
27
  import { t as spawnEntry } from "./spawn-entry-jw19m8Nl.mjs";
27
- import { i as staleNativeDirRefusal, n as recordPrebuild, r as runPrebuild, t as planPrebuild } from "./prebuild-D2fsca_Z.mjs";
28
- import { c as formatDiagnostic, i as buildAndroid, o as productFlavorRefusal, s as readProductFlavors } from "./gradle-BsO1MdsH.mjs";
29
- import { a as VERIFY_TIMEOUT_MS, d as verifyAndroidReleaseLaunch, f as verifyLaunch, i as RELEASE_VERIFY_WAIT_MS, l as unattributedLaunchLines, n as LAUNCH_FATAL, r as LAUNCH_UNVERIFIED, t as LAUNCH_BUNDLING, u as unverifiedLaunchLines } from "./launch-verify-2DBRizjo.mjs";
30
- import { a as acquireRunLease, c as lostRefusal, d as runLease, f as waitFlagConflict, l as parseDeviceWait, o as leaseExpiryText, s as lostLine, t as DEBUG_VERIFY_STEP_MS, u as releaseLeaseOnSignal } from "./device-lease-run-CqjShqWW.mjs";
31
- import { a as easAuthNote, c as loadProjectProvider, f as resolveEasCliBin, i as checkEasAuth, m as uploadRemote, n as UPLOAD_TIMEOUT_MS, o as exitAfterFlush, p as resolveRemote, r as cacheLevel, s as isEasAuthFailureText, t as RESOLVE_TIMEOUT_MS } from "./remote-cache-C8-V13XN.mjs";
32
- import { a as ensureMetroReachable, c as remoteAndroidDeps, n as binOnPath, o as ensureRemoteBootOwned, u as resolveRemoteContext, w as ownedSessionName } from "./device-remote-D5WVWqOq.mjs";
33
- import { d as withNativeBuildRun, r as cancelledFailure, u as runCancellation } from "./native-run-NaBrRFtQ.mjs";
34
- import { a as isEasBuildFailure, o as planEasDevelopmentBuild, s as resolveEasDevelopmentBuild } from "./simslim-B8-7AoTI.mjs";
35
- import { c as startDevServer, m as detectAppIds, p as detectAndroidPackage } from "./start-ClDfwXa4.mjs";
36
- import { _ as AvdRecoveryError, a as refusePlan, c as skippedMissReason, d as refreshUpdatesManifestFile, g as AvdBootError, h as ensureOwnedDevice, i as printPlan, l as UPDATES_MANIFEST_NAME, m as ensureBooted, n as planFlagRefusal, o as explainBuildMiss, p as didSetUpDevice, r as planPayload, s as fingerprintErrorMissReason, t as planCachedBuild, u as detectEntryFile, v as warmMetro } from "./build-plan-DjeSb2Xl.mjs";
37
- import { i as launchOutcomeRecord, n as ensureWorkspaceStorageSafely, r as isPhysicalDeviceRequest, t as ensureDevServer } from "./native-runtime-DBzRnEKS.mjs";
38
- import { t as selectFromPool } from "./device-pool-CUaEWMMm.mjs";
39
- import { n as nativeRunCommand, r as siblingPlatformSlots, t as launchSlotScope } from "./slot-launch-B8VrHxn1.mjs";
40
- import { a as printNativeCrashReport, r as captureNativeCrashes, t as errorDiagnostics } from "./error-diagnostics-B9L4Ae0x.mjs";
41
- import { n as resolveAndroidCas, r as resolveAndroidCompilerCache } from "./android-cas-8U4DPqeR.mjs";
28
+ import { i as staleNativeDirRefusal, n as recordPrebuild, r as runPrebuild, t as planPrebuild } from "./prebuild-D7RUMkwN.mjs";
29
+ import { c as formatDiagnostic, i as buildAndroid, o as productFlavorRefusal, s as readProductFlavors } from "./gradle-bsBRy6i9.mjs";
30
+ import { a as VERIFY_TIMEOUT_MS, d as verifyAndroidReleaseLaunch, f as verifyLaunch, i as RELEASE_VERIFY_WAIT_MS, l as unattributedLaunchLines, n as LAUNCH_FATAL, r as LAUNCH_UNVERIFIED, t as LAUNCH_BUNDLING, u as unverifiedLaunchLines } from "./launch-verify-lTnWZikR.mjs";
31
+ import { a as acquireRunLease, c as lostRefusal, d as runLease, f as waitFlagConflict, l as parseDeviceWait, o as leaseExpiryText, s as lostLine, t as DEBUG_VERIFY_STEP_MS, u as releaseLeaseOnSignal } from "./device-lease-run-B8YltIH_.mjs";
32
+ import { a as writeHostedAndroid, i as writeDevicePlacement, t as readHostedAndroid } from "./ios-state-8L3lgum6.mjs";
33
+ import { _ as AvdRecoveryError, a as refusePlan, b as devicePlacementLine, c as skippedMissReason, d as refreshUpdatesManifestFile, g as AvdBootError, h as ensureOwnedDevice, i as printPlan, l as UPDATES_MANIFEST_NAME, m as ensureBooted, n as planFlagRefusal, o as explainBuildMiss, p as didSetUpDevice, r as planPayload, s as fingerprintErrorMissReason, t as planCachedBuild, u as detectEntryFile, v as warmMetro, y as automaticDevicePlacement } from "./build-plan-BJSXzsCL.mjs";
34
+ import { n as prepareHostedAndroid, t as placeHostedAndroid } from "./hosted-android-DgU31wY3.mjs";
35
+ import { n as nativeRunCommand, r as siblingPlatformSlots, t as launchSlotScope } from "./slot-launch-DmjBmpQV.mjs";
36
+ import { a as easAuthNote, c as loadProjectProvider, f as resolveEasCliBin, i as checkEasAuth, m as uploadRemote, n as UPLOAD_TIMEOUT_MS, o as exitAfterFlush, p as resolveRemote, r as cacheLevel, s as isEasAuthFailureText, t as RESOLVE_TIMEOUT_MS } from "./remote-cache-Ci4DQYYj.mjs";
37
+ import { a as ensureMetroReachable, c as remoteAndroidDeps, n as binOnPath, o as ensureRemoteBootOwned, u as resolveRemoteContext, w as ownedSessionName } from "./device-remote-hFSkpfQu.mjs";
38
+ import { d as runCancellationSignal, f as withNativeBuildRun, r as cancelledFailure, u as runCancellation } from "./native-run-D1Q56rAD.mjs";
39
+ import { a as isEasBuildFailure, o as planEasDevelopmentBuild, s as resolveEasDevelopmentBuild } from "./simslim-BWNrc3M3.mjs";
40
+ import { c as startDevServer, m as detectAppIds, p as detectAndroidPackage } from "./start-BHIersSm.mjs";
41
+ import { i as launchOutcomeRecord, n as ensureWorkspaceStorageSafely, r as isPhysicalDeviceRequest, t as ensureDevServer } from "./native-runtime-YhaSxP4v.mjs";
42
+ import { t as selectFromPool } from "./device-pool-3o7s5lo4.mjs";
43
+ import { a as printNativeCrashReport, r as captureNativeCrashes, t as errorDiagnostics } from "./error-diagnostics-C-NMnhrk.mjs";
44
+ import { n as resolveAndroidCas, r as resolveAndroidCompilerCache } from "./android-cas-ZFpjJBL_.mjs";
42
45
  import { constants, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync } from "node:fs";
43
46
  import { basename, dirname, isAbsolute, join } from "node:path";
47
+ import { workspaceId } from "@stim-cli/core";
44
48
  import { InvalidArgumentError } from "commander";
45
- import { buildDiagnostics, machineCapacity, parseMachine } from "@stim-cli/core/state";
49
+ import { buildDiagnostics, hostedAndroidStatus, machineCapacity, parseMachine } from "@stim-cli/core/state";
46
50
  import chalk from "chalk";
47
51
  import { createWarnOnce, loadCacheProvider, resolveTieredBuild, storeTieredBuild } from "@stim-cli/cache";
52
+ //#region src/commands/android/remote.ts
53
+ async function connectAndroidHosting({ root, slot, machine, release, metroCheck, selectors, read = readHostedAndroid, resolveSerial = resolveOwnedAvdSerial, prepare = prepareHostedAndroid }) {
54
+ let recorded;
55
+ try {
56
+ recorded = read(root, slot)[slot];
57
+ } catch (error) {
58
+ return { failure: {
59
+ code: "STIM_HOSTING_REFUSED",
60
+ message: error.message,
61
+ remedy: `Restore that slot's recorded machine and session from the host, then run stim stop${slot === "default" ? "" : ` --slot ${slot}`} to reconcile it.`
62
+ } };
63
+ }
64
+ if (recorded && recorded.machine !== machine) return { failure: {
65
+ code: "STIM_BAD_ARG",
66
+ message: `This workspace's Android emulator runs on ${recorded.machine}; run stim stop first.`
67
+ } };
68
+ if (!machine) return { target: null };
69
+ if (!release && !metroCheck) return { failure: {
70
+ code: "STIM_BAD_ARG",
71
+ message: "Hosted Debug runs require the local Metro supervisor; --no-metro-check cannot be used.",
72
+ remedy: "Run stim stop; stim start, then retry without --no-metro-check."
73
+ } };
74
+ const local = deviceSlotPlatforms(getProject(root), slot)?.android;
75
+ if (local?.owned && local.avdName) {
76
+ let running = true;
77
+ try {
78
+ const resolved = resolveSerial(local.avdName, { timeoutMs: 5e3 });
79
+ running = Boolean(resolved.serial) || !resolved.notRunning && !resolved.missing;
80
+ } catch {}
81
+ if (running) return { failure: {
82
+ code: "STIM_BAD_ARG",
83
+ message: `This workspace's Android emulator for slot ${slot} runs on this Mac; run stim stop first.`
84
+ } };
85
+ }
86
+ try {
87
+ return { target: await prepare(machine, selectors, recorded) };
88
+ } catch (error) {
89
+ return { failure: {
90
+ code: error.code ?? "STIM_HOSTING_REFUSED",
91
+ message: error.message,
92
+ ...error instanceof Error && "remedy" in error && typeof error.remedy === "string" ? { remedy: error.remedy } : {}
93
+ } };
94
+ }
95
+ }
96
+ async function selectAndroidPlacement({ target: selected, automatic = automaticDevicePlacement, checkBudget = budgetGate, note, phase, noWait, buildMachine, localSelectors, ...args }) {
97
+ const selectors = selected.kind === "hosted" ? {
98
+ ...selected.systemImage ? { systemImage: selected.systemImage } : {},
99
+ ...selected.deviceProfile ? { deviceProfile: selected.deviceProfile } : {}
100
+ } : {};
101
+ if (selected.kind !== "hosted" || selected.machine !== "auto") {
102
+ const connected = await connectAndroidHosting({
103
+ ...args,
104
+ selectors,
105
+ machine: selected.kind === "hosted" ? selected.machine : null
106
+ });
107
+ if ("failure" in connected) return connected;
108
+ return {
109
+ target: selected,
110
+ hostedTarget: connected.target,
111
+ selectors,
112
+ budget: connected.target ? {
113
+ reclaimed: [],
114
+ refusal: null
115
+ } : await checkBudget({
116
+ root: args.root,
117
+ note
118
+ })
119
+ };
120
+ }
121
+ const refusal = localSelectors(selected);
122
+ if (refusal) return { failure: refusal };
123
+ try {
124
+ const placed = await automatic({
125
+ root: args.root,
126
+ slot: args.slot,
127
+ platform: "android",
128
+ selectors,
129
+ buildMachine,
130
+ noWait
131
+ });
132
+ phase("placement:", devicePlacementLine(placed.placement, placed.skipped));
133
+ if (placed.target && !args.release && !args.metroCheck) return { failure: {
134
+ code: "STIM_BAD_ARG",
135
+ message: "Hosted Debug runs require the local Metro supervisor; --no-metro-check cannot be used.",
136
+ remedy: "Run stim stop; stim start, then retry without --no-metro-check."
137
+ } };
138
+ return {
139
+ target: placed.target ? {
140
+ ...selected,
141
+ machine: placed.target.host.machine
142
+ } : {
143
+ kind: "emulator",
144
+ systemImage: selected.systemImage,
145
+ deviceProfile: selected.deviceProfile
146
+ },
147
+ hostedTarget: placed.target,
148
+ selectors,
149
+ budget: placed.target ? {
150
+ reclaimed: [],
151
+ refusal: null
152
+ } : await checkBudget({
153
+ root: args.root,
154
+ note
155
+ }),
156
+ devicePlacement: placed.placement
157
+ };
158
+ } catch (error) {
159
+ return { failure: {
160
+ code: error.code ?? "STIM_HOSTING_REFUSED",
161
+ message: error.message,
162
+ ...error instanceof Error && "remedy" in error && typeof error.remedy === "string" ? { remedy: error.remedy } : {}
163
+ } };
164
+ }
165
+ }
166
+ //#endregion
167
+ //#region src/commands/android/result.ts
168
+ function androidFacts({ root, slot, serial, avdName = null, deviceName = null, systemImage = null, deviceProfile = 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, devServer = null, buildMachine = "auto", builtOn, offloadedTo = null, offloadFallback = null, host }) {
169
+ return {
170
+ platform: PLATFORM,
171
+ ...host ? { host } : {},
172
+ ...slot && slot !== "default" ? { slot } : {},
173
+ serial: serial ?? null,
174
+ avdName: avdName ?? null,
175
+ deviceName: deviceName ?? avdName ?? null,
176
+ systemImage: systemImage ?? null,
177
+ deviceProfile: deviceProfile ?? null,
178
+ fingerprint: fingerprint ?? null,
179
+ cacheKey: cacheKey ?? null,
180
+ variant: variant ?? null,
181
+ metroPort: metroPort ?? null,
182
+ cacheHit: cacheLevel(cacheHit),
183
+ cacheSkipped: Boolean(cacheSkipped),
184
+ buildMachine,
185
+ ...builtOn ? { builtOn } : {},
186
+ ...offloadedTo ? { offloadedTo } : {},
187
+ ...offloadFallback ? { offloadFallback } : {},
188
+ waitedForBuild: waitedForBuild ? {
189
+ pid: waitedForBuild.pid ?? null,
190
+ ms: waitedForBuild.ms ?? 0
191
+ } : null,
192
+ appPath: appPath ?? null,
193
+ bundleId: bundleId ?? null,
194
+ installSkipped: Boolean(installSkipped),
195
+ launched: launched === "unverified" || launched === "bundling" ? launched : Boolean(launched),
196
+ ccache,
197
+ debugHttpHost: debugHttpHost ?? null,
198
+ debugHttpHostNote: debugHttpHostNote ?? null,
199
+ devClientUrl: devClientUrl ?? null,
200
+ logs: logs ?? null,
201
+ ...root && !host ? { agentDevice: { stateDir: workspaceAgentDeviceDir(root) } } : {},
202
+ durationMs: typeof durationMs === "number" && Number.isFinite(durationMs) ? durationMs : null,
203
+ ...lease === void 0 ? {} : { lease },
204
+ ...devServer ? { devServer } : {}
205
+ };
206
+ }
207
+ function lastBuildRecord({ devicePlacement, fingerprint, cacheKey, cacheHit, cacheSkipped = false, durationMs, appPath, bundleId, startedAt, status, errorCode = null, avdName = null, deviceName = null, missReason = null, diagnostics = null, configuration = null, buildMachine = "auto", builtOn, offloadedTo = null, offloadFallback = null }) {
208
+ const record = {
209
+ platform: PLATFORM,
210
+ avdName: avdName ?? null,
211
+ deviceName: deviceName ?? avdName ?? null,
212
+ fingerprint: fingerprint ?? null,
213
+ cacheKey: cacheKey ?? null,
214
+ cacheHit: cacheLevel(cacheHit),
215
+ cacheSkipped: Boolean(cacheSkipped),
216
+ durationMs: Number.isFinite(durationMs) ? durationMs : null,
217
+ appPath: appPath ?? null,
218
+ bundleId: bundleId ?? null,
219
+ startedAt,
220
+ status,
221
+ configuration: configuration ?? null
222
+ };
223
+ if (errorCode) record.errorCode = errorCode;
224
+ if (devicePlacement) record.devicePlacement = devicePlacement;
225
+ record.buildMachine = buildMachine;
226
+ if (builtOn) record.builtOn = builtOn;
227
+ if (offloadedTo) record.offloadedTo = offloadedTo;
228
+ if (offloadFallback) record.offloadFallback = offloadFallback;
229
+ if (missReason && !cacheLevel(cacheHit)) record.missReason = missReason;
230
+ const recorded = status === "failed" ? buildDiagnostics(diagnostics) : [];
231
+ if (recorded.length) record.diagnostics = recorded;
232
+ return record;
233
+ }
234
+ async function finishAndroidUpload(uploadPending, remote, phase) {
235
+ if (!uploadPending) return false;
236
+ const upload = await uploadPending;
237
+ if (upload?.uploaded) phase("cache", `uploaded (${remote?.name})`);
238
+ else if (upload?.timedOut) {
239
+ phase("cache", chalk.yellow(`${remote?.name} upload still running after ${formatDuration(UPLOAD_TIMEOUT_MS)}; not waiting`));
240
+ return true;
241
+ } else if (upload?.failed) {
242
+ const authNote = remote?.name === "eas" && isEasAuthFailureText(upload.failed) ? easAuthNote({
243
+ code: "logged-out",
244
+ reason: upload.failed,
245
+ phase: "upload"
246
+ }) : null;
247
+ phase("cache", chalk.yellow(authNote || `${remote?.name} upload failed: ${upload.failed}`));
248
+ }
249
+ return false;
250
+ }
251
+ function reportAndroidResult({ host, root, 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, reclaimed = [], devServer = null, links }) {
252
+ const offloadedTo = record.offloadedTo ?? null;
253
+ recordRun({
254
+ failed: false,
255
+ cacheHit: cacheLevel(record.cacheHit),
256
+ waited: waitedForBuild,
257
+ durationMs,
258
+ offloadedTo
259
+ });
260
+ const facts = androidFacts({
261
+ host,
262
+ root,
263
+ slot,
264
+ serial,
265
+ avdName: record.avdName,
266
+ deviceName: record.deviceName,
267
+ systemImage: record.systemImage,
268
+ deviceProfile: record.deviceProfile,
269
+ debugHttpHost: launched.debugHttpHost ?? null,
270
+ debugHttpHostNote: launched.debugHttpHostNote ?? null,
271
+ devClientUrl: launched.devClientUrl ?? null,
272
+ fingerprint: record.fingerprint,
273
+ cacheKey: record.cacheKey,
274
+ variant,
275
+ metroPort,
276
+ cacheHit: record.cacheHit,
277
+ cacheSkipped: !useBuildCache,
278
+ waitedForBuild,
279
+ appPath: apkPath,
280
+ bundleId: androidPackage,
281
+ installSkipped,
282
+ launched: launchState,
283
+ ccache,
284
+ logs: logsDir,
285
+ durationMs,
286
+ lease,
287
+ devServer,
288
+ buildMachine: record.buildMachine,
289
+ builtOn: record.builtOn ?? void 0,
290
+ offloadedTo,
291
+ offloadFallback: record.offloadFallback ?? null
292
+ });
293
+ writer.close();
294
+ if (record.devicePlacement) facts.devicePlacement = record.devicePlacement;
295
+ if (json) emit(JSON.stringify({
296
+ ...facts,
297
+ ...links ? { links } : {},
298
+ ...reclaimed.length ? { reclaimed } : {}
299
+ }));
300
+ else {
301
+ const summary = `${launchWarning ? "WARNING" : "OK"}: ${androidPackage} launched on ${host ? `${host.device?.name ?? "Android emulator"} on ${host.machine}` : serial}, ${release ? `${variant} (embedded JS, no Metro)` : `Metro port ${metroPort}`} (${cacheOutcome(record.cacheHit, remote?.name ?? providerName, offloadedTo)})`;
302
+ const outcome = launchWarning ? chalk.yellow(`${summary} -- ${launchWarning}`) : launchState === "unverified" ? chalk.yellow(`${summary} -- launch UNVERIFIED`) : launchState === "bundling" ? chalk.green(`${summary} -- bundle requested, still building`) : chalk.green(summary);
303
+ const deviceName = record.avdName || record.deviceName || serial;
304
+ const cacheResult = useBuildCache ? cacheOutcome(record.cacheHit, remote?.name ?? providerName, offloadedTo) : `bypassed; ${cacheOutcome(false, null, offloadedTo)}`;
305
+ const metroResult = release ? `embedded (${variant})` : !metroCheck ? `check skipped on port ${metroPort}` : launchState === "unverified" ? `state unverified on port ${metroPort}` : `running on port ${metroPort}${devServer ? ` (started: ${devServer.reason})` : ""}`;
306
+ emit([
307
+ outcome,
308
+ phaseLine("device", host ? `${deviceName} on ${host.machine}` : `${deviceName} (${serial})`),
309
+ phaseLine("app", androidPackage),
310
+ phaseLine("metro", metroResult),
311
+ phaseLine("cache", cacheResult),
312
+ ...ccache.status === "not-run" ? [phaseLine("compilation cache", ccacheActivityLine(ccache))] : [],
313
+ phaseLine("logs", logsDir || "unavailable (remote device)"),
314
+ ...!host ? [phaseLine("agent-device", `AGENT_DEVICE_STATE_DIR=${workspaceAgentDeviceDir(root)}`)] : []
315
+ ].join("\n"));
316
+ if (links) console.error(chalk.dim(workspaceLinkLine(links)));
317
+ }
318
+ return facts;
319
+ }
320
+ function cacheOutcome(cacheHit, providerName = null, offloadedTo = null) {
321
+ if (cacheHit === "remote") return `cache hit from ${providerName || "the remote cache"}`;
322
+ if (cacheHit === "local") return "cache hit";
323
+ return offloadedTo ? `built on ${offloadedTo}` : "built";
324
+ }
325
+ function persistLastBuild({ recordBuild = recordFinishedBuild, root, record, startedAt, durationMs, status, errorCode = null, diagnostics = null, out }) {
326
+ const lastBuild = lastBuildRecord({
327
+ ...record,
328
+ startedAt,
329
+ durationMs,
330
+ status,
331
+ errorCode,
332
+ diagnostics
333
+ });
334
+ try {
335
+ recordBuild(root, lastBuild);
336
+ } catch (err) {
337
+ out(phaseLine("state", chalk.yellow(`could not record lastBuild: ${err?.message || err}`)));
338
+ }
339
+ return lastBuild;
340
+ }
341
+ //#endregion
342
+ //#region src/commands/android/hosted.ts
343
+ async function finishHostedAndroidRun({ target, artifact, root, slot = "default", release, isExpo, metroPort, logsDir, selectors, place, writePlacement, verifyLaunch: verify, writeLaunch, readApkPackage, resolveDevClientScheme, now, started, startedAt, fail, phase, out, recordBuild, ...report }) {
344
+ const packageName = artifact.androidPackage ?? readApkPackage(artifact.apkPath);
345
+ if (!artifact.apkPath || !packageName) return fail("STIM_INSTALL_FAILED", "The built APK has no package identity.");
346
+ const launchedAt = now();
347
+ try {
348
+ const run = await place(target, {
349
+ root,
350
+ slot,
351
+ bundle: artifact.apkPath,
352
+ handoff: artifact.handoff,
353
+ bundleId: packageName,
354
+ release,
355
+ metroPort,
356
+ selectors,
357
+ ...!release && isExpo ? { devClientScheme: resolveDevClientScheme(root, artifact.apkPath) ?? void 0 } : {},
358
+ reserved: (placement) => writePlacement(root, slot, placement),
359
+ note: out
360
+ });
361
+ writePlacement(root, slot, run.placement);
362
+ writeLaunch(root, "android", {
363
+ appId: packageName,
364
+ deviceId: run.placement.session,
365
+ metroPort,
366
+ release,
367
+ launchedAt: new Date(launchedAt).toISOString()
368
+ });
369
+ let launched = run.launched;
370
+ if (!release) {
371
+ const evidence = await verify({
372
+ requireBundleResponse: true,
373
+ platform: "android",
374
+ logsDir: logsDir ?? void 0,
375
+ since: launchedAt,
376
+ metroPort,
377
+ mode: isExpo ? MODE_EXPO : MODE_BARE,
378
+ slot: launchSlotScope(root, slot),
379
+ platformShared: siblingPlatformSlots(root, "android", slot).length > 0
380
+ });
381
+ if (evidence.fatal) return fail("STIM_LAUNCH_FAILED", "Metro could not deliver the hosted app bundle.", "Run stim logs --errors and fix the bundle error.");
382
+ launched = evidence.verified ? true : evidence.requested ? "bundling" : "unverified";
383
+ }
384
+ if (launched === "unverified") out(`Launch on ${target.host.machine} is unverified; check the hosted app, then retry stim android --remote ${target.host.machine}.`);
385
+ const host = hostedAndroidStatus(run.placement);
386
+ report.record.deviceName = host.device?.name ?? null;
387
+ report.record.bundleId = packageName;
388
+ persistLastBuild({
389
+ recordBuild,
390
+ root,
391
+ record: report.record,
392
+ startedAt,
393
+ durationMs: now() - started,
394
+ status: "success",
395
+ out
396
+ });
397
+ await finishAndroidUpload(artifact.uploadPending, artifact.remote, phase);
398
+ await artifact.providerUpload;
399
+ return {
400
+ ok: true,
401
+ facts: reportAndroidResult({
402
+ ...report,
403
+ root,
404
+ slot,
405
+ release,
406
+ metroPort,
407
+ logsDir,
408
+ host,
409
+ serial: null,
410
+ apkPath: artifact.apkPath,
411
+ androidPackage: packageName,
412
+ installSkipped: false,
413
+ waitedForBuild: artifact.waitedForBuild,
414
+ remote: artifact.remote,
415
+ providerName: artifact.providerName,
416
+ launchState: launched,
417
+ launched: {},
418
+ ccache: artifact.ccache,
419
+ durationMs: now() - started
420
+ })
421
+ };
422
+ } catch (error) {
423
+ return fail(error.code ?? "STIM_HOSTING_REFUSED", error.message, `Retry stim android --remote ${target.host.machine}, or run stim stop to reconcile the recorded placement.`);
424
+ } finally {
425
+ if (artifact.swapDir) rmSync(artifact.swapDir, {
426
+ recursive: true,
427
+ force: true
428
+ });
429
+ }
430
+ }
431
+ //#endregion
48
432
  //#region src/engine/apk-swap.ts
49
433
  const ANDROID_BUNDLE_NAME = "index.android.bundle";
50
434
  const LAST_LINES = 5;
@@ -419,10 +803,10 @@ function fail$1(code, message, remedy, extra = {}) {
419
803
  extra
420
804
  };
421
805
  }
422
- async function acquireAndroidArtifact({ root, buildLog, writer, settings, isExpo, device, physical, remote: remoteTarget, buildPlan, cacheProviderConfig, requestedBuildCache, easBuild, androidPackage: initialPackage, record, maxBuilds, progress }, { deviceAbi, fingerprint, untracked, resolveCached, storeCached, storedAssets, captureAssets, acquireLock, releaseLock, waitForBuild, loadProvider, easAuth, resolveRemoteBuild, uploadRemoteBuild, loadCacheProviderModule, acquireSlot, releaseSlot, planPrebuildFor, prebuild, build, ccacheFor, swapApk, readState, now }) {
423
- const { phase, out, estimates, stats, step, miss, hit: lateHit, place, waitingOn } = progress;
806
+ async function acquireAndroidArtifact({ root, buildLog, writer, settings, isExpo, device, physical, hostedAbi, remote: remoteTarget, buildPlan, cacheProviderConfig, requestedBuildCache, easBuild, androidPackage: initialPackage, record, maxBuilds, progress }, { deviceAbi, fingerprint, untracked, resolveCached, storeCached, storedAssets, captureAssets, acquireLock, releaseLock, waitForBuild, loadProvider, easAuth, resolveRemoteBuild, uploadRemoteBuild, loadCacheProviderModule, acquireSlot, releaseSlot, planPrebuildFor, prebuild, build, ccacheFor, swapApk, readState, now }) {
807
+ const { phase, out, estimates, stats, step, miss, hit: lateHit, place, waitingOn, waitingFor } = progress;
424
808
  let fallbackMachine = null;
425
- let hereReason = "no build machine is paired";
809
+ let hereReason = "no remote Mac is paired";
426
810
  let slotWaitMs;
427
811
  const buildMachine = record.buildMachine ?? "auto";
428
812
  const fallBack = (reason, line = reason) => {
@@ -470,7 +854,8 @@ async function acquireAndroidArtifact({ root, buildLog, writer, settings, isExpo
470
854
  deviceAbi,
471
855
  compiler: cas?.id,
472
856
  buildProfile,
473
- targetAbiOnly: buildPlan.targetAbiOnly
857
+ targetAbiOnly: buildPlan.targetAbiOnly,
858
+ hostedAbi
474
859
  });
475
860
  let hash = "";
476
861
  let providerUpload = null;
@@ -487,6 +872,7 @@ async function acquireAndroidArtifact({ root, buildLog, writer, settings, isExpo
487
872
  let storeKey = "";
488
873
  let storeSources = [];
489
874
  let apkPath = null;
875
+ let handoff = null;
490
876
  let swapFellBack = false;
491
877
  async function resolveInitialFingerprint() {
492
878
  if (easBuild?.ok) {
@@ -501,7 +887,7 @@ async function acquireAndroidArtifact({ root, buildLog, writer, settings, isExpo
501
887
  stats.setCacheKey(cacheKey);
502
888
  return true;
503
889
  }
504
- step("cache-lookup");
890
+ if (useBuildCache) step("cache-lookup");
505
891
  const fingerprintTimer = stepTimer(now);
506
892
  try {
507
893
  const computed = await fingerprint(root, { platform: PLATFORM });
@@ -727,7 +1113,8 @@ async function acquireAndroidArtifact({ root, buildLog, writer, settings, isExpo
727
1113
  max: maxBuilds,
728
1114
  root,
729
1115
  logFile: buildLog,
730
- out
1116
+ out,
1117
+ waitingFor: (info) => waitingFor?.(info, "build-slot")
731
1118
  });
732
1119
  slotWaitMs = buildSlot.slotWaitMs;
733
1120
  } catch (err) {
@@ -870,6 +1257,7 @@ async function acquireAndroidArtifact({ root, buildLog, writer, settings, isExpo
870
1257
  }
871
1258
  const { timings } = outcome;
872
1259
  apkPath = stored;
1260
+ handoff = outcome.handoff ?? null;
873
1261
  record.builtOn = outcome.machine;
874
1262
  record.offloadedTo = outcome.machine;
875
1263
  stats.setPlacement({
@@ -977,13 +1365,15 @@ async function acquireAndroidArtifact({ root, buildLog, writer, settings, isExpo
977
1365
  if (offload) stats.setPlacement({
978
1366
  decision: "fell-back",
979
1367
  slotWaitMs,
1368
+ deviceSlotWaitMs: stats.deviceSlotWaitMs(),
980
1369
  reason: record.offloadFallback ?? "offload failed",
981
1370
  ...fallbackMachine ? { machine: fallbackMachine } : {}
982
1371
  });
983
1372
  else stats.setPlacement({
984
1373
  decision: "here",
985
1374
  reason: hereReason,
986
- slotWaitMs
1375
+ slotWaitMs,
1376
+ deviceSlotWaitMs: stats.deviceSlotWaitMs()
987
1377
  });
988
1378
  record.builtOn = "here";
989
1379
  phase("build", `compiling ${variant || "debug"} with Gradle`);
@@ -1083,223 +1473,53 @@ async function acquireAndroidArtifact({ root, buildLog, writer, settings, isExpo
1083
1473
  }
1084
1474
  }
1085
1475
  if (remote) uploadPending = uploadRemoteBuild({
1086
- logWriter: writer,
1087
- provider: remote.provider,
1088
- platform: PLATFORM,
1089
- projectRoot: root,
1090
- fingerprintHash: storeHash,
1091
- buildPath: apkPath,
1092
- runOptions: remoteRunOptions
1093
- });
1094
- }
1095
- }
1096
- } catch (error) {
1097
- if (!(error instanceof OffloadRefusal)) throw error;
1098
- const refusal = error;
1099
- phaseFailure = fail$1(refusal.code, refusal.message, refusal.remedy, { lastBuildStatus: true });
1100
- return false;
1101
- } finally {
1102
- if (openOffload.choice) closeOffload(openOffload.choice);
1103
- releaseHeldLock();
1104
- releaseHeldSlot();
1105
- }
1106
- return true;
1107
- }
1108
- if (!easBuild) {
1109
- await prepareCachedArtifact();
1110
- if (apkPath) lateHit();
1111
- else if (swapFellBack) miss(reasonForMiss([]).reason);
1112
- if (!await buildArtifact()) return refused();
1113
- }
1114
- return {
1115
- ok: true,
1116
- artifact: {
1117
- apkPath,
1118
- androidPackage,
1119
- swapDir,
1120
- waitedForBuild,
1121
- ccache: ccacheActivity,
1122
- uploadPending,
1123
- providerUpload,
1124
- providerName,
1125
- remote,
1126
- abandonedRemote
1127
- }
1128
- };
1129
- }
1130
- function tail(lines, n = FALLBACK_LINES) {
1131
- return (Array.isArray(lines) ? lines : []).slice(-n);
1132
- }
1133
- //#endregion
1134
- //#region src/commands/android/result.ts
1135
- function androidFacts({ root, slot, serial, avdName = null, deviceName = null, systemImage = null, deviceProfile = 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, devServer = null, buildMachine = "auto", builtOn, offloadedTo = null, offloadFallback = null }) {
1136
- return {
1137
- platform: PLATFORM,
1138
- ...slot && slot !== "default" ? { slot } : {},
1139
- serial: serial ?? null,
1140
- avdName: avdName ?? null,
1141
- deviceName: deviceName ?? avdName ?? null,
1142
- systemImage: systemImage ?? null,
1143
- deviceProfile: deviceProfile ?? null,
1144
- fingerprint: fingerprint ?? null,
1145
- cacheKey: cacheKey ?? null,
1146
- variant: variant ?? null,
1147
- metroPort: metroPort ?? null,
1148
- cacheHit: cacheLevel(cacheHit),
1149
- cacheSkipped: Boolean(cacheSkipped),
1150
- buildMachine,
1151
- ...builtOn ? { builtOn } : {},
1152
- ...offloadedTo ? { offloadedTo } : {},
1153
- ...offloadFallback ? { offloadFallback } : {},
1154
- waitedForBuild: waitedForBuild ? {
1155
- pid: waitedForBuild.pid ?? null,
1156
- ms: waitedForBuild.ms ?? 0
1157
- } : null,
1158
- appPath: appPath ?? null,
1159
- bundleId: bundleId ?? null,
1160
- installSkipped: Boolean(installSkipped),
1161
- launched: launched === "unverified" || launched === "bundling" ? launched : Boolean(launched),
1162
- ccache,
1163
- debugHttpHost: debugHttpHost ?? null,
1164
- debugHttpHostNote: debugHttpHostNote ?? null,
1165
- devClientUrl: devClientUrl ?? null,
1166
- logs: logs ?? null,
1167
- ...root ? { agentDevice: { stateDir: workspaceAgentDeviceDir(root) } } : {},
1168
- durationMs: typeof durationMs === "number" && Number.isFinite(durationMs) ? durationMs : null,
1169
- ...lease === void 0 ? {} : { lease },
1170
- ...devServer ? { devServer } : {}
1171
- };
1172
- }
1173
- function lastBuildRecord({ fingerprint, cacheKey, cacheHit, cacheSkipped = false, durationMs, appPath, bundleId, startedAt, status, errorCode = null, avdName = null, deviceName = null, missReason = null, diagnostics = null, configuration = null, buildMachine = "auto", builtOn, offloadedTo = null, offloadFallback = null }) {
1174
- const record = {
1175
- platform: PLATFORM,
1176
- avdName: avdName ?? null,
1177
- deviceName: deviceName ?? avdName ?? null,
1178
- fingerprint: fingerprint ?? null,
1179
- cacheKey: cacheKey ?? null,
1180
- cacheHit: cacheLevel(cacheHit),
1181
- cacheSkipped: Boolean(cacheSkipped),
1182
- durationMs: Number.isFinite(durationMs) ? durationMs : null,
1183
- appPath: appPath ?? null,
1184
- bundleId: bundleId ?? null,
1185
- startedAt,
1186
- status,
1187
- configuration: configuration ?? null
1188
- };
1189
- if (errorCode) record.errorCode = errorCode;
1190
- record.buildMachine = buildMachine;
1191
- if (builtOn) record.builtOn = builtOn;
1192
- if (offloadedTo) record.offloadedTo = offloadedTo;
1193
- if (offloadFallback) record.offloadFallback = offloadFallback;
1194
- if (missReason && !cacheLevel(cacheHit)) record.missReason = missReason;
1195
- const recorded = status === "failed" ? buildDiagnostics(diagnostics) : [];
1196
- if (recorded.length) record.diagnostics = recorded;
1197
- return record;
1198
- }
1199
- async function finishAndroidUpload(uploadPending, remote, phase) {
1200
- if (!uploadPending) return false;
1201
- const upload = await uploadPending;
1202
- if (upload?.uploaded) phase("cache", `uploaded (${remote?.name})`);
1203
- else if (upload?.timedOut) {
1204
- phase("cache", chalk.yellow(`${remote?.name} upload still running after ${formatDuration(UPLOAD_TIMEOUT_MS)}; not waiting`));
1476
+ logWriter: writer,
1477
+ provider: remote.provider,
1478
+ platform: PLATFORM,
1479
+ projectRoot: root,
1480
+ fingerprintHash: storeHash,
1481
+ buildPath: apkPath,
1482
+ runOptions: remoteRunOptions
1483
+ });
1484
+ }
1485
+ }
1486
+ } catch (error) {
1487
+ if (!(error instanceof OffloadRefusal)) throw error;
1488
+ const refusal = error;
1489
+ phaseFailure = fail$1(refusal.code, refusal.message, refusal.remedy, { lastBuildStatus: true });
1490
+ return false;
1491
+ } finally {
1492
+ if (openOffload.choice) closeOffload(openOffload.choice);
1493
+ releaseHeldLock();
1494
+ releaseHeldSlot();
1495
+ }
1205
1496
  return true;
1206
- } else if (upload?.failed) {
1207
- const authNote = remote?.name === "eas" && isEasAuthFailureText(upload.failed) ? easAuthNote({
1208
- code: "logged-out",
1209
- reason: upload.failed,
1210
- phase: "upload"
1211
- }) : null;
1212
- phase("cache", chalk.yellow(authNote || `${remote?.name} upload failed: ${upload.failed}`));
1213
1497
  }
1214
- return false;
1215
- }
1216
- function reportAndroidResult({ root, 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, reclaimed = [], devServer = null, links }) {
1217
- const offloadedTo = record.offloadedTo ?? null;
1218
- recordRun({
1219
- failed: false,
1220
- cacheHit: cacheLevel(record.cacheHit),
1221
- waited: waitedForBuild,
1222
- durationMs,
1223
- offloadedTo
1224
- });
1225
- const facts = androidFacts({
1226
- root,
1227
- slot,
1228
- serial,
1229
- avdName: record.avdName,
1230
- deviceName: record.deviceName,
1231
- systemImage: record.systemImage,
1232
- deviceProfile: record.deviceProfile,
1233
- debugHttpHost: launched.debugHttpHost ?? null,
1234
- debugHttpHostNote: launched.debugHttpHostNote ?? null,
1235
- devClientUrl: launched.devClientUrl ?? null,
1236
- fingerprint: record.fingerprint,
1237
- cacheKey: record.cacheKey,
1238
- variant,
1239
- metroPort,
1240
- cacheHit: record.cacheHit,
1241
- cacheSkipped: !useBuildCache,
1242
- waitedForBuild,
1243
- appPath: apkPath,
1244
- bundleId: androidPackage,
1245
- installSkipped,
1246
- launched: launchState,
1247
- ccache,
1248
- logs: logsDir,
1249
- durationMs,
1250
- lease,
1251
- devServer,
1252
- buildMachine: record.buildMachine,
1253
- builtOn: record.builtOn ?? void 0,
1254
- offloadedTo,
1255
- offloadFallback: record.offloadFallback ?? null
1256
- });
1257
- writer.close();
1258
- if (json) emit(JSON.stringify({
1259
- ...facts,
1260
- ...links ? { links } : {},
1261
- ...reclaimed.length ? { reclaimed } : {}
1262
- }));
1263
- else {
1264
- const summary = `${launchWarning ? "WARNING" : "OK"}: ${androidPackage} launched on ${serial}, ${release ? `${variant} (embedded JS, no Metro)` : `Metro port ${metroPort}`} (${cacheOutcome(record.cacheHit, remote?.name ?? providerName, offloadedTo)})`;
1265
- const outcome = launchWarning ? chalk.yellow(`${summary} -- ${launchWarning}`) : launchState === "unverified" ? chalk.yellow(`${summary} -- launch UNVERIFIED`) : launchState === "bundling" ? chalk.green(`${summary} -- bundle requested, still building`) : chalk.green(summary);
1266
- const deviceName = record.avdName || record.deviceName || serial;
1267
- const cacheResult = useBuildCache ? cacheOutcome(record.cacheHit, remote?.name ?? providerName, offloadedTo) : `bypassed; ${cacheOutcome(false, null, offloadedTo)}`;
1268
- const metroResult = release ? `embedded (${variant})` : !metroCheck ? `check skipped on port ${metroPort}` : launchState === "unverified" ? `state unverified on port ${metroPort}` : `running on port ${metroPort}${devServer ? ` (started: ${devServer.reason})` : ""}`;
1269
- emit([
1270
- outcome,
1271
- phaseLine("device", `${deviceName} (${serial})`),
1272
- phaseLine("app", androidPackage),
1273
- phaseLine("metro", metroResult),
1274
- phaseLine("cache", cacheResult),
1275
- ...ccache.status === "not-run" ? [phaseLine("compilation cache", ccacheActivityLine(ccache))] : [],
1276
- phaseLine("logs", logsDir || "unavailable (remote device)"),
1277
- phaseLine("agent-device", `AGENT_DEVICE_STATE_DIR=${workspaceAgentDeviceDir(root)}`)
1278
- ].join("\n"));
1279
- if (links) console.error(chalk.dim(workspaceLinkLine(links)));
1498
+ if (!easBuild) {
1499
+ await prepareCachedArtifact();
1500
+ if (apkPath) lateHit();
1501
+ else if (swapFellBack) miss(reasonForMiss([]).reason);
1502
+ if (!await buildArtifact()) return refused();
1280
1503
  }
1281
- return facts;
1282
- }
1283
- function cacheOutcome(cacheHit, providerName = null, offloadedTo = null) {
1284
- if (cacheHit === "remote") return `cache hit from ${providerName || "the remote cache"}`;
1285
- if (cacheHit === "local") return "cache hit";
1286
- return offloadedTo ? `built on ${offloadedTo}` : "built";
1504
+ return {
1505
+ ok: true,
1506
+ artifact: {
1507
+ apkPath,
1508
+ handoff,
1509
+ androidPackage,
1510
+ swapDir,
1511
+ waitedForBuild,
1512
+ ccache: ccacheActivity,
1513
+ uploadPending,
1514
+ providerUpload,
1515
+ providerName,
1516
+ remote,
1517
+ abandonedRemote
1518
+ }
1519
+ };
1287
1520
  }
1288
- function persistLastBuild({ recordBuild = recordFinishedBuild, root, record, startedAt, durationMs, status, errorCode = null, diagnostics = null, out }) {
1289
- const lastBuild = lastBuildRecord({
1290
- ...record,
1291
- startedAt,
1292
- durationMs,
1293
- status,
1294
- errorCode,
1295
- diagnostics
1296
- });
1297
- try {
1298
- recordBuild(root, lastBuild);
1299
- } catch (err) {
1300
- out(phaseLine("state", chalk.yellow(`could not record lastBuild: ${err?.message || err}`)));
1301
- }
1302
- return lastBuild;
1521
+ function tail(lines, n = FALLBACK_LINES) {
1522
+ return (Array.isArray(lines) ? lines : []).slice(-n);
1303
1523
  }
1304
1524
  //#endregion
1305
1525
  //#region src/commands/android/collector.ts
@@ -1618,7 +1838,7 @@ async function finishAndroidRun({ lease, releaseLease, root, slot, json, metroCh
1618
1838
  });
1619
1839
  if (booted.failed) {
1620
1840
  const diag = diagnoseBootFailure(booted, emuLog, runCommand, physical);
1621
- return fail(NO_DEVICE, diag.message, diag.remedy, {
1841
+ return fail(diag.code, diag.message, diag.remedy, {
1622
1842
  lines: diag.lines,
1623
1843
  logPath: diag.logPath ? displayPath(root, diag.logPath) : null
1624
1844
  });
@@ -1951,12 +2171,23 @@ function reportMetroRoute({ launched, remote, metroPort, androidPackage, serial,
1951
2171
  });
1952
2172
  }
1953
2173
  function diagnoseBootFailure(booted, logFile, runCommand, physical) {
1954
- return noDeviceDiagnostic({
2174
+ if (booted.code) return {
2175
+ code: booted.code,
2176
+ message: booted.reason ?? "The emulator did not boot.",
2177
+ remedy: booted.remedy ?? null,
2178
+ lines: [],
2179
+ logPath: null
2180
+ };
2181
+ const diag = noDeviceDiagnostic({
1955
2182
  reason: booted.reason ?? "The emulator did not boot.",
1956
2183
  logFile,
1957
2184
  remedy: booted.remedy ?? `Run \`stim status\` to see what Stim thinks it owns; re-running \`${runCommand}\` creates a fresh owned AVD.`,
1958
2185
  localEmulator: !physical
1959
2186
  });
2187
+ return {
2188
+ code: NO_DEVICE,
2189
+ ...diag
2190
+ };
1960
2191
  }
1961
2192
  //#endregion
1962
2193
  //#region src/commands/android/plan.ts
@@ -2045,13 +2276,78 @@ function resolveAndroidRunPlan({ settings, settingsContext, slot, easProfile, va
2045
2276
  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.");
2046
2277
  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.");
2047
2278
  const noWait = waitFlag === false;
2048
- const waitFlagged = waitFlag !== void 0;
2049
- if (waitConflict) return fail("STIM_BAD_ARG", "--wait and --no-wait ask for opposite things.", "Pass `--wait <seconds>` to wait for the lease, or `--no-wait` to install without one.");
2050
- if (waitFlagged && !physical) return fail("STIM_BAD_ARG", "--wait and --no-wait only apply to a `--device` run.", "This workspace owns its emulator, so nothing contends for it. Drop the flag, or pass `--device`.");
2279
+ if (waitConflict) return fail("STIM_BAD_ARG", "--wait and --no-wait ask for opposite things.", "Pass only one of `--wait <seconds>` and `--no-wait`.");
2051
2280
  const waitParsed = parseDeviceWait(noWait ? void 0 : waitFlag);
2052
- if ("error" in waitParsed) return fail("STIM_BAD_ARG", waitParsed.error, "Pass a whole number of seconds, e.g. --wait 90. `--wait 0` refuses a leased device at once.");
2281
+ if ("error" in waitParsed) return fail("STIM_BAD_ARG", waitParsed.error, "Pass a whole number of seconds, e.g. --wait 90. `--wait 0` refuses a busy lease or device slot at once.");
2053
2282
  const waitSeconds = waitParsed.seconds;
2054
- const remoteBackend = physical ? null : commandRemoteBackend ?? remoteAndroidSetting(settings);
2283
+ const remoteTarget = commandRemoteBackend !== null ? parseIosRemote(commandRemoteBackend) : remoteAndroidSetting(settings);
2284
+ if (physical && remoteTarget?.kind === "machine") return fail("STIM_BAD_ARG", "--device installs on a phone connected to this machine, and --remote installs on a remote one.", "Pass only one of --device and --remote; unset android.remote for a local phone.");
2285
+ const machine = remoteTarget?.kind === "machine" ? remoteTarget.machine : null;
2286
+ const remoteBackend = !physical && remoteTarget?.kind === "backend" ? remoteTarget.backend : null;
2287
+ if (!machine) {
2288
+ const refusal = androidDeviceSelectorRefusal({
2289
+ settingsContext,
2290
+ slot,
2291
+ systemImageFlag,
2292
+ deviceProfileFlag,
2293
+ systemImage,
2294
+ deviceProfile,
2295
+ physical,
2296
+ remoteBackend
2297
+ }, {
2298
+ listSystemImages,
2299
+ listDeviceProfiles,
2300
+ supportsFold,
2301
+ ownedAvd
2302
+ });
2303
+ if (refusal) return refusal;
2304
+ }
2305
+ const target = physical ? {
2306
+ kind: "physical",
2307
+ serial: typeof deviceFlag === "string" ? deviceFlag : null,
2308
+ lease: {
2309
+ waitSeconds,
2310
+ noWait
2311
+ }
2312
+ } : machine ? {
2313
+ kind: "hosted",
2314
+ machine,
2315
+ systemImage,
2316
+ deviceProfile
2317
+ } : remoteBackend ? {
2318
+ kind: "remote",
2319
+ backend: remoteBackend,
2320
+ systemImage,
2321
+ deviceProfile
2322
+ } : {
2323
+ kind: "emulator",
2324
+ systemImage,
2325
+ deviceProfile
2326
+ };
2327
+ return {
2328
+ ok: true,
2329
+ plan: {
2330
+ build: {
2331
+ variant,
2332
+ release,
2333
+ profile: buildProfile,
2334
+ cas,
2335
+ cache: cachePolicy,
2336
+ compilerCache: optimizations.android.compilerCache,
2337
+ gradleBuildCache: optimizations.android.gradleBuildCache,
2338
+ pch: optimizations.android.pch,
2339
+ targetAbiOnly: optimizations.android.targetAbiOnly
2340
+ },
2341
+ target,
2342
+ deviceSlotWaitMs: noWait ? 0 : waitFlag === void 0 && !physical ? DEFAULT_DEVICE_SLOT_WAIT_MS : waitSeconds * 1e3,
2343
+ isExpo,
2344
+ metroWarmup: optimizations.metroWarmup,
2345
+ cacheProviderConfig
2346
+ }
2347
+ };
2348
+ }
2349
+ function androidDeviceSelectorRefusal({ settingsContext, slot, systemImageFlag, deviceProfileFlag, systemImage, deviceProfile, physical, remoteBackend }, { listSystemImages = listInstalledSystemImages, listDeviceProfiles = listAvdDeviceProfiles, supportsFold = systemImageSupportsFold, ownedAvd = slotOwnedAvd } = {}) {
2350
+ const root = settingsContext.projectPath;
2055
2351
  const settingsLayersForOrigin = settingsLayers(settingsContext);
2056
2352
  const imageRefusal = systemImageRefusal({
2057
2353
  slot,
@@ -2093,43 +2389,7 @@ function resolveAndroidRunPlan({ settings, settingsContext, slot, easProfile, va
2093
2389
  });
2094
2390
  if (foldRefusal) return fail(foldRefusal.code, foldRefusal.message, foldRefusal.remedy);
2095
2391
  }
2096
- const target = physical ? {
2097
- kind: "physical",
2098
- serial: typeof deviceFlag === "string" ? deviceFlag : null,
2099
- lease: {
2100
- waitSeconds,
2101
- noWait
2102
- }
2103
- } : remoteBackend ? {
2104
- kind: "remote",
2105
- backend: remoteBackend,
2106
- systemImage,
2107
- deviceProfile
2108
- } : {
2109
- kind: "emulator",
2110
- systemImage,
2111
- deviceProfile
2112
- };
2113
- return {
2114
- ok: true,
2115
- plan: {
2116
- build: {
2117
- variant,
2118
- release,
2119
- profile: buildProfile,
2120
- cas,
2121
- cache: cachePolicy,
2122
- compilerCache: optimizations.android.compilerCache,
2123
- gradleBuildCache: optimizations.android.gradleBuildCache,
2124
- pch: optimizations.android.pch,
2125
- targetAbiOnly: optimizations.android.targetAbiOnly
2126
- },
2127
- target,
2128
- isExpo,
2129
- metroWarmup: optimizations.metroWarmup,
2130
- cacheProviderConfig
2131
- }
2132
- };
2392
+ return null;
2133
2393
  }
2134
2394
  //#endregion
2135
2395
  //#region src/commands/android/next-build.ts
@@ -2268,7 +2528,7 @@ async function planAndroid(opts, overrides = {}) {
2268
2528
  if (target.kind === "physical") return refuse(planFlagRefusal("--device"));
2269
2529
  return refuse({
2270
2530
  code: "STIM_BAD_ARG",
2271
- message: `android.remote routes this workspace's runs to a ${target.backend} device, whose ABI --plan cannot read without a session.`,
2531
+ message: `android.remote routes this workspace's runs to a ${target.kind === "hosted" ? target.machine : target.backend} device, whose ABI --plan cannot read without a session.`,
2272
2532
  remedy: "Run `stim android` to build for the remote device, or unset android.remote to plan the owned emulator."
2273
2533
  });
2274
2534
  }
@@ -2324,11 +2584,10 @@ function androidCommand(program) {
2324
2584
  registerAndroid(program);
2325
2585
  }
2326
2586
  function registerAndroid(program) {
2327
- 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. A Debug run starts the dev server when it is not running.").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)", parseDeviceSlotOption).option("--build-machine <value>", "Build on auto, local, or one named machine; a name refuses without fallback", parseBuildMachineOption).option("--json", "Emit the facts as a single JSON line on stdout; every other line goes to stderr").option("--plan", "Predict the next build without building, booting or installing: resolve the fingerprint, check the local then remote cache, and print the expected cache result and duration. Writes no Stim state.").option("--no-metro-check", "Skip the reserved-port Metro health check and do not start it (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-profile <id>", "Hardware profile to create this workspace's owned AVD with, as an `avdmanager list device -c` id (e.g. \"pixel_fold\", \"pixel_tablet\"). Overrides the android.deviceProfile setting for this invocation. An unknown id refuses with STIM_BAD_ARG and prints the available ids.").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) => {
2328
- if (REMOTE_DEVICE_BACKENDS.includes(value)) return value;
2329
- if (!parseMachine(value)) throw new InvalidArgumentError(`expected one of: ${REMOTE_DEVICE_BACKENDS.join(", ")}`);
2330
- throw new InvalidArgumentError("STIM_BAD_ARG: Android on a paired Mac is not available yet. Use eas or proxy, or run Android locally.");
2331
- }).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) => {
2587
+ 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. A Debug run starts the dev server when it is not running.").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)", parseDeviceSlotOption).option("--remote-build <value>", "Build on auto, local, or one named machine; a name refuses without fallback", parseBuildMachineOption).option("--json", "Emit the facts as a single JSON line on stdout; every other line goes to stderr").option("--plan", "Predict the next build without building, booting or installing: resolve the fingerprint, check the local then remote cache, and print the expected cache result and duration. Writes no Stim state.").option("--no-metro-check", "Skip the reserved-port Metro health check and do not start it (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-profile <id>", "Hardware profile to create this workspace's owned AVD with, as an `avdmanager list device -c` id (e.g. \"pixel_fold\", \"pixel_tablet\"). Overrides the android.deviceProfile setting for this invocation. An unknown id refuses with STIM_BAD_ARG and prints the available ids.").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 <target>", "Run on eas, proxy, or a named approved Mac from remote.machines; auto places on an approved Mac when this Mac is full or busy", (value) => {
2588
+ if (!parseMachine(value)) throw new InvalidArgumentError("expected eas, proxy, auto, or a tailnet machine name");
2589
+ return value;
2590
+ }).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). Without --device, wait for an owned-device slot (default 600 seconds; --no-wait refuses immediately).").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. Without --device, refuse a full device cap or existing slot queue immediately.").action(async (opts) => {
2332
2591
  if (opts.plan) return planAndroid(opts);
2333
2592
  const root = findProjectRoot$1(process.cwd());
2334
2593
  if (!root) {
@@ -2351,7 +2610,7 @@ function registerAndroid(program) {
2351
2610
  try {
2352
2611
  return await runAndroid({
2353
2612
  root,
2354
- buildMachine: opts.buildMachine,
2613
+ buildMachine: opts.remoteBuild,
2355
2614
  slot: opts.slot,
2356
2615
  easProfile: opts.easProfile,
2357
2616
  json: Boolean(opts.json),
@@ -2372,7 +2631,7 @@ function registerAndroid(program) {
2372
2631
  }, { write: (line) => console.error(chalk.dim(phaseLine("lock", line))) })).ok) process.exit(runCancellation() ? 130 : 1);
2373
2632
  });
2374
2633
  }
2375
- 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, deviceProfile: deviceProfileFlag, 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, listDeviceProfiles, ensureDeviceBooted = ensureBooted, resolveAvdSerial = resolveOwnedAvdSerial, waitForDeviceBoot = waitForBoot, dataFreeBytes, trimCaches, wipeDevice, resolveMetro = resolveProjectMetro, warmMetro: prewarmMetro = warmMetro, startServer = startDevServer, readState = readWorkspaceState$1, 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, planPrebuildFor = planPrebuild, 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, recordBuild, recordStats = recordRunStats, readEstimates = readRunEstimates, now = Date.now, out = (line) => console.error(line), emit = (line) => console.log(line) } = {}) {
2634
+ 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, deviceProfile: deviceProfileFlag, 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, listDeviceProfiles, ensureDeviceBooted = ensureBooted, resolveAvdSerial = resolveOwnedAvdSerial, waitForDeviceBoot = waitForBoot, dataFreeBytes, trimCaches, wipeDevice, resolveMetro = resolveProjectMetro, warmMetro: prewarmMetro = warmMetro, startServer = startDevServer, readState = readWorkspaceState$1, 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, planPrebuildFor = planPrebuild, 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, recordBuild, recordStats = recordRunStats, recordCapacityRefusal: recordRefusal, readEstimates = readRunEstimates, now = Date.now, out = (line) => console.error(line), emit = (line) => console.log(line) } = {}) {
2376
2635
  return {
2377
2636
  root,
2378
2637
  json,
@@ -2456,6 +2715,7 @@ function resolveRunAndroidOptions({ root, json = false, remoteDevice: commandRem
2456
2715
  writeState,
2457
2716
  recordBuild,
2458
2717
  recordStats,
2718
+ recordCapacityRefusal: recordRefusal,
2459
2719
  readEstimates,
2460
2720
  now,
2461
2721
  out,
@@ -2498,6 +2758,11 @@ function androidSlotOptions(options) {
2498
2758
  function avdSetupFailure(error, root, logFile) {
2499
2759
  const refusal = (0, ownership_claim_exports.claimFailure)(error, "stim android");
2500
2760
  if (refusal) return refusal;
2761
+ if (error instanceof DeviceAdmissionRefusal) return {
2762
+ code: error.code,
2763
+ message: error.message,
2764
+ remedy: error.remedy
2765
+ };
2501
2766
  const diag = noDeviceDiagnostic({
2502
2767
  reason: `Could not ensure an owned Android emulator: ${error?.message || error}`,
2503
2768
  logFile,
@@ -2515,7 +2780,7 @@ function avdSetupFailure(error, root, logFile) {
2515
2780
  };
2516
2781
  }
2517
2782
  async function runAndroid(options = {}) {
2518
- let { root, json, commandRemoteBackend, resolveSettingsFor, resolveRemoteDeviceContext, makeRemoteDeviceDeps, resolveEasBin, ensureRemoteMetro, ensureRemoteOwned, detectRemoteProviders, metroCheck, useBuildCache: requestedBuildCache, easProfile, resolveEasBuild, variantFlag, systemImageFlag, deviceProfileFlag, deviceFlag, waitFlag, waitConflict, acquireLease, makeRunLease, selectPool, onLeaseSignal, listDevices, deviceModel, deviceAbi, isEmulatorDevice, readApkPackage, getLimits, checkCapacity, acquireSlot, releaseSlot, ensureDevice, listSystemImages, listDeviceProfiles, ensureDeviceBooted, resolveAvdSerial, waitForDeviceBoot, dataFreeBytes, trimCaches, wipeDevice, resolveMetro, prewarmMetro, startServer, readState, pidAlive, verifyCollector, verifyLaunched, ensureStorage, fingerprint, untracked, resolveCached, storeCached, storedAssets, captureAssets, acquireLock, releaseLock, waitForBuild, loadProvider, easAuth, resolveRemoteBuild, uploadRemoteBuild, resolveCacheProvider, loadCacheProviderModule, planPrebuildFor, prebuild, build, ccacheFor, install, launch, launchRelease, verifyReleaseLaunched, swapApk, resolveDevClientScheme, spawn, kill, createWriter, writeLaunch, writeState, recordBuild, recordStats, readEstimates, now, out, emit } = androidSlotOptions(options);
2783
+ let { root, json, commandRemoteBackend, resolveSettingsFor, resolveRemoteDeviceContext, makeRemoteDeviceDeps, resolveEasBin, ensureRemoteMetro, ensureRemoteOwned, detectRemoteProviders, metroCheck, useBuildCache: requestedBuildCache, easProfile, resolveEasBuild, variantFlag, systemImageFlag, deviceProfileFlag, deviceFlag, waitFlag, waitConflict, acquireLease, makeRunLease, selectPool, onLeaseSignal, listDevices, deviceModel, deviceAbi, isEmulatorDevice, readApkPackage, getLimits, checkCapacity, acquireSlot, releaseSlot, ensureDevice, listSystemImages, listDeviceProfiles, ensureDeviceBooted, resolveAvdSerial, waitForDeviceBoot, dataFreeBytes, trimCaches, wipeDevice, resolveMetro, prewarmMetro, startServer, readState, pidAlive, verifyCollector, verifyLaunched, ensureStorage, fingerprint, untracked, resolveCached, storeCached, storedAssets, captureAssets, acquireLock, releaseLock, waitForBuild, loadProvider, easAuth, resolveRemoteBuild, uploadRemoteBuild, resolveCacheProvider, loadCacheProviderModule, planPrebuildFor, prebuild, build, ccacheFor, install, launch, launchRelease, verifyReleaseLaunched, swapApk, resolveDevClientScheme, spawn, kill, createWriter, writeLaunch, writeState, recordBuild, recordStats, recordCapacityRefusal: recordRefusal = recordCapacityRefusal, readEstimates, now, out, emit } = androidSlotOptions(options);
2519
2784
  const progress = options.progress ?? NO_BUILD_PROGRESS;
2520
2785
  const slot = validateDeviceSlot(options.slot);
2521
2786
  const started = now();
@@ -2621,6 +2886,7 @@ async function runAndroid(options = {}) {
2621
2886
  code,
2622
2887
  message,
2623
2888
  remedy: remedy ?? null,
2889
+ ...record.devicePlacement ? { devicePlacement: record.devicePlacement } : {},
2624
2890
  ...ccacheActivity.status === "not-run" ? {} : { ccache: ccacheActivity },
2625
2891
  ...lease === void 0 ? {} : { lease },
2626
2892
  ...reclaimed.length ? { reclaimed } : {}
@@ -2682,16 +2948,59 @@ async function runAndroid(options = {}) {
2682
2948
  });
2683
2949
  if (!planned.ok) return fail(planned.code, planned.message, planned.remedy, { lines: planned.lines });
2684
2950
  const { plan } = planned;
2685
- const { build: buildPlan, target, isExpo, cacheProviderConfig } = plan;
2951
+ const deviceSlotWait = {
2952
+ signal: runCancellationSignal(),
2953
+ waitMs: plan.deviceSlotWaitMs,
2954
+ displayName: basename(root),
2955
+ waitingFor: (info) => progress.waitingFor(info, "device-slot"),
2956
+ onWait: (ms) => {
2957
+ stats.addDeviceSlotWaitMs(ms);
2958
+ if (record.devicePlacement?.decision === "local" && ms > 0) record.devicePlacement.decision = "waited-locally";
2959
+ }
2960
+ };
2961
+ const { build: buildPlan, isExpo, cacheProviderConfig } = plan;
2686
2962
  const { variant, release, cache: cachePolicy } = buildPlan;
2687
2963
  record.configuration = variant ?? "debug";
2688
2964
  const useBuildCache = cachePolicy.read;
2689
- const physical = target.kind === "physical";
2690
- const remoteBackend = target.kind === "remote" ? target.backend : null;
2691
- const budget = await (options.checkBudget ?? budgetGate)({
2965
+ const hosting = await selectAndroidPlacement({
2692
2966
  root,
2693
- note: out
2967
+ slot,
2968
+ target: plan.target,
2969
+ release,
2970
+ metroCheck,
2971
+ noWait: plan.deviceSlotWaitMs === 0,
2972
+ buildMachine: options.buildMachine,
2973
+ read: options.readHostedAndroid,
2974
+ resolveSerial: resolveAvdSerial,
2975
+ prepare: options.prepareHostedAndroid,
2976
+ automatic: options.automaticDevicePlacement,
2977
+ localSelectors: (target) => androidDeviceSelectorRefusal({
2978
+ settingsContext,
2979
+ slot,
2980
+ systemImageFlag,
2981
+ deviceProfileFlag,
2982
+ systemImage: target.systemImage,
2983
+ deviceProfile: target.deviceProfile,
2984
+ physical: false,
2985
+ remoteBackend: null
2986
+ }, {
2987
+ listSystemImages,
2988
+ listDeviceProfiles
2989
+ }),
2990
+ checkBudget: options.checkBudget,
2991
+ note: out,
2992
+ phase
2694
2993
  });
2994
+ if ("failure" in hosting) return fail(hosting.failure.code, hosting.failure.message, hosting.failure.remedy);
2995
+ const { target, hostedTarget, budget, selectors } = hosting;
2996
+ record.devicePlacement = hosting.devicePlacement;
2997
+ writeDevicePlacement(root, slot, PLATFORM, record.devicePlacement);
2998
+ const physical = target.kind === "physical";
2999
+ const remoteBackend = target.kind === "remote" ? target.backend : null;
3000
+ if (hostedTarget) {
3001
+ if (settings.androidEmulatorApp !== void 0) phase("device", "androidEmulatorApp is ignored on a hosting Mac; the emulator boots headless.");
3002
+ if (publicUrlSetting(settings) || tunnelModeSetting(settings)) phase("metro", "metro.publicUrl and metro.tunnel are ignored on a hosting Mac; Metro uses the private tailnet bridge.");
3003
+ }
2695
3004
  reclaimed = budget.reclaimed;
2696
3005
  if (budget.refusal) return fail(budget.refusal.code, budget.refusal.message, budget.refusal.remedy);
2697
3006
  const remoteContext = remoteBackend ? await resolveRemoteDeviceContext({
@@ -2734,7 +3043,7 @@ async function runAndroid(options = {}) {
2734
3043
  const gate = await ensureDevServer({
2735
3044
  root,
2736
3045
  port: reservedPort,
2737
- settings,
3046
+ settings: hostedMetroSettings(settings, Boolean(hostedTarget)),
2738
3047
  remote: remoteContext !== null,
2739
3048
  note: out,
2740
3049
  resolve: resolveMetro,
@@ -2788,7 +3097,14 @@ async function runAndroid(options = {}) {
2788
3097
  let bootDuration = "";
2789
3098
  let bootPromise;
2790
3099
  let startRemoteBoot = null;
2791
- if (target.kind === "physical" && !target.serial) {
3100
+ if (hostedTarget) {
3101
+ device = {
3102
+ owned: false,
3103
+ systemImage: hostedTarget.choice.systemImage,
3104
+ deviceProfile: hostedTarget.choice.deviceProfile
3105
+ };
3106
+ bootPromise = Promise.resolve({ ok: true });
3107
+ } else if (target.kind === "physical" && !target.serial) {
2792
3108
  const pooled = await pooledAndroidDevice({
2793
3109
  root,
2794
3110
  selectPool,
@@ -2824,12 +3140,19 @@ async function runAndroid(options = {}) {
2824
3140
  } catch (err) {
2825
3141
  return fail("STIM_BAD_ARG", err.message, "Run `stim settings set androidEmulatorApp emulator` or `stim settings set androidEmulatorApp stim-desktop`.");
2826
3142
  }
2827
- const capacity = checkCapacity({
3143
+ const capacity = plan.deviceSlotWaitMs === 0 ? checkCapacity({
2828
3144
  platform: PLATFORM,
2829
3145
  project,
2830
3146
  max: limits.maxDevices
2831
- });
2832
- if (capacity) return fail(capacity.code, capacity.message, capacity.remedy);
3147
+ }) : null;
3148
+ if (capacity) {
3149
+ if (capacity.code === "STIM_AT_CAPACITY") recordRefusal({
3150
+ platform: PLATFORM,
3151
+ max: limits.maxDevices,
3152
+ workspace: workspaceId(root)
3153
+ }, now());
3154
+ return fail(capacity.code, capacity.message, capacity.remedy);
3155
+ }
2833
3156
  const prepare = stepClock(now);
2834
3157
  try {
2835
3158
  device = await ensureDevice({
@@ -2839,6 +3162,7 @@ async function runAndroid(options = {}) {
2839
3162
  settingsRoot,
2840
3163
  settings,
2841
3164
  flags: {
3165
+ deviceSlotWait,
2842
3166
  systemImage: target.systemImage,
2843
3167
  systemImageFlag: systemImageFlag?.trim() || null,
2844
3168
  deviceProfile: target.deviceProfile
@@ -2858,6 +3182,7 @@ async function runAndroid(options = {}) {
2858
3182
  platform: PLATFORM,
2859
3183
  device,
2860
3184
  projectPath: root,
3185
+ deviceSlotWait,
2861
3186
  out,
2862
3187
  logFile: emuLog
2863
3188
  })).catch((e) => ({
@@ -2907,6 +3232,7 @@ async function runAndroid(options = {}) {
2907
3232
  isExpo,
2908
3233
  device,
2909
3234
  physical,
3235
+ hostedAbi: hostedTarget?.choice.architecture,
2910
3236
  remote: Boolean(remoteDevice),
2911
3237
  buildPlan,
2912
3238
  cacheProviderConfig,
@@ -2924,7 +3250,8 @@ async function runAndroid(options = {}) {
2924
3250
  miss: progress.miss,
2925
3251
  hit: progress.hit,
2926
3252
  place: progress.place,
2927
- waitingOn: progress.waitingOn
3253
+ waitingOn: progress.waitingOn,
3254
+ waitingFor: progress.waitingFor
2928
3255
  }
2929
3256
  }, {
2930
3257
  deviceAbi,
@@ -2963,6 +3290,40 @@ async function runAndroid(options = {}) {
2963
3290
  androidPackage = artifact.androidPackage;
2964
3291
  record.appPath = apkPath;
2965
3292
  if (runCancellation()) return fail("STIM_CANCELLED", "before install");
3293
+ if (hostedTarget) return finishHostedAndroidRun({
3294
+ target: hostedTarget,
3295
+ artifact,
3296
+ root,
3297
+ slot,
3298
+ release,
3299
+ isExpo,
3300
+ metroPort,
3301
+ logsDir,
3302
+ selectors,
3303
+ place: options.placeHostedAndroid ?? placeHostedAndroid,
3304
+ writePlacement: options.writeHostedAndroid ?? writeHostedAndroid,
3305
+ verifyLaunch: verifyLaunched,
3306
+ writeLaunch,
3307
+ readApkPackage,
3308
+ resolveDevClientScheme,
3309
+ now,
3310
+ started,
3311
+ startedAt,
3312
+ record,
3313
+ fail,
3314
+ phase,
3315
+ out,
3316
+ json,
3317
+ useBuildCache,
3318
+ variant,
3319
+ metroCheck,
3320
+ writer,
3321
+ emit,
3322
+ recordRun,
3323
+ reclaimed,
3324
+ devServer,
3325
+ recordBuild
3326
+ });
2966
3327
  if (startRemoteBoot) {
2967
3328
  progress.step("device");
2968
3329
  bootPromise = startRemoteBoot();
@@ -3091,6 +3452,7 @@ async function runAndroid(options = {}) {
3091
3452
  serial: void 0
3092
3453
  },
3093
3454
  projectPath: root,
3455
+ deviceSlotWait,
3094
3456
  out,
3095
3457
  logFile: emuLog
3096
3458
  })).catch((e) => ({