stim 1.18.0 → 1.20.0

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 (178) hide show
  1. package/README.md +21 -4
  2. package/dist/{activity-BxnkDZDi.mjs → activity-BrGh9EAI.mjs} +3 -3
  3. package/dist/{agent-device-usage-output-CZ66YA4T.mjs → agent-device-usage-output-DVyv_hWj.mjs} +7 -7
  4. package/dist/android-0vv-OruW.mjs +6 -0
  5. package/dist/android-Brx6EKvJ.mjs +1258 -0
  6. package/dist/{android-DC3ZLuw0.mjs → android-BsYNR81W.mjs} +103 -45
  7. package/dist/{android-CBwCNicO.mjs → android-CK3fpEyZ.mjs} +2 -2
  8. package/dist/{android-cas-BBFyr9CA.mjs → android-cas-DCAuXryb.mjs} +9 -5
  9. package/dist/android-cas-compiler.mjs +1 -1
  10. package/dist/api-run.mjs +159 -8
  11. package/dist/{api-D0VtFhqk.mjs → api-zuX3ynYN.mjs} +9 -7
  12. package/dist/api.d.mts +104 -4
  13. package/dist/api.mjs +1 -1
  14. package/dist/{app-install-D-PUENug.mjs → app-install-0LgaQRT7.mjs} +39 -18
  15. package/dist/artifact-CnC0mfov.mjs +807 -0
  16. package/dist/artifact-DClXJNWx.mjs +772 -0
  17. package/dist/artifact-lifecycle-DzPpBYvK.mjs +30 -0
  18. package/dist/{web-DTU0uiWj.mjs → browser-web-CzBmhmgs.mjs} +136 -174
  19. package/dist/{budget-DIhL2BKx.mjs → budget-BONq9vXY.mjs} +3096 -3244
  20. package/dist/build-BixRI6sB.mjs +462 -0
  21. package/dist/build-plan-CP0tckNU.mjs +553 -0
  22. package/dist/{cache-manifest-BI7ozfmH.mjs → cache-manifest-qA3dT2x2.mjs} +1 -1
  23. package/dist/cache-manifest.mjs +1 -1
  24. package/dist/ccache-CjHQ5F8g.mjs +142 -0
  25. package/dist/{chrome-DJnmnS5q.mjs → chrome-Dp2lMLBe.mjs} +1 -1
  26. package/dist/{cli-DNJMwADx.mjs → cli-B2R8wH1F.mjs} +20 -20
  27. package/dist/cli.mjs +1 -1
  28. package/dist/{client-DzhapoMB.mjs → client-BLy1Pw2V.mjs} +172 -165
  29. package/dist/collector-run.d.mts +10 -0
  30. package/dist/collector-run.mjs +7 -7
  31. package/dist/{command-output-CBtzyIgF.mjs → command-output-BjPN99ZB.mjs} +2 -2
  32. package/dist/{config-CgYMahz7.mjs → config-D0LhTY3S.mjs} +13 -13
  33. package/dist/{created-devices-DKbeWsiK.mjs → created-devices-HU_DAb_Q.mjs} +7 -6
  34. package/dist/{dependency-state-xKoOypBm.mjs → dependency-state-B2m8eqsS.mjs} +2 -2
  35. package/dist/{detached-entry-BLfcgc4O.mjs → detached-entry-BXq4fIl-.mjs} +1 -1
  36. package/dist/{dev-client-CLnIH02x.mjs → dev-client-ClATaJoH.mjs} +2 -2
  37. package/dist/{device-D7oCHGWi.mjs → device-B7lsC0K1.mjs} +8 -8
  38. package/dist/device-DVCwAFlX.mjs +727 -0
  39. package/dist/{device-capacity-EUOOYr8T.mjs → device-capacity-Ca_ke1LE.mjs} +25 -15
  40. package/dist/device-host-worker.mjs +38 -18
  41. package/dist/{device-ios-6MewKKnc.mjs → device-ios-CRUqPxKw.mjs} +12 -11
  42. package/dist/{device-lease-Co-EwifR.mjs → device-lease-DHOBXGgS.mjs} +135 -22
  43. package/dist/{device-lease-run-w1xhCqIW.mjs → device-lease-run-CvDeHTkG.mjs} +4 -4
  44. package/dist/{device-pool-F5WvRPtD.mjs → device-pool-L2-5GxMy.mjs} +3 -3
  45. package/dist/{device-remote-tHbd1fKy.mjs → device-remote-Bmg42063.mjs} +17 -69
  46. package/dist/doctor-C2MSuMMg.mjs +515 -0
  47. package/dist/{doctor-B9ckYUoU.mjs → doctor-DPAqhVCK.mjs} +334 -721
  48. package/dist/doctor-watchman-CB4NnVZm.mjs +260 -0
  49. package/dist/{simslim-EHBQ2R4G.mjs → eas-build-Dw_ec2Km.mjs} +6 -152
  50. package/dist/{stim-installations-aR_WsNOA.mjs → eas-session-ledger-DZlC-Fq3.mjs} +57 -4
  51. package/dist/{error-diagnostics-BRXOqDzG.mjs → error-diagnostics-Du7I_GuY.mjs} +17 -12
  52. package/dist/errors-DzkMYh8Q.mjs +205 -0
  53. package/dist/{exec-DWhCZDGm.mjs → exec-B81Qodjn.mjs} +69 -15
  54. package/dist/{gc-Dh6mXOKC.mjs → gc-PWp34zGb.mjs} +23 -22
  55. package/dist/{gradle-C6GKnunQ.mjs → gradle-C2OEKCCI.mjs} +50 -125
  56. package/dist/{guide-tm6igAaa.mjs → guide-Dai9lphv.mjs} +862 -538
  57. package/dist/{hosted-android-Bv285t_t.mjs → hosted-android-DQ-Agsd8.mjs} +5 -5
  58. package/dist/{hosted-client-C-rMtppF.mjs → hosted-client-Io01s5By.mjs} +9 -3
  59. package/dist/{hosted-ios-D5mNmtj4.mjs → hosted-ios-DIxFnYp-.mjs} +3 -3
  60. package/dist/{hosted-logs-DN8Iz4mJ.mjs → hosted-logs-DP-ABU_M.mjs} +8 -8
  61. package/dist/{hosted-macos-B-wEjlwG.mjs → hosted-macos-Aziw9T_9.mjs} +7 -6
  62. package/dist/{hosted-native-DeeOG8xZ.mjs → hosted-native-BdAQi21M.mjs} +23 -16
  63. package/dist/{idle-shutdown-jjtxAn5C.mjs → idle-shutdown-DdMqoSUB.mjs} +7 -7
  64. package/dist/ios-BZ9dg2Fr.mjs +9 -0
  65. package/dist/{ios-B3SbOWdw.mjs → ios-CXLIUxXJ.mjs} +180 -24
  66. package/dist/ios-CZuv6gRv.mjs +1112 -0
  67. package/dist/{ios-Bvbf0SGY.mjs → ios-VGvNdsv2.mjs} +1 -1
  68. package/dist/{ios-device-Bn7jWDB1.mjs → ios-device-B1B14xND.mjs} +1 -1
  69. package/dist/{ios-device-vOaByMhw.mjs → ios-device-BreZOpM0.mjs} +3 -3
  70. package/dist/ios-project-B9drt_aY.mjs +197 -0
  71. package/dist/{ios-state-B2pES6Vs.mjs → ios-state-CjrL2qv-.mjs} +1 -1
  72. package/dist/js-swap-C8icNYIY.mjs +432 -0
  73. package/dist/launch-DxH0THLP.mjs +745 -0
  74. package/dist/launch-YrbqaIFj.mjs +1530 -0
  75. package/dist/{launch-verify-hskNcWkZ.mjs → launch-verify-BzRdbuO1.mjs} +26 -7
  76. package/dist/{logs-1Ylc1Ri6.mjs → logs-DHaRNfg7.mjs} +11 -11
  77. package/dist/macos-C0iljhsN.mjs +2 -0
  78. package/dist/{macos-DF6C7wUU.mjs → macos-V_3Ui7g4.mjs} +125 -155
  79. package/dist/macos-run.mjs +1 -1
  80. package/dist/maintenance-run.mjs +13 -10
  81. package/dist/{metro-Dh8T4Ytd.mjs → metro-D_j9sN-B.mjs} +209 -43
  82. package/dist/{metro-gateway-CEUw9RMH.mjs → metro-gateway-BBJldOdi.mjs} +2 -2
  83. package/dist/miss-reason-BPAzZS0r.mjs +238 -0
  84. package/dist/{named-ports-BFIz1Shz.mjs → named-ports-C2lvoPsb.mjs} +12 -5
  85. package/dist/native-android-lHzaew7r.mjs +226 -0
  86. package/dist/native-gradle-inputs-BQnIdAlL.mjs +356 -0
  87. package/dist/{native-runtime-D_603JiC.mjs → native-runtime-BRmWL7ru.mjs} +9 -8
  88. package/dist/native-xcode-inputs-eutS19jU.mjs +307 -0
  89. package/dist/native-xcode-ios-DP3cHLwu.mjs +434 -0
  90. package/dist/{ndjson-LGZAMsqa.mjs → ndjson-GOnbyvso.mjs} +6 -6
  91. package/dist/offload-worker.d.mts +12 -6
  92. package/dist/offload-worker.mjs +409 -73
  93. package/dist/{ownership-D6Se7gg_.mjs → ownership-DXzMMSx6.mjs} +2 -2
  94. package/dist/{placement-log-BcPhRwR4.mjs → placement-log-reFbjqEr.mjs} +7 -3
  95. package/dist/plan-BDk2AeY_.mjs +260 -0
  96. package/dist/plan-placement-D_YYlE6N.mjs +73 -0
  97. package/dist/{ports-BLTtfBq-.mjs → ports-jzvAJXXL.mjs} +3 -3
  98. package/dist/{prebuild-juCyreaR.mjs → prebuild-Bw44QxVh.mjs} +4 -4
  99. package/dist/{preview-D5V07Nrl.mjs → preview-BaBJ72Al.mjs} +51 -34
  100. package/dist/project-B8_duAzW.mjs +1067 -0
  101. package/dist/project-plan-BPwbLwQu.mjs +10 -0
  102. package/dist/{pull-request-u4vp7zmK.mjs → pull-request-vOhLNmIb.mjs} +1 -1
  103. package/dist/pull-requests.mjs +1 -1
  104. package/dist/react-native-android-chm1DMlA.mjs +921 -0
  105. package/dist/react-native-build-oAw6gQeW.mjs +239 -0
  106. package/dist/react-native-doctor-DYXoh-6I.mjs +78 -0
  107. package/dist/react-native-ios-SCTK_CT5.mjs +595 -0
  108. package/dist/{recordings-BAWltsVt.mjs → recordings-Bk-VivUV.mjs} +1 -1
  109. package/dist/{reload-DvyWzVjJ.mjs → reload-D34GkY78.mjs} +17 -14
  110. package/dist/remote-BU9UyhCJ.mjs +192 -0
  111. package/dist/{remote-cache-BGg7aGIZ.mjs → remote-cache-Bsts6f6Y.mjs} +5 -5
  112. package/dist/result-BDoRk1k3.mjs +186 -0
  113. package/dist/{run-vhtDgpHO.mjs → run-qvff78hv.mjs} +3 -3
  114. package/dist/{server-bare-B9E7S1aR.mjs → server-bare-dWjAarki.mjs} +6 -6
  115. package/dist/server-command-7XIKlWtO.mjs +69 -0
  116. package/dist/{server-expo-DoBHhuzK.mjs → server-expo-CsYu2aKL.mjs} +107 -82
  117. package/dist/server-expo-DV43vep4.mjs +2 -0
  118. package/dist/settings-Bbztm4oV.mjs +1503 -0
  119. package/dist/{settings-Byp2AQ_E.mjs → settings-CT3E71-i.mjs} +6 -7
  120. package/dist/{state-lKh8shBp.d.mts → settings-vk3mJGSm.d.mts} +4 -2
  121. package/dist/settings.schema.json +156 -6
  122. package/dist/simslim-ChMfe8rg.mjs +149 -0
  123. package/dist/{slot-launch-C1nqh4mu.mjs → slot-launch-B9hn96wd.mjs} +6 -6
  124. package/dist/{stage-g_WqWeaE.mjs → stage-BNKKdDNc.mjs} +6 -3
  125. package/dist/start-CbQCSnXQ.mjs +2 -0
  126. package/dist/{start-D-IeOyoK.mjs → start-CdvcTaXr.mjs} +113 -77
  127. package/dist/{state-xnqpx3AD.mjs → state-Boo0IMJq.mjs} +4 -2
  128. package/dist/{state-BG-aAk4k.mjs → state-DI84A-uu.mjs} +29 -10
  129. package/dist/{stats-DY6ekz8F.mjs → stats-BIIDrjA8.mjs} +6 -6
  130. package/dist/{status-BEeL6IKz.mjs → status-BDGHwkCs.mjs} +3 -1
  131. package/dist/{status-DCjZeE-K.mjs → status-DARMkc9q.mjs} +80 -294
  132. package/dist/status-watch-DvmI3SmN.mjs +296 -0
  133. package/dist/{stim-desktop-Behws_fi.mjs → stim-desktop-8GQX2OV8.mjs} +1 -1
  134. package/dist/{stop-DIJtD2GL.mjs → stop-CCZS-C9m.mjs} +2 -2
  135. package/dist/stop-WQPQHLuy.mjs +72 -0
  136. package/dist/{stop-BTjs3xbc.mjs → stop-iNemHsJC.mjs} +81 -33
  137. package/dist/supervisor-run.d.mts +7 -2
  138. package/dist/supervisor-run.mjs +32 -20
  139. package/dist/{support-Bwvbrq5-.mjs → support-CnE_SIjt.mjs} +17 -7
  140. package/dist/{support-BaayeYCf.mjs → support-DCco5fut.mjs} +61 -8
  141. package/dist/swiftpm-macos-afeNUExW.mjs +119 -0
  142. package/dist/{native-run-B06FkmZx.mjs → tailnet-CoJUb2tB.mjs} +113 -6
  143. package/dist/toolchain-BIupIro1.mjs +2 -0
  144. package/dist/{toolchain-Bnyd_K3A.mjs → toolchain-DqyDXs30.mjs} +225 -315
  145. package/dist/{trigger-CJMhwXe8.mjs → trigger-C0MWBi8O.mjs} +3 -3
  146. package/dist/trigger-SHLOWJ4O.mjs +2 -0
  147. package/dist/{warm-progress-CjB-MP6H.mjs → warm-progress-C3ASD6z5.mjs} +134 -37
  148. package/dist/{status-Dcr0Qexd.mjs → watchman-CQl51YZu.mjs} +75 -7
  149. package/dist/web-B3eldHFH.mjs +123 -0
  150. package/dist/web-BLXpOe6b.mjs +2 -0
  151. package/dist/web-run.mjs +10 -10
  152. package/dist/{workspace-state-C59dqG-Z.mjs → workspace-state-CSIFuuDa.mjs} +33 -33
  153. package/dist/{workspaces-xXItD8Dj.mjs → workspaces-D2xSHBDY.mjs} +1297 -1211
  154. package/dist/{worktree-BSYlofkq.mjs → worktree-BxDqaTev.mjs} +102 -63
  155. package/dist/worktree-q7_OZNY0.mjs +2 -0
  156. package/package.json +8 -6
  157. package/shim/bundle-response.cjs +2 -1
  158. package/shim/bundle-response.d.cts +1 -1
  159. package/shim/expo-metro-config.cjs +1 -1
  160. package/shim/native-android.gradle +101 -0
  161. package/dist/android-BKA16DTf.mjs +0 -4
  162. package/dist/android-BvwuMUJ9.mjs +0 -3572
  163. package/dist/guide-status-Bhf9SJXc.mjs +0 -125
  164. package/dist/ios-CPdkoKaa.mjs +0 -3953
  165. package/dist/ios-DNpdygiv.mjs +0 -7
  166. package/dist/macos-Ck7OBMNY.mjs +0 -2
  167. package/dist/metro-store-BAHFcJm6.mjs +0 -106
  168. package/dist/plan-placement-CtxkCO54.mjs +0 -1859
  169. package/dist/project-C3ChRLZy.mjs +0 -212
  170. package/dist/projects-DD7dBTCj.mjs +0 -92
  171. package/dist/server-expo-Db9HBUoa.mjs +0 -2
  172. package/dist/settings-CQyMElEP.mjs +0 -701
  173. package/dist/start-NTc1Ifqs.mjs +0 -2
  174. package/dist/stop-BGhKMOuH.mjs +0 -48
  175. package/dist/trigger-DWAq9hkw.mjs +0 -2
  176. package/dist/web-B6_bFDn7.mjs +0 -2
  177. package/dist/worktree-BJfnU6r5.mjs +0 -2
  178. package/dist/worktree-CsKYexJM.mjs +0 -753
@@ -1,57 +1,31 @@
1
1
  import { n as ownership_claim_exports, t as process_identity_exports } from "./process-identity-Biv3arCO.mjs";
