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
@@ -0,0 +1,955 @@
1
+ import { _ as reportSignal, g as process_identity_exports, r as WorkspaceProcessCleanupError, t as getExecutor, x as ownership_claim_exports, y as trackReportResource } from "./exec-DEoAhcze.mjs";
2
+ import { t as runOperationFile } from "./operation-exec-BW0em61G.mjs";
3
+ import { join } from "node:path";
4
+ import { ambientEnv, configDir, ensurePrivateDir } from "@stim-cli/core";
5
+ import { allMetroPorts, cannotHost, clientIdentity, configCorruptRepair, endpoint, findEnclosingWorktreeRoot, findPeer as findPeer$1, getConcurrencyLimits, getConfigPath, getConfigPath as getConfigPath$1, getProject, getProjectSettings, getRepoSettings, isJsonObject, isPathPrefix, loadConfig, loadConfig as loadConfig$1, machineEndpoint, parseMachine, parseMachine as parseMachine$1, validateAutomaticPool, withConfigLock, withConfigLock as withConfigLock$1, withConfigLockAsync, withConfigLockAsync as withConfigLockAsync$1 } from "@stim-cli/core/state";
6
+ import { dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, normalize, parse, sep as sep$1 } from "path";
7
+ import { WebSocket } from "ws";
8
+ import { HandshakeRefusal, MAX_SERVER_MESSAGE_BYTES, TRANSPORT_OVERHEAD_BYTES, decodeSecret, initiateNoise, newSecret } from "@stim-cli/core/noise-session";
9
+ import { existsSync, lstatSync, readdirSync, readlinkSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
10
+ import { InvalidArgumentError } from "commander";
11
+ //#region src/offload/access-ticket.ts
12
+ /** The setup ticket in `STIM_ACCESS_TICKET`, which Stim Desktop's Add Machine flow sets, or undefined. */
13
+ function readAccessTicket() {
14
+ const ticket = ambientEnv().STIM_ACCESS_TICKET?.trim();
15
+ return ticket && decodeSecret(ticket) ? ticket : void 0;
16
+ }
17
+ function readHostPermissions(value) {
18
+ const host = value;
19
+ return host && typeof host.name === "string" && typeof host.screenRecording === "boolean" && typeof host.accessibility === "boolean" ? {
20
+ name: host.name,
21
+ screenRecording: host.screenRecording,
22
+ accessibility: host.accessibility
23
+ } : void 0;
24
+ }
25
+ //#endregion
26
+ //#region src/offload/tailnet.ts
27
+ const MAC_APP_TAILSCALE = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
28
+ const HELLO_TIMEOUT_MS = 1e4;
29
+ /** How long a setup ticket `stim doctor --fix` generates stays valid; `stim-server setup` takes at most 2 hours. */
30
+ const GENERATED_TICKET_MS = 714e4;
31
+ function parseTailscaleStatus(output) {
32
+ try {
33
+ const status = JSON.parse(output);
34
+ return isJsonObject(status) && status.BackendState === "Running" ? status : null;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+ function tailscaleStatus() {
40
+ for (const binary of ["tailscale", MAC_APP_TAILSCALE]) {
41
+ const output = getExecutor().runFileQuiet(binary, ["status", "--json"], { timeoutMs: 5e3 });
42
+ if (output !== null) return parseTailscaleStatus(output);
43
+ }
44
+ return null;
45
+ }
46
+ async function tailscaleStatusAsync(signal) {
47
+ for (const binary of ["tailscale", MAC_APP_TAILSCALE]) {
48
+ signal?.throwIfAborted();
49
+ let output;
50
+ try {
51
+ output = await getExecutor().runFileAsync(binary, ["status", "--json"], {
52
+ timeoutMs: 5e3,
53
+ signal
54
+ });
55
+ } catch {
56
+ signal?.throwIfAborted();
57
+ continue;
58
+ }
59
+ signal?.throwIfAborted();
60
+ return parseTailscaleStatus(output);
61
+ }
62
+ return null;
63
+ }
64
+ const TURNED_AWAY = /* @__PURE__ */ new Set([
65
+ "unauthorized",
66
+ "forbidden",
67
+ "protocol-unsupported",
68
+ "server-key-mismatch"
69
+ ]);
70
+ /** The connect code for a protocol 1 stim-server, which closes on the binary Noise prefix before answering it. */
71
+ const SERVER_OUTDATED = "server-outdated";
72
+ /**
73
+ * The doctor finding for a protocol 1 stim-server on `entry`, shared by the build and device-host checks so doctor
74
+ * prints it once. `command` is the setup command, when this machine has an unexpired ticket for it.
75
+ */
76
+ function outdatedServer(entry, version, command) {
77
+ const update = `On ${entry}, run \`stim-server service update --release ${version}\` (add \`--label <name>\` for a custom service label; a server Stim Desktop runs updates with Stim Desktop)`;
78
+ return {
79
+ title: `Remote machine ${entry} runs an older stim-server`,
80
+ detail: `The stim-server on ${entry} speaks an older protocol than this Stim, so it cannot authenticate this machine.`,
81
+ fix: command ? `${update}, then run this there to approve this machine: ${command}\nAfter approving, run \`stim doctor\` on this machine to reconnect.` : `${update}, then run \`stim doctor --fix\` here.`
82
+ };
83
+ }
84
+ /**
85
+ * Opens a WebSocket to `target` and runs the protocol 2 handshake with this machine's client key. A returning client
86
+ * goes ahead only with the server that proves it holds the pinned key.
87
+ */
88
+ function openAuthenticated(target, auth, timeoutMs, signal, operation) {
89
+ signal?.throwIfAborted();
90
+ return new Promise((resolve) => {
91
+ const options = {
92
+ handshakeTimeout: timeoutMs,
93
+ servername: target.servername,
94
+ headers: { Host: target.host },
95
+ maxPayload: MAX_SERVER_MESSAGE_BYTES + TRANSPORT_OVERHEAD_BYTES
96
+ };
97
+ const socket = new WebSocket(target.url, options);
98
+ if (reportSignal(operation)) trackReportResource(operation, new Promise((done) => socket.once("close", () => done())), () => socket.terminate());
99
+ let settled = false;
100
+ const finish = (value) => {
101
+ if (settled) return;
102
+ settled = true;
103
+ signal?.removeEventListener("abort", cancel);
104
+ clearTimeout(timer);
105
+ if (!("send" in value)) {
106
+ socket.removeAllListeners("open");
107
+ socket.removeAllListeners("message");
108
+ socket.removeAllListeners("error");
109
+ socket.on("error", () => {});
110
+ socket.terminate();
111
+ }
112
+ resolve(value);
113
+ };
114
+ const cancel = () => finish({
115
+ failure: "cancelled",
116
+ refused: false,
117
+ code: "STIM_CANCELLED"
118
+ });
119
+ const timer = setTimeout(() => finish({
120
+ failure: "no reply in time",
121
+ refused: false
122
+ }), timeoutMs);
123
+ signal?.addEventListener("abort", cancel, { once: true });
124
+ if (signal?.aborted) return cancel();
125
+ const failed = (error) => finish({
126
+ failure: /Unexpected server response: 502/.test(error.message) ? "no stim-server answers behind its tailscale serve route (HTTP 502)" : error.message,
127
+ refused: false
128
+ });
129
+ socket.once("error", failed);
130
+ socket.once("open", () => {
131
+ const secret = "ticket" in auth ? decodeSecret(auth.ticket) : null;
132
+ let keyPair;
133
+ try {
134
+ keyPair = clientIdentity();
135
+ } catch (error) {
136
+ return finish({
137
+ failure: `this machine's client key is unavailable: ${error.message}`,
138
+ refused: false
139
+ });
140
+ }
141
+ (async () => {
142
+ try {
143
+ const noise = await initiateNoise(socket, {
144
+ keyPair,
145
+ ..."serverKey" in auth ? { serverKey: Buffer.from(auth.serverKey, "base64url") } : {},
146
+ ...secret ? {
147
+ secret,
148
+ name: "name" in auth ? auth.name : ""
149
+ } : {},
150
+ timeoutMs
151
+ });
152
+ socket.off("error", failed);
153
+ socket.on("error", () => {});
154
+ finish(noise);
155
+ } catch (error) {
156
+ const code = error instanceof HandshakeRefusal ? error.code : "handshake-failed";
157
+ if (code === "unexpected binary frame") return finish({
158
+ failure: "its stim-server speaks an older protocol than this Stim",
159
+ refused: true,
160
+ code: SERVER_OUTDATED
161
+ });
162
+ finish({
163
+ failure: error.message,
164
+ refused: TURNED_AWAY.has(code) || code === "unknown-secret",
165
+ code
166
+ });
167
+ }
168
+ })();
169
+ });
170
+ });
171
+ }
172
+ /** Sends `hello` on an authenticated connection; a close before the reply carries the server's refusal code. */
173
+ function sayHello(socket, params, timeoutMs) {
174
+ return new Promise((resolve) => {
175
+ const done = (reply) => {
176
+ clearTimeout(timer);
177
+ socket.off("message", onMessage);
178
+ socket.off("close", onClose);
179
+ resolve(reply);
180
+ };
181
+ const onMessage = (data, isBinary) => {
182
+ if (isBinary) return;
183
+ let message;
184
+ try {
185
+ message = JSON.parse(data.toString("utf8"));
186
+ } catch {
187
+ message = null;
188
+ }
189
+ if (!isJsonObject(message) || message.id !== 1) return;
190
+ if (isJsonObject(message.error)) return done({ error: {
191
+ code: String(message.error.code),
192
+ message: String(message.error.message)
193
+ } });
194
+ done({ result: isJsonObject(message.result) ? message.result : {} });
195
+ };
196
+ const onClose = (code, reason) => done({ error: {
197
+ code: reason.length ? reason.toString() : "closed",
198
+ message: `the connection closed before a hello result (${code}${reason.length ? ` ${reason.toString()}` : ""})`
199
+ } });
200
+ const timer = setTimeout(() => done({ error: {
201
+ code: "timeout",
202
+ message: "no reply in time"
203
+ } }), timeoutMs);
204
+ socket.on("message", onMessage);
205
+ socket.on("close", onClose);
206
+ socket.send(JSON.stringify({
207
+ id: 1,
208
+ method: "hello",
209
+ params
210
+ }));
211
+ });
212
+ }
213
+ async function hello(target, auth, operation) {
214
+ const socket = await openAuthenticated(target, auth, HELLO_TIMEOUT_MS, reportSignal(operation), operation);
215
+ if (!("send" in socket)) return socket.code ? { error: {
216
+ code: socket.code,
217
+ message: socket.failure
218
+ } } : { failed: socket.failure };
219
+ try {
220
+ if ("onServerKey" in auth) await auth.onServerKey?.(socket.remoteKey.toString("base64url"));
221
+ } catch (error) {
222
+ socket.close(1e3);
223
+ throw error;
224
+ }
225
+ const reply = await sayHello(socket, {
226
+ client: {
227
+ name: "stim",
228
+ version: "1"
229
+ },
230
+ access: auth.access
231
+ }, HELLO_TIMEOUT_MS);
232
+ socket.close(1e3);
233
+ if ("error" in reply) return reply;
234
+ const { result } = reply;
235
+ const device = isJsonObject(result.device) ? result.device : null;
236
+ if (!device || typeof device.id !== "string" || !Array.isArray(result.capabilities)) return { failed: "the reply was not a hello result" };
237
+ return {
238
+ result,
239
+ serverKey: socket.remoteKey.toString("base64url")
240
+ };
241
+ }
242
+ const realIo = {
243
+ status: tailscaleStatus,
244
+ hello
245
+ };
246
+ function doctorIo(operation) {
247
+ return {
248
+ status: () => tailscaleStatusAsync(operation?.signal),
249
+ hello: (target, auth) => hello(target, auth, operation)
250
+ };
251
+ }
252
+ /** Where to reach a paired machine now, or why Stim cannot; the handshake then checks its pinned key. */
253
+ async function resolveEndpoint(credential, status = () => tailscaleStatusAsync(signal), signal) {
254
+ return machineEndpoint(credential.machine, parseMachine(credential.machine) ? await status() : null);
255
+ }
256
+ /** The remedy for a pending request: the printed setup command, or where the person finds it. */
257
+ function setupRemedy(entry, command) {
258
+ return command ? `A person on ${entry} runs this there to approve this machine: ${command}\nAfter approving, run \`stim doctor\` on this machine to reconnect.` : `A person on ${entry} runs the \`stim-server setup\` command Stim Desktop showed for this machine.`;
259
+ }
260
+ function setupCommand(ticket, expiresAt, flags, version) {
261
+ return `STIM_SETUP_TICKET=${ticket} STIM_SETUP_ORIGIN=doctor npx --yes --package @stim-cli/server@${version} stim-server setup --expires ${expiresAt} ${flags.map((flag) => `--${flag}`).join(" ")}`;
262
+ }
263
+ function newPending(input, ticket, expiresAt, now) {
264
+ return {
265
+ machine: input.machine,
266
+ transport: "tailscale",
267
+ address: input.address,
268
+ state: "pending",
269
+ requestedAt: new Date(now).toISOString(),
270
+ ticket,
271
+ ...expiresAt ? { expiresAt } : {}
272
+ };
273
+ }
274
+ function approved(input, reply, requestedAt) {
275
+ return {
276
+ state: "approved",
277
+ credential: {
278
+ machine: input.machine,
279
+ transport: "tailscale",
280
+ address: input.address,
281
+ state: "approved",
282
+ requestedAt,
283
+ serverKey: reply.serverKey,
284
+ deviceId: reply.result.device.id
285
+ },
286
+ result: reply.result
287
+ };
288
+ }
289
+ /**
290
+ * Connects to one remote machine for one access and says what changed. A returning client pins the stored server key.
291
+ * A pending client spends its setup ticket in first contact until the worker's `stim-server setup` approves it; the
292
+ * worker answers `unknown-secret` before that. With `fix`, a machine with no credential, or a revoked or lapsed one,
293
+ * gets a new setup ticket: `STIM_ACCESS_TICKET` when set (Stim Desktop's Add Machine flow), else one this machine
294
+ * generates, with the command to run on the worker. A pending credential is replaced when `STIM_ACCESS_TICKET` names
295
+ * another ticket.
296
+ */
297
+ async function reachMachine(input) {
298
+ const now = input.now ?? Date.now();
299
+ const { credential, io, access } = input;
300
+ const given = readAccessTicket();
301
+ const ask = () => {
302
+ if (given) return firstContact(newPending(input, given, void 0, now));
303
+ const other = input.other();
304
+ const shared = other?.state === "pending" && other.expiresAt ? other : void 0;
305
+ const ticket = shared ? {
306
+ ticket: shared.ticket,
307
+ expiresAt: shared.expiresAt
308
+ } : input.tickets.get(input.machine) ?? {
309
+ ticket: newSecret(),
310
+ expiresAt: new Date(now + GENERATED_TICKET_MS).toISOString()
311
+ };
312
+ input.tickets.set(input.machine, ticket);
313
+ return firstContact(newPending(input, ticket.ticket, ticket.expiresAt, now));
314
+ };
315
+ const firstContact = async (asked) => {
316
+ let pending = asked;
317
+ if (pending.serverKey) {
318
+ const resumed = await io.hello(input.endpoint, {
319
+ access,
320
+ serverKey: pending.serverKey
321
+ });
322
+ if ("result" in resumed && resumed.result.capabilities.includes(access)) return approved(input, resumed, pending.requestedAt);
323
+ }
324
+ const reply = await io.hello(input.endpoint, {
325
+ access,
326
+ ticket: pending.ticket,
327
+ name: input.deviceName,
328
+ onServerKey: (serverKey) => {
329
+ pending = {
330
+ ...pending,
331
+ serverKey
332
+ };
333
+ return input.persist(pending);
334
+ }
335
+ });
336
+ if ("result" in reply && reply.result.capabilities.includes(access)) return approved(input, reply, pending.requestedAt);
337
+ const command = pending.expiresAt ? setupCommand(pending.ticket, pending.expiresAt, input.setupFlags, input.serverVersion) : null;
338
+ if ("error" in reply && reply.error.code === "server-outdated") return {
339
+ state: "outdated",
340
+ credential: pending,
341
+ command
342
+ };
343
+ const sibling = "error" in reply && reply.error.code === "unknown-secret" ? input.other()?.serverKey : void 0;
344
+ if (sibling && sibling !== pending.serverKey) {
345
+ const again = await io.hello(input.endpoint, {
346
+ access,
347
+ serverKey: sibling
348
+ });
349
+ if ("result" in again && again.result.capabilities.includes(access)) return approved(input, again, pending.requestedAt);
350
+ }
351
+ if ("error" in reply && (reply.error.code === "unknown-secret" || reply.error.code === "forbidden")) return {
352
+ state: "pending",
353
+ credential: pending,
354
+ command,
355
+ reason: reply.error.message
356
+ };
357
+ return {
358
+ state: "unreachable",
359
+ credential: pending,
360
+ reason: "failed" in reply ? reply.failed : "error" in reply ? reply.error.message : "it did not grant this access"
361
+ };
362
+ };
363
+ if (!credential) return input.fix ? ask() : { state: "not-asked" };
364
+ if (credential.state === "approved") {
365
+ const reply = await io.hello(input.endpoint, {
366
+ access,
367
+ serverKey: credential.serverKey
368
+ });
369
+ if ("result" in reply && reply.result.capabilities.includes(access)) return approved(input, reply, credential.requestedAt);
370
+ if ("error" in reply && reply.error.code === "server-key-mismatch") return {
371
+ state: "key-changed",
372
+ credential
373
+ };
374
+ if ("error" in reply && reply.error.code === "server-outdated") return {
375
+ state: "outdated",
376
+ credential,
377
+ command: null
378
+ };
379
+ if ("error" in reply && (reply.error.code === "unauthorized" || reply.error.code === "forbidden")) return input.fix ? ask() : {
380
+ state: "revoked",
381
+ credential,
382
+ reason: reply.error.message
383
+ };
384
+ if ("result" in reply) return input.fix ? ask() : {
385
+ state: "revoked",
386
+ credential,
387
+ reason: "it no longer grants this access"
388
+ };
389
+ return {
390
+ state: "unreachable",
391
+ credential,
392
+ reason: "failed" in reply ? reply.failed : reply.error.message
393
+ };
394
+ }
395
+ if (input.fix && given && given !== credential.ticket) return ask();
396
+ if (credential.expiresAt && Date.parse(credential.expiresAt) <= now) return input.fix ? ask() : {
397
+ state: "lapsed",
398
+ credential
399
+ };
400
+ return firstContact(credential);
401
+ }
402
+ //#endregion
403
+ //#region src/devices/device-slots.ts
404
+ const DEFAULT_DEVICE_SLOT = "default";
405
+ const DEVICE_SLOT_RULE = "A device slot must be 1-64 letters, digits, underscores or hyphens, starting with a letter or digit.";
406
+ function isDeviceSlot(slot) {
407
+ return /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(slot) && ![
408
+ "constructor",
409
+ "prototype",
410
+ "__proto__"
411
+ ].includes(slot);
412
+ }
413
+ function validateDeviceSlot(slot = "default") {
414
+ if (!isDeviceSlot(slot)) {
415
+ const error = /* @__PURE__ */ new Error(DEVICE_SLOT_RULE);
416
+ Object.assign(error, { code: "STIM_BAD_ARG" });
417
+ throw error;
418
+ }
419
+ return slot;
420
+ }
421
+ function parseDeviceSlotOption(slot) {
422
+ if (!isDeviceSlot(slot)) throw new InvalidArgumentError(DEVICE_SLOT_RULE);
423
+ if (slot === "web") throw new InvalidArgumentError(`web names this workspace's owned Chrome (stim stop --slot web); choose another device slot name.`);
424
+ return slot;
425
+ }
426
+ function deviceSlotPlatforms(project, slot = DEFAULT_DEVICE_SLOT) {
427
+ validateDeviceSlot(slot);
428
+ if (slot === DEFAULT_DEVICE_SLOT) return project?.platforms;
429
+ return project?.deviceSlots && Object.hasOwn(project.deviceSlots, slot) ? project.deviceSlots[slot] : void 0;
430
+ }
431
+ function projectDeviceSlots(project) {
432
+ const slots = [{
433
+ slot: DEFAULT_DEVICE_SLOT,
434
+ platforms: project?.platforms ?? {}
435
+ }];
436
+ for (const [slot, platforms] of Object.entries(project?.deviceSlots ?? {})) {
437
+ validateDeviceSlot(slot);
438
+ if (slot === DEFAULT_DEVICE_SLOT) throw new Error("The default device slot must use the existing platforms record.");
439
+ if (!platforms || typeof platforms !== "object" || Array.isArray(platforms)) throw new Error(`Invalid device slot record: ${slot}`);
440
+ slots.push({
441
+ slot,
442
+ platforms
443
+ });
444
+ }
445
+ return slots;
446
+ }
447
+ function assignSlotDevice(project, platform, device, slot = DEFAULT_DEVICE_SLOT) {
448
+ validateDeviceSlot(slot);
449
+ if (platform !== "ios" && platform !== "android") throw new Error(`Unknown device platform: ${platform}`);
450
+ if (slot === DEFAULT_DEVICE_SLOT) project.platforms = {
451
+ ...project.platforms,
452
+ [platform]: device
453
+ };
454
+ else project.deviceSlots = {
455
+ ...project.deviceSlots,
456
+ [slot]: {
457
+ ...deviceSlotPlatforms(project, slot),
458
+ [platform]: device
459
+ }
460
+ };
461
+ }
462
+ function removeSlotDevice(project, platform, slot = DEFAULT_DEVICE_SLOT) {
463
+ const platforms = deviceSlotPlatforms(project, slot);
464
+ if (!platforms) return;
465
+ delete platforms[platform];
466
+ if (slot !== DEFAULT_DEVICE_SLOT && Object.keys(platforms).length === 0) {
467
+ delete project.deviceSlots[slot];
468
+ if (Object.keys(project.deviceSlots).length === 0) delete project.deviceSlots;
469
+ }
470
+ }
471
+ function deviceSlotKey(platform, slot = "default") {
472
+ validateDeviceSlot(slot);
473
+ if (platform !== "ios" && platform !== "android") throw new Error(`Unknown device platform: ${platform}`);
474
+ return slot === "default" ? platform : `${platform}:${slot}`;
475
+ }
476
+ /**
477
+ * The slot key in a form that is legal as a file name. NTFS reads a colon in a file name as an
478
+ * alternate data stream: the write and the read of the exact path both succeed while `readdirSync`
479
+ * cannot see the file, so every non-default slot's logs would be invisible to `stim logs`. A slot
480
+ * name can contain neither a colon nor a dot, so the two forms stay one-to-one.
481
+ */
482
+ function deviceSlotFileKey(platform, slot = "default") {
483
+ return deviceSlotKey(platform, slot).replace(":", ".");
484
+ }
485
+ function parseDeviceSlotKey(key) {
486
+ const [platform, slot = "default", extra] = key.split(":");
487
+ if (platform !== "ios" && platform !== "android" || extra !== void 0) return null;
488
+ try {
489
+ validateDeviceSlot(slot);
490
+ } catch {
491
+ return null;
492
+ }
493
+ if (key !== deviceSlotKey(platform, slot)) return null;
494
+ return {
495
+ platform,
496
+ slot
497
+ };
498
+ }
499
+ //#endregion
500
+ //#region src/fs-util.ts
501
+ const WINDOWS = process.platform === "win32";
502
+ const DRIVE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
503
+ function volumeRootFor(path) {
504
+ const normalized = normalize(String(path));
505
+ if (WINDOWS) return parse(normalized).root.replace(/^[a-z]:/, (drive) => drive.toUpperCase());
506
+ const m = normalized.match(/^\/volumes\/([^/]+)/i);
507
+ return m ? `/Volumes/${m[1]}` : "/";
508
+ }
509
+ function resolveWorkspaceRealish(workspacePath) {
510
+ const raw = String(workspacePath);
511
+ if (!isAbsolute$1(raw)) return null;
512
+ let current = raw;
513
+ const MAX_HOPS = 40;
514
+ for (let hops = 0; hops < MAX_HOPS; hops++) {
515
+ const normalized = normalize(current);
516
+ const root = parse(normalized).root;
517
+ const segments = normalized.slice(root.length).split(sep$1).filter(Boolean);
518
+ let prefix = root.endsWith(sep$1) ? root.slice(0, -sep$1.length) : root;
519
+ let followedSymlink = false;
520
+ for (const segment of segments) {
521
+ prefix += `${sep$1}${segment}`;
522
+ let st;
523
+ try {
524
+ st = lstatSync(prefix);
525
+ } catch (err) {
526
+ if (err && err.code === "ENOENT") return normalized;
527
+ return null;
528
+ }
529
+ if (st.isSymbolicLink()) {
530
+ let target;
531
+ try {
532
+ target = readlinkSync(prefix);
533
+ } catch {
534
+ return null;
535
+ }
536
+ current = `${isAbsolute$1(target) ? target : normalize(join$1(dirname$1(prefix), target))}${normalized.slice(prefix.length)}`;
537
+ followedSymlink = true;
538
+ break;
539
+ }
540
+ }
541
+ if (!followedSymlink) return normalized;
542
+ }
543
+ return null;
544
+ }
545
+ function isRealMount(entryDev, rootDev) {
546
+ return entryDev != null && rootDev != null && entryDev !== rootDev;
547
+ }
548
+ function listWindowsVolumeRoots(statFn) {
549
+ const roots = [];
550
+ for (const letter of DRIVE_LETTERS) {
551
+ const root = `${letter}:${sep$1}`;
552
+ try {
553
+ statFn(root);
554
+ } catch {
555
+ continue;
556
+ }
557
+ roots.push(root);
558
+ }
559
+ return roots;
560
+ }
561
+ function listMountedVolumes({ statFn = statSync } = {}) {
562
+ if (WINDOWS) return listWindowsVolumeRoots(statFn);
563
+ const roots = ["/"];
564
+ let rootDev;
565
+ try {
566
+ rootDev = statFn("/").dev;
567
+ } catch {
568
+ return roots;
569
+ }
570
+ try {
571
+ for (const name of readdirSync("/Volumes")) {
572
+ const path = join$1("/Volumes", name);
573
+ let entryDev;
574
+ try {
575
+ entryDev = statFn(path).dev;
576
+ } catch {
577
+ continue;
578
+ }
579
+ if (isRealMount(entryDev, rootDev)) roots.push(path);
580
+ }
581
+ } catch {}
582
+ return roots;
583
+ }
584
+ function resolveVolumeRoot(path) {
585
+ const realish = resolveWorkspaceRealish(path);
586
+ return realish === null ? null : volumeRootFor(realish);
587
+ }
588
+ function uncShareIsReachable(volume, statFn) {
589
+ if (!WINDOWS || !volume.startsWith(`${sep$1}${sep$1}`)) return false;
590
+ try {
591
+ return statFn(volume).isDirectory();
592
+ } catch {
593
+ return false;
594
+ }
595
+ }
596
+ function isOnMountedVolume(path, mountedVolumes, { statFn = statSync } = {}) {
597
+ const mounted = new Set(mountedVolumes || listMountedVolumes());
598
+ const volume = resolveVolumeRoot(path);
599
+ if (volume === null) return false;
600
+ return mounted.has(volume) || uncShareIsReachable(volume, statFn);
601
+ }
602
+ function directorySize(dir, { timeoutMs }) {
603
+ return measuredDirectorySize(dir, { timeoutMs }) ?? 0;
604
+ }
605
+ async function directorySizeAsync(dir, options) {
606
+ return await measuredDirectorySizeAsync(dir, options) ?? 0;
607
+ }
608
+ async function measuredDirectorySizeAsync(dir, { timeoutMs, signal, operation }) {
609
+ let out;
610
+ try {
611
+ out = await runOperationFile(operation, getExecutor(), "du", ["-sk", dir], {
612
+ timeoutMs,
613
+ killSignal: "SIGKILL",
614
+ signal,
615
+ cwd: operation?.cwd
616
+ });
617
+ } catch (error) {
618
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
619
+ signal?.throwIfAborted();
620
+ operation?.signal.throwIfAborted();
621
+ return null;
622
+ }
623
+ if (!out) return 0;
624
+ const kb = parseInt(out.split(/\s+/)[0] ?? "", 10);
625
+ return isNaN(kb) ? 0 : kb * 1024;
626
+ }
627
+ function measuredDirectorySize(dir, { timeoutMs }) {
628
+ let out;
629
+ try {
630
+ out = getExecutor().runFile("du", ["-sk", dir], { timeoutMs });
631
+ } catch {
632
+ return null;
633
+ }
634
+ if (!out) return 0;
635
+ const kb = parseInt(out.split(/\s+/)[0] ?? "", 10);
636
+ return isNaN(kb) ? 0 : kb * 1024;
637
+ }
638
+ function formatBytes(bytes) {
639
+ if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)}G`;
640
+ if (bytes >= 1024 ** 2) return `${Math.round(bytes / 1024 ** 2)}M`;
641
+ return `${Math.round(bytes / 1024)}K`;
642
+ }
643
+ //#endregion
644
+ //#region src/devices/avd-claim.ts
645
+ function acquireAvdClaim(avdName) {
646
+ const root = join(configDir(), "avd-locks", `${encodeURIComponent(avdName.toLowerCase())}.lock`);
647
+ const label = `AVD ${avdName}`;
648
+ const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
649
+ root,
650
+ mode: "exclusive",
651
+ label
652
+ });
653
+ if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
654
+ if (attempt.acquired) return attempt.acquired;
655
+ throw new ownership_claim_exports.ClaimRefusedError({
656
+ root,
657
+ claimPath: attempt.held?.path ?? attempt.waitingFor?.[0]?.path ?? root,
658
+ label,
659
+ reason: "another operation is creating, recovering, or tearing down this AVD"
660
+ });
661
+ }
662
+ //#endregion
663
+ //#region src/workspace/config.ts
664
+ function getConfigDir() {
665
+ return configDir();
666
+ }
667
+ function refuseRelativeStimPaths(env = process.env) {
668
+ for (const name of [
669
+ "STIM_HOME",
670
+ "STIM_BUILD_CACHE",
671
+ "STIM_METRO_CACHE"
672
+ ]) {
673
+ const value = env[name];
674
+ if (!value || isAbsolute$1(value)) continue;
675
+ const error = /* @__PURE__ */ new Error(`${name}=${value} is not an absolute path. Set it to an absolute path, or unset it to use the default.`);
676
+ error.code = "STIM_RELATIVE_PATH";
677
+ throw error;
678
+ }
679
+ }
680
+ function saveConfig(config) {
681
+ ensurePrivateDir(getConfigDir());
682
+ const target = getConfigPath();
683
+ const tmp = `${target}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
684
+ writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
685
+ try {
686
+ renameSync(tmp, target);
687
+ } catch (err) {
688
+ try {
689
+ unlinkSync(tmp);
690
+ } catch {}
691
+ throw err;
692
+ }
693
+ }
694
+ const CONFIG_VERSION = 2;
695
+ function ensureConfig() {
696
+ return withConfigLock(() => {
697
+ const existing = loadConfig();
698
+ if (existing) {
699
+ if (existing.version !== CONFIG_VERSION) {
700
+ existing.version = CONFIG_VERSION;
701
+ saveConfig(existing);
702
+ }
703
+ return existing;
704
+ }
705
+ const fresh = {
706
+ version: CONFIG_VERSION,
707
+ projects: {},
708
+ repos: {}
709
+ };
710
+ saveConfig(fresh);
711
+ return fresh;
712
+ });
713
+ }
714
+ function requireAbsoluteProjectPath(projectPath) {
715
+ if (isAbsolute$1(projectPath)) return;
716
+ throw new Error(`Project key "${projectPath}" is not an absolute path. The registry is keyed by the realpath of a workspace root, so a relative key can never resolve.`);
717
+ }
718
+ function upsertProject(projectPath, fields) {
719
+ requireAbsoluteProjectPath(projectPath);
720
+ return withConfigLock(() => {
721
+ const cfg = ensureConfig();
722
+ const existing = cfg.projects[projectPath] || {
723
+ metroPort: null,
724
+ platforms: {}
725
+ };
726
+ cfg.projects[projectPath] = {
727
+ ...existing,
728
+ ...typeof fields === "function" ? fields(existing) : fields
729
+ };
730
+ saveConfig(cfg);
731
+ return cfg.projects[projectPath];
732
+ });
733
+ }
734
+ function removeProject(projectPath) {
735
+ withConfigLock(() => {
736
+ const cfg = loadConfig();
737
+ if (!cfg?.projects?.[projectPath]) return;
738
+ if (Object.keys(cfg.projects[projectPath].ports ?? {}).length) throw new Error(`Named ports remain for ${projectPath}; stop or release them before removing its registry entry.`);
739
+ for (const { platforms } of projectDeviceSlots(cfg.projects[projectPath])) if (platforms.android?.owned && platforms.android.avdName) (0, ownership_claim_exports.releaseClaim)(acquireAvdClaim(platforms.android.avdName));
740
+ delete cfg.projects[projectPath];
741
+ saveConfig(cfg);
742
+ });
743
+ }
744
+ function claimMetroPort(projectPath, port) {
745
+ return withConfigLock(() => {
746
+ const cfg = ensureConfig();
747
+ if (!cfg.projects[projectPath]) throw new Error(`Project not registered: ${projectPath}`);
748
+ for (const [path, proj] of Object.entries(cfg.projects)) {
749
+ if (path !== projectPath && proj?.metroPort === port) return null;
750
+ if (Object.values(proj?.ports ?? {}).includes(port)) return null;
751
+ }
752
+ cfg.projects[projectPath].metroPort = port;
753
+ saveConfig(cfg);
754
+ return port;
755
+ });
756
+ }
757
+ function releaseMetroPort(projectPath, port) {
758
+ withConfigLock(() => {
759
+ const cfg = loadConfig();
760
+ const project = cfg?.projects?.[projectPath];
761
+ if (!cfg || project?.metroPort !== port) return;
762
+ project.metroPort = null;
763
+ saveConfig(cfg);
764
+ });
765
+ }
766
+ function setDevice(projectPath, platform, deviceFields, slot = "default") {
767
+ requireAbsoluteProjectPath(projectPath);
768
+ withConfigLock(() => {
769
+ const cfg = ensureConfig();
770
+ if (!cfg.projects[projectPath]) throw new Error(`Project not registered: ${projectPath}`);
771
+ assignSlotDevice(cfg.projects[projectPath], platform, deviceFields, slot);
772
+ saveConfig(cfg);
773
+ });
774
+ }
775
+ function releaseAndroidConsolePort(projectPath, consolePort, slot = "default") {
776
+ return withConfigLock(() => {
777
+ const cfg = loadConfig();
778
+ const android = deviceSlotPlatforms(cfg?.projects?.[projectPath], slot)?.android;
779
+ if (!cfg || !android || android.consolePort !== consolePort) return false;
780
+ delete android.consolePort;
781
+ saveConfig(cfg);
782
+ return true;
783
+ });
784
+ }
785
+ function recordIosSchemeApprovals(projectPath, deviceUdid, schemeApprovals, slot = "default") {
786
+ withConfigLock(() => {
787
+ const cfg = loadConfig();
788
+ const ios = deviceSlotPlatforms(cfg?.projects?.[projectPath], slot)?.ios;
789
+ if (!cfg || !ios?.owned || ios.deviceUdid !== deviceUdid) return;
790
+ ios.schemeApprovals = schemeApprovals;
791
+ saveConfig(cfg);
792
+ });
793
+ }
794
+ function clearAndroidBootPending(projectPath, avdName, slot = "default") {
795
+ withConfigLock(() => {
796
+ const cfg = loadConfig();
797
+ const android = deviceSlotPlatforms(cfg?.projects?.[projectPath], slot)?.android;
798
+ if (!cfg || android?.avdName !== avdName || !android.bootPending) return;
799
+ delete android.bootPending;
800
+ saveConfig(cfg);
801
+ });
802
+ }
803
+ function clearDevice(projectPath, platform, slot = "default", expectedId) {
804
+ return withConfigLock(() => {
805
+ const cfg = loadConfig();
806
+ const project = cfg?.projects?.[projectPath];
807
+ if (!cfg || !project) return false;
808
+ const current = deviceSlotPlatforms(project, slot)?.[platform];
809
+ if (expectedId !== void 0 && (platform === "ios" ? current?.deviceUdid : current?.avdName) !== expectedId) return false;
810
+ removeSlotDevice(project, platform, slot);
811
+ saveConfig(cfg);
812
+ return true;
813
+ });
814
+ }
815
+ function setSupervisor(projectPath, { pid, port, startedAt, processToken }) {
816
+ requireAbsoluteProjectPath(projectPath);
817
+ return withConfigLock(() => {
818
+ const cfg = ensureConfig();
819
+ if (!cfg.projects[projectPath]) cfg.projects[projectPath] = {
820
+ metroPort: null,
821
+ platforms: {}
822
+ };
823
+ cfg.projects[projectPath].supervisor = {
824
+ pid,
825
+ port,
826
+ startedAt,
827
+ processToken
828
+ };
829
+ saveConfig(cfg);
830
+ return cfg.projects[projectPath].supervisor;
831
+ });
832
+ }
833
+ function clearSupervisor(projectPath, expected) {
834
+ return withConfigLock(() => {
835
+ const cfg = loadConfig();
836
+ const current = cfg?.projects?.[projectPath]?.supervisor;
837
+ if (!current) return true;
838
+ if (expected !== void 0 && !(0, process_identity_exports.sameProcessRecord)(current, expected)) return false;
839
+ delete cfg.projects[projectPath].supervisor;
840
+ saveConfig(cfg);
841
+ return true;
842
+ });
843
+ }
844
+ function targetSettings(cfg, target, create) {
845
+ if (target.scope === "machine") return cfg;
846
+ if (target.scope === "workspace") {
847
+ if (!cfg.projects[target.projectPath]) {
848
+ if (!create) return null;
849
+ cfg.projects[target.projectPath] = {
850
+ metroPort: null,
851
+ platforms: {}
852
+ };
853
+ }
854
+ const project = cfg.projects[target.projectPath];
855
+ if (!project.settings && create) project.settings = {};
856
+ return project.settings ?? null;
857
+ }
858
+ if (!cfg.repos[target.gitCommonDir]) {
859
+ if (!create) return null;
860
+ cfg.repos[target.gitCommonDir] = {};
861
+ }
862
+ const repo = cfg.repos[target.gitCommonDir];
863
+ if (!repo.settings && create) repo.settings = {};
864
+ return repo.settings ?? null;
865
+ }
866
+ function localDevicePoolOff(dottedKey, value) {
867
+ if (dottedKey === "remote.devicePoolDisabled") return Array.isArray(value) && value.includes("local");
868
+ if (dottedKey !== "remote.machines") return false;
869
+ const disabled = loadConfig()?.remote?.devicePoolDisabled;
870
+ return Array.isArray(disabled) && disabled.includes("local");
871
+ }
872
+ function writeConfigSetting(target, dottedKey, value) {
873
+ if (target.scope === "workspace") requireAbsoluteProjectPath(target.projectPath);
874
+ const status = target.scope === "machine" && localDevicePoolOff(dottedKey, value) ? realIo.status() : null;
875
+ return withConfigLock(() => applyConfigSetting(target, dottedKey, value, status));
876
+ }
877
+ /** Waits for the config lock and device-pool probe asynchronously; cancellation prevents a pending write. */
878
+ async function writeConfigSettingAsync(target, dottedKey, value, signal) {
879
+ if (target.scope === "workspace") requireAbsoluteProjectPath(target.projectPath);
880
+ signal?.throwIfAborted();
881
+ const status = target.scope === "machine" && (dottedKey === "remote.machines" || localDevicePoolOff(dottedKey, value)) ? await tailscaleStatusAsync(signal) : null;
882
+ return withConfigLockAsync(() => applyConfigSetting(target, dottedKey, value, status), signal);
883
+ }
884
+ function applyConfigSetting(target, dottedKey, value, status) {
885
+ const cfg = value === void 0 ? loadConfig() : ensureConfig();
886
+ const settings = cfg ? targetSettings(cfg, target, value !== void 0) : null;
887
+ if (!cfg || !settings) return false;
888
+ if (value === void 0) {
889
+ if (!deleteNested(settings, dottedKey)) return false;
890
+ } else writeNested(settings, dottedKey, value);
891
+ if (target.scope === "machine") {
892
+ if (dottedKey === "remote.buildPoolDisabled" || dottedKey === "remote.machines") validateAutomaticPool("build", cfg);
893
+ if (dottedKey === "remote.devicePoolDisabled" || dottedKey === "remote.machines") validateAutomaticPool("device", cfg, (machine) => !cannotHost(status, machine));
894
+ }
895
+ saveConfig(cfg);
896
+ return true;
897
+ }
898
+ function writeNested(obj, dottedKey, value) {
899
+ const keys = dottedKey.split(".");
900
+ let cur = obj;
901
+ for (let i = 0; i < keys.length - 1; i++) {
902
+ const k = keys[i];
903
+ if (k === void 0) continue;
904
+ const next = cur[k];
905
+ if (typeof next !== "object" || next === null) cur[k] = {};
906
+ cur = cur[k];
907
+ }
908
+ const leaf = keys[keys.length - 1];
909
+ if (leaf !== void 0) cur[leaf] = value;
910
+ }
911
+ function deleteNested(obj, dottedKey) {
912
+ const keys = dottedKey.split(".");
913
+ const chain = [obj];
914
+ let cur = obj;
915
+ for (let i = 0; i < keys.length - 1; i++) {
916
+ const k = keys[i];
917
+ if (k === void 0) return false;
918
+ const next = cur[k];
919
+ if (next == null || typeof next !== "object") return false;
920
+ cur = next;
921
+ chain.push(cur);
922
+ }
923
+ const leaf = keys[keys.length - 1];
924
+ if (leaf === void 0) return false;
925
+ if (!(leaf in cur)) return false;
926
+ delete cur[leaf];
927
+ for (let i = chain.length - 2; i >= 0; i--) {
928
+ const parent = chain[i];
929
+ const key = keys[i];
930
+ const child = chain[i + 1];
931
+ if (parent === void 0 || key === void 0 || child === void 0) break;
932
+ if (Object.keys(child).length === 0) delete parent[key];
933
+ else break;
934
+ }
935
+ return true;
936
+ }
937
+ function allConsolePortsAndSerials({ isMounted = isOnMountedVolume } = {}) {
938
+ const cfg = loadConfig();
939
+ const result = {
940
+ androidConsolePorts: [],
941
+ androidPhysicalSerials: []
942
+ };
943
+ if (!cfg) return result;
944
+ for (const [path, proj] of Object.entries(cfg.projects || {})) {
945
+ if (!existsSync(path) && isMounted(path)) continue;
946
+ for (const { platforms } of projectDeviceSlots(proj)) {
947
+ const android = platforms.android;
948
+ if (typeof android?.consolePort === "number") result.androidConsolePorts.push(android.consolePort);
949
+ if (android?.serial && !android.avdName) result.androidPhysicalSerials.push(android.serial);
950
+ }
951
+ }
952
+ return result;
953
+ }
954
+ //#endregion
955
+ export { outdatedServer as $, writeConfigSettingAsync as A, deviceSlotFileKey as B, saveConfig as C, withConfigLock$1 as D, upsertProject as E, isOnMountedVolume as F, projectDeviceSlots as G, deviceSlotPlatforms as H, listMountedVolumes as I, SERVER_OUTDATED as J, removeSlotDevice as K, measuredDirectorySizeAsync as L, directorySize as M, directorySizeAsync as N, withConfigLockAsync$1 as O, formatBytes as P, openAuthenticated as Q, volumeRootFor as R, removeProject as S, setSupervisor as T, parseDeviceSlotKey as U, deviceSlotKey as V, parseDeviceSlotOption as W, endpoint as X, doctorIo as Y, findPeer$1 as Z, loadConfig$1 as _, clearDevice as a, setupRemedy as at, releaseAndroidConsolePort as b, ensureConfig as c, getConfigDir as d, parseMachine$1 as et, getConfigPath$1 as f, isPathPrefix as g, getRepoSettings as h, clearAndroidBootPending as i, sayHello as it, acquireAvdClaim as j, writeConfigSetting as k, findEnclosingWorktreeRoot as l, getProjectSettings as m, allMetroPorts as n, realIo as nt, clearSupervisor as o, tailscaleStatusAsync as ot, getProject as p, validateDeviceSlot as q, claimMetroPort as r, resolveEndpoint as rt, configCorruptRepair as s, readHostPermissions as st, allConsolePortsAndSerials as t, reachMachine as tt, getConcurrencyLimits as u, recordIosSchemeApprovals as v, setDevice as w, releaseMetroPort as x, refuseRelativeStimPaths as y, assignSlotDevice as z };