devharness 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (627) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +185 -0
  3. package/build/atomic-write.d.ts +28 -0
  4. package/build/atomic-write.d.ts.map +1 -0
  5. package/build/atomic-write.js +126 -0
  6. package/build/atomic-write.js.map +1 -0
  7. package/build/cdp-manager.d.ts +524 -0
  8. package/build/cdp-manager.d.ts.map +1 -0
  9. package/build/cdp-manager.js +1828 -0
  10. package/build/cdp-manager.js.map +1 -0
  11. package/build/cdp-manager.test.d.ts +6 -0
  12. package/build/cdp-manager.test.d.ts.map +1 -0
  13. package/build/cdp-manager.test.js +546 -0
  14. package/build/cdp-manager.test.js.map +1 -0
  15. package/build/chrome-launcher.d.ts +400 -0
  16. package/build/chrome-launcher.d.ts.map +1 -0
  17. package/build/chrome-launcher.js +1071 -0
  18. package/build/chrome-launcher.js.map +1 -0
  19. package/build/chrome-launcher.test.d.ts +11 -0
  20. package/build/chrome-launcher.test.d.ts.map +1 -0
  21. package/build/chrome-launcher.test.js +204 -0
  22. package/build/chrome-launcher.test.js.map +1 -0
  23. package/build/clickable-cache.d.ts +73 -0
  24. package/build/clickable-cache.d.ts.map +1 -0
  25. package/build/clickable-cache.js +90 -0
  26. package/build/clickable-cache.js.map +1 -0
  27. package/build/command-recorder.d.ts +401 -0
  28. package/build/command-recorder.d.ts.map +1 -0
  29. package/build/command-recorder.js +701 -0
  30. package/build/command-recorder.js.map +1 -0
  31. package/build/config.d.ts +390 -0
  32. package/build/config.d.ts.map +1 -0
  33. package/build/config.js +826 -0
  34. package/build/config.js.map +1 -0
  35. package/build/config.test.d.ts +2 -0
  36. package/build/config.test.d.ts.map +1 -0
  37. package/build/config.test.js +112 -0
  38. package/build/config.test.js.map +1 -0
  39. package/build/connection-manager.d.ts +159 -0
  40. package/build/connection-manager.d.ts.map +1 -0
  41. package/build/connection-manager.js +432 -0
  42. package/build/connection-manager.js.map +1 -0
  43. package/build/connection-manager.test.d.ts +11 -0
  44. package/build/connection-manager.test.d.ts.map +1 -0
  45. package/build/connection-manager.test.js +114 -0
  46. package/build/connection-manager.test.js.map +1 -0
  47. package/build/console-monitor.d.ts +150 -0
  48. package/build/console-monitor.d.ts.map +1 -0
  49. package/build/console-monitor.js +454 -0
  50. package/build/console-monitor.js.map +1 -0
  51. package/build/dashboard/bundle.js +1 -0
  52. package/build/dashboard/dashboard-client.d.ts +42 -0
  53. package/build/dashboard/dashboard-client.d.ts.map +1 -0
  54. package/build/dashboard/dashboard-client.js +184 -0
  55. package/build/dashboard/dashboard-client.js.map +1 -0
  56. package/build/dashboard/dashboard-hub.d.ts +50 -0
  57. package/build/dashboard/dashboard-hub.d.ts.map +1 -0
  58. package/build/dashboard/dashboard-hub.js +654 -0
  59. package/build/dashboard/dashboard-hub.js.map +1 -0
  60. package/build/dashboard/hub-lock.d.ts +16 -0
  61. package/build/dashboard/hub-lock.d.ts.map +1 -0
  62. package/build/dashboard/hub-lock.js +77 -0
  63. package/build/dashboard/hub-lock.js.map +1 -0
  64. package/build/dashboard/index.d.ts +25 -0
  65. package/build/dashboard/index.d.ts.map +1 -0
  66. package/build/dashboard/index.js +77 -0
  67. package/build/dashboard/index.js.map +1 -0
  68. package/build/dashboard/types.d.ts +83 -0
  69. package/build/dashboard/types.d.ts.map +1 -0
  70. package/build/dashboard/types.js +6 -0
  71. package/build/dashboard/types.js.map +1 -0
  72. package/build/debug-logger.d.ts +85 -0
  73. package/build/debug-logger.d.ts.map +1 -0
  74. package/build/debug-logger.js +195 -0
  75. package/build/debug-logger.js.map +1 -0
  76. package/build/debugger-aware-wrapper.d.ts +37 -0
  77. package/build/debugger-aware-wrapper.d.ts.map +1 -0
  78. package/build/debugger-aware-wrapper.js +128 -0
  79. package/build/debugger-aware-wrapper.js.map +1 -0
  80. package/build/dom-change-monitor.d.ts +92 -0
  81. package/build/dom-change-monitor.d.ts.map +1 -0
  82. package/build/dom-change-monitor.js +437 -0
  83. package/build/dom-change-monitor.js.map +1 -0
  84. package/build/element-collector.d.ts +16 -0
  85. package/build/element-collector.d.ts.map +1 -0
  86. package/build/element-collector.js +207 -0
  87. package/build/element-collector.js.map +1 -0
  88. package/build/error-helpers.d.ts +43 -0
  89. package/build/error-helpers.d.ts.map +1 -0
  90. package/build/error-helpers.js +143 -0
  91. package/build/error-helpers.js.map +1 -0
  92. package/build/formatters/console-formatter.d.ts +160 -0
  93. package/build/formatters/console-formatter.d.ts.map +1 -0
  94. package/build/formatters/console-formatter.js +338 -0
  95. package/build/formatters/console-formatter.js.map +1 -0
  96. package/build/helpers/parser-plugins.d.ts +44 -0
  97. package/build/helpers/parser-plugins.d.ts.map +1 -0
  98. package/build/helpers/parser-plugins.js +102 -0
  99. package/build/helpers/parser-plugins.js.map +1 -0
  100. package/build/helpers/paths.d.ts +79 -0
  101. package/build/helpers/paths.d.ts.map +1 -0
  102. package/build/helpers/paths.js +168 -0
  103. package/build/helpers/paths.js.map +1 -0
  104. package/build/helpers/process-liveness.d.ts +24 -0
  105. package/build/helpers/process-liveness.d.ts.map +1 -0
  106. package/build/helpers/process-liveness.js +36 -0
  107. package/build/helpers/process-liveness.js.map +1 -0
  108. package/build/helpers/process-liveness.test.d.ts +2 -0
  109. package/build/helpers/process-liveness.test.d.ts.map +1 -0
  110. package/build/helpers/process-liveness.test.js +40 -0
  111. package/build/helpers/process-liveness.test.js.map +1 -0
  112. package/build/helpers/sequence-tree.d.ts +27 -0
  113. package/build/helpers/sequence-tree.d.ts.map +1 -0
  114. package/build/helpers/sequence-tree.js +80 -0
  115. package/build/helpers/sequence-tree.js.map +1 -0
  116. package/build/helpers/sequence-tree.test.d.ts +2 -0
  117. package/build/helpers/sequence-tree.test.d.ts.map +1 -0
  118. package/build/helpers/sequence-tree.test.js +122 -0
  119. package/build/helpers/sequence-tree.test.js.map +1 -0
  120. package/build/index.d.ts +3 -0
  121. package/build/index.d.ts.map +1 -0
  122. package/build/index.js +2007 -0
  123. package/build/index.js.map +1 -0
  124. package/build/interaction-recorder.d.ts +214 -0
  125. package/build/interaction-recorder.d.ts.map +1 -0
  126. package/build/interaction-recorder.js +2007 -0
  127. package/build/interaction-recorder.js.map +1 -0
  128. package/build/issue-tracker.d.ts +107 -0
  129. package/build/issue-tracker.d.ts.map +1 -0
  130. package/build/issue-tracker.js +582 -0
  131. package/build/issue-tracker.js.map +1 -0
  132. package/build/issue-tracker.test.d.ts +2 -0
  133. package/build/issue-tracker.test.d.ts.map +1 -0
  134. package/build/issue-tracker.test.js +88 -0
  135. package/build/issue-tracker.test.js.map +1 -0
  136. package/build/launch-chrome-port.test.d.ts +14 -0
  137. package/build/launch-chrome-port.test.d.ts.map +1 -0
  138. package/build/launch-chrome-port.test.js +97 -0
  139. package/build/launch-chrome-port.test.js.map +1 -0
  140. package/build/log-processor/orchestrator.d.ts +32 -0
  141. package/build/log-processor/orchestrator.d.ts.map +1 -0
  142. package/build/log-processor/orchestrator.js +27 -0
  143. package/build/log-processor/orchestrator.js.map +1 -0
  144. package/build/logpoint-execution-tracker.d.ts +57 -0
  145. package/build/logpoint-execution-tracker.d.ts.map +1 -0
  146. package/build/logpoint-execution-tracker.js +112 -0
  147. package/build/logpoint-execution-tracker.js.map +1 -0
  148. package/build/mcp-supervisor.d.ts +3 -0
  149. package/build/mcp-supervisor.d.ts.map +1 -0
  150. package/build/mcp-supervisor.js +211 -0
  151. package/build/mcp-supervisor.js.map +1 -0
  152. package/build/messages.d.ts +165 -0
  153. package/build/messages.d.ts.map +1 -0
  154. package/build/messages.js +574 -0
  155. package/build/messages.js.map +1 -0
  156. package/build/network-monitor.d.ts +172 -0
  157. package/build/network-monitor.d.ts.map +1 -0
  158. package/build/network-monitor.js +350 -0
  159. package/build/network-monitor.js.map +1 -0
  160. package/build/overlays.d.ts +49 -0
  161. package/build/overlays.d.ts.map +1 -0
  162. package/build/overlays.js +409 -0
  163. package/build/overlays.js.map +1 -0
  164. package/build/persistent-profiles.test.d.ts +15 -0
  165. package/build/persistent-profiles.test.d.ts.map +1 -0
  166. package/build/persistent-profiles.test.js +470 -0
  167. package/build/persistent-profiles.test.js.map +1 -0
  168. package/build/port-reserver.d.ts +32 -0
  169. package/build/port-reserver.d.ts.map +1 -0
  170. package/build/port-reserver.js +118 -0
  171. package/build/port-reserver.js.map +1 -0
  172. package/build/puppeteer-manager.d.ts +47 -0
  173. package/build/puppeteer-manager.d.ts.map +1 -0
  174. package/build/puppeteer-manager.js +116 -0
  175. package/build/puppeteer-manager.js.map +1 -0
  176. package/build/recording-coverage.test.d.ts +12 -0
  177. package/build/recording-coverage.test.d.ts.map +1 -0
  178. package/build/recording-coverage.test.js +72 -0
  179. package/build/recording-coverage.test.js.map +1 -0
  180. package/build/reference-validator.d.ts +47 -0
  181. package/build/reference-validator.d.ts.map +1 -0
  182. package/build/reference-validator.js +86 -0
  183. package/build/reference-validator.js.map +1 -0
  184. package/build/reference-validator.test.d.ts +5 -0
  185. package/build/reference-validator.test.d.ts.map +1 -0
  186. package/build/reference-validator.test.js +243 -0
  187. package/build/reference-validator.test.js.map +1 -0
  188. package/build/replay-cursor.d.ts +27 -0
  189. package/build/replay-cursor.d.ts.map +1 -0
  190. package/build/replay-cursor.js +240 -0
  191. package/build/replay-cursor.js.map +1 -0
  192. package/build/runners/docker-compose-runner.d.ts +69 -0
  193. package/build/runners/docker-compose-runner.d.ts.map +1 -0
  194. package/build/runners/docker-compose-runner.js +438 -0
  195. package/build/runners/docker-compose-runner.js.map +1 -0
  196. package/build/runners/docker-runner.d.ts +65 -0
  197. package/build/runners/docker-runner.d.ts.map +1 -0
  198. package/build/runners/docker-runner.js +414 -0
  199. package/build/runners/docker-runner.js.map +1 -0
  200. package/build/runners/index.d.ts +19 -0
  201. package/build/runners/index.d.ts.map +1 -0
  202. package/build/runners/index.js +35 -0
  203. package/build/runners/index.js.map +1 -0
  204. package/build/runners/native-runner.d.ts +95 -0
  205. package/build/runners/native-runner.d.ts.map +1 -0
  206. package/build/runners/native-runner.js +370 -0
  207. package/build/runners/native-runner.js.map +1 -0
  208. package/build/runners/types.d.ts +147 -0
  209. package/build/runners/types.d.ts.map +1 -0
  210. package/build/runners/types.js +25 -0
  211. package/build/runners/types.js.map +1 -0
  212. package/build/self-restart.d.ts +12 -0
  213. package/build/self-restart.d.ts.map +1 -0
  214. package/build/self-restart.js +41 -0
  215. package/build/self-restart.js.map +1 -0
  216. package/build/self-restart.test.d.ts +2 -0
  217. package/build/self-restart.test.d.ts.map +1 -0
  218. package/build/self-restart.test.js +34 -0
  219. package/build/self-restart.test.js.map +1 -0
  220. package/build/server-claims.d.ts +161 -0
  221. package/build/server-claims.d.ts.map +1 -0
  222. package/build/server-claims.js +382 -0
  223. package/build/server-claims.js.map +1 -0
  224. package/build/server-claims.test.d.ts +2 -0
  225. package/build/server-claims.test.d.ts.map +1 -0
  226. package/build/server-claims.test.js +197 -0
  227. package/build/server-claims.test.js.map +1 -0
  228. package/build/server-manager.d.ts +438 -0
  229. package/build/server-manager.d.ts.map +1 -0
  230. package/build/server-manager.js +1674 -0
  231. package/build/server-manager.js.map +1 -0
  232. package/build/server-manager.test.d.ts +17 -0
  233. package/build/server-manager.test.d.ts.map +1 -0
  234. package/build/server-manager.test.js +122 -0
  235. package/build/server-manager.test.js.map +1 -0
  236. package/build/server-ownership.test.d.ts +2 -0
  237. package/build/server-ownership.test.d.ts.map +1 -0
  238. package/build/server-ownership.test.js +238 -0
  239. package/build/server-ownership.test.js.map +1 -0
  240. package/build/server-watcher.d.ts +21 -0
  241. package/build/server-watcher.d.ts.map +1 -0
  242. package/build/server-watcher.js +105 -0
  243. package/build/server-watcher.js.map +1 -0
  244. package/build/session-detector.d.ts +47 -0
  245. package/build/session-detector.d.ts.map +1 -0
  246. package/build/session-detector.js +229 -0
  247. package/build/session-detector.js.map +1 -0
  248. package/build/session-detector.test.d.ts +9 -0
  249. package/build/session-detector.test.d.ts.map +1 -0
  250. package/build/session-detector.test.js +45 -0
  251. package/build/session-detector.test.js.map +1 -0
  252. package/build/sourcemap-handler.d.ts +114 -0
  253. package/build/sourcemap-handler.d.ts.map +1 -0
  254. package/build/sourcemap-handler.js +479 -0
  255. package/build/sourcemap-handler.js.map +1 -0
  256. package/build/supervisor/child-manager.d.ts +43 -0
  257. package/build/supervisor/child-manager.d.ts.map +1 -0
  258. package/build/supervisor/child-manager.js +148 -0
  259. package/build/supervisor/child-manager.js.map +1 -0
  260. package/build/supervisor/child-manager.test.d.ts +2 -0
  261. package/build/supervisor/child-manager.test.d.ts.map +1 -0
  262. package/build/supervisor/child-manager.test.js +107 -0
  263. package/build/supervisor/child-manager.test.js.map +1 -0
  264. package/build/supervisor/client-watcher.d.ts +48 -0
  265. package/build/supervisor/client-watcher.d.ts.map +1 -0
  266. package/build/supervisor/client-watcher.js +186 -0
  267. package/build/supervisor/client-watcher.js.map +1 -0
  268. package/build/supervisor/client-watcher.test.d.ts +2 -0
  269. package/build/supervisor/client-watcher.test.d.ts.map +1 -0
  270. package/build/supervisor/client-watcher.test.js +121 -0
  271. package/build/supervisor/client-watcher.test.js.map +1 -0
  272. package/build/supervisor/idle-config.d.ts +30 -0
  273. package/build/supervisor/idle-config.d.ts.map +1 -0
  274. package/build/supervisor/idle-config.js +76 -0
  275. package/build/supervisor/idle-config.js.map +1 -0
  276. package/build/supervisor/idle-config.test.d.ts +2 -0
  277. package/build/supervisor/idle-config.test.d.ts.map +1 -0
  278. package/build/supervisor/idle-config.test.js +119 -0
  279. package/build/supervisor/idle-config.test.js.map +1 -0
  280. package/build/supervisor/ndjson-reader.d.ts +47 -0
  281. package/build/supervisor/ndjson-reader.d.ts.map +1 -0
  282. package/build/supervisor/ndjson-reader.js +75 -0
  283. package/build/supervisor/ndjson-reader.js.map +1 -0
  284. package/build/supervisor/ndjson-reader.test.d.ts +2 -0
  285. package/build/supervisor/ndjson-reader.test.d.ts.map +1 -0
  286. package/build/supervisor/ndjson-reader.test.js +92 -0
  287. package/build/supervisor/ndjson-reader.test.js.map +1 -0
  288. package/build/supervisor/pidfile.d.ts +11 -0
  289. package/build/supervisor/pidfile.d.ts.map +1 -0
  290. package/build/supervisor/pidfile.js +29 -0
  291. package/build/supervisor/pidfile.js.map +1 -0
  292. package/build/supervisor/pidfile.test.d.ts +8 -0
  293. package/build/supervisor/pidfile.test.d.ts.map +1 -0
  294. package/build/supervisor/pidfile.test.js +46 -0
  295. package/build/supervisor/pidfile.test.js.map +1 -0
  296. package/build/supervisor/restart-coordinator.d.ts +73 -0
  297. package/build/supervisor/restart-coordinator.d.ts.map +1 -0
  298. package/build/supervisor/restart-coordinator.js +300 -0
  299. package/build/supervisor/restart-coordinator.js.map +1 -0
  300. package/build/supervisor/restart-coordinator.test.d.ts +2 -0
  301. package/build/supervisor/restart-coordinator.test.d.ts.map +1 -0
  302. package/build/supervisor/restart-coordinator.test.js +434 -0
  303. package/build/supervisor/restart-coordinator.test.js.map +1 -0
  304. package/build/test-support/fake-execute-tool-call.d.ts +11 -0
  305. package/build/test-support/fake-execute-tool-call.d.ts.map +1 -0
  306. package/build/test-support/fake-execute-tool-call.js +19 -0
  307. package/build/test-support/fake-execute-tool-call.js.map +1 -0
  308. package/build/tool-error.d.ts +14 -0
  309. package/build/tool-error.d.ts.map +1 -0
  310. package/build/tool-error.js +18 -0
  311. package/build/tool-error.js.map +1 -0
  312. package/build/tool-response.d.ts +293 -0
  313. package/build/tool-response.d.ts.map +1 -0
  314. package/build/tool-response.js +365 -0
  315. package/build/tool-response.js.map +1 -0
  316. package/build/tools/assert-tools.d.ts +62 -0
  317. package/build/tools/assert-tools.d.ts.map +1 -0
  318. package/build/tools/assert-tools.js +330 -0
  319. package/build/tools/assert-tools.js.map +1 -0
  320. package/build/tools/breakpoint-await-abort.test.d.ts +2 -0
  321. package/build/tools/breakpoint-await-abort.test.d.ts.map +1 -0
  322. package/build/tools/breakpoint-await-abort.test.js +124 -0
  323. package/build/tools/breakpoint-await-abort.test.js.map +1 -0
  324. package/build/tools/breakpoint-tools.d.ts +99 -0
  325. package/build/tools/breakpoint-tools.d.ts.map +1 -0
  326. package/build/tools/breakpoint-tools.js +1197 -0
  327. package/build/tools/breakpoint-tools.js.map +1 -0
  328. package/build/tools/config-tools.d.ts +58 -0
  329. package/build/tools/config-tools.d.ts.map +1 -0
  330. package/build/tools/config-tools.js +180 -0
  331. package/build/tools/config-tools.js.map +1 -0
  332. package/build/tools/connection-routing.test.d.ts +12 -0
  333. package/build/tools/connection-routing.test.d.ts.map +1 -0
  334. package/build/tools/connection-routing.test.js +206 -0
  335. package/build/tools/connection-routing.test.js.map +1 -0
  336. package/build/tools/console-tools.d.ts +84 -0
  337. package/build/tools/console-tools.d.ts.map +1 -0
  338. package/build/tools/console-tools.js +241 -0
  339. package/build/tools/console-tools.js.map +1 -0
  340. package/build/tools/content-tools.d.ts +74 -0
  341. package/build/tools/content-tools.d.ts.map +1 -0
  342. package/build/tools/content-tools.js +582 -0
  343. package/build/tools/content-tools.js.map +1 -0
  344. package/build/tools/dashboard-tools.d.ts +38 -0
  345. package/build/tools/dashboard-tools.d.ts.map +1 -0
  346. package/build/tools/dashboard-tools.js +123 -0
  347. package/build/tools/dashboard-tools.js.map +1 -0
  348. package/build/tools/dom-tools.d.ts +41 -0
  349. package/build/tools/dom-tools.d.ts.map +1 -0
  350. package/build/tools/dom-tools.js +277 -0
  351. package/build/tools/dom-tools.js.map +1 -0
  352. package/build/tools/download-tools.d.ts +55 -0
  353. package/build/tools/download-tools.d.ts.map +1 -0
  354. package/build/tools/download-tools.js +280 -0
  355. package/build/tools/download-tools.js.map +1 -0
  356. package/build/tools/execution-tools.d.ts +40 -0
  357. package/build/tools/execution-tools.d.ts.map +1 -0
  358. package/build/tools/execution-tools.js +121 -0
  359. package/build/tools/execution-tools.js.map +1 -0
  360. package/build/tools/input-abort.test.d.ts +2 -0
  361. package/build/tools/input-abort.test.d.ts.map +1 -0
  362. package/build/tools/input-abort.test.js +227 -0
  363. package/build/tools/input-abort.test.js.map +1 -0
  364. package/build/tools/input-ambiguous-selector.test.d.ts +2 -0
  365. package/build/tools/input-ambiguous-selector.test.d.ts.map +1 -0
  366. package/build/tools/input-ambiguous-selector.test.js +34 -0
  367. package/build/tools/input-ambiguous-selector.test.js.map +1 -0
  368. package/build/tools/input-tools.d.ts +168 -0
  369. package/build/tools/input-tools.d.ts.map +1 -0
  370. package/build/tools/input-tools.js +1397 -0
  371. package/build/tools/input-tools.js.map +1 -0
  372. package/build/tools/inspection-tools.d.ts +112 -0
  373. package/build/tools/inspection-tools.d.ts.map +1 -0
  374. package/build/tools/inspection-tools.js +637 -0
  375. package/build/tools/inspection-tools.js.map +1 -0
  376. package/build/tools/interpolation.d.ts +30 -0
  377. package/build/tools/interpolation.d.ts.map +1 -0
  378. package/build/tools/interpolation.js +143 -0
  379. package/build/tools/interpolation.js.map +1 -0
  380. package/build/tools/issues-abort-hygiene.test.d.ts +2 -0
  381. package/build/tools/issues-abort-hygiene.test.d.ts.map +1 -0
  382. package/build/tools/issues-abort-hygiene.test.js +79 -0
  383. package/build/tools/issues-abort-hygiene.test.js.map +1 -0
  384. package/build/tools/issues-tools.d.ts +98 -0
  385. package/build/tools/issues-tools.d.ts.map +1 -0
  386. package/build/tools/issues-tools.js +821 -0
  387. package/build/tools/issues-tools.js.map +1 -0
  388. package/build/tools/issues-tools.test.d.ts +8 -0
  389. package/build/tools/issues-tools.test.d.ts.map +1 -0
  390. package/build/tools/issues-tools.test.js +184 -0
  391. package/build/tools/issues-tools.test.js.map +1 -0
  392. package/build/tools/modal-tools.d.ts +73 -0
  393. package/build/tools/modal-tools.d.ts.map +1 -0
  394. package/build/tools/modal-tools.js +161 -0
  395. package/build/tools/modal-tools.js.map +1 -0
  396. package/build/tools/network-tools.d.ts +72 -0
  397. package/build/tools/network-tools.d.ts.map +1 -0
  398. package/build/tools/network-tools.js +432 -0
  399. package/build/tools/network-tools.js.map +1 -0
  400. package/build/tools/page-tools.d.ts +78 -0
  401. package/build/tools/page-tools.d.ts.map +1 -0
  402. package/build/tools/page-tools.js +255 -0
  403. package/build/tools/page-tools.js.map +1 -0
  404. package/build/tools/plugin-tools.d.ts +16 -0
  405. package/build/tools/plugin-tools.d.ts.map +1 -0
  406. package/build/tools/plugin-tools.js +17 -0
  407. package/build/tools/plugin-tools.js.map +1 -0
  408. package/build/tools/replay-abort.test.d.ts +2 -0
  409. package/build/tools/replay-abort.test.d.ts.map +1 -0
  410. package/build/tools/replay-abort.test.js +259 -0
  411. package/build/tools/replay-abort.test.js.map +1 -0
  412. package/build/tools/replay-add-conditional.test.d.ts +2 -0
  413. package/build/tools/replay-add-conditional.test.d.ts.map +1 -0
  414. package/build/tools/replay-add-conditional.test.js +205 -0
  415. package/build/tools/replay-add-conditional.test.js.map +1 -0
  416. package/build/tools/replay-background-run.test.d.ts +2 -0
  417. package/build/tools/replay-background-run.test.d.ts.map +1 -0
  418. package/build/tools/replay-background-run.test.js +176 -0
  419. package/build/tools/replay-background-run.test.js.map +1 -0
  420. package/build/tools/replay-capture-variables.test.d.ts +2 -0
  421. package/build/tools/replay-capture-variables.test.d.ts.map +1 -0
  422. package/build/tools/replay-capture-variables.test.js +331 -0
  423. package/build/tools/replay-capture-variables.test.js.map +1 -0
  424. package/build/tools/replay-conditional-idb-and-launch.test.d.ts +11 -0
  425. package/build/tools/replay-conditional-idb-and-launch.test.d.ts.map +1 -0
  426. package/build/tools/replay-conditional-idb-and-launch.test.js +429 -0
  427. package/build/tools/replay-conditional-idb-and-launch.test.js.map +1 -0
  428. package/build/tools/replay-conditional-timeout.test.d.ts +14 -0
  429. package/build/tools/replay-conditional-timeout.test.d.ts.map +1 -0
  430. package/build/tools/replay-conditional-timeout.test.js +126 -0
  431. package/build/tools/replay-conditional-timeout.test.js.map +1 -0
  432. package/build/tools/replay-declare.test.d.ts +2 -0
  433. package/build/tools/replay-declare.test.d.ts.map +1 -0
  434. package/build/tools/replay-declare.test.js +106 -0
  435. package/build/tools/replay-declare.test.js.map +1 -0
  436. package/build/tools/replay-declared-browser-cleanup.test.d.ts +2 -0
  437. package/build/tools/replay-declared-browser-cleanup.test.d.ts.map +1 -0
  438. package/build/tools/replay-declared-browser-cleanup.test.js +103 -0
  439. package/build/tools/replay-declared-browser-cleanup.test.js.map +1 -0
  440. package/build/tools/replay-declared-profile.test.d.ts +2 -0
  441. package/build/tools/replay-declared-profile.test.d.ts.map +1 -0
  442. package/build/tools/replay-declared-profile.test.js +116 -0
  443. package/build/tools/replay-declared-profile.test.js.map +1 -0
  444. package/build/tools/replay-executor.d.ts +556 -0
  445. package/build/tools/replay-executor.d.ts.map +1 -0
  446. package/build/tools/replay-executor.js +2347 -0
  447. package/build/tools/replay-executor.js.map +1 -0
  448. package/build/tools/replay-folder-save.test.d.ts +2 -0
  449. package/build/tools/replay-folder-save.test.d.ts.map +1 -0
  450. package/build/tools/replay-folder-save.test.js +63 -0
  451. package/build/tools/replay-folder-save.test.js.map +1 -0
  452. package/build/tools/replay-formatters.d.ts +148 -0
  453. package/build/tools/replay-formatters.d.ts.map +1 -0
  454. package/build/tools/replay-formatters.js +856 -0
  455. package/build/tools/replay-formatters.js.map +1 -0
  456. package/build/tools/replay-kill-chrome-on-finish.test.d.ts +2 -0
  457. package/build/tools/replay-kill-chrome-on-finish.test.d.ts.map +1 -0
  458. package/build/tools/replay-kill-chrome-on-finish.test.js +175 -0
  459. package/build/tools/replay-kill-chrome-on-finish.test.js.map +1 -0
  460. package/build/tools/replay-multi-connection.test.d.ts +2 -0
  461. package/build/tools/replay-multi-connection.test.d.ts.map +1 -0
  462. package/build/tools/replay-multi-connection.test.js +547 -0
  463. package/build/tools/replay-multi-connection.test.js.map +1 -0
  464. package/build/tools/replay-output-and-record-options.test.d.ts +2 -0
  465. package/build/tools/replay-output-and-record-options.test.d.ts.map +1 -0
  466. package/build/tools/replay-output-and-record-options.test.js +240 -0
  467. package/build/tools/replay-output-and-record-options.test.js.map +1 -0
  468. package/build/tools/replay-production-error-shape.test.d.ts +7 -0
  469. package/build/tools/replay-production-error-shape.test.d.ts.map +1 -0
  470. package/build/tools/replay-production-error-shape.test.js +93 -0
  471. package/build/tools/replay-production-error-shape.test.js.map +1 -0
  472. package/build/tools/replay-rebase.test.d.ts +2 -0
  473. package/build/tools/replay-rebase.test.d.ts.map +1 -0
  474. package/build/tools/replay-rebase.test.js +58 -0
  475. package/build/tools/replay-rebase.test.js.map +1 -0
  476. package/build/tools/replay-retry-classification.test.d.ts +9 -0
  477. package/build/tools/replay-retry-classification.test.d.ts.map +1 -0
  478. package/build/tools/replay-retry-classification.test.js +57 -0
  479. package/build/tools/replay-retry-classification.test.js.map +1 -0
  480. package/build/tools/replay-run-registry.d.ts +55 -0
  481. package/build/tools/replay-run-registry.d.ts.map +1 -0
  482. package/build/tools/replay-run-registry.js +65 -0
  483. package/build/tools/replay-run-registry.js.map +1 -0
  484. package/build/tools/replay-sequence-reload.test.d.ts +2 -0
  485. package/build/tools/replay-sequence-reload.test.d.ts.map +1 -0
  486. package/build/tools/replay-sequence-reload.test.js +82 -0
  487. package/build/tools/replay-sequence-reload.test.js.map +1 -0
  488. package/build/tools/replay-socket-scope.test.d.ts +2 -0
  489. package/build/tools/replay-socket-scope.test.d.ts.map +1 -0
  490. package/build/tools/replay-socket-scope.test.js +75 -0
  491. package/build/tools/replay-socket-scope.test.js.map +1 -0
  492. package/build/tools/replay-step-connection.test.d.ts +2 -0
  493. package/build/tools/replay-step-connection.test.d.ts.map +1 -0
  494. package/build/tools/replay-step-connection.test.js +292 -0
  495. package/build/tools/replay-step-connection.test.js.map +1 -0
  496. package/build/tools/replay-step-timeout.test.d.ts +2 -0
  497. package/build/tools/replay-step-timeout.test.d.ts.map +1 -0
  498. package/build/tools/replay-step-timeout.test.js +204 -0
  499. package/build/tools/replay-step-timeout.test.js.map +1 -0
  500. package/build/tools/replay-tags.test.d.ts +2 -0
  501. package/build/tools/replay-tags.test.d.ts.map +1 -0
  502. package/build/tools/replay-tags.test.js +99 -0
  503. package/build/tools/replay-tags.test.js.map +1 -0
  504. package/build/tools/replay-teardown-and-foreach.test.d.ts +13 -0
  505. package/build/tools/replay-teardown-and-foreach.test.d.ts.map +1 -0
  506. package/build/tools/replay-teardown-and-foreach.test.js +318 -0
  507. package/build/tools/replay-teardown-and-foreach.test.js.map +1 -0
  508. package/build/tools/replay-tool-name-validation.test.d.ts +2 -0
  509. package/build/tools/replay-tool-name-validation.test.d.ts.map +1 -0
  510. package/build/tools/replay-tool-name-validation.test.js +280 -0
  511. package/build/tools/replay-tool-name-validation.test.js.map +1 -0
  512. package/build/tools/replay-tools.d.ts +288 -0
  513. package/build/tools/replay-tools.d.ts.map +1 -0
  514. package/build/tools/replay-tools.js +2994 -0
  515. package/build/tools/replay-tools.js.map +1 -0
  516. package/build/tools/request-abort.test.d.ts +2 -0
  517. package/build/tools/request-abort.test.d.ts.map +1 -0
  518. package/build/tools/request-abort.test.js +130 -0
  519. package/build/tools/request-abort.test.js.map +1 -0
  520. package/build/tools/request-tools.d.ts +56 -0
  521. package/build/tools/request-tools.d.ts.map +1 -0
  522. package/build/tools/request-tools.js +174 -0
  523. package/build/tools/request-tools.js.map +1 -0
  524. package/build/tools/screenshot-tools.d.ts +135 -0
  525. package/build/tools/screenshot-tools.d.ts.map +1 -0
  526. package/build/tools/screenshot-tools.js +636 -0
  527. package/build/tools/screenshot-tools.js.map +1 -0
  528. package/build/tools/server-tools.d.ts +88 -0
  529. package/build/tools/server-tools.d.ts.map +1 -0
  530. package/build/tools/server-tools.js +485 -0
  531. package/build/tools/server-tools.js.map +1 -0
  532. package/build/tools/source-tools.d.ts +46 -0
  533. package/build/tools/source-tools.d.ts.map +1 -0
  534. package/build/tools/source-tools.js +47 -0
  535. package/build/tools/source-tools.js.map +1 -0
  536. package/build/tools/storage-tools.d.ts +141 -0
  537. package/build/tools/storage-tools.d.ts.map +1 -0
  538. package/build/tools/storage-tools.js +894 -0
  539. package/build/tools/storage-tools.js.map +1 -0
  540. package/build/tools/storage-tools.test.d.ts +13 -0
  541. package/build/tools/storage-tools.test.d.ts.map +1 -0
  542. package/build/tools/storage-tools.test.js +707 -0
  543. package/build/tools/storage-tools.test.js.map +1 -0
  544. package/build/tools/tab-tools.d.ts +42 -0
  545. package/build/tools/tab-tools.d.ts.map +1 -0
  546. package/build/tools/tab-tools.js +296 -0
  547. package/build/tools/tab-tools.js.map +1 -0
  548. package/build/tools/wait-tools.d.ts +85 -0
  549. package/build/tools/wait-tools.d.ts.map +1 -0
  550. package/build/tools/wait-tools.js +236 -0
  551. package/build/tools/wait-tools.js.map +1 -0
  552. package/build/tools/wait-tools.test.d.ts +9 -0
  553. package/build/tools/wait-tools.test.d.ts.map +1 -0
  554. package/build/tools/wait-tools.test.js +193 -0
  555. package/build/tools/wait-tools.test.js.map +1 -0
  556. package/build/types.d.ts +133 -0
  557. package/build/types.d.ts.map +1 -0
  558. package/build/types.js +5 -0
  559. package/build/types.js.map +1 -0
  560. package/build/ui-verifier.d.ts +88 -0
  561. package/build/ui-verifier.d.ts.map +1 -0
  562. package/build/ui-verifier.js +540 -0
  563. package/build/ui-verifier.js.map +1 -0
  564. package/build/utils/abort.d.ts +68 -0
  565. package/build/utils/abort.d.ts.map +1 -0
  566. package/build/utils/abort.js +159 -0
  567. package/build/utils/abort.js.map +1 -0
  568. package/build/utils/abort.test.d.ts +2 -0
  569. package/build/utils/abort.test.d.ts.map +1 -0
  570. package/build/utils/abort.test.js +174 -0
  571. package/build/utils/abort.test.js.map +1 -0
  572. package/build/utils/modal-detection-core.d.ts +150 -0
  573. package/build/utils/modal-detection-core.d.ts.map +1 -0
  574. package/build/utils/modal-detection-core.js +332 -0
  575. package/build/utils/modal-detection-core.js.map +1 -0
  576. package/build/utils/modal-detection-core.test.d.ts +5 -0
  577. package/build/utils/modal-detection-core.test.d.ts.map +1 -0
  578. package/build/utils/modal-detection-core.test.js +492 -0
  579. package/build/utils/modal-detection-core.test.js.map +1 -0
  580. package/build/utils/modal-detector.d.ts +29 -0
  581. package/build/utils/modal-detector.d.ts.map +1 -0
  582. package/build/utils/modal-detector.js +281 -0
  583. package/build/utils/modal-detector.js.map +1 -0
  584. package/build/utils/modal-dismissal.d.ts +32 -0
  585. package/build/utils/modal-dismissal.d.ts.map +1 -0
  586. package/build/utils/modal-dismissal.js +190 -0
  587. package/build/utils/modal-dismissal.js.map +1 -0
  588. package/build/utils/port-check.d.ts +31 -0
  589. package/build/utils/port-check.d.ts.map +1 -0
  590. package/build/utils/port-check.js +87 -0
  591. package/build/utils/port-check.js.map +1 -0
  592. package/build/utils/port-check.test.d.ts +2 -0
  593. package/build/utils/port-check.test.d.ts.map +1 -0
  594. package/build/utils/port-check.test.js +66 -0
  595. package/build/utils/port-check.test.js.map +1 -0
  596. package/build/utils/selector-resolver.d.ts +66 -0
  597. package/build/utils/selector-resolver.d.ts.map +1 -0
  598. package/build/utils/selector-resolver.js +200 -0
  599. package/build/utils/selector-resolver.js.map +1 -0
  600. package/build/validation-helpers.d.ts +41 -0
  601. package/build/validation-helpers.d.ts.map +1 -0
  602. package/build/validation-helpers.js +236 -0
  603. package/build/validation-helpers.js.map +1 -0
  604. package/build/window-sizing.d.ts +26 -0
  605. package/build/window-sizing.d.ts.map +1 -0
  606. package/build/window-sizing.js +87 -0
  607. package/build/window-sizing.js.map +1 -0
  608. package/build/window-sizing.test.d.ts +8 -0
  609. package/build/window-sizing.test.d.ts.map +1 -0
  610. package/build/window-sizing.test.js +113 -0
  611. package/build/window-sizing.test.js.map +1 -0
  612. package/docs/README.md +74 -0
  613. package/docs/automation.md +562 -0
  614. package/docs/debugging.md +229 -0
  615. package/docs/installation.md +133 -0
  616. package/docs/instructions.md +227 -0
  617. package/docs/mcp-instructions.md +15 -0
  618. package/docs/message-system.md +327 -0
  619. package/docs/messages.md +3480 -0
  620. package/docs/parser-plugins.md +132 -0
  621. package/docs/replay.md +1411 -0
  622. package/docs/troubleshooting.md +272 -0
  623. package/docs/working_with_llm_notes.md +5 -0
  624. package/package.json +87 -0
  625. package/skills/devharness/SKILL.md +195 -0
  626. package/skills/devharness/references/sequences.md +613 -0
  627. package/skills/devharness/references/tool-categories.md +99 -0
@@ -0,0 +1,1071 @@
1
+ /**
2
+ * Chrome Launcher
3
+ * Utilities for launching Chrome with debugging enabled
4
+ */
5
+ import { spawn } from 'child_process';
6
+ import * as os from 'os';
7
+ import * as path from 'path';
8
+ import * as net from 'net';
9
+ import * as fs from 'fs';
10
+ import { randomBytes } from 'crypto';
11
+ import { debugLog } from './debug-logger.js';
12
+ /**
13
+ * Prefix used for all launcher-created temporary Chrome profile directories.
14
+ * The startup sweep only ever considers directories with this prefix.
15
+ */
16
+ export const EPHEMERAL_PROFILE_PREFIX = 'chrome-debug-profile-';
17
+ /**
18
+ * Legal characters for a named persistent profile (issue 13).
19
+ *
20
+ * Deliberately strict: the name becomes a directory under the profile root, so
21
+ * anything that could escape it (`/`, `..`, leading dot) or confuse the startup
22
+ * sweep is rejected rather than sanitised - silently renaming a profile would
23
+ * hand the caller a different identity than they asked for.
24
+ */
25
+ export const PROFILE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
26
+ /** Thrown when a profile name is not a safe single directory segment. */
27
+ export class InvalidProfileNameError extends Error {
28
+ profile;
29
+ constructor(profile) {
30
+ super(`Invalid profile name "${profile}". Use 1-64 characters: letters, digits, dot, dash or underscore, starting with a letter or digit.`);
31
+ this.profile = profile;
32
+ this.name = 'InvalidProfileNameError';
33
+ }
34
+ }
35
+ /** Thrown when an operation would disturb a profile a live Chrome is holding. */
36
+ export class ProfileInUseError extends Error {
37
+ profile;
38
+ port;
39
+ constructor(profile, port) {
40
+ super(`Profile "${profile}" is in use by the Chrome running on port ${port}.`);
41
+ this.profile = profile;
42
+ this.port = port;
43
+ this.name = 'ProfileInUseError';
44
+ }
45
+ }
46
+ /**
47
+ * Thrown when a profile is held by a Chrome this process did not launch -
48
+ * typically another cdp-tools session on the same machine, because the default
49
+ * persistent profile root (`~/.cdp-tools/profiles`) is global.
50
+ *
51
+ * Detected from the profile's Chrome `SingletonLock`, so we know the holding
52
+ * PID but not which debug port it listens on.
53
+ *
54
+ * LIMITATION: the check is POSIX-only. `SingletonLock` is a symlink on
55
+ * macOS/Linux; on Windows Chrome uses a plain lock file we cannot read a PID
56
+ * from, so this error is never raised there and the cross-session races it
57
+ * guards against remain possible.
58
+ */
59
+ export class ProfileLockedError extends Error {
60
+ profile;
61
+ pid;
62
+ dir;
63
+ constructor(profile, pid, dir) {
64
+ super(`Profile "${profile}" is locked by a Chrome started by another process (PID ${pid}, ${dir}). ` +
65
+ `Only one live Chrome may hold a profile; quit that browser (or use a different profile name) and retry.`);
66
+ this.profile = profile;
67
+ this.pid = pid;
68
+ this.dir = dir;
69
+ this.name = 'ProfileLockedError';
70
+ }
71
+ }
72
+ /**
73
+ * Validate a persistent profile name, returning it trimmed.
74
+ * @throws InvalidProfileNameError
75
+ */
76
+ export function normalizeProfileName(name) {
77
+ const trimmed = (name ?? '').trim();
78
+ if (!PROFILE_NAME_PATTERN.test(trimmed) || trimmed.startsWith(EPHEMERAL_PROFILE_PREFIX)) {
79
+ throw new InvalidProfileNameError(name);
80
+ }
81
+ return trimmed;
82
+ }
83
+ /**
84
+ * Decide which port a launchChrome call should use (bug-005).
85
+ *
86
+ * Extracted from the MCP handler so the decision is testable without a browser
87
+ * or an MCP server: src/index.ts calls main() on import, so anything left
88
+ * inline there can only be "tested" by grepping the source.
89
+ *
90
+ * Rules:
91
+ * - An explicit `port` is always honoured, never silently relocated.
92
+ * - `forceNewInstance` must produce a fresh process, so an explicit port that
93
+ * is already occupied is an error rather than a hand-off to whatever is
94
+ * listening; occupancy is only consulted in that case.
95
+ * - `forceNewInstance` without a port picks a known-free port instead of the
96
+ * reserved one, which an existing instance may already be using.
97
+ */
98
+ export async function resolveLaunchPort(req) {
99
+ const { explicitPort, forceNewInstance, reservedPort } = req;
100
+ if (!forceNewInstance) {
101
+ return { decision: 'use', port: explicitPort ?? reservedPort };
102
+ }
103
+ if (explicitPort !== undefined) {
104
+ if (await req.isPortOccupied(explicitPort)) {
105
+ return { decision: 'forced-port-in-use', port: explicitPort };
106
+ }
107
+ return { decision: 'use', port: explicitPort };
108
+ }
109
+ return { decision: 'use', port: await req.findFreePort() };
110
+ }
111
+ /**
112
+ * Decide whether a launchChrome call may reuse an existing Chrome, given the
113
+ * named profile it asked for (issue 13 / bug: profile pre-check ordering).
114
+ *
115
+ * The ordering this encodes is the whole point: a live instance already running
116
+ * the requested profile is REUSED, exactly as it would be without a profile, so
117
+ * the idempotent `launchChrome({ profile, reference })` "make sure it's up"
118
+ * pattern keeps working. "Profile in use" is only an error when the call would
119
+ * have to put a SECOND Chrome on a profile another instance holds.
120
+ */
121
+ export function decideProfileReuse(req) {
122
+ const { wantedProfileDir, existing, holderPort } = req;
123
+ if (!wantedProfileDir) {
124
+ return { decision: 'ok' };
125
+ }
126
+ if (!existing) {
127
+ // We would spawn - a holder elsewhere means a second Chrome on one profile.
128
+ return holderPort !== undefined ? { decision: 'in-use', port: holderPort } : { decision: 'ok' };
129
+ }
130
+ if (existing.profileDir === wantedProfileDir) {
131
+ return { decision: 'ok' }; // same profile, same browser - plain reuse
132
+ }
133
+ if (holderPort !== undefined && holderPort !== existing.port) {
134
+ // Reuse is out (wrong profile) and spawning is out (someone holds it).
135
+ return { decision: 'in-use', port: holderPort };
136
+ }
137
+ return { decision: 'mismatch', port: existing.port, actualProfile: existing.profileDir };
138
+ }
139
+ export class ChromeLauncher {
140
+ chromeProcesses = new Map();
141
+ launchLocks = new Map();
142
+ /** profile name -> in-flight launch for that profile (see launch()) */
143
+ profileLaunchLocks = new Map();
144
+ lastCloseEvents = [];
145
+ maxCloseEvents = 10; // Keep last 10 close events
146
+ pendingCloseReason = new Map(); // Track reason before kill
147
+ onExitCallback = null;
148
+ /** port -> profile dir currently in use by the Chrome on that port */
149
+ profileDirs = new Map();
150
+ profileRoot;
151
+ persistentProfileRootOption;
152
+ staleProfileMaxAgeMs;
153
+ /** Resolves once the startup sweep (if any) has finished. Exposed for tests. */
154
+ startupSweep;
155
+ constructor(options = {}) {
156
+ this.profileRoot = options.profileRoot ?? os.tmpdir();
157
+ this.persistentProfileRootOption =
158
+ options.persistentProfileRoot ?? path.join(os.homedir(), '.cdp-tools', 'profiles');
159
+ this.staleProfileMaxAgeMs = options.staleProfileMaxAgeMs ?? 60 * 60 * 1000;
160
+ // Sweep profiles left behind by crashed/killed sessions. Fire-and-forget:
161
+ // a failure here must never prevent the launcher from being usable.
162
+ this.startupSweep = options.sweepStaleProfilesOnStartup === false
163
+ ? Promise.resolve([])
164
+ : this.sweepStaleProfiles().catch((error) => {
165
+ debugLog('ChromeLauncher', `Startup profile sweep failed: ${error}`).catch(() => { });
166
+ return [];
167
+ });
168
+ }
169
+ /**
170
+ * Set a callback to be invoked when any Chrome process exits.
171
+ * Used for cleanup (closing stale connections) and port re-reservation.
172
+ */
173
+ setOnExitCallback(callback) {
174
+ this.onExitCallback = callback;
175
+ }
176
+ /**
177
+ * Get the Chrome executable path for the current platform
178
+ */
179
+ getChromePath() {
180
+ const platform = os.platform();
181
+ switch (platform) {
182
+ case 'darwin': // macOS
183
+ return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
184
+ case 'win32': // Windows
185
+ return 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
186
+ case 'linux':
187
+ // Try common Linux paths
188
+ return '/usr/bin/google-chrome';
189
+ default:
190
+ throw new Error(`Unsupported platform: ${platform}`);
191
+ }
192
+ }
193
+ /**
194
+ * Create Chrome preferences file that disables password manager popups
195
+ * This is required because command-line flags alone don't reliably disable
196
+ * the "Change your password" leak detection popup
197
+ */
198
+ createChromePreferences(userDataDir, overwrite = true) {
199
+ const defaultDir = path.join(userDataDir, 'Default');
200
+ const prefsPath = path.join(defaultDir, 'Preferences');
201
+ // Create Default directory if it doesn't exist
202
+ if (!fs.existsSync(defaultDir)) {
203
+ fs.mkdirSync(defaultDir, { recursive: true });
204
+ }
205
+ // Persistent/named profiles (issue 13) only get the seed once. Rewriting
206
+ // Preferences on every launch would throw away everything the profile has
207
+ // accumulated - which is the entire point of a persistent profile.
208
+ if (!overwrite && fs.existsSync(prefsPath)) {
209
+ debugLog('ChromeLauncher', `Keeping existing Chrome preferences at ${prefsPath}`);
210
+ return;
211
+ }
212
+ // Chrome preferences to disable password-related popups
213
+ const preferences = {
214
+ credentials_enable_service: false,
215
+ profile: {
216
+ password_manager_enabled: false,
217
+ password_manager_leak_detection: false,
218
+ },
219
+ safebrowsing: {
220
+ enabled: false,
221
+ enhanced: false,
222
+ },
223
+ autofill: {
224
+ profile_enabled: false,
225
+ credit_card_enabled: false,
226
+ },
227
+ };
228
+ fs.writeFileSync(prefsPath, JSON.stringify(preferences, null, 2));
229
+ debugLog('ChromeLauncher', `Created Chrome preferences at ${prefsPath} with password manager disabled`);
230
+ }
231
+ /**
232
+ * Check if a port is already in use using TCP connection
233
+ */
234
+ async isPortInUse(port) {
235
+ await debugLog('ChromeLauncher', `isPortInUse() checking port ${port}`);
236
+ return new Promise((resolve) => {
237
+ try {
238
+ const socket = new net.Socket();
239
+ socket.setTimeout(500);
240
+ socket.on('connect', () => {
241
+ socket.destroy();
242
+ debugLog('ChromeLauncher', `isPortInUse() port ${port} is in use`).catch(() => { });
243
+ resolve(true);
244
+ });
245
+ socket.on('timeout', () => {
246
+ socket.destroy();
247
+ debugLog('ChromeLauncher', `isPortInUse() timeout checking port ${port}, assuming free`).catch(() => { });
248
+ resolve(false);
249
+ });
250
+ socket.on('error', (err) => {
251
+ socket.destroy();
252
+ if (err.code === 'ECONNREFUSED') {
253
+ // Port is not in use
254
+ debugLog('ChromeLauncher', `isPortInUse() port ${port} is free (ECONNREFUSED)`).catch(() => { });
255
+ resolve(false);
256
+ }
257
+ else {
258
+ // Other error, assume port is in use to be safe
259
+ debugLog('ChromeLauncher', `isPortInUse() error checking port ${port}: ${err.code}, assuming in use`).catch(() => { });
260
+ resolve(true);
261
+ }
262
+ });
263
+ try {
264
+ socket.connect(port, 'localhost');
265
+ }
266
+ catch (connectError) {
267
+ debugLog('ChromeLauncher', `isPortInUse() socket.connect() threw for port ${port}: ${connectError}`).catch(() => { });
268
+ socket.destroy();
269
+ resolve(true); // Assume in use to be safe
270
+ }
271
+ }
272
+ catch (error) {
273
+ debugLog('ChromeLauncher', `isPortInUse() unexpected error for port ${port}: ${error}`).catch(() => { });
274
+ resolve(true); // Assume in use to be safe
275
+ }
276
+ });
277
+ }
278
+ /**
279
+ * Wait for Chrome debugging port to become ready
280
+ * Polls the /json/version endpoint until Chrome is inspectable
281
+ */
282
+ async waitForChromeReady(port, maxAttempts = 15) {
283
+ for (let i = 0; i < maxAttempts; i++) {
284
+ try {
285
+ // Use a race between fetch and timeout
286
+ const fetchPromise = fetch(`http://localhost:${port}/json/version`);
287
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 1000));
288
+ const response = await Promise.race([fetchPromise, timeoutPromise]);
289
+ if (response.ok) {
290
+ // Validate it's actually Chrome responding, not the port reserver
291
+ const text = await response.text();
292
+ if (text.trim() === 'chrome-not-running') {
293
+ await debugLog('ChromeLauncher', `Port ${port} is reserved by PortReserver, Chrome failed to bind`);
294
+ throw new Error(`Port ${port} is reserved by another MCP instance. Chrome cannot bind to this port.`);
295
+ }
296
+ // Valid Chrome response
297
+ await debugLog('ChromeLauncher', `Chrome ready on port ${port} after ${i + 1} attempts`);
298
+ return;
299
+ }
300
+ }
301
+ catch (error) {
302
+ // Re-throw port reservation errors immediately
303
+ if (error instanceof Error && error.message.includes('reserved by another MCP instance')) {
304
+ throw error;
305
+ }
306
+ // Chrome not ready yet, continue polling
307
+ // Only log every 5 attempts to reduce noise
308
+ if (i % 5 === 0) {
309
+ await debugLog('ChromeLauncher', `Waiting for Chrome on port ${port} (attempt ${i + 1}/${maxAttempts}) - error: ${error}`);
310
+ }
311
+ }
312
+ // Exponential backoff: 500ms + (attempt * 200ms)
313
+ await new Promise(resolve => setTimeout(resolve, 500 + i * 200));
314
+ }
315
+ throw new Error(`Chrome debugging port ${port} failed to become inspectable within timeout. Chrome may have crashed during startup.`);
316
+ }
317
+ /**
318
+ * Launch Chrome with debugging enabled
319
+ * Uses atomic release-and-launch to prevent race conditions
320
+ * Waits for Chrome to actually bind to the port before resolving
321
+ */
322
+ async launch(port = 9222, url, portReserver, headless = false, extraArgs = [], profileName) {
323
+ if (profileName === undefined) {
324
+ return this.launchOnPort(port, url, portReserver, headless, extraArgs, profileName);
325
+ }
326
+ // Named profiles are serialised by NAME as well as by port (bug-006 follow-up).
327
+ // The per-port lock does not help here: two launches for the same profile on
328
+ // different ports both pass the "is this profile held?" guard while the first
329
+ // Chrome is still spawning (findPortForProfile requires isRunning(), which is
330
+ // false during that window). Chrome then hands the second process off to the
331
+ // first singleton and it exits, surfacing as a generic spawn failure.
332
+ const name = normalizeProfileName(profileName);
333
+ for (let i = 0; i < 50; i++) {
334
+ const inFlight = this.profileLaunchLocks.get(name);
335
+ if (!inFlight)
336
+ break;
337
+ await debugLog('ChromeLauncher', `Another launch is in progress for profile "${name}", waiting...`);
338
+ // Its failure is its caller's problem, not ours - we just need it settled.
339
+ const result = await inFlight.then(r => r, () => undefined);
340
+ if (result && result.port === port && this.isRunning(port)) {
341
+ // Same port, same profile: this is the per-port hand-off case, so give
342
+ // both callers the one launch instead of "already running on port X".
343
+ return result;
344
+ }
345
+ }
346
+ const launchPromise = this.launchOnPort(port, url, portReserver, headless, extraArgs, name);
347
+ this.profileLaunchLocks.set(name, launchPromise);
348
+ try {
349
+ return await launchPromise;
350
+ }
351
+ finally {
352
+ if (this.profileLaunchLocks.get(name) === launchPromise) {
353
+ this.profileLaunchLocks.delete(name);
354
+ }
355
+ }
356
+ }
357
+ /**
358
+ * Port-scoped half of launch(): the per-port lock, the "already running" and
359
+ * profile-ownership guards, and the spawn itself.
360
+ */
361
+ async launchOnPort(port, url, portReserver, headless = false, extraArgs = [], profileName) {
362
+ await debugLog('ChromeLauncher', `launch() called with port ${port}, portReserver=${!!portReserver}, isReserved=${portReserver?.isReserved()}`);
363
+ // CRITICAL: Check if another launch is in progress for this port
364
+ // This prevents race conditions where two launch() calls happen simultaneously
365
+ const existingLaunch = this.launchLocks.get(port);
366
+ if (existingLaunch) {
367
+ await debugLog('ChromeLauncher', `Another launch is in progress for port ${port}, waiting for it to complete...`);
368
+ return existingLaunch; // Return the same promise, so both callers wait for the same launch
369
+ }
370
+ // Use isRunning() to verify the process is actually alive (handles external kills)
371
+ if (this.isRunning(port)) {
372
+ throw new Error(`Chrome is already running on port ${port}. Use killChrome() to stop it first, or specify a different port.`);
373
+ }
374
+ // Create a promise for this launch and store it in the lock map
375
+ // This prevents concurrent launches on the same port
376
+ // A named profile can only be held by one live Chrome - a second launch on
377
+ // the same user-data-dir is handed off to the first process and ours exits.
378
+ if (profileName !== undefined) {
379
+ const name = normalizeProfileName(profileName);
380
+ const holder = this.findPortForProfile(name);
381
+ if (holder !== undefined) {
382
+ throw new ProfileInUseError(name, holder);
383
+ }
384
+ // ...and the same profile may be held by a Chrome belonging to ANOTHER
385
+ // cdp-tools session (the persistent profile root is global by default),
386
+ // which our own maps know nothing about. Without this the launch "works",
387
+ // Chrome hands off to the existing singleton, and the caller gets an
388
+ // unexplained spawn failure. POSIX-only - see ProfileLockedError.
389
+ const lockPid = await this.findProfileLockHolder(name);
390
+ if (lockPid !== undefined) {
391
+ throw new ProfileLockedError(name, lockPid, this.getPersistentProfilePath(name));
392
+ }
393
+ }
394
+ const launchPromise = this.performLaunch(port, url, portReserver, headless, extraArgs, profileName);
395
+ this.launchLocks.set(port, launchPromise);
396
+ try {
397
+ const result = await launchPromise;
398
+ return result;
399
+ }
400
+ finally {
401
+ // Always clean up the lock when done (success or failure)
402
+ this.launchLocks.delete(port);
403
+ }
404
+ }
405
+ /**
406
+ * Internal method that performs the actual Chrome launch
407
+ * Separated from launch() to allow mutex/locking logic
408
+ */
409
+ async performLaunch(port, url, portReserver, headless = false, extraArgs = [], profileName) {
410
+ await debugLog('ChromeLauncher', `performLaunch() starting for port ${port}`);
411
+ // Check if port is in use by something OTHER than our port reserver
412
+ // This prevents multiple Chrome instances from being launched on the same port
413
+ let isPortInUse;
414
+ try {
415
+ isPortInUse = await this.isPortInUse(port);
416
+ await debugLog('ChromeLauncher', `performLaunch() isPortInUse check completed: ${isPortInUse}`);
417
+ }
418
+ catch (portCheckError) {
419
+ await debugLog('ChromeLauncher', `performLaunch() isPortInUse check failed: ${portCheckError}`);
420
+ throw new Error(`Failed to check port availability: ${portCheckError}`);
421
+ }
422
+ const isOurReservation = portReserver && portReserver.isReserved() && portReserver.getPort() === port;
423
+ if (isPortInUse && !isOurReservation) {
424
+ // Port is in use by something else (not our reservation)
425
+ throw new Error(`Port ${port} is already in use by another process or MCP instance. Please choose a different port.`);
426
+ }
427
+ if (!isPortInUse && !isOurReservation) {
428
+ // Port is free but not reserved by us - this is fine, we can use it
429
+ await debugLog('ChromeLauncher', `Port ${port} is free and not reserved, proceeding with launch`);
430
+ }
431
+ const chromePath = this.getChromePath();
432
+ // Unique by construction: the port can only be held by one live Chrome at a
433
+ // time, and the random suffix covers same-millisecond relaunches on the same
434
+ // port. Using Date.now() alone let two concurrent launches on *different*
435
+ // ports share a profile (launchLocks only serialises per-port) - bug-006.
436
+ const profile = this.createProfileRecord(port, profileName);
437
+ const userDataDir = profile.dir;
438
+ this.profileDirs.set(port, profile);
439
+ await debugLog('ChromeLauncher', `Using ${profile.ephemeral ? 'ephemeral' : 'persistent'} profile ${userDataDir} for port ${port}`);
440
+ if (!profile.ephemeral) {
441
+ // First use of a named profile: the directory does not exist yet.
442
+ await fs.promises.mkdir(userDataDir, { recursive: true });
443
+ }
444
+ // Create Chrome preferences file to disable password manager popups
445
+ // This is more reliable than command-line flags alone
446
+ this.createChromePreferences(userDataDir, profile.ephemeral);
447
+ const args = [
448
+ `--remote-debugging-port=${port}`,
449
+ `--user-data-dir=${userDataDir}`,
450
+ '--no-first-run',
451
+ '--no-default-browser-check',
452
+ '--disable-popup-blocking',
453
+ '--disable-prompt-on-repost',
454
+ '--disable-background-timer-throttling',
455
+ '--disable-backgrounding-occluded-windows',
456
+ '--disable-renderer-backgrounding',
457
+ // Disable password manager and related popups that can block automation
458
+ '--password-store=basic',
459
+ '--use-mock-keychain',
460
+ '--disable-save-password-bubble',
461
+ '--disable-features=PasswordLeakDetection,PasswordCheck,PasswordImport,PasswordManagerOnboarding',
462
+ ];
463
+ // Add headless mode if requested (prevents focus stealing)
464
+ if (headless) {
465
+ args.push('--headless=new'); // Use new headless mode
466
+ }
467
+ else {
468
+ args.push('--start-minimized'); // Launch minimized to reduce focus stealing
469
+ }
470
+ // Pass-through: any extra Chrome flags from the launchChrome call (extraArgs)
471
+ // and/or the CDP_TOOLS_EXTRA_CHROME_ARGS env var (space-separated). Merged
472
+ // after the managed defaults and before the URL (Chrome treats the trailing
473
+ // positional arg as the page to open). Lets callers enable things like a fake
474
+ // camera (--use-fake-device-for-media-stream) without patching this file.
475
+ const envExtra = (process.env.CDP_TOOLS_EXTRA_CHROME_ARGS || '').split(/\s+/).filter(Boolean);
476
+ const passthrough = [...extraArgs, ...envExtra];
477
+ if (passthrough.length) {
478
+ await debugLog('ChromeLauncher', `Passing through extra Chrome args: ${passthrough.join(' ')}`);
479
+ args.push(...passthrough);
480
+ }
481
+ if (url) {
482
+ args.push(url);
483
+ }
484
+ // Track whether we released the reservation (so we can re-reserve on failure)
485
+ let didReleaseReservation = false;
486
+ try {
487
+ // ATOMIC OPERATION: Release port reservation immediately before spawning Chrome
488
+ // This minimizes the race condition window to just a few milliseconds
489
+ if (isOurReservation) {
490
+ await debugLog('ChromeLauncher', `Releasing port reservation for ${port} immediately before spawn`);
491
+ await portReserver.release();
492
+ didReleaseReservation = true;
493
+ await debugLog('ChromeLauncher', `Port ${port} released, spawning Chrome immediately...`);
494
+ }
495
+ await debugLog('ChromeLauncher', `Spawning Chrome process on port ${port}...`);
496
+ const chromeProcess = spawn(chromePath, args, {
497
+ stdio: 'ignore',
498
+ });
499
+ const pid = chromeProcess.pid;
500
+ await debugLog('ChromeLauncher', `Chrome process spawned with PID ${pid}`);
501
+ // CRITICAL FIX: Add to tracking map IMMEDIATELY after spawn to prevent orphans
502
+ // This ensures the process is tracked even if waitForChromeReady fails
503
+ this.chromeProcesses.set(port, chromeProcess);
504
+ await debugLog('ChromeLauncher', `Added Chrome process (PID: ${pid}) to tracking map for port ${port}`);
505
+ // Set up auto-cleanup when process exits (BEFORE waitForChromeReady)
506
+ // This ensures cleanup happens even if the launch fails later
507
+ chromeProcess.once('exit', (code, signal) => {
508
+ debugLog('ChromeLauncher', `Chrome process on port ${port} (PID: ${pid}) exited (code: ${code}, signal: ${signal}), removing from tracking`);
509
+ this.chromeProcesses.delete(port);
510
+ // Record the close event with reason
511
+ const pendingReason = this.pendingCloseReason.get(port);
512
+ this.pendingCloseReason.delete(port);
513
+ let reason;
514
+ if (pendingReason) {
515
+ reason = pendingReason;
516
+ }
517
+ else if (signal) {
518
+ reason = 'signal';
519
+ }
520
+ else if (code !== 0 && code !== null) {
521
+ reason = 'crash';
522
+ }
523
+ else {
524
+ reason = 'external'; // Closed externally (user closed browser, etc.)
525
+ }
526
+ const closeEvent = this.recordCloseEvent(port, pid || -1, reason, code, signal);
527
+ // Remove the throwaway profile now that its Chrome is gone. Covers
528
+ // external closes and crashes as well as killInstance() - bug-007.
529
+ this.removeProfileDir(port, profile).catch(() => { });
530
+ // Invoke the exit callback if set (for port re-reservation)
531
+ if (this.onExitCallback) {
532
+ debugLog('ChromeLauncher', `Invoking onExit callback for port ${port}`);
533
+ // Don't await - fire and forget to avoid blocking exit handler
534
+ Promise.resolve(this.onExitCallback(closeEvent)).catch((err) => {
535
+ debugLog('ChromeLauncher', `onExit callback error: ${err}`);
536
+ });
537
+ }
538
+ });
539
+ // Handle process errors and unexpected exits
540
+ let processExited = false;
541
+ const exitHandler = () => {
542
+ processExited = true;
543
+ };
544
+ chromeProcess.once('exit', exitHandler);
545
+ chromeProcess.once('error', exitHandler);
546
+ // Wait for Chrome to actually start and bind to the port
547
+ await debugLog('ChromeLauncher', `Waiting for Chrome to become ready on port ${port}...`);
548
+ try {
549
+ await this.waitForChromeReady(port);
550
+ }
551
+ catch (waitError) {
552
+ await debugLog('ChromeLauncher', `waitForChromeReady failed: ${waitError}`);
553
+ // Re-reserve the port if we released it (cleanup after failure)
554
+ if (didReleaseReservation && portReserver) {
555
+ try {
556
+ await debugLog('ChromeLauncher', `Re-reserving port ${port} after Chrome launch failure`);
557
+ await portReserver.reserve(port);
558
+ await debugLog('ChromeLauncher', `Successfully re-reserved port ${port}`);
559
+ }
560
+ catch (reserveError) {
561
+ await debugLog('ChromeLauncher', `Failed to re-reserve port ${port}: ${reserveError}`);
562
+ }
563
+ }
564
+ // Clean up if Chrome failed to start - use SIGKILL for immediate termination
565
+ if (chromeProcess && !chromeProcess.killed && pid) {
566
+ await debugLog('ChromeLauncher', `Force killing orphaned Chrome process (PID: ${pid})`);
567
+ try {
568
+ chromeProcess.kill('SIGKILL');
569
+ // Wait a moment to ensure kill completes
570
+ await new Promise(resolve => setTimeout(resolve, 100));
571
+ }
572
+ catch (killError) {
573
+ await debugLog('ChromeLauncher', `Failed to kill orphaned process: ${killError}`);
574
+ }
575
+ }
576
+ // Remove from tracking map since we're about to throw
577
+ // (exit handler will also remove it when process dies, but this is defensive)
578
+ this.chromeProcesses.delete(port);
579
+ await this.removeProfileDir(port, profile);
580
+ throw waitError;
581
+ }
582
+ // Check if process exited during startup
583
+ if (processExited) {
584
+ // Remove from tracking since process is dead
585
+ this.chromeProcesses.delete(port);
586
+ throw new Error('Chrome process exited unexpectedly during startup');
587
+ }
588
+ // Remove temporary exit handler now that Chrome is confirmed running
589
+ // (The permanent exit handler was already set up earlier)
590
+ chromeProcess.removeListener('exit', exitHandler);
591
+ chromeProcess.removeListener('error', exitHandler);
592
+ await debugLog('ChromeLauncher', `Chrome successfully started on port ${port} with PID ${pid}`);
593
+ return { port, pid: pid || -1 };
594
+ }
595
+ catch (error) {
596
+ await debugLog('ChromeLauncher', `Failed to launch Chrome: ${error}`);
597
+ // Re-reserve the port if we released it (cleanup after any failure)
598
+ if (didReleaseReservation && portReserver) {
599
+ try {
600
+ await debugLog('ChromeLauncher', `Re-reserving port ${port} after outer catch block failure`);
601
+ await portReserver.reserve(port);
602
+ await debugLog('ChromeLauncher', `Successfully re-reserved port ${port}`);
603
+ }
604
+ catch (reserveError) {
605
+ await debugLog('ChromeLauncher', `Failed to re-reserve port ${port}: ${reserveError}`);
606
+ }
607
+ }
608
+ throw new Error(`Failed to launch Chrome: ${error}`);
609
+ }
610
+ }
611
+ /**
612
+ * Check if a process with the given PID is actually running
613
+ * This handles the case where Chrome is killed externally (e.g., Activity Monitor, kill command)
614
+ */
615
+ isProcessAlive(pid) {
616
+ try {
617
+ // process.kill with signal 0 doesn't kill the process, just checks if it exists
618
+ process.kill(pid, 0);
619
+ return true;
620
+ }
621
+ catch {
622
+ // ESRCH = No such process, EPERM = exists but no permission (still alive)
623
+ return false;
624
+ }
625
+ }
626
+ /**
627
+ * Check if Chrome is running on a specific port, or if any Chrome instance is running
628
+ * This verifies the process is actually alive, not just tracked
629
+ */
630
+ isRunning(port) {
631
+ if (port !== undefined) {
632
+ const chromeProcess = this.chromeProcesses.get(port);
633
+ if (!chromeProcess || chromeProcess.killed) {
634
+ return false;
635
+ }
636
+ // Verify the process is actually alive (handles external kills)
637
+ const pid = chromeProcess.pid;
638
+ if (!pid || !this.isProcessAlive(pid)) {
639
+ // Process is dead but we didn't know - clean up tracking
640
+ this.chromeProcesses.delete(port);
641
+ this.recordCloseEvent(port, pid || -1, 'external', null, null);
642
+ return false;
643
+ }
644
+ return true;
645
+ }
646
+ // Check if any Chrome instance is running - verify each one
647
+ for (const [p, chromeProcess] of this.chromeProcesses.entries()) {
648
+ const pid = chromeProcess.pid;
649
+ if (pid && this.isProcessAlive(pid)) {
650
+ return true;
651
+ }
652
+ else {
653
+ // Clean up dead process
654
+ this.chromeProcesses.delete(p);
655
+ this.recordCloseEvent(p, pid || -1, 'external', null, null);
656
+ }
657
+ }
658
+ return false;
659
+ }
660
+ /**
661
+ * Get all running Chrome ports
662
+ */
663
+ getRunningPorts() {
664
+ return Array.from(this.chromeProcesses.keys());
665
+ }
666
+ /**
667
+ * Kill Chrome process(es)
668
+ * If port is specified, kills only that instance. Otherwise kills all instances.
669
+ * First attempts graceful shutdown with SIGTERM, then force kills with SIGKILL if needed
670
+ */
671
+ async kill(port) {
672
+ if (port !== undefined) {
673
+ // Kill specific instance
674
+ await this.killInstance(port);
675
+ }
676
+ else {
677
+ // Kill all instances
678
+ const ports = Array.from(this.chromeProcesses.keys());
679
+ await debugLog('ChromeLauncher', `Killing all Chrome instances (${ports.length} total)`);
680
+ for (const p of ports) {
681
+ await this.killInstance(p);
682
+ }
683
+ }
684
+ }
685
+ /**
686
+ * Kill a specific Chrome instance by port
687
+ */
688
+ async killInstance(port) {
689
+ // Snapshot the profile we are killing *now*: a relaunch on this port can
690
+ // land between profileDirs.set() and chromeProcesses.set() in
691
+ // performLaunch(), and deleting whatever profile happens to be tracked by
692
+ // the time we get around to it would wipe the new launch's fresh dir.
693
+ // Passing the snapshot as `expected` makes every deletion below a no-op
694
+ // once someone else owns the port.
695
+ const profile = this.profileDirs.get(port);
696
+ const chromeProcess = this.chromeProcesses.get(port);
697
+ if (!chromeProcess || chromeProcess.killed) {
698
+ // Nothing to kill, but a profile may still be tracked (e.g. the process
699
+ // was reaped externally) - make sure it does not leak.
700
+ if (profile)
701
+ await this.removeProfileDir(port, profile);
702
+ return;
703
+ }
704
+ const pid = chromeProcess.pid;
705
+ if (!pid) {
706
+ this.chromeProcesses.delete(port);
707
+ if (profile)
708
+ await this.removeProfileDir(port, profile);
709
+ return;
710
+ }
711
+ await debugLog('ChromeLauncher', `Killing Chrome process on port ${port} (PID: ${pid})`);
712
+ // Try graceful shutdown first (SIGTERM)
713
+ try {
714
+ chromeProcess.kill('SIGTERM');
715
+ await debugLog('ChromeLauncher', `Sent SIGTERM to Chrome on port ${port} (PID: ${pid})`);
716
+ }
717
+ catch (error) {
718
+ await debugLog('ChromeLauncher', `Failed to send SIGTERM: ${error}`);
719
+ }
720
+ // Wait 500ms for graceful shutdown, then force kill if needed
721
+ await new Promise((resolve) => {
722
+ const timeout = setTimeout(() => {
723
+ try {
724
+ // Check if process still exists using signal 0 (doesn't actually kill)
725
+ process.kill(pid, 0);
726
+ // Process still exists, force kill
727
+ debugLog('ChromeLauncher', `Chrome on port ${port} didn't exit gracefully, sending SIGKILL (PID: ${pid})`);
728
+ try {
729
+ chromeProcess.kill('SIGKILL');
730
+ debugLog('ChromeLauncher', `Sent SIGKILL to Chrome on port ${port} (PID: ${pid})`);
731
+ }
732
+ catch (killError) {
733
+ debugLog('ChromeLauncher', `Failed to send SIGKILL: ${killError}`);
734
+ }
735
+ }
736
+ catch {
737
+ // Process already dead (signal 0 threw error)
738
+ debugLog('ChromeLauncher', `Chrome on port ${port} exited gracefully (PID: ${pid})`);
739
+ }
740
+ resolve();
741
+ }, 500);
742
+ // If process exits before timeout, clear the timeout
743
+ chromeProcess.once('exit', () => {
744
+ clearTimeout(timeout);
745
+ debugLog('ChromeLauncher', `Chrome process on port ${port} exited (PID: ${pid})`);
746
+ resolve();
747
+ });
748
+ });
749
+ this.chromeProcesses.delete(port);
750
+ // Chrome is gone (or was force-killed) - drop its throwaway profile,
751
+ // unless a relaunch has already claimed this port (see snapshot above).
752
+ if (profile)
753
+ await this.removeProfileDir(port, profile);
754
+ await debugLog('ChromeLauncher', `Chrome cleanup complete for port ${port}`);
755
+ }
756
+ /**
757
+ * Build the profile record for a launch on `port`.
758
+ *
759
+ * The name embeds the port (only one live Chrome can hold a debug port, so
760
+ * this alone separates concurrent launches) plus a timestamp and 4 random
761
+ * bytes (so sequential relaunches on the same port within one millisecond
762
+ * still differ). Naming on Date.now() alone let same-millisecond launches on
763
+ * different ports share a profile - bug-006.
764
+ *
765
+ * With `profileName` (issue 13) the record is a stable directory under the
766
+ * persistent profile root and is marked `ephemeral: false`, which every
767
+ * cleanup path skips. The port is deliberately NOT part of a named profile's
768
+ * directory: the profile is the identity, the port is just where this run of
769
+ * it happens to listen.
770
+ */
771
+ createProfileRecord(port, profileName) {
772
+ if (profileName !== undefined) {
773
+ return { dir: this.getPersistentProfilePath(profileName), ephemeral: false };
774
+ }
775
+ const name = `${EPHEMERAL_PROFILE_PREFIX}p${port}-${Date.now()}-${randomBytes(4).toString('hex')}`;
776
+ return { dir: path.join(this.profileRoot, name), ephemeral: true };
777
+ }
778
+ /**
779
+ * Directory named persistent profiles live under (see
780
+ * `chrome.persistentProfileRoot`; defaults to `~/.cdp-tools/profiles`).
781
+ */
782
+ getPersistentProfileRoot() {
783
+ return typeof this.persistentProfileRootOption === 'function'
784
+ ? this.persistentProfileRootOption()
785
+ : this.persistentProfileRootOption;
786
+ }
787
+ /**
788
+ * Absolute path of a named persistent profile. Does not create it.
789
+ * @throws InvalidProfileNameError
790
+ */
791
+ getPersistentProfilePath(profileName) {
792
+ return path.join(this.getPersistentProfileRoot(), normalizeProfileName(profileName));
793
+ }
794
+ /**
795
+ * Port of the live Chrome currently holding a named profile, if any.
796
+ * @throws InvalidProfileNameError
797
+ */
798
+ findPortForProfile(profileName) {
799
+ const dir = this.getPersistentProfilePath(profileName);
800
+ for (const [port, record] of this.profileDirs.entries()) {
801
+ if (record.dir === dir && this.isRunning(port)) {
802
+ return port;
803
+ }
804
+ }
805
+ return undefined;
806
+ }
807
+ /**
808
+ * PID of a live Chrome holding a named persistent profile, if the profile's
809
+ * SingletonLock says so. Unlike findPortForProfile() this sees Chromes we did
810
+ * NOT launch (other cdp-tools sessions sharing the global profile root, or a
811
+ * Chrome started by hand on the same user-data-dir).
812
+ *
813
+ * LIMITATION: POSIX-only. It reads the SingletonLock symlink target, which
814
+ * Windows Chrome does not create - there it always reports "not locked", so
815
+ * callers must treat a negative result as "no evidence of a holder" rather
816
+ * than proof the profile is free.
817
+ *
818
+ * @throws InvalidProfileNameError
819
+ */
820
+ async findProfileLockHolder(profileName) {
821
+ const dir = this.getPersistentProfilePath(profileName);
822
+ return this.readProfileLockPid(dir);
823
+ }
824
+ /**
825
+ * Names of persistent profiles that exist on disk.
826
+ */
827
+ async listPersistentProfiles() {
828
+ try {
829
+ const entries = await fs.promises.readdir(this.getPersistentProfileRoot(), { withFileTypes: true });
830
+ return entries.filter(e => e.isDirectory()).map(e => e.name).sort();
831
+ }
832
+ catch {
833
+ return [];
834
+ }
835
+ }
836
+ /**
837
+ * Wipe a named persistent profile and recreate it empty, so the next launch
838
+ * starts from a clean browser (no cookies, no IndexedDB, no enrolment).
839
+ *
840
+ * Refuses while a Chrome we launched still holds the profile: deleting a
841
+ * live user-data-dir corrupts the running browser and the deletion would be
842
+ * partly undone as Chrome flushes state back out on exit. Kill that instance
843
+ * (`killChrome({ port })`) first.
844
+ *
845
+ * Also refuses when the profile's Chrome SingletonLock names a live PID we
846
+ * did not launch. The default profile root is global (`~/.cdp-tools/profiles`),
847
+ * so another cdp-tools session may be running this exact profile; without the
848
+ * lock check this call would rm -rf a live browser's user-data-dir and destroy
849
+ * the very identity persistent profiles exist to preserve. That check is
850
+ * POSIX-only (see ProfileLockedError) - on Windows this remains unguarded.
851
+ *
852
+ * @throws InvalidProfileNameError | ProfileInUseError | ProfileLockedError
853
+ */
854
+ async resetPersistentProfile(profileName) {
855
+ const name = normalizeProfileName(profileName);
856
+ const holder = this.findPortForProfile(name);
857
+ if (holder !== undefined) {
858
+ throw new ProfileInUseError(name, holder);
859
+ }
860
+ const lockPid = await this.findProfileLockHolder(name);
861
+ if (lockPid !== undefined) {
862
+ throw new ProfileLockedError(name, lockPid, this.getPersistentProfilePath(name));
863
+ }
864
+ const dir = this.getPersistentProfilePath(name);
865
+ const existed = fs.existsSync(dir);
866
+ await fs.promises.rm(dir, { recursive: true, force: true });
867
+ await fs.promises.mkdir(dir, { recursive: true });
868
+ await debugLog('ChromeLauncher', `Reset persistent profile "${name}" at ${dir} (existed: ${existed})`);
869
+ return { profile: name, path: dir, existed };
870
+ }
871
+ /**
872
+ * Profile directory currently tracked for a port (if any).
873
+ */
874
+ getProfileDir(port) {
875
+ return this.profileDirs.get(port)?.dir;
876
+ }
877
+ /**
878
+ * All tracked profiles, keyed by port. Persistent profiles are included but
879
+ * are never deleted by the launcher.
880
+ */
881
+ getProfiles() {
882
+ return new Map(this.profileDirs);
883
+ }
884
+ /**
885
+ * Stop tracking a port's profile and, if it is ephemeral, delete it from disk.
886
+ *
887
+ * `expected` guards against a late exit event from a previous Chrome deleting
888
+ * the freshly created profile of a relaunch on the same port: if the tracked
889
+ * record is no longer the one we started with, nothing is touched.
890
+ */
891
+ async removeProfileDir(port, expected) {
892
+ const record = this.profileDirs.get(port);
893
+ if (!record) {
894
+ return;
895
+ }
896
+ if (expected && record !== expected) {
897
+ // A newer launch owns this port now - leave its profile alone.
898
+ return;
899
+ }
900
+ this.profileDirs.delete(port);
901
+ if (!record.ephemeral) {
902
+ // Persistent/named profile (issue 13) - tracking only, never delete.
903
+ await debugLog('ChromeLauncher', `Keeping persistent profile ${record.dir} for port ${port}`);
904
+ return;
905
+ }
906
+ try {
907
+ await fs.promises.rm(record.dir, { recursive: true, force: true });
908
+ await debugLog('ChromeLauncher', `Removed ephemeral profile ${record.dir} for port ${port}`);
909
+ }
910
+ catch (error) {
911
+ await debugLog('ChromeLauncher', `Failed to remove profile ${record.dir}: ${error}`);
912
+ }
913
+ }
914
+ /**
915
+ * Delete ephemeral profile directories left behind by previous sessions
916
+ * (crashes, SIGKILL, machine restart). Returns the directories removed.
917
+ *
918
+ * Conservative on purpose - a directory is skipped when:
919
+ * - it is younger than staleProfileMaxAgeMs (another MCP instance may be
920
+ * launching Chrome into it right now), or
921
+ * - its Chrome SingletonLock names a PID that is still alive, or
922
+ * - it is currently tracked by this launcher.
923
+ */
924
+ async sweepStaleProfiles() {
925
+ const removed = [];
926
+ let entries;
927
+ try {
928
+ entries = await fs.promises.readdir(this.profileRoot, { withFileTypes: true });
929
+ }
930
+ catch (error) {
931
+ await debugLog('ChromeLauncher', `Profile sweep could not read ${this.profileRoot}: ${error}`);
932
+ return removed;
933
+ }
934
+ const inUse = new Set(Array.from(this.profileDirs.values()).map(r => r.dir));
935
+ const now = Date.now();
936
+ for (const entry of entries) {
937
+ if (!entry.isDirectory() || !entry.name.startsWith(EPHEMERAL_PROFILE_PREFIX)) {
938
+ continue;
939
+ }
940
+ const dir = path.join(this.profileRoot, entry.name);
941
+ if (inUse.has(dir)) {
942
+ continue;
943
+ }
944
+ try {
945
+ const stats = await fs.promises.stat(dir);
946
+ if (now - stats.mtimeMs < this.staleProfileMaxAgeMs) {
947
+ continue; // Too fresh - may belong to a launch in progress elsewhere
948
+ }
949
+ }
950
+ catch {
951
+ continue; // Vanished or unreadable - leave it alone
952
+ }
953
+ if (await this.isProfileLocked(dir)) {
954
+ await debugLog('ChromeLauncher', `Profile sweep skipping ${dir} - still locked by a live Chrome`);
955
+ continue;
956
+ }
957
+ try {
958
+ await fs.promises.rm(dir, { recursive: true, force: true });
959
+ removed.push(dir);
960
+ }
961
+ catch (error) {
962
+ await debugLog('ChromeLauncher', `Profile sweep failed to remove ${dir}: ${error}`);
963
+ }
964
+ }
965
+ if (removed.length) {
966
+ await debugLog('ChromeLauncher', `Profile sweep removed ${removed.length} stale profile(s)`);
967
+ }
968
+ return removed;
969
+ }
970
+ /**
971
+ * Is a profile directory still held by a running Chrome?
972
+ * Chrome writes a SingletonLock symlink whose target is "<hostname>-<pid>".
973
+ * Unreadable/absent lock means "not locked" (Windows uses a plain lockfile).
974
+ */
975
+ async isProfileLocked(dir) {
976
+ return (await this.readProfileLockPid(dir)) !== undefined;
977
+ }
978
+ /**
979
+ * PID named by a profile directory's Chrome SingletonLock, if that process is
980
+ * still alive. POSIX-only: on Windows the lock is a plain file with no
981
+ * readable PID, so this always returns undefined ("no evidence of a holder").
982
+ */
983
+ async readProfileLockPid(dir) {
984
+ try {
985
+ const target = await fs.promises.readlink(path.join(dir, 'SingletonLock'));
986
+ const pid = Number(target.split('-').pop());
987
+ return Number.isInteger(pid) && pid > 0 && this.isProcessAlive(pid) ? pid : undefined;
988
+ }
989
+ catch {
990
+ return undefined;
991
+ }
992
+ }
993
+ /**
994
+ * Reset the launcher state (useful if Chrome was closed externally)
995
+ */
996
+ reset(port) {
997
+ // Note: profile dirs are only untracked here, never deleted - reset() is a
998
+ // "forget my state" escape hatch and the Chrome holding the profile may
999
+ // still be alive. Anything orphaned this way is picked up by the startup
1000
+ // sweep on the next run.
1001
+ if (port !== undefined) {
1002
+ this.chromeProcesses.delete(port);
1003
+ this.profileDirs.delete(port);
1004
+ }
1005
+ else {
1006
+ this.chromeProcesses.clear();
1007
+ this.profileDirs.clear();
1008
+ }
1009
+ }
1010
+ /**
1011
+ * Record a Chrome close event
1012
+ */
1013
+ recordCloseEvent(port, pid, reason, exitCode, signal) {
1014
+ const event = {
1015
+ port,
1016
+ pid,
1017
+ reason,
1018
+ timestamp: new Date(),
1019
+ exitCode,
1020
+ signal,
1021
+ };
1022
+ this.lastCloseEvents.push(event);
1023
+ // Keep only the last N events
1024
+ if (this.lastCloseEvents.length > this.maxCloseEvents) {
1025
+ this.lastCloseEvents.shift();
1026
+ }
1027
+ debugLog('ChromeLauncher', `Recorded close event: port=${port}, pid=${pid}, reason=${reason}, exitCode=${exitCode}, signal=${signal}`);
1028
+ return event;
1029
+ }
1030
+ /**
1031
+ * Get the last close events
1032
+ */
1033
+ getLastCloseEvents() {
1034
+ return [...this.lastCloseEvents];
1035
+ }
1036
+ /**
1037
+ * Get Chrome launcher status for all instances
1038
+ * Verifies each process is actually alive and cleans up dead ones
1039
+ */
1040
+ getStatus() {
1041
+ const instances = [];
1042
+ const deadPorts = [];
1043
+ for (const [port, chromeProcess] of this.chromeProcesses.entries()) {
1044
+ const pid = chromeProcess.pid || -1;
1045
+ const alive = pid > 0 && this.isProcessAlive(pid);
1046
+ if (!alive) {
1047
+ deadPorts.push(port);
1048
+ }
1049
+ instances.push({ port, pid, running: alive });
1050
+ }
1051
+ // Clean up dead processes
1052
+ for (const port of deadPorts) {
1053
+ const chromeProcess = this.chromeProcesses.get(port);
1054
+ const pid = chromeProcess?.pid || -1;
1055
+ this.chromeProcesses.delete(port);
1056
+ this.recordCloseEvent(port, pid, 'external', null, null);
1057
+ }
1058
+ // Return only alive instances
1059
+ return {
1060
+ instances: instances.filter(i => i.running),
1061
+ lastCloseEvents: this.lastCloseEvents
1062
+ };
1063
+ }
1064
+ /**
1065
+ * Set the pending close reason for a port (call before killing)
1066
+ */
1067
+ setPendingCloseReason(port, reason) {
1068
+ this.pendingCloseReason.set(port, reason);
1069
+ }
1070
+ }
1071
+ //# sourceMappingURL=chrome-launcher.js.map