stim 1.19.0 → 1.20.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 (172) hide show
  1. package/README.md +20 -3
  2. package/dist/{activity-BSw0x84R.mjs → activity-BaMpUQ4-.mjs} +3 -3
  3. package/dist/{agent-device-usage-output-Bjxu_VmX.mjs → agent-device-usage-output-VPaOgGaF.mjs} +7 -7
  4. package/dist/android-CLbBJ2ve.mjs +1258 -0
  5. package/dist/{android-DE5g_5gv.mjs → android-CZh6ElG5.mjs} +2 -2
  6. package/dist/{android-BdQC6zs1.mjs → android-DE8-JfGW.mjs} +105 -47
  7. package/dist/{android-cas-C87geAg-.mjs → android-cas-BFpS84v2.mjs} +9 -5
  8. package/dist/android-cas-compiler.mjs +1 -1
  9. package/dist/android-eXfIjKJr.mjs +6 -0
  10. package/dist/api-run.mjs +159 -8
  11. package/dist/{api-DIf_s86s.mjs → api-zuX3ynYN.mjs} +5 -1
  12. package/dist/api.d.mts +98 -4
  13. package/dist/api.mjs +1 -1
  14. package/dist/{app-install-CFI4ptUL.mjs → app-install-Cg9YJy3x.mjs} +9 -8
  15. package/dist/artifact-Ctb2jpw6.mjs +807 -0
  16. package/dist/artifact-DXk_Fle_.mjs +772 -0
  17. package/dist/artifact-lifecycle-DzPpBYvK.mjs +30 -0
  18. package/dist/{browser-web-CDhqQxhv.mjs → browser-web-xQWC2RWl.mjs} +19 -19
  19. package/dist/{budget-Dz-kyXLv.mjs → budget-RFbwFpG5.mjs} +3101 -3062
  20. package/dist/build-COBA1tBc.mjs +462 -0
  21. package/dist/{build-facts-B87vo-Im.d.mts → build-facts-DcLv2HSd.d.mts} +1 -32
  22. package/dist/build-plan-CRjAA6Qi.mjs +553 -0
  23. package/dist/{cache-manifest-DiwKSyv9.mjs → cache-manifest-qA3dT2x2.mjs} +1 -1
  24. package/dist/cache-manifest.mjs +1 -1
  25. package/dist/{manifest-CX0d1IkZ.mjs → ccache-CtbqWxIC.mjs} +13 -19
  26. package/dist/{chrome-BTUOo8p2.mjs → chrome-Dp2lMLBe.mjs} +1 -1
  27. package/dist/{cli-C9Zvgbdc.mjs → cli-sGqlPu1n.mjs} +20 -20
  28. package/dist/cli.mjs +1 -1
  29. package/dist/{client-CuUo8Nvg.mjs → client-zqjFd79D.mjs} +89 -307
  30. package/dist/collector-run.d.mts +4 -0
  31. package/dist/collector-run.mjs +7 -7
  32. package/dist/{command-output-Bt8ygHE7.mjs → command-output-CPDq_90S.mjs} +2 -2
  33. package/dist/{config-DC5jEtd1.mjs → config-D0LhTY3S.mjs} +8 -12
  34. package/dist/{created-devices-D0s26Hx5.mjs → created-devices-CXBMtoj5.mjs} +7 -6
  35. package/dist/{dependency-state-DKaS1OM9.mjs → dependency-state-B2m8eqsS.mjs} +2 -2
  36. package/dist/{detached-entry-pB_IPfV4.mjs → detached-entry-BXq4fIl-.mjs} +1 -1
  37. package/dist/{dev-client-CDY_zAmD.mjs → dev-client-BqYjMIaC.mjs} +2 -2
  38. package/dist/{device-BHw0WlHl.mjs → device-BgNj-qVj.mjs} +8 -8
  39. package/dist/device-D4DqjSZZ.mjs +727 -0
  40. package/dist/{device-capacity-BoN1jQkO.mjs → device-capacity-DrwHwR97.mjs} +13 -14
  41. package/dist/device-host-worker.mjs +38 -18
  42. package/dist/device-ios-DE8KJd3b.mjs +413 -0
  43. package/dist/{device-lease-Cu7PtQpZ.mjs → device-lease-C309csaB.mjs} +138 -24
  44. package/dist/{device-lease-run-D38SnZtt.mjs → device-lease-run-Baelt3nc.mjs} +4 -4
  45. package/dist/{device-pool-CP4rAsHx.mjs → device-pool-DtK9S6AD.mjs} +3 -3
  46. package/dist/{device-remote-B5GiUifk.mjs → device-remote-DjsNw8DD.mjs} +17 -68
  47. package/dist/doctor-DiP9HsIP.mjs +515 -0
  48. package/dist/{doctor-PRJH94v6.mjs → doctor-Dunu3Nv1.mjs} +333 -744
  49. package/dist/{doctor-watchman-wZhyTdp4.mjs → doctor-watchman-XJzT8tK-.mjs} +15 -13
  50. package/dist/{simslim-D9LkJxIO.mjs → eas-build-0gadvrca.mjs} +6 -152
  51. package/dist/{stim-installations-DbmUjsLv.mjs → eas-session-ledger-DJDeL4Uq.mjs} +57 -4
  52. package/dist/{error-diagnostics--v5HGOU0.mjs → error-diagnostics-BhQtoLrZ.mjs} +17 -12
  53. package/dist/errors-DzkMYh8Q.mjs +205 -0
  54. package/dist/{exec-i8GDPwq9.mjs → exec-B81Qodjn.mjs} +40 -10
  55. package/dist/{gc-CyHMcKcn.mjs → gc-D5cz5G0k.mjs} +22 -21
  56. package/dist/{device-ios-B2ruohE-.mjs → gradle-CZZo1TSA.mjs} +21 -650
  57. package/dist/{guide-dRaQJJk9.mjs → guide-DakjcoUt.mjs} +503 -251
  58. package/dist/{hosted-android-Bs7fytj7.mjs → hosted-android-C-D8Dj0_.mjs} +5 -5
  59. package/dist/{hosted-client-C5G4h_OJ.mjs → hosted-client--hNFgixu.mjs} +9 -3
  60. package/dist/{hosted-ios-Bik035eL.mjs → hosted-ios-Chx9XQyI.mjs} +3 -3
  61. package/dist/{hosted-logs-CI9Az9a4.mjs → hosted-logs-Bkz0Hxzh.mjs} +8 -8
  62. package/dist/{hosted-macos-DABm3ScO.mjs → hosted-macos-ckTjN-0V.mjs} +7 -6
  63. package/dist/{hosted-native-DbV2aUVw.mjs → hosted-native-3KXZ5-qn.mjs} +17 -14
  64. package/dist/{idle-shutdown-DYiMBn34.mjs → idle-shutdown-DhAUyf5s.mjs} +7 -7
  65. package/dist/{ios-DYUr9LGc.mjs → ios-BDBN-xpf.mjs} +8 -7
  66. package/dist/ios-CQ8bHQXB.mjs +1112 -0
  67. package/dist/{ios-DCrkgXEM.mjs → ios-VGvNdsv2.mjs} +1 -1
  68. package/dist/{ios-device-CrW8aMb5.mjs → ios-device-B1B14xND.mjs} +1 -1
  69. package/dist/{ios-device-CJ_ZOGky.mjs → ios-device-BWHKGaJq.mjs} +3 -3
  70. package/dist/{ios-BWbHFZVz.mjs → ios-o896T9PE.mjs} +195 -35
  71. package/dist/ios-project-BeOXKPt2.mjs +197 -0
  72. package/dist/{ios-state-By0XuLTQ.mjs → ios-state-BsXMwo5y.mjs} +1 -1
  73. package/dist/js-swap-Z_mSuRrx.mjs +432 -0
  74. package/dist/{react-native-ios-CSpIxexZ.mjs → launch-BootbMk4.mjs} +59 -612
  75. package/dist/launch-V1zqiOYn.mjs +745 -0
  76. package/dist/{launch-verify-DCveSE6d.mjs → launch-verify-Dl78EVgR.mjs} +26 -7
  77. package/dist/{logs-Bao1HOMy.mjs → logs-Bh4eqCCc.mjs} +11 -11
  78. package/dist/{macos-ciyBdTY5.mjs → macos-D7irBW3A.mjs} +42 -50
  79. package/dist/macos-kivF8qKz.mjs +2 -0
  80. package/dist/macos-run.mjs +1 -1
  81. package/dist/maintenance-run.mjs +13 -10
  82. package/dist/{metro-DI2AjPF_.mjs → metro-_ZIE3Axg.mjs} +18 -5
  83. package/dist/{metro-gateway-BxpJLIwh.mjs → metro-gateway-BzlQZQmU.mjs} +2 -2
  84. package/dist/miss-reason-SuTptY-I.mjs +238 -0
  85. package/dist/{named-ports-B4iWH4E-.mjs → named-ports-O1TIl7tx.mjs} +3 -3
  86. package/dist/native-android-D94RlJ-X.mjs +226 -0
  87. package/dist/native-gradle-inputs-BQnIdAlL.mjs +356 -0
  88. package/dist/{native-runtime-BGxsbfny.mjs → native-runtime-D024BAvm.mjs} +9 -8
  89. package/dist/native-xcode-inputs-UElfGs59.mjs +306 -0
  90. package/dist/native-xcode-ios-CjS-nDVY.mjs +434 -0
  91. package/dist/{ndjson-LGZAMsqa.mjs → ndjson-GOnbyvso.mjs} +6 -6
  92. package/dist/offload-worker.d.mts +41 -7
  93. package/dist/offload-worker.mjs +409 -74
  94. package/dist/{ownership-EB0ySdGK.mjs → ownership-BrjepDiv.mjs} +2 -2
  95. package/dist/{placement-log-Dxe34rJm.mjs → placement-log-BItXET1x.mjs} +1 -1
  96. package/dist/plan-Jwu_vr-F.mjs +260 -0
  97. package/dist/plan-placement-B466UYMU.mjs +73 -0
  98. package/dist/{ports-ByPc0_97.mjs → ports-BFRL6JTj.mjs} +3 -3
  99. package/dist/{prebuild-BKJ3wJI1.mjs → prebuild-DB_zLOLc.mjs} +4 -4
  100. package/dist/{preview-BbSottmO.mjs → preview-BRLFRhTz.mjs} +50 -32
  101. package/dist/project-DX8fXyw_.mjs +1131 -0
  102. package/dist/project-plan-BPwbLwQu.mjs +10 -0
  103. package/dist/{pull-request-IHNdMnG8.mjs → pull-request-vOhLNmIb.mjs} +1 -1
  104. package/dist/pull-requests.mjs +1 -1
  105. package/dist/react-native-android-CuXM3QS8.mjs +921 -0
  106. package/dist/react-native-build-JUF31S8U.mjs +239 -0
  107. package/dist/react-native-doctor-BQHqiXaQ.mjs +78 -0
  108. package/dist/react-native-ios-DtVO4qf2.mjs +595 -0
  109. package/dist/{recordings-Bv8McZN4.mjs → recordings-Bk-VivUV.mjs} +1 -1
  110. package/dist/{reload-rAPdqeRR.mjs → reload-D9V7D8gP.mjs} +17 -13
  111. package/dist/remote-CZiTijxG.mjs +192 -0
  112. package/dist/{remote-cache-N3hAEaAL.mjs → remote-cache-fxcl5C-L.mjs} +5 -5
  113. package/dist/result-BrfgXtdO.mjs +186 -0
  114. package/dist/{run-lAkle1-c.mjs → run-ClC8UVB5.mjs} +3 -3
  115. package/dist/{server-bare-CkIjnegq.mjs → server-bare-dWjAarki.mjs} +5 -5
  116. package/dist/server-command-DnJYLnr4.mjs +69 -0
  117. package/dist/{server-expo-n3tUi3Em.mjs → server-expo-CUuzP9Mp.mjs} +107 -82
  118. package/dist/server-expo-Dvkd7JcF.mjs +2 -0
  119. package/dist/{settings-CxgngUZk.mjs → settings-Bbztm4oV.mjs} +179 -129
  120. package/dist/{settings-2am7TSxp.mjs → settings-BuKMOANA.mjs} +6 -6
  121. package/dist/{state-lKh8shBp.d.mts → settings-vk3mJGSm.d.mts} +4 -2
  122. package/dist/settings.schema.json +84 -0
  123. package/dist/simslim-ChMfe8rg.mjs +149 -0
  124. package/dist/{slot-launch-HV_KtPIw.mjs → slot-launch-CeCRr6MK.mjs} +6 -6
  125. package/dist/{stage-BgaZJVZY.mjs → stage-BNKKdDNc.mjs} +6 -3
  126. package/dist/{start-DtxthMfI.mjs → start-CRkG-mZ7.mjs} +97 -68
  127. package/dist/start-mh6bS27z.mjs +2 -0
  128. package/dist/{state-Tp4uImDS.mjs → state-BUpAmt2r.mjs} +1 -1
  129. package/dist/{state-D2KtnALG.mjs → state-lqxdOH_g.mjs} +32 -12
  130. package/dist/{stats-8EJ5ivFT.mjs → stats-CaB1MxMi.mjs} +5 -5
  131. package/dist/{status-BEeL6IKz.mjs → status-BDGHwkCs.mjs} +3 -1
  132. package/dist/{status-BOyp0CUl.mjs → status-C7vzkInu.mjs} +81 -293
  133. package/dist/status-watch-ZaVqRdtW.mjs +296 -0
  134. package/dist/{stim-desktop-C65jk_q2.mjs → stim-desktop-8GQX2OV8.mjs} +1 -1
  135. package/dist/{stop-zDlknFoz.mjs → stop-BO80KJoT.mjs} +21 -20
  136. package/dist/{stop-DRipAg-e.mjs → stop-BU7JaFCg.mjs} +2 -2
  137. package/dist/{stop-D0UIpZuh.mjs → stop-Cftw_sXJ.mjs} +5 -5
  138. package/dist/supervisor-run.d.mts +7 -2
  139. package/dist/supervisor-run.mjs +32 -20
  140. package/dist/{support-DsNaFg6D.mjs → support-Ca6PZbpr.mjs} +17 -7
  141. package/dist/{support-CNqYkV0y.mjs → support-Co0wk8Cv.mjs} +61 -8
  142. package/dist/{swiftpm-macos-aJzPAj1t.mjs → swiftpm-macos-siFSkmCO.mjs} +5 -5
  143. package/dist/tailnet-D7-tE_ug.mjs +265 -0
  144. package/dist/toolchain-B12psPA2.mjs +2 -0
  145. package/dist/{toolchain-C3xSoyUl.mjs → toolchain-SqehyPLw.mjs} +206 -53
  146. package/dist/{trigger-CAhmG-4s.mjs → trigger-C0MWBi8O.mjs} +2 -2
  147. package/dist/trigger-SHLOWJ4O.mjs +2 -0
  148. package/dist/{warm-progress-pFLB2e6P.mjs → warm-progress-5FAXkXIg.mjs} +6 -6
  149. package/dist/{watchman-BL1IdOtq.mjs → watchman-CqmaGcIb.mjs} +6 -6
  150. package/dist/{web-DbbrPUn0.mjs → web-BTjzE8mc.mjs} +9 -13
  151. package/dist/web-CHN5gh34.mjs +2 -0
  152. package/dist/web-run.mjs +10 -10
  153. package/dist/{workspace-state-CR9X86xJ.mjs → workspace-state-CvgTFe-h.mjs} +36 -36
  154. package/dist/{workspaces-Djb2gfi9.mjs → workspaces-C4kqIOlx.mjs} +1260 -1258
  155. package/dist/worktree-Cgnm6vZt.mjs +2 -0
  156. package/dist/{worktree-BL4M1rhB.mjs → worktree-DtRsh6go.mjs} +86 -50
  157. package/package.json +6 -5
  158. package/shim/native-android.gradle +101 -0
  159. package/dist/android-BXc6BmEi.mjs +0 -3732
  160. package/dist/android-BvV1gGdN.mjs +0 -4
  161. package/dist/ios-ITYno9rO.mjs +0 -2241
  162. package/dist/macos-nIOATJmR.mjs +0 -2
  163. package/dist/metro-store-BAHFcJm6.mjs +0 -106
  164. package/dist/miss-reason-CjQCOKqq.mjs +0 -1743
  165. package/dist/plan-placement-BVY59qa8.mjs +0 -290
  166. package/dist/project-B5eQ1USs.mjs +0 -612
  167. package/dist/react-native-ios-BMh2i25I.mjs +0 -2
  168. package/dist/server-expo-DGsdUGX2.mjs +0 -2
  169. package/dist/start-D8NbxNBb.mjs +0 -2
  170. package/dist/trigger-CoalsMTV.mjs +0 -2
  171. package/dist/web-DFpFeZu5.mjs +0 -2
  172. package/dist/worktree-DV-A0LNL.mjs +0 -2
