stim 1.20.2 → 1.21.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 (217) hide show
  1. package/README.md +88 -8
  2. package/dist/acquire-artifact-CD4lbiWE.mjs +153 -0
  3. package/dist/act-DHAw2A2S.mjs +2667 -0
  4. package/dist/activity-BzVfHSrY.mjs +1376 -0
  5. package/dist/{agent-device-usage-output-Dzgv6lgR.mjs → agent-device-usage-output-zh1vhbLp.mjs} +21 -65
  6. package/dist/{android-HqP1_vB_.mjs → android-BEJ8Jpl7.mjs} +2 -2
  7. package/dist/{android-DCKZrNvV.mjs → android-BLgbNqox.mjs} +128 -78
  8. package/dist/{android-Dlzxshgp.mjs → android-CV_GCELZ.mjs} +5 -5
  9. package/dist/{android-Mz_Inn3w.mjs → android-DUYkNF4u.mjs} +184 -102
  10. package/dist/{android-cas-Bl-cLJBk.mjs → android-cas-Ct5b1xCK.mjs} +7 -6
  11. package/dist/android-cas-compiler.mjs +2 -2
  12. package/dist/android-project-BCAAmMwz.mjs +12 -0
  13. package/dist/api.d.mts +740 -8
  14. package/dist/api.mjs +585 -1
  15. package/dist/{artifact-HSwkIWEQ.mjs → artifact-B8T3S5gi.mjs} +223 -201
  16. package/dist/{artifact-D3czlUvT.mjs → artifact-DAuZ-FNU.mjs} +158 -148
  17. package/dist/{browser-web-CA-F1GQo.mjs → browser-web-QQzVjz3O.mjs} +26 -24
  18. package/dist/budget-Bs6SxxzO.mjs +9154 -0
  19. package/dist/{build-DYDbfkk5.mjs → build-CoPzQRNw.mjs} +144 -96
  20. package/dist/build-cache-DmyR9Zzc.mjs +602 -0
  21. package/dist/build-plan-CaFskB7_.mjs +188 -0
  22. package/dist/{cache-manifest-XH-fvDUj.mjs → cache-manifest-DkkqguoK.mjs} +2 -2
  23. package/dist/cache-manifest.mjs +1 -1
  24. package/dist/{ccache-zdKiYQ3E.mjs → ccache-BJRZ9pG3.mjs} +10 -9
  25. package/dist/{cdp-Dq_ypAlJ.mjs → cdp-CWmIUMbw.mjs} +18 -5
  26. package/dist/{chrome-BnzZMqsR.mjs → chrome-B6tfCta1.mjs} +7 -4
  27. package/dist/{cli-CLP3diBm.mjs → cli-0DYf8QhQ.mjs} +20 -20
  28. package/dist/cli.mjs +1 -1
  29. package/dist/client-B7QVOlCr.d.mts +253 -0
  30. package/dist/client-UXD70ZbM.mjs +1406 -0
  31. package/dist/collector-run.d.mts +4 -40
  32. package/dist/collector-run.mjs +9 -8
  33. package/dist/{command-output-BTy8hZ0F.mjs → command-output-B1UrjZHL.mjs} +1 -1
  34. package/dist/config-CYcBF478.mjs +949 -0
  35. package/dist/{created-devices-Dzd5C2LF.mjs → created-devices-CqeBlrEn.mjs} +3 -3
  36. package/dist/{dependency-state-DG0p31-y.mjs → dependency-state-BbKQW83h.mjs} +20 -5
  37. package/dist/{detached-entry-B9C_zeQC.mjs → detached-entry-DFRyG1_o.mjs} +1 -1
  38. package/dist/{dev-client-DAWVIGBu.mjs → dev-client-UOeESKm6.mjs} +2 -2
  39. package/dist/device-DCxH7_By.mjs +1068 -0
  40. package/dist/{device-BECOiufl.mjs → device-ExJq8Ei7.mjs} +35 -35
  41. package/dist/{device-capacity-CBUAcRyQ.mjs → device-capacity-CMlqEcvw.mjs} +43 -13
  42. package/dist/device-host-worker.mjs +65 -61
  43. package/dist/{device-ios-COSi6oS1.mjs → device-ios-DRUeKaFV.mjs} +48 -45
  44. package/dist/{device-lease-run-Cw2M4z51.mjs → device-lease-run-C8raGFoP.mjs} +14 -26
  45. package/dist/{device-pool-Bf-pOhSw.mjs → device-pool-DMRpVX9a.mjs} +5 -5
  46. package/dist/doctor-BLHCYxzi.mjs +2 -0
  47. package/dist/{doctor-kfMBnUw6.mjs → doctor-DTvaDOda.mjs} +281 -317
  48. package/dist/{doctor-CB8oZKCk.mjs → doctor-Dhqebm0q.mjs} +183 -91
  49. package/dist/doctor-fingerprint.mjs +19 -0
  50. package/dist/{doctor-watchman-BRyV2adA.mjs → doctor-watchman-Cv1F10Kj.mjs} +40 -21
  51. package/dist/{eas-build-JyfqyHfC.mjs → eas-build-BomfRnFT.mjs} +53 -20
  52. package/dist/{build-plan-B7SZsc2N.mjs → eas-fallback-DJwZpy0c.mjs} +26 -208
  53. package/dist/{watchman-CPQFZLXY.mjs → eas-session-ledger-op6A54yi.mjs} +229 -11
  54. package/dist/{error-diagnostics-BR677IDM.mjs → error-diagnostics-Bf0rZvG9.mjs} +172 -82
  55. package/dist/exec-H20xSg2T.d.mts +65 -0
  56. package/dist/exec-_UbtCQcG.mjs +750 -0
  57. package/dist/gc-C-WwL0yi.mjs +3 -0
  58. package/dist/{gc-qWOMdnER.mjs → gc-_-dCXdyL.mjs} +584 -796
  59. package/dist/git-C3iTsJaZ.mjs +73 -0
  60. package/dist/{guide-G9cAbVAc.mjs → guide-Yqwt2bd6.mjs} +941 -269
  61. package/dist/{hosted-android-C3MHGld1.mjs → hosted-android-Bw-honsd.mjs} +8 -6
  62. package/dist/{hosted-client-ZEKpS9vw.mjs → hosted-client-DB_gBPfY.mjs} +34 -19
  63. package/dist/{hosted-ios-l-o3iM9J.mjs → hosted-ios-CUvOf4Vr.mjs} +1 -1
  64. package/dist/{hosted-logs-EzQM7-3Z.mjs → hosted-logs-DL_P5AdN.mjs} +77 -43
  65. package/dist/{hosted-macos-BACVAVqE.mjs → hosted-macos-QI141yKk.mjs} +21 -17
  66. package/dist/{hosted-native-A4s1QPXe.mjs → hosted-native-Ksyjgzq-.mjs} +30 -21
  67. package/dist/idle-BVeRGroT.d.mts +24 -0
  68. package/dist/{idle-shutdown-DtxhPTKe.mjs → idle-shutdown-DaKmZi0z.mjs} +11 -10
  69. package/dist/{ios-BwVCF7oA.mjs → ios-CQ68hG9s.mjs} +8 -8
  70. package/dist/{ios-RkW4pnQf.mjs → ios-CpByZy12.mjs} +140 -80
  71. package/dist/{ios-device-B5veSDDQ.mjs → ios-device-CXGP1bsD.mjs} +1 -1
  72. package/dist/{ios-device-D7pi2Sca.mjs → ios-device-CwFMehUS.mjs} +51 -26
  73. package/dist/{ios-project-K9hY_Jab.mjs → ios-project-WuwwBdvW.mjs} +13 -13
  74. package/dist/{ios-nOLJGVdy.mjs → ios-qNJt4dFm.mjs} +1 -1
  75. package/dist/ios-signing-DTTIChGs.mjs +489 -0
  76. package/dist/{ios-state-CBLe1xYw.mjs → ios-state-DvFX-K_l.mjs} +1 -1
  77. package/dist/{ios-Ce7bc7br.mjs → ios-ubFsBzwq.mjs} +129 -86
  78. package/dist/{js-swap-Bb3nsS07.mjs → js-swap-Cc1dttps.mjs} +53 -26
  79. package/dist/{launch-F4sOqAhB.mjs → launch-CD1yVc7U.mjs} +91 -90
  80. package/dist/{launch-C3qJ6Agb.mjs → launch-Zq2mOSDy.mjs} +82 -541
  81. package/dist/logs-BS00Anxf.mjs +352 -0
  82. package/dist/logs-CMm05HxN.mjs +255 -0
  83. package/dist/{macos-D7uDvO-z.mjs → macos-B65--pPd.mjs} +1 -1
  84. package/dist/{macos-Ch5VJmcS.mjs → macos-C-0qJ6zq.mjs} +225 -212
  85. package/dist/macos-run.mjs +1 -1
  86. package/dist/maintenance-run.mjs +122 -247
  87. package/dist/{metro-DnCEjCxs.mjs → metro-DEXpIhHC.mjs} +67 -12
  88. package/dist/{metro-gateway-CX9Xlkq2.mjs → metro-gateway-BMJNlj3E.mjs} +20 -12
  89. package/dist/{named-ports-4WopRPg-.mjs → named-ports-BWTzt6mu.mjs} +8 -63
  90. package/dist/native-android-JACyjTZs.mjs +844 -0
  91. package/dist/{native-gradle-inputs-BsLRrl7u.mjs → native-gradle-inputs-Bep9JcSF.mjs} +128 -140
  92. package/dist/native-inputs-TRTTTRxt.mjs +91 -0
  93. package/dist/native-run-DY_RLx3C.mjs +277 -0
  94. package/dist/{native-runtime-k8dTIrbK.mjs → native-runtime-Dg1Q7tfF.mjs} +9 -8
  95. package/dist/{native-xcode-inputs-CykopJdM.mjs → native-xcode-inputs-CB2iQeWq.mjs} +27 -18
  96. package/dist/{native-xcode-ios-Bbf30Gm0.mjs → native-xcode-ios-CXX39F7o.mjs} +96 -47
  97. package/dist/{ndjson-GOnbyvso.mjs → ndjson-CgE_o3Dl.mjs} +3 -3
  98. package/dist/offload-worker.d.mts +1 -46
  99. package/dist/offload-worker.mjs +197 -66
  100. package/dist/{ownership-DCtw_2IA.mjs → ownership-DEmM5vhh.mjs} +3 -3
  101. package/dist/{page-DRHy329N.mjs → page-CPaHeYqj.mjs} +7 -4
  102. package/dist/paths-BsJMoPP2.mjs +12 -0
  103. package/dist/paths-DQtTgsBf.mjs +42 -0
  104. package/dist/{placement-log-MOfPVgaM.mjs → placement-log-BqGbTa61.mjs} +5 -5
  105. package/dist/{plan-placement-V0TdzYXp.mjs → plan-placement-Bwj-IjY1.mjs} +6 -6
  106. package/dist/{ports-DdSUicVo.mjs → ports-D-MmCTdv.mjs} +3 -3
  107. package/dist/prebuild-uD_ipCxF.mjs +449 -0
  108. package/dist/{project-_phNIJfs.mjs → project-BZNjmW_8.mjs} +65 -37
  109. package/dist/{pull-request-CQ8sPbZa.mjs → pull-request-Lfetn8Kr.mjs} +2 -2
  110. package/dist/pull-request-zjCSAryH.d.mts +120 -0
  111. package/dist/pull-requests.d.mts +1 -119
  112. package/dist/pull-requests.mjs +1 -1
  113. package/dist/{react-native-android-CM1XRDwv.mjs → react-native-android-DuzH0cn3.mjs} +74 -48
  114. package/dist/{gradle-DSrXb6lG.mjs → react-native-build-rhk4smwQ.mjs} +292 -15
  115. package/dist/react-native-doctor-CQpuIMso.mjs +234 -0
  116. package/dist/{react-native-ios-Dhq_eiYQ.mjs → react-native-ios-Bhq1rJeV.mjs} +46 -28
  117. package/dist/{recordings-D0d5aQt0.mjs → recordings-DYteQSu4.mjs} +12 -9
  118. package/dist/{reload-C8JhxjSh.mjs → reload-CmTRFJaX.mjs} +93 -47
  119. package/dist/reload-DzjSl5P_.mjs +2 -0
  120. package/dist/{remote-BiR6aYjy.mjs → remote-ByYkXdGD.mjs} +15 -10
  121. package/dist/result-DNiS-tTP.mjs +434 -0
  122. package/dist/{run-BtPgpKks.mjs → run-D23HmJMs.mjs} +4 -5
  123. package/dist/selection-CSl9i5qU.mjs +619 -0
  124. package/dist/{server-bare-R2m_gsH8.mjs → server-bare-CxTrvDg8.mjs} +3 -4
  125. package/dist/{server-command-CTtCkmcQ.mjs → server-command-CtzloH3v.mjs} +14 -7
  126. package/dist/server-expo-C-90EhcG.mjs +2 -0
  127. package/dist/{server-expo-DsguMGUF.mjs → server-expo-DT_F_uVI.mjs} +50 -9
  128. package/dist/{settings-B717odTD.mjs → settings-CsJDEXgK.mjs} +234 -24
  129. package/dist/{settings-vk3mJGSm.d.mts → settings-DkdmlelE.d.mts} +6 -4
  130. package/dist/settings-T1gtc3bf.mjs +114 -0
  131. package/dist/settings-write-HyVZ8eQ8.mjs +359 -0
  132. package/dist/settings.schema.json +163 -16
  133. package/dist/{simslim-CCr3rE-A.mjs → simslim-Cgk17SXV.mjs} +14 -12
  134. package/dist/{slot-launch-DPt0m9Nd.mjs → slot-launch-DF8VfT64.mjs} +15 -20
  135. package/dist/{spawn-entry-Dcb7sDUz.mjs → spawn-entry-jw19m8Nl.mjs} +0 -1
  136. package/dist/stage-BIS0ydsV.mjs +183 -0
  137. package/dist/{start-BWqzODpb.mjs → start-7lpH7hmN.mjs} +63 -44
  138. package/dist/{start-TXeMmmO2.mjs → start-RChynRDp.mjs} +1 -1
  139. package/dist/{state-D1Icyjlu.mjs → state-C4ZtUisW.mjs} +40 -13
  140. package/dist/{state-CG_OFoaA.mjs → state-XMziexv2.mjs} +3 -2
  141. package/dist/{stats-BoY5sEPU.mjs → stats-53T7OTb9.mjs} +7 -13
  142. package/dist/stats-report-BFfMpWWF.mjs +16 -0
  143. package/dist/{status-WgFbkRzj.mjs → status-BL7jVyAB.mjs} +535 -447
  144. package/dist/status-CSi6tlRq.mjs +2 -0
  145. package/dist/{status-BTA7kKiQ.mjs → status-DqEUSSxs.mjs} +6 -4
  146. package/dist/{status-watch-C23ILtf9.mjs → status-watch-GDCfqpTE.mjs} +187 -22
  147. package/dist/status-worker.d.mts +1 -0
  148. package/dist/status-worker.mjs +19 -0
  149. package/dist/stim-desktop-C12we7lz.mjs +112 -0
  150. package/dist/{stop-DR6VoDIk.mjs → stop-Bk_1qiV6.mjs} +2 -2
  151. package/dist/{stop-BNmBMi1F.mjs → stop-CPzlM1L-.mjs} +138 -98
  152. package/dist/{stop-DPEhunlw.mjs → stop-D7h6jAXQ.mjs} +11 -10
  153. package/dist/supervisor-run.d.mts +8 -17
  154. package/dist/supervisor-run.mjs +63 -27
  155. package/dist/{support-DMTqj3Hm.mjs → support-67vraa1Q.mjs} +18 -11
  156. package/dist/{support-CwjbFs9H.mjs → support-Dlu37g6H.mjs} +12 -12
  157. package/dist/{swiftpm-macos-DRNVJWlb.mjs → swiftpm-macos-p8WZbxKU.mjs} +35 -24
  158. package/dist/toolchain-DhKwp9iI.mjs +2 -0
  159. package/dist/toolchain-Hp9sCpK4.mjs +873 -0
  160. package/dist/{trigger-CigAYjeL.mjs → trigger-BAahxUPq.mjs} +20 -5
  161. package/dist/trigger-Bf3uDcpv.mjs +2 -0
  162. package/dist/{warm-progress-BuY-Fvfn.mjs → warm-progress-BHX5r0Ei.mjs} +250 -156
  163. package/dist/{web-BDBMxDpx.mjs → web-CJmIj6AI.mjs} +18 -13
  164. package/dist/web-DknQETPs.mjs +2 -0
  165. package/dist/web-run.mjs +11 -12
  166. package/dist/windows-report.d.mts +1 -0
  167. package/dist/windows-report.mjs +18 -0
  168. package/dist/windows-report.ps1 +43 -0
  169. package/dist/workspace-process-lock-1_1OoVW5.mjs +57 -0
  170. package/dist/{workspace-state-DJt_MMvf.mjs → workspace-state-Cjuigeeu.mjs} +62 -58
  171. package/dist/{workspaces-C36s2jb9.mjs → workspaces-BccMFmGQ.mjs} +1188 -1079
  172. package/dist/worktree-CK2MpJW5.mjs +2 -0
  173. package/dist/{worktree-CgvJYJ-Q.mjs → worktree-DQ6Zi2Mv.mjs} +304 -53
  174. package/dist/xcode-Zlx3qjEs.mjs +896 -0
  175. package/package.json +7 -5
  176. package/shim/android-optimizations.gradle +7 -5
  177. package/dist/activity-BSV745Kz.mjs +0 -629
  178. package/dist/api-qvOzWyfO.mjs +0 -154
  179. package/dist/api-run.mjs +0 -294
  180. package/dist/app-install-BIKtMfPq.mjs +0 -896
  181. package/dist/artifact-lifecycle-DzPpBYvK.mjs +0 -30
  182. package/dist/budget-f7516Zk0.mjs +0 -4106
  183. package/dist/build-facts-DcLv2HSd.d.mts +0 -111
  184. package/dist/cancellation-C_22VQTH.mjs +0 -24
  185. package/dist/client-qQAaXSUs.mjs +0 -1771
  186. package/dist/config-BYDxlMY3.mjs +0 -562
  187. package/dist/device-DuiPJQSn.mjs +0 -290
  188. package/dist/device-lease-BX1sJQ2E.mjs +0 -1224
  189. package/dist/device-remote-xRT0Vmo9.mjs +0 -1272
  190. package/dist/eas-session-ledger-Cm0Gwq2F.mjs +0 -207
  191. package/dist/exec-DqKo9GKJ.mjs +0 -268
  192. package/dist/git-DDfFzkMm.mjs +0 -45
  193. package/dist/launch-verify-B6Q8hZP-.mjs +0 -428
  194. package/dist/logs-BGrEUaEs.mjs +0 -383
  195. package/dist/miss-reason-mKzZ4VvN.mjs +0 -238
  196. package/dist/native-android-D5qc3-_v.mjs +0 -226
  197. package/dist/ndjson-Du3j0AQ9.d.mts +0 -17
  198. package/dist/plan-C4TjdPsR.mjs +0 -260
  199. package/dist/prebuild-DYbucPXy.mjs +0 -212
  200. package/dist/preview-78a9kR_-.mjs +0 -1011
  201. package/dist/react-native-build-CtCccJtb.mjs +0 -239
  202. package/dist/react-native-doctor-CfRnFfjs.mjs +0 -78
  203. package/dist/remote-cache-OyOyVIS3.mjs +0 -526
  204. package/dist/result-CaJg5Iat.mjs +0 -186
  205. package/dist/server-expo-Bigriskl.mjs +0 -2
  206. package/dist/settings-BORNaaCO.mjs +0 -402
  207. package/dist/spawn-claims-DVS5RkMS.mjs +0 -72
  208. package/dist/stage-KNTQopqv.mjs +0 -157
  209. package/dist/stim-desktop-BzBN2Em2.mjs +0 -63
  210. package/dist/tailnet-Dh-qiYzG.mjs +0 -265
  211. package/dist/toolchain-BFkAcstd.mjs +0 -2
  212. package/dist/toolchain-DxSsNssu.mjs +0 -1657
  213. package/dist/trigger-9_USgmxz.mjs +0 -2
  214. package/dist/web-Dq-i3wzJ.mjs +0 -2
  215. package/dist/worktree-DpLdCHga.mjs +0 -2
  216. package/dist/{api-run.d.mts → doctor-fingerprint.d.mts} +0 -0
  217. package/dist/{errors-DzkMYh8Q.mjs → errors-ccaPc4ox.mjs} +2 -2