2
- import { t as getExecutor } from "./exec-DWhCZDGm.mjs";
2
+ import { t as getExecutor } from "./exec-B81Qodjn.mjs";
3
+ import { D as withConfigLock$1, H as projectDeviceSlots, M as isOnMountedVolume, N as listMountedVolumes, P as measuredDirectorySize, _ as loadConfig$1, d as getConfigDir, g as isPathPrefix, j as formatBytes, k as acquireAvdClaim, p as getProject, z as deviceSlotPlatforms } from "./config-D0LhTY3S.mjs";
4
+ import { Et as repoRoot, st as resolveTailscaleBinary } from "./settings-Bbztm4oV.mjs";
5
+ import { h as listBuildLocks } from "./device-lease-DHOBXGgS.mjs";
6
+ import { T as workspaceName, r as readWorkspaceState$1, s as workspaceLastUsed, x as workspaceDir, y as workspaceAgentDeviceDir } from "./workspace-state-CSIFuuDa.mjs";
3
7
  import { t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
4
- import { a as formatElapsed, l as phaseLine } from "./command-output-CBtzyIgF.mjs";
5
- import { D as withConfigLock$1, H as projectDeviceSlots, M as isOnMountedVolume, N as listMountedVolumes, P as measuredDirectorySize, _ as loadConfig$1, d as getConfigDir, g as isPathPrefix, j as formatBytes, k as acquireAvdClaim, p as getProject, z as deviceSlotPlatforms } from "./config-CgYMahz7.mjs";
6
- import { y as repoRoot } from "./worktree-CsKYexJM.mjs";
7
- import { o as pidExists, u as signalProcessTree } from "./metro-Dh8T4Ytd.mjs";
8
- import { S as workspaceName, g as workspaceAgentDeviceDir, r as readWorkspaceState$1, s as workspaceLastUsed, v as workspaceDir } from "./workspace-state-C59dqG-Z.mjs";
9
- import { _ as removeSingletonFiles, c as webClaimRoot, f as webProfileDir, g as liveProfileHolder, h as chromeProcessState, i as clearWebRecord, o as readWebRecord, t as CDP_PORT_LABEL, u as webFacts } from "./state-BG-aAk4k.mjs";
10
- import { t as agentDeviceDaemon, u as readAgentDeviceRecords } from "./activity-BxnkDZDi.mjs";
11
- import { h as listBuildLocks } from "./device-lease-Co-EwifR.mjs";
8
+ import { a as formatElapsed, l as phaseLine } from "./command-output-BjPN99ZB.mjs";
9
+ import { B as ownedAvdSystemImage, I as ownedAvdDirectory, J as resolveOwnedAvdSerial, L as ownedAvdMatchesConfiguration, M as listOrphanedAvdDirectories, O as listAdbDevices, X as shutdownAndroidEmulator, Z as sleepSync, _ as avdStorageRoots, ct as eraseParkedAfter, et as waitForAndroidEmulatorShutdown, f as assertOwnedAvdStopped, ft as readParked, h as avdPathExists, nt as wipeAvdUserData, pt as removeParkedAfter, rt as isStimOwnedAvd, ut as parkSim, x as deleteAvd } from "./android-BsYNR81W.mjs";
10
+ import { n as readCreatedDevices$1, r as recordCreatedDevice, t as forgetCreatedDevice } from "./created-devices-HU_DAb_Q.mjs";
11
+ import { d as signalProcessTree, s as pidExists } from "./metro-D_j9sN-B.mjs";
12
12
  import { n as declareSpawnsOn, o as stopDeclaringSpawnsOn } from "./spawn-claims-Kqiifsk2.mjs";
13
- import { n as readCreatedDevices$1, r as recordCreatedDevice, t as forgetCreatedDevice } from "./created-devices-DKbeWsiK.mjs";
14
- import { B as ownedAvdSystemImage, I as ownedAvdDirectory, J as resolveOwnedAvdSerial, L as ownedAvdMatchesConfiguration, M as listOrphanedAvdDirectories, O as listAdbDevices, X as shutdownAndroidEmulator, Z as sleepSync, _ as avdStorageRoots, dt as parkSim, et as waitForAndroidEmulatorShutdown, f as assertOwnedAvdStopped, h as avdPathExists, it as isStimOwnedAvd, lt as eraseParkedAfter, mt as removeParkedAfter, nt as wipeAvdUserData, pt as readParked, x as deleteAvd } from "./android-DC3ZLuw0.mjs";
15
- import { D as shutdownIosSim, T as resolveOwnedIosSim, _ as parkedSimName, c as eraseIosSim, o as deleteIosSim, p as listIosDeviceTypes, s as deleteParkedIosSim, v as parseRuntimeVersion, x as renameIosSim } from "./ios-B3SbOWdw.mjs";
16
- import { J as resolveTailscaleBinary } from "./settings-CQyMElEP.mjs";
17
- import { a as withWorkspaceProcessLock, o as workspaceProcessLockError, r as releaseBrowserPort, s as workspaceProcessLockPath } from "./named-ports-BFIz1Shz.mjs";
18
- import { i as macosRuntimeClaim } from "./state-xnqpx3AD.mjs";
13
+ import { a as withWorkspaceProcessLock, o as workspaceProcessLockError, r as releaseBrowserPort, s as workspaceProcessLockPath } from "./named-ports-C2lvoPsb.mjs";
14
+ import { _ as removeSingletonFiles, c as webClaimRoot, f as webProfileDir, g as liveProfileHolder, h as chromeProcessState, i as clearWebRecord, o as readWebRecord, t as CDP_PORT_LABEL, u as webFacts } from "./state-DI84A-uu.mjs";
15
+ import { t as agentDeviceDaemon, u as readAgentDeviceRecords } from "./activity-BrGh9EAI.mjs";
16
+ import { E as resolveOwnedIosSim, O as shutdownIosSim, S as renameIosSim, c as eraseIosSim, o as deleteIosSim, p as listIosDeviceTypes, s as deleteParkedIosSim, v as parkedSimName, y as parseRuntimeVersion } from "./ios-CXLIUxXJ.mjs";
17
+ import { a as macosRuntimeClaim, n as macosDir } from "./state-Boo0IMJq.mjs";
19
18
  import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync } from "node:fs";
20
19
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
21
20
  import { basename as basename$1, isAbsolute as isAbsolute$1, join as join$1, relative as relative$1, resolve as resolve$1 } from "path";
22
- import { buildSlotsDir, hostedNativeRecords, isJsonObject, macosAppState, parseHostedNativePlacement, readJsonFile, readMacosRecord, tryAcquireBuildSlotClaim, unreadableHostedNative } from "@stim-cli/core/state";
21
+ import { buildSlotsDir, hostedNativeRecords, isJsonObject, macosAppState, parseHostedNativePlacement, readJsonFile, readMacosRecord, requireAutomaticMachine, tryAcquireBuildSlotClaim, unreadableHostedNative } from "@stim-cli/core/state";
22
+ import { existsSync as existsSync$1, lstatSync as lstatSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, rmSync as rmSync$1, statSync as statSync$1 } from "fs";
23
+ import { createHash, randomUUID } from "node:crypto";
23
24
  import { homedir } from "node:os";
24
25
  import chalk from "chalk";
25
26
  import { isDeepStrictEqual } from "node:util";
26
- import { existsSync as existsSync$1, lstatSync as lstatSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, rmSync as rmSync$1, statSync as statSync$1 } from "fs";
27
- import { createHash, randomUUID } from "node:crypto";
28
27
  import { resolve4 } from "node:dns/promises";
29
28
  import { request } from "node:https";
30
- //#region src/commands/gc/paths.ts
31
- function canonicalPath(p) {
32
- try {
33
- return realpathSync$1(p);
34
- } catch {
35
- return resolve$1(p);
36
- }
37
- }
38
- //#endregion
39
- //#region src/commands/gc/results.ts
40
- const results = [];
41
- function recordGcResult(kind, status, label, { id = null, bytes = null, detail = null } = {}) {
42
- results.push({
43
- kind,
44
- status,
45
- label,
46
- id,
47
- bytes,
48
- detail
49
- });
50
- }
51
- function takeGcResults() {
52
- return results.splice(0);
53
- }
54
- //#endregion
55
29
  //#region src/engine/build-slots.ts
56
30
  const SLOT_PREFIX = "slot-";
57
31
  const SLOT_POLL_MS = 1e3;
