stim 1.20.1 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/README.md +94 -11
  2. package/dist/acquire-artifact-CD4lbiWE.mjs +153 -0
  3. package/dist/act-DHAw2A2S.mjs +2667 -0
  4. package/dist/activity-BzVfHSrY.mjs +1376 -0
  5. package/dist/{agent-device-usage-output-VPaOgGaF.mjs → agent-device-usage-output-zh1vhbLp.mjs} +21 -66
  6. package/dist/{android-CZh6ElG5.mjs → android-BEJ8Jpl7.mjs} +3 -3
  7. package/dist/{android-CLbBJ2ve.mjs → android-BLgbNqox.mjs} +128 -79
  8. package/dist/{android-eXfIjKJr.mjs → android-CV_GCELZ.mjs} +5 -5
  9. package/dist/{android-DE8-JfGW.mjs → android-DUYkNF4u.mjs} +188 -106
  10. package/dist/{android-cas-BFpS84v2.mjs → android-cas-Ct5b1xCK.mjs} +7 -6
  11. package/dist/android-cas-compiler.mjs +2 -2
  12. package/dist/android-project-BCAAmMwz.mjs +12 -0
  13. package/dist/api.d.mts +740 -8
  14. package/dist/api.mjs +585 -1
  15. package/dist/{artifact-Ctb2jpw6.mjs → artifact-B8T3S5gi.mjs} +223 -201
  16. package/dist/{artifact-DXk_Fle_.mjs → artifact-DAuZ-FNU.mjs} +158 -148
  17. package/dist/{browser-web-xQWC2RWl.mjs → browser-web-QQzVjz3O.mjs} +28 -26
  18. package/dist/budget-Bs6SxxzO.mjs +9154 -0
  19. package/dist/{build-COBA1tBc.mjs → build-CoPzQRNw.mjs} +144 -101
  20. package/dist/build-cache-DmyR9Zzc.mjs +602 -0
  21. package/dist/build-plan-CaFskB7_.mjs +188 -0
  22. package/dist/{cache-manifest-qA3dT2x2.mjs → cache-manifest-DkkqguoK.mjs} +10 -5
  23. package/dist/cache-manifest.mjs +1 -1
  24. package/dist/{ccache-CtbqWxIC.mjs → ccache-BJRZ9pG3.mjs} +10 -9
  25. package/dist/{cdp-Dq_ypAlJ.mjs → cdp-CWmIUMbw.mjs} +18 -5
  26. package/dist/{chrome-Dp2lMLBe.mjs → chrome-B6tfCta1.mjs} +7 -4
  27. package/dist/{cli-sGqlPu1n.mjs → cli-0DYf8QhQ.mjs} +20 -20
  28. package/dist/cli.mjs +1 -1
  29. package/dist/client-B7QVOlCr.d.mts +253 -0
  30. package/dist/client-UXD70ZbM.mjs +1406 -0
  31. package/dist/collector-run.d.mts +4 -39
  32. package/dist/collector-run.mjs +9 -9
  33. package/dist/{command-output-CPDq_90S.mjs → command-output-B1UrjZHL.mjs} +1 -1
  34. package/dist/config-CYcBF478.mjs +949 -0
  35. package/dist/{created-devices-CXBMtoj5.mjs → created-devices-CqeBlrEn.mjs} +3 -3
  36. package/dist/{dependency-state-B2m8eqsS.mjs → dependency-state-BbKQW83h.mjs} +20 -5
  37. package/dist/{detached-entry-BXq4fIl-.mjs → detached-entry-DFRyG1_o.mjs} +1 -1
  38. package/dist/{dev-client-BqYjMIaC.mjs → dev-client-UOeESKm6.mjs} +2 -2
  39. package/dist/device-DCxH7_By.mjs +1068 -0
  40. package/dist/{device-D4DqjSZZ.mjs → device-ExJq8Ei7.mjs} +35 -36
  41. package/dist/{device-capacity-DrwHwR97.mjs → device-capacity-CMlqEcvw.mjs} +43 -14
  42. package/dist/device-host-worker.mjs +65 -62
  43. package/dist/{device-ios-DE8KJd3b.mjs → device-ios-DRUeKaFV.mjs} +49 -49
  44. package/dist/{device-lease-run-Baelt3nc.mjs → device-lease-run-C8raGFoP.mjs} +14 -26
  45. package/dist/{device-pool-DtK9S6AD.mjs → device-pool-DMRpVX9a.mjs} +5 -5
  46. package/dist/doctor-BLHCYxzi.mjs +2 -0
  47. package/dist/{doctor-Dunu3Nv1.mjs → doctor-DTvaDOda.mjs} +317 -344
  48. package/dist/{doctor-DiP9HsIP.mjs → doctor-Dhqebm0q.mjs} +183 -91
  49. package/dist/doctor-fingerprint.mjs +19 -0
  50. package/dist/{doctor-watchman-XJzT8tK-.mjs → doctor-watchman-Cv1F10Kj.mjs} +40 -23
  51. package/dist/{eas-build-0gadvrca.mjs → eas-build-BomfRnFT.mjs} +53 -20
  52. package/dist/{build-plan-CRjAA6Qi.mjs → eas-fallback-DJwZpy0c.mjs} +26 -208
  53. package/dist/{watchman-CqmaGcIb.mjs → eas-session-ledger-op6A54yi.mjs} +229 -11
  54. package/dist/{error-diagnostics-BhQtoLrZ.mjs → error-diagnostics-Bf0rZvG9.mjs} +172 -82
  55. package/dist/exec-H20xSg2T.d.mts +65 -0
  56. package/dist/exec-_UbtCQcG.mjs +750 -0
  57. package/dist/gc-C-WwL0yi.mjs +3 -0
  58. package/dist/{gc-D5cz5G0k.mjs → gc-_-dCXdyL.mjs} +625 -786
  59. package/dist/git-C3iTsJaZ.mjs +73 -0
  60. package/dist/{guide-DakjcoUt.mjs → guide-Yqwt2bd6.mjs} +990 -280
  61. package/dist/{hosted-android-C-D8Dj0_.mjs → hosted-android-Bw-honsd.mjs} +8 -6
  62. package/dist/{hosted-client--hNFgixu.mjs → hosted-client-DB_gBPfY.mjs} +47 -24
  63. package/dist/{hosted-ios-Chx9XQyI.mjs → hosted-ios-CUvOf4Vr.mjs} +1 -1
  64. package/dist/{hosted-logs-Bkz0Hxzh.mjs → hosted-logs-DL_P5AdN.mjs} +77 -43
  65. package/dist/{hosted-macos-ckTjN-0V.mjs → hosted-macos-QI141yKk.mjs} +21 -15
  66. package/dist/{hosted-native-3KXZ5-qn.mjs → hosted-native-Ksyjgzq-.mjs} +31 -20
  67. package/dist/idle-BVeRGroT.d.mts +24 -0
  68. package/dist/{idle-shutdown-DhAUyf5s.mjs → idle-shutdown-DaKmZi0z.mjs} +11 -10
  69. package/dist/{ios-BDBN-xpf.mjs → ios-CQ68hG9s.mjs} +8 -8
  70. package/dist/{ios-o896T9PE.mjs → ios-CpByZy12.mjs} +141 -81
  71. package/dist/{ios-device-B1B14xND.mjs → ios-device-CXGP1bsD.mjs} +1 -1
  72. package/dist/{ios-device-BWHKGaJq.mjs → ios-device-CwFMehUS.mjs} +60 -35
  73. package/dist/{ios-project-BeOXKPt2.mjs → ios-project-WuwwBdvW.mjs} +13 -13
  74. package/dist/{ios-VGvNdsv2.mjs → ios-qNJt4dFm.mjs} +1 -1
  75. package/dist/ios-signing-DTTIChGs.mjs +489 -0
  76. package/dist/{ios-state-BsXMwo5y.mjs → ios-state-DvFX-K_l.mjs} +1 -1
  77. package/dist/{ios-CQ8bHQXB.mjs → ios-ubFsBzwq.mjs} +129 -86
  78. package/dist/{js-swap-Z_mSuRrx.mjs → js-swap-Cc1dttps.mjs} +60 -33
  79. package/dist/{launch-V1zqiOYn.mjs → launch-CD1yVc7U.mjs} +92 -91
  80. package/dist/{launch-BootbMk4.mjs → launch-Zq2mOSDy.mjs} +87 -545
  81. package/dist/logs-BS00Anxf.mjs +352 -0
  82. package/dist/logs-CMm05HxN.mjs +255 -0
  83. package/dist/macos-B65--pPd.mjs +2 -0
  84. package/dist/{macos-D7irBW3A.mjs → macos-C-0qJ6zq.mjs} +244 -215
  85. package/dist/macos-run.mjs +1 -1
  86. package/dist/maintenance-run.mjs +122 -247
  87. package/dist/{metro-_ZIE3Axg.mjs → metro-DEXpIhHC.mjs} +71 -17
  88. package/dist/{metro-gateway-BzlQZQmU.mjs → metro-gateway-BMJNlj3E.mjs} +20 -12
  89. package/dist/{named-ports-O1TIl7tx.mjs → named-ports-BWTzt6mu.mjs} +8 -64
  90. package/dist/native-android-JACyjTZs.mjs +844 -0
  91. package/dist/{native-gradle-inputs-BQnIdAlL.mjs → native-gradle-inputs-Bep9JcSF.mjs} +128 -147
  92. package/dist/native-inputs-TRTTTRxt.mjs +91 -0
  93. package/dist/native-run-DY_RLx3C.mjs +277 -0
  94. package/dist/{native-runtime-D024BAvm.mjs → native-runtime-Dg1Q7tfF.mjs} +10 -10
  95. package/dist/{native-xcode-inputs-UElfGs59.mjs → native-xcode-inputs-CB2iQeWq.mjs} +28 -20
  96. package/dist/{native-xcode-ios-CjS-nDVY.mjs → native-xcode-ios-CXX39F7o.mjs} +96 -47
  97. package/dist/{ndjson-GOnbyvso.mjs → ndjson-CgE_o3Dl.mjs} +3 -3
  98. package/dist/offload-worker.d.mts +1 -46
  99. package/dist/offload-worker.mjs +213 -86
  100. package/dist/{ownership-BrjepDiv.mjs → ownership-DEmM5vhh.mjs} +3 -3
  101. package/dist/{page-Bv9s2KwY.mjs → page-CPaHeYqj.mjs} +7 -4
  102. package/dist/paths-BsJMoPP2.mjs +12 -0
  103. package/dist/paths-DQtTgsBf.mjs +42 -0
  104. package/dist/{placement-log-BItXET1x.mjs → placement-log-BqGbTa61.mjs} +5 -5
  105. package/dist/{plan-placement-B466UYMU.mjs → plan-placement-Bwj-IjY1.mjs} +6 -6
  106. package/dist/{ports-BFRL6JTj.mjs → ports-D-MmCTdv.mjs} +3 -3
  107. package/dist/prebuild-uD_ipCxF.mjs +449 -0
  108. package/dist/{project-DX8fXyw_.mjs → project-BZNjmW_8.mjs} +65 -38
  109. package/dist/{pull-request-vOhLNmIb.mjs → pull-request-Lfetn8Kr.mjs} +5 -18
  110. package/dist/pull-request-zjCSAryH.d.mts +120 -0
  111. package/dist/pull-requests.d.mts +1 -119
  112. package/dist/pull-requests.mjs +1 -1
  113. package/dist/{react-native-android-CuXM3QS8.mjs → react-native-android-DuzH0cn3.mjs} +83 -53
  114. package/dist/{gradle-CZZo1TSA.mjs → react-native-build-rhk4smwQ.mjs} +292 -15
  115. package/dist/react-native-doctor-CQpuIMso.mjs +234 -0
  116. package/dist/{react-native-ios-DtVO4qf2.mjs → react-native-ios-Bhq1rJeV.mjs} +46 -28
  117. package/dist/{recordings-Bk-VivUV.mjs → recordings-DYteQSu4.mjs} +12 -9
  118. package/dist/{reload-D9V7D8gP.mjs → reload-CmTRFJaX.mjs} +95 -49
  119. package/dist/reload-DzjSl5P_.mjs +2 -0
  120. package/dist/{remote-CZiTijxG.mjs → remote-ByYkXdGD.mjs} +15 -10
  121. package/dist/result-DNiS-tTP.mjs +434 -0
  122. package/dist/{run-ClC8UVB5.mjs → run-D23HmJMs.mjs} +4 -5
  123. package/dist/selection-CSl9i5qU.mjs +619 -0
  124. package/dist/{server-bare-dWjAarki.mjs → server-bare-CxTrvDg8.mjs} +3 -4
  125. package/dist/{server-command-DnJYLnr4.mjs → server-command-CtzloH3v.mjs} +14 -7
  126. package/dist/server-expo-C-90EhcG.mjs +2 -0
  127. package/dist/{server-expo-CUuzP9Mp.mjs → server-expo-DT_F_uVI.mjs} +51 -15
  128. package/dist/{settings-Bbztm4oV.mjs → settings-CsJDEXgK.mjs} +301 -132
  129. package/dist/{settings-vk3mJGSm.d.mts → settings-DkdmlelE.d.mts} +6 -4
  130. package/dist/settings-T1gtc3bf.mjs +114 -0
  131. package/dist/settings-write-HyVZ8eQ8.mjs +359 -0
  132. package/dist/settings.schema.json +163 -16
  133. package/dist/{simslim-ChMfe8rg.mjs → simslim-Cgk17SXV.mjs} +14 -13
  134. package/dist/{slot-launch-CeCRr6MK.mjs → slot-launch-DF8VfT64.mjs} +15 -20
  135. package/dist/{spawn-entry-Dcb7sDUz.mjs → spawn-entry-jw19m8Nl.mjs} +0 -1
  136. package/dist/stage-BIS0ydsV.mjs +183 -0
  137. package/dist/{start-CRkG-mZ7.mjs → start-7lpH7hmN.mjs} +66 -48
  138. package/dist/{start-mh6bS27z.mjs → start-RChynRDp.mjs} +1 -1
  139. package/dist/{state-lqxdOH_g.mjs → state-C4ZtUisW.mjs} +41 -14
  140. package/dist/{state-BUpAmt2r.mjs → state-XMziexv2.mjs} +3 -2
  141. package/dist/{stats-CaB1MxMi.mjs → stats-53T7OTb9.mjs} +7 -13
  142. package/dist/stats-report-BFfMpWWF.mjs +16 -0
  143. package/dist/{status-C7vzkInu.mjs → status-BL7jVyAB.mjs} +538 -467
  144. package/dist/status-CSi6tlRq.mjs +2 -0
  145. package/dist/{status-BDGHwkCs.mjs → status-DqEUSSxs.mjs} +6 -4
  146. package/dist/{status-watch-ZaVqRdtW.mjs → status-watch-GDCfqpTE.mjs} +187 -22
  147. package/dist/status-worker.d.mts +1 -0
  148. package/dist/status-worker.mjs +19 -0
  149. package/dist/stim-desktop-C12we7lz.mjs +112 -0
  150. package/dist/{stop-BU7JaFCg.mjs → stop-Bk_1qiV6.mjs} +2 -2
  151. package/dist/{stop-BO80KJoT.mjs → stop-CPzlM1L-.mjs} +138 -99
  152. package/dist/{stop-Cftw_sXJ.mjs → stop-D7h6jAXQ.mjs} +11 -10
  153. package/dist/supervisor-run.d.mts +8 -17
  154. package/dist/supervisor-run.mjs +63 -28
  155. package/dist/{support-Ca6PZbpr.mjs → support-67vraa1Q.mjs} +18 -11
  156. package/dist/{support-Co0wk8Cv.mjs → support-Dlu37g6H.mjs} +12 -12
  157. package/dist/{swiftpm-macos-siFSkmCO.mjs → swiftpm-macos-p8WZbxKU.mjs} +35 -24
  158. package/dist/toolchain-DhKwp9iI.mjs +2 -0
  159. package/dist/toolchain-Hp9sCpK4.mjs +873 -0
  160. package/dist/{trigger-C0MWBi8O.mjs → trigger-BAahxUPq.mjs} +20 -5
  161. package/dist/trigger-Bf3uDcpv.mjs +2 -0
  162. package/dist/{warm-progress-5FAXkXIg.mjs → warm-progress-BHX5r0Ei.mjs} +252 -176
  163. package/dist/{web-BTjzE8mc.mjs → web-CJmIj6AI.mjs} +18 -13
  164. package/dist/web-DknQETPs.mjs +2 -0
  165. package/dist/web-run.mjs +16 -17
  166. package/dist/windows-report.d.mts +1 -0
  167. package/dist/windows-report.mjs +18 -0
  168. package/dist/windows-report.ps1 +43 -0
  169. package/dist/workspace-process-lock-1_1OoVW5.mjs +57 -0
  170. package/dist/{workspace-state-CvgTFe-h.mjs → workspace-state-Cjuigeeu.mjs} +70 -56
  171. package/dist/{workspaces-C4kqIOlx.mjs → workspaces-BccMFmGQ.mjs} +1188 -1080
  172. package/dist/worktree-CK2MpJW5.mjs +2 -0
  173. package/dist/{worktree-DtRsh6go.mjs → worktree-DQ6Zi2Mv.mjs} +368 -110
  174. package/dist/xcode-Zlx3qjEs.mjs +896 -0
  175. package/package.json +7 -5
  176. package/shim/android-optimizations.gradle +7 -5
  177. package/dist/activity-BaMpUQ4-.mjs +0 -629
  178. package/dist/api-run.mjs +0 -296
  179. package/dist/api-zuX3ynYN.mjs +0 -154
  180. package/dist/app-install-Cg9YJy3x.mjs +0 -873
  181. package/dist/artifact-lifecycle-DzPpBYvK.mjs +0 -30
  182. package/dist/budget-RFbwFpG5.mjs +0 -4109
  183. package/dist/build-facts-DcLv2HSd.d.mts +0 -111
  184. package/dist/client-zqjFd79D.mjs +0 -1779
  185. package/dist/config-D0LhTY3S.mjs +0 -524
  186. package/dist/device-BgNj-qVj.mjs +0 -290
  187. package/dist/device-lease-C309csaB.mjs +0 -1228
  188. package/dist/device-remote-DjsNw8DD.mjs +0 -1272
  189. package/dist/eas-session-ledger-DJDeL4Uq.mjs +0 -205
  190. package/dist/exec-B81Qodjn.mjs +0 -254
  191. package/dist/launch-verify-Dl78EVgR.mjs +0 -428
  192. package/dist/logs-Bh4eqCCc.mjs +0 -384
  193. package/dist/macos-kivF8qKz.mjs +0 -2
  194. package/dist/miss-reason-SuTptY-I.mjs +0 -238
  195. package/dist/native-android-D94RlJ-X.mjs +0 -226
  196. package/dist/ndjson-Du3j0AQ9.d.mts +0 -17
  197. package/dist/plan-Jwu_vr-F.mjs +0 -260
  198. package/dist/prebuild-DB_zLOLc.mjs +0 -213
  199. package/dist/preview-BRLFRhTz.mjs +0 -1004
  200. package/dist/process-identity-Biv3arCO.mjs +0 -40
  201. package/dist/react-native-build-JUF31S8U.mjs +0 -239
  202. package/dist/react-native-doctor-BQHqiXaQ.mjs +0 -78
  203. package/dist/remote-cache-fxcl5C-L.mjs +0 -526
  204. package/dist/result-BrfgXtdO.mjs +0 -186
  205. package/dist/server-expo-Dvkd7JcF.mjs +0 -2
  206. package/dist/settings-BuKMOANA.mjs +0 -402
  207. package/dist/spawn-claims-Kqiifsk2.mjs +0 -72
  208. package/dist/stage-BNKKdDNc.mjs +0 -157
  209. package/dist/stim-desktop-8GQX2OV8.mjs +0 -63
  210. package/dist/tailnet-D7-tE_ug.mjs +0 -265
  211. package/dist/toolchain-B12psPA2.mjs +0 -2
  212. package/dist/toolchain-SqehyPLw.mjs +0 -1657
  213. package/dist/trigger-SHLOWJ4O.mjs +0 -2
  214. package/dist/web-CHN5gh34.mjs +0 -2
  215. package/dist/worktree-Cgnm6vZt.mjs +0 -2
  216. package/dist/{api-run.d.mts → doctor-fingerprint.d.mts} +0 -0
  217. package/dist/{errors-DzkMYh8Q.mjs → errors-ccaPc4ox.mjs} +2 -2
@@ -0,0 +1,1376 @@
1
+ import { c as stopDeclaringSpawnsOn, f as ownership_claim_exports, i as declareSpawnsOn, p as process_identity_exports, t as getExecutor } from "./exec-_UbtCQcG.mjs";
2
+ import { m as workspaceLogsDir$1, v as withDirLock$1, y as withDirLockAsync$1 } from "./paths-DQtTgsBf.mjs";
3
+ import { H as deviceSlotKey, J as validateDeviceSlot, W as parseDeviceSlotKey, d as getConfigDir } from "./config-CYcBF478.mjs";
4
+ import { a as formatElapsed, i as formatDuration, m as shortHash } from "./command-output-B1UrjZHL.mjs";
5
+ import { c as withWorkspaceStateLock, d as writeWorkspaceState, l as withWorkspaceStateLockAsync, n as clearWorkspaceStateKeys, r as readWorkspaceState$1 } from "./workspace-state-Cjuigeeu.mjs";
6
+ import { h as resolveBuild } from "./build-cache-DmyR9Zzc.mjs";
7
+ import { randomBytes } from "node:crypto";
8
+ import { closeSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { basename, join, resolve } from "node:path";
11
+ import { ambientContext } from "@stim-cli/core";
12
+ import { agentDeviceStateDirs, isJsonObject, readViewedDevices } from "@stim-cli/core/state";
13
+ import { readFile, readdir } from "node:fs/promises";
14
+ //#region src/devices/automation-tools.ts
15
+ const AUTOMATION_TOOLS = [
16
+ {
17
+ tool: "agent-browser",
18
+ platforms: ["web"],
19
+ cdp: /agent-browser/i
20
+ },
21
+ {
22
+ tool: "agent-device",
23
+ platforms: [
24
+ "ios",
25
+ "android",
26
+ "web"
27
+ ],
28
+ host: /agent-device/i,
29
+ cdp: /agent-device/i
30
+ },
31
+ {
32
+ tool: "argent",
33
+ platforms: [
34
+ "ios",
35
+ "android",
36
+ "web"
37
+ ],
38
+ host: /\/bin\/[\w-]+\/(simulator-server(\.exe)?\s+(ios|android|android_device)|ax-service)\b|com\.argent\.androiddevtools/,
39
+ instrumentation: /com\.argent\.androiddevtools/,
40
+ cdp: /@swmansion\/argent\/dist\/tool-server/
41
+ },
42
+ {
43
+ tool: "idb",
44
+ platforms: ["ios"],
45
+ host: /idb_companion/
46
+ },
47
+ {
48
+ tool: "maestro",
49
+ platforms: ["ios", "android"],
50
+ host: /maestro/i
51
+ },
52
+ {
53
+ tool: "appium",
54
+ platforms: ["ios", "android"],
55
+ host: /appium|WebDriverAgent/i
56
+ },
57
+ {
58
+ tool: "xcodebuild",
59
+ platforms: ["ios"],
60
+ host: /\bxcodebuild\b.*\btest(-without-building)?\b/
61
+ },
62
+ {
63
+ tool: "simctl",
64
+ platforms: ["ios"],
65
+ host: /\bsimctl\s+(io|spawn)\b/
66
+ },
67
+ {
68
+ tool: "uiautomator",
69
+ platforms: ["android"],
70
+ instrumentation: /uiautomator/
71
+ },
72
+ {
73
+ tool: "instrumentation",
74
+ platforms: ["android"],
75
+ instrumentation: /androidx\.test|\binstrument\b/
76
+ },
77
+ {
78
+ tool: "chrome-devtools-mcp",
79
+ platforms: ["web"],
80
+ cdp: /chrome-devtools-mcp/i
81
+ },
82
+ {
83
+ tool: "playwright",
84
+ platforms: ["web"],
85
+ cdp: /playwright/i
86
+ },
87
+ {
88
+ tool: "puppeteer",
89
+ platforms: ["web"],
90
+ cdp: /puppeteer/i
91
+ }
92
+ ];
93
+ const HOST_EXCLUSIONS = [/\bsimctl\s+spawn\b.*\blog\s+stream\b/, /\blogcat\b/];
94
+ /** Stim's own DevTools clients: the browser supervisor, `stim-frames` in stim-server, Stim Desktop, the stim CLI. */
95
+ const CDP_EXCLUSIONS = [/\bstim-(frames|server|web)\b|StimDesktop|\/stim(-cli)?\/(dist|bin)\/|\/bin\/stim(\s|$)|^stim(\s|$)/];
96
+ function namesDevice(command, id) {
97
+ const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
98
+ return new RegExp(`(^|[^A-Za-z0-9])${escaped}([^A-Za-z0-9]|$)`).test(command);
99
+ }
100
+ /** The tool a host process drives the device `id` with, or null when the command does not name the device. */
101
+ function hostDriverTool(command, platform, id) {
102
+ if (!namesDevice(command, id) || HOST_EXCLUSIONS.some((pattern) => pattern.test(command))) return null;
103
+ return AUTOMATION_TOOLS.find((entry) => entry.platforms.includes(platform) && entry.host?.test(command))?.tool ?? null;
104
+ }
105
+ /** The tool behind an on-device process's arguments, or null when it is not instrumentation. */
106
+ function instrumentationTool(args) {
107
+ return AUTOMATION_TOOLS.find((entry) => entry.platforms.includes("android") && entry.instrumentation?.test(args))?.tool ?? null;
108
+ }
109
+ /**
110
+ * The tool a DevTools client's command line names, or null for Stim's own clients. A client no entry names is
111
+ * reported by its script or executable name.
112
+ */
113
+ function cdpClientTool(command) {
114
+ if (CDP_EXCLUSIONS.some((pattern) => pattern.test(command))) return null;
115
+ const known = AUTOMATION_TOOLS.find((entry) => entry.platforms.includes("web") && entry.cdp?.test(command));
116
+ if (known) return known.tool;
117
+ const [executable = "", script] = command.trim().split(/\s+/);
118
+ const name = basename(executable);
119
+ return (/^(node|bun|deno|python3?)$/.test(name) && script ? basename(script) : name) || "unknown DevTools client";
120
+ }
121
+ //#endregion
122
+ //#region src/engine/build-lock.ts
123
+ const LOCK_SUFFIX$1 = ".lock";
124
+ const WAIT_POLL_MS = 1e3;
125
+ const WAIT_PROGRESS_MS = 3e4;
126
+ const WAIT_CEILING_MS = 54e5;
127
+ function sleepAsync(ms) {
128
+ return new Promise((resolve) => setTimeout(resolve, ms));
129
+ }
130
+ function segment(value) {
131
+ return String(value).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^\.+/, "") || "unknown";
132
+ }
133
+ function buildLocksDir() {
134
+ return join(getConfigDir(), "build-locks");
135
+ }
136
+ function buildLockPath(platform, key) {
137
+ return join(buildLocksDir(), `${segment(platform)}-${segment(key)}${LOCK_SUFFIX$1}`);
138
+ }
139
+ function label(platform) {
140
+ return `${platform} build`;
141
+ }
142
+ function toRecord(holder) {
143
+ const details = holder.details;
144
+ return {
145
+ pid: holder.owner.pid,
146
+ projectRoot: typeof details.projectRoot === "string" ? details.projectRoot : null,
147
+ startedAt: holder.startedAt || null,
148
+ logFile: typeof details.logFile === "string" ? details.logFile : null
149
+ };
150
+ }
151
+ function readBuildLock(pathOrSpec) {
152
+ const path = typeof pathOrSpec === "string" ? pathOrSpec : buildLockPath(pathOrSpec.platform, pathOrSpec.key);
153
+ const survey = (0, ownership_claim_exports.readClaimSet)(path);
154
+ const holder = survey.live[0] ?? survey.dead[0];
155
+ return holder ? toRecord(holder) : null;
156
+ }
157
+ function acquireBuildLock({ platform, key, root = null, logFile = null }) {
158
+ const path = buildLockPath(platform, key);
159
+ const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
160
+ root: path,
161
+ mode: "exclusive",
162
+ label: label(platform),
163
+ details: {
164
+ projectRoot: root,
165
+ logFile
166
+ }
167
+ });
168
+ const reaped = attempt.reaped[0];
169
+ if (attempt.acquired) {
170
+ const handle = {
171
+ acquired: true,
172
+ path,
173
+ lock: {
174
+ pid: attempt.acquired.owner.pid,
175
+ projectRoot: root,
176
+ startedAt: attempt.acquired.startedAt,
177
+ logFile
178
+ },
179
+ claim: attempt.acquired
180
+ };
181
+ declareSpawnsOn(ambientContext(), attempt.acquired);
182
+ return reaped ? {
183
+ ...handle,
184
+ tookOver: toRecord(reaped)
185
+ } : handle;
186
+ }
187
+ if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
188
+ const holder = attempt.held ?? attempt.waitingFor?.[0];
189
+ return holder ? {
190
+ held: toRecord(holder),
191
+ path
192
+ } : { path };
193
+ }
194
+ function releaseBuildLock(handle) {
195
+ stopDeclaringSpawnsOn(ambientContext(), handle?.claim);
196
+ return (0, ownership_claim_exports.releaseClaim)(handle?.claim);
197
+ }
198
+ function listBuildLocks() {
199
+ const dir = buildLocksDir();
200
+ let names;
201
+ try {
202
+ names = readdirSync(dir);
203
+ } catch {
204
+ return [];
205
+ }
206
+ const locks = [];
207
+ for (const name of names) {
208
+ if (!name.endsWith(LOCK_SUFFIX$1)) continue;
209
+ const path = join(dir, name);
210
+ try {
211
+ if (!statSync(path).isDirectory()) continue;
212
+ } catch {
213
+ continue;
214
+ }
215
+ const stem = name.slice(0, -5);
216
+ const cut = stem.indexOf("-");
217
+ const survey = (0, ownership_claim_exports.readClaimSet)(path);
218
+ const holder = survey.live[0] ?? survey.dead[0];
219
+ const info = holder ? toRecord(holder) : null;
220
+ locks.push({
221
+ path,
222
+ name,
223
+ platform: cut > 0 ? stem.slice(0, cut) : stem,
224
+ key: cut > 0 ? stem.slice(cut + 1) : null,
225
+ pid: info?.pid ?? null,
226
+ projectRoot: info?.projectRoot ?? null,
227
+ startedAt: info?.startedAt ?? null,
228
+ logFile: info?.logFile ?? null,
229
+ alive: survey.live.length > 0,
230
+ unresolved: survey.unresolved.length > 0
231
+ });
232
+ }
233
+ return locks;
234
+ }
235
+ function waitingLine({ projectRoot, pid, elapsedMs, logFile }) {
236
+ const where = projectRoot || "another workspace";
237
+ const tail = logFile ? ` -- tail ${logFile}` : "";
238
+ return `${"build".padEnd(11)} waiting on ${where} (pid ${pid ?? "?"}, ${formatElapsed(elapsedMs)} elapsed)${tail} -- stim guide lifecycle concurrency`;
239
+ }
240
+ function takeoverLine({ projectRoot, pid, logFile, startedAt = null, now = Date.now }) {
241
+ const where = projectRoot || "another workspace";
242
+ const started = startedAt ? Date.parse(startedAt) : NaN;
243
+ const ago = Number.isFinite(started) ? ` (it started ${formatElapsed(now() - started)} ago)` : "";
244
+ const tail = logFile ? ` -- read ${logFile} before this one finishes` : "";
245
+ 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}`;
246
+ }
247
+ 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 }) {
248
+ const path = buildLockPath(platform, key);
249
+ const started = now();
250
+ let lastProgress = started;
251
+ let holder = readBuildLock(path);
252
+ for (;;) {
253
+ const hit = resolve(platform, key);
254
+ if (hit) return {
255
+ hit,
256
+ waitedMs: now() - started
257
+ };
258
+ const survey = (0, ownership_claim_exports.readClaimSet)(path);
259
+ const unresolved = survey.unresolved[0];
260
+ if (unresolved) throw new ownership_claim_exports.ClaimRefusedError({
261
+ claimPath: unresolved.path,
262
+ root: path,
263
+ label: label(platform),
264
+ reason: unresolved.reason
265
+ });
266
+ const live = survey.live[0];
267
+ if (live) holder = toRecord(live);
268
+ else {
269
+ const dead = survey.dead[0];
270
+ if (dead) return {
271
+ builderFailed: `the builder (pid ${dead.owner.pid}) is gone`,
272
+ holder: toRecord(dead),
273
+ waitedMs: now() - started
274
+ };
275
+ return {
276
+ lockReleased: true,
277
+ holder,
278
+ waitedMs: now() - started
279
+ };
280
+ }
281
+ const elapsed = now() - started;
282
+ if (elapsed >= ceilingMs) {
283
+ 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)}`);
284
+ err.code = "STIM_BUILD_WAIT_TIMEOUT";
285
+ err.lockPath = path;
286
+ err.holder = holder ?? void 0;
287
+ throw err;
288
+ }
289
+ if (now() - lastProgress >= progressMs) {
290
+ lastProgress = now();
291
+ out(waitingLine({
292
+ projectRoot: holder.projectRoot,
293
+ pid: holder.pid,
294
+ elapsedMs: elapsed,
295
+ logFile: holder.logFile
296
+ }));
297
+ }
298
+ await sleep(intervalMs);
299
+ }
300
+ }
301
+ async function waitForSharedBuild({ platform, key, fingerprint, root, logFile, command, acquire, wait, now, phase, waitingOn, warn, out }) {
302
+ const waitStarted = now();
303
+ let failedHolder;
304
+ let released = null;
305
+ for (;;) {
306
+ let attempt = null;
307
+ try {
308
+ attempt = acquire({
309
+ platform,
310
+ key,
311
+ root,
312
+ logFile
313
+ });
314
+ } catch (err) {
315
+ const refusal = (0, ownership_claim_exports.claimFailure)(err, command);
316
+ if (refusal) return { refusal };
317
+ warn(`could not take the build lock: ${err?.message || err}; building anyway`);
318
+ }
319
+ if (attempt?.acquired) {
320
+ const previous = attempt.tookOver ?? failedHolder;
321
+ if (previous) warn(takeoverLine(previous));
322
+ waitingOn(null);
323
+ return {
324
+ refusal: null,
325
+ lock: attempt,
326
+ hit: null,
327
+ released
328
+ };
329
+ }
330
+ if (!attempt?.held) {
331
+ waitingOn(null);
332
+ return {
333
+ refusal: null,
334
+ lock: null,
335
+ hit: null,
336
+ released
337
+ };
338
+ }
339
+ released = null;
340
+ const holder = attempt.held;
341
+ const who = holder.projectRoot || "another workspace";
342
+ phase(`${who} is already building ${shortHash(fingerprint)} (pid ${holder.pid})${holder.logFile ? ` -- tail ${holder.logFile}` : ""} -- stim guide lifecycle concurrency`);
343
+ waitingOn(holder.projectRoot || null);
344
+ let waited;
345
+ try {
346
+ const ceilingMs = WAIT_CEILING_MS - (now() - waitStarted);
347
+ 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}.`), {
348
+ code: "STIM_BUILD_WAIT_TIMEOUT",
349
+ lockPath: attempt.path
350
+ });
351
+ waited = await wait({
352
+ platform,
353
+ key,
354
+ out,
355
+ ceilingMs
356
+ });
357
+ } catch (err) {
358
+ const refusal = (0, ownership_claim_exports.claimFailure)(err, command);
359
+ if (refusal) return { refusal };
360
+ const timeout = err;
361
+ if (timeout?.code !== "STIM_BUILD_WAIT_TIMEOUT") throw err;
362
+ return { refusal: {
363
+ code: "STIM_BUILD_WAIT_TIMEOUT",
364
+ message: timeout.message,
365
+ remedy: `Check pid ${holder.pid}; if it is not really building, remove ${timeout.lockPath} and run \`${command}\` again.`
366
+ } };
367
+ }
368
+ if (waited.hit) {
369
+ waitingOn(null);
370
+ phase(`waited ${formatDuration(waited.waitedMs)} for ${who}'s build -> installed from cache -- stim guide lifecycle concurrency`);
371
+ return {
372
+ refusal: null,
373
+ lock: null,
374
+ hit: {
375
+ path: waited.hit,
376
+ waited: {
377
+ pid: holder.pid,
378
+ ms: waited.waitedMs
379
+ }
380
+ },
381
+ released
382
+ };
383
+ }
384
+ if (waited.lockReleased) {
385
+ failedHolder = void 0;
386
+ released = {
387
+ facts: {
388
+ pid: holder.pid,
389
+ ms: waited.waitedMs
390
+ },
391
+ who
392
+ };
393
+ phase(`${who}'s build lock was released; rechecking this workspace before building`);
394
+ } else {
395
+ warn(`${who}'s build ended without an artifact (${waited.builderFailed}); retrying the build lock`);
396
+ failedHolder = holder;
397
+ }
398
+ }
399
+ }
400
+ const DEFAULT_LEASE_MS = 3e5;
401
+ const LEASE_SUFFIX = ".json";
402
+ const LOCK_SUFFIX = ".lock";
403
+ const DURATION = /^[1-9][0-9]*(s|m)$/;
404
+ function deviceLocksDir() {
405
+ return join(getConfigDir(), "device-locks");
406
+ }
407
+ function leaseStem(platform, id) {
408
+ return `${segment(platform)}-${segment(id)}`;
409
+ }
410
+ function deviceLeasePath(platform, id) {
411
+ return join(deviceLocksDir(), `${leaseStem(platform, id)}${LEASE_SUFFIX}`);
412
+ }
413
+ function deviceLeaseLockPath(platform, id) {
414
+ return join(deviceLocksDir(), `${leaseStem(platform, id)}${LOCK_SUFFIX}`);
415
+ }
416
+ const fileLeaseIo = {
417
+ withHolderLock: withWorkspaceStateLock,
418
+ now: () => Date.now(),
419
+ readLease(path) {
420
+ try {
421
+ return readFileSync(path, "utf-8");
422
+ } catch {
423
+ return null;
424
+ }
425
+ },
426
+ writeLease(path, body) {
427
+ mkdirSync(deviceLocksDir(), { recursive: true });
428
+ const tmp = `${path}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
429
+ writeFileSync(tmp, body);
430
+ try {
431
+ renameSync(tmp, path);
432
+ } catch (error) {
433
+ rmSync(tmp, { force: true });
434
+ throw error;
435
+ }
436
+ },
437
+ removeLease(path) {
438
+ rmSync(path, { force: true });
439
+ },
440
+ listLeaseNames() {
441
+ try {
442
+ return readdirSync(deviceLocksDir());
443
+ } catch {
444
+ return [];
445
+ }
446
+ },
447
+ withLeaseLock(lockPath, fn) {
448
+ return withDirLock$1(lockPath, fn, { ensureParent: () => mkdirSync(deviceLocksDir(), { recursive: true }) });
449
+ },
450
+ readHolder(root) {
451
+ return parseWorkspaceLeases(readWorkspaceState$1(root)?.deviceLeases);
452
+ },
453
+ writeHolder(root, leases) {
454
+ if (Object.keys(leases).length === 0) clearWorkspaceStateKeys(root, ["deviceLeases"]);
455
+ else writeWorkspaceState(root, { deviceLeases: leases });
456
+ }
457
+ };
458
+ function text(value) {
459
+ return typeof value === "string" && value.trim() !== "" ? value : null;
460
+ }
461
+ function isPlatform(value) {
462
+ return value === "ios" || value === "android";
463
+ }
464
+ function parseLease(raw) {
465
+ if (typeof raw !== "string") return null;
466
+ let parsed;
467
+ try {
468
+ parsed = JSON.parse(raw);
469
+ } catch {
470
+ return null;
471
+ }
472
+ if (!isJsonObject(parsed)) return null;
473
+ const entry = parsed;
474
+ if (entry.version !== 1 || !isPlatform(entry.platform)) return null;
475
+ const id = text(entry.id);
476
+ const holder = text(entry.holder);
477
+ const token = text(entry.token);
478
+ const expiresAt = text(entry.expiresAt);
479
+ if (!id || !holder || !token || !expiresAt || !Number.isFinite(Date.parse(expiresAt))) return null;
480
+ if (entry.slot !== void 0) {
481
+ if (typeof entry.slot !== "string") return null;
482
+ try {
483
+ validateDeviceSlot(entry.slot);
484
+ } catch {
485
+ return null;
486
+ }
487
+ }
488
+ return {
489
+ version: 1,
490
+ ...typeof entry.slot === "string" ? { slot: entry.slot } : {},
491
+ platform: entry.platform,
492
+ id,
493
+ deviceName: text(entry.deviceName),
494
+ holder,
495
+ token,
496
+ grantedAt: text(entry.grantedAt),
497
+ expiresAt
498
+ };
499
+ }
500
+ function parseWorkspaceLeases(value) {
501
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
502
+ const leases = {};
503
+ for (const [platform, entry] of Object.entries(value)) {
504
+ if (!parseDeviceSlotKey(platform) || !entry || typeof entry !== "object") continue;
505
+ const record = entry;
506
+ const id = text(record.id);
507
+ const token = text(record.token);
508
+ if (!id || !token) continue;
509
+ leases[platform] = {
510
+ id,
511
+ token,
512
+ kind: record.kind === "run" ? "run" : "declared"
513
+ };
514
+ }
515
+ return leases;
516
+ }
517
+ function leaseIsExpired(lease, now) {
518
+ return Date.parse(lease.expiresAt) <= now;
519
+ }
520
+ function parseLeaseDuration(value) {
521
+ const raw = String(value ?? "").trim();
522
+ const match = DURATION.exec(raw);
523
+ if (!match) return { error: `--for must be a whole number of seconds or minutes, such as 90s or 5m (got "${raw}").` };
524
+ const ms = Number(raw.slice(0, -1)) * (match[1] === "s" ? 1e3 : 6e4);
525
+ if (ms < 1e4 || ms > 18e5) return { error: `--for must be between 10s and 30m (got "${raw}").` };
526
+ return { ms };
527
+ }
528
+ function listLeaseFiles(io = fileLeaseIo) {
529
+ const dir = deviceLocksDir();
530
+ const entries = [];
531
+ for (const name of io.listLeaseNames().toSorted()) {
532
+ if (!name.endsWith(LEASE_SUFFIX)) continue;
533
+ const path = join(dir, name);
534
+ const lease = parseLease(io.readLease(path));
535
+ const stem = name.slice(0, -5);
536
+ const cut = stem.indexOf("-");
537
+ entries.push({
538
+ path,
539
+ name,
540
+ platform: lease?.platform ?? (cut > 0 ? stem.slice(0, cut) : stem),
541
+ id: lease?.id ?? (cut > 0 ? stem.slice(cut + 1) : null),
542
+ lease
543
+ });
544
+ }
545
+ return entries;
546
+ }
547
+ function writeLease(lease, io) {
548
+ io.writeLease(deviceLeasePath(lease.platform, lease.id), `${JSON.stringify(lease, null, 2)}\n`);
549
+ return lease;
550
+ }
551
+ function takeLease({ root, platform, slot = "default", id, deviceName = null, kind, durationMs = DEFAULT_LEASE_MS }, io = fileLeaseIo) {
552
+ return withHolderLock(io, root, () => {
553
+ const key = deviceSlotKey(platform, slot);
554
+ const previous = io.readHolder(root)[key];
555
+ if (previous && previous.id !== id) releaseHeld(root, platform, previous, io, slot);
556
+ for (const entry of listLeaseFiles(io)) {
557
+ const other = entry.lease;
558
+ if (!other || other.platform !== platform || other.holder !== root || (other.slot ?? "default") !== slot || other.id === id) continue;
559
+ releaseByRoot(root, platform, other.id, io);
560
+ }
561
+ const declared = previous?.kind === "declared" && previous.id === id;
562
+ const path = deviceLeasePath(platform, id);
563
+ const result = io.withLeaseLock(deviceLeaseLockPath(platform, id), () => {
564
+ const now = io.now();
565
+ const raw = io.readLease(path);
566
+ const current = parseLease(raw);
567
+ if (raw !== null && !current) return {
568
+ status: "unreadable",
569
+ path
570
+ };
571
+ const asked = now + durationMs;
572
+ if (current && !leaseIsExpired(current, now)) {
573
+ if (current.holder !== root || (current.slot ?? "default") !== slot) return {
574
+ status: "held",
575
+ lease: current
576
+ };
577
+ const floor = declared && kind === "run" ? Math.max(asked, Date.parse(current.expiresAt)) : asked;
578
+ return {
579
+ status: "set",
580
+ lease: writeLease({
581
+ ...current,
582
+ deviceName: deviceName ?? current.deviceName,
583
+ expiresAt: new Date(floor).toISOString()
584
+ }, io)
585
+ };
586
+ }
587
+ const expiresAt = new Date(asked).toISOString();
588
+ return {
589
+ status: "taken",
590
+ lease: writeLease({
591
+ version: 1,
592
+ ...slot === "default" ? {} : { slot },
593
+ platform,
594
+ id,
595
+ deviceName,
596
+ holder: root,
597
+ token: randomBytes(12).toString("hex"),
598
+ grantedAt: new Date(now).toISOString(),
599
+ expiresAt
600
+ }, io)
601
+ };
602
+ });
603
+ if (result.status === "taken" || result.status === "set") {
604
+ const recorded = result.status === "set" && declared ? "declared" : kind;
605
+ io.writeHolder(root, {
606
+ ...io.readHolder(root),
607
+ [key]: {
608
+ id,
609
+ token: result.lease.token,
610
+ kind: recorded
611
+ }
612
+ });
613
+ }
614
+ return result;
615
+ });
616
+ }
617
+ function raiseLease({ root, platform, slot = "default", minMs }, io = fileLeaseIo) {
618
+ const record = io.readHolder(root)[deviceSlotKey(platform, slot)];
619
+ if (!record) return { status: "none" };
620
+ const path = deviceLeasePath(platform, record.id);
621
+ return io.withLeaseLock(deviceLeaseLockPath(platform, record.id), () => {
622
+ const current = parseLease(io.readLease(path));
623
+ if (!current || current.token !== record.token) return {
624
+ status: "lost",
625
+ lease: current
626
+ };
627
+ const floor = io.now() + minMs;
628
+ if (Date.parse(current.expiresAt) >= floor) return {
629
+ status: "raised",
630
+ lease: current
631
+ };
632
+ return {
633
+ status: "raised",
634
+ lease: writeLease({
635
+ ...current,
636
+ expiresAt: new Date(floor).toISOString()
637
+ }, io)
638
+ };
639
+ });
640
+ }
641
+ function summarize(lease) {
642
+ return {
643
+ platform: lease.platform,
644
+ id: lease.id,
645
+ deviceName: lease.deviceName,
646
+ expiresAt: lease.expiresAt
647
+ };
648
+ }
649
+ function releaseHeld(root, platform, record, io, slot = "default") {
650
+ const key = deviceSlotKey(platform, slot);
651
+ const path = deviceLeasePath(platform, record.id);
652
+ const released = io.withLeaseLock(deviceLeaseLockPath(platform, record.id), () => {
653
+ const current = parseLease(io.readLease(path));
654
+ if (!current || current.token !== record.token) return null;
655
+ io.removeLease(path);
656
+ return current;
657
+ });
658
+ const held = io.readHolder(root);
659
+ if (held[key]?.token === record.token) {
660
+ delete held[key];
661
+ io.writeHolder(root, held);
662
+ }
663
+ return released;
664
+ }
665
+ function releaseByRoot(root, platform, id, io) {
666
+ const path = deviceLeasePath(platform, id);
667
+ return io.withLeaseLock(deviceLeaseLockPath(platform, id), () => {
668
+ const current = parseLease(io.readLease(path));
669
+ if (!current || current.holder !== root) return null;
670
+ io.removeLease(path);
671
+ return current;
672
+ });
673
+ }
674
+ function releaseRunLease({ root, platform, slot = "default" }, io = fileLeaseIo) {
675
+ return withHolderLock(io, root, () => {
676
+ const record = io.readHolder(root)[deviceSlotKey(platform, slot)];
677
+ if (!record || record.kind !== "run") return null;
678
+ const released = releaseHeld(root, platform, record, io, slot);
679
+ return released ? summarize(released) : null;
680
+ });
681
+ }
682
+ function releaseWorkspaceLeases(root, { platform = null, slot = null } = {}, io = fileLeaseIo) {
683
+ return withHolderLock(io, root, () => {
684
+ const released = [];
685
+ for (const [name, record] of Object.entries(io.readHolder(root))) {
686
+ const parsed = parseDeviceSlotKey(name);
687
+ if (!parsed || platform && parsed.platform !== platform || slot !== null && parsed.slot !== slot) continue;
688
+ const lease = releaseHeld(root, parsed.platform, record, io, parsed.slot);
689
+ if (lease) released.push(summarize(lease));
690
+ }
691
+ for (const entry of listLeaseFiles(io)) {
692
+ const lease = entry.lease;
693
+ if (!lease || lease.holder !== root) continue;
694
+ if (platform && lease.platform !== platform) continue;
695
+ if (slot !== null && (lease.slot ?? "default") !== slot) continue;
696
+ const byRoot = releaseByRoot(root, lease.platform, lease.id, io);
697
+ if (byRoot) released.push(summarize(byRoot));
698
+ }
699
+ return released;
700
+ });
701
+ }
702
+ /** Releases workspace leases with asynchronous lock waits and the same ownership checks. */
703
+ async function releaseWorkspaceLeasesAsync(root, options = {}) {
704
+ const released = [];
705
+ const lockOptions = { ensureParent: () => mkdirSync(deviceLocksDir(), { recursive: true }) };
706
+ for (;;) {
707
+ const attempt = {};
708
+ const io = {
709
+ ...fileLeaseIo,
710
+ removeLease(path) {
711
+ const lease = parseLease(fileLeaseIo.readLease(path));
712
+ fileLeaseIo.removeLease(path);
713
+ if (lease) released.push(summarize(lease));
714
+ },
715
+ withLeaseLock(path, fn) {
716
+ try {
717
+ return withDirLock$1(path, fn, {
718
+ ...lockOptions,
719
+ waitMs: 0
720
+ });
721
+ } catch (error) {
722
+ if (error instanceof Error && "code" in error && error.code === "STIM_LOCK_TIMEOUT") attempt.blocked = {
723
+ path,
724
+ error
725
+ };
726
+ throw error;
727
+ }
728
+ }
729
+ };
730
+ await withWorkspaceStateLockAsync(root, () => {
731
+ try {
732
+ releaseWorkspaceLeases(root, options, io);
733
+ } catch (error) {
734
+ if (error !== attempt.blocked?.error) throw error;
735
+ }
736
+ });
737
+ const blocked = attempt.blocked;
738
+ if (!blocked) return released;
739
+ await withDirLockAsync$1(blocked.path, () => void 0, lockOptions);
740
+ }
741
+ }
742
+ function removeExpiredLease(entry, io = fileLeaseIo) {
743
+ const lease = entry.lease;
744
+ if (!lease) return false;
745
+ return io.withLeaseLock(deviceLeaseLockPath(lease.platform, lease.id), () => {
746
+ const current = parseLease(io.readLease(entry.path));
747
+ if (!current || !leaseIsExpired(current, io.now())) return false;
748
+ io.removeLease(entry.path);
749
+ return true;
750
+ });
751
+ }
752
+ function selectPoolDevice({ candidates, leases, held = null, now }) {
753
+ const ordered = candidates.toSorted((a, b) => {
754
+ const left = a.id.toLowerCase();
755
+ const right = b.id.toLowerCase();
756
+ return left < right ? -1 : left > right ? 1 : 0;
757
+ });
758
+ if (held) {
759
+ const mine = ordered.find((candidate) => candidate.id === held);
760
+ return mine ? {
761
+ status: "selected",
762
+ candidate: mine
763
+ } : {
764
+ status: "held-disconnected",
765
+ id: held
766
+ };
767
+ }
768
+ const preferred = ordered.filter((candidate) => !candidate.fallback);
769
+ const pool = preferred.length > 0 ? preferred : ordered;
770
+ if (pool.length === 0) return { status: "none" };
771
+ const byId = new Map(leases.map((lease) => [lease.id, lease]));
772
+ const free = (candidate) => {
773
+ const lease = byId.get(candidate.id);
774
+ return !lease || leaseIsExpired(lease, now);
775
+ };
776
+ const holders = [];
777
+ for (const candidate of pool) {
778
+ if (free(candidate)) return {
779
+ status: "selected",
780
+ candidate
781
+ };
782
+ const lease = byId.get(candidate.id);
783
+ holders.push({
784
+ id: lease.id,
785
+ holder: lease.holder,
786
+ expiresAt: lease.expiresAt
787
+ });
788
+ }
789
+ return {
790
+ status: "busy",
791
+ holders,
792
+ spare: ordered.filter((candidate) => candidate.fallback && free(candidate)).map((candidate) => candidate.id)
793
+ };
794
+ }
795
+ function withHolderLock(io, root, fn) {
796
+ return io.withHolderLock ? io.withHolderLock(root, fn) : fn();
797
+ }
798
+ //#endregion
799
+ //#region src/devices/activity.ts
800
+ const ACTIVE_WINDOW_MS = 6e5;
801
+ const LOG_TAIL_BYTES = 262144;
802
+ function classifyActivity(evidence, now) {
803
+ const last = evidence.recency.reduce((best, entry) => Number.isFinite(entry.at) && (!best || entry.at > best.at) ? entry : best, null);
804
+ const recent = {};
805
+ for (const { basis, at } of evidence.recency) if (Number.isFinite(at) && !(Date.parse(recent[basis] ?? "") >= at)) recent[basis] = new Date(at).toISOString();
806
+ const lastActivityAt = last ? {
807
+ lastActivityAt: new Date(last.at).toISOString(),
808
+ recent
809
+ } : {};
810
+ const [first] = evidence.drivers;
811
+ if (first) {
812
+ const { basis: _basis, ...driver } = first;
813
+ return {
814
+ state: "driven",
815
+ driver,
816
+ ...lastActivityAt,
817
+ basis: [...new Set(evidence.drivers.map((entry) => entry.basis))]
818
+ };
819
+ }
820
+ if (evidence.unknown.length) return {
821
+ state: "unknown",
822
+ ...lastActivityAt,
823
+ basis: [...new Set(evidence.unknown)]
824
+ };
825
+ return {
826
+ state: Boolean(last && now - last.at < ACTIVE_WINDOW_MS) ? "active" : "idle",
827
+ ...lastActivityAt,
828
+ basis: [...new Set(evidence.recency.map((entry) => entry.basis))]
829
+ };
830
+ }
831
+ function idleForMs(activity, now) {
832
+ if (activity?.state !== "idle" || !activity.lastActivityAt) return null;
833
+ const at = Date.parse(activity.lastActivityAt);
834
+ return Number.isFinite(at) ? Math.max(0, now - at) : null;
835
+ }
836
+ function field(entry, pidKey, startKey) {
837
+ const pid = entry[pidKey];
838
+ const startTime = entry[startKey];
839
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || typeof startTime !== "string") return null;
840
+ return {
841
+ pid,
842
+ startTime
843
+ };
844
+ }
845
+ function parseObject(raw) {
846
+ if (raw === null) return null;
847
+ try {
848
+ const parsed = JSON.parse(raw);
849
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
850
+ } catch {
851
+ return null;
852
+ }
853
+ }
854
+ function parseAgentDeviceRecord(kind, path, raw) {
855
+ const entry = parseObject(raw);
856
+ const stem = basename(path, ".json");
857
+ const device = entry?.device && typeof entry.device === "object" ? entry.device : null;
858
+ const recordedId = kind === "claim" ? device?.id : entry?.deviceId;
859
+ const deviceId = typeof recordedId === "string" && recordedId ? recordedId : kind === "runner-lease" ? stem : null;
860
+ const createdAtMs = typeof entry?.createdAtMs === "number" ? entry.createdAtMs : null;
861
+ return {
862
+ path,
863
+ kind,
864
+ deviceId,
865
+ session: kind === "claim" && typeof entry?.session === "string" && entry.session ? entry.session : null,
866
+ workspace: kind === "claim" && typeof entry?.workspace === "string" && entry.workspace ? entry.workspace : null,
867
+ stateDir: kind === "claim" && typeof entry?.stateDir === "string" && entry.stateDir ? entry.stateDir : null,
868
+ deviceName: kind === "claim" && typeof device?.name === "string" ? device.name : null,
869
+ readable: Boolean(entry && deviceId),
870
+ owner: entry ? field(entry, "ownerPid", "ownerStartTime") : null,
871
+ runner: entry && kind === "runner-lease" ? field(entry, "runnerPid", "runnerStartTime") : null,
872
+ claimProtocol: kind === "runner-lease" && entry?.deviceClaimProtocol === 1,
873
+ createdAtMs,
874
+ xctestrunPath: typeof entry?.xctestrunPath === "string" ? entry.xctestrunPath : null
875
+ };
876
+ }
877
+ function processMatches(entry, startOf) {
878
+ const start = startOf(entry.pid);
879
+ if (start.status === "gone") return "dead";
880
+ if (start.status === "unknown") return "unknown";
881
+ const recorded = Date.parse(entry.startTime);
882
+ if (!Number.isFinite(recorded)) return "unknown";
883
+ return Math.floor(start.startedAtMs / 1e3) === Math.floor(recorded / 1e3) ? "live" : "dead";
884
+ }
885
+ function agentDeviceDaemon(stateDir, startOf = process_identity_exports.inspectProcessStart) {
886
+ let raw;
887
+ try {
888
+ raw = readFileSync(join(stateDir, "daemon.json"), "utf8");
889
+ } catch (error) {
890
+ return error.code === "ENOENT" ? "none" : "unknown";
891
+ }
892
+ const entry = parseObject(raw);
893
+ const owner = entry && field(entry, "pid", "processStartTime");
894
+ return owner ? processMatches(owner, startOf) : "unknown";
895
+ }
896
+ function agentDeviceLiveness(record, startOf) {
897
+ const results = (record.kind === "claim" ? [record.owner] : [record.owner, record.runner]).map((entry) => entry ? processMatches(entry, startOf) : "unknown");
898
+ if (results.includes("dead")) return "dead";
899
+ if (!record.readable || results.includes("unknown")) return "unknown";
900
+ return "live";
901
+ }
902
+ const HOST_PROCESS_COLUMNS = "pid=,ppid=,rss=,%cpu=,lstart=,command=";
903
+ function parseProcessTable(output) {
904
+ const rows = [];
905
+ for (const line of output.split("\n")) {
906
+ 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);
907
+ if (!match) continue;
908
+ const at = Date.parse(match[5]);
909
+ rows.push({
910
+ pid: Number(match[1]),
911
+ ppid: Number(match[2]),
912
+ rssKb: Number(match[3]),
913
+ cpuPercent: Number(match[4].replace(",", ".")),
914
+ startedAt: Number.isFinite(at) ? new Date(at).toISOString() : null,
915
+ command: match[6]
916
+ });
917
+ }
918
+ return rows;
919
+ }
920
+ function parseAndroidInstrumentation(output) {
921
+ const found = [];
922
+ for (const line of output.split("\n")) {
923
+ const match = /^\s*(\d+)\s+(.*)$/.exec(line);
924
+ if (!match) continue;
925
+ const tool = instrumentationTool(match[2]);
926
+ if (tool) found.push({
927
+ pid: Number(match[1]),
928
+ tool
929
+ });
930
+ }
931
+ return found;
932
+ }
933
+ function latestDeviceLogAt(lines, target) {
934
+ return latestRecordAt(lines, (record) => {
935
+ if (record.src !== "device" || record.platform !== target.platform || !isDeviceRecord(record)) return false;
936
+ if (target.slot === "default") return record.slot === void 0;
937
+ return record.slot === target.slot && (record.deviceId === void 0 || record.deviceId === target.id);
938
+ });
939
+ }
940
+ function latestBundleRequestAt(lines, platform) {
941
+ return latestRecordAt(lines, (record) => record.event === "bundle_response_started" && record.platform === platform);
942
+ }
943
+ function latestRecordAt(lines, matches) {
944
+ for (let i = lines.length - 1; i >= 0; i--) {
945
+ const record = parseObject(lines[i]);
946
+ if (record && typeof record.ts === "number" && matches(record)) return record.ts;
947
+ }
948
+ return null;
949
+ }
950
+ function isDeviceRecord(record) {
951
+ return !(typeof record.event === "string" && record.event.startsWith("collector_"));
952
+ }
953
+ /** The last lines of the file at `path`, at most its final 256 KiB, or null when it cannot be read. */
954
+ function tailLines(path) {
955
+ let fd;
956
+ try {
957
+ fd = openSync(path, "r");
958
+ const size = fstatSync(fd).size;
959
+ const length = Math.min(size, LOG_TAIL_BYTES);
960
+ const buffer = Buffer.alloc(length);
961
+ readSync(fd, buffer, 0, length, size - length);
962
+ const lines = buffer.toString("utf8").split("\n");
963
+ return length < size ? lines.slice(1) : lines;
964
+ } catch {
965
+ return null;
966
+ } finally {
967
+ if (fd !== void 0) closeSync(fd);
968
+ }
969
+ }
970
+ function envDir(name, env = process.env, cwd = process.cwd()) {
971
+ const value = env[name]?.trim();
972
+ return value ? resolve(cwd, value) : null;
973
+ }
974
+ function agentDeviceDirs(home, env = process.env) {
975
+ const root = join(home, ".agent-device");
976
+ const leaseDirs = [envDir("AGENT_DEVICE_IOS_RUNNER_LEASE_DIR", env) ?? join(root, "apple-runner", "leases"), join(root, "ios-runner", "leases")];
977
+ return [{
978
+ kind: "claim",
979
+ dir: envDir("AGENT_DEVICE_CLAIMS_DIR", env) ?? join(root, "device-claims")
980
+ }, ...[...new Set(leaseDirs)].map((dir) => ({
981
+ kind: "runner-lease",
982
+ dir
983
+ }))];
984
+ }
985
+ /**
986
+ * When an agent-device session last recorded an event: its events file's modification time. agent-device names a
987
+ * session's directory after the session with every character outside `[a-zA-Z0-9._-]` replaced by `_`.
988
+ */
989
+ function agentSessionActedAt(home, session, stateDir, env = process.env) {
990
+ let latest = null;
991
+ for (const root of stateDir ? [stateDir] : agentDeviceStateDirs(home, env)) try {
992
+ const at = statSync(join(root, "sessions", session.replaceAll(/[^a-zA-Z0-9._-]/g, "_"), "events.ndjson")).mtimeMs;
993
+ latest = latest === null ? at : Math.max(latest, at);
994
+ } catch {}
995
+ return latest;
996
+ }
997
+ function readAgentDeviceRecords(home, env = process.env) {
998
+ return agentDeviceDirs(home, env).flatMap(({ kind, dir }) => {
999
+ let names;
1000
+ try {
1001
+ names = readdirSync(dir).filter((name) => name.endsWith(".json"));
1002
+ } catch {
1003
+ return [];
1004
+ }
1005
+ return names.map((name) => {
1006
+ const path = join(dir, name);
1007
+ let raw = null;
1008
+ try {
1009
+ raw = readFileSync(path, "utf8");
1010
+ } catch {}
1011
+ return parseAgentDeviceRecord(kind, path, raw);
1012
+ });
1013
+ });
1014
+ }
1015
+ async function readAgentDeviceRecordsAsync(home, env = process.env) {
1016
+ const records = [];
1017
+ for (const { kind, dir } of agentDeviceDirs(home, env)) {
1018
+ let names;
1019
+ try {
1020
+ names = (await readdir(dir)).filter((name) => name.endsWith(".json"));
1021
+ } catch {
1022
+ continue;
1023
+ }
1024
+ for (const name of names) {
1025
+ const path = join(dir, name);
1026
+ let raw = null;
1027
+ try {
1028
+ raw = await readFile(path, "utf8");
1029
+ } catch {}
1030
+ records.push(parseAgentDeviceRecord(kind, path, raw));
1031
+ }
1032
+ }
1033
+ return records;
1034
+ }
1035
+ function isClaimProtocolRunner(record, row) {
1036
+ if (!record.claimProtocol || record.runner?.pid !== row.pid || row.startedAt === null) return false;
1037
+ const recorded = Date.parse(record.runner.startTime);
1038
+ return Math.floor(recorded / 1e3) === Math.floor(Date.parse(row.startedAt) / 1e3);
1039
+ }
1040
+ function lsofArgs(port) {
1041
+ return [
1042
+ "-nP",
1043
+ `-iTCP:${port}`,
1044
+ "-sTCP:ESTABLISHED",
1045
+ "-Fpn"
1046
+ ];
1047
+ }
1048
+ function lsofNoMatch(error) {
1049
+ const stdout = error.stdout;
1050
+ return error.status === 1 && (stdout === "" || stdout === void 0) ? "" : null;
1051
+ }
1052
+ function createDeviceProcessTables() {
1053
+ const exec = getExecutor();
1054
+ let host;
1055
+ const android = /* @__PURE__ */ new Map();
1056
+ const tcp = /* @__PURE__ */ new Map();
1057
+ return {
1058
+ tcpConnections(port) {
1059
+ if (!tcp.has(port)) {
1060
+ let output;
1061
+ try {
1062
+ output = exec.runFile("lsof", lsofArgs(port), { timeoutMs: 5e3 });
1063
+ } catch (error) {
1064
+ output = lsofNoMatch(error);
1065
+ }
1066
+ tcp.set(port, output);
1067
+ }
1068
+ return tcp.get(port) ?? null;
1069
+ },
1070
+ host() {
1071
+ if (host === void 0) {
1072
+ const output = exec.runFileQuiet("ps", [
1073
+ "-axww",
1074
+ "-o",
1075
+ HOST_PROCESS_COLUMNS
1076
+ ], { timeoutMs: 1e4 });
1077
+ host = output === null ? null : parseProcessTable(output);
1078
+ }
1079
+ return host;
1080
+ },
1081
+ android(serial) {
1082
+ if (!android.has(serial)) android.set(serial, exec.runFileQuiet("adb", [
1083
+ "-s",
1084
+ serial,
1085
+ "shell",
1086
+ "ps",
1087
+ "-A",
1088
+ "-o",
1089
+ "PID,ARGS"
1090
+ ], { timeoutMs: 5e3 }));
1091
+ return android.get(serial) ?? null;
1092
+ }
1093
+ };
1094
+ }
1095
+ async function readHostProcessTableAsync() {
1096
+ const output = await getExecutor().runFileQuietAsync("ps", [
1097
+ "-axww",
1098
+ "-o",
1099
+ HOST_PROCESS_COLUMNS
1100
+ ], { timeoutMs: 1e4 });
1101
+ return output === null ? null : parseProcessTable(output);
1102
+ }
1103
+ /** Reads bounded host/instrumentation children asynchronously, sharing observations across device targets. */
1104
+ function createActivityReaderAsync(options = {}) {
1105
+ const exec = getExecutor();
1106
+ let host;
1107
+ let hostValue = null;
1108
+ const android = /* @__PURE__ */ new Map();
1109
+ const androidValues = /* @__PURE__ */ new Map();
1110
+ const read = createActivityReader({
1111
+ ...options,
1112
+ tables: {
1113
+ ...createDeviceProcessTables(),
1114
+ host: () => hostValue,
1115
+ android: (serial) => androidValues.get(serial) ?? null
1116
+ }
1117
+ });
1118
+ return async (target) => {
1119
+ host ??= readHostProcessTableAsync();
1120
+ hostValue = await host;
1121
+ if (target.platform === "android") {
1122
+ let pending = android.get(target.id);
1123
+ if (!pending) {
1124
+ pending = exec.runFileQuietAsync("adb", [
1125
+ "-s",
1126
+ target.id,
1127
+ "shell",
1128
+ "ps",
1129
+ "-A",
1130
+ "-o",
1131
+ "PID,ARGS"
1132
+ ], { timeoutMs: 5e3 });
1133
+ android.set(target.id, pending);
1134
+ }
1135
+ androidValues.set(target.id, await pending);
1136
+ }
1137
+ return read(target);
1138
+ };
1139
+ }
1140
+ function workspaceDeviceRecency(root) {
1141
+ const recency = [];
1142
+ const usedAt = Date.parse(String(readWorkspaceState$1(root)?.lastUsedAt ?? ""));
1143
+ if (Number.isFinite(usedAt)) recency.push({
1144
+ basis: "workspace-use",
1145
+ at: usedAt
1146
+ });
1147
+ for (const file of ["client.ndjson", "device.ndjson"]) try {
1148
+ recency.push({
1149
+ basis: "device-log",
1150
+ at: statSync(join(workspaceLogsDir$1(root), file)).mtimeMs
1151
+ });
1152
+ } catch {}
1153
+ return recency;
1154
+ }
1155
+ /** The newest workspace use or change to its in-app or native device logs. */
1156
+ function workspaceDeviceLastUseAt(root) {
1157
+ const times = workspaceDeviceRecency(root).map(({ at }) => at).filter(Number.isFinite);
1158
+ return times.length ? Math.max(...times) : NaN;
1159
+ }
1160
+ 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 } = {}) {
1161
+ let agentDevice;
1162
+ let stimLeases;
1163
+ let viewed;
1164
+ const logs = /* @__PURE__ */ new Map();
1165
+ const log = (path) => {
1166
+ if (!logs.has(path)) logs.set(path, tailLines(path));
1167
+ return logs.get(path) ?? null;
1168
+ };
1169
+ return (target) => {
1170
+ const evidence = {
1171
+ drivers: [],
1172
+ unknown: [],
1173
+ recency: []
1174
+ };
1175
+ agentDevice ??= readAgentDeviceRecords(home, env).map((record) => ({
1176
+ record,
1177
+ liveness: agentDeviceLiveness(record, startOf)
1178
+ }));
1179
+ for (const { record, liveness } of agentDevice) {
1180
+ if (record.deviceId !== null && record.deviceId !== target.id) continue;
1181
+ if (record.claimProtocol) continue;
1182
+ const basis = record.kind === "claim" ? "agent-device-claim" : "agent-device-lease";
1183
+ if (liveness === "live") {
1184
+ evidence.drivers.push({
1185
+ basis,
1186
+ tool: "agent-device",
1187
+ pid: record.owner?.pid ?? null,
1188
+ since: record.createdAtMs !== null ? new Date(record.createdAtMs).toISOString() : null
1189
+ });
1190
+ const actedAt = record.session ? agentSessionActedAt(home, record.session, record.stateDir, env) : null;
1191
+ if (actedAt !== null) evidence.recency.push({
1192
+ basis: "agent-action",
1193
+ at: actedAt
1194
+ });
1195
+ } else if (liveness === "unknown") evidence.unknown.push(basis);
1196
+ }
1197
+ try {
1198
+ stimLeases ??= leaseFiles();
1199
+ } catch {
1200
+ stimLeases = [];
1201
+ evidence.unknown.push("device-lock");
1202
+ }
1203
+ for (const entry of stimLeases) {
1204
+ if (entry.platform !== target.platform || entry.id !== target.id) continue;
1205
+ if (!entry.lease) evidence.unknown.push("device-lock");
1206
+ else if (!leaseIsExpired(entry.lease, now)) evidence.drivers.push({
1207
+ basis: "device-lock",
1208
+ tool: "stim device lock",
1209
+ pid: null,
1210
+ since: entry.lease.grantedAt
1211
+ });
1212
+ }
1213
+ viewed ??= viewers();
1214
+ if (viewed.some((device) => device.platform === target.platform && device.id === target.id)) evidence.recency.push({
1215
+ basis: "viewer",
1216
+ at: now
1217
+ });
1218
+ const processes = tables.host();
1219
+ if (processes === null) evidence.unknown.push("driver-process");
1220
+ for (const row of processes ?? []) {
1221
+ if (agentDevice.some(({ record }) => isClaimProtocolRunner(record, row))) continue;
1222
+ const tool = hostDriverTool(row.command, target.platform, target.id);
1223
+ if (tool) evidence.drivers.push({
1224
+ basis: "driver-process",
1225
+ tool,
1226
+ pid: row.pid,
1227
+ since: row.startedAt
1228
+ });
1229
+ }
1230
+ if (target.platform === "android") {
1231
+ const output = tables.android(target.id);
1232
+ if (output === null) evidence.unknown.push("instrumentation");
1233
+ for (const { pid, tool } of parseAndroidInstrumentation(output ?? "")) evidence.drivers.push({
1234
+ basis: "instrumentation",
1235
+ tool,
1236
+ pid,
1237
+ since: null
1238
+ });
1239
+ }
1240
+ if (target.workspace) {
1241
+ const dir = workspaceLogsDir$1(target.workspace);
1242
+ const deviceAt = latestDeviceLogAt(log(join(dir, "device.ndjson")) ?? [], target);
1243
+ if (deviceAt !== null) evidence.recency.push({
1244
+ basis: "device-log",
1245
+ at: deviceAt
1246
+ });
1247
+ const bundleAt = latestBundleRequestAt(log(join(dir, "metro.ndjson")) ?? [], target.platform);
1248
+ if (bundleAt !== null) evidence.recency.push({
1249
+ basis: "metro-bundle",
1250
+ at: bundleAt
1251
+ });
1252
+ const usedAt = Date.parse(String(readWorkspaceState$1(target.workspace)?.lastUsedAt ?? ""));
1253
+ if (Number.isFinite(usedAt)) evidence.recency.push({
1254
+ basis: "workspace-use",
1255
+ at: usedAt
1256
+ });
1257
+ }
1258
+ return classifyActivity(evidence, now);
1259
+ };
1260
+ }
1261
+ function workspaceActivity(workspace, now = Date.now()) {
1262
+ const dir = workspaceLogsDir$1(workspace);
1263
+ const state = readWorkspaceState$1(workspace);
1264
+ const recency = [];
1265
+ const deviceAt = latestRecordAt(tailLines(join(dir, "device.ndjson")) ?? [], (record) => record.src === "device" && isDeviceRecord(record));
1266
+ if (deviceAt !== null) recency.push({
1267
+ basis: "device-log",
1268
+ at: deviceAt
1269
+ });
1270
+ const bundleAt = latestRecordAt(tailLines(join(dir, "metro.ndjson")) ?? [], (record) => record.event === "bundle_response_started");
1271
+ if (bundleAt !== null) recency.push({
1272
+ basis: "metro-bundle",
1273
+ at: bundleAt
1274
+ });
1275
+ for (const [basis, value] of [["workspace-use", state?.lastUsedAt], ["supervisor-start", state?.supervisor?.startedAt]]) {
1276
+ const at = Date.parse(String(value ?? ""));
1277
+ if (Number.isFinite(at)) recency.push({
1278
+ basis,
1279
+ at
1280
+ });
1281
+ }
1282
+ return classifyActivity({
1283
+ drivers: [],
1284
+ unknown: [],
1285
+ recency
1286
+ }, now);
1287
+ }
1288
+ /**
1289
+ * The pids of processes connected to loopback `port` as clients, from `lsof -Fpn` output: a connection whose remote
1290
+ * end is the port. Chrome's own accepted sockets have the port as their local end and are skipped.
1291
+ */
1292
+ function parseTcpClients(output, port) {
1293
+ const pids = /* @__PURE__ */ new Set();
1294
+ let pid = null;
1295
+ for (const line of output.split("\n")) if (line.startsWith("p")) pid = Number(line.slice(1));
1296
+ else if (line.startsWith("n") && pid !== null && Number.isInteger(pid)) {
1297
+ const remote = line.slice(1).split("->")[1];
1298
+ if (remote && Number(remote.slice(remote.lastIndexOf(":") + 1)) === port) pids.add(pid);
1299
+ }
1300
+ return [...pids];
1301
+ }
1302
+ function webClientPids(connections, target) {
1303
+ return parseTcpClients(connections, target.port).filter((pid) => !target.ownPids.includes(pid));
1304
+ }
1305
+ function webDrivers(clients, processes) {
1306
+ return clients.flatMap((pid) => {
1307
+ const row = processes.find((candidate) => candidate.pid === pid);
1308
+ const tool = row ? cdpClientTool(row.command) : "unknown DevTools client";
1309
+ return tool ? [{
1310
+ tool,
1311
+ pid,
1312
+ since: row?.startedAt ?? null
1313
+ }] : [];
1314
+ });
1315
+ }
1316
+ /**
1317
+ * The DevTools clients of the owned browser that are not Stim's, read without blocking the event loop; null when
1318
+ * lsof or ps could not be read.
1319
+ */
1320
+ async function probeWebDrivers(target) {
1321
+ const exec = getExecutor();
1322
+ let connections;
1323
+ try {
1324
+ connections = await exec.runFileAsync("lsof", lsofArgs(target.port), { timeoutMs: 5e3 });
1325
+ } catch (error) {
1326
+ connections = lsofNoMatch(error);
1327
+ }
1328
+ if (connections === null) return null;
1329
+ const clients = webClientPids(connections, target);
1330
+ if (!clients.length) return [];
1331
+ let table;
1332
+ try {
1333
+ table = await exec.runFileAsync("ps", [
1334
+ "-axww",
1335
+ "-o",
1336
+ HOST_PROCESS_COLUMNS
1337
+ ], { timeoutMs: 5e3 });
1338
+ } catch {
1339
+ return null;
1340
+ }
1341
+ return webDrivers(clients, parseProcessTable(table));
1342
+ }
1343
+ /**
1344
+ * The owned browser's activity: a DevTools client other than Stim's is a driver (basis `cdp-client`). Recency comes
1345
+ * from the newest agent action the browser supervisor recorded (basis `agent-action`) and the newest other page
1346
+ * record (basis `page-log`).
1347
+ */
1348
+ function readWebActivity(target, tables, now = Date.now()) {
1349
+ const evidence = {
1350
+ drivers: [],
1351
+ unknown: [],
1352
+ recency: []
1353
+ };
1354
+ const connections = tables.tcpConnections(target.port);
1355
+ const clients = connections === null ? [] : webClientPids(connections, target);
1356
+ const processes = clients.length ? (tables.clients ?? tables.host)() : [];
1357
+ if (connections === null || processes === null) evidence.unknown.push("cdp-client");
1358
+ else for (const driver of webDrivers(clients, processes)) evidence.drivers.push({
1359
+ ...driver,
1360
+ basis: "cdp-client"
1361
+ });
1362
+ const lines = tailLines(join(workspaceLogsDir$1(target.workspace), "web.ndjson")) ?? [];
1363
+ const actionAt = latestRecordAt(lines, (record) => record.src === "agent");
1364
+ if (actionAt !== null) evidence.recency.push({
1365
+ basis: "agent-action",
1366
+ at: actionAt
1367
+ });
1368
+ const pageAt = latestRecordAt(lines, (record) => record.src !== "agent");
1369
+ if (pageAt !== null) evidence.recency.push({
1370
+ basis: "page-log",
1371
+ at: pageAt
1372
+ });
1373
+ return classifyActivity(evidence, now);
1374
+ }
1375
+ //#endregion
1376
+ 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 };