execbro 0.0.1-reserve → 1.10.13

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 (475) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +496 -0
  3. package/build/__tests__/helpers/fake-cdp-server.d.ts +56 -0
  4. package/build/__tests__/helpers/fake-cdp-server.d.ts.map +1 -0
  5. package/build/__tests__/helpers/fake-cdp-server.js +120 -0
  6. package/build/__tests__/helpers/fake-cdp-server.js.map +1 -0
  7. package/build/__tests__/helpers/tap-test-helpers.d.ts +36 -0
  8. package/build/__tests__/helpers/tap-test-helpers.d.ts.map +1 -0
  9. package/build/__tests__/helpers/tap-test-helpers.js +130 -0
  10. package/build/__tests__/helpers/tap-test-helpers.js.map +1 -0
  11. package/build/__tests__/integration/auto-reconnect.test.d.ts +2 -0
  12. package/build/__tests__/integration/auto-reconnect.test.d.ts.map +1 -0
  13. package/build/__tests__/integration/auto-reconnect.test.js +118 -0
  14. package/build/__tests__/integration/auto-reconnect.test.js.map +1 -0
  15. package/build/__tests__/integration/connection-health.test.d.ts +2 -0
  16. package/build/__tests__/integration/connection-health.test.d.ts.map +1 -0
  17. package/build/__tests__/integration/connection-health.test.js +157 -0
  18. package/build/__tests__/integration/connection-health.test.js.map +1 -0
  19. package/build/__tests__/integration/execute-in-app.test.d.ts +2 -0
  20. package/build/__tests__/integration/execute-in-app.test.d.ts.map +1 -0
  21. package/build/__tests__/integration/execute-in-app.test.js +124 -0
  22. package/build/__tests__/integration/execute-in-app.test.js.map +1 -0
  23. package/build/__tests__/integration/per-call-timeout.test.d.ts +2 -0
  24. package/build/__tests__/integration/per-call-timeout.test.d.ts.map +1 -0
  25. package/build/__tests__/integration/per-call-timeout.test.js +103 -0
  26. package/build/__tests__/integration/per-call-timeout.test.js.map +1 -0
  27. package/build/__tests__/integration/tap-strategies.test.d.ts +2 -0
  28. package/build/__tests__/integration/tap-strategies.test.d.ts.map +1 -0
  29. package/build/__tests__/integration/tap-strategies.test.js +330 -0
  30. package/build/__tests__/integration/tap-strategies.test.js.map +1 -0
  31. package/build/__tests__/integration/tools.test.d.ts +2 -0
  32. package/build/__tests__/integration/tools.test.d.ts.map +1 -0
  33. package/build/__tests__/integration/tools.test.js +227 -0
  34. package/build/__tests__/integration/tools.test.js.map +1 -0
  35. package/build/__tests__/setup.d.ts +2 -0
  36. package/build/__tests__/setup.d.ts.map +1 -0
  37. package/build/__tests__/setup.js +15 -0
  38. package/build/__tests__/setup.js.map +1 -0
  39. package/build/__tests__/unit/androidEmulatorAvds.test.d.ts +2 -0
  40. package/build/__tests__/unit/androidEmulatorAvds.test.d.ts.map +1 -0
  41. package/build/__tests__/unit/androidEmulatorAvds.test.js +98 -0
  42. package/build/__tests__/unit/androidEmulatorAvds.test.js.map +1 -0
  43. package/build/__tests__/unit/buildToken.test.d.ts +2 -0
  44. package/build/__tests__/unit/buildToken.test.d.ts.map +1 -0
  45. package/build/__tests__/unit/buildToken.test.js +22 -0
  46. package/build/__tests__/unit/buildToken.test.js.map +1 -0
  47. package/build/__tests__/unit/bundle.test.d.ts +2 -0
  48. package/build/__tests__/unit/bundle.test.d.ts.map +1 -0
  49. package/build/__tests__/unit/bundle.test.js +53 -0
  50. package/build/__tests__/unit/bundle.test.js.map +1 -0
  51. package/build/__tests__/unit/burst.test.d.ts +2 -0
  52. package/build/__tests__/unit/burst.test.d.ts.map +1 -0
  53. package/build/__tests__/unit/burst.test.js +54 -0
  54. package/build/__tests__/unit/burst.test.js.map +1 -0
  55. package/build/__tests__/unit/connection-health.test.d.ts +2 -0
  56. package/build/__tests__/unit/connection-health.test.d.ts.map +1 -0
  57. package/build/__tests__/unit/connection-health.test.js +38 -0
  58. package/build/__tests__/unit/connection-health.test.js.map +1 -0
  59. package/build/__tests__/unit/debugGlobals.test.d.ts +2 -0
  60. package/build/__tests__/unit/debugGlobals.test.d.ts.map +1 -0
  61. package/build/__tests__/unit/debugGlobals.test.js +14 -0
  62. package/build/__tests__/unit/debugGlobals.test.js.map +1 -0
  63. package/build/__tests__/unit/deviceDiscovery.test.d.ts +2 -0
  64. package/build/__tests__/unit/deviceDiscovery.test.d.ts.map +1 -0
  65. package/build/__tests__/unit/deviceDiscovery.test.js +119 -0
  66. package/build/__tests__/unit/deviceDiscovery.test.js.map +1 -0
  67. package/build/__tests__/unit/deviceResolver.test.d.ts +2 -0
  68. package/build/__tests__/unit/deviceResolver.test.d.ts.map +1 -0
  69. package/build/__tests__/unit/deviceResolver.test.js +261 -0
  70. package/build/__tests__/unit/deviceResolver.test.js.map +1 -0
  71. package/build/__tests__/unit/escapeNonAscii.test.d.ts +2 -0
  72. package/build/__tests__/unit/escapeNonAscii.test.d.ts.map +1 -0
  73. package/build/__tests__/unit/escapeNonAscii.test.js +76 -0
  74. package/build/__tests__/unit/escapeNonAscii.test.js.map +1 -0
  75. package/build/__tests__/unit/exec.test.d.ts +2 -0
  76. package/build/__tests__/unit/exec.test.d.ts.map +1 -0
  77. package/build/__tests__/unit/exec.test.js +54 -0
  78. package/build/__tests__/unit/exec.test.js.map +1 -0
  79. package/build/__tests__/unit/executor.test.d.ts +2 -0
  80. package/build/__tests__/unit/executor.test.d.ts.map +1 -0
  81. package/build/__tests__/unit/executor.test.js +220 -0
  82. package/build/__tests__/unit/executor.test.js.map +1 -0
  83. package/build/__tests__/unit/failureArtifact.test.d.ts +2 -0
  84. package/build/__tests__/unit/failureArtifact.test.d.ts.map +1 -0
  85. package/build/__tests__/unit/failureArtifact.test.js +207 -0
  86. package/build/__tests__/unit/failureArtifact.test.js.map +1 -0
  87. package/build/__tests__/unit/fastRefreshRecorder.test.d.ts +2 -0
  88. package/build/__tests__/unit/fastRefreshRecorder.test.d.ts.map +1 -0
  89. package/build/__tests__/unit/fastRefreshRecorder.test.js +68 -0
  90. package/build/__tests__/unit/fastRefreshRecorder.test.js.map +1 -0
  91. package/build/__tests__/unit/fastRefreshTools.test.d.ts +2 -0
  92. package/build/__tests__/unit/fastRefreshTools.test.d.ts.map +1 -0
  93. package/build/__tests__/unit/fastRefreshTools.test.js +112 -0
  94. package/build/__tests__/unit/fastRefreshTools.test.js.map +1 -0
  95. package/build/__tests__/unit/fiberMatch.test.d.ts +2 -0
  96. package/build/__tests__/unit/fiberMatch.test.d.ts.map +1 -0
  97. package/build/__tests__/unit/fiberMatch.test.js +114 -0
  98. package/build/__tests__/unit/fiberMatch.test.js.map +1 -0
  99. package/build/__tests__/unit/fingerprint.test.d.ts +2 -0
  100. package/build/__tests__/unit/fingerprint.test.d.ts.map +1 -0
  101. package/build/__tests__/unit/fingerprint.test.js +99 -0
  102. package/build/__tests__/unit/fingerprint.test.js.map +1 -0
  103. package/build/__tests__/unit/focusedInput.test.d.ts +2 -0
  104. package/build/__tests__/unit/focusedInput.test.d.ts.map +1 -0
  105. package/build/__tests__/unit/focusedInput.test.js +58 -0
  106. package/build/__tests__/unit/focusedInput.test.js.map +1 -0
  107. package/build/__tests__/unit/focusedInputTools.test.d.ts +2 -0
  108. package/build/__tests__/unit/focusedInputTools.test.d.ts.map +1 -0
  109. package/build/__tests__/unit/focusedInputTools.test.js +79 -0
  110. package/build/__tests__/unit/focusedInputTools.test.js.map +1 -0
  111. package/build/__tests__/unit/iconSemantics.test.d.ts +2 -0
  112. package/build/__tests__/unit/iconSemantics.test.d.ts.map +1 -0
  113. package/build/__tests__/unit/iconSemantics.test.js +80 -0
  114. package/build/__tests__/unit/iconSemantics.test.js.map +1 -0
  115. package/build/__tests__/unit/imageBuffer.test.d.ts +2 -0
  116. package/build/__tests__/unit/imageBuffer.test.d.ts.map +1 -0
  117. package/build/__tests__/unit/imageBuffer.test.js +128 -0
  118. package/build/__tests__/unit/imageBuffer.test.js.map +1 -0
  119. package/build/__tests__/unit/logs.test.d.ts +2 -0
  120. package/build/__tests__/unit/logs.test.d.ts.map +1 -0
  121. package/build/__tests__/unit/logs.test.js +90 -0
  122. package/build/__tests__/unit/logs.test.js.map +1 -0
  123. package/build/__tests__/unit/measureComponent.test.d.ts +2 -0
  124. package/build/__tests__/unit/measureComponent.test.d.ts.map +1 -0
  125. package/build/__tests__/unit/measureComponent.test.js +66 -0
  126. package/build/__tests__/unit/measureComponent.test.js.map +1 -0
  127. package/build/__tests__/unit/measureComponentTools.test.d.ts +2 -0
  128. package/build/__tests__/unit/measureComponentTools.test.d.ts.map +1 -0
  129. package/build/__tests__/unit/measureComponentTools.test.js +94 -0
  130. package/build/__tests__/unit/measureComponentTools.test.js.map +1 -0
  131. package/build/__tests__/unit/metro.test.d.ts +2 -0
  132. package/build/__tests__/unit/metro.test.d.ts.map +1 -0
  133. package/build/__tests__/unit/metro.test.js +61 -0
  134. package/build/__tests__/unit/metro.test.js.map +1 -0
  135. package/build/__tests__/unit/network.test.d.ts +2 -0
  136. package/build/__tests__/unit/network.test.d.ts.map +1 -0
  137. package/build/__tests__/unit/network.test.js +102 -0
  138. package/build/__tests__/unit/network.test.js.map +1 -0
  139. package/build/__tests__/unit/networkInterceptor.test.d.ts +2 -0
  140. package/build/__tests__/unit/networkInterceptor.test.d.ts.map +1 -0
  141. package/build/__tests__/unit/networkInterceptor.test.js +104 -0
  142. package/build/__tests__/unit/networkInterceptor.test.js.map +1 -0
  143. package/build/__tests__/unit/probe.test.d.ts +2 -0
  144. package/build/__tests__/unit/probe.test.d.ts.map +1 -0
  145. package/build/__tests__/unit/probe.test.js +62 -0
  146. package/build/__tests__/unit/probe.test.js.map +1 -0
  147. package/build/__tests__/unit/rnGlobalsBootstrap.test.d.ts +2 -0
  148. package/build/__tests__/unit/rnGlobalsBootstrap.test.d.ts.map +1 -0
  149. package/build/__tests__/unit/rnGlobalsBootstrap.test.js +29 -0
  150. package/build/__tests__/unit/rnGlobalsBootstrap.test.js.map +1 -0
  151. package/build/__tests__/unit/screenState.test.d.ts +2 -0
  152. package/build/__tests__/unit/screenState.test.d.ts.map +1 -0
  153. package/build/__tests__/unit/screenState.test.js +384 -0
  154. package/build/__tests__/unit/screenState.test.js.map +1 -0
  155. package/build/__tests__/unit/screenshot-diff.test.d.ts +2 -0
  156. package/build/__tests__/unit/screenshot-diff.test.d.ts.map +1 -0
  157. package/build/__tests__/unit/screenshot-diff.test.js +97 -0
  158. package/build/__tests__/unit/screenshot-diff.test.js.map +1 -0
  159. package/build/__tests__/unit/tap-evidence.test.d.ts +2 -0
  160. package/build/__tests__/unit/tap-evidence.test.d.ts.map +1 -0
  161. package/build/__tests__/unit/tap-evidence.test.js +89 -0
  162. package/build/__tests__/unit/tap-evidence.test.js.map +1 -0
  163. package/build/__tests__/unit/tap.test.d.ts +2 -0
  164. package/build/__tests__/unit/tap.test.d.ts.map +1 -0
  165. package/build/__tests__/unit/tap.test.js +767 -0
  166. package/build/__tests__/unit/tap.test.js.map +1 -0
  167. package/build/__tests__/unit/timeoutClamp.test.d.ts +2 -0
  168. package/build/__tests__/unit/timeoutClamp.test.d.ts.map +1 -0
  169. package/build/__tests__/unit/timeoutClamp.test.js +17 -0
  170. package/build/__tests__/unit/timeoutClamp.test.js.map +1 -0
  171. package/build/__tests__/unit/toolDescriptions.test.d.ts +2 -0
  172. package/build/__tests__/unit/toolDescriptions.test.d.ts.map +1 -0
  173. package/build/__tests__/unit/toolDescriptions.test.js +145 -0
  174. package/build/__tests__/unit/toolDescriptions.test.js.map +1 -0
  175. package/build/__tests__/unit/transportClassify.test.d.ts +2 -0
  176. package/build/__tests__/unit/transportClassify.test.d.ts.map +1 -0
  177. package/build/__tests__/unit/transportClassify.test.js +60 -0
  178. package/build/__tests__/unit/transportClassify.test.js.map +1 -0
  179. package/build/__tests__/unit/verifyAction.test.d.ts +2 -0
  180. package/build/__tests__/unit/verifyAction.test.d.ts.map +1 -0
  181. package/build/__tests__/unit/verifyAction.test.js +26 -0
  182. package/build/__tests__/unit/verifyAction.test.js.map +1 -0
  183. package/build/core/android.d.ts +299 -0
  184. package/build/core/android.d.ts.map +1 -0
  185. package/build/core/android.js +1582 -0
  186. package/build/core/android.js.map +1 -0
  187. package/build/core/appDetection.d.ts +20 -0
  188. package/build/core/appDetection.d.ts.map +1 -0
  189. package/build/core/appDetection.js +167 -0
  190. package/build/core/appDetection.js.map +1 -0
  191. package/build/core/bundle.d.ts +49 -0
  192. package/build/core/bundle.d.ts.map +1 -0
  193. package/build/core/bundle.js +356 -0
  194. package/build/core/bundle.js.map +1 -0
  195. package/build/core/componentSearch.d.ts +41 -0
  196. package/build/core/componentSearch.d.ts.map +1 -0
  197. package/build/core/componentSearch.js +588 -0
  198. package/build/core/componentSearch.js.map +1 -0
  199. package/build/core/componentTree.d.ts +13 -0
  200. package/build/core/componentTree.d.ts.map +1 -0
  201. package/build/core/componentTree.js +275 -0
  202. package/build/core/componentTree.js.map +1 -0
  203. package/build/core/config.d.ts +9 -0
  204. package/build/core/config.d.ts.map +1 -0
  205. package/build/core/config.js +28 -0
  206. package/build/core/config.js.map +1 -0
  207. package/build/core/connection.d.ts +102 -0
  208. package/build/core/connection.d.ts.map +1 -0
  209. package/build/core/connection.js +1710 -0
  210. package/build/core/connection.js.map +1 -0
  211. package/build/core/connectionState.d.ts +122 -0
  212. package/build/core/connectionState.d.ts.map +1 -0
  213. package/build/core/connectionState.js +313 -0
  214. package/build/core/connectionState.js.map +1 -0
  215. package/build/core/debugGlobals.d.ts +6 -0
  216. package/build/core/debugGlobals.d.ts.map +1 -0
  217. package/build/core/debugGlobals.js +370 -0
  218. package/build/core/debugGlobals.js.map +1 -0
  219. package/build/core/deviceDiscovery.d.ts +52 -0
  220. package/build/core/deviceDiscovery.d.ts.map +1 -0
  221. package/build/core/deviceDiscovery.js +74 -0
  222. package/build/core/deviceDiscovery.js.map +1 -0
  223. package/build/core/deviceResolver.d.ts +44 -0
  224. package/build/core/deviceResolver.d.ts.map +1 -0
  225. package/build/core/deviceResolver.js +225 -0
  226. package/build/core/deviceResolver.js.map +1 -0
  227. package/build/core/errorScreenParser.d.ts +30 -0
  228. package/build/core/errorScreenParser.d.ts.map +1 -0
  229. package/build/core/errorScreenParser.js +198 -0
  230. package/build/core/errorScreenParser.js.map +1 -0
  231. package/build/core/errors.d.ts +14 -0
  232. package/build/core/errors.d.ts.map +1 -0
  233. package/build/core/errors.js +17 -0
  234. package/build/core/errors.js.map +1 -0
  235. package/build/core/escapeNonAscii.d.ts +18 -0
  236. package/build/core/escapeNonAscii.d.ts.map +1 -0
  237. package/build/core/escapeNonAscii.js +352 -0
  238. package/build/core/escapeNonAscii.js.map +1 -0
  239. package/build/core/exec.d.ts +22 -0
  240. package/build/core/exec.d.ts.map +1 -0
  241. package/build/core/exec.js +54 -0
  242. package/build/core/exec.js.map +1 -0
  243. package/build/core/executor.d.ts +9 -0
  244. package/build/core/executor.d.ts.map +1 -0
  245. package/build/core/executor.js +12 -0
  246. package/build/core/executor.js.map +1 -0
  247. package/build/core/failureArtifact.d.ts +120 -0
  248. package/build/core/failureArtifact.d.ts.map +1 -0
  249. package/build/core/failureArtifact.js +113 -0
  250. package/build/core/failureArtifact.js.map +1 -0
  251. package/build/core/fastRefreshRecorder.d.ts +23 -0
  252. package/build/core/fastRefreshRecorder.d.ts.map +1 -0
  253. package/build/core/fastRefreshRecorder.js +83 -0
  254. package/build/core/fastRefreshRecorder.js.map +1 -0
  255. package/build/core/fastRefreshTools.d.ts +19 -0
  256. package/build/core/fastRefreshTools.d.ts.map +1 -0
  257. package/build/core/fastRefreshTools.js +71 -0
  258. package/build/core/fastRefreshTools.js.map +1 -0
  259. package/build/core/feedback.d.ts +18 -0
  260. package/build/core/feedback.d.ts.map +1 -0
  261. package/build/core/feedback.js +73 -0
  262. package/build/core/feedback.js.map +1 -0
  263. package/build/core/fiberMatch.d.ts +25 -0
  264. package/build/core/fiberMatch.d.ts.map +1 -0
  265. package/build/core/fiberMatch.js +35 -0
  266. package/build/core/fiberMatch.js.map +1 -0
  267. package/build/core/fingerprint.d.ts +4 -0
  268. package/build/core/fingerprint.d.ts.map +1 -0
  269. package/build/core/fingerprint.js +47 -0
  270. package/build/core/fingerprint.js.map +1 -0
  271. package/build/core/focusedInput.d.ts +27 -0
  272. package/build/core/focusedInput.d.ts.map +1 -0
  273. package/build/core/focusedInput.js +110 -0
  274. package/build/core/focusedInput.js.map +1 -0
  275. package/build/core/focusedInputTools.d.ts +21 -0
  276. package/build/core/focusedInputTools.d.ts.map +1 -0
  277. package/build/core/focusedInputTools.js +51 -0
  278. package/build/core/focusedInputTools.js.map +1 -0
  279. package/build/core/format.d.ts +8 -0
  280. package/build/core/format.d.ts.map +1 -0
  281. package/build/core/format.js +34 -0
  282. package/build/core/format.js.map +1 -0
  283. package/build/core/guides.d.ts +21 -0
  284. package/build/core/guides.d.ts.map +1 -0
  285. package/build/core/guides.js +397 -0
  286. package/build/core/guides.js.map +1 -0
  287. package/build/core/iconSemantics.d.ts +18 -0
  288. package/build/core/iconSemantics.d.ts.map +1 -0
  289. package/build/core/iconSemantics.js +114 -0
  290. package/build/core/iconSemantics.js.map +1 -0
  291. package/build/core/imageBuffer.d.ts +37 -0
  292. package/build/core/imageBuffer.d.ts.map +1 -0
  293. package/build/core/imageBuffer.js +58 -0
  294. package/build/core/imageBuffer.js.map +1 -0
  295. package/build/core/index.d.ts +37 -0
  296. package/build/core/index.d.ts.map +1 -0
  297. package/build/core/index.js +66 -0
  298. package/build/core/index.js.map +1 -0
  299. package/build/core/inspector.d.ts +55 -0
  300. package/build/core/inspector.d.ts.map +1 -0
  301. package/build/core/inspector.js +603 -0
  302. package/build/core/inspector.js.map +1 -0
  303. package/build/core/ios.d.ts +263 -0
  304. package/build/core/ios.d.ts.map +1 -0
  305. package/build/core/ios.js +1705 -0
  306. package/build/core/ios.js.map +1 -0
  307. package/build/core/jsExecute.d.ts +62 -0
  308. package/build/core/jsExecute.d.ts.map +1 -0
  309. package/build/core/jsExecute.js +696 -0
  310. package/build/core/jsExecute.js.map +1 -0
  311. package/build/core/license.d.ts +48 -0
  312. package/build/core/license.d.ts.map +1 -0
  313. package/build/core/license.js +267 -0
  314. package/build/core/license.js.map +1 -0
  315. package/build/core/logbox.d.ts +58 -0
  316. package/build/core/logbox.d.ts.map +1 -0
  317. package/build/core/logbox.js +330 -0
  318. package/build/core/logbox.js.map +1 -0
  319. package/build/core/logs.d.ts +44 -0
  320. package/build/core/logs.d.ts.map +1 -0
  321. package/build/core/logs.js +149 -0
  322. package/build/core/logs.js.map +1 -0
  323. package/build/core/measureComponent.d.ts +19 -0
  324. package/build/core/measureComponent.d.ts.map +1 -0
  325. package/build/core/measureComponent.js +94 -0
  326. package/build/core/measureComponent.js.map +1 -0
  327. package/build/core/measureComponentTools.d.ts +6 -0
  328. package/build/core/measureComponentTools.d.ts.map +1 -0
  329. package/build/core/measureComponentTools.js +43 -0
  330. package/build/core/measureComponentTools.js.map +1 -0
  331. package/build/core/metro.d.ts +31 -0
  332. package/build/core/metro.d.ts.map +1 -0
  333. package/build/core/metro.js +124 -0
  334. package/build/core/metro.js.map +1 -0
  335. package/build/core/nativeOnlyHints.d.ts +14 -0
  336. package/build/core/nativeOnlyHints.d.ts.map +1 -0
  337. package/build/core/nativeOnlyHints.js +58 -0
  338. package/build/core/nativeOnlyHints.js.map +1 -0
  339. package/build/core/network.d.ts +43 -0
  340. package/build/core/network.d.ts.map +1 -0
  341. package/build/core/network.js +217 -0
  342. package/build/core/network.js.map +1 -0
  343. package/build/core/networkInterceptor.d.ts +30 -0
  344. package/build/core/networkInterceptor.d.ts.map +1 -0
  345. package/build/core/networkInterceptor.js +211 -0
  346. package/build/core/networkInterceptor.js.map +1 -0
  347. package/build/core/ocr.d.ts +87 -0
  348. package/build/core/ocr.d.ts.map +1 -0
  349. package/build/core/ocr.js +335 -0
  350. package/build/core/ocr.js.map +1 -0
  351. package/build/core/paths.d.ts +2 -0
  352. package/build/core/paths.d.ts.map +1 -0
  353. package/build/core/paths.js +28 -0
  354. package/build/core/paths.js.map +1 -0
  355. package/build/core/posthog.d.ts +5 -0
  356. package/build/core/posthog.d.ts.map +1 -0
  357. package/build/core/posthog.js +52 -0
  358. package/build/core/posthog.js.map +1 -0
  359. package/build/core/pressables.d.ts +46 -0
  360. package/build/core/pressables.d.ts.map +1 -0
  361. package/build/core/pressables.js +1719 -0
  362. package/build/core/pressables.js.map +1 -0
  363. package/build/core/probe.d.ts +12 -0
  364. package/build/core/probe.d.ts.map +1 -0
  365. package/build/core/probe.js +56 -0
  366. package/build/core/probe.js.map +1 -0
  367. package/build/core/redux.d.ts +22 -0
  368. package/build/core/redux.d.ts.map +1 -0
  369. package/build/core/redux.js +116 -0
  370. package/build/core/redux.js.map +1 -0
  371. package/build/core/register.d.ts +8 -0
  372. package/build/core/register.d.ts.map +1 -0
  373. package/build/core/register.js +202 -0
  374. package/build/core/register.js.map +1 -0
  375. package/build/core/rnGlobalsBootstrap.d.ts +16 -0
  376. package/build/core/rnGlobalsBootstrap.d.ts.map +1 -0
  377. package/build/core/rnGlobalsBootstrap.js +116 -0
  378. package/build/core/rnGlobalsBootstrap.js.map +1 -0
  379. package/build/core/screenLayout.d.ts +53 -0
  380. package/build/core/screenLayout.d.ts.map +1 -0
  381. package/build/core/screenLayout.js +878 -0
  382. package/build/core/screenLayout.js.map +1 -0
  383. package/build/core/screenState.d.ts +166 -0
  384. package/build/core/screenState.d.ts.map +1 -0
  385. package/build/core/screenState.js +1352 -0
  386. package/build/core/screenState.js.map +1 -0
  387. package/build/core/sdkBridge.d.ts +67 -0
  388. package/build/core/sdkBridge.d.ts.map +1 -0
  389. package/build/core/sdkBridge.js +148 -0
  390. package/build/core/sdkBridge.js.map +1 -0
  391. package/build/core/state.d.ts +37 -0
  392. package/build/core/state.d.ts.map +1 -0
  393. package/build/core/state.js +124 -0
  394. package/build/core/state.js.map +1 -0
  395. package/build/core/telemetry.d.ts +50 -0
  396. package/build/core/telemetry.d.ts.map +1 -0
  397. package/build/core/telemetry.js +653 -0
  398. package/build/core/telemetry.js.map +1 -0
  399. package/build/core/toolHelpers.d.ts +24 -0
  400. package/build/core/toolHelpers.d.ts.map +1 -0
  401. package/build/core/toolHelpers.js +118 -0
  402. package/build/core/toolHelpers.js.map +1 -0
  403. package/build/core/types.d.ts +162 -0
  404. package/build/core/types.d.ts.map +1 -0
  405. package/build/core/types.js +2 -0
  406. package/build/core/types.js.map +1 -0
  407. package/build/index.d.ts +4 -0
  408. package/build/index.d.ts.map +1 -0
  409. package/build/index.js +132 -0
  410. package/build/index.js.map +1 -0
  411. package/build/pro/screenshot-diff.d.ts +10 -0
  412. package/build/pro/screenshot-diff.d.ts.map +1 -0
  413. package/build/pro/screenshot-diff.js +38 -0
  414. package/build/pro/screenshot-diff.js.map +1 -0
  415. package/build/pro/tap.d.ts +291 -0
  416. package/build/pro/tap.d.ts.map +1 -0
  417. package/build/pro/tap.js +1839 -0
  418. package/build/pro/tap.js.map +1 -0
  419. package/build/pro/verifyAction.d.ts +50 -0
  420. package/build/pro/verifyAction.d.ts.map +1 -0
  421. package/build/pro/verifyAction.js +285 -0
  422. package/build/pro/verifyAction.js.map +1 -0
  423. package/build/tools/_deviceArg.d.ts +45 -0
  424. package/build/tools/_deviceArg.d.ts.map +1 -0
  425. package/build/tools/_deviceArg.js +58 -0
  426. package/build/tools/_deviceArg.js.map +1 -0
  427. package/build/tools/accountTools.d.ts +46 -0
  428. package/build/tools/accountTools.d.ts.map +1 -0
  429. package/build/tools/accountTools.js +223 -0
  430. package/build/tools/accountTools.js.map +1 -0
  431. package/build/tools/bundleTools.d.ts +3 -0
  432. package/build/tools/bundleTools.d.ts.map +1 -0
  433. package/build/tools/bundleTools.js +375 -0
  434. package/build/tools/bundleTools.js.map +1 -0
  435. package/build/tools/componentTools.d.ts +3 -0
  436. package/build/tools/componentTools.d.ts.map +1 -0
  437. package/build/tools/componentTools.js +746 -0
  438. package/build/tools/componentTools.js.map +1 -0
  439. package/build/tools/connectionTools.d.ts +3 -0
  440. package/build/tools/connectionTools.d.ts.map +1 -0
  441. package/build/tools/connectionTools.js +625 -0
  442. package/build/tools/connectionTools.js.map +1 -0
  443. package/build/tools/deviceTools.d.ts +3 -0
  444. package/build/tools/deviceTools.d.ts.map +1 -0
  445. package/build/tools/deviceTools.js +423 -0
  446. package/build/tools/deviceTools.js.map +1 -0
  447. package/build/tools/executionTools.d.ts +3 -0
  448. package/build/tools/executionTools.d.ts.map +1 -0
  449. package/build/tools/executionTools.js +243 -0
  450. package/build/tools/executionTools.js.map +1 -0
  451. package/build/tools/interactionTools.d.ts +3 -0
  452. package/build/tools/interactionTools.d.ts.map +1 -0
  453. package/build/tools/interactionTools.js +539 -0
  454. package/build/tools/interactionTools.js.map +1 -0
  455. package/build/tools/logTools.d.ts +3 -0
  456. package/build/tools/logTools.d.ts.map +1 -0
  457. package/build/tools/logTools.js +371 -0
  458. package/build/tools/logTools.js.map +1 -0
  459. package/build/tools/metaTools.d.ts +7 -0
  460. package/build/tools/metaTools.d.ts.map +1 -0
  461. package/build/tools/metaTools.js +186 -0
  462. package/build/tools/metaTools.js.map +1 -0
  463. package/build/tools/networkTools.d.ts +3 -0
  464. package/build/tools/networkTools.d.ts.map +1 -0
  465. package/build/tools/networkTools.js +478 -0
  466. package/build/tools/networkTools.js.map +1 -0
  467. package/build/tools/reduxTools.d.ts +3 -0
  468. package/build/tools/reduxTools.d.ts.map +1 -0
  469. package/build/tools/reduxTools.js +83 -0
  470. package/build/tools/reduxTools.js.map +1 -0
  471. package/build/tools/screenshotTools.d.ts +3 -0
  472. package/build/tools/screenshotTools.d.ts.map +1 -0
  473. package/build/tools/screenshotTools.js +703 -0
  474. package/build/tools/screenshotTools.js.map +1 -0
  475. package/package.json +63 -2
@@ -0,0 +1,1710 @@
1
+ import WebSocket from "ws";
2
+ import { connectedApps, pendingExecutions, getNextMessageId, getLogBuffer, getNetworkBuffer, setActiveSimulatorUdid, clearActiveSimulatorIfSource, updateLastCDPMessageTime, getLastCDPMessageTime, clearLastCDPMessageTime } from "./state.js";
3
+ import { mapConsoleType } from "./logs.js";
4
+ import { injectNetworkInterceptor, sendNetworkEnable, isInterceptorEvent, applyInterceptedEvent } from "./networkInterceptor.js";
5
+ import { findSimulatorByName } from "./ios.js";
6
+ import { getAdbIdForAvd } from "./android.js";
7
+ import { fetchDevices, selectMainDevice, scanMetroPorts } from "./metro.js";
8
+ import { probeCdpAlive } from "./probe.js";
9
+ import { UserInputError } from "./errors.js";
10
+ import { scheduleAppDetection } from "./appDetection.js";
11
+ import { markConnectionEstablished } from "./jsExecute.js";
12
+ import { DEFAULT_RECONNECTION_CONFIG, MIN_STABLE_CONNECTION_MS, initConnectionState, updateConnectionState, getConnectionState, recordConnectionGap, closeConnectionGap, saveConnectionMetadata, clearConnectionMetadata, getConnectionMetadata, saveReconnectionTimer, cancelReconnectionTimer, calculateBackoffDelay, initContextHealth, markContextHealthy, markContextStale, getContextHealth, updateContextHealth, formatDuration, recordConnectionEvent, } from "./connectionState.js";
13
+ // Connection locks to prevent concurrent connection attempts to the same device
14
+ const connectionLocks = new Set();
15
+ // Track Network.enable message IDs to detect CDP network support
16
+ const pendingNetworkEnableIds = new Set();
17
+ // Track SDK-probe message IDs so we can decode their responses and set
18
+ // app.sdkPresent. The probe runs on connect and then periodically because
19
+ // the SDK's init() may execute after we attach.
20
+ const pendingSdkProbeIds = new Map(); // msgId -> appKey
21
+ const SDK_PROBE_INITIAL_DELAY_MS = 200;
22
+ const SDK_PROBE_INTERVAL_MS = 3000;
23
+ // Consecutive SDK-absent probes required to flip sdkPresent true→false.
24
+ // Hysteresis against the post-reload window where the JS context exists but
25
+ // the SDK's init() hasn't re-run yet (see ConnectedApp.sdkMissCount).
26
+ const SDK_ABSENCE_CONFIRM_COUNT = 2;
27
+ // Fast re-probe schedule after a context is (re)created, so the SDK is
28
+ // re-detected within a few hundred ms instead of waiting up to a full
29
+ // SDK_PROBE_INTERVAL_MS tick. Covers reloads not driven by reload_app
30
+ // (shake-to-reload, Metro "r").
31
+ const SDK_REPROBE_DELAYS_MS = [200, 500, 1000, 2000];
32
+ function sendSdkProbe(ws, appKey) {
33
+ if (ws.readyState !== WebSocket.OPEN)
34
+ return;
35
+ const id = getNextMessageId();
36
+ pendingSdkProbeIds.set(id, appKey);
37
+ try {
38
+ ws.send(JSON.stringify({
39
+ id,
40
+ method: "Runtime.evaluate",
41
+ params: {
42
+ expression: 'typeof (globalThis.__EXECBRO__ ?? globalThis.__RN_AI_DEVTOOLS__)?.getNetworkEntries === "function"',
43
+ returnByValue: true
44
+ }
45
+ }));
46
+ }
47
+ catch {
48
+ pendingSdkProbeIds.delete(id);
49
+ }
50
+ }
51
+ function startSdkProbeLoop(ws, appKey) {
52
+ const initial = setTimeout(() => sendSdkProbe(ws, appKey), SDK_PROBE_INITIAL_DELAY_MS);
53
+ const interval = setInterval(() => {
54
+ const app = connectedApps.get(appKey);
55
+ if (!app || ws.readyState !== WebSocket.OPEN) {
56
+ clearInterval(interval);
57
+ return;
58
+ }
59
+ sendSdkProbe(ws, appKey);
60
+ }, SDK_PROBE_INTERVAL_MS);
61
+ const app = connectedApps.get(appKey);
62
+ if (app) {
63
+ app.sdkProbeTimer = interval;
64
+ }
65
+ // Best-effort cleanup if the socket closes before the initial probe fires
66
+ ws.once("close", () => {
67
+ clearTimeout(initial);
68
+ clearInterval(interval);
69
+ });
70
+ }
71
+ // Fire a burst of SDK probes on the heels of a (re)created execution context.
72
+ // After a reload the context is recreated but the SDK's init() re-runs slightly
73
+ // later; without this, sdkPresent only recovers on the next 3s interval tick,
74
+ // widening the window where get_network_requests falls off the SDK buffer.
75
+ function scheduleFastSdkReprobe(ws, appKey) {
76
+ for (const delay of SDK_REPROBE_DELAYS_MS) {
77
+ const t = setTimeout(() => {
78
+ if (!connectedApps.has(appKey) || ws.readyState !== WebSocket.OPEN)
79
+ return;
80
+ sendSdkProbe(ws, appKey);
81
+ }, delay);
82
+ ws.once("close", () => clearTimeout(t));
83
+ }
84
+ }
85
+ // Suppress auto-reconnection for intentionally disconnected devices
86
+ const reconnectionSuppressed = new Set();
87
+ /**
88
+ * Suppress auto-reconnection for all current connections.
89
+ * Used by disconnect_metro to prevent close handlers from re-connecting.
90
+ */
91
+ export function suppressReconnection() {
92
+ for (const key of connectedApps.keys()) {
93
+ reconnectionSuppressed.add(key);
94
+ }
95
+ }
96
+ /**
97
+ * Suppress auto-reconnection for a specific connection key.
98
+ * Used by disconnect_metro with device targeting.
99
+ */
100
+ export function suppressReconnectionForKey(appKey) {
101
+ reconnectionSuppressed.add(appKey);
102
+ }
103
+ /**
104
+ * Clear reconnection suppression (called when user explicitly reconnects via scan_metro).
105
+ */
106
+ export function clearReconnectionSuppression() {
107
+ reconnectionSuppressed.clear();
108
+ }
109
+ const STALE_ACTIVITY_THRESHOLD_MS = 30_000;
110
+ const PING_INTERVAL_MS = 1_000; // WebSocket ping/pong keepalive interval
111
+ /**
112
+ * Max time (ms) to wait for a Runtime.evaluate("1+1") reply during liveness probing.
113
+ * Tight budget: live targets respond in under 50ms; zombie CDP targets never respond.
114
+ */
115
+ const PROBE_TIMEOUT_MS = 1500;
116
+ const RECONNECT_SETTLE_MS = 500;
117
+ /**
118
+ * Creates a WebSocket connection with Origin header fallback.
119
+ * RN 0.85+ Metro requires the Origin header, but Expo SDK 55 Bridgeless
120
+ * rejects connections that include one. The rejection can manifest as:
121
+ * - Connection refused before open (error/close before open event)
122
+ * - Immediate close after open (open fires, then close with code 1006)
123
+ * Tries with Origin first, falls back to without on quick failure.
124
+ */
125
+ export function createWebSocketWithOriginFallback(url, timeoutMs = 5000) {
126
+ const STABILIZE_MS = 500; // Wait after open to detect immediate close
127
+ return new Promise((resolve, reject) => {
128
+ const wsUrl = new URL(url);
129
+ const origin = `http://${wsUrl.hostname}:${wsUrl.port}`;
130
+ let settled = false;
131
+ const tryConnect = (withOrigin) => {
132
+ const options = withOrigin ? { headers: { Origin: origin } } : undefined;
133
+ const ws = new WebSocket(url, options);
134
+ let opened = false;
135
+ ws.on("open", () => {
136
+ opened = true;
137
+ if (withOrigin) {
138
+ // Wait briefly to detect immediate close (Expo SDK 55 pattern)
139
+ setTimeout(() => {
140
+ if (settled)
141
+ return;
142
+ if (ws.readyState === WebSocket.OPEN) {
143
+ settled = true;
144
+ console.error("[execbro] WebSocket connected with Origin header");
145
+ resolve(ws);
146
+ }
147
+ // If not OPEN, the close handler will trigger fallback
148
+ }, STABILIZE_MS);
149
+ }
150
+ else {
151
+ if (settled)
152
+ return;
153
+ settled = true;
154
+ console.error("[execbro] WebSocket connected without Origin header");
155
+ resolve(ws);
156
+ }
157
+ });
158
+ const handleFailure = (error) => {
159
+ if (settled)
160
+ return;
161
+ ws.removeAllListeners();
162
+ ws.terminate();
163
+ if (withOrigin) {
164
+ const reason = opened ? "immediately closed after open" : "rejected before open";
165
+ console.error(`[execbro] Origin header connection ${reason}, retrying without...`);
166
+ tryConnect(false);
167
+ }
168
+ else {
169
+ settled = true;
170
+ reject(error || new Error("WebSocket connection failed"));
171
+ }
172
+ };
173
+ ws.on("error", (err) => handleFailure(err));
174
+ ws.on("close", () => handleFailure());
175
+ };
176
+ tryConnect(true);
177
+ setTimeout(() => {
178
+ if (!settled) {
179
+ settled = true;
180
+ reject(new Error("WebSocket connection timed out"));
181
+ }
182
+ }, timeoutMs);
183
+ });
184
+ }
185
+ // Helper to find appKey from device info by searching connectedApps
186
+ function findAppKeyForDevice(device) {
187
+ for (const [key, app] of connectedApps.entries()) {
188
+ if (app.deviceInfo.id === device.id) {
189
+ return key;
190
+ }
191
+ }
192
+ return null;
193
+ }
194
+ // Helper to convert WebSocket readyState to readable name
195
+ export function getWebSocketStateName(state) {
196
+ switch (state) {
197
+ case WebSocket.CONNECTING: return "CONNECTING";
198
+ case WebSocket.OPEN: return "OPEN";
199
+ case WebSocket.CLOSING: return "CLOSING";
200
+ case WebSocket.CLOSED: return "CLOSED";
201
+ default: return `UNKNOWN(${state})`;
202
+ }
203
+ }
204
+ /**
205
+ * Purge stale connections for scanned ports.
206
+ * When Metro restarts with a different app, /json returns new device IDs.
207
+ * Old connections keyed by the previous device IDs remain in connectedApps.
208
+ * This function removes connections on scanned ports whose device IDs
209
+ * are no longer present in the fresh device list from Metro.
210
+ */
211
+ export function purgeStaleConnectionsForPorts(freshDevicesByPort) {
212
+ const purged = [];
213
+ const scannedPorts = new Set(freshDevicesByPort.keys());
214
+ for (const [appKey, app] of connectedApps.entries()) {
215
+ // Only check connections on ports we just scanned
216
+ if (!scannedPorts.has(app.port))
217
+ continue;
218
+ const freshDevices = freshDevicesByPort.get(app.port) || [];
219
+ // Check both device ID and appId — device IDs are per-physical-device
220
+ // (stable across Metro restarts), so a different app on the same device
221
+ // would have the same ID but a different appId
222
+ const matchingDevice = freshDevices.find(d => d.id === app.deviceInfo.id);
223
+ const stillValid = matchingDevice && matchingDevice.appId === app.deviceInfo.appId;
224
+ if (!stillValid) {
225
+ const name = app.deviceInfo.deviceName || app.deviceInfo.title;
226
+ const reason = !matchingDevice
227
+ ? "device no longer in Metro /json"
228
+ : `appId changed: ${app.deviceInfo.appId} → ${matchingDevice.appId}`;
229
+ console.error(`[execbro] Purging stale connection: ${name} (port ${app.port}, id ${app.deviceInfo.id}) — ${reason}`);
230
+ // Suppress reconnection so close handler doesn't try to reconnect
231
+ reconnectionSuppressed.add(appKey);
232
+ cancelReconnectionTimer(appKey);
233
+ clearLastCDPMessageTime(appKey);
234
+ clearActiveSimulatorIfSource(appKey);
235
+ try {
236
+ app.ws.close();
237
+ }
238
+ catch { /* ignore */ }
239
+ connectedApps.delete(appKey);
240
+ purged.push(name);
241
+ }
242
+ }
243
+ return purged;
244
+ }
245
+ // Format CDP RemoteObject to readable string
246
+ export function formatRemoteObject(result) {
247
+ if (result.type === "undefined") {
248
+ return "undefined";
249
+ }
250
+ if (result.subtype === "null") {
251
+ return "null";
252
+ }
253
+ // For objects/arrays with a value, stringify it
254
+ if (result.value !== undefined) {
255
+ if (typeof result.value === "object") {
256
+ return JSON.stringify(result.value, null, 2);
257
+ }
258
+ return String(result.value);
259
+ }
260
+ // Use description for complex objects
261
+ if (result.description) {
262
+ return result.description;
263
+ }
264
+ // Handle unserializable values (NaN, Infinity, etc.)
265
+ if (result.unserializableValue) {
266
+ return result.unserializableValue;
267
+ }
268
+ return `[${result.type}${result.subtype ? ` ${result.subtype}` : ""}]`;
269
+ }
270
+ /**
271
+ * Extract a clean, informative error message from CDP exception details
272
+ * Handles various error formats from Hermes and other JS engines
273
+ */
274
+ function extractExceptionMessage(exceptionDetails) {
275
+ const parts = [];
276
+ // Get the exception object if available
277
+ const exc = exceptionDetails.exception;
278
+ if (exc) {
279
+ // For error objects, className tells us the error type (ReferenceError, TypeError, etc.)
280
+ const errorType = exc.className || (exc.subtype === 'error' ? 'Error' : '');
281
+ // The description usually contains "ErrorType: message" or full stack trace
282
+ // We want to extract just the first line (the actual error message)
283
+ if (exc.description) {
284
+ const firstLine = exc.description.split('\n')[0].trim();
285
+ // If description already includes the error type, use it directly
286
+ if (firstLine.includes(':')) {
287
+ parts.push(firstLine);
288
+ }
289
+ else if (errorType) {
290
+ // Combine error type with description
291
+ parts.push(`${errorType}: ${firstLine}`);
292
+ }
293
+ else {
294
+ parts.push(firstLine);
295
+ }
296
+ }
297
+ else if (exc.value !== undefined) {
298
+ // For primitive exceptions (throw "string" or throw 123)
299
+ const valueStr = typeof exc.value === 'string' ? exc.value : JSON.stringify(exc.value);
300
+ if (errorType) {
301
+ parts.push(`${errorType}: ${valueStr}`);
302
+ }
303
+ else {
304
+ parts.push(valueStr);
305
+ }
306
+ }
307
+ else if (errorType) {
308
+ // Just the error type, no message
309
+ parts.push(errorType);
310
+ }
311
+ }
312
+ // Fall back to exceptionDetails.text if we couldn't extract from exception object
313
+ // But avoid just "Uncaught" which is not helpful
314
+ if (parts.length === 0) {
315
+ const text = exceptionDetails.text;
316
+ if (text && text.toLowerCase() !== 'uncaught') {
317
+ parts.push(text);
318
+ }
319
+ }
320
+ // Add location info for syntax/compilation errors (helps identify the problem)
321
+ if (exceptionDetails.lineNumber !== undefined && exceptionDetails.columnNumber !== undefined) {
322
+ // Only add location if it's meaningful (not 0:0 which is often just wrapper)
323
+ if (exceptionDetails.lineNumber > 0 || exceptionDetails.columnNumber > 0) {
324
+ parts.push(`at line ${exceptionDetails.lineNumber}:${exceptionDetails.columnNumber}`);
325
+ }
326
+ }
327
+ // If we still have nothing, provide a generic message
328
+ if (parts.length === 0) {
329
+ return 'JavaScript execution failed (no error details available)';
330
+ }
331
+ return parts.join(' ');
332
+ }
333
+ // Format a CDP object preview recursively
334
+ function formatPreview(preview) {
335
+ const isArray = preview.subtype === "array";
336
+ const props = preview.properties || [];
337
+ const formatted = props.map((p) => {
338
+ let value;
339
+ if (p.valuePreview) {
340
+ value = formatPreview(p.valuePreview);
341
+ }
342
+ else if (p.subtype === "null") {
343
+ value = "null";
344
+ }
345
+ else if (p.type === "string") {
346
+ value = `"${p.value}"`;
347
+ }
348
+ else {
349
+ value = p.value ?? "undefined";
350
+ }
351
+ return isArray ? value : `${p.name}: ${value}`;
352
+ });
353
+ const overflow = preview.overflow ? ", ..." : "";
354
+ return isArray
355
+ ? `[${formatted.join(", ")}${overflow}]`
356
+ : `{${formatted.join(", ")}${overflow}}`;
357
+ }
358
+ // Format a single CDP console argument (sync — without object resolution)
359
+ function formatConsoleArg(arg) {
360
+ // Explicit undefined (matches browser: console.log(undefined) → "undefined")
361
+ if (arg.type === "undefined") {
362
+ return "undefined";
363
+ }
364
+ // Primitives
365
+ if (arg.type === "string" || arg.type === "number" || arg.type === "boolean") {
366
+ return String(arg.value);
367
+ }
368
+ // Objects/arrays with preview — expand inline
369
+ if (arg.preview?.properties) {
370
+ return formatPreview(arg.preview);
371
+ }
372
+ // Raw value (e.g. null sent as value)
373
+ if (arg.value !== undefined) {
374
+ return JSON.stringify(arg.value);
375
+ }
376
+ // Description fallback (functions, symbols, errors without preview)
377
+ if (arg.description) {
378
+ return arg.description;
379
+ }
380
+ // Object without any resolved data
381
+ if (arg.type === "object") {
382
+ return "[Object]";
383
+ }
384
+ return `[${arg.type || "unknown"}]`;
385
+ }
386
+ // Fetch object properties via CDP Runtime.getProperties
387
+ function fetchObjectProperties(ws, objectId, depth = 2) {
388
+ return new Promise((resolve) => {
389
+ const msgId = getNextMessageId();
390
+ const timeout = setTimeout(() => {
391
+ resolve("Object"); // Fallback on timeout
392
+ }, 3000);
393
+ const handler = (data) => {
394
+ try {
395
+ const response = JSON.parse(data.toString());
396
+ if (response.id === msgId) {
397
+ clearTimeout(timeout);
398
+ ws.removeListener("message", handler);
399
+ if (response.result?.result) {
400
+ resolve(formatCDPProperties(ws, response.result.result, depth));
401
+ }
402
+ else {
403
+ resolve("Object");
404
+ }
405
+ }
406
+ }
407
+ catch {
408
+ // Ignore parse errors
409
+ }
410
+ };
411
+ ws.on("message", handler);
412
+ ws.send(JSON.stringify({
413
+ id: msgId,
414
+ method: "Runtime.getProperties",
415
+ params: { objectId, ownProperties: true, generatePreview: true }
416
+ }));
417
+ });
418
+ }
419
+ // Format CDP property descriptors into a readable string
420
+ async function formatCDPProperties(ws, properties, depth) {
421
+ const parts = [];
422
+ let isArrayLike = false;
423
+ // Detect arrays by checking for numeric keys and "length" property
424
+ const propNames = properties.filter((p) => !p.isAccessor && p.name !== "__proto__").map((p) => p.name);
425
+ const hasLength = propNames.includes("length");
426
+ const numericKeys = propNames.filter((n) => /^\d+$/.test(n));
427
+ if (hasLength && numericKeys.length > 0 && numericKeys.length >= propNames.length - 1) {
428
+ isArrayLike = true;
429
+ }
430
+ const filteredProps = properties.filter((p) => !p.isAccessor && p.name !== "__proto__" && (!isArrayLike || p.name !== "length"));
431
+ for (const prop of filteredProps) {
432
+ const val = prop.value;
433
+ if (!val)
434
+ continue;
435
+ const formatted = await formatPropertyValue(ws, val, depth - 1);
436
+ if (isArrayLike) {
437
+ parts.push(formatted);
438
+ }
439
+ else {
440
+ parts.push(`${prop.name}: ${formatted}`);
441
+ }
442
+ }
443
+ return isArrayLike
444
+ ? `[${parts.join(", ")}]`
445
+ : `{${parts.join(", ")}}`;
446
+ }
447
+ // Format a single property value
448
+ async function formatPropertyValue(ws, val, remainingDepth) {
449
+ const type = val.type;
450
+ const subtype = val.subtype;
451
+ if (subtype === "null")
452
+ return "null";
453
+ if (type === "undefined")
454
+ return "undefined";
455
+ if (type === "string")
456
+ return `"${val.value}"`;
457
+ if (type === "number" || type === "boolean")
458
+ return String(val.value);
459
+ if (type === "function")
460
+ return "[Function]";
461
+ // Nested object — recurse if depth allows
462
+ if (type === "object" && val.objectId && remainingDepth > 0) {
463
+ return fetchObjectProperties(ws, val.objectId, remainingDepth);
464
+ }
465
+ // Object but no depth left — use description
466
+ if (type === "object") {
467
+ return val.description || "[Object]";
468
+ }
469
+ if (val.value !== undefined)
470
+ return JSON.stringify(val.value);
471
+ if (val.description)
472
+ return val.description;
473
+ return `[${type}]`;
474
+ }
475
+ // Resolve all object args in a console message, returning formatted text
476
+ async function resolveConsoleArgs(ws, args) {
477
+ const parts = await Promise.all(args.map(async (arg) => {
478
+ // Primitives — format synchronously
479
+ if (arg.type === "string" || arg.type === "number" || arg.type === "boolean") {
480
+ return String(arg.value);
481
+ }
482
+ // Objects/arrays with preview — expand inline
483
+ if (arg.preview?.properties) {
484
+ return formatPreview(arg.preview);
485
+ }
486
+ // Object with objectId — fetch properties via CDP
487
+ if (arg.type === "object" && arg.objectId) {
488
+ return fetchObjectProperties(ws, arg.objectId);
489
+ }
490
+ // Raw value
491
+ if (arg.value !== undefined) {
492
+ return JSON.stringify(arg.value);
493
+ }
494
+ // Description fallback
495
+ if (arg.description) {
496
+ return arg.description;
497
+ }
498
+ return "[object]";
499
+ }));
500
+ return parts.join(" ");
501
+ }
502
+ // Handle CDP messages
503
+ export function handleCDPMessage(message, device, ws) {
504
+ // Track last CDP activity for connection liveness detection (per-device)
505
+ const cdpAppKey = findAppKeyForDevice(device);
506
+ if (cdpAppKey) {
507
+ updateLastCDPMessageTime(cdpAppKey, new Date());
508
+ }
509
+ const deviceName = device.deviceName || device.title || "unknown";
510
+ // Handle responses to our requests (e.g., Runtime.evaluate)
511
+ if (typeof message.id === "number") {
512
+ const pending = pendingExecutions.get(message.id);
513
+ if (pending) {
514
+ clearTimeout(pending.timeoutId);
515
+ pendingExecutions.delete(message.id);
516
+ // Check for CDP-level error (protocol error, not JS exception)
517
+ if (message.error) {
518
+ const error = message.error;
519
+ // Build comprehensive error message including code and data if available
520
+ const parts = [];
521
+ if (error.message)
522
+ parts.push(error.message);
523
+ if (error.code !== undefined)
524
+ parts.push(`(code: ${error.code})`);
525
+ if (error.data)
526
+ parts.push(`- ${error.data}`);
527
+ const errorMessage = parts.length > 0 ? parts.join(' ') : 'Unknown CDP protocol error';
528
+ pending.resolve({ success: false, error: errorMessage });
529
+ return;
530
+ }
531
+ // Check for JavaScript exception in result
532
+ const result = message.result;
533
+ if (result?.exceptionDetails) {
534
+ const errorMessage = extractExceptionMessage(result.exceptionDetails);
535
+ pending.resolve({ success: false, error: errorMessage });
536
+ return;
537
+ }
538
+ // Success - format the result
539
+ if (result?.result) {
540
+ pending.resolve({ success: true, result: formatRemoteObject(result.result) });
541
+ return;
542
+ }
543
+ pending.resolve({ success: true, result: "undefined" });
544
+ }
545
+ // Track SDK-probe responses
546
+ if (pendingSdkProbeIds.has(message.id)) {
547
+ const appKey = pendingSdkProbeIds.get(message.id);
548
+ pendingSdkProbeIds.delete(message.id);
549
+ const app = connectedApps.get(appKey);
550
+ if (app) {
551
+ const result = message.result?.result;
552
+ const present = result?.value === true;
553
+ const wasPresent = app.sdkPresent === true;
554
+ // Hysteresis on the true→false edge: tolerate transient SDK
555
+ // absence right after a reload (context recreated, init() not
556
+ // re-run yet) so we don't restore duplicate-prone CDP/interceptor
557
+ // writes and bounce get_network_requests off the SDK buffer.
558
+ let nextPresent = present;
559
+ if (!present && wasPresent) {
560
+ app.sdkMissCount = (app.sdkMissCount ?? 0) + 1;
561
+ if (app.sdkMissCount < SDK_ABSENCE_CONFIRM_COUNT) {
562
+ nextPresent = true; // hold "present" until absence is confirmed
563
+ }
564
+ else {
565
+ app.sdkMissCount = 0;
566
+ }
567
+ }
568
+ else {
569
+ app.sdkMissCount = 0;
570
+ }
571
+ app.sdkPresent = nextPresent;
572
+ if (nextPresent !== wasPresent) {
573
+ console.error(`[execbro] SDK ${present ? "detected" : "no longer detected"} on ${app.deviceInfo.title}; CDP/JS-interceptor buffer writes ${present ? "suppressed" : "restored"}`);
574
+ // Toggle the in-app interceptor's emit flag so it stops
575
+ // (or resumes) producing console.debug lines and CDP
576
+ // traffic. Fire-and-forget — the flag is idempotent.
577
+ if (app.ws.readyState === WebSocket.OPEN) {
578
+ try {
579
+ app.ws.send(JSON.stringify({
580
+ id: getNextMessageId(),
581
+ method: "Runtime.evaluate",
582
+ params: {
583
+ expression: `globalThis.__RN_NET_DISABLED__ = ${present ? "true" : "false"}`,
584
+ returnByValue: true
585
+ }
586
+ }));
587
+ }
588
+ catch {
589
+ // ignore — next probe will retry
590
+ }
591
+ }
592
+ }
593
+ }
594
+ return;
595
+ }
596
+ // Track Network.enable responses to detect CDP network support
597
+ if (pendingNetworkEnableIds.has(message.id)) {
598
+ pendingNetworkEnableIds.delete(message.id);
599
+ const nAppKey = findAppKeyForDevice(device);
600
+ if (nAppKey) {
601
+ const app = connectedApps.get(nAppKey);
602
+ if (app) {
603
+ if (message.error) {
604
+ app.cdpNetworkSupported = false;
605
+ console.error(`[execbro] CDP Network domain not supported, using JS interceptor`);
606
+ }
607
+ else {
608
+ app.cdpNetworkSupported = true;
609
+ console.error(`[execbro] CDP Network domain supported`);
610
+ }
611
+ }
612
+ }
613
+ }
614
+ return;
615
+ }
616
+ const method = message.method;
617
+ // Handle Runtime.consoleAPICalled
618
+ if (method === "Runtime.consoleAPICalled") {
619
+ const params = message.params;
620
+ const type = params.type || "log";
621
+ const level = mapConsoleType(type);
622
+ const args = params.args || [];
623
+ // Check for network interceptor events before processing as logs
624
+ const interceptorJson = isInterceptorEvent(args);
625
+ if (interceptorJson !== null) {
626
+ const iAppKey = findAppKeyForDevice(device);
627
+ const iApp = iAppKey ? connectedApps.get(iAppKey) : null;
628
+ // Skip when CDP Network domain is the source, OR when the in-app
629
+ // SDK is the source — both would otherwise produce duplicate
630
+ // entries for every request.
631
+ if (!iApp?.cdpNetworkSupported && !iApp?.sdkPresent) {
632
+ applyInterceptedEvent(interceptorJson, getNetworkBuffer(deviceName));
633
+ }
634
+ return;
635
+ }
636
+ // Check if any args need async object resolution
637
+ const hasObjectArgs = ws && args.some((a) => a.type === "object" && a.objectId && !a.preview?.properties);
638
+ if (hasObjectArgs) {
639
+ // Resolve object args asynchronously via CDP
640
+ resolveConsoleArgs(ws, args).then((messageText) => {
641
+ getLogBuffer(deviceName).add({
642
+ timestamp: new Date(),
643
+ level,
644
+ message: messageText || "[console call with empty resolution]",
645
+ args: args.map((a) => a.value)
646
+ });
647
+ }).catch(() => {
648
+ // Fallback to sync formatting on error
649
+ const messageText = args.map(formatConsoleArg).join(" ");
650
+ getLogBuffer(deviceName).add({
651
+ timestamp: new Date(),
652
+ level,
653
+ message: messageText || "[console call with unresolvable args]",
654
+ args: args.map((a) => a.value)
655
+ });
656
+ });
657
+ }
658
+ else {
659
+ const messageText = args.map(formatConsoleArg).join(" ");
660
+ if (messageText.trim()) {
661
+ getLogBuffer(deviceName).add({
662
+ timestamp: new Date(),
663
+ level,
664
+ message: messageText,
665
+ args: args.map((a) => a.value)
666
+ });
667
+ }
668
+ }
669
+ }
670
+ // Handle Log.entryAdded
671
+ if (method === "Log.entryAdded") {
672
+ const params = message.params;
673
+ if (params.entry) {
674
+ const level = mapConsoleType(params.entry.level || "log");
675
+ getLogBuffer(deviceName).add({
676
+ timestamp: new Date(),
677
+ level,
678
+ message: params.entry.text || ""
679
+ });
680
+ }
681
+ }
682
+ // Handle Network.requestWillBeSent
683
+ if (method === "Network.requestWillBeSent") {
684
+ // Skip CDP capture when the in-app SDK is the source of truth —
685
+ // otherwise every request lands in both buffers under different ids.
686
+ // The follow-up Network.responseReceived/loadingFinished/loadingFailed
687
+ // handlers below only mutate existing entries, so they self-skip.
688
+ const nAppKey = findAppKeyForDevice(device);
689
+ const nApp = nAppKey ? connectedApps.get(nAppKey) : null;
690
+ if (nApp?.sdkPresent) {
691
+ return;
692
+ }
693
+ // An arriving Network.requestWillBeSent is proof the CDP Network domain
694
+ // is active. Some backends (e.g. RN 0.85 Bridgeless/Hermes) enable the
695
+ // domain but never send a response to Network.enable, so the
696
+ // response-based detection above never fires and cdpNetworkSupported
697
+ // stays undefined — leaving the JS interceptor un-suppressed and every
698
+ // request double-captured (CDP + interceptor). Mark support here so the
699
+ // interceptor dedup guard (see Runtime.consoleAPICalled handler) trips.
700
+ if (nApp && !nApp.cdpNetworkSupported) {
701
+ nApp.cdpNetworkSupported = true;
702
+ console.error(`[execbro] CDP Network domain active (detected via requestWillBeSent on ${nApp.deviceInfo.title})`);
703
+ }
704
+ const params = message.params;
705
+ const request = {
706
+ requestId: params.requestId,
707
+ timestamp: new Date(),
708
+ method: params.request.method,
709
+ url: params.request.url,
710
+ headers: params.request.headers || {},
711
+ postData: params.request.postData,
712
+ timing: {
713
+ requestTime: params.timestamp
714
+ },
715
+ completed: false
716
+ };
717
+ getNetworkBuffer(deviceName).set(params.requestId, request);
718
+ }
719
+ // Handle Network.responseReceived
720
+ if (method === "Network.responseReceived") {
721
+ const params = message.params;
722
+ const existing = getNetworkBuffer(deviceName).get(params.requestId);
723
+ if (existing) {
724
+ existing.status = params.response.status;
725
+ existing.statusText = params.response.statusText;
726
+ existing.responseHeaders = params.response.headers || {};
727
+ existing.mimeType = params.response.mimeType;
728
+ if (params.timestamp && existing.timing?.requestTime) {
729
+ existing.timing.responseTime = params.timestamp;
730
+ }
731
+ getNetworkBuffer(deviceName).set(params.requestId, existing);
732
+ }
733
+ }
734
+ // Handle Network.loadingFinished
735
+ if (method === "Network.loadingFinished") {
736
+ const params = message.params;
737
+ const existing = getNetworkBuffer(deviceName).get(params.requestId);
738
+ if (existing) {
739
+ existing.completed = true;
740
+ existing.contentLength = params.encodedDataLength;
741
+ if (params.timestamp && existing.timing?.requestTime) {
742
+ existing.timing.duration = Math.round((params.timestamp - existing.timing.requestTime) * 1000);
743
+ }
744
+ getNetworkBuffer(deviceName).set(params.requestId, existing);
745
+ }
746
+ }
747
+ // Handle Network.loadingFailed
748
+ if (method === "Network.loadingFailed") {
749
+ const params = message.params;
750
+ const existing = getNetworkBuffer(deviceName).get(params.requestId);
751
+ if (existing) {
752
+ existing.completed = true;
753
+ existing.error = params.canceled ? "Canceled" : (params.errorText || "Request failed");
754
+ getNetworkBuffer(deviceName).set(params.requestId, existing);
755
+ }
756
+ }
757
+ // Handle Runtime context lifecycle events for health tracking
758
+ const appKey = findAppKeyForDevice(device);
759
+ if (appKey) {
760
+ // Handle Runtime.executionContextCreated
761
+ if (method === "Runtime.executionContextCreated") {
762
+ const params = message.params;
763
+ markContextHealthy(appKey, params.context.id);
764
+ console.error(`[execbro] Context created: ${params.context.id}`);
765
+ // Re-inject network interceptor and re-check CDP Network support
766
+ const ctxApp = connectedApps.get(appKey);
767
+ if (ctxApp?.ws?.readyState === WebSocket.OPEN) {
768
+ injectNetworkInterceptor(ctxApp.ws);
769
+ const nEnableId = sendNetworkEnable(ctxApp.ws);
770
+ pendingNetworkEnableIds.add(nEnableId);
771
+ // The context was just recreated (e.g. reload). Re-probe the SDK
772
+ // quickly so sdkPresent re-establishes within a few hundred ms
773
+ // instead of on the next 3s interval tick.
774
+ scheduleFastSdkReprobe(ctxApp.ws, appKey);
775
+ }
776
+ }
777
+ // Handle Runtime.executionContextDestroyed
778
+ if (method === "Runtime.executionContextDestroyed") {
779
+ markContextStale(appKey);
780
+ console.error(`[execbro] Context destroyed`);
781
+ }
782
+ // Handle Runtime.executionContextsCleared
783
+ if (method === "Runtime.executionContextsCleared") {
784
+ markContextStale(appKey);
785
+ console.error(`[execbro] All contexts cleared`);
786
+ }
787
+ }
788
+ }
789
+ // Connect to a device via CDP WebSocket
790
+ export async function connectToDevice(device, port, options = {}) {
791
+ const { isReconnection = false, reconnectionConfig = DEFAULT_RECONNECTION_CONFIG } = options;
792
+ return new Promise(async (resolve, reject) => {
793
+ const appKey = `${port}-${device.id}`;
794
+ // Check if already connected with a valid WebSocket
795
+ const existingApp = connectedApps.get(appKey);
796
+ if (existingApp) {
797
+ if (existingApp.ws.readyState === WebSocket.OPEN) {
798
+ // Verify WebSocket is actually alive with a ping/pong check.
799
+ // readyState can show OPEN even when the remote end is gone
800
+ // (TCP hasn't timed out yet, especially after Metro restart).
801
+ const isAlive = await new Promise((pongResolve) => {
802
+ const timeout = setTimeout(() => pongResolve(false), 2000);
803
+ existingApp.ws.once("pong", () => {
804
+ clearTimeout(timeout);
805
+ pongResolve(true);
806
+ });
807
+ try {
808
+ existingApp.ws.ping();
809
+ }
810
+ catch {
811
+ clearTimeout(timeout);
812
+ pongResolve(false);
813
+ }
814
+ });
815
+ if (isAlive) {
816
+ resolve(`Already connected to ${device.title}`);
817
+ return;
818
+ }
819
+ // Ping failed — connection is dead, clean up
820
+ console.error(`[execbro] Existing connection to ${device.title} failed liveness check (no pong), reconnecting`);
821
+ recordConnectionEvent("liveness-failed", appKey, device.title, "no pong within 2000ms");
822
+ reconnectionSuppressed.add(appKey);
823
+ try {
824
+ existingApp.ws.terminate();
825
+ }
826
+ catch { /* ignore */ }
827
+ connectedApps.delete(appKey);
828
+ clearLastCDPMessageTime(appKey);
829
+ clearActiveSimulatorIfSource(appKey);
830
+ }
831
+ else {
832
+ // WebSocket exists but not OPEN - clean up stale entry
833
+ console.error(`[execbro] Cleaning up stale connection for ${device.title} (state: ${getWebSocketStateName(existingApp.ws.readyState)})`);
834
+ connectedApps.delete(appKey);
835
+ }
836
+ }
837
+ // Skip if this device is already connected on a different port
838
+ // (Metro's /json advertises all devices it can see, even ones built by other Metro servers)
839
+ const deviceName = device.deviceName || device.title;
840
+ for (const [existingKey, existingApp] of connectedApps.entries()) {
841
+ if (existingKey !== appKey && existingApp.ws.readyState === WebSocket.OPEN) {
842
+ const existingName = existingApp.deviceInfo.deviceName || existingApp.deviceInfo.title;
843
+ if (existingName === deviceName) {
844
+ resolve(`Skipped ${deviceName} (already connected on port ${existingApp.port})`);
845
+ return;
846
+ }
847
+ }
848
+ }
849
+ // Prevent concurrent connection attempts to the same device
850
+ if (connectionLocks.has(appKey)) {
851
+ resolve(`Connection already in progress for ${device.title}`);
852
+ return;
853
+ }
854
+ connectionLocks.add(appKey);
855
+ // Cancel any pending reconnection timer for this appKey
856
+ cancelReconnectionTimer(appKey);
857
+ // Save connection metadata for potential reconnection
858
+ saveConnectionMetadata(appKey, {
859
+ port,
860
+ deviceInfo: device,
861
+ webSocketUrl: device.webSocketDebuggerUrl
862
+ });
863
+ try {
864
+ const ws = await createWebSocketWithOriginFallback(device.webSocketDebuggerUrl);
865
+ // Register close/error handlers IMMEDIATELY — before any async work
866
+ // below (probeCdpAlive, Runtime.enable, simctl/adb lookups). Previously
867
+ // they were attached only at the end of the setup block, so any close
868
+ // event during that window was silently swallowed: the entry stayed
869
+ // in connectedApps, readyState flipped to CLOSED, but no reconnect
870
+ // was scheduled and no `closed` event was recorded.
871
+ let pingInterval = null;
872
+ let staleRejected = false;
873
+ const handleClose = () => {
874
+ if (staleRejected)
875
+ return; // probe-fail path handles its own cleanup
876
+ if (pingInterval) {
877
+ clearInterval(pingInterval);
878
+ pingInterval = null;
879
+ }
880
+ // Release connection lock if still held
881
+ connectionLocks.delete(appKey);
882
+ connectedApps.delete(appKey);
883
+ clearLastCDPMessageTime(appKey);
884
+ // Clear active simulator UDID if this connection set it
885
+ clearActiveSimulatorIfSource(appKey);
886
+ // Keep log and network buffers across reconnections
887
+ // They are only cleared on demand via clear_logs / clear_network tools
888
+ // Check if connection was stable before resetting attempts
889
+ const state = getConnectionState(appKey);
890
+ let wasStable = false;
891
+ if (state?.lastConnectedTime) {
892
+ const connectionDuration = Date.now() - state.lastConnectedTime.getTime();
893
+ wasStable = connectionDuration >= MIN_STABLE_CONNECTION_MS;
894
+ if (wasStable) {
895
+ // Connection was stable - reset attempts for fresh start
896
+ updateConnectionState(appKey, { reconnectionAttempts: 0 });
897
+ console.error(`[execbro] Connection was stable for ${Math.round(connectionDuration / 1000)}s, resetting reconnection attempts`);
898
+ }
899
+ }
900
+ // Record the gap and trigger reconnection
901
+ recordConnectionGap(appKey, "Connection closed");
902
+ if (state) {
903
+ updateConnectionState(appKey, {
904
+ status: "disconnected",
905
+ lastDisconnectTime: new Date()
906
+ });
907
+ }
908
+ const uptimeMs = state?.lastConnectedTime ? Date.now() - state.lastConnectedTime.getTime() : 0;
909
+ const earlyPhase = state ? "" : " (before state init)";
910
+ console.error(`[execbro] Disconnected from ${device.title}${earlyPhase}`);
911
+ recordConnectionEvent("closed", appKey, device.title, `uptime ${formatDuration(uptimeMs)}${wasStable ? " (stable)" : " (unstable)"}${earlyPhase}`);
912
+ // Schedule auto-reconnection if enabled (skip if intentionally disconnected)
913
+ if (reconnectionConfig.enabled && !reconnectionSuppressed.has(appKey)) {
914
+ // If close fired before initConnectionState, scheduleReconnection
915
+ // would early-return (it requires existing state). Init it now
916
+ // so the reconnect loop can still run.
917
+ if (!state)
918
+ initConnectionState(appKey);
919
+ scheduleReconnection(appKey, reconnectionConfig);
920
+ }
921
+ else if (reconnectionSuppressed.has(appKey)) {
922
+ reconnectionSuppressed.delete(appKey);
923
+ console.error(`[execbro] Reconnection suppressed for ${device.title} (intentional disconnect)`);
924
+ recordConnectionEvent("reconnect-suppressed", appKey, device.title, "intentional disconnect");
925
+ }
926
+ else if (!reconnectionConfig.enabled) {
927
+ recordConnectionEvent("reconnect-suppressed", appKey, device.title, "reconnect disabled by caller");
928
+ }
929
+ };
930
+ ws.on("close", handleClose);
931
+ ws.on("error", (error) => {
932
+ console.error(`[execbro] WebSocket error for ${device.title}: ${error?.message || error}`);
933
+ });
934
+ // Verify the JS context is actually alive. Metro's /json can advertise
935
+ // zombie CDP pages that complete the WS handshake but no longer execute.
936
+ // A stale target here causes every downstream tool to time out or return
937
+ // degenerate state (e.g. __DEV__ === false on a debug bundle).
938
+ const alive = await probeCdpAlive(ws, PROBE_TIMEOUT_MS);
939
+ if (!alive) {
940
+ staleRejected = true;
941
+ ws.off("close", handleClose);
942
+ connectionLocks.delete(appKey);
943
+ try {
944
+ ws.terminate();
945
+ }
946
+ catch { /* ignore */ }
947
+ clearConnectionMetadata(appKey);
948
+ console.error(`[execbro] Rejecting stale CDP target for ${device.title} (no probe response)`);
949
+ recordConnectionEvent("stale-target", appKey, device.title, `no probe response within ${PROBE_TIMEOUT_MS}ms${isReconnection ? " (during reconnect)" : ""}`);
950
+ resolve(`Skipped ${device.deviceName || device.title} (stale CDP target — no response from JS context)`);
951
+ return;
952
+ }
953
+ // Connection established — run setup
954
+ connectionLocks.delete(appKey);
955
+ connectedApps.set(appKey, { ws, deviceInfo: device, port, platform: "android" });
956
+ markConnectionEstablished();
957
+ // Initialize or update connection state
958
+ // Note: We do NOT reset reconnectionAttempts here - that happens
959
+ // only when connection has been stable for MIN_STABLE_CONNECTION_MS
960
+ if (isReconnection) {
961
+ closeConnectionGap(appKey);
962
+ updateConnectionState(appKey, {
963
+ status: "connected",
964
+ lastConnectedTime: new Date()
965
+ // reconnectionAttempts NOT reset here - see ws.on("close") for stable connection check
966
+ });
967
+ // Reset context health for reconnection
968
+ initContextHealth(appKey);
969
+ console.error(`[execbro] Reconnected to ${device.title}`);
970
+ recordConnectionEvent("reconnect-success", appKey, device.title);
971
+ }
972
+ else {
973
+ initConnectionState(appKey);
974
+ initContextHealth(appKey);
975
+ console.error(`[execbro] Connected to ${device.title}`);
976
+ recordConnectionEvent("connect-success", appKey, device.title);
977
+ }
978
+ // Enable Runtime domain to receive console messages
979
+ ws.send(JSON.stringify({
980
+ id: getNextMessageId(),
981
+ method: "Runtime.enable"
982
+ }));
983
+ // Also enable Log domain
984
+ ws.send(JSON.stringify({
985
+ id: getNextMessageId(),
986
+ method: "Log.enable"
987
+ }));
988
+ // Inject JS network interceptor (immediate capture, may fail if context not ready)
989
+ injectNetworkInterceptor(ws);
990
+ // Also try CDP Network.enable (takes priority if supported)
991
+ const networkEnableId = sendNetworkEnable(ws);
992
+ pendingNetworkEnableIds.add(networkEnableId);
993
+ // Resolve native identifiers from the device name in parallel:
994
+ // - iOS: simulator UDID via findSimulatorByName
995
+ // - Android: adb serial via getAdbIdForAvd (maps AVD name → emulator-NNNN)
996
+ // Both are best-effort; failures are swallowed. Identifiers enable
997
+ // automatic device scoping and power the registry-first resolver fast path.
998
+ if (device.deviceName) {
999
+ const [simulatorUdid, adbSerial] = await Promise.all([
1000
+ findSimulatorByName(device.deviceName).catch(() => null),
1001
+ getAdbIdForAvd(device.deviceName).catch(() => null)
1002
+ ]);
1003
+ const connectedApp = connectedApps.get(appKey);
1004
+ if (connectedApp) {
1005
+ if (simulatorUdid) {
1006
+ setActiveSimulatorUdid(simulatorUdid, appKey);
1007
+ connectedApp.platform = "ios";
1008
+ connectedApp.simulatorUdid = simulatorUdid;
1009
+ console.error(`[execbro] Linked to iOS simulator: ${simulatorUdid}`);
1010
+ }
1011
+ if (adbSerial) {
1012
+ connectedApp.platform = "android";
1013
+ connectedApp.adbSerial = adbSerial;
1014
+ console.error(`[execbro] Linked to Android emulator: ${adbSerial}`);
1015
+ }
1016
+ }
1017
+ }
1018
+ // Fire-and-forget app detection (500ms delayed, non-blocking)
1019
+ const appForDetection = connectedApps.get(appKey);
1020
+ if (appForDetection) {
1021
+ scheduleAppDetection(appForDetection);
1022
+ }
1023
+ // Start periodic SDK probe. When __RN_AI_DEVTOOLS__ is detected,
1024
+ // app.sdkPresent flips to true and CDP/JS-interceptor buffer
1025
+ // writes are suppressed (SDK becomes the single source).
1026
+ startSdkProbeLoop(ws, appKey);
1027
+ // Start WebSocket ping/pong keepalive to detect dead connections
1028
+ // (especially important for physical devices over Wi-Fi).
1029
+ // pingInterval was declared up-top so the early-registered close
1030
+ // handler can clear it; close/error handlers are already attached.
1031
+ let pongReceived = true;
1032
+ pingInterval = setInterval(() => {
1033
+ if (!pongReceived) {
1034
+ console.error(`[execbro] No pong from ${device.title}, terminating connection`);
1035
+ if (pingInterval) {
1036
+ clearInterval(pingInterval);
1037
+ pingInterval = null;
1038
+ }
1039
+ ws.terminate();
1040
+ return;
1041
+ }
1042
+ pongReceived = false;
1043
+ ws.ping();
1044
+ }, PING_INTERVAL_MS);
1045
+ ws.on("pong", () => {
1046
+ pongReceived = true;
1047
+ });
1048
+ ws.on("message", (data) => {
1049
+ try {
1050
+ const message = JSON.parse(data.toString());
1051
+ handleCDPMessage(message, device, ws);
1052
+ }
1053
+ catch {
1054
+ // Ignore non-JSON messages
1055
+ }
1056
+ });
1057
+ resolve(`Connected to ${device.title} (${device.deviceName})`);
1058
+ }
1059
+ catch (error) {
1060
+ // Connection failed (both with and without Origin header)
1061
+ connectionLocks.delete(appKey);
1062
+ cancelReconnectionTimer(appKey);
1063
+ connectedApps.delete(appKey);
1064
+ clearLastCDPMessageTime(appKey);
1065
+ clearActiveSimulatorIfSource(appKey);
1066
+ const errorMsg = error instanceof Error ? error.message : String(error);
1067
+ if (!isReconnection) {
1068
+ reject(`Failed to connect to ${device.title}: ${errorMsg}`);
1069
+ }
1070
+ else {
1071
+ console.error(`[execbro] Reconnection error: ${errorMsg}`);
1072
+ }
1073
+ }
1074
+ });
1075
+ }
1076
+ /**
1077
+ * Schedule a reconnection attempt with exponential backoff
1078
+ */
1079
+ function scheduleReconnection(appKey, config = DEFAULT_RECONNECTION_CONFIG) {
1080
+ const state = getConnectionState(appKey);
1081
+ if (!state)
1082
+ return;
1083
+ const meta = getConnectionMetadata(appKey);
1084
+ const deviceTitle = meta?.deviceInfo.title;
1085
+ const attempts = state.reconnectionAttempts;
1086
+ if (attempts >= config.maxAttempts) {
1087
+ console.error(`[execbro] Max reconnection attempts (${config.maxAttempts}) reached for ${appKey}`);
1088
+ updateConnectionState(appKey, { status: "disconnected" });
1089
+ recordConnectionEvent("max-attempts-reached", appKey, deviceTitle, `${config.maxAttempts} attempts exhausted`);
1090
+ return;
1091
+ }
1092
+ const delay = calculateBackoffDelay(attempts, config);
1093
+ console.error(`[execbro] Scheduling reconnection attempt ${attempts + 1}/${config.maxAttempts} in ${delay}ms`);
1094
+ recordConnectionEvent("reconnect-scheduled", appKey, deviceTitle, `attempt ${attempts + 1}/${config.maxAttempts} in ${delay}ms`);
1095
+ updateConnectionState(appKey, {
1096
+ status: "reconnecting",
1097
+ reconnectionAttempts: attempts + 1
1098
+ });
1099
+ const timer = setTimeout(() => {
1100
+ attemptReconnection(appKey, config);
1101
+ }, delay);
1102
+ saveReconnectionTimer(appKey, timer);
1103
+ }
1104
+ /**
1105
+ * Attempt to reconnect to a previously connected device
1106
+ */
1107
+ async function attemptReconnection(appKey, config = DEFAULT_RECONNECTION_CONFIG) {
1108
+ const metadata = getConnectionMetadata(appKey);
1109
+ if (!metadata) {
1110
+ console.error(`[execbro] No metadata for reconnection: ${appKey}`);
1111
+ recordConnectionEvent("reconnect-failed", appKey, undefined, "no metadata");
1112
+ return false;
1113
+ }
1114
+ const deviceTitle = metadata.deviceInfo.title;
1115
+ recordConnectionEvent("reconnect-attempt", appKey, deviceTitle);
1116
+ // Quick check: is Metro even running on this port?
1117
+ const { scanMetroPorts } = await import("./metro.js");
1118
+ const openPorts = await scanMetroPorts(metadata.port, metadata.port);
1119
+ if (openPorts.length === 0) {
1120
+ console.error(`[execbro] Metro not running on port ${metadata.port}, stopping reconnection for ${appKey}`);
1121
+ updateConnectionState(appKey, { status: "disconnected" });
1122
+ recordConnectionEvent("metro-down", appKey, deviceTitle, `port ${metadata.port} not open`);
1123
+ return false;
1124
+ }
1125
+ try {
1126
+ // Re-fetch devices to get fresh WebSocket URL (may have changed)
1127
+ const devices = await fetchDevices(metadata.port);
1128
+ // Try to find the same device first, otherwise select main device
1129
+ const device = devices.find(d => d.id === metadata.deviceInfo.id)
1130
+ || selectMainDevice(devices);
1131
+ if (!device) {
1132
+ console.error(`[execbro] Device no longer available for ${appKey}`);
1133
+ recordConnectionEvent("reconnect-failed", appKey, deviceTitle, "device no longer advertised by Metro");
1134
+ // Schedule next attempt
1135
+ scheduleReconnection(appKey, config);
1136
+ return false;
1137
+ }
1138
+ await connectToDevice(device, metadata.port, { isReconnection: true, reconnectionConfig: config });
1139
+ return true;
1140
+ }
1141
+ catch (error) {
1142
+ const msg = error instanceof Error ? error.message : String(error);
1143
+ console.error(`[execbro] Reconnection failed: ${msg}`);
1144
+ recordConnectionEvent("reconnect-failed", appKey, deviceTitle, msg);
1145
+ // Schedule next attempt
1146
+ scheduleReconnection(appKey, config);
1147
+ return false;
1148
+ }
1149
+ }
1150
+ // Get list of connected apps
1151
+ export function getConnectedApps() {
1152
+ return Array.from(connectedApps.entries()).map(([key, app]) => ({
1153
+ key,
1154
+ app,
1155
+ isConnected: app.ws.readyState === WebSocket.OPEN
1156
+ }));
1157
+ }
1158
+ // Get first connected app with an OPEN WebSocket (or null if none)
1159
+ export function getFirstConnectedApp() {
1160
+ // Find first app with OPEN WebSocket, cleaning up stale entries
1161
+ for (const [key, app] of connectedApps.entries()) {
1162
+ if (app.ws.readyState === WebSocket.OPEN) {
1163
+ return app;
1164
+ }
1165
+ // Clean up stale entry
1166
+ console.error(`[execbro] Cleaning up stale connection in getFirstConnectedApp: ${key} (state: ${getWebSocketStateName(app.ws.readyState)})`);
1167
+ connectedApps.delete(key);
1168
+ }
1169
+ return null;
1170
+ }
1171
+ /**
1172
+ * Find the connected RN app running on a specific iOS simulator (by UDID).
1173
+ * Returns null if no connected app matches — caller should skip any CDP-based
1174
+ * enrichment to avoid pulling data from a different simulator's app.
1175
+ */
1176
+ export function getConnectedAppBySimulatorUdid(udid) {
1177
+ for (const [key, app] of connectedApps.entries()) {
1178
+ if (app.ws.readyState !== WebSocket.OPEN) {
1179
+ connectedApps.delete(key);
1180
+ continue;
1181
+ }
1182
+ if (app.platform === "ios" && app.simulatorUdid === udid) {
1183
+ return app;
1184
+ }
1185
+ }
1186
+ return null;
1187
+ }
1188
+ /**
1189
+ * Find the connected RN app running on a specific Android device.
1190
+ * Falls back to the sole connected Android app when `deviceId` is not provided
1191
+ * or cannot be resolved via deviceName substring match — this keeps the common
1192
+ * single-device case working while preventing cross-device data leakage when
1193
+ * multiple Android apps are connected.
1194
+ */
1195
+ export function getConnectedAppByAndroidDeviceId(deviceId) {
1196
+ const androidApps = [];
1197
+ for (const [key, app] of connectedApps.entries()) {
1198
+ if (app.ws.readyState !== WebSocket.OPEN) {
1199
+ connectedApps.delete(key);
1200
+ continue;
1201
+ }
1202
+ if (app.platform === "android")
1203
+ androidApps.push(app);
1204
+ }
1205
+ if (androidApps.length === 0)
1206
+ return null;
1207
+ if (androidApps.length === 1)
1208
+ return androidApps[0];
1209
+ if (deviceId) {
1210
+ const lower = deviceId.toLowerCase();
1211
+ const match = androidApps.find(a => {
1212
+ const name = (a.deviceInfo.deviceName || a.deviceInfo.title || "").toLowerCase();
1213
+ return name.includes(lower) || lower.includes(name);
1214
+ });
1215
+ if (match)
1216
+ return match;
1217
+ }
1218
+ return null;
1219
+ }
1220
+ /**
1221
+ * Lowercase and strip separators (whitespace, `_`, `-`) so substring matches
1222
+ * survive punctuation drift between caller input and the device's reported
1223
+ * identifier (e.g. "SM_A356N" vs "SM-A356N - 15 - API 35").
1224
+ */
1225
+ function normalizeDeviceId(value) {
1226
+ if (!value)
1227
+ return "";
1228
+ return value.toLowerCase().replace(/[\s_\-]+/g, "");
1229
+ }
1230
+ export function getConnectedAppByDevice(device) {
1231
+ if (!device) {
1232
+ return getFirstConnectedApp();
1233
+ }
1234
+ const lowerDevice = device.toLowerCase();
1235
+ const normDevice = normalizeDeviceId(device);
1236
+ const matches = [];
1237
+ const allOpenNames = [];
1238
+ for (const [key, app] of connectedApps.entries()) {
1239
+ if (app.ws.readyState !== WebSocket.OPEN) {
1240
+ connectedApps.delete(key);
1241
+ continue;
1242
+ }
1243
+ const deviceName = app.deviceInfo.deviceName || app.deviceInfo.title || "";
1244
+ allOpenNames.push(deviceName);
1245
+ // Match against deviceName + the underlying hardware identifiers, with
1246
+ // separator-insensitive normalization so "SM_A356N" finds
1247
+ // "SM-A356N - 15 - API 35" and "emulator-5554" finds the Android app
1248
+ // attached to that serial.
1249
+ const haystacks = [
1250
+ normalizeDeviceId(deviceName),
1251
+ normalizeDeviceId(app.simulatorUdid),
1252
+ normalizeDeviceId(app.adbSerial)
1253
+ ].filter((s) => s.length > 0);
1254
+ if (haystacks.some((h) => h.includes(normDevice))) {
1255
+ matches.push(app);
1256
+ }
1257
+ }
1258
+ // Prefer exact (case-insensitive) match when present — disambiguates
1259
+ // "iPhone 17 Pro" from "iPhone 17 Pro Max" when both are connected.
1260
+ const exact = matches.find(a => {
1261
+ const name = a.deviceInfo.deviceName || a.deviceInfo.title || "";
1262
+ return name.toLowerCase() === lowerDevice;
1263
+ });
1264
+ if (exact)
1265
+ return exact;
1266
+ if (matches.length === 1) {
1267
+ return matches[0];
1268
+ }
1269
+ if (matches.length > 1) {
1270
+ const names = matches.map(a => a.deviceInfo.deviceName || a.deviceInfo.title).join(", ");
1271
+ throw new UserInputError(`Multiple devices match "${device}": ${names}. Be more specific (use the full device name for an exact match).`);
1272
+ }
1273
+ if (allOpenNames.length === 0) {
1274
+ throw new UserInputError(`No connected device matches "${device}". No devices are currently connected — run scan_metro to discover and connect to Metro servers.`);
1275
+ }
1276
+ const quoted = allOpenNames.map(n => `"${n}"`).join(", ");
1277
+ throw new UserInputError(`No connected device matches "${device}". Connected devices: ${quoted}. Retry with one of these names (substring match, case-insensitive).`);
1278
+ }
1279
+ // Check if any app is connected with an OPEN WebSocket
1280
+ export function hasConnectedApp() {
1281
+ for (const [, app] of connectedApps.entries()) {
1282
+ if (app.ws.readyState === WebSocket.OPEN) {
1283
+ return true;
1284
+ }
1285
+ }
1286
+ return false;
1287
+ }
1288
+ /**
1289
+ * Run a quick health check to verify the page context is responsive
1290
+ * Returns true if the context can execute code, false otherwise
1291
+ */
1292
+ export async function runQuickHealthCheck(app) {
1293
+ const HEALTH_CHECK_TIMEOUT = 2000;
1294
+ const messageId = getNextMessageId();
1295
+ return new Promise((resolve) => {
1296
+ const timeoutId = setTimeout(() => {
1297
+ pendingExecutions.delete(messageId);
1298
+ resolve(false);
1299
+ }, HEALTH_CHECK_TIMEOUT);
1300
+ pendingExecutions.set(messageId, {
1301
+ resolve: (result) => {
1302
+ clearTimeout(timeoutId);
1303
+ pendingExecutions.delete(messageId);
1304
+ // Update context health tracking
1305
+ const appKey = findAppKeyForDevice(app.deviceInfo);
1306
+ if (appKey) {
1307
+ updateContextHealth(appKey, {
1308
+ lastHealthCheck: new Date(),
1309
+ lastHealthCheckSuccess: result.success,
1310
+ isStale: !result.success,
1311
+ });
1312
+ }
1313
+ resolve(result.success);
1314
+ },
1315
+ timeoutId,
1316
+ });
1317
+ try {
1318
+ app.ws.send(JSON.stringify({
1319
+ id: messageId,
1320
+ method: "Runtime.evaluate",
1321
+ params: { expression: "1+1", returnByValue: true },
1322
+ }));
1323
+ }
1324
+ catch {
1325
+ clearTimeout(timeoutId);
1326
+ pendingExecutions.delete(messageId);
1327
+ resolve(false);
1328
+ }
1329
+ });
1330
+ }
1331
+ const LOG_PIPELINE_MARKER_PREFIX = "__rn_devtools_health_";
1332
+ /**
1333
+ * Inject a sentinel console.log via CDP and wait for it to appear in the log buffer.
1334
+ * Returns true if the marker arrives within TIMEOUT_MS.
1335
+ */
1336
+ async function sendAndWaitForMarker(app, buffer) {
1337
+ const marker = `${LOG_PIPELINE_MARKER_PREFIX}${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1338
+ const TIMEOUT_MS = 3000;
1339
+ try {
1340
+ app.ws.send(JSON.stringify({
1341
+ id: getNextMessageId(),
1342
+ method: "Runtime.evaluate",
1343
+ params: { expression: `console.log("${marker}")`, returnByValue: true },
1344
+ }));
1345
+ }
1346
+ catch {
1347
+ return false;
1348
+ }
1349
+ // Poll the buffer for the marker (check every 50ms, up to TIMEOUT_MS)
1350
+ const startTime = Date.now();
1351
+ while (Date.now() - startTime < TIMEOUT_MS) {
1352
+ if (buffer.getAll().some(entry => entry.message.includes(marker))) {
1353
+ buffer.removeByText(marker);
1354
+ return true;
1355
+ }
1356
+ await new Promise(resolve => setTimeout(resolve, 50));
1357
+ }
1358
+ return false;
1359
+ }
1360
+ /**
1361
+ * End-to-end log pipeline verification with automatic recovery.
1362
+ *
1363
+ * 1. Injects a sentinel console.log via CDP and verifies it appears in the buffer.
1364
+ * 2. If the marker doesn't arrive, attempts recovery:
1365
+ * a. Re-sends Runtime.enable + Log.enable (fixes missing domain subscription)
1366
+ * b. Retries the marker once
1367
+ * 3. If still broken, force-reconnects via ensureConnection and retries.
1368
+ *
1369
+ * Returns { ok, recovered, message } — the caller decides what to show.
1370
+ */
1371
+ export async function verifyLogPipeline(app) {
1372
+ const deviceName = app.deviceInfo.deviceName || app.deviceInfo.title || "unknown";
1373
+ const buffer = getLogBuffer(deviceName);
1374
+ // --- Attempt 1: quick check ---
1375
+ if (await sendAndWaitForMarker(app, buffer)) {
1376
+ return { ok: true, recovered: false, message: null };
1377
+ }
1378
+ // --- Attempt 2: re-enable Runtime/Log domains and retry ---
1379
+ try {
1380
+ app.ws.send(JSON.stringify({ id: getNextMessageId(), method: "Runtime.enable" }));
1381
+ app.ws.send(JSON.stringify({ id: getNextMessageId(), method: "Log.enable" }));
1382
+ }
1383
+ catch {
1384
+ // WS send failed — fall through to reconnect
1385
+ }
1386
+ // Brief pause for domain activation
1387
+ await new Promise(resolve => setTimeout(resolve, 300));
1388
+ if (await sendAndWaitForMarker(app, buffer)) {
1389
+ return { ok: true, recovered: true, message: "[CONNECTION] Log pipeline was stale — re-enabled CDP domains. Logs are flowing again." };
1390
+ }
1391
+ // --- Attempt 3: force reconnect and retry ---
1392
+ const appKey = `${app.port}-${app.deviceInfo.id}`;
1393
+ const port = app.port;
1394
+ cancelReconnectionTimer(appKey);
1395
+ try {
1396
+ app.ws.close();
1397
+ }
1398
+ catch { /* ignore */ }
1399
+ connectedApps.delete(appKey);
1400
+ const result = await ensureConnection({ port, forceRefresh: false, healthCheck: true });
1401
+ if (!result.connected || !result.healthCheckPassed) {
1402
+ return {
1403
+ ok: false,
1404
+ recovered: false,
1405
+ message: "[CONNECTION] Log pipeline broken and reconnection failed. Ensure the app is running, then call scan_metro.",
1406
+ };
1407
+ }
1408
+ // Wait for the new connection to settle
1409
+ await new Promise(resolve => setTimeout(resolve, 500));
1410
+ // Get the fresh app reference after reconnection
1411
+ const freshApp = getFirstConnectedApp();
1412
+ if (!freshApp) {
1413
+ return {
1414
+ ok: false,
1415
+ recovered: false,
1416
+ message: "[CONNECTION] Reconnected but no app available. Call scan_metro.",
1417
+ };
1418
+ }
1419
+ const freshDeviceName = freshApp.deviceInfo.deviceName || freshApp.deviceInfo.title || "unknown";
1420
+ const freshBuffer = getLogBuffer(freshDeviceName);
1421
+ if (await sendAndWaitForMarker(freshApp, freshBuffer)) {
1422
+ return {
1423
+ ok: true,
1424
+ recovered: true,
1425
+ message: "[CONNECTION] Log pipeline was broken — reconnected and verified. Logs are flowing again.",
1426
+ };
1427
+ }
1428
+ return {
1429
+ ok: false,
1430
+ recovered: false,
1431
+ message: "[WARNING] Log pipeline verification failed after reconnection. Console events are not reaching the buffer. Try reload_app or scan_metro.",
1432
+ };
1433
+ }
1434
+ /**
1435
+ * Check if a log message is a health check marker (for filtering from output).
1436
+ */
1437
+ export function isHealthCheckMarker(message) {
1438
+ return message.includes(LOG_PIPELINE_MARKER_PREFIX);
1439
+ }
1440
+ /**
1441
+ * Find the first available Metro port
1442
+ */
1443
+ async function findFirstMetroPort() {
1444
+ const ports = await scanMetroPorts();
1445
+ return ports.length > 0 ? ports[0] : null;
1446
+ }
1447
+ /**
1448
+ * Ensure a healthy connection to a React Native app
1449
+ * This will verify or establish a connection, optionally running a health check
1450
+ */
1451
+ export async function ensureConnection(options = {}) {
1452
+ const { port, healthCheck = true, forceRefresh = false } = options;
1453
+ let wasReconnected = false;
1454
+ // forceRefresh: close every currently-connected app, then fall through to
1455
+ // single-device fresh connect. Full multi-device re-discovery is scan_metro's
1456
+ // job — ensureConnection only fresh-connects one when nothing is open.
1457
+ if (forceRefresh) {
1458
+ for (const { key, app } of getConnectedApps()) {
1459
+ cancelReconnectionTimer(key);
1460
+ try {
1461
+ app.ws.close();
1462
+ }
1463
+ catch { /* ignore */ }
1464
+ connectedApps.delete(key);
1465
+ }
1466
+ }
1467
+ let openApps = getConnectedApps()
1468
+ .filter(a => a.isConnected)
1469
+ .map(a => a.app);
1470
+ // Nothing connected — establish a single-device connection from scratch.
1471
+ if (openApps.length === 0) {
1472
+ const targetPort = port ?? await findFirstMetroPort();
1473
+ if (!targetPort) {
1474
+ return {
1475
+ connected: false,
1476
+ wasReconnected: false,
1477
+ healthCheckPassed: false,
1478
+ connectionInfos: [],
1479
+ error: "No Metro server found. Make sure Metro bundler is running.",
1480
+ };
1481
+ }
1482
+ const devices = await fetchDevices(targetPort);
1483
+ const mainDevice = selectMainDevice(devices);
1484
+ if (!mainDevice) {
1485
+ return {
1486
+ connected: false,
1487
+ wasReconnected: false,
1488
+ healthCheckPassed: false,
1489
+ connectionInfos: [],
1490
+ error: `No debuggable devices found on port ${targetPort}. Make sure the app is running.`,
1491
+ };
1492
+ }
1493
+ try {
1494
+ // Clear any prior reconnection suppression — without this a stale
1495
+ // suppression entry from a previous disconnect_metro keeps the
1496
+ // close-handler from auto-recovering if Metro drops the WS just
1497
+ // after probe.
1498
+ clearReconnectionSuppression();
1499
+ const connectResult = await connectToDevice(mainDevice, targetPort);
1500
+ wasReconnected = true;
1501
+ if (connectResult.includes("stale CDP target")) {
1502
+ return {
1503
+ connected: false,
1504
+ wasReconnected: false,
1505
+ healthCheckPassed: false,
1506
+ connectionInfos: [],
1507
+ error: `${connectResult}. The CDP page advertised by Metro is no longer responsive — restart the React Native app, then retry.`,
1508
+ };
1509
+ }
1510
+ // Race window: settle briefly and retry getting the open app list.
1511
+ for (let i = 0; i < 5 && openApps.length === 0; i++) {
1512
+ if (i > 0)
1513
+ await new Promise(resolve => setTimeout(resolve, 100));
1514
+ openApps = getConnectedApps().filter(a => a.isConnected).map(a => a.app);
1515
+ }
1516
+ }
1517
+ catch (error) {
1518
+ let errorMessage;
1519
+ if (error instanceof Error) {
1520
+ errorMessage = error.message;
1521
+ }
1522
+ else if (error !== undefined && error !== null) {
1523
+ errorMessage = String(error);
1524
+ }
1525
+ else {
1526
+ errorMessage = "WebSocket connection failed with no error details";
1527
+ }
1528
+ return {
1529
+ connected: false,
1530
+ wasReconnected: false,
1531
+ healthCheckPassed: false,
1532
+ connectionInfos: [],
1533
+ error: `Connection failed: ${errorMessage}`,
1534
+ };
1535
+ }
1536
+ }
1537
+ if (openApps.length === 0) {
1538
+ return {
1539
+ connected: false,
1540
+ wasReconnected: false,
1541
+ healthCheckPassed: false,
1542
+ connectionInfos: [],
1543
+ error: "Connection succeeded but app is not available",
1544
+ };
1545
+ }
1546
+ // Per-device health-check; reconnect failed devices individually so a dead
1547
+ // Android doesn't take down a healthy iOS report and vice versa.
1548
+ const perApp = [];
1549
+ for (const candidate of openApps) {
1550
+ let app = candidate;
1551
+ let healthy = true;
1552
+ if (healthCheck) {
1553
+ healthy = await runQuickHealthCheck(app);
1554
+ if (!healthy && !wasReconnected) {
1555
+ const failedTitle = app.deviceInfo.title;
1556
+ console.error(`[execbro] Health check failed for ${failedTitle}, attempting reconnection...`);
1557
+ const appKey = `${app.port}-${app.deviceInfo.id}`;
1558
+ const appPort = app.port;
1559
+ cancelReconnectionTimer(appKey);
1560
+ try {
1561
+ app.ws.close();
1562
+ }
1563
+ catch { /* ignore */ }
1564
+ connectedApps.delete(appKey);
1565
+ const devices = await fetchDevices(appPort);
1566
+ const mainDevice = selectMainDevice(devices);
1567
+ if (mainDevice) {
1568
+ try {
1569
+ await connectToDevice(mainDevice, appPort);
1570
+ wasReconnected = true;
1571
+ const fresh = getConnectedApps()
1572
+ .filter(a => a.isConnected)
1573
+ .map(a => a.app);
1574
+ const replacement = fresh.find(a => a.deviceInfo.id === mainDevice.id);
1575
+ if (replacement) {
1576
+ app = replacement;
1577
+ healthy = await runQuickHealthCheck(app);
1578
+ }
1579
+ else {
1580
+ healthy = false;
1581
+ }
1582
+ }
1583
+ catch {
1584
+ healthy = false;
1585
+ }
1586
+ }
1587
+ else {
1588
+ healthy = false;
1589
+ }
1590
+ }
1591
+ }
1592
+ perApp.push({ app, healthy });
1593
+ }
1594
+ const connectionInfos = perApp.map(({ app, healthy }) => {
1595
+ const appKey = `${app.port}-${app.deviceInfo.id}`;
1596
+ const connectionState = getConnectionState(appKey);
1597
+ const contextHealth = getContextHealth(appKey);
1598
+ let uptime = "unknown";
1599
+ if (connectionState?.lastConnectedTime) {
1600
+ uptime = formatDuration(Date.now() - connectionState.lastConnectedTime.getTime());
1601
+ }
1602
+ return {
1603
+ deviceName: app.deviceInfo.deviceName || app.deviceInfo.title,
1604
+ deviceTitle: app.deviceInfo.title,
1605
+ platform: app.platform,
1606
+ port: app.port,
1607
+ uptime,
1608
+ contextId: contextHealth?.contextId ?? null,
1609
+ healthCheckPassed: healthy,
1610
+ };
1611
+ });
1612
+ const anyOpen = perApp.some(r => r.app.ws.readyState === WebSocket.OPEN);
1613
+ const allHealthy = perApp.every(r => r.healthy);
1614
+ return {
1615
+ connected: anyOpen,
1616
+ wasReconnected,
1617
+ healthCheckPassed: allHealthy,
1618
+ connectionInfos,
1619
+ ...(!anyOpen && {
1620
+ error: "App became unavailable after reconnection attempt. Try running scan_metro then ensure_connection."
1621
+ }),
1622
+ };
1623
+ }
1624
+ export function getPassiveConnectionStatus(targetAppKey) {
1625
+ if (targetAppKey) {
1626
+ // Check a specific device's connection
1627
+ const app = connectedApps.get(targetAppKey);
1628
+ if (!app || app.ws.readyState !== WebSocket.OPEN) {
1629
+ return { connected: false, needsPing: false, reason: "no_connection" };
1630
+ }
1631
+ const health = getContextHealth(targetAppKey);
1632
+ if (health?.isStale) {
1633
+ return { connected: false, needsPing: false, reason: "context_stale" };
1634
+ }
1635
+ const lastMessage = getLastCDPMessageTime(targetAppKey);
1636
+ if (!lastMessage) {
1637
+ return { connected: false, needsPing: false, reason: "no_activity" };
1638
+ }
1639
+ const elapsed = Date.now() - lastMessage.getTime();
1640
+ if (elapsed > STALE_ACTIVITY_THRESHOLD_MS) {
1641
+ return { connected: true, needsPing: true, reason: "activity_stale" };
1642
+ }
1643
+ return { connected: true, needsPing: false, reason: "ok" };
1644
+ }
1645
+ // Default: check first connected app (backwards compatible)
1646
+ if (!hasConnectedApp()) {
1647
+ return { connected: false, needsPing: false, reason: "no_connection" };
1648
+ }
1649
+ const app = getFirstConnectedApp();
1650
+ if (app) {
1651
+ const appKey = `${app.port}-${app.deviceInfo.id}`;
1652
+ const health = getContextHealth(appKey);
1653
+ if (health?.isStale) {
1654
+ return { connected: false, needsPing: false, reason: "context_stale" };
1655
+ }
1656
+ }
1657
+ const lastMessage = getLastCDPMessageTime();
1658
+ if (!lastMessage) {
1659
+ return { connected: false, needsPing: false, reason: "no_activity" };
1660
+ }
1661
+ const elapsed = Date.now() - lastMessage.getTime();
1662
+ if (elapsed > STALE_ACTIVITY_THRESHOLD_MS) {
1663
+ return { connected: true, needsPing: true, reason: "activity_stale" };
1664
+ }
1665
+ return { connected: true, needsPing: false, reason: "ok" };
1666
+ }
1667
+ export async function checkAndEnsureConnection(device) {
1668
+ // Resolve targeted device to appKey if specified
1669
+ let targetAppKey;
1670
+ let targetApp = null;
1671
+ if (device) {
1672
+ try {
1673
+ targetApp = getConnectedAppByDevice(device);
1674
+ if (targetApp) {
1675
+ targetAppKey = `${targetApp.port}-${targetApp.deviceInfo.id}`;
1676
+ }
1677
+ }
1678
+ catch {
1679
+ // Device not found — fall through to reconnection
1680
+ }
1681
+ }
1682
+ const passive = getPassiveConnectionStatus(targetAppKey);
1683
+ if (passive.connected && !passive.needsPing) {
1684
+ return { connected: true, wasReconnected: false, message: null };
1685
+ }
1686
+ if (passive.connected && passive.needsPing) {
1687
+ const app = targetApp ?? getFirstConnectedApp();
1688
+ if (app) {
1689
+ const healthy = await runQuickHealthCheck(app);
1690
+ if (healthy) {
1691
+ return { connected: true, wasReconnected: false, message: null };
1692
+ }
1693
+ }
1694
+ }
1695
+ const result = await ensureConnection({ forceRefresh: true, healthCheck: true });
1696
+ if (result.connected && result.healthCheckPassed) {
1697
+ await new Promise(resolve => setTimeout(resolve, RECONNECT_SETTLE_MS));
1698
+ return {
1699
+ connected: true,
1700
+ wasReconnected: true,
1701
+ message: "[CONNECTION] Was stale, re-established. Earlier data may be incomplete; new data will appear on next call.",
1702
+ };
1703
+ }
1704
+ return {
1705
+ connected: false,
1706
+ wasReconnected: false,
1707
+ message: "[CONNECTION] No active connection. Could not reconnect. Ensure Metro and the app are running, then call scan_metro.",
1708
+ };
1709
+ }
1710
+ //# sourceMappingURL=connection.js.map