stim 1.20.2 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/README.md +88 -8
  2. package/dist/acquire-artifact-CD4lbiWE.mjs +153 -0
  3. package/dist/act-DHAw2A2S.mjs +2667 -0
  4. package/dist/activity-BzVfHSrY.mjs +1376 -0
  5. package/dist/{agent-device-usage-output-Dzgv6lgR.mjs → agent-device-usage-output-zh1vhbLp.mjs} +21 -65
  6. package/dist/{android-HqP1_vB_.mjs → android-BEJ8Jpl7.mjs} +2 -2
  7. package/dist/{android-DCKZrNvV.mjs → android-BLgbNqox.mjs} +128 -78
  8. package/dist/{android-Dlzxshgp.mjs → android-CV_GCELZ.mjs} +5 -5
  9. package/dist/{android-Mz_Inn3w.mjs → android-DUYkNF4u.mjs} +184 -102
  10. package/dist/{android-cas-Bl-cLJBk.mjs → android-cas-Ct5b1xCK.mjs} +7 -6
  11. package/dist/android-cas-compiler.mjs +2 -2
  12. package/dist/android-project-BCAAmMwz.mjs +12 -0
  13. package/dist/api.d.mts +740 -8
  14. package/dist/api.mjs +585 -1
  15. package/dist/{artifact-HSwkIWEQ.mjs → artifact-B8T3S5gi.mjs} +223 -201
  16. package/dist/{artifact-D3czlUvT.mjs → artifact-DAuZ-FNU.mjs} +158 -148
  17. package/dist/{browser-web-CA-F1GQo.mjs → browser-web-QQzVjz3O.mjs} +26 -24
  18. package/dist/budget-Bs6SxxzO.mjs +9154 -0
  19. package/dist/{build-DYDbfkk5.mjs → build-CoPzQRNw.mjs} +144 -96
  20. package/dist/build-cache-DmyR9Zzc.mjs +602 -0
  21. package/dist/build-plan-CaFskB7_.mjs +188 -0
  22. package/dist/{cache-manifest-XH-fvDUj.mjs → cache-manifest-DkkqguoK.mjs} +2 -2
  23. package/dist/cache-manifest.mjs +1 -1
  24. package/dist/{ccache-zdKiYQ3E.mjs → ccache-BJRZ9pG3.mjs} +10 -9
  25. package/dist/{cdp-Dq_ypAlJ.mjs → cdp-CWmIUMbw.mjs} +18 -5
  26. package/dist/{chrome-BnzZMqsR.mjs → chrome-B6tfCta1.mjs} +7 -4
  27. package/dist/{cli-CLP3diBm.mjs → cli-0DYf8QhQ.mjs} +20 -20
  28. package/dist/cli.mjs +1 -1
  29. package/dist/client-B7QVOlCr.d.mts +253 -0
  30. package/dist/client-UXD70ZbM.mjs +1406 -0
  31. package/dist/collector-run.d.mts +4 -40
  32. package/dist/collector-run.mjs +9 -8
  33. package/dist/{command-output-BTy8hZ0F.mjs → command-output-B1UrjZHL.mjs} +1 -1
  34. package/dist/config-CYcBF478.mjs +949 -0
  35. package/dist/{created-devices-Dzd5C2LF.mjs → created-devices-CqeBlrEn.mjs} +3 -3
  36. package/dist/{dependency-state-DG0p31-y.mjs → dependency-state-BbKQW83h.mjs} +20 -5
  37. package/dist/{detached-entry-B9C_zeQC.mjs → detached-entry-DFRyG1_o.mjs} +1 -1
  38. package/dist/{dev-client-DAWVIGBu.mjs → dev-client-UOeESKm6.mjs} +2 -2
  39. package/dist/device-DCxH7_By.mjs +1068 -0
  40. package/dist/{device-BECOiufl.mjs → device-ExJq8Ei7.mjs} +35 -35
  41. package/dist/{device-capacity-CBUAcRyQ.mjs → device-capacity-CMlqEcvw.mjs} +43 -13
  42. package/dist/device-host-worker.mjs +65 -61
  43. package/dist/{device-ios-COSi6oS1.mjs → device-ios-DRUeKaFV.mjs} +48 -45
  44. package/dist/{device-lease-run-Cw2M4z51.mjs → device-lease-run-C8raGFoP.mjs} +14 -26
  45. package/dist/{device-pool-Bf-pOhSw.mjs → device-pool-DMRpVX9a.mjs} +5 -5
  46. package/dist/doctor-BLHCYxzi.mjs +2 -0
  47. package/dist/{doctor-kfMBnUw6.mjs → doctor-DTvaDOda.mjs} +281 -317
  48. package/dist/{doctor-CB8oZKCk.mjs → doctor-Dhqebm0q.mjs} +183 -91
  49. package/dist/doctor-fingerprint.mjs +19 -0
  50. package/dist/{doctor-watchman-BRyV2adA.mjs → doctor-watchman-Cv1F10Kj.mjs} +40 -21
  51. package/dist/{eas-build-JyfqyHfC.mjs → eas-build-BomfRnFT.mjs} +53 -20
  52. package/dist/{build-plan-B7SZsc2N.mjs → eas-fallback-DJwZpy0c.mjs} +26 -208
  53. package/dist/{watchman-CPQFZLXY.mjs → eas-session-ledger-op6A54yi.mjs} +229 -11
  54. package/dist/{error-diagnostics-BR677IDM.mjs → error-diagnostics-Bf0rZvG9.mjs} +172 -82
  55. package/dist/exec-H20xSg2T.d.mts +65 -0
  56. package/dist/exec-_UbtCQcG.mjs +750 -0
  57. package/dist/gc-C-WwL0yi.mjs +3 -0
  58. package/dist/{gc-qWOMdnER.mjs → gc-_-dCXdyL.mjs} +584 -796
  59. package/dist/git-C3iTsJaZ.mjs +73 -0
  60. package/dist/{guide-G9cAbVAc.mjs → guide-Yqwt2bd6.mjs} +941 -269
  61. package/dist/{hosted-android-C3MHGld1.mjs → hosted-android-Bw-honsd.mjs} +8 -6
  62. package/dist/{hosted-client-ZEKpS9vw.mjs → hosted-client-DB_gBPfY.mjs} +34 -19
  63. package/dist/{hosted-ios-l-o3iM9J.mjs → hosted-ios-CUvOf4Vr.mjs} +1 -1
  64. package/dist/{hosted-logs-EzQM7-3Z.mjs → hosted-logs-DL_P5AdN.mjs} +77 -43
  65. package/dist/{hosted-macos-BACVAVqE.mjs → hosted-macos-QI141yKk.mjs} +21 -17
  66. package/dist/{hosted-native-A4s1QPXe.mjs → hosted-native-Ksyjgzq-.mjs} +30 -21
  67. package/dist/idle-BVeRGroT.d.mts +24 -0
  68. package/dist/{idle-shutdown-DtxhPTKe.mjs → idle-shutdown-DaKmZi0z.mjs} +11 -10
  69. package/dist/{ios-BwVCF7oA.mjs → ios-CQ68hG9s.mjs} +8 -8
  70. package/dist/{ios-RkW4pnQf.mjs → ios-CpByZy12.mjs} +140 -80
  71. package/dist/{ios-device-B5veSDDQ.mjs → ios-device-CXGP1bsD.mjs} +1 -1
  72. package/dist/{ios-device-D7pi2Sca.mjs → ios-device-CwFMehUS.mjs} +51 -26
  73. package/dist/{ios-project-K9hY_Jab.mjs → ios-project-WuwwBdvW.mjs} +13 -13
  74. package/dist/{ios-nOLJGVdy.mjs → ios-qNJt4dFm.mjs} +1 -1
  75. package/dist/ios-signing-DTTIChGs.mjs +489 -0
  76. package/dist/{ios-state-CBLe1xYw.mjs → ios-state-DvFX-K_l.mjs} +1 -1
  77. package/dist/{ios-Ce7bc7br.mjs → ios-ubFsBzwq.mjs} +129 -86
  78. package/dist/{js-swap-Bb3nsS07.mjs → js-swap-Cc1dttps.mjs} +53 -26
  79. package/dist/{launch-F4sOqAhB.mjs → launch-CD1yVc7U.mjs} +91 -90
  80. package/dist/{launch-C3qJ6Agb.mjs → launch-Zq2mOSDy.mjs} +82 -541
  81. package/dist/logs-BS00Anxf.mjs +352 -0
  82. package/dist/logs-CMm05HxN.mjs +255 -0
  83. package/dist/{macos-D7uDvO-z.mjs → macos-B65--pPd.mjs} +1 -1
  84. package/dist/{macos-Ch5VJmcS.mjs → macos-C-0qJ6zq.mjs} +225 -212
  85. package/dist/macos-run.mjs +1 -1
  86. package/dist/maintenance-run.mjs +122 -247
  87. package/dist/{metro-DnCEjCxs.mjs → metro-DEXpIhHC.mjs} +67 -12
  88. package/dist/{metro-gateway-CX9Xlkq2.mjs → metro-gateway-BMJNlj3E.mjs} +20 -12
  89. package/dist/{named-ports-4WopRPg-.mjs → named-ports-BWTzt6mu.mjs} +8 -63
  90. package/dist/native-android-JACyjTZs.mjs +844 -0
  91. package/dist/{native-gradle-inputs-BsLRrl7u.mjs → native-gradle-inputs-Bep9JcSF.mjs} +128 -140
  92. package/dist/native-inputs-TRTTTRxt.mjs +91 -0
  93. package/dist/native-run-DY_RLx3C.mjs +277 -0
  94. package/dist/{native-runtime-k8dTIrbK.mjs → native-runtime-Dg1Q7tfF.mjs} +9 -8
  95. package/dist/{native-xcode-inputs-CykopJdM.mjs → native-xcode-inputs-CB2iQeWq.mjs} +27 -18
  96. package/dist/{native-xcode-ios-Bbf30Gm0.mjs → native-xcode-ios-CXX39F7o.mjs} +96 -47
  97. package/dist/{ndjson-GOnbyvso.mjs → ndjson-CgE_o3Dl.mjs} +3 -3
  98. package/dist/offload-worker.d.mts +1 -46
  99. package/dist/offload-worker.mjs +197 -66
  100. package/dist/{ownership-DCtw_2IA.mjs → ownership-DEmM5vhh.mjs} +3 -3
  101. package/dist/{page-DRHy329N.mjs → page-CPaHeYqj.mjs} +7 -4
  102. package/dist/paths-BsJMoPP2.mjs +12 -0
  103. package/dist/paths-DQtTgsBf.mjs +42 -0
  104. package/dist/{placement-log-MOfPVgaM.mjs → placement-log-BqGbTa61.mjs} +5 -5
  105. package/dist/{plan-placement-V0TdzYXp.mjs → plan-placement-Bwj-IjY1.mjs} +6 -6
  106. package/dist/{ports-DdSUicVo.mjs → ports-D-MmCTdv.mjs} +3 -3
  107. package/dist/prebuild-uD_ipCxF.mjs +449 -0
  108. package/dist/{project-_phNIJfs.mjs → project-BZNjmW_8.mjs} +65 -37
  109. package/dist/{pull-request-CQ8sPbZa.mjs → pull-request-Lfetn8Kr.mjs} +2 -2
  110. package/dist/pull-request-zjCSAryH.d.mts +120 -0
  111. package/dist/pull-requests.d.mts +1 -119
  112. package/dist/pull-requests.mjs +1 -1
  113. package/dist/{react-native-android-CM1XRDwv.mjs → react-native-android-DuzH0cn3.mjs} +74 -48
  114. package/dist/{gradle-DSrXb6lG.mjs → react-native-build-rhk4smwQ.mjs} +292 -15
  115. package/dist/react-native-doctor-CQpuIMso.mjs +234 -0
  116. package/dist/{react-native-ios-Dhq_eiYQ.mjs → react-native-ios-Bhq1rJeV.mjs} +46 -28
  117. package/dist/{recordings-D0d5aQt0.mjs → recordings-DYteQSu4.mjs} +12 -9
  118. package/dist/{reload-C8JhxjSh.mjs → reload-CmTRFJaX.mjs} +93 -47
  119. package/dist/reload-DzjSl5P_.mjs +2 -0
  120. package/dist/{remote-BiR6aYjy.mjs → remote-ByYkXdGD.mjs} +15 -10
  121. package/dist/result-DNiS-tTP.mjs +434 -0
  122. package/dist/{run-BtPgpKks.mjs → run-D23HmJMs.mjs} +4 -5
  123. package/dist/selection-CSl9i5qU.mjs +619 -0
  124. package/dist/{server-bare-R2m_gsH8.mjs → server-bare-CxTrvDg8.mjs} +3 -4
  125. package/dist/{server-command-CTtCkmcQ.mjs → server-command-CtzloH3v.mjs} +14 -7
  126. package/dist/server-expo-C-90EhcG.mjs +2 -0
  127. package/dist/{server-expo-DsguMGUF.mjs → server-expo-DT_F_uVI.mjs} +50 -9
  128. package/dist/{settings-B717odTD.mjs → settings-CsJDEXgK.mjs} +234 -24
  129. package/dist/{settings-vk3mJGSm.d.mts → settings-DkdmlelE.d.mts} +6 -4
  130. package/dist/settings-T1gtc3bf.mjs +114 -0
  131. package/dist/settings-write-HyVZ8eQ8.mjs +359 -0
  132. package/dist/settings.schema.json +163 -16
  133. package/dist/{simslim-CCr3rE-A.mjs → simslim-Cgk17SXV.mjs} +14 -12
  134. package/dist/{slot-launch-DPt0m9Nd.mjs → slot-launch-DF8VfT64.mjs} +15 -20
  135. package/dist/{spawn-entry-Dcb7sDUz.mjs → spawn-entry-jw19m8Nl.mjs} +0 -1
  136. package/dist/stage-BIS0ydsV.mjs +183 -0
  137. package/dist/{start-BWqzODpb.mjs → start-7lpH7hmN.mjs} +63 -44
  138. package/dist/{start-TXeMmmO2.mjs → start-RChynRDp.mjs} +1 -1
  139. package/dist/{state-D1Icyjlu.mjs → state-C4ZtUisW.mjs} +40 -13
  140. package/dist/{state-CG_OFoaA.mjs → state-XMziexv2.mjs} +3 -2
  141. package/dist/{stats-BoY5sEPU.mjs → stats-53T7OTb9.mjs} +7 -13
  142. package/dist/stats-report-BFfMpWWF.mjs +16 -0
  143. package/dist/{status-WgFbkRzj.mjs → status-BL7jVyAB.mjs} +535 -447
  144. package/dist/status-CSi6tlRq.mjs +2 -0
  145. package/dist/{status-BTA7kKiQ.mjs → status-DqEUSSxs.mjs} +6 -4
  146. package/dist/{status-watch-C23ILtf9.mjs → status-watch-GDCfqpTE.mjs} +187 -22
  147. package/dist/status-worker.d.mts +1 -0
  148. package/dist/status-worker.mjs +19 -0
  149. package/dist/stim-desktop-C12we7lz.mjs +112 -0
  150. package/dist/{stop-DR6VoDIk.mjs → stop-Bk_1qiV6.mjs} +2 -2
  151. package/dist/{stop-BNmBMi1F.mjs → stop-CPzlM1L-.mjs} +138 -98
  152. package/dist/{stop-DPEhunlw.mjs → stop-D7h6jAXQ.mjs} +11 -10
  153. package/dist/supervisor-run.d.mts +8 -17
  154. package/dist/supervisor-run.mjs +63 -27
  155. package/dist/{support-DMTqj3Hm.mjs → support-67vraa1Q.mjs} +18 -11
  156. package/dist/{support-CwjbFs9H.mjs → support-Dlu37g6H.mjs} +12 -12
  157. package/dist/{swiftpm-macos-DRNVJWlb.mjs → swiftpm-macos-p8WZbxKU.mjs} +35 -24
  158. package/dist/toolchain-DhKwp9iI.mjs +2 -0
  159. package/dist/toolchain-Hp9sCpK4.mjs +873 -0
  160. package/dist/{trigger-CigAYjeL.mjs → trigger-BAahxUPq.mjs} +20 -5
  161. package/dist/trigger-Bf3uDcpv.mjs +2 -0
  162. package/dist/{warm-progress-BuY-Fvfn.mjs → warm-progress-BHX5r0Ei.mjs} +250 -156
  163. package/dist/{web-BDBMxDpx.mjs → web-CJmIj6AI.mjs} +18 -13
  164. package/dist/web-DknQETPs.mjs +2 -0
  165. package/dist/web-run.mjs +11 -12
  166. package/dist/windows-report.d.mts +1 -0
  167. package/dist/windows-report.mjs +18 -0
  168. package/dist/windows-report.ps1 +43 -0
  169. package/dist/workspace-process-lock-1_1OoVW5.mjs +57 -0
  170. package/dist/{workspace-state-DJt_MMvf.mjs → workspace-state-Cjuigeeu.mjs} +62 -58
  171. package/dist/{workspaces-C36s2jb9.mjs → workspaces-BccMFmGQ.mjs} +1188 -1079
  172. package/dist/worktree-CK2MpJW5.mjs +2 -0
  173. package/dist/{worktree-CgvJYJ-Q.mjs → worktree-DQ6Zi2Mv.mjs} +304 -53
  174. package/dist/xcode-Zlx3qjEs.mjs +896 -0
  175. package/package.json +7 -5
  176. package/shim/android-optimizations.gradle +7 -5
  177. package/dist/activity-BSV745Kz.mjs +0 -629
  178. package/dist/api-qvOzWyfO.mjs +0 -154
  179. package/dist/api-run.mjs +0 -294
  180. package/dist/app-install-BIKtMfPq.mjs +0 -896
  181. package/dist/artifact-lifecycle-DzPpBYvK.mjs +0 -30
  182. package/dist/budget-f7516Zk0.mjs +0 -4106
  183. package/dist/build-facts-DcLv2HSd.d.mts +0 -111
  184. package/dist/cancellation-C_22VQTH.mjs +0 -24
  185. package/dist/client-qQAaXSUs.mjs +0 -1771
  186. package/dist/config-BYDxlMY3.mjs +0 -562
  187. package/dist/device-DuiPJQSn.mjs +0 -290
  188. package/dist/device-lease-BX1sJQ2E.mjs +0 -1224
  189. package/dist/device-remote-xRT0Vmo9.mjs +0 -1272
  190. package/dist/eas-session-ledger-Cm0Gwq2F.mjs +0 -207
  191. package/dist/exec-DqKo9GKJ.mjs +0 -268
  192. package/dist/git-DDfFzkMm.mjs +0 -45
  193. package/dist/launch-verify-B6Q8hZP-.mjs +0 -428
  194. package/dist/logs-BGrEUaEs.mjs +0 -383
  195. package/dist/miss-reason-mKzZ4VvN.mjs +0 -238
  196. package/dist/native-android-D5qc3-_v.mjs +0 -226
  197. package/dist/ndjson-Du3j0AQ9.d.mts +0 -17
  198. package/dist/plan-C4TjdPsR.mjs +0 -260
  199. package/dist/prebuild-DYbucPXy.mjs +0 -212
  200. package/dist/preview-78a9kR_-.mjs +0 -1011
  201. package/dist/react-native-build-CtCccJtb.mjs +0 -239
  202. package/dist/react-native-doctor-CfRnFfjs.mjs +0 -78
  203. package/dist/remote-cache-OyOyVIS3.mjs +0 -526
  204. package/dist/result-CaJg5Iat.mjs +0 -186
  205. package/dist/server-expo-Bigriskl.mjs +0 -2
  206. package/dist/settings-BORNaaCO.mjs +0 -402
  207. package/dist/spawn-claims-DVS5RkMS.mjs +0 -72
  208. package/dist/stage-KNTQopqv.mjs +0 -157
  209. package/dist/stim-desktop-BzBN2Em2.mjs +0 -63
  210. package/dist/tailnet-Dh-qiYzG.mjs +0 -265
  211. package/dist/toolchain-BFkAcstd.mjs +0 -2
  212. package/dist/toolchain-DxSsNssu.mjs +0 -1657
  213. package/dist/trigger-9_USgmxz.mjs +0 -2
  214. package/dist/web-Dq-i3wzJ.mjs +0 -2
  215. package/dist/worktree-DpLdCHga.mjs +0 -2
  216. package/dist/{api-run.d.mts → doctor-fingerprint.d.mts} +0 -0
  217. package/dist/{errors-DzkMYh8Q.mjs → errors-ccaPc4ox.mjs} +2 -2
@@ -0,0 +1,949 @@
1
+ import { f as ownership_claim_exports, g as trackReportResource, m as reportSignal, p as process_identity_exports, t as getExecutor } from "./exec-_UbtCQcG.mjs";
2
+ import { join } from "node:path";
3
+ import { ambientEnv, configDir, ensurePrivateDir } from "@stim-cli/core";
4
+ 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";
5
+ import { dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, normalize, parse, sep as sep$1 } from "path";
6
+ import { WebSocket } from "ws";
7
+ import { HandshakeRefusal, MAX_SERVER_MESSAGE_BYTES, TRANSPORT_OVERHEAD_BYTES, decodeSecret, initiateNoise, newSecret } from "@stim-cli/core/noise-session";
8
+ import { existsSync, lstatSync, readdirSync, readlinkSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
9
+ import { InvalidArgumentError } from "commander";
10
+ //#region src/offload/access-ticket.ts
11
+ /** The setup ticket in `STIM_ACCESS_TICKET`, which Stim Desktop's Add Machine flow sets, or undefined. */
12
+ function readAccessTicket() {
13
+ const ticket = ambientEnv().STIM_ACCESS_TICKET?.trim();
14
+ return ticket && decodeSecret(ticket) ? ticket : void 0;
15
+ }
16
+ function readHostPermissions(value) {
17
+ const host = value;
18
+ return host && typeof host.name === "string" && typeof host.screenRecording === "boolean" && typeof host.accessibility === "boolean" ? {
19
+ name: host.name,
20
+ screenRecording: host.screenRecording,
21
+ accessibility: host.accessibility
22
+ } : void 0;
23
+ }
24
+ //#endregion
25
+ //#region src/offload/tailnet.ts
26
+ const MAC_APP_TAILSCALE = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
27
+ const HELLO_TIMEOUT_MS = 1e4;
28
+ /** How long a setup ticket `stim doctor --fix` generates stays valid; `stim-server setup` takes at most 2 hours. */
29
+ const GENERATED_TICKET_MS = 714e4;
30
+ function parseTailscaleStatus(output) {
31
+ try {
32
+ const status = JSON.parse(output);
33
+ return isJsonObject(status) && status.BackendState === "Running" ? status : null;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+ function tailscaleStatus() {
39
+ for (const binary of ["tailscale", MAC_APP_TAILSCALE]) {
40
+ const output = getExecutor().runFileQuiet(binary, ["status", "--json"], { timeoutMs: 5e3 });
41
+ if (output !== null) return parseTailscaleStatus(output);
42
+ }
43
+ return null;
44
+ }
45
+ async function tailscaleStatusAsync(signal) {
46
+ for (const binary of ["tailscale", MAC_APP_TAILSCALE]) {
47
+ signal?.throwIfAborted();
48
+ let output;
49
+ try {
50
+ output = await getExecutor().runFileAsync(binary, ["status", "--json"], {
51
+ timeoutMs: 5e3,
52
+ signal
53
+ });
54
+ } catch {
55
+ signal?.throwIfAborted();
56
+ continue;
57
+ }
58
+ signal?.throwIfAborted();
59
+ return parseTailscaleStatus(output);
60
+ }
61
+ return null;
62
+ }
63
+ const TURNED_AWAY = /* @__PURE__ */ new Set([
64
+ "unauthorized",
65
+ "forbidden",
66
+ "protocol-unsupported",
67
+ "server-key-mismatch"
68
+ ]);
69
+ /** The connect code for a protocol 1 stim-server, which closes on the binary Noise prefix before answering it. */
70
+ const SERVER_OUTDATED = "server-outdated";
71
+ /**
72
+ * The doctor finding for a protocol 1 stim-server on `entry`, shared by the build and device-host checks so doctor
73
+ * prints it once. `command` is the setup command, when this machine has an unexpired ticket for it.
74
+ */
75
+ function outdatedServer(entry, version, command) {
76
+ 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)`;
77
+ return {
78
+ title: `Remote machine ${entry} runs an older stim-server`,
79
+ detail: `The stim-server on ${entry} speaks an older protocol than this Stim, so it cannot authenticate this machine.`,
80
+ fix: command ? `${update}, then run this there to approve this machine: ${command}` : `${update}, then run \`stim doctor --fix\` here.`
81
+ };
82
+ }
83
+ /**
84
+ * Opens a WebSocket to `target` and runs the protocol 2 handshake with this machine's client key. A returning client
85
+ * goes ahead only with the server that proves it holds the pinned key.
86
+ */
87
+ function openAuthenticated(target, auth, timeoutMs, signal, operation) {
88
+ signal?.throwIfAborted();
89
+ return new Promise((resolve) => {
90
+ const options = {
91
+ handshakeTimeout: timeoutMs,
92
+ servername: target.servername,
93
+ headers: { Host: target.host },
94
+ maxPayload: MAX_SERVER_MESSAGE_BYTES + TRANSPORT_OVERHEAD_BYTES
95
+ };
96
+ const socket = new WebSocket(target.url, options);
97
+ if (reportSignal(operation)) trackReportResource(operation, new Promise((done) => socket.once("close", () => done())), () => socket.terminate());
98
+ let settled = false;
99
+ const finish = (value) => {
100
+ if (settled) return;
101
+ settled = true;
102
+ signal?.removeEventListener("abort", cancel);
103
+ clearTimeout(timer);
104
+ if (!("send" in value)) {
105
+ socket.removeAllListeners("open");
106
+ socket.removeAllListeners("message");
107
+ socket.removeAllListeners("error");
108
+ socket.on("error", () => {});
109
+ socket.terminate();
110
+ }
111
+ resolve(value);
112
+ };
113
+ const cancel = () => finish({
114
+ failure: "cancelled",
115
+ refused: false,
116
+ code: "STIM_CANCELLED"
117
+ });
118
+ const timer = setTimeout(() => finish({
119
+ failure: "no reply in time",
120
+ refused: false
121
+ }), timeoutMs);
122
+ signal?.addEventListener("abort", cancel, { once: true });
123
+ if (signal?.aborted) return cancel();
124
+ const failed = (error) => finish({
125
+ failure: /Unexpected server response: 502/.test(error.message) ? "no stim-server answers behind its tailscale serve route (HTTP 502)" : error.message,
126
+ refused: false
127
+ });
128
+ socket.once("error", failed);
129
+ socket.once("open", () => {
130
+ const secret = "ticket" in auth ? decodeSecret(auth.ticket) : null;
131
+ let keyPair;
132
+ try {
133
+ keyPair = clientIdentity();
134
+ } catch (error) {
135
+ return finish({
136
+ failure: `this machine's client key is unavailable: ${error.message}`,
137
+ refused: false
138
+ });
139
+ }
140
+ (async () => {
141
+ try {
142
+ const noise = await initiateNoise(socket, {
143
+ keyPair,
144
+ ..."serverKey" in auth ? { serverKey: Buffer.from(auth.serverKey, "base64url") } : {},
145
+ ...secret ? {
146
+ secret,
147
+ name: "name" in auth ? auth.name : ""
148
+ } : {},
149
+ timeoutMs
150
+ });
151
+ socket.off("error", failed);
152
+ socket.on("error", () => {});
153
+ finish(noise);
154
+ } catch (error) {
155
+ const code = error instanceof HandshakeRefusal ? error.code : "handshake-failed";
156
+ if (code === "unexpected binary frame") return finish({
157
+ failure: "its stim-server speaks an older protocol than this Stim",
158
+ refused: true,
159
+ code: SERVER_OUTDATED
160
+ });
161
+ finish({
162
+ failure: error.message,
163
+ refused: TURNED_AWAY.has(code) || code === "unknown-secret",
164
+ code
165
+ });
166
+ }
167
+ })();
168
+ });
169
+ });
170
+ }
171
+ /** Sends `hello` on an authenticated connection; a close before the reply carries the server's refusal code. */
172
+ function sayHello(socket, params, timeoutMs) {
173
+ return new Promise((resolve) => {
174
+ const done = (reply) => {
175
+ clearTimeout(timer);
176
+ socket.off("message", onMessage);
177
+ socket.off("close", onClose);
178
+ resolve(reply);
179
+ };
180
+ const onMessage = (data, isBinary) => {
181
+ if (isBinary) return;
182
+ let message;
183
+ try {
184
+ message = JSON.parse(data.toString("utf8"));
185
+ } catch {
186
+ message = null;
187
+ }
188
+ if (!isJsonObject(message) || message.id !== 1) return;
189
+ if (isJsonObject(message.error)) return done({ error: {
190
+ code: String(message.error.code),
191
+ message: String(message.error.message)
192
+ } });
193
+ done({ result: isJsonObject(message.result) ? message.result : {} });
194
+ };
195
+ const onClose = (code, reason) => done({ error: {
196
+ code: reason.length ? reason.toString() : "closed",
197
+ message: `the connection closed before a hello result (${code}${reason.length ? ` ${reason.toString()}` : ""})`
198
+ } });
199
+ const timer = setTimeout(() => done({ error: {
200
+ code: "timeout",
201
+ message: "no reply in time"
202
+ } }), timeoutMs);
203
+ socket.on("message", onMessage);
204
+ socket.on("close", onClose);
205
+ socket.send(JSON.stringify({
206
+ id: 1,
207
+ method: "hello",
208
+ params
209
+ }));
210
+ });
211
+ }
212
+ async function hello(target, auth, operation) {
213
+ const socket = await openAuthenticated(target, auth, HELLO_TIMEOUT_MS, reportSignal(operation), operation);
214
+ if (!("send" in socket)) return socket.code ? { error: {
215
+ code: socket.code,
216
+ message: socket.failure
217
+ } } : { failed: socket.failure };
218
+ try {
219
+ if ("onServerKey" in auth) await auth.onServerKey?.(socket.remoteKey.toString("base64url"));
220
+ } catch (error) {
221
+ socket.close(1e3);
222
+ throw error;
223
+ }
224
+ const reply = await sayHello(socket, {
225
+ client: {
226
+ name: "stim",
227
+ version: "1"
228
+ },
229
+ access: auth.access
230
+ }, HELLO_TIMEOUT_MS);
231
+ socket.close(1e3);
232
+ if ("error" in reply) return reply;
233
+ const { result } = reply;
234
+ const device = isJsonObject(result.device) ? result.device : null;
235
+ if (!device || typeof device.id !== "string" || !Array.isArray(result.capabilities)) return { failed: "the reply was not a hello result" };
236
+ return {
237
+ result,
238
+ serverKey: socket.remoteKey.toString("base64url")
239
+ };
240
+ }
241
+ const realIo = {
242
+ status: tailscaleStatus,
243
+ hello
244
+ };
245
+ function doctorIo(operation) {
246
+ return {
247
+ status: () => tailscaleStatusAsync(operation?.signal),
248
+ hello: (target, auth) => hello(target, auth, operation)
249
+ };
250
+ }
251
+ /** Where to reach a paired machine now, or why Stim cannot; the handshake then checks its pinned key. */
252
+ async function resolveEndpoint(credential, status = () => tailscaleStatusAsync(signal), signal) {
253
+ return machineEndpoint(credential.machine, parseMachine(credential.machine) ? await status() : null);
254
+ }
255
+ /** The remedy for a pending request: the printed setup command, or where the person finds it. */
256
+ function setupRemedy(entry, command) {
257
+ return command ? `A person on ${entry} runs this there to approve this machine: ${command}` : `A person on ${entry} runs the \`stim-server setup\` command Stim Desktop showed for this machine.`;
258
+ }
259
+ function setupCommand(ticket, expiresAt, flags, version) {
260
+ return `STIM_SETUP_TICKET=${ticket} npx --yes --package @stim-cli/server@${version} stim-server setup --expires ${expiresAt} ${flags.map((flag) => `--${flag}`).join(" ")}`;
261
+ }
262
+ function newPending(input, ticket, expiresAt, now) {
263
+ return {
264
+ machine: input.machine,
265
+ transport: "tailscale",
266
+ address: input.address,
267
+ state: "pending",
268
+ requestedAt: new Date(now).toISOString(),
269
+ ticket,
270
+ ...expiresAt ? { expiresAt } : {}
271
+ };
272
+ }
273
+ function approved(input, reply, requestedAt) {
274
+ return {
275
+ state: "approved",
276
+ credential: {
277
+ machine: input.machine,
278
+ transport: "tailscale",
279
+ address: input.address,
280
+ state: "approved",
281
+ requestedAt,
282
+ serverKey: reply.serverKey,
283
+ deviceId: reply.result.device.id
284
+ },
285
+ result: reply.result
286
+ };
287
+ }
288
+ /**
289
+ * Connects to one remote machine for one access and says what changed. A returning client pins the stored server key.
290
+ * A pending client spends its setup ticket in first contact until the worker's `stim-server setup` approves it; the
291
+ * worker answers `unknown-secret` before that. With `fix`, a machine with no credential, or a revoked or lapsed one,
292
+ * gets a new setup ticket: `STIM_ACCESS_TICKET` when set (Stim Desktop's Add Machine flow), else one this machine
293
+ * generates, with the command to run on the worker. A pending credential is replaced when `STIM_ACCESS_TICKET` names
294
+ * another ticket.
295
+ */
296
+ async function reachMachine(input) {
297
+ const now = input.now ?? Date.now();
298
+ const { credential, io, access } = input;
299
+ const given = readAccessTicket();
300
+ const ask = () => {
301
+ if (given) return firstContact(newPending(input, given, void 0, now));
302
+ const other = input.other();
303
+ const shared = other?.state === "pending" && other.expiresAt ? other : void 0;
304
+ const ticket = shared ? {
305
+ ticket: shared.ticket,
306
+ expiresAt: shared.expiresAt
307
+ } : input.tickets.get(input.machine) ?? {
308
+ ticket: newSecret(),
309
+ expiresAt: new Date(now + GENERATED_TICKET_MS).toISOString()
310
+ };
311
+ input.tickets.set(input.machine, ticket);
312
+ return firstContact(newPending(input, ticket.ticket, ticket.expiresAt, now));
313
+ };
314
+ const firstContact = async (asked) => {
315
+ let pending = asked;
316
+ if (pending.serverKey) {
317
+ const resumed = await io.hello(input.endpoint, {
318
+ access,
319
+ serverKey: pending.serverKey
320
+ });
321
+ if ("result" in resumed && resumed.result.capabilities.includes(access)) return approved(input, resumed, pending.requestedAt);
322
+ }
323
+ const reply = await io.hello(input.endpoint, {
324
+ access,
325
+ ticket: pending.ticket,
326
+ name: input.deviceName,
327
+ onServerKey: (serverKey) => {
328
+ pending = {
329
+ ...pending,
330
+ serverKey
331
+ };
332
+ return input.persist(pending);
333
+ }
334
+ });
335
+ if ("result" in reply && reply.result.capabilities.includes(access)) return approved(input, reply, pending.requestedAt);
336
+ const command = pending.expiresAt ? setupCommand(pending.ticket, pending.expiresAt, input.setupFlags, input.serverVersion) : null;
337
+ if ("error" in reply && reply.error.code === "server-outdated") return {
338
+ state: "outdated",
339
+ credential: pending,
340
+ command
341
+ };
342
+ const sibling = "error" in reply && reply.error.code === "unknown-secret" ? input.other()?.serverKey : void 0;
343
+ if (sibling && sibling !== pending.serverKey) {
344
+ const again = await io.hello(input.endpoint, {
345
+ access,
346
+ serverKey: sibling
347
+ });
348
+ if ("result" in again && again.result.capabilities.includes(access)) return approved(input, again, pending.requestedAt);
349
+ }
350
+ if ("error" in reply && (reply.error.code === "unknown-secret" || reply.error.code === "forbidden")) return {
351
+ state: "pending",
352
+ credential: pending,
353
+ command,
354
+ reason: reply.error.message
355
+ };
356
+ return {
357
+ state: "unreachable",
358
+ credential: pending,
359
+ reason: "failed" in reply ? reply.failed : "error" in reply ? reply.error.message : "it did not grant this access"
360
+ };
361
+ };
362
+ if (!credential) return input.fix ? ask() : { state: "not-asked" };
363
+ if (credential.state === "approved") {
364
+ const reply = await io.hello(input.endpoint, {
365
+ access,
366
+ serverKey: credential.serverKey
367
+ });
368
+ if ("result" in reply && reply.result.capabilities.includes(access)) return approved(input, reply, credential.requestedAt);
369
+ if ("error" in reply && reply.error.code === "server-key-mismatch") return {
370
+ state: "key-changed",
371
+ credential
372
+ };
373
+ if ("error" in reply && reply.error.code === "server-outdated") return {
374
+ state: "outdated",
375
+ credential,
376
+ command: null
377
+ };
378
+ if ("error" in reply && (reply.error.code === "unauthorized" || reply.error.code === "forbidden")) return input.fix ? ask() : {
379
+ state: "revoked",
380
+ credential,
381
+ reason: reply.error.message
382
+ };
383
+ if ("result" in reply) return input.fix ? ask() : {
384
+ state: "revoked",
385
+ credential,
386
+ reason: "it no longer grants this access"
387
+ };
388
+ return {
389
+ state: "unreachable",
390
+ credential,
391
+ reason: "failed" in reply ? reply.failed : reply.error.message
392
+ };
393
+ }
394
+ if (input.fix && given && given !== credential.ticket) return ask();
395
+ if (credential.expiresAt && Date.parse(credential.expiresAt) <= now) return input.fix ? ask() : {
396
+ state: "lapsed",
397
+ credential
398
+ };
399
+ return firstContact(credential);
400
+ }
401
+ //#endregion
402
+ //#region src/devices/device-slots.ts
403
+ const DEFAULT_DEVICE_SLOT = "default";
404
+ const DEVICE_SLOT_RULE = "A device slot must be 1-64 letters, digits, underscores or hyphens, starting with a letter or digit.";
405
+ function isDeviceSlot(slot) {
406
+ return /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(slot) && ![
407
+ "constructor",
408
+ "prototype",
409
+ "__proto__"
410
+ ].includes(slot);
411
+ }
412
+ function validateDeviceSlot(slot = "default") {
413
+ if (!isDeviceSlot(slot)) {
414
+ const error = /* @__PURE__ */ new Error(DEVICE_SLOT_RULE);
415
+ Object.assign(error, { code: "STIM_BAD_ARG" });
416
+ throw error;
417
+ }
418
+ return slot;
419
+ }
420
+ function parseDeviceSlotOption(slot) {
421
+ if (!isDeviceSlot(slot)) throw new InvalidArgumentError(DEVICE_SLOT_RULE);
422
+ if (slot === "web") throw new InvalidArgumentError(`web names this workspace's owned Chrome (stim stop --slot web); choose another device slot name.`);
423
+ return slot;
424
+ }
425
+ function deviceSlotPlatforms(project, slot = DEFAULT_DEVICE_SLOT) {
426
+ validateDeviceSlot(slot);
427
+ if (slot === DEFAULT_DEVICE_SLOT) return project?.platforms;
428
+ return project?.deviceSlots && Object.hasOwn(project.deviceSlots, slot) ? project.deviceSlots[slot] : void 0;
429
+ }
430
+ function projectDeviceSlots(project) {
431
+ const slots = [{
432
+ slot: DEFAULT_DEVICE_SLOT,
433
+ platforms: project?.platforms ?? {}
434
+ }];
435
+ for (const [slot, platforms] of Object.entries(project?.deviceSlots ?? {})) {
436
+ validateDeviceSlot(slot);
437
+ if (slot === DEFAULT_DEVICE_SLOT) throw new Error("The default device slot must use the existing platforms record.");
438
+ if (!platforms || typeof platforms !== "object" || Array.isArray(platforms)) throw new Error(`Invalid device slot record: ${slot}`);
439
+ slots.push({
440
+ slot,
441
+ platforms
442
+ });
443
+ }
444
+ return slots;
445
+ }
446
+ function assignSlotDevice(project, platform, device, slot = DEFAULT_DEVICE_SLOT) {
447
+ validateDeviceSlot(slot);
448
+ if (platform !== "ios" && platform !== "android") throw new Error(`Unknown device platform: ${platform}`);
449
+ if (slot === DEFAULT_DEVICE_SLOT) project.platforms = {
450
+ ...project.platforms,
451
+ [platform]: device
452
+ };
453
+ else project.deviceSlots = {
454
+ ...project.deviceSlots,
455
+ [slot]: {
456
+ ...deviceSlotPlatforms(project, slot),
457
+ [platform]: device
458
+ }
459
+ };
460
+ }
461
+ function removeSlotDevice(project, platform, slot = DEFAULT_DEVICE_SLOT) {
462
+ const platforms = deviceSlotPlatforms(project, slot);
463
+ if (!platforms) return;
464
+ delete platforms[platform];
465
+ if (slot !== DEFAULT_DEVICE_SLOT && Object.keys(platforms).length === 0) {
466
+ delete project.deviceSlots[slot];
467
+ if (Object.keys(project.deviceSlots).length === 0) delete project.deviceSlots;
468
+ }
469
+ }
470
+ function deviceSlotKey(platform, slot = "default") {
471
+ validateDeviceSlot(slot);
472
+ if (platform !== "ios" && platform !== "android") throw new Error(`Unknown device platform: ${platform}`);
473
+ return slot === "default" ? platform : `${platform}:${slot}`;
474
+ }
475
+ /**
476
+ * The slot key in a form that is legal as a file name. NTFS reads a colon in a file name as an
477
+ * alternate data stream: the write and the read of the exact path both succeed while `readdirSync`
478
+ * cannot see the file, so every non-default slot's logs would be invisible to `stim logs`. A slot
479
+ * name can contain neither a colon nor a dot, so the two forms stay one-to-one.
480
+ */
481
+ function deviceSlotFileKey(platform, slot = "default") {
482
+ return deviceSlotKey(platform, slot).replace(":", ".");
483
+ }
484
+ function parseDeviceSlotKey(key) {
485
+ const [platform, slot = "default", extra] = key.split(":");
486
+ if (platform !== "ios" && platform !== "android" || extra !== void 0) return null;
487
+ try {
488
+ validateDeviceSlot(slot);
489
+ } catch {
490
+ return null;
491
+ }
492
+ if (key !== deviceSlotKey(platform, slot)) return null;
493
+ return {
494
+ platform,
495
+ slot
496
+ };
497
+ }
498
+ //#endregion
499
+ //#region src/fs-util.ts
500
+ const WINDOWS = process.platform === "win32";
501
+ const DRIVE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
502
+ function volumeRootFor(path) {
503
+ const normalized = normalize(String(path));
504
+ if (WINDOWS) return parse(normalized).root.replace(/^[a-z]:/, (drive) => drive.toUpperCase());
505
+ const m = normalized.match(/^\/volumes\/([^/]+)/i);
506
+ return m ? `/Volumes/${m[1]}` : "/";
507
+ }
508
+ function resolveWorkspaceRealish(workspacePath) {
509
+ const raw = String(workspacePath);
510
+ if (!isAbsolute$1(raw)) return null;
511
+ let current = raw;
512
+ const MAX_HOPS = 40;
513
+ for (let hops = 0; hops < MAX_HOPS; hops++) {
514
+ const normalized = normalize(current);
515
+ const root = parse(normalized).root;
516
+ const segments = normalized.slice(root.length).split(sep$1).filter(Boolean);
517
+ let prefix = root.endsWith(sep$1) ? root.slice(0, -sep$1.length) : root;
518
+ let followedSymlink = false;
519
+ for (const segment of segments) {
520
+ prefix += `${sep$1}${segment}`;
521
+ let st;
522
+ try {
523
+ st = lstatSync(prefix);
524
+ } catch (err) {
525
+ if (err && err.code === "ENOENT") return normalized;
526
+ return null;
527
+ }
528
+ if (st.isSymbolicLink()) {
529
+ let target;
530
+ try {
531
+ target = readlinkSync(prefix);
532
+ } catch {
533
+ return null;
534
+ }
535
+ current = `${isAbsolute$1(target) ? target : normalize(join$1(dirname$1(prefix), target))}${normalized.slice(prefix.length)}`;
536
+ followedSymlink = true;
537
+ break;
538
+ }
539
+ }
540
+ if (!followedSymlink) return normalized;
541
+ }
542
+ return null;
543
+ }
544
+ function isRealMount(entryDev, rootDev) {
545
+ return entryDev != null && rootDev != null && entryDev !== rootDev;
546
+ }
547
+ function listWindowsVolumeRoots(statFn) {
548
+ const roots = [];
549
+ for (const letter of DRIVE_LETTERS) {
550
+ const root = `${letter}:${sep$1}`;
551
+ try {
552
+ statFn(root);
553
+ } catch {
554
+ continue;
555
+ }
556
+ roots.push(root);
557
+ }
558
+ return roots;
559
+ }
560
+ function listMountedVolumes({ statFn = statSync } = {}) {
561
+ if (WINDOWS) return listWindowsVolumeRoots(statFn);
562
+ const roots = ["/"];
563
+ let rootDev;
564
+ try {
565
+ rootDev = statFn("/").dev;
566
+ } catch {
567
+ return roots;
568
+ }
569
+ try {
570
+ for (const name of readdirSync("/Volumes")) {
571
+ const path = join$1("/Volumes", name);
572
+ let entryDev;
573
+ try {
574
+ entryDev = statFn(path).dev;
575
+ } catch {
576
+ continue;
577
+ }
578
+ if (isRealMount(entryDev, rootDev)) roots.push(path);
579
+ }
580
+ } catch {}
581
+ return roots;
582
+ }
583
+ function resolveVolumeRoot(path) {
584
+ const realish = resolveWorkspaceRealish(path);
585
+ return realish === null ? null : volumeRootFor(realish);
586
+ }
587
+ function uncShareIsReachable(volume, statFn) {
588
+ if (!WINDOWS || !volume.startsWith(`${sep$1}${sep$1}`)) return false;
589
+ try {
590
+ return statFn(volume).isDirectory();
591
+ } catch {
592
+ return false;
593
+ }
594
+ }
595
+ function isOnMountedVolume(path, mountedVolumes, { statFn = statSync } = {}) {
596
+ const mounted = new Set(mountedVolumes || listMountedVolumes());
597
+ const volume = resolveVolumeRoot(path);
598
+ if (volume === null) return false;
599
+ return mounted.has(volume) || uncShareIsReachable(volume, statFn);
600
+ }
601
+ function directorySize(dir, { timeoutMs }) {
602
+ return measuredDirectorySize(dir, { timeoutMs }) ?? 0;
603
+ }
604
+ async function directorySizeAsync(dir, { timeoutMs }) {
605
+ return await measuredDirectorySizeAsync(dir, { timeoutMs }) ?? 0;
606
+ }
607
+ async function measuredDirectorySizeAsync(dir, { timeoutMs }) {
608
+ let out;
609
+ try {
610
+ out = await getExecutor().runFileAsync("du", ["-sk", dir], {
611
+ timeoutMs,
612
+ killSignal: "SIGKILL"
613
+ });
614
+ } catch {
615
+ return null;
616
+ }
617
+ if (!out) return 0;
618
+ const kb = parseInt(out.split(/\s+/)[0] ?? "", 10);
619
+ return isNaN(kb) ? 0 : kb * 1024;
620
+ }
621
+ function measuredDirectorySize(dir, { timeoutMs }) {
622
+ let out;
623
+ try {
624
+ out = getExecutor().runFile("du", ["-sk", dir], { timeoutMs });
625
+ } catch {
626
+ return null;
627
+ }
628
+ if (!out) return 0;
629
+ const kb = parseInt(out.split(/\s+/)[0] ?? "", 10);
630
+ return isNaN(kb) ? 0 : kb * 1024;
631
+ }
632
+ function formatBytes(bytes) {
633
+ if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)}G`;
634
+ if (bytes >= 1024 ** 2) return `${Math.round(bytes / 1024 ** 2)}M`;
635
+ return `${Math.round(bytes / 1024)}K`;
636
+ }
637
+ //#endregion
638
+ //#region src/devices/avd-claim.ts
639
+ function acquireAvdClaim(avdName) {
640
+ const root = join(configDir(), "avd-locks", `${encodeURIComponent(avdName.toLowerCase())}.lock`);
641
+ const label = `AVD ${avdName}`;
642
+ const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
643
+ root,
644
+ mode: "exclusive",
645
+ label
646
+ });
647
+ if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
648
+ if (attempt.acquired) return attempt.acquired;
649
+ throw new ownership_claim_exports.ClaimRefusedError({
650
+ root,
651
+ claimPath: attempt.held?.path ?? attempt.waitingFor?.[0]?.path ?? root,
652
+ label,
653
+ reason: "another operation is creating, recovering, or tearing down this AVD"
654
+ });
655
+ }
656
+ //#endregion
657
+ //#region src/workspace/config.ts
658
+ function getConfigDir() {
659
+ return configDir();
660
+ }
661
+ function refuseRelativeStimPaths(env = process.env) {
662
+ for (const name of [
663
+ "STIM_HOME",
664
+ "STIM_BUILD_CACHE",
665
+ "STIM_METRO_CACHE"
666
+ ]) {
667
+ const value = env[name];
668
+ if (!value || isAbsolute$1(value)) continue;
669
+ 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.`);
670
+ error.code = "STIM_RELATIVE_PATH";
671
+ throw error;
672
+ }
673
+ }
674
+ function saveConfig(config) {
675
+ ensurePrivateDir(getConfigDir());
676
+ const target = getConfigPath();
677
+ const tmp = `${target}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
678
+ writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
679
+ try {
680
+ renameSync(tmp, target);
681
+ } catch (err) {
682
+ try {
683
+ unlinkSync(tmp);
684
+ } catch {}
685
+ throw err;
686
+ }
687
+ }
688
+ const CONFIG_VERSION = 2;
689
+ function ensureConfig() {
690
+ return withConfigLock(() => {
691
+ const existing = loadConfig();
692
+ if (existing) {
693
+ if (existing.version !== CONFIG_VERSION) {
694
+ existing.version = CONFIG_VERSION;
695
+ saveConfig(existing);
696
+ }
697
+ return existing;
698
+ }
699
+ const fresh = {
700
+ version: CONFIG_VERSION,
701
+ projects: {},
702
+ repos: {}
703
+ };
704
+ saveConfig(fresh);
705
+ return fresh;
706
+ });
707
+ }
708
+ function requireAbsoluteProjectPath(projectPath) {
709
+ if (isAbsolute$1(projectPath)) return;
710
+ 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.`);
711
+ }
712
+ function upsertProject(projectPath, fields) {
713
+ requireAbsoluteProjectPath(projectPath);
714
+ return withConfigLock(() => {
715
+ const cfg = ensureConfig();
716
+ const existing = cfg.projects[projectPath] || {
717
+ metroPort: null,
718
+ platforms: {}
719
+ };
720
+ cfg.projects[projectPath] = {
721
+ ...existing,
722
+ ...typeof fields === "function" ? fields(existing) : fields
723
+ };
724
+ saveConfig(cfg);
725
+ return cfg.projects[projectPath];
726
+ });
727
+ }
728
+ function removeProject(projectPath) {
729
+ withConfigLock(() => {
730
+ const cfg = loadConfig();
731
+ if (!cfg?.projects?.[projectPath]) return;
732
+ 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.`);
733
+ for (const { platforms } of projectDeviceSlots(cfg.projects[projectPath])) if (platforms.android?.owned && platforms.android.avdName) (0, ownership_claim_exports.releaseClaim)(acquireAvdClaim(platforms.android.avdName));
734
+ delete cfg.projects[projectPath];
735
+ saveConfig(cfg);
736
+ });
737
+ }
738
+ function claimMetroPort(projectPath, port) {
739
+ return withConfigLock(() => {
740
+ const cfg = ensureConfig();
741
+ if (!cfg.projects[projectPath]) throw new Error(`Project not registered: ${projectPath}`);
742
+ for (const [path, proj] of Object.entries(cfg.projects)) {
743
+ if (path !== projectPath && proj?.metroPort === port) return null;
744
+ if (Object.values(proj?.ports ?? {}).includes(port)) return null;
745
+ }
746
+ cfg.projects[projectPath].metroPort = port;
747
+ saveConfig(cfg);
748
+ return port;
749
+ });
750
+ }
751
+ function releaseMetroPort(projectPath, port) {
752
+ withConfigLock(() => {
753
+ const cfg = loadConfig();
754
+ const project = cfg?.projects?.[projectPath];
755
+ if (!cfg || project?.metroPort !== port) return;
756
+ project.metroPort = null;
757
+ saveConfig(cfg);
758
+ });
759
+ }
760
+ function setDevice(projectPath, platform, deviceFields, slot = "default") {
761
+ requireAbsoluteProjectPath(projectPath);
762
+ withConfigLock(() => {
763
+ const cfg = ensureConfig();
764
+ if (!cfg.projects[projectPath]) throw new Error(`Project not registered: ${projectPath}`);
765
+ assignSlotDevice(cfg.projects[projectPath], platform, deviceFields, slot);
766
+ saveConfig(cfg);
767
+ });
768
+ }
769
+ function releaseAndroidConsolePort(projectPath, consolePort, slot = "default") {
770
+ return withConfigLock(() => {
771
+ const cfg = loadConfig();
772
+ const android = deviceSlotPlatforms(cfg?.projects?.[projectPath], slot)?.android;
773
+ if (!cfg || !android || android.consolePort !== consolePort) return false;
774
+ delete android.consolePort;
775
+ saveConfig(cfg);
776
+ return true;
777
+ });
778
+ }
779
+ function recordIosSchemeApprovals(projectPath, deviceUdid, schemeApprovals, slot = "default") {
780
+ withConfigLock(() => {
781
+ const cfg = loadConfig();
782
+ const ios = deviceSlotPlatforms(cfg?.projects?.[projectPath], slot)?.ios;
783
+ if (!cfg || !ios?.owned || ios.deviceUdid !== deviceUdid) return;
784
+ ios.schemeApprovals = schemeApprovals;
785
+ saveConfig(cfg);
786
+ });
787
+ }
788
+ function clearAndroidBootPending(projectPath, avdName, slot = "default") {
789
+ withConfigLock(() => {
790
+ const cfg = loadConfig();
791
+ const android = deviceSlotPlatforms(cfg?.projects?.[projectPath], slot)?.android;
792
+ if (!cfg || android?.avdName !== avdName || !android.bootPending) return;
793
+ delete android.bootPending;
794
+ saveConfig(cfg);
795
+ });
796
+ }
797
+ function clearDevice(projectPath, platform, slot = "default", expectedId) {
798
+ return withConfigLock(() => {
799
+ const cfg = loadConfig();
800
+ const project = cfg?.projects?.[projectPath];
801
+ if (!cfg || !project) return false;
802
+ const current = deviceSlotPlatforms(project, slot)?.[platform];
803
+ if (expectedId !== void 0 && (platform === "ios" ? current?.deviceUdid : current?.avdName) !== expectedId) return false;
804
+ removeSlotDevice(project, platform, slot);
805
+ saveConfig(cfg);
806
+ return true;
807
+ });
808
+ }
809
+ function setSupervisor(projectPath, { pid, port, startedAt, processToken }) {
810
+ requireAbsoluteProjectPath(projectPath);
811
+ return withConfigLock(() => {
812
+ const cfg = ensureConfig();
813
+ if (!cfg.projects[projectPath]) cfg.projects[projectPath] = {
814
+ metroPort: null,
815
+ platforms: {}
816
+ };
817
+ cfg.projects[projectPath].supervisor = {
818
+ pid,
819
+ port,
820
+ startedAt,
821
+ processToken
822
+ };
823
+ saveConfig(cfg);
824
+ return cfg.projects[projectPath].supervisor;
825
+ });
826
+ }
827
+ function clearSupervisor(projectPath, expected) {
828
+ return withConfigLock(() => {
829
+ const cfg = loadConfig();
830
+ const current = cfg?.projects?.[projectPath]?.supervisor;
831
+ if (!current) return true;
832
+ if (expected !== void 0 && !(0, process_identity_exports.sameProcessRecord)(current, expected)) return false;
833
+ delete cfg.projects[projectPath].supervisor;
834
+ saveConfig(cfg);
835
+ return true;
836
+ });
837
+ }
838
+ function targetSettings(cfg, target, create) {
839
+ if (target.scope === "machine") return cfg;
840
+ if (target.scope === "workspace") {
841
+ if (!cfg.projects[target.projectPath]) {
842
+ if (!create) return null;
843
+ cfg.projects[target.projectPath] = {
844
+ metroPort: null,
845
+ platforms: {}
846
+ };
847
+ }
848
+ const project = cfg.projects[target.projectPath];
849
+ if (!project.settings && create) project.settings = {};
850
+ return project.settings ?? null;
851
+ }
852
+ if (!cfg.repos[target.gitCommonDir]) {
853
+ if (!create) return null;
854
+ cfg.repos[target.gitCommonDir] = {};
855
+ }
856
+ const repo = cfg.repos[target.gitCommonDir];
857
+ if (!repo.settings && create) repo.settings = {};
858
+ return repo.settings ?? null;
859
+ }
860
+ function localDevicePoolOff(dottedKey, value) {
861
+ if (dottedKey === "remote.devicePoolDisabled") return Array.isArray(value) && value.includes("local");
862
+ if (dottedKey !== "remote.machines") return false;
863
+ const disabled = loadConfig()?.remote?.devicePoolDisabled;
864
+ return Array.isArray(disabled) && disabled.includes("local");
865
+ }
866
+ function writeConfigSetting(target, dottedKey, value) {
867
+ if (target.scope === "workspace") requireAbsoluteProjectPath(target.projectPath);
868
+ const status = target.scope === "machine" && localDevicePoolOff(dottedKey, value) ? realIo.status() : null;
869
+ return withConfigLock(() => applyConfigSetting(target, dottedKey, value, status));
870
+ }
871
+ /** Waits for the config lock and device-pool probe asynchronously; cancellation prevents a pending write. */
872
+ async function writeConfigSettingAsync(target, dottedKey, value, signal) {
873
+ if (target.scope === "workspace") requireAbsoluteProjectPath(target.projectPath);
874
+ signal?.throwIfAborted();
875
+ const status = target.scope === "machine" && (dottedKey === "remote.machines" || localDevicePoolOff(dottedKey, value)) ? await tailscaleStatusAsync(signal) : null;
876
+ return withConfigLockAsync(() => applyConfigSetting(target, dottedKey, value, status), signal);
877
+ }
878
+ function applyConfigSetting(target, dottedKey, value, status) {
879
+ const cfg = value === void 0 ? loadConfig() : ensureConfig();
880
+ const settings = cfg ? targetSettings(cfg, target, value !== void 0) : null;
881
+ if (!cfg || !settings) return false;
882
+ if (value === void 0) {
883
+ if (!deleteNested(settings, dottedKey)) return false;
884
+ } else writeNested(settings, dottedKey, value);
885
+ if (target.scope === "machine") {
886
+ if (dottedKey === "remote.buildPoolDisabled" || dottedKey === "remote.machines") validateAutomaticPool("build", cfg);
887
+ if (dottedKey === "remote.devicePoolDisabled" || dottedKey === "remote.machines") validateAutomaticPool("device", cfg, (machine) => !cannotHost(status, machine));
888
+ }
889
+ saveConfig(cfg);
890
+ return true;
891
+ }
892
+ function writeNested(obj, dottedKey, value) {
893
+ const keys = dottedKey.split(".");
894
+ let cur = obj;
895
+ for (let i = 0; i < keys.length - 1; i++) {
896
+ const k = keys[i];
897
+ if (k === void 0) continue;
898
+ const next = cur[k];
899
+ if (typeof next !== "object" || next === null) cur[k] = {};
900
+ cur = cur[k];
901
+ }
902
+ const leaf = keys[keys.length - 1];
903
+ if (leaf !== void 0) cur[leaf] = value;
904
+ }
905
+ function deleteNested(obj, dottedKey) {
906
+ const keys = dottedKey.split(".");
907
+ const chain = [obj];
908
+ let cur = obj;
909
+ for (let i = 0; i < keys.length - 1; i++) {
910
+ const k = keys[i];
911
+ if (k === void 0) return false;
912
+ const next = cur[k];
913
+ if (next == null || typeof next !== "object") return false;
914
+ cur = next;
915
+ chain.push(cur);
916
+ }
917
+ const leaf = keys[keys.length - 1];
918
+ if (leaf === void 0) return false;
919
+ if (!(leaf in cur)) return false;
920
+ delete cur[leaf];
921
+ for (let i = chain.length - 2; i >= 0; i--) {
922
+ const parent = chain[i];
923
+ const key = keys[i];
924
+ const child = chain[i + 1];
925
+ if (parent === void 0 || key === void 0 || child === void 0) break;
926
+ if (Object.keys(child).length === 0) delete parent[key];
927
+ else break;
928
+ }
929
+ return true;
930
+ }
931
+ function allConsolePortsAndSerials({ isMounted = isOnMountedVolume } = {}) {
932
+ const cfg = loadConfig();
933
+ const result = {
934
+ androidConsolePorts: [],
935
+ androidPhysicalSerials: []
936
+ };
937
+ if (!cfg) return result;
938
+ for (const [path, proj] of Object.entries(cfg.projects || {})) {
939
+ if (!existsSync(path) && isMounted(path)) continue;
940
+ for (const { platforms } of projectDeviceSlots(proj)) {
941
+ const android = platforms.android;
942
+ if (typeof android?.consolePort === "number") result.androidConsolePorts.push(android.consolePort);
943
+ if (android?.serial && !android.avdName) result.androidPhysicalSerials.push(android.serial);
944
+ }
945
+ }
946
+ return result;
947
+ }
948
+ //#endregion
949
+ export { openAuthenticated as $, writeConfigSettingAsync as A, assignSlotDevice as B, saveConfig as C, withConfigLock$1 as D, upsertProject as E, isOnMountedVolume as F, parseDeviceSlotOption as G, deviceSlotKey as H, listMountedVolumes as I, validateDeviceSlot as J, projectDeviceSlots as K, measuredDirectorySize as L, directorySize as M, directorySizeAsync as N, withConfigLockAsync$1 as O, formatBytes as P, findPeer$1 as Q, measuredDirectorySizeAsync as R, removeProject as S, setSupervisor as T, deviceSlotPlatforms as U, deviceSlotFileKey as V, parseDeviceSlotKey as W, doctorIo as X, SERVER_OUTDATED as Y, endpoint as Z, loadConfig$1 as _, clearDevice as a, sayHello as at, releaseAndroidConsolePort as b, ensureConfig as c, readHostPermissions as ct, getConfigDir as d, outdatedServer as et, getConfigPath$1 as f, isPathPrefix as g, getRepoSettings as h, clearAndroidBootPending as i, resolveEndpoint as it, acquireAvdClaim as j, writeConfigSetting as k, findEnclosingWorktreeRoot as l, getProjectSettings as m, allMetroPorts as n, reachMachine as nt, clearSupervisor as o, setupRemedy as ot, getProject as p, removeSlotDevice as q, claimMetroPort as r, realIo as rt, configCorruptRepair as s, tailscaleStatusAsync as st, allConsolePortsAndSerials as t, parseMachine$1 as tt, getConcurrencyLimits as u, recordIosSchemeApprovals as v, setDevice as w, releaseMetroPort as x, refuseRelativeStimPaths as y, volumeRootFor as z };