stim 1.20.2 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/README.md +141 -8
  2. package/dist/acquire-artifact-Co2k4f7O.mjs +171 -0
  3. package/dist/act-DsuqEkiM.mjs +2801 -0
  4. package/dist/activity-CMLmzp7o.mjs +1397 -0
  5. package/dist/{agent-device-usage-output-Dzgv6lgR.mjs → agent-device-usage-output-DUFaTMdd.mjs} +23 -67
  6. package/dist/{android-DCKZrNvV.mjs → android-ByRMUoiJ.mjs} +157 -93
  7. package/dist/{android-Mz_Inn3w.mjs → android-C7sVmyI9.mjs} +299 -116
  8. package/dist/{android-Dlzxshgp.mjs → android-DGkNqa_c.mjs} +5 -5
  9. package/dist/{android-HqP1_vB_.mjs → android-DR5lZ2Az.mjs} +2 -2
  10. package/dist/android-cas-compiler.mjs +2 -2
  11. package/dist/{android-cas-Bl-cLJBk.mjs → android-cas-ngferVq2.mjs} +7 -6
  12. package/dist/android-project-BCAAmMwz.mjs +12 -0
  13. package/dist/android-project-DVWyCbWU.d.mts +391 -0
  14. package/dist/api.d.mts +810 -64
  15. package/dist/api.mjs +782 -1
  16. package/dist/{artifact-HSwkIWEQ.mjs → artifact-8EY8SAPw.mjs} +240 -213
  17. package/dist/{artifact-D3czlUvT.mjs → artifact-DJPy518Y.mjs} +203 -164
  18. package/dist/artifact-platform-CXcmX_j3.mjs +27 -0
  19. package/dist/{browser-web-CA-F1GQo.mjs → browser-web-Bcv99PBM.mjs} +194 -64
  20. package/dist/budget-Cvwn54qV.mjs +9285 -0
  21. package/dist/{build-DYDbfkk5.mjs → build-CabC3G23.mjs} +153 -98
  22. package/dist/build-cache-Dv7RN200.mjs +602 -0
  23. package/dist/build-plan-DUC8Ka9y.mjs +248 -0
  24. package/dist/{cache-manifest-XH-fvDUj.mjs → cache-manifest-DSeQjt-Q.mjs} +2 -2
  25. package/dist/cache-manifest.mjs +1 -1
  26. package/dist/{ccache-zdKiYQ3E.mjs → ccache-Btwc36-I.mjs} +10 -9
  27. package/dist/{cdp-Dq_ypAlJ.mjs → cdp-3fwICjzi.mjs} +18 -5
  28. package/dist/{chrome-BnzZMqsR.mjs → chrome-Bbw689-6.mjs} +7 -4
  29. package/dist/{cli-CLP3diBm.mjs → cli-CHZa9Lvk.mjs} +20 -20
  30. package/dist/cli.mjs +1 -1
  31. package/dist/client-OyRoEuSr.mjs +1577 -0
  32. package/dist/collector-run.d.mts +4 -40
  33. package/dist/collector-run.mjs +9 -8
  34. package/dist/{command-output-BTy8hZ0F.mjs → command-output-gwpm28fu.mjs} +1 -1
  35. package/dist/config-DlrsduwG.mjs +955 -0
  36. package/dist/{created-devices-Dzd5C2LF.mjs → created-devices-x9rN6bhs.mjs} +14 -4
  37. package/dist/{dependency-state-DG0p31-y.mjs → dependency-state-1I9KZT1Y.mjs} +20 -5
  38. package/dist/{detached-entry-B9C_zeQC.mjs → detached-entry-BwNEtIrP.mjs} +1 -1
  39. package/dist/{dev-client-DAWVIGBu.mjs → dev-client-BSDR5h3F.mjs} +2 -2
  40. package/dist/{device-BECOiufl.mjs → device-B-L3Fbxy.mjs} +91 -34
  41. package/dist/device-DklWZLqb.mjs +1068 -0
  42. package/dist/{device-capacity-CBUAcRyQ.mjs → device-capacity-DK5_EwRS.mjs} +42 -13
  43. package/dist/device-host-worker.mjs +76 -79
  44. package/dist/{device-ios-COSi6oS1.mjs → device-ios-DoB6KFCO.mjs} +77 -51
  45. package/dist/{device-lease-run-Cw2M4z51.mjs → device-lease-run-xJPQ_QQG.mjs} +14 -26
  46. package/dist/{device-pool-Bf-pOhSw.mjs → device-pool-BMkdsefq.mjs} +5 -5
  47. package/dist/{doctor-kfMBnUw6.mjs → doctor-Bo7BXLux.mjs} +271 -320
  48. package/dist/{doctor-CB8oZKCk.mjs → doctor-D5_EzwfA.mjs} +186 -91
  49. package/dist/doctor-fingerprint-DZLoYFVb.mjs +16 -0
  50. package/dist/doctor-fingerprint.mjs +19 -0
  51. package/dist/doctor-thjWAV0z.mjs +2 -0
  52. package/dist/{doctor-watchman-BRyV2adA.mjs → doctor-watchman-N2wDotwQ.mjs} +40 -21
  53. package/dist/{eas-build-JyfqyHfC.mjs → eas-build-CwknaQYj.mjs} +53 -20
  54. package/dist/{build-plan-B7SZsc2N.mjs → eas-fallback-CSgAulr5.mjs} +41 -214
  55. package/dist/{watchman-CPQFZLXY.mjs → eas-session-ledger-DlY_serQ.mjs} +230 -11
  56. package/dist/{error-diagnostics-BR677IDM.mjs → error-diagnostics-DBWO2rxE.mjs} +172 -82
  57. package/dist/exec-DEoAhcze.mjs +858 -0
  58. package/dist/exec-DnYc4HGH.d.mts +78 -0
  59. package/dist/{gc-qWOMdnER.mjs → gc-CXxN-4SE.mjs} +1091 -1029
  60. package/dist/gc-DnNDQyaP.mjs +3 -0
  61. package/dist/{guide-G9cAbVAc.mjs → guide-Br0LEfKy.mjs} +1165 -297
  62. package/dist/{hosted-android-C3MHGld1.mjs → hosted-android-B-9f-jkd.mjs} +10 -8
  63. package/dist/{hosted-client-ZEKpS9vw.mjs → hosted-client-DZAPrcro.mjs} +34 -19
  64. package/dist/{hosted-ios-l-o3iM9J.mjs → hosted-ios-CsCS21hn.mjs} +1 -1
  65. package/dist/{hosted-logs-EzQM7-3Z.mjs → hosted-logs-FIRx9X2f.mjs} +77 -43
  66. package/dist/{hosted-macos-BACVAVqE.mjs → hosted-macos-_awYSpfv.mjs} +35 -22
  67. package/dist/{hosted-native-A4s1QPXe.mjs → hosted-native-BdzE668-.mjs} +49 -27
  68. package/dist/idle-C4cZ757K.d.mts +24 -0
  69. package/dist/{idle-shutdown-DtxhPTKe.mjs → idle-shutdown-DDg3SdvY.mjs} +11 -10
  70. package/dist/ios-CDvlmUs7.d.mts +24 -0
  71. package/dist/{ios-BwVCF7oA.mjs → ios-Ds1clbIt.mjs} +8 -8
  72. package/dist/{ios-device-D7pi2Sca.mjs → ios-device--h_-8ENa.mjs} +51 -26
  73. package/dist/{ios-device-B5veSDDQ.mjs → ios-device-BXZ0c7Kb.mjs} +1 -1
  74. package/dist/{ios-RkW4pnQf.mjs → ios-hcBpnEGk.mjs} +200 -93
  75. package/dist/{ios-Ce7bc7br.mjs → ios-pm4nL6By.mjs} +160 -108
  76. package/dist/{ios-project-K9hY_Jab.mjs → ios-project-CZhKODF3.mjs} +13 -13
  77. package/dist/ios-signing-BtAffm0A.mjs +489 -0
  78. package/dist/{ios-state-CBLe1xYw.mjs → ios-state-Dtw5K2aI.mjs} +1 -1
  79. package/dist/{ios-nOLJGVdy.mjs → ios-xtlRHaeB.mjs} +1 -1
  80. package/dist/{js-swap-Bb3nsS07.mjs → js-swap-Bwhh4S7V.mjs} +30 -25
  81. package/dist/{launch-C3qJ6Agb.mjs → launch-BjxemNpK.mjs} +85 -543
  82. package/dist/{launch-F4sOqAhB.mjs → launch-CCzXlc8r.mjs} +91 -90
  83. package/dist/logs-BfwwadsZ.mjs +479 -0
  84. package/dist/logs-jTS_lHSo.mjs +248 -0
  85. package/dist/{macos-D7uDvO-z.mjs → macos-DVlSSyUg.mjs} +1 -1
  86. package/dist/macos-NfV2D7dO.mjs +694 -0
  87. package/dist/macos-run.mjs +1 -1
  88. package/dist/maintenance-run.mjs +123 -248
  89. package/dist/{metro-DnCEjCxs.mjs → metro-DiXtA3Ty.mjs} +107 -30
  90. package/dist/metro-gateway-CKZqKqx7.mjs +209 -0
  91. package/dist/{named-ports-4WopRPg-.mjs → named-ports-DkEzOfzO.mjs} +42 -90
  92. package/dist/native-android-CqgnOgsT.mjs +645 -0
  93. package/dist/native-gradle-artifact-inputs-DHFnmrVf.mjs +270 -0
  94. package/dist/native-gradle-inputs-Dnh84Zbd.mjs +211 -0
  95. package/dist/native-inputs-TRTTTRxt.mjs +91 -0
  96. package/dist/native-run-NwUuxbkg.mjs +276 -0
  97. package/dist/{native-runtime-k8dTIrbK.mjs → native-runtime-B7KIafpM.mjs} +11 -9
  98. package/dist/native-source-Y0yLhFyQ.mjs +133 -0
  99. package/dist/{native-xcode-inputs-CykopJdM.mjs → native-xcode-inputs-D_VOfFL7.mjs} +26 -18
  100. package/dist/{native-xcode-ios-Bbf30Gm0.mjs → native-xcode-ios-BYDOHWLN.mjs} +104 -60
  101. package/dist/{ndjson-GOnbyvso.mjs → ndjson-CgE_o3Dl.mjs} +3 -3
  102. package/dist/offload-worker.d.mts +59 -41
  103. package/dist/offload-worker.mjs +478 -115
  104. package/dist/operation-exec-BW0em61G.mjs +171 -0
  105. package/dist/{ownership-DCtw_2IA.mjs → ownership-C5fo5Edb.mjs} +3 -3
  106. package/dist/{page-DRHy329N.mjs → page-DBL2PiUU.mjs} +12 -5
  107. package/dist/paths-BsJMoPP2.mjs +12 -0
  108. package/dist/paths-DQtTgsBf.mjs +42 -0
  109. package/dist/{placement-log-MOfPVgaM.mjs → placement-log-BnGgJRRZ.mjs} +5 -5
  110. package/dist/{plan-placement-V0TdzYXp.mjs → plan-placement-DIsMtN-d.mjs} +15 -11
  111. package/dist/{ports-DdSUicVo.mjs → ports-Cy1RkmuQ.mjs} +3 -3
  112. package/dist/prebuild-D4JoEAEs.mjs +484 -0
  113. package/dist/{project-_phNIJfs.mjs → project-wM_XFTvE.mjs} +65 -37
  114. package/dist/{pull-request-CQ8sPbZa.mjs → pull-request-W1IuLXDt.mjs} +102 -20
  115. package/dist/pull-request-eODyIqZF.d.mts +124 -0
  116. package/dist/pull-requests.d.mts +2 -120
  117. package/dist/pull-requests.mjs +2 -2
  118. package/dist/{react-native-android-CM1XRDwv.mjs → react-native-android-6OZ7MSM7.mjs} +121 -72
  119. package/dist/{gradle-DSrXb6lG.mjs → react-native-build-Dvd8hC21.mjs} +292 -15
  120. package/dist/react-native-doctor-DsoSF1Y9.mjs +234 -0
  121. package/dist/{react-native-ios-Dhq_eiYQ.mjs → react-native-ios-4u1Rn-GR.mjs} +71 -39
  122. package/dist/{recordings-D0d5aQt0.mjs → recordings-CQmfzAh7.mjs} +12 -9
  123. package/dist/registration-BcRgF6Ox.mjs +51 -0
  124. package/dist/{reload-C8JhxjSh.mjs → reload-BzkYabB-.mjs} +94 -47
  125. package/dist/reload-D7AVlb8N.mjs +2 -0
  126. package/dist/{remote-BiR6aYjy.mjs → remote-DjGDmlTZ.mjs} +26 -18
  127. package/dist/result-D2Ykz_iD.mjs +435 -0
  128. package/dist/{run-BtPgpKks.mjs → run-DidHghD1.mjs} +5 -5
  129. package/dist/selection-DSMbWEX2.mjs +620 -0
  130. package/dist/{server-bare-R2m_gsH8.mjs → server-bare-CafzuyFB.mjs} +3 -4
  131. package/dist/{server-command-CTtCkmcQ.mjs → server-command-C6QjeupS.mjs} +14 -7
  132. package/dist/{server-expo-DsguMGUF.mjs → server-expo-CrjB29pq.mjs} +54 -11
  133. package/dist/server-expo-D58c9L0M.mjs +2 -0
  134. package/dist/{settings-B717odTD.mjs → settings-C7O8A6ZI.mjs} +309 -119
  135. package/dist/settings-DpR-1Wnn.mjs +114 -0
  136. package/dist/settings-write-CzD60rOD.mjs +360 -0
  137. package/dist/settings.schema.json +176 -16
  138. package/dist/{simslim-CCr3rE-A.mjs → simslim-BNckWpXi.mjs} +14 -12
  139. package/dist/{slot-launch-DPt0m9Nd.mjs → slot-launch-CbfmNgM6.mjs} +15 -20
  140. package/dist/{spawn-entry-Dcb7sDUz.mjs → spawn-entry-jw19m8Nl.mjs} +0 -1
  141. package/dist/stage-Bps5oius.mjs +200 -0
  142. package/dist/{start-TXeMmmO2.mjs → start-Br-oxqMD.mjs} +1 -1
  143. package/dist/start-EojN4WFz.mjs +1364 -0
  144. package/dist/{state-D1Icyjlu.mjs → state--smjFSTL.mjs} +55 -13
  145. package/dist/{state-CG_OFoaA.mjs → state-08I3hw3_.mjs} +3 -2
  146. package/dist/{stats-BoY5sEPU.mjs → stats-D3GerlFW.mjs} +7 -13
  147. package/dist/stats-report-BzaPrRm6.mjs +16 -0
  148. package/dist/{status-BTA7kKiQ.mjs → status-BRROykC8.mjs} +7 -5
  149. package/dist/status-EvrK72L2.mjs +2 -0
  150. package/dist/{status-WgFbkRzj.mjs → status-WBDajew2.mjs} +554 -452
  151. package/dist/{status-watch-C23ILtf9.mjs → status-watch-Bg_u-pwR.mjs} +222 -25
  152. package/dist/status-worker.d.mts +1 -0
  153. package/dist/status-worker.mjs +19 -0
  154. package/dist/stim-desktop-D5OGpYZM.mjs +112 -0
  155. package/dist/{stop-DR6VoDIk.mjs → stop-DTZ9uTw3.mjs} +2 -2
  156. package/dist/{stop-DPEhunlw.mjs → stop-Dky10AEJ.mjs} +33 -23
  157. package/dist/{stop-BNmBMi1F.mjs → stop-Mv6W9FAk.mjs} +137 -98
  158. package/dist/supervisor-run.d.mts +8 -17
  159. package/dist/supervisor-run.mjs +56 -28
  160. package/dist/{support-CwjbFs9H.mjs → support-CNioewsd.mjs} +14 -14
  161. package/dist/{support-DMTqj3Hm.mjs → support-yUiOjnVY.mjs} +20 -13
  162. package/dist/swiftpm-artifact-inputs-DFcGsXDP.mjs +153 -0
  163. package/dist/swiftpm-macos-BIak-Rxv.mjs +532 -0
  164. package/dist/toolchain-BY8X8HXx.mjs +2 -0
  165. package/dist/toolchain-DpJMHq0n.mjs +876 -0
  166. package/dist/{trigger-CigAYjeL.mjs → trigger-Cbyc41d_.mjs} +21 -6
  167. package/dist/trigger-yb_nFzPF.mjs +2 -0
  168. package/dist/{warm-progress-BuY-Fvfn.mjs → warm-progress-CA3TgT3-.mjs} +263 -156
  169. package/dist/web-CZ07gd77.mjs +2 -0
  170. package/dist/{web-BDBMxDpx.mjs → web-DKx6yoFA.mjs} +20 -14
  171. package/dist/web-run.mjs +13 -12
  172. package/dist/windows-report.d.mts +1 -0
  173. package/dist/windows-report.mjs +18 -0
  174. package/dist/windows-report.ps1 +43 -0
  175. package/dist/{workspace-state-DJt_MMvf.mjs → workspace-state-U08dEzhG.mjs} +62 -58
  176. package/dist/{workspaces-C36s2jb9.mjs → workspaces-CpANmBlq.mjs} +1723 -1286
  177. package/dist/{worktree-CgvJYJ-Q.mjs → worktree-CHecZ4Oq.mjs} +1097 -547
  178. package/dist/worktree-ZqZVjD9K.mjs +2 -0
  179. package/dist/xcode-BXn9-xYx.mjs +896 -0
  180. package/package.json +7 -5
  181. package/shim/android-audio/Main.java +24 -0
  182. package/shim/android-audio/README.md +17 -0
  183. package/shim/android-audio.jar +0 -0
  184. package/shim/android-optimizations.gradle +7 -5
  185. package/dist/activity-BSV745Kz.mjs +0 -629
  186. package/dist/api-qvOzWyfO.mjs +0 -154
  187. package/dist/api-run.mjs +0 -294
  188. package/dist/app-install-BIKtMfPq.mjs +0 -896
  189. package/dist/artifact-lifecycle-DzPpBYvK.mjs +0 -30
  190. package/dist/budget-f7516Zk0.mjs +0 -4106
  191. package/dist/build-facts-DcLv2HSd.d.mts +0 -111
  192. package/dist/cancellation-C_22VQTH.mjs +0 -24
  193. package/dist/client-qQAaXSUs.mjs +0 -1771
  194. package/dist/config-BYDxlMY3.mjs +0 -562
  195. package/dist/device-DuiPJQSn.mjs +0 -290
  196. package/dist/device-lease-BX1sJQ2E.mjs +0 -1224
  197. package/dist/device-remote-xRT0Vmo9.mjs +0 -1272
  198. package/dist/eas-session-ledger-Cm0Gwq2F.mjs +0 -207
  199. package/dist/exec-DqKo9GKJ.mjs +0 -268
  200. package/dist/git-DDfFzkMm.mjs +0 -45
  201. package/dist/launch-verify-B6Q8hZP-.mjs +0 -428
  202. package/dist/logs-BGrEUaEs.mjs +0 -383
  203. package/dist/macos-Ch5VJmcS.mjs +0 -584
  204. package/dist/metro-gateway-CX9Xlkq2.mjs +0 -103
  205. package/dist/miss-reason-mKzZ4VvN.mjs +0 -238
  206. package/dist/native-android-D5qc3-_v.mjs +0 -226
  207. package/dist/native-gradle-inputs-BsLRrl7u.mjs +0 -349
  208. package/dist/ndjson-Du3j0AQ9.d.mts +0 -17
  209. package/dist/plan-C4TjdPsR.mjs +0 -260
  210. package/dist/prebuild-DYbucPXy.mjs +0 -212
  211. package/dist/preview-78a9kR_-.mjs +0 -1011
  212. package/dist/react-native-build-CtCccJtb.mjs +0 -239
  213. package/dist/react-native-doctor-CfRnFfjs.mjs +0 -78
  214. package/dist/remote-cache-OyOyVIS3.mjs +0 -526
  215. package/dist/result-CaJg5Iat.mjs +0 -186
  216. package/dist/server-expo-Bigriskl.mjs +0 -2
  217. package/dist/settings-BORNaaCO.mjs +0 -402
  218. package/dist/settings-vk3mJGSm.d.mts +0 -13
  219. package/dist/spawn-claims-DVS5RkMS.mjs +0 -72
  220. package/dist/stage-KNTQopqv.mjs +0 -157
  221. package/dist/start-BWqzODpb.mjs +0 -1116
  222. package/dist/stim-desktop-BzBN2Em2.mjs +0 -63
  223. package/dist/swiftpm-macos-DRNVJWlb.mjs +0 -119
  224. package/dist/tailnet-Dh-qiYzG.mjs +0 -265
  225. package/dist/toolchain-BFkAcstd.mjs +0 -2
  226. package/dist/toolchain-DxSsNssu.mjs +0 -1657
  227. package/dist/trigger-9_USgmxz.mjs +0 -2
  228. package/dist/web-Dq-i3wzJ.mjs +0 -2
  229. package/dist/worktree-DpLdCHga.mjs +0 -2
  230. package/dist/{api-run.d.mts → doctor-fingerprint.d.mts} +0 -0
  231. package/dist/{errors-DzkMYh8Q.mjs → errors-ccaPc4ox.mjs} +2 -2
