stim 1.34.0 → 1.36.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 (152) hide show
  1. package/dist/{acquire-artifact-B0SWoVNt.mjs → acquire-artifact-DGTHCd0Q.mjs} +19 -3
  2. package/dist/{act-7f7wIvCQ.mjs → act-CvN9xyV6.mjs} +18 -19
  3. package/dist/{activity-DkS-Ir6V.mjs → activity-BpdDvA8Q.mjs} +4 -4
  4. package/dist/{agent-device-usage-output-BVdgKbfz.mjs → agent-device-usage-output-pbhaW7_k.mjs} +4 -4
  5. package/dist/{android-B2DSKg-A.mjs → android-5wkcMFDG.mjs} +1 -1
  6. package/dist/{android-BU4uBX8I.mjs → android-B0qTdwnt.mjs} +5 -5
  7. package/dist/{android-Iq0M2t2d.mjs → android-SJx1V3R3.mjs} +5 -5
  8. package/dist/{android-cas-Dhcjkys1.mjs → android-cas-BS9_55U5.mjs} +4 -4
  9. package/dist/{android-D7qjT2fq.mjs → android-e9WP10cE.mjs} +47 -41
  10. package/dist/{android-image-DFk26aY6.mjs → android-image-BZ57YL6R.mjs} +2 -2
  11. package/dist/{android-project-DhO6el8N.d.mts → android-project-v_1nej_x.d.mts} +3 -1
  12. package/dist/api.d.mts +20 -6
  13. package/dist/api.mjs +31 -26
  14. package/dist/{apk-signed-tail-CPpA-3vu.mjs → apk-signed-tail-eZyXIrnc.mjs} +2 -2
  15. package/dist/{artifact-DmRtKFXW.mjs → artifact-C3srrGVU.mjs} +36 -14
  16. package/dist/{artifact-CNnTZs3R.mjs → artifact-ubNdxo_K.mjs} +52 -19
  17. package/dist/{auto-placement-DA65VQH7.mjs → auto-placement-CKFpFCuq.mjs} +7 -8
  18. package/dist/{browser-web-Dn_hgKZC.mjs → browser-web-B63vLrj6.mjs} +11 -11
  19. package/dist/{build-D7SuVoRX.mjs → build-B_Dx9SKJ.mjs} +72 -25
  20. package/dist/{build-cache-Cqj4FGnX.mjs → build-cache-BW8T4oD2.mjs} +10 -5
  21. package/dist/{build-plan-Ct1FiA3E.mjs → build-plan-CeIooJqn.mjs} +6 -6
  22. package/dist/{cache-manifest-8JX9S1QJ.mjs → cache-manifest-CxWkZmUr.mjs} +1 -1
  23. package/dist/cache-manifest.mjs +1 -1
  24. package/dist/{cli-DjdvQG5U.mjs → cli-COdXswmc.mjs} +19 -18
  25. package/dist/cli.mjs +1 -1
  26. package/dist/{client-kU37gURU.mjs → client-KSWIeQLA.mjs} +9 -8
  27. package/dist/collector-run.mjs +4 -4
  28. package/dist/{command-output-DjExb_tZ.mjs → command-output-T3cDctRE.mjs} +1 -1
  29. package/dist/{config-BW0fAtoK.mjs → config-BA3TgoIP.mjs} +59 -10
  30. package/dist/{dependencies-CZ7c8XX_.mjs → dependencies-LH-7k1Vr.mjs} +35 -38
  31. package/dist/{dependency-state-mL5g4H6K.mjs → dependency-state-Blfp3AkG.mjs} +1 -1
  32. package/dist/{dev-client-D3wOllNd.mjs → dev-client-MO_DjTZr.mjs} +2 -2
  33. package/dist/{device-DH5yf6TU.mjs → device-BZWt_aPJ.mjs} +11 -11
  34. package/dist/{device-CWPFAqs0.mjs → device-DNTWFFB1.mjs} +12 -12
  35. package/dist/{device-android-ChfF5I9A.mjs → device-android-BB9Lugr8.mjs} +8 -8
  36. package/dist/{ccache-ChBMXlBq.mjs → device-capacity-CpAVcKgq.mjs} +1006 -23
  37. package/dist/device-host-worker.mjs +13 -14
  38. package/dist/{device-pool-EZziQ0uS.mjs → device-pool-BJToGG-q.mjs} +3 -3
  39. package/dist/{device-provision-CfpEy-Vm.mjs → device-provision-CxQDCtrc.mjs} +9 -9
  40. package/dist/{doctor-CGBsS_68.mjs → doctor-C-46SkBe.mjs} +1 -1
  41. package/dist/{doctor-dwRboaXx.mjs → doctor-CYZ45LnX.mjs} +20 -21
  42. package/dist/{doctor-B5uEZiKp.mjs → doctor-DV639Iw_.mjs} +15 -16
  43. package/dist/doctor-fingerprint.mjs +1 -1
  44. package/dist/{doctor-watchman-r7M9UPi0.mjs → doctor-watchman-TZxYt_98.mjs} +4 -4
  45. package/dist/{eas-build-BpHIt_M3.mjs → eas-build-Xf5Jd6LR.mjs} +2 -2
  46. package/dist/{eas-fallback-CNPcjq3K.mjs → eas-fallback-DNYPNinw.mjs} +4 -4
  47. package/dist/{eas-session-ledger-CXwagcQ7.mjs → eas-session-ledger-CXZ4aix0.mjs} +5 -5
  48. package/dist/{error-diagnostics-Ym2mjmJ0.mjs → error-diagnostics-CvB5bpPU.mjs} +8 -8
  49. package/dist/{gc-B464w_a5.mjs → gc-8Qm_K1iy.mjs} +2 -2
  50. package/dist/{gc-C7YxKNci.mjs → gc-DiUhrVFo.mjs} +17 -17
  51. package/dist/{guide-BFW-cB7R.mjs → guide-CuvHRP53.mjs} +130 -65
  52. package/dist/{hosted-android-B7yfqXEy.mjs → hosted-android-D__HGVIO.mjs} +3 -3
  53. package/dist/{hosted-client-CQuzZwAu.mjs → hosted-client-DMOOu8vD.mjs} +6 -6
  54. package/dist/{hosted-logs-DfYCqrrR.mjs → hosted-logs-CClIktQM.mjs} +6 -6
  55. package/dist/{hosted-logs-sync-Dlpa2mQF.mjs → hosted-logs-sync-B7XCeqVm.mjs} +5 -5
  56. package/dist/{hosted-macos-JKefapEm.mjs → hosted-macos-D4sDJIbB.mjs} +5 -5
  57. package/dist/{hosted-native-B7OFt5gB.mjs → hosted-native-BiiUG_Q6.mjs} +7 -7
  58. package/dist/{ios-sUPbWqEh.mjs → ios-BZuhvLGr.mjs} +8 -8
  59. package/dist/{ios-BdZdNsAj.mjs → ios-BhfpRfNf.mjs} +4 -4
  60. package/dist/{ios-DSBs9c6d.mjs → ios-BhpUWyN7.mjs} +39 -32
  61. package/dist/{ios-device-Drplrgzo.mjs → ios-device-vhYL1N--.mjs} +1 -1
  62. package/dist/{ios-project-DoTQYghg.mjs → ios-project-Cz0S1Sz-.mjs} +7 -7
  63. package/dist/{ios-signing-R34vbrNU.mjs → ios-signing-BcH88ecn.mjs} +1 -1
  64. package/dist/{ios-state-CWsBysyN.mjs → ios-state-Avwz2cbZ.mjs} +1 -1
  65. package/dist/{js-swap-DrokhOfo.mjs → js-swap-Desu2iII.mjs} +4 -4
  66. package/dist/{launch-Cy-MWEZc.mjs → launch-6ckG9UrC.mjs} +19 -19
  67. package/dist/{lightened-ALwQcQtV.mjs → lightened-B6TnpHMX.mjs} +4 -4
  68. package/dist/{logs-DuED-01Z.mjs → logs-B-DMy1K0.mjs} +7 -7
  69. package/dist/{logs-DWT7jGpF.mjs → logs-CO0CJRiC.mjs} +5 -5
  70. package/dist/{macos-CtBtA_hW.mjs → macos-B1aCXhem.mjs} +19 -19
  71. package/dist/{macos-kWGcfLzx.mjs → macos-CduEmWAK.mjs} +1 -1
  72. package/dist/macos-run.mjs +1 -1
  73. package/dist/maintenance-run.mjs +5 -5
  74. package/dist/{metro-B5PVz0Wv.mjs → metro-DM3yJL1M.mjs} +2 -2
  75. package/dist/{metro-request-2LmFpC1z.mjs → metro-request-O8Q-JvBg.mjs} +1 -1
  76. package/dist/{named-ports-CA0nB9Yw.mjs → named-ports-BvB_CO9B.mjs} +2 -2
  77. package/dist/{native-android-tISP2EVp.mjs → native-android-28NOG2-y.mjs} +17 -17
  78. package/dist/{native-gradle-artifact-inputs-DVoeZkrw.mjs → native-gradle-artifact-inputs-DxVEtS6_.mjs} +3 -3
  79. package/dist/{native-runtime-CNgxeOCG.mjs → native-runtime-C3_NkUlW.mjs} +6 -6
  80. package/dist/{native-xcode-inputs-DGzkhpu4.mjs → native-xcode-inputs-BWFTw5cJ.mjs} +2 -2
  81. package/dist/{native-xcode-ios-B9nsfId2.mjs → native-xcode-ios-B4Qoc7-U.mjs} +16 -16
  82. package/dist/offload-worker.d.mts +1 -1
  83. package/dist/offload-worker.mjs +19 -19
  84. package/dist/{ownership-hC7xvvxF.mjs → ownership-C3y8wLSl.mjs} +2 -2
  85. package/dist/{phone-reverses-Bo0EzJ-P.mjs → phone-reverses-BhSxbqom.mjs} +4 -4
  86. package/dist/{placement-log-BsGvKLtU.mjs → placement-log-B8RCSzOm.mjs} +1 -1
  87. package/dist/{plan-placement-Am0IjuxB.mjs → plan-placement-BI4r55aP.mjs} +6 -6
  88. package/dist/{ports-CNzg1LYD.mjs → ports-DhOSyWm9.mjs} +3 -3
  89. package/dist/{prebuild-BfJII57G.mjs → prebuild-Di2IhUue.mjs} +9 -9
  90. package/dist/{project-DU3jjnBE.mjs → project-Clk1JAWL.mjs} +15 -15
  91. package/dist/{react-native-android-3dZiJv-4.mjs → react-native-android-bdHBMrnf.mjs} +23 -23
  92. package/dist/{react-native-build-CuvNPt-q.mjs → react-native-build-CJega_NM.mjs} +7 -7
  93. package/dist/{react-native-doctor-a35BwMUO.mjs → react-native-doctor-CY7BGITE.mjs} +5 -5
  94. package/dist/{react-native-ios-CqnReM1t.mjs → react-native-ios-Bo5EDVel.mjs} +25 -20
  95. package/dist/recorded-build-BuGL_fnm.mjs +58 -0
  96. package/dist/{recordings-AKZfsW2W.mjs → recordings-6i2-x8LU.mjs} +1 -1
  97. package/dist/{reload-CC3sNo10.mjs → reload-A_axEbeS.mjs} +14 -14
  98. package/dist/{reload-YPnvXGlB.mjs → reload-BvDuvygc.mjs} +1 -1
  99. package/dist/remote-PD6CM0xp.mjs +158 -0
  100. package/dist/{remote-BfaIHnhM.mjs → remote-WK_swZEw.mjs} +8 -8
  101. package/dist/{result-H-AxY7Ud.mjs → result-BkDpN5Mz.mjs} +23 -18
  102. package/dist/{run-N-BbRxnC.mjs → run-CTFi4Nq2.mjs} +1 -1
  103. package/dist/{server-bare-BTlICuaX.mjs → server-bare-W0aPsRZE.mjs} +1 -1
  104. package/dist/{server-command-Bn_gOObU.mjs → server-command-S00-ddjK.mjs} +4 -4
  105. package/dist/server-expo-BWJDa7Qb.mjs +2 -0
  106. package/dist/{server-expo-CUJ2fuv1.mjs → server-expo-cc-cmfsl.mjs} +2 -2
  107. package/dist/{settings-B5IBDfTx.mjs → settings-Bap-qjnp.mjs} +1 -1
  108. package/dist/{settings-DoWHJopy.mjs → settings-C_Xu4RNZ.mjs} +2 -2
  109. package/dist/{settings-payload-Bxw3hid5.mjs → settings-payload-D5jjhmrw.mjs} +4 -4
  110. package/dist/{settings-write-C6d7EC54.mjs → settings-write-C5huI-iS.mjs} +5 -5
  111. package/dist/settings.schema.json +1 -1
  112. package/dist/{simslim-DKdJx0LL.mjs → simslim-DlBvwgeF.mjs} +1 -1
  113. package/dist/{slot-launch-kq0-7w9C.mjs → slot-launch-qEQVTth7.mjs} +6 -6
  114. package/dist/{stage-CBfMTUK3.mjs → stage-CxVI_MlP.mjs} +1 -1
  115. package/dist/{start-Diu8q6_U.mjs → start-CVzivp35.mjs} +1 -1
  116. package/dist/{start-WTMumMHi.mjs → start-YpT_JpQQ.mjs} +10 -10
  117. package/dist/{state-DSOUqO-o.mjs → state-B4B5P4LU.mjs} +1 -1
  118. package/dist/{state-CtmYTRWm.mjs → state-B9VFI6r0.mjs} +2 -2
  119. package/dist/{stats-D4cW247z.mjs → stats-COoePEri.mjs} +4 -4
  120. package/dist/{status-CEwxo-PS.mjs → status-CIg0k7Qb.mjs} +21 -22
  121. package/dist/{status-B-oeWS0Z.mjs → status-CIvU5-TF.mjs} +1 -1
  122. package/dist/{status-DTFhDnd1.mjs → status-CQWmNl1f.mjs} +1 -1
  123. package/dist/{status-watch-D63R5nSI.mjs → status-watch--rfZynD_.mjs} +4 -4
  124. package/dist/status-worker.mjs +1 -1
  125. package/dist/{stim-desktop-_WTria-r.mjs → stim-desktop-D0P_bzmd.mjs} +1 -1
  126. package/dist/{stop-HL2RvhrE.mjs → stop-BEBBruvD.mjs} +4 -4
  127. package/dist/{stop-Dm9ON6Os.mjs → stop-BiI4_VFG.mjs} +2 -2
  128. package/dist/{stop-vX4w3_8V.mjs → stop-C3d5qG7C.mjs} +15 -15
  129. package/dist/supervisor-run.mjs +12 -13
  130. package/dist/support-BJbXovoO.mjs +2089 -0
  131. package/dist/{support-BNe1_Hg7.mjs → support-Vckdzrmk.mjs} +5 -5
  132. package/dist/{swiftpm-artifact-inputs-uq5oV7xR.mjs → swiftpm-artifact-inputs-DbuF-Yrr.mjs} +1 -1
  133. package/dist/{swiftpm-macos-ClF203_2.mjs → swiftpm-macos-Bu8xeBfM.mjs} +8 -8
  134. package/dist/{toolchain-CECahBDa.mjs → toolchain-CIRdNl-c.mjs} +1 -1
  135. package/dist/{toolchain-CYonNjd_.mjs → toolchain-DCnWAxQL.mjs} +71 -7
  136. package/dist/{warm-progress-q11Vdvhh.mjs → warm-progress-DwsGBpph.mjs} +3 -3
  137. package/dist/{web-BdaMBr2O.mjs → web-D2En0Wa5.mjs} +8 -8
  138. package/dist/web-Dpj70t4p.mjs +2 -0
  139. package/dist/web-run.mjs +4 -4
  140. package/dist/{workspace-state-BdZ-QgmB.mjs → workspace-state-BhwAJBgA.mjs} +2 -2
  141. package/dist/{workspaces-B9v5V7Es.mjs → workspaces-Dh5N70qW.mjs} +11 -11
  142. package/dist/{worktree-C4MBOMz7.mjs → worktree-DLm4s2Ib.mjs} +1 -1
  143. package/dist/{worktree-C3N86KpI.mjs → worktree-Yw3EW77B.mjs} +24 -24
  144. package/package.json +7 -7
  145. package/dist/device-capacity-CHC3dKWg.mjs +0 -867
  146. package/dist/device-ios-B3-Ah--R.mjs +0 -716
  147. package/dist/device-lease-run-i3FNX4OE.mjs +0 -282
  148. package/dist/idle-shutdown-DPD8oV36.mjs +0 -142
  149. package/dist/selection-CH-MUVGD.mjs +0 -791
  150. package/dist/server-expo-B59N16uT.mjs +0 -2
  151. package/dist/support-ivsfDj0v.mjs +0 -287
  152. package/dist/web-CIpHpGp8.mjs +0 -2
