stim 1.17.2 → 1.19.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 (162) hide show
  1. package/README.md +50 -1
  2. package/dist/{activity-IRvJY_ru.mjs → activity-BSw0x84R.mjs} +4 -4
  3. package/dist/{agent-device-usage-output-BK41vnzN.mjs → agent-device-usage-output-Bjxu_VmX.mjs} +8 -8
  4. package/dist/{android-B9yaNKwa.mjs → android-BXc6BmEi.mjs} +658 -404
  5. package/dist/{android-B_YNZy61.mjs → android-BdQC6zs1.mjs} +9 -9
  6. package/dist/android-BvV1gGdN.mjs +4 -0
  7. package/dist/{android-DE4v59Mb.mjs → android-DE5g_5gv.mjs} +2 -2
  8. package/dist/{android-cas-ZFpjJBL_.mjs → android-cas-C87geAg-.mjs} +5 -5
  9. package/dist/android-cas-compiler.mjs +2 -2
  10. package/dist/api-DIf_s86s.mjs +150 -0
  11. package/dist/api-run.d.mts +1 -0
  12. package/dist/api-run.mjs +145 -0
  13. package/dist/api.d.mts +218 -0
  14. package/dist/api.mjs +2 -0
  15. package/dist/{app-install-VPkI5WEg.mjs → app-install-CFI4ptUL.mjs} +33 -13
  16. package/dist/{attempt-Bk2ouQVL.mjs → attempt-DW-mQqM7.mjs} +14 -4
  17. package/dist/{web-Dl0KSRHf.mjs → browser-web-CDhqQxhv.mjs} +138 -176
  18. package/dist/{budget-DfcgFdQB.mjs → budget-Dz-kyXLv.mjs} +1391 -58
  19. package/dist/build-facts-B87vo-Im.d.mts +142 -0
  20. package/dist/{cache-manifest-DnUHBJaU.mjs → cache-manifest-DiwKSyv9.mjs} +3 -3
  21. package/dist/cache-manifest.mjs +1 -1
  22. package/dist/{chrome-BrcVWQyD.mjs → chrome-BTUOo8p2.mjs} +1 -1
  23. package/dist/cli-C9Zvgbdc.mjs +68 -0
  24. package/dist/cli.mjs +1 -1
  25. package/dist/{client-SvBdgObD.mjs → client-CuUo8Nvg.mjs} +381 -49
  26. package/dist/collector-run.d.mts +6 -0
  27. package/dist/collector-run.mjs +9 -9
  28. package/dist/{command-output-CDAIFD-8.mjs → command-output-Bt8ygHE7.mjs} +2 -2
  29. package/dist/{config-D0DbpZtM.mjs → config-DC5jEtd1.mjs} +9 -5
  30. package/dist/{created-devices-CpvzBL25.mjs → created-devices-D0s26Hx5.mjs} +2 -2
  31. package/dist/{dependency-state-CLcBHH2M.mjs → dependency-state-DKaS1OM9.mjs} +2 -2
  32. package/dist/{detached-entry-CiTEgMR1.mjs → detached-entry-pB_IPfV4.mjs} +1 -1
  33. package/dist/{dev-client-B-XC4uRH.mjs → dev-client-CDY_zAmD.mjs} +2 -2
  34. package/dist/{device-C8IOoJPv.mjs → device-BHw0WlHl.mjs} +8 -8
  35. package/dist/{device-capacity-ZC4XSZ4R.mjs → device-capacity-BoN1jQkO.mjs} +25 -14
  36. package/dist/device-host-worker.mjs +19 -19
  37. package/dist/{gradle-dJpre3Yn.mjs → device-ios-B2ruohE-.mjs} +661 -107
  38. package/dist/{device-lease-DpoS36_u.mjs → device-lease-Cu7PtQpZ.mjs} +9 -9
  39. package/dist/{device-lease-run-B8YltIH_.mjs → device-lease-run-D38SnZtt.mjs} +4 -4
  40. package/dist/{device-pool-3o7s5lo4.mjs → device-pool-CP4rAsHx.mjs} +3 -3
  41. package/dist/{device-remote-hFSkpfQu.mjs → device-remote-B5GiUifk.mjs} +15 -16
  42. package/dist/{doctor-CFv1fCrF.mjs → doctor-PRJH94v6.mjs} +61 -34
  43. package/dist/doctor-watchman-wZhyTdp4.mjs +259 -0
  44. package/dist/{error-diagnostics-C-NMnhrk.mjs → error-diagnostics--v5HGOU0.mjs} +11 -11
  45. package/dist/{exec-CaErVv7t.mjs → exec-i8GDPwq9.mjs} +93 -6
  46. package/dist/{gc-DEmkg3Xh.mjs → gc-CyHMcKcn.mjs} +25 -471
  47. package/dist/{guide-CSmhMTf8.mjs → guide-dRaQJJk9.mjs} +681 -507
  48. package/dist/{hosted-android-E1Heoqnv.mjs → hosted-android-Bs7fytj7.mjs} +3 -3
  49. package/dist/{hosted-client-Bp4knSwu.mjs → hosted-client-C5G4h_OJ.mjs} +2 -2
  50. package/dist/{hosted-ios-BnbeYOUD.mjs → hosted-ios-Bik035eL.mjs} +1 -1
  51. package/dist/{hosted-logs-OcgIBvCT.mjs → hosted-logs-CI9Az9a4.mjs} +9 -9
  52. package/dist/{hosted-macos-DcxKjtfH.mjs → hosted-macos-DABm3ScO.mjs} +5 -5
  53. package/dist/{hosted-native-1Ebbig4w.mjs → hosted-native-DbV2aUVw.mjs} +13 -9
  54. package/dist/{idle-shutdown-IGyNV8Cr.mjs → idle-shutdown-DYiMBn34.mjs} +7 -8
  55. package/dist/{ios-DSfnPfse.mjs → ios-BWbHFZVz.mjs} +19 -7
  56. package/dist/{ios-Cr1h5gnO.mjs → ios-DCrkgXEM.mjs} +1 -1
  57. package/dist/ios-DYUr9LGc.mjs +8 -0
  58. package/dist/ios-ITYno9rO.mjs +2241 -0
  59. package/dist/{ios-device-pAN4t54-.mjs → ios-device-CJ_ZOGky.mjs} +3 -3
  60. package/dist/{ios-device-Ee7sDXfp.mjs → ios-device-CrW8aMb5.mjs} +1 -1
  61. package/dist/{ios-state-8L3lgum6.mjs → ios-state-By0XuLTQ.mjs} +1 -1
  62. package/dist/{launch-verify-lTnWZikR.mjs → launch-verify-DCveSE6d.mjs} +5 -5
  63. package/dist/{logs-BAaS7gS2.mjs → logs-Bao1HOMy.mjs} +13 -13
  64. package/dist/{macos-BIRQS90f.mjs → macos-ciyBdTY5.mjs} +194 -132
  65. package/dist/macos-nIOATJmR.mjs +2 -0
  66. package/dist/macos-run.mjs +1 -1
  67. package/dist/maintenance-run.mjs +349 -47
  68. package/dist/manifest-CX0d1IkZ.mjs +148 -0
  69. package/dist/{metro-g8NqiJp-.mjs → metro-DI2AjPF_.mjs} +196 -43
  70. package/dist/{metro-gateway-C-ijGI2F.mjs → metro-gateway-BxpJLIwh.mjs} +4 -4
  71. package/dist/{metro-store-CDDZHtPl.mjs → metro-store-BAHFcJm6.mjs} +16 -2
  72. package/dist/{build-plan-BDsSi09V.mjs → miss-reason-CjQCOKqq.mjs} +207 -249
  73. package/dist/{named-ports-C8m8EQs2.mjs → named-ports-B4iWH4E-.mjs} +70 -8
  74. package/dist/{native-runtime-BEK171xQ.mjs → native-runtime-BGxsbfny.mjs} +6 -6
  75. package/dist/{ndjson-PclOIwVY.mjs → ndjson-LGZAMsqa.mjs} +8 -4
  76. package/dist/offload-worker.d.mts +3 -45
  77. package/dist/offload-worker.mjs +11 -11
  78. package/dist/{ownership-DyRQru0q.mjs → ownership-EB0ySdGK.mjs} +3 -3
  79. package/dist/{page-QF_Ke10s.mjs → page-Bv9s2KwY.mjs} +1 -1
  80. package/dist/placement-log-Dxe34rJm.mjs +88 -0
  81. package/dist/plan-placement-BVY59qa8.mjs +290 -0
  82. package/dist/{ports-4nYqxPWC.mjs → ports-ByPc0_97.mjs} +3 -3
  83. package/dist/{prebuild-D7RUMkwN.mjs → prebuild-BKJ3wJI1.mjs} +5 -5
  84. package/dist/preview-BbSottmO.mjs +986 -0
  85. package/dist/{rolldown-runtime-BsOwBuO_.mjs → process-identity-Biv3arCO.mjs} +11 -1
  86. package/dist/project-B5eQ1USs.mjs +612 -0
  87. package/dist/{pull-request-pxnDCQzn.mjs → pull-request-IHNdMnG8.mjs} +1 -1
  88. package/dist/pull-requests.mjs +1 -1
  89. package/dist/react-native-ios-BMh2i25I.mjs +2 -0
  90. package/dist/react-native-ios-CSpIxexZ.mjs +2083 -0
  91. package/dist/{recordings-BNgsD1Tr.mjs → recordings-Bv8McZN4.mjs} +1 -1
  92. package/dist/{reload-DcRxj3qG.mjs → reload-rAPdqeRR.mjs} +14 -15
  93. package/dist/{remote-cache-Ci4DQYYj.mjs → remote-cache-N3hAEaAL.mjs} +5 -5
  94. package/dist/{run-CkE9y0nc.mjs → run-lAkle1-c.mjs} +5 -5
  95. package/dist/{server-bare-BkrSxSsZ.mjs → server-bare-CkIjnegq.mjs} +5 -6
  96. package/dist/server-expo-DGsdUGX2.mjs +2 -0
  97. package/dist/{server-expo-CHTyXD0G.mjs → server-expo-n3tUi3Em.mjs} +5 -6
  98. package/dist/{settings-SR9OmW9h.mjs → settings-2am7TSxp.mjs} +5 -5
  99. package/dist/{settings-Cfd-0r2d.mjs → settings-CxgngUZk.mjs} +116 -103
  100. package/dist/{settings-BBpMcDbD.mjs → settings-Djl-mpjX.mjs} +1 -1
  101. package/dist/settings.schema.json +198 -9
  102. package/dist/{simslim-BWNrc3M3.mjs → simslim-D9LkJxIO.mjs} +7 -7
  103. package/dist/{slot-launch-DmjBmpQV.mjs → slot-launch-HV_KtPIw.mjs} +6 -6
  104. package/dist/{spawn-claims-D0XfSe8u.mjs → spawn-claims-Kqiifsk2.mjs} +1 -1
  105. package/dist/{spawn-entry-jw19m8Nl.mjs → spawn-entry-Dcb7sDUz.mjs} +1 -0
  106. package/dist/{stage-CzaiwFsI.mjs → stage-BgaZJVZY.mjs} +14 -3
  107. package/dist/{start-AlfCPRVU.mjs → start-D8NbxNBb.mjs} +1 -1
  108. package/dist/{start-DUz3mYdb.mjs → start-DtxthMfI.mjs} +34 -27
  109. package/dist/{state-PeQiJuIu.mjs → state-D2KtnALG.mjs} +4 -4
  110. package/dist/{state-DyaNTgfR.mjs → state-Tp4uImDS.mjs} +5 -3
  111. package/dist/{state-B4PQFXMW.d.mts → state-lKh8shBp.d.mts} +2 -3
  112. package/dist/{stats-Bq4fJOdB.mjs → stats-8EJ5ivFT.mjs} +6 -6
  113. package/dist/{status-DlKvVJBE.mjs → status-BEeL6IKz.mjs} +6 -4
  114. package/dist/{status-BA2pehwf.mjs → status-BOyp0CUl.mjs} +31 -34
  115. package/dist/{stim-desktop-Cnz8CPnl.mjs → stim-desktop-C65jk_q2.mjs} +1 -1
  116. package/dist/{stim-installations-QMEI32v1.mjs → stim-installations-DbmUjsLv.mjs} +1 -1
  117. package/dist/stop-D0UIpZuh.mjs +72 -0
  118. package/dist/{stop-zF1y04f2.mjs → stop-DRipAg-e.mjs} +2 -2
  119. package/dist/{stop-DQIasgKp.mjs → stop-zDlknFoz.mjs} +79 -33
  120. package/dist/supervisor-run.d.mts +24 -22
  121. package/dist/supervisor-run.mjs +21 -22
  122. package/dist/{support-CsJNVfOc.mjs → support-CNqYkV0y.mjs} +7 -7
  123. package/dist/{support-WvD__tSV.mjs → support-DsNaFg6D.mjs} +5 -5
  124. package/dist/swiftpm-macos-aJzPAj1t.mjs +119 -0
  125. package/dist/{toolchain-DNyR3NPq.mjs → toolchain-C3xSoyUl.mjs} +475 -269
  126. package/dist/{trigger-Bemu8ytw.mjs → trigger-CAhmG-4s.mjs} +6 -6
  127. package/dist/trigger-CoalsMTV.mjs +2 -0
  128. package/dist/{warm-progress-DF4JRvYI.mjs → warm-progress-pFLB2e6P.mjs} +135 -38
  129. package/dist/{status-CzS4bPO9.mjs → watchman-BL1IdOtq.mjs} +75 -7
  130. package/dist/web-DFpFeZu5.mjs +2 -0
  131. package/dist/web-DbbrPUn0.mjs +127 -0
  132. package/dist/web-run.mjs +10 -10
  133. package/dist/{workspace-state-DqcCbpE7.mjs → workspace-state-CR9X86xJ.mjs} +3 -3
  134. package/dist/{ownership-BMLqyBgJ.mjs → workspaces-Djb2gfi9.mjs} +661 -23
  135. package/dist/{worktree-Be_7NLKy.mjs → worktree-BL4M1rhB.mjs} +252 -50
  136. package/dist/{worktree-j3IcFAI5.mjs → worktree-DV-A0LNL.mjs} +1 -1
  137. package/package.json +10 -5
  138. package/shim/bundle-response.cjs +2 -1
  139. package/shim/bundle-response.d.cts +1 -1
  140. package/shim/expo-metro-config.cjs +1 -1
  141. package/dist/build-progress-D0WkjCPd.mjs +0 -1036
  142. package/dist/build-slots-41v_d2x9.mjs +0 -142
  143. package/dist/cli-CYXb29oE.mjs +0 -51
  144. package/dist/deps-iKw0eD4P.mjs +0 -438
  145. package/dist/device-ios-C2Um-XUR.mjs +0 -412
  146. package/dist/errors-B6W7FkxQ.mjs +0 -15
  147. package/dist/guide-status-Du-ZQ3FD.mjs +0 -125
  148. package/dist/idle-BXG__1BA.mjs +0 -176
  149. package/dist/in-use-CvGOUn-x.mjs +0 -130
  150. package/dist/ios-BJ4XUSe1.mjs +0 -3867
  151. package/dist/native-run-D1Q56rAD.mjs +0 -158
  152. package/dist/preview-B-g9d7kV.mjs +0 -318
  153. package/dist/process-identity-BmRm1qV0.mjs +0 -12
  154. package/dist/project-D47Rh0ly.mjs +0 -276
  155. package/dist/server-expo-CeakhI8P.mjs +0 -2
  156. package/dist/state-BKa8wNN5.mjs +0 -148
  157. package/dist/stop-BIiM49EY.mjs +0 -48
  158. package/dist/stop-cause-rhmGfwvt.mjs +0 -97
  159. package/dist/trigger-CXts6qt1.mjs +0 -2
  160. package/dist/watchman-APYjXSQa.mjs +0 -48
  161. package/dist/workspace-process-lock-2mNzOlaB.mjs +0 -58
  162. package/dist/workspaces-Bcib97-6.mjs +0 -306