@@ -1,38 +1,60 @@
1
- import { A as acquireAvdClaim, D as withConfigLock$1, F as listMountedVolumes, I as measuredDirectorySize, N as formatBytes, P as isOnMountedVolume, V as deviceSlotPlatforms, W as projectDeviceSlots, _ as loadConfig$1, d as getConfigDir, g as isPathPrefix, j as ownership_claim_exports, k as process_identity_exports, p as getProject } from "./config-BYDxlMY3.mjs";
2
- import { t as getExecutor } from "./exec-DqKo9GKJ.mjs";
3
- import { Dt as repoRoot, ct as resolveTailscaleBinary } from "./settings-B717odTD.mjs";
4
- import { h as listBuildLocks } from "./device-lease-BX1sJQ2E.mjs";
5
- import { T as workspaceName, r as readWorkspaceState$1, s as workspaceLastUsed, x as workspaceDir, y as workspaceAgentDeviceDir } from "./workspace-state-DJt_MMvf.mjs";
1
+ import { c as stopDeclaringSpawnsOn, f as ownership_claim_exports, i as declareSpawnsOn, p as process_identity_exports, t as getExecutor } from "./exec-_UbtCQcG.mjs";
2
+ import { d as workspaceDir, h as workspaceName$1, l as workspaceAgentDeviceDir } from "./paths-DQtTgsBf.mjs";
3
+ import { D as withConfigLock$1, F as isOnMountedVolume, I as listMountedVolumes, K as projectDeviceSlots, O as withConfigLockAsync$1, P as formatBytes, R as measuredDirectorySizeAsync, U as deviceSlotPlatforms, _ as loadConfig$1, d as getConfigDir, g as isPathPrefix, j as acquireAvdClaim, p as getProject } from "./config-CYcBF478.mjs";
6
4
  import { t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
7
- import { a as formatElapsed, l as phaseLine } from "./command-output-BTy8hZ0F.mjs";
8
- 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-Mz_Inn3w.mjs";
9
- import { n as readCreatedDevices$1, r as recordCreatedDevice, t as forgetCreatedDevice } from "./created-devices-Dzd5C2LF.mjs";
10
- import { d as signalProcessTree, s as pidExists } from "./metro-DnCEjCxs.mjs";
11
- import { n as declareSpawnsOn, o as stopDeclaringSpawnsOn } from "./spawn-claims-DVS5RkMS.mjs";
12
- import { a as withWorkspaceProcessLock, o as workspaceProcessLockError, r as releaseBrowserPort, s as workspaceProcessLockPath } from "./named-ports-4WopRPg-.mjs";
13
- 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-D1Icyjlu.mjs";
14
- import { t as agentDeviceDaemon, u as readAgentDeviceRecords } from "./activity-BSV745Kz.mjs";
15
- 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-RkW4pnQf.mjs";
16
- import { a as macosRuntimeClaim, n as macosDir } from "./state-CG_OFoaA.mjs";
17
- import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync } from "node:fs";
18
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
19
- import { basename as basename$1, isAbsolute as isAbsolute$1, join as join$1, relative as relative$1, resolve as resolve$1 } from "path";
20
- import { buildSlotsDir, hostedNativeRecords, isJsonObject, macosAppState, parseHostedNativePlacement, readJsonFile, readMacosRecord, requireAutomaticMachine, tryAcquireBuildSlotClaim, unreadableHostedNative } from "@stim-cli/core/state";
21
- 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";
5
+ import { a as formatElapsed, l as phaseLine } from "./command-output-B1UrjZHL.mjs";
6
+ import { l as withWorkspaceStateLockAsync, r as readWorkspaceState$1, u as workspaceLastUsed } from "./workspace-state-Cjuigeeu.mjs";
7
+ import { It as repoRootAsync, Qt as cancellableSleep, en as throwIfCancelled, ut as resolveTailscaleBinary } from "./settings-CsJDEXgK.mjs";
8
+ import { a as macosRuntimeClaim, n as macosDir } from "./state-XMziexv2.mjs";
9
+ import { l as pidExists, m as signalProcessTreeAsync, p as signalProcessTree } from "./metro-DEXpIhHC.mjs";
10
+ import { _ as chromeProcessStateAsync, c as webClaimRoot, i as clearWebRecord, o as readWebRecord, p as webProfileDir, t as CDP_PORT_LABEL, u as webFacts, v as liveProfileHolder, y as removeSingletonFiles } from "./state-C4ZtUisW.mjs";
11
+ import { t as canonicalPath } from "./paths-BsJMoPP2.mjs";
12
+ import { N as listBuildLocks, d as readAgentDeviceRecords, t as agentDeviceDaemon } from "./activity-BzVfHSrY.mjs";
13
+ import { n as readCreatedDevices$1, r as recordCreatedDevice, t as forgetCreatedDevice } from "./created-devices-CqeBlrEn.mjs";
14
+ import { A as listAdbDevicesAsync, B as ownedAvdMatchesConfiguration, F as listOrphanedAvdDirectories, Q as resolveOwnedAvdSerialAsync, W as ownedAvdSystemImage, _ as avdStorageRoots, _t as removeParkedAfter, at as wipeAvdUserData, et as shutdownAndroidEmulator, f as assertOwnedAvdStopped, ft as eraseParkedAfter, gt as readParked, h as avdPathExists, mt as parkSim, rt as waitForAndroidEmulatorShutdown, st as isStimOwnedAvd, x as deleteAvd, z as ownedAvdDirectory } from "./android-DUYkNF4u.mjs";
15
+ import { E as resolveOwnedIosSim, O as shutdownIosSim, S as renameIosSim, c as eraseIosSim, f as listAllIosSimsAsync, o as deleteIosSim, p as listIosDeviceTypesAsync, s as deleteParkedIosSim, v as parkedSimName, y as parseRuntimeVersion } from "./ios-CpByZy12.mjs";
16
+ import { n as workspaceProcessLockError, r as workspaceProcessLockPath, t as withWorkspaceProcessLock } from "./workspace-process-lock-1_1OoVW5.mjs";
17
+ import { r as releaseBrowserPort } from "./named-ports-BWTzt6mu.mjs";
22
18
  import { createHash, randomUUID } from "node:crypto";
19
+ import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
20
+ import { isDeepStrictEqual } from "node:util";
23
21
  import { homedir } from "node:os";
22
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
23
+ import { ambientContext, ambientEnv, ambientWrite } from "@stim-cli/core";
24
+ import { buildSlotsDir, hostedNativeRecords, isJsonObject, macosAppState, parseHostedNativePlacement, readJsonFile, readMacosRecord, requireAutomaticMachine, tryAcquireBuildSlotClaim, unreadableHostedNative } from "@stim-cli/core/state";
25
+ import { basename as basename$1, isAbsolute as isAbsolute$1, join as join$1, relative as relative$1 } from "path";
24
26
  import chalk from "chalk";
25
- import { isDeepStrictEqual } from "node:util";
27
+ import { existsSync as existsSync$1, lstatSync as lstatSync$1, readdirSync as readdirSync$1, rmSync as rmSync$1, statSync as statSync$1 } from "fs";
26
28
  import { resolve4 } from "node:dns/promises";
27
29
  import { request } from "node:https";
30
+ //#region src/commands/gc/results.ts
31
+ const results = [];
32
+ function recordGcResult(kind, status, label, { id = null, bytes = null, detail = null } = {}) {
33
+ results.push({
34
+ kind,
35
+ status,
36
+ label,
37
+ id,
38
+ bytes,
39
+ detail
40
+ });
41
+ }
42
+ function takeGcResults() {
43
+ return results.splice(0);
44
+ }
45
+ const GC_OUTPUT = {
46
+ log: (line) => console.log(line),
47
+ record: recordGcResult,
48
+ fail: () => {
49
+ process.exitCode = 1;
50
+ }
51
+ };
52
+ //#endregion
28
53
  //#region src/engine/build-slots.ts
29
54
  const SLOT_PREFIX = "slot-";
30
55
  const SLOT_POLL_MS = 1e3;
31
56
  const SLOT_PROGRESS_MS = 3e4;
32
57
  const SLOT_CEILING_MS = 54e5;
33
- function sleepAsync$1(ms) {
34
- return new Promise((resolve) => setTimeout(resolve, ms));
35
- }
36
58
  function toRecord(holder) {
37
59
  const details = holder.details;
38
60
  return {
@@ -56,7 +78,7 @@ function tryAcquireBuildSlot({ max, root = null, logFile = null }) {
56
78
  }
57
79
  });
58
80
  if (!got) return null;