@@ -101,7 +75,8 @@ function tryAcquireBuildSlot({ max, root = null, logFile = null }) {
101
75
  function slotWaitingLine({ max, elapsedMs }) {
102
76
  return `${"build".padEnd(11)} waiting for a build slot (all ${max} in use, ${formatElapsed(elapsedMs)} elapsed) -- stim guide lifecycle concurrency`;
103
77
  }
104
- async function acquireBuildSlot({ max, root = null, logFile = null, now = Date.now, out = () => {}, waitingFor = () => {}, sleep = sleepAsync$1, intervalMs = SLOT_POLL_MS, progressMs = SLOT_PROGRESS_MS, ceilingMs = SLOT_CEILING_MS }) {
78
+ async function acquireBuildSlot({ max, root = null, logFile = null, now = Date.now, out = () => {}, waitingFor = () => {}, sleep = sleepAsync$1, intervalMs = SLOT_POLL_MS, progressMs = SLOT_PROGRESS_MS, ceilingMs = SLOT_CEILING_MS, automatic = false }) {
79
+ if (automatic) requireAutomaticMachine("build", "local");
105
80
  if (!max || max <= 0) return {
106
81
  acquired: true,
107
82
  unlimited: true
@@ -111,6 +86,7 @@ async function acquireBuildSlot({ max, root = null, logFile = null, now = Date.n
111
86
  let waited = false;
112
87
  try {
113
88
  for (;;) {
89
+ if (automatic) requireAutomaticMachine("build", "local");
114
90
  const got = tryAcquireBuildSlot({
115
91
  max,
116
92
  root,
@@ -187,1285 +163,1389 @@ function listBuildSlots() {
187
163
  return slots;
188
164
  }
189
165
  //#endregion
190
- //#region src/devices/agent-device-cleanup.ts
191
- function parseAgentDeviceSessions(output) {
192
- const result = JSON.parse(output);
193
- if (result?.success !== true || !Array.isArray(result.data?.sessions)) throw new Error("agent-device did not return a successful session list");
194
- return result.data.sessions.flatMap((entry) => {
195
- if (!entry || typeof entry !== "object") return [];
196
- const row = entry;
197
- const id = row.platform === "ios" ? row.device_udid : row.id;
198
- if (row.platform !== "ios" && row.platform !== "android" || typeof row.name !== "string" || !row.name.trim() || typeof id !== "string" || !id || typeof row.createdAt !== "number" || !Number.isFinite(row.createdAt) || row.platform === "ios" && row.id !== void 0 && row.id !== id) return [];
199
- return [{
200
- name: typeof row.address === "string" && row.address.trim() ? row.address : row.name,
201
- platform: row.platform,
202
- id,
203
- device: typeof row.device === "string" ? row.device : null,
204
- createdAt: row.createdAt
205
- }];
206
- });
166
+ //#region src/supervisor/ownership.ts
167
+ function resolveSupervisorTarget({ state, record, reservedPort, isAlive = pidExists, inspectIdentity = process_identity_exports.inspectProcessIdentity } = {}) {
168
+ const statePid = numberOrNull(state?.pid);
169
+ const recordPid = numberOrNull(record?.pid);
170
+ const pid = statePid ?? recordPid;
171
+ if (!pid) return { status: "none" };
172
+ if (statePid && recordPid && statePid !== recordPid) return {
173
+ status: "unverified",
174
+ pid,
175
+ reason: `workspace state.json records supervisor pid ${statePid} but the registry records pid ${recordPid}`
176
+ };
177
+ if (!isAlive(pid)) return {
178
+ status: "stale",
179
+ pid
180
+ };
181
+ const port = numberOrNull(state?.port) ?? numberOrNull(record?.port);
182
+ if (reservedPort !== null && reservedPort !== void 0 && port !== null && port !== reservedPort) return {
183
+ status: "unverified",
184
+ pid,
185
+ reason: `supervisor pid ${pid} records port ${port}, but this project reserved port ${reservedPort}`
186
+ };
187
+ if (reservedPort !== null && reservedPort !== void 0 && port === null) return {
188
+ status: "unverified",
189
+ pid,
190
+ reason: `supervisor pid ${pid} has no recorded port, so it cannot be matched against reserved port ${reservedPort}`
191
+ };
192
+ const identityRecord = statePid ? state : record;
193
+ if (statePid && recordPid && state?.processToken !== record?.processToken) return {
194
+ status: "unverified",
195
+ pid,
196
+ reason: "workspace state and registry process identities disagree"
197
+ };
198
+ const identity = inspectIdentity(identityRecord);
199
+ if (identity === "gone" || identity === "different") return {
200
+ status: "stale",
201
+ pid
202
+ };
203
+ if (identity !== "same") return {
204
+ status: "unverified",
205
+ pid,
206
+ reason: "the recorded process identity could not be verified"
207
+ };
208
+ return {
209
+ status: "ours",
210
+ processToken: identityRecord.processToken,
211
+ pid,
212
+ port,
213
+ mode: state?.mode ?? record?.mode ?? null,
214
+ startedAt: state?.startedAt ?? record?.startedAt ?? null
215
+ };
207
216
  }
208
- function isOwnDeviceSession(session, device, owner) {
209
- if (session.platform !== device.platform || device.id !== null && session.id !== device.id) return false;
210
- if (device.platform === "android" && session.device !== device.avdName.replaceAll("_", " ")) return false;
211
- if (!owner) return true;
212
- return owner.claims.some((claim) => claim.deviceId === session.id && claim.session === session.name && claim.workspace !== null && (within(owner.workspace, claim.workspace) || claim.workspace === owner.repoRoot));
217
+ function numberOrNull(v) {
218
+ return typeof v === "number" && Number.isSafeInteger(v) && v > 0 ? v : null;
213
219
  }
214
- function within(root, path) {
215
- const rest = relative(root, path);
216
- return rest === "" || !isAbsolute(rest) && rest.split(sep)[0] !== "..";
220
+ //#endregion
221
+ //#region src/engine/public-http-probe.ts
222
+ const DNS_TIMEOUT_MS = 5e3;
223
+ async function resolveAddresses(hostname, signal) {
224
+ if (signal.aborted) throw signal.reason;
225
+ let timer;
226
+ let onAbort;
227
+ const unavailable = new Promise((_resolve, reject) => {
228
+ onAbort = () => reject(signal.reason);
229
+ signal.addEventListener("abort", onAbort, { once: true });
230
+ if (signal.aborted) onAbort();
231
+ else timer = setTimeout(() => reject(/* @__PURE__ */ new Error("DNS lookup timed out")), DNS_TIMEOUT_MS);
232
+ });
233
+ try {
234
+ return await Promise.race([resolve4(hostname), unavailable]);
235
+ } finally {
236
+ clearTimeout(timer);
237
+ if (onAbort) signal.removeEventListener("abort", onAbort);
238
+ }
217
239
  }
218
- function canonical(path) {
240
+ async function probePublicHttp(url, signal) {
219
241
  try {
220
- return realpathSync(path);
242
+ return (await fetch(url, {
243
+ signal,
244
+ redirect: "follow"
245
+ })).status;
246
+ } catch (error) {
247
+ const cause = error?.cause;
248
+ if (cause?.code !== "ENOTFOUND" && cause?.code !== "EAI_AGAIN") return null;
249
+ }
250
+ const hostname = new URL(url).hostname;
251
+ if (!hostname.endsWith(".trycloudflare.com")) return null;
252
+ let addresses;
253
+ try {
254
+ addresses = await resolveAddresses(hostname, signal);
221
255
  } catch {
222
256
  return null;
223
257
  }
258
+ const firstAddress = addresses[0];
259
+ if (!firstAddress) return null;
260
+ return new Promise((resolve) => {
261
+ const request$1 = request(url, {
262
+ signal,
263
+ lookup: (_hostname, options, callback) => {
264
+ if (options.all) callback(null, addresses.map((address) => ({
265
+ address,
266
+ family: 4
267
+ })));
268
+ else callback(null, firstAddress, 4);
269
+ }
270
+ }, (response) => {
271
+ response.resume();
272
+ resolve(response.statusCode ?? null);
273
+ });
274
+ request$1.on("error", () => resolve(null));
275
+ request$1.end();
276
+ });
224
277
  }
225
- function deviceClaims(device) {
226
- return readAgentDeviceRecords(homedir()).filter((record) => record.kind === "claim" && (device.id === null || record.deviceId === device.id)).map((record) => ({
227
- deviceId: record.deviceId,
228
- session: record.session,
229
- workspace: record.workspace && canonical(record.workspace)
230
- }));
278
+ //#endregion
279
+ //#region src/engine/tunnel.ts
280
+ function tunnelArgv(provider, port, ngrokUrl, logFile, tailscaleBinary = "tailscale") {
281
+ if (provider === "tailscale") return {
282
+ bin: tailscaleBinary,
283
+ args: [
284
+ "serve",
285
+ `--https=${port}`,
286
+ `http://127.0.0.1:${port}`
287
+ ]
288
+ };
289
+ if (provider === "cloudflared") return {
290
+ bin: "cloudflared",
291
+ args: [
292
+ "tunnel",
293
+ "--url",
294
+ `http://127.0.0.1:${port}`,
295
+ ...logFile ? ["--logfile", logFile] : []
296
+ ]
297
+ };
298
+ return {
299
+ bin: "ngrok",
300
+ args: [
301
+ "http",
302
+ String(port),
303
+ `--log=${logFile || "stdout"}`,
304
+ "--log-format=json",
305
+ ...ngrokUrl ? ["--url", ngrokUrl] : []
306
+ ]
307
+ };
231
308
  }
232
- function explainFailure(error) {
233
- const stdout = error?.stdout;
234
- let failure;
309
+ const CLOUDFLARED_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
310
+ function parseCloudflaredLine(line) {
311
+ const match = line.match(CLOUDFLARED_URL_RE);
312
+ return match ? match[0] : null;
313
+ }
314
+ function parseNgrokLine(line) {
315
+ let data;
235
316
  try {
236
- failure = typeof stdout === "string" ? JSON.parse(stdout)?.error : void 0;
317
+ data = JSON.parse(line);
237
318
  } catch {
238
- failure = void 0;
319
+ return null;
239
320
  }
240
- if (typeof failure?.message !== "string" || !failure.message) return error;
241
- const reason = typeof failure.details?.reason === "string" ? ` (${failure.details.reason})` : "";
242
- return /* @__PURE__ */ new Error(`agent-device refused: ${failure.message}${reason}`);
321
+ if (typeof data !== "object" || data === null) return null;
322
+ const url = data.url;
323
+ return typeof url === "string" && /^https?:\/\//i.test(url) ? url : null;
243
324
  }
244
- function report(message) {
245
- process.stderr.write(`${phaseLine("device", message)}\n`);
325
+ function parseTailscaleLine(line) {
326
+ return /^https:\/\/[^\s/]+\.ts\.net(:\d+)?\/?$/.test(line) ? line : null;
246
327
  }
247
- function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot) {
248
- const exec = getExecutor();
249
- try {
250
- if (!exec.findExecutable("agent-device")) return;
251
- const root = workspace === void 0 ? void 0 : canonical(workspace);
252
- if (root === null) return;
253
- const owner = root === void 0 ? void 0 : {
254
- workspace: root,
255
- repoRoot: repoRoot && (canonical(repoRoot) ?? void 0),
256
- claims: deviceClaims(device)
257
- };
258
- const stateDir = root === void 0 ? void 0 : workspaceAgentDeviceDir(root);
259
- const stateDirs = [void 0, ...stateDir && !["none", "dead"].includes(agentDeviceDaemon(stateDir)) ? [stateDir] : []];
260
- for (const dir of stateDirs) {
261
- const deadline = Date.now() + 15e3;
262
- const run = (args) => {
263
- const remaining = deadline - Date.now();
264
- if (remaining <= 0) throw new Error("agent-device cleanup timed out");
265
- try {
266
- return exec.runFile("agent-device", [
267
- ...args,
268
- ...dir ? ["--state-dir", dir] : [],
269
- "--json",
270
- "--daemon-transport",
271
- "socket"
272
- ], {
273
- timeoutMs: Math.min(5e3, remaining),
274
- killSignal: "SIGKILL"
275
- });
276
- } catch (error) {
277
- throw explainFailure(error);
278
- }
279
- };
280
- const list = () => parseAgentDeviceSessions(run([
281
- "session",
282
- "list",
283
- "--session",
284
- "stim-teardown-inventory"
285
- ]));
286
- try {
287
- const sessions = list().filter((session) => isOwnDeviceSession(session, device, owner));
288
- for (const session of sessions) try {
289
- if (!list().some((current) => current.name === session.name && current.platform === session.platform && current.id === session.id && current.createdAt === session.createdAt)) continue;
290
- if (!stillOwned(session.id)) continue;
291
- if (JSON.parse(run([
292
- "close",
293
- "--session",
294
- session.name,
295
- "--session-lock",
296
- "reject",
297
- "--platform",
298
- device.platform,
299
- device.platform === "ios" ? "--udid" : "--serial",
300
- session.id
301
- ]))?.success !== true) throw new Error("agent-device did not confirm session close");
302
- report(`closed agent-device session ${session.name} on ${session.id}`);
303
- } catch (error) {
304
- report(`could not close agent-device session ${session.name} on ${session.id}: ${error.message}`);
305
- }
306
- } catch (error) {
307
- report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
308
- }
309
- }
310
- } catch (error) {
311
- report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
312
- }
328
+ function parserFor(provider) {
329
+ if (provider === "tailscale") return parseTailscaleLine;
330
+ return provider === "cloudflared" ? parseCloudflaredLine : parseNgrokLine;
313
331
  }
314
- //#endregion
315
- //#region src/devices/teardown.ts
316
- function teardownParkedAvd(avdName) {
317
- try {
318
- return removeParkedAfter("android", avdName, () => {
319
- const result = teardownOwnedAvd(avdName, {
320
- del: true,
321
- owner: { pool: true }
322
- });
323
- if (result.status !== "torn-down" && result.status !== "missing") throw new Error(result.reason);
324
- }) ? {
325
- status: "torn-down",
326
- label: avdName
327
- } : {
328
- status: "skipped",
329
- kind: "not-parked",
330
- reason: "emulator is no longer parked"
331
- };
332
- } catch (error) {
333
- return {
334
- status: "failed",
335
- reason: String(error?.message || error)
336
- };
337
- }
332
+ const URL_TIMEOUT_MS = 15e3;
333
+ const REACHABLE_TIMEOUT_MS = 24e4;
334
+ const REACHABLE_POLL_MS = 2e3;
335
+ const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
336
+ async function withManagedTunnelLock(root, fn, options = {}) {
337
+ return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
338
+ ...options,
339
+ rejectOwnerPurposes: ["workspace removal"]
340
+ });
338
341
  }
339
- function teardownParkedIosSim(udid, { label, deleteSim = deleteParkedIosSim } = {}) {
340
- try {
341
- const removed = removeParkedAfter("ios", udid, () => {
342
- closeOwnedDeviceSessions({
343
- platform: "ios",
344
- id: udid
345
- }, () => Boolean(resolveOwnedIosSim(udid).sim));
346
- deleteSim(udid);
347
- });
348
- if (!removed) return {
349
- status: "skipped",
350
- kind: "not-parked",
351
- reason: "simulator is no longer parked"
352
- };
353
- return {
354
- status: "torn-down",
355
- label: label ?? removed.name
356
- };
357
- } catch (error) {
358
- return {
359
- status: "failed",
360
- reason: String(error?.message || error)
361
- };
342
+ async function withManagedTunnelRemovalLock(root, fn, options = {}) {
343
+ return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
344
+ ...options,
345
+ ownerPurpose: "workspace removal"
346
+ });
347
+ }
348
+ async function withManagedRemoteWorktreeLock(worktreeRoot, fn, options = {}) {
349
+ return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
350
+ ...options,
351
+ external: true,
352
+ rejectOwnerPurposes: ["worktree removal"]
353
+ });
354
+ }
355
+ async function withManagedRemoteWorktreeRemovalLock(worktreeRoot, fn, options = {}) {
356
+ return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
357
+ ...options,
358
+ external: true,
359
+ ownerPurpose: "worktree removal"
360
+ });
361
+ }
362
+ function managedLockHolders(root) {
363
+ const locks = [{
364
+ label: "managed tunnel",
365
+ path: workspaceProcessLockPath(managedTunnelLockRoot(root), "metro-tunnel", false)
366
+ }, {
367
+ label: "managed remote",
368
+ path: workspaceProcessLockPath(managedRemoteWorktreeLockRoot(root), "managed-remote", true)
369
+ }];
370
+ const reasons = [];
371
+ for (const { label, path } of locks) {
372
+ const claims = (0, ownership_claim_exports.readClaimSet)(path);
373
+ const holder = claims.live[0];
374
+ if (holder) {
375
+ const purpose = typeof holder.details.purpose === "string" ? ` for ${holder.details.purpose}` : "";
376
+ reasons.push(`the ${label} lock is held${purpose}`);
377
+ } else if (claims.unresolved[0]) reasons.push(`the ${label} lock cannot be resolved: ${claims.unresolved[0].reason}`);
362
378
  }
379
+ return reasons;
363
380
  }
364
- function eraseParkedIosSim(udid, { label } = {}) {
365
- try {
366
- const erased = eraseParkedAfter("ios", udid, (_record, eraseStarted) => {
367
- closeOwnedDeviceSessions({
368
- platform: "ios",
369
- id: udid
370
- }, () => Boolean(resolveOwnedIosSim(udid).sim));
371
- const resolved = resolveOwnedIosSim(udid);
372
- if (resolved.missing) throw new Error(`simulator ${udid} is not on this machine`);
373
- if (resolved.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(resolved.notOwned)}`);
374
- const state = resolved.sim.state;
375
- if (state !== "Shutdown") throw new Error(`simulator ${udid} is ${state}, not Shutdown; it was kept`);
376
- eraseStarted();
377
- eraseIosSim(udid);
378
- });
379
- if (!erased) return {
380
- status: "skipped",
381
- kind: "not-parked",
382
- reason: "simulator is no longer parked"
381
+ function managedTunnelLockRoot(root) {
382
+ const key = createHash("sha256").update(resolve(root)).digest("hex");
383
+ return join(getConfigDir(), "process-locks", key);
384
+ }
385
+ function managedRemoteWorktreeLockRoot(worktreeRoot) {
386
+ const key = createHash("sha256").update(resolve(worktreeRoot)).digest("hex");
387
+ return join(getConfigDir(), "process-locks", "worktrees", key);
388
+ }
389
+ async function defaultProbeReachable(url, signal) {
390
+ const status = await probePublicHttp(url, signal);
391
+ return status !== null && status > 0;
392
+ }
393
+ function waitForUrl(child, parseLine, timeoutMs) {
394
+ return new Promise((resolve) => {
395
+ let settled = false;
396
+ let lastLine = null;
397
+ let timer;
398
+ let onOut;
399
+ let onErr;
400
+ let onError;
401
+ let onExit;
402
+ const finish = (url, exited = false) => {
403
+ if (settled) return;
404
+ settled = true;
405
+ clearTimeout(timer);
406
+ child.stdout?.removeListener("data", onOut);
407
+ child.stderr?.removeListener("data", onErr);
408
+ child.removeListener("error", onError);
409
+ child.removeListener("exit", onExit);
410
+ resolve({
411
+ url,
412
+ exited,
413
+ lastLine
414
+ });
383
415
  };
384
- return {
385
- status: "torn-down",
386
- label: label ?? erased.name
416
+ const onLine = (line) => {
417
+ if (settled) return;
418
+ if (line.trim()) lastLine = line.trim();
419
+ const url = parseLine(line);
420
+ if (url) finish(url);
387
421
  };
388
- } catch (error) {
389
- return {
390
- status: "failed",
391
- reason: String(error?.message || error)
422
+ const outReader = createLineReader(onLine);
423
+ const errReader = createLineReader(onLine);
424
+ onOut = (chunk) => outReader.push(chunk);
425
+ onErr = (chunk) => errReader.push(chunk);
426
+ onError = () => finish(null);
427
+ onExit = () => {
428
+ outReader.flush();
429
+ errReader.flush();
430
+ finish(null, true);
392
431
  };
393
- }
432
+ child.stdout?.setEncoding?.("utf-8");
433
+ child.stderr?.setEncoding?.("utf-8");
434
+ child.stdout?.on("data", onOut);
435
+ child.stderr?.on("data", onErr);
436
+ child.on("error", onError);
437
+ child.on("exit", onExit);
438
+ timer = setTimeout(() => finish(null), timeoutMs);
439
+ });
394
440
  }
395
- function eraseParkedAvd(avdName) {
396
- try {
397
- return eraseParkedAfter("android", avdName, (_record, eraseStarted) => {
398
- if (resolveOwnedAvdSerial(avdName).serial) throw new Error(`AVD ${avdName} is running; it was kept`);
399
- const result = teardownOwnedAvd(avdName, {
400
- wipe: true,
401
- owner: { pool: true },
402
- onWipeStart: eraseStarted
441
+ function waitForFileUrl(child, logFile, parseLine, timeoutMs) {
442
+ return new Promise((resolve) => {
443
+ let settled = false;
444
+ const finish = (url, exited = false) => {
445
+ if (settled) return;
446
+ settled = true;
447
+ clearInterval(interval);
448
+ clearTimeout(timer);
449
+ child.removeListener("error", onError);
450
+ child.removeListener("exit", onExit);
451
+ resolve({
452
+ url,
453
+ exited
403
454
  });
404
- if (result.status !== "torn-down") throw new Error(result.reason ?? `AVD ${avdName} is ${result.status}`);
405
- }) ? {
406
- status: "torn-down",
407
- label: avdName
408
- } : {
409
- status: "skipped",
410
- kind: "not-parked",
411
- reason: "emulator is no longer parked"
412
455
  };
413
- } catch (error) {
414
- return {
415
- status: "failed",
416
- reason: String(error?.message || error)
456
+ const probe = () => {
457
+ try {
458
+ for (const line of readFileSync(logFile, "utf-8").split(/\r?\n/)) {
459
+ const url = parseLine(line);
460
+ if (url) return finish(url);
461
+ }
462
+ } catch {}
417
463
  };
418
- }
419
- }
420
- const IOS_SHUTDOWN_SETTLE_MS = 15e3;
421
- const ADB_RELEASE_WAIT_MS = 3e3;
422
- function repoRootAbove(workspace) {
423
- if (workspace === void 0 || !getExecutor().findExecutable("agent-device")) return void 0;
424
- const root = repoRoot(workspace);
425
- try {
426
- return root && realpathSync(root) !== realpathSync(workspace) ? root : void 0;
427
- } catch {
428
- return;
429
- }
464
+ const onError = () => finish(null);
465
+ const onExit = () => finish(null, true);
466
+ child.on("error", onError);
467
+ child.on("exit", onExit);
468
+ const interval = setInterval(probe, 25);
469
+ const timer = setTimeout(() => finish(null), timeoutMs);
470
+ probe();
471
+ });
430
472
  }
431
- const IOS_SHUTDOWN_POLL_MS = 250;
432
- const IOS_SHUTDOWN_ATTEMPTS = 2;
433
- function settleIosSimShutdown(udid, { now = Date.now, sleep = sleepSync }) {
434
- let resolved = resolveOwnedIosSim(udid);
435
- for (let attempt = 1;; attempt++) {
436
- const deadline = now() + IOS_SHUTDOWN_SETTLE_MS;
437
- while (resolved.sim && resolved.sim.state !== "Shutdown" && now() < deadline) {
438
- sleep(IOS_SHUTDOWN_POLL_MS);
439
- resolved = resolveOwnedIosSim(udid);
440
- }
441
- if (!resolved.sim || resolved.sim.state === "Shutdown" || attempt === IOS_SHUTDOWN_ATTEMPTS) return resolved;
442
- shutdownIosSim(udid);
443
- resolved = resolveOwnedIosSim(udid);
444
- }
473
+ function createTunnelLogFile(provider) {
474
+ const dir = join(getConfigDir(), "tunnel-logs");
475
+ mkdirSync(dir, { recursive: true });
476
+ const path = join(dir, `${provider}-${randomUUID()}.log`);
477
+ closeSync(openSync(path, "wx", 384));
478
+ return path;
445
479
  }
446
- function iosShutdownFailureReason(udid, state) {
447
- return `simulator ${udid} is still ${state} after ${IOS_SHUTDOWN_ATTEMPTS} shutdown attempts and ${IOS_SHUTDOWN_ATTEMPTS * IOS_SHUTDOWN_SETTLE_MS / 1e3}s of waiting`;
480
+ function removeTunnelLogFile(path) {
481
+ if (!path) return;
482
+ try {
483
+ unlinkSync(path);
484
+ } catch {}
448
485
  }
449
- function parkOwnedIosSim(settled, udid, park) {
450
- if (settled.missing) throw new Error(`simulator ${udid} disappeared after shutdown`);
451
- if (settled.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(settled.notOwned)}`);
452
- const sim = settled.sim;
453
- if (sim.state !== "Shutdown") throw new Error(iosShutdownFailureReason(udid, sim.state));
454
- const model = listIosDeviceTypes().find((d) => d.identifier === sim.deviceTypeIdentifier)?.name ?? null;
455
- const runtime = parseRuntimeVersion(sim.runtime);
456
- const name = parkedSimName(sim.udid, {
457
- model,
458
- runtime
459
- });
460
- renameIosSim(sim.udid, name);
461
- const record = {
462
- udid: sim.udid,
463
- name,
464
- deviceTypeIdentifier: sim.deviceTypeIdentifier,
465
- runtimeIdentifier: sim.runtime,
466
- parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
467
- simslimManaged: Boolean(park.simslimManaged),
468
- ...park.appId ? { bundleId: park.appId } : {},
469
- ...park.cacheKey ? { cacheKey: park.cacheKey } : {}
470
- };
471
- return {
472
- record,
473
- evicted: parkSim({
474
- platform: "ios",
475
- projectPath: park.projectPath,
476
- slot: park.slot,
477
- record,
478
- max: park.max
479
- })
480
- };
486
+ function removeRecordedTunnelLogFile(path) {
487
+ if (!path) return;
488
+ const dir = resolve(join(getConfigDir(), "tunnel-logs"));
489
+ const resolved = resolve(path);
490
+ if (!resolved.startsWith(`${dir}${sep}`) || resolve(join(dir, basename(resolved))) !== resolved) return;
491
+ removeTunnelLogFile(resolved);
481
492
  }
482
- function teardownOwnedIosSim(udid, { del = false, label, park, workspace, shutdownClock = {} } = {}) {
483
- let parkFallback;
493
+ async function waitUntilReachable({ url, now, sleep, probe, timeoutMs }) {
494
+ const deadline = now() + timeoutMs;
495
+ for (;;) {
496
+ if (await probe(url, new AbortController().signal).catch(() => false)) return true;
497
+ if (now() >= deadline) return false;
498
+ await sleep(REACHABLE_POLL_MS);
499
+ }
500
+ }
501
+ const CLEANUP_TIMEOUT_MS = 1e3;
502
+ const CLEANUP_POLL_MS = 25;
503
+ async function startTunnel({ provider, port, spawnFn = null, urlTimeoutMs = URL_TIMEOUT_MS, reachableTimeoutMs = REACHABLE_TIMEOUT_MS, probeReachable = defaultProbeReachable, now = Date.now, sleep = defaultSleep, ngrokUrl = null, requireReachable = true, cleanupTimeoutMs = CLEANUP_TIMEOUT_MS, isChildAlive = pidExists, readProcessToken = process_identity_exports.captureProcessToken, logFile = null }) {
504
+ const spawn = spawnFn || ((cmd, args, opts) => getExecutor().spawn(cmd, args, opts));
505
+ const outputFile = provider === "tailscale" ? null : logFile || (spawnFn ? null : createTunnelLogFile(provider));
506
+ let child;
484
507
  try {
485
- const resolved = resolveOwnedIosSim(udid);
486
- if (resolved.notOwned) return {
487
- status: "skipped",
488
- kind: "not-owned",
489
- reason: `sim "${resolved.notOwned}" is not Stim-owned: Stim has no record of creating it`
490
- };
491
- if (resolved.missing) return { status: "missing" };
492
- closeOwnedDeviceSessions({
493
- platform: "ios",
494
- id: udid
495
- }, () => Boolean(resolveOwnedIosSim(udid).sim), workspace);
496
- const current = resolveOwnedIosSim(udid);
497
- if (current.missing) return { status: "missing" };
498
- if (current.notOwned) return {
499
- status: "skipped",
500
- kind: "not-owned",
501
- reason: "simulator ownership changed before shutdown"
502
- };
503
- shutdownIosSim(udid);
504
- const sim = resolved.sim;
505
- if (del && park && park.max > 0) try {
506
- const { record, evicted } = parkOwnedIosSim(settleIosSimShutdown(udid, shutdownClock), udid, park);
507
- const removed = [];
508
- const failures = [];
509
- for (const entry of evicted) {
510
- const result = teardownParkedIosSim(entry.udid, { label: entry.name });
511
- if (result.status === "torn-down") removed.push({
512
- udid: entry.udid,
513
- name: entry.name
514
- });
515
- else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name} (${entry.udid}): ${result.reason}`);
516
- }
517
- return {
518
- status: "torn-down",
519
- label: label ?? sim.name ?? udid,
520
- parked: {
521
- udid: record.udid,
522
- name: record.name
523
- },
524
- evicted: removed,
525
- ...failures.length ? { evictionFailures: failures } : {}
526
- };
527
- } catch (e) {
528
- parkFallback = String(e?.message || e);
529
- }
530
- if (del) {
531
- deleteIosSim(udid);
532
- return {
533
- status: "torn-down",
534
- label: label ?? sim.name ?? udid,
535
- ...parkFallback ? { parkFallback } : {}
536
- };
537
- }
538
- const settled = settleIosSimShutdown(udid, shutdownClock);
539
- if (settled.sim && settled.sim.state !== "Shutdown") return {
540
- status: "failed",
541
- label: label ?? sim.name ?? udid,
542
- reason: iosShutdownFailureReason(udid, settled.sim.state)
543
- };
544
- const root = sim.state === "Shutdown" ? void 0 : repoRootAbove(workspace);
545
- if (root && settled.sim) closeOwnedDeviceSessions({
546
- platform: "ios",
547
- id: udid
548
- }, () => resolveOwnedIosSim(udid).sim?.state === "Shutdown", workspace, root);
508
+ const binary = provider === "tailscale" ? resolveTailscaleBinary((bin) => getExecutor().findExecutable(bin)) : provider;
509
+ if (!binary) throw new Error("Install Tailscale and sign in to your tailnet.");
510
+ const { bin, args } = tunnelArgv(provider, port, ngrokUrl, outputFile, binary);
511
+ child = spawn(bin, args, {
512
+ stdio: outputFile ? [
513
+ "ignore",
514
+ "ignore",
515
+ "ignore"
516
+ ] : [
517
+ "ignore",
518
+ "pipe",
519
+ "pipe"
520
+ ],
521
+ detached: true
522
+ });
523
+ } catch (err) {
524
+ removeTunnelLogFile(outputFile);
549
525
  return {
550
- status: "torn-down",
551
- label: label ?? sim.name ?? udid
526
+ failed: true,
527
+ reason: `Could not start ${provider}: ${describe(err)}`
552
528
  };
553
- } catch (e) {
554
- return {
529
+ }
530
+ let childExited = false;
531
+ child.once("exit", () => {
532
+ childExited = true;
533
+ });
534
+ const cleanup = async (alreadyExited = childExited) => {
535
+ const stopped = await terminateChild(child, {
536
+ alreadyExited,
537
+ timeoutMs: cleanupTimeoutMs,
538
+ now,
539
+ sleep,
540
+ isAlive: isChildAlive
541
+ });
542
+ if (stopped) removeTunnelLogFile(outputFile);
543
+ return stopped ? { status: "stopped" } : {
555
544
  status: "failed",
556
- reason: String(e?.message || e),
557
- ...parkFallback ? { parkFallback } : {}
545
+ reason: `Sent SIGKILL but could not confirm that pid ${child.pid ?? "unknown"} exited.`
558
546
  };
559
- }
560
- }
561
- function assertAvdReferences(avdName, owner) {
562
- const config = loadConfig$1();
563
- if (!config) throw new Error("Cannot verify AVD references without a Stim config; the data was kept.");
564
- if (owner && "projectPath" in owner && owner.expectedRecord && !isDeepStrictEqual(deviceSlotPlatforms(config.projects[owner.projectPath], owner.slot)?.android, owner.expectedRecord)) throw new Error(`AVD ${avdName} has a changed workspace record; its data was kept.`);
565
- for (const [projectPath, project] of Object.entries(config.projects)) for (const { slot, platforms } of projectDeviceSlots(project)) {
566
- if (platforms.android?.avdName?.toLowerCase() !== avdName.toLowerCase()) continue;
567
- if (!owner || !("projectPath" in owner) || owner.projectPath !== projectPath || (owner.slot ?? "default") !== slot || !platforms.android.owned) throw new Error(`AVD ${avdName} is referenced by ${projectPath} (${slot}); its data was kept.`);
568
- }
569
- if (readParked("android", { config }).some((entry) => entry.name.toLowerCase() === avdName.toLowerCase()) && !(owner && "pool" in owner)) throw new Error(`AVD ${avdName} is in the emulator pool; its data was kept.`);
570
- }
571
- function removeOrphanedAvdDirectory(candidate, owner) {
572
- const surveyed = listOrphanedAvdDirectories(candidate.name).find((entry) => entry.directory === candidate.directory);
573
- if (!surveyed || surveyed.name !== candidate.name || surveyed.dev !== candidate.dev || surveyed.ino !== candidate.ino) throw new Error(`AVD data at ${candidate.directory} changed or is registered; it was kept.`);
574
- assertOwnedAvdStopped(candidate.name, { resolveDirectory: () => candidate.directory });
575
- const detached = withConfigLock$1(() => {
576
- const current = lstatSync(candidate.directory);
577
- if (!current.isDirectory() || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`AVD data at ${candidate.directory} changed or is registered; it was kept.`);
578
- assertAvdReferences(candidate.name, owner);
579
- const name = `stim-gc-${randomUUID()}`;
580
- recordCreatedDevice("android", name);
581
- const destination = join(dirname(candidate.directory), `${name}.avd`);
547
+ };
548
+ const failAfterCleanup = async (reason, alreadyExited = childExited) => {
549
+ const stopped = await cleanup(alreadyExited);
550
+ return stopped.status === "stopped" ? {
551
+ failed: true,
552
+ reason
553
+ } : {
554
+ failed: true,
555
+ reason: `${reason} ${stopped.reason}`,
556
+ cleanupFailed: true
557
+ };
558
+ };
559
+ const pid = child.pid;
560
+ if (!pid) return failAfterCleanup(`${provider} started but reported no pid.`);
561
+ const captureChildIdentity = () => {
582
562
  try {
583
- renameSync(candidate.directory, destination);
584
- } catch (error) {
585
- forgetCreatedDevice("android", name);
586
- throw error;
563
+ return readProcessToken(pid);
564
+ } catch {
565
+ return null;
587
566
  }
588
- forgetCreatedDevice("android", candidate.name);
589
- return destination;
590
- });
591
- const current = lstatSync(detached);
592
- if (!current.isDirectory() || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`Detached AVD data changed at ${detached}; it was kept.`);
593
- try {
594
- rmSync(detached, { recursive: true });
595
- } catch (error) {
596
- throw new Error(`Could not remove AVD data at ${detached}; retry gc --delete: ${error.message}`, { cause: error });
597
- }
598
- forgetCreatedDevice("android", basename(detached, ".avd"));
599
- }
600
- function teardownUnregisteredAvd(avdName, del, owner, surveyed) {
601
- const candidates = surveyed ? [surveyed] : listOrphanedAvdDirectories(avdName);
602
- if (candidates.length === 0) {
603
- if (avdStorageRoots().some((root) => avdPathExists(join(root, `${avdName}.ini`)) || avdPathExists(join(root, `${avdName}.avd`)))) throw new Error(`AVD ${avdName} is not listed but its storage remains; it was kept.`);
604
- return { status: "missing" };
605
- }
606
- if (!del) return {
607
- status: "skipped",
608
- reason: "AVD registration is missing; its data was kept."
609
567
  };
610
- for (const candidate of candidates) {
611
- if (candidate.name !== avdName) throw new Error(`AVD data does not match ${avdName}; it was kept.`);
612
- removeOrphanedAvdDirectory(candidate, owner);
568
+ const initialProcessToken = captureChildIdentity();
569
+ if (!initialProcessToken) return failAfterCleanup(`${provider} started but its process identity token could not be read.`);
570
+ const { url, exited, lastLine } = outputFile ? await waitForFileUrl(child, outputFile, parserFor(provider), urlTimeoutMs) : await waitForUrl(child, parserFor(provider), urlTimeoutMs);
571
+ if (!url) return failAfterCleanup(exited ? provider === "tailscale" && lastLine ? `${provider} exited before printing a tunnel URL: ${lastLine}` : `${provider} exited before printing a tunnel URL.` : `${provider} did not print a tunnel URL within ${urlTimeoutMs}ms.`, exited || childExited);
572
+ if (!outputFile) resumeChildPipes(child);
573
+ if (requireReachable) {
574
+ if (!await waitUntilReachable({
575
+ url,
576
+ now,
577
+ sleep,
578
+ probe: probeReachable,
579
+ timeoutMs: reachableTimeoutMs
580
+ })) return failAfterCleanup(`${url} did not become reachable within ${reachableTimeoutMs}ms; ${provider} may still be registering, or the network path is blocked.`);
613
581
  }
582
+ const finalProcessToken = captureChildIdentity();
583
+ if (!finalProcessToken || finalProcessToken !== initialProcessToken) return failAfterCleanup(`${provider} process identity changed before its tunnel could be recorded.`);
584
+ if (!outputFile) unrefChildPipes(child);
585
+ child.unref?.();
586
+ if (childExited) return failAfterCleanup(`${provider} exited before its tunnel could be recorded.`, true);
614
587
  return {
615
- status: "torn-down",
616
- label: avdName
588
+ url,
589
+ pid,
590
+ processToken: initialProcessToken,
591
+ logFile: outputFile,
592
+ cleanup: () => cleanup()
617
593
  };
618
594
  }
619
- function teardownOwnedAvd(avdName, options = {}) {
620
- let claim;
621
- try {
622
- if (!isStimOwnedAvd(avdName)) return {
623
- status: "skipped",
624
- kind: "not-owned",
625
- reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
595
+ async function startTunnelSequence({ providers, port, ngrokUrl = null, requireReachable = true, start = startTunnel }) {
596
+ const failures = [];
597
+ for (const provider of providers) {
598
+ const result = await start({
599
+ provider,
600
+ port,
601
+ ngrokUrl: provider === "ngrok" ? ngrokUrl : null,
602
+ requireReachable
603
+ });
604
+ if (!("failed" in result)) return {
605
+ provider,
606
+ ...result
626
607
  };
627
- claim = acquireAvdClaim(avdName);
628
- const result = teardownClaimedAvd(avdName, claim, options);
629
- if (result.status === "torn-down" || result.status === "missing") options.onRemoved?.();
630
- return result;
631
- } catch (error) {
632
- return {
633
- status: "failed",
634
- reason: String(error?.message || error)
608
+ failures.push(`${provider}: ${result.reason}`);
609
+ if (result.cleanupFailed) return {
610
+ failed: true,
611
+ reason: failures.join(" ")
635
612
  };
636
- } finally {
637
- (0, ownership_claim_exports.releaseClaim)(claim);
638
613
  }
614
+ return {
615
+ failed: true,
616
+ reason: failures.join(" ") || "No managed tunnel provider was selected."
617
+ };
639
618
  }
640
- function serialFree(serial) {
641
- const adb = listAdbDevices();
642
- return ![
643
- ...adb.emulators,
644
- ...adb.physical,
645
- ...adb.unhealthy
646
- ].some((entry) => entry.serial === serial);
619
+ function closeChildPipes(child) {
620
+ child.stdout?.destroy?.();
621
+ child.stderr?.destroy?.();
647
622
  }
648
- function teardownClaimedAvd(avdName, claim, { del = false, wipe = false, park, owner, workspace, orphanedDirectory, onlyIfMissing = false, onWipeStart, waitForShutdown = waitForAndroidEmulatorShutdown, assertStopped = assertOwnedAvdStopped, resolveAvd = resolveOwnedAvdSerial }) {
649
- let parkFallback;
623
+ function unrefChildPipes(child) {
624
+ resumeChildPipes(child);
625
+ for (const stream of [child.stdout, child.stderr]) stream?.unref?.();
626
+ }
627
+ function resumeChildPipes(child) {
628
+ child.stdout?.resume?.();
629
+ child.stderr?.resume?.();
630
+ }
631
+ async function signalAndWaitForExit(child, signal, { timeoutMs, now, sleep, isAlive, platform }) {
632
+ let exited = false;
633
+ const onExit = () => {
634
+ exited = true;
635
+ };
636
+ child.once("exit", onExit);
650
637
  try {
651
- if (del || wipe) withConfigLock$1(() => assertAvdReferences(avdName, owner));
652
- const resolved = resolveAvd(avdName);
653
- if (resolved.notOwned) return {
654
- status: "skipped",
655
- kind: "not-owned",
656
- reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
657
- };
658
- if (resolved.missing) {
659
- if (wipe) throw new Error(`AVD ${avdName} is not registered; its data was kept.`);
660
- return teardownUnregisteredAvd(avdName, del, owner, orphanedDirectory);
661
- }
662
- if (onlyIfMissing) return {
663
- status: "skipped",
664
- reason: "AVD registration appeared; its record was kept."
665
- };
666
- if (orphanedDirectory) return {
667
- status: "skipped",
668
- reason: "AVD registration appeared; its data was kept."
669
- };
670
- const serial = resolved.serial;
671
- const stillStopped = (sessionSerial) => {
672
- const current = resolveAvd(avdName);
673
- return !current.notOwned && !current.missing && !current.serial && serialFree(sessionSerial);
674
- };
675
- if (serial) {
676
- const stillOwned = () => {
677
- const current = resolveAvd(avdName);
678
- return !current.notOwned && !current.missing && current.serial === serial;
679
- };
680
- closeOwnedDeviceSessions({
681
- platform: "android",
682
- id: serial,
683
- avdName
684
- }, stillOwned, workspace);
685
- if (!stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
686
- waitForShutdown(avdName, (timeoutMs) => {
687
- (0, ownership_claim_exports.markClaimChildPending)(claim);
688
- try {
689
- shutdownAndroidEmulator(serial, timeoutMs);
690
- } finally {
691
- (0, ownership_claim_exports.clearClaimChild)(claim);
692
- }
693
- });
694
- const root = repoRootAbove(workspace);
695
- if (root) {
696
- const deadline = Date.now() + ADB_RELEASE_WAIT_MS;
697
- while (!serialFree(serial) && Date.now() < deadline) sleepSync(250);
698
- closeOwnedDeviceSessions({
699
- platform: "android",
700
- id: serial,
701
- avdName
702
- }, stillStopped, workspace, root);
703
- }
704
- } else {
705
- closeOwnedDeviceSessions({
706
- platform: "android",
707
- id: null,
708
- avdName
709
- }, stillStopped, workspace);
710
- waitForShutdown(avdName, null);
711
- }
712
- if (wipe && !del) {
713
- const current = resolveAvd(avdName);
714
- if (current.notOwned || current.missing) throw new Error(`AVD ${avdName} changed before its data was wiped.`);
715
- if (current.serial) throw new Error(`Owned AVD ${avdName} started again before its data was wiped.`);
716
- assertStopped(avdName);
717
- withConfigLock$1(() => assertAvdReferences(avdName, owner));
718
- const directory = ownedAvdDirectory(avdName);
719
- if (!directory) throw new Error(`The data directory of AVD ${avdName} could not be resolved.`);
720
- onWipeStart?.();
721
- wipeAvdUserData(directory);
722
- }
723
- if (del) {
724
- const current = resolveAvd(avdName);
725
- if (current.notOwned) return {
726
- status: "skipped",
727
- kind: "not-owned",
728
- reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
729
- };
730
- if (current.missing) return teardownUnregisteredAvd(avdName, del, owner);
731
- if (current.serial) throw new Error(`Owned AVD ${avdName} started again before deletion.`);
732
- assertStopped(avdName);
733
- withConfigLock$1(() => assertAvdReferences(avdName, owner));
734
- if (park && park.max > 0) try {
735
- const systemImage = ownedAvdSystemImage(avdName);
736
- if (!systemImage || !park.configuration || !ownedAvdMatchesConfiguration(avdName, park.configuration)) throw new Error("the AVD has no verified creation configuration");
737
- const evicted = parkSim({
738
- platform: "android",
739
- projectPath: park.projectPath,
740
- slot: park.slot,
741
- max: park.max,
742
- record: {
743
- udid: avdName,
744
- name: avdName,
745
- systemImage,
746
- configuration: park.configuration,
747
- parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
748
- ...park.appId ? { packageName: park.appId } : {},
749
- ...park.cacheKey ? { cacheKey: park.cacheKey } : {}
750
- }
751
- });
752
- const removed = [];
753
- const failures = [];
754
- for (const entry of evicted) {
755
- const result = teardownParkedAvd(entry.name);
756
- if (result.status === "torn-down") removed.push({
757
- udid: entry.name,
758
- name: entry.name,
759
- platform: "android"
760
- });
761
- else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name}: ${result.reason}`);
762
- }
763
- return {
764
- status: "torn-down",
765
- label: avdName,
766
- parked: {
767
- udid: avdName,
768
- name: avdName,
769
- platform: "android"
770
- },
771
- evicted: removed,
772
- ...failures.length ? { evictionFailures: failures } : {}
773
- };
774
- } catch (error) {
775
- parkFallback = String(error?.message || error);
776
- }
777
- const directory = ownedAvdDirectory(avdName);
778
- const paths = avdStorageRoots().map((root) => join(root, `${avdName}.ini`));
779
- if (directory) paths.push(directory);
780
- (0, ownership_claim_exports.markClaimChildPending)(claim);
781
- try {
782
- deleteAvd(avdName);
783
- } finally {
784
- (0, ownership_claim_exports.clearClaimChild)(claim);
785
- }
786
- const remaining = paths.filter(avdPathExists);
787
- if (remaining.length) throw new Error(`AVD ${avdName} was only partially deleted; data remains at ${remaining.join(", ")}.`);
788
- }
789
- return {
790
- status: "torn-down",
791
- label: avdName,
792
- serial: resolved.serial ?? null,
793
- ...parkFallback ? { parkFallback } : {}
794
- };
795
- } catch (e) {
796
- return {
797
- status: "failed",
798
- reason: String(e?.message || e)
799
- };
638
+ if (platform === "win32" && child.pid) signalProcessTree(child.pid, signal, { platform });
639
+ child.kill(signal);
640
+ } catch (err) {
641
+ child.removeListener("exit", onExit);
642
+ return isEsrch(err);
800
643
  }
644
+ const hasExited = () => exited;
645
+ const deadline = now() + timeoutMs;
646
+ while (!hasExited() && now() < deadline) await sleep(CLEANUP_POLL_MS);
647
+ if (!hasExited() && child.pid && !isAlive(child.pid)) exited = true;
648
+ child.removeListener("exit", onExit);
649
+ return hasExited();
801
650
  }
802
- const BROWSER_EXIT_WAIT_MS = 15e3;
803
- const CHROME_KILL_WAIT_MS = 5e3;
804
- async function stopOwnedChrome(record) {
805
- for (const signal of ["SIGTERM", "SIGKILL"]) {
806
- const state = chromeProcessState(record);
807
- if (state === "gone") return true;
808
- if (state === "unknown") return false;
809
- try {
810
- signalProcessTree(record.pid, signal, { group: true });
811
- } catch {}
812
- const deadline = Date.now() + CHROME_KILL_WAIT_MS;
813
- while (chromeProcessState(record) !== "gone" && Date.now() < deadline) await sleepAsync(50);
651
+ async function terminateChild(child, { alreadyExited, timeoutMs, now, sleep, isAlive, platform = process.platform }) {
652
+ if (alreadyExited) {
653
+ closeChildPipes(child);
654
+ return true;
655
+ }
656
+ if (await signalAndWaitForExit(child, "SIGTERM", {
657
+ timeoutMs,
658
+ now,
659
+ sleep,
660
+ isAlive,
661
+ platform
662
+ })) {
663
+ closeChildPipes(child);
664
+ return true;
814
665
  }
815
- return chromeProcessState(record) === "gone";
666
+ const killed = await signalAndWaitForExit(child, "SIGKILL", {
667
+ timeoutMs,
668
+ now,
669
+ sleep,
670
+ isAlive,
671
+ platform
672
+ });
673
+ closeChildPipes(child);
674
+ return killed;
816
675
  }
817
- const sleepAsync = (ms) => new Promise((done) => setTimeout(done, ms));
818
- /**
819
- * Closes the workspace's Stim-owned Chrome and its supervisor, after verifying both process identities. With
820
- * `deleteProfile`, it also deletes the profile directory, only when the created-devices ledger lists it.
821
- */
822
- async function teardownOwnedBrowser(root, options = {}) {
676
+ function describe(err) {
677
+ return err?.message || String(err);
678
+ }
679
+ const STOP_TIMEOUT_MS = 5e3;
680
+ const STOP_POLL_MS = 100;
681
+ function isEsrch(err) {
682
+ return err?.code === "ESRCH";
683
+ }
684
+ async function stopTunnel(record, { isAlive = pidExists, inspectIdentity = process_identity_exports.inspectProcessIdentity, platform = process.platform, kill = (pid) => signalProcessTree(pid, "SIGTERM", { platform }), now = Date.now, sleep = defaultSleep, timeoutMs = STOP_TIMEOUT_MS } = {}) {
685
+ const missing = () => {
686
+ removeRecordedTunnelLogFile(record?.logFile);
687
+ return { status: "missing" };
688
+ };
689
+ const pid = record?.pid;
690
+ if (!pid || !isAlive(pid)) return missing();
691
+ if (!record.processToken) return {
692
+ status: "failed",
693
+ reason: `tunnel pid ${pid} has no process identity token, so Stim cannot verify ownership. Keep the record. Inspect the process, stop it with the provider tooling, remove the stale metroTunnel state, and retry.`
694
+ };
695
+ const identity = inspectIdentity(record);
696
+ if (identity === "gone" || identity === "different") return missing();
697
+ if (identity !== "same") return {
698
+ status: "failed",
699
+ reason: `could not verify the process identity for tunnel pid ${pid}; refusing to signal it.`
700
+ };
823
701
  try {
824
- return await withWorkspaceProcessLock(workspaceDir(root), "web", () => teardownBrowserHeld(root, options), {
825
- external: true,
826
- ownerPurpose: "browser teardown"
827
- });
828
- } catch (error) {
829
- return {
702
+ kill(pid);
703
+ } catch (err) {
704
+ return isEsrch(err) ? missing() : {
830
705
  status: "failed",
831
- reason: String(error?.message || error)
706
+ reason: describe(err)
832
707
  };
833
708
  }
709
+ const deadline = now() + timeoutMs;
710
+ const exited = () => {
711
+ if (!isAlive(pid)) return true;
712
+ const current = inspectIdentity(record);
713
+ return current === "gone" || current === "different";
714
+ };
715
+ while (now() < deadline && !exited()) await sleep(STOP_POLL_MS);
716
+ if (!exited()) return {
717
+ status: "failed",
718
+ reason: `pid ${pid} did not exit within ${timeoutMs}ms.`
719
+ };
720
+ removeRecordedTunnelLogFile(record.logFile);
721
+ return { status: "stopped" };
834
722
  }
835
- /** {@link teardownOwnedBrowser} for a caller that already holds the {@link BROWSER_LOCK} lock. */
836
- async function teardownBrowserHeld(root, { deleteProfile = false } = {}) {
723
+ //#endregion
724
+ //#region src/commands/gc/paths.ts
725
+ function canonicalPath(p) {
837
726
  try {
838
- const record = readWebRecord(root);
839
- const profile = record?.profile ?? webProfileDir(root);
840
- if (record) {
841
- const supervisor = (0, process_identity_exports.inspectProcessIdentity)(record);
842
- const chrome = record.chromeProcess ? (0, process_identity_exports.inspectProcessIdentity)(record.chromeProcess) : "gone";
843
- if (supervisor === "unknown" || chrome === "unknown") return {
844
- status: "skipped",
845
- kind: "not-verified",
846
- reason: `the browser supervisor (pid ${record.pid}) or its Chrome could not be verified; its record was kept`
847
- };
848
- if (supervisor === "same") {
849
- try {
850
- process.kill(record.pid, "SIGTERM");
851
- } catch {}
852
- if (!await (0, process_identity_exports.waitForProcessExit)(record, BROWSER_EXIT_WAIT_MS)) return {
853
- status: "failed",
854
- reason: `browser supervisor pid ${record.pid} did not exit`
855
- };
856
- }
857
- if (record.chromeProcess && !await stopOwnedChrome(record.chromeProcess)) return {
858
- status: "failed",
859
- reason: `Chrome pid ${record.chromeProcess.pid} did not exit`
860
- };
861
- const holder = liveProfileHolder(profile);
862
- if (holder !== null) return {
863
- status: "skipped",
864
- kind: "not-verified",
865
- reason: `pid ${holder} still holds ${profile}, and the browser record does not name it; it was left running`
866
- };
867
- const claims = (0, ownership_claim_exports.clearFreeClaimSet)({
868
- root: webClaimRoot(root),
869
- label: "browser supervisor"
870
- });
871
- if (claims.status !== "cleared") return {
872
- status: "skipped",
873
- kind: "not-verified",
874
- reason: claims.status === "held" ? `the browser supervisor claim is still held${claims.holder ? ` by pid ${claims.holder.owner.pid}` : ""}` : `the browser supervisor claim could not be cleared (${claims.reason}); remove it with ${(0, ownership_claim_exports.claimRemoveCommand)(webClaimRoot(root))}`
875
- };
876
- clearWebRecord(root, record);
877
- await releaseBrowserPort(root, record.cdpPort);
878
- } else {
879
- const holder = liveProfileHolder(profile);
880
- if (holder !== null) return {
881
- status: "skipped",
882
- kind: "not-verified",
883
- reason: `pid ${holder} holds ${profile} but no browser supervisor is recorded; it was left running`
884
- };
885
- const port = getProject(root)?.ports?.[CDP_PORT_LABEL];
886
- if (typeof port === "number") await releaseBrowserPort(root, port);
887
- }
888
- if (existsSync(profile)) removeSingletonFiles(profile);
889
- if (deleteProfile && existsSync(profile)) {
890
- if (!readCreatedDevices$1().web.has(profile)) return {
891
- status: "skipped",
892
- kind: "not-owned",
893
- reason: `${profile} is not in Stim's ledger; it was kept`
894
- };
895
- rmSync(profile, {
896
- recursive: true,
897
- force: true
898
- });
899
- forgetCreatedDevice("web", profile);
900
- }
901
- return record ? {
902
- status: "torn-down",
903
- label: `Chrome (${profile})`
904
- } : { status: "missing" };
905
- } catch (error) {
906
- return {
907
- status: "failed",
908
- reason: String(error?.message || error)
909
- };
727
+ return realpathSync$1(p);
728
+ } catch {
729
+ return resolve$1(p);
910
730
  }
911
731
  }
912
732
  //#endregion
913
- //#region src/engine/public-http-probe.ts
914
- const DNS_TIMEOUT_MS = 5e3;
915
- async function resolveAddresses(hostname, signal) {
916
- if (signal.aborted) throw signal.reason;
917
- let timer;
918
- let onAbort;
919
- const unavailable = new Promise((_resolve, reject) => {
920
- onAbort = () => reject(signal.reason);
921
- signal.addEventListener("abort", onAbort, { once: true });
922
- if (signal.aborted) onAbort();
923
- else timer = setTimeout(() => reject(/* @__PURE__ */ new Error("DNS lookup timed out")), DNS_TIMEOUT_MS);
733
+ //#region src/commands/gc/results.ts
734
+ const results = [];
735
+ function recordGcResult(kind, status, label, { id = null, bytes = null, detail = null } = {}) {
736
+ results.push({
737
+ kind,
738
+ status,
739
+ label,
740
+ id,
741
+ bytes,
742
+ detail
924
743
  });
925
- try {
926
- return await Promise.race([resolve4(hostname), unavailable]);
927
- } finally {
928
- clearTimeout(timer);
929
- if (onAbort) signal.removeEventListener("abort", onAbort);
930
- }
931
744
  }
932
- async function probePublicHttp(url, signal) {
745
+ function takeGcResults() {
746
+ return results.splice(0);
747
+ }
748
+ //#endregion
749
+ //#region src/devices/agent-device-cleanup.ts
750
+ function parseAgentDeviceSessions(output) {
751
+ const result = JSON.parse(output);
752
+ if (result?.success !== true || !Array.isArray(result.data?.sessions)) throw new Error("agent-device did not return a successful session list");
753
+ return result.data.sessions.flatMap((entry) => {
754
+ if (!entry || typeof entry !== "object") return [];
755
+ const row = entry;
756
+ const id = row.platform === "ios" ? row.device_udid : row.id;
757
+ if (row.platform !== "ios" && row.platform !== "android" || typeof row.name !== "string" || !row.name.trim() || typeof id !== "string" || !id || typeof row.createdAt !== "number" || !Number.isFinite(row.createdAt) || row.platform === "ios" && row.id !== void 0 && row.id !== id) return [];
758
+ return [{
759
+ name: typeof row.address === "string" && row.address.trim() ? row.address : row.name,
760
+ platform: row.platform,
761
+ id,
762
+ device: typeof row.device === "string" ? row.device : null,
763
+ createdAt: row.createdAt
764
+ }];
765
+ });
766
+ }
767
+ function isOwnDeviceSession(session, device, owner) {
768
+ if (session.platform !== device.platform || device.id !== null && session.id !== device.id) return false;
769
+ if (device.platform === "android" && session.device !== device.avdName.replaceAll("_", " ")) return false;
770
+ if (!owner) return true;
771
+ return owner.claims.some((claim) => claim.deviceId === session.id && claim.session === session.name && claim.workspace !== null && (within(owner.workspace, claim.workspace) || claim.workspace === owner.repoRoot));
772
+ }
773
+ function within(root, path) {
774
+ const rest = relative(root, path);
775
+ return rest === "" || !isAbsolute(rest) && rest.split(sep)[0] !== "..";
776
+ }
777
+ function canonical(path) {
933
778
  try {
934
- return (await fetch(url, {
935
- signal,
936
- redirect: "follow"
937
- })).status;
938
- } catch (error) {
939
- const cause = error?.cause;
940
- if (cause?.code !== "ENOTFOUND" && cause?.code !== "EAI_AGAIN") return null;
779
+ return realpathSync(path);
780
+ } catch {
781
+ return null;
941
782
  }
942
- const hostname = new URL(url).hostname;
943
- if (!hostname.endsWith(".trycloudflare.com")) return null;
944
- let addresses;
783
+ }
784
+ function deviceClaims(device) {
785
+ return readAgentDeviceRecords(homedir()).filter((record) => record.kind === "claim" && (device.id === null || record.deviceId === device.id)).map((record) => ({
786
+ deviceId: record.deviceId,
787
+ session: record.session,
788
+ workspace: record.workspace && canonical(record.workspace)
789
+ }));
790
+ }
791
+ function explainFailure(error) {
792
+ const stdout = error?.stdout;
793
+ let failure;
945
794
  try {
946
- addresses = await resolveAddresses(hostname, signal);
795
+ failure = typeof stdout === "string" ? JSON.parse(stdout)?.error : void 0;
947
796
  } catch {
948
- return null;
797
+ failure = void 0;
949
798
  }
950
- const firstAddress = addresses[0];
951
- if (!firstAddress) return null;
952
- return new Promise((resolve) => {
953
- const request$1 = request(url, {
954
- signal,
955
- lookup: (_hostname, options, callback) => {
956
- if (options.all) callback(null, addresses.map((address) => ({
957
- address,
958
- family: 4
959
- })));
960
- else callback(null, firstAddress, 4);
799
+ if (typeof failure?.message !== "string" || !failure.message) return error;
800
+ const reason = typeof failure.details?.reason === "string" ? ` (${failure.details.reason})` : "";
801
+ return /* @__PURE__ */ new Error(`agent-device refused: ${failure.message}${reason}`);
802
+ }
803
+ function report(message) {
804
+ process.stderr.write(`${phaseLine("device", message)}\n`);
805
+ }
806
+ function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot) {
807
+ const exec = getExecutor();
808
+ try {
809
+ if (!exec.findExecutable("agent-device")) return;
810
+ const root = workspace === void 0 ? void 0 : canonical(workspace);
811
+ if (root === null) return;
812
+ const owner = root === void 0 ? void 0 : {
813
+ workspace: root,
814
+ repoRoot: repoRoot && (canonical(repoRoot) ?? void 0),
815
+ claims: deviceClaims(device)
816
+ };
817
+ const stateDir = root === void 0 ? void 0 : workspaceAgentDeviceDir(root);
818
+ const stateDirs = [void 0, ...stateDir && !["none", "dead"].includes(agentDeviceDaemon(stateDir)) ? [stateDir] : []];
819
+ for (const dir of stateDirs) {
820
+ const deadline = Date.now() + 15e3;
821
+ const run = (args) => {
822
+ const remaining = deadline - Date.now();
823
+ if (remaining <= 0) throw new Error("agent-device cleanup timed out");
824
+ try {
825
+ return exec.runFile("agent-device", [
826
+ ...args,
827
+ ...dir ? ["--state-dir", dir] : [],
828
+ "--json",
829
+ "--daemon-transport",
830
+ "socket"
831
+ ], {
832
+ timeoutMs: Math.min(5e3, remaining),
833
+ killSignal: "SIGKILL"
834
+ });
835
+ } catch (error) {
836
+ throw explainFailure(error);
837
+ }
838
+ };
839
+ const list = () => parseAgentDeviceSessions(run([
840
+ "session",
841
+ "list",
842
+ "--session",
843
+ "stim-teardown-inventory"
844
+ ]));
845
+ try {
846
+ const sessions = list().filter((session) => isOwnDeviceSession(session, device, owner));
847
+ for (const session of sessions) try {
848
+ if (!list().some((current) => current.name === session.name && current.platform === session.platform && current.id === session.id && current.createdAt === session.createdAt)) continue;
849
+ if (!stillOwned(session.id)) continue;
850
+ if (JSON.parse(run([
851
+ "close",
852
+ "--session",
853
+ session.name,
854
+ "--session-lock",
855
+ "reject",
856
+ "--platform",
857
+ device.platform,
858
+ device.platform === "ios" ? "--udid" : "--serial",
859
+ session.id
860
+ ]))?.success !== true) throw new Error("agent-device did not confirm session close");
861
+ report(`closed agent-device session ${session.name} on ${session.id}`);
862
+ } catch (error) {
863
+ report(`could not close agent-device session ${session.name} on ${session.id}: ${error.message}`);
864
+ }
865
+ } catch (error) {
866
+ report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
961
867
  }
962
- }, (response) => {
963
- response.resume();
964
- resolve(response.statusCode ?? null);
965
- });
966
- request$1.on("error", () => resolve(null));
967
- request$1.end();
968
- });
868
+ }
869
+ } catch (error) {
870
+ report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
871
+ }
969
872
  }
970
873
  //#endregion
971
- //#region src/engine/tunnel.ts
972
- function tunnelArgv(provider, port, ngrokUrl, logFile, tailscaleBinary = "tailscale") {
973
- if (provider === "tailscale") return {
974
- bin: tailscaleBinary,
975
- args: [
976
- "serve",
977
- `--https=${port}`,
978
- `http://127.0.0.1:${port}`
979
- ]
980
- };
981
- if (provider === "cloudflared") return {
982
- bin: "cloudflared",
983
- args: [
984
- "tunnel",
985
- "--url",
986
- `http://127.0.0.1:${port}`,
987
- ...logFile ? ["--logfile", logFile] : []
988
- ]
989
- };
990
- return {
991
- bin: "ngrok",
992
- args: [
993
- "http",
994
- String(port),
995
- `--log=${logFile || "stdout"}`,
996
- "--log-format=json",
997
- ...ngrokUrl ? ["--url", ngrokUrl] : []
998
- ]
999
- };
1000
- }
1001
- const CLOUDFLARED_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
1002
- function parseCloudflaredLine(line) {
1003
- const match = line.match(CLOUDFLARED_URL_RE);
1004
- return match ? match[0] : null;
874
+ //#region src/devices/teardown.ts
875
+ function teardownParkedAvd(avdName) {
876
+ try {
877
+ return removeParkedAfter("android", avdName, () => {
878
+ const result = teardownOwnedAvd(avdName, {
879
+ del: true,
880
+ owner: { pool: true }
881
+ });
882
+ if (result.status !== "torn-down" && result.status !== "missing") throw new Error(result.reason);
883
+ }) ? {
884
+ status: "torn-down",
885
+ label: avdName
886
+ } : {
887
+ status: "skipped",
888
+ kind: "not-parked",
889
+ reason: "emulator is no longer parked"
890
+ };
891
+ } catch (error) {
892
+ return {
893
+ status: "failed",
894
+ reason: String(error?.message || error)
895
+ };
896
+ }
1005
897
  }
1006
- function parseNgrokLine(line) {
1007
- let data;
898
+ function teardownParkedIosSim(udid, { label, deleteSim = deleteParkedIosSim } = {}) {
1008
899
  try {
1009
- data = JSON.parse(line);
1010
- } catch {
1011
- return null;
900
+ const removed = removeParkedAfter("ios", udid, () => {
901
+ closeOwnedDeviceSessions({
902
+ platform: "ios",
903
+ id: udid
904
+ }, () => Boolean(resolveOwnedIosSim(udid).sim));
905
+ deleteSim(udid);
906
+ });
907
+ if (!removed) return {
908
+ status: "skipped",
909
+ kind: "not-parked",
910
+ reason: "simulator is no longer parked"
911
+ };
912
+ return {
913
+ status: "torn-down",
914
+ label: label ?? removed.name
915
+ };
916
+ } catch (error) {
917
+ return {
918
+ status: "failed",
919
+ reason: String(error?.message || error)
920
+ };
1012
921
  }
1013
- if (typeof data !== "object" || data === null) return null;
1014
- const url = data.url;
1015
- return typeof url === "string" && /^https?:\/\//i.test(url) ? url : null;
1016
922
  }
1017
- function parseTailscaleLine(line) {
1018
- return /^https:\/\/[^\s/]+\.ts\.net(:\d+)?\/?$/.test(line) ? line : null;
923
+ function eraseParkedIosSim(udid, { label } = {}) {
924
+ try {
925
+ const erased = eraseParkedAfter("ios", udid, (_record, eraseStarted) => {
926
+ closeOwnedDeviceSessions({
927
+ platform: "ios",
928
+ id: udid
929
+ }, () => Boolean(resolveOwnedIosSim(udid).sim));
930
+ const resolved = resolveOwnedIosSim(udid);
931
+ if (resolved.missing) throw new Error(`simulator ${udid} is not on this machine`);
932
+ if (resolved.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(resolved.notOwned)}`);
933
+ const state = resolved.sim.state;
934
+ if (state !== "Shutdown") throw new Error(`simulator ${udid} is ${state}, not Shutdown; it was kept`);
935
+ eraseStarted();
936
+ eraseIosSim(udid);
937
+ });
938
+ if (!erased) return {
939
+ status: "skipped",
940
+ kind: "not-parked",
941
+ reason: "simulator is no longer parked"
942
+ };
943
+ return {
944
+ status: "torn-down",
945
+ label: label ?? erased.name
946
+ };
947
+ } catch (error) {
948
+ return {
949
+ status: "failed",
950
+ reason: String(error?.message || error)
951
+ };
952
+ }
1019
953
  }
1020
- function parserFor(provider) {
1021
- if (provider === "tailscale") return parseTailscaleLine;
1022
- return provider === "cloudflared" ? parseCloudflaredLine : parseNgrokLine;
954
+ function eraseParkedAvd(avdName) {
955
+ try {
956
+ return eraseParkedAfter("android", avdName, (_record, eraseStarted) => {
957
+ if (resolveOwnedAvdSerial(avdName, { timeoutMs: AVD_INVENTORY_TIMEOUT_MS }).serial) throw new Error(`AVD ${avdName} is running; it was kept`);
958
+ const result = teardownOwnedAvd(avdName, {
959
+ wipe: true,
960
+ owner: { pool: true },
961
+ onWipeStart: eraseStarted
962
+ });
963
+ if (result.status !== "torn-down") throw new Error(result.reason ?? `AVD ${avdName} is ${result.status}`);
964
+ }) ? {
965
+ status: "torn-down",
966
+ label: avdName
967
+ } : {
968
+ status: "skipped",
969
+ kind: "not-parked",
970
+ reason: "emulator is no longer parked"
971
+ };
972
+ } catch (error) {
973
+ return {
974
+ status: "failed",
975
+ reason: String(error?.message || error)
976
+ };
977
+ }
1023
978
  }
1024
- const URL_TIMEOUT_MS = 15e3;
1025
- const REACHABLE_TIMEOUT_MS = 24e4;
1026
- const REACHABLE_POLL_MS = 2e3;
1027
- const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
1028
- async function withManagedTunnelLock(root, fn, options = {}) {
1029
- return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
1030
- ...options,
1031
- rejectOwnerPurposes: ["workspace removal"]
1032
- });
979
+ const IOS_SHUTDOWN_SETTLE_MS = 15e3;
980
+ const ADB_RELEASE_WAIT_MS = 3e3;
981
+ const AVD_INVENTORY_TIMEOUT_MS = 15e3;
982
+ function repoRootAbove(workspace) {
983
+ if (workspace === void 0 || !getExecutor().findExecutable("agent-device")) return void 0;
984
+ const root = repoRoot(workspace);
985
+ try {
986
+ return root && realpathSync(root) !== realpathSync(workspace) ? root : void 0;
987
+ } catch {
988
+ return;
989
+ }
1033
990
  }
1034
- async function withManagedTunnelRemovalLock(root, fn, options = {}) {
1035
- return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
1036
- ...options,
1037
- ownerPurpose: "workspace removal"
1038
- });
1039
- }
1040
- async function withManagedRemoteWorktreeLock(worktreeRoot, fn, options = {}) {
1041
- return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
1042
- ...options,
1043
- external: true,
1044
- rejectOwnerPurposes: ["worktree removal"]
1045
- });
1046
- }
1047
- async function withManagedRemoteWorktreeRemovalLock(worktreeRoot, fn, options = {}) {
1048
- return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
1049
- ...options,
1050
- external: true,
1051
- ownerPurpose: "worktree removal"
1052
- });
1053
- }
1054
- function managedLockHolders(root) {
1055
- const locks = [{
1056
- label: "managed tunnel",
1057
- path: workspaceProcessLockPath(managedTunnelLockRoot(root), "metro-tunnel", false)
1058
- }, {
1059
- label: "managed remote",
1060
- path: workspaceProcessLockPath(managedRemoteWorktreeLockRoot(root), "managed-remote", true)
1061
- }];
1062
- const reasons = [];
1063
- for (const { label, path } of locks) {
1064
- const claims = (0, ownership_claim_exports.readClaimSet)(path);
1065
- const holder = claims.live[0];
1066
- if (holder) {
1067
- const purpose = typeof holder.details.purpose === "string" ? ` for ${holder.details.purpose}` : "";
1068
- reasons.push(`the ${label} lock is held${purpose}`);
1069
- } else if (claims.unresolved[0]) reasons.push(`the ${label} lock cannot be resolved: ${claims.unresolved[0].reason}`);
991
+ const IOS_SHUTDOWN_POLL_MS = 250;
992
+ const IOS_SHUTDOWN_ATTEMPTS = 2;
993
+ function settleIosSimShutdown(udid, { now = Date.now, sleep = sleepSync }) {
994
+ let resolved = resolveOwnedIosSim(udid);
995
+ for (let attempt = 1;; attempt++) {
996
+ const deadline = now() + IOS_SHUTDOWN_SETTLE_MS;
997
+ while (resolved.sim && resolved.sim.state !== "Shutdown" && now() < deadline) {
998
+ sleep(IOS_SHUTDOWN_POLL_MS);
999
+ resolved = resolveOwnedIosSim(udid);
1000
+ }
1001
+ if (!resolved.sim || resolved.sim.state === "Shutdown" || attempt === IOS_SHUTDOWN_ATTEMPTS) return resolved;
1002
+ shutdownIosSim(udid);
1003
+ resolved = resolveOwnedIosSim(udid);
1070
1004
  }
1071
- return reasons;
1072
- }
1073
- function managedTunnelLockRoot(root) {
1074
- const key = createHash("sha256").update(resolve(root)).digest("hex");
1075
- return join(getConfigDir(), "process-locks", key);
1076
1005
  }
1077
- function managedRemoteWorktreeLockRoot(worktreeRoot) {
1078
- const key = createHash("sha256").update(resolve(worktreeRoot)).digest("hex");
1079
- return join(getConfigDir(), "process-locks", "worktrees", key);
1006
+ function iosShutdownFailureReason(udid, state) {
1007
+ return `simulator ${udid} is still ${state} after ${IOS_SHUTDOWN_ATTEMPTS} shutdown attempts and ${IOS_SHUTDOWN_ATTEMPTS * IOS_SHUTDOWN_SETTLE_MS / 1e3}s of waiting`;
1080
1008
  }
1081
- async function defaultProbeReachable(url, signal) {
1082
- const status = await probePublicHttp(url, signal);
1083
- return status !== null && status > 0;
1009
+ function parkOwnedIosSim(settled, udid, park) {
1010
+ if (settled.missing) throw new Error(`simulator ${udid} disappeared after shutdown`);
1011
+ if (settled.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(settled.notOwned)}`);
1012
+ const sim = settled.sim;
1013
+ if (sim.state !== "Shutdown") throw new Error(iosShutdownFailureReason(udid, sim.state));
1014
+ const model = listIosDeviceTypes().find((d) => d.identifier === sim.deviceTypeIdentifier)?.name ?? null;
1015
+ const runtime = parseRuntimeVersion(sim.runtime);
1016
+ const name = parkedSimName(sim.udid, {
1017
+ model,
1018
+ runtime
1019
+ });
1020
+ renameIosSim(sim.udid, name);
1021
+ const record = {
1022
+ udid: sim.udid,
1023
+ name,
1024
+ deviceTypeIdentifier: sim.deviceTypeIdentifier,
1025
+ runtimeIdentifier: sim.runtime,
1026
+ parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
1027
+ simslimManaged: Boolean(park.simslimManaged),
1028
+ ...park.appId ? { bundleId: park.appId } : {},
1029
+ ...park.cacheKey ? { cacheKey: park.cacheKey } : {}
1030
+ };
1031
+ return {
1032
+ record,
1033
+ evicted: parkSim({
1034
+ platform: "ios",
1035
+ projectPath: park.projectPath,
1036
+ slot: park.slot,
1037
+ record,
1038
+ max: park.max
1039
+ })
1040
+ };
1084
1041
  }
1085
- function waitForUrl(child, parseLine, timeoutMs) {
1086
- return new Promise((resolve) => {
1087
- let settled = false;
1088
- let lastLine = null;
1089
- let timer;
1090
- let onOut;
1091
- let onErr;
1092
- let onError;
1093
- let onExit;
1094
- const finish = (url, exited = false) => {
1095
- if (settled) return;
1096
- settled = true;
1097
- clearTimeout(timer);
1098
- child.stdout?.removeListener("data", onOut);
1099
- child.stderr?.removeListener("data", onErr);
1100
- child.removeListener("error", onError);
1101
- child.removeListener("exit", onExit);
1102
- resolve({
1103
- url,
1104
- exited,
1105
- lastLine
1106
- });
1042
+ function teardownOwnedIosSim(udid, { del = false, label, park, workspace, shutdownClock = {} } = {}) {
1043
+ let parkFallback;
1044
+ try {
1045
+ const resolved = resolveOwnedIosSim(udid);
1046
+ if (resolved.notOwned) return {
1047
+ status: "skipped",
1048
+ kind: "not-owned",
1049
+ reason: `sim "${resolved.notOwned}" is not Stim-owned: Stim has no record of creating it`
1107
1050
  };
1108
- const onLine = (line) => {
1109
- if (settled) return;
1110
- if (line.trim()) lastLine = line.trim();
1111
- const url = parseLine(line);
1112
- if (url) finish(url);
1051
+ if (resolved.missing) return { status: "missing" };
1052
+ closeOwnedDeviceSessions({
1053
+ platform: "ios",
1054
+ id: udid
1055
+ }, () => Boolean(resolveOwnedIosSim(udid).sim), workspace);
1056
+ const current = resolveOwnedIosSim(udid);
1057
+ if (current.missing) return { status: "missing" };
1058
+ if (current.notOwned) return {
1059
+ status: "skipped",
1060
+ kind: "not-owned",
1061
+ reason: "simulator ownership changed before shutdown"
1113
1062
  };
1114
- const outReader = createLineReader(onLine);
1115
- const errReader = createLineReader(onLine);
1116
- onOut = (chunk) => outReader.push(chunk);
1117
- onErr = (chunk) => errReader.push(chunk);
1118
- onError = () => finish(null);
1119
- onExit = () => {
1120
- outReader.flush();
1121
- errReader.flush();
1122
- finish(null, true);
1063
+ shutdownIosSim(udid);
1064
+ const sim = resolved.sim;
1065
+ if (del && park && park.max > 0) try {
1066
+ const { record, evicted } = parkOwnedIosSim(settleIosSimShutdown(udid, shutdownClock), udid, park);
1067
+ const removed = [];
1068
+ const failures = [];
1069
+ for (const entry of evicted) {
1070
+ const result = teardownParkedIosSim(entry.udid, { label: entry.name });
1071
+ if (result.status === "torn-down") removed.push({
1072
+ udid: entry.udid,
1073
+ name: entry.name
1074
+ });
1075
+ else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name} (${entry.udid}): ${result.reason}`);
1076
+ }
1077
+ return {
1078
+ status: "torn-down",
1079
+ label: label ?? sim.name ?? udid,
1080
+ parked: {
1081
+ udid: record.udid,
1082
+ name: record.name
1083
+ },
1084
+ evicted: removed,
1085
+ ...failures.length ? { evictionFailures: failures } : {}
1086
+ };
1087
+ } catch (e) {
1088
+ parkFallback = String(e?.message || e);
1089
+ }
1090
+ if (del) {
1091
+ deleteIosSim(udid);
1092
+ return {
1093
+ status: "torn-down",
1094
+ label: label ?? sim.name ?? udid,
1095
+ ...parkFallback ? { parkFallback } : {}
1096
+ };
1097
+ }
1098
+ const settled = settleIosSimShutdown(udid, shutdownClock);
1099
+ if (settled.sim && settled.sim.state !== "Shutdown") return {
1100
+ status: "failed",
1101
+ label: label ?? sim.name ?? udid,
1102
+ reason: iosShutdownFailureReason(udid, settled.sim.state)
1123
1103
  };
1124
- child.stdout?.setEncoding?.("utf-8");
1125
- child.stderr?.setEncoding?.("utf-8");
1126
- child.stdout?.on("data", onOut);
1127
- child.stderr?.on("data", onErr);
1128
- child.on("error", onError);
1129
- child.on("exit", onExit);
1130
- timer = setTimeout(() => finish(null), timeoutMs);
1131
- });
1132
- }
1133
- function waitForFileUrl(child, logFile, parseLine, timeoutMs) {
1134
- return new Promise((resolve) => {
1135
- let settled = false;
1136
- const finish = (url, exited = false) => {
1137
- if (settled) return;
1138
- settled = true;
1139
- clearInterval(interval);
1140
- clearTimeout(timer);
1141
- child.removeListener("error", onError);
1142
- child.removeListener("exit", onExit);
1143
- resolve({
1144
- url,
1145
- exited
1146
- });
1104
+ const root = sim.state === "Shutdown" ? void 0 : repoRootAbove(workspace);
1105
+ if (root && settled.sim) closeOwnedDeviceSessions({
1106
+ platform: "ios",
1107
+ id: udid
1108
+ }, () => resolveOwnedIosSim(udid).sim?.state === "Shutdown", workspace, root);
1109
+ return {
1110
+ status: "torn-down",
1111
+ label: label ?? sim.name ?? udid
1147
1112
  };
1148
- const probe = () => {
1149
- try {
1150
- for (const line of readFileSync(logFile, "utf-8").split(/\r?\n/)) {
1151
- const url = parseLine(line);
1152
- if (url) return finish(url);
1153
- }
1154
- } catch {}
1113
+ } catch (e) {
1114
+ return {
1115
+ status: "failed",
1116
+ reason: String(e?.message || e),
1117
+ ...parkFallback ? { parkFallback } : {}
1155
1118
  };
1156
- const onError = () => finish(null);
1157
- const onExit = () => finish(null, true);
1158
- child.on("error", onError);
1159
- child.on("exit", onExit);
1160
- const interval = setInterval(probe, 25);
1161
- const timer = setTimeout(() => finish(null), timeoutMs);
1162
- probe();
1163
- });
1119
+ }
1164
1120
  }
1165
- function createTunnelLogFile(provider) {
1166
- const dir = join(getConfigDir(), "tunnel-logs");
1167
- mkdirSync(dir, { recursive: true });
1168
- const path = join(dir, `${provider}-${randomUUID()}.log`);
1169
- closeSync(openSync(path, "wx", 384));
1170
- return path;
1121
+ function assertAvdReferences(avdName, owner) {
1122
+ const config = loadConfig$1();
1123
+ if (!config) throw new Error("Cannot verify AVD references without a Stim config; the data was kept.");
1124
+ if (owner && "projectPath" in owner && owner.expectedRecord && !isDeepStrictEqual(deviceSlotPlatforms(config.projects[owner.projectPath], owner.slot)?.android, owner.expectedRecord)) throw new Error(`AVD ${avdName} has a changed workspace record; its data was kept.`);
1125
+ for (const [projectPath, project] of Object.entries(config.projects)) for (const { slot, platforms } of projectDeviceSlots(project)) {
1126
+ if (platforms.android?.avdName?.toLowerCase() !== avdName.toLowerCase()) continue;
1127
+ if (!owner || !("projectPath" in owner) || owner.projectPath !== projectPath || (owner.slot ?? "default") !== slot || !platforms.android.owned) throw new Error(`AVD ${avdName} is referenced by ${projectPath} (${slot}); its data was kept.`);
1128
+ }
1129
+ if (readParked("android", { config }).some((entry) => entry.name.toLowerCase() === avdName.toLowerCase()) && !(owner && "pool" in owner)) throw new Error(`AVD ${avdName} is in the emulator pool; its data was kept.`);
1171
1130
  }
1172
- function removeTunnelLogFile(path) {
1173
- if (!path) return;
1174
- try {
1175
- unlinkSync(path);
1176
- } catch {}
1177
- }
1178
- function removeRecordedTunnelLogFile(path) {
1179
- if (!path) return;
1180
- const dir = resolve(join(getConfigDir(), "tunnel-logs"));
1181
- const resolved = resolve(path);
1182
- if (!resolved.startsWith(`${dir}${sep}`) || resolve(join(dir, basename(resolved))) !== resolved) return;
1183
- removeTunnelLogFile(resolved);
1131
+ function removeOrphanedAvdDirectory(candidate, owner) {
1132
+ const surveyed = listOrphanedAvdDirectories(candidate.name).find((entry) => entry.directory === candidate.directory);
1133
+ if (!surveyed || surveyed.name !== candidate.name || surveyed.dev !== candidate.dev || surveyed.ino !== candidate.ino) throw new Error(`AVD data at ${candidate.directory} changed or is registered; it was kept.`);
1134
+ assertOwnedAvdStopped(candidate.name, { resolveDirectory: () => candidate.directory });
1135
+ const detached = withConfigLock$1(() => {
1136
+ const current = lstatSync(candidate.directory);
1137
+ if (!current.isDirectory() || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`AVD data at ${candidate.directory} changed or is registered; it was kept.`);
1138
+ assertAvdReferences(candidate.name, owner);
1139
+ const name = `stim-gc-${randomUUID()}`;
1140
+ recordCreatedDevice("android", name);
1141
+ const destination = join(dirname(candidate.directory), `${name}.avd`);
1142
+ try {
1143
+ renameSync(candidate.directory, destination);
1144
+ } catch (error) {
1145
+ forgetCreatedDevice("android", name);
1146
+ throw error;
1147
+ }
1148
+ forgetCreatedDevice("android", candidate.name);
1149
+ return destination;
1150
+ });
1151
+ const current = lstatSync(detached);
1152
+ if (!current.isDirectory() || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`Detached AVD data changed at ${detached}; it was kept.`);
1153
+ try {
1154
+ rmSync(detached, { recursive: true });
1155
+ } catch (error) {
1156
+ throw new Error(`Could not remove AVD data at ${detached}; retry gc --delete: ${error.message}`, { cause: error });
1157
+ }
1158
+ forgetCreatedDevice("android", basename(detached, ".avd"));
1184
1159
  }
1185
- async function waitUntilReachable({ url, now, sleep, probe, timeoutMs }) {
1186
- const deadline = now() + timeoutMs;
1187
- for (;;) {
1188
- if (await probe(url, new AbortController().signal).catch(() => false)) return true;
1189
- if (now() >= deadline) return false;
1190
- await sleep(REACHABLE_POLL_MS);
1160
+ function teardownUnregisteredAvd(avdName, del, owner, surveyed) {
1161
+ const candidates = surveyed ? [surveyed] : listOrphanedAvdDirectories(avdName);
1162
+ if (candidates.length === 0) {
1163
+ if (avdStorageRoots().some((root) => avdPathExists(join(root, `${avdName}.ini`)) || avdPathExists(join(root, `${avdName}.avd`)))) throw new Error(`AVD ${avdName} is not listed but its storage remains; it was kept.`);
1164
+ return { status: "missing" };
1191
1165
  }
1166
+ if (!del) return {
1167
+ status: "skipped",
1168
+ reason: "AVD registration is missing; its data was kept."
1169
+ };
1170
+ for (const candidate of candidates) {
1171
+ if (candidate.name !== avdName) throw new Error(`AVD data does not match ${avdName}; it was kept.`);
1172
+ removeOrphanedAvdDirectory(candidate, owner);
1173
+ }
1174
+ return {
1175
+ status: "torn-down",
1176
+ label: avdName
1177
+ };
1192
1178
  }
1193
- const CLEANUP_TIMEOUT_MS = 1e3;
1194
- const CLEANUP_POLL_MS = 25;
1195
- async function startTunnel({ provider, port, spawnFn = null, urlTimeoutMs = URL_TIMEOUT_MS, reachableTimeoutMs = REACHABLE_TIMEOUT_MS, probeReachable = defaultProbeReachable, now = Date.now, sleep = defaultSleep, ngrokUrl = null, requireReachable = true, cleanupTimeoutMs = CLEANUP_TIMEOUT_MS, isChildAlive = pidExists, readProcessToken = process_identity_exports.captureProcessToken, logFile = null }) {
1196
- const spawn = spawnFn || ((cmd, args, opts) => getExecutor().spawn(cmd, args, opts));
1197
- const outputFile = provider === "tailscale" ? null : logFile || (spawnFn ? null : createTunnelLogFile(provider));
1198
- let child;
1179
+ function teardownOwnedAvd(avdName, options = {}) {
1180
+ let claim;
1199
1181
  try {
1200
- const binary = provider === "tailscale" ? resolveTailscaleBinary((bin) => getExecutor().findExecutable(bin)) : provider;
1201
- if (!binary) throw new Error("Install Tailscale and sign in to your tailnet.");
1202
- const { bin, args } = tunnelArgv(provider, port, ngrokUrl, outputFile, binary);
1203
- child = spawn(bin, args, {
1204
- stdio: outputFile ? [
1205
- "ignore",
1206
- "ignore",
1207
- "ignore"
1208
- ] : [
1209
- "ignore",
1210
- "pipe",
1211
- "pipe"
1212
- ],
1213
- detached: true
1214
- });
1215
- } catch (err) {
1216
- removeTunnelLogFile(outputFile);
1182
+ if (!isStimOwnedAvd(avdName)) return {
1183
+ status: "skipped",
1184
+ kind: "not-owned",
1185
+ reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
1186
+ };
1187
+ claim = acquireAvdClaim(avdName);
1188
+ const result = teardownClaimedAvd(avdName, claim, options);
1189
+ if (result.status === "torn-down" || result.status === "missing") options.onRemoved?.();
1190
+ return result;
1191
+ } catch (error) {
1217
1192
  return {
1218
- failed: true,
1219
- reason: `Could not start ${provider}: ${describe(err)}`
1193
+ status: "failed",
1194
+ reason: String(error?.message || error)
1220
1195
  };
1196
+ } finally {
1197
+ (0, ownership_claim_exports.releaseClaim)(claim);
1221
1198
  }
1222
- let childExited = false;
1223
- child.once("exit", () => {
1224
- childExited = true;
1225
- });
1226
- const cleanup = async (alreadyExited = childExited) => {
1227
- const stopped = await terminateChild(child, {
1228
- alreadyExited,
1229
- timeoutMs: cleanupTimeoutMs,
1230
- now,
1231
- sleep,
1232
- isAlive: isChildAlive
1233
- });
1234
- if (stopped) removeTunnelLogFile(outputFile);
1235
- return stopped ? { status: "stopped" } : {
1236
- status: "failed",
1237
- reason: `Sent SIGKILL but could not confirm that pid ${child.pid ?? "unknown"} exited.`
1199
+ }
1200
+ function serialFree(serial, timeoutMs = AVD_INVENTORY_TIMEOUT_MS) {
1201
+ const adb = listAdbDevices({ timeoutMs });
1202
+ return ![
1203
+ ...adb.emulators,
1204
+ ...adb.physical,
1205
+ ...adb.unhealthy
1206
+ ].some((entry) => entry.serial === serial);
1207
+ }
1208
+ function teardownClaimedAvd(avdName, claim, { del = false, wipe = false, park, owner, workspace, orphanedDirectory, onlyIfMissing = false, onWipeStart, waitForShutdown = waitForAndroidEmulatorShutdown, assertStopped = assertOwnedAvdStopped, resolveAvd = (name) => resolveOwnedAvdSerial(name, { timeoutMs: AVD_INVENTORY_TIMEOUT_MS }) }) {
1209
+ let parkFallback;
1210
+ try {
1211
+ if (del || wipe) withConfigLock$1(() => assertAvdReferences(avdName, owner));
1212
+ const resolved = resolveAvd(avdName);
1213
+ if (resolved.notOwned) return {
1214
+ status: "skipped",
1215
+ kind: "not-owned",
1216
+ reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
1238
1217
  };
1239
- };
1240
- const failAfterCleanup = async (reason, alreadyExited = childExited) => {
1241
- const stopped = await cleanup(alreadyExited);
1242
- return stopped.status === "stopped" ? {
1243
- failed: true,
1244
- reason
1245
- } : {
1246
- failed: true,
1247
- reason: `${reason} ${stopped.reason}`,
1248
- cleanupFailed: true
1218
+ if (resolved.missing) {
1219
+ if (wipe) throw new Error(`AVD ${avdName} is not registered; its data was kept.`);
1220
+ return teardownUnregisteredAvd(avdName, del, owner, orphanedDirectory);
1221
+ }
1222
+ if (onlyIfMissing) return {
1223
+ status: "skipped",
1224
+ reason: "AVD registration appeared; its record was kept."
1249
1225
  };
1250
- };
1251
- const pid = child.pid;
1252
- if (!pid) return failAfterCleanup(`${provider} started but reported no pid.`);
1253
- const captureChildIdentity = () => {
1254
- try {
1255
- return readProcessToken(pid);
1256
- } catch {
1257
- return null;
1226
+ if (orphanedDirectory) return {
1227
+ status: "skipped",
1228
+ reason: "AVD registration appeared; its data was kept."
1229
+ };
1230
+ const serial = resolved.serial;
1231
+ const stillStopped = (sessionSerial) => {
1232
+ const current = resolveAvd(avdName);
1233
+ return !current.notOwned && !current.missing && !current.serial && serialFree(sessionSerial);
1234
+ };
1235
+ if (serial) {
1236
+ const stillOwned = () => {
1237
+ const current = resolveAvd(avdName);
1238
+ return !current.notOwned && !current.missing && current.serial === serial;
1239
+ };
1240
+ closeOwnedDeviceSessions({
1241
+ platform: "android",
1242
+ id: serial,
1243
+ avdName
1244
+ }, stillOwned, workspace);
1245
+ if (!stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
1246
+ waitForShutdown(avdName, (timeoutMs) => {
1247
+ (0, ownership_claim_exports.markClaimChildPending)(claim);
1248
+ try {
1249
+ shutdownAndroidEmulator(serial, timeoutMs);
1250
+ } finally {
1251
+ (0, ownership_claim_exports.clearClaimChild)(claim);
1252
+ }
1253
+ });
1254
+ const root = repoRootAbove(workspace);
1255
+ if (root) {
1256
+ const deadline = Date.now() + ADB_RELEASE_WAIT_MS;
1257
+ while (!serialFree(serial, Math.max(1, deadline - Date.now())) && Date.now() < deadline) sleepSync(250);
1258
+ closeOwnedDeviceSessions({
1259
+ platform: "android",
1260
+ id: serial,
1261
+ avdName
1262
+ }, stillStopped, workspace, root);
1263
+ }
1264
+ } else {
1265
+ closeOwnedDeviceSessions({
1266
+ platform: "android",
1267
+ id: null,
1268
+ avdName
1269
+ }, stillStopped, workspace);
1270
+ waitForShutdown(avdName, null);
1258
1271
  }
1259
- };
1260
- const initialProcessToken = captureChildIdentity();
1261
- if (!initialProcessToken) return failAfterCleanup(`${provider} started but its process identity token could not be read.`);
1262
- const { url, exited, lastLine } = outputFile ? await waitForFileUrl(child, outputFile, parserFor(provider), urlTimeoutMs) : await waitForUrl(child, parserFor(provider), urlTimeoutMs);
1263
- if (!url) return failAfterCleanup(exited ? provider === "tailscale" && lastLine ? `${provider} exited before printing a tunnel URL: ${lastLine}` : `${provider} exited before printing a tunnel URL.` : `${provider} did not print a tunnel URL within ${urlTimeoutMs}ms.`, exited || childExited);
1264
- if (!outputFile) resumeChildPipes(child);
1265
- if (requireReachable) {
1266
- if (!await waitUntilReachable({
1267
- url,
1268
- now,
1269
- sleep,
1270
- probe: probeReachable,
1271
- timeoutMs: reachableTimeoutMs
1272
- })) return failAfterCleanup(`${url} did not become reachable within ${reachableTimeoutMs}ms; ${provider} may still be registering, or the network path is blocked.`);
1272
+ if (wipe && !del) {
1273
+ const current = resolveAvd(avdName);
1274
+ if (current.notOwned || current.missing) throw new Error(`AVD ${avdName} changed before its data was wiped.`);
1275
+ if (current.serial) throw new Error(`Owned AVD ${avdName} started again before its data was wiped.`);
1276
+ assertStopped(avdName);
1277
+ withConfigLock$1(() => assertAvdReferences(avdName, owner));
1278
+ const directory = ownedAvdDirectory(avdName);
1279
+ if (!directory) throw new Error(`The data directory of AVD ${avdName} could not be resolved.`);
1280
+ onWipeStart?.();
1281
+ wipeAvdUserData(directory);
1282
+ }
1283
+ if (del) {
1284
+ const current = resolveAvd(avdName);
1285
+ if (current.notOwned) return {
1286
+ status: "skipped",
1287
+ kind: "not-owned",
1288
+ reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
1289
+ };
1290
+ if (current.missing) return teardownUnregisteredAvd(avdName, del, owner);
1291
+ if (current.serial) throw new Error(`Owned AVD ${avdName} started again before deletion.`);
1292
+ assertStopped(avdName);
1293
+ withConfigLock$1(() => assertAvdReferences(avdName, owner));
1294
+ if (park && park.max > 0) try {
1295
+ const systemImage = ownedAvdSystemImage(avdName);
1296
+ if (!systemImage || !park.configuration || !ownedAvdMatchesConfiguration(avdName, park.configuration)) throw new Error("the AVD has no verified creation configuration");
1297
+ const evicted = parkSim({
1298
+ platform: "android",
1299
+ projectPath: park.projectPath,
1300
+ slot: park.slot,
1301
+ max: park.max,
1302
+ record: {
1303
+ udid: avdName,
1304
+ name: avdName,
1305
+ systemImage,
1306
+ configuration: park.configuration,
1307
+ parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
1308
+ ...park.appId ? { packageName: park.appId } : {},
1309
+ ...park.cacheKey ? { cacheKey: park.cacheKey } : {}
1310
+ }
1311
+ });
1312
+ const removed = [];
1313
+ const failures = [];
1314
+ for (const entry of evicted) {
1315
+ const result = teardownParkedAvd(entry.name);
1316
+ if (result.status === "torn-down") removed.push({
1317
+ udid: entry.name,
1318
+ name: entry.name,
1319
+ platform: "android"
1320
+ });
1321
+ else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name}: ${result.reason}`);
1322
+ }
1323
+ return {
1324
+ status: "torn-down",
1325
+ label: avdName,
1326
+ parked: {
1327
+ udid: avdName,
1328
+ name: avdName,
1329
+ platform: "android"
1330
+ },
1331
+ evicted: removed,
1332
+ ...failures.length ? { evictionFailures: failures } : {}
1333
+ };
1334
+ } catch (error) {
1335
+ parkFallback = String(error?.message || error);
1336
+ }
1337
+ const directory = ownedAvdDirectory(avdName);
1338
+ const paths = avdStorageRoots().map((root) => join(root, `${avdName}.ini`));
1339
+ if (directory) paths.push(directory);
1340
+ (0, ownership_claim_exports.markClaimChildPending)(claim);
1341
+ try {
1342
+ deleteAvd(avdName);
1343
+ } finally {
1344
+ (0, ownership_claim_exports.clearClaimChild)(claim);
1345
+ }
1346
+ const remaining = paths.filter(avdPathExists);
1347
+ if (remaining.length) throw new Error(`AVD ${avdName} was only partially deleted; data remains at ${remaining.join(", ")}.`);
1348
+ }
1349
+ return {
1350
+ status: "torn-down",
1351
+ label: avdName,
1352
+ serial: resolved.serial ?? null,
1353
+ ...parkFallback ? { parkFallback } : {}
1354
+ };
1355
+ } catch (e) {
1356
+ return {
1357
+ status: "failed",
1358
+ reason: String(e?.message || e)
1359
+ };
1273
1360
  }
1274
- const finalProcessToken = captureChildIdentity();
1275
- if (!finalProcessToken || finalProcessToken !== initialProcessToken) return failAfterCleanup(`${provider} process identity changed before its tunnel could be recorded.`);
1276
- if (!outputFile) unrefChildPipes(child);
1277
- child.unref?.();
1278
- if (childExited) return failAfterCleanup(`${provider} exited before its tunnel could be recorded.`, true);
1279
- return {
1280
- url,
1281
- pid,
1282
- processToken: initialProcessToken,
1283
- logFile: outputFile,
1284
- cleanup: () => cleanup()
1285
- };
1286
1361
  }
1287
- async function startTunnelSequence({ providers, port, ngrokUrl = null, requireReachable = true, start = startTunnel }) {
1288
- const failures = [];
1289
- for (const provider of providers) {
1290
- const result = await start({
1291
- provider,
1292
- port,
1293
- ngrokUrl: provider === "ngrok" ? ngrokUrl : null,
1294
- requireReachable
1362
+ const BROWSER_EXIT_WAIT_MS = 15e3;
1363
+ const CHROME_KILL_WAIT_MS = 5e3;
1364
+ async function stopOwnedChrome(record, profile) {
1365
+ for (const signal of ["SIGTERM", "SIGKILL"]) {
1366
+ const state = chromeProcessState(record, profile);
1367
+ if (state === "gone") return true;
1368
+ if (state === "unknown") return false;
1369
+ try {
1370
+ signalProcessTree(record.pid, signal, { group: true });
1371
+ } catch {}
1372
+ const deadline = Date.now() + CHROME_KILL_WAIT_MS;
1373
+ while (chromeProcessState(record, profile) !== "gone" && Date.now() < deadline) await sleepAsync(50);
1374
+ }
1375
+ return chromeProcessState(record, profile) === "gone";
1376
+ }
1377
+ const sleepAsync = (ms) => new Promise((done) => setTimeout(done, ms));
1378
+ /**
1379
+ * Closes the workspace's Stim-owned Chrome and its supervisor, after verifying both process identities. With
1380
+ * `deleteProfile`, it also deletes the profile directory, only when the created-devices ledger lists it.
1381
+ */
1382
+ async function teardownOwnedBrowser(root, options = {}) {
1383
+ try {
1384
+ return await withWorkspaceProcessLock(workspaceDir(root), "web", () => teardownBrowserHeld(root, options), {
1385
+ external: true,
1386
+ ownerPurpose: "browser teardown"
1295
1387
  });
1296
- if (!("failed" in result)) return {
1297
- provider,
1298
- ...result
1299
- };
1300
- failures.push(`${provider}: ${result.reason}`);
1301
- if (result.cleanupFailed) return {
1302
- failed: true,
1303
- reason: failures.join(" ")
1388
+ } catch (error) {
1389
+ return {
1390
+ status: "failed",
1391
+ reason: String(error?.message || error)
1304
1392
  };
1305
1393
  }
1306
- return {
1307
- failed: true,
1308
- reason: failures.join(" ") || "No managed tunnel provider was selected."
1309
- };
1310
- }
1311
- function closeChildPipes(child) {
1312
- child.stdout?.destroy?.();
1313
- child.stderr?.destroy?.();
1314
- }
1315
- function unrefChildPipes(child) {
1316
- resumeChildPipes(child);
1317
- for (const stream of [child.stdout, child.stderr]) stream?.unref?.();
1318
1394
  }
1319
- function resumeChildPipes(child) {
1320
- child.stdout?.resume?.();
1321
- child.stderr?.resume?.();
1322
- }
1323
- async function signalAndWaitForExit(child, signal, { timeoutMs, now, sleep, isAlive, platform }) {
1324
- let exited = false;
1325
- const onExit = () => {
1326
- exited = true;
1327
- };
1328
- child.once("exit", onExit);
1395
+ /** {@link teardownOwnedBrowser} for a caller that already holds the {@link BROWSER_LOCK} lock. */
1396
+ async function teardownBrowserHeld(root, { deleteProfile = false } = {}) {
1329
1397
  try {
1330
- if (platform === "win32" && child.pid) signalProcessTree(child.pid, signal, { platform });
1331
- child.kill(signal);
1332
- } catch (err) {
1333
- child.removeListener("exit", onExit);
1334
- return isEsrch(err);
1398
+ const record = readWebRecord(root);
1399
+ const profile = record?.profile ?? webProfileDir(root);
1400
+ if (record) {
1401
+ const supervisor = (0, process_identity_exports.inspectProcessIdentity)(record);
1402
+ const chrome = record.chromeProcess ? (0, process_identity_exports.inspectProcessIdentity)(record.chromeProcess) : "gone";
1403
+ if (supervisor === "unknown" || chrome === "unknown") return {
1404
+ status: "skipped",
1405
+ kind: "not-verified",
1406
+ reason: `the browser supervisor (pid ${record.pid}) or its Chrome could not be verified; its record was kept`
1407
+ };
1408
+ if (supervisor === "same") {
1409
+ try {
1410
+ process.kill(record.pid, "SIGTERM");
1411
+ } catch {}
1412
+ if (!await (0, process_identity_exports.waitForProcessExit)(record, BROWSER_EXIT_WAIT_MS)) return {
1413
+ status: "failed",
1414
+ reason: `browser supervisor pid ${record.pid} did not exit`
1415
+ };
1416
+ }
1417
+ if (record.chromeProcess && !await stopOwnedChrome(record.chromeProcess, profile)) return {
1418
+ status: "failed",
1419
+ reason: `Chrome pid ${record.chromeProcess.pid} did not exit`
1420
+ };
1421
+ const exited = record.chromeProcess;
1422
+ const holder = liveProfileHolder(profile, { alive: (pid) => pidExists(pid) && !(pid === exited?.pid && (0, process_identity_exports.inspectProcessIdentity)(exited) === "gone") });
1423
+ if (holder !== null) return {
1424
+ status: "skipped",
1425
+ kind: "not-verified",
1426
+ reason: `pid ${holder} still holds ${profile}, and the browser record does not name it; it was left running`
1427
+ };
1428
+ const claims = (0, ownership_claim_exports.clearFreeClaimSet)({
1429
+ root: webClaimRoot(root),
1430
+ label: "browser supervisor"
1431
+ });
1432
+ if (claims.status !== "cleared") return {
1433
+ status: "skipped",
1434
+ kind: "not-verified",
1435
+ reason: claims.status === "held" ? `the browser supervisor claim is still held${claims.holder ? ` by pid ${claims.holder.owner.pid}` : ""}` : `the browser supervisor claim could not be cleared (${claims.reason}); remove it with ${(0, ownership_claim_exports.claimRemoveCommand)(webClaimRoot(root))}`
1436
+ };
1437
+ clearWebRecord(root, record);
1438
+ await releaseBrowserPort(root, record.cdpPort);
1439
+ } else {
1440
+ const holder = liveProfileHolder(profile);
1441
+ if (holder !== null) return {
1442
+ status: "skipped",
1443
+ kind: "not-verified",
1444
+ reason: `pid ${holder} holds ${profile} but no browser supervisor is recorded; it was left running`
1445
+ };
1446
+ const port = getProject(root)?.ports?.[CDP_PORT_LABEL];
1447
+ if (typeof port === "number") await releaseBrowserPort(root, port);
1448
+ }
1449
+ if (existsSync(profile)) removeSingletonFiles(profile);
1450
+ if (deleteProfile && existsSync(profile)) {
1451
+ if (!readCreatedDevices$1().web.has(profile)) return {
1452
+ status: "skipped",
1453
+ kind: "not-owned",
1454
+ reason: `${profile} is not in Stim's ledger; it was kept`
1455
+ };
1456
+ rmSync(profile, {
1457
+ recursive: true,
1458
+ force: true
1459
+ });
1460
+ forgetCreatedDevice("web", profile);
1461
+ }
1462
+ return record ? {
1463
+ status: "torn-down",
1464
+ label: `Chrome (${profile})`
1465
+ } : { status: "missing" };
1466
+ } catch (error) {
1467
+ return {
1468
+ status: "failed",
1469
+ reason: String(error?.message || error)
1470
+ };
1335
1471
  }
1336
- const hasExited = () => exited;
1337
- const deadline = now() + timeoutMs;
1338
- while (!hasExited() && now() < deadline) await sleep(CLEANUP_POLL_MS);
1339
- if (!hasExited() && child.pid && !isAlive(child.pid)) exited = true;
1340
- child.removeListener("exit", onExit);
1341
- return hasExited();
1342
1472
  }
1343
- async function terminateChild(child, { alreadyExited, timeoutMs, now, sleep, isAlive, platform = process.platform }) {
1344
- if (alreadyExited) {
1345
- closeChildPipes(child);
1346
- return true;
1347
- }
1348
- if (await signalAndWaitForExit(child, "SIGTERM", {
1349
- timeoutMs,
1350
- now,
1351
- sleep,
1352
- isAlive,
1353
- platform
1354
- })) {
1355
- closeChildPipes(child);
1356
- return true;
1473
+ //#endregion
1474
+ //#region src/macos/instances.ts
1475
+ const BUNDLE_EXECUTABLE = /^[^/]+\.app\/Contents\/MacOS\/[^/]+$/;
1476
+ function bundleDirs(root) {
1477
+ const dir = macosDir(root);
1478
+ if (process.platform !== "darwin" || !existsSync(dir)) return [];
1479
+ return [.../* @__PURE__ */ new Set([dir, realpathSync(dir)])];
1480
+ }
1481
+ function isBundleExecutable(path, dirs) {
1482
+ return dirs.some((dir) => path.startsWith(`${dir}/`) && BUNDLE_EXECUTABLE.test(path.slice(dir.length + 1)));
1483
+ }
1484
+ /** Pids from `ps -axww -o pid=,comm=` output whose command names a bundle executable inside `dirs`. */
1485
+ function bundleCandidates(psOutput, dirs) {
1486
+ const pids = [];
1487
+ for (const line of psOutput.split("\n")) {
1488
+ const match = /^\s*(\d+)\s+(.+)$/.exec(line);
1489
+ if (match && isBundleExecutable(match[2], dirs)) pids.push(Number(match[1]));
1357
1490
  }
1358
- const killed = await signalAndWaitForExit(child, "SIGKILL", {
1359
- timeoutMs,
1360
- now,
1361
- sleep,
1362
- isAlive,
1363
- platform
1364
- });
1365
- closeChildPipes(child);
1366
- return killed;
1491
+ return pids;
1367
1492
  }
1368
- function describe(err) {
1369
- return err?.message || String(err);
1493
+ /** Whether `lsof -Fn -d txt` output for one process names a bundle executable inside `dirs`. */
1494
+ function lsofNamesBundleExecutable(lsofOutput, dirs) {
1495
+ return lsofOutput.split("\n").some((line) => line.startsWith("n") && isBundleExecutable(line.slice(1), dirs));
1370
1496
  }
1371
- const STOP_TIMEOUT_MS = 5e3;
1372
- const STOP_POLL_MS = 100;
1373
- function isEsrch(err) {
1374
- return err?.code === "ESRCH";
1497
+ function candidatePids(dirs) {
1498
+ if (!dirs.length) return [];
1499
+ return bundleCandidates(getExecutor().runFile("ps", [
1500
+ "-axww",
1501
+ "-o",
1502
+ "pid=,comm="
1503
+ ], { timeoutMs: 1e4 }), dirs).filter((pid) => pid !== process.pid);
1375
1504
  }
1376
- async function stopTunnel(record, { isAlive = pidExists, inspectIdentity = process_identity_exports.inspectProcessIdentity, platform = process.platform, kill = (pid) => signalProcessTree(pid, "SIGTERM", { platform }), now = Date.now, sleep = defaultSleep, timeoutMs = STOP_TIMEOUT_MS } = {}) {
1377
- const missing = () => {
1378
- removeRecordedTunnelLogFile(record?.logFile);
1379
- return { status: "missing" };
1380
- };
1381
- const pid = record?.pid;
1382
- if (!pid || !isAlive(pid)) return missing();
1383
- if (!record.processToken) return {
1384
- status: "failed",
1385
- reason: `tunnel pid ${pid} has no process identity token, so Stim cannot verify ownership. Keep the record. Inspect the process, stop it with the provider tooling, remove the stale metroTunnel state, and retry.`
1386
- };
1387
- const identity = inspectIdentity(record);
1388
- if (identity === "gone" || identity === "different") return missing();
1389
- if (identity !== "same") return {
1390
- status: "failed",
1391
- reason: `could not verify the process identity for tunnel pid ${pid}; refusing to signal it.`
1392
- };
1393
- try {
1394
- kill(pid);
1395
- } catch (err) {
1396
- return isEsrch(err) ? missing() : {
1397
- status: "failed",
1398
- reason: describe(err)
1399
- };
1400
- }
1401
- const deadline = now() + timeoutMs;
1402
- const exited = () => {
1403
- if (!isAlive(pid)) return true;
1404
- const current = inspectIdentity(record);
1405
- return current === "gone" || current === "different";
1406
- };
1407
- while (now() < deadline && !exited()) await sleep(STOP_POLL_MS);
1408
- if (!exited()) return {
1409
- status: "failed",
1410
- reason: `pid ${pid} did not exit within ${timeoutMs}ms.`
1411
- };
1412
- removeRecordedTunnelLogFile(record.logFile);
1413
- return { status: "stopped" };
1505
+ /** Pids that claim to run an app bundle from the workspace's macOS directory. `ps` reports argv[0], so this is a lead. */
1506
+ function bundleInstancePids(root) {
1507
+ return candidatePids(bundleDirs(root));
1414
1508
  }
1415
- //#endregion
1416
- //#region src/supervisor/ownership.ts
1417
- function resolveSupervisorTarget({ state, record, reservedPort, isAlive = pidExists, inspectIdentity = process_identity_exports.inspectProcessIdentity } = {}) {
1418
- const statePid = numberOrNull(state?.pid);
1419
- const recordPid = numberOrNull(record?.pid);
1420
- const pid = statePid ?? recordPid;
1421
- if (!pid) return { status: "none" };
1422
- if (statePid && recordPid && statePid !== recordPid) return {
1423
- status: "unverified",
1424
- pid,
1425
- reason: `workspace state.json records supervisor pid ${statePid} but the registry records pid ${recordPid}`
1426
- };
1427
- if (!isAlive(pid)) return {
1428
- status: "stale",
1429
- pid
1430
- };
1431
- const port = numberOrNull(state?.port) ?? numberOrNull(record?.port);
1432
- if (reservedPort !== null && reservedPort !== void 0 && port !== null && port !== reservedPort) return {
1433
- status: "unverified",
1434
- pid,
1435
- reason: `supervisor pid ${pid} records port ${port}, but this project reserved port ${reservedPort}`
1436
- };
1437
- if (reservedPort !== null && reservedPort !== void 0 && port === null) return {
1438
- status: "unverified",
1439
- pid,
1440
- reason: `supervisor pid ${pid} has no recorded port, so it cannot be matched against reserved port ${reservedPort}`
1441
- };
1442
- const identityRecord = statePid ? state : record;
1443
- if (statePid && recordPid && state?.processToken !== record?.processToken) return {
1444
- status: "unverified",
1445
- pid,
1446
- reason: "workspace state and registry process identities disagree"
1447
- };
1448
- const identity = inspectIdentity(identityRecord);
1449
- if (identity === "gone" || identity === "different") return {
1450
- status: "stale",
1451
- pid
1452
- };
1453
- if (identity !== "same") return {
1454
- status: "unverified",
1455
- pid,
1456
- reason: "the recorded process identity could not be verified"
1457
- };
1458
- return {
1459
- status: "ours",
1460
- processToken: identityRecord.processToken,
1461
- pid,
1462
- port,
1463
- mode: state?.mode ?? record?.mode ?? null,
1464
- startedAt: state?.startedAt ?? record?.startedAt ?? null
1465
- };
1509
+ function unverified(pid, why) {
1510
+ return Object.assign(/* @__PURE__ */ new Error(`Cannot verify macOS app pid ${pid}: ${why}; no signal was sent.`), { code: "STIM_MACOS_OWNER_UNVERIFIED" });
1466
1511
  }
1467
- function numberOrNull(v) {
1468
- return typeof v === "number" && Number.isSafeInteger(v) && v > 0 ? v : null;
1512
+ /**
1513
+ * Every running process whose executable is an app bundle Stim built in the workspace's macOS directory, however it
1514
+ * was launched. Each identity is captured before its executable is confirmed with lsof.
1515
+ */
1516
+ function runningBundleInstances(root) {
1517
+ const dirs = bundleDirs(root);
1518
+ const instances = [];
1519
+ for (const pid of candidatePids(dirs)) {
1520
+ const processToken = (0, process_identity_exports.captureProcessToken)(pid);
1521
+ const start = (0, process_identity_exports.processStartMicros)(pid);
1522
+ if (!processToken || start.status !== "running") {
1523
+ if (start.status === "gone") continue;
1524
+ throw unverified(pid, "its identity cannot be captured");
1525
+ }
1526
+ const record = {
1527
+ pid,
1528
+ processToken,
1529
+ startedAtMicros: start.startedAtMicros
1530
+ };
1531
+ let open;
1532
+ try {
1533
+ open = getExecutor().runFile("lsof", [
1534
+ "-a",
1535
+ "-p",
1536
+ String(pid),
1537
+ "-d",
1538
+ "txt",
1539
+ "-Fn"
1540
+ ], { timeoutMs: 1e4 });
1541
+ } catch (error) {
1542
+ if (["gone", "different"].includes((0, process_identity_exports.inspectProcessIdentity)(record))) continue;
1543
+ open = String(error.stdout ?? "");
1544
+ if (!open.trim()) throw unverified(pid, `lsof failed (${error.message.split("\n")[0]})`);
1545
+ }
1546
+ if (lsofNamesBundleExecutable(open, dirs)) instances.push(record);
1547
+ }
1548
+ return instances;
1469
1549
  }
1470
1550
  //#endregion
1471
1551
  //#region src/workspace/in-use.ts
@@ -1486,6 +1566,12 @@ function workspaceInUse(root, { supervisor = true, managedLocks = true, hosted =
1486
1566
  if (macos?.host) reasons.push(`its macOS app runs on ${macos.host.machine}`);
1487
1567
  else if (macos?.state === "running" || macos?.state === "orphaned" || macos?.build.state === "running") reasons.push("its macOS app or build is running");
1488
1568
  else if (macos?.state === "unverified" || !macos && readWorkspaceState$1(root)?.macos !== void 0) reasons.push("its macOS process owner cannot be verified");
1569
+ else try {
1570
+ const pids = bundleInstancePids(root);
1571
+ if (pids.length) reasons.push(`its macOS app is running (pid ${pids.join(", ")})`);
1572
+ } catch (error) {
1573
+ reasons.push(`its running macOS apps cannot be listed: ${error.message}`);
1574
+ }
1489
1575
  const runtime = (0, ownership_claim_exports.readClaimSet)(macosRuntimeClaim(root));
1490
1576
  if (runtime.live.length) reasons.push("its macOS runtime claim is held");
1491
1577
  else if (runtime.unresolved[0]) reasons.push(`its macOS runtime cannot be verified: ${runtime.unresolved[0].reason}`);
@@ -1855,4 +1941,4 @@ function sizeText(bytes) {
1855
1941
  return bytes === null ? "size unknown" : formatBytes(bytes);
1856
1942
  }
1857
1943
  //#endregion
1858
- export { teardownParkedIosSim as A, eraseParkedAvd as C, teardownOwnedBrowser as D, teardownOwnedAvd as E, takeGcResults as F, canonicalPath as I, listBuildSlots as M, releaseBuildSlot as N, teardownOwnedIosSim as O, recordGcResult as P, probePublicHttp as S, teardownBrowserHeld as T, terminateChild as _, collectWorkspaceOutputs as a, withManagedTunnelLock as b, listWorkspaceDirs as c, emptyWorkspaceDir as d, withIdleWorkspace as f, stopTunnel as g, startTunnelSequence as h, collectOrphanedWorkspaces as i, acquireBuildSlot as j, teardownParkedAvd as k, rootPresence as l, resolveSupervisorTarget as m, WORKSPACE_OUTPUT_DIRS as n, deleteOrphanedWorkspaces as o, workspaceInUse as p, clearWorkspaceOutputs as r, isInsideWorkspaces as s, REBUILD_COST as t, sizeText as u, withManagedRemoteWorktreeLock as v, eraseParkedIosSim as w, withManagedTunnelRemovalLock as x, withManagedRemoteWorktreeRemovalLock as y };
1944
+ export { withManagedRemoteWorktreeRemovalLock as A, recordGcResult as C, stopTunnel as D, startTunnelSequence as E, acquireBuildSlot as F, listBuildSlots as I, releaseBuildSlot as L, withManagedTunnelRemovalLock as M, probePublicHttp as N, terminateChild as O, resolveSupervisorTarget as P, teardownParkedIosSim as S, canonicalPath as T, teardownBrowserHeld as _, collectWorkspaceOutputs as a, teardownOwnedIosSim as b, listWorkspaceDirs as c, emptyWorkspaceDir as d, withIdleWorkspace as f, eraseParkedIosSim as g, eraseParkedAvd as h, collectOrphanedWorkspaces as i, withManagedTunnelLock as j, withManagedRemoteWorktreeLock as k, rootPresence as l, runningBundleInstances as m, WORKSPACE_OUTPUT_DIRS as n, deleteOrphanedWorkspaces as o, workspaceInUse as p, clearWorkspaceOutputs as r, isInsideWorkspaces as s, REBUILD_COST as t, sizeText as u, teardownOwnedAvd as v, takeGcResults as w, teardownParkedAvd as x, teardownOwnedBrowser as y };