@@ -0,0 +1,1397 @@
1
+ import { c as declareSpawnsOn, f as stopDeclaringSpawnsOn, g as process_identity_exports, t as getExecutor, x as ownership_claim_exports } from "./exec-DEoAhcze.mjs";
2
+ import { m as workspaceLogsDir$1, v as withDirLock$1, y as withDirLockAsync$1 } from "./paths-DQtTgsBf.mjs";
3
+ import { U as parseDeviceSlotKey, V as deviceSlotKey, d as getConfigDir, q as validateDeviceSlot } from "./config-DlrsduwG.mjs";
4
+ import { a as formatElapsed, i as formatDuration, m as shortHash } from "./command-output-gwpm28fu.mjs";
5
+ import { n as runOperationFileQuiet } from "./operation-exec-BW0em61G.mjs";
6
+ import { c as withWorkspaceStateLock, d as writeWorkspaceState, l as withWorkspaceStateLockAsync, n as clearWorkspaceStateKeys, r as readWorkspaceState$1 } from "./workspace-state-U08dEzhG.mjs";
7
+ import { h as resolveBuild } from "./build-cache-Dv7RN200.mjs";
8
+ import { randomBytes } from "node:crypto";
9
+ import { closeSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { basename, join, resolve } from "node:path";
12
+ import { ambientContext } from "@stim-cli/core";
13
+ import { agentDeviceStateDirs, isJsonObject, readViewedDevices } from "@stim-cli/core/state";
14
+ import { readFile, readdir } from "node:fs/promises";
15
+ //#region src/devices/automation-tools.ts
16
+ const AUTOMATION_TOOLS = [
17
+ {
18
+ tool: "agent-browser",
19
+ platforms: ["web"],
20
+ cdp: /agent-browser/i
21
+ },
22
+ {
23
+ tool: "agent-device",
24
+ platforms: [
25
+ "ios",
26
+ "android",
27
+ "web"
28
+ ],
29
+ host: /agent-device/i,
30
+ cdp: /agent-device/i
31
+ },
32
+ {
33
+ tool: "argent",
34
+ platforms: [
35
+ "ios",
36
+ "android",
37
+ "web"
38
+ ],
39
+ host: /\/bin\/[\w-]+\/(simulator-server(\.exe)?\s+(ios|android|android_device)|ax-service)\b|com\.argent\.androiddevtools/,
40
+ instrumentation: /com\.argent\.androiddevtools/,
41
+ cdp: /@swmansion\/argent\/dist\/tool-server/
42
+ },
43
+ {
44
+ tool: "idb",
45
+ platforms: ["ios"],
46
+ host: /idb_companion/
47
+ },
48
+ {
49
+ tool: "maestro",
50
+ platforms: ["ios", "android"],
51
+ host: /maestro/i
52
+ },
53
+ {
54
+ tool: "appium",
55
+ platforms: ["ios", "android"],
56
+ host: /appium|WebDriverAgent/i
57
+ },
58
+ {
59
+ tool: "xcodebuild",
60
+ platforms: ["ios"],
61
+ host: /\bxcodebuild\b.*\btest(-without-building)?\b/
62
+ },
63
+ {
64
+ tool: "simctl",
65
+ platforms: ["ios"],
66
+ host: /\bsimctl\s+(io|spawn)\b/
67
+ },
68
+ {
69
+ tool: "uiautomator",
70
+ platforms: ["android"],
71
+ instrumentation: /uiautomator/
72
+ },
73
+ {
74
+ tool: "instrumentation",
75
+ platforms: ["android"],
76
+ instrumentation: /androidx\.test|\binstrument\b/
77
+ },
78
+ {
79
+ tool: "chrome-devtools-mcp",
80
+ platforms: ["web"],
81
+ cdp: /chrome-devtools-mcp/i
82
+ },
83
+ {
84
+ tool: "playwright",
85
+ platforms: ["web"],
86
+ cdp: /playwright/i
87
+ },
88
+ {
89
+ tool: "puppeteer",
90
+ platforms: ["web"],
91
+ cdp: /puppeteer/i
92
+ }
93
+ ];
94
+ const HOST_EXCLUSIONS = [/\bsimctl\s+spawn\b.*\blog\s+stream\b/, /\blogcat\b/];
95
+ /** Stim's own DevTools clients: the browser supervisor, `stim-frames` in stim-server, Stim Desktop, the stim CLI. */
96
+ const CDP_EXCLUSIONS = [/\bstim-(frames|server|web)\b|StimDesktop|\/stim(-cli)?\/(dist|bin)\/|\/bin\/stim(\s|$)|^stim(\s|$)/];
97
+ function namesDevice(command, id) {
98
+ const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
99
+ return new RegExp(`(^|[^A-Za-z0-9])${escaped}([^A-Za-z0-9]|$)`).test(command);
100
+ }
101
+ /** The tool a host process drives the device `id` with, or null when the command does not name the device. */
102
+ function hostDriverTool(command, platform, id) {
103
+ if (!namesDevice(command, id) || HOST_EXCLUSIONS.some((pattern) => pattern.test(command))) return null;
104
+ return AUTOMATION_TOOLS.find((entry) => entry.platforms.includes(platform) && entry.host?.test(command))?.tool ?? null;
105
+ }
106
+ /** The tool behind an on-device process's arguments, or null when it is not instrumentation. */
107
+ function instrumentationTool(args) {
108
+ return AUTOMATION_TOOLS.find((entry) => entry.platforms.includes("android") && entry.instrumentation?.test(args))?.tool ?? null;
109
+ }
110
+ /**
111
+ * The tool a DevTools client's command line names, or null for Stim's own clients. A client no entry names is
112
+ * reported by its script or executable name.
113
+ */
114
+ function cdpClientTool(command) {
115
+ if (CDP_EXCLUSIONS.some((pattern) => pattern.test(command))) return null;
116
+ const known = AUTOMATION_TOOLS.find((entry) => entry.platforms.includes("web") && entry.cdp?.test(command));
117
+ if (known) return known.tool;
118
+ const [executable = "", script] = command.trim().split(/\s+/);
119
+ const name = basename(executable);
120
+ return (/^(node|bun|deno|python3?)$/.test(name) && script ? basename(script) : name) || "unknown DevTools client";
121
+ }
122
+ //#endregion
123
+ //#region src/engine/build-lock.ts
124
+ const LOCK_SUFFIX$1 = ".lock";
125
+ const WAIT_POLL_MS = 1e3;
126
+ const WAIT_PROGRESS_MS = 3e4;
127
+ const WAIT_CEILING_MS = 54e5;
128
+ function sleepAsync(ms) {
129
+ return new Promise((resolve) => setTimeout(resolve, ms));
130
+ }
131
+ function segment(value) {
132
+ return String(value).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^\.+/, "") || "unknown";
133
+ }
134
+ function buildLocksDir() {
135
+ return join(getConfigDir(), "build-locks");
136
+ }
137
+ function buildLockPath(platform, key) {
138
+ return join(buildLocksDir(), `${segment(platform)}-${segment(key)}${LOCK_SUFFIX$1}`);
139
+ }
140
+ function label(platform) {
141
+ return `${platform} build`;
142
+ }
143
+ function toRecord(holder) {
144
+ const details = holder.details;
145
+ return {
146
+ pid: holder.owner.pid,
147
+ projectRoot: typeof details.projectRoot === "string" ? details.projectRoot : null,
148
+ startedAt: holder.startedAt || null,
149
+ logFile: typeof details.logFile === "string" ? details.logFile : null
150
+ };
151
+ }
152
+ function readBuildLock(pathOrSpec) {
153
+ const path = typeof pathOrSpec === "string" ? pathOrSpec : buildLockPath(pathOrSpec.platform, pathOrSpec.key);
154
+ const survey = (0, ownership_claim_exports.readClaimSet)(path);
155
+ const holder = survey.live[0] ?? survey.dead[0];
156
+ return holder ? toRecord(holder) : null;
157
+ }
158
+ function acquireBuildLock({ platform, key, root = null, logFile = null }) {
159
+ const path = buildLockPath(platform, key);
160
+ const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
161
+ root: path,
162
+ mode: "exclusive",
163
+ label: label(platform),
164
+ details: {
165
+ projectRoot: root,
166
+ logFile
167
+ }
168
+ });
169
+ const reaped = attempt.reaped[0];
170
+ if (attempt.acquired) {
171
+ const handle = {
172
+ acquired: true,
173
+ path,
174
+ lock: {
175
+ pid: attempt.acquired.owner.pid,
176
+ projectRoot: root,
177
+ startedAt: attempt.acquired.startedAt,
178
+ logFile
179
+ },
180
+ claim: attempt.acquired
181
+ };
182
+ declareSpawnsOn(ambientContext(), attempt.acquired);
183
+ return reaped ? {
184
+ ...handle,
185
+ tookOver: toRecord(reaped)
186
+ } : handle;
187
+ }
188
+ if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
189
+ const holder = attempt.held ?? attempt.waitingFor?.[0];
190
+ return holder ? {
191
+ held: toRecord(holder),
192
+ path
193
+ } : { path };
194
+ }
195
+ function releaseBuildLock(handle) {
196
+ stopDeclaringSpawnsOn(ambientContext(), handle?.claim);
197
+ return (0, ownership_claim_exports.releaseClaim)(handle?.claim);
198
+ }
199
+ function listBuildLocks() {
200
+ const dir = buildLocksDir();
201
+ let names;
202
+ try {
203
+ names = readdirSync(dir);
204
+ } catch {
205
+ return [];
206
+ }
207
+ const locks = [];
208
+ for (const name of names) {
209
+ if (!name.endsWith(LOCK_SUFFIX$1)) continue;
210
+ const path = join(dir, name);
211
+ try {
212
+ if (!statSync(path).isDirectory()) continue;
213
+ } catch {
214
+ continue;
215
+ }
216
+ const stem = name.slice(0, -5);
217
+ const cut = stem.indexOf("-");
218
+ const survey = (0, ownership_claim_exports.readClaimSet)(path);
219
+ const holder = survey.live[0] ?? survey.dead[0];
220
+ const info = holder ? toRecord(holder) : null;
221
+ locks.push({
222
+ path,
223
+ name,
224
+ platform: cut > 0 ? stem.slice(0, cut) : stem,
225
+ key: cut > 0 ? stem.slice(cut + 1) : null,
226
+ pid: info?.pid ?? null,
227
+ projectRoot: info?.projectRoot ?? null,
228
+ startedAt: info?.startedAt ?? null,
229
+ logFile: info?.logFile ?? null,
230
+ alive: survey.live.length > 0,
231
+ unresolved: survey.unresolved.length > 0
232
+ });
233
+ }
234
+ return locks;
235
+ }
236
+ function waitingLine({ projectRoot, pid, elapsedMs, logFile }) {
237
+ const where = projectRoot || "another workspace";
238
+ const tail = logFile ? ` -- tail ${logFile}` : "";
239
+ return `${"build".padEnd(11)} waiting on ${where} (pid ${pid ?? "?"}, ${formatElapsed(elapsedMs)} elapsed)${tail} -- stim guide lifecycle concurrency`;
240
+ }
241
+ function takeoverLine({ projectRoot, pid, logFile, startedAt = null, now = Date.now }) {
242
+ const where = projectRoot || "another workspace";
243
+ const started = startedAt ? Date.parse(startedAt) : NaN;
244
+ const ago = Number.isFinite(started) ? ` (it started ${formatElapsed(now() - started)} ago)` : "";
245
+ const tail = logFile ? ` -- read ${logFile} before this one finishes` : "";
246
+ return `RETRY: ${where}'s build of this fingerprint (pid ${pid ?? "?"}) FAILED without an artifact${ago}, and this run rebuilds the SAME inputs, so expect the same failure unless something changed${tail}`;
247
+ }
248
+ async function waitForBuild({ platform, key, resolve = resolveBuild, intervalMs = WAIT_POLL_MS, progressMs = WAIT_PROGRESS_MS, ceilingMs = WAIT_CEILING_MS, out = () => {}, now = Date.now, sleep = sleepAsync }) {
249
+ const path = buildLockPath(platform, key);
250
+ const started = now();
251
+ let lastProgress = started;
252
+ let holder = readBuildLock(path);
253
+ for (;;) {
254
+ const hit = resolve(platform, key);
255
+ if (hit) return {
256
+ hit,
257
+ waitedMs: now() - started
258
+ };
259
+ const survey = (0, ownership_claim_exports.readClaimSet)(path);
260
+ const unresolved = survey.unresolved[0];
261
+ if (unresolved) throw new ownership_claim_exports.ClaimRefusedError({
262
+ claimPath: unresolved.path,
263
+ root: path,
264
+ label: label(platform),
265
+ reason: unresolved.reason
266
+ });
267
+ const live = survey.live[0];
268
+ if (live) holder = toRecord(live);
269
+ else {
270
+ const dead = survey.dead[0];
271
+ if (dead) return {
272
+ builderFailed: `the builder (pid ${dead.owner.pid}) is gone`,
273
+ holder: toRecord(dead),
274
+ waitedMs: now() - started
275
+ };
276
+ return {
277
+ lockReleased: true,
278
+ holder,
279
+ waitedMs: now() - started
280
+ };
281
+ }
282
+ const elapsed = now() - started;
283
+ if (elapsed >= ceilingMs) {
284
+ const err = /* @__PURE__ */ new Error(`Waited ${formatElapsed(elapsed)} for ${holder?.projectRoot || "another workspace"}'s ${platform} build of ${key} without an artifact, and pid ${holder?.pid} is still the live holder. The lock is ${path}; remove it if that process is not really building:\n ${(0, ownership_claim_exports.claimRemoveCommand)(path)}`);
285
+ err.code = "STIM_BUILD_WAIT_TIMEOUT";
286
+ err.lockPath = path;
287
+ err.holder = holder ?? void 0;
288
+ throw err;
289
+ }
290
+ if (now() - lastProgress >= progressMs) {
291
+ lastProgress = now();
292
+ out(waitingLine({
293
+ projectRoot: holder.projectRoot,
294
+ pid: holder.pid,
295
+ elapsedMs: elapsed,
296
+ logFile: holder.logFile
297
+ }));
298
+ }
299
+ await sleep(intervalMs);
300
+ }
301
+ }
302
+ async function waitForSharedBuild({ platform, key, fingerprint, root, logFile, command, acquire, wait, now, phase, waitingOn, warn, out }) {
303
+ const waitStarted = now();
304
+ let failedHolder;
305
+ let released = null;
306
+ for (;;) {
307
+ let attempt = null;
308
+ try {
309
+ attempt = acquire({
310
+ platform,
311
+ key,
312
+ root,
313
+ logFile
314
+ });
315
+ } catch (err) {
316
+ const refusal = (0, ownership_claim_exports.claimFailure)(err, command);
317
+ if (refusal) return { refusal };
318
+ warn(`could not take the build lock: ${err?.message || err}; building anyway`);
319
+ }
320
+ if (attempt?.acquired) {
321
+ const previous = attempt.tookOver ?? failedHolder;
322
+ if (previous) warn(takeoverLine(previous));
323
+ waitingOn(null);
324
+ return {
325
+ refusal: null,
326
+ lock: attempt,
327
+ hit: null,
328
+ released
329
+ };
330
+ }
331
+ if (!attempt?.held) {
332
+ waitingOn(null);
333
+ return {
334
+ refusal: null,
335
+ lock: null,
336
+ hit: null,
337
+ released
338
+ };
339
+ }
340
+ released = null;
341
+ const holder = attempt.held;
342
+ const who = holder.projectRoot || "another workspace";
343
+ phase(`${who} is already building ${shortHash(fingerprint)} (pid ${holder.pid})${holder.logFile ? ` -- tail ${holder.logFile}` : ""} -- stim guide lifecycle concurrency`);
344
+ waitingOn(holder.projectRoot || null);
345
+ let waited;
346
+ try {
347
+ const ceilingMs = WAIT_CEILING_MS - (now() - waitStarted);
348
+ if (ceilingMs <= 0) throw Object.assign(/* @__PURE__ */ new Error(`Waited ${formatDuration(now() - waitStarted)} for shared builds without an artifact; ${who} (pid ${holder.pid}) holds ${attempt.path}.`), {
349
+ code: "STIM_BUILD_WAIT_TIMEOUT",
350
+ lockPath: attempt.path
351
+ });
352
+ waited = await wait({
353
+ platform,
354
+ key,
355
+ out,
356
+ ceilingMs
357
+ });
358
+ } catch (err) {
359
+ const refusal = (0, ownership_claim_exports.claimFailure)(err, command);
360
+ if (refusal) return { refusal };
361
+ const timeout = err;
362
+ if (timeout?.code !== "STIM_BUILD_WAIT_TIMEOUT") throw err;
363
+ return { refusal: {
364
+ code: "STIM_BUILD_WAIT_TIMEOUT",
365
+ message: timeout.message,
366
+ remedy: `Check pid ${holder.pid}; if it is not really building, remove ${timeout.lockPath} and run \`${command}\` again.`
367
+ } };
368
+ }
369
+ if (waited.hit) {
370
+ waitingOn(null);
371
+ phase(`waited ${formatDuration(waited.waitedMs)} for ${who}'s build -> installed from cache -- stim guide lifecycle concurrency`);
372
+ return {
373
+ refusal: null,
374
+ lock: null,
375
+ hit: {
376
+ path: waited.hit,
377
+ waited: {
378
+ pid: holder.pid,
379
+ ms: waited.waitedMs
380
+ }
381
+ },
382
+ released
383
+ };
384
+ }
385
+ if (waited.lockReleased) {
386
+ failedHolder = void 0;
387
+ released = {
388
+ facts: {
389
+ pid: holder.pid,
390
+ ms: waited.waitedMs
391
+ },
392
+ who
393
+ };
394
+ phase(`${who}'s build lock was released; rechecking this workspace before building`);
395
+ } else {
396
+ warn(`${who}'s build ended without an artifact (${waited.builderFailed}); retrying the build lock`);
397
+ failedHolder = holder;
398
+ }
399
+ }
400
+ }
401
+ const DEFAULT_LEASE_MS = 3e5;
402
+ const LEASE_SUFFIX = ".json";
403
+ const LOCK_SUFFIX = ".lock";
404
+ const DURATION = /^[1-9][0-9]*(s|m)$/;
405
+ function deviceLocksDir() {
406
+ return join(getConfigDir(), "device-locks");
407
+ }
408
+ function leaseStem(platform, id) {
409
+ return `${segment(platform)}-${segment(id)}`;
410
+ }
411
+ function deviceLeasePath(platform, id) {
412
+ return join(deviceLocksDir(), `${leaseStem(platform, id)}${LEASE_SUFFIX}`);
413
+ }
414
+ function deviceLeaseLockPath(platform, id) {
415
+ return join(deviceLocksDir(), `${leaseStem(platform, id)}${LOCK_SUFFIX}`);
416
+ }
417
+ const fileLeaseIo = {
418
+ withHolderLock: withWorkspaceStateLock,
419
+ now: () => Date.now(),
420
+ readLease(path) {
421
+ try {
422
+ return readFileSync(path, "utf-8");
423
+ } catch {
424
+ return null;
425
+ }
426
+ },
427
+ writeLease(path, body) {
428
+ mkdirSync(deviceLocksDir(), { recursive: true });
429
+ const tmp = `${path}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
430
+ writeFileSync(tmp, body);
431
+ try {
432
+ renameSync(tmp, path);
433
+ } catch (error) {
434
+ rmSync(tmp, { force: true });
435
+ throw error;
436
+ }
437
+ },
438
+ removeLease(path) {
439
+ rmSync(path, { force: true });
440
+ },
441
+ listLeaseNames() {
442
+ try {
443
+ return readdirSync(deviceLocksDir());
444
+ } catch {
445
+ return [];
446
+ }
447
+ },
448
+ withLeaseLock(lockPath, fn) {
449
+ return withDirLock$1(lockPath, fn, { ensureParent: () => mkdirSync(deviceLocksDir(), { recursive: true }) });
450
+ },
451
+ readHolder(root) {
452
+ return parseWorkspaceLeases(readWorkspaceState$1(root)?.deviceLeases);
453
+ },
454
+ writeHolder(root, leases) {
455
+ if (Object.keys(leases).length === 0) clearWorkspaceStateKeys(root, ["deviceLeases"]);
456
+ else writeWorkspaceState(root, { deviceLeases: leases });
457
+ }
458
+ };
459
+ function text(value) {
460
+ return typeof value === "string" && value.trim() !== "" ? value : null;
461
+ }
462
+ function isPlatform(value) {
463
+ return value === "ios" || value === "android";
464
+ }
465
+ function parseLease(raw) {
466
+ if (typeof raw !== "string") return null;
467
+ let parsed;
468
+ try {
469
+ parsed = JSON.parse(raw);
470
+ } catch {
471
+ return null;
472
+ }
473
+ if (!isJsonObject(parsed)) return null;
474
+ const entry = parsed;
475
+ if (entry.version !== 1 || !isPlatform(entry.platform)) return null;
476
+ const id = text(entry.id);
477
+ const holder = text(entry.holder);
478
+ const token = text(entry.token);
479
+ const expiresAt = text(entry.expiresAt);
480
+ if (!id || !holder || !token || !expiresAt || !Number.isFinite(Date.parse(expiresAt))) return null;
481
+ if (entry.slot !== void 0) {
482
+ if (typeof entry.slot !== "string") return null;
483
+ try {
484
+ validateDeviceSlot(entry.slot);
485
+ } catch {
486
+ return null;
487
+ }
488
+ }
489
+ return {
490
+ version: 1,
491
+ ...typeof entry.slot === "string" ? { slot: entry.slot } : {},
492
+ platform: entry.platform,
493
+ id,
494
+ deviceName: text(entry.deviceName),
495
+ holder,
496
+ token,
497
+ grantedAt: text(entry.grantedAt),
498
+ expiresAt
499
+ };
500
+ }
501
+ function parseWorkspaceLeases(value) {
502
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
503
+ const leases = {};
504
+ for (const [platform, entry] of Object.entries(value)) {
505
+ if (!parseDeviceSlotKey(platform) || !entry || typeof entry !== "object") continue;
506
+ const record = entry;
507
+ const id = text(record.id);
508
+ const token = text(record.token);
509
+ if (!id || !token) continue;
510
+ leases[platform] = {
511
+ id,
512
+ token,
513
+ kind: record.kind === "run" ? "run" : "declared"
514
+ };
515
+ }
516
+ return leases;
517
+ }
518
+ function leaseIsExpired(lease, now) {
519
+ return Date.parse(lease.expiresAt) <= now;
520
+ }
521
+ function parseLeaseDuration(value) {
522
+ const raw = String(value ?? "").trim();
523
+ const match = DURATION.exec(raw);
524
+ if (!match) return { error: `--for must be a whole number of seconds or minutes, such as 90s or 5m (got "${raw}").` };
525
+ const ms = Number(raw.slice(0, -1)) * (match[1] === "s" ? 1e3 : 6e4);
526
+ if (ms < 1e4 || ms > 18e5) return { error: `--for must be between 10s and 30m (got "${raw}").` };
527
+ return { ms };
528
+ }
529
+ function listLeaseFiles(io = fileLeaseIo) {
530
+ const dir = deviceLocksDir();
531
+ const entries = [];
532
+ for (const name of io.listLeaseNames().toSorted()) {
533
+ if (!name.endsWith(LEASE_SUFFIX)) continue;
534
+ const path = join(dir, name);
535
+ const lease = parseLease(io.readLease(path));
536
+ const stem = name.slice(0, -5);
537
+ const cut = stem.indexOf("-");
538
+ entries.push({
539
+ path,
540
+ name,
541
+ platform: lease?.platform ?? (cut > 0 ? stem.slice(0, cut) : stem),
542
+ id: lease?.id ?? (cut > 0 ? stem.slice(cut + 1) : null),
543
+ lease
544
+ });
545
+ }
546
+ return entries;
547
+ }
548
+ function writeLease(lease, io) {
549
+ io.writeLease(deviceLeasePath(lease.platform, lease.id), `${JSON.stringify(lease, null, 2)}\n`);
550
+ return lease;
551
+ }
552
+ function takeLease({ root, platform, slot = "default", id, deviceName = null, kind, durationMs = DEFAULT_LEASE_MS }, io = fileLeaseIo) {
553
+ return withHolderLock(io, root, () => {
554
+ const key = deviceSlotKey(platform, slot);
555
+ const previous = io.readHolder(root)[key];
556
+ if (previous && previous.id !== id) releaseHeld(root, platform, previous, io, slot);
557
+ for (const entry of listLeaseFiles(io)) {
558
+ const other = entry.lease;
559
+ if (!other || other.platform !== platform || other.holder !== root || (other.slot ?? "default") !== slot || other.id === id) continue;
560
+ releaseByRoot(root, platform, other.id, io);
561
+ }
562
+ const declared = previous?.kind === "declared" && previous.id === id;
563
+ const path = deviceLeasePath(platform, id);
564
+ const result = io.withLeaseLock(deviceLeaseLockPath(platform, id), () => {
565
+ const now = io.now();
566
+ const raw = io.readLease(path);
567
+ const current = parseLease(raw);
568
+ if (raw !== null && !current) return {
569
+ status: "unreadable",
570
+ path
571
+ };
572
+ const asked = now + durationMs;
573
+ if (current && !leaseIsExpired(current, now)) {
574
+ if (current.holder !== root || (current.slot ?? "default") !== slot) return {
575
+ status: "held",
576
+ lease: current
577
+ };
578
+ const floor = declared && kind === "run" ? Math.max(asked, Date.parse(current.expiresAt)) : asked;
579
+ return {
580
+ status: "set",
581
+ lease: writeLease({
582
+ ...current,
583
+ deviceName: deviceName ?? current.deviceName,
584
+ expiresAt: new Date(floor).toISOString()
585
+ }, io)
586
+ };
587
+ }
588
+ const expiresAt = new Date(asked).toISOString();
589
+ return {
590
+ status: "taken",
591
+ lease: writeLease({
592
+ version: 1,
593
+ ...slot === "default" ? {} : { slot },
594
+ platform,
595
+ id,
596
+ deviceName,
597
+ holder: root,
598
+ token: randomBytes(12).toString("hex"),
599
+ grantedAt: new Date(now).toISOString(),
600
+ expiresAt
601
+ }, io)
602
+ };
603
+ });
604
+ if (result.status === "taken" || result.status === "set") {
605
+ const recorded = result.status === "set" && declared ? "declared" : kind;
606
+ io.writeHolder(root, {
607
+ ...io.readHolder(root),
608
+ [key]: {
609
+ id,
610
+ token: result.lease.token,
611
+ kind: recorded
612
+ }
613
+ });
614
+ }
615
+ return result;
616
+ });
617
+ }
618
+ function raiseLease({ root, platform, slot = "default", minMs }, io = fileLeaseIo) {
619
+ const record = io.readHolder(root)[deviceSlotKey(platform, slot)];
620
+ if (!record) return { status: "none" };
621
+ const path = deviceLeasePath(platform, record.id);
622
+ return io.withLeaseLock(deviceLeaseLockPath(platform, record.id), () => {
623
+ const current = parseLease(io.readLease(path));
624
+ if (!current || current.token !== record.token) return {
625
+ status: "lost",
626
+ lease: current
627
+ };
628
+ const floor = io.now() + minMs;
629
+ if (Date.parse(current.expiresAt) >= floor) return {
630
+ status: "raised",
631
+ lease: current
632
+ };
633
+ return {
634
+ status: "raised",
635
+ lease: writeLease({
636
+ ...current,
637
+ expiresAt: new Date(floor).toISOString()
638
+ }, io)
639
+ };
640
+ });
641
+ }
642
+ function summarize(lease) {
643
+ return {
644
+ platform: lease.platform,
645
+ id: lease.id,
646
+ deviceName: lease.deviceName,
647
+ expiresAt: lease.expiresAt
648
+ };
649
+ }
650
+ function releaseHeld(root, platform, record, io, slot = "default") {
651
+ const key = deviceSlotKey(platform, slot);
652
+ const path = deviceLeasePath(platform, record.id);
653
+ const released = io.withLeaseLock(deviceLeaseLockPath(platform, record.id), () => {
654
+ const current = parseLease(io.readLease(path));
655
+ if (!current || current.token !== record.token) return null;
656
+ io.removeLease(path);
657
+ return current;
658
+ });
659
+ const held = io.readHolder(root);
660
+ if (held[key]?.token === record.token) {
661
+ delete held[key];
662
+ io.writeHolder(root, held);
663
+ }
664
+ return released;
665
+ }
666
+ function releaseByRoot(root, platform, id, io) {
667
+ const path = deviceLeasePath(platform, id);
668
+ return io.withLeaseLock(deviceLeaseLockPath(platform, id), () => {
669
+ const current = parseLease(io.readLease(path));
670
+ if (!current || current.holder !== root) return null;
671
+ io.removeLease(path);
672
+ return current;
673
+ });
674
+ }
675
+ function releaseRunLease({ root, platform, slot = "default" }, io = fileLeaseIo) {
676
+ return withHolderLock(io, root, () => {
677
+ const record = io.readHolder(root)[deviceSlotKey(platform, slot)];
678
+ if (!record || record.kind !== "run") return null;
679
+ const released = releaseHeld(root, platform, record, io, slot);
680
+ return released ? summarize(released) : null;
681
+ });
682
+ }
683
+ function releaseWorkspaceLeases(root, { platform = null, slot = null } = {}, io = fileLeaseIo) {
684
+ return withHolderLock(io, root, () => {
685
+ const released = [];
686
+ for (const [name, record] of Object.entries(io.readHolder(root))) {
687
+ const parsed = parseDeviceSlotKey(name);
688
+ if (!parsed || platform && parsed.platform !== platform || slot !== null && parsed.slot !== slot) continue;
689
+ const lease = releaseHeld(root, parsed.platform, record, io, parsed.slot);
690
+ if (lease) released.push(summarize(lease));
691
+ }
692
+ for (const entry of listLeaseFiles(io)) {
693
+ const lease = entry.lease;
694
+ if (!lease || lease.holder !== root) continue;
695
+ if (platform && lease.platform !== platform) continue;
696
+ if (slot !== null && (lease.slot ?? "default") !== slot) continue;
697
+ const byRoot = releaseByRoot(root, lease.platform, lease.id, io);
698
+ if (byRoot) released.push(summarize(byRoot));
699
+ }
700
+ return released;
701
+ });
702
+ }
703
+ /** Releases workspace leases with asynchronous lock waits and the same ownership checks. */
704
+ async function releaseWorkspaceLeasesAsync(root, options = {}) {
705
+ const released = [];
706
+ const lockOptions = { ensureParent: () => mkdirSync(deviceLocksDir(), { recursive: true }) };
707
+ for (;;) {
708
+ const attempt = {};
709
+ const io = {
710
+ ...fileLeaseIo,
711
+ removeLease(path) {
712
+ const lease = parseLease(fileLeaseIo.readLease(path));
713
+ fileLeaseIo.removeLease(path);
714
+ if (lease) released.push(summarize(lease));
715
+ },
716
+ withLeaseLock(path, fn) {
717
+ try {
718
+ return withDirLock$1(path, fn, {
719
+ ...lockOptions,
720
+ waitMs: 0
721
+ });
722
+ } catch (error) {
723
+ if (error instanceof Error && "code" in error && error.code === "STIM_LOCK_TIMEOUT") attempt.blocked = {
724
+ path,
725
+ error
726
+ };
727
+ throw error;
728
+ }
729
+ }
730
+ };
731
+ await withWorkspaceStateLockAsync(root, () => {
732
+ try {
733
+ releaseWorkspaceLeases(root, options, io);
734
+ } catch (error) {
735
+ if (error !== attempt.blocked?.error) throw error;
736
+ }
737
+ });
738
+ const blocked = attempt.blocked;
739
+ if (!blocked) return released;
740
+ await withDirLockAsync$1(blocked.path, () => void 0, lockOptions);
741
+ }
742
+ }
743
+ function removeExpiredLease(entry, io = fileLeaseIo) {
744
+ const lease = entry.lease;
745
+ if (!lease) return false;
746
+ return io.withLeaseLock(deviceLeaseLockPath(lease.platform, lease.id), () => {
747
+ const current = parseLease(io.readLease(entry.path));
748
+ if (!current || !leaseIsExpired(current, io.now())) return false;
749
+ io.removeLease(entry.path);
750
+ return true;
751
+ });
752
+ }
753
+ function selectPoolDevice({ candidates, leases, held = null, now }) {
754
+ const ordered = candidates.toSorted((a, b) => {
755
+ const left = a.id.toLowerCase();
756
+ const right = b.id.toLowerCase();
757
+ return left < right ? -1 : left > right ? 1 : 0;
758
+ });
759
+ if (held) {
760
+ const mine = ordered.find((candidate) => candidate.id === held);
761
+ return mine ? {
762
+ status: "selected",
763
+ candidate: mine
764
+ } : {
765
+ status: "held-disconnected",
766
+ id: held
767
+ };
768
+ }
769
+ const preferred = ordered.filter((candidate) => !candidate.fallback);
770
+ const pool = preferred.length > 0 ? preferred : ordered;
771
+ if (pool.length === 0) return { status: "none" };
772
+ const byId = new Map(leases.map((lease) => [lease.id, lease]));
773
+ const free = (candidate) => {
774
+ const lease = byId.get(candidate.id);
775
+ return !lease || leaseIsExpired(lease, now);
776
+ };
777
+ const holders = [];
778
+ for (const candidate of pool) {
779
+ if (free(candidate)) return {
780
+ status: "selected",
781
+ candidate
782
+ };
783
+ const lease = byId.get(candidate.id);
784
+ holders.push({
785
+ id: lease.id,
786
+ holder: lease.holder,
787
+ expiresAt: lease.expiresAt
788
+ });
789
+ }
790
+ return {
791
+ status: "busy",
792
+ holders,
793
+ spare: ordered.filter((candidate) => candidate.fallback && free(candidate)).map((candidate) => candidate.id)
794
+ };
795
+ }
796
+ function withHolderLock(io, root, fn) {
797
+ return io.withHolderLock ? io.withHolderLock(root, fn) : fn();
798
+ }
799
+ //#endregion
800
+ //#region src/devices/activity.ts
801
+ const ACTIVE_WINDOW_MS = 6e5;
802
+ const LOG_TAIL_BYTES = 262144;
803
+ function classifyActivity(evidence, now) {
804
+ const last = evidence.recency.reduce((best, entry) => Number.isFinite(entry.at) && (!best || entry.at > best.at) ? entry : best, null);
805
+ const recent = {};
806
+ for (const { basis, at } of evidence.recency) if (Number.isFinite(at) && !(Date.parse(recent[basis] ?? "") >= at)) recent[basis] = new Date(at).toISOString();
807
+ const lastActivityAt = last ? {
808
+ lastActivityAt: new Date(last.at).toISOString(),
809
+ recent
810
+ } : {};
811
+ const [first] = evidence.drivers;
812
+ if (first) {
813
+ const { basis: _basis, ...driver } = first;
814
+ return {
815
+ state: "driven",
816
+ driver,
817
+ ...lastActivityAt,
818
+ basis: [...new Set(evidence.drivers.map((entry) => entry.basis))]
819
+ };
820
+ }
821
+ if (evidence.unknown.length) return {
822
+ state: "unknown",
823
+ ...lastActivityAt,
824
+ basis: [...new Set(evidence.unknown)]
825
+ };
826
+ return {
827
+ state: Boolean(last && now - last.at < ACTIVE_WINDOW_MS) ? "active" : "idle",
828
+ ...lastActivityAt,
829
+ basis: [...new Set(evidence.recency.map((entry) => entry.basis))]
830
+ };
831
+ }
832
+ function idleForMs(activity, now) {
833
+ if (activity?.state !== "idle" || !activity.lastActivityAt) return null;
834
+ const at = Date.parse(activity.lastActivityAt);
835
+ return Number.isFinite(at) ? Math.max(0, now - at) : null;
836
+ }
837
+ function field(entry, pidKey, startKey) {
838
+ const pid = entry[pidKey];
839
+ const startTime = entry[startKey];
840
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || typeof startTime !== "string") return null;
841
+ return {
842
+ pid,
843
+ startTime
844
+ };
845
+ }
846
+ function parseObject(raw) {
847
+ if (raw === null) return null;
848
+ try {
849
+ const parsed = JSON.parse(raw);
850
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
851
+ } catch {
852
+ return null;
853
+ }
854
+ }
855
+ function parseAgentDeviceRecord(kind, path, raw) {
856
+ const entry = parseObject(raw);
857
+ const stem = basename(path, ".json");
858
+ const device = entry?.device && typeof entry.device === "object" ? entry.device : null;
859
+ const recordedId = kind === "claim" ? device?.id : entry?.deviceId;
860
+ const deviceId = typeof recordedId === "string" && recordedId ? recordedId : kind === "runner-lease" ? stem : null;
861
+ const createdAtMs = typeof entry?.createdAtMs === "number" ? entry.createdAtMs : null;
862
+ return {
863
+ path,
864
+ kind,
865
+ deviceId,
866
+ session: kind === "claim" && typeof entry?.session === "string" && entry.session ? entry.session : null,
867
+ workspace: kind === "claim" && typeof entry?.workspace === "string" && entry.workspace ? entry.workspace : null,
868
+ stateDir: kind === "claim" && typeof entry?.stateDir === "string" && entry.stateDir ? entry.stateDir : null,
869
+ deviceName: kind === "claim" && typeof device?.name === "string" ? device.name : null,
870
+ readable: Boolean(entry && deviceId),
871
+ owner: entry ? field(entry, "ownerPid", "ownerStartTime") : null,
872
+ runner: entry && kind === "runner-lease" ? field(entry, "runnerPid", "runnerStartTime") : null,
873
+ claimProtocol: kind === "runner-lease" && entry?.deviceClaimProtocol === 1,
874
+ createdAtMs,
875
+ xctestrunPath: typeof entry?.xctestrunPath === "string" ? entry.xctestrunPath : null
876
+ };
877
+ }
878
+ function processMatches(entry, startOf) {
879
+ const start = startOf(entry.pid);
880
+ if (start.status === "gone") return "dead";
881
+ if (start.status === "unknown") return "unknown";
882
+ const recorded = Date.parse(entry.startTime);
883
+ if (!Number.isFinite(recorded)) return "unknown";
884
+ return Math.floor(start.startedAtMs / 1e3) === Math.floor(recorded / 1e3) ? "live" : "dead";
885
+ }
886
+ function agentDeviceDaemon(stateDir, startOf = process_identity_exports.inspectProcessStart) {
887
+ let raw;
888
+ try {
889
+ raw = readFileSync(join(stateDir, "daemon.json"), "utf8");
890
+ } catch (error) {
891
+ return error.code === "ENOENT" ? "none" : "unknown";
892
+ }
893
+ const entry = parseObject(raw);
894
+ const owner = entry && field(entry, "pid", "processStartTime");
895
+ return owner ? processMatches(owner, startOf) : "unknown";
896
+ }
897
+ function agentDeviceLiveness(record, startOf) {
898
+ const results = (record.kind === "claim" ? [record.owner] : [record.owner, record.runner]).map((entry) => entry ? processMatches(entry, startOf) : "unknown");
899
+ if (results.includes("dead")) return "dead";
900
+ if (!record.readable || results.includes("unknown")) return "unknown";
901
+ return "live";
902
+ }
903
+ const HOST_PROCESS_COLUMNS = "pid=,ppid=,rss=,%cpu=,lstart=,command=";
904
+ function parseProcessTable(output) {
905
+ const rows = [];
906
+ for (const line of output.split("\n")) {
907
+ const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+(?:[.,]\d+)?)\s+(\w{3}\s+\w{3}\s+\d+\s+\d{1,2}:\d{2}:\d{2}\s+\d{4})\s+(.*)$/.exec(line);
908
+ if (!match) continue;
909
+ const at = Date.parse(match[5]);
910
+ rows.push({
911
+ pid: Number(match[1]),
912
+ ppid: Number(match[2]),
913
+ rssKb: Number(match[3]),
914
+ cpuPercent: Number(match[4].replace(",", ".")),
915
+ startedAt: Number.isFinite(at) ? new Date(at).toISOString() : null,
916
+ command: match[6]
917
+ });
918
+ }
919
+ return rows;
920
+ }
921
+ function parseAndroidInstrumentation(output) {
922
+ const found = [];
923
+ for (const line of output.split("\n")) {
924
+ const match = /^\s*(\d+)\s+(.*)$/.exec(line);
925
+ if (!match) continue;
926
+ const tool = instrumentationTool(match[2]);
927
+ if (tool) found.push({
928
+ pid: Number(match[1]),
929
+ tool
930
+ });
931
+ }
932
+ return found;
933
+ }
934
+ function latestDeviceLogAt(lines, target) {
935
+ return latestRecordAt(lines, (record) => {
936
+ if (record.src !== "device" || record.platform !== target.platform || !isDeviceRecord(record)) return false;
937
+ if (target.slot === "default") return record.slot === void 0;
938
+ return record.slot === target.slot && (record.deviceId === void 0 || record.deviceId === target.id);
939
+ });
940
+ }
941
+ function latestBundleRequestAt(lines, platform) {
942
+ return latestRecordAt(lines, (record) => record.event === "bundle_response_started" && record.platform === platform);
943
+ }
944
+ function latestRecordAt(lines, matches) {
945
+ for (let i = lines.length - 1; i >= 0; i--) {
946
+ const record = parseObject(lines[i]);
947
+ if (record && typeof record.ts === "number" && matches(record)) return record.ts;
948
+ }
949
+ return null;
950
+ }
951
+ function isDeviceRecord(record) {
952
+ return !(typeof record.event === "string" && record.event.startsWith("collector_"));
953
+ }
954
+ /** The last lines of the file at `path`, at most its final 256 KiB, or null when it cannot be read. */
955
+ function tailLines(path) {
956
+ let fd;
957
+ try {
958
+ fd = openSync(path, "r");
959
+ const size = fstatSync(fd).size;
960
+ const length = Math.min(size, LOG_TAIL_BYTES);
961
+ const buffer = Buffer.alloc(length);
962
+ readSync(fd, buffer, 0, length, size - length);
963
+ const lines = buffer.toString("utf8").split("\n");
964
+ return length < size ? lines.slice(1) : lines;
965
+ } catch {
966
+ return null;
967
+ } finally {
968
+ if (fd !== void 0) closeSync(fd);
969
+ }
970
+ }
971
+ function envDir(name, env = process.env, cwd = process.cwd()) {
972
+ const value = env[name]?.trim();
973
+ return value ? resolve(cwd, value) : null;
974
+ }
975
+ function agentDeviceDirs(home, env = process.env) {
976
+ const root = join(home, ".agent-device");
977
+ const leaseDirs = [envDir("AGENT_DEVICE_IOS_RUNNER_LEASE_DIR", env) ?? join(root, "apple-runner", "leases"), join(root, "ios-runner", "leases")];
978
+ return [{
979
+ kind: "claim",
980
+ dir: envDir("AGENT_DEVICE_CLAIMS_DIR", env) ?? join(root, "device-claims")
981
+ }, ...[...new Set(leaseDirs)].map((dir) => ({
982
+ kind: "runner-lease",
983
+ dir
984
+ }))];
985
+ }
986
+ /**
987
+ * When an agent-device session last recorded an event: its events file's modification time. agent-device names a
988
+ * session's directory after the session with every character outside `[a-zA-Z0-9._-]` replaced by `_`.
989
+ */
990
+ function agentSessionActedAt(home, session, stateDir, env = process.env) {
991
+ let latest = null;
992
+ for (const root of stateDir ? [stateDir] : agentDeviceStateDirs(home, env)) try {
993
+ const at = statSync(join(root, "sessions", session.replaceAll(/[^a-zA-Z0-9._-]/g, "_"), "events.ndjson")).mtimeMs;
994
+ latest = latest === null ? at : Math.max(latest, at);
995
+ } catch {}
996
+ return latest;
997
+ }
998
+ function readAgentDeviceRecords(home, env = process.env) {
999
+ return agentDeviceDirs(home, env).flatMap(({ kind, dir }) => {
1000
+ let names;
1001
+ try {
1002
+ names = readdirSync(dir).filter((name) => name.endsWith(".json"));
1003
+ } catch {
1004
+ return [];
1005
+ }
1006
+ return names.map((name) => {
1007
+ const path = join(dir, name);
1008
+ let raw = null;
1009
+ try {
1010
+ raw = readFileSync(path, "utf8");
1011
+ } catch {}
1012
+ return parseAgentDeviceRecord(kind, path, raw);
1013
+ });
1014
+ });
1015
+ }
1016
+ async function readAgentDeviceRecordsAsync(home, env = process.env) {
1017
+ const records = [];
1018
+ for (const { kind, dir } of agentDeviceDirs(home, env)) {
1019
+ let names;
1020
+ try {
1021
+ names = (await readdir(dir)).filter((name) => name.endsWith(".json"));
1022
+ } catch {
1023
+ continue;
1024
+ }
1025
+ for (const name of names) {
1026
+ const path = join(dir, name);
1027
+ let raw = null;
1028
+ try {
1029
+ raw = await readFile(path, "utf8");
1030
+ } catch {}
1031
+ records.push(parseAgentDeviceRecord(kind, path, raw));
1032
+ }
1033
+ }
1034
+ return records;
1035
+ }
1036
+ function isClaimProtocolRunner(record, row) {
1037
+ if (!record.claimProtocol || record.runner?.pid !== row.pid || row.startedAt === null) return false;
1038
+ const recorded = Date.parse(record.runner.startTime);
1039
+ return Math.floor(recorded / 1e3) === Math.floor(Date.parse(row.startedAt) / 1e3);
1040
+ }
1041
+ function lsofArgs(port) {
1042
+ return [
1043
+ "-nP",
1044
+ `-iTCP:${port}`,
1045
+ "-sTCP:ESTABLISHED",
1046
+ "-Fpn"
1047
+ ];
1048
+ }
1049
+ function lsofNoMatch(error) {
1050
+ const stdout = error.stdout;
1051
+ return error.status === 1 && (stdout === "" || stdout === void 0) ? "" : null;
1052
+ }
1053
+ function createDeviceProcessTables() {
1054
+ const exec = getExecutor();
1055
+ let host;
1056
+ const android = /* @__PURE__ */ new Map();
1057
+ const tcp = /* @__PURE__ */ new Map();
1058
+ return {
1059
+ tcpConnections(port) {
1060
+ if (!tcp.has(port)) {
1061
+ let output;
1062
+ try {
1063
+ output = exec.runFile("lsof", lsofArgs(port), { timeoutMs: 5e3 });
1064
+ } catch (error) {
1065
+ output = lsofNoMatch(error);
1066
+ }
1067
+ tcp.set(port, output);
1068
+ }
1069
+ return tcp.get(port) ?? null;
1070
+ },
1071
+ host() {
1072
+ if (host === void 0) {
1073
+ const output = exec.runFileQuiet("ps", [
1074
+ "-axww",
1075
+ "-o",
1076
+ HOST_PROCESS_COLUMNS
1077
+ ], { timeoutMs: 1e4 });
1078
+ host = output === null ? null : parseProcessTable(output);
1079
+ }
1080
+ return host;
1081
+ },
1082
+ android(serial) {
1083
+ if (!android.has(serial)) android.set(serial, exec.runFileQuiet("adb", [
1084
+ "-s",
1085
+ serial,
1086
+ "shell",
1087
+ "ps",
1088
+ "-A",
1089
+ "-o",
1090
+ "PID,ARGS"
1091
+ ], { timeoutMs: 5e3 }));
1092
+ return android.get(serial) ?? null;
1093
+ }
1094
+ };
1095
+ }
1096
+ async function readHostProcessTableAsync(signal, operation) {
1097
+ const output = await runOperationFileQuiet(operation, getExecutor(), "ps", [
1098
+ "-axww",
1099
+ "-o",
1100
+ HOST_PROCESS_COLUMNS
1101
+ ], {
1102
+ timeoutMs: 1e4,
1103
+ killSignal: "SIGKILL",
1104
+ signal,
1105
+ cwd: operation?.cwd
1106
+ });
1107
+ return output === null ? null : parseProcessTable(output);
1108
+ }
1109
+ /** Reads bounded host/instrumentation children asynchronously, sharing observations across device targets. */
1110
+ function createActivityReaderAsync(options = {}) {
1111
+ const exec = getExecutor();
1112
+ let host;
1113
+ let hostValue = null;
1114
+ const android = /* @__PURE__ */ new Map();
1115
+ const androidValues = /* @__PURE__ */ new Map();
1116
+ const read = createActivityReader({
1117
+ ...options,
1118
+ tables: {
1119
+ ...createDeviceProcessTables(),
1120
+ host: () => hostValue,
1121
+ android: (serial) => androidValues.get(serial) ?? null
1122
+ }
1123
+ });
1124
+ const probe = async (target) => {
1125
+ options.signal?.throwIfAborted();
1126
+ host ??= readHostProcessTableAsync(options.signal, options.operation);
1127
+ hostValue = await host;
1128
+ options.signal?.throwIfAborted();
1129
+ if (target.platform === "android") {
1130
+ let pending = android.get(target.id);
1131
+ if (!pending) {
1132
+ pending = runOperationFileQuiet(options.operation, exec, "adb", [
1133
+ "-s",
1134
+ target.id,
1135
+ "shell",
1136
+ "ps",
1137
+ "-A",
1138
+ "-o",
1139
+ "PID,ARGS"
1140
+ ], {
1141
+ timeoutMs: 5e3,
1142
+ killSignal: "SIGKILL",
1143
+ signal: options.signal,
1144
+ cwd: process.platform === "win32" ? homedir() : options.operation?.cwd
1145
+ });
1146
+ android.set(target.id, pending);
1147
+ }
1148
+ androidValues.set(target.id, await pending);
1149
+ }
1150
+ options.signal?.throwIfAborted();
1151
+ return read(target);
1152
+ };
1153
+ let queued = Promise.resolve();
1154
+ return (target) => {
1155
+ if (!options.operation) return probe(target);
1156
+ const result = queued.then(() => probe(target));
1157
+ queued = result;
1158
+ return result;
1159
+ };
1160
+ }
1161
+ function workspaceDeviceRecency(root) {
1162
+ const recency = [];
1163
+ const usedAt = Date.parse(String(readWorkspaceState$1(root)?.lastUsedAt ?? ""));
1164
+ if (Number.isFinite(usedAt)) recency.push({
1165
+ basis: "workspace-use",
1166
+ at: usedAt
1167
+ });
1168
+ for (const file of ["client.ndjson", "device.ndjson"]) try {
1169
+ recency.push({
1170
+ basis: "device-log",
1171
+ at: statSync(join(workspaceLogsDir$1(root), file)).mtimeMs
1172
+ });
1173
+ } catch {}
1174
+ return recency;
1175
+ }
1176
+ /** The newest workspace use or change to its in-app or native device logs. */
1177
+ function workspaceDeviceLastUseAt(root) {
1178
+ const times = workspaceDeviceRecency(root).map(({ at }) => at).filter(Number.isFinite);
1179
+ return times.length ? Math.max(...times) : NaN;
1180
+ }
1181
+ function createActivityReader({ now = Date.now(), env = process.env, home = env.HOME || env.USERPROFILE || homedir(), startOf = process_identity_exports.inspectProcessStart, leaseFiles = listLeaseFiles, tables = createDeviceProcessTables(), viewers = readViewedDevices } = {}) {
1182
+ let agentDevice;
1183
+ let stimLeases;
1184
+ let viewed;
1185
+ const logs = /* @__PURE__ */ new Map();
1186
+ const log = (path) => {
1187
+ if (!logs.has(path)) logs.set(path, tailLines(path));
1188
+ return logs.get(path) ?? null;
1189
+ };
1190
+ return (target) => {
1191
+ const evidence = {
1192
+ drivers: [],
1193
+ unknown: [],
1194
+ recency: []
1195
+ };
1196
+ agentDevice ??= readAgentDeviceRecords(home, env).map((record) => ({
1197
+ record,
1198
+ liveness: agentDeviceLiveness(record, startOf)
1199
+ }));
1200
+ for (const { record, liveness } of agentDevice) {
1201
+ if (record.deviceId !== null && record.deviceId !== target.id) continue;
1202
+ if (record.claimProtocol) continue;
1203
+ const basis = record.kind === "claim" ? "agent-device-claim" : "agent-device-lease";
1204
+ if (liveness === "live") {
1205
+ evidence.drivers.push({
1206
+ basis,
1207
+ tool: "agent-device",
1208
+ pid: record.owner?.pid ?? null,
1209
+ since: record.createdAtMs !== null ? new Date(record.createdAtMs).toISOString() : null
1210
+ });
1211
+ const actedAt = record.session ? agentSessionActedAt(home, record.session, record.stateDir, env) : null;
1212
+ if (actedAt !== null) evidence.recency.push({
1213
+ basis: "agent-action",
1214
+ at: actedAt
1215
+ });
1216
+ } else if (liveness === "unknown") evidence.unknown.push(basis);
1217
+ }
1218
+ try {
1219
+ stimLeases ??= leaseFiles();
1220
+ } catch {
1221
+ stimLeases = [];
1222
+ evidence.unknown.push("device-lock");
1223
+ }
1224
+ for (const entry of stimLeases) {
1225
+ if (entry.platform !== target.platform || entry.id !== target.id) continue;
1226
+ if (!entry.lease) evidence.unknown.push("device-lock");
1227
+ else if (!leaseIsExpired(entry.lease, now)) evidence.drivers.push({
1228
+ basis: "device-lock",
1229
+ tool: "stim device lock",
1230
+ pid: null,
1231
+ since: entry.lease.grantedAt
1232
+ });
1233
+ }
1234
+ viewed ??= viewers();
1235
+ if (viewed.some((device) => device.platform === target.platform && device.id === target.id)) evidence.recency.push({
1236
+ basis: "viewer",
1237
+ at: now
1238
+ });
1239
+ const processes = tables.host();
1240
+ if (processes === null) evidence.unknown.push("driver-process");
1241
+ for (const row of processes ?? []) {
1242
+ if (agentDevice.some(({ record }) => isClaimProtocolRunner(record, row))) continue;
1243
+ const tool = hostDriverTool(row.command, target.platform, target.id);
1244
+ if (tool) evidence.drivers.push({
1245
+ basis: "driver-process",
1246
+ tool,
1247
+ pid: row.pid,
1248
+ since: row.startedAt
1249
+ });
1250
+ }
1251
+ if (target.platform === "android") {
1252
+ const output = tables.android(target.id);
1253
+ if (output === null) evidence.unknown.push("instrumentation");
1254
+ for (const { pid, tool } of parseAndroidInstrumentation(output ?? "")) evidence.drivers.push({
1255
+ basis: "instrumentation",
1256
+ tool,
1257
+ pid,
1258
+ since: null
1259
+ });
1260
+ }
1261
+ if (target.workspace) {
1262
+ const dir = workspaceLogsDir$1(target.workspace);
1263
+ const deviceAt = latestDeviceLogAt(log(join(dir, "device.ndjson")) ?? [], target);
1264
+ if (deviceAt !== null) evidence.recency.push({
1265
+ basis: "device-log",
1266
+ at: deviceAt
1267
+ });
1268
+ const bundleAt = latestBundleRequestAt(log(join(dir, "metro.ndjson")) ?? [], target.platform);
1269
+ if (bundleAt !== null) evidence.recency.push({
1270
+ basis: "metro-bundle",
1271
+ at: bundleAt
1272
+ });
1273
+ const usedAt = Date.parse(String(readWorkspaceState$1(target.workspace)?.lastUsedAt ?? ""));
1274
+ if (Number.isFinite(usedAt)) evidence.recency.push({
1275
+ basis: "workspace-use",
1276
+ at: usedAt
1277
+ });
1278
+ }
1279
+ return classifyActivity(evidence, now);
1280
+ };
1281
+ }
1282
+ function workspaceActivity(workspace, now = Date.now()) {
1283
+ const dir = workspaceLogsDir$1(workspace);
1284
+ const state = readWorkspaceState$1(workspace);
1285
+ const recency = [];
1286
+ const deviceAt = latestRecordAt(tailLines(join(dir, "device.ndjson")) ?? [], (record) => record.src === "device" && isDeviceRecord(record));
1287
+ if (deviceAt !== null) recency.push({
1288
+ basis: "device-log",
1289
+ at: deviceAt
1290
+ });
1291
+ const bundleAt = latestRecordAt(tailLines(join(dir, "metro.ndjson")) ?? [], (record) => record.event === "bundle_response_started");
1292
+ if (bundleAt !== null) recency.push({
1293
+ basis: "metro-bundle",
1294
+ at: bundleAt
1295
+ });
1296
+ for (const [basis, value] of [["workspace-use", state?.lastUsedAt], ["supervisor-start", state?.supervisor?.startedAt]]) {
1297
+ const at = Date.parse(String(value ?? ""));
1298
+ if (Number.isFinite(at)) recency.push({
1299
+ basis,
1300
+ at
1301
+ });
1302
+ }
1303
+ return classifyActivity({
1304
+ drivers: [],
1305
+ unknown: [],
1306
+ recency
1307
+ }, now);
1308
+ }
1309
+ /**
1310
+ * The pids of processes connected to loopback `port` as clients, from `lsof -Fpn` output: a connection whose remote
1311
+ * end is the port. Chrome's own accepted sockets have the port as their local end and are skipped.
1312
+ */
1313
+ function parseTcpClients(output, port) {
1314
+ const pids = /* @__PURE__ */ new Set();
1315
+ let pid = null;
1316
+ for (const line of output.split("\n")) if (line.startsWith("p")) pid = Number(line.slice(1));
1317
+ else if (line.startsWith("n") && pid !== null && Number.isInteger(pid)) {
1318
+ const remote = line.slice(1).split("->")[1];
1319
+ if (remote && Number(remote.slice(remote.lastIndexOf(":") + 1)) === port) pids.add(pid);
1320
+ }
1321
+ return [...pids];
1322
+ }
1323
+ function webClientPids(connections, target) {
1324
+ return parseTcpClients(connections, target.port).filter((pid) => !target.ownPids.includes(pid));
1325
+ }
1326
+ function webDrivers(clients, processes) {
1327
+ return clients.flatMap((pid) => {
1328
+ const row = processes.find((candidate) => candidate.pid === pid);
1329
+ const tool = row ? cdpClientTool(row.command) : "unknown DevTools client";
1330
+ return tool ? [{
1331
+ tool,
1332
+ pid,
1333
+ since: row?.startedAt ?? null
1334
+ }] : [];
1335
+ });
1336
+ }
1337
+ /**
1338
+ * The DevTools clients of the owned browser that are not Stim's, read without blocking the event loop; null when
1339
+ * lsof or ps could not be read.
1340
+ */
1341
+ async function probeWebDrivers(target) {
1342
+ const exec = getExecutor();
1343
+ let connections;
1344
+ try {
1345
+ connections = await exec.runFileAsync("lsof", lsofArgs(target.port), { timeoutMs: 5e3 });
1346
+ } catch (error) {
1347
+ connections = lsofNoMatch(error);
1348
+ }
1349
+ if (connections === null) return null;
1350
+ const clients = webClientPids(connections, target);
1351
+ if (!clients.length) return [];
1352
+ let table;
1353
+ try {
1354
+ table = await exec.runFileAsync("ps", [
1355
+ "-axww",
1356
+ "-o",
1357
+ HOST_PROCESS_COLUMNS
1358
+ ], { timeoutMs: 5e3 });
1359
+ } catch {
1360
+ return null;
1361
+ }
1362
+ return webDrivers(clients, parseProcessTable(table));
1363
+ }
1364
+ /**
1365
+ * The owned browser's activity: a DevTools client other than Stim's is a driver (basis `cdp-client`). Recency comes
1366
+ * from the newest agent action the browser supervisor recorded (basis `agent-action`) and the newest other page
1367
+ * record (basis `page-log`).
1368
+ */
1369
+ function readWebActivity(target, tables, now = Date.now()) {
1370
+ const evidence = {
1371
+ drivers: [],
1372
+ unknown: [],
1373
+ recency: []
1374
+ };
1375
+ const connections = tables.tcpConnections(target.port);
1376
+ const clients = connections === null ? [] : webClientPids(connections, target);
1377
+ const processes = clients.length ? (tables.clients ?? tables.host)() : [];
1378
+ if (connections === null || processes === null) evidence.unknown.push("cdp-client");
1379
+ else for (const driver of webDrivers(clients, processes)) evidence.drivers.push({
1380
+ ...driver,
1381
+ basis: "cdp-client"
1382
+ });
1383
+ const lines = tailLines(join(workspaceLogsDir$1(target.workspace), "web.ndjson")) ?? [];
1384
+ const actionAt = latestRecordAt(lines, (record) => record.src === "agent");
1385
+ if (actionAt !== null) evidence.recency.push({
1386
+ basis: "agent-action",
1387
+ at: actionAt
1388
+ });
1389
+ const pageAt = latestRecordAt(lines, (record) => record.src !== "agent");
1390
+ if (pageAt !== null) evidence.recency.push({
1391
+ basis: "page-log",
1392
+ at: pageAt
1393
+ });
1394
+ return classifyActivity(evidence, now);
1395
+ }
1396
+ //#endregion
1397
+ export { selectPoolDevice as A, parseLeaseDuration as C, releaseWorkspaceLeases as D, releaseRunLease as E, waitForBuild as F, waitForSharedBuild as I, acquireBuildLock as M, listBuildLocks as N, releaseWorkspaceLeasesAsync as O, releaseBuildLock as P, parseLease as S, raiseLease as T, workspaceDeviceLastUseAt as _, createDeviceProcessTables as a, leaseIsExpired as b, parseAgentDeviceRecord as c, readAgentDeviceRecords as d, readAgentDeviceRecordsAsync as f, workspaceActivity as g, tailLines as h, createActivityReaderAsync as i, takeLease as j, removeExpiredLease as k, probeWebDrivers as l, readWebActivity as m, agentDeviceLiveness as n, envDir as o, readHostProcessTableAsync as p, createActivityReader as r, idleForMs as s, agentDeviceDaemon as t, processMatches as u, deviceLeasePath as v, parseWorkspaceLeases as w, listLeaseFiles as x, fileLeaseIo as y };