59
- declareSpawnsOn(got.claim);
81
+ declareSpawnsOn(ambientContext(), got.claim);
60
82
  return {
61
83
  acquired: true,
62
84
  path: got.path,
@@ -74,7 +96,11 @@ function tryAcquireBuildSlot({ max, root = null, logFile = null }) {
74
96
  function slotWaitingLine({ max, elapsedMs }) {
75
97
  return `${"build".padEnd(11)} waiting for a build slot (all ${max} in use, ${formatElapsed(elapsedMs)} elapsed) -- stim guide lifecycle concurrency`;
76
98
  }
77
- 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 }) {
99
+ async function acquireBuildSlot({ max, root = null, logFile = null, now = Date.now, out = () => {}, waitingFor = () => {}, signal, sleep = (ms) => cancellableSleep(ms, {
100
+ signal,
101
+ what: "build slot"
102
+ }), intervalMs = SLOT_POLL_MS, progressMs = SLOT_PROGRESS_MS, ceilingMs = SLOT_CEILING_MS, automatic = false }) {
103
+ throwIfCancelled(signal, "build slot");
78
104
  if (automatic) requireAutomaticMachine("build", "local");
79
105
  if (!max || max <= 0) return {
80
106
  acquired: true,
@@ -85,6 +111,7 @@ async function acquireBuildSlot({ max, root = null, logFile = null, now = Date.n
85
111
  let waited = false;
86
112
  try {
87
113
  for (;;) {
114
+ throwIfCancelled(signal, "build slot");
88
115
  if (automatic) requireAutomaticMachine("build", "local");
89
116
  const got = tryAcquireBuildSlot({
90
117
  max,
@@ -123,7 +150,7 @@ async function acquireBuildSlot({ max, root = null, logFile = null, now = Date.n
123
150
  }
124
151
  function releaseBuildSlot(handle) {
125
152
  if (!handle || handle.unlimited) return false;
126
- stopDeclaringSpawnsOn(handle.claim);
153
+ stopDeclaringSpawnsOn(ambientContext(), handle.claim);
127
154
  return (0, ownership_claim_exports.releaseClaim)(handle.claim);
128
155
  }
129
156
  function listBuildSlots() {
@@ -162,59 +189,49 @@ function listBuildSlots() {
162
189
  return slots;
163
190
  }
164
191
  //#endregion
165
- //#region src/supervisor/ownership.ts
166
- function resolveSupervisorTarget({ state, record, reservedPort, isAlive = pidExists, inspectIdentity = process_identity_exports.inspectProcessIdentity } = {}) {
167
- const statePid = numberOrNull(state?.pid);
168
- const recordPid = numberOrNull(record?.pid);
169
- const pid = statePid ?? recordPid;
170
- if (!pid) return { status: "none" };
171
- if (statePid && recordPid && statePid !== recordPid) return {
172
- status: "unverified",
173
- pid,
174
- reason: `workspace state.json records supervisor pid ${statePid} but the registry records pid ${recordPid}`
175
- };
176
- if (!isAlive(pid)) return {
177
- status: "stale",
178
- pid
179
- };
180
- const port = numberOrNull(state?.port) ?? numberOrNull(record?.port);
181
- if (reservedPort !== null && reservedPort !== void 0 && port !== null && port !== reservedPort) return {
182
- status: "unverified",
183
- pid,
184
- reason: `supervisor pid ${pid} records port ${port}, but this project reserved port ${reservedPort}`
185
- };
186
- if (reservedPort !== null && reservedPort !== void 0 && port === null) return {
187
- status: "unverified",
188
- pid,
189
- reason: `supervisor pid ${pid} has no recorded port, so it cannot be matched against reserved port ${reservedPort}`
190
- };
191
- const identityRecord = statePid ? state : record;
192
- if (statePid && recordPid && state?.processToken !== record?.processToken) return {
193
- status: "unverified",
194
- pid,
195
- reason: "workspace state and registry process identities disagree"
196
- };
197
- const identity = inspectIdentity(identityRecord);
198
- if (identity === "gone" || identity === "different") return {
199
- status: "stale",
200
- pid
201
- };
202
- if (identity !== "same") return {
203
- status: "unverified",
204
- pid,
205
- reason: "the recorded process identity could not be verified"
206
- };
192
+ //#region src/devices/report-only-usage.ts
193
+ function canonical$1(path) {
194
+ try {
195
+ return realpathSync.native(path);
196
+ } catch {
197
+ return resolve(path);
198
+ }
199
+ }
200
+ function parseDu(output) {
201
+ const sizes = /* @__PURE__ */ new Map();
202
+ for (const line of output.split("\n")) {
203
+ const match = /^(\d+)\t(.+?)\r?$/.exec(line);
204
+ if (match) sizes.set(resolve(match[2]), Number(match[1]) * 1024);
205
+ }
206
+ return sizes;
207
+ }
208
+ async function measureDu(args) {
209
+ let output = "";
210
+ let complete = true;
211
+ try {
212
+ output = await getExecutor().runFileAsync("du", args, { timeoutMs: 2e4 });
213
+ } catch (error) {
214
+ complete = false;
215
+ const failure = error;
216
+ if (failure.status === 1 && !failure.code && typeof failure.stdout === "string") output = failure.stdout;
217
+ }
207
218
  return {
208
- status: "ours",
209
- processToken: identityRecord.processToken,
210
- pid,
211
- port,
212
- mode: state?.mode ?? record?.mode ?? null,
213
- startedAt: state?.startedAt ?? record?.startedAt ?? null
219
+ sizes: parseDu(output),
220
+ complete
214
221
  };
215
222
  }
216
- function numberOrNull(v) {
217
- return typeof v === "number" && Number.isSafeInteger(v) && v > 0 ? v : null;
223
+ function writeUsageCache(file, value, signal) {
224
+ signal?.throwIfAborted();
225
+ const temporary = `${file}.${randomUUID()}.tmp`;
226
+ try {
227
+ mkdirSync(dirname(file), { recursive: true });
228
+ writeFileSync(temporary, JSON.stringify(value));
229
+ renameSync(temporary, file);
230
+ } catch {
231
+ try {
232
+ rmSync(temporary, { force: true });
233
+ } catch {}
234
+ }
218
235
  }
219
236
  //#endregion
220
237
  //#region src/engine/public-http-probe.ts
@@ -275,1012 +292,545 @@ async function probePublicHttp(url, signal) {
275
292
  });
276
293
  }
277
294
  //#endregion
278
- //#region src/engine/tunnel.ts
279
- function tunnelArgv(provider, port, ngrokUrl, logFile, tailscaleBinary = "tailscale") {
280
- if (provider === "tailscale") return {
281
- bin: tailscaleBinary,
282
- args: [
283
- "serve",
284
- `--https=${port}`,
285
- `http://127.0.0.1:${port}`
286
- ]
287
- };
288
- if (provider === "cloudflared") return {
289
- bin: "cloudflared",
290
- args: [
291
- "tunnel",
292
- "--url",
293
- `http://127.0.0.1:${port}`,
294
- ...logFile ? ["--logfile", logFile] : []
295
- ]
296
- };
297
- return {
298
- bin: "ngrok",
299
- args: [
300
- "http",
301
- String(port),
302
- `--log=${logFile || "stdout"}`,
303
- "--log-format=json",
304
- ...ngrokUrl ? ["--url", ngrokUrl] : []
305
- ]
306
- };
295
+ //#region src/devices/agent-device-cleanup.ts
296
+ function parseAgentDeviceSessions(output) {
297
+ const result = JSON.parse(output);
298
+ if (result?.success !== true || !Array.isArray(result.data?.sessions)) throw new Error("agent-device did not return a successful session list");
299
+ return result.data.sessions.flatMap((entry) => {
300
+ if (!entry || typeof entry !== "object") return [];
301
+ const row = entry;
302
+ const id = row.platform === "ios" ? row.device_udid : row.id;
303
+ 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 [];
304
+ return [{
305
+ name: typeof row.address === "string" && row.address.trim() ? row.address : row.name,
306
+ platform: row.platform,
307
+ id,
308
+ device: typeof row.device === "string" ? row.device : null,
309
+ createdAt: row.createdAt
310
+ }];
311
+ });
307
312
  }
308
- const CLOUDFLARED_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
309
- function parseCloudflaredLine(line) {
310
- const match = line.match(CLOUDFLARED_URL_RE);
311
- return match ? match[0] : null;
313
+ function isOwnDeviceSession(session, device, owner) {
314
+ if (session.platform !== device.platform || device.id !== null && session.id !== device.id) return false;
315
+ if (device.platform === "android" && session.device !== device.avdName.replaceAll("_", " ")) return false;
316
+ if (!owner) return true;
317
+ 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));
312
318
  }
313
- function parseNgrokLine(line) {
314
- let data;
319
+ function within(root, path) {
320
+ const rest = relative(root, path);
321
+ return rest === "" || !isAbsolute(rest) && rest.split(sep)[0] !== "..";
322
+ }
323
+ function canonical(path) {
315
324
  try {
316
- data = JSON.parse(line);
325
+ return realpathSync(path);
317
326
  } catch {
318
327
  return null;
319
328
  }
320
- if (typeof data !== "object" || data === null) return null;
321
- const url = data.url;
322
- return typeof url === "string" && /^https?:\/\//i.test(url) ? url : null;
323
- }
324
- function parseTailscaleLine(line) {
325
- return /^https:\/\/[^\s/]+\.ts\.net(:\d+)?\/?$/.test(line) ? line : null;
326
- }
327
- function parserFor(provider) {
328
- if (provider === "tailscale") return parseTailscaleLine;
329
- return provider === "cloudflared" ? parseCloudflaredLine : parseNgrokLine;
330
329
  }
331
- const URL_TIMEOUT_MS = 15e3;
332
- const REACHABLE_TIMEOUT_MS = 24e4;
333
- const REACHABLE_POLL_MS = 2e3;
334
- const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
335
- async function withManagedTunnelLock(root, fn, options = {}) {
336
- return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
337
- ...options,
338
- rejectOwnerPurposes: ["workspace removal"]
339
- });
340
- }
341
- async function withManagedTunnelRemovalLock(root, fn, options = {}) {
342
- return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
343
- ...options,
344
- ownerPurpose: "workspace removal"
345
- });
346
- }
347
- async function withManagedRemoteWorktreeLock(worktreeRoot, fn, options = {}) {
348
- return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
349
- ...options,
350
- external: true,
351
- rejectOwnerPurposes: ["worktree removal"]
352
- });
353
- }
354
- async function withManagedRemoteWorktreeRemovalLock(worktreeRoot, fn, options = {}) {
355
- return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
356
- ...options,
357
- external: true,
358
- ownerPurpose: "worktree removal"
359
- });
330
+ function deviceClaims(device) {
331
+ return readAgentDeviceRecords(ambientEnv().HOME || ambientEnv().USERPROFILE || homedir(), ambientEnv()).filter((record) => record.kind === "claim" && (device.id === null || record.deviceId === device.id)).map((record) => ({
332
+ deviceId: record.deviceId,
333
+ session: record.session,
334
+ workspace: record.workspace && canonical(record.workspace)
335
+ }));
360
336
  }
361
- function managedLockHolders(root) {
362
- const locks = [{
363
- label: "managed tunnel",
364
- path: workspaceProcessLockPath(managedTunnelLockRoot(root), "metro-tunnel", false)
365
- }, {
366
- label: "managed remote",
367
- path: workspaceProcessLockPath(managedRemoteWorktreeLockRoot(root), "managed-remote", true)
368
- }];
369
- const reasons = [];
370
- for (const { label, path } of locks) {
371
- const claims = (0, ownership_claim_exports.readClaimSet)(path);
372
- const holder = claims.live[0];
373
- if (holder) {
374
- const purpose = typeof holder.details.purpose === "string" ? ` for ${holder.details.purpose}` : "";
375
- reasons.push(`the ${label} lock is held${purpose}`);
376
- } else if (claims.unresolved[0]) reasons.push(`the ${label} lock cannot be resolved: ${claims.unresolved[0].reason}`);
337
+ function explainFailure(error) {
338
+ const stdout = error?.stdout;
339
+ let failure;
340
+ try {
341
+ failure = typeof stdout === "string" ? JSON.parse(stdout)?.error : void 0;
342
+ } catch {
343
+ failure = void 0;
377
344
  }
378
- return reasons;
379
- }
380
- function managedTunnelLockRoot(root) {
381
- const key = createHash("sha256").update(resolve(root)).digest("hex");
382
- return join(getConfigDir(), "process-locks", key);
383
- }
384
- function managedRemoteWorktreeLockRoot(worktreeRoot) {
385
- const key = createHash("sha256").update(resolve(worktreeRoot)).digest("hex");
386
- return join(getConfigDir(), "process-locks", "worktrees", key);
345
+ if (typeof failure?.message !== "string" || !failure.message) return error;
346
+ const reason = typeof failure.details?.reason === "string" ? ` (${failure.details.reason})` : "";
347
+ return /* @__PURE__ */ new Error(`agent-device refused: ${failure.message}${reason}`);
387
348
  }
388
- async function defaultProbeReachable(url, signal) {
389
- const status = await probePublicHttp(url, signal);
390
- return status !== null && status > 0;
349
+ function report(message) {
350
+ ambientWrite("stderr", `${phaseLine("device", message)}\n`);
391
351
  }
392
- function waitForUrl(child, parseLine, timeoutMs) {
393
- return new Promise((resolve) => {
394
- let settled = false;
395
- let lastLine = null;
396
- let timer;
397
- let onOut;
398
- let onErr;
399
- let onError;
400
- let onExit;
401
- const finish = (url, exited = false) => {
402
- if (settled) return;
403
- settled = true;
404
- clearTimeout(timer);
405
- child.stdout?.removeListener("data", onOut);
406
- child.stderr?.removeListener("data", onErr);
407
- child.removeListener("error", onError);
408
- child.removeListener("exit", onExit);
409
- resolve({
410
- url,
411
- exited,
412
- lastLine
413
- });
414
- };
415
- const onLine = (line) => {
416
- if (settled) return;
417
- if (line.trim()) lastLine = line.trim();
418
- const url = parseLine(line);
419
- if (url) finish(url);
420
- };
421
- const outReader = createLineReader(onLine);
422
- const errReader = createLineReader(onLine);
423
- onOut = (chunk) => outReader.push(chunk);
424
- onErr = (chunk) => errReader.push(chunk);
425
- onError = () => finish(null);
426
- onExit = () => {
427
- outReader.flush();
428
- errReader.flush();
429
- finish(null, true);
430
- };
431
- child.stdout?.setEncoding?.("utf-8");
432
- child.stderr?.setEncoding?.("utf-8");
433
- child.stdout?.on("data", onOut);
434
- child.stderr?.on("data", onErr);
435
- child.on("error", onError);
436
- child.on("exit", onExit);
437
- timer = setTimeout(() => finish(null), timeoutMs);
438
- });
439
- }
440
- function waitForFileUrl(child, logFile, parseLine, timeoutMs) {
441
- return new Promise((resolve) => {
442
- let settled = false;
443
- const finish = (url, exited = false) => {
444
- if (settled) return;
445
- settled = true;
446
- clearInterval(interval);
447
- clearTimeout(timer);
448
- child.removeListener("error", onError);
449
- child.removeListener("exit", onExit);
450
- resolve({
451
- url,
452
- exited
453
- });
352
+ async function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot) {
353
+ const exec = getExecutor();
354
+ try {
355
+ if (!exec.findExecutable("agent-device")) return;
356
+ const root = workspace === void 0 ? void 0 : canonical(workspace);
357
+ if (root === null) return;
358
+ const owner = root === void 0 ? void 0 : {
359
+ workspace: root,
360
+ repoRoot: repoRoot && (canonical(repoRoot) ?? void 0),
361
+ claims: deviceClaims(device)
454
362
  };
455
- const probe = () => {
363
+ const stateDir = root === void 0 ? void 0 : workspaceAgentDeviceDir(root);
364
+ const stateDirs = [void 0, ...stateDir && !["none", "dead"].includes(agentDeviceDaemon(stateDir)) ? [stateDir] : []];
365
+ for (const dir of stateDirs) {
366
+ const deadline = Date.now() + 15e3;
367
+ const run = async (args) => {
368
+ const remaining = deadline - Date.now();
369
+ if (remaining <= 0) throw new Error("agent-device cleanup timed out");
370
+ try {
371
+ return await exec.runFileAsync("agent-device", [
372
+ ...args,
373
+ ...dir ? ["--state-dir", dir] : [],
374
+ "--json",
375
+ "--daemon-transport",
376
+ "socket"
377
+ ], {
378
+ timeoutMs: Math.min(5e3, remaining),
379
+ killSignal: "SIGKILL"
380
+ });
381
+ } catch (error) {
382
+ throw explainFailure(error);
383
+ }
384
+ };
385
+ const list = async () => parseAgentDeviceSessions(await run([
386
+ "session",
387
+ "list",
388
+ "--session",
389
+ "stim-teardown-inventory"
390
+ ]));
456
391
  try {
457
- for (const line of readFileSync(logFile, "utf-8").split(/\r?\n/)) {
458
- const url = parseLine(line);
459
- if (url) return finish(url);
392
+ const sessions = (await list()).filter((session) => isOwnDeviceSession(session, device, owner));
393
+ for (const session of sessions) try {
394
+ if (!(await list()).some((current) => current.name === session.name && current.platform === session.platform && current.id === session.id && current.createdAt === session.createdAt)) continue;
395
+ if (!await stillOwned(session.id)) continue;
396
+ if (JSON.parse(await run([
397
+ "close",
398
+ "--session",
399
+ session.name,
400
+ "--session-lock",
401
+ "reject",
402
+ "--platform",
403
+ device.platform,
404
+ device.platform === "ios" ? "--udid" : "--serial",
405
+ session.id
406
+ ]))?.success !== true) throw new Error("agent-device did not confirm session close");
407
+ report(`closed agent-device session ${session.name} on ${session.id}`);
408
+ } catch (error) {
409
+ report(`could not close agent-device session ${session.name} on ${session.id}: ${error.message}`);
460
410
  }
461
- } catch {}
462
- };
463
- const onError = () => finish(null);
464
- const onExit = () => finish(null, true);
465
- child.on("error", onError);
466
- child.on("exit", onExit);
467
- const interval = setInterval(probe, 25);
468
- const timer = setTimeout(() => finish(null), timeoutMs);
469
- probe();
470
- });
471
- }
472
- function createTunnelLogFile(provider) {
473
- const dir = join(getConfigDir(), "tunnel-logs");
474
- mkdirSync(dir, { recursive: true });
475
- const path = join(dir, `${provider}-${randomUUID()}.log`);
476
- closeSync(openSync(path, "wx", 384));
477
- return path;
411
+ } catch (error) {
412
+ report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
413
+ }
414
+ }
415
+ } catch (error) {
416
+ report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
417
+ }
478
418
  }
479
- function removeTunnelLogFile(path) {
480
- if (!path) return;
419
+ //#endregion
420
+ //#region src/devices/teardown.ts
421
+ async function teardownParkedAvd(avdName) {
481
422
  try {
482
- unlinkSync(path);
483
- } catch {}
484
- }
485
- function removeRecordedTunnelLogFile(path) {
486
- if (!path) return;
487
- const dir = resolve(join(getConfigDir(), "tunnel-logs"));
488
- const resolved = resolve(path);
489
- if (!resolved.startsWith(`${dir}${sep}`) || resolve(join(dir, basename(resolved))) !== resolved) return;
490
- removeTunnelLogFile(resolved);
491
- }
492
- async function waitUntilReachable({ url, now, sleep, probe, timeoutMs }) {
493
- const deadline = now() + timeoutMs;
494
- for (;;) {
495
- if (await probe(url, new AbortController().signal).catch(() => false)) return true;
496
- if (now() >= deadline) return false;
497
- await sleep(REACHABLE_POLL_MS);
423
+ return await removeParkedAfter("android", avdName, async () => {
424
+ const result = await teardownOwnedAvd(avdName, {
425
+ del: true,
426
+ owner: { pool: true }
427
+ });
428
+ if (result.status !== "torn-down" && result.status !== "missing") throw new Error(result.reason);
429
+ }) ? {
430
+ status: "torn-down",
431
+ label: avdName
432
+ } : {
433
+ status: "skipped",
434
+ kind: "not-parked",
435
+ reason: "emulator is no longer parked"
436
+ };
437
+ } catch (error) {
438
+ return {
439
+ status: "failed",
440
+ reason: String(error?.message || error)
441
+ };
498
442
  }
499
443
  }
500
- const CLEANUP_TIMEOUT_MS = 1e3;
501
- const CLEANUP_POLL_MS = 25;
502
- 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 }) {
503
- const spawn = spawnFn || ((cmd, args, opts) => getExecutor().spawn(cmd, args, opts));
504
- const outputFile = provider === "tailscale" ? null : logFile || (spawnFn ? null : createTunnelLogFile(provider));
505
- let child;
444
+ async function teardownParkedIosSim(udid, { label, deleteSim = deleteParkedIosSim } = {}) {
506
445
  try {
507
- const binary = provider === "tailscale" ? resolveTailscaleBinary((bin) => getExecutor().findExecutable(bin)) : provider;
508
- if (!binary) throw new Error("Install Tailscale and sign in to your tailnet.");
509
- const { bin, args } = tunnelArgv(provider, port, ngrokUrl, outputFile, binary);
510
- child = spawn(bin, args, {
511
- stdio: outputFile ? [
512
- "ignore",
513
- "ignore",
514
- "ignore"
515
- ] : [
516
- "ignore",
517
- "pipe",
518
- "pipe"
519
- ],
520
- detached: true
446
+ const removed = await removeParkedAfter("ios", udid, async () => {
447
+ await closeOwnedDeviceSessions({
448
+ platform: "ios",
449
+ id: udid
450
+ }, async () => Boolean((await resolveOwnedIosSimNow(udid)).sim));
451
+ await deleteSim(udid);
521
452
  });
522
- } catch (err) {
523
- removeTunnelLogFile(outputFile);
453
+ if (!removed) return {
454
+ status: "skipped",
455
+ kind: "not-parked",
456
+ reason: "simulator is no longer parked"
457
+ };
524
458
  return {
525
- failed: true,
526
- reason: `Could not start ${provider}: ${describe(err)}`
459
+ status: "torn-down",
460
+ label: label ?? removed.name
461
+ };
462
+ } catch (error) {
463
+ return {
464
+ status: "failed",
465
+ reason: String(error?.message || error)
527
466
  };
528
467
  }
529
- let childExited = false;
530
- child.once("exit", () => {
531
- childExited = true;
532
- });
533
- const cleanup = async (alreadyExited = childExited) => {
534
- const stopped = await terminateChild(child, {
535
- alreadyExited,
536
- timeoutMs: cleanupTimeoutMs,
537
- now,
538
- sleep,
539
- isAlive: isChildAlive
468
+ }
469
+ async function eraseParkedIosSim(udid, { label } = {}) {
470
+ try {
471
+ const erased = await eraseParkedAfter("ios", udid, async (_record, eraseStarted) => {
472
+ await closeOwnedDeviceSessions({
473
+ platform: "ios",
474
+ id: udid
475
+ }, async () => Boolean((await resolveOwnedIosSimNow(udid)).sim));
476
+ const resolved = resolveOwnedIosSim(udid, await listAllIosSimsAsync());
477
+ if (resolved.missing) throw new Error(`simulator ${udid} is not on this machine`);
478
+ if (resolved.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(resolved.notOwned)}`);
479
+ const state = resolved.sim.state;
480
+ if (state !== "Shutdown") throw new Error(`simulator ${udid} is ${state}, not Shutdown; it was kept`);
481
+ eraseStarted();
482
+ await eraseIosSim(udid);
540
483
  });
541
- if (stopped) removeTunnelLogFile(outputFile);
542
- return stopped ? { status: "stopped" } : {
484
+ if (!erased) return {
485
+ status: "skipped",
486
+ kind: "not-parked",
487
+ reason: "simulator is no longer parked"
488
+ };
489
+ return {
490
+ status: "torn-down",
491
+ label: label ?? erased.name
492
+ };
493
+ } catch (error) {
494
+ return {
543
495
  status: "failed",
544
- reason: `Sent SIGKILL but could not confirm that pid ${child.pid ?? "unknown"} exited.`
496
+ reason: String(error?.message || error)
545
497
  };
546
- };
547
- const failAfterCleanup = async (reason, alreadyExited = childExited) => {
548
- const stopped = await cleanup(alreadyExited);
549
- return stopped.status === "stopped" ? {
550
- failed: true,
551
- reason
498
+ }
499
+ }
500
+ async function eraseParkedAvd(avdName) {
501
+ try {
502
+ return await eraseParkedAfter("android", avdName, async (_record, eraseStarted) => {
503
+ if ((await resolveOwnedAvdSerialAsync(avdName, { timeoutMs: AVD_INVENTORY_TIMEOUT_MS })).serial) throw new Error(`AVD ${avdName} is running; it was kept`);
504
+ const result = await teardownOwnedAvd(avdName, {
505
+ wipe: true,
506
+ owner: { pool: true },
507
+ onWipeStart: eraseStarted
508
+ });
509
+ if (result.status !== "torn-down") throw new Error(result.reason ?? `AVD ${avdName} is ${result.status}`);
510
+ }) ? {
511
+ status: "torn-down",
512
+ label: avdName
552
513
  } : {
553
- failed: true,
554
- reason: `${reason} ${stopped.reason}`,
555
- cleanupFailed: true
514
+ status: "skipped",
515
+ kind: "not-parked",
516
+ reason: "emulator is no longer parked"
556
517
  };
557
- };
558
- const pid = child.pid;
559
- if (!pid) return failAfterCleanup(`${provider} started but reported no pid.`);
560
- const captureChildIdentity = () => {
561
- try {
562
- return readProcessToken(pid);
563
- } catch {
564
- return null;
565
- }
566
- };
567
- const initialProcessToken = captureChildIdentity();
568
- if (!initialProcessToken) return failAfterCleanup(`${provider} started but its process identity token could not be read.`);
569
- const { url, exited, lastLine } = outputFile ? await waitForFileUrl(child, outputFile, parserFor(provider), urlTimeoutMs) : await waitForUrl(child, parserFor(provider), urlTimeoutMs);
570
- 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);
571
- if (!outputFile) resumeChildPipes(child);
572
- if (requireReachable) {
573
- if (!await waitUntilReachable({
574
- url,
575
- now,
576
- sleep,
577
- probe: probeReachable,
578
- timeoutMs: reachableTimeoutMs
579
- })) return failAfterCleanup(`${url} did not become reachable within ${reachableTimeoutMs}ms; ${provider} may still be registering, or the network path is blocked.`);
580
- }
581
- const finalProcessToken = captureChildIdentity();
582
- if (!finalProcessToken || finalProcessToken !== initialProcessToken) return failAfterCleanup(`${provider} process identity changed before its tunnel could be recorded.`);
583
- if (!outputFile) unrefChildPipes(child);
584
- child.unref?.();
585
- if (childExited) return failAfterCleanup(`${provider} exited before its tunnel could be recorded.`, true);
586
- return {
587
- url,
588
- pid,
589
- processToken: initialProcessToken,
590
- logFile: outputFile,
591
- cleanup: () => cleanup()
592
- };
593
- }
594
- async function startTunnelSequence({ providers, port, ngrokUrl = null, requireReachable = true, start = startTunnel }) {
595
- const failures = [];
596
- for (const provider of providers) {
597
- const result = await start({
598
- provider,
599
- port,
600
- ngrokUrl: provider === "ngrok" ? ngrokUrl : null,
601
- requireReachable
602
- });
603
- if (!("failed" in result)) return {
604
- provider,
605
- ...result
606
- };
607
- failures.push(`${provider}: ${result.reason}`);
608
- if (result.cleanupFailed) return {
609
- failed: true,
610
- reason: failures.join(" ")
518
+ } catch (error) {
519
+ return {
520
+ status: "failed",
521
+ reason: String(error?.message || error)
611
522
  };
612
523
  }
613
- return {
614
- failed: true,
615
- reason: failures.join(" ") || "No managed tunnel provider was selected."
616
- };
617
- }
618
- function closeChildPipes(child) {
619
- child.stdout?.destroy?.();
620
- child.stderr?.destroy?.();
621
- }
622
- function unrefChildPipes(child) {
623
- resumeChildPipes(child);
624
- for (const stream of [child.stdout, child.stderr]) stream?.unref?.();
625
- }
626
- function resumeChildPipes(child) {
627
- child.stdout?.resume?.();
628
- child.stderr?.resume?.();
629
524
  }
630
- async function signalAndWaitForExit(child, signal, { timeoutMs, now, sleep, isAlive, platform }) {
631
- let exited = false;
632
- const onExit = () => {
633
- exited = true;
634
- };
635
- child.once("exit", onExit);
525
+ const IOS_SHUTDOWN_SETTLE_MS = 15e3;
526
+ const ADB_RELEASE_WAIT_MS = 3e3;
527
+ const AVD_INVENTORY_TIMEOUT_MS = 15e3;
528
+ async function repoRootAbove(workspace) {
529
+ if (workspace === void 0 || !getExecutor().findExecutable("agent-device")) return void 0;
530
+ const root = await repoRootAsync(workspace);
636
531
  try {
637
- if (platform === "win32" && child.pid) signalProcessTree(child.pid, signal, { platform });
638
- child.kill(signal);
639
- } catch (err) {
640
- child.removeListener("exit", onExit);
641
- return isEsrch(err);
532
+ return root && realpathSync(root) !== realpathSync(workspace) ? root : void 0;
533
+ } catch {
534
+ return;
642
535
  }
643
- const hasExited = () => exited;
644
- const deadline = now() + timeoutMs;
645
- while (!hasExited() && now() < deadline) await sleep(CLEANUP_POLL_MS);
646
- if (!hasExited() && child.pid && !isAlive(child.pid)) exited = true;
647
- child.removeListener("exit", onExit);
648
- return hasExited();
649
536
  }
650
- async function terminateChild(child, { alreadyExited, timeoutMs, now, sleep, isAlive, platform = process.platform }) {
651
- if (alreadyExited) {
652
- closeChildPipes(child);
653
- return true;
654
- }
655
- if (await signalAndWaitForExit(child, "SIGTERM", {
656
- timeoutMs,
657
- now,
658
- sleep,
659
- isAlive,
660
- platform
661
- })) {
662
- closeChildPipes(child);
663
- return true;
664
- }
665
- const killed = await signalAndWaitForExit(child, "SIGKILL", {
666
- timeoutMs,
667
- now,
668
- sleep,
669
- isAlive,
670
- platform
671
- });
672
- closeChildPipes(child);
673
- return killed;
537
+ const IOS_SHUTDOWN_POLL_MS = 250;
538
+ const IOS_SHUTDOWN_ATTEMPTS = 2;
539
+ async function resolveOwnedIosSimNow(udid) {
540
+ return resolveOwnedIosSim(udid, await listAllIosSimsAsync());
674
541
  }
675
- function describe(err) {
676
- return err?.message || String(err);
542
+ async function settleIosSimShutdown(udid, { now = Date.now, sleep = sleepAsync }) {
543
+ let resolved = await resolveOwnedIosSimNow(udid);
544
+ for (let attempt = 1;; attempt++) {
545
+ const deadline = now() + IOS_SHUTDOWN_SETTLE_MS;
546
+ while (resolved.sim && resolved.sim.state !== "Shutdown" && now() < deadline) {
547
+ await sleep(IOS_SHUTDOWN_POLL_MS);
548
+ resolved = await resolveOwnedIosSimNow(udid);
549
+ }
550
+ if (!resolved.sim || resolved.sim.state === "Shutdown" || attempt === IOS_SHUTDOWN_ATTEMPTS) return resolved;
551
+ await shutdownIosSim(udid);
552
+ resolved = await resolveOwnedIosSimNow(udid);
553
+ }
677
554
  }
678
- const STOP_TIMEOUT_MS = 5e3;
679
- const STOP_POLL_MS = 100;
680
- function isEsrch(err) {
681
- return err?.code === "ESRCH";
555
+ function iosShutdownFailureReason(udid, state) {
556
+ return `simulator ${udid} is still ${state} after ${IOS_SHUTDOWN_ATTEMPTS} shutdown attempts and ${IOS_SHUTDOWN_ATTEMPTS * IOS_SHUTDOWN_SETTLE_MS / 1e3}s of waiting`;
682
557
  }
683
- 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 } = {}) {
684
- const missing = () => {
685
- removeRecordedTunnelLogFile(record?.logFile);
686
- return { status: "missing" };
687
- };
688
- const pid = record?.pid;
689
- if (!pid || !isAlive(pid)) return missing();
690
- if (!record.processToken) return {
691
- status: "failed",
692
- 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.`
558
+ async function parkOwnedIosSim(settled, udid, park) {
559
+ if (settled.missing) throw new Error(`simulator ${udid} disappeared after shutdown`);
560
+ if (settled.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(settled.notOwned)}`);
561
+ const sim = settled.sim;
562
+ if (sim.state !== "Shutdown") throw new Error(iosShutdownFailureReason(udid, sim.state));
563
+ const model = (await listIosDeviceTypesAsync()).find((d) => d.identifier === sim.deviceTypeIdentifier)?.name ?? null;
564
+ const runtime = parseRuntimeVersion(sim.runtime);
565
+ const name = parkedSimName(sim.udid, {
566
+ model,
567
+ runtime
568
+ });
569
+ await renameIosSim(sim.udid, name);
570
+ const record = {
571
+ udid: sim.udid,
572
+ name,
573
+ deviceTypeIdentifier: sim.deviceTypeIdentifier,
574
+ runtimeIdentifier: sim.runtime,
575
+ parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
576
+ simslimManaged: Boolean(park.simslimManaged),
577
+ ...park.appId ? { bundleId: park.appId } : {},
578
+ ...park.cacheKey ? { cacheKey: park.cacheKey } : {}
693
579
  };
694
- const identity = inspectIdentity(record);
695
- if (identity === "gone" || identity === "different") return missing();
696
- if (identity !== "same") return {
697
- status: "failed",
698
- reason: `could not verify the process identity for tunnel pid ${pid}; refusing to signal it.`
580
+ return {
581
+ record,
582
+ evicted: parkSim({
583
+ platform: "ios",
584
+ projectPath: park.projectPath,
585
+ slot: park.slot,
586
+ record,
587
+ max: park.max
588
+ })
699
589
  };
590
+ }
591
+ async function teardownOwnedIosSim(udid, { del = false, label, park, workspace, shutdownClock = {} } = {}) {
592
+ let parkFallback;
700
593
  try {
701
- kill(pid);
702
- } catch (err) {
703
- return isEsrch(err) ? missing() : {
594
+ const resolved = await resolveOwnedIosSimNow(udid);
595
+ if (resolved.notOwned) return {
596
+ status: "skipped",
597
+ kind: "not-owned",
598
+ reason: `sim "${resolved.notOwned}" is not Stim-owned: Stim has no record of creating it`
599
+ };
600
+ if (resolved.missing) return { status: "missing" };
601
+ await closeOwnedDeviceSessions({
602
+ platform: "ios",
603
+ id: udid
604
+ }, async () => Boolean((await resolveOwnedIosSimNow(udid)).sim), workspace);
605
+ const current = await resolveOwnedIosSimNow(udid);
606
+ if (current.missing) return { status: "missing" };
607
+ if (current.notOwned) return {
608
+ status: "skipped",
609
+ kind: "not-owned",
610
+ reason: "simulator ownership changed before shutdown"
611
+ };
612
+ await shutdownIosSim(udid);
613
+ const sim = resolved.sim;
614
+ if (del && park && park.max > 0) try {
615
+ const { record, evicted } = await parkOwnedIosSim(await settleIosSimShutdown(udid, shutdownClock), udid, park);
616
+ const removed = [];
617
+ const failures = [];
618
+ for (const entry of evicted) {
619
+ const result = await teardownParkedIosSim(entry.udid, { label: entry.name });
620
+ if (result.status === "torn-down") removed.push({
621
+ udid: entry.udid,
622
+ name: entry.name
623
+ });
624
+ else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name} (${entry.udid}): ${result.reason}`);
625
+ }
626
+ return {
627
+ status: "torn-down",
628
+ label: label ?? sim.name ?? udid,
629
+ parked: {
630
+ udid: record.udid,
631
+ name: record.name
632
+ },
633
+ evicted: removed,
634
+ ...failures.length ? { evictionFailures: failures } : {}
635
+ };
636
+ } catch (e) {
637
+ parkFallback = String(e?.message || e);
638
+ }
639
+ if (del) {
640
+ await deleteIosSim(udid);
641
+ return {
642
+ status: "torn-down",
643
+ label: label ?? sim.name ?? udid,
644
+ ...parkFallback ? { parkFallback } : {}
645
+ };
646
+ }
647
+ const settled = await settleIosSimShutdown(udid, shutdownClock);
648
+ if (settled.sim && settled.sim.state !== "Shutdown") return {
704
649
  status: "failed",
705
- reason: describe(err)
650
+ label: label ?? sim.name ?? udid,
651
+ reason: iosShutdownFailureReason(udid, settled.sim.state)
652
+ };
653
+ const root = sim.state === "Shutdown" ? void 0 : await repoRootAbove(workspace);
654
+ if (root && settled.sim) await closeOwnedDeviceSessions({
655
+ platform: "ios",
656
+ id: udid
657
+ }, async () => (await resolveOwnedIosSimNow(udid)).sim?.state === "Shutdown", workspace, root);
658
+ return {
659
+ status: "torn-down",
660
+ label: label ?? sim.name ?? udid
661
+ };
662
+ } catch (e) {
663
+ return {
664
+ status: "failed",
665
+ reason: String(e?.message || e),
666
+ ...parkFallback ? { parkFallback } : {}
706
667
  };
707
- }
708
- const deadline = now() + timeoutMs;
709
- const exited = () => {
710
- if (!isAlive(pid)) return true;
711
- const current = inspectIdentity(record);
712
- return current === "gone" || current === "different";
713
- };
714
- while (now() < deadline && !exited()) await sleep(STOP_POLL_MS);
715
- if (!exited()) return {
716
- status: "failed",
717
- reason: `pid ${pid} did not exit within ${timeoutMs}ms.`
718
- };
719
- removeRecordedTunnelLogFile(record.logFile);
720
- return { status: "stopped" };
721
- }
722
- //#endregion
723
- //#region src/commands/gc/paths.ts
724
- function canonicalPath(p) {
725
- try {
726
- return realpathSync$1(p);
727
- } catch {
728
- return resolve$1(p);
729
668
  }
730
669
  }
731
- //#endregion
732
- //#region src/commands/gc/results.ts
733
- const results = [];
734
- function recordGcResult(kind, status, label, { id = null, bytes = null, detail = null } = {}) {
735
- results.push({
736
- kind,
737
- status,
738
- label,
739
- id,
740
- bytes,
741
- detail
742
- });
743
- }
744
- function takeGcResults() {
745
- return results.splice(0);
670
+ function assertAvdReferences(avdName, owner) {
671
+ const config = loadConfig$1();
672
+ if (!config) throw new Error("Cannot verify AVD references without a Stim config; the data was kept.");
673
+ 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.`);
674
+ for (const [projectPath, project] of Object.entries(config.projects)) for (const { slot, platforms } of projectDeviceSlots(project)) {
675
+ if (platforms.android?.avdName?.toLowerCase() !== avdName.toLowerCase()) continue;
676
+ 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.`);
677
+ }
678
+ 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.`);
746
679
  }
747
- //#endregion
748
- //#region src/devices/agent-device-cleanup.ts
749
- function parseAgentDeviceSessions(output) {
750
- const result = JSON.parse(output);
751
- if (result?.success !== true || !Array.isArray(result.data?.sessions)) throw new Error("agent-device did not return a successful session list");
752
- return result.data.sessions.flatMap((entry) => {
753
- if (!entry || typeof entry !== "object") return [];
754
- const row = entry;
755
- const id = row.platform === "ios" ? row.device_udid : row.id;
756
- 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 [];
757
- return [{
758
- name: typeof row.address === "string" && row.address.trim() ? row.address : row.name,
759
- platform: row.platform,
760
- id,
761
- device: typeof row.device === "string" ? row.device : null,
762
- createdAt: row.createdAt
763
- }];
680
+ function removeOrphanedAvdDirectory(candidate, owner) {
681
+ const surveyed = listOrphanedAvdDirectories(candidate.name).find((entry) => entry.directory === candidate.directory);
682
+ 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.`);
683
+ assertOwnedAvdStopped(candidate.name, { resolveDirectory: () => candidate.directory });
684
+ const detached = withConfigLock$1(() => {
685
+ const current = lstatSync(candidate.directory);
686
+ 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.`);
687
+ assertAvdReferences(candidate.name, owner);
688
+ const name = `stim-gc-${randomUUID()}`;
689
+ recordCreatedDevice("android", name);
690
+ const destination = join(dirname(candidate.directory), `${name}.avd`);
691
+ try {
692
+ renameSync(candidate.directory, destination);
693
+ } catch (error) {
694
+ forgetCreatedDevice("android", name);
695
+ throw error;
696
+ }
697
+ forgetCreatedDevice("android", candidate.name);
698
+ return destination;
764
699
  });
765
- }
766
- function isOwnDeviceSession(session, device, owner) {
767
- if (session.platform !== device.platform || device.id !== null && session.id !== device.id) return false;
768
- if (device.platform === "android" && session.device !== device.avdName.replaceAll("_", " ")) return false;
769
- if (!owner) return true;
770
- 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));
771
- }
772
- function within(root, path) {
773
- const rest = relative(root, path);
774
- return rest === "" || !isAbsolute(rest) && rest.split(sep)[0] !== "..";
775
- }
776
- function canonical(path) {
700
+ const current = lstatSync(detached);
701
+ if (!current.isDirectory() || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`Detached AVD data changed at ${detached}; it was kept.`);
777
702
  try {
778
- return realpathSync(path);
779
- } catch {
780
- return null;
703
+ rmSync(detached, { recursive: true });
704
+ } catch (error) {
705
+ throw new Error(`Could not remove AVD data at ${detached}; retry gc --delete: ${error.message}`, { cause: error });
781
706
  }
707
+ forgetCreatedDevice("android", basename(detached, ".avd"));
782
708
  }
783
- function deviceClaims(device) {
784
- return readAgentDeviceRecords(homedir()).filter((record) => record.kind === "claim" && (device.id === null || record.deviceId === device.id)).map((record) => ({
785
- deviceId: record.deviceId,
786
- session: record.session,
787
- workspace: record.workspace && canonical(record.workspace)
788
- }));
789
- }
790
- function explainFailure(error) {
791
- const stdout = error?.stdout;
792
- let failure;
793
- try {
794
- failure = typeof stdout === "string" ? JSON.parse(stdout)?.error : void 0;
795
- } catch {
796
- failure = void 0;
709
+ function teardownUnregisteredAvd(avdName, del, owner, surveyed) {
710
+ const candidates = surveyed ? [surveyed] : listOrphanedAvdDirectories(avdName);
711
+ if (candidates.length === 0) {
712
+ 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.`);
713
+ return { status: "missing" };
797
714
  }
798
- if (typeof failure?.message !== "string" || !failure.message) return error;
799
- const reason = typeof failure.details?.reason === "string" ? ` (${failure.details.reason})` : "";
800
- return /* @__PURE__ */ new Error(`agent-device refused: ${failure.message}${reason}`);
801
- }
802
- function report(message) {
803
- process.stderr.write(`${phaseLine("device", message)}\n`);
804
- }
805
- function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot) {
806
- const exec = getExecutor();
807
- try {
808
- if (!exec.findExecutable("agent-device")) return;
809
- const root = workspace === void 0 ? void 0 : canonical(workspace);
810
- if (root === null) return;
811
- const owner = root === void 0 ? void 0 : {
812
- workspace: root,
813
- repoRoot: repoRoot && (canonical(repoRoot) ?? void 0),
814
- claims: deviceClaims(device)
815
- };
816
- const stateDir = root === void 0 ? void 0 : workspaceAgentDeviceDir(root);
817
- const stateDirs = [void 0, ...stateDir && !["none", "dead"].includes(agentDeviceDaemon(stateDir)) ? [stateDir] : []];
818
- for (const dir of stateDirs) {
819
- const deadline = Date.now() + 15e3;
820
- const run = (args) => {
821
- const remaining = deadline - Date.now();
822
- if (remaining <= 0) throw new Error("agent-device cleanup timed out");
823
- try {
824
- return exec.runFile("agent-device", [
825
- ...args,
826
- ...dir ? ["--state-dir", dir] : [],
827
- "--json",
828
- "--daemon-transport",
829
- "socket"
830
- ], {
831
- timeoutMs: Math.min(5e3, remaining),
832
- killSignal: "SIGKILL"
833
- });
834
- } catch (error) {
835
- throw explainFailure(error);
836
- }
837
- };
838
- const list = () => parseAgentDeviceSessions(run([
839
- "session",
840
- "list",
841
- "--session",
842
- "stim-teardown-inventory"
843
- ]));
844
- try {
845
- const sessions = list().filter((session) => isOwnDeviceSession(session, device, owner));
846
- for (const session of sessions) try {
847
- if (!list().some((current) => current.name === session.name && current.platform === session.platform && current.id === session.id && current.createdAt === session.createdAt)) continue;
848
- if (!stillOwned(session.id)) continue;
849
- if (JSON.parse(run([
850
- "close",
851
- "--session",
852
- session.name,
853
- "--session-lock",
854
- "reject",
855
- "--platform",
856
- device.platform,
857
- device.platform === "ios" ? "--udid" : "--serial",
858
- session.id
859
- ]))?.success !== true) throw new Error("agent-device did not confirm session close");
860
- report(`closed agent-device session ${session.name} on ${session.id}`);
861
- } catch (error) {
862
- report(`could not close agent-device session ${session.name} on ${session.id}: ${error.message}`);
863
- }
864
- } catch (error) {
865
- report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
866
- }
867
- }
868
- } catch (error) {
869
- report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
715
+ if (!del) return {
716
+ status: "skipped",
717
+ reason: "AVD registration is missing; its data was kept."
718
+ };
719
+ for (const candidate of candidates) {
720
+ if (candidate.name !== avdName) throw new Error(`AVD data does not match ${avdName}; it was kept.`);
721
+ removeOrphanedAvdDirectory(candidate, owner);
870
722
  }
723
+ return {
724
+ status: "torn-down",
725
+ label: avdName
726
+ };
871
727
  }
872
- //#endregion
873
- //#region src/devices/teardown.ts
874
- function teardownParkedAvd(avdName) {
728
+ async function teardownOwnedAvd(avdName, options = {}) {
729
+ let claim;
875
730
  try {
876
- return removeParkedAfter("android", avdName, () => {
877
- const result = teardownOwnedAvd(avdName, {
878
- del: true,
879
- owner: { pool: true }
880
- });
881
- if (result.status !== "torn-down" && result.status !== "missing") throw new Error(result.reason);
882
- }) ? {
883
- status: "torn-down",
884
- label: avdName
885
- } : {
731
+ if (!isStimOwnedAvd(avdName)) return {
886
732
  status: "skipped",
887
- kind: "not-parked",
888
- reason: "emulator is no longer parked"
733
+ kind: "not-owned",
734
+ reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
889
735
  };
736
+ claim = acquireAvdClaim(avdName);
737
+ const result = await teardownClaimedAvd(avdName, claim, options);
738
+ if (result.status === "torn-down" || result.status === "missing") options.onRemoved?.();
739
+ return result;
890
740
  } catch (error) {
891
741
  return {
892
742
  status: "failed",
893
743
  reason: String(error?.message || error)
894
744
  };
745
+ } finally {
746
+ (0, ownership_claim_exports.releaseClaim)(claim);
895
747
  }
896
748
  }
897
- function teardownParkedIosSim(udid, { label, deleteSim = deleteParkedIosSim } = {}) {
749
+ async function serialFree(serial, timeoutMs = AVD_INVENTORY_TIMEOUT_MS) {
750
+ const adb = await listAdbDevicesAsync({ timeoutMs });
751
+ return ![
752
+ ...adb.emulators,
753
+ ...adb.physical,
754
+ ...adb.unhealthy
755
+ ].some((entry) => entry.serial === serial);
756
+ }
757
+ async function teardownClaimedAvd(avdName, claim, { del = false, wipe = false, park, owner, workspace, orphanedDirectory, onlyIfMissing = false, onWipeStart, waitForShutdown = waitForAndroidEmulatorShutdown, assertStopped = assertOwnedAvdStopped, resolveAvd = (name) => resolveOwnedAvdSerialAsync(name, { timeoutMs: AVD_INVENTORY_TIMEOUT_MS }) }) {
758
+ let parkFallback;
898
759
  try {
899
- const removed = removeParkedAfter("ios", udid, () => {
900
- closeOwnedDeviceSessions({
901
- platform: "ios",
902
- id: udid
903
- }, () => Boolean(resolveOwnedIosSim(udid).sim));
904
- deleteSim(udid);
905
- });
906
- if (!removed) return {
760
+ if (del || wipe) await withConfigLockAsync$1(() => assertAvdReferences(avdName, owner));
761
+ const resolved = await resolveAvd(avdName);
762
+ if (resolved.notOwned) return {
907
763
  status: "skipped",
908
- kind: "not-parked",
909
- reason: "simulator is no longer parked"
910
- };
911
- return {
912
- status: "torn-down",
913
- label: label ?? removed.name
914
- };
915
- } catch (error) {
916
- return {
917
- status: "failed",
918
- reason: String(error?.message || error)
764
+ kind: "not-owned",
765
+ reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
919
766
  };
920
- }
921
- }
922
- function eraseParkedIosSim(udid, { label } = {}) {
923
- try {
924
- const erased = eraseParkedAfter("ios", udid, (_record, eraseStarted) => {
925
- closeOwnedDeviceSessions({
926
- platform: "ios",
927
- id: udid
928
- }, () => Boolean(resolveOwnedIosSim(udid).sim));
929
- const resolved = resolveOwnedIosSim(udid);
930
- if (resolved.missing) throw new Error(`simulator ${udid} is not on this machine`);
931
- if (resolved.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(resolved.notOwned)}`);
932
- const state = resolved.sim.state;
933
- if (state !== "Shutdown") throw new Error(`simulator ${udid} is ${state}, not Shutdown; it was kept`);
934
- eraseStarted();
935
- eraseIosSim(udid);
936
- });
937
- if (!erased) return {
767
+ if (resolved.missing) {
768
+ if (wipe) throw new Error(`AVD ${avdName} is not registered; its data was kept.`);
769
+ return teardownUnregisteredAvd(avdName, del, owner, orphanedDirectory);
770
+ }
771
+ if (onlyIfMissing) return {
938
772
  status: "skipped",
939
- kind: "not-parked",
940
- reason: "simulator is no longer parked"
773
+ reason: "AVD registration appeared; its record was kept."
941
774
  };
942
- return {
943
- status: "torn-down",
944
- label: label ?? erased.name
775
+ if (orphanedDirectory) return {
776
+ status: "skipped",
777
+ reason: "AVD registration appeared; its data was kept."
945
778
  };
946
- } catch (error) {
947
- return {
948
- status: "failed",
949
- reason: String(error?.message || error)
950
- };
951
- }
952
- }
953
- function eraseParkedAvd(avdName) {
954
- try {
955
- return eraseParkedAfter("android", avdName, (_record, eraseStarted) => {
956
- if (resolveOwnedAvdSerial(avdName, { timeoutMs: AVD_INVENTORY_TIMEOUT_MS }).serial) throw new Error(`AVD ${avdName} is running; it was kept`);
957
- const result = teardownOwnedAvd(avdName, {
958
- wipe: true,
959
- owner: { pool: true },
960
- onWipeStart: eraseStarted
961
- });
962
- if (result.status !== "torn-down") throw new Error(result.reason ?? `AVD ${avdName} is ${result.status}`);
963
- }) ? {
964
- status: "torn-down",
965
- label: avdName
966
- } : {
967
- status: "skipped",
968
- kind: "not-parked",
969
- reason: "emulator is no longer parked"
970
- };
971
- } catch (error) {
972
- return {
973
- status: "failed",
974
- reason: String(error?.message || error)
975
- };
976
- }
977
- }
978
- const IOS_SHUTDOWN_SETTLE_MS = 15e3;
979
- const ADB_RELEASE_WAIT_MS = 3e3;
980
- const AVD_INVENTORY_TIMEOUT_MS = 15e3;
981
- function repoRootAbove(workspace) {
982
- if (workspace === void 0 || !getExecutor().findExecutable("agent-device")) return void 0;
983
- const root = repoRoot(workspace);
984
- try {
985
- return root && realpathSync(root) !== realpathSync(workspace) ? root : void 0;
986
- } catch {
987
- return;
988
- }
989
- }
990
- const IOS_SHUTDOWN_POLL_MS = 250;
991
- const IOS_SHUTDOWN_ATTEMPTS = 2;
992
- function settleIosSimShutdown(udid, { now = Date.now, sleep = sleepSync }) {
993
- let resolved = resolveOwnedIosSim(udid);
994
- for (let attempt = 1;; attempt++) {
995
- const deadline = now() + IOS_SHUTDOWN_SETTLE_MS;
996
- while (resolved.sim && resolved.sim.state !== "Shutdown" && now() < deadline) {
997
- sleep(IOS_SHUTDOWN_POLL_MS);
998
- resolved = resolveOwnedIosSim(udid);
999
- }
1000
- if (!resolved.sim || resolved.sim.state === "Shutdown" || attempt === IOS_SHUTDOWN_ATTEMPTS) return resolved;
1001
- shutdownIosSim(udid);
1002
- resolved = resolveOwnedIosSim(udid);
1003
- }
1004
- }
1005
- function iosShutdownFailureReason(udid, state) {
1006
- return `simulator ${udid} is still ${state} after ${IOS_SHUTDOWN_ATTEMPTS} shutdown attempts and ${IOS_SHUTDOWN_ATTEMPTS * IOS_SHUTDOWN_SETTLE_MS / 1e3}s of waiting`;
1007
- }
1008
- function parkOwnedIosSim(settled, udid, park) {
1009
- if (settled.missing) throw new Error(`simulator ${udid} disappeared after shutdown`);
1010
- if (settled.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(settled.notOwned)}`);
1011
- const sim = settled.sim;
1012
- if (sim.state !== "Shutdown") throw new Error(iosShutdownFailureReason(udid, sim.state));
1013
- const model = listIosDeviceTypes().find((d) => d.identifier === sim.deviceTypeIdentifier)?.name ?? null;
1014
- const runtime = parseRuntimeVersion(sim.runtime);
1015
- const name = parkedSimName(sim.udid, {
1016
- model,
1017
- runtime
1018
- });
1019
- renameIosSim(sim.udid, name);
1020
- const record = {
1021
- udid: sim.udid,
1022
- name,
1023
- deviceTypeIdentifier: sim.deviceTypeIdentifier,
1024
- runtimeIdentifier: sim.runtime,
1025
- parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
1026
- simslimManaged: Boolean(park.simslimManaged),
1027
- ...park.appId ? { bundleId: park.appId } : {},
1028
- ...park.cacheKey ? { cacheKey: park.cacheKey } : {}
1029
- };
1030
- return {
1031
- record,
1032
- evicted: parkSim({
1033
- platform: "ios",
1034
- projectPath: park.projectPath,
1035
- slot: park.slot,
1036
- record,
1037
- max: park.max
1038
- })
1039
- };
1040
- }
1041
- function teardownOwnedIosSim(udid, { del = false, label, park, workspace, shutdownClock = {} } = {}) {
1042
- let parkFallback;
1043
- try {
1044
- const resolved = resolveOwnedIosSim(udid);
1045
- if (resolved.notOwned) return {
1046
- status: "skipped",
1047
- kind: "not-owned",
1048
- reason: `sim "${resolved.notOwned}" is not Stim-owned: Stim has no record of creating it`
1049
- };
1050
- if (resolved.missing) return { status: "missing" };
1051
- closeOwnedDeviceSessions({
1052
- platform: "ios",
1053
- id: udid
1054
- }, () => Boolean(resolveOwnedIosSim(udid).sim), workspace);
1055
- const current = resolveOwnedIosSim(udid);
1056
- if (current.missing) return { status: "missing" };
1057
- if (current.notOwned) return {
1058
- status: "skipped",
1059
- kind: "not-owned",
1060
- reason: "simulator ownership changed before shutdown"
1061
- };
1062
- shutdownIosSim(udid);
1063
- const sim = resolved.sim;
1064
- if (del && park && park.max > 0) try {
1065
- const { record, evicted } = parkOwnedIosSim(settleIosSimShutdown(udid, shutdownClock), udid, park);
1066
- const removed = [];
1067
- const failures = [];
1068
- for (const entry of evicted) {
1069
- const result = teardownParkedIosSim(entry.udid, { label: entry.name });
1070
- if (result.status === "torn-down") removed.push({
1071
- udid: entry.udid,
1072
- name: entry.name
1073
- });
1074
- else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name} (${entry.udid}): ${result.reason}`);
1075
- }
1076
- return {
1077
- status: "torn-down",
1078
- label: label ?? sim.name ?? udid,
1079
- parked: {
1080
- udid: record.udid,
1081
- name: record.name
1082
- },
1083
- evicted: removed,
1084
- ...failures.length ? { evictionFailures: failures } : {}
1085
- };
1086
- } catch (e) {
1087
- parkFallback = String(e?.message || e);
1088
- }
1089
- if (del) {
1090
- deleteIosSim(udid);
1091
- return {
1092
- status: "torn-down",
1093
- label: label ?? sim.name ?? udid,
1094
- ...parkFallback ? { parkFallback } : {}
1095
- };
1096
- }
1097
- const settled = settleIosSimShutdown(udid, shutdownClock);
1098
- if (settled.sim && settled.sim.state !== "Shutdown") return {
1099
- status: "failed",
1100
- label: label ?? sim.name ?? udid,
1101
- reason: iosShutdownFailureReason(udid, settled.sim.state)
1102
- };
1103
- const root = sim.state === "Shutdown" ? void 0 : repoRootAbove(workspace);
1104
- if (root && settled.sim) closeOwnedDeviceSessions({
1105
- platform: "ios",
1106
- id: udid
1107
- }, () => resolveOwnedIosSim(udid).sim?.state === "Shutdown", workspace, root);
1108
- return {
1109
- status: "torn-down",
1110
- label: label ?? sim.name ?? udid
1111
- };
1112
- } catch (e) {
1113
- return {
1114
- status: "failed",
1115
- reason: String(e?.message || e),
1116
- ...parkFallback ? { parkFallback } : {}
1117
- };
1118
- }
1119
- }
1120
- function assertAvdReferences(avdName, owner) {
1121
- const config = loadConfig$1();
1122
- if (!config) throw new Error("Cannot verify AVD references without a Stim config; the data was kept.");
1123
- 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.`);
1124
- for (const [projectPath, project] of Object.entries(config.projects)) for (const { slot, platforms } of projectDeviceSlots(project)) {
1125
- if (platforms.android?.avdName?.toLowerCase() !== avdName.toLowerCase()) continue;
1126
- 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.`);
1127
- }
1128
- 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.`);
1129
- }
1130
- function removeOrphanedAvdDirectory(candidate, owner) {
1131
- const surveyed = listOrphanedAvdDirectories(candidate.name).find((entry) => entry.directory === candidate.directory);
1132
- 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.`);
1133
- assertOwnedAvdStopped(candidate.name, { resolveDirectory: () => candidate.directory });
1134
- const detached = withConfigLock$1(() => {
1135
- const current = lstatSync(candidate.directory);
1136
- 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.`);
1137
- assertAvdReferences(candidate.name, owner);
1138
- const name = `stim-gc-${randomUUID()}`;
1139
- recordCreatedDevice("android", name);
1140
- const destination = join(dirname(candidate.directory), `${name}.avd`);
1141
- try {
1142
- renameSync(candidate.directory, destination);
1143
- } catch (error) {
1144
- forgetCreatedDevice("android", name);
1145
- throw error;
1146
- }
1147
- forgetCreatedDevice("android", candidate.name);
1148
- return destination;
1149
- });
1150
- const current = lstatSync(detached);
1151
- if (!current.isDirectory() || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`Detached AVD data changed at ${detached}; it was kept.`);
1152
- try {
1153
- rmSync(detached, { recursive: true });
1154
- } catch (error) {
1155
- throw new Error(`Could not remove AVD data at ${detached}; retry gc --delete: ${error.message}`, { cause: error });
1156
- }
1157
- forgetCreatedDevice("android", basename(detached, ".avd"));
1158
- }
1159
- function teardownUnregisteredAvd(avdName, del, owner, surveyed) {
1160
- const candidates = surveyed ? [surveyed] : listOrphanedAvdDirectories(avdName);
1161
- if (candidates.length === 0) {
1162
- 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.`);
1163
- return { status: "missing" };
1164
- }
1165
- if (!del) return {
1166
- status: "skipped",
1167
- reason: "AVD registration is missing; its data was kept."
1168
- };
1169
- for (const candidate of candidates) {
1170
- if (candidate.name !== avdName) throw new Error(`AVD data does not match ${avdName}; it was kept.`);
1171
- removeOrphanedAvdDirectory(candidate, owner);
1172
- }
1173
- return {
1174
- status: "torn-down",
1175
- label: avdName
1176
- };
1177
- }
1178
- function teardownOwnedAvd(avdName, options = {}) {
1179
- let claim;
1180
- try {
1181
- if (!isStimOwnedAvd(avdName)) return {
1182
- status: "skipped",
1183
- kind: "not-owned",
1184
- reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
1185
- };
1186
- claim = acquireAvdClaim(avdName);
1187
- const result = teardownClaimedAvd(avdName, claim, options);
1188
- if (result.status === "torn-down" || result.status === "missing") options.onRemoved?.();
1189
- return result;
1190
- } catch (error) {
1191
- return {
1192
- status: "failed",
1193
- reason: String(error?.message || error)
1194
- };
1195
- } finally {
1196
- (0, ownership_claim_exports.releaseClaim)(claim);
1197
- }
1198
- }
1199
- function serialFree(serial, timeoutMs = AVD_INVENTORY_TIMEOUT_MS) {
1200
- const adb = listAdbDevices({ timeoutMs });
1201
- return ![
1202
- ...adb.emulators,
1203
- ...adb.physical,
1204
- ...adb.unhealthy
1205
- ].some((entry) => entry.serial === serial);
1206
- }
1207
- 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 }) }) {
1208
- let parkFallback;
1209
- try {
1210
- if (del || wipe) withConfigLock$1(() => assertAvdReferences(avdName, owner));
1211
- const resolved = resolveAvd(avdName);
1212
- if (resolved.notOwned) return {
1213
- status: "skipped",
1214
- kind: "not-owned",
1215
- reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
1216
- };
1217
- if (resolved.missing) {
1218
- if (wipe) throw new Error(`AVD ${avdName} is not registered; its data was kept.`);
1219
- return teardownUnregisteredAvd(avdName, del, owner, orphanedDirectory);
1220
- }
1221
- if (onlyIfMissing) return {
1222
- status: "skipped",
1223
- reason: "AVD registration appeared; its record was kept."
1224
- };
1225
- if (orphanedDirectory) return {
1226
- status: "skipped",
1227
- reason: "AVD registration appeared; its data was kept."
1228
- };
1229
- const serial = resolved.serial;
1230
- const stillStopped = (sessionSerial) => {
1231
- const current = resolveAvd(avdName);
1232
- return !current.notOwned && !current.missing && !current.serial && serialFree(sessionSerial);
779
+ const serial = resolved.serial;
780
+ const stillStopped = async (sessionSerial) => {
781
+ const current = await resolveAvd(avdName);
782
+ return !current.notOwned && !current.missing && !current.serial && await serialFree(sessionSerial);
1233
783
  };
1234
784
  if (serial) {
1235
- const stillOwned = () => {
1236
- const current = resolveAvd(avdName);
785
+ const stillOwned = async () => {
786
+ const current = await resolveAvd(avdName);
1237
787
  return !current.notOwned && !current.missing && current.serial === serial;
1238
788
  };
1239
- closeOwnedDeviceSessions({
789
+ await closeOwnedDeviceSessions({
1240
790
  platform: "android",
1241
791
  id: serial,
1242
792
  avdName
1243
793
  }, stillOwned, workspace);
1244
- if (!stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
1245
- waitForShutdown(avdName, (timeoutMs) => {
794
+ if (!await stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
795
+ await waitForShutdown(avdName, async (timeoutMs) => {
1246
796
  (0, ownership_claim_exports.markClaimChildPending)(claim);
1247
797
  try {
1248
- shutdownAndroidEmulator(serial, timeoutMs);
798
+ await shutdownAndroidEmulator(serial, timeoutMs);
1249
799
  } finally {
1250
800
  (0, ownership_claim_exports.clearClaimChild)(claim);
1251
801
  }
1252
802
  });
1253
- const root = repoRootAbove(workspace);
803
+ const root = await repoRootAbove(workspace);
1254
804
  if (root) {
1255
805
  const deadline = Date.now() + ADB_RELEASE_WAIT_MS;
1256
- while (!serialFree(serial, Math.max(1, deadline - Date.now())) && Date.now() < deadline) sleepSync(250);
1257
- closeOwnedDeviceSessions({
806
+ while (!await serialFree(serial, Math.max(1, deadline - Date.now())) && Date.now() < deadline) await sleepAsync(250);
807
+ await closeOwnedDeviceSessions({
1258
808
  platform: "android",
1259
809
  id: serial,
1260
810
  avdName
1261
811
  }, stillStopped, workspace, root);
1262
812
  }
1263
813
  } else {
1264
- closeOwnedDeviceSessions({
814
+ await closeOwnedDeviceSessions({
1265
815
  platform: "android",
1266
816
  id: null,
1267
817
  avdName
1268
818
  }, stillStopped, workspace);
1269
- waitForShutdown(avdName, null);
819
+ await waitForShutdown(avdName, null);
1270
820
  }
1271
821
  if (wipe && !del) {
1272
- const current = resolveAvd(avdName);
822
+ const current = await resolveAvd(avdName);
1273
823
  if (current.notOwned || current.missing) throw new Error(`AVD ${avdName} changed before its data was wiped.`);
1274
824
  if (current.serial) throw new Error(`Owned AVD ${avdName} started again before its data was wiped.`);
1275
825
  assertStopped(avdName);
1276
- withConfigLock$1(() => assertAvdReferences(avdName, owner));
826
+ await withConfigLockAsync$1(() => assertAvdReferences(avdName, owner));
1277
827
  const directory = ownedAvdDirectory(avdName);
1278
828
  if (!directory) throw new Error(`The data directory of AVD ${avdName} could not be resolved.`);
1279
829
  onWipeStart?.();
1280
830
  wipeAvdUserData(directory);
1281
831
  }
1282
832
  if (del) {
1283
- const current = resolveAvd(avdName);
833
+ const current = await resolveAvd(avdName);
1284
834
  if (current.notOwned) return {
1285
835
  status: "skipped",
1286
836
  kind: "not-owned",
@@ -1289,7 +839,7 @@ function teardownClaimedAvd(avdName, claim, { del = false, wipe = false, park, o
1289
839
  if (current.missing) return teardownUnregisteredAvd(avdName, del, owner);
1290
840
  if (current.serial) throw new Error(`Owned AVD ${avdName} started again before deletion.`);
1291
841
  assertStopped(avdName);
1292
- withConfigLock$1(() => assertAvdReferences(avdName, owner));
842
+ await withConfigLockAsync$1(() => assertAvdReferences(avdName, owner));
1293
843
  if (park && park.max > 0) try {
1294
844
  const systemImage = ownedAvdSystemImage(avdName);
1295
845
  if (!systemImage || !park.configuration || !ownedAvdMatchesConfiguration(avdName, park.configuration)) throw new Error("the AVD has no verified creation configuration");
@@ -1311,7 +861,7 @@ function teardownClaimedAvd(avdName, claim, { del = false, wipe = false, park, o
1311
861
  const removed = [];
1312
862
  const failures = [];
1313
863
  for (const entry of evicted) {
1314
- const result = teardownParkedAvd(entry.name);
864
+ const result = await teardownParkedAvd(entry.name);
1315
865
  if (result.status === "torn-down") removed.push({
1316
866
  udid: entry.name,
1317
867
  name: entry.name,
@@ -1338,7 +888,7 @@ function teardownClaimedAvd(avdName, claim, { del = false, wipe = false, park, o
1338
888
  if (directory) paths.push(directory);
1339
889
  (0, ownership_claim_exports.markClaimChildPending)(claim);
1340
890
  try {
1341
- deleteAvd(avdName);
891
+ await deleteAvd(avdName);
1342
892
  } finally {
1343
893
  (0, ownership_claim_exports.clearClaimChild)(claim);
1344
894
  }
@@ -1362,16 +912,16 @@ const BROWSER_EXIT_WAIT_MS = 15e3;
1362
912
  const CHROME_KILL_WAIT_MS = 5e3;
1363
913
  async function stopOwnedChrome(record, profile) {
1364
914
  for (const signal of ["SIGTERM", "SIGKILL"]) {
1365
- const state = chromeProcessState(record, profile);
915
+ const state = await chromeProcessStateAsync(record, profile);
1366
916
  if (state === "gone") return true;
1367
917
  if (state === "unknown") return false;
1368
918
  try {
1369
- signalProcessTree(record.pid, signal, { group: true });
919
+ await signalProcessTreeAsync(record.pid, signal, { group: true });
1370
920
  } catch {}
1371
921
  const deadline = Date.now() + CHROME_KILL_WAIT_MS;
1372
- while (chromeProcessState(record, profile) !== "gone" && Date.now() < deadline) await sleepAsync(50);
922
+ while (await chromeProcessStateAsync(record, profile) !== "gone" && Date.now() < deadline) await sleepAsync(50);
1373
923
  }
1374
- return chromeProcessState(record, profile) === "gone";
924
+ return await chromeProcessStateAsync(record, profile) === "gone";
1375
925
  }
1376
926
  const sleepAsync = (ms) => new Promise((done) => setTimeout(done, ms));
1377
927
  /**
@@ -1433,7 +983,7 @@ async function teardownBrowserHeld(root, { deleteProfile = false } = {}) {
1433
983
  kind: "not-verified",
1434
984
  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))}`
1435
985
  };
1436
- clearWebRecord(root, record);
986
+ await withWorkspaceStateLockAsync(root, () => clearWebRecord(root, record));
1437
987
  await releaseBrowserPort(root, record.cdpPort);
1438
988
  } else {
1439
989
  const holder = liveProfileHolder(profile);
@@ -1469,82 +1019,620 @@ async function teardownBrowserHeld(root, { deleteProfile = false } = {}) {
1469
1019
  };
1470
1020
  }
1471
1021
  }
1472
- //#endregion
1473
- //#region src/macos/instances.ts
1474
- const BUNDLE_EXECUTABLE = /^[^/]+\.app\/Contents\/MacOS\/[^/]+$/;
1475
- function bundleDirs(root) {
1476
- const dir = macosDir(root);
1477
- if (process.platform !== "darwin" || !existsSync(dir)) return [];
1478
- return [.../* @__PURE__ */ new Set([dir, realpathSync(dir)])];
1022
+ //#endregion
1023
+ //#region src/macos/instances.ts
1024
+ const BUNDLE_EXECUTABLE = /^[^/]+\.app\/Contents\/MacOS\/[^/]+$/;
1025
+ function bundleDirs(root) {
1026
+ const dir = macosDir(root);
1027
+ if (process.platform !== "darwin" || !existsSync(dir)) return [];
1028
+ return [.../* @__PURE__ */ new Set([dir, realpathSync(dir)])];
1029
+ }
1030
+ function isBundleExecutable(path, dirs) {
1031
+ return dirs.some((dir) => path.startsWith(`${dir}/`) && BUNDLE_EXECUTABLE.test(path.slice(dir.length + 1)));
1032
+ }
1033
+ /** Pids from `ps -axww -o pid=,comm=` output whose command names a bundle executable inside `dirs`. */
1034
+ function bundleCandidates(psOutput, dirs) {
1035
+ const pids = [];
1036
+ for (const line of psOutput.split("\n")) {
1037
+ const match = /^\s*(\d+)\s+(.+)$/.exec(line);
1038
+ if (match && isBundleExecutable(match[2], dirs)) pids.push(Number(match[1]));
1039
+ }
1040
+ return pids;
1041
+ }
1042
+ /** Whether `lsof -Fn -d txt` output for one process names a bundle executable inside `dirs`. */
1043
+ function lsofNamesBundleExecutable(lsofOutput, dirs) {
1044
+ return lsofOutput.split("\n").some((line) => line.startsWith("n") && isBundleExecutable(line.slice(1), dirs));
1045
+ }
1046
+ function candidatePids(dirs) {
1047
+ if (!dirs.length) return [];
1048
+ return bundleCandidates(getExecutor().runFile("ps", [
1049
+ "-axww",
1050
+ "-o",
1051
+ "pid=,comm="
1052
+ ], { timeoutMs: 1e4 }), dirs).filter((pid) => pid !== process.pid);
1053
+ }
1054
+ /** Pids that claim to run an app bundle from the workspace's macOS directory. `ps` reports argv[0], so this is a lead. */
1055
+ function bundleInstancePids(root) {
1056
+ return candidatePids(bundleDirs(root));
1057
+ }
1058
+ function unverified(pid, why) {
1059
+ return Object.assign(/* @__PURE__ */ new Error(`Cannot verify macOS app pid ${pid}: ${why}; no signal was sent.`), { code: "STIM_MACOS_OWNER_UNVERIFIED" });
1060
+ }
1061
+ function bundleRecord(pid) {
1062
+ const processToken = (0, process_identity_exports.captureProcessToken)(pid);
1063
+ const start = (0, process_identity_exports.processStartMicros)(pid);
1064
+ if (!processToken || start.status !== "running") {
1065
+ if (start.status === "gone") return null;
1066
+ throw unverified(pid, "its identity cannot be captured");
1067
+ }
1068
+ return {
1069
+ pid,
1070
+ processToken,
1071
+ startedAtMicros: start.startedAtMicros
1072
+ };
1073
+ }
1074
+ function failedBundleListing(record, error) {
1075
+ if (["gone", "different"].includes((0, process_identity_exports.inspectProcessIdentity)(record))) return null;
1076
+ const open = String(error.stdout ?? "");
1077
+ if (!open.trim()) throw unverified(record.pid, `lsof failed (${error.message.split("\n")[0]})`);
1078
+ return open;
1079
+ }
1080
+ /** Verifies owned bundle instances without blocking on process and executable probes. */
1081
+ async function runningBundleInstancesAsync(root) {
1082
+ const dirs = bundleDirs(root);
1083
+ if (!dirs.length) return [];
1084
+ const listing = await getExecutor().runFileAsync("ps", [
1085
+ "-axww",
1086
+ "-o",
1087
+ "pid=,comm="
1088
+ ], {
1089
+ timeoutMs: 1e4,
1090
+ killSignal: "SIGKILL"
1091
+ });
1092
+ const instances = [];
1093
+ for (const pid of bundleCandidates(listing, dirs).filter((candidate) => candidate !== process.pid)) {
1094
+ const record = bundleRecord(pid);
1095
+ if (!record) continue;
1096
+ let open;
1097
+ try {
1098
+ open = await getExecutor().runFileAsync("lsof", [
1099
+ "-a",
1100
+ "-p",
1101
+ String(pid),
1102
+ "-d",
1103
+ "txt",
1104
+ "-Fn"
1105
+ ], {
1106
+ timeoutMs: 1e4,
1107
+ killSignal: "SIGKILL"
1108
+ });
1109
+ } catch (error) {
1110
+ const partial = failedBundleListing(record, error);
1111
+ if (partial === null) continue;
1112
+ open = partial;
1113
+ }
1114
+ if (lsofNamesBundleExecutable(open, dirs)) instances.push(record);
1115
+ }
1116
+ return instances;
1117
+ }
1118
+ //#endregion
1119
+ //#region src/engine/tunnel.ts
1120
+ function tunnelArgv(provider, port, ngrokUrl, logFile, tailscaleBinary = "tailscale") {
1121
+ if (provider === "tailscale") return {
1122
+ bin: tailscaleBinary,
1123
+ args: [
1124
+ "serve",
1125
+ `--https=${port}`,
1126
+ `http://127.0.0.1:${port}`
1127
+ ]
1128
+ };
1129
+ if (provider === "cloudflared") return {
1130
+ bin: "cloudflared",
1131
+ args: [
1132
+ "tunnel",
1133
+ "--url",
1134
+ `http://127.0.0.1:${port}`,
1135
+ ...logFile ? ["--logfile", logFile] : []
1136
+ ]
1137
+ };
1138
+ return {
1139
+ bin: "ngrok",
1140
+ args: [
1141
+ "http",
1142
+ String(port),
1143
+ `--log=${logFile || "stdout"}`,
1144
+ "--log-format=json",
1145
+ ...ngrokUrl ? ["--url", ngrokUrl] : []
1146
+ ]
1147
+ };
1148
+ }
1149
+ const CLOUDFLARED_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
1150
+ function parseCloudflaredLine(line) {
1151
+ const match = line.match(CLOUDFLARED_URL_RE);
1152
+ return match ? match[0] : null;
1153
+ }
1154
+ function parseNgrokLine(line) {
1155
+ let data;
1156
+ try {
1157
+ data = JSON.parse(line);
1158
+ } catch {
1159
+ return null;
1160
+ }
1161
+ if (typeof data !== "object" || data === null) return null;
1162
+ const url = data.url;
1163
+ return typeof url === "string" && /^https?:\/\//i.test(url) ? url : null;
1164
+ }
1165
+ function parseTailscaleLine(line) {
1166
+ return /^https:\/\/[^\s/]+\.ts\.net(:\d+)?\/?$/.test(line) ? line : null;
1167
+ }
1168
+ function parserFor(provider) {
1169
+ if (provider === "tailscale") return parseTailscaleLine;
1170
+ return provider === "cloudflared" ? parseCloudflaredLine : parseNgrokLine;
1171
+ }
1172
+ const URL_TIMEOUT_MS = 15e3;
1173
+ const REACHABLE_TIMEOUT_MS = 24e4;
1174
+ const REACHABLE_POLL_MS = 2e3;
1175
+ const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
1176
+ async function withManagedTunnelLock(root, fn, options = {}) {
1177
+ return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
1178
+ ...options,
1179
+ rejectOwnerPurposes: ["workspace removal"]
1180
+ });
1181
+ }
1182
+ async function withManagedTunnelRemovalLock(root, fn, options = {}) {
1183
+ return withWorkspaceProcessLock(managedTunnelLockRoot(root), "metro-tunnel", fn, {
1184
+ ...options,
1185
+ ownerPurpose: "workspace removal"
1186
+ });
1187
+ }
1188
+ async function withManagedRemoteWorktreeLock(worktreeRoot, fn, options = {}) {
1189
+ return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
1190
+ ...options,
1191
+ external: true,
1192
+ rejectOwnerPurposes: ["worktree removal"]
1193
+ });
1194
+ }
1195
+ async function withManagedRemoteWorktreeRemovalLock(worktreeRoot, fn, options = {}) {
1196
+ return withWorkspaceProcessLock(managedRemoteWorktreeLockRoot(worktreeRoot), "managed-remote", fn, {
1197
+ ...options,
1198
+ external: true,
1199
+ ownerPurpose: "worktree removal"
1200
+ });
1201
+ }
1202
+ function managedLockHolders(root) {
1203
+ const locks = [{
1204
+ label: "managed tunnel",
1205
+ path: workspaceProcessLockPath(managedTunnelLockRoot(root), "metro-tunnel", false)
1206
+ }, {
1207
+ label: "managed remote",
1208
+ path: workspaceProcessLockPath(managedRemoteWorktreeLockRoot(root), "managed-remote", true)
1209
+ }];
1210
+ const reasons = [];
1211
+ for (const { label, path } of locks) {
1212
+ const claims = (0, ownership_claim_exports.readClaimSet)(path);
1213
+ const holder = claims.live[0];
1214
+ if (holder) {
1215
+ const purpose = typeof holder.details.purpose === "string" ? ` for ${holder.details.purpose}` : "";
1216
+ reasons.push(`the ${label} lock is held${purpose}`);
1217
+ } else if (claims.unresolved[0]) reasons.push(`the ${label} lock cannot be resolved: ${claims.unresolved[0].reason}`);
1218
+ }
1219
+ return reasons;
1220
+ }
1221
+ function managedTunnelLockRoot(root) {
1222
+ const key = createHash("sha256").update(resolve(root)).digest("hex");
1223
+ return join(getConfigDir(), "process-locks", key);
1224
+ }
1225
+ function managedRemoteWorktreeLockRoot(worktreeRoot) {
1226
+ const key = createHash("sha256").update(resolve(worktreeRoot)).digest("hex");
1227
+ return join(getConfigDir(), "process-locks", "worktrees", key);
1228
+ }
1229
+ async function defaultProbeReachable(url, signal) {
1230
+ const status = await probePublicHttp(url, signal);
1231
+ return status !== null && status > 0;
1232
+ }
1233
+ function waitForUrl(child, parseLine, timeoutMs) {
1234
+ return new Promise((resolve) => {
1235
+ let settled = false;
1236
+ let lastLine = null;
1237
+ let timer;
1238
+ let onOut;
1239
+ let onErr;
1240
+ let onError;
1241
+ let onExit;
1242
+ const finish = (url, exited = false) => {
1243
+ if (settled) return;
1244
+ settled = true;
1245
+ clearTimeout(timer);
1246
+ child.stdout?.removeListener("data", onOut);
1247
+ child.stderr?.removeListener("data", onErr);
1248
+ child.removeListener("error", onError);
1249
+ child.removeListener("exit", onExit);
1250
+ resolve({
1251
+ url,
1252
+ exited,
1253
+ lastLine
1254
+ });
1255
+ };
1256
+ const onLine = (line) => {
1257
+ if (settled) return;
1258
+ if (line.trim()) lastLine = line.trim();
1259
+ const url = parseLine(line);
1260
+ if (url) finish(url);
1261
+ };
1262
+ const outReader = createLineReader(onLine);
1263
+ const errReader = createLineReader(onLine);
1264
+ onOut = (chunk) => outReader.push(chunk);
1265
+ onErr = (chunk) => errReader.push(chunk);
1266
+ onError = () => finish(null);
1267
+ onExit = () => {
1268
+ outReader.flush();
1269
+ errReader.flush();
1270
+ finish(null, true);
1271
+ };
1272
+ child.stdout?.setEncoding?.("utf-8");
1273
+ child.stderr?.setEncoding?.("utf-8");
1274
+ child.stdout?.on("data", onOut);
1275
+ child.stderr?.on("data", onErr);
1276
+ child.on("error", onError);
1277
+ child.on("exit", onExit);
1278
+ timer = setTimeout(() => finish(null), timeoutMs);
1279
+ });
1280
+ }
1281
+ function waitForFileUrl(child, logFile, parseLine, timeoutMs) {
1282
+ return new Promise((resolve) => {
1283
+ let settled = false;
1284
+ let watcher = null;
1285
+ const finish = (url, exited = false) => {
1286
+ if (settled) return;
1287
+ settled = true;
1288
+ watcher?.close();
1289
+ clearInterval(interval);
1290
+ clearTimeout(timer);
1291
+ child.removeListener("error", onError);
1292
+ child.removeListener("exit", onExit);
1293
+ resolve({
1294
+ url,
1295
+ exited
1296
+ });
1297
+ };
1298
+ const probe = () => {
1299
+ try {
1300
+ for (const line of readFileSync(logFile, "utf-8").split(/\r?\n/)) {
1301
+ const url = parseLine(line);
1302
+ if (url) return finish(url);
1303
+ }
1304
+ } catch {}
1305
+ };
1306
+ const onError = () => finish(null);
1307
+ const onExit = () => finish(null, true);
1308
+ child.on("error", onError);
1309
+ child.on("exit", onExit);
1310
+ try {
1311
+ watcher = watch(dirname(logFile), (_event, name) => {
1312
+ if (name === null || name.toString() === basename(logFile)) probe();
1313
+ });
1314
+ watcher.on("error", () => {
1315
+ watcher?.close();
1316
+ watcher = null;
1317
+ });
1318
+ watcher.unref();
1319
+ } catch {}
1320
+ const interval = setInterval(probe, Math.min(1e3, timeoutMs));
1321
+ const timer = setTimeout(() => finish(null), timeoutMs);
1322
+ probe();
1323
+ });
1324
+ }
1325
+ function createTunnelLogFile(provider) {
1326
+ const dir = join(getConfigDir(), "tunnel-logs");
1327
+ mkdirSync(dir, { recursive: true });
1328
+ const path = join(dir, `${provider}-${randomUUID()}.log`);
1329
+ closeSync(openSync(path, "wx", 384));
1330
+ return path;
1331
+ }
1332
+ function removeTunnelLogFile(path) {
1333
+ if (!path) return;
1334
+ try {
1335
+ unlinkSync(path);
1336
+ } catch {}
1337
+ }
1338
+ function removeRecordedTunnelLogFile(path) {
1339
+ if (!path) return;
1340
+ const dir = resolve(join(getConfigDir(), "tunnel-logs"));
1341
+ const resolved = resolve(path);
1342
+ if (!resolved.startsWith(`${dir}${sep}`) || resolve(join(dir, basename(resolved))) !== resolved) return;
1343
+ removeTunnelLogFile(resolved);
1344
+ }
1345
+ async function waitUntilReachable({ url, now, sleep, probe, timeoutMs }) {
1346
+ const deadline = now() + timeoutMs;
1347
+ for (;;) {
1348
+ if (await probe(url, new AbortController().signal).catch(() => false)) return true;
1349
+ if (now() >= deadline) return false;
1350
+ await sleep(REACHABLE_POLL_MS);
1351
+ }
1352
+ }
1353
+ const CLEANUP_TIMEOUT_MS = 1e3;
1354
+ const CLEANUP_POLL_MS = 25;
1355
+ 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 }) {
1356
+ const spawn = spawnFn || ((cmd, args, opts) => getExecutor().spawn(cmd, args, opts));
1357
+ const outputFile = provider === "tailscale" ? null : logFile || (spawnFn ? null : createTunnelLogFile(provider));
1358
+ let child;
1359
+ try {
1360
+ const binary = provider === "tailscale" ? resolveTailscaleBinary((bin) => getExecutor().findExecutable(bin)) : provider;
1361
+ if (!binary) throw new Error("Install Tailscale and sign in to your tailnet.");
1362
+ const { bin, args } = tunnelArgv(provider, port, ngrokUrl, outputFile, binary);
1363
+ child = spawn(bin, args, {
1364
+ stdio: outputFile ? [
1365
+ "ignore",
1366
+ "ignore",
1367
+ "ignore"
1368
+ ] : [
1369
+ "ignore",
1370
+ "pipe",
1371
+ "pipe"
1372
+ ],
1373
+ detached: true
1374
+ });
1375
+ } catch (err) {
1376
+ removeTunnelLogFile(outputFile);
1377
+ return {
1378
+ failed: true,
1379
+ reason: `Could not start ${provider}: ${describe(err)}`
1380
+ };
1381
+ }
1382
+ let childExited = false;
1383
+ child.once("exit", () => {
1384
+ childExited = true;
1385
+ });
1386
+ const cleanup = async (alreadyExited = childExited) => {
1387
+ const stopped = await terminateChild(child, {
1388
+ alreadyExited,
1389
+ timeoutMs: cleanupTimeoutMs,
1390
+ now,
1391
+ sleep,
1392
+ isAlive: isChildAlive
1393
+ });
1394
+ if (stopped) removeTunnelLogFile(outputFile);
1395
+ return stopped ? { status: "stopped" } : {
1396
+ status: "failed",
1397
+ reason: `Sent SIGKILL but could not confirm that pid ${child.pid ?? "unknown"} exited.`
1398
+ };
1399
+ };
1400
+ const failAfterCleanup = async (reason, alreadyExited = childExited) => {
1401
+ const stopped = await cleanup(alreadyExited);
1402
+ return stopped.status === "stopped" ? {
1403
+ failed: true,
1404
+ reason
1405
+ } : {
1406
+ failed: true,
1407
+ reason: `${reason} ${stopped.reason}`,
1408
+ cleanupFailed: true
1409
+ };
1410
+ };
1411
+ const pid = child.pid;
1412
+ if (!pid) return failAfterCleanup(`${provider} started but reported no pid.`);
1413
+ const captureChildIdentity = () => {
1414
+ try {
1415
+ return readProcessToken(pid);
1416
+ } catch {
1417
+ return null;
1418
+ }
1419
+ };
1420
+ const initialProcessToken = captureChildIdentity();
1421
+ if (!initialProcessToken) return failAfterCleanup(`${provider} started but its process identity token could not be read.`);
1422
+ const { url, exited, lastLine } = outputFile ? await waitForFileUrl(child, outputFile, parserFor(provider), urlTimeoutMs) : await waitForUrl(child, parserFor(provider), urlTimeoutMs);
1423
+ 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);
1424
+ if (!outputFile) resumeChildPipes(child);
1425
+ if (requireReachable) {
1426
+ if (!await waitUntilReachable({
1427
+ url,
1428
+ now,
1429
+ sleep,
1430
+ probe: probeReachable,
1431
+ timeoutMs: reachableTimeoutMs
1432
+ })) return failAfterCleanup(`${url} did not become reachable within ${reachableTimeoutMs}ms; ${provider} may still be registering, or the network path is blocked.`);
1433
+ }
1434
+ const finalProcessToken = captureChildIdentity();
1435
+ if (!finalProcessToken || finalProcessToken !== initialProcessToken) return failAfterCleanup(`${provider} process identity changed before its tunnel could be recorded.`);
1436
+ if (!outputFile) unrefChildPipes(child);
1437
+ child.unref?.();
1438
+ if (childExited) return failAfterCleanup(`${provider} exited before its tunnel could be recorded.`, true);
1439
+ return {
1440
+ url,
1441
+ pid,
1442
+ processToken: initialProcessToken,
1443
+ logFile: outputFile,
1444
+ cleanup: () => cleanup()
1445
+ };
1446
+ }
1447
+ async function startTunnelSequence({ providers, port, ngrokUrl = null, requireReachable = true, start = startTunnel }) {
1448
+ const failures = [];
1449
+ for (const provider of providers) {
1450
+ const result = await start({
1451
+ provider,
1452
+ port,
1453
+ ngrokUrl: provider === "ngrok" ? ngrokUrl : null,
1454
+ requireReachable
1455
+ });
1456
+ if (!("failed" in result)) return {
1457
+ provider,
1458
+ ...result
1459
+ };
1460
+ failures.push(`${provider}: ${result.reason}`);
1461
+ if (result.cleanupFailed) return {
1462
+ failed: true,
1463
+ reason: failures.join(" ")
1464
+ };
1465
+ }
1466
+ return {
1467
+ failed: true,
1468
+ reason: failures.join(" ") || "No managed tunnel provider was selected."
1469
+ };
1470
+ }
1471
+ function closeChildPipes(child) {
1472
+ child.stdout?.destroy?.();
1473
+ child.stderr?.destroy?.();
1479
1474
  }
1480
- function isBundleExecutable(path, dirs) {
1481
- return dirs.some((dir) => path.startsWith(`${dir}/`) && BUNDLE_EXECUTABLE.test(path.slice(dir.length + 1)));
1475
+ function unrefChildPipes(child) {
1476
+ resumeChildPipes(child);
1477
+ for (const stream of [child.stdout, child.stderr]) stream?.unref?.();
1482
1478
  }
1483
- /** Pids from `ps -axww -o pid=,comm=` output whose command names a bundle executable inside `dirs`. */
1484
- function bundleCandidates(psOutput, dirs) {
1485
- const pids = [];
1486
- for (const line of psOutput.split("\n")) {
1487
- const match = /^\s*(\d+)\s+(.+)$/.exec(line);
1488
- if (match && isBundleExecutable(match[2], dirs)) pids.push(Number(match[1]));
1479
+ function resumeChildPipes(child) {
1480
+ child.stdout?.resume?.();
1481
+ child.stderr?.resume?.();
1482
+ }
1483
+ async function signalAndWaitForExit(child, signal, { timeoutMs, now, sleep, isAlive, platform }) {
1484
+ let exited = false;
1485
+ const onExit = () => {
1486
+ exited = true;
1487
+ };
1488
+ child.once("exit", onExit);
1489
+ try {
1490
+ if (platform === "win32" && child.pid) signalProcessTree(child.pid, signal, { platform });
1491
+ child.kill(signal);
1492
+ } catch (err) {
1493
+ child.removeListener("exit", onExit);
1494
+ return isEsrch(err);
1489
1495
  }
1490
- return pids;
1496
+ const hasExited = () => exited;
1497
+ const deadline = now() + timeoutMs;
1498
+ while (!hasExited() && now() < deadline) await sleep(CLEANUP_POLL_MS);
1499
+ if (!hasExited() && child.pid && !isAlive(child.pid)) exited = true;
1500
+ child.removeListener("exit", onExit);
1501
+ return hasExited();
1491
1502
  }
1492
- /** Whether `lsof -Fn -d txt` output for one process names a bundle executable inside `dirs`. */
1493
- function lsofNamesBundleExecutable(lsofOutput, dirs) {
1494
- return lsofOutput.split("\n").some((line) => line.startsWith("n") && isBundleExecutable(line.slice(1), dirs));
1503
+ async function terminateChild(child, { alreadyExited, timeoutMs, now, sleep, isAlive, platform = process.platform }) {
1504
+ if (alreadyExited) {
1505
+ closeChildPipes(child);
1506
+ return true;
1507
+ }
1508
+ if (await signalAndWaitForExit(child, "SIGTERM", {
1509
+ timeoutMs,
1510
+ now,
1511
+ sleep,
1512
+ isAlive,
1513
+ platform
1514
+ })) {
1515
+ closeChildPipes(child);
1516
+ return true;
1517
+ }
1518
+ const killed = await signalAndWaitForExit(child, "SIGKILL", {
1519
+ timeoutMs,
1520
+ now,
1521
+ sleep,
1522
+ isAlive,
1523
+ platform
1524
+ });
1525
+ closeChildPipes(child);
1526
+ return killed;
1495
1527
  }
1496
- function candidatePids(dirs) {
1497
- if (!dirs.length) return [];
1498
- return bundleCandidates(getExecutor().runFile("ps", [
1499
- "-axww",
1500
- "-o",
1501
- "pid=,comm="
1502
- ], { timeoutMs: 1e4 }), dirs).filter((pid) => pid !== process.pid);
1528
+ function describe(err) {
1529
+ return err?.message || String(err);
1503
1530
  }
1504
- /** Pids that claim to run an app bundle from the workspace's macOS directory. `ps` reports argv[0], so this is a lead. */
1505
- function bundleInstancePids(root) {
1506
- return candidatePids(bundleDirs(root));
1531
+ const STOP_TIMEOUT_MS = 5e3;
1532
+ const STOP_POLL_MS = 100;
1533
+ function isEsrch(err) {
1534
+ return err?.code === "ESRCH";
1507
1535
  }
1508
- function unverified(pid, why) {
1509
- return Object.assign(/* @__PURE__ */ new Error(`Cannot verify macOS app pid ${pid}: ${why}; no signal was sent.`), { code: "STIM_MACOS_OWNER_UNVERIFIED" });
1536
+ async function stopTunnel(record, { isAlive = pidExists, inspectIdentity = process_identity_exports.inspectProcessIdentity, platform = process.platform, kill = (pid) => signalProcessTreeAsync(pid, "SIGTERM", { platform }), now = Date.now, sleep = defaultSleep, timeoutMs = STOP_TIMEOUT_MS } = {}) {
1537
+ const missing = () => {
1538
+ removeRecordedTunnelLogFile(record?.logFile);
1539
+ return { status: "missing" };
1540
+ };
1541
+ const pid = record?.pid;
1542
+ if (!pid || !isAlive(pid)) return missing();
1543
+ if (!record.processToken) return {
1544
+ status: "failed",
1545
+ 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.`
1546
+ };
1547
+ const identity = inspectIdentity(record);
1548
+ if (identity === "gone" || identity === "different") return missing();
1549
+ if (identity !== "same") return {
1550
+ status: "failed",
1551
+ reason: `could not verify the process identity for tunnel pid ${pid}; refusing to signal it.`
1552
+ };
1553
+ try {
1554
+ await kill(pid);
1555
+ } catch (err) {
1556
+ return isEsrch(err) ? missing() : {
1557
+ status: "failed",
1558
+ reason: describe(err)
1559
+ };
1560
+ }
1561
+ const deadline = now() + timeoutMs;
1562
+ const exited = () => {
1563
+ if (!isAlive(pid)) return true;
1564
+ const current = inspectIdentity(record);
1565
+ return current === "gone" || current === "different";
1566
+ };
1567
+ while (now() < deadline && !exited()) await sleep(STOP_POLL_MS);
1568
+ if (!exited()) return {
1569
+ status: "failed",
1570
+ reason: `pid ${pid} did not exit within ${timeoutMs}ms.`
1571
+ };
1572
+ removeRecordedTunnelLogFile(record.logFile);
1573
+ return { status: "stopped" };
1510
1574
  }
1511
- /**
1512
- * Every running process whose executable is an app bundle Stim built in the workspace's macOS directory, however it
1513
- * was launched. Each identity is captured before its executable is confirmed with lsof.
1514
- */
1515
- function runningBundleInstances(root) {
1516
- const dirs = bundleDirs(root);
1517
- const instances = [];
1518
- for (const pid of candidatePids(dirs)) {
1519
- const processToken = (0, process_identity_exports.captureProcessToken)(pid);
1520
- const start = (0, process_identity_exports.processStartMicros)(pid);
1521
- if (!processToken || start.status !== "running") {
1522
- if (start.status === "gone") continue;
1523
- throw unverified(pid, "its identity cannot be captured");
1524
- }
1525
- const record = {
1575
+ //#endregion
1576
+ //#region src/supervisor/ownership.ts
1577
+ function resolveSupervisorTarget({ state, record, reservedPort, isAlive = pidExists, inspectIdentity = process_identity_exports.inspectProcessIdentity } = {}) {
1578
+ const statePid = numberOrNull(state?.pid);
1579
+ const recordPid = numberOrNull(record?.pid);
1580
+ const pid = statePid ?? recordPid;
1581
+ if (!pid) return { status: "none" };
1582
+ if (statePid && recordPid && statePid !== recordPid) {
1583
+ const gone = (entry) => ["gone", "different"].includes(inspectIdentity(entry));
1584
+ if (gone(state) && gone(record)) return {
1585
+ status: "stale",
1586
+ pid
1587
+ };
1588
+ return {
1589
+ status: "unverified",
1526
1590
  pid,
1527
- processToken,
1528
- startedAtMicros: start.startedAtMicros
1591
+ reason: `workspace state.json records supervisor pid ${statePid} but the registry records pid ${recordPid}`
1529
1592
  };
1530
- let open;
1531
- try {
1532
- open = getExecutor().runFile("lsof", [
1533
- "-a",
1534
- "-p",
1535
- String(pid),
1536
- "-d",
1537
- "txt",
1538
- "-Fn"
1539
- ], { timeoutMs: 1e4 });
1540
- } catch (error) {
1541
- if (["gone", "different"].includes((0, process_identity_exports.inspectProcessIdentity)(record))) continue;
1542
- open = String(error.stdout ?? "");
1543
- if (!open.trim()) throw unverified(pid, `lsof failed (${error.message.split("\n")[0]})`);
1544
- }
1545
- if (lsofNamesBundleExecutable(open, dirs)) instances.push(record);
1546
1593
  }
1547
- return instances;
1594
+ if (!isAlive(pid)) return {
1595
+ status: "stale",
1596
+ pid
1597
+ };
1598
+ const port = numberOrNull(state?.port) ?? numberOrNull(record?.port);
1599
+ if (reservedPort !== null && reservedPort !== void 0 && port !== null && port !== reservedPort) return {
1600
+ status: "unverified",
1601
+ pid,
1602
+ reason: `supervisor pid ${pid} records port ${port}, but this project reserved port ${reservedPort}`
1603
+ };
1604
+ if (reservedPort !== null && reservedPort !== void 0 && port === null) return {
1605
+ status: "unverified",
1606
+ pid,
1607
+ reason: `supervisor pid ${pid} has no recorded port, so it cannot be matched against reserved port ${reservedPort}`
1608
+ };
1609
+ const identityRecord = statePid ? state : record;
1610
+ if (statePid && recordPid && state?.processToken !== record?.processToken) return {
1611
+ status: "unverified",
1612
+ pid,
1613
+ reason: "workspace state and registry process identities disagree"
1614
+ };
1615
+ const identity = inspectIdentity(identityRecord);
1616
+ if (identity === "gone" || identity === "different") return {
1617
+ status: "stale",
1618
+ pid
1619
+ };
1620
+ if (identity !== "same") return {
1621
+ status: "unverified",
1622
+ pid,
1623
+ reason: "the recorded process identity could not be verified"
1624
+ };
1625
+ return {
1626
+ status: "ours",
1627
+ processToken: identityRecord.processToken,
1628
+ pid,
1629
+ port,
1630
+ mode: state?.mode ?? record?.mode ?? null,
1631
+ startedAt: state?.startedAt ?? record?.startedAt ?? null
1632
+ };
1633
+ }
1634
+ function numberOrNull(v) {
1635
+ return typeof v === "number" && Number.isSafeInteger(v) && v > 0 ? v : null;
1548
1636
  }
1549
1637
  //#endregion
1550
1638
  //#region src/workspace/in-use.ts
@@ -1666,7 +1754,7 @@ function readRecordedRoot(dir, name) {
1666
1754
  projectRoot: null,
1667
1755
  problem: "its workspace.json records no absolute project root"
1668
1756
  };
1669
- if (workspaceName(projectRoot) !== name) return {
1757
+ if (workspaceName$1(projectRoot) !== name) return {
1670
1758
  projectRoot: null,
1671
1759
  problem: `its workspace.json records ${projectRoot}, whose workspace directory has another name`
1672
1760
  };
@@ -1760,10 +1848,21 @@ function collectOrphanedWorkspaces(registryKeys, mountedVolumes, { measure = tru
1760
1848
  isMounted: (path) => isOnMountedVolume(path, mountedVolumes),
1761
1849
  inUse: (root) => workspaceInUse(root)
1762
1850
  });
1763
- return {
1764
- orphaned: measure ? classified.orphaned.map((entry) => Object.assign({}, entry, { bytes: sizeOf(entry.dir) })) : classified.orphaned,
1765
- skipped: classified.skipped
1766
- };
1851
+ if (!measure) return classified;
1852
+ return (async () => {
1853
+ const orphaned = [];
1854
+ for (const entry of classified.orphaned) {
1855
+ const bytes = await measuredDirectorySizeAsync(entry.dir, { timeoutMs: SIZE_TIMEOUT_MS });
1856
+ orphaned.push({
1857
+ ...entry,
1858
+ bytes
1859
+ });
1860
+ }
1861
+ return {
1862
+ orphaned,
1863
+ skipped: classified.skipped
1864
+ };
1865
+ })();
1767
1866
  }
1768
1867
  const REBUILD_COST = "The next build of an unchanged app installs from the shared build cache; after a native change the compilation cache speeds the rebuild, but on React Native 0.86 Swift does not use it (explicit modules are off), so that build recompiles Swift. The next stim macos in a cleared workspace is a full Swift build.";
1769
1868
  const WORKSPACE_OUTPUT_DIRS = [
@@ -1789,9 +1888,6 @@ function planWorkspaceOutputs(entries, { olderThan, now }) {
1789
1888
  });
1790
1889
  }
1791
1890
  const SIZE_TIMEOUT_MS = 6e4;
1792
- function sizeOf(dir, timeoutMs = SIZE_TIMEOUT_MS) {
1793
- return measuredDirectorySize(dir, { timeoutMs });
1794
- }
1795
1891
  function outputPaths(dir) {
1796
1892
  const paths = WORKSPACE_OUTPUT_DIRS.map((name) => join$1(dir, name)).filter((path) => existsSync$1(path));
1797
1893
  const macos = join$1(dir, "macos");
@@ -1801,10 +1897,10 @@ function outputPaths(dir) {
1801
1897
  } catch {}
1802
1898
  return paths;
1803
1899
  }
1804
- function outputBytes(paths, timeoutMs) {
1900
+ async function outputBytes(paths, timeoutMs) {
1805
1901
  let total = 0;
1806
1902
  for (const path of paths) {
1807
- const size = sizeOf(path, timeoutMs);
1903
+ const size = await measuredDirectorySizeAsync(path, { timeoutMs });
1808
1904
  if (size === null) return null;
1809
1905
  total += size;
1810
1906
  }
@@ -1812,19 +1908,32 @@ function outputBytes(paths, timeoutMs) {
1812
1908
  }
1813
1909
  function collectWorkspaceOutputs({ olderThan, now, exclude = [], measure = true, sizeTimeoutMs = SIZE_TIMEOUT_MS }) {
1814
1910
  const mountedVolumes = listMountedVolumes();
1815
- const entries = listWorkspaceDirs().filter((entry) => !exclude.includes(entry.dir)).filter((entry) => entry.projectRoot === null || isOnMountedVolume(entry.projectRoot, mountedVolumes)).map((entry) => Object.assign({}, entry, { paths: outputPaths(entry.dir) })).filter((entry) => entry.paths.length > 0).map(({ paths, ...entry }) => Object.assign({}, entry, { bytes: measure ? outputBytes(paths, sizeTimeoutMs) : null })).map((entry) => Object.assign({}, entry, {
1816
- lastUsed: entry.projectRoot === null ? NaN : workspaceLastUsed(entry.projectRoot),
1817
- inUse: entry.projectRoot === null ? [] : workspaceInUse(entry.projectRoot)
1818
- }));
1819
- return {
1911
+ const entries = listWorkspaceDirs().filter((entry) => !exclude.includes(entry.dir)).filter((entry) => entry.projectRoot === null || isOnMountedVolume(entry.projectRoot, mountedVolumes)).map((entry) => Object.assign({}, entry, { paths: outputPaths(entry.dir) })).filter((entry) => entry.paths.length > 0);
1912
+ const report = (sizes) => ({
1820
1913
  root: workspacesRoot(),
1821
- workspaces: planWorkspaceOutputs(entries, {
1914
+ workspaces: planWorkspaceOutputs(sizes.map((entry) => ({
1915
+ ...entry,
1916
+ lastUsed: entry.projectRoot === null ? NaN : workspaceLastUsed(entry.projectRoot),
1917
+ inUse: entry.projectRoot === null ? [] : workspaceInUse(entry.projectRoot)
1918
+ })), {
1822
1919
  olderThan,
1823
1920
  now
1824
1921
  })
1825
- };
1922
+ });
1923
+ if (!measure) return report(entries.map(({ paths: _paths, ...entry }) => ({
1924
+ ...entry,
1925
+ bytes: null
1926
+ })));
1927
+ return (async () => {
1928
+ const sizes = [];
1929
+ for (const { paths, ...entry } of entries) sizes.push({
1930
+ ...entry,
1931
+ bytes: await outputBytes(paths, sizeTimeoutMs)
1932
+ });
1933
+ return report(sizes);
1934
+ })();
1826
1935
  }
1827
- async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
1936
+ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }, { log, record } = GC_OUTPUT) {
1828
1937
  let failures = 0;
1829
1938
  let cleared = 0;
1830
1939
  const clearedRoots = [];
@@ -1832,8 +1941,8 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
1832
1941
  const root = entry.projectRoot;
1833
1942
  if (!entry.willClear || root === null) {
1834
1943
  if (entry.keptReason) {
1835
- console.log(chalk.dim(`Kept the build outputs of ${root ?? entry.dir}: ${entry.keptReason}`));
1836
- recordGcResult("workspaceOutputs", "kept", root ?? entry.dir, {
1944
+ log(chalk.dim(`Kept the build outputs of ${root ?? entry.dir}: ${entry.keptReason}`));
1945
+ record("workspaceOutputs", "kept", root ?? entry.dir, {
1837
1946
  bytes: entry.bytes,
1838
1947
  detail: entry.keptReason
1839
1948
  });
@@ -1861,8 +1970,8 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
1861
1970
  }, { purpose: "gc" });
1862
1971
  } catch (error) {
1863
1972
  failures++;
1864
- console.log(chalk.red(`Could not clear the build outputs of ${root}: ${error?.message || String(error)}`));
1865
- recordGcResult("workspaceOutputs", "failed", root, {
1973
+ log(chalk.red(`Could not clear the build outputs of ${root}: ${error?.message || String(error)}`));
1974
+ record("workspaceOutputs", "failed", root, {
1866
1975
  bytes: entry.bytes,
1867
1976
  detail: error?.message || String(error)
1868
1977
  });
@@ -1870,8 +1979,8 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
1870
1979
  }
1871
1980
  const kept = run.ran ? run.value : `in use: ${run.reasons.join("; ")}`;
1872
1981
  if (kept) {
1873
- console.log(chalk.yellow(`Kept the build outputs of ${root}: ${kept}`));
1874
- recordGcResult("workspaceOutputs", "kept", root, {
1982
+ log(chalk.yellow(`Kept the build outputs of ${root}: ${kept}`));
1983
+ record("workspaceOutputs", "kept", root, {
1875
1984
  bytes: entry.bytes,
1876
1985
  detail: kept
1877
1986
  });
@@ -1879,10 +1988,10 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
1879
1988
  }
1880
1989
  cleared += entry.bytes ?? 0;
1881
1990
  clearedRoots.push(root);
1882
- console.log(chalk.green(`Cleared the build outputs of ${root} (${sizeText(entry.bytes)})`));
1883
- recordGcResult("workspaceOutputs", "done", root, { bytes: entry.bytes });
1991
+ log(chalk.green(`Cleared the build outputs of ${root} (${sizeText(entry.bytes)})`));
1992
+ record("workspaceOutputs", "done", root, { bytes: entry.bytes });
1884
1993
  }
1885
- if (cleared) console.log(chalk.dim(`Cleared ${formatBytes(cleared)} of workspace build outputs. ${REBUILD_COST}`));
1994
+ if (cleared) log(chalk.dim(`Cleared ${formatBytes(cleared)} of workspace build outputs. ${REBUILD_COST}`));
1886
1995
  return {
1887
1996
  failures,
1888
1997
  cleared: clearedRoots
@@ -1940,4 +2049,4 @@ function sizeText(bytes) {
1940
2049
  return bytes === null ? "size unknown" : formatBytes(bytes);
1941
2050
  }
1942
2051
  //#endregion
1943
- 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 };
2052
+ export { teardownParkedIosSim as A, takeGcResults as B, eraseParkedAvd as C, teardownOwnedBrowser as D, teardownOwnedAvd as E, acquireBuildSlot as F, listBuildSlots as I, releaseBuildSlot as L, canonical$1 as M, measureDu as N, teardownOwnedIosSim as O, writeUsageCache as P, GC_OUTPUT as R, runningBundleInstancesAsync 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, probePublicHttp 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, recordGcResult as z };