@@ -0,0 +1,2089 @@
1
+ import { C as operationState, D as ownership_claim_exports, l as cancellableSleep, n as isTimeoutError, p as withWorkspaceProcessLock, r as reportSignal, t as getExecutor } from "./exec-CGuDzt40.mjs";
2
+ import { D as workspaceId$1, M as withDirLock$1, k as workspaceLogsDir$1 } from "./workspace-state-BhwAJBgA.mjs";
3
+ import { a as formatElapsed, h as shortUdid, l as phaseLine, r as clockTime } from "./command-output-T3cDctRE.mjs";
4
+ import { $ as notOnTailnetDiagnosis, A as withConfigLockAsync$1, B as findPeer$1, G as reachMachine, I as diagnoseOutcome, K as realIo, L as diagnoseTailscale, Q as diagnosisFinding, T as setDevice, U as outdatedServer, W as parseMachine$1, X as tailscaleStatusAsync, Y as setupRemedy, Z as diagnoseLocalUnreachable, a as clearDevice, d as getConfigDir, et as unconfirmedKeyFinding, ft as deviceSlotKey, gt as projectDeviceSlots, pt as deviceSlotPlatforms, tt as readHostPermissions, v as loadConfig$1, w as saveConfig, z as endpoint$1 } from "./config-BA3TgoIP.mjs";
5
+ import { D as iosSimSlimProfileSetting, E as iosSimSlimDefaultSetting, _ as inertSettingWarning, a as SETTING_SHAPE_REMEDY, b as iosProjectDirSetting, p as cacheProviderSettingError, rt as settingShapeErrors, st as unknownSettingKeys, x as iosProjectDirSettingError } from "./settings-Bap-qjnp.mjs";
6
+ import { Ct as selectParked, bt as parkedMaxSetting, gt as dropParked, ht as adoptParked, xt as readParked } from "./android-SJx1V3R3.mjs";
7
+ import { i as createNdjsonWriter } from "./ndjson-CrjIsGzw.mjs";
8
+ import { _ as withDeviceBootAdmission, _n as APP_READINESS_TIMEOUT_MS, f as runtimeMismatch, g as unknownIosRuntimeRefusal, h as unknownIosDeviceTypeRefusal, l as deviceTypeMismatch, n as DeviceAdmissionRefusal, on as MANIFEST_VERIFY_TIMEOUT_MS, t as DEFAULT_DEVICE_SLOT_WAIT_MS, u as layerNote } from "./device-capacity-CpAVcKgq.mjs";
9
+ import { _t as teardownParkedIosSim, ht as teardownOwnedIosSim } from "./workspaces-Dh5N70qW.mjs";
10
+ import { J as describeDiagnostic, s as cocoapodsInstall } from "./toolchain-DCnWAxQL.mjs";
11
+ import { E as releaseRunLease, S as parseLease, T as raiseLease, b as leaseIsExpired, j as takeLease, v as deviceLeasePath, y as fileLeaseIo } from "./activity-BpdDvA8Q.mjs";
12
+ import { A as springBoardRunning, B as reportMemoryCulprits, C as renameIosSim, D as resolveOwnedIosSim, E as resolveIosCreation, F as hostMemoryPressureAdvice, M as waitForSpringBoard, N as configuredIosSimulatorViewer, P as parseIosSimulatorApp, R as readHostMemoryPressure, S as readSpringBoardPid, T as resetIosPrivacy, _ as ownedSimName, a as createOwnedIosSim, d as listAllIosSims, f as listAllIosSimsAsync, h as listIosRuntimesAsync, n as bootIosSim, p as listIosDeviceTypesAsync, u as iosSimulatorFailureAdvice, w as resetIosKeychain, y as parseRuntimeVersion } from "./ios-BhfpRfNf.mjs";
13
+ import { p as resolveOptimizations } from "./errors-DHm_U5gv.mjs";
14
+ import { releaseClaim, tryAcquireClaim } from "@stim-cli/core/ownership-claim";
15
+ import { createHash } from "node:crypto";
16
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
17
+ import { homedir, hostname } from "node:os";
18
+ import { basename, join } from "node:path";
19
+ import { LOG_ROTATE_BYTES, ambientContext, ambientEnv, withDirLockAsync } from "@stim-cli/core";
20
+ import { MACHINE_CREDENTIALS_VERSION, acceptsShape, automaticMachineEnabled, buildMachinesFile, buildMachinesLock, cannotHost, currentTailnet, deviceHostMachinesClaims, deviceHostMachinesFile, deviceHostMachinesLock, isJsonObject, localEndpoint, parseMachine, placementWord, readBuildMachines, readDeviceHostMachines, settingDefinition, wrongTailnet } from "@stim-cli/core/state";
21
+ import chalk from "chalk";
22
+ import { InvalidArgumentError } from "commander";
23
+ import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
24
+ //#region src/device-host/machines.ts
25
+ function note$1(title, detail, fix = null) {
26
+ return {
27
+ code: "device-host-machine",
28
+ level: "note",
29
+ title,
30
+ detail,
31
+ fix
32
+ };
33
+ }
34
+ function configuredMachines() {
35
+ const remote = loadConfig$1()?.remote;
36
+ if (remote !== void 0 && !isJsonObject(remote)) return null;
37
+ const machines = remote?.machines;
38
+ if (machines === void 0) return [];
39
+ return acceptsShape(settingDefinition("remote.machines"), machines) ? machines : null;
40
+ }
41
+ /**
42
+ * The `remote.machines` entries this machine asked for device-host access, in setting order, without the ones the
43
+ * tailnet reports as running another OS than macOS, which can never host; null when the setting is invalid.
44
+ */
45
+ async function hostingMachines(status, signal) {
46
+ const configured = configuredMachines();
47
+ if (configured === null) return null;
48
+ let candidates = configured;
49
+ try {
50
+ const asked = readDeviceHostMachines();
51
+ candidates = configured.filter((machine) => asked.some((each) => each.machine === machine));
52
+ } catch {
53
+ candidates = configured;
54
+ }
55
+ if (!candidates.some((machine) => parseMachine$1(machine)?.transport === "tailscale")) return candidates;
56
+ const current = await (status ? status() : tailscaleStatusAsync(signal));
57
+ signal?.throwIfAborted();
58
+ return candidates.filter((machine) => !cannotHost(current, machine));
59
+ }
60
+ async function store(machines, operation) {
61
+ reportSignal(operation)?.throwIfAborted();
62
+ const write = () => {
63
+ reportSignal(operation)?.throwIfAborted();
64
+ const file = deviceHostMachinesFile();
65
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
66
+ try {
67
+ writeFileSync(tmp, `${JSON.stringify({
68
+ version: MACHINE_CREDENTIALS_VERSION,
69
+ machines
70
+ }, null, 2)}\n`, { mode: 384 });
71
+ renameSync(tmp, file);
72
+ } finally {
73
+ rmSync(tmp, { force: true });
74
+ }
75
+ };
76
+ const options = { ensureParent: () => mkdirSync(getConfigDir(), { recursive: true }) };
77
+ const signal = reportSignal(operation);
78
+ if (signal) await withDirLockAsync(deviceHostMachinesLock(), write, {
79
+ ...options,
80
+ signal
81
+ });
82
+ else withDirLock$1(deviceHostMachinesLock(), write, options);
83
+ }
84
+ /** Forgets this machine's hosting credential for `machine`; false when another run holds the credential claim. */
85
+ async function forgetDeviceHostMachine(machine) {
86
+ const claim = tryAcquireClaim({
87
+ root: deviceHostMachinesClaims(),
88
+ mode: "exclusive",
89
+ label: "device-host approval"
90
+ });
91
+ if (!claim.acquired) {
92
+ releaseClaim(claim.pending);
93
+ return false;
94
+ }
95
+ try {
96
+ const credentials = readDeviceHostMachines();
97
+ if (credentials.some((each) => each.machine === machine)) await store(credentials.filter((each) => each.machine !== machine));
98
+ return true;
99
+ } finally {
100
+ releaseClaim(claim.acquired);
101
+ }
102
+ }
103
+ function notOnTailnet(machine, ambiguous, stored, current) {
104
+ const diagnosis = notOnTailnetDiagnosis(machine, ambiguous, stored ? wrongTailnet(stored, current) : null);
105
+ return {
106
+ diagnosis,
107
+ report: {
108
+ machine,
109
+ state: "not-on-tailnet",
110
+ ...stored?.tailnet ? { tailnet: stored.tailnet } : {},
111
+ diagnosis
112
+ }
113
+ };
114
+ }
115
+ /**
116
+ * Reports the device-host approval of each remote machine, separate from its build approval. Only `fix` makes a setup
117
+ * ticket, and only `forget`, which defaults to `fix`, forgets the credentials of machines not in `entries`; every
118
+ * server key stays pinned.
119
+ */
120
+ async function inspectDeviceHostMachines({ fix, forget = fix, version = "<version>", tickets = /* @__PURE__ */ new Map(), operation, confirmKey }, 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 keys 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 keys.`, "Restore the existing file from a trusted backup or restore read access before running doctor again."));
133
+ if (!entries.length) {
134
+ if (!forget) 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 (forget) {
164
+ const kept = credentials.filter((each) => entries.includes(each.machine));
165
+ if (kept.length !== credentials.length) {
166
+ await store(kept, operation);
167
+ credentials = kept;
168
+ }
169
+ }
170
+ if (!entries.length) return {
171
+ findings: [],
172
+ machines: []
173
+ };
174
+ const tailnet = entries.some((entry) => parseMachine$1(entry)?.transport !== "local");
175
+ const status = tailnet ? await io.status() : null;
176
+ const inspected = {
177
+ findings: [],
178
+ machines: []
179
+ };
180
+ let off = null;
181
+ if (tailnet && !isJsonObject(status)) {
182
+ off = await diagnoseTailscale(io);
183
+ inspected.findings.push(note$1("Remote machines are unreachable", `remote.machines names remote machines. ${off.message}`, off.remedy));
184
+ }
185
+ const self = isJsonObject(status) && isJsonObject(status.Self) ? status.Self : {};
186
+ const deviceName = (typeof self.HostName === "string" && self.HostName ? self.HostName : hostname()).replace(/[\p{Cc}\p{Cf}]/gu, "").trim().slice(0, 64) || "Mac";
187
+ const namedNodes = /* @__PURE__ */ new Set();
188
+ for (const machine of new Set(entries)) {
189
+ const parsed = parseMachine$1(machine);
190
+ if (!parsed) {
191
+ inspected.machines.push({
192
+ machine,
193
+ state: "invalid"
194
+ });
195
+ inspected.findings.push(note$1(`Remote machine ${machine} is not a tailnet name or local: address`, "Expected `name`, `name:port`, `local:<host>` or `local:<host>:port`."));
196
+ continue;
197
+ }
198
+ const local = parsed.transport === "local";
199
+ if (!local && off) {
200
+ inspected.machines.push({
201
+ machine,
202
+ state: "tailscale-off",
203
+ diagnosis: off
204
+ });
205
+ continue;
206
+ }
207
+ const current = local ? null : await io.status();
208
+ if (!local && !isJsonObject(current)) {
209
+ const diagnosis = await diagnoseTailscale(io);
210
+ inspected.machines.push({
211
+ machine,
212
+ state: "tailscale-off",
213
+ diagnosis
214
+ });
215
+ inspected.findings.push(diagnosisFinding("device-host-machine", machine, diagnosis));
216
+ continue;
217
+ }
218
+ const peer = local ? null : findPeer$1(current, parsed.name);
219
+ if (peer === "missing" || peer === "ambiguous") {
220
+ const { report, diagnosis } = notOnTailnet(machine, peer === "ambiguous", credentials.find((each) => each.machine === machine), current);
221
+ inspected.machines.push(report);
222
+ inspected.findings.push(diagnosisFinding("device-host-machine", machine, diagnosis));
223
+ continue;
224
+ }
225
+ const signedIn = local ? null : currentTailnet(current);
226
+ const target = peer ? endpoint$1(peer, parsed.port) : localEndpoint(parsed.name, parsed.port);
227
+ const dnsName = peer ? { dnsName: peer.dnsName } : {};
228
+ const nodeRoute = peer ? `${peer.nodeId}:${parsed.port}` : `local:${parsed.name}:${parsed.port}`;
229
+ if (namedNodes.has(nodeRoute)) {
230
+ inspected.machines.push({
231
+ machine,
232
+ ...dnsName,
233
+ state: "invalid"
234
+ });
235
+ inspected.findings.push(note$1(`Remote machine ${machine} repeats a named node`, "Name each tailnet node and serve port only once in remote.machines."));
236
+ continue;
237
+ }
238
+ namedNodes.add(nodeRoute);
239
+ if (cannotHost(current, machine)) {
240
+ inspected.machines.push({
241
+ machine,
242
+ ...dnsName,
243
+ state: "not-asked"
244
+ });
245
+ continue;
246
+ }
247
+ const credential = credentials.find((each) => each.machine === machine);
248
+ reportSignal(operation)?.throwIfAborted();
249
+ const outcome = await reachMachine({
250
+ machine,
251
+ access: "device-host",
252
+ endpoint: target,
253
+ address: peer?.dnsName ?? parsed.name,
254
+ ...signedIn ? { tailnet: signedIn } : {},
255
+ credential,
256
+ other: () => readBuildMachines().find((each) => each.machine === machine),
257
+ persist: async (next) => {
258
+ credentials = [...credentials.filter((each) => each.machine !== machine), next];
259
+ await store(credentials, operation);
260
+ },
261
+ fix,
262
+ deviceName,
263
+ setupFlags: ["build", "device-host"],
264
+ serverVersion: version,
265
+ io,
266
+ tickets,
267
+ ...local ? { confirmKey: (serverKey) => confirmKey?.(machine, serverKey) ?? Promise.resolve(false) } : {}
268
+ });
269
+ reportSignal(operation)?.throwIfAborted();
270
+ if (outcome.state !== "not-asked" && outcome.credential) {
271
+ const next = outcome.credential;
272
+ if (JSON.stringify(next) !== JSON.stringify(credential)) {
273
+ credentials = [...credentials.filter((each) => each.machine !== machine), next];
274
+ await store(credentials, operation);
275
+ }
276
+ }
277
+ const saved = outcome.state !== "not-asked" ? outcome.credential : void 0;
278
+ const known = {
279
+ machine,
280
+ ...dnsName,
281
+ ...saved?.deviceId ? { deviceId: saved.deviceId } : {},
282
+ ...saved?.tailnet ? { tailnet: saved.tailnet } : {},
283
+ ...saved ? { requestedAt: saved.requestedAt } : {}
284
+ };
285
+ if (outcome.state === "approved") {
286
+ const host = readHostPermissions(outcome.result.host);
287
+ if (host && (host.screenRecording === false || host.accessibility === false)) {
288
+ 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)"] : []];
289
+ const detail = [...host.screenRecording === false ? ["Viewing hosted macOS apps needs Screen Recording."] : [], ...host.accessibility === false ? ["Controlling them needs the control permission."] : []];
290
+ 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.`));
291
+ }
292
+ inspected.machines.push({
293
+ ...known,
294
+ state: "approved",
295
+ ...host ? { host } : {}
296
+ });
297
+ continue;
298
+ }
299
+ if (outcome.state === "pending") {
300
+ inspected.machines.push({
301
+ ...known,
302
+ state: "pending",
303
+ ...outcome.credential.expiresAt ? { expiresAt: outcome.credential.expiresAt } : {}
304
+ });
305
+ inspected.findings.push(note$1(`Remote machine ${machine} has not approved this machine for device hosting yet`, `It does not hold this machine's setup ticket yet${outcome.credential.expiresAt ? `; the ticket expires at ${outcome.credential.expiresAt}` : ""}.`, setupRemedy(machine, outcome.command)));
306
+ continue;
307
+ }
308
+ if (outcome.state === "not-asked") {
309
+ inspected.machines.push({
310
+ ...known,
311
+ state: "not-asked"
312
+ });
313
+ inspected.findings.push(note$1(`Remote machine ${machine} has not approved this machine for device hosting`, "This machine has not asked for hosting access.", `Run \`stim remote add ${machine}\` to make a setup ticket and print the command to run there.`));
314
+ continue;
315
+ }
316
+ if (outcome.state === "outdated") {
317
+ const outdated = outdatedServer(machine, version, outcome.command);
318
+ inspected.machines.push({
319
+ ...known,
320
+ state: "outdated",
321
+ diagnosis: {
322
+ layer: "compat",
323
+ code: "server-outdated",
324
+ title: "Server is outdated",
325
+ message: outdated.detail,
326
+ remedy: outdated.fix
327
+ }
328
+ });
329
+ inspected.findings.push(note$1(outdated.title, outdated.detail, outdated.fix));
330
+ continue;
331
+ }
332
+ if (outcome.state === "unconfirmed") {
333
+ inspected.machines.push({
334
+ ...known,
335
+ state: "key-unconfirmed",
336
+ fingerprint: outcome.fingerprint
337
+ });
338
+ inspected.findings.push(unconfirmedKeyFinding("device-host-machine", machine, outcome.fingerprint));
339
+ continue;
340
+ }
341
+ if (outcome.state === "unreachable") {
342
+ const failure = {
343
+ entry: machine,
344
+ port: parsed.port,
345
+ target,
346
+ reason: outcome.reason,
347
+ transport: outcome.transport
348
+ };
349
+ const startup = !peer && outcome.transport?.kind === "http" && outcome.transport.status === 503 ? await io.startup?.(target) : null;
350
+ const diagnosis = peer ? await diagnoseOutcome(io, {
351
+ ...failure,
352
+ peer
353
+ }) : diagnoseLocalUnreachable({
354
+ ...failure,
355
+ startup
356
+ });
357
+ inspected.machines.push({
358
+ ...known,
359
+ state: "unreachable",
360
+ diagnosis
361
+ });
362
+ inspected.findings.push(diagnosisFinding("device-host-machine", machine, diagnosis));
363
+ continue;
364
+ }
365
+ inspected.machines.push({
366
+ ...known,
367
+ state: outcome.state
368
+ });
369
+ inspected.findings.push(outcome.state === "key-changed" ? note$1(`Remote machine ${machine} answered with a different server key`, "Stim refuses to connect to a stim-server that does not prove it holds the key pinned at approval.", `If that machine or its Stim home was replaced, run \`stim remote remove ${machine}\`, then \`stim remote add ${machine}\`.`) : outcome.state === "lapsed" ? note$1(`Hosting request to ${machine} lapsed`, `${machine} did not approve this machine before its setup ticket expired at ${outcome.credential.expiresAt}.`, `Run \`stim remote add ${machine}\` to make a new ticket.`) : note$1(`Remote machine ${machine} no longer accepts this machine for device hosting`, `${outcome.reason} The pinned key is preserved.`, `Run \`stim remote add ${machine}\` to ask again.`));
370
+ }
371
+ return inspected;
372
+ } finally {
373
+ releaseClaim(claim.acquired);
374
+ }
375
+ }
376
+ //#endregion
377
+ //#region src/offload/build-machines.ts
378
+ /** The `remote.machines` entries that approved this machine for builds, in that order. */
379
+ function pairedMachines(entries = configuredMachines() ?? []) {
380
+ const credentials = readBuildMachines().filter((each) => each.state === "approved");
381
+ return entries.flatMap((entry) => credentials.filter((each) => each.machine === entry));
382
+ }
383
+ async function updateCredentials(change, operation) {
384
+ reportSignal(operation)?.throwIfAborted();
385
+ const dir = getConfigDir();
386
+ const write = () => {
387
+ reportSignal(operation)?.throwIfAborted();
388
+ const file = buildMachinesFile();
389
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
390
+ writeFileSync(tmp, `${JSON.stringify({
391
+ version: MACHINE_CREDENTIALS_VERSION,
392
+ machines: change(readBuildMachines())
393
+ }, null, 2)}\n`, { mode: 384 });
394
+ try {
395
+ renameSync(tmp, file);
396
+ } catch (error) {
397
+ rmSync(tmp, { force: true });
398
+ throw error;
399
+ }
400
+ };
401
+ const options = { ensureParent: () => mkdirSync(dir, { recursive: true }) };
402
+ const signal = reportSignal(operation);
403
+ if (signal) await withDirLockAsync(buildMachinesLock(), write, {
404
+ ...options,
405
+ signal
406
+ });
407
+ else withDirLock$1(buildMachinesLock(), write, options);
408
+ }
409
+ async function forgetBuildMachine(machine) {
410
+ if (readBuildMachines().some((each) => each.machine === machine)) await updateCredentials((credentials) => credentials.filter((each) => each.machine !== machine));
411
+ }
412
+ function note(title, detail, fix = null) {
413
+ return {
414
+ code: "build-machine",
415
+ level: "note",
416
+ title,
417
+ detail,
418
+ fix
419
+ };
420
+ }
421
+ const PROBLEM_TITLES = {
422
+ unreachable: "does not answer a build offer",
423
+ checkout: "cannot build an app outside a git checkout",
424
+ "stim-build": "runs an incompatible Stim build",
425
+ arch: "has another CPU architecture",
426
+ xcode: "has another Xcode",
427
+ "macos-sdk": "has another macOS SDK",
428
+ "simulator-sdk": "has another simulator SDK",
429
+ cocoapods: "has no CocoaPods, or another version",
430
+ bundler: "has no Bundler",
431
+ runtime: "has no simulator for this project's iOS runtime",
432
+ jdk: "runs another JDK major version",
433
+ "android-sdk": "has no Android SDK",
434
+ ndk: "lacks this project's NDK",
435
+ "build-tools": "lacks this project's Android build-tools",
436
+ "compile-sdk": "lacks this project's Android compile platform",
437
+ disk: "is low on disk",
438
+ busy: "is too busy to take builds",
439
+ platform: "cannot build this platform"
440
+ };
441
+ const NOTE_TITLES = {
442
+ jdk: "builds Android with its own JDK",
443
+ ndk: "downloads this project's NDK on its first Android build",
444
+ "build-tools": "downloads this project's Android build-tools on its first Android build",
445
+ "compile-sdk": "downloads this project's Android compile platform on its first Android build"
446
+ };
447
+ function commandFix(problem, entry) {
448
+ return problem.commands?.length && problem.lead ? [`On ${entry}, ${problem.lead}:`, ...problem.commands].join("\n") : null;
449
+ }
450
+ function problemFix(problem, entry) {
451
+ const { code } = problem;
452
+ if (problem.followsFrom) return `Fix the ${problem.followsFrom} problem on ${entry} first; this follows from it.`;
453
+ switch (code) {
454
+ 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}.`;
455
+ case "checkout": return "Offload syncs the files git lists; run Stim from a git checkout of the app.";
456
+ case "stim-build": return `Update stim-server on ${entry} to a Stim release with the same build compatibility version as this machine (an older stim-server needs the same Stim build): there, run \`stim-server service update --release <version>\` for this machine's release, or \`--from <dir>\` with the packed packages of this machine's checkout.`;
457
+ case "arch": return "Use a remote machine with the same CPU architecture as this machine.";
458
+ case "xcode":
459
+ case "simulator-sdk":
460
+ case "macos-sdk": return commandFix(problem, entry) ?? `Install and select the same Xcode on ${entry} and this Mac (\`xcode-select -p\` on each).`;
461
+ case "cocoapods": return commandFix(problem, entry) ?? `Install the same CocoaPods version on ${entry}, on the PATH its stim-server sets. A build sent there with --remote-build ${entry} still runs; a version that differs rewrites the COCOAPODS line of a committed Podfile.lock.`;
462
+ case "bundler": return `Install Bundler (\`gem install bundler\`) on ${entry}, on the PATH its stim-server's login shell sets.`;
463
+ case "runtime": return commandFix(problem, entry) ?? `Install that iOS simulator runtime on ${entry} with at least one iPhone simulator, or set ios.runtime here to one ${entry} has.`;
464
+ case "jdk": return commandFix(problem, entry) ?? `Start stim-server on ${entry} with JAVA_HOME at a JDK of the same major version as this machine's.`;
465
+ case "android-sdk":
466
+ case "ndk":
467
+ case "build-tools":
468
+ case "compile-sdk": return commandFix(problem, entry) ?? `Install the missing package with sdkmanager in the Android SDK stim-server on ${entry} uses (its ANDROID_HOME, else ~/Library/Android/sdk on macOS or ~/Android/Sdk on Linux).`;
469
+ case "disk": return `Free space on ${entry}'s worker root (server.workerRoot there).`;
470
+ case "platform": return `iOS and macOS builds need a Mac; ${entry} can take Android builds only.`;
471
+ case "busy": return `Builds stay on this machine 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.`;
472
+ }
473
+ }
474
+ const TRANSIENT = /* @__PURE__ */ new Set(["unreachable", "busy"]);
475
+ const UNPREDICTABLE = /* @__PURE__ */ new Set([
476
+ "unreachable",
477
+ "busy",
478
+ "disk",
479
+ "checkout",
480
+ "cocoapods"
481
+ ]);
482
+ /** Why a build on the machine named for it cannot work, from the toolchain differences found before this machine changes the checkout. */
483
+ function predictedRefusal(entry, problems) {
484
+ const known = problems.filter((problem) => !UNPREDICTABLE.has(problem.code));
485
+ if (known.length === 0) return null;
486
+ return {
487
+ reason: known.map((problem) => problem.reason).join("; "),
488
+ remedy: known.map((problem) => problemFix(problem, entry)).join("\n")
489
+ };
490
+ }
491
+ function offloadFindings(entry, problems, notes, operation) {
492
+ const selected = operation?.env.STIM_REMOTE_BUILD?.trim() || loadConfig$1()?.remote?.build;
493
+ const consequence = typeof selected === "string" && placementWord(selected) === "remote-only" ? "remote.build is remote-only, so automatic builds skip this machine and refuse rather than build on this Mac:" : "Builds fall back to this machine:";
494
+ return [...problems.map((problem) => ({
495
+ code: `build-machine-${problem.code}`,
496
+ level: TRANSIENT.has(problem.code) ? "note" : "cost",
497
+ title: `Remote machine ${entry} ${PROBLEM_TITLES[problem.code]}`,
498
+ detail: `${consequence} ${problem.reason}.${problem.code === "cocoapods" ? ` A build sent there with --remote-build ${entry} still runs.` : ""}`,
499
+ fix: problemFix(problem, entry)
500
+ })), ...notes.map((each) => ({
501
+ code: `build-machine-${each.code}`,
502
+ level: "note",
503
+ title: `Remote machine ${entry} ${NOTE_TITLES[each.code] ?? "needs a first-build download"}`,
504
+ detail: `The build goes there anyway: ${each.reason}.`,
505
+ fix: commandFix(each, entry)
506
+ }))];
507
+ }
508
+ async function inspectMachine(entry, tailnet, deviceName, fix, version, tickets, io, check, operation, confirmKey) {
509
+ const parsed = parseMachine$1(entry);
510
+ if (!parsed) return {
511
+ report: {
512
+ machine: entry,
513
+ state: "invalid"
514
+ },
515
+ finding: note(`Remote machine ${entry} is not a tailnet name or local: address`, "Expected `name`, `name:port`, `local:<host>` or `local:<host>:port`.")
516
+ };
517
+ const local = parsed.transport === "local";
518
+ if (!local && "off" in tailnet) return {
519
+ report: {
520
+ machine: entry,
521
+ state: "tailscale-off",
522
+ diagnosis: tailnet.off
523
+ },
524
+ finding: null
525
+ };
526
+ const status = "status" in tailnet ? tailnet.status : {};
527
+ const peer = local ? null : findPeer$1(status, parsed.name);
528
+ const credential = readBuildMachines().find((each) => each.machine === entry);
529
+ const signedIn = local ? null : currentTailnet(status);
530
+ if (peer === "missing" || peer === "ambiguous") {
531
+ const diagnosis = notOnTailnetDiagnosis(entry, peer === "ambiguous", credential ? wrongTailnet(credential, status) : null);
532
+ return {
533
+ report: {
534
+ machine: entry,
535
+ state: "not-on-tailnet",
536
+ ...credential?.tailnet ? { tailnet: credential.tailnet } : {},
537
+ diagnosis
538
+ },
539
+ finding: diagnosisFinding("build-machine", entry, diagnosis)
540
+ };
541
+ }
542
+ const target = peer ? endpoint$1(peer, parsed.port) : localEndpoint(parsed.name, parsed.port);
543
+ const address = peer?.dnsName ?? parsed.name;
544
+ const other = () => {
545
+ try {
546
+ return readDeviceHostMachines().find((each) => each.machine === entry);
547
+ } catch {
548
+ return;
549
+ }
550
+ };
551
+ const persist = (next) => updateCredentials((credentials) => [...credentials.filter((each) => each.machine !== entry), next], operation);
552
+ reportSignal(operation)?.throwIfAborted();
553
+ const outcome = await reachMachine({
554
+ machine: entry,
555
+ access: "build",
556
+ endpoint: target,
557
+ address,
558
+ ...signedIn ? { tailnet: signedIn } : {},
559
+ credential,
560
+ other,
561
+ persist,
562
+ fix,
563
+ deviceName,
564
+ setupFlags: cannotHost(status, entry) ? ["build"] : ["build", "device-host"],
565
+ serverVersion: version,
566
+ io,
567
+ tickets,
568
+ ...local ? { confirmKey: (serverKey) => confirmKey?.(entry, serverKey) ?? Promise.resolve(false) } : {}
569
+ });
570
+ const known = {
571
+ machine: entry,
572
+ ...peer ? { dnsName: peer.dnsName } : {}
573
+ };
574
+ reportSignal(operation)?.throwIfAborted();
575
+ if (outcome.state !== "not-asked" && outcome.credential) {
576
+ const next = outcome.credential;
577
+ if (JSON.stringify(next) !== JSON.stringify(credential)) await updateCredentials((credentials) => [...credentials.filter((each) => each.machine !== entry), next], operation);
578
+ }
579
+ const paired = {
580
+ ...known,
581
+ ...outcome.state !== "not-asked" && outcome.credential?.tailnet ? { tailnet: outcome.credential.tailnet } : {},
582
+ ...outcome.state !== "not-asked" && outcome.credential?.deviceId ? { deviceId: outcome.credential.deviceId } : {},
583
+ ...outcome.state !== "not-asked" && outcome.credential ? { requestedAt: outcome.credential.requestedAt } : {}
584
+ };
585
+ switch (outcome.state) {
586
+ case "not-asked": return {
587
+ report: {
588
+ ...known,
589
+ state: "not-asked"
590
+ },
591
+ finding: note(`Remote machine ${entry} has not approved this machine for builds`, "This machine has not asked it for build access.", `Run \`stim remote add ${entry}\` to make a setup ticket and print the command to run there.`)
592
+ };
593
+ case "key-changed": return {
594
+ report: {
595
+ ...paired,
596
+ state: "key-changed"
597
+ },
598
+ finding: note(`Remote machine ${entry} answered with a different server key`, `${address} did not prove it holds the stim-server key this machine pinned at approval. Stim does not connect to it.`, `If that machine or its Stim home was replaced, run \`stim remote remove ${entry}\` to forget the old pairing, then \`stim remote add ${entry}\`.`)
599
+ };
600
+ case "unconfirmed": return {
601
+ report: {
602
+ ...paired,
603
+ state: "key-unconfirmed",
604
+ fingerprint: outcome.fingerprint
605
+ },
606
+ finding: unconfirmedKeyFinding("build-machine", entry, outcome.fingerprint)
607
+ };
608
+ case "pending": return {
609
+ report: {
610
+ ...paired,
611
+ state: "pending",
612
+ ...outcome.credential.expiresAt ? { expiresAt: outcome.credential.expiresAt } : {}
613
+ },
614
+ finding: note(`Remote machine ${entry} has not approved this machine for builds yet`, `It does not hold this machine's setup ticket yet${outcome.credential.expiresAt ? `; the ticket expires at ${outcome.credential.expiresAt}` : ""}.`, setupRemedy(entry, outcome.command))
615
+ };
616
+ case "lapsed": return {
617
+ report: {
618
+ ...paired,
619
+ state: "lapsed"
620
+ },
621
+ finding: note(`Build request to ${entry} lapsed`, `${entry} did not approve this machine before its setup ticket expired at ${outcome.credential.expiresAt}.`, `Run \`stim remote add ${entry}\` to make a new ticket.`)
622
+ };
623
+ case "revoked": return {
624
+ report: {
625
+ ...paired,
626
+ state: "revoked"
627
+ },
628
+ finding: note(`Remote machine ${entry} no longer accepts this machine for builds`, `${outcome.reason} It was revoked.`, `Run \`stim remote add ${entry}\` to ask again.`)
629
+ };
630
+ case "outdated": {
631
+ const outdated = outdatedServer(entry, version, outcome.command);
632
+ const diagnosis = {
633
+ layer: "compat",
634
+ code: "server-outdated",
635
+ title: "Server is outdated",
636
+ message: outdated.detail,
637
+ remedy: outdated.fix
638
+ };
639
+ return {
640
+ report: {
641
+ ...paired,
642
+ state: "outdated",
643
+ diagnosis
644
+ },
645
+ finding: note(outdated.title, outdated.detail, outdated.fix)
646
+ };
647
+ }
648
+ case "unreachable": {
649
+ const failure = {
650
+ entry,
651
+ port: parsed.port,
652
+ target,
653
+ reason: outcome.reason,
654
+ transport: outcome.transport
655
+ };
656
+ const startup = !peer && outcome.transport?.kind === "http" && outcome.transport.status === 503 ? await io.startup?.(target) : null;
657
+ const diagnosis = peer ? await diagnoseOutcome(io, {
658
+ ...failure,
659
+ peer
660
+ }) : diagnoseLocalUnreachable({
661
+ ...failure,
662
+ startup
663
+ });
664
+ return {
665
+ report: {
666
+ ...paired,
667
+ state: "unreachable",
668
+ diagnosis
669
+ },
670
+ finding: diagnosisFinding("build-machine", entry, diagnosis)
671
+ };
672
+ }
673
+ }
674
+ const permissions = readHostPermissions(outcome.result.host);
675
+ const host = permissions ? { host: permissions } : {};
676
+ if (!check) return {
677
+ report: {
678
+ ...paired,
679
+ state: "approved",
680
+ ...host
681
+ },
682
+ finding: null
683
+ };
684
+ const { capacity, problems, notes = [] } = await check(outcome.credential);
685
+ return {
686
+ report: {
687
+ ...paired,
688
+ state: "approved",
689
+ ...host,
690
+ offloadable: problems.length === 0,
691
+ reasons: problems.map((problem) => problem.reason),
692
+ problems,
693
+ ...notes.length ? { notes } : {},
694
+ ...capacity ? { capacity } : {}
695
+ },
696
+ finding: null,
697
+ extra: offloadFindings(entry, problems, notes, operation)
698
+ };
699
+ }
700
+ /**
701
+ * Doctor findings and a report for each `remote.machines` entry. The worker's stim-server key is pinned on first
702
+ * contact; every later connection must prove it holds that key. With `fix`, makes a setup ticket for a named machine
703
+ * this machine holds no pairing for, or that revoked it. With `forget`, which defaults to `fix`, forgets pairings of
704
+ * machines not in `entries`. With
705
+ * `check`, each approved machine is asked for one build offer and reports every reason it would not take this
706
+ * project's build.
707
+ */
708
+ async function inspectBuildMachines({ fix, forget = fix, check = null, version = "<version>", tickets = /* @__PURE__ */ new Map(), operation, confirmKey }, io = realIo, entries = configuredMachines()) {
709
+ if (entries === null) return {
710
+ findings: [note("Invalid remote.machines setting", "Use a remote object with machines as an array of tailnet names. Existing credentials and pinned keys are preserved.", "Run `stim guide settings` and correct the setting before running doctor again.")],
711
+ machines: []
712
+ };
713
+ const unnamed = (each) => !entries.includes(each.machine);
714
+ if (forget && readBuildMachines().some(unnamed)) await updateCredentials((credentials) => credentials.filter((each) => !unnamed(each)), operation);
715
+ if (entries.length === 0) return {
716
+ findings: [],
717
+ machines: []
718
+ };
719
+ const tailnet = entries.some((entry) => parseMachine$1(entry)?.transport !== "local");
720
+ const read = tailnet ? await io.status() : null;
721
+ const view = isJsonObject(read) ? { status: read } : tailnet ? { off: await diagnoseTailscale(io) } : { status: {} };
722
+ const inspection = {
723
+ findings: [],
724
+ machines: []
725
+ };
726
+ if ("off" in view) inspection.findings.push(note("Remote machines are unreachable", `remote.machines names remote machines. ${view.off.message}`, view.off.remedy));
727
+ const self = "status" in view && isJsonObject(view.status.Self) ? view.status.Self : {};
728
+ const deviceName = (typeof self.HostName === "string" && self.HostName ? self.HostName : hostname()).replace(/[\p{Cc}\p{Cf}]/gu, "").trim().slice(0, 64) || "Mac";
729
+ const inspected = await Promise.all(entries.map((entry) => inspectMachine(entry, view, deviceName, fix, version, tickets, io, check, operation, confirmKey)));
730
+ for (const { report, finding, extra = [] } of inspected) {
731
+ inspection.machines.push(report);
732
+ if (finding) inspection.findings.push(finding);
733
+ inspection.findings.push(...extra);
734
+ }
735
+ return inspection;
736
+ }
737
+ //#endregion
738
+ //#region src/engine/device-lease-run.ts
739
+ const LEASE_STEP_FLOOR_MS = 6e4;
740
+ const DEBUG_VERIFY_STEP_MS = MANIFEST_VERIFY_TIMEOUT_MS + APP_READINESS_TIMEOUT_MS;
741
+ const DEVICE_WAIT_POLL_MS = 2e3;
742
+ const DEVICE_WAIT_LINE_MS = 3e4;
743
+ const DEVICE_BUSY = "STIM_DEVICE_BUSY";
744
+ const DEVICE_LOST = "STIM_DEVICE_LOST";
745
+ function leaseStepMs(boundMs = 0) {
746
+ return Math.max(LEASE_STEP_FLOOR_MS, boundMs);
747
+ }
748
+ function parseDeviceWait(value) {
749
+ if (value === void 0 || value === null) return { seconds: 60 };
750
+ const seconds = String(value).trim() === "" ? NaN : Number(value);
751
+ if (!Number.isInteger(seconds) || seconds < 0) return { error: `Invalid --wait value ${JSON.stringify(value)}. Pass a whole number of seconds, e.g. --wait 90.` };
752
+ return { seconds };
753
+ }
754
+ function waitFlagConflict(argv) {
755
+ return argv.some((arg) => arg === "--wait" || arg.startsWith("--wait=")) && argv.includes("--no-wait");
756
+ }
757
+ function leaseExpiryText(expiresAt, now) {
758
+ const remaining = Date.parse(expiresAt) - now;
759
+ return `${clockTime(expiresAt)} (${formatElapsed(Math.max(0, remaining))} from now)`;
760
+ }
761
+ function describeDevice(lease) {
762
+ return lease.deviceName ? `${lease.deviceName} (${lease.id})` : lease.id;
763
+ }
764
+ function facts(lease) {
765
+ return {
766
+ platform: lease.platform,
767
+ id: lease.id,
768
+ deviceName: lease.deviceName,
769
+ holder: lease.holder,
770
+ expiresAt: lease.expiresAt
771
+ };
772
+ }
773
+ function waitingLine(lease, now) {
774
+ return `waiting for ${lease.holder} to release ${describeDevice(lease)}; its lease runs until ${leaseExpiryText(lease.expiresAt, now)} -- stim guide lifecycle lease`;
775
+ }
776
+ function appIdMatch(ours, theirs) {
777
+ if (!ours || !theirs) return "unknown";
778
+ return ours === theirs ? "same" : "different";
779
+ }
780
+ function bypassLines(lease, now, match) {
781
+ const consequence = {
782
+ same: "Both workspaces build the same app id, so this install terminates the app that workspace is running.",
783
+ different: "The other workspace's app id differs, so the launch below backgrounds it rather than ending it.",
784
+ unknown: "Stim cannot tell whether the two workspaces build the same app id; if they do, this install terminates the holder's running app."
785
+ }[match];
786
+ return [`--no-wait: ${lease.holder} holds ${describeDevice(lease)} until ${leaseExpiryText(lease.expiresAt, now)}, and this run is proceeding without a lease.`, consequence];
787
+ }
788
+ function busyRefusal({ lease, now, idLabel, waitSeconds }) {
789
+ return {
790
+ code: DEVICE_BUSY,
791
+ message: `${lease.holder} holds ${describeDevice(lease)} until ${leaseExpiryText(lease.expiresAt, now)}` + (waitSeconds > 0 ? `, and this command waited ${waitSeconds}s for it.` : "."),
792
+ remedy: `Wait longer with \`--wait <seconds>\`, pick another device with \`--device <${idLabel}>\`, or pass \`--no-wait\` to install anyway -- which takes no lease and, when both workspaces build the same app id, terminates the holder's running app.`,
793
+ lease: facts(lease)
794
+ };
795
+ }
796
+ function unreadableRefusal(path) {
797
+ return {
798
+ code: DEVICE_BUSY,
799
+ message: `The lease file at ${path} does not parse, so Stim cannot tell who holds that device.`,
800
+ remedy: `Read ${path} and, once you know no run depends on it, delete it. \`stim gc\` reports it on every run.`,
801
+ lease: {
802
+ platform: null,
803
+ id: null,
804
+ deviceName: null,
805
+ holder: null,
806
+ expiresAt: null
807
+ }
808
+ };
809
+ }
810
+ function untokenedRefusal(lease, now) {
811
+ return {
812
+ code: DEVICE_BUSY,
813
+ message: `This workspace holds ${describeDevice(lease)} until ${leaseExpiryText(lease.expiresAt, now)}, but its own record of that lease is gone, so this run cannot prove the lease is still its own.`,
814
+ remedy: "Run `stim device unlock`, which releases by holder, then run this command again.",
815
+ lease: facts(lease)
816
+ };
817
+ }
818
+ function otherDeviceRefusal(leasedId, requestedId) {
819
+ return {
820
+ code: "STIM_NO_DEVICE",
821
+ message: `This workspace already leases ${leasedId}, and this run asked for ${requestedId}.`,
822
+ remedy: `Run \`stim device unlock\` to give up ${leasedId}, or use it with \`--device ${leasedId}\`.`,
823
+ lease: null
824
+ };
825
+ }
826
+ function lostLine(holder, expiresAt, now) {
827
+ return `${holder ?? "another workspace"} took this device's lease${expiresAt ? ` until ${leaseExpiryText(expiresAt, now)}` : ""}. The app is already installed, so this run continues without one.`;
828
+ }
829
+ function lostRefusal(holder, expiresAt, now) {
830
+ const who = holder ?? "another workspace";
831
+ const until = expiresAt ? ` until ${leaseExpiryText(expiresAt, now)}` : "";
832
+ return {
833
+ code: DEVICE_LOST,
834
+ message: `${who} took this device's lease${until} before this run could install.`,
835
+ remedy: "Run this command again; it waits for that lease with `--wait <seconds>`.",
836
+ lease: null
837
+ };
838
+ }
839
+ function runLease({ root, platform, slot = "default", kind, expiresAt, io = fileLeaseIo }) {
840
+ const handle = {
841
+ kind,
842
+ expiresAt,
843
+ lost: false,
844
+ raise(boundMs) {
845
+ if (handle.kind === null || handle.lost) return {
846
+ ok: !handle.lost,
847
+ holder: null,
848
+ expiresAt: handle.expiresAt
849
+ };
850
+ const result = raiseLease({
851
+ root,
852
+ platform,
853
+ slot,
854
+ minMs: leaseStepMs(boundMs)
855
+ }, io);
856
+ if (result.status === "raised") {
857
+ handle.expiresAt = result.lease.expiresAt;
858
+ return {
859
+ ok: true,
860
+ holder: result.lease.holder,
861
+ expiresAt: result.lease.expiresAt
862
+ };
863
+ }
864
+ handle.lost = true;
865
+ const taken = result.status === "lost" ? result.lease : null;
866
+ return {
867
+ ok: false,
868
+ holder: taken?.holder ?? null,
869
+ expiresAt: taken?.expiresAt ?? null
870
+ };
871
+ },
872
+ release() {
873
+ if (handle.kind === null) return;
874
+ releaseRunLease({
875
+ root,
876
+ platform,
877
+ slot
878
+ }, io);
879
+ },
880
+ facts() {
881
+ if (handle.kind === null || handle.lost || handle.expiresAt === null) return null;
882
+ return {
883
+ kind: handle.kind,
884
+ expiresAt: handle.expiresAt
885
+ };
886
+ }
887
+ };
888
+ return handle;
889
+ }
890
+ /** Release the run's lease as soon as `context`'s operation is aborted; returns a function that stops listening. */
891
+ function releaseLeaseOnAbort(context, release) {
892
+ const onAbort = () => {
893
+ operationState(context).handled = true;
894
+ release();
895
+ };
896
+ if (context.signal.aborted) onAbort();
897
+ else context.signal.addEventListener("abort", onAbort, { once: true });
898
+ return () => context.signal.removeEventListener("abort", onAbort);
899
+ }
900
+ async function waitForDevice({ root, platform, slot = "default", id, idLabel, waitSeconds, noWait = false, appId = null, holderAppId = () => null, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), warn = () => {}, io = fileLeaseIo, deadline, lastLine }) {
901
+ const path = deviceLeasePath(platform, id);
902
+ for (;;) {
903
+ const raw = io.readLease(path);
904
+ const current = parseLease(raw);
905
+ if (raw !== null && !current) return {
906
+ status: "refused",
907
+ refusal: unreadableRefusal(path)
908
+ };
909
+ if (!current || leaseIsExpired(current, now())) return { status: "free" };
910
+ if (current.holder === root && (current.slot ?? "default") === slot) return {
911
+ status: "ours",
912
+ lease: current
913
+ };
914
+ if (noWait) {
915
+ for (const line of bypassLines(current, now(), appIdMatch(appId, holderAppId(current.holder)))) warn(line);
916
+ return {
917
+ status: "bypassed",
918
+ lease: current
919
+ };
920
+ }
921
+ if (now() >= deadline) return {
922
+ status: "refused",
923
+ refusal: busyRefusal({
924
+ lease: current,
925
+ now: now(),
926
+ idLabel,
927
+ waitSeconds
928
+ })
929
+ };
930
+ if (lastLine.at === null || now() - lastLine.at >= 3e4) {
931
+ lastLine.at = now();
932
+ warn(waitingLine(current, now()));
933
+ }
934
+ await sleep(DEVICE_WAIT_POLL_MS);
935
+ }
936
+ }
937
+ async function acquireRunLease({ root, platform, slot = "default", id, deviceName = null, idLabel, waitSeconds, noWait, installBoundMs, appId = null, holderAppId = () => null, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), warn = () => {}, io = fileLeaseIo }) {
938
+ const recorded = io.readHolder(root)[deviceSlotKey(platform, slot)];
939
+ let held = recorded;
940
+ if (recorded && recorded.id !== id) {
941
+ const other = parseLease(io.readLease(deviceLeasePath(platform, recorded.id)));
942
+ if (other && other.token === recorded.token && !leaseIsExpired(other, now())) return {
943
+ status: "refused",
944
+ refusal: otherDeviceRefusal(recorded.id, id)
945
+ };
946
+ held = void 0;
947
+ }
948
+ const path = deviceLeasePath(platform, id);
949
+ const deadline = now() + waitSeconds * 1e3;
950
+ const lastLine = { at: null };
951
+ for (;;) {
952
+ const current = parseLease(io.readLease(path));
953
+ const mine = current && held && current.token === held.token ? {
954
+ lease: current,
955
+ record: held
956
+ } : null;
957
+ if (mine && (mine.record.kind === "run" || !leaseIsExpired(mine.lease, now()))) {
958
+ const raised = raiseLease({
959
+ root,
960
+ platform,
961
+ slot,
962
+ minMs: leaseStepMs(installBoundMs)
963
+ }, io);
964
+ if (raised.status === "raised") return {
965
+ status: "leased",
966
+ kind: mine.record.kind,
967
+ expiresAt: raised.lease.expiresAt
968
+ };
969
+ }
970
+ const outcome = await waitForDevice({
971
+ root,
972
+ platform,
973
+ slot,
974
+ id,
975
+ idLabel,
976
+ waitSeconds,
977
+ noWait,
978
+ appId,
979
+ holderAppId,
980
+ now,
981
+ sleep,
982
+ warn,
983
+ io,
984
+ deadline,
985
+ lastLine
986
+ });
987
+ if (outcome.status === "refused") return outcome;
988
+ if (outcome.status === "bypassed") return { status: "unleased" };
989
+ if (outcome.status === "ours") return {
990
+ status: "refused",
991
+ refusal: untokenedRefusal(outcome.lease, now())
992
+ };
993
+ const taken = takeLease({
994
+ root,
995
+ platform,
996
+ slot,
997
+ id,
998
+ deviceName,
999
+ kind: "run",
1000
+ durationMs: leaseStepMs(installBoundMs)
1001
+ }, io);
1002
+ if (taken.status === "unreadable") return {
1003
+ status: "refused",
1004
+ refusal: unreadableRefusal(taken.path)
1005
+ };
1006
+ if (taken.status === "taken" || taken.status === "set") return {
1007
+ status: "leased",
1008
+ kind: taken.status === "set" ? held?.kind ?? "run" : "run",
1009
+ expiresAt: taken.lease.expiresAt
1010
+ };
1011
+ }
1012
+ }
1013
+ //#endregion
1014
+ //#region src/offload/selection.ts
1015
+ const FINGERPRINT_MISMATCH_REMEDY = "The build Mac fingerprints the synced checkout differently; the message names the sources that differ. Stim sends tracked and untracked files and the gitignored files the fingerprint reads, except under node_modules. A source still differs when the build Mac's dependency install or pod install writes it differently, or when the app config reads an environment variable or file that differs. Make that input agree on both Macs, for example by committing the lockfile pod install rewrote there, or rerun with --remote-build auto or --remote-build local.";
1016
+ /** The remedy for a React Native worker's fingerprint-mismatch reason, or undefined for any other reason. */
1017
+ function fingerprintMismatchRemedy(reason) {
1018
+ return reason.startsWith("fingerprint-mismatch: the checkout there ") ? FINGERPRINT_MISMATCH_REMEDY : void 0;
1019
+ }
1020
+ const MISSING_PACKAGE_MANAGER = /^deps-failed: spawn (pnpm|yarn|npm) ENOENT/;
1021
+ /** The remedy for a worker whose dependency install could not start its package manager; undefined for any other reason. */
1022
+ function missingPackageManagerRemedy(machine, reason) {
1023
+ const tool = MISSING_PACKAGE_MANAGER.exec(reason)?.[1];
1024
+ if (!tool) return void 0;
1025
+ return `${machine} has no ${tool} on the PATH of its stim-server service, and the app's lockfile needs it. ${tool === "npm" ? "Reinstall Node there, which ships npm" : `Install it with \`corepack enable ${tool}\` or \`npm install --global ${tool}\`, using sudo where Node is installed system-wide`}, then run the same command again, or rerun with --remote-build auto or --remote-build local.`;
1026
+ }
1027
+ const PODS_FAILED_PREFIX = "pod install failed: ";
1028
+ /** The reason a failed worker job is reported with: a `pods-failed` code reads as a failed pod install. */
1029
+ function workerFailureReason(code, message) {
1030
+ return code === "pods-failed" ? `${PODS_FAILED_PREFIX}${String(message ?? "")}` : `${String(code ?? "failed")}: ${String(message ?? "")}`;
1031
+ }
1032
+ /** The remedy for a worker whose `pod install` failed, naming the install when it has no CocoaPods; undefined for any other reason. */
1033
+ function podsFailedRemedy(machine, reason, localCocoapods) {
1034
+ if (!reason.startsWith(PODS_FAILED_PREFIX)) return void 0;
1035
+ const retry = "run the same command again, or rerun with --remote-build auto or --remote-build local";
1036
+ if (!reason.includes("CocoaPods is not installed: no `pod` executable on PATH.")) return `Fix the pod install error on ${machine} (run pod install in its checkout under server.workerRoot there to see all of it), then ${retry}.`;
1037
+ const { lead, commands } = cocoapodsInstall(localCocoapods);
1038
+ return [
1039
+ `On ${machine}, ${lead}:`,
1040
+ ...commands,
1041
+ `Then ${retry}.`
1042
+ ].join("\n");
1043
+ }
1044
+ var OffloadRefusal = class extends Error {
1045
+ code = "STIM_OFFLOAD_REFUSED";
1046
+ remedy;
1047
+ constructor(machine, reason, remedy = "Run stim doctor --fix to ask for build access if not paired; it prints the stim-server setup command a person runs on the worker to approve this machine. Check stim settings get remote.machines, or rerun with --remote-build auto, remote or local.") {
1048
+ super(reason.startsWith(`${machine}: `) ? reason : `${machine}: ${reason}`);
1049
+ this.remedy = remedy;
1050
+ }
1051
+ };
1052
+ function resolveBuildMachine(flag, env, setting) {
1053
+ const selected = flag ?? (env?.trim() || void 0) ?? setting ?? "auto";
1054
+ if (typeof selected !== "string" || !selected.trim() || !parseMachine(selected)) throw Object.assign(/* @__PURE__ */ new Error("--remote-build / remote.build must be local, auto, remote, remote-only, or a tailnet machine name."), { code: "STIM_BAD_ARG" });
1055
+ return placementWord(selected) ?? selected.trim();
1056
+ }
1057
+ function parseBuildMachineOption(value) {
1058
+ try {
1059
+ return resolveBuildMachine(value);
1060
+ } catch (error) {
1061
+ throw new InvalidArgumentError(error.message);
1062
+ }
1063
+ }
1064
+ function automaticBuild(selected) {
1065
+ const word = placementWord(selected);
1066
+ return word !== null && word !== "local";
1067
+ }
1068
+ function requireLocalBuild(selected, reason) {
1069
+ const word = placementWord(selected);
1070
+ const lead = reason ? `${reason}; ` : "";
1071
+ if (word === "remote-only") throw new OffloadRefusal(selected, `${lead}remote-only never builds on this machine`);
1072
+ if ((word === "auto" || word === "remote") && !automaticMachineEnabled("build", "local")) throw new OffloadRefusal(selected, `${lead}local is disabled in remote.buildPoolDisabled; enable it or select a machine explicitly`);
1073
+ }
1074
+ function namedBuildMachine(selected) {
1075
+ return placementWord(selected) === null;
1076
+ }
1077
+ function requireConfiguredMachine(selected, machines) {
1078
+ if (!namedBuildMachine(selected)) return selected;
1079
+ const parsed = parseMachine(selected);
1080
+ const entries = Array.isArray(machines) ? machines.filter((entry) => typeof entry === "string") : [];
1081
+ const match = entries.find((entry) => {
1082
+ const candidate = parseMachine(entry);
1083
+ return candidate?.transport === parsed.transport && candidate.name === parsed.name && candidate.port === parsed.port;
1084
+ });
1085
+ if (!match) throw new OffloadRefusal(selected, `not listed in remote.machines (configured: ${entries.join(", ") || "none"})`);
1086
+ return match;
1087
+ }
1088
+ const settingSource = (flag, env, setting) => flag !== void 0 ? "flag" : env?.trim() ? "env" : setting !== void 0 ? "setting" : "default";
1089
+ /** The settings that decide where a build runs, with the layer each value came from, for this run's `--remote-build`. */
1090
+ function buildPlacementSettings(selected) {
1091
+ const flag = operationState(ambientContext()).buildMachineFlag;
1092
+ return [{
1093
+ key: "remote.build",
1094
+ value: selected,
1095
+ from: settingSource(flag, ambientEnv().STIM_REMOTE_BUILD, loadConfig$1()?.remote?.build)
1096
+ }];
1097
+ }
1098
+ function resolveBuildPlacement(flag) {
1099
+ const remote = loadConfig$1()?.remote;
1100
+ operationState(ambientContext()).buildMachineFlag = flag;
1101
+ let selected = "auto";
1102
+ try {
1103
+ selected = resolveBuildMachine(flag, ambientEnv().STIM_REMOTE_BUILD, remote?.build);
1104
+ selected = requireConfiguredMachine(selected, remote?.machines);
1105
+ if (namedBuildMachine(selected) && pairedMachines([selected]).length === 0) throw new OffloadRefusal(selected, "no remote machine is paired");
1106
+ return { selected };
1107
+ } catch (error) {
1108
+ return {
1109
+ selected,
1110
+ failure: {
1111
+ code: error.code,
1112
+ message: error.message,
1113
+ remedy: error instanceof OffloadRefusal ? error.remedy : "Pass --remote-build local, auto, remote, remote-only, or an entry from stim settings get remote.machines."
1114
+ }
1115
+ };
1116
+ }
1117
+ }
1118
+ //#endregion
1119
+ //#region src/devices/springboard-crash-reports.ts
1120
+ const REPORT_WINDOW_MS = 6e5;
1121
+ const MAX_CANDIDATES = 100;
1122
+ const MAX_STORED = 40;
1123
+ const MAX_REPORT_BYTES = 1048576;
1124
+ const MAX_COPIED_BYTES = 8388608;
1125
+ function selectSpringBoardReports(entries, udid, sinceMs) {
1126
+ const owner = `.${udid}`.toLowerCase();
1127
+ return entries.flatMap(({ name, text }) => {
1128
+ if (!/^SpringBoard-.+\.ips$/.test(name)) return [];
1129
+ try {
1130
+ const report = JSON.parse(text.slice(text.indexOf("\n") + 1));
1131
+ if (!isJsonObject(report)) return [];
1132
+ const { coalitionName, captureTime } = report;
1133
+ const capturedAt = Date.parse(typeof captureTime === "string" ? captureTime : "");
1134
+ return typeof coalitionName === "string" && coalitionName.toLowerCase().endsWith(owner) && capturedAt >= sinceMs ? [name] : [];
1135
+ } catch {
1136
+ return [];
1137
+ }
1138
+ });
1139
+ }
1140
+ /**
1141
+ * Copies this simulator's recent SpringBoard crash reports from DiagnosticReports and its Retired folder into
1142
+ * `destDir`, which keeps at most 40 of them. The source folders are only read; macOS deletes their reports after
1143
+ * about two days. Returns the matching reports now in `destDir`, how many matched but did not fit its limits, and
1144
+ * the message of a write that failed.
1145
+ */
1146
+ async function copySpringBoardCrashReports({ udid, destDir, now = Date.now(), home = homedir() }) {
1147
+ const since = now - REPORT_WINDOW_MS;
1148
+ const reportsDir = join(home, "Library/Logs/DiagnosticReports");
1149
+ const candidates = /* @__PURE__ */ new Map();
1150
+ for (const directory of [reportsDir, join(reportsDir, "Retired")]) {
1151
+ let names;
1152
+ try {
1153
+ names = await readdir(directory);
1154
+ } catch (error) {
1155
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") continue;
1156
+ throw error;
1157
+ }
1158
+ for (const name of names) {
1159
+ if (!/^SpringBoard-.+\.ips$/.test(name) || candidates.has(name)) continue;
1160
+ try {
1161
+ const path = join(directory, name);
1162
+ const { mtimeMs, size } = await stat(path);
1163
+ if (mtimeMs >= since && size <= MAX_REPORT_BYTES) candidates.set(name, {
1164
+ path,
1165
+ mtimeMs
1166
+ });
1167
+ } catch {}
1168
+ }
1169
+ }
1170
+ const newest = [...candidates].toSorted((a, b) => b[1].mtimeMs - a[1].mtimeMs).slice(0, MAX_CANDIDATES);
1171
+ const entries = [];
1172
+ for (const [name, { path }] of newest) try {
1173
+ entries.push({
1174
+ name,
1175
+ text: await readFile(path, "utf8")
1176
+ });
1177
+ } catch {}
1178
+ const matching = selectSpringBoardReports(entries, udid, since);
1179
+ if (matching.length === 0) return {
1180
+ files: [],
1181
+ dropped: 0
1182
+ };
1183
+ await mkdir(destDir, {
1184
+ recursive: true,
1185
+ mode: 448
1186
+ });
1187
+ const present = new Set(await readdir(destDir));
1188
+ let stored = [...present].filter((name) => /^SpringBoard-.+\.ips$/.test(name)).length;
1189
+ let copiedBytes = 0;
1190
+ const files = [];
1191
+ for (const { name, text } of entries) {
1192
+ if (!matching.includes(name)) continue;
1193
+ if (present.has(name)) {
1194
+ files.push(name);
1195
+ continue;
1196
+ }
1197
+ const bytes = Buffer.byteLength(text);
1198
+ if (stored >= MAX_STORED || copiedBytes + bytes > MAX_COPIED_BYTES) continue;
1199
+ try {
1200
+ await writeFile(join(destDir, name), text, {
1201
+ flag: "wx",
1202
+ mode: 384
1203
+ });
1204
+ } catch (error) {
1205
+ if (error instanceof Error && "code" in error && error.code === "EEXIST") {
1206
+ files.push(name);
1207
+ continue;
1208
+ }
1209
+ await rm(join(destDir, name), { force: true }).catch(() => {});
1210
+ return {
1211
+ files,
1212
+ dropped: 0,
1213
+ failure: error instanceof Error ? error.message : String(error)
1214
+ };
1215
+ }
1216
+ stored++;
1217
+ copiedBytes += bytes;
1218
+ files.push(name);
1219
+ }
1220
+ return {
1221
+ files,
1222
+ dropped: matching.length - files.length
1223
+ };
1224
+ }
1225
+ //#endregion
1226
+ //#region src/engine/ios-boot-log.ts
1227
+ function iosBootRecorder(root, slot, udid) {
1228
+ return (event, { level = "info", ...fields }) => {
1229
+ try {
1230
+ const writer = createNdjsonWriter(join(workspaceLogsDir$1(root), "device.ndjson"), { maxBytes: LOG_ROTATE_BYTES });
1231
+ writer.write({
1232
+ src: "device",
1233
+ platform: "ios",
1234
+ level,
1235
+ event,
1236
+ deviceId: udid,
1237
+ ...slot && slot !== "default" ? { slot } : {},
1238
+ ...fields
1239
+ });
1240
+ writer.close();
1241
+ } catch {}
1242
+ };
1243
+ }
1244
+ //#endregion
1245
+ //#region src/engine/device-ios.ts
1246
+ function recordBootPhase(record, udid, origin) {
1247
+ return (event) => {
1248
+ const pid = "pid" in event ? event.pid : void 0;
1249
+ const boot = event.phase === "boot_requested" ? origin : void 0;
1250
+ record("ios_boot", {
1251
+ phase: event.phase,
1252
+ ...pid === void 0 ? {} : { pid },
1253
+ ...boot === void 0 ? {} : { boot },
1254
+ msg: `ios boot ${udid}: ${event.phase}${pid === void 0 ? "" : ` pid ${pid}`}${boot === void 0 ? "" : ` (${boot})`}`
1255
+ });
1256
+ };
1257
+ }
1258
+ function startIosBoot(udid, configure, label, out, simulatorApp, root, slot, origin, deviceSlotWait) {
1259
+ const done = withDeviceBootAdmission({
1260
+ platform: "ios",
1261
+ key: udid
1262
+ }, async () => {
1263
+ out(chalk.dim(phaseLine("device", `booting ${label} (${udid})`)));
1264
+ await bootIosSim(udid, {
1265
+ label,
1266
+ out,
1267
+ simulatorApp,
1268
+ env: ambientContext()?.env,
1269
+ onPhase: recordBootPhase(iosBootRecorder(root, slot, udid), udid, origin)
1270
+ });
1271
+ await configure();
1272
+ await muteBootedSimulator(udid, out);
1273
+ }, {
1274
+ ...deviceSlotWait,
1275
+ root,
1276
+ out
1277
+ });
1278
+ done.catch(() => {});
1279
+ return {
1280
+ udid,
1281
+ done
1282
+ };
1283
+ }
1284
+ const SPRINGBOARD_MUTE_WAIT_MS = 6e4;
1285
+ async function muteBootedSimulator(udid, out) {
1286
+ try {
1287
+ const springBoard = await waitForSpringBoard(udid, Date.now() + SPRINGBOARD_MUTE_WAIT_MS);
1288
+ if (springBoard.state === "unreadable") throw new Error(springBoard.reason);
1289
+ if (springBoard.state === "timed-out") throw new Error(`SpringBoard did not finish starting within ${SPRINGBOARD_MUTE_WAIT_MS / 1e3}s.`);
1290
+ const response = JSON.parse(await getExecutor().runFileAsync("xcrun", [
1291
+ "devicectl",
1292
+ "device",
1293
+ "settings",
1294
+ "audio",
1295
+ "--device",
1296
+ udid,
1297
+ "--volume",
1298
+ "0",
1299
+ "--json-output",
1300
+ "-"
1301
+ ], { timeoutMs: 3e4 }));
1302
+ if (!isJsonObject(response) || !isJsonObject(response.info) || response.info.outcome !== "success" || !isJsonObject(response.result) || typeof response.result.deviceIdentifier !== "string" || response.result.deviceIdentifier.toUpperCase() !== udid.toUpperCase() || response.result.volume !== 0) throw new Error("Xcode did not confirm volume zero for this simulator.");
1303
+ out(chalk.dim(phaseLine("device", "simulator muted; turn on Sound in its Stim Desktop options")));
1304
+ } catch (error) {
1305
+ out(chalk.yellow(phaseLine("device", `Could not mute this simulator: ${error instanceof Error ? error.message : String(error)}`)));
1306
+ }
1307
+ }
1308
+ var IosDeviceMismatchError = class extends Error {
1309
+ remedy = "Run `stim worktree remove` (or `stim gc --delete`) to reap the current sim, then `stim ios` again to create the requested one, or pass `--slot <name>` to create it beside the current one.";
1310
+ };
1311
+ async function ensureOwnedIosDevice({ record, projectPath, slot, settingsRoot, label, settings, flags, note, out, reconcileIosSimulator, simSnapshot }) {
1312
+ const pressure = readHostMemoryPressure();
1313
+ const memoryAdvice = hostMemoryPressureAdvice(pressure);
1314
+ if (memoryAdvice) out(chalk.yellow(phaseLine("memory", memoryAdvice)));
1315
+ reportMemoryCulprits(pressure, (named) => out(chalk.yellow(phaseLine("memory", named))));
1316
+ const simslimProfile = iosSimSlimProfileSetting(settings, settingsRoot);
1317
+ const simslimDefault = iosSimSlimDefaultSetting(settings);
1318
+ if (record?.deviceUdid) {
1319
+ if (record.owned) {
1320
+ const listed = simSnapshot ? simSnapshot.read() : listAllIosSims({ includeUnavailable: true });
1321
+ const resolved = resolveOwnedIosSim(record.deviceUdid, listed);
1322
+ if (resolved.notOwned) note(chalk.yellow(`Note: recorded sim is now named "${resolved.notOwned}", not Stim-owned -- creating a fresh owned sim instead of booting it.`));
1323
+ else if (resolved.missing) {} else {
1324
+ const sim = resolved.sim;
1325
+ const wantedType = flags.deviceType || settings?.ios?.deviceType;
1326
+ const deviceTypes = await listIosDeviceTypesAsync({ snapshot: simSnapshot });
1327
+ const mismatch = deviceTypeMismatch(sim.deviceTypeIdentifier, wantedType, deviceTypes);
1328
+ if (mismatch) throw new IosDeviceMismatchError(`${mismatch}. Stim will not silently boot a different model.`);
1329
+ const versionMismatch = flags.runtimeFlag ? runtimeMismatch(sim.runtime, flags.runtimeFlag, await listIosRuntimesAsync({ snapshot: simSnapshot })) : null;
1330
+ if (versionMismatch) throw new IosDeviceMismatchError(`${versionMismatch}. Stim will not silently boot a different iOS version.`);
1331
+ const model = {
1332
+ model: deviceTypes.find((d) => d.identifier === sim.deviceTypeIdentifier)?.name ?? null,
1333
+ runtime: parseRuntimeVersion(sim.runtime)
1334
+ };
1335
+ const name = ownedIosName(sim, label, model, projectPath, listed) === sim.name ? sim.name : await withIosDeviceNameLock(() => renameToOwnedName(sim, label, model, projectPath));
1336
+ const updated = {
1337
+ deviceUdid: sim.udid,
1338
+ owned: true,
1339
+ deviceName: name,
1340
+ ...record.simslimManaged ? { simslimManaged: true } : {},
1341
+ ...record.simslimApplied ? { simslimApplied: record.simslimApplied } : {},
1342
+ ...record.adopted ? { adopted: true } : {},
1343
+ ...record.adoptionPending ? { adoptionPending: true } : {},
1344
+ ...record.parkedCacheKey ? { parkedCacheKey: record.parkedCacheKey } : {},
1345
+ ...record.schemeApprovals ? { schemeApprovals: record.schemeApprovals } : {}
1346
+ };
1347
+ const configure = async (freshBoot) => {
1348
+ if (record.adoptionPending) await resetAdoptedSim(sim.udid, out);
1349
+ return configureOwnedIosSim({
1350
+ record: updated,
1351
+ projectPath,
1352
+ slot,
1353
+ profile: simslimProfile,
1354
+ slimByDefault: simslimDefault,
1355
+ freshBoot,
1356
+ out,
1357
+ reconcileIosSimulator
1358
+ });
1359
+ };
1360
+ const facts = {
1361
+ deviceType: model.model,
1362
+ runtime: model.runtime
1363
+ };
1364
+ if (sim.state !== "Booted") return {
1365
+ ...updated,
1366
+ booting: startIosBoot(sim.udid, () => configure(true), name, out, flags.simulatorApp, projectPath, slot, record.adoptionPending ? "adopted" : "existing", flags.deviceSlotWait),
1367
+ ...facts
1368
+ };
1369
+ const { record: configured, rebooted } = await configure(false);
1370
+ if (rebooted) await muteBootedSimulator(sim.udid, out);
1371
+ return {
1372
+ ...configured,
1373
+ ...facts,
1374
+ booted: sim.udid
1375
+ };
1376
+ }
1377
+ } else {
1378
+ const sim = (await listAllIosSimsAsync()).find((s) => s.udid === record.deviceUdid);
1379
+ if (sim) {
1380
+ if (sim.state !== "Booted") {
1381
+ note(chalk.yellow(`Note: assigned sim ${sim.name} (${sim.udid}) is shut down and is not owned by Stim, so it will not be booted automatically.`));
1382
+ note(chalk.dim("Boot it yourself, or run `stim gc --delete` to clear the assignment so Stim can create an owned sim."));
1383
+ }
1384
+ return record;
1385
+ }
1386
+ }
1387
+ await withConfigLockAsync$1(() => {
1388
+ const current = deviceSlotPlatforms(loadConfig$1()?.projects?.[projectPath], slot)?.ios;
1389
+ if (!current) return;
1390
+ if (current.deviceUdid !== record.deviceUdid || current.owned !== record.owned) throw new Error("The simulator assignment changed during recovery. Run `stim ios` again.");
1391
+ clearDevice(projectPath, "ios", slot);
1392
+ });
1393
+ }
1394
+ const choice = await resolveIosCreation({
1395
+ deviceType: flags.deviceType || settings.ios?.deviceType,
1396
+ runtime: flags.runtime || settings.ios?.runtime
1397
+ });
1398
+ const adopted = parkedMaxSetting("ios").max > 0 ? await withIosDeviceNameLock(() => takeParkedIosSim({
1399
+ projectPath,
1400
+ slot,
1401
+ label,
1402
+ choice,
1403
+ out
1404
+ })) : null;
1405
+ if (adopted) {
1406
+ const booting = startIosBoot(adopted.deviceUdid, async () => {
1407
+ await resetAdoptedSim(adopted.deviceUdid, out);
1408
+ await configureOwnedIosSim({
1409
+ record: adopted,
1410
+ projectPath,
1411
+ slot,
1412
+ profile: simslimProfile,
1413
+ slimByDefault: simslimDefault,
1414
+ freshBoot: true,
1415
+ out,
1416
+ reconcileIosSimulator
1417
+ });
1418
+ }, adopted.deviceName, out, flags.simulatorApp, projectPath, slot, "adopted", flags.deviceSlotWait);
1419
+ return {
1420
+ ...adopted,
1421
+ booting,
1422
+ deviceType: choice.deviceType,
1423
+ runtime: choice.runtime
1424
+ };
1425
+ }
1426
+ const created = await withIosDeviceNameLock(async () => {
1427
+ const suffix = ownedIosNameSuffix(label, {
1428
+ model: choice.deviceType,
1429
+ runtime: choice.runtime
1430
+ }, projectPath, void 0, await listAllIosSimsAsync({ includeUnavailable: true }));
1431
+ const result = await createOwnedIosSim(label, { suffix }, choice);
1432
+ await withConfigLockAsync$1(() => setDevice(projectPath, "ios", {
1433
+ deviceUdid: result.udid,
1434
+ owned: true,
1435
+ deviceName: result.name
1436
+ }, slot));
1437
+ return result;
1438
+ });
1439
+ const newRecord = {
1440
+ deviceUdid: created.udid,
1441
+ owned: true,
1442
+ deviceName: created.name
1443
+ };
1444
+ const booting = startIosBoot(created.udid, () => configureOwnedIosSim({
1445
+ record: newRecord,
1446
+ projectPath,
1447
+ slot,
1448
+ profile: simslimProfile,
1449
+ slimByDefault: simslimDefault,
1450
+ freshBoot: true,
1451
+ out,
1452
+ reconcileIosSimulator
1453
+ }), created.name, out, flags.simulatorApp, projectPath, slot, "created", flags.deviceSlotWait);
1454
+ return {
1455
+ ...newRecord,
1456
+ created: true,
1457
+ booting,
1458
+ deviceType: created.deviceType,
1459
+ runtime: created.runtime
1460
+ };
1461
+ }
1462
+ function withIosDeviceNameLock(fn) {
1463
+ return withWorkspaceProcessLock(getConfigDir(), "ios-device-names", async () => fn(), { external: true });
1464
+ }
1465
+ function ownedIosNameSuffix(label, model, projectPath, udid, listed) {
1466
+ const names = new Set(listed.filter((sim) => sim.udid !== udid).map((sim) => sim.name));
1467
+ for (const project of Object.values(loadConfig$1()?.projects ?? {})) for (const { platforms } of projectDeviceSlots(project)) {
1468
+ const record = platforms.ios;
1469
+ if (record?.deviceUdid !== udid && record?.deviceName) names.add(record.deviceName);
1470
+ }
1471
+ for (const parked of readParked("ios")) if (parked.udid !== udid) names.add(parked.name);
1472
+ let suffix = "";
1473
+ for (let attempt = 0; names.has(ownedSimName(label, model, suffix)); attempt++) suffix = ` ${workspaceId$1(projectPath)}${attempt ? `-${attempt}` : ""}`;
1474
+ return suffix;
1475
+ }
1476
+ function ownedIosName(sim, label, model, projectPath, listed) {
1477
+ return ownedSimName(label, model, ownedIosNameSuffix(label, model, projectPath, sim.udid, listed));
1478
+ }
1479
+ async function renameToOwnedName(sim, label, model, projectPath) {
1480
+ const wanted = ownedIosName(sim, label, model, projectPath, await listAllIosSimsAsync({ includeUnavailable: true }));
1481
+ if (sim.name === wanted) return wanted;
1482
+ try {
1483
+ await renameIosSim(sim.udid, wanted);
1484
+ return wanted;
1485
+ } catch {
1486
+ return sim.name;
1487
+ }
1488
+ }
1489
+ async function resetAdoptedSim(udid, out) {
1490
+ out(chalk.dim(phaseLine("device", `resetting privacy grants and the keychain on ${udid}`)));
1491
+ await resetIosPrivacy(udid);
1492
+ await resetIosKeychain(udid);
1493
+ }
1494
+ async function takeParkedIosSim({ projectPath, slot, label, choice, out }) {
1495
+ const candidates = selectParked(readParked("ios"), {
1496
+ deviceTypeIdentifier: choice.deviceTypeId,
1497
+ runtimeIdentifier: choice.runtimeId
1498
+ });
1499
+ if (candidates.length === 0) return null;
1500
+ const listed = new Map((await listAllIosSimsAsync({ includeUnavailable: true })).map((sim) => [sim.udid, sim]));
1501
+ for (const parked of candidates) {
1502
+ const sim = listed.get(parked.udid);
1503
+ if (!sim || !sim.available) {
1504
+ const result = sim ? await teardownParkedIosSim(parked.udid, { label: parked.name }) : null;
1505
+ if (sim ? result?.status === "torn-down" : await dropParked("ios", parked.udid)) out(chalk.dim(phaseLine("device", `deleted parked ${parked.name} (${parked.udid}): ${sim ? "unavailable" : "gone"}`)));
1506
+ else if (result?.status === "failed") out(chalk.yellow(phaseLine("device", `could not delete unavailable parked ${parked.name} (${parked.udid}): ${result.reason}`)));
1507
+ continue;
1508
+ }
1509
+ if (!sim.name.startsWith("stim-")) {
1510
+ out(chalk.yellow(phaseLine("device", `kept parked record for ${parked.udid}: simulator is now named ${JSON.stringify(sim.name)} and is not Stim-owned`)));
1511
+ continue;
1512
+ }
1513
+ const model = {
1514
+ model: choice.deviceType,
1515
+ runtime: choice.runtime
1516
+ };
1517
+ const name = ownedSimName(label, model, ownedIosNameSuffix(label, model, projectPath, parked.udid, [...listed.values()]));
1518
+ const device = {
1519
+ deviceUdid: parked.udid,
1520
+ owned: true,
1521
+ deviceName: name,
1522
+ adopted: true,
1523
+ adoptionPending: true,
1524
+ ...parked.simslimManaged ? { simslimManaged: true } : {},
1525
+ ...parked.cacheKey ? { parkedCacheKey: parked.cacheKey } : {}
1526
+ };
1527
+ const adopted = await adoptParked({
1528
+ platform: "ios",
1529
+ projectPath,
1530
+ slot,
1531
+ udid: parked.udid,
1532
+ device
1533
+ });
1534
+ if (!adopted) continue;
1535
+ try {
1536
+ await renameIosSim(parked.udid, name);
1537
+ } catch {}
1538
+ return adopted.schemeApprovals && adopted.schemeApprovals.length > 0 ? {
1539
+ ...device,
1540
+ schemeApprovals: adopted.schemeApprovals
1541
+ } : device;
1542
+ }
1543
+ return null;
1544
+ }
1545
+ function clearIosAdoptionPending(projectPath, slot = "default") {
1546
+ return withConfigLockAsync$1(() => {
1547
+ const cfg = loadConfig$1();
1548
+ const ios = deviceSlotPlatforms(cfg?.projects?.[projectPath], slot)?.ios;
1549
+ if (!cfg || !ios?.adoptionPending) return;
1550
+ delete ios.adopted;
1551
+ delete ios.adoptionPending;
1552
+ delete ios.parkedCacheKey;
1553
+ saveConfig(cfg);
1554
+ });
1555
+ }
1556
+ function simslimProfileIdentity(profile, slimByDefault) {
1557
+ if (profile) return `profile:${createHash("sha256").update(readFileSync(profile)).digest("hex")}`;
1558
+ return slimByDefault ? "default" : null;
1559
+ }
1560
+ async function startedSpringBoardPid(udid) {
1561
+ const springBoard = await waitForSpringBoard(udid, Date.now() + SPRINGBOARD_MUTE_WAIT_MS);
1562
+ return springBoard.state === "started" ? springBoard.pid : null;
1563
+ }
1564
+ async function configureOwnedIosSim({ record, projectPath, slot, profile, slimByDefault, freshBoot, out, reconcileIosSimulator }) {
1565
+ const identity = simslimProfileIdentity(profile, slimByDefault);
1566
+ const slimConfigured = identity !== null || Boolean(record.simslimManaged);
1567
+ const applied = record.simslimApplied;
1568
+ const springBoardBefore = slimConfigured ? await readSpringBoardPid(record.deviceUdid) : null;
1569
+ if (identity !== null && record.simslimManaged && !freshBoot && applied?.profile === identity && springBoardBefore !== null && springBoardBefore === applied.springBoardPid) {
1570
+ await withConfigLockAsync$1(() => setDevice(projectPath, "ios", { ...record }, slot));
1571
+ return {
1572
+ record,
1573
+ rebooted: false
1574
+ };
1575
+ }
1576
+ if (profile) out(chalk.dim(phaseLine("device", `applying the SimSlim profile to ${record.deviceUdid}`)));
1577
+ else if (slimByDefault) out(chalk.dim(phaseLine("device", `applying SimSlim's default services to ${record.deviceUdid}`)));
1578
+ else if (record.simslimManaged) out(chalk.dim(phaseLine("device", `restoring stock simulator services on ${record.deviceUdid}`)));
1579
+ const previouslyManaged = Boolean(record.simslimManaged);
1580
+ if (identity !== null && !record.simslimManaged || applied) {
1581
+ const { simslimApplied: _applied, ...rest } = record;
1582
+ const pending = identity !== null ? {
1583
+ ...rest,
1584
+ simslimManaged: true
1585
+ } : rest;
1586
+ await withConfigLockAsync$1(() => setDevice(projectPath, "ios", pending, slot));
1587
+ record = pending;
1588
+ }
1589
+ const result = await reconcileIosSimulator({
1590
+ context: ambientContext(),
1591
+ udid: record.deviceUdid,
1592
+ profile,
1593
+ slimByDefault,
1594
+ previouslyManaged,
1595
+ out
1596
+ });
1597
+ const updated = { ...record };
1598
+ if (result.managed) updated.simslimManaged = true;
1599
+ else delete updated.simslimManaged;
1600
+ const springBoardAfter = slimConfigured ? await startedSpringBoardPid(record.deviceUdid) : null;
1601
+ if (result.managed && !result.notInstalled && identity !== null && springBoardAfter !== null) updated.simslimApplied = {
1602
+ profile: identity,
1603
+ springBoardPid: springBoardAfter
1604
+ };
1605
+ await withConfigLockAsync$1(() => setDevice(projectPath, "ios", updated, slot));
1606
+ const rebooted = slimConfigured && springBoardAfter !== springBoardBefore;
1607
+ if (rebooted && !result.notInstalled) iosBootRecorder(projectPath, slot, record.deviceUdid)("ios_boot", {
1608
+ phase: "simslim_reboot",
1609
+ pidBefore: springBoardBefore,
1610
+ pidAfter: springBoardAfter,
1611
+ msg: `ios boot ${record.deviceUdid}: simslim_reboot (SpringBoard pid ${springBoardBefore ?? "unknown"} to ${springBoardAfter ?? "unknown"})`
1612
+ });
1613
+ return {
1614
+ record: updated,
1615
+ rebooted
1616
+ };
1617
+ }
1618
+ const SPRINGBOARD_RESPAWN_POLLS = 20;
1619
+ const SPAWN_STALL_TOLERANCE_MS = 18e4;
1620
+ async function ensureIosBooted(args) {
1621
+ const { springBoardDown, ...first } = await bootOwnedIosSim(args);
1622
+ const { device, projectPath, out } = args;
1623
+ const udid = device?.deviceUdid;
1624
+ if (!springBoardDown || !device || !udid) return first;
1625
+ out(chalk.yellow(phaseLine("device", `SpringBoard is not running on ${udid}; restarting the simulator once`)));
1626
+ const record = iosBootRecorder(projectPath, args.slot, udid);
1627
+ await preserveSpringBoardReports(record, udid, projectPath);
1628
+ const stopped = await teardownOwnedIosSim(udid, { workspace: projectPath });
1629
+ if (stopped.status !== "torn-down") return {
1630
+ ...first,
1631
+ reason: `${first.reason} The restart did not run: ${stopped.reason ?? stopped.status}.`
1632
+ };
1633
+ record("ios_boot", {
1634
+ phase: "restart",
1635
+ level: "warn",
1636
+ msg: `ios boot ${udid}: restart (SpringBoard is not running after boot; restarting once)`
1637
+ });
1638
+ const { booted: _booted, booting: _booting, ...unbooted } = device;
1639
+ const { springBoardDown: stillDown, ...second } = await bootOwnedIosSim({
1640
+ ...args,
1641
+ device: unbooted,
1642
+ origin: "restart"
1643
+ });
1644
+ if (stillDown) await preserveSpringBoardReports(record, udid, projectPath);
1645
+ return stillDown ? {
1646
+ ...second,
1647
+ reason: `${second.reason} Stim already restarted the simulator once, so it did not restart it again.`
1648
+ } : second;
1649
+ }
1650
+ async function preserveSpringBoardReports(record, udid, root) {
1651
+ const dir = workspaceLogsDir$1(root);
1652
+ try {
1653
+ const { files, dropped, failure } = await copySpringBoardCrashReports({
1654
+ udid,
1655
+ destDir: dir
1656
+ });
1657
+ const limit = dropped ? `; ${dropped} more matched but exceeded the copy limits` : "";
1658
+ record("springboard_crash_reports", {
1659
+ level: files.length || dropped || failure ? "warn" : "info",
1660
+ files,
1661
+ dropped,
1662
+ ...failure ? { failure } : {},
1663
+ dir,
1664
+ msg: failure ? `could not copy a SpringBoard crash report for ${udid} (${files.length} copied before it): ${failure}` : files.length ? `copied ${files.length} SpringBoard crash report(s) for ${udid} to ${dir}: ${files.join(", ")}${limit}` : dropped ? `${dropped} SpringBoard crash report(s) for ${udid} matched but exceeded the copy limits` : `no SpringBoard crash report for ${udid} from the last 10 minutes`
1665
+ });
1666
+ } catch (error) {
1667
+ record("springboard_crash_reports", {
1668
+ level: "warn",
1669
+ files: [],
1670
+ dropped: 0,
1671
+ failure: error instanceof Error ? error.message : String(error),
1672
+ dir,
1673
+ msg: `could not copy SpringBoard crash reports for ${udid}: ${error instanceof Error ? error.message : String(error)}`
1674
+ });
1675
+ }
1676
+ }
1677
+ async function bootOwnedIosSim({ device, projectPath, slot, origin, deviceSlotWait, simulatorApp, timeoutMs, pollMs, out }) {
1678
+ const udid = device?.deviceUdid;
1679
+ if (!udid) return {
1680
+ failed: true,
1681
+ reason: "No iOS simulator is recorded for this project."
1682
+ };
1683
+ const onPhase = recordBootPhase(iosBootRecorder(projectPath, slot, udid), udid, origin ?? (device.adoptionPending ? "adopted" : "existing"));
1684
+ const openViewer = async () => {
1685
+ await configuredIosSimulatorViewer(simulatorApp, ambientContext()?.env).open(udid);
1686
+ onPhase({ phase: "viewer_opened" });
1687
+ };
1688
+ const ready = async () => {
1689
+ try {
1690
+ const stallDeadline = Date.now() + SPAWN_STALL_TOLERANCE_MS;
1691
+ const listSpringBoard = async () => {
1692
+ for (;;) try {
1693
+ return await getExecutor().runFileAsync("xcrun", [
1694
+ "simctl",
1695
+ "spawn",
1696
+ udid,
1697
+ "launchctl",
1698
+ "list"
1699
+ ], { timeoutMs: 3e4 });
1700
+ } catch (error) {
1701
+ if (!isTimeoutError(error) || Date.now() >= stallDeadline) throw error;
1702
+ const sims = await listAllIosSimsAsync().catch(() => null);
1703
+ if (sims && sims.find((s) => s.udid === udid)?.state !== "Booted") throw error;
1704
+ }
1705
+ };
1706
+ let listing = await listSpringBoard();
1707
+ for (let poll = 0; !springBoardRunning(listing) && poll < SPRINGBOARD_RESPAWN_POLLS; poll++) {
1708
+ await cancellableSleep(pollMs);
1709
+ listing = await listSpringBoard();
1710
+ }
1711
+ if (!springBoardRunning(listing)) return {
1712
+ failed: true,
1713
+ springBoardDown: true,
1714
+ reason: `Simulator ${udid} reports Booted, but SpringBoard (the system shell) is not running, so apps cannot launch. Run \`stim stop\`, then \`stim ios\` again. If it repeats, look for SpringBoard crash reports in ~/Library/Logs/DiagnosticReports.`
1715
+ };
1716
+ return {
1717
+ ok: true,
1718
+ udid
1719
+ };
1720
+ } catch (error) {
1721
+ return {
1722
+ failed: true,
1723
+ reason: `Simulator ${udid} failed its process-spawn readiness check: ${error?.message || error}. ${await iosSimulatorFailureAdvice()}`
1724
+ };
1725
+ }
1726
+ };
1727
+ const booting = device?.booting;
1728
+ if (booting?.udid === udid) {
1729
+ try {
1730
+ await booting.done;
1731
+ } catch (e) {
1732
+ return bootFailure(udid, e);
1733
+ }
1734
+ return ready();
1735
+ }
1736
+ const readiness = device?.booted === udid ? await ready() : null;
1737
+ if (readiness?.ok) {
1738
+ if (simulatorApp !== void 0) await openViewer();
1739
+ return readiness;
1740
+ }
1741
+ let resolved;
1742
+ try {
1743
+ resolved = resolveOwnedIosSim(udid, await listAllIosSimsAsync());
1744
+ } catch (e) {
1745
+ return {
1746
+ failed: true,
1747
+ reason: `Could not list simulators: ${e?.message || e}`
1748
+ };
1749
+ }
1750
+ if (resolved.missing) return {
1751
+ failed: true,
1752
+ reason: `Simulator ${udid} no longer exists. Run \`stim ios\` again to create a fresh owned sim.`
1753
+ };
1754
+ if (resolved.notOwned) return {
1755
+ failed: true,
1756
+ reason: `Simulator ${udid} is now named "${resolved.notOwned}" and is not Stim-owned; refusing to boot it.`
1757
+ };
1758
+ const sim = resolved.sim;
1759
+ if (sim.state === "Booted") {
1760
+ const result = readiness ?? await ready();
1761
+ if (result.ok && simulatorApp !== void 0) await openViewer();
1762
+ return result;
1763
+ }
1764
+ const bootDeadline = Date.now() + timeoutMs;
1765
+ try {
1766
+ await withDeviceBootAdmission({
1767
+ platform: "ios",
1768
+ key: udid
1769
+ }, async () => {
1770
+ out(chalk.dim(phaseLine("device", `booting ${sim.name} (${udid})`)));
1771
+ await bootIosSim(udid, {
1772
+ timeoutMs,
1773
+ label: sim.name,
1774
+ out,
1775
+ simulatorApp,
1776
+ env: ambientContext()?.env,
1777
+ onPhase
1778
+ });
1779
+ await muteBootedSimulator(udid, out);
1780
+ }, {
1781
+ ...deviceSlotWait,
1782
+ root: projectPath,
1783
+ out
1784
+ });
1785
+ } catch (e) {
1786
+ return bootFailure(udid, e);
1787
+ }
1788
+ const deadline = Math.max(bootDeadline, Date.now() + 2 * pollMs);
1789
+ while (Date.now() < deadline) {
1790
+ await cancellableSleep(pollMs);
1791
+ let state = null;
1792
+ try {
1793
+ state = (await listAllIosSimsAsync({ timeoutMs: 3e4 })).find((s) => s.udid === udid)?.state ?? null;
1794
+ } catch {}
1795
+ if (state === "Booted") return ready();
1796
+ }
1797
+ return {
1798
+ failed: true,
1799
+ reason: `Simulator ${udid} did not reach the Booted state within ${Math.round(timeoutMs / 1e3)}s.`
1800
+ };
1801
+ }
1802
+ function bootFailure(udid, error) {
1803
+ const refusal = error instanceof DeviceAdmissionRefusal ? error : (0, ownership_claim_exports.claimFailure)(error, "stim ios");
1804
+ if (refusal) return {
1805
+ failed: true,
1806
+ code: refusal.code,
1807
+ reason: refusal.message,
1808
+ remedy: refusal.remedy
1809
+ };
1810
+ return {
1811
+ failed: true,
1812
+ reason: `Could not boot simulator ${udid}: ${error?.message || error}`
1813
+ };
1814
+ }
1815
+ function buildLogFile(root) {
1816
+ return join(workspaceLogsDir$1(root), `build-ios.ndjson`);
1817
+ }
1818
+ function iosProjectPathError(settings, root, isExpo) {
1819
+ const error = iosProjectDirSettingError(settings, root);
1820
+ if (error || !iosProjectDirSetting(settings, root).custom || !isExpo()) return error;
1821
+ return "ios.projectPath applies to bare React Native apps only: expo prebuild generates and builds ios/. Remove it for this Expo app.";
1822
+ }
1823
+ function resolveIosBuildSetup(remoteBuild, settings, warn, env) {
1824
+ const [shapeError, ...lines] = settingShapeErrors(settings);
1825
+ if (shapeError) return {
1826
+ ok: false,
1827
+ failure: {
1828
+ code: "STIM_BAD_ARG",
1829
+ message: shapeError,
1830
+ lines,
1831
+ remedy: SETTING_SHAPE_REMEDY
1832
+ }
1833
+ };
1834
+ const placement = resolveBuildPlacement(remoteBuild);
1835
+ if (placement.failure) return {
1836
+ ok: false,
1837
+ failure: {
1838
+ ...placement.failure,
1839
+ setup: true
1840
+ }
1841
+ };
1842
+ let optimizations;
1843
+ try {
1844
+ optimizations = resolveOptimizations(settings, env);
1845
+ } catch (error) {
1846
+ return {
1847
+ ok: false,
1848
+ failure: {
1849
+ code: "STIM_BAD_ARG",
1850
+ message: error.message,
1851
+ remedy: SETTING_SHAPE_REMEDY
1852
+ }
1853
+ };
1854
+ }
1855
+ for (const key of unknownSettingKeys(settings)) warn("setting", inertSettingWarning(settings, key));
1856
+ const cacheProviderError = cacheProviderSettingError(settings);
1857
+ if (cacheProviderError) warn("cache", `${cacheProviderError} Using the local cache.`);
1858
+ return {
1859
+ ok: true,
1860
+ buildMachine: placement.selected,
1861
+ optimizations
1862
+ };
1863
+ }
1864
+ const MAX_PRINTED_DIAGNOSTICS = 6;
1865
+ function deviceLabel(device, udid) {
1866
+ const name = device?.deviceName || device?.name || null;
1867
+ return name ? `${name} (${shortUdid(udid)})` : shortUdid(udid);
1868
+ }
1869
+ function deviceShortName(device, udid) {
1870
+ return device?.deviceName || device?.name || shortUdid(udid);
1871
+ }
1872
+ function appNameFromPath(appPath) {
1873
+ if (typeof appPath !== "string" || appPath.trim() === "") return null;
1874
+ const name = basename(appPath).replace(/\.app$/i, "");
1875
+ return name === "" ? null : name;
1876
+ }
1877
+ function iosConfigurationSetting(settings) {
1878
+ const ios = settings?.["ios"];
1879
+ if (!ios || typeof ios !== "object" || Array.isArray(ios)) return null;
1880
+ const raw = ios["configuration"];
1881
+ return typeof raw === "string" && raw.trim() !== "" ? raw.trim() : null;
1882
+ }
1883
+ function resolveConfiguration(flag, settings) {
1884
+ return (typeof flag === "string" && flag.trim() !== "" ? flag.trim() : null) || iosConfigurationSetting(settings);
1885
+ }
1886
+ function resolveSchemeSelection({ scheme, easProfile }, settings) {
1887
+ if (typeof scheme === "string" || easProfile !== void 0) return scheme ?? void 0;
1888
+ return iosStringSetting(settings, "scheme") ?? void 0;
1889
+ }
1890
+ function iosStringSetting(settings, key) {
1891
+ const ios = settings?.["ios"];
1892
+ if (!ios || typeof ios !== "object" || Array.isArray(ios)) return null;
1893
+ const raw = ios[key];
1894
+ return typeof raw === "string" && raw.trim() !== "" ? raw.trim() : null;
1895
+ }
1896
+ function resolveDeviceType(flag, settings) {
1897
+ return (typeof flag === "string" && flag.trim() !== "" ? flag.trim() : null) || iosStringSetting(settings, "deviceType");
1898
+ }
1899
+ function resolveRuntime(flag, settings) {
1900
+ return (typeof flag === "string" && flag.trim() !== "" ? flag.trim() : null) || iosStringSetting(settings, "runtime");
1901
+ }
1902
+ function resolveSimulatorAppFlag(flag, physical, remoteBackend, automatic = false) {
1903
+ if (flag === void 0) return {};
1904
+ let simulatorApp;
1905
+ try {
1906
+ simulatorApp = parseIosSimulatorApp(flag, "--simulator-app");
1907
+ } catch (error) {
1908
+ return { refusal: {
1909
+ code: "STIM_BAD_ARG",
1910
+ message: error.message
1911
+ } };
1912
+ }
1913
+ if (physical || remoteBackend && !automatic) return { refusal: {
1914
+ code: "STIM_BAD_ARG",
1915
+ message: "--simulator-app only applies to a local owned iOS simulator.",
1916
+ remedy: "Drop --simulator-app when using a physical or remote device."
1917
+ } };
1918
+ return { simulatorApp };
1919
+ }
1920
+ async function deviceModelRefusal({ slot, deviceTypeFlag, runtimeFlag, deviceType, runtime, deviceTypeOrigin = null, runtimeOrigin = null, physical, remoteBackend, hosted = false, listRuntimes, signal, snapshot }) {
1921
+ if (slot && slot !== "default" && remoteBackend) return {
1922
+ code: "STIM_BAD_ARG",
1923
+ message: "Named slots currently support local simulators and physical devices.",
1924
+ remedy: "Use the default slot for a remote session."
1925
+ };
1926
+ if (typeof deviceTypeFlag === "string" && deviceTypeFlag.trim() === "") return {
1927
+ code: "STIM_BAD_ARG",
1928
+ message: "--device-type was given an empty name.",
1929
+ remedy: "Pass `--device-type <name>` with a model `xcrun simctl list devicetypes` names, e.g. \"iPad Pro 13-inch (M5)\"."
1930
+ };
1931
+ if (typeof runtimeFlag === "string" && runtimeFlag.trim() === "") return {
1932
+ code: "STIM_BAD_ARG",
1933
+ message: "--runtime was given an empty version.",
1934
+ remedy: "Pass `--runtime <version>` with a runtime `xcrun simctl list runtimes` reports, e.g. \"18.5\"."
1935
+ };
1936
+ if (remoteBackend) {
1937
+ const eas = remoteBackend === "eas";
1938
+ const given = [!eas && deviceTypeFlag && "--device-type", runtimeFlag && "--runtime"].filter(Boolean).join(" and ");
1939
+ if (given) return {
1940
+ code: "STIM_BAD_ARG",
1941
+ message: `${given} ${given.includes(" and ") ? "apply" : "applies"} only to a local owned iOS simulator; the ${remoteBackend} remote backend chooses its own ${eas ? "iOS runtime" : "device"}.`,
1942
+ remedy: `Drop ${given} for a remote run. For a local owned simulator, drop --remote and unset ios.remote with \`stim settings unset ios.remote --scope <machine|workspace|repo|committed>\`.`
1943
+ };
1944
+ return null;
1945
+ }
1946
+ if (physical || hosted) return null;
1947
+ if (!deviceType && !runtime) return null;
1948
+ let runtimes;
1949
+ try {
1950
+ runtimes = await listRuntimes({
1951
+ signal,
1952
+ snapshot
1953
+ });
1954
+ } catch (error) {
1955
+ return {
1956
+ code: "STIM_NO_DEVICE",
1957
+ message: `Could not read the installed simulator runtimes: ${error?.message || error}`,
1958
+ remedy: "Run `stim doctor` to check the simulator toolchain, then try again."
1959
+ };
1960
+ }
1961
+ const runtimeRefusal = unknownIosRuntimeRefusal(runtime, runtimes);
1962
+ if (runtimeRefusal) {
1963
+ const { message, remedy } = layerNote(runtimeRefusal, "ios.runtime", runtimeFlag, runtimeOrigin);
1964
+ return {
1965
+ code: "STIM_BAD_ARG",
1966
+ message,
1967
+ remedy
1968
+ };
1969
+ }
1970
+ const deviceTypeRefusal = unknownIosDeviceTypeRefusal(deviceType, runtimes, runtime);
1971
+ if (deviceTypeRefusal) {
1972
+ const { message, remedy } = layerNote(deviceTypeRefusal, "ios.deviceType", deviceTypeFlag, deviceTypeOrigin);
1973
+ return {
1974
+ code: "STIM_BAD_ARG",
1975
+ message,
1976
+ remedy
1977
+ };
1978
+ }
1979
+ return null;
1980
+ }
1981
+ function ownedSimFailure(error) {
1982
+ return {
1983
+ code: "STIM_NO_DEVICE",
1984
+ message: `Could not ensure an owned iOS simulator: ${error?.message || error}`,
1985
+ remedy: error instanceof IosDeviceMismatchError ? error.remedy : "Run `stim doctor` to check the simulator toolchain, then try again."
1986
+ };
1987
+ }
1988
+ function isReleaseConfiguration(configuration) {
1989
+ return typeof configuration === "string" && configuration.trim() !== "" && configuration.trim().toLowerCase() !== "debug";
1990
+ }
1991
+ function simulatorBuildArch({ physical, remoteArch, hostArch, configuration }) {
1992
+ if (physical) return null;
1993
+ if (remoteArch) return remoteArch;
1994
+ return isReleaseConfiguration(configuration) ? null : hostArch;
1995
+ }
1996
+ function iosProviderRunOptions(configuration, arch) {
1997
+ if (arch) return {
1998
+ configuration: configuration || "Debug",
1999
+ arch
2000
+ };
2001
+ return configuration ? { configuration } : null;
2002
+ }
2003
+ function podAction(podState, verdict, { prebuildRan = false } = {}) {
2004
+ if (verdict?.stale) return {
2005
+ install: true,
2006
+ reason: verdict.reason
2007
+ };
2008
+ if (verdict?.noPods && podState?.hasPodfile) return {
2009
+ install: true,
2010
+ reason: "ios/Podfile exists but no pods are installed"
2011
+ };
2012
+ if (prebuildRan && podState?.hasPodfile) return {
2013
+ install: true,
2014
+ reason: "prebuild updated ios/ in place"
2015
+ };
2016
+ return { install: false };
2017
+ }
2018
+ function xcodeFailureReport(result, logPath) {
2019
+ const diagnostics = result.diagnostics;
2020
+ const code = result.exitCode;
2021
+ const how = code === null ? "" : ` (exit code ${code})`;
2022
+ return {
2023
+ message: diagnostics.length ? `\`xcodebuild\` failed${how} with ${diagnostics.length} diagnostic${diagnostics.length === 1 ? "" : "s"}.` : `\`xcodebuild\` failed${how} with no recognizable diagnostic.`,
2024
+ remedy: diagnostics.find((d) => d?.remedy)?.remedy || `See ${logPath} for the transcript.`
2025
+ };
2026
+ }
2027
+ function printDiagnostics(note, result) {
2028
+ const diagnostics = result.diagnostics;
2029
+ const shown = diagnostics.slice(0, MAX_PRINTED_DIAGNOSTICS);
2030
+ for (const diagnostic of shown) note(chalk.red(phaseLine("error", describeDiagnostic(diagnostic))));
2031
+ const hidden = diagnostics.length - shown.length + result.truncated;
2032
+ if (hidden > 0) note(chalk.dim(phaseLine("error", `... and ${hidden} more diagnostic${hidden === 1 ? "" : "s"} in the log`)));
2033
+ if (diagnostics.length === 0) {
2034
+ note(chalk.red(phaseLine("error", "xcodebuild failed with no recognizable diagnostic; last lines:")));
2035
+ for (const line of result.tail.slice(-5)) note(chalk.dim(phaseLine("", line)));
2036
+ }
2037
+ }
2038
+ function resolveIosWait(opts, physical) {
2039
+ const noWait = opts.wait === false;
2040
+ if (opts.waitConflict) return { failure: {
2041
+ code: "STIM_BAD_ARG",
2042
+ message: "--wait and --no-wait ask for opposite things.",
2043
+ remedy: "Pass only one of `--wait <seconds>` and `--no-wait`."
2044
+ } };
2045
+ const waitParsed = parseDeviceWait(noWait ? void 0 : opts.wait);
2046
+ if ("error" in waitParsed) return { failure: {
2047
+ code: "STIM_BAD_ARG",
2048
+ message: waitParsed.error,
2049
+ remedy: "Pass a whole number of seconds, e.g. --wait 90. `--wait 0` refuses a busy lease or device slot at once."
2050
+ } };
2051
+ return {
2052
+ waitSeconds: waitParsed.seconds,
2053
+ noWait,
2054
+ checkCapacity: !physical && (noWait || waitParsed.seconds === 0),
2055
+ deviceSlotWaitMs: noWait ? 0 : opts.wait === void 0 && !physical ? DEFAULT_DEVICE_SLOT_WAIT_MS : waitParsed.seconds * 1e3
2056
+ };
2057
+ }
2058
+ function createIosDeviceWaitRun(stats, progress, signal) {
2059
+ const controller = new AbortController();
2060
+ let waiting = false;
2061
+ let boot;
2062
+ let outcome;
2063
+ return {
2064
+ policy: {
2065
+ signal: signal ? AbortSignal.any([controller.signal, signal]) : controller.signal,
2066
+ waitingFor(info) {
2067
+ waiting = info !== null;
2068
+ progress.waitingFor(info, "device-slot");
2069
+ },
2070
+ onWait: stats.addDeviceSlotWaitMs,
2071
+ onBootStart: () => progress.bootStarted()
2072
+ },
2073
+ record(next) {
2074
+ if (waiting) outcome = next;
2075
+ else stats.record(next);
2076
+ },
2077
+ booting(pending) {
2078
+ boot ??= pending?.done;
2079
+ },
2080
+ async finish() {
2081
+ const queued = waiting;
2082
+ controller.abort();
2083
+ if (queued) await boot?.catch(() => {});
2084
+ if (outcome) stats.record(outcome);
2085
+ }
2086
+ };
2087
+ }
2088
+ //#endregion
2089
+ export { inspectBuildMachines as $, fingerprintMismatchRemedy as A, DEVICE_BUSY as B, clearIosAdoptionPending as C, OffloadRefusal as D, muteBootedSimulator as E, requireLocalBuild as F, lostLine as G, DEVICE_WAIT_POLL_MS as H, resolveBuildMachine as I, releaseLeaseOnAbort as J, lostRefusal as K, resolveBuildPlacement as L, namedBuildMachine as M, parseBuildMachineOption as N, automaticBuild as O, podsFailedRemedy as P, forgetBuildMachine as Q, workerFailureReason as R, xcodeFailureReport as S, ensureOwnedIosDevice as T, acquireRunLease as U, DEVICE_WAIT_LINE_MS as V, leaseExpiryText as W, waitFlagConflict as X, runLease as Y, waitForDevice as Z, resolveIosWait as _, deviceModelRefusal as a, inspectDeviceHostMachines as at, resolveSimulatorAppFlag as b, iosProjectPathError as c, ownedSimFailure as d, pairedMachines as et, podAction as f, resolveIosBuildSetup as g, resolveDeviceType as h, deviceLabel as i, hostingMachines as it, missingPackageManagerRemedy as j, buildPlacementSettings as k, iosProviderRunOptions as l, resolveConfiguration as m, buildLogFile as n, configuredMachines as nt, deviceShortName as o, printDiagnostics as p, parseDeviceWait as q, createIosDeviceWaitRun as r, forgetDeviceHostMachine as rt, iosConfigurationSetting as s, appNameFromPath as t, predictedRefusal as tt, isReleaseConfiguration as u, resolveRuntime as v, ensureIosBooted as w, simulatorBuildArch as x, resolveSchemeSelection as y, DEBUG_VERIFY_STEP_MS as z };