@@ -0,0 +1,1258 @@
1
+ import { n as ownership_claim_exports } from "./process-identity-Biv3arCO.mjs";
2
+ import { t as getExecutor } from "./exec-B81Qodjn.mjs";
3
+ import { E as upsertProject, L as deviceSlotFileKey, V as parseDeviceSlotOption, W as validateDeviceSlot, p as getProject, u as getConcurrencyLimits, z as deviceSlotPlatforms } from "./config-D0LhTY3S.mjs";
4
+ import { I as projectSettingsContext, K as resolveProjectSettings, L as publicUrlSetting, N as metroWarmupUrlSetting, V as remoteEasFallbackSetting, W as resolveCacheProviderConfig, a as SETTING_SHAPE_REMEDY, at as detectProviders, g as deviceReclaimIdleMinutesSetting, j as metroPortSetting, q as resolveSettings, tt as tunnelModeSetting } from "./settings-Bbztm4oV.mjs";
5
+ import { _ as waitForBuild, g as releaseBuildLock, m as acquireBuildLock } from "./device-lease-C309csaB.mjs";
6
+ import { c as writeWorkspaceState, f as emulatorLogFile, i as recordWorkspaceUse, r as readWorkspaceState$1, w as workspaceLogsDir$1 } from "./workspace-state-CvgTFe-h.mjs";
7
+ import { _ as stepTimer, f as refuseNoProject, g as stepClock, i as formatDuration, l as phaseLine } from "./command-output-CPDq_90S.mjs";
8
+ import { I as workspaceDevServerMode, R as writeWorkspaceLaunch, c as projectShortcut, d as projectRegistry, r as findProjectRoot, t as NO_PROJECT_REFUSAL } from "./project-DX8fXyw_.mjs";
9
+ import { G as probeEmulatorSerial, J as resolveOwnedAvdSerial, O as listAdbDevices, U as physicalDeviceModel, Y as resolvePhysicalDevice, a as androidDeviceAbi, ht as configuredAndroidEmulatorApp, j as listInstalledSystemImages, tt as waitForBoot } from "./android-DE8-JfGW.mjs";
10
+ import { s as pidExists, u as resolveProjectMetro } from "./metro-_ZIE3Axg.mjs";
11
+ import { F as acquireBuildSlot, L as releaseBuildSlot } from "./workspaces-C4kqIOlx.mjs";
12
+ import { At as createRunRecorder, Ft as recordRunStats, It as statsProjectKey$1, Nt as recordCapacityRefusal, Ot as startBuildProgress, jt as readRunEstimates, kt as tapBuildLog, t as budgetGate, yt as NO_BUILD_PROGRESS } from "./budget-RFbwFpG5.mjs";
13
+ import { S as withNativeBuildRun, b as runCancellation, p as cancelledFailure, x as runCancellationSignal } from "./tailnet-D7-tE_ug.mjs";
14
+ import { r as createNdjsonWriter } from "./ndjson-GOnbyvso.mjs";
15
+ import { f as resolveEasCliBin, i as checkEasAuth, m as uploadRemote, p as resolveRemote } from "./remote-cache-fxcl5C-L.mjs";
16
+ import { f as setRemoteLogSink, g as parseBuildMachineOption, y as resolveBuildPlacement } from "./client-zqjFd79D.mjs";
17
+ import { c as startDevServer } from "./start-CRkG-mZ7.mjs";
18
+ import { d as verifyAndroidReleaseLaunch, f as verifyLaunch } from "./launch-verify-Dl78EVgR.mjs";
19
+ import { b as launchAndroidReleaseApp, h as installAndroidApp, r as DEFAULT_METRO_PORT, t as ADB_INSTALL_TIMEOUT_MS, y as launchAndroidApp } from "./app-install-Cg9YJy3x.mjs";
20
+ import { n as ensureWorkspaceStorageSafely, r as isPhysicalDeviceRequest, t as ensureDevServer } from "./native-runtime-D024BAvm.mjs";
21
+ import { n as devicePlacementRecord } from "./placement-log-BItXET1x.mjs";
22
+ import { t as CCACHE_NOT_RUN } from "./ccache-CtbqWxIC.mjs";
23
+ import { n as DeviceAdmissionRefusal, r as checkDeviceCapacity } from "./device-capacity-DrwHwR97.mjs";
24
+ import { S as pooledAndroidDevice, a as PLATFORM, b as noDeviceDiagnostic, g as dumpApkManifest, h as displayPath, o as androidBuildOptions, p as apkPackage, r as NO_DEVICE, s as androidDevClientScheme } from "./support-Ca6PZbpr.mjs";
25
+ import { r as resolveAndroidRunPlan, t as androidDeviceSelectorRefusal } from "./plan-Jwu_vr-F.mjs";
26
+ import { t as acquireAndroidArtifact } from "./artifact-DXk_Fle_.mjs";
27
+ import { a as acquireRunLease, d as runLease, f as waitFlagConflict, o as leaseExpiryText, u as releaseLeaseOnSignal } from "./device-lease-run-Baelt3nc.mjs";
28
+ import { a as reportAndroidResult, i as persistLastBuild, n as finishAndroidUpload } from "./result-BrfgXtdO.mjs";
29
+ import { r as hostedMetroSettings } from "./metro-gateway-BzlQZQmU.mjs";
30
+ import { a as writeHostedAndroid, i as writeDevicePlacement, t as readHostedAndroid } from "./ios-state-BsXMwo5y.mjs";
31
+ import { a as refusePlan, c as devicePlacementLine, i as printPlan, n as planFlagRefusal, o as checkEasFallback, s as automaticDevicePlacement } from "./build-plan-CRjAA6Qi.mjs";
32
+ import { n as prepareHostedAndroid, t as placeHostedAndroid } from "./hosted-android-C-D8Dj0_.mjs";
33
+ import { r as siblingPlatformSlots, t as launchSlotScope } from "./slot-launch-CeCRr6MK.mjs";
34
+ import { S as ownedSessionName, a as ensureMetroReachable, c as remoteAndroidDeps, n as binOnPath, o as ensureRemoteBootOwned, u as resolveRemoteContext } from "./device-remote-DjsNw8DD.mjs";
35
+ import { a as resolveEasDevelopmentBuild, r as isEasBuildFailure } from "./eas-build-0gadvrca.mjs";
36
+ import { n as verifyCollectorOwnership } from "./ownership-BrjepDiv.mjs";
37
+ import { a as AvdRecoveryError, i as AvdBootError, n as ensureBooted, o as warmMetro, r as ensureOwnedDevice, t as didSetUpDevice } from "./device-D4DqjSZZ.mjs";
38
+ import { t as selectFromPool } from "./device-pool-DtK9S6AD.mjs";
39
+ import { r as finishAndroidRun } from "./launch-V1zqiOYn.mjs";
40
+ import { t as unsupportedPlan } from "./project-plan-BPwbLwQu.mjs";
41
+ import { basename, join } from "node:path";
42
+ import { hostedAndroidStatus, parseMachine } from "@stim-cli/core/state";
43
+ import { loadCacheProvider } from "@stim-cli/cache";
44
+ import { workspaceId } from "@stim-cli/core";
45
+ import { InvalidArgumentError } from "commander";
46
+ import chalk from "chalk";
47
+ //#region src/commands/android/remote.ts
48
+ async function connectAndroidHosting({ root, slot, machine, release, appMode = release ? "release" : "development", metroCheck, selectors, read = readHostedAndroid, resolveSerial = resolveOwnedAvdSerial, prepare = prepareHostedAndroid }) {
49
+ let recorded;
50
+ try {
51
+ recorded = read(root, slot)[slot];
52
+ } catch (error) {
53
+ return { failure: {
54
+ code: "STIM_HOSTING_REFUSED",
55
+ message: error.message,
56
+ remedy: `Restore that slot's recorded machine and session from the host, then run stim stop${slot === "default" ? "" : ` --slot ${slot}`} to reconcile it.`
57
+ } };
58
+ }
59
+ if (recorded && recorded.machine !== machine) return { failure: {
60
+ code: "STIM_BAD_ARG",
61
+ message: `This workspace's Android emulator runs on ${recorded.machine}; run stim stop first.`
62
+ } };
63
+ if (!machine) return { target: null };
64
+ if (appMode === "development" && !metroCheck) return { failure: {
65
+ code: "STIM_BAD_ARG",
66
+ message: "Hosted Debug runs require the local Metro supervisor; --no-metro-check cannot be used.",
67
+ remedy: "Run stim stop; stim start, then retry without --no-metro-check."
68
+ } };
69
+ const local = deviceSlotPlatforms(getProject(root), slot)?.android;
70
+ if (local?.owned && local.avdName) {
71
+ let running = true;
72
+ try {
73
+ const resolved = resolveSerial(local.avdName, { timeoutMs: 5e3 });
74
+ running = Boolean(resolved.serial) || !resolved.notRunning && !resolved.missing;
75
+ } catch {}
76
+ if (running) return { failure: {
77
+ code: "STIM_BAD_ARG",
78
+ message: `This workspace's Android emulator for slot ${slot} runs on this Mac; run stim stop first.`
79
+ } };
80
+ }
81
+ try {
82
+ return { target: await prepare(machine, selectors, recorded, appMode === "process" ? appMode : void 0) };
83
+ } catch (error) {
84
+ return { failure: {
85
+ code: error.code ?? "STIM_HOSTING_REFUSED",
86
+ message: error.message,
87
+ ...error instanceof Error && "remedy" in error && typeof error.remedy === "string" ? { remedy: error.remedy } : {}
88
+ } };
89
+ }
90
+ }
91
+ async function selectAndroidPlacement({ target: selected, automatic = automaticDevicePlacement, checkBudget = budgetGate, note, phase, log, fromFlag, noWait, buildMachine, localSelectors, eas, ...args }) {
92
+ const appMode = args.appMode ?? (args.release ? "release" : "development");
93
+ const selectors = selected.kind === "hosted" ? {
94
+ ...selected.systemImage ? { systemImage: selected.systemImage } : {},
95
+ ...selected.deviceProfile ? { deviceProfile: selected.deviceProfile } : {}
96
+ } : {};
97
+ if (selected.kind !== "hosted" || selected.machine !== "auto") {
98
+ const connected = await connectAndroidHosting({
99
+ ...args,
100
+ selectors,
101
+ machine: selected.kind === "hosted" ? selected.machine : null
102
+ });
103
+ if ("failure" in connected) return connected;
104
+ return {
105
+ target: selected,
106
+ hostedTarget: connected.target,
107
+ selectors,
108
+ budget: connected.target ? {
109
+ reclaimed: [],
110
+ refusal: null
111
+ } : await checkBudget({
112
+ root: args.root,
113
+ note
114
+ })
115
+ };
116
+ }
117
+ const refusal = localSelectors(selected);
118
+ if (refusal) return { failure: refusal };
119
+ try {
120
+ const placed = await automatic({
121
+ root: args.root,
122
+ slot: args.slot,
123
+ platform: "android",
124
+ ...appMode === "process" ? { appMode } : {},
125
+ selectors,
126
+ buildMachine,
127
+ noWait,
128
+ ...eas ? { eas } : {}
129
+ });
130
+ phase("placement:", devicePlacementLine(placed.placement, placed.skipped));
131
+ log?.(devicePlacementRecord({
132
+ platform: "android",
133
+ fromFlag,
134
+ easFallback: Boolean(eas),
135
+ placed
136
+ }));
137
+ if (placed.placement.decision === "eas") return {
138
+ target: {
139
+ kind: "remote",
140
+ backend: "eas",
141
+ systemImage: selected.systemImage,
142
+ deviceProfile: selected.deviceProfile
143
+ },
144
+ hostedTarget: null,
145
+ selectors,
146
+ budget: await checkBudget({
147
+ root: args.root,
148
+ note
149
+ }),
150
+ devicePlacement: placed.placement
151
+ };
152
+ if (placed.target && appMode === "development" && !args.metroCheck) return { failure: {
153
+ code: "STIM_BAD_ARG",
154
+ message: "Hosted Debug runs require the local Metro supervisor; --no-metro-check cannot be used.",
155
+ remedy: "Run stim stop; stim start, then retry without --no-metro-check."
156
+ } };
157
+ return {
158
+ target: placed.target ? {
159
+ ...selected,
160
+ machine: placed.target.host.machine
161
+ } : {
162
+ kind: "emulator",
163
+ systemImage: selected.systemImage,
164
+ deviceProfile: selected.deviceProfile
165
+ },
166
+ hostedTarget: placed.target,
167
+ selectors,
168
+ budget: placed.target ? {
169
+ reclaimed: [],
170
+ refusal: null
171
+ } : await checkBudget({
172
+ root: args.root,
173
+ note
174
+ }),
175
+ devicePlacement: placed.placement
176
+ };
177
+ } catch (error) {
178
+ return { failure: {
179
+ code: error.code ?? "STIM_HOSTING_REFUSED",
180
+ message: error.message,
181
+ ...error instanceof Error && "remedy" in error && typeof error.remedy === "string" ? { remedy: error.remedy } : {}
182
+ } };
183
+ }
184
+ }
185
+ //#endregion
186
+ //#region src/commands/android/hosted.ts
187
+ async function finishHostedAndroidRun({ target, artifact, root, slot = "default", release, appMode = release ? "release" : "development", isExpo, metroPort, logsDir, selectors, place, writePlacement, verifyLaunch: verify, writeLaunch, readApkPackage, resolveDevClientScheme, now, started, startedAt, fail, phase, enterPhase, out, recordBuild, ...report }) {
188
+ const packageName = artifact.androidPackage ?? readApkPackage(artifact.apkPath);
189
+ if (!artifact.apkPath || !packageName) return fail("STIM_INSTALL_FAILED", "The built APK has no package identity.");
190
+ const launchedAt = now();
191
+ try {
192
+ const run = await place(target, {
193
+ root,
194
+ slot,
195
+ bundle: artifact.apkPath,
196
+ handoff: artifact.handoff,
197
+ bundleId: packageName,
198
+ release,
199
+ ...appMode === "process" ? { mode: appMode } : {},
200
+ metroPort,
201
+ selectors,
202
+ ...appMode === "development" && isExpo ? { devClientScheme: resolveDevClientScheme(root, artifact.apkPath) ?? void 0 } : {},
203
+ reserved: (placement) => writePlacement(root, slot, placement),
204
+ note: out,
205
+ enterPhase
206
+ });
207
+ writePlacement(root, slot, run.placement);
208
+ writeLaunch(root, "android", {
209
+ appId: packageName,
210
+ deviceId: run.placement.session,
211
+ metroPort,
212
+ release,
213
+ ...appMode === "process" ? { runtime: "process" } : {},
214
+ launchedAt: new Date(launchedAt).toISOString()
215
+ }, slot);
216
+ let launched = run.launched;
217
+ if (appMode === "development") {
218
+ const evidence = await verify({
219
+ requireBundleResponse: true,
220
+ platform: "android",
221
+ logsDir: logsDir ?? void 0,
222
+ since: launchedAt,
223
+ metroPort,
224
+ mode: workspaceDevServerMode(root, isExpo),
225
+ slot: launchSlotScope(root, slot),
226
+ platformShared: siblingPlatformSlots(root, "android", slot).length > 0
227
+ });
228
+ 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.");
229
+ launched = evidence.verified ? true : evidence.requested ? "bundling" : "unverified";
230
+ }
231
+ if (launched === "unverified") out(`Launch on ${target.host.machine} is unverified; check the hosted app, then retry stim android --remote ${target.host.machine}.`);
232
+ const host = hostedAndroidStatus(run.placement);
233
+ report.record.deviceName = host.device?.name ?? null;
234
+ report.record.bundleId = packageName;
235
+ persistLastBuild({
236
+ recordBuild,
237
+ root,
238
+ record: report.record,
239
+ startedAt,
240
+ durationMs: now() - started,
241
+ status: "success",
242
+ out
243
+ });
244
+ await finishAndroidUpload(artifact.uploadPending, artifact.remote, phase);
245
+ await artifact.providerUpload;
246
+ return {
247
+ ok: true,
248
+ facts: reportAndroidResult({
249
+ ...report,
250
+ ...appMode === "process" ? { runtimeKind: "process" } : {},
251
+ root,
252
+ slot,
253
+ release,
254
+ metroPort,
255
+ logsDir,
256
+ host,
257
+ serial: null,
258
+ apkPath: artifact.apkPath,
259
+ androidPackage: packageName,
260
+ installSkipped: false,
261
+ waitedForBuild: artifact.waitedForBuild,
262
+ remote: artifact.remote,
263
+ providerName: artifact.providerName,
264
+ launchState: launched,
265
+ launched: {},
266
+ ccache: artifact.ccache,
267
+ durationMs: now() - started
268
+ })
269
+ };
270
+ } catch (error) {
271
+ 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.`);
272
+ }
273
+ }
274
+ //#endregion
275
+ //#region src/commands/android/next-build.ts
276
+ function runOnlyFlag(opts) {
277
+ if (isPhysicalDeviceRequest(opts.device)) return "--device";
278
+ if (opts.remote) return "--remote";
279
+ if (opts.wait !== void 0) return opts.wait === false ? "--no-wait" : "--wait";
280
+ if (opts.metroCheck === false) return "--no-metro-check";
281
+ return null;
282
+ }
283
+ async function planAndroid(opts, overrides = {}) {
284
+ const deps = {
285
+ findRoot: findProjectRoot,
286
+ projectRegistry,
287
+ ...overrides
288
+ };
289
+ const json = Boolean(opts.json);
290
+ const flag = runOnlyFlag(opts);
291
+ if (flag) return refusePlan(planFlagRefusal(flag), json);
292
+ const root = deps.findRoot(process.cwd());
293
+ if (!root) return refusePlan(NO_PROJECT_REFUSAL, json);
294
+ const selected = deps.projectRegistry.selectAndroid(root);
295
+ if ("problem" in selected) return refusePlan({
296
+ code: "STIM_NO_PROJECT",
297
+ ...selected.problem
298
+ }, json);
299
+ const resolved = resolveProjectSettings(root);
300
+ const project = await selected.load(resolved);
301
+ if (!project.plan) return refusePlan(unsupportedPlan("android"), json);
302
+ const result = await project.plan(opts, resolved);
303
+ if ("platform" in result) printPlan(result, json);
304
+ else refusePlan(result.refusal, json, result.lines);
305
+ }
306
+ //#endregion
307
+ //#region src/commands/android.ts
308
+ function androidCommand(program) {
309
+ registerAndroid(program);
310
+ }
311
+ function registerAndroid(program) {
312
+ program.command("android").description("Build (or install from the shared cache), install and launch this workspace's Android app on its owned emulator. React Native and Expo Debug runs start 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; local runs here, whatever android.remote says", (value) => {
313
+ if (!parseMachine(value)) throw new InvalidArgumentError("expected eas, proxy, auto, local, or a tailnet machine name");
314
+ return value;
315
+ }).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) => {
316
+ if (opts.plan) return planAndroid(opts);
317
+ const root = findProjectRoot(process.cwd());
318
+ if (!root) {
319
+ refuseNoProject({ json: Boolean(opts.json) });
320
+ return;
321
+ }
322
+ if (!(await runAndroidOperation(root, {
323
+ ...opts,
324
+ waitConflict: waitFlagConflict(process.argv)
325
+ })).ok) process.exit(runCancellation() ? 130 : 1);
326
+ });
327
+ }
328
+ function runAndroidOperation(root, opts = {}) {
329
+ return withNativeBuildRun(root, {
330
+ command: "android",
331
+ platform: PLATFORM,
332
+ slot: opts.slot ?? "default"
333
+ }, async (claim) => {
334
+ recordWorkspaceUse(root);
335
+ const progress = startBuildProgress({
336
+ root,
337
+ platform: PLATFORM,
338
+ slot: opts.slot ?? "default",
339
+ claim,
340
+ note: (line) => console.error(chalk.dim(line))
341
+ });
342
+ try {
343
+ return await runAndroid({
344
+ root,
345
+ buildMachine: opts.remoteBuild,
346
+ slot: opts.slot,
347
+ easProfile: opts.easProfile,
348
+ json: Boolean(opts.json),
349
+ metroCheck: opts.metroCheck !== false,
350
+ useBuildCache: opts.buildCache !== false,
351
+ variant: opts.variant ?? null,
352
+ systemImage: opts.systemImage ?? null,
353
+ deviceProfile: opts.deviceProfile ?? null,
354
+ remoteDevice: opts.remote ?? null,
355
+ device: opts.device ?? null,
356
+ wait: opts.wait,
357
+ waitConflict: opts.waitConflict,
358
+ progress
359
+ });
360
+ } finally {
361
+ progress.clear();
362
+ }
363
+ }, { write: (line) => console.error(chalk.dim(phaseLine("lock", line))) });
364
+ }
365
+ 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, acquireLock = acquireBuildLock, releaseLock = releaseBuildLock, waitForBuild: waitForBuild$1 = waitForBuild, easAuth = checkEasAuth, resolveRemoteBuild = resolveRemote, uploadRemoteBuild = uploadRemote, resolveCacheProvider = resolveCacheProviderConfig, loadCacheProviderModule = loadCacheProvider, install = installAndroidApp, launch = launchAndroidApp, launchRelease = launchAndroidReleaseApp, verifyReleaseLaunched = verifyAndroidReleaseLaunch, 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) } = {}) {
366
+ return {
367
+ root,
368
+ json,
369
+ commandRemoteBackend,
370
+ resolveSettingsFor,
371
+ resolveRemoteDeviceContext,
372
+ makeRemoteDeviceDeps,
373
+ resolveEasBin,
374
+ ensureRemoteMetro,
375
+ ensureRemoteOwned,
376
+ detectRemoteProviders,
377
+ metroCheck,
378
+ useBuildCache,
379
+ easProfile,
380
+ resolveEasBuild,
381
+ variantFlag,
382
+ systemImageFlag,
383
+ deviceProfileFlag,
384
+ deviceFlag,
385
+ waitFlag,
386
+ waitConflict,
387
+ acquireLease,
388
+ makeRunLease,
389
+ selectPool,
390
+ onLeaseSignal,
391
+ listDevices,
392
+ deviceModel,
393
+ deviceAbi,
394
+ isEmulatorDevice,
395
+ readApkPackage,
396
+ getLimits,
397
+ checkCapacity,
398
+ acquireSlot,
399
+ releaseSlot,
400
+ ensureDevice,
401
+ listSystemImages,
402
+ listDeviceProfiles,
403
+ ensureDeviceBooted,
404
+ resolveAvdSerial,
405
+ waitForDeviceBoot,
406
+ dataFreeBytes,
407
+ trimCaches,
408
+ wipeDevice,
409
+ resolveMetro,
410
+ prewarmMetro,
411
+ startServer,
412
+ readState,
413
+ pidAlive,
414
+ verifyCollector,
415
+ verifyLaunched,
416
+ ensureStorage,
417
+ acquireLock,
418
+ releaseLock,
419
+ waitForBuild: waitForBuild$1,
420
+ easAuth,
421
+ resolveRemoteBuild,
422
+ uploadRemoteBuild,
423
+ resolveCacheProvider,
424
+ loadCacheProviderModule,
425
+ install,
426
+ launch,
427
+ launchRelease,
428
+ verifyReleaseLaunched,
429
+ resolveDevClientScheme,
430
+ spawn,
431
+ kill,
432
+ createWriter,
433
+ writeLaunch,
434
+ writeState,
435
+ recordBuild,
436
+ recordStats,
437
+ recordCapacityRefusal: recordRefusal,
438
+ readEstimates,
439
+ now,
440
+ out,
441
+ emit
442
+ };
443
+ }
444
+ function androidSlotOptions(options) {
445
+ const base = resolveRunAndroidOptions(options);
446
+ const slot = validateDeviceSlot(options.slot);
447
+ if (slot === "default") return base;
448
+ return {
449
+ ...base,
450
+ ensureDevice: (args) => base.ensureDevice({
451
+ ...args,
452
+ slot
453
+ }),
454
+ ensureDeviceBooted: (args) => base.ensureDeviceBooted({
455
+ ...args,
456
+ slot
457
+ }),
458
+ checkCapacity: (args) => base.checkCapacity({
459
+ ...args,
460
+ slot
461
+ }),
462
+ selectPool: (args) => base.selectPool({
463
+ ...args,
464
+ slot
465
+ }),
466
+ acquireLease: (args) => base.acquireLease({
467
+ ...args,
468
+ slot
469
+ }),
470
+ makeRunLease: (args) => base.makeRunLease({
471
+ ...args,
472
+ slot
473
+ }),
474
+ writeLaunch: ((projectRoot, platform, record) => base.writeLaunch(projectRoot, platform, record, slot))
475
+ };
476
+ }
477
+ function avdSetupFailure(error, root, logFile) {
478
+ const refusal = (0, ownership_claim_exports.claimFailure)(error, "stim android");
479
+ if (refusal) return refusal;
480
+ if (error instanceof DeviceAdmissionRefusal) return {
481
+ code: error.code,
482
+ message: error.message,
483
+ remedy: error.remedy
484
+ };
485
+ const diag = noDeviceDiagnostic({
486
+ reason: `Could not ensure an owned Android emulator: ${error?.message || error}`,
487
+ logFile,
488
+ localEmulator: !(error instanceof AvdRecoveryError),
489
+ remedy: error instanceof AvdBootError ? error.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\"`)."
490
+ });
491
+ return {
492
+ code: NO_DEVICE,
493
+ message: diag.message,
494
+ remedy: diag.remedy,
495
+ extra: {
496
+ lines: diag.lines,
497
+ logPath: diag.logPath ? displayPath(root, diag.logPath) : null
498
+ }
499
+ };
500
+ }
501
+ async function runAndroid(options = {}) {
502
+ 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, acquireLock, releaseLock, waitForBuild, easAuth, resolveRemoteBuild, uploadRemoteBuild, resolveCacheProvider, loadCacheProviderModule, install, launch, launchRelease, verifyReleaseLaunched, resolveDevClientScheme, spawn, kill, createWriter, writeLaunch, writeState, recordBuild, recordStats, recordCapacityRefusal: recordRefusal = recordCapacityRefusal, readEstimates, now, out, emit } = androidSlotOptions(options);
503
+ const progress = options.progress ?? NO_BUILD_PROGRESS;
504
+ const slot = validateDeviceSlot(options.slot);
505
+ const started = now();
506
+ const startedAt = new Date(started).toISOString();
507
+ const selectedProject = (options.projectRegistry ?? projectRegistry).selectAndroid(root);
508
+ if ("problem" in selectedProject) {
509
+ const { message, remedy } = selectedProject.problem;
510
+ out(phaseLine("error", chalk.red(`STIM_NO_PROJECT: ${message}`)));
511
+ out(phaseLine("remedy", remedy));
512
+ if (json) emit(JSON.stringify({
513
+ code: "STIM_NO_PROJECT",
514
+ message,
515
+ remedy
516
+ }));
517
+ return {
518
+ ok: false,
519
+ error: {
520
+ code: "STIM_NO_PROJECT",
521
+ message,
522
+ remedy
523
+ }
524
+ };
525
+ }
526
+ try {
527
+ await ensureStorage(root, { note: out });
528
+ } catch (error) {
529
+ const code = error?.code || "STIM_WORKSPACE_STATE";
530
+ const message = `Could not prepare this workspace's Stim state: ${error?.message || error}`;
531
+ const remedy = "Check that STIM_HOME is writable and has free space. An EPERM on a directory you can write is a sandbox: allow writes to STIM_HOME, or run Stim with the sandbox disabled (`stim guide errors sandbox`).";
532
+ out(phaseLine("error", chalk.red(`${code}: ${message}`)));
533
+ out(phaseLine("remedy", remedy));
534
+ if (json) emit(JSON.stringify({
535
+ code,
536
+ message,
537
+ remedy
538
+ }));
539
+ return {
540
+ ok: false,
541
+ error: {
542
+ code,
543
+ message,
544
+ remedy
545
+ }
546
+ };
547
+ }
548
+ const logsDir = workspaceLogsDir$1(root);
549
+ const buildLog = join(logsDir, `build-${deviceSlotFileKey("android", slot)}.ndjson`);
550
+ const writer = tapBuildLog(createWriter(buildLog, {
551
+ truncate: true,
552
+ fields: {
553
+ platform: PLATFORM,
554
+ slot
555
+ }
556
+ }), progress);
557
+ setRemoteLogSink((entry) => writer.write(entry));
558
+ const record = {
559
+ fingerprint: null,
560
+ cacheKey: null,
561
+ cacheHit: false,
562
+ appPath: null,
563
+ bundleId: null,
564
+ avdName: null,
565
+ deviceName: null
566
+ };
567
+ const phase = (label, text) => out(phaseLine(label, text));
568
+ const stats = createRunRecorder({
569
+ platform: PLATFORM,
570
+ write: recordStats,
571
+ now,
572
+ note: (line) => out(phaseLine("stats", chalk.dim(line))),
573
+ phases: () => progress.durations(),
574
+ deviceSetup: () => progress.deviceSetupKnown()
575
+ });
576
+ const recordRun = stats.record;
577
+ let ccacheActivity = CCACHE_NOT_RUN;
578
+ let reclaimed = [];
579
+ const fail = (code, message, remedy, { lastBuildStatus = false, setup = false, diagnostics = [], buildDiagnostics: rawDiagnostics = [], lines = [], logPath = null, lease } = {}) => {
580
+ const cancellation = cancelledFailure(PLATFORM, {
581
+ code,
582
+ message
583
+ });
584
+ if (cancellation) ({code, message, remedy, lines} = cancellation);
585
+ if (lastBuildStatus) persistLastBuild({
586
+ recordBuild,
587
+ root,
588
+ record,
589
+ startedAt,
590
+ durationMs: now() - started,
591
+ status: "failed",
592
+ errorCode: code,
593
+ diagnostics: rawDiagnostics,
594
+ out
595
+ });
596
+ out(phaseLine("error", chalk.red(`${code}: ${message}`)));
597
+ for (const diagnostic of diagnostics) out(phaseLine("error", chalk.red(diagnostic)));
598
+ for (const line of lines) out(phaseLine("", chalk.dim(line)));
599
+ if (remedy) out(phaseLine("remedy", remedy));
600
+ if (logPath) out(phaseLine("log", logPath));
601
+ if (!setup) recordRun({
602
+ failed: true,
603
+ durationMs: now() - started
604
+ });
605
+ if (json) emit(JSON.stringify({
606
+ code,
607
+ message,
608
+ remedy: remedy ?? null,
609
+ ...record.devicePlacement ? { devicePlacement: record.devicePlacement } : {},
610
+ ...ccacheActivity.status === "not-run" ? {} : { ccache: ccacheActivity },
611
+ ...lease === void 0 ? {} : { lease },
612
+ ...reclaimed.length ? { reclaimed } : {}
613
+ }));
614
+ writer.close();
615
+ return {
616
+ ok: false,
617
+ error: {
618
+ code,
619
+ message,
620
+ remedy: remedy ?? null
621
+ }
622
+ };
623
+ };
624
+ const settingsRoot = root;
625
+ const settingsContext = projectSettingsContext(root);
626
+ const projectKey = statsProjectKey$1({
627
+ root,
628
+ commonDir: settingsContext.gitCommonDir,
629
+ repoRoot: settingsContext.repoRoot
630
+ });
631
+ stats.setProject(projectKey);
632
+ progress.estimate(projectKey);
633
+ let estimatesRead = null;
634
+ const estimates = () => estimatesRead ??= readEstimates({
635
+ projectKey,
636
+ platform: PLATFORM
637
+ });
638
+ const settings = resolveSettingsFor(settingsContext);
639
+ const integration = await selectedProject.load({
640
+ context: settingsContext,
641
+ settings
642
+ });
643
+ const placement = resolveBuildPlacement(options.buildMachine);
644
+ record.buildMachine = placement.selected;
645
+ if (placement.failure) {
646
+ const { code, message, remedy } = placement.failure;
647
+ return fail(code, message, remedy, { setup: true });
648
+ }
649
+ const planned = resolveAndroidRunPlan({
650
+ settings,
651
+ settingsContext,
652
+ slot,
653
+ easProfile,
654
+ variant: variantFlag,
655
+ systemImage: systemImageFlag,
656
+ deviceProfile: deviceProfileFlag,
657
+ device: deviceFlag,
658
+ wait: waitFlag,
659
+ waitConflict,
660
+ remote: commandRemoteBackend,
661
+ buildCache: requestedBuildCache
662
+ }, {
663
+ resolveCacheProvider,
664
+ runtimeKind: integration.runtimeKind,
665
+ variantProblem: integration.variantProblem,
666
+ detectExpo: () => integration.isExpo,
667
+ listSystemImages,
668
+ listDeviceProfiles,
669
+ warn: (label, message) => out(phaseLine(label, chalk.yellow(message)))
670
+ });
671
+ if (!planned.ok) return fail(planned.code, planned.message, planned.remedy, { lines: planned.lines });
672
+ const { plan } = planned;
673
+ if (!integration.targets.includes(plan.target.kind)) return fail("STIM_BAD_ARG", `This project integration does not support the ${plan.target.kind} Android target.`, "Use a local Android emulator for this integration.");
674
+ if (easProfile !== void 0 && !integration.eas) return fail("STIM_BAD_ARG", "This project integration does not support EAS builds.", "Use its local Android build recipe.");
675
+ const deviceSlotWait = {
676
+ automatic: plan.target.kind === "hosted" && plan.target.machine === "auto",
677
+ signal: runCancellationSignal(),
678
+ waitMs: plan.deviceSlotWaitMs,
679
+ displayName: basename(root),
680
+ reclaimIdleMinutes: deviceReclaimIdleMinutesSetting(settings),
681
+ waitingFor: (info) => progress.waitingFor(info, "device-slot"),
682
+ onWait: (ms) => {
683
+ stats.addDeviceSlotWaitMs(ms);
684
+ if (record.devicePlacement?.decision === "local" && ms > 0) record.devicePlacement.decision = "waited-locally";
685
+ }
686
+ };
687
+ const { build: buildPlan, isExpo, cacheProviderConfig } = plan;
688
+ const { variant, release } = buildPlan;
689
+ const runtimeKind = integration.runtimeKind(buildPlan);
690
+ const appMode = runtimeKind === "process" ? "process" : runtimeKind === "metro" ? "development" : "release";
691
+ record.configuration = variant ?? "debug";
692
+ const hosting = await selectAndroidPlacement({
693
+ root,
694
+ slot,
695
+ target: plan.target,
696
+ release,
697
+ appMode,
698
+ metroCheck,
699
+ noWait: plan.deviceSlotWaitMs === 0,
700
+ buildMachine: options.buildMachine,
701
+ read: options.readHostedAndroid,
702
+ resolveSerial: resolveAvdSerial,
703
+ prepare: options.prepareHostedAndroid,
704
+ automatic: options.automaticDevicePlacement,
705
+ ...integration.eas && remoteEasFallbackSetting(settings) ? { eas: () => (options.checkEasFallback ?? checkEasFallback)({
706
+ root,
707
+ platform: PLATFORM,
708
+ slot,
709
+ release,
710
+ isExpo,
711
+ tunnelMode: tunnelModeSetting(settings),
712
+ publicUrl: publicUrlSetting(settings),
713
+ localOnlyFlags: [...typeof systemImageFlag === "string" ? ["--system-image"] : [], ...typeof deviceProfileFlag === "string" ? ["--device-profile"] : []]
714
+ }) } : {},
715
+ localSelectors: (target) => androidDeviceSelectorRefusal({
716
+ settingsContext,
717
+ slot,
718
+ systemImageFlag,
719
+ deviceProfileFlag,
720
+ systemImage: target.systemImage,
721
+ deviceProfile: target.deviceProfile,
722
+ physical: false,
723
+ remoteBackend: null
724
+ }, {
725
+ listSystemImages,
726
+ listDeviceProfiles
727
+ }),
728
+ checkBudget: options.checkBudget,
729
+ note: out,
730
+ phase,
731
+ log: (entry) => writer.write(entry),
732
+ fromFlag: commandRemoteBackend !== null
733
+ });
734
+ if ("failure" in hosting) return fail(hosting.failure.code, hosting.failure.message, hosting.failure.remedy);
735
+ const { target, hostedTarget, budget, selectors } = hosting;
736
+ record.devicePlacement = hosting.devicePlacement;
737
+ writeDevicePlacement(root, slot, PLATFORM, record.devicePlacement);
738
+ const physical = target.kind === "physical";
739
+ const remoteBackend = target.kind === "remote" ? target.backend : null;
740
+ if (hostedTarget) {
741
+ if (settings.androidEmulatorApp !== void 0) phase("device", "androidEmulatorApp is ignored on a hosting Mac; the emulator boots headless.");
742
+ if (appMode === "development" && (publicUrlSetting(settings) || tunnelModeSetting(settings))) phase("metro", "metro.publicUrl and metro.tunnel are ignored on a hosting Mac; Metro uses the private tailnet bridge.");
743
+ }
744
+ reclaimed = budget.reclaimed;
745
+ if (budget.refusal) return fail(budget.refusal.code, budget.refusal.message, budget.refusal.remedy);
746
+ const remoteContext = remoteBackend ? await resolveRemoteDeviceContext({
747
+ root,
748
+ platform: PLATFORM,
749
+ backend: remoteBackend,
750
+ easBin: resolveEasBin(root)?.file ?? null
751
+ }) : null;
752
+ if (remoteContext && "failed" in remoteContext) return fail(remoteContext.code ?? "STIM_NO_REMOTE_SESSION", remoteContext.failed, remoteContext.remedy);
753
+ const easBuild = await resolveEasBuild({
754
+ root,
755
+ platform: PLATFORM,
756
+ profile: easProfile,
757
+ note: out,
758
+ isExpo,
759
+ physical,
760
+ selectors: [variantFlag],
761
+ buildCache: requestedBuildCache
762
+ });
763
+ if (isEasBuildFailure(easBuild)) return fail(easBuild.code, easBuild.message, easBuild.remedy);
764
+ const appIds = integration.appIds();
765
+ let androidPackage = appIds.androidPackage;
766
+ record.bundleId = androidPackage;
767
+ const registerProject = () => upsertProject(root, {
768
+ bundleId: appIds.bundleId ?? void 0,
769
+ androidPackage: androidPackage ?? void 0,
770
+ isExpo
771
+ });
772
+ if (remoteBackend !== "eas") registerProject();
773
+ const project = getProject(root);
774
+ const label = projectShortcut(root, project);
775
+ let remoteDevice = null;
776
+ const reservedPort = project?.metroPort ?? null;
777
+ let metroPort = null;
778
+ let devServer = null;
779
+ async function prepareMetro() {
780
+ if (metroCheck) {
781
+ const gate = await ensureDevServer({
782
+ root,
783
+ port: reservedPort,
784
+ settings: hostedMetroSettings(settings, Boolean(hostedTarget)),
785
+ settingsContext,
786
+ remote: remoteContext !== null,
787
+ note: out,
788
+ resolve: resolveMetro,
789
+ start: startServer,
790
+ readState
791
+ });
792
+ reclaimed = [...reclaimed, ...gate.reclaimed];
793
+ if (!gate.ok) return {
794
+ ok: false,
795
+ error: {
796
+ code: gate.code,
797
+ message: gate.message,
798
+ remedy: gate.remedy,
799
+ lines: gate.lines
800
+ }
801
+ };
802
+ metroPort = gate.port;
803
+ devServer = gate.devServer;
804
+ phase("metro", `port ${metroPort} (${devServer ? `started: ${devServer.reason}` : `pid ${gate.pid ?? "unknown, started outside Stim"}`})`);
805
+ return {
806
+ ok: true,
807
+ prepared: { metroPort }
808
+ };
809
+ } else {
810
+ const pin = metroPortSetting(settings);
811
+ if (pin.error) return {
812
+ ok: false,
813
+ error: {
814
+ code: "STIM_BAD_ARG",
815
+ message: pin.error,
816
+ remedy: SETTING_SHAPE_REMEDY
817
+ }
818
+ };
819
+ metroPort = pin.port ?? reservedPort ?? 8081;
820
+ phase("metro", pin.port ?? reservedPort ? `port ${metroPort} (not checked)` : `no reservation; using ${DEFAULT_METRO_PORT} (not checked)`);
821
+ }
822
+ return {
823
+ ok: true,
824
+ prepared: { metroPort }
825
+ };
826
+ }
827
+ const runtime = options.runtimePlan ?? integration.runtime({
828
+ build: buildPlan,
829
+ prepareMetro,
830
+ phase
831
+ });
832
+ const useBuildCache = buildPlan.cache.read;
833
+ const preparation = await runtime.prepare();
834
+ if (!preparation.ok) {
835
+ const { code, message, remedy, lines } = preparation.error;
836
+ return fail(code, message, remedy, { lines });
837
+ }
838
+ const runtimePreparation = preparation.prepared;
839
+ metroPort = runtimePreparation.metroPort;
840
+ if (remoteContext) {
841
+ remoteDevice = makeRemoteDeviceDeps(remoteContext.ctx);
842
+ if (metroPort !== null) {
843
+ const reachable = await ensureRemoteMetro({
844
+ ctx: remoteDevice.ctx,
845
+ metroPort,
846
+ isExpo,
847
+ tunnelMode: tunnelModeSetting(settings) ?? void 0,
848
+ publicUrl: publicUrlSetting(settings),
849
+ available: detectRemoteProviders(binOnPath, tunnelModeSetting(settings) ?? "auto")
850
+ });
851
+ if ("failed" in reachable) return fail(reachable.code ?? "STIM_NO_REMOTE_SESSION", reachable.failed, reachable.remedy);
852
+ }
853
+ checkCapacity = remoteDevice.checkCapacity;
854
+ ensureDevice = remoteDevice.ensureDevice;
855
+ ensureDeviceBooted = remoteDevice.ensureDeviceBooted;
856
+ install = remoteDevice.install;
857
+ launch = remoteDevice.launch;
858
+ }
859
+ const emuLog = emulatorLogFile(root);
860
+ const limits = getLimits();
861
+ let device;
862
+ let bootDuration = "";
863
+ let bootPending = false;
864
+ let bootPromise;
865
+ let startRemoteBoot = null;
866
+ if (hostedTarget) {
867
+ device = {
868
+ owned: false,
869
+ systemImage: hostedTarget.choice.systemImage,
870
+ deviceProfile: hostedTarget.choice.deviceProfile
871
+ };
872
+ bootPromise = Promise.resolve({ ok: true });
873
+ } else if (target.kind === "physical" && !target.serial) {
874
+ const pooled = await pooledAndroidDevice({
875
+ root,
876
+ selectPool,
877
+ listDevices,
878
+ isEmulatorDevice,
879
+ deviceModel,
880
+ waitSeconds: target.lease.waitSeconds,
881
+ noWait: target.lease.noWait,
882
+ now,
883
+ warn: (line) => out(phaseLine("lease", chalk.yellow(line)))
884
+ });
885
+ if ("code" in pooled) return fail(pooled.code, pooled.message, pooled.remedy, pooled.extra);
886
+ device = pooled.device;
887
+ bootPromise = Promise.resolve({
888
+ ok: true,
889
+ serial: pooled.device.serial
890
+ });
891
+ } else if (target.kind === "physical") {
892
+ const resolved = resolvePhysicalDevice(target.serial, listDevices(), isEmulatorDevice);
893
+ if (!resolved.serial) return fail(NO_DEVICE, resolved.error, resolved.remedy);
894
+ device = {
895
+ serial: resolved.serial,
896
+ deviceName: deviceModel(resolved.serial) ?? resolved.serial,
897
+ owned: false
898
+ };
899
+ bootPromise = Promise.resolve({
900
+ ok: true,
901
+ serial: resolved.serial
902
+ });
903
+ } else {
904
+ if (!remoteDevice) try {
905
+ configuredAndroidEmulatorApp();
906
+ } catch (err) {
907
+ return fail("STIM_BAD_ARG", err.message, "Run `stim settings set androidEmulatorApp emulator` or `stim settings set androidEmulatorApp stim-desktop`.");
908
+ }
909
+ const capacity = plan.deviceSlotWaitMs === 0 ? checkCapacity({
910
+ platform: PLATFORM,
911
+ project,
912
+ max: limits.maxDevices
913
+ }) : null;
914
+ if (capacity) {
915
+ if (capacity.code === "STIM_AT_CAPACITY") recordRefusal({
916
+ platform: PLATFORM,
917
+ max: limits.maxDevices,
918
+ workspace: workspaceId(root)
919
+ }, now());
920
+ return fail(capacity.code, capacity.message, capacity.remedy);
921
+ }
922
+ const prepare = stepClock(now);
923
+ try {
924
+ device = await ensureDevice({
925
+ platform: PLATFORM,
926
+ project,
927
+ projectPath: root,
928
+ settingsRoot,
929
+ settings,
930
+ flags: {
931
+ deviceSlotWait,
932
+ systemImage: target.systemImage,
933
+ systemImageFlag: systemImageFlag?.trim() || null,
934
+ deviceProfile: target.deviceProfile
935
+ },
936
+ note: out,
937
+ out,
938
+ logFile: emuLog
939
+ });
940
+ } catch (err) {
941
+ const failure = avdSetupFailure(err, root, emuLog);
942
+ return fail(failure.code, failure.message, failure.remedy, failure.extra);
943
+ }
944
+ progress.deviceSetup(didSetUpDevice(device, Boolean(remoteDevice)));
945
+ const prepareMs = prepare();
946
+ if (device.created || prepareMs >= 2e3) phase("device", `${device.avdName || device.deviceName || label} ${device.created ? "created" : "prepared"} (${formatDuration(prepareMs)})`);
947
+ const boot = () => Promise.resolve(ensureDeviceBooted({
948
+ platform: PLATFORM,
949
+ device,
950
+ projectPath: root,
951
+ deviceSlotWait,
952
+ out,
953
+ logFile: emuLog
954
+ })).catch((e) => ({
955
+ failed: true,
956
+ reason: String(e?.message || e),
957
+ serial: void 0
958
+ }));
959
+ const startBoot = () => {
960
+ const bootTimer = stepTimer(now);
961
+ bootPending = remoteDevice?.ctx.backend !== "eas";
962
+ return (remoteDevice?.ctx.backend === "eas" ? ensureRemoteOwned({
963
+ root,
964
+ platform: PLATFORM,
965
+ sessionName: ownedSessionName(remoteDevice.ctx.label),
966
+ startedAt: new Date(now()).toISOString(),
967
+ boot,
968
+ createdSessionId: remoteDevice.createdSessionId,
969
+ abandonCreatedSession: remoteDevice.abandonCreatedSession,
970
+ webPreviewUrl: remoteDevice.webPreviewUrl,
971
+ writeState,
972
+ register: registerProject,
973
+ notice: (line) => out(chalk.dim(phaseLine("lock", line)))
974
+ }) : boot()).then((result) => {
975
+ bootDuration = bootTimer();
976
+ bootPending = false;
977
+ return result;
978
+ });
979
+ };
980
+ if (remoteDevice) startRemoteBoot = startBoot;
981
+ else bootPromise = startBoot();
982
+ }
983
+ record.avdName = device.avdName ?? null;
984
+ record.deviceName = device.deviceName ?? device.avdName ?? null;
985
+ record.systemImage = device.systemImage;
986
+ record.deviceProfile = device.deviceProfile;
987
+ let preparedArtifact;
988
+ const runFromFingerprint = async () => {
989
+ if (metroCheck && metroPort !== null && plan.metroWarmup) prewarmMetro({
990
+ port: metroPort,
991
+ platform: "android",
992
+ isExpo,
993
+ appId: androidPackage,
994
+ bundleUrl: metroWarmupUrlSetting(settings, "android")
995
+ });
996
+ const acquiredArtifact = await acquireAndroidArtifact({
997
+ root,
998
+ buildLog,
999
+ writer,
1000
+ recipe: integration.artifact({
1001
+ root,
1002
+ buildLog,
1003
+ writer,
1004
+ settings,
1005
+ buildPlan,
1006
+ target: androidBuildOptions({
1007
+ release,
1008
+ physical,
1009
+ device,
1010
+ variant,
1011
+ deviceAbi,
1012
+ targetAbiOnly: buildPlan.targetAbiOnly,
1013
+ hostedAbi: hostedTarget?.choice.architecture
1014
+ }),
1015
+ phase,
1016
+ out,
1017
+ step: progress.step,
1018
+ estimates
1019
+ }),
1020
+ targetOffloadRefusal: physical ? "device builds build here" : remoteDevice ? "remote device builds build here" : null,
1021
+ buildPlan,
1022
+ cacheProviderConfig,
1023
+ requestedBuildCache,
1024
+ easBuild,
1025
+ androidPackage,
1026
+ record,
1027
+ maxBuilds: limits.maxBuilds,
1028
+ progress: {
1029
+ phase,
1030
+ out,
1031
+ stats,
1032
+ step: progress.step,
1033
+ miss: progress.miss,
1034
+ hit: progress.hit,
1035
+ place: progress.place,
1036
+ waitingOn: progress.waitingOn,
1037
+ waitingFor: progress.waitingFor
1038
+ }
1039
+ }, {
1040
+ acquireLock,
1041
+ releaseLock,
1042
+ waitForBuild,
1043
+ easAuth,
1044
+ resolveRemoteBuild,
1045
+ uploadRemoteBuild,
1046
+ loadCacheProviderModule,
1047
+ acquireSlot,
1048
+ releaseSlot,
1049
+ now
1050
+ });
1051
+ if (!acquiredArtifact.ok) {
1052
+ ccacheActivity = acquiredArtifact.ccache;
1053
+ const { failure } = acquiredArtifact;
1054
+ return fail(failure.code, failure.message, failure.remedy, failure.extra);
1055
+ }
1056
+ const { artifact } = acquiredArtifact;
1057
+ preparedArtifact = artifact;
1058
+ const { apkPath } = artifact;
1059
+ ccacheActivity = artifact.ccache;
1060
+ androidPackage = artifact.androidPackage;
1061
+ record.appPath = apkPath;
1062
+ if (runCancellation()) return fail("STIM_CANCELLED", "before install");
1063
+ if (hostedTarget) return finishHostedAndroidRun({
1064
+ target: hostedTarget,
1065
+ artifact,
1066
+ root,
1067
+ slot,
1068
+ release,
1069
+ appMode,
1070
+ isExpo,
1071
+ metroPort,
1072
+ logsDir,
1073
+ selectors,
1074
+ place: options.placeHostedAndroid ?? placeHostedAndroid,
1075
+ writePlacement: options.writeHostedAndroid ?? writeHostedAndroid,
1076
+ verifyLaunch: verifyLaunched,
1077
+ writeLaunch,
1078
+ readApkPackage,
1079
+ resolveDevClientScheme,
1080
+ now,
1081
+ started,
1082
+ startedAt,
1083
+ record,
1084
+ fail,
1085
+ phase,
1086
+ enterPhase: progress.step,
1087
+ out,
1088
+ json,
1089
+ useBuildCache,
1090
+ variant,
1091
+ metroCheck,
1092
+ writer,
1093
+ emit,
1094
+ recordRun,
1095
+ reclaimed,
1096
+ devServer,
1097
+ recordBuild
1098
+ });
1099
+ if (startRemoteBoot) {
1100
+ progress.step("device");
1101
+ bootPromise = startRemoteBoot();
1102
+ const booted = await bootPromise;
1103
+ if (booted.failed) {
1104
+ if (booted.code) return fail(booted.code, booted.reason ?? "The remote device did not boot.", booted.remedy ?? null);
1105
+ const diag = noDeviceDiagnostic({
1106
+ reason: booted.reason ?? "The remote device did not boot.",
1107
+ logFile: emuLog,
1108
+ remedy: "Run `stim status` to inspect the remote device, then retry the command.",
1109
+ localEmulator: false
1110
+ });
1111
+ return fail(NO_DEVICE, diag.message, diag.remedy, {
1112
+ lines: diag.lines,
1113
+ logPath: diag.logPath ? displayPath(root, diag.logPath) : null
1114
+ });
1115
+ }
1116
+ }
1117
+ let leaseHandle = null;
1118
+ let stopLeaseSignals = null;
1119
+ const releaseLease = () => {
1120
+ const held = leaseHandle;
1121
+ const stopSignals = stopLeaseSignals;
1122
+ leaseHandle = null;
1123
+ stopLeaseSignals = null;
1124
+ stopSignals?.();
1125
+ try {
1126
+ held?.release();
1127
+ } catch (err) {
1128
+ out(phaseLine("lease", chalk.dim(`could not release this run's lease: ${err?.message || err}`)));
1129
+ }
1130
+ };
1131
+ if (physical) {
1132
+ progress.step("device");
1133
+ const acquired = await acquireLease({
1134
+ root,
1135
+ platform: PLATFORM,
1136
+ id: device.serial,
1137
+ deviceName: device.deviceName ?? null,
1138
+ idLabel: "serial",
1139
+ waitSeconds: target.lease.waitSeconds,
1140
+ noWait: target.lease.noWait,
1141
+ installBoundMs: ADB_INSTALL_TIMEOUT_MS,
1142
+ appId: androidPackage,
1143
+ holderAppId: (holder) => getProject(holder)?.androidPackage ?? null,
1144
+ now,
1145
+ warn: (line) => out(phaseLine("lease", chalk.yellow(line)))
1146
+ });
1147
+ if (acquired.status === "refused") return fail(acquired.refusal.code, acquired.refusal.message, acquired.refusal.remedy, { lease: acquired.refusal.lease });
1148
+ leaseHandle = makeRunLease({
1149
+ root,
1150
+ platform: PLATFORM,
1151
+ kind: acquired.status === "leased" ? acquired.kind : null,
1152
+ expiresAt: acquired.status === "leased" ? acquired.expiresAt : null
1153
+ });
1154
+ if (acquired.status === "leased") {
1155
+ stopLeaseSignals = onLeaseSignal(releaseLease);
1156
+ phase("lease", `${acquired.kind} lease on ${device.serial} until ${leaseExpiryText(acquired.expiresAt, now())}`);
1157
+ }
1158
+ }
1159
+ try {
1160
+ return await finishAndroidRun({
1161
+ runtime,
1162
+ runtimePreparation,
1163
+ slot,
1164
+ lease: leaseHandle,
1165
+ releaseLease,
1166
+ root,
1167
+ json,
1168
+ metroCheck,
1169
+ useBuildCache,
1170
+ variant,
1171
+ release,
1172
+ isExpo,
1173
+ metroPort,
1174
+ logsDir,
1175
+ emuLog,
1176
+ device,
1177
+ physical,
1178
+ remoteDevice,
1179
+ bootPromise,
1180
+ resolveAvdSerial,
1181
+ waitForDeviceBoot,
1182
+ dataFreeBytes,
1183
+ trimCaches,
1184
+ wipeDevice,
1185
+ bootDuration: () => bootDuration,
1186
+ bootPending: () => bootPending,
1187
+ apkPath,
1188
+ androidPackage,
1189
+ androidPackageProblem: appIds.androidPackageProblem ?? null,
1190
+ record,
1191
+ waitedForBuild: artifact.waitedForBuild,
1192
+ ccache: ccacheActivity,
1193
+ uploadPending: artifact.uploadPending,
1194
+ providerUpload: artifact.providerUpload,
1195
+ providerName: artifact.providerName,
1196
+ remote: artifact.remote,
1197
+ abandonedRemote: artifact.abandonedRemote,
1198
+ started,
1199
+ startedAt,
1200
+ writer,
1201
+ phase,
1202
+ fail,
1203
+ readApkPackage,
1204
+ readProjectPackage: () => integration.appIds().androidPackage,
1205
+ packageRemedy: integration.packageRemedy,
1206
+ install,
1207
+ launch,
1208
+ launchRelease,
1209
+ resolveDevClientScheme,
1210
+ verifyLaunched,
1211
+ verifyReleaseLaunched,
1212
+ spawn,
1213
+ kill,
1214
+ pidAlive,
1215
+ verifyCollector,
1216
+ writeLaunch,
1217
+ recordBuild,
1218
+ now,
1219
+ out,
1220
+ emit,
1221
+ recordRun,
1222
+ reclaimed,
1223
+ devServer,
1224
+ enterPhase: progress.step,
1225
+ enterActivity: progress.activity,
1226
+ rebootDevice: () => Promise.resolve(ensureDeviceBooted({
1227
+ platform: PLATFORM,
1228
+ device: {
1229
+ ...device,
1230
+ serial: void 0
1231
+ },
1232
+ projectPath: root,
1233
+ deviceSlotWait,
1234
+ out,
1235
+ logFile: emuLog
1236
+ })).catch((e) => ({
1237
+ failed: true,
1238
+ reason: String(e?.message || e)
1239
+ }))
1240
+ });
1241
+ } finally {
1242
+ releaseLease();
1243
+ }
1244
+ };
1245
+ try {
1246
+ return await runFromFingerprint();
1247
+ } catch (error) {
1248
+ recordRun({
1249
+ failed: true,
1250
+ durationMs: now() - started
1251
+ });
1252
+ throw error;
1253
+ } finally {
1254
+ preparedArtifact?.release();
1255
+ }
1256
+ }
1257
+ //#endregion
1258
+ export { runAndroidOperation as i, registerAndroid as n, runAndroid as r, androidCommand as t };