@@ -1,14 +1,19 @@
1
- import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
- import { _ as loadConfig$1, d as getConfigDir } from "./config-D0DbpZtM.mjs";
3
- import { w as withDirLock } from "./workspace-state-DqcCbpE7.mjs";
4
- import { T as CCACHE_UNAVAILABLE, a as toolchainMismatches, l as COMPILATION_CACHE_UNAVAILABLE, s as manifestDigest } from "./toolchain-DNyR3NPq.mjs";
5
- import { createHash } from "node:crypto";
1
+ import { n as debugLog, t as getExecutor } from "./exec-i8GDPwq9.mjs";
2
+ import { a as formatElapsed } from "./command-output-Bt8ygHE7.mjs";
3
+ import { _ as loadConfig$1, d as getConfigDir } from "./config-DC5jEtd1.mjs";
4
+ import { a as updateWorkspaceState, r as readWorkspaceState$1, t as clearWorkspaceStateKey, v as workspaceDir, w as withDirLock } from "./workspace-state-CR9X86xJ.mjs";
5
+ import { i as signalDeclaredSpawns, r as refuseDeclaredSpawns, t as allowDeclaredSpawns } from "./spawn-claims-Kqiifsk2.mjs";
6
+ import { a as withWorkspaceProcessLock } from "./named-ports-B4iWH4E-.mjs";
7
+ import { r as CCACHE_UNAVAILABLE, t as manifestDigest } from "./manifest-CX0d1IkZ.mjs";
8
+ import { a as toolchainMismatches, m as COMPILATION_CACHE_UNAVAILABLE, u as readRubyVersion } from "./toolchain-C3xSoyUl.mjs";
6
9
  import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, writeFileSync, writeSync } from "node:fs";
7
10
  import { basename, dirname, join, relative } from "node:path";
11
+ import { OFFLOAD_MODES, acceptsShape, automaticMachineEnabled, buildMachinesFile, buildMachinesLock, deviceHostMachinesClaims, deviceHostMachinesFile, deviceHostMachinesLock, endpoint, findPeer as findPeer$1, isJsonObject, parseMachine, parseMachine as parseMachine$1, pinnedEndpoint, readBuildMachines, readDeviceHostMachines, requireAutomaticMachine, runId, saturation, settingDefinition } from "@stim-cli/core/state";
12
+ import { hostname } from "node:os";
8
13
  import { InvalidArgumentError } from "commander";
9
- import { OFFLOAD_MODES, acceptsShape, buildMachinesFile, buildMachinesLock, deviceHostMachinesClaims, deviceHostMachinesFile, deviceHostMachinesLock, endpoint, findPeer as findPeer$1, isJsonObject, parseMachine, parseMachine as parseMachine$1, pinnedEndpoint, readBuildMachines, readDeviceHostMachines, saturation, settingDefinition } from "@stim-cli/core/state";
10
14
  import { releaseClaim, tryAcquireClaim } from "@stim-cli/core/ownership-claim";
11
- import { hostname } from "node:os";
15
+ import { createHash } from "node:crypto";
16
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
12
17
  import { WebSocket } from "ws";
13
18
  //#region src/offload/access-ticket.ts
14
19
  function readAccessTicket() {
@@ -689,6 +694,9 @@ function parseBuildMachineOption(value) {
689
694
  throw new InvalidArgumentError(error.message);
690
695
  }
691
696
  }
697
+ function requireLocalBuild(selected) {
698
+ if (selected === "auto" && !automaticMachineEnabled("build", "local")) throw new OffloadRefusal("auto", "local is disabled in remote.buildPoolDisabled; enable it or select a machine explicitly");
699
+ }
692
700
  function namedBuildMachine(selected) {
693
701
  return selected !== "auto" && selected !== "local";
694
702
  }
@@ -703,8 +711,26 @@ function requireConfiguredMachine(selected, machines) {
703
711
  if (!match) throw new OffloadRefusal(selected, `not listed in remote.machines (configured: ${entries.join(", ") || "none"})`);
704
712
  return match;
705
713
  }
714
+ const settingSource = (flag, env, setting) => flag !== void 0 ? "flag" : env?.trim() ? "env" : setting !== void 0 ? "setting" : "default";
715
+ let buildMachineFlag;
716
+ /** The settings that decide where a build runs, with the layer each value came from, for this run's `--remote-build`. */
717
+ function buildPlacementSettings(selected) {
718
+ const flag = buildMachineFlag;
719
+ const remote = loadConfig$1()?.remote;
720
+ const rawMode = process.env.STIM_REMOTE_BUILD_MODE || remote?.buildMode;
721
+ return [{
722
+ key: "remote.build",
723
+ value: selected,
724
+ from: settingSource(flag, process.env.STIM_REMOTE_BUILD, remote?.build)
725
+ }, {
726
+ key: "remote.buildMode",
727
+ value: selected === "local" ? "off" : OFFLOAD_MODES.includes(rawMode) ? rawMode : "auto",
728
+ from: settingSource(void 0, process.env.STIM_REMOTE_BUILD_MODE, remote?.buildMode)
729
+ }];
730
+ }
706
731
  function resolveBuildPlacement(flag) {
707
732
  const remote = loadConfig$1()?.remote;
733
+ buildMachineFlag = flag;
708
734
  let selected = "auto";
709
735
  try {
710
736
  selected = resolveBuildMachine(flag, process.env.STIM_REMOTE_BUILD, remote?.build);
@@ -723,6 +749,176 @@ function resolveBuildPlacement(flag) {
723
749
  }
724
750
  }
725
751
  //#endregion
752
+ //#region src/remote-log.ts
753
+ let sink = null;
754
+ /** Where this run's failed requests to another Mac are written: its workspace log, so `stim logs` shows them. */
755
+ function setRemoteLogSink(next) {
756
+ sink = next;
757
+ }
758
+ function reportRemoteFailure(event, fields) {
759
+ try {
760
+ sink?.({
761
+ src: "build",
762
+ level: "warn",
763
+ event,
764
+ ...fields
765
+ });
766
+ } catch {}
767
+ }
768
+ //#endregion
769
+ //#region src/engine/native-run.ts
770
+ const NATIVE_RUN_LOCK = "native-run";
771
+ const NATIVE_RUN_WAIT_MS = 18e5;
772
+ const WAIT_HEARTBEAT_MS = 3e4;
773
+ const CANCEL_REQUEST_KEY = "nativeRunCancel";
774
+ function nativeRunHolder(holder) {
775
+ const { command, platform, slot } = holder.details;
776
+ return {
777
+ command: typeof command === "string" ? command : null,
778
+ platform: typeof platform === "string" ? platform : null,
779
+ slot: typeof slot === "string" ? slot : "default"
780
+ };
781
+ }
782
+ function describeNativeRunHolder(holder, now) {
783
+ const { command, slot } = nativeRunHolder(holder);
784
+ const started = Date.parse(holder.startedAt);
785
+ const facts = [`pid ${holder.owner.pid}`];
786
+ if (Number.isFinite(started)) facts.push(`running for ${formatElapsed(now - started)}`);
787
+ if (command === null) return `another Stim run (${facts.join(", ")})`;
788
+ return `\`stim ${command}${slot === "default" ? "" : ` --slot ${slot}`}\` (${facts.join(", ")})`;
789
+ }
790
+ /**
791
+ * Report a wait on the native-run lock: one line as soon as a holder is seen, a line for each new
792
+ * holder, and a heartbeat every WAIT_HEARTBEAT_MS while the same holder keeps it.
793
+ */
794
+ function nativeRunWaitNotice({ write, now = Date.now, describe = describeNativeRunHolder }) {
795
+ let claimId = null;
796
+ let lastAt = 0;
797
+ return (holder, firstLine) => {
798
+ const at = now();
799
+ if (holder.claimId !== claimId) {
800
+ claimId = holder.claimId;
801
+ lastAt = at;
802
+ write(firstLine ?? `waiting for ${describe(holder, at)} in this workspace to finish`);
803
+ return;
804
+ }
805
+ if (at - lastAt < WAIT_HEARTBEAT_MS) return;
806
+ lastAt = at;
807
+ write(`still waiting for ${describe(holder, at)}`);
808
+ };
809
+ }
810
+ /**
811
+ * What `stop` does about the run holding the native-run lock. A build is interrupted only when the stop
812
+ * leaves it nothing to deploy to: the whole workspace stops, the build targets the stopped slot, or the
813
+ * stopped slot is the workspace's only device. A build for another slot that stays is left running and
814
+ * the stop proceeds without the lock. Only an owner whose identity is proven live is signalled.
815
+ */
816
+ function decideStopAction({ stopSlot, holder, ownerIdentity, deviceSlots }) {
817
+ if (holder.command === "macos") {
818
+ if (stopSlot !== void 0) return { action: "proceed" };
819
+ return ownerIdentity === "same" ? { action: "interrupt" } : { action: "refuse" };
820
+ }
821
+ if (holder.command !== "ios" && holder.command !== "android") return { action: "wait" };
822
+ if (!(stopSlot === void 0 || holder.slot === stopSlot || deviceSlots.includes(stopSlot) && deviceSlots.every((slot) => slot === stopSlot))) return { action: "proceed" };
823
+ return ownerIdentity === "same" ? { action: "interrupt" } : { action: "refuse" };
824
+ }
825
+ function requestNativeRunCancel(root, claimId, pid = process.pid) {
826
+ updateWorkspaceState(root, (state) => ({
827
+ ...state,
828
+ [CANCEL_REQUEST_KEY]: {
829
+ claimId,
830
+ pid,
831
+ at: (/* @__PURE__ */ new Date()).toISOString()
832
+ }
833
+ }));
834
+ }
835
+ function cancelRequestFor(root, claimId) {
836
+ const request = readWorkspaceState$1(root)?.[CANCEL_REQUEST_KEY];
837
+ if (request?.claimId !== claimId || typeof request.pid !== "number") return null;
838
+ return { pid: request.pid };
839
+ }
840
+ function clearNativeRunCancel(root, claimId) {
841
+ try {
842
+ clearWorkspaceStateKey(root, CANCEL_REQUEST_KEY, (value) => value?.claimId === claimId);
843
+ } catch {}
844
+ }
845
+ let cancelled = null;
846
+ let cancellationSignal;
847
+ /** The active native run aborts this signal when it is cancelled. */
848
+ function runCancellationSignal() {
849
+ return cancellationSignal;
850
+ }
851
+ /** Why this process's native run was cancelled, or null while it was not. */
852
+ function runCancellation() {
853
+ return cancelled;
854
+ }
855
+ function cancelledFailure(platform, { code, message }) {
856
+ if (cancelled === null) return null;
857
+ return {
858
+ code: "STIM_CANCELLED",
859
+ message: `The ${platform} run was ${cancelled} before it finished.`,
860
+ remedy: `Run \`stim ${platform}\` again when you want the app on a device.`,
861
+ lines: [`it stopped at ${code ?? "an unfinished step"}${message ? `: ${message}` : ""}`]
862
+ };
863
+ }
864
+ function cancelOnInterrupt({ root, claim, write, exit, controller }) {
865
+ let signals = 0;
866
+ const onInterrupt = () => {
867
+ signals += 1;
868
+ if (signals > 1) {
869
+ exit(130);
870
+ return;
871
+ }
872
+ const request = cancelRequestFor(root, claim.claimId);
873
+ cancelled = request ? `cancelled by \`stim stop\` (pid ${request.pid})` : "cancelled by an interrupt";
874
+ controller.abort();
875
+ refuseDeclaredSpawns(`the run was ${cancelled}`);
876
+ const signalled = signalDeclaredSpawns("SIGINT");
877
+ write(signalled > 0 ? `${cancelled}; stopping the running build tool` : request ? `${cancelled}; stopping before the next build tool or install` : cancelled);
878
+ if (signalled === 0 && !request) exit(130);
879
+ };
880
+ process.on("SIGINT", onInterrupt);
881
+ return () => {
882
+ process.off("SIGINT", onInterrupt);
883
+ allowDeclaredSpawns();
884
+ clearNativeRunCancel(root, claim.claimId);
885
+ };
886
+ }
887
+ /**
888
+ * Run an `ios`, `android` or `macos` invocation under the workspace's native-run lock: the claim records the
889
+ * command, platform and slot, and a wait for another holder is reported on `write`. The first SIGINT
890
+ * forwards the interrupt to the running declared build tool; a run `stim stop` cancelled between build
891
+ * tools starts no further one; a terminal interrupt with no build tool running, or a second SIGINT, exits
892
+ * with 130 at once.
893
+ */
894
+ function withNativeBuildRun(root, details, fn, { write, exit = (code) => process.exit(code) }) {
895
+ const notice = nativeRunWaitNotice({ write });
896
+ return withWorkspaceProcessLock(workspaceDir(root), NATIVE_RUN_LOCK, async (claim) => {
897
+ cancelled = null;
898
+ const controller = new AbortController();
899
+ cancellationSignal = controller.signal;
900
+ const dispose = cancelOnInterrupt({
901
+ root,
902
+ claim,
903
+ write,
904
+ exit,
905
+ controller
906
+ });
907
+ try {
908
+ return await fn(claim);
909
+ } finally {
910
+ dispose();
911
+ cancellationSignal = void 0;
912
+ }
913
+ }, {
914
+ external: true,
915
+ waitMs: NATIVE_RUN_WAIT_MS,
916
+ declareSpawns: true,
917
+ details: { ...details },
918
+ onHeld: (holder) => notice(holder)
919
+ });
920
+ }
921
+ //#endregion
726
922
  //#region src/offload/client.ts
727
923
  const CONNECT_TIMEOUT_MS = 1e4;
728
924
  const OFFER_TIMEOUT_MS = 2e4;
@@ -750,9 +946,11 @@ function offloadMode(env = process.env) {
750
946
  return OFFLOAD_MODES.includes(raw) ? raw : "auto";
751
947
  }
752
948
  function buildPlacementCandidates(selected) {
949
+ const config = loadConfig$1();
753
950
  return {
951
+ localEnabled: selected !== "auto" || automaticMachineEnabled("build", "local", config),
754
952
  mode: selected === "local" ? "off" : offloadMode(),
755
- machines: selected === "local" ? [] : namedBuildMachine(selected) ? pairedMachines([selected]).slice(0, 1) : pairedMachines()
953
+ machines: selected === "local" ? [] : namedBuildMachine(selected) ? pairedMachines([selected]).slice(0, 1) : pairedMachines().filter((entry) => automaticMachineEnabled("build", entry.machine, config))
756
954
  };
757
955
  }
758
956
  function remotePhaseText(name, msg, machine) {
@@ -778,41 +976,58 @@ function placementLoad(choice) {
778
976
  * this Mac has a free `concurrency.maxBuilds` slot and its load per core is under `server.maxLoadPerCore`.
779
977
  * A named selection ignores local capacity and mode, and refuses an unsupported build or missing pairing.
780
978
  */
781
- function offloadPlacement({ mode, machines, here, unsupported, selected = "auto" }) {
979
+ function offloadPlacement({ mode, machines, here, unsupported, localEnabled = true, selected = "auto" }) {
782
980
  if (namedBuildMachine(selected)) {
783
981
  if (unsupported) throw new OffloadRefusal(selected, `${unsupported}, so a named remote Mac cannot take it`);
784
982
  if (machines === 0) throw new OffloadRefusal(selected, "no remote Mac is paired");
785
983
  return {
786
984
  offload: true,
985
+ code: "named",
787
986
  reason: `selected with --remote-build ${selected}`
788
987
  };
789
988
  }
790
989
  if (selected === "local") return {
791
990
  offload: false,
991
+ code: "local-selected",
792
992
  reason: "--remote-build local"
793
993
  };
994
+ if (!localEnabled) {
995
+ const reason = mode === "off" ? "remote.buildMode is off" : unsupported ?? (machines === 0 ? "no enabled remote Mac is paired" : null);
996
+ if (reason) throw new OffloadRefusal("auto", `${reason}; local is disabled in remote.buildPoolDisabled`);
997
+ return {
998
+ offload: true,
999
+ code: "local-disabled",
1000
+ reason: "local is disabled in remote.buildPoolDisabled"
1001
+ };
1002
+ }
794
1003
  if (mode === "off") return {
795
1004
  offload: false,
1005
+ code: "mode-off",
796
1006
  reason: "remote.buildMode is off"
797
1007
  };
798
1008
  if (machines === 0) return {
799
1009
  offload: false,
1010
+ code: "no-remote-mac",
800
1011
  reason: "no remote Mac is paired"
801
1012
  };
802
1013
  if (unsupported) return {
803
1014
  offload: false,
1015
+ code: "unsupported",
804
1016
  reason: unsupported
805
1017
  };
806
1018
  if (mode === "force") return {
807
1019
  offload: true,
1020
+ code: "forced",
808
1021
  reason: "remote.buildMode is force"
809
1022
  };
810
1023
  const busy = saturation(here);
811
1024
  return busy ? {
812
1025
  offload: true,
1026
+ code: "this-mac-busy",
813
1027
  reason: `this Mac is busy: ${busy} (${capacityText(here)})`
814
1028
  } : {
815
1029
  offload: false,
1030
+ code: "this-mac-free",
816
1031
  reason: `${capacityText(here)} here`
817
1032
  };
818
1033
  }
@@ -848,28 +1063,29 @@ function offerProblems(offer, target) {
848
1063
  * A machine that reports no load (a stim-server older than capacity) takes an `auto` build only while
849
1064
  * every local slot is busy. The rest rank warmest first, then least loaded.
850
1065
  */
851
- function pickOffer({ mode, here, offers, target, selected = "auto" }) {
1066
+ function pickOffer({ mode, here, offers, target, localEnabled = true, selected = "auto" }) {
852
1067
  const slotsFull = here.maxBuilds > 0 && here.builds >= here.maxBuilds;
853
1068
  const reasons = [];
1069
+ const candidates = [];
1070
+ const skip = (machine, code, msg, detail) => {
1071
+ reasons.push(`${machine}: ${msg}`);
1072
+ candidates.push({
1073
+ machine,
1074
+ code,
1075
+ msg,
1076
+ ...detail ? { detail } : {}
1077
+ });
1078
+ };
854
1079
  const ranked = [];
855
1080
  offers.forEach(({ machine, offer, failure }, index) => {
856
1081
  if (namedBuildMachine(selected) && machine !== selected) return;
857
- if (!offer) return void reasons.push(`${machine}: ${failure ?? "no offer"}`);
1082
+ if (!offer) return skip(machine, "unreachable", failure ?? "no offer");
858
1083
  const problems = offerProblems(offer, target);
859
- if (problems.length) {
860
- reasons.push(`${machine}: ${problems.map((problem) => problem.reason).join("; ")}`);
861
- return;
862
- }
1084
+ if (problems.length) return skip(machine, problemCode(problems[0]), problems.map((problem) => problem.reason).join("; "), problems.map((problem) => problem.code));
863
1085
  const load = offer.capacity.loadPerCore;
864
- if (selected === "auto" && mode === "auto" && !slotsFull) {
865
- if (typeof load !== "number") {
866
- reasons.push(`${machine}: capacity unknown (older stim-server) while this Mac has a free slot`);
867
- return;
868
- }
869
- if (load >= here.loadPerCore) {
870
- reasons.push(`${machine}: no less loaded (load ${load}/core there, ${here.loadPerCore}/core here)`);
871
- return;
872
- }
1086
+ if (selected === "auto" && localEnabled && mode === "auto" && !slotsFull) {
1087
+ if (typeof load !== "number") return skip(machine, "load", "capacity unknown (older stim-server) while this Mac has a free slot");
1088
+ if (load >= here.loadPerCore) return skip(machine, "load", `no less loaded (load ${load}/core there, ${here.loadPerCore}/core here)`);
873
1089
  }
874
1090
  const score = Number(offer.warm.checkout) + Number(offer.warm.dependencies) + Number(offer.warm.build);
875
1091
  ranked.push({
@@ -877,13 +1093,22 @@ function pickOffer({ mode, here, offers, target, selected = "auto" }) {
877
1093
  score,
878
1094
  load: typeof load === "number" ? load : Number.POSITIVE_INFINITY
879
1095
  });
1096
+ candidates.push({
1097
+ machine,
1098
+ code: "accepted",
1099
+ msg: typeof load === "number" ? `can take the build (load ${load}/core there)` : "can take the build"
1100
+ });
880
1101
  });
881
1102
  ranked.sort((a, b) => b.score - a.score || a.load - b.load);
882
1103
  return {
883
1104
  order: ranked.map((each) => each.index),
884
- reasons
1105
+ reasons,
1106
+ candidates
885
1107
  };
886
1108
  }
1109
+ function problemCode({ code }) {
1110
+ return code === "disk" || code === "busy" || code === "unreachable" ? code : code === "runtime" ? "no-matching-device" : "version-mismatch";
1111
+ }
887
1112
  const openSockets = /* @__PURE__ */ new Set();
888
1113
  let closingOnExit = false;
889
1114
  /**
@@ -966,7 +1191,32 @@ var BuildConnection = class BuildConnection {
966
1191
  socket.on("error", () => {});
967
1192
  }
968
1193
  /** Resolves why it could not connect; `refused` when the machine answered and turned this Mac away. */
969
- static open(target, token, timeoutMs, capability = "build") {
1194
+ static async open(target, token, timeoutMs, capability = "build") {
1195
+ const started = performance.now();
1196
+ const result = await BuildConnection.connect(target, token, timeoutMs, capability);
1197
+ const ms = Math.round(performance.now() - started);
1198
+ debugLog.log("remote_connect", {
1199
+ host: target.host,
1200
+ capability,
1201
+ ms,
1202
+ timeoutMs,
1203
+ ...result instanceof BuildConnection ? { ok: true } : {
1204
+ ok: false,
1205
+ failure: result.failure,
1206
+ code: result.code
1207
+ }
1208
+ });
1209
+ if (!(result instanceof BuildConnection)) reportRemoteFailure("remote_connect_failed", {
1210
+ host: target.host,
1211
+ capability,
1212
+ ms,
1213
+ timeoutMs,
1214
+ msg: result.failure,
1215
+ code: result.code
1216
+ });
1217
+ return result;
1218
+ }
1219
+ static connect(target, token, timeoutMs, capability) {
970
1220
  return new Promise((resolve) => {
971
1221
  const options = {
972
1222
  handshakeTimeout: timeoutMs,
@@ -996,7 +1246,8 @@ var BuildConnection = class BuildConnection {
996
1246
  protocol: 1,
997
1247
  client: {
998
1248
  name: "stim",
999
- version: "1"
1249
+ version: "1",
1250
+ runId: runId()
1000
1251
  },
1001
1252
  auth: { deviceToken: token }
1002
1253
  }
@@ -1028,17 +1279,36 @@ var BuildConnection = class BuildConnection {
1028
1279
  message: this.closed
1029
1280
  } });
1030
1281
  const id = this.nextId++;
1282
+ const started = performance.now();
1283
+ const done = (reply) => {
1284
+ const ms = Math.round(performance.now() - started);
1285
+ debugLog.log("remote_request", {
1286
+ method,
1287
+ ms,
1288
+ ..."error" in reply ? {
1289
+ ok: false,
1290
+ code: reply.error.code
1291
+ } : { ok: true }
1292
+ });
1293
+ if ("error" in reply) reportRemoteFailure("remote_request_failed", {
1294
+ method,
1295
+ ms,
1296
+ code: reply.error.code,
1297
+ msg: reply.error.message
1298
+ });
1299
+ return reply;
1300
+ };
1031
1301
  return new Promise((resolve) => {
1032
1302
  const timer = setTimeout(() => {
1033
1303
  this.pending.delete(id);
1034
- resolve({ error: {
1304
+ resolve(done({ error: {
1035
1305
  code: "timeout",
1036
1306
  message: `${method} got no reply in ${timeoutMs / 1e3} s`
1037
- } });
1307
+ } }));
1038
1308
  }, timeoutMs);
1039
1309
  this.pending.set(id, (reply) => {
1040
1310
  clearTimeout(timer);
1041
- resolve(reply);
1311
+ resolve(done(reply));
1042
1312
  });
1043
1313
  this.socket.send(JSON.stringify({
1044
1314
  id,
@@ -1048,6 +1318,7 @@ var BuildConnection = class BuildConnection {
1048
1318
  });
1049
1319
  }
1050
1320
  async sendBinary(frame) {
1321
+ if (this.closed) return;
1051
1322
  this.socket.send(frame, { binary: true });
1052
1323
  while (this.socket.bufferedAmount > MAX_BUFFERED && !this.closed) await new Promise((resolve) => setTimeout(resolve, 5));
1053
1324
  }
@@ -1085,6 +1356,14 @@ var BuildConnection = class BuildConnection {
1085
1356
  }
1086
1357
  forget() {
1087
1358
  this.ended = true;
1359
+ this.closed ??= "the connection was closed by this client";
1360
+ for (const resolve of this.pending.values()) resolve({ error: {
1361
+ code: "closed",
1362
+ message: this.closed
1363
+ } });
1364
+ this.pending.clear();
1365
+ this.progress = null;
1366
+ this.binary = null;
1088
1367
  clearInterval(this.keepalive);
1089
1368
  this.socket.removeAllListeners("close");
1090
1369
  openSockets.delete(this.socket);
@@ -1189,7 +1468,8 @@ async function probeMachine(credential, identity, { connectMs = CONNECT_TIMEOUT_
1189
1468
  };
1190
1469
  const reply = await connection.request("build.offer", {
1191
1470
  repo: identity.repo,
1192
- ...identity.lockfile ? { lockfile: identity.lockfile } : {}
1471
+ ...identity.lockfile ? { lockfile: identity.lockfile } : {},
1472
+ ...identity.rubyVersion ? { rubyVersion: identity.rubyVersion } : {}
1193
1473
  }, offerMs);
1194
1474
  const failure = replyError(reply);
1195
1475
  if (failure || !("result" in reply)) {
@@ -1209,7 +1489,7 @@ async function probeMachine(credential, identity, { connectMs = CONNECT_TIMEOUT_
1209
1489
  * Asks every paired machine for an offer in parallel and keeps the connection to the one `pickOffer` chooses, or
1210
1490
  * returns why none takes the build. A named selection probes only its credential and skips auto load gating.
1211
1491
  */
1212
- async function chooseBuildMachine({ projectRoot, target, mode, here, note, machines = pairedMachines(), selected = "auto" }) {
1492
+ async function chooseBuildMachine({ projectRoot, target, mode, here, note, machines = pairedMachines(), selected = "auto", onCandidates }) {
1213
1493
  const started = Date.now();
1214
1494
  let identity;
1215
1495
  try {
@@ -1218,8 +1498,14 @@ async function chooseBuildMachine({ projectRoot, target, mode, here, note, machi
1218
1498
  return `this app is not in a git checkout (${error.message.split("\n")[0]})`;
1219
1499
  }
1220
1500
  if (namedBuildMachine(selected)) machines = machines.filter((each) => each.machine === selected).slice(0, 1);
1221
- const asked = await Promise.all(machines.map((credential) => probeMachine(credential, identity)));
1222
- const { order, reasons } = pickOffer({
1501
+ else machines = machines.filter((each) => automaticMachineEnabled("build", each.machine));
1502
+ const rubyVersion = readRubyVersion(projectRoot) ?? void 0;
1503
+ const asked = await Promise.all(machines.map((credential) => probeMachine(credential, {
1504
+ ...identity,
1505
+ rubyVersion
1506
+ })));
1507
+ const { order, reasons, candidates } = pickOffer({
1508
+ localEnabled: selected !== "auto" || automaticMachineEnabled("build", "local"),
1223
1509
  selected,
1224
1510
  mode,
1225
1511
  here,
@@ -1233,6 +1519,7 @@ async function chooseBuildMachine({ projectRoot, target, mode, here, note, machi
1233
1519
  asked.forEach((each, at) => {
1234
1520
  if (!order.includes(at) && "connection" in each) each.connection.close();
1235
1521
  });
1522
+ onCandidates?.(candidates);
1236
1523
  if (order.length === 0) return reasons.length ? reasons.join("; ") : "no remote Mac is paired";
1237
1524
  for (const reason of reasons) note(reason);
1238
1525
  const [first, ...rest] = order.map((at) => {
@@ -1249,7 +1536,8 @@ async function chooseBuildMachine({ projectRoot, target, mode, here, note, machi
1249
1536
  target,
1250
1537
  offerMs: Date.now() - started,
1251
1538
  identity,
1252
- runnersUp: rest
1539
+ runnersUp: rest,
1540
+ automatic: selected === "auto"
1253
1541
  };
1254
1542
  }
1255
1543
  const ARTIFACT_NAME = {
@@ -1262,7 +1550,7 @@ const ARTIFACT_NAME = {
1262
1550
  * `RESUME_WINDOW_MS`, shorter than the machine's grace for a dropped connection, has passed. Returns why it
1263
1551
  * could not.
1264
1552
  */
1265
- async function resumeJob(credential, job, abandoned) {
1553
+ async function resumeJob(credential, job, abandoned, signal) {
1266
1554
  const deadline = Date.now() + RESUME_WINDOW_MS;
1267
1555
  let delay = RESUME_DELAY_MS;
1268
1556
  let last = "no attempt";
@@ -1276,9 +1564,20 @@ async function resumeJob(credential, job, abandoned) {
1276
1564
  if (connection.refused) return `the machine turned this Mac away (${connection.failure})`;
1277
1565
  last = connection.failure;
1278
1566
  } else {
1567
+ if (abandoned()) {
1568
+ connection.close();
1569
+ break;
1570
+ }
1279
1571
  const early = [];
1280
1572
  connection.onProgress((event) => early.push(event));
1573
+ const cancel = () => connection.close();
1574
+ signal?.addEventListener("abort", cancel, { once: true });
1281
1575
  const reply = await connection.request("build.attach", { job }, OFFER_TIMEOUT_MS);
1576
+ signal?.removeEventListener("abort", cancel);
1577
+ if (abandoned()) {
1578
+ connection.close();
1579
+ break;
1580
+ }
1282
1581
  if ("result" in reply) {
1283
1582
  const outcome = isJsonObject(reply.result) ? reply.result.outcome : null;
1284
1583
  return {
@@ -1294,8 +1593,8 @@ async function resumeJob(credential, job, abandoned) {
1294
1593
  }
1295
1594
  connection.drop();
1296
1595
  }
1297
- if (Date.now() + delay > deadline) break;
1298
- await new Promise((resolve) => setTimeout(resolve, delay));
1596
+ if (abandoned() || Date.now() + delay > deadline) break;
1597
+ await setTimeout$1(delay, void 0, { signal }).catch(() => {});
1299
1598
  delay = Math.min(delay * 2, RESUME_MAX_DELAY_MS);
1300
1599
  }
1301
1600
  return `no connection to it within ${RESUME_WINDOW_MS / 6e4} min (${last})`;
@@ -1363,23 +1662,36 @@ async function offloadBuild({ choice, expectedFingerprint, request, stagingDir,
1363
1662
  reason: reason.split("\n")[0].slice(0, 300)
1364
1663
  };
1365
1664
  };
1665
+ const signal = runCancellationSignal();
1666
+ const throwIfCancelled = () => {
1667
+ if (signal?.aborted) throw Object.assign(/* @__PURE__ */ new Error("The offloaded build was cancelled."), { code: "STIM_CANCELLED" });
1668
+ };
1669
+ let settle;
1670
+ let settled = false;
1671
+ const outcome = new Promise((resolve) => {
1672
+ settle = (value) => {
1673
+ settled = true;
1674
+ resolve(value);
1675
+ };
1676
+ });
1677
+ const cancel = () => {
1678
+ settle({
1679
+ ok: false,
1680
+ code: "cancelled"
1681
+ });
1682
+ closeOffload(choice);
1683
+ };
1684
+ signal?.addEventListener("abort", cancel, { once: true });
1366
1685
  try {
1686
+ throwIfCancelled();
1367
1687
  let job = null;
1368
1688
  let early = [];
1369
- let settle;
1370
- let settled = false;
1371
- const outcome = new Promise((resolve) => {
1372
- settle = (value) => {
1373
- settled = true;
1374
- resolve(value);
1375
- };
1376
- });
1377
1689
  let resuming = false;
1378
1690
  const reattach = async (why) => {
1379
1691
  if (resuming || settled) return;
1380
1692
  resuming = true;
1381
1693
  note(`offload: the connection to ${choice.machine} dropped (${why}); reattaching to the build there`);
1382
- const resumed = await resumeJob(choice.credential, job, () => settled);
1694
+ const resumed = await resumeJob(choice.credential, job, () => settled, signal);
1383
1695
  resuming = false;
1384
1696
  if (settled) {
1385
1697
  if (typeof resumed !== "string") resumed.connection.close();
@@ -1422,7 +1734,13 @@ async function offloadBuild({ choice, expectedFingerprint, request, stagingDir,
1422
1734
  return true;
1423
1735
  };
1424
1736
  for (;;) {
1737
+ if (choice.automatic && !automaticMachineEnabled("build", choice.machine)) {
1738
+ const reason = `${choice.machine} is disabled in remote.buildPoolDisabled`;
1739
+ if (moveOn(reason)) continue;
1740
+ return fail(reason);
1741
+ }
1425
1742
  const synced = await syncSource(choice.connection, identity, onEnter);
1743
+ throwIfCancelled();
1426
1744
  if ("failure" in synced) {
1427
1745
  if (moveOn(synced.failure)) continue;
1428
1746
  return fail(synced.failure);
@@ -1432,6 +1750,8 @@ async function offloadBuild({ choice, expectedFingerprint, request, stagingDir,
1432
1750
  workerStarted = Date.now();
1433
1751
  early = [];
1434
1752
  choice.connection.onProgress((event) => job === null ? early.push(event) : handle(event));
1753
+ if (choice.automatic) requireAutomaticMachine("build", choice.machine);
1754
+ throwIfCancelled();
1435
1755
  const reply = await choice.connection.request("build.start", {
1436
1756
  repo: identity.repo,
1437
1757
  project: identity.project,
@@ -1454,6 +1774,7 @@ async function offloadBuild({ choice, expectedFingerprint, request, stagingDir,
1454
1774
  },
1455
1775
  stimBuild: choice.target.local.stimBuild
1456
1776
  });
1777
+ throwIfCancelled();
1457
1778
  if ("result" in reply) {
1458
1779
  job = reply.result.job;
1459
1780
  break;
@@ -1471,6 +1792,7 @@ async function offloadBuild({ choice, expectedFingerprint, request, stagingDir,
1471
1792
  }), JOB_TIMEOUT_MS);
1472
1793
  const result = await outcome;
1473
1794
  clearTimeout(timer);
1795
+ throwIfCancelled();
1474
1796
  const { connection, machine } = choice;
1475
1797
  connection.onProgress(null);
1476
1798
  const workerMs = Date.now() - workerStarted;
@@ -1479,6 +1801,7 @@ async function offloadBuild({ choice, expectedFingerprint, request, stagingDir,
1479
1801
  const name = typeof artifact?.name === "string" ? artifact.name : "";
1480
1802
  if (!ARTIFACT_NAME[request.platform].test(name) || typeof artifact.sha256 !== "string") return fail(`the machine reported no .${request.platform === "android" ? "apk" : "app"} artifact`);
1481
1803
  onEnter("fetch");
1804
+ throwIfCancelled();
1482
1805
  const fetchStarted = Date.now();
1483
1806
  rmSync(stagingDir, {
1484
1807
  recursive: true,
@@ -1508,6 +1831,7 @@ async function offloadBuild({ choice, expectedFingerprint, request, stagingDir,
1508
1831
  const fetched = await connection.request("build.artifact", { job }, 9e5);
1509
1832
  connection.onBinary(null);
1510
1833
  closeSync(fd);
1834
+ throwIfCancelled();
1511
1835
  const fetchFailure = replyError(fetched);
1512
1836
  if (fetchFailure || !("result" in fetched)) return fail(`fetch: ${fetchFailure ?? "no reply"}`);
1513
1837
  const digest = hash.digest("hex");
@@ -1520,6 +1844,7 @@ async function offloadBuild({ choice, expectedFingerprint, request, stagingDir,
1520
1844
  "-C",
1521
1845
  stagingDir
1522
1846
  ], { timeoutMs: 6e5 });
1847
+ throwIfCancelled();
1523
1848
  rmSync(archive, { force: true });
1524
1849
  const artifactPath = join(stagingDir, name);
1525
1850
  if (request.platform === "ios" && !existsSync(join(artifactPath, "Info.plist"))) return fail(`fetch: ${name} has no Info.plist`);
@@ -1556,7 +1881,11 @@ async function offloadBuild({ choice, expectedFingerprint, request, stagingDir,
1556
1881
  }
1557
1882
  };
1558
1883
  } catch (error) {
1884
+ closeOffload(choice);
1885
+ throwIfCancelled();
1559
1886
  return fail(error.message.split("\n")[0] ?? String(error));
1887
+ } finally {
1888
+ signal?.removeEventListener("abort", cancel);
1560
1889
  }
1561
1890
  }
1562
1891
  function packageName(projectRoot) {
@@ -1634,7 +1963,10 @@ function offloadCheck(projectRoot, targets) {
1634
1963
  reason: `this app is not in a git checkout (${identity})`
1635
1964
  }]
1636
1965
  };
1637
- const probe = await probeMachine(credential, identity, {
1966
+ const probe = await probeMachine(credential, {
1967
+ ...identity,
1968
+ rubyVersion: readRubyVersion(projectRoot) ?? void 0
1969
+ }, {
1638
1970
  connectMs: DOCTOR_CONNECT_MS,
1639
1971
  offerMs: DOCTOR_OFFER_MS
1640
1972
  });
@@ -1662,4 +1994,4 @@ function offloadCheck(projectRoot, targets) {
1662
1994
  };
1663
1995
  }
1664
1996
  //#endregion
1665
- export { realIo as S, configuredMachines as _, offloadBuild as a, parseMachine$1 as b, placementLoad as c, OffloadRefusal as d, namedBuildMachine as f, inspectBuildMachines as g, resolveBuildPlacement as h, closeOffload as i, remotePhaseText as l, resolveBuildMachine as m, buildPlacementCandidates as n, offloadCheck as o, parseBuildMachineOption as p, chooseBuildMachine as r, offloadPlacement as s, BuildConnection as t, simulatorRuntime as u, hostingMachines as v, pinnedEndpoint$1 as x, inspectDeviceHostMachines as y };
1997
+ export { resolveBuildPlacement as A, setRemoteLogSink as C, parseBuildMachineOption as D, namedBuildMachine as E, parseMachine$1 as F, pinnedEndpoint$1 as I, realIo as L, configuredMachines as M, hostingMachines as N, requireLocalBuild as O, inspectDeviceHostMachines as P, withNativeBuildRun as S, buildPlacementSettings as T, nativeRunHolder as _, offloadBuild as a, runCancellation as b, placementLoad as c, NATIVE_RUN_LOCK as d, NATIVE_RUN_WAIT_MS as f, describeNativeRunHolder as g, decideStopAction as h, closeOffload as i, inspectBuildMachines as j, resolveBuildMachine as k, remotePhaseText as l, clearNativeRunCancel as m, buildPlacementCandidates as n, offloadCheck as o, cancelledFailure as p, chooseBuildMachine as r, offloadPlacement as s, BuildConnection as t, simulatorRuntime as u, nativeRunWaitNotice as v, OffloadRefusal as w, runCancellationSignal as x, requestNativeRunCancel as y };