stim 1.20.2 → 1.22.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 (231) hide show
  1. package/README.md +141 -8
  2. package/dist/acquire-artifact-Co2k4f7O.mjs +171 -0
  3. package/dist/act-DsuqEkiM.mjs +2801 -0
  4. package/dist/activity-CMLmzp7o.mjs +1397 -0
  5. package/dist/{agent-device-usage-output-Dzgv6lgR.mjs → agent-device-usage-output-DUFaTMdd.mjs} +23 -67
  6. package/dist/{android-DCKZrNvV.mjs → android-ByRMUoiJ.mjs} +157 -93
  7. package/dist/{android-Mz_Inn3w.mjs → android-C7sVmyI9.mjs} +299 -116
  8. package/dist/{android-Dlzxshgp.mjs → android-DGkNqa_c.mjs} +5 -5
  9. package/dist/{android-HqP1_vB_.mjs → android-DR5lZ2Az.mjs} +2 -2
  10. package/dist/android-cas-compiler.mjs +2 -2
  11. package/dist/{android-cas-Bl-cLJBk.mjs → android-cas-ngferVq2.mjs} +7 -6
  12. package/dist/android-project-BCAAmMwz.mjs +12 -0
  13. package/dist/android-project-DVWyCbWU.d.mts +391 -0
  14. package/dist/api.d.mts +810 -64
  15. package/dist/api.mjs +782 -1
  16. package/dist/{artifact-HSwkIWEQ.mjs → artifact-8EY8SAPw.mjs} +240 -213
  17. package/dist/{artifact-D3czlUvT.mjs → artifact-DJPy518Y.mjs} +203 -164
  18. package/dist/artifact-platform-CXcmX_j3.mjs +27 -0
  19. package/dist/{browser-web-CA-F1GQo.mjs → browser-web-Bcv99PBM.mjs} +194 -64
  20. package/dist/budget-Cvwn54qV.mjs +9285 -0
  21. package/dist/{build-DYDbfkk5.mjs → build-CabC3G23.mjs} +153 -98
  22. package/dist/build-cache-Dv7RN200.mjs +602 -0
  23. package/dist/build-plan-DUC8Ka9y.mjs +248 -0
  24. package/dist/{cache-manifest-XH-fvDUj.mjs → cache-manifest-DSeQjt-Q.mjs} +2 -2
  25. package/dist/cache-manifest.mjs +1 -1
  26. package/dist/{ccache-zdKiYQ3E.mjs → ccache-Btwc36-I.mjs} +10 -9
  27. package/dist/{cdp-Dq_ypAlJ.mjs → cdp-3fwICjzi.mjs} +18 -5
  28. package/dist/{chrome-BnzZMqsR.mjs → chrome-Bbw689-6.mjs} +7 -4
  29. package/dist/{cli-CLP3diBm.mjs → cli-CHZa9Lvk.mjs} +20 -20
  30. package/dist/cli.mjs +1 -1
  31. package/dist/client-OyRoEuSr.mjs +1577 -0
  32. package/dist/collector-run.d.mts +4 -40
  33. package/dist/collector-run.mjs +9 -8
  34. package/dist/{command-output-BTy8hZ0F.mjs → command-output-gwpm28fu.mjs} +1 -1
  35. package/dist/config-DlrsduwG.mjs +955 -0
  36. package/dist/{created-devices-Dzd5C2LF.mjs → created-devices-x9rN6bhs.mjs} +14 -4
  37. package/dist/{dependency-state-DG0p31-y.mjs → dependency-state-1I9KZT1Y.mjs} +20 -5
  38. package/dist/{detached-entry-B9C_zeQC.mjs → detached-entry-BwNEtIrP.mjs} +1 -1
  39. package/dist/{dev-client-DAWVIGBu.mjs → dev-client-BSDR5h3F.mjs} +2 -2
  40. package/dist/{device-BECOiufl.mjs → device-B-L3Fbxy.mjs} +91 -34
  41. package/dist/device-DklWZLqb.mjs +1068 -0
  42. package/dist/{device-capacity-CBUAcRyQ.mjs → device-capacity-DK5_EwRS.mjs} +42 -13
  43. package/dist/device-host-worker.mjs +76 -79
  44. package/dist/{device-ios-COSi6oS1.mjs → device-ios-DoB6KFCO.mjs} +77 -51
  45. package/dist/{device-lease-run-Cw2M4z51.mjs → device-lease-run-xJPQ_QQG.mjs} +14 -26
  46. package/dist/{device-pool-Bf-pOhSw.mjs → device-pool-BMkdsefq.mjs} +5 -5
  47. package/dist/{doctor-kfMBnUw6.mjs → doctor-Bo7BXLux.mjs} +271 -320
  48. package/dist/{doctor-CB8oZKCk.mjs → doctor-D5_EzwfA.mjs} +186 -91
  49. package/dist/doctor-fingerprint-DZLoYFVb.mjs +16 -0
  50. package/dist/doctor-fingerprint.mjs +19 -0
  51. package/dist/doctor-thjWAV0z.mjs +2 -0
  52. package/dist/{doctor-watchman-BRyV2adA.mjs → doctor-watchman-N2wDotwQ.mjs} +40 -21
  53. package/dist/{eas-build-JyfqyHfC.mjs → eas-build-CwknaQYj.mjs} +53 -20
  54. package/dist/{build-plan-B7SZsc2N.mjs → eas-fallback-CSgAulr5.mjs} +41 -214
  55. package/dist/{watchman-CPQFZLXY.mjs → eas-session-ledger-DlY_serQ.mjs} +230 -11
  56. package/dist/{error-diagnostics-BR677IDM.mjs → error-diagnostics-DBWO2rxE.mjs} +172 -82
  57. package/dist/exec-DEoAhcze.mjs +858 -0
  58. package/dist/exec-DnYc4HGH.d.mts +78 -0
  59. package/dist/{gc-qWOMdnER.mjs → gc-CXxN-4SE.mjs} +1091 -1029
  60. package/dist/gc-DnNDQyaP.mjs +3 -0
  61. package/dist/{guide-G9cAbVAc.mjs → guide-Br0LEfKy.mjs} +1165 -297
  62. package/dist/{hosted-android-C3MHGld1.mjs → hosted-android-B-9f-jkd.mjs} +10 -8
  63. package/dist/{hosted-client-ZEKpS9vw.mjs → hosted-client-DZAPrcro.mjs} +34 -19
  64. package/dist/{hosted-ios-l-o3iM9J.mjs → hosted-ios-CsCS21hn.mjs} +1 -1
  65. package/dist/{hosted-logs-EzQM7-3Z.mjs → hosted-logs-FIRx9X2f.mjs} +77 -43
  66. package/dist/{hosted-macos-BACVAVqE.mjs → hosted-macos-_awYSpfv.mjs} +35 -22
  67. package/dist/{hosted-native-A4s1QPXe.mjs → hosted-native-BdzE668-.mjs} +49 -27
  68. package/dist/idle-C4cZ757K.d.mts +24 -0
  69. package/dist/{idle-shutdown-DtxhPTKe.mjs → idle-shutdown-DDg3SdvY.mjs} +11 -10
  70. package/dist/ios-CDvlmUs7.d.mts +24 -0
  71. package/dist/{ios-BwVCF7oA.mjs → ios-Ds1clbIt.mjs} +8 -8
  72. package/dist/{ios-device-D7pi2Sca.mjs → ios-device--h_-8ENa.mjs} +51 -26
  73. package/dist/{ios-device-B5veSDDQ.mjs → ios-device-BXZ0c7Kb.mjs} +1 -1
  74. package/dist/{ios-RkW4pnQf.mjs → ios-hcBpnEGk.mjs} +200 -93
  75. package/dist/{ios-Ce7bc7br.mjs → ios-pm4nL6By.mjs} +160 -108
  76. package/dist/{ios-project-K9hY_Jab.mjs → ios-project-CZhKODF3.mjs} +13 -13
  77. package/dist/ios-signing-BtAffm0A.mjs +489 -0
  78. package/dist/{ios-state-CBLe1xYw.mjs → ios-state-Dtw5K2aI.mjs} +1 -1
  79. package/dist/{ios-nOLJGVdy.mjs → ios-xtlRHaeB.mjs} +1 -1
  80. package/dist/{js-swap-Bb3nsS07.mjs → js-swap-Bwhh4S7V.mjs} +30 -25
  81. package/dist/{launch-C3qJ6Agb.mjs → launch-BjxemNpK.mjs} +85 -543
  82. package/dist/{launch-F4sOqAhB.mjs → launch-CCzXlc8r.mjs} +91 -90
  83. package/dist/logs-BfwwadsZ.mjs +479 -0
  84. package/dist/logs-jTS_lHSo.mjs +248 -0
  85. package/dist/{macos-D7uDvO-z.mjs → macos-DVlSSyUg.mjs} +1 -1
  86. package/dist/macos-NfV2D7dO.mjs +694 -0
  87. package/dist/macos-run.mjs +1 -1
  88. package/dist/maintenance-run.mjs +123 -248
  89. package/dist/{metro-DnCEjCxs.mjs → metro-DiXtA3Ty.mjs} +107 -30
  90. package/dist/metro-gateway-CKZqKqx7.mjs +209 -0
  91. package/dist/{named-ports-4WopRPg-.mjs → named-ports-DkEzOfzO.mjs} +42 -90
  92. package/dist/native-android-CqgnOgsT.mjs +645 -0
  93. package/dist/native-gradle-artifact-inputs-DHFnmrVf.mjs +270 -0
  94. package/dist/native-gradle-inputs-Dnh84Zbd.mjs +211 -0
  95. package/dist/native-inputs-TRTTTRxt.mjs +91 -0
  96. package/dist/native-run-NwUuxbkg.mjs +276 -0
  97. package/dist/{native-runtime-k8dTIrbK.mjs → native-runtime-B7KIafpM.mjs} +11 -9
  98. package/dist/native-source-Y0yLhFyQ.mjs +133 -0
  99. package/dist/{native-xcode-inputs-CykopJdM.mjs → native-xcode-inputs-D_VOfFL7.mjs} +26 -18
  100. package/dist/{native-xcode-ios-Bbf30Gm0.mjs → native-xcode-ios-BYDOHWLN.mjs} +104 -60
  101. package/dist/{ndjson-GOnbyvso.mjs → ndjson-CgE_o3Dl.mjs} +3 -3
  102. package/dist/offload-worker.d.mts +59 -41
  103. package/dist/offload-worker.mjs +478 -115
  104. package/dist/operation-exec-BW0em61G.mjs +171 -0
  105. package/dist/{ownership-DCtw_2IA.mjs → ownership-C5fo5Edb.mjs} +3 -3
  106. package/dist/{page-DRHy329N.mjs → page-DBL2PiUU.mjs} +12 -5
  107. package/dist/paths-BsJMoPP2.mjs +12 -0
  108. package/dist/paths-DQtTgsBf.mjs +42 -0
  109. package/dist/{placement-log-MOfPVgaM.mjs → placement-log-BnGgJRRZ.mjs} +5 -5
  110. package/dist/{plan-placement-V0TdzYXp.mjs → plan-placement-DIsMtN-d.mjs} +15 -11
  111. package/dist/{ports-DdSUicVo.mjs → ports-Cy1RkmuQ.mjs} +3 -3
  112. package/dist/prebuild-D4JoEAEs.mjs +484 -0
  113. package/dist/{project-_phNIJfs.mjs → project-wM_XFTvE.mjs} +65 -37
  114. package/dist/{pull-request-CQ8sPbZa.mjs → pull-request-W1IuLXDt.mjs} +102 -20
  115. package/dist/pull-request-eODyIqZF.d.mts +124 -0
  116. package/dist/pull-requests.d.mts +2 -120
  117. package/dist/pull-requests.mjs +2 -2
  118. package/dist/{react-native-android-CM1XRDwv.mjs → react-native-android-6OZ7MSM7.mjs} +121 -72
  119. package/dist/{gradle-DSrXb6lG.mjs → react-native-build-Dvd8hC21.mjs} +292 -15
  120. package/dist/react-native-doctor-DsoSF1Y9.mjs +234 -0
  121. package/dist/{react-native-ios-Dhq_eiYQ.mjs → react-native-ios-4u1Rn-GR.mjs} +71 -39
  122. package/dist/{recordings-D0d5aQt0.mjs → recordings-CQmfzAh7.mjs} +12 -9
  123. package/dist/registration-BcRgF6Ox.mjs +51 -0
  124. package/dist/{reload-C8JhxjSh.mjs → reload-BzkYabB-.mjs} +94 -47
  125. package/dist/reload-D7AVlb8N.mjs +2 -0
  126. package/dist/{remote-BiR6aYjy.mjs → remote-DjGDmlTZ.mjs} +26 -18
  127. package/dist/result-D2Ykz_iD.mjs +435 -0
  128. package/dist/{run-BtPgpKks.mjs → run-DidHghD1.mjs} +5 -5
  129. package/dist/selection-DSMbWEX2.mjs +620 -0
  130. package/dist/{server-bare-R2m_gsH8.mjs → server-bare-CafzuyFB.mjs} +3 -4
  131. package/dist/{server-command-CTtCkmcQ.mjs → server-command-C6QjeupS.mjs} +14 -7
  132. package/dist/{server-expo-DsguMGUF.mjs → server-expo-CrjB29pq.mjs} +54 -11
  133. package/dist/server-expo-D58c9L0M.mjs +2 -0
  134. package/dist/{settings-B717odTD.mjs → settings-C7O8A6ZI.mjs} +309 -119
  135. package/dist/settings-DpR-1Wnn.mjs +114 -0
  136. package/dist/settings-write-CzD60rOD.mjs +360 -0
  137. package/dist/settings.schema.json +176 -16
  138. package/dist/{simslim-CCr3rE-A.mjs → simslim-BNckWpXi.mjs} +14 -12
  139. package/dist/{slot-launch-DPt0m9Nd.mjs → slot-launch-CbfmNgM6.mjs} +15 -20
  140. package/dist/{spawn-entry-Dcb7sDUz.mjs → spawn-entry-jw19m8Nl.mjs} +0 -1
  141. package/dist/stage-Bps5oius.mjs +200 -0
  142. package/dist/{start-TXeMmmO2.mjs → start-Br-oxqMD.mjs} +1 -1
  143. package/dist/start-EojN4WFz.mjs +1364 -0
  144. package/dist/{state-D1Icyjlu.mjs → state--smjFSTL.mjs} +55 -13
  145. package/dist/{state-CG_OFoaA.mjs → state-08I3hw3_.mjs} +3 -2
  146. package/dist/{stats-BoY5sEPU.mjs → stats-D3GerlFW.mjs} +7 -13
  147. package/dist/stats-report-BzaPrRm6.mjs +16 -0
  148. package/dist/{status-BTA7kKiQ.mjs → status-BRROykC8.mjs} +7 -5
  149. package/dist/status-EvrK72L2.mjs +2 -0
  150. package/dist/{status-WgFbkRzj.mjs → status-WBDajew2.mjs} +554 -452
  151. package/dist/{status-watch-C23ILtf9.mjs → status-watch-Bg_u-pwR.mjs} +222 -25
  152. package/dist/status-worker.d.mts +1 -0
  153. package/dist/status-worker.mjs +19 -0
  154. package/dist/stim-desktop-D5OGpYZM.mjs +112 -0
  155. package/dist/{stop-DR6VoDIk.mjs → stop-DTZ9uTw3.mjs} +2 -2
  156. package/dist/{stop-DPEhunlw.mjs → stop-Dky10AEJ.mjs} +33 -23
  157. package/dist/{stop-BNmBMi1F.mjs → stop-Mv6W9FAk.mjs} +137 -98
  158. package/dist/supervisor-run.d.mts +8 -17
  159. package/dist/supervisor-run.mjs +56 -28
  160. package/dist/{support-CwjbFs9H.mjs → support-CNioewsd.mjs} +14 -14
  161. package/dist/{support-DMTqj3Hm.mjs → support-yUiOjnVY.mjs} +20 -13
  162. package/dist/swiftpm-artifact-inputs-DFcGsXDP.mjs +153 -0
  163. package/dist/swiftpm-macos-BIak-Rxv.mjs +532 -0
  164. package/dist/toolchain-BY8X8HXx.mjs +2 -0
  165. package/dist/toolchain-DpJMHq0n.mjs +876 -0
  166. package/dist/{trigger-CigAYjeL.mjs → trigger-Cbyc41d_.mjs} +21 -6
  167. package/dist/trigger-yb_nFzPF.mjs +2 -0
  168. package/dist/{warm-progress-BuY-Fvfn.mjs → warm-progress-CA3TgT3-.mjs} +263 -156
  169. package/dist/web-CZ07gd77.mjs +2 -0
  170. package/dist/{web-BDBMxDpx.mjs → web-DKx6yoFA.mjs} +20 -14
  171. package/dist/web-run.mjs +13 -12
  172. package/dist/windows-report.d.mts +1 -0
  173. package/dist/windows-report.mjs +18 -0
  174. package/dist/windows-report.ps1 +43 -0
  175. package/dist/{workspace-state-DJt_MMvf.mjs → workspace-state-U08dEzhG.mjs} +62 -58
  176. package/dist/{workspaces-C36s2jb9.mjs → workspaces-CpANmBlq.mjs} +1723 -1286
  177. package/dist/{worktree-CgvJYJ-Q.mjs → worktree-CHecZ4Oq.mjs} +1097 -547
  178. package/dist/worktree-ZqZVjD9K.mjs +2 -0
  179. package/dist/xcode-BXn9-xYx.mjs +896 -0
  180. package/package.json +7 -5
  181. package/shim/android-audio/Main.java +24 -0
  182. package/shim/android-audio/README.md +17 -0
  183. package/shim/android-audio.jar +0 -0
  184. package/shim/android-optimizations.gradle +7 -5
  185. package/dist/activity-BSV745Kz.mjs +0 -629
  186. package/dist/api-qvOzWyfO.mjs +0 -154
  187. package/dist/api-run.mjs +0 -294
  188. package/dist/app-install-BIKtMfPq.mjs +0 -896
  189. package/dist/artifact-lifecycle-DzPpBYvK.mjs +0 -30
  190. package/dist/budget-f7516Zk0.mjs +0 -4106
  191. package/dist/build-facts-DcLv2HSd.d.mts +0 -111
  192. package/dist/cancellation-C_22VQTH.mjs +0 -24
  193. package/dist/client-qQAaXSUs.mjs +0 -1771
  194. package/dist/config-BYDxlMY3.mjs +0 -562
  195. package/dist/device-DuiPJQSn.mjs +0 -290
  196. package/dist/device-lease-BX1sJQ2E.mjs +0 -1224
  197. package/dist/device-remote-xRT0Vmo9.mjs +0 -1272
  198. package/dist/eas-session-ledger-Cm0Gwq2F.mjs +0 -207
  199. package/dist/exec-DqKo9GKJ.mjs +0 -268
  200. package/dist/git-DDfFzkMm.mjs +0 -45
  201. package/dist/launch-verify-B6Q8hZP-.mjs +0 -428
  202. package/dist/logs-BGrEUaEs.mjs +0 -383
  203. package/dist/macos-Ch5VJmcS.mjs +0 -584
  204. package/dist/metro-gateway-CX9Xlkq2.mjs +0 -103
  205. package/dist/miss-reason-mKzZ4VvN.mjs +0 -238
  206. package/dist/native-android-D5qc3-_v.mjs +0 -226
  207. package/dist/native-gradle-inputs-BsLRrl7u.mjs +0 -349
  208. package/dist/ndjson-Du3j0AQ9.d.mts +0 -17
  209. package/dist/plan-C4TjdPsR.mjs +0 -260
  210. package/dist/prebuild-DYbucPXy.mjs +0 -212
  211. package/dist/preview-78a9kR_-.mjs +0 -1011
  212. package/dist/react-native-build-CtCccJtb.mjs +0 -239
  213. package/dist/react-native-doctor-CfRnFfjs.mjs +0 -78
  214. package/dist/remote-cache-OyOyVIS3.mjs +0 -526
  215. package/dist/result-CaJg5Iat.mjs +0 -186
  216. package/dist/server-expo-Bigriskl.mjs +0 -2
  217. package/dist/settings-BORNaaCO.mjs +0 -402
  218. package/dist/settings-vk3mJGSm.d.mts +0 -13
  219. package/dist/spawn-claims-DVS5RkMS.mjs +0 -72
  220. package/dist/stage-KNTQopqv.mjs +0 -157
  221. package/dist/start-BWqzODpb.mjs +0 -1116
  222. package/dist/stim-desktop-BzBN2Em2.mjs +0 -63
  223. package/dist/swiftpm-macos-DRNVJWlb.mjs +0 -119
  224. package/dist/tailnet-Dh-qiYzG.mjs +0 -265
  225. package/dist/toolchain-BFkAcstd.mjs +0 -2
  226. package/dist/toolchain-DxSsNssu.mjs +0 -1657
  227. package/dist/trigger-9_USgmxz.mjs +0 -2
  228. package/dist/web-Dq-i3wzJ.mjs +0 -2
  229. package/dist/worktree-DpLdCHga.mjs +0 -2
  230. package/dist/{api-run.d.mts → doctor-fingerprint.d.mts} +0 -0
  231. package/dist/{errors-DzkMYh8Q.mjs → errors-ccaPc4ox.mjs} +2 -2
@@ -1,6 +1,7 @@
1
- import { F as listMountedVolumes, P as isOnMountedVolume, _ as loadConfig, k as process_identity_exports, n as allMetroPorts, r as claimMetroPort, x as releaseMetroPort } from "./config-BYDxlMY3.mjs";
2
- import { t as getExecutor } from "./exec-DqKo9GKJ.mjs";
3
- import { r as readWorkspaceState } from "./workspace-state-DJt_MMvf.mjs";
1
+ import { g as process_identity_exports, r as WorkspaceProcessCleanupError, t as getExecutor } from "./exec-DEoAhcze.mjs";
2
+ import { F as isOnMountedVolume, I as listMountedVolumes, O as withConfigLockAsync, _ as loadConfig, n as allMetroPorts, r as claimMetroPort, x as releaseMetroPort } from "./config-DlrsduwG.mjs";
3
+ import { t as runOperationFile } from "./operation-exec-BW0em61G.mjs";
4
+ import { r as readWorkspaceState } from "./workspace-state-U08dEzhG.mjs";
4
5
  import { sep } from "path";
5
6
  import { existsSync, readlinkSync, realpathSync } from "fs";
6
7
  import { readFile } from "node:fs/promises";
@@ -192,38 +193,51 @@ function createPortProbe() {
192
193
  throw new PortInspectionError(`Cannot tell whether TCP port ${port} is free: the TCP listener table failed (${listening.message.replace(/\s+/g, " ").trim()}), lsof could not list listeners, and a loopback connect failed.`);
193
194
  };
194
195
  }
195
- function isMetroRunning(port) {
196
+ function isMetroRunning(port, signal) {
196
197
  return new Promise((resolve) => {
197
198
  const req = request({
198
199
  hostname: "localhost",
199
200
  port,
200
201
  path: "/status",
201
- timeout: 2e3
202
+ timeout: 2e3,
203
+ signal
202
204
  }, (res) => {
203
205
  let data = "";
206
+ let bytes = 0;
204
207
  res.on("data", (chunk) => {
208
+ bytes += Buffer.byteLength(chunk);
209
+ if (bytes > 65536) return finish(false);
205
210
  data += chunk;
206
211
  });
207
- res.on("end", () => resolve(data.includes("packager-status:running")));
212
+ res.on("end", () => finish(data.includes("packager-status:running")));
213
+ res.on("error", () => finish(false));
214
+ res.on("aborted", () => finish(false));
208
215
  });
209
- req.on("error", () => resolve(false));
210
- req.on("timeout", () => {
216
+ const deadline = setTimeout(() => finish(false), 2e3);
217
+ const finish = (running) => {
218
+ clearTimeout(deadline);
211
219
  req.destroy();
212
- resolve(false);
213
- });
220
+ resolve(running);
221
+ };
222
+ req.on("error", () => finish(false));
223
+ req.on("timeout", () => finish(false));
214
224
  req.end();
215
225
  });
216
226
  }
217
227
  const FIRST_PORT = 8082;
218
- async function computeNextPort(isFree = createPortProbe()) {
228
+ async function computeNextPort(isFree = createPortProbe(), signal) {
229
+ signal?.throwIfAborted();
219
230
  const taken = new Set(allMetroPorts());
220
231
  for (let port = FIRST_PORT; port < 8282; port++) {
221
232
  if (taken.has(port)) continue;
222
- if (await isFree(port)) return port;
233
+ const free = await isFree(port);
234
+ signal?.throwIfAborted();
235
+ if (free) return port;
223
236
  }
224
237
  throw new Error(`Found no free Metro port between ${FIRST_PORT} and 8281. Free one up, or stop a stale bundler (\`stim status\`, \`stim stop <port>\`).`);
225
238
  }
226
- async function findReclaimablePort(excludeProjectPath, probe = isMetroRunning, { isMounted = isOnMountedVolume, mountedVolumes } = {}) {
239
+ async function findReclaimablePort(excludeProjectPath, probe = isMetroRunning, { isMounted = isOnMountedVolume, mountedVolumes, signal } = {}) {
240
+ signal?.throwIfAborted();
227
241
  const cfg = loadConfig();
228
242
  if (!cfg?.projects) return null;
229
243
  const mounted = mountedVolumes || listMountedVolumes();
@@ -238,26 +252,31 @@ async function findReclaimablePort(excludeProjectPath, probe = isMetroRunning, {
238
252
  ownerPath: path
239
253
  });
240
254
  }
241
- for (const c of candidates) if (!await probe(c.port)) return c;
255
+ for (const c of candidates) {
256
+ const alive = await probe(c.port);
257
+ signal?.throwIfAborted();
258
+ if (!alive) return c;
259
+ }
242
260
  return null;
243
261
  }
244
- async function allocatePort(projectPath, probe = isMetroRunning, isFree = createPortProbe()) {
245
- const reclaim = await findReclaimablePort(projectPath, probe);
262
+ async function allocatePort(projectPath, probe = isMetroRunning, isFree = createPortProbe(), signal) {
263
+ const reclaim = await findReclaimablePort(projectPath, probe, { signal });
246
264
  if (reclaim && await isFree(reclaim.port)) {
247
- releaseMetroPort(reclaim.ownerPath, reclaim.port);
265
+ await withConfigLockAsync(() => releaseMetroPort(reclaim.ownerPath, reclaim.port), signal);
248
266
  return reclaim.port;
249
267
  }
250
- return computeNextPort(isFree);
268
+ return computeNextPort(isFree, signal);
251
269
  }
252
270
  const RESERVE_ATTEMPTS = 5;
253
- async function reserveMetroPort(projectPath, probe = isMetroRunning, isFree, pinned = null) {
271
+ async function reserveMetroPort(projectPath, probe = (port) => isMetroRunning(port, signal), isFree, pinned = null, signal) {
272
+ signal?.throwIfAborted();
254
273
  if (pinned !== null) {
255
- if (claimMetroPort(projectPath, pinned) !== null) return pinned;
274
+ if (await withConfigLockAsync(() => claimMetroPort(projectPath, pinned), signal) !== null) return pinned;
256
275
  throw new Error(`metro.port ${pinned} is already reserved by another workspace's Metro or named port.`);
257
276
  }
258
277
  for (let attempt = 0; attempt < RESERVE_ATTEMPTS; attempt++) {
259
- const port = await allocatePort(projectPath, probe, isFree);
260
- const claimed = claimMetroPort(projectPath, port);
278
+ const port = await allocatePort(projectPath, probe, isFree, signal);
279
+ const claimed = await withConfigLockAsync(() => claimMetroPort(projectPath, port), signal);
261
280
  if (claimed !== null) return claimed;
262
281
  }
263
282
  throw new Error(`Could not reserve a Metro port after ${RESERVE_ATTEMPTS} attempts: another Stim run claimed each one first. Retry.`);
@@ -305,6 +324,25 @@ function listeningPids(port, platform = process.platform) {
305
324
  if (pids.length > 0 || platform !== "win32") return pids;
306
325
  return parseNetstatPids(getExecutor().runQuiet("netstat -ano", { timeoutMs: 1e4 }), port);
307
326
  }
327
+ /** Reads listener pids without blocking on lsof or netstat. */
328
+ async function listeningPidsAsync(port, platform = process.platform, signal) {
329
+ const pids = parseLsofPids(await getExecutor().runFileQuietAsync("lsof", [
330
+ "-nP",
331
+ `-iTCP:${port}`,
332
+ "-sTCP:LISTEN",
333
+ "-t"
334
+ ], {
335
+ timeoutMs: 1e4,
336
+ killSignal: "SIGKILL",
337
+ signal
338
+ }));
339
+ if (pids.length > 0 || platform !== "win32") return pids;
340
+ return parseNetstatPids(await getExecutor().runFileQuietAsync("netstat", ["-ano"], {
341
+ timeoutMs: 1e4,
342
+ killSignal: "SIGKILL",
343
+ signal
344
+ }), port);
345
+ }
308
346
  function parseLsofCwd(out) {
309
347
  if (!out) return null;
310
348
  const lines = String(out).split("\n");
@@ -313,8 +351,12 @@ function parseLsofCwd(out) {
313
351
  const nLine = lines.slice(idx + 1).find((l) => l.startsWith("n"));
314
352
  return nLine ? nLine.slice(1) : null;
315
353
  }
316
- function lsofOutput(args) {
317
- return getExecutor().runFileAsync("lsof", args, { timeoutMs: 1e4 }).catch((error) => typeof error?.stdout === "string" ? error.stdout : null);
354
+ function lsofOutput(args, signal) {
355
+ return getExecutor().runFileAsync("lsof", args, {
356
+ timeoutMs: 1e4,
357
+ killSignal: "SIGKILL",
358
+ signal
359
+ }).catch((error) => typeof error?.stdout === "string" ? error.stdout : null);
318
360
  }
319
361
  /**
320
362
  * `listeningPids` for several ports with one lsof call. A port lsof could not be asked about is
@@ -346,7 +388,7 @@ function parseLsofCwds(out) {
346
388
  return cwds;
347
389
  }
348
390
  /** `processCwd` for several pids with at most one lsof call. A pid lsof could not be asked about is missing from the map. */
349
- async function processCwds(pids) {
391
+ async function processCwds(pids, signal) {
350
392
  const cwds = /* @__PURE__ */ new Map();
351
393
  const viaLsof = [];
352
394
  for (const pid of new Set(pids)) {
@@ -365,7 +407,7 @@ async function processCwds(pids) {
365
407
  "-d",
366
408
  "cwd",
367
409
  "-Fn"
368
- ]);
410
+ ], signal);
369
411
  if (out === null) return cwds;
370
412
  const found = parseLsofCwds(out);
371
413
  for (const pid of viaLsof) cwds.set(pid, found.get(pid) ?? null);
@@ -459,16 +501,51 @@ function signalProcessTree(pid, signal = "SIGTERM", { group = false, platform =
459
501
  "/F"
460
502
  ], { timeoutMs: 3e4 }) !== null;
461
503
  }
462
- function killMetroTree(leader, processToken) {
504
+ /** Signals a process tree without blocking on Windows taskkill. */
505
+ async function signalProcessTreeAsync(pid, signal = "SIGTERM", options = {}) {
506
+ options.operation?.signal.throwIfAborted();
507
+ if (!Number.isSafeInteger(pid) || pid <= 1) throw new Error(`Refusing to signal pid ${pid}.`);
508
+ if ((options.platform ?? process.platform) !== "win32") return signalProcessTree(pid, signal, options);
509
+ if (options.operation) {
510
+ await runOperationFile(options.operation, getExecutor(), "taskkill", [
511
+ "/PID",
512
+ String(pid),
513
+ "/T",
514
+ "/F"
515
+ ], {
516
+ cwd: options.operation.cwd,
517
+ timeoutMs: 3e4,
518
+ killSignal: "SIGKILL"
519
+ });
520
+ return true;
521
+ }
522
+ return await getExecutor().runFileQuietAsync("taskkill", [
523
+ "/PID",
524
+ String(pid),
525
+ "/T",
526
+ "/F"
527
+ ], {
528
+ timeoutMs: 3e4,
529
+ killSignal: "SIGKILL"
530
+ }) !== null;
531
+ }
532
+ /** Stops the verified Metro process tree without blocking on Windows taskkill. */
533
+ async function killMetroTreeAsync(leader, processToken, operation) {
534
+ operation?.signal.throwIfAborted();
463
535
  if (!leader || leader === process.pid || (0, process_identity_exports.inspectProcessIdentity)({
464
536
  pid: leader,
465
537
  processToken
466
538
  }) !== "same") return false;
467
539
  try {
468
- return signalProcessTree(leader, "SIGTERM", { group: true });
469
- } catch {
540
+ return await signalProcessTreeAsync(leader, "SIGTERM", {
541
+ group: true,
542
+ operation
543
+ });
544
+ } catch (error) {
545
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
546
+ operation?.signal.throwIfAborted();
470
547
  return false;
471
548
  }
472
549
  }
473
550
  //#endregion
474
- export { listeningPidsByPort as a, processCwd as c, signalProcessTree as d, createPortProbe as f, PortInspectionError as g, PORT_INSPECTION_REMEDY as h, listeningPids as i, processCwds as l, reserveMetroPort as m, isInsideProject as n, parseNetstatPids as o, isMetroRunning as p, killMetroTree as r, pidExists as s, NOT_OURS_FOREIGN_CWD as t, resolveProjectMetro as u };
551
+ export { PORT_INSPECTION_REMEDY as _, listeningPidsAsync as a, pidExists as c, resolveProjectMetro as d, signalProcessTree as f, reserveMetroPort as g, isMetroRunning as h, listeningPids as i, processCwd as l, createPortProbe as m, isInsideProject as n, listeningPidsByPort as o, signalProcessTreeAsync as p, killMetroTreeAsync as r, parseNetstatPids as s, NOT_OURS_FOREIGN_CWD as t, processCwds as u, PortInspectionError as v };
@@ -0,0 +1,209 @@
1
+ import { g as process_identity_exports } from "./exec-DEoAhcze.mjs";
2
+ import { g as workspaceStateFile$1 } from "./paths-DQtTgsBf.mjs";
3
+ import { ot as tailscaleStatusAsync } from "./config-DlrsduwG.mjs";
4
+ import { d as cancelledError, f as throwIfCancelled } from "./operation-exec-BW0em61G.mjs";
5
+ import { l as withWorkspaceStateLockAsync, o as updateWorkspaceState, r as readWorkspaceState$1, s as updateWorkspaceStateAsync } from "./workspace-state-U08dEzhG.mjs";
6
+ import { randomBytes, randomUUID } from "node:crypto";
7
+ import { statSync, watch } from "node:fs";
8
+ import { HOSTED_METRO_REQUESTS_KEY, findPeer, hostedMetroGateways, hostedMetroRequests, isJsonObject, parseMachine } from "@stim-cli/core/state";
9
+ import { isIP } from "node:net";
10
+ //#region src/workspace/workspace-state-watch.ts
11
+ /** Filesystem events are hints; callers retain their own bounded repair and state verification. */
12
+ function watchWorkspaceState(root, changed) {
13
+ let stopped = false;
14
+ let current;
15
+ let startup;
16
+ const clear = () => {
17
+ clearTimeout(startup);
18
+ const previous = current;
19
+ current = void 0;
20
+ previous?.watcher.close();
21
+ };
22
+ const refresh = () => {
23
+ if (stopped) return;
24
+ try {
25
+ const file = workspaceStateFile$1(root);
26
+ const stat = statSync(file);
27
+ if (current?.dev === stat.dev && current.ino === stat.ino) return;
28
+ clear();
29
+ const watcher = watch(file, { persistent: false }, () => {
30
+ if (stopped || current?.watcher !== watcher) return;
31
+ refresh();
32
+ changed();
33
+ });
34
+ current = {
35
+ watcher,
36
+ dev: stat.dev,
37
+ ino: stat.ino
38
+ };
39
+ const lost = () => {
40
+ if (current?.watcher === watcher) clear();
41
+ };
42
+ watcher.once("error", lost);
43
+ watcher.once("close", lost);
44
+ startup = setTimeout(() => {
45
+ if (stopped) return;
46
+ refresh();
47
+ changed();
48
+ }, 500);
49
+ } catch {
50
+ clear();
51
+ }
52
+ };
53
+ refresh();
54
+ return {
55
+ refresh,
56
+ close: () => {
57
+ stopped = true;
58
+ clear();
59
+ }
60
+ };
61
+ }
62
+ //#endregion
63
+ //#region src/device-host/metro-gateway.ts
64
+ async function waitForGateway(root, read, { waitMs, signal, what }) {
65
+ throwIfCancelled(signal, what);
66
+ const startedAt = Date.now();
67
+ const deadline = startedAt + waitMs;
68
+ let observer;
69
+ let timer;
70
+ let queued;
71
+ let onAbort;
72
+ try {
73
+ return await new Promise((resolve, reject) => {
74
+ const check = () => {
75
+ clearImmediate(queued);
76
+ queued = void 0;
77
+ clearTimeout(timer);
78
+ try {
79
+ throwIfCancelled(signal, what);
80
+ observer?.refresh();
81
+ const result = read();
82
+ const remaining = deadline - Date.now();
83
+ if (result !== null || remaining <= 0) resolve(result);
84
+ else {
85
+ const repairMs = Date.now() - startedAt < 500 ? 100 : 1e3;
86
+ timer = setTimeout(check, Math.min(remaining, repairMs));
87
+ }
88
+ } catch (error) {
89
+ reject(error);
90
+ }
91
+ };
92
+ onAbort = () => reject(cancelledError(what));
93
+ signal?.addEventListener("abort", onAbort, { once: true });
94
+ observer = watchWorkspaceState(root, () => {
95
+ queued ??= setImmediate(check);
96
+ });
97
+ check();
98
+ });
99
+ } finally {
100
+ observer?.close();
101
+ clearTimeout(timer);
102
+ clearImmediate(queued);
103
+ if (onAbort) signal?.removeEventListener("abort", onAbort);
104
+ }
105
+ }
106
+ function gatewayAddresses(credential, status) {
107
+ const machine = parseMachine(credential.machine);
108
+ const peer = machine ? findPeer(status, machine.name) : "missing";
109
+ if (typeof peer === "string") throw new Error(`${credential.machine} no longer resolves to one tailnet peer. Run stim doctor.`);
110
+ const self = isJsonObject(status) && isJsonObject(status.Self) ? status.Self : null;
111
+ const address = (Array.isArray(self?.TailscaleIPs) ? self.TailscaleIPs : []).find((ip) => typeof ip === "string" && isIP(ip) === isIP(peer.address) && isIP(ip) !== 0);
112
+ if (!address) throw new Error("This machine has no Tailscale address compatible with the hosting Mac.");
113
+ return {
114
+ address,
115
+ peer: peer.address
116
+ };
117
+ }
118
+ function requireHostedMetro(root) {
119
+ const supervisor = readWorkspaceState$1(root)?.supervisor;
120
+ if (supervisor?.hostedMetro === true && (0, process_identity_exports.inspectProcessIdentity)(supervisor) === "same") return;
121
+ throw Object.assign(/* @__PURE__ */ new Error("Hosted Debug runs require a running Metro supervisor with private gateway support. Run stim stop; stim start, then retry."), {
122
+ code: "STIM_HOSTING_REFUSED",
123
+ remedy: "Run stim stop; stim start, then retry."
124
+ });
125
+ }
126
+ async function requestHostedMetro(root, session, credential, signal) {
127
+ throwIfCancelled(signal, "hosted Metro gateway");
128
+ requireHostedMetro(root);
129
+ const addresses = gatewayAddresses(credential, await tailscaleStatusAsync(signal));
130
+ throwIfCancelled(signal, "hosted Metro gateway");
131
+ requireHostedMetro(root);
132
+ const previous = hostedMetroRequests(readWorkspaceState$1(root))[session];
133
+ const request = previous && previous.address === addresses.address && previous.peer === addresses.peer ? previous : {
134
+ id: randomUUID(),
135
+ machine: credential.machine,
136
+ ...addresses,
137
+ secret: randomBytes(32).toString("hex")
138
+ };
139
+ await updateWorkspaceStateAsync(root, (state) => ({
140
+ ...state,
141
+ [HOSTED_METRO_REQUESTS_KEY]: {
142
+ ...hostedMetroRequests(state),
143
+ [session]: request
144
+ }
145
+ }), signal);
146
+ const opened = await waitForGateway(root, () => {
147
+ const state = readWorkspaceState$1(root);
148
+ const gateway = hostedMetroGateways(state)[session];
149
+ if (gateway?.id === request.id && gateway.processToken === state?.supervisor?.processToken) {
150
+ if (gateway.error) throw new Error(gateway.error);
151
+ if (gateway.port) return {
152
+ gatewayPort: gateway.port,
153
+ secret: request.secret
154
+ };
155
+ }
156
+ return null;
157
+ }, {
158
+ waitMs: 15e3,
159
+ signal,
160
+ what: "hosted Metro gateway"
161
+ });
162
+ if (opened) return opened;
163
+ throw new Error(`The Metro supervisor did not open a private gateway for ${credential.machine}. Run stim stop; stim start, then retry.`);
164
+ }
165
+ function clearHostedMetro(root, session) {
166
+ updateWorkspaceState(root, (state) => {
167
+ const requests = { ...hostedMetroRequests(state) };
168
+ delete requests[session];
169
+ return {
170
+ ...state,
171
+ [HOSTED_METRO_REQUESTS_KEY]: requests
172
+ };
173
+ });
174
+ }
175
+ async function closeHostedMetro(root, session, signal) {
176
+ await withWorkspaceStateLockAsync(root, () => clearHostedMetro(root, session), signal);
177
+ if (!await waitForGateway(root, () => {
178
+ const state = readWorkspaceState$1(root);
179
+ const gateway = hostedMetroGateways(state)[session];
180
+ const supervisor = state?.supervisor;
181
+ if (!gateway?.port || !supervisor || gateway.processToken !== supervisor.processToken) return true;
182
+ return (0, process_identity_exports.inspectProcessIdentity)(supervisor) === "gone" ? true : null;
183
+ }, {
184
+ waitMs: 5e3,
185
+ signal,
186
+ what: "close hosted Metro"
187
+ })) throw new Error("The Metro supervisor did not close its private gateway. Restart Metro, then retry stim stop.");
188
+ }
189
+ function hostedMetroSettings(settings, hosted) {
190
+ return hosted ? {
191
+ ...settings,
192
+ ios: {
193
+ ...isJsonObject(settings.ios) ? settings.ios : {},
194
+ remote: void 0
195
+ },
196
+ android: {
197
+ ...isJsonObject(settings.android) ? settings.android : {},
198
+ remote: void 0
199
+ },
200
+ metro: {
201
+ ...isJsonObject(settings.metro) ? settings.metro : {},
202
+ tunnel: void 0,
203
+ publicUrl: void 0,
204
+ ngrokUrl: void 0
205
+ }
206
+ } : settings;
207
+ }
208
+ //#endregion
209
+ export { requireHostedMetro as a, requestHostedMetro as i, closeHostedMetro as n, watchWorkspaceState as o, hostedMetroSettings as r, clearHostedMetro as t };
@@ -1,63 +1,7 @@
1
- import { C as saveConfig, D as withConfigLock, _ as loadConfig, c as ensureConfig, d as getConfigDir, j as ownership_claim_exports, k as process_identity_exports, p as getProject } from "./config-BYDxlMY3.mjs";
2
- import { t as getExecutor } from "./exec-DqKo9GKJ.mjs";
3
- import { d as signalProcessTree, f as createPortProbe, o as parseNetstatPids } from "./metro-DnCEjCxs.mjs";
4
- import { n as declareSpawnsOn, o as stopDeclaringSpawnsOn } from "./spawn-claims-DVS5RkMS.mjs";
1
+ import { g as process_identity_exports, i as withWorkspaceProcessLock, t as getExecutor } from "./exec-DEoAhcze.mjs";
2
+ import { C as saveConfig, O as withConfigLockAsync, _ as loadConfig, c as ensureConfig, d as getConfigDir, p as getProject } from "./config-DlrsduwG.mjs";
3
+ import { f as signalProcessTree, m as createPortProbe, s as parseNetstatPids } from "./metro-DiXtA3Ty.mjs";
5
4
  import { realpathSync } from "node:fs";
6
- import { join } from "node:path";
7
- import { setTimeout } from "node:timers/promises";
8
- //#region src/engine/workspace-process-lock.ts
9
- const DEFAULT_WAIT_MS = 6e4;
10
- const POLL_MS = 25;
11
- function workspaceProcessLockError(err) {
12
- const code = err?.code;
13
- if (code === "STIM_LOCK_REFUSED") return "refused";
14
- if (code === "STIM_LOCK_TIMEOUT") return "timeout";
15
- return null;
16
- }
17
- function workspaceProcessLockPath(root, name, external) {
18
- return external ? join(root, `${name}.lock`) : join(root, ".stim", `${name}.lock`);
19
- }
20
- async function withWorkspaceProcessLock(root, name, fn, { now = Date.now, signal, sleep = (ms) => setTimeout(ms, void 0, { signal }), waitMs = DEFAULT_WAIT_MS, ownerPurpose, details = {}, onHeld, rejectOwnerPurposes = [], external = false, declareSpawns = false } = {}) {
21
- const path = workspaceProcessLockPath(root, name, external);
22
- const deadline = now() + waitMs;
23
- for (;;) {
24
- signal?.throwIfAborted();
25
- const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
26
- root: path,
27
- mode: "exclusive",
28
- label: `${name} lock`,
29
- details: ownerPurpose ? {
30
- ...details,
31
- purpose: ownerPurpose
32
- } : details
33
- });
34
- if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
35
- if (attempt.acquired) {
36
- if (declareSpawns) declareSpawnsOn(attempt.acquired);
37
- try {
38
- return await fn(attempt.acquired);
39
- } finally {
40
- stopDeclaringSpawnsOn(attempt.acquired);
41
- (0, ownership_claim_exports.releaseClaim)(attempt.acquired);
42
- }
43
- }
44
- const holder = attempt.held ?? attempt.waitingFor?.[0];
45
- const purpose = holder?.details.purpose;
46
- if (typeof purpose === "string" && rejectOwnerPurposes.includes(purpose)) {
47
- const error = /* @__PURE__ */ new Error(`The ${name} lock at ${path} is held for ${purpose}.`);
48
- error.code = "STIM_LOCK_REFUSED";
49
- throw error;
50
- }
51
- if (holder && onHeld) await onHeld(holder);
52
- if (now() >= deadline) {
53
- const error = /* @__PURE__ */ new Error(`Timed out waiting for the ${name} lock at ${path}.`);
54
- error.code = "STIM_LOCK_TIMEOUT";
55
- throw error;
56
- }
57
- await sleep(POLL_MS);
58
- }
59
- }
60
- //#endregion
61
5
  //#region src/named-ports.ts
62
6
  const FIRST_PORT = 8900;
63
7
  const LAST_PORT = 8999;
@@ -112,8 +56,11 @@ function processCommand(pid, platform) {
112
56
  "args="
113
57
  ], { timeoutMs: 5e3 }) ?? "").trim();
114
58
  }
115
- function withPortsLock(fn) {
116
- return withWorkspaceProcessLock(getConfigDir(), "ports", fn, { external: true });
59
+ function withPortsLock(fn, signal) {
60
+ return withWorkspaceProcessLock(getConfigDir(), "ports", fn, {
61
+ external: true,
62
+ signal
63
+ });
117
64
  }
118
65
  async function getNamedPort(projectPath, label, options = {}) {
119
66
  validatePortLabel(label);
@@ -123,14 +70,14 @@ async function getNamedPort(projectPath, label, options = {}) {
123
70
  function reserveBrowserPort(projectPath, options = {}) {
124
71
  return allocateNamedPort(projectPath, BROWSER_LABEL, options);
125
72
  }
126
- function releaseBrowserPort(projectPath, port) {
127
- return withPortsLock(async () => withConfigLock(() => {
73
+ function releaseBrowserPort(projectPath, port, signal) {
74
+ return withPortsLock(() => withConfigLockAsync(() => {
128
75
  const cfg = loadConfig();
129
76
  const ports = cfg?.projects?.[projectPath]?.ports;
130
77
  if (!cfg || ports?.[BROWSER_LABEL] !== port) return;
131
78
  delete ports[BROWSER_LABEL];
132
79
  saveConfig(cfg);
133
- }));
80
+ }, signal), signal);
134
81
  }
135
82
  async function allocateNamedPort(projectPath, label, { isFree = createPortProbe() }) {
136
83
  const root = realpathSync(projectPath);
@@ -140,7 +87,7 @@ async function allocateNamedPort(projectPath, label, { isFree = createPortProbe(
140
87
  for (let port = FIRST_PORT; port <= LAST_PORT; port++) {
141
88
  if (Object.values(loadConfig()?.projects ?? {}).some((project) => project.metroPort === port || Object.values(project.ports ?? {}).includes(port))) continue;
142
89
  if (!await isFree(port)) continue;
143
- if (withConfigLock(() => {
90
+ if (await withConfigLockAsync(() => {
144
91
  const cfg = ensureConfig();
145
92
  if (Object.values(cfg.projects).some((project) => project.metroPort === port || Object.values(project.ports ?? {}).includes(port))) return false;
146
93
  const project = cfg.projects[root] ??= {
@@ -158,9 +105,10 @@ async function allocateNamedPort(projectPath, label, { isFree = createPortProbe(
158
105
  throw new Error(`No free named port between ${FIRST_PORT} and ${LAST_PORT}. Use stim ports stop or stim ports release in a workspace that no longer needs its ports.`);
159
106
  });
160
107
  }
161
- async function stopListeners(port, label, dryRun, log, platform) {
108
+ async function stopListeners(port, label, dryRun, log, platform, signal) {
162
109
  const listeners = () => portListeners(port, platform);
163
110
  for (const pid of listeners()) {
111
+ signal?.throwIfAborted();
164
112
  if (pid === process.pid) throw new Error(`Refusing to stop Stim itself on ${label} (${port}).`);
165
113
  const identity = (0, process_identity_exports.captureProcessIdentity)(pid);
166
114
  if (!identity.ok) {
@@ -182,10 +130,10 @@ async function stopListeners(port, label, dryRun, log, platform) {
182
130
  if (status !== "same") throw new Error(`Cannot verify pid ${pid} on ${label} (${port}); allocation kept.`);
183
131
  try {
184
132
  signalProcessTree(pid, "SIGTERM", { platform });
185
- if (!await (0, process_identity_exports.waitForProcessExit)(record, 2e3)) {
133
+ if (!await (0, process_identity_exports.waitForProcessExit)(record, 2e3, signal)) {
186
134
  if ((0, process_identity_exports.inspectProcessIdentity)(record) !== "same") throw new Error(`Cannot verify pid ${pid} before SIGKILL.`);
187
135
  signalProcessTree(pid, "SIGKILL", { platform });
188
- if (!await (0, process_identity_exports.waitForProcessExit)(record, 2e3)) throw new Error(`Pid ${pid} did not exit.`);
136
+ if (!await (0, process_identity_exports.waitForProcessExit)(record, 2e3, signal)) throw new Error(`Pid ${pid} did not exit.`);
189
137
  }
190
138
  } catch (error) {
191
139
  if (error.code !== "ESRCH") throw error;
@@ -194,36 +142,40 @@ async function stopListeners(port, label, dryRun, log, platform) {
194
142
  }
195
143
  if (!dryRun && listeners().length) throw new Error(`Port ${port} still has a listener; allocation kept.`);
196
144
  }
197
- async function clearNamedPorts(projectPath, { label, stop = false, dryRun = false, log = console.error, platform = process.platform } = {}) {
145
+ async function clearNamedPorts(projectPath, { label, stop = false, dryRun = false, log = console.error, platform = process.platform, signal } = {}) {
198
146
  if (label !== void 0) validatePortLabel(label);
199
147
  await withPortsLock(async () => {
200
148
  const ports = getProject(projectPath)?.ports ?? {};
201
149
  const selected = Object.entries(ports).filter(([name]) => name !== BROWSER_LABEL && (label === void 0 || name === label));
202
150
  if (stop && selected.length > 0 && platform !== "win32" && !getExecutor().findExecutable("lsof")) throw new Error("Cannot stop named ports: lsof is not installed.");
203
151
  const failures = [];
204
- for (const [name, port] of selected) try {
205
- if (!Number.isInteger(port) || port < FIRST_PORT || port > LAST_PORT) throw new Error(`Invalid named port ${name} (${port}); repair its config record before cleanup.`);
206
- if (stop) {
207
- if (Object.values(loadConfig()?.projects ?? {}).some((project) => project.metroPort === port)) throw new Error(`Port ${port} is reserved for managed Metro; allocation kept.`);
208
- await stopListeners(port, name, dryRun, log, platform);
209
- }
210
- if (dryRun) {
211
- log(`would release ${name} (${port})`);
212
- continue;
152
+ for (const [name, port] of selected) {
153
+ signal?.throwIfAborted();
154
+ try {
155
+ if (!Number.isInteger(port) || port < FIRST_PORT || port > LAST_PORT) throw new Error(`Invalid named port ${name} (${port}); repair its config record before cleanup.`);
156
+ if (stop) {
157
+ if (Object.values(loadConfig()?.projects ?? {}).some((project) => project.metroPort === port)) throw new Error(`Port ${port} is reserved for managed Metro; allocation kept.`);
158
+ await stopListeners(port, name, dryRun, log, platform, signal);
159
+ }
160
+ if (dryRun) {
161
+ log(`would release ${name} (${port})`);
162
+ continue;
163
+ }
164
+ await withConfigLockAsync(() => {
165
+ const cfg = loadConfig();
166
+ const current = cfg?.projects?.[projectPath]?.ports;
167
+ if (!cfg || current?.[name] !== port) return;
168
+ delete current[name];
169
+ saveConfig(cfg);
170
+ }, signal);
171
+ log(`released ${name} (${port})`);
172
+ } catch (error) {
173
+ signal?.throwIfAborted();
174
+ failures.push(`${name} (${port}): ${error.message}`);
213
175
  }
214
- withConfigLock(() => {
215
- const cfg = loadConfig();
216
- const current = cfg?.projects?.[projectPath]?.ports;
217
- if (!cfg || current?.[name] !== port) return;
218
- delete current[name];
219
- saveConfig(cfg);
220
- });
221
- log(`released ${name} (${port})`);
222
- } catch (error) {
223
- failures.push(`${name} (${port}): ${error.message}`);
224
176
  }
225
177
  if (failures.length) throw new Error(failures.join("\n"));
226
- });
178
+ }, signal);
227
179
  }
228
180
  //#endregion
229
- export { withWorkspaceProcessLock as a, reserveBrowserPort as i, getNamedPort as n, workspaceProcessLockError as o, releaseBrowserPort as r, workspaceProcessLockPath as s, clearNamedPorts as t };
181
+ export { reserveBrowserPort as i, getNamedPort as n, releaseBrowserPort as r, clearNamedPorts as t };