stim 1.20.2 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/README.md +141 -8
  2. package/dist/acquire-artifact-Co2k4f7O.mjs +171 -0
  3. package/dist/act-DsuqEkiM.mjs +2801 -0
  4. package/dist/activity-CMLmzp7o.mjs +1397 -0
  5. package/dist/{agent-device-usage-output-Dzgv6lgR.mjs → agent-device-usage-output-DUFaTMdd.mjs} +23 -67
  6. package/dist/{android-DCKZrNvV.mjs → android-ByRMUoiJ.mjs} +157 -93
  7. package/dist/{android-Mz_Inn3w.mjs → android-C7sVmyI9.mjs} +299 -116
  8. package/dist/{android-Dlzxshgp.mjs → android-DGkNqa_c.mjs} +5 -5
  9. package/dist/{android-HqP1_vB_.mjs → android-DR5lZ2Az.mjs} +2 -2
  10. package/dist/android-cas-compiler.mjs +2 -2
  11. package/dist/{android-cas-Bl-cLJBk.mjs → android-cas-ngferVq2.mjs} +7 -6
  12. package/dist/android-project-BCAAmMwz.mjs +12 -0
  13. package/dist/android-project-DVWyCbWU.d.mts +391 -0
  14. package/dist/api.d.mts +810 -64
  15. package/dist/api.mjs +782 -1
  16. package/dist/{artifact-HSwkIWEQ.mjs → artifact-8EY8SAPw.mjs} +240 -213
  17. package/dist/{artifact-D3czlUvT.mjs → artifact-DJPy518Y.mjs} +203 -164
  18. package/dist/artifact-platform-CXcmX_j3.mjs +27 -0
  19. package/dist/{browser-web-CA-F1GQo.mjs → browser-web-Bcv99PBM.mjs} +194 -64
  20. package/dist/budget-Cvwn54qV.mjs +9285 -0
  21. package/dist/{build-DYDbfkk5.mjs → build-CabC3G23.mjs} +153 -98
  22. package/dist/build-cache-Dv7RN200.mjs +602 -0
  23. package/dist/build-plan-DUC8Ka9y.mjs +248 -0
  24. package/dist/{cache-manifest-XH-fvDUj.mjs → cache-manifest-DSeQjt-Q.mjs} +2 -2
  25. package/dist/cache-manifest.mjs +1 -1
  26. package/dist/{ccache-zdKiYQ3E.mjs → ccache-Btwc36-I.mjs} +10 -9
  27. package/dist/{cdp-Dq_ypAlJ.mjs → cdp-3fwICjzi.mjs} +18 -5
  28. package/dist/{chrome-BnzZMqsR.mjs → chrome-Bbw689-6.mjs} +7 -4
  29. package/dist/{cli-CLP3diBm.mjs → cli-CHZa9Lvk.mjs} +20 -20
  30. package/dist/cli.mjs +1 -1
  31. package/dist/client-OyRoEuSr.mjs +1577 -0
  32. package/dist/collector-run.d.mts +4 -40
  33. package/dist/collector-run.mjs +9 -8
  34. package/dist/{command-output-BTy8hZ0F.mjs → command-output-gwpm28fu.mjs} +1 -1
  35. package/dist/config-DlrsduwG.mjs +955 -0
  36. package/dist/{created-devices-Dzd5C2LF.mjs → created-devices-x9rN6bhs.mjs} +14 -4
  37. package/dist/{dependency-state-DG0p31-y.mjs → dependency-state-1I9KZT1Y.mjs} +20 -5
  38. package/dist/{detached-entry-B9C_zeQC.mjs → detached-entry-BwNEtIrP.mjs} +1 -1
  39. package/dist/{dev-client-DAWVIGBu.mjs → dev-client-BSDR5h3F.mjs} +2 -2
  40. package/dist/{device-BECOiufl.mjs → device-B-L3Fbxy.mjs} +91 -34
  41. package/dist/device-DklWZLqb.mjs +1068 -0
  42. package/dist/{device-capacity-CBUAcRyQ.mjs → device-capacity-DK5_EwRS.mjs} +42 -13
  43. package/dist/device-host-worker.mjs +76 -79
  44. package/dist/{device-ios-COSi6oS1.mjs → device-ios-DoB6KFCO.mjs} +77 -51
  45. package/dist/{device-lease-run-Cw2M4z51.mjs → device-lease-run-xJPQ_QQG.mjs} +14 -26
  46. package/dist/{device-pool-Bf-pOhSw.mjs → device-pool-BMkdsefq.mjs} +5 -5
  47. package/dist/{doctor-kfMBnUw6.mjs → doctor-Bo7BXLux.mjs} +271 -320
  48. package/dist/{doctor-CB8oZKCk.mjs → doctor-D5_EzwfA.mjs} +186 -91
  49. package/dist/doctor-fingerprint-DZLoYFVb.mjs +16 -0
  50. package/dist/doctor-fingerprint.mjs +19 -0
  51. package/dist/doctor-thjWAV0z.mjs +2 -0
  52. package/dist/{doctor-watchman-BRyV2adA.mjs → doctor-watchman-N2wDotwQ.mjs} +40 -21
  53. package/dist/{eas-build-JyfqyHfC.mjs → eas-build-CwknaQYj.mjs} +53 -20
  54. package/dist/{build-plan-B7SZsc2N.mjs → eas-fallback-CSgAulr5.mjs} +41 -214
  55. package/dist/{watchman-CPQFZLXY.mjs → eas-session-ledger-DlY_serQ.mjs} +230 -11
  56. package/dist/{error-diagnostics-BR677IDM.mjs → error-diagnostics-DBWO2rxE.mjs} +172 -82
  57. package/dist/exec-DEoAhcze.mjs +858 -0
  58. package/dist/exec-DnYc4HGH.d.mts +78 -0
  59. package/dist/{gc-qWOMdnER.mjs → gc-CXxN-4SE.mjs} +1091 -1029
  60. package/dist/gc-DnNDQyaP.mjs +3 -0
  61. package/dist/{guide-G9cAbVAc.mjs → guide-Br0LEfKy.mjs} +1165 -297
  62. package/dist/{hosted-android-C3MHGld1.mjs → hosted-android-B-9f-jkd.mjs} +10 -8
  63. package/dist/{hosted-client-ZEKpS9vw.mjs → hosted-client-DZAPrcro.mjs} +34 -19
  64. package/dist/{hosted-ios-l-o3iM9J.mjs → hosted-ios-CsCS21hn.mjs} +1 -1
  65. package/dist/{hosted-logs-EzQM7-3Z.mjs → hosted-logs-FIRx9X2f.mjs} +77 -43
  66. package/dist/{hosted-macos-BACVAVqE.mjs → hosted-macos-_awYSpfv.mjs} +35 -22
  67. package/dist/{hosted-native-A4s1QPXe.mjs → hosted-native-BdzE668-.mjs} +49 -27
  68. package/dist/idle-C4cZ757K.d.mts +24 -0
  69. package/dist/{idle-shutdown-DtxhPTKe.mjs → idle-shutdown-DDg3SdvY.mjs} +11 -10
  70. package/dist/ios-CDvlmUs7.d.mts +24 -0
  71. package/dist/{ios-BwVCF7oA.mjs → ios-Ds1clbIt.mjs} +8 -8
  72. package/dist/{ios-device-D7pi2Sca.mjs → ios-device--h_-8ENa.mjs} +51 -26
  73. package/dist/{ios-device-B5veSDDQ.mjs → ios-device-BXZ0c7Kb.mjs} +1 -1
  74. package/dist/{ios-RkW4pnQf.mjs → ios-hcBpnEGk.mjs} +200 -93
  75. package/dist/{ios-Ce7bc7br.mjs → ios-pm4nL6By.mjs} +160 -108
  76. package/dist/{ios-project-K9hY_Jab.mjs → ios-project-CZhKODF3.mjs} +13 -13
  77. package/dist/ios-signing-BtAffm0A.mjs +489 -0
  78. package/dist/{ios-state-CBLe1xYw.mjs → ios-state-Dtw5K2aI.mjs} +1 -1
  79. package/dist/{ios-nOLJGVdy.mjs → ios-xtlRHaeB.mjs} +1 -1
  80. package/dist/{js-swap-Bb3nsS07.mjs → js-swap-Bwhh4S7V.mjs} +30 -25
  81. package/dist/{launch-C3qJ6Agb.mjs → launch-BjxemNpK.mjs} +85 -543
  82. package/dist/{launch-F4sOqAhB.mjs → launch-CCzXlc8r.mjs} +91 -90
  83. package/dist/logs-BfwwadsZ.mjs +479 -0
  84. package/dist/logs-jTS_lHSo.mjs +248 -0
  85. package/dist/{macos-D7uDvO-z.mjs → macos-DVlSSyUg.mjs} +1 -1
  86. package/dist/macos-NfV2D7dO.mjs +694 -0
  87. package/dist/macos-run.mjs +1 -1
  88. package/dist/maintenance-run.mjs +123 -248
  89. package/dist/{metro-DnCEjCxs.mjs → metro-DiXtA3Ty.mjs} +107 -30
  90. package/dist/metro-gateway-CKZqKqx7.mjs +209 -0
  91. package/dist/{named-ports-4WopRPg-.mjs → named-ports-DkEzOfzO.mjs} +42 -90
  92. package/dist/native-android-CqgnOgsT.mjs +645 -0
  93. package/dist/native-gradle-artifact-inputs-DHFnmrVf.mjs +270 -0
  94. package/dist/native-gradle-inputs-Dnh84Zbd.mjs +211 -0
  95. package/dist/native-inputs-TRTTTRxt.mjs +91 -0
  96. package/dist/native-run-NwUuxbkg.mjs +276 -0
  97. package/dist/{native-runtime-k8dTIrbK.mjs → native-runtime-B7KIafpM.mjs} +11 -9
  98. package/dist/native-source-Y0yLhFyQ.mjs +133 -0
  99. package/dist/{native-xcode-inputs-CykopJdM.mjs → native-xcode-inputs-D_VOfFL7.mjs} +26 -18
  100. package/dist/{native-xcode-ios-Bbf30Gm0.mjs → native-xcode-ios-BYDOHWLN.mjs} +104 -60
  101. package/dist/{ndjson-GOnbyvso.mjs → ndjson-CgE_o3Dl.mjs} +3 -3
  102. package/dist/offload-worker.d.mts +59 -41
  103. package/dist/offload-worker.mjs +478 -115
  104. package/dist/operation-exec-BW0em61G.mjs +171 -0
  105. package/dist/{ownership-DCtw_2IA.mjs → ownership-C5fo5Edb.mjs} +3 -3
  106. package/dist/{page-DRHy329N.mjs → page-DBL2PiUU.mjs} +12 -5
  107. package/dist/paths-BsJMoPP2.mjs +12 -0
  108. package/dist/paths-DQtTgsBf.mjs +42 -0
  109. package/dist/{placement-log-MOfPVgaM.mjs → placement-log-BnGgJRRZ.mjs} +5 -5
  110. package/dist/{plan-placement-V0TdzYXp.mjs → plan-placement-DIsMtN-d.mjs} +15 -11
  111. package/dist/{ports-DdSUicVo.mjs → ports-Cy1RkmuQ.mjs} +3 -3
  112. package/dist/prebuild-D4JoEAEs.mjs +484 -0
  113. package/dist/{project-_phNIJfs.mjs → project-wM_XFTvE.mjs} +65 -37
  114. package/dist/{pull-request-CQ8sPbZa.mjs → pull-request-W1IuLXDt.mjs} +102 -20
  115. package/dist/pull-request-eODyIqZF.d.mts +124 -0
  116. package/dist/pull-requests.d.mts +2 -120
  117. package/dist/pull-requests.mjs +2 -2
  118. package/dist/{react-native-android-CM1XRDwv.mjs → react-native-android-6OZ7MSM7.mjs} +121 -72
  119. package/dist/{gradle-DSrXb6lG.mjs → react-native-build-Dvd8hC21.mjs} +292 -15
  120. package/dist/react-native-doctor-DsoSF1Y9.mjs +234 -0
  121. package/dist/{react-native-ios-Dhq_eiYQ.mjs → react-native-ios-4u1Rn-GR.mjs} +71 -39
  122. package/dist/{recordings-D0d5aQt0.mjs → recordings-CQmfzAh7.mjs} +12 -9
  123. package/dist/registration-BcRgF6Ox.mjs +51 -0
  124. package/dist/{reload-C8JhxjSh.mjs → reload-BzkYabB-.mjs} +94 -47
  125. package/dist/reload-D7AVlb8N.mjs +2 -0
  126. package/dist/{remote-BiR6aYjy.mjs → remote-DjGDmlTZ.mjs} +26 -18
  127. package/dist/result-D2Ykz_iD.mjs +435 -0
  128. package/dist/{run-BtPgpKks.mjs → run-DidHghD1.mjs} +5 -5
  129. package/dist/selection-DSMbWEX2.mjs +620 -0
  130. package/dist/{server-bare-R2m_gsH8.mjs → server-bare-CafzuyFB.mjs} +3 -4
  131. package/dist/{server-command-CTtCkmcQ.mjs → server-command-C6QjeupS.mjs} +14 -7
  132. package/dist/{server-expo-DsguMGUF.mjs → server-expo-CrjB29pq.mjs} +54 -11
  133. package/dist/server-expo-D58c9L0M.mjs +2 -0
  134. package/dist/{settings-B717odTD.mjs → settings-C7O8A6ZI.mjs} +309 -119
  135. package/dist/settings-DpR-1Wnn.mjs +114 -0
  136. package/dist/settings-write-CzD60rOD.mjs +360 -0
  137. package/dist/settings.schema.json +176 -16
  138. package/dist/{simslim-CCr3rE-A.mjs → simslim-BNckWpXi.mjs} +14 -12
  139. package/dist/{slot-launch-DPt0m9Nd.mjs → slot-launch-CbfmNgM6.mjs} +15 -20
  140. package/dist/{spawn-entry-Dcb7sDUz.mjs → spawn-entry-jw19m8Nl.mjs} +0 -1
  141. package/dist/stage-Bps5oius.mjs +200 -0
  142. package/dist/{start-TXeMmmO2.mjs → start-Br-oxqMD.mjs} +1 -1
  143. package/dist/start-EojN4WFz.mjs +1364 -0
  144. package/dist/{state-D1Icyjlu.mjs → state--smjFSTL.mjs} +55 -13
  145. package/dist/{state-CG_OFoaA.mjs → state-08I3hw3_.mjs} +3 -2
  146. package/dist/{stats-BoY5sEPU.mjs → stats-D3GerlFW.mjs} +7 -13
  147. package/dist/stats-report-BzaPrRm6.mjs +16 -0
  148. package/dist/{status-BTA7kKiQ.mjs → status-BRROykC8.mjs} +7 -5
  149. package/dist/status-EvrK72L2.mjs +2 -0
  150. package/dist/{status-WgFbkRzj.mjs → status-WBDajew2.mjs} +554 -452
  151. package/dist/{status-watch-C23ILtf9.mjs → status-watch-Bg_u-pwR.mjs} +222 -25
  152. package/dist/status-worker.d.mts +1 -0
  153. package/dist/status-worker.mjs +19 -0
  154. package/dist/stim-desktop-D5OGpYZM.mjs +112 -0
  155. package/dist/{stop-DR6VoDIk.mjs → stop-DTZ9uTw3.mjs} +2 -2
  156. package/dist/{stop-DPEhunlw.mjs → stop-Dky10AEJ.mjs} +33 -23
  157. package/dist/{stop-BNmBMi1F.mjs → stop-Mv6W9FAk.mjs} +137 -98
  158. package/dist/supervisor-run.d.mts +8 -17
  159. package/dist/supervisor-run.mjs +56 -28
  160. package/dist/{support-CwjbFs9H.mjs → support-CNioewsd.mjs} +14 -14
  161. package/dist/{support-DMTqj3Hm.mjs → support-yUiOjnVY.mjs} +20 -13
  162. package/dist/swiftpm-artifact-inputs-DFcGsXDP.mjs +153 -0
  163. package/dist/swiftpm-macos-BIak-Rxv.mjs +532 -0
  164. package/dist/toolchain-BY8X8HXx.mjs +2 -0
  165. package/dist/toolchain-DpJMHq0n.mjs +876 -0
  166. package/dist/{trigger-CigAYjeL.mjs → trigger-Cbyc41d_.mjs} +21 -6
  167. package/dist/trigger-yb_nFzPF.mjs +2 -0
  168. package/dist/{warm-progress-BuY-Fvfn.mjs → warm-progress-CA3TgT3-.mjs} +263 -156
  169. package/dist/web-CZ07gd77.mjs +2 -0
  170. package/dist/{web-BDBMxDpx.mjs → web-DKx6yoFA.mjs} +20 -14
  171. package/dist/web-run.mjs +13 -12
  172. package/dist/windows-report.d.mts +1 -0
  173. package/dist/windows-report.mjs +18 -0
  174. package/dist/windows-report.ps1 +43 -0
  175. package/dist/{workspace-state-DJt_MMvf.mjs → workspace-state-U08dEzhG.mjs} +62 -58
  176. package/dist/{workspaces-C36s2jb9.mjs → workspaces-CpANmBlq.mjs} +1723 -1286
  177. package/dist/{worktree-CgvJYJ-Q.mjs → worktree-CHecZ4Oq.mjs} +1097 -547
  178. package/dist/worktree-ZqZVjD9K.mjs +2 -0
  179. package/dist/xcode-BXn9-xYx.mjs +896 -0
  180. package/package.json +7 -5
  181. package/shim/android-audio/Main.java +24 -0
  182. package/shim/android-audio/README.md +17 -0
  183. package/shim/android-audio.jar +0 -0
  184. package/shim/android-optimizations.gradle +7 -5
  185. package/dist/activity-BSV745Kz.mjs +0 -629
  186. package/dist/api-qvOzWyfO.mjs +0 -154
  187. package/dist/api-run.mjs +0 -294
  188. package/dist/app-install-BIKtMfPq.mjs +0 -896
  189. package/dist/artifact-lifecycle-DzPpBYvK.mjs +0 -30
  190. package/dist/budget-f7516Zk0.mjs +0 -4106
  191. package/dist/build-facts-DcLv2HSd.d.mts +0 -111
  192. package/dist/cancellation-C_22VQTH.mjs +0 -24
  193. package/dist/client-qQAaXSUs.mjs +0 -1771
  194. package/dist/config-BYDxlMY3.mjs +0 -562
  195. package/dist/device-DuiPJQSn.mjs +0 -290
  196. package/dist/device-lease-BX1sJQ2E.mjs +0 -1224
  197. package/dist/device-remote-xRT0Vmo9.mjs +0 -1272
  198. package/dist/eas-session-ledger-Cm0Gwq2F.mjs +0 -207
  199. package/dist/exec-DqKo9GKJ.mjs +0 -268
  200. package/dist/git-DDfFzkMm.mjs +0 -45
  201. package/dist/launch-verify-B6Q8hZP-.mjs +0 -428
  202. package/dist/logs-BGrEUaEs.mjs +0 -383
  203. package/dist/macos-Ch5VJmcS.mjs +0 -584
  204. package/dist/metro-gateway-CX9Xlkq2.mjs +0 -103
  205. package/dist/miss-reason-mKzZ4VvN.mjs +0 -238
  206. package/dist/native-android-D5qc3-_v.mjs +0 -226
  207. package/dist/native-gradle-inputs-BsLRrl7u.mjs +0 -349
  208. package/dist/ndjson-Du3j0AQ9.d.mts +0 -17
  209. package/dist/plan-C4TjdPsR.mjs +0 -260
  210. package/dist/prebuild-DYbucPXy.mjs +0 -212
  211. package/dist/preview-78a9kR_-.mjs +0 -1011
  212. package/dist/react-native-build-CtCccJtb.mjs +0 -239
  213. package/dist/react-native-doctor-CfRnFfjs.mjs +0 -78
  214. package/dist/remote-cache-OyOyVIS3.mjs +0 -526
  215. package/dist/result-CaJg5Iat.mjs +0 -186
  216. package/dist/server-expo-Bigriskl.mjs +0 -2
  217. package/dist/settings-BORNaaCO.mjs +0 -402
  218. package/dist/settings-vk3mJGSm.d.mts +0 -13
  219. package/dist/spawn-claims-DVS5RkMS.mjs +0 -72
  220. package/dist/stage-KNTQopqv.mjs +0 -157
  221. package/dist/start-BWqzODpb.mjs +0 -1116
  222. package/dist/stim-desktop-BzBN2Em2.mjs +0 -63
  223. package/dist/swiftpm-macos-DRNVJWlb.mjs +0 -119
  224. package/dist/tailnet-Dh-qiYzG.mjs +0 -265
  225. package/dist/toolchain-BFkAcstd.mjs +0 -2
  226. package/dist/toolchain-DxSsNssu.mjs +0 -1657
  227. package/dist/trigger-9_USgmxz.mjs +0 -2
  228. package/dist/web-Dq-i3wzJ.mjs +0 -2
  229. package/dist/worktree-DpLdCHga.mjs +0 -2
  230. package/dist/{api-run.d.mts → doctor-fingerprint.d.mts} +0 -0
  231. package/dist/{errors-DzkMYh8Q.mjs → errors-ccaPc4ox.mjs} +2 -2
@@ -1,1771 +0,0 @@
1
- import { _ as loadConfig$1, d as getConfigDir } from "./config-BYDxlMY3.mjs";
2
- import { r as debugLog, t as getExecutor } from "./exec-DqKo9GKJ.mjs";
3
- import { t as git } from "./git-DDfFzkMm.mjs";
4
- import { D as withDirLock } from "./workspace-state-DJt_MMvf.mjs";
5
- import { a as markApproved, c as pinnedEndpoint$1, i as lapsedLine, l as realIo, n as findPeer$1, o as parseMachine$1, r as isLapsed, s as pendingReport, t as endpoint, u as requestedLine, x as runCancellationSignal } from "./tailnet-Dh-qiYzG.mjs";
6
- import { r as throwIfCancelled } from "./cancellation-C_22VQTH.mjs";
7
- import { n as CCACHE_UNAVAILABLE } from "./ccache-zdKiYQ3E.mjs";
8
- import { l as toolchainMismatches, m as readRubyVersion, v as COMPILATION_CACHE_UNAVAILABLE } from "./toolchain-DxSsNssu.mjs";
9
- import { c as gitVisiblePaths, i as nativeGradleTransfer, l as nativeTransferManifest, m as manifestDigest, s as checkNativeTransferMembership, u as sourceManifest } from "./native-gradle-inputs-BsLRrl7u.mjs";
10
- import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, writeFileSync, writeSync } from "node:fs";
11
- import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
12
- import { OFFLOAD_MODES, acceptsShape, automaticMachineEnabled, buildMachinesFile, buildMachinesLock, deviceHostMachinesClaims, deviceHostMachinesFile, deviceHostMachinesLock, isJsonObject, parseMachine, readBuildMachines, readDeviceHostMachines, requireAutomaticMachine, runId, saturation, settingDefinition } from "@stim-cli/core/state";
13
- import { InvalidArgumentError } from "commander";
14
- import { releaseClaim, tryAcquireClaim } from "@stim-cli/core/ownership-claim";
15
- import { createHash } from "node:crypto";
16
- import { hostname } from "node:os";
17
- import { setTimeout as setTimeout$1 } from "node:timers/promises";
18
- import { WebSocket } from "ws";
19
- //#region src/offload/access-ticket.ts
20
- function readAccessTicket() {
21
- const ticket = process.env.STIM_ACCESS_TICKET?.trim();
22
- return ticket && /^[A-Za-z0-9_-]{43}$/.test(ticket) ? {
23
- ticket,
24
- ticketHash: createHash("sha256").update(ticket).digest("hex")
25
- } : void 0;
26
- }
27
- function readHostPermissions(value) {
28
- const host = value;
29
- return host && typeof host.name === "string" && typeof host.screenRecording === "boolean" && typeof host.accessibility === "boolean" ? {
30
- name: host.name,
31
- screenRecording: host.screenRecording,
32
- accessibility: host.accessibility
33
- } : void 0;
34
- }
35
- //#endregion
36
- //#region src/device-host/machines.ts
37
- function note$1(title, detail, fix = null) {
38
- return {
39
- code: "device-host-machine",
40
- level: "note",
41
- title,
42
- detail,
43
- fix
44
- };
45
- }
46
- const approval$1 = (machine, id) => `A person on ${machine} approves it with \`stim-server devices grant ${id} --device-host\`.`;
47
- function configuredMachines() {
48
- const remote = loadConfig$1()?.remote;
49
- if (remote !== void 0 && !isJsonObject(remote)) return null;
50
- const machines = remote?.machines;
51
- if (machines === void 0) return [];
52
- return acceptsShape(settingDefinition("remote.machines"), machines) ? machines : null;
53
- }
54
- /** The `remote.machines` entries this Mac asked for device-host access, in setting order; null when the setting is invalid. */
55
- function hostingMachines() {
56
- const configured = configuredMachines();
57
- if (configured === null) return null;
58
- try {
59
- const asked = readDeviceHostMachines();
60
- return configured.filter((machine) => asked.some((each) => each.machine === machine));
61
- } catch {
62
- return configured;
63
- }
64
- }
65
- function store(machines) {
66
- withDirLock(deviceHostMachinesLock(), () => {
67
- const file = deviceHostMachinesFile();
68
- const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
69
- try {
70
- writeFileSync(tmp, `${JSON.stringify({
71
- version: 1,
72
- machines
73
- }, null, 2)}\n`, { mode: 384 });
74
- renameSync(tmp, file);
75
- } finally {
76
- rmSync(tmp, { force: true });
77
- }
78
- }, { ensureParent: () => mkdirSync(getConfigDir(), { recursive: true }) });
79
- }
80
- async function request(machine, peer, port, deviceName, io) {
81
- const ticket = readAccessTicket();
82
- const reply = await io.hello(endpoint(peer, port), {
83
- request: "device-host",
84
- deviceName,
85
- ...ticket ? { setupTicket: ticket.ticket } : {}
86
- });
87
- if (!("result" in reply) || reply.result?.approval?.state !== "pending" || typeof reply.result.deviceToken !== "string" || !reply.result.deviceToken || typeof reply.result.device?.id !== "string" || !reply.result.device.id || typeof reply.result.approval.expiresAt !== "string" || !reply.result.approval.expiresAt) return {
88
- report: {
89
- machine,
90
- dnsName: peer.dnsName,
91
- state: "unreachable"
92
- },
93
- finding: note$1(`Could not ask ${machine} for hosting access`, "The worker did not return a pending request.")
94
- };
95
- const credential = {
96
- machine,
97
- nodeId: peer.nodeId,
98
- dnsName: peer.dnsName,
99
- deviceId: reply.result.device.id,
100
- deviceToken: reply.result.deviceToken,
101
- state: "pending",
102
- requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
103
- expiresAt: reply.result.approval.expiresAt,
104
- ...ticket ? { ticketHash: ticket.ticketHash } : {}
105
- };
106
- return {
107
- report: {
108
- machine,
109
- dnsName: peer.dnsName,
110
- state: "pending",
111
- deviceId: credential.deviceId,
112
- requestedAt: credential.requestedAt,
113
- expiresAt: credential.expiresAt
114
- },
115
- finding: note$1(`Asked ${machine} for hosting access`, `The request lapses at ${reply.result.approval.expiresAt}.`, approval$1(machine, credential.deviceId)),
116
- credential
117
- };
118
- }
119
- /** Reports the device-host approval of each remote Mac, separate from its build approval. Only --fix asks or forgets credentials; every token stays pinned. */
120
- async function inspectDeviceHostMachines({ fix }, io = realIo, entries = configuredMachines()) {
121
- if (entries === null) return {
122
- findings: [note$1("Invalid remote.machines setting", "Use a remote object with machines as an array of tailnet names. Existing hosting credentials and pinned nodes are preserved.", "Run `stim guide settings` and correct the setting before running doctor again.")],
123
- machines: []
124
- };
125
- const unavailable = (state, finding) => ({
126
- findings: [finding],
127
- machines: entries.map((machine) => ({
128
- machine,
129
- state
130
- }))
131
- });
132
- const unreadable = () => unavailable("credentials-unavailable", note$1("Hosting credentials are unreadable", `Stim preserves ${deviceHostMachinesFile()} and its pinned nodes.`, "Restore the existing file from a trusted backup or restore read access before running doctor again."));
133
- if (!entries.length) {
134
- if (!fix) return {
135
- findings: [],
136
- machines: []
137
- };
138
- try {
139
- if (!readDeviceHostMachines().length) return {
140
- findings: [],
141
- machines: []
142
- };
143
- } catch {
144
- return unreadable();
145
- }
146
- }
147
- const claim = tryAcquireClaim({
148
- root: deviceHostMachinesClaims(),
149
- mode: "exclusive",
150
- label: "device-host approval"
151
- });
152
- if (!claim.acquired) {
153
- releaseClaim(claim.pending);
154
- return unavailable("busy", note$1("Device-host approval is in use", "Another doctor run holds the hosting credential claim. Run doctor again after it finishes."));
155
- }
156
- try {
157
- let credentials;
158
- try {
159
- credentials = readDeviceHostMachines();
160
- } catch {
161
- return unreadable();
162
- }
163
- if (fix) {
164
- const kept = credentials.filter((each) => entries.includes(each.machine));
165
- if (kept.length !== credentials.length) {
166
- store(kept);
167
- credentials = kept;
168
- }
169
- }
170
- if (!entries.length) return {
171
- findings: [],
172
- machines: []
173
- };
174
- const status = io.status();
175
- if (!isJsonObject(status)) return unavailable("tailscale-off", note$1("Remote Macs are unreachable", "remote.machines names remote Macs, but Tailscale is not running."));
176
- const self = isJsonObject(status.Self) ? status.Self : {};
177
- const deviceName = (typeof self.HostName === "string" && self.HostName ? self.HostName : hostname()).replace(/[\p{Cc}\p{Cf}]/gu, "").trim().slice(0, 64) || "Mac";
178
- const inspected = {
179
- findings: [],
180
- machines: []
181
- };
182
- const namedNodes = /* @__PURE__ */ new Set();
183
- for (const machine of new Set(entries)) {
184
- const parsed = parseMachine$1(machine);
185
- if (!parsed) {
186
- inspected.machines.push({
187
- machine,
188
- state: "invalid"
189
- });
190
- inspected.findings.push(note$1(`Remote Mac ${machine} is not a tailnet name`, "Expected `name` or `name:port`."));
191
- continue;
192
- }
193
- const current = io.status();
194
- if (!isJsonObject(current)) {
195
- inspected.machines.push({
196
- machine,
197
- state: "tailscale-off"
198
- });
199
- inspected.findings.push(note$1(`Remote Mac ${machine} is unreachable`, "Tailscale stopped before this connection."));
200
- continue;
201
- }
202
- const peer = findPeer$1(current, parsed.name);
203
- if (peer === "missing" || peer === "ambiguous") {
204
- inspected.machines.push({
205
- machine,
206
- state: "not-on-tailnet"
207
- });
208
- inspected.findings.push(note$1(`Remote Mac ${machine} is not on this tailnet`, peer === "missing" ? "No peer has that name." : "Several peers match that name."));
209
- continue;
210
- }
211
- const nodeRoute = `${peer.nodeId}:${parsed.port}`;
212
- if (namedNodes.has(nodeRoute)) {
213
- inspected.machines.push({
214
- machine,
215
- dnsName: peer.dnsName,
216
- state: "invalid"
217
- });
218
- inspected.findings.push(note$1(`Remote Mac ${machine} repeats a named node`, "Name each tailnet node and serve port only once in remote.machines."));
219
- continue;
220
- }
221
- namedNodes.add(nodeRoute);
222
- const credential = credentials.find((each) => each.machine === machine);
223
- const known = {
224
- machine,
225
- dnsName: peer.dnsName,
226
- ...credential ? {
227
- deviceId: credential.deviceId,
228
- requestedAt: credential.requestedAt
229
- } : {}
230
- };
231
- if (credential && credential.nodeId !== peer.nodeId) {
232
- inspected.machines.push({
233
- ...known,
234
- state: "node-changed"
235
- });
236
- inspected.findings.push(note$1(`Remote Mac ${machine} is a different tailnet node`, "Stim refuses to connect or send the saved token.", `If that Mac was replaced, remove ${machine} from remote.machines, run \`stim doctor --fix\`, then add it back and run \`stim doctor --fix\` again to request approval on the new node.`));
237
- continue;
238
- }
239
- let ask = !credential && fix;
240
- if (credential) {
241
- const reply = await io.hello(endpoint(peer, parsed.port), { deviceToken: credential.deviceToken });
242
- if ("result" in reply && reply.result?.device?.id === credential.deviceId && Array.isArray(reply.result.capabilities) && reply.result.capabilities.includes("device-host")) {
243
- if (credential.state !== "approved") {
244
- credentials = credentials.map((each) => each.machine === machine ? markApproved(each) : each);
245
- store(credentials);
246
- }
247
- const host = readHostPermissions(reply.result.host);
248
- if (host && (host.screenRecording === false || host.accessibility === false)) {
249
- const panes = [...host.screenRecording === false ? ["Screen & System Audio Recording (Screen Recording on macOS 14)"] : [], ...host.accessibility === false ? ["Device Control and Data Access (Accessibility on macOS 26 and earlier)"] : []];
250
- const detail = [...host.screenRecording === false ? ["Viewing hosted macOS apps needs Screen Recording."] : [], ...host.accessibility === false ? ["Controlling them needs the control permission."] : []];
251
- inspected.findings.push(note$1(`Remote Mac ${machine} needs ${panes.join(" and ")} for ${host.name}`, detail.join(" "), `On ${machine}, approve ${host.name} in System Settings > Privacy & Security, or run \`stim-server service install\` there again to show the requests.`));
252
- }
253
- inspected.machines.push({
254
- ...known,
255
- state: "approved",
256
- ...host ? { host } : {}
257
- });
258
- continue;
259
- }
260
- const pending = "error" in reply && reply.error?.code === "approval-pending";
261
- const revoked = "error" in reply && reply.error?.code === "unauthorized";
262
- const ticket = fix && pending && credential.state !== "approved" ? readAccessTicket() : void 0;
263
- ask = fix && (revoked || !!ticket && credential.ticketHash !== ticket.ticketHash);
264
- if (pending && !ask) {
265
- inspected.machines.push({
266
- ...known,
267
- ...pendingReport(credential)
268
- });
269
- inspected.findings.push(note$1(`Remote Mac ${machine} has not approved this Mac for device hosting yet`, requestedLine(credential), approval$1(machine, credential.deviceId)));
270
- continue;
271
- }
272
- if (!ask) {
273
- const lapsed = revoked && isLapsed(credential);
274
- inspected.machines.push({
275
- ...known,
276
- state: lapsed ? "lapsed" : revoked ? "revoked" : "unreachable"
277
- });
278
- inspected.findings.push(note$1(lapsed ? `Hosting request to ${machine} lapsed` : `Remote Mac ${machine} ${revoked ? "no longer accepts this Mac for device hosting" : "did not confirm hosting access"}`, lapsed ? `${lapsedLine(machine, credential)} The saved token and pinned node are preserved.` : "The saved token and pinned node are preserved.", revoked ? "Run `stim doctor --fix` to ask again." : `Check stim-server and its tailnet serve route on ${machine}, then run doctor again.`));
279
- continue;
280
- }
281
- }
282
- if (ask) {
283
- const result = await request(machine, peer, parsed.port, deviceName, io);
284
- if (result.credential) {
285
- credentials = [...credentials.filter((each) => each.machine !== machine), result.credential];
286
- store(credentials);
287
- }
288
- inspected.machines.push(result.report);
289
- inspected.findings.push(result.finding);
290
- } else {
291
- inspected.machines.push({
292
- ...known,
293
- state: "not-asked"
294
- });
295
- inspected.findings.push(note$1(`Remote Mac ${machine} has not approved this Mac for device hosting`, "This Mac has not asked for hosting access.", "Run `stim doctor --fix` to ask."));
296
- }
297
- }
298
- return inspected;
299
- } finally {
300
- releaseClaim(claim.acquired);
301
- }
302
- }
303
- //#endregion
304
- //#region src/offload/build-machines.ts
305
- /** The `remote.machines` entries that this Mac asked for build access, in that order. */
306
- function pairedMachines(entries = configuredMachines() ?? []) {
307
- const credentials = readBuildMachines();
308
- return entries.flatMap((entry) => credentials.filter((each) => each.machine === entry));
309
- }
310
- function updateCredentials(change) {
311
- const dir = getConfigDir();
312
- withDirLock(buildMachinesLock(), () => {
313
- const file = buildMachinesFile();
314
- const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
315
- writeFileSync(tmp, `${JSON.stringify({
316
- version: 1,
317
- machines: change(readBuildMachines())
318
- }, null, 2)}\n`, { mode: 384 });
319
- try {
320
- renameSync(tmp, file);
321
- } catch (error) {
322
- rmSync(tmp, { force: true });
323
- throw error;
324
- }
325
- }, { ensureParent: () => mkdirSync(dir, { recursive: true }) });
326
- }
327
- function note(title, detail, fix = null) {
328
- return {
329
- code: "build-machine",
330
- level: "note",
331
- title,
332
- detail,
333
- fix
334
- };
335
- }
336
- const approval = (entry, deviceId) => `A person on ${entry} approves it with \`stim-server devices grant ${deviceId} --build\`.`;
337
- const PROBLEM_TITLES = {
338
- unreachable: "does not answer a build offer",
339
- checkout: "cannot build an app outside a git checkout",
340
- "stim-build": "runs another Stim build",
341
- arch: "has another CPU architecture",
342
- xcode: "has another Xcode",
343
- "macos-sdk": "has another macOS SDK",
344
- "simulator-sdk": "has another simulator SDK",
345
- cocoapods: "has another CocoaPods",
346
- bundler: "has no Bundler",
347
- runtime: "has no simulator for this project's iOS runtime",
348
- jdk: "runs another JDK major version",
349
- "android-sdk": "has no Android SDK",
350
- ndk: "lacks this project's NDK",
351
- "build-tools": "lacks this project's Android build-tools",
352
- "compile-sdk": "lacks this project's Android compile platform",
353
- disk: "is low on disk",
354
- busy: "is too busy to take builds"
355
- };
356
- function problemFix(code, entry) {
357
- switch (code) {
358
- case "unreachable": return `Check that stim-server runs on ${entry} and that its tailscale serve route answers, then run \`stim doctor\` again. To keep it running there, run \`stim-server service install --serve\` on ${entry}.`;
359
- case "checkout": return "Offload syncs the files git lists; run Stim from a git checkout of the app.";
360
- case "stim-build": return `Update stim-server on ${entry} to the same Stim build as this Mac: there, run \`stim-server service update --release <version>\` for this Mac's release, or \`--from <dir>\` with the packed packages of this Mac's checkout.`;
361
- case "arch": return "Use a remote Mac with the same CPU architecture as this Mac.";
362
- case "xcode":
363
- case "simulator-sdk":
364
- case "macos-sdk": return `Install and select the same Xcode on ${entry} and this Mac (\`xcode-select -p\` on each).`;
365
- case "cocoapods": return `Install the same CocoaPods version on ${entry}, on the PATH its stim-server's login shell sets.`;
366
- case "bundler": return `Install Bundler (\`gem install bundler\`) on ${entry}, on the PATH its stim-server's login shell sets.`;
367
- case "runtime": return `Install that iOS simulator runtime on ${entry} with at least one iPhone simulator, or set ios.runtime here to one ${entry} has.`;
368
- case "jdk": return `Start stim-server on ${entry} with JAVA_HOME at a JDK of the same major version as this Mac's.`;
369
- case "android-sdk":
370
- case "ndk":
371
- case "build-tools":
372
- case "compile-sdk": return `Install the missing package with sdkmanager in the Android SDK stim-server on ${entry} uses (its ANDROID_HOME, else ~/Library/Android/sdk).`;
373
- case "disk": return `Free space on ${entry}'s worker root (server.workerRoot there).`;
374
- case "busy": return `Builds stay on this Mac until ${entry} has capacity. On ${entry}, server.maxLoadPerCore sets the load it accepts and concurrency.maxBuilds how many of its own builds it runs; it takes one offloaded build at a time.`;
375
- }
376
- }
377
- const TRANSIENT = /* @__PURE__ */ new Set(["unreachable", "busy"]);
378
- function offloadFindings(entry, problems) {
379
- return problems.map((problem) => ({
380
- code: `build-machine-${problem.code}`,
381
- level: TRANSIENT.has(problem.code) ? "note" : "cost",
382
- title: `Remote Mac ${entry} ${PROBLEM_TITLES[problem.code]}`,
383
- detail: `Builds fall back to this Mac: ${problem.reason}.`,
384
- fix: problemFix(problem.code, entry)
385
- }));
386
- }
387
- async function requestAccess(entry, peer, port, deviceName, io) {
388
- const ticket = readAccessTicket();
389
- const reply = await io.hello(endpoint(peer, port), {
390
- request: "build",
391
- deviceName,
392
- ...ticket ? { setupTicket: ticket.ticket } : {}
393
- });
394
- if (!("result" in reply) || !reply.result.approval || !reply.result.deviceToken) {
395
- const reason = "failed" in reply ? reply.failed : "error" in reply ? reply.error.message : "it granted no request";
396
- return {
397
- report: {
398
- machine: entry,
399
- state: "unreachable",
400
- dnsName: peer.dnsName
401
- },
402
- finding: note(`Could not ask ${entry} for build access`, reason)
403
- };
404
- }
405
- const credential = {
406
- machine: entry,
407
- nodeId: peer.nodeId,
408
- dnsName: peer.dnsName,
409
- deviceId: reply.result.device.id,
410
- deviceToken: reply.result.deviceToken,
411
- state: "pending",
412
- requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
413
- expiresAt: reply.result.approval.expiresAt,
414
- ...ticket ? { ticketHash: ticket.ticketHash } : {}
415
- };
416
- updateCredentials((credentials) => [...credentials.filter((each) => each.machine !== entry), credential]);
417
- return {
418
- report: {
419
- machine: entry,
420
- state: "pending",
421
- dnsName: peer.dnsName,
422
- deviceId: credential.deviceId,
423
- requestedAt: credential.requestedAt,
424
- expiresAt: credential.expiresAt
425
- },
426
- finding: note(`Asked ${entry} for build access`, `The request lapses at ${reply.result.approval.expiresAt}.`, approval(entry, credential.deviceId))
427
- };
428
- }
429
- async function inspectMachine(entry, status, deviceName, fix, io, check) {
430
- const parsed = parseMachine$1(entry);
431
- if (!parsed) return {
432
- report: {
433
- machine: entry,
434
- state: "invalid"
435
- },
436
- finding: note(`Remote Mac ${entry} is not a tailnet name`, "Expected `name` or `name:port`.")
437
- };
438
- const peer = findPeer$1(status, parsed.name);
439
- if (peer === "missing" || peer === "ambiguous") {
440
- const detail = peer === "missing" ? "No peer on this tailnet has that name." : "Several peers match that name.";
441
- return {
442
- report: {
443
- machine: entry,
444
- state: "not-on-tailnet"
445
- },
446
- finding: note(`Remote Mac ${entry} is not on this tailnet`, detail)
447
- };
448
- }
449
- const credential = readBuildMachines().find((each) => each.machine === entry);
450
- const known = {
451
- machine: entry,
452
- dnsName: peer.dnsName
453
- };
454
- if (credential && credential.nodeId !== peer.nodeId) return {
455
- report: {
456
- ...known,
457
- state: "node-changed",
458
- deviceId: credential.deviceId,
459
- requestedAt: credential.requestedAt
460
- },
461
- finding: note(`Remote Mac ${entry} is a different tailnet node`, `This Mac paired with node ${credential.nodeId}, but ${peer.dnsName} is now node ${peer.nodeId}. Stim does not connect to it.`, `If that Mac was replaced, remove ${entry} from remote.machines, run \`stim doctor --fix\` to forget the old pairing, then add it back and run \`stim doctor --fix\` again.`)
462
- };
463
- if (!credential) {
464
- if (fix) return requestAccess(entry, peer, parsed.port, deviceName, io);
465
- return {
466
- report: {
467
- ...known,
468
- state: "not-asked"
469
- },
470
- finding: note(`Remote Mac ${entry} has not approved this Mac for builds`, "This Mac has not asked it for build access.", "Run `stim doctor --fix` to ask.")
471
- };
472
- }
473
- const paired = {
474
- ...known,
475
- deviceId: credential.deviceId,
476
- requestedAt: credential.requestedAt
477
- };
478
- const reply = await io.hello(endpoint(peer, parsed.port), { deviceToken: credential.deviceToken });
479
- if ("result" in reply && reply.result.capabilities.includes("build")) {
480
- const permissions = readHostPermissions(reply.result.host);
481
- const host = permissions ? { host: permissions } : {};
482
- if (credential.state !== "approved") updateCredentials((credentials) => credentials.map((each) => each.machine === entry ? markApproved(each) : each));
483
- if (!check) return {
484
- report: {
485
- ...paired,
486
- state: "approved",
487
- ...host
488
- },
489
- finding: null
490
- };
491
- const { capacity, problems } = await check({
492
- ...credential,
493
- state: "approved"
494
- });
495
- return {
496
- report: {
497
- ...paired,
498
- state: "approved",
499
- ...host,
500
- offloadable: problems.length === 0,
501
- reasons: problems.map((problem) => problem.reason),
502
- problems,
503
- ...capacity ? { capacity } : {}
504
- },
505
- finding: null,
506
- extra: offloadFindings(entry, problems)
507
- };
508
- }
509
- if ("error" in reply && reply.error.code === "approval-pending") {
510
- const ticket = fix && credential.state !== "approved" ? readAccessTicket() : void 0;
511
- if (ticket && credential.ticketHash !== ticket.ticketHash) return requestAccess(entry, peer, parsed.port, deviceName, io);
512
- return {
513
- report: {
514
- ...paired,
515
- ...pendingReport(credential)
516
- },
517
- finding: note(`Remote Mac ${entry} has not approved this Mac for builds yet`, requestedLine(credential), approval(entry, credential.deviceId))
518
- };
519
- }
520
- if ("error" in reply && reply.error.code === "unauthorized") {
521
- if (fix) return requestAccess(entry, peer, parsed.port, deviceName, io);
522
- if (isLapsed(credential)) return {
523
- report: {
524
- ...paired,
525
- state: "lapsed"
526
- },
527
- finding: note(`Build request to ${entry} lapsed`, lapsedLine(entry, credential), "Run `stim doctor --fix` to ask again.")
528
- };
529
- return {
530
- report: {
531
- ...paired,
532
- state: "revoked"
533
- },
534
- finding: note(`Remote Mac ${entry} no longer accepts this Mac for builds`, `${reply.error.message} It was revoked or denied.`, "Run `stim doctor --fix` to ask again.")
535
- };
536
- }
537
- const reason = "failed" in reply ? reply.failed : "error" in reply ? reply.error.message : "unexpected reply";
538
- return {
539
- report: {
540
- ...paired,
541
- state: "unreachable"
542
- },
543
- finding: note(`Could not reach remote Mac ${entry}`, reason, `Check that stim-server runs on ${entry}. To keep it running there, run \`stim-server service install --serve\` on ${entry}.`)
544
- };
545
- }
546
- /**
547
- * Doctor findings and a report for each `remote.machines` entry. The worker's node is pinned when access is
548
- * requested; a later connection goes only to the current MagicDNS name of that same node, checked before the token
549
- * is sent. With `fix`, requests access from a named machine this Mac holds no pairing for, requests again when the
550
- * pinned node forgot this Mac or, with `STIM_ACCESS_TICKET` set, when a pending request carries another ticket, and forgets pairings of machines no longer named. With `check`, each approved
551
- * machine is asked for one build offer and reports every reason it would not take this project's build.
552
- */
553
- async function inspectBuildMachines({ fix, check = null }, io = realIo, entries = configuredMachines()) {
554
- if (entries === null) return {
555
- findings: [note("Invalid remote.machines setting", "Use a remote object with machines as an array of tailnet names. Existing credentials and pinned nodes are preserved.", "Run `stim guide settings` and correct the setting before running doctor again.")],
556
- machines: []
557
- };
558
- const unnamed = (each) => !entries.includes(each.machine);
559
- if (fix && readBuildMachines().some(unnamed)) updateCredentials((credentials) => credentials.filter((each) => !unnamed(each)));
560
- if (entries.length === 0) return {
561
- findings: [],
562
- machines: []
563
- };
564
- const status = io.status();
565
- if (!isJsonObject(status)) return {
566
- findings: [note("Remote Macs are unreachable", "remote.machines names remote Macs, but Tailscale is not running.")],
567
- machines: entries.map((machine) => ({
568
- machine,
569
- state: "tailscale-off"
570
- }))
571
- };
572
- const self = isJsonObject(status.Self) ? status.Self : {};
573
- const deviceName = (typeof self.HostName === "string" && self.HostName ? self.HostName : hostname()).replace(/[\p{Cc}\p{Cf}]/gu, "").trim().slice(0, 64) || "Mac";
574
- const inspection = {
575
- findings: [],
576
- machines: []
577
- };
578
- const inspected = await Promise.all(entries.map((entry) => inspectMachine(entry, status, deviceName, fix, io, check)));
579
- for (const { report, finding, extra = [] } of inspected) {
580
- inspection.machines.push(report);
581
- if (finding) inspection.findings.push(finding);
582
- inspection.findings.push(...extra);
583
- }
584
- return inspection;
585
- }
586
- //#endregion
587
- //#region src/offload/selection.ts
588
- var OffloadRefusal = class extends Error {
589
- code = "STIM_OFFLOAD_REFUSED";
590
- remedy = "Run stim doctor --fix to ask for build access if not paired. On the worker, a person finds the id with stim-server devices and approves it with stim-server devices grant <id> --build. Check stim settings get remote.machines, or rerun with --remote-build auto or --remote-build local.";
591
- constructor(machine, reason) {
592
- super(reason.startsWith(`${machine}: `) ? reason : `${machine}: ${reason}`);
593
- }
594
- };
595
- function resolveBuildMachine(flag, env, setting) {
596
- const selected = flag ?? (env?.trim() || void 0) ?? setting ?? "auto";
597
- if (typeof selected !== "string" || !selected.trim() || !parseMachine(selected)) throw Object.assign(/* @__PURE__ */ new Error("--remote-build / remote.build must be auto, local, or a tailnet machine name."), { code: "STIM_BAD_ARG" });
598
- const trimmed = selected.trim();
599
- const reserved = trimmed.toLowerCase();
600
- return reserved === "auto" || reserved === "local" ? reserved : trimmed;
601
- }
602
- function parseBuildMachineOption(value) {
603
- try {
604
- return resolveBuildMachine(value);
605
- } catch (error) {
606
- throw new InvalidArgumentError(error.message);
607
- }
608
- }
609
- function requireLocalBuild(selected) {
610
- if (selected === "auto" && !automaticMachineEnabled("build", "local")) throw new OffloadRefusal("auto", "local is disabled in remote.buildPoolDisabled; enable it or select a machine explicitly");
611
- }
612
- function namedBuildMachine(selected) {
613
- return selected !== "auto" && selected !== "local";
614
- }
615
- function requireConfiguredMachine(selected, machines) {
616
- if (!namedBuildMachine(selected)) return selected;
617
- const parsed = parseMachine(selected);
618
- const entries = Array.isArray(machines) ? machines.filter((entry) => typeof entry === "string") : [];
619
- const match = entries.find((entry) => {
620
- const candidate = parseMachine(entry);
621
- return candidate?.name === parsed.name && candidate.port === parsed.port;
622
- });
623
- if (!match) throw new OffloadRefusal(selected, `not listed in remote.machines (configured: ${entries.join(", ") || "none"})`);
624
- return match;
625
- }
626
- const settingSource = (flag, env, setting) => flag !== void 0 ? "flag" : env?.trim() ? "env" : setting !== void 0 ? "setting" : "default";
627
- let buildMachineFlag;
628
- /** The settings that decide where a build runs, with the layer each value came from, for this run's `--remote-build`. */
629
- function buildPlacementSettings(selected) {
630
- const flag = buildMachineFlag;
631
- const remote = loadConfig$1()?.remote;
632
- const rawMode = process.env.STIM_REMOTE_BUILD_MODE || remote?.buildMode;
633
- return [{
634
- key: "remote.build",
635
- value: selected,
636
- from: settingSource(flag, process.env.STIM_REMOTE_BUILD, remote?.build)
637
- }, {
638
- key: "remote.buildMode",
639
- value: selected === "local" ? "off" : OFFLOAD_MODES.includes(rawMode) ? rawMode : "auto",
640
- from: settingSource(void 0, process.env.STIM_REMOTE_BUILD_MODE, remote?.buildMode)
641
- }];
642
- }
643
- function resolveBuildPlacement(flag) {
644
- const remote = loadConfig$1()?.remote;
645
- buildMachineFlag = flag;
646
- let selected = "auto";
647
- try {
648
- selected = resolveBuildMachine(flag, process.env.STIM_REMOTE_BUILD, remote?.build);
649
- selected = requireConfiguredMachine(selected, remote?.machines);
650
- if (namedBuildMachine(selected) && pairedMachines([selected]).length === 0) throw new OffloadRefusal(selected, "no remote Mac is paired");
651
- return { selected };
652
- } catch (error) {
653
- return {
654
- selected,
655
- failure: {
656
- code: error.code,
657
- message: error.message,
658
- remedy: error instanceof OffloadRefusal ? error.remedy : "Pass --remote-build auto, local, or an entry from stim settings get remote.machines."
659
- }
660
- };
661
- }
662
- }
663
- //#endregion
664
- //#region src/remote-log.ts
665
- let sink = null;
666
- /** Where this run's failed requests to another Mac are written: its workspace log, so `stim logs` shows them. */
667
- function setRemoteLogSink(next) {
668
- sink = next;
669
- }
670
- function reportRemoteFailure(event, fields) {
671
- try {
672
- sink?.({
673
- src: "build",
674
- level: "warn",
675
- event,
676
- ...fields
677
- });
678
- } catch {}
679
- }
680
- //#endregion
681
- //#region src/offload/client.ts
682
- const CONNECT_TIMEOUT_MS = 1e4;
683
- const OFFER_TIMEOUT_MS = 2e4;
684
- const REQUEST_TIMEOUT_MS = 12e4;
685
- const JOB_TIMEOUT_MS = 39e5;
686
- const PING_MS = 15e3;
687
- const SILENT_MS = 6e4;
688
- const CLOSE_TIMEOUT_MS = 2e3;
689
- const CLOSE_ABNORMAL = 1006;
690
- const TURNED_AWAY = /* @__PURE__ */ new Set([
691
- "unauthorized",
692
- "forbidden",
693
- "approval-pending",
694
- "protocol-unsupported"
695
- ]);
696
- const RESUME_WINDOW_MS = 18e4;
697
- const RESUME_DELAY_MS = 2e3;
698
- const RESUME_MAX_DELAY_MS = 15e3;
699
- const PAGE_BYTES = 49152;
700
- const CHUNK_BYTES = 61440;
701
- const MAX_BUFFERED = 8388608;
702
- const DIGEST_BYTES = 32;
703
- function offloadMode(env = process.env) {
704
- const raw = env.STIM_REMOTE_BUILD_MODE || loadConfig$1()?.remote?.buildMode;
705
- return OFFLOAD_MODES.includes(raw) ? raw : "auto";
706
- }
707
- function buildPlacementCandidates(selected) {
708
- const config = loadConfig$1();
709
- return {
710
- localEnabled: selected !== "auto" || automaticMachineEnabled("build", "local", config),
711
- mode: selected === "local" ? "off" : offloadMode(),
712
- machines: selected === "local" ? [] : namedBuildMachine(selected) ? pairedMachines([selected]).slice(0, 1) : pairedMachines().filter((entry) => automaticMachineEnabled("build", entry.machine, config))
713
- };
714
- }
715
- function remotePhaseText(name, msg, machine) {
716
- const text = msg.trim();
717
- return `${text.startsWith(`${name} `) ? text.slice(name.length).trimStart() : text} (on ${machine})`;
718
- }
719
- const plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
720
- function capacityText(capacity) {
721
- const builds = capacity.maxBuilds > 0 ? `${capacity.builds} of ${capacity.maxBuilds} build slots busy` : plural(capacity.builds, "build");
722
- return `load ${capacity.loadPerCore}/core, ${builds}`;
723
- }
724
- function placementLoad(choice) {
725
- const { loadPerCore, builds, maxBuilds } = choice.offer.capacity;
726
- if (typeof loadPerCore !== "number" || typeof builds !== "number") return "";
727
- return `; ${capacityText({
728
- loadPerCore,
729
- builds,
730
- maxBuilds: maxBuilds ?? 0
731
- })} there`;
732
- }
733
- /**
734
- * Whether a build may go to a remote Mac at all, before any machine is asked. `auto` keeps the build here while
735
- * this Mac has a free `concurrency.maxBuilds` slot and its load per core is under `server.maxLoadPerCore`.
736
- * A named selection ignores local capacity and mode, and refuses an unsupported build or missing pairing.
737
- */
738
- function offloadPlacement({ mode, machines, here, unsupported, localEnabled = true, selected = "auto" }) {
739
- if (namedBuildMachine(selected)) {
740
- if (unsupported) throw new OffloadRefusal(selected, `${unsupported}, so a named remote Mac cannot take it`);
741
- if (machines === 0) throw new OffloadRefusal(selected, "no remote Mac is paired");
742
- return {
743
- offload: true,
744
- code: "named",
745
- reason: `selected with --remote-build ${selected}`
746
- };
747
- }
748
- if (selected === "local") return {
749
- offload: false,
750
- code: "local-selected",
751
- reason: "--remote-build local"
752
- };
753
- if (!localEnabled) {
754
- const reason = mode === "off" ? "remote.buildMode is off" : unsupported ?? (machines === 0 ? "no enabled remote Mac is paired" : null);
755
- if (reason) throw new OffloadRefusal("auto", `${reason}; local is disabled in remote.buildPoolDisabled`);
756
- return {
757
- offload: true,
758
- code: "local-disabled",
759
- reason: "local is disabled in remote.buildPoolDisabled"
760
- };
761
- }
762
- if (mode === "off") return {
763
- offload: false,
764
- code: "mode-off",
765
- reason: "remote.buildMode is off"
766
- };
767
- if (machines === 0) return {
768
- offload: false,
769
- code: "no-remote-mac",
770
- reason: "no remote Mac is paired"
771
- };
772
- if (unsupported) return {
773
- offload: false,
774
- code: "unsupported",
775
- reason: unsupported
776
- };
777
- if (mode === "force") return {
778
- offload: true,
779
- code: "forced",
780
- reason: "remote.buildMode is force"
781
- };
782
- const busy = saturation(here);
783
- return busy ? {
784
- offload: true,
785
- code: "this-mac-busy",
786
- reason: `this Mac is busy: ${busy} (${capacityText(here)})`
787
- } : {
788
- offload: false,
789
- code: "this-mac-free",
790
- reason: `${capacityText(here)} here`
791
- };
792
- }
793
- const gb = (bytes) => (bytes / 1024 ** 3).toFixed(1);
794
- /** Every reason a machine's offer cannot take this build, in either mode; empty when it can. */
795
- function offerProblems(offer, target) {
796
- const problems = toolchainMismatches(target, offer.toolchain);
797
- const capacity = offer.capacity;
798
- const { running, max, diskFreeBytes, minDiskFreeBytes, declined } = capacity;
799
- if (diskFreeBytes !== null && diskFreeBytes < minDiskFreeBytes) problems.push({
800
- code: "disk",
801
- reason: `${gb(diskFreeBytes)} GB free, needs ${gb(minDiskFreeBytes)} GB`
802
- });
803
- else if (declined || declined === void 0 && running >= max) {
804
- const why = declined ?? `already running ${running} offloaded build(s), its limit`;
805
- const load = typeof capacity.loadPerCore === "number" && typeof capacity.builds === "number" ? `; ${capacityText({
806
- loadPerCore: capacity.loadPerCore,
807
- builds: capacity.builds,
808
- maxBuilds: capacity.maxBuilds ?? 0
809
- })}` : "";
810
- problems.push({
811
- code: "busy",
812
- reason: `busy (${why}${load})`
813
- });
814
- }
815
- return problems;
816
- }
817
- /**
818
- * The asked machines that can take the build, best first, and one reason per machine that cannot. A machine with any
819
- * `offerProblems` never takes it. In `auto`, a machine must also be expected to build faster than this Mac: while
820
- * every local build slot is busy any machine that accepts will do, otherwise its load per core must be lower than
821
- * this Mac's. A named selection considers only that machine and skips this local-capacity gating.
822
- * A machine that reports no load (a stim-server older than capacity) takes an `auto` build only while
823
- * every local slot is busy. The rest rank warmest first, then least loaded.
824
- */
825
- function pickOffer({ mode, here, offers, target, localEnabled = true, selected = "auto" }) {
826
- const slotsFull = here.maxBuilds > 0 && here.builds >= here.maxBuilds;
827
- const reasons = [];
828
- const candidates = [];
829
- const skip = (machine, code, msg, detail) => {
830
- reasons.push(`${machine}: ${msg}`);
831
- candidates.push({
832
- machine,
833
- code,
834
- msg,
835
- ...detail ? { detail } : {}
836
- });
837
- };
838
- const ranked = [];
839
- offers.forEach(({ machine, offer, failure }, index) => {
840
- if (namedBuildMachine(selected) && machine !== selected) return;
841
- if (!offer) return skip(machine, "unreachable", failure ?? "no offer");
842
- const problems = offerProblems(offer, target);
843
- if (problems.length) return skip(machine, problemCode(problems[0]), problems.map((problem) => problem.reason).join("; "), problems.map((problem) => problem.code));
844
- const load = offer.capacity.loadPerCore;
845
- if (selected === "auto" && localEnabled && mode === "auto" && !slotsFull) {
846
- if (typeof load !== "number") return skip(machine, "load", "capacity unknown (older stim-server) while this Mac has a free slot");
847
- if (load >= here.loadPerCore) return skip(machine, "load", `no less loaded (load ${load}/core there, ${here.loadPerCore}/core here)`);
848
- }
849
- const score = Number(offer.warm.checkout) + Number(offer.warm.dependencies) + Number(offer.warm.build);
850
- ranked.push({
851
- index,
852
- score,
853
- load: typeof load === "number" ? load : Number.POSITIVE_INFINITY
854
- });
855
- candidates.push({
856
- machine,
857
- code: "accepted",
858
- msg: typeof load === "number" ? `can take the build (load ${load}/core there)` : "can take the build"
859
- });
860
- });
861
- ranked.sort((a, b) => b.score - a.score || a.load - b.load);
862
- return {
863
- order: ranked.map((each) => each.index),
864
- reasons,
865
- candidates
866
- };
867
- }
868
- function problemCode({ code }) {
869
- return code === "disk" || code === "busy" || code === "unreachable" ? code : code === "runtime" ? "no-matching-device" : "version-mismatch";
870
- }
871
- const openSockets = /* @__PURE__ */ new Set();
872
- let closingOnExit = false;
873
- /**
874
- * A close frame on every connection still open when this process exits, as on an interrupt, so the machine cancels
875
- * its builds instead of keeping them for a new connection.
876
- */
877
- function closeOnExit(socket) {
878
- if (!closingOnExit) {
879
- closingOnExit = true;
880
- process.once("exit", () => {
881
- for (const each of openSockets) each.close(1e3);
882
- });
883
- }
884
- openSockets.add(socket);
885
- socket.once("close", () => openSockets.delete(socket));
886
- }
887
- /**
888
- * One authenticated connection to another Mac's stim-server, for build offload or device hosting. Closing it cancels
889
- * its build jobs there; a connection that drops, or that stays silent for `SILENT_MS` after `watch()`, leaves them
890
- * running for a while so a new connection can `build.attach` to them.
891
- */
892
- var BuildConnection = class BuildConnection {
893
- nextId = 2;
894
- pending = /* @__PURE__ */ new Map();
895
- progress = null;
896
- binary = null;
897
- closed = null;
898
- heard = Date.now();
899
- watched = false;
900
- ended = false;
901
- keepalive;
902
- socket;
903
- features;
904
- constructor(socket, features) {
905
- this.socket = socket;
906
- this.features = features;
907
- closeOnExit(socket);
908
- this.keepalive = setInterval(() => {
909
- if (this.watched && Date.now() - this.heard > SILENT_MS) return void socket.terminate();
910
- socket.ping();
911
- }, PING_MS);
912
- this.keepalive.unref();
913
- socket.on("pong", () => this.heard = Date.now());
914
- socket.on("message", (data, isBinary) => {
915
- this.heard = Date.now();
916
- const frame = Buffer.isBuffer(data) ? data : Buffer.concat(Array.isArray(data) ? data : [Buffer.from(data)]);
917
- if (isBinary) return this.binary?.(frame);
918
- let message;
919
- try {
920
- message = JSON.parse(frame.toString("utf8"));
921
- } catch {
922
- return;
923
- }
924
- if (!isJsonObject(message)) return;
925
- if (message.event === "build.progress") return this.progress?.(message);
926
- const resolve = typeof message.id === "number" ? this.pending.get(message.id) : void 0;
927
- if (resolve) {
928
- this.pending.delete(message.id);
929
- resolve(message);
930
- }
931
- });
932
- socket.on("close", (code, reason) => {
933
- clearInterval(this.keepalive);
934
- this.closed = `the connection closed (${code}${reason.length ? ` ${reason.toString()}` : ""})`;
935
- for (const resolve of this.pending.values()) resolve({ error: {
936
- code: "closed",
937
- message: this.closed
938
- } });
939
- this.pending.clear();
940
- const dropped = code === CLOSE_ABNORMAL;
941
- this.progress?.({
942
- job: "",
943
- outcome: {
944
- ok: false,
945
- code: dropped ? "dropped" : "closed",
946
- message: this.closed
947
- }
948
- });
949
- });
950
- socket.on("error", () => {});
951
- }
952
- /** Resolves why it could not connect; `refused` when the machine answered and turned this Mac away. */
953
- static async open(target, token, timeoutMs, capability = "build") {
954
- const started = performance.now();
955
- const result = await BuildConnection.connect(target, token, timeoutMs, capability);
956
- const ms = Math.round(performance.now() - started);
957
- debugLog.log("remote_connect", {
958
- host: target.host,
959
- capability,
960
- ms,
961
- timeoutMs,
962
- ...result instanceof BuildConnection ? { ok: true } : {
963
- ok: false,
964
- failure: result.failure,
965
- code: result.code
966
- }
967
- });
968
- if (!(result instanceof BuildConnection)) reportRemoteFailure("remote_connect_failed", {
969
- host: target.host,
970
- capability,
971
- ms,
972
- timeoutMs,
973
- msg: result.failure,
974
- code: result.code
975
- });
976
- return result;
977
- }
978
- static connect(target, token, timeoutMs, capability) {
979
- return new Promise((resolve) => {
980
- const options = {
981
- handshakeTimeout: timeoutMs,
982
- servername: target.servername,
983
- headers: { Host: target.host }
984
- };
985
- const socket = new WebSocket(target.url, options);
986
- const fail = (reason, refused = false, code) => {
987
- clearTimeout(timer);
988
- socket.removeAllListeners();
989
- socket.on("error", () => {});
990
- socket.terminate();
991
- resolve({
992
- failure: reason,
993
- refused,
994
- ...code ? { code } : {}
995
- });
996
- };
997
- const timer = setTimeout(() => fail("no reply in time"), timeoutMs);
998
- socket.once("error", (error) => fail(/Unexpected server response: 502/.test(error.message) ? "no stim-server answers behind its tailscale serve route (HTTP 502)" : error.message));
999
- socket.once("close", () => fail("the connection closed before hello"));
1000
- socket.once("open", () => {
1001
- socket.send(JSON.stringify({
1002
- id: 1,
1003
- method: "hello",
1004
- params: {
1005
- protocol: 1,
1006
- client: {
1007
- name: "stim",
1008
- version: "1",
1009
- runId: runId()
1010
- },
1011
- auth: { deviceToken: token }
1012
- }
1013
- }));
1014
- });
1015
- socket.once("message", (data) => {
1016
- clearTimeout(timer);
1017
- socket.removeAllListeners();
1018
- let reply;
1019
- try {
1020
- reply = JSON.parse(String(data));
1021
- } catch {
1022
- return fail("the reply was not a hello result");
1023
- }
1024
- if (isJsonObject(reply) && isJsonObject(reply.error)) return fail(String(reply.error.message), TURNED_AWAY.has(String(reply.error.code)), String(reply.error.code));
1025
- const capabilities = isJsonObject(reply) && isJsonObject(reply.result) ? reply.result.capabilities : null;
1026
- if (!Array.isArray(capabilities) || !capabilities.includes(capability)) return fail(`it has not granted this Mac ${capability === "build" ? "build" : "hosting"} access`, true, "forbidden");
1027
- const features = isJsonObject(reply) && isJsonObject(reply.result) ? reply.result.features : null;
1028
- resolve(new BuildConnection(socket, Array.isArray(features) ? features.filter((each) => typeof each === "string") : []));
1029
- });
1030
- });
1031
- }
1032
- supports(feature) {
1033
- return this.features.includes(feature);
1034
- }
1035
- request(method, params, timeoutMs = REQUEST_TIMEOUT_MS) {
1036
- if (this.closed) return Promise.resolve({ error: {
1037
- code: "closed",
1038
- message: this.closed
1039
- } });
1040
- const id = this.nextId++;
1041
- const started = performance.now();
1042
- const done = (reply) => {
1043
- const ms = Math.round(performance.now() - started);
1044
- debugLog.log("remote_request", {
1045
- method,
1046
- ms,
1047
- ..."error" in reply ? {
1048
- ok: false,
1049
- code: reply.error.code
1050
- } : { ok: true }
1051
- });
1052
- if ("error" in reply) reportRemoteFailure("remote_request_failed", {
1053
- method,
1054
- ms,
1055
- code: reply.error.code,
1056
- msg: reply.error.message
1057
- });
1058
- return reply;
1059
- };
1060
- return new Promise((resolve) => {
1061
- const timer = setTimeout(() => {
1062
- this.pending.delete(id);
1063
- resolve(done({ error: {
1064
- code: "timeout",
1065
- message: `${method} got no reply in ${timeoutMs / 1e3} s`
1066
- } }));
1067
- }, timeoutMs);
1068
- this.pending.set(id, (reply) => {
1069
- clearTimeout(timer);
1070
- resolve(done(reply));
1071
- });
1072
- this.socket.send(JSON.stringify({
1073
- id,
1074
- method,
1075
- params
1076
- }));
1077
- });
1078
- }
1079
- async sendBinary(frame) {
1080
- if (this.closed) return;
1081
- this.socket.send(frame, { binary: true });
1082
- while (this.socket.bufferedAmount > MAX_BUFFERED && !this.closed) await new Promise((resolve) => setTimeout(resolve, 5));
1083
- }
1084
- onProgress(listener) {
1085
- this.progress = listener;
1086
- }
1087
- onBinary(listener) {
1088
- this.binary = listener;
1089
- }
1090
- get failure() {
1091
- return this.closed;
1092
- }
1093
- /**
1094
- * Treats `SILENT_MS` without a frame as a dropped connection from now on. Only while waiting on a job: a long
1095
- * upload or this process hashing the checkout can delay pongs without anything being wrong.
1096
- */
1097
- watch() {
1098
- this.heard = Date.now();
1099
- this.watched = true;
1100
- }
1101
- /** Closes with a close frame, which tells the machine to cancel this connection's jobs. */
1102
- close() {
1103
- if (this.ended) return;
1104
- this.forget();
1105
- if (this.socket.readyState === WebSocket.CLOSED) return;
1106
- const timer = setTimeout(() => this.socket.terminate(), CLOSE_TIMEOUT_MS);
1107
- this.socket.once("close", () => clearTimeout(timer));
1108
- this.socket.close(1e3);
1109
- }
1110
- /** Ends the connection without a close frame, which leaves its jobs on the machine for a new connection. */
1111
- drop() {
1112
- if (this.ended) return;
1113
- this.forget();
1114
- this.socket.terminate();
1115
- }
1116
- forget() {
1117
- this.ended = true;
1118
- this.closed ??= "the connection was closed by this client";
1119
- for (const resolve of this.pending.values()) resolve({ error: {
1120
- code: "closed",
1121
- message: this.closed
1122
- } });
1123
- this.pending.clear();
1124
- this.progress = null;
1125
- this.binary = null;
1126
- clearInterval(this.keepalive);
1127
- this.socket.removeAllListeners("close");
1128
- openSockets.delete(this.socket);
1129
- }
1130
- };
1131
- function replyError(reply) {
1132
- return "error" in reply ? `${reply.error.code}: ${reply.error.message}` : null;
1133
- }
1134
- /** The repository on the remote Mac: one area per git common dir, shared by all its worktrees. */
1135
- function repoIdentity(projectRoot) {
1136
- const repoRoot = realpathSync(git(projectRoot, ["rev-parse", "--show-toplevel"]));
1137
- const common = realpathSync(git(repoRoot, [
1138
- "rev-parse",
1139
- "--path-format=absolute",
1140
- "--git-common-dir"
1141
- ]));
1142
- const digest = createHash("sha256").update(common).digest("hex").slice(0, 12);
1143
- const name = (basename(dirname(common)) || "repo").replace(/[^A-Za-z0-9._-]/g, "_").replace(/^[^A-Za-z0-9]+/, "");
1144
- const lock = [
1145
- "pnpm-lock.yaml",
1146
- "yarn.lock",
1147
- "package-lock.json"
1148
- ].find((file) => existsSync(join(repoRoot, file)));
1149
- return {
1150
- repoRoot,
1151
- project: relative(repoRoot, realpathSync(projectRoot)),
1152
- repo: `${name.slice(0, 60) || "repo"}-${digest}`,
1153
- lockfile: lock ? createHash("sha256").update(readFileSync(join(repoRoot, lock))).digest("hex") : null
1154
- };
1155
- }
1156
- const sha256 = (content) => createHash("sha256").update(content).digest("hex");
1157
- /** Why the native inputs cannot be sent to a remote Mac, such as an ignored file among them, or null when they can. */
1158
- function nativeTransferRefusal(repoRoot, snapshot) {
1159
- try {
1160
- checkNativeTransferMembership(snapshot, gitVisiblePaths(repoRoot));
1161
- return null;
1162
- } catch (error) {
1163
- return error.message.split("\n")[0];
1164
- }
1165
- }
1166
- function blobContent(repoRoot, file) {
1167
- const absolute = join(repoRoot, file.path);
1168
- return file.kind === "directory" ? Buffer.alloc(0) : file.kind === "link" ? Buffer.from(readlinkSync(absolute)) : readFileSync(absolute);
1169
- }
1170
- /** Closes every connection the choice still holds. */
1171
- function closeOffload(choice) {
1172
- choice.connection.close();
1173
- for (const each of choice.runnersUp.splice(0)) each.connection.close();
1174
- }
1175
- const NATIVE_BUILD_FEATURE = {
1176
- xcode: "native-xcode-build",
1177
- gradle: "native-gradle-build"
1178
- };
1179
- const unsupportedNative = (provider) => `This worker does not support native ${provider === "xcode" ? "Xcode" : "Gradle"} builds.`;
1180
- const ANDROID_PACKAGE = /^[A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)+$/;
1181
- function nativeTransferInput(native) {
1182
- return native?.provider === "xcode" ? native.snapshot : native?.transfer;
1183
- }
1184
- /** Connects to one paired machine and asks it for an offer; the caller closes the connection it returns. */
1185
- async function probeMachine(credential, identity, { connectMs = CONNECT_TIMEOUT_MS, offerMs = OFFER_TIMEOUT_MS } = {}) {
1186
- const target = await pinnedEndpoint$1(credential);
1187
- if (typeof target === "string") return {
1188
- credential,
1189
- failure: target
1190
- };
1191
- const connection = await BuildConnection.open(target, credential.deviceToken, connectMs);
1192
- if (!(connection instanceof BuildConnection)) return {
1193
- credential,
1194
- failure: connection.failure
1195
- };
1196
- if (identity.native && !connection.supports(NATIVE_BUILD_FEATURE[identity.native])) {
1197
- connection.close();
1198
- return {
1199
- credential,
1200
- failure: unsupportedNative(identity.native)
1201
- };
1202
- }
1203
- const native = identity.native === "gradle" || identity.native && connection.supports("native-xcode-toolchain") ? identity.native : void 0;
1204
- const reply = await connection.request("build.offer", {
1205
- repo: identity.repo,
1206
- ...identity.lockfile ? { lockfile: identity.lockfile } : {},
1207
- ...native ? { native } : identity.rubyVersion ? { rubyVersion: identity.rubyVersion } : {}
1208
- }, offerMs);
1209
- const failure = replyError(reply);
1210
- if (failure || !("result" in reply)) {
1211
- connection.close();
1212
- return {
1213
- credential,
1214
- failure: failure ?? "no offer"
1215
- };
1216
- }
1217
- return {
1218
- credential,
1219
- connection,
1220
- offer: reply.result
1221
- };
1222
- }
1223
- /**
1224
- * Asks every paired machine for an offer in parallel and keeps the connection to the one `pickOffer` chooses, or
1225
- * returns why none takes the build. A named selection probes only its credential and skips auto load gating.
1226
- */
1227
- async function chooseBuildMachine({ projectRoot, target, mode, here, note, machines = pairedMachines(), selected = "auto", onCandidates }) {
1228
- const started = Date.now();
1229
- let identity;
1230
- try {
1231
- identity = repoIdentity(projectRoot);
1232
- } catch (error) {
1233
- return `this app is not in a git checkout (${error.message.split("\n")[0]})`;
1234
- }
1235
- if (namedBuildMachine(selected)) machines = machines.filter((each) => each.machine === selected).slice(0, 1);
1236
- else machines = machines.filter((each) => automaticMachineEnabled("build", each.machine));
1237
- const rubyVersion = readRubyVersion(projectRoot) ?? void 0;
1238
- const asked = await Promise.all(machines.map((credential) => probeMachine(credential, {
1239
- ...identity,
1240
- rubyVersion,
1241
- ...target.platform !== "macos" ? { native: target.native } : {}
1242
- })));
1243
- const { order, reasons, candidates } = pickOffer({
1244
- localEnabled: selected !== "auto" || automaticMachineEnabled("build", "local"),
1245
- selected,
1246
- mode,
1247
- here,
1248
- target,
1249
- offers: asked.map((each) => ({
1250
- machine: each.credential.machine,
1251
- offer: "offer" in each ? each.offer : null,
1252
- ..."failure" in each ? { failure: each.failure } : {}
1253
- }))
1254
- });
1255
- asked.forEach((each, at) => {
1256
- if (!order.includes(at) && "connection" in each) each.connection.close();
1257
- });
1258
- onCandidates?.(candidates);
1259
- if (order.length === 0) return reasons.length ? reasons.join("; ") : "no remote Mac is paired";
1260
- for (const reason of reasons) note(reason);
1261
- const [first, ...rest] = order.map((at) => {
1262
- const pick = asked[at];
1263
- return {
1264
- machine: pick.credential.machine,
1265
- credential: pick.credential,
1266
- connection: pick.connection,
1267
- offer: pick.offer
1268
- };
1269
- });
1270
- return {
1271
- ...first,
1272
- target,
1273
- offerMs: Date.now() - started,
1274
- identity,
1275
- runnersUp: rest,
1276
- automatic: selected === "auto"
1277
- };
1278
- }
1279
- function validArtifactIdentity(request, fingerprint) {
1280
- if (request.platform === "macos") return true;
1281
- return request.native?.provider === "gradle" ? fingerprint === null : typeof fingerprint === "string" && fingerprint.length > 0;
1282
- }
1283
- const ARTIFACT_NAME = {
1284
- ios: /^[^/]+\.app$/,
1285
- android: /^[^/]+\.apk$/,
1286
- macos: /^[^/]+\.app$/
1287
- };
1288
- /**
1289
- * Reconnects to the machine that runs `job` and takes the job over with `build.attach`, retrying until
1290
- * `RESUME_WINDOW_MS`, shorter than the machine's grace for a dropped connection, has passed. Returns why it
1291
- * could not.
1292
- */
1293
- async function resumeJob(credential, job, abandoned, signal, native) {
1294
- const deadline = Date.now() + RESUME_WINDOW_MS;
1295
- let delay = RESUME_DELAY_MS;
1296
- let last = "no attempt";
1297
- while (!abandoned()) {
1298
- const target = await pinnedEndpoint$1(credential);
1299
- const connection = typeof target === "string" ? {
1300
- failure: target,
1301
- refused: false
1302
- } : await BuildConnection.open(target, credential.deviceToken, CONNECT_TIMEOUT_MS);
1303
- if (!(connection instanceof BuildConnection)) {
1304
- if (connection.refused) return `the machine turned this Mac away (${connection.failure})`;
1305
- last = connection.failure;
1306
- } else {
1307
- if (abandoned()) {
1308
- connection.close();
1309
- break;
1310
- }
1311
- if (native && !connection.supports(NATIVE_BUILD_FEATURE[native])) {
1312
- connection.close();
1313
- return unsupportedNative(native);
1314
- }
1315
- const early = [];
1316
- connection.onProgress((event) => early.push(event));
1317
- const cancel = () => connection.close();
1318
- signal?.addEventListener("abort", cancel, { once: true });
1319
- const reply = await connection.request("build.attach", { job }, OFFER_TIMEOUT_MS);
1320
- signal?.removeEventListener("abort", cancel);
1321
- if (abandoned()) {
1322
- connection.close();
1323
- break;
1324
- }
1325
- if ("result" in reply) {
1326
- const outcome = isJsonObject(reply.result) ? reply.result.outcome : null;
1327
- return {
1328
- connection,
1329
- outcome: isJsonObject(outcome) ? outcome : null,
1330
- early
1331
- };
1332
- }
1333
- last = replyError(reply);
1334
- if (reply.error.code !== "closed" && reply.error.code !== "timeout") {
1335
- connection.close();
1336
- return `the machine did not hand the build back (${last})`;
1337
- }
1338
- connection.drop();
1339
- }
1340
- if (abandoned() || Date.now() + delay > deadline) break;
1341
- await setTimeout$1(delay, void 0, { signal }).catch(() => {});
1342
- delay = Math.min(delay * 2, RESUME_MAX_DELAY_MS);
1343
- }
1344
- return `no connection to it within ${RESUME_WINDOW_MS / 6e4} min (${last})`;
1345
- }
1346
- /** Mirrors the checkout on the machine: the manifest in pages, then every blob it lacks. */
1347
- async function syncSource(connection, identity, onEnter, native) {
1348
- onEnter("sync");
1349
- const syncStarted = Date.now();
1350
- const manifest = native && "declaration" in native ? nativeGradleTransfer(join(identity.repoRoot, identity.project), native.declaration).files : native ? nativeTransferManifest(identity.repoRoot, native, sourceManifest(identity.repoRoot)) : sourceManifest(identity.repoRoot);
1351
- if (native && "declaration" in native && manifestDigest(manifest) !== native.digest) return { failure: "The native Gradle source changed before upload." };
1352
- const bySha = new Map(manifest.map((file) => [file.sha256, file]));
1353
- const missing = [];
1354
- for (let index = 0; index < manifest.length || index === 0;) {
1355
- const page = [];
1356
- let bytes = 0;
1357
- while (index < manifest.length && (page.length === 0 || bytes < PAGE_BYTES)) {
1358
- const file = manifest[index++];
1359
- bytes += Buffer.byteLength(JSON.stringify(file)) + 1;
1360
- page.push(file);
1361
- }
1362
- const done = index >= manifest.length;
1363
- const reply = await connection.request("build.sync", {
1364
- repo: identity.repo,
1365
- files: page,
1366
- done
1367
- });
1368
- const failure = replyError(reply);
1369
- if (failure || !("result" in reply)) return { failure: `sync: ${failure ?? "no reply"}` };
1370
- missing.push(...reply.result.missing ?? []);
1371
- if (done) break;
1372
- }
1373
- let uploadedBytes = 0;
1374
- for (const digest of missing) {
1375
- const file = bySha.get(digest);
1376
- if (!file) return { failure: `sync: the machine asked for an unknown blob ${digest}` };
1377
- const content = blobContent(identity.repoRoot, file);
1378
- if (sha256(content) !== digest) return { failure: `sync: ${file.path} changed while syncing` };
1379
- const header = Buffer.from(digest, "hex");
1380
- for (let offset = 0; offset < content.length || offset === 0; offset += CHUNK_BYTES) {
1381
- await connection.sendBinary(Buffer.concat([header, content.subarray(offset, offset + CHUNK_BYTES)]));
1382
- if (content.length === 0) break;
1383
- }
1384
- uploadedBytes += content.length;
1385
- if (connection.failure) return { failure: `sync: ${connection.failure}` };
1386
- }
1387
- return {
1388
- files: manifest.length,
1389
- uploaded: missing.length,
1390
- uploadedBytes,
1391
- syncMs: Date.now() - syncStarted,
1392
- digest: manifestDigest(manifest)
1393
- };
1394
- }
1395
- function stagedArtifactEscapes(stagingDir, artifactPath, hasContents) {
1396
- if ((hasContents ? [artifactPath, join(artifactPath, "Contents")] : [artifactPath]).some((path) => lstatSync(path, { throwIfNoEntry: false })?.isSymbolicLink())) return true;
1397
- if (!existsSync(artifactPath)) return false;
1398
- const inside = relative(realpathSync(stagingDir), realpathSync(artifactPath));
1399
- return inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside);
1400
- }
1401
- /**
1402
- * Builds on the chosen machine and brings the `.app` or `.apk` back into `stagingDir`, verified against the
1403
- * sha256 the machine reports. The caller re-fingerprints and stores it.
1404
- */
1405
- async function offloadBuild({ choice, expectedFingerprint, request, stagingDir, onPhase, onEnter, onRecord, note }) {
1406
- const { identity } = choice;
1407
- const native = request.platform !== "macos" ? request.native : void 0;
1408
- let sourceDigest = null;
1409
- const started = Date.now();
1410
- const fail = (reason) => {
1411
- closeOffload(choice);
1412
- return {
1413
- ok: false,
1414
- machine: choice.machine,
1415
- reason: reason.split("\n")[0].slice(0, 300)
1416
- };
1417
- };
1418
- const signal = runCancellationSignal();
1419
- const checkCancelled = () => throwIfCancelled(signal, "offloaded build");
1420
- let settle;
1421
- let settled = false;
1422
- const outcome = new Promise((resolve) => {
1423
- settle = (value) => {
1424
- settled = true;
1425
- resolve(value);
1426
- };
1427
- });
1428
- const cancel = () => {
1429
- settle({
1430
- ok: false,
1431
- code: "cancelled"
1432
- });
1433
- closeOffload(choice);
1434
- };
1435
- signal?.addEventListener("abort", cancel, { once: true });
1436
- try {
1437
- checkCancelled();
1438
- if (!validArtifactIdentity(request, expectedFingerprint)) return fail("The build request has an invalid reusable artifact identity.");
1439
- if (native?.provider === "xcode" && native.snapshot.hash !== expectedFingerprint) return fail("The native source snapshot does not match the requested artifact identity.");
1440
- let job = null;
1441
- let early = [];
1442
- let resuming = false;
1443
- const reattach = async (why) => {
1444
- if (resuming || settled) return;
1445
- resuming = true;
1446
- note(`offload: the connection to ${choice.machine} dropped (${why}); reattaching to the build there`);
1447
- const resumed = await resumeJob(choice.credential, job, () => settled, signal, native?.provider);
1448
- resuming = false;
1449
- if (settled) {
1450
- if (typeof resumed !== "string") resumed.connection.close();
1451
- return;
1452
- }
1453
- if (typeof resumed === "string") return settle({
1454
- ok: false,
1455
- code: "closed",
1456
- message: `${why}; ${resumed}`
1457
- });
1458
- choice.connection = resumed.connection;
1459
- resumed.connection.watch();
1460
- note(`offload: reattached to the build on ${choice.machine}`);
1461
- resumed.connection.onProgress(handle);
1462
- if (resumed.outcome) settle(resumed.outcome);
1463
- for (const event of resumed.early.splice(0)) handle(event);
1464
- };
1465
- const handle = (event) => {
1466
- if (event.job && event.job !== job) return;
1467
- if (event.outcome?.code === "dropped" && !event.job && job !== null) {
1468
- reattach(String(event.outcome.message));
1469
- return;
1470
- }
1471
- if (event.outcome) return settle(event.outcome);
1472
- if (event.record) onRecord(event.record);
1473
- if (event.phase && typeof event.msg === "string") {
1474
- onEnter(event.phase);
1475
- onPhase(event.phase, event.msg);
1476
- }
1477
- };
1478
- let syncMs = 0;
1479
- let uploadedBytes = 0;
1480
- let workerStarted = 0;
1481
- const moveOn = (why) => {
1482
- const next = choice.runnersUp.shift();
1483
- if (!next) return false;
1484
- note(`placement: ${next.machine} (${choice.machine} could not take the build: ${why})`);
1485
- choice.connection.close();
1486
- Object.assign(choice, next);
1487
- return true;
1488
- };
1489
- for (;;) {
1490
- if (choice.automatic && !automaticMachineEnabled("build", choice.machine)) {
1491
- const reason = `${choice.machine} is disabled in remote.buildPoolDisabled`;
1492
- if (moveOn(reason)) continue;
1493
- return fail(reason);
1494
- }
1495
- if (native && !choice.connection.supports(NATIVE_BUILD_FEATURE[native.provider])) {
1496
- const reason = unsupportedNative(native.provider);
1497
- if (moveOn(reason)) continue;
1498
- return fail(reason);
1499
- }
1500
- const synced = await syncSource(choice.connection, identity, onEnter, nativeTransferInput(native));
1501
- checkCancelled();
1502
- if ("failure" in synced) {
1503
- if (moveOn(synced.failure)) continue;
1504
- return fail(synced.failure);
1505
- }
1506
- ({syncMs, uploadedBytes} = synced);
1507
- sourceDigest = synced.digest;
1508
- onPhase("sync", `${synced.files} files, uploaded ${synced.uploaded} (${mb(uploadedBytes)}) in ${seconds(syncMs)}`);
1509
- workerStarted = Date.now();
1510
- early = [];
1511
- choice.connection.onProgress((event) => job === null ? early.push(event) : handle(event));
1512
- if (choice.automatic) requireAutomaticMachine("build", choice.machine);
1513
- checkCancelled();
1514
- const reply = await choice.connection.request("build.start", {
1515
- repo: identity.repo,
1516
- project: identity.project,
1517
- platform: request.platform,
1518
- fingerprint: request.platform === "macos" ? synced.digest : expectedFingerprint,
1519
- ...native ? { native: {
1520
- provider: native.provider,
1521
- sourceDigest: synced.digest,
1522
- ...native.provider === "xcode" ? {
1523
- cacheKey: native.cacheKey,
1524
- arch: native.arch,
1525
- parameters: native.snapshot.parameters
1526
- } : { inputs: native.transfer.declaration }
1527
- } } : {},
1528
- ...request.platform === "macos" ? { macos: {
1529
- product: request.product,
1530
- infoPlist: request.infoPlist,
1531
- bundleId: request.bundleId,
1532
- ...request.resources !== void 0 ? { resources: request.resources } : {},
1533
- ...request.assetCatalog !== void 0 ? { assetCatalog: request.assetCatalog } : {}
1534
- } } : {
1535
- configuration: request.platform === "ios" ? request.configuration : null,
1536
- scheme: request.platform === "ios" ? request.scheme : null,
1537
- iosProjectPath: request.platform === "ios" ? request.iosProjectPath ?? null : null,
1538
- runtime: request.platform === "ios" ? request.runtime : null,
1539
- packageName: packageName(join(identity.repoRoot, identity.project)),
1540
- isExpo: request.isExpo,
1541
- optimizations: request.platform === "ios" && isJsonObject(request.optimizations) ? request.optimizations : null,
1542
- ...request.platform === "android" ? { android: request.android } : {}
1543
- },
1544
- stimBuild: choice.target.local.stimBuild
1545
- });
1546
- checkCancelled();
1547
- if ("result" in reply) {
1548
- job = reply.result.job;
1549
- break;
1550
- }
1551
- const refused = `start: ${replyError(reply)}`;
1552
- if (reply.error.code === "bad-request" || !moveOn(refused)) return fail(refused);
1553
- }
1554
- for (const each of choice.runnersUp.splice(0)) each.connection.close();
1555
- choice.connection.watch();
1556
- for (const event of early.splice(0)) handle(event);
1557
- const timer = setTimeout(() => settle({
1558
- ok: false,
1559
- code: "timeout",
1560
- message: "the build did not finish in time"
1561
- }), JOB_TIMEOUT_MS);
1562
- const result = await outcome;
1563
- clearTimeout(timer);
1564
- checkCancelled();
1565
- const { connection, machine } = choice;
1566
- connection.onProgress(null);
1567
- const workerMs = Date.now() - workerStarted;
1568
- if (result.ok !== true) return fail(`${String(result.code ?? "failed")}: ${String(result.message ?? "")}`);
1569
- if (native && (result.sourceDigest !== sourceDigest || result.fingerprint !== expectedFingerprint)) return fail("The worker returned a different native source or artifact identity.");
1570
- if (native?.provider === "gradle" && !ANDROID_PACKAGE.test(String(result.androidPackage ?? ""))) return fail("The worker returned no verified native APK package.");
1571
- const artifact = result.artifact;
1572
- const name = typeof artifact?.name === "string" ? artifact.name : "";
1573
- if (!ARTIFACT_NAME[request.platform].test(name) || typeof artifact.sha256 !== "string") return fail(`the machine reported no .${request.platform === "android" ? "apk" : "app"} artifact`);
1574
- onEnter("fetch");
1575
- checkCancelled();
1576
- const fetchStarted = Date.now();
1577
- rmSync(stagingDir, {
1578
- recursive: true,
1579
- force: true
1580
- });
1581
- mkdirSync(stagingDir, { recursive: true });
1582
- const archive = join(stagingDir, "app.tgz");
1583
- const fd = openSync(archive, "w");
1584
- const hash = createHash("sha256");
1585
- let received = 0;
1586
- let badFrame = false;
1587
- connection.onBinary((frame) => {
1588
- if (frame.length < DIGEST_BYTES || frame.subarray(0, DIGEST_BYTES).toString("hex") !== artifact.sha256) {
1589
- badFrame = true;
1590
- return;
1591
- }
1592
- const data = frame.subarray(DIGEST_BYTES);
1593
- try {
1594
- writeSync(fd, data);
1595
- } catch {
1596
- badFrame = true;
1597
- return;
1598
- }
1599
- hash.update(data);
1600
- received += data.length;
1601
- });
1602
- const fetched = await connection.request("build.artifact", { job }, 9e5);
1603
- connection.onBinary(null);
1604
- closeSync(fd);
1605
- checkCancelled();
1606
- const fetchFailure = replyError(fetched);
1607
- if (fetchFailure || !("result" in fetched)) return fail(`fetch: ${fetchFailure ?? "no reply"}`);
1608
- const digest = hash.digest("hex");
1609
- const declared = fetched.result;
1610
- if (badFrame) return fail("fetch: an artifact frame had another digest or could not be written here");
1611
- if (digest !== artifact.sha256 || declared.sha256 !== digest || declared.size !== received) return fail(`fetch: the artifact's sha256 ${digest.slice(0, 12)} does not match ${String(artifact.sha256).slice(0, 12)}`);
1612
- await getExecutor().runFileAsync("tar", [
1613
- "-xf",
1614
- archive,
1615
- "-C",
1616
- stagingDir
1617
- ], { timeoutMs: 6e5 });
1618
- checkCancelled();
1619
- rmSync(archive, { force: true });
1620
- const artifactPath = join(stagingDir, name);
1621
- if (stagedArtifactEscapes(stagingDir, artifactPath, request.platform === "macos")) return fail(`fetch: ${machine} returned ${name} as or through a symbolic link or outside the staging directory`);
1622
- if (request.platform === "ios" && !existsSync(join(artifactPath, "Info.plist"))) return fail(`fetch: ${name} has no Info.plist`);
1623
- if (request.platform === "android" && !(lstatSync(artifactPath, { throwIfNoEntry: false })?.isFile() ?? false)) return fail(`fetch: ${name} is not a file`);
1624
- if (request.platform === "macos") {
1625
- for (const segments of [["Contents", "Info.plist"], [
1626
- "Contents",
1627
- "MacOS",
1628
- request.product
1629
- ]]) if (!lstatSync(join(artifactPath, ...segments), { throwIfNoEntry: false })?.isFile()) return fail(`fetch: ${name} has no regular ${segments.join("/")}`);
1630
- }
1631
- const fetchMs = Date.now() - fetchStarted;
1632
- connection.close();
1633
- return {
1634
- ok: true,
1635
- machine,
1636
- artifactPath,
1637
- ...typeof declared.handoff === "string" && /^[0-9a-f]{64}$/.test(declared.handoff) ? { handoff: {
1638
- nodeId: choice.credential.nodeId,
1639
- token: declared.handoff,
1640
- sha256: digest
1641
- } } : {},
1642
- compilationCache: request.platform === "ios" ? compilationActivity(result.compilationCache) : COMPILATION_CACHE_UNAVAILABLE,
1643
- ...native?.provider === "gradle" ? { androidPackage: result.androidPackage } : {},
1644
- ccache: request.platform === "android" ? ccacheActivity(result.compilationCache) : CCACHE_UNAVAILABLE,
1645
- timings: {
1646
- offerMs: choice.offerMs,
1647
- syncMs,
1648
- workerMs,
1649
- fetchMs,
1650
- totalMs: Date.now() - started + choice.offerMs,
1651
- worker: isJsonObject(result.timings) ? result.timings : {},
1652
- uploadedBytes,
1653
- artifactBytes: received
1654
- }
1655
- };
1656
- } catch (error) {
1657
- closeOffload(choice);
1658
- checkCancelled();
1659
- return fail(error.message.split("\n")[0] ?? String(error));
1660
- } finally {
1661
- signal?.removeEventListener("abort", cancel);
1662
- }
1663
- }
1664
- function packageName(projectRoot) {
1665
- try {
1666
- const name = JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf8")).name;
1667
- return typeof name === "string" ? name : null;
1668
- } catch {
1669
- return null;
1670
- }
1671
- }
1672
- const numberOrNull = (field) => typeof field === "number" ? field : null;
1673
- function compilationActivity(value) {
1674
- if (!isJsonObject(value) || value.status !== "reported") return COMPILATION_CACHE_UNAVAILABLE;
1675
- return {
1676
- status: "reported",
1677
- hits: numberOrNull(value.hits),
1678
- cacheableTasks: numberOrNull(value.cacheableTasks),
1679
- hitRatePercent: numberOrNull(value.hitRatePercent)
1680
- };
1681
- }
1682
- function ccacheActivity(value) {
1683
- if (!isJsonObject(value) || value.status !== "reported") return CCACHE_UNAVAILABLE;
1684
- return {
1685
- status: "reported",
1686
- hits: numberOrNull(value.hits),
1687
- misses: numberOrNull(value.misses),
1688
- hitRatePercent: numberOrNull(value.hitRatePercent)
1689
- };
1690
- }
1691
- const mb = (bytes) => `${(bytes / 1024 ** 2).toFixed(1)} MB`;
1692
- const seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
1693
- function simulatorRuntime(udid) {
1694
- try {
1695
- const listed = JSON.parse(getExecutor().runFileQuiet("xcrun", [
1696
- "simctl",
1697
- "list",
1698
- "devices",
1699
- "-j"
1700
- ], { timeoutMs: 2e4 }) ?? "{}");
1701
- for (const [runtime, devices] of Object.entries(listed.devices ?? {})) if (devices.some((device) => device.udid === udid)) return runtime;
1702
- } catch {}
1703
- return null;
1704
- }
1705
- const DOCTOR_CONNECT_MS = 5e3;
1706
- const DOCTOR_OFFER_MS = 8e3;
1707
- const PLATFORM_LABEL = {
1708
- ios: "iOS",
1709
- android: "Android",
1710
- macos: "macOS"
1711
- };
1712
- const SHARED_PROBLEMS = /* @__PURE__ */ new Set([
1713
- "stim-build",
1714
- "arch",
1715
- "disk",
1716
- "busy"
1717
- ]);
1718
- /**
1719
- * Doctor's offload check for `projectRoot`: one bounded offer per machine, judged for each build `targets` returns
1720
- * by `offerProblems` exactly as placement judges it. No build runs. With several targets, a problem that only
1721
- * one platform has names it.
1722
- */
1723
- function offloadCheck(projectRoot, targets) {
1724
- let identity;
1725
- try {
1726
- identity = repoIdentity(projectRoot);
1727
- } catch (error) {
1728
- identity = error.message.split("\n")[0] ?? "git failed";
1729
- }
1730
- let resolved = null;
1731
- return async (credential) => {
1732
- if (typeof identity === "string") return {
1733
- capacity: null,
1734
- problems: [{
1735
- code: "checkout",
1736
- reason: `this app is not in a git checkout (${identity})`
1737
- }]
1738
- };
1739
- const probe = await probeMachine(credential, {
1740
- ...identity,
1741
- rubyVersion: readRubyVersion(projectRoot) ?? void 0
1742
- }, {
1743
- connectMs: DOCTOR_CONNECT_MS,
1744
- offerMs: DOCTOR_OFFER_MS
1745
- });
1746
- if ("failure" in probe) return {
1747
- capacity: null,
1748
- problems: [{
1749
- code: "unreachable",
1750
- reason: probe.failure
1751
- }]
1752
- };
1753
- probe.connection.close();
1754
- resolved ??= targets();
1755
- const resolvedTargets = await resolved;
1756
- const problems = [];
1757
- for (const target of resolvedTargets) for (const problem of offerProblems(probe.offer, target)) {
1758
- const labeled = resolvedTargets.length > 1 && !SHARED_PROBLEMS.has(problem.code) ? {
1759
- ...problem,
1760
- reason: `${PLATFORM_LABEL[target.platform]}: ${problem.reason}`
1761
- } : problem;
1762
- if (!problems.some((each) => each.code === labeled.code && each.reason === labeled.reason)) problems.push(labeled);
1763
- }
1764
- return {
1765
- capacity: probe.offer.capacity,
1766
- problems
1767
- };
1768
- };
1769
- }
1770
- //#endregion
1771
- export { inspectDeviceHostMachines as C, hostingMachines as S, requireLocalBuild as _, nativeTransferRefusal as a, inspectBuildMachines as b, offloadPlacement as c, simulatorRuntime as d, setRemoteLogSink as f, parseBuildMachineOption as g, namedBuildMachine as h, closeOffload as i, placementLoad as l, buildPlacementSettings as m, buildPlacementCandidates as n, offloadBuild as o, OffloadRefusal as p, chooseBuildMachine as r, offloadCheck as s, BuildConnection as t, remotePhaseText as u, resolveBuildMachine as v, configuredMachines as x, resolveBuildPlacement as y };