stim 1.19.0 → 1.20.0

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