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,1674 @@
1
+ /**
2
+ * Server Manager
3
+ * Manages development servers (any language/framework) - start, stop, restart, and monitor
4
+ * Supports multiple runner types: native (spawn), docker, docker-compose
5
+ * Persists state to .cdp-tools/servers.json for recovery and auto-run
6
+ * Logs to .cdp-tools/logs/<server-id>/ for cross-MCP access (native runner only)
7
+ */
8
+ import * as fs from 'fs';
9
+ import * as net from 'net';
10
+ import { debugLog } from './debug-logger.js';
11
+ import { getOutputPath } from './helpers/paths.js';
12
+ import { atomicWriteFile } from './atomic-write.js';
13
+ import { configManager } from './config.js';
14
+ import { ServerFileWatcher } from './server-watcher.js';
15
+ import { serverClaims } from './server-claims.js';
16
+ import { detectRunnerType, createRunner, resetDockerCaches, NativeRunner, DockerRunner, DockerComposeRunner, } from './runners/index.js';
17
+ /**
18
+ * Patterns for commands that auto-restart their own process on file changes
19
+ * (Node's --watch, nodemon, etc.). Pairing one of these with an attached CDP
20
+ * debugger on the same process is a known-bad combination: the auto-restart
21
+ * supervisor races with a paused (frozen) process, which can produce
22
+ * EADDRINUSE crash-loops and ambiguous "failed but still listening" states,
23
+ * since the restart happens entirely outside anything cdp-tools tracks.
24
+ */
25
+ const AUTO_RESTART_PATTERNS = [
26
+ { name: '--watch', regex: /(^|\s)--watch(\b|=)/ },
27
+ { name: 'nodemon', regex: /\bnodemon\b/ },
28
+ { name: 'tsx watch', regex: /\btsx\s+watch\b/ },
29
+ { name: 'ts-node-dev', regex: /\bts-node-dev\b/ },
30
+ { name: 'node-dev', regex: /\bnode-dev\b/ },
31
+ { name: 'bun --hot/--watch', regex: /\bbun\b.*(--hot\b|--watch\b)/ },
32
+ { name: 'deno run --watch', regex: /\bdeno\s+run\b.*--watch\b/ },
33
+ ];
34
+ /**
35
+ * Returns the matched auto-restart pattern name if `command` looks like it
36
+ * self-restarts on file changes, or null otherwise.
37
+ */
38
+ export function detectAutoRestartCommand(command) {
39
+ for (const { name, regex } of AUTO_RESTART_PATTERNS) {
40
+ if (regex.test(command)) {
41
+ return name;
42
+ }
43
+ }
44
+ return null;
45
+ }
46
+ /**
47
+ * Extract the Node inspector port from a `--inspect`/`--inspect-brk` flag in
48
+ * a command string, if present. Falls back to Node's default port (9229)
49
+ * when the flag is given with no explicit port. Does not handle a separate
50
+ * `--inspect-port=` flag overriding a bare `--inspect`'s port - a rare
51
+ * combination not worth the extra complexity here.
52
+ */
53
+ export function extractInspectorPort(command) {
54
+ const match = command.match(/--inspect(?:-brk)?(?:=(?:[\w.]+:)?(\d+))?/);
55
+ if (!match) {
56
+ return null;
57
+ }
58
+ return match[1] ? parseInt(match[1], 10) : 9229;
59
+ }
60
+ /**
61
+ * Validate server ID to prevent security issues.
62
+ * Server IDs are used in container names, file paths, and project names.
63
+ * Only alphanumeric characters, dashes, and underscores are allowed.
64
+ */
65
+ function validateServerId(id) {
66
+ if (!id || id.length === 0) {
67
+ throw new Error('Server ID cannot be empty');
68
+ }
69
+ if (id.length > 64) {
70
+ throw new Error('Server ID cannot exceed 64 characters');
71
+ }
72
+ // Only allow alphanumeric, dash, underscore
73
+ if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
74
+ throw new Error(`Invalid server ID "${id}". Server IDs can only contain letters, numbers, dashes, and underscores.`);
75
+ }
76
+ // Cannot start with a dash (would cause issues with CLI args)
77
+ if (id.startsWith('-')) {
78
+ throw new Error('Server ID cannot start with a dash');
79
+ }
80
+ }
81
+ /**
82
+ * Port Monitor - monitors ports using persistent TCP connections
83
+ */
84
+ export class PortMonitor {
85
+ ports = new Map();
86
+ onFailureCallback;
87
+ getIntervalForLevel;
88
+ // Refcount so multiple paused CDP connections can share one PortMonitor safely:
89
+ // monitoring only actually stops on the first pause and only actually resumes
90
+ // once every pause has a matching resume.
91
+ pauseCount = 0;
92
+ constructor(getIntervalForLevel) {
93
+ // Default intervals if no config provided
94
+ this.getIntervalForLevel = getIntervalForLevel ?? ((level) => {
95
+ switch (level) {
96
+ case 'block': return 1000;
97
+ case 'error': return 2000;
98
+ case 'inform': return 5000;
99
+ default: return 2000;
100
+ }
101
+ });
102
+ }
103
+ onFailure(callback) {
104
+ this.onFailureCallback = callback;
105
+ }
106
+ async startMonitoring(port, level, description, interval) {
107
+ const existing = this.ports.get(port);
108
+ if (existing) {
109
+ existing.level = level;
110
+ existing.description = description;
111
+ existing.interval = interval;
112
+ if (existing.status === 'down' && existing.acknowledged) {
113
+ existing.acknowledged = false;
114
+ }
115
+ // Self-heal: if this port's monitor is dormant (no active socket and no
116
+ // pending reconnect - e.g. left stuck by an imbalanced pause/resume),
117
+ // reconnect it rather than silently staying dead.
118
+ if (!existing.socket && !existing.reconnectTimer) {
119
+ this.connectToPort(port);
120
+ }
121
+ await debugLog('PortMonitor', `Updated monitoring for port ${port}: level=${level}, interval=${interval ?? 'default'}`);
122
+ return;
123
+ }
124
+ const monitored = {
125
+ port,
126
+ level,
127
+ description,
128
+ interval,
129
+ status: 'connecting',
130
+ socket: null,
131
+ acknowledged: false,
132
+ };
133
+ this.ports.set(port, monitored);
134
+ await debugLog('PortMonitor', `Starting monitoring for port ${port} (level: ${level}, interval: ${interval ?? 'default'})`);
135
+ this.connectToPort(port);
136
+ }
137
+ connectToPort(port) {
138
+ const monitored = this.ports.get(port);
139
+ if (!monitored)
140
+ return;
141
+ if (monitored.reconnectTimer) {
142
+ clearTimeout(monitored.reconnectTimer);
143
+ monitored.reconnectTimer = undefined;
144
+ }
145
+ if (monitored.socket) {
146
+ monitored.socket.removeAllListeners();
147
+ monitored.socket.destroy();
148
+ monitored.socket = null;
149
+ }
150
+ if (monitored.status !== 'up') {
151
+ monitored.status = 'connecting';
152
+ }
153
+ const socket = new net.Socket();
154
+ socket.setTimeout(5000);
155
+ let hadError = false;
156
+ socket.on('connect', async () => {
157
+ const wasDown = monitored.status === 'down' || monitored.status === 'connecting';
158
+ monitored.status = 'up';
159
+ monitored.socket = socket;
160
+ monitored.failedAt = undefined;
161
+ monitored.acknowledged = false;
162
+ if (wasDown) {
163
+ await debugLog('PortMonitor', `Port ${port} is UP`);
164
+ }
165
+ socket.destroy();
166
+ });
167
+ socket.on('close', async () => {
168
+ monitored.socket = null;
169
+ if (!hadError) {
170
+ this.scheduleReconnect(port);
171
+ }
172
+ });
173
+ socket.on('error', async (err) => {
174
+ hadError = true;
175
+ const wasUp = monitored.status === 'up';
176
+ if (err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT' || err.code === 'ENOTFOUND') {
177
+ if (wasUp || monitored.status === 'connecting') {
178
+ monitored.status = 'down';
179
+ monitored.failedAt = monitored.failedAt || new Date();
180
+ monitored.socket = null;
181
+ if (wasUp) {
182
+ await debugLog('PortMonitor', `Port ${port} went DOWN: ${err.message}`);
183
+ if (this.onFailureCallback) {
184
+ this.onFailureCallback(port, monitored.level);
185
+ }
186
+ }
187
+ else {
188
+ await debugLog('PortMonitor', `Port ${port} is down: ${err.message}`);
189
+ }
190
+ }
191
+ }
192
+ else {
193
+ await debugLog('PortMonitor', `Port ${port} connection error (ignoring): ${err.message}`);
194
+ }
195
+ this.scheduleReconnect(port);
196
+ });
197
+ socket.on('timeout', () => {
198
+ hadError = true;
199
+ socket.destroy();
200
+ if (monitored.status === 'connecting' || monitored.status === 'up') {
201
+ const wasUp = monitored.status === 'up';
202
+ monitored.status = 'down';
203
+ monitored.failedAt = monitored.failedAt || new Date();
204
+ if (wasUp) {
205
+ debugLog('PortMonitor', `Port ${port} went DOWN: connection timeout`);
206
+ if (this.onFailureCallback) {
207
+ this.onFailureCallback(port, monitored.level);
208
+ }
209
+ }
210
+ }
211
+ this.scheduleReconnect(port);
212
+ });
213
+ socket.connect(port, 'localhost');
214
+ }
215
+ scheduleReconnect(port) {
216
+ const monitored = this.ports.get(port);
217
+ if (!monitored || monitored.reconnectTimer)
218
+ return;
219
+ // Use per-port interval if set, otherwise get from config based on level
220
+ const interval = monitored.interval ?? this.getIntervalForLevel(monitored.level);
221
+ monitored.reconnectTimer = setTimeout(() => {
222
+ monitored.reconnectTimer = undefined;
223
+ if (this.ports.has(port)) {
224
+ this.connectToPort(port);
225
+ }
226
+ }, interval);
227
+ }
228
+ async stopMonitoring(port) {
229
+ const monitored = this.ports.get(port);
230
+ if (!monitored)
231
+ return false;
232
+ if (monitored.socket) {
233
+ monitored.socket.removeAllListeners();
234
+ monitored.socket.destroy();
235
+ }
236
+ if (monitored.reconnectTimer) {
237
+ clearTimeout(monitored.reconnectTimer);
238
+ }
239
+ this.ports.delete(port);
240
+ await debugLog('PortMonitor', `Stopped monitoring port ${port}`);
241
+ return true;
242
+ }
243
+ async acknowledgeFailure(port) {
244
+ const monitored = this.ports.get(port);
245
+ if (!monitored || monitored.status !== 'down')
246
+ return false;
247
+ monitored.acknowledged = true;
248
+ await debugLog('PortMonitor', `Acknowledged failure for port ${port}`);
249
+ return true;
250
+ }
251
+ getFailedPorts() {
252
+ const failed = [];
253
+ for (const [port, monitored] of this.ports) {
254
+ if (monitored.status === 'down' && !monitored.acknowledged && monitored.failedAt) {
255
+ failed.push({
256
+ port,
257
+ level: monitored.level,
258
+ description: monitored.description,
259
+ failedAt: monitored.failedAt,
260
+ });
261
+ }
262
+ }
263
+ return failed;
264
+ }
265
+ getFailedPortsByLevel(level) {
266
+ return this.getFailedPorts().filter(p => p.level === level);
267
+ }
268
+ hasBlockingFailures() {
269
+ return this.getFailedPortsByLevel('block').length > 0;
270
+ }
271
+ isMonitoring(port) {
272
+ return this.ports.has(port);
273
+ }
274
+ getStatus(port) {
275
+ if (port !== undefined) {
276
+ const monitored = this.ports.get(port);
277
+ if (!monitored)
278
+ return [];
279
+ return [{
280
+ port: monitored.port,
281
+ level: monitored.level,
282
+ description: monitored.description,
283
+ interval: monitored.interval,
284
+ status: monitored.status,
285
+ failedAt: monitored.failedAt,
286
+ acknowledged: monitored.acknowledged,
287
+ }];
288
+ }
289
+ return Array.from(this.ports.values()).map(m => ({
290
+ port: m.port,
291
+ level: m.level,
292
+ description: m.description,
293
+ interval: m.interval,
294
+ status: m.status,
295
+ failedAt: m.failedAt,
296
+ acknowledged: m.acknowledged,
297
+ }));
298
+ }
299
+ getPersistedState() {
300
+ return Array.from(this.ports.values()).map(m => ({
301
+ port: m.port,
302
+ level: m.level,
303
+ description: m.description,
304
+ interval: m.interval,
305
+ }));
306
+ }
307
+ async restoreFromState(ports) {
308
+ for (const p of ports) {
309
+ await this.startMonitoring(p.port, p.level, p.description, p.interval);
310
+ }
311
+ }
312
+ async stopAll() {
313
+ for (const port of this.ports.keys()) {
314
+ await this.stopMonitoring(port);
315
+ }
316
+ }
317
+ /**
318
+ * Pause all port monitoring (e.g., when paused at a breakpoint)
319
+ * Stops reconnection attempts but preserves port state
320
+ */
321
+ pauseMonitoring() {
322
+ this.pauseCount++;
323
+ if (this.pauseCount > 1) {
324
+ // Another connection already has monitoring paused; nothing new to do.
325
+ debugLog('PortMonitor', `Port monitoring pause count now ${this.pauseCount}`);
326
+ return;
327
+ }
328
+ for (const monitored of this.ports.values()) {
329
+ // Clear any pending reconnect timers
330
+ if (monitored.reconnectTimer) {
331
+ clearTimeout(monitored.reconnectTimer);
332
+ monitored.reconnectTimer = undefined;
333
+ }
334
+ // Close any active socket connections
335
+ if (monitored.socket) {
336
+ monitored.socket.removeAllListeners();
337
+ monitored.socket.destroy();
338
+ monitored.socket = null;
339
+ }
340
+ }
341
+ debugLog('PortMonitor', 'Port monitoring paused');
342
+ }
343
+ /**
344
+ * Resume all port monitoring (e.g., when resuming from a breakpoint)
345
+ * Restarts connection attempts for all monitored ports, once every
346
+ * outstanding pauseMonitoring() call has a matching resumeMonitoring().
347
+ */
348
+ resumeMonitoring() {
349
+ if (this.pauseCount === 0) {
350
+ // Unbalanced resume (shouldn't normally happen) - nothing to do.
351
+ return;
352
+ }
353
+ this.pauseCount--;
354
+ if (this.pauseCount > 0) {
355
+ debugLog('PortMonitor', `Port monitoring still paused by ${this.pauseCount} other connection(s)`);
356
+ return;
357
+ }
358
+ for (const port of this.ports.keys()) {
359
+ this.connectToPort(port);
360
+ }
361
+ debugLog('PortMonitor', 'Port monitoring resumed');
362
+ }
363
+ }
364
+ // ============================================================================
365
+ // Server Manager
366
+ // ============================================================================
367
+ export class ServerManager {
368
+ servers = new Map();
369
+ portMonitor = null;
370
+ pendingStartups = new Map();
371
+ /** Mutex to serialize saveState calls - prevents concurrent file writes */
372
+ saveMutex = Promise.resolve();
373
+ /** Injected from index.ts - decouples ServerManager from ConnectionManager directly */
374
+ pauseChecker = null;
375
+ /** Watch-mode restart coordination, keyed by serverId - see WatchRestartState */
376
+ watchState = new Map();
377
+ /** Ownership claims - who may stop a shared dev server (see server-claims.ts) */
378
+ claims;
379
+ constructor(claimsStore = serverClaims) {
380
+ this.claims = claimsStore;
381
+ }
382
+ /**
383
+ * Set the function used to check whether a CDP connection at a given
384
+ * inspector port is currently paused at a breakpoint. Mirrors the existing
385
+ * setChromeLauncher() injection pattern.
386
+ */
387
+ setPauseChecker(fn) {
388
+ this.pauseChecker = fn;
389
+ }
390
+ /**
391
+ * Get the port monitor instance (lazy-initialized)
392
+ * Must be called after configManager.load() for config values to be used
393
+ */
394
+ getPortMonitor() {
395
+ if (!this.portMonitor) {
396
+ // Lazy init - ensures config is loaded before we read intervals
397
+ this.portMonitor = new PortMonitor((level) => configManager.getIntervalForLevel(level));
398
+ }
399
+ return this.portMonitor;
400
+ }
401
+ getServersFilePath(global) {
402
+ return getOutputPath('servers.json', { global: global ?? false });
403
+ }
404
+ /**
405
+ * Initialize - load state and recover/start auto-run servers
406
+ * Loads from both local (project) and global (~/.cdp-tools/) storage
407
+ */
408
+ async initialize() {
409
+ const recovered = [];
410
+ const started = [];
411
+ const failed = [];
412
+ const monitoredPorts = [];
413
+ // Load from both local and global storage
414
+ const localPersisted = await this.loadState(false);
415
+ const globalPersisted = await this.loadState(true);
416
+ // Combine servers from both sources (local servers marked with global=false, global with global=true)
417
+ const allServers = [
418
+ ...localPersisted.servers.map(s => ({ ...s, global: false })),
419
+ ...globalPersisted.servers.map(s => ({ ...s, global: true })),
420
+ ];
421
+ // Restore monitored ports from local config
422
+ if (localPersisted.monitoredPorts.length > 0) {
423
+ await this.getPortMonitor().restoreFromState(localPersisted.monitoredPorts);
424
+ monitoredPorts.push(...localPersisted.monitoredPorts.map(p => p.port));
425
+ await debugLog('ServerManager', `Restored ${monitoredPorts.length} monitored ports`);
426
+ }
427
+ // Restore pending startups from both local and global
428
+ const allPendingStartups = [
429
+ ...localPersisted.pendingStartups,
430
+ ...globalPersisted.pendingStartups,
431
+ ];
432
+ for (const persisted of allPendingStartups) {
433
+ const pending = {
434
+ serverId: persisted.serverId,
435
+ startedAt: new Date(persisted.startedAt),
436
+ timeoutAt: new Date(persisted.timeoutAt),
437
+ acknowledged: persisted.acknowledged,
438
+ reason: persisted.reason,
439
+ };
440
+ this.pendingStartups.set(persisted.serverId, pending);
441
+ await debugLog('ServerManager', `Restored pending startup: ${persisted.serverId}`);
442
+ }
443
+ // Reset Docker caches before processing servers - ensures we check Docker availability
444
+ // fresh on each startup, catching cases where Docker stopped since last run
445
+ resetDockerCaches();
446
+ // Read before the loop below claims anything: a server we are about to
447
+ // claim during recovery would otherwise look owned by us and never be
448
+ // collected, no matter how long ago its real owners died (issue #139).
449
+ const abandoned = this.findAbandonedServerIds();
450
+ const collected = [];
451
+ for (const server of allServers) {
452
+ const runnerType = server.type || 'native';
453
+ const runner = createRunner(runnerType, server.id);
454
+ // Restore runner state using the interface method (pass PersistedRunnerState directly)
455
+ runner.restore(server);
456
+ const isRunning = await runner.isRunning();
457
+ if (isRunning) {
458
+ const managed = {
459
+ id: server.id,
460
+ runner,
461
+ autoRun: server.autoRun,
462
+ monitorPort: server.monitorPort,
463
+ global: server.global,
464
+ watch: server.watch,
465
+ watchPaths: server.watchPaths,
466
+ };
467
+ this.servers.set(server.id, managed);
468
+ // The process survived, but any watcher was tied to the previous
469
+ // ServerManager instance (e.g. before devharness's own supervisor
470
+ // restarted it) - re-establish it now.
471
+ if (server.watch) {
472
+ this.startWatcher(managed, server.watchPaths && server.watchPaths.length > 0 ? server.watchPaths : [server.cwd]);
473
+ }
474
+ // For native runner, init cursor to EOF (native-specific method)
475
+ if (runner.type === 'native' && 'initializeCursorToEOF' in runner) {
476
+ await runner.initializeCursorToEOF();
477
+ }
478
+ // Every session that ever claimed this server is gone, so nobody is
479
+ // coming back for it - this is the closed-window leak, collected at
480
+ // the first opportunity rather than left running until reboot.
481
+ // Another window on this project may be using it even though it never
482
+ // claimed it - only the last session out collects.
483
+ if (abandoned.get(server.global ?? false)?.has(server.id) &&
484
+ !this.claims.hasOtherLiveSessionIn(server.cwd)) {
485
+ try {
486
+ await debugLog('ServerClaims', `Collecting abandoned server ${server.id}`);
487
+ await this.stopServer(server.id);
488
+ collected.push(server.id);
489
+ continue;
490
+ }
491
+ catch (error) {
492
+ await debugLog('ServerClaims', `Failed to collect ${server.id}: ${error}`);
493
+ }
494
+ }
495
+ // Reattaching is use: this session now depends on the server, so it
496
+ // takes a claim alongside whoever else already holds one.
497
+ await this.claims.claim(server.id, server.cwd, server.global ?? false);
498
+ recovered.push(server.id);
499
+ await debugLog('ServerManager', `Recovered running server: ${server.id} (${runnerType}, global=${server.global})`);
500
+ }
501
+ else if (server.autoRun) {
502
+ // Not running but autoRun - restart
503
+ let success = false;
504
+ const maxRetries = 3;
505
+ for (let attempt = 1; attempt <= maxRetries && !success; attempt++) {
506
+ try {
507
+ if (server.port) {
508
+ await this.waitForPortRelease(server.port, 5);
509
+ }
510
+ await this.startServer({
511
+ command: server.command,
512
+ cwd: server.cwd,
513
+ id: server.id,
514
+ autoRun: true,
515
+ runner: runnerType,
516
+ monitorPort: server.monitorPort,
517
+ global: server.global,
518
+ watch: server.watch,
519
+ watchPaths: server.watchPaths,
520
+ });
521
+ started.push(server.id);
522
+ success = true;
523
+ await debugLog('ServerManager', `Auto-started server: ${server.id} (attempt ${attempt})`);
524
+ }
525
+ catch (err) {
526
+ await debugLog('ServerManager', `Auto-start attempt ${attempt}/${maxRetries} failed for ${server.id}: ${err}`);
527
+ if (attempt < maxRetries) {
528
+ await new Promise(resolve => setTimeout(resolve, 500 * Math.pow(2, attempt - 1)));
529
+ }
530
+ }
531
+ }
532
+ if (!success) {
533
+ failed.push(server.id);
534
+ // Still add to map so it can be manually restarted
535
+ this.servers.set(server.id, {
536
+ id: server.id,
537
+ runner,
538
+ autoRun: server.autoRun,
539
+ monitorPort: server.monitorPort,
540
+ global: server.global,
541
+ watch: server.watch,
542
+ watchPaths: server.watchPaths,
543
+ });
544
+ // Create a pending startup failure to block tools until acknowledged
545
+ const now = new Date();
546
+ this.pendingStartups.set(server.id, {
547
+ serverId: server.id,
548
+ startedAt: now,
549
+ timeoutAt: now, // Already timed out
550
+ acknowledged: false,
551
+ reason: 'died', // Failed to start = died
552
+ });
553
+ await debugLog('ServerManager', `Created blocking failure for autoRun server: ${server.id}`);
554
+ }
555
+ }
556
+ else {
557
+ // Not running and not autoRun - keep config for manual restart
558
+ this.servers.set(server.id, {
559
+ id: server.id,
560
+ runner,
561
+ autoRun: false,
562
+ monitorPort: server.monitorPort,
563
+ global: server.global,
564
+ watch: server.watch,
565
+ watchPaths: server.watchPaths,
566
+ });
567
+ }
568
+ }
569
+ // Process restored pending startups
570
+ // For each pending startup, check if server is still running and if timeout has passed
571
+ for (const [serverId, pending] of this.pendingStartups) {
572
+ const managed = this.servers.get(serverId);
573
+ if (!managed) {
574
+ // Server no longer exists, remove pending state
575
+ this.pendingStartups.delete(serverId);
576
+ await debugLog('ServerManager', `Removed pending startup for non-existent server: ${serverId}`);
577
+ continue;
578
+ }
579
+ const isRunning = await managed.runner.isRunning();
580
+ const now = new Date();
581
+ if (!isRunning) {
582
+ // Server died - set reason, but respect existing acknowledgment
583
+ pending.reason = 'died';
584
+ // Only reset acknowledged if it wasn't already acknowledged
585
+ // (user already dealt with this failure before MCP restart)
586
+ await debugLog('ServerManager', `Pending startup server died: ${serverId} (acknowledged: ${pending.acknowledged})`);
587
+ }
588
+ else if (now >= pending.timeoutAt) {
589
+ // Timeout already passed - set reason, but respect existing acknowledgment
590
+ pending.reason = 'timeout';
591
+ await debugLog('ServerManager', `Pending startup already timed out: ${serverId} (acknowledged: ${pending.acknowledged})`);
592
+ }
593
+ else {
594
+ // Still within timeout window - resume background detection
595
+ const remainingMs = pending.timeoutAt.getTime() - now.getTime();
596
+ await debugLog('ServerManager', `Resuming port detection for ${serverId}, ${Math.round(remainingMs / 1000)}s remaining`);
597
+ this.resumePortDetection(serverId, remainingMs);
598
+ }
599
+ }
600
+ if (collected.length > 0) {
601
+ console.error(`[cdp-tools] Collected ${collected.length} abandoned dev server(s): ${collected.join(', ')}`);
602
+ }
603
+ await this.saveState();
604
+ return { recovered, started, failed, monitoredPorts, collected };
605
+ }
606
+ /**
607
+ * Resume port detection for a server after MCP restart
608
+ * @param serverId The server to resume detection for
609
+ * @param remainingMs Time remaining until timeout
610
+ */
611
+ resumePortDetection(serverId, remainingMs) {
612
+ // Run detection in background (don't await)
613
+ this.detectPortInBackgroundWithTimeout(serverId, remainingMs);
614
+ }
615
+ async loadState(global) {
616
+ const filePath = this.getServersFilePath(global);
617
+ try {
618
+ if (fs.existsSync(filePath)) {
619
+ const content = await fs.promises.readFile(filePath, 'utf-8');
620
+ const data = JSON.parse(content);
621
+ // Handle migration from old format (no runner type)
622
+ const servers = (data.servers || []).map((s) => ({
623
+ ...s,
624
+ type: s.type || 'native',
625
+ global: global ?? false,
626
+ }));
627
+ return {
628
+ servers,
629
+ monitoredPorts: data.monitoredPorts || [],
630
+ pendingStartups: data.pendingStartups || [],
631
+ };
632
+ }
633
+ }
634
+ catch (err) {
635
+ await debugLog('ServerManager', `Failed to load state from ${filePath}: ${err}`);
636
+ }
637
+ return { servers: [], monitoredPorts: [], pendingStartups: [] };
638
+ }
639
+ /**
640
+ * Save state to disk - serialized through mutex to prevent concurrent writes
641
+ */
642
+ async saveState() {
643
+ // Queue behind any pending save to serialize all writes
644
+ this.saveMutex = this.saveMutex.then(() => this.doSaveState()).catch(() => { });
645
+ return this.saveMutex;
646
+ }
647
+ /**
648
+ * Internal save implementation - uses atomic writes to prevent corruption
649
+ */
650
+ async doSaveState() {
651
+ // Split servers by global flag
652
+ const localServers = [];
653
+ const globalServers = [];
654
+ for (const [id, managed] of this.servers) {
655
+ const runner = managed.runner;
656
+ const status = await runner.getStatus();
657
+ let containerId;
658
+ if (runner instanceof DockerRunner) {
659
+ containerId = runner.getContainerId() ?? undefined;
660
+ }
661
+ const serverData = {
662
+ type: runner.type,
663
+ id,
664
+ command: this.getRunnerCommand(runner),
665
+ cwd: this.getRunnerCwd(runner),
666
+ pid: status.pid,
667
+ pidStartedAt: status.pidStartedAt,
668
+ containerId,
669
+ port: status.port,
670
+ autoRun: managed.autoRun,
671
+ startedAt: status.startedAt?.toISOString() ?? new Date().toISOString(),
672
+ monitorPort: managed.monitorPort,
673
+ watch: managed.watch,
674
+ watchPaths: managed.watchPaths,
675
+ };
676
+ if (managed.global) {
677
+ globalServers.push(serverData);
678
+ }
679
+ else {
680
+ localServers.push(serverData);
681
+ }
682
+ }
683
+ const monitoredPorts = this.getPortMonitor().getPersistedState();
684
+ // Convert pending startups to persisted format, split by server's global flag
685
+ const localPendingStartups = [];
686
+ const globalPendingStartups = [];
687
+ for (const [serverId, pending] of this.pendingStartups) {
688
+ const managed = this.servers.get(serverId);
689
+ const persisted = {
690
+ serverId: pending.serverId,
691
+ startedAt: pending.startedAt.toISOString(),
692
+ timeoutAt: pending.timeoutAt.toISOString(),
693
+ acknowledged: pending.acknowledged,
694
+ reason: pending.reason,
695
+ };
696
+ if (managed?.global) {
697
+ globalPendingStartups.push(persisted);
698
+ }
699
+ else {
700
+ localPendingStartups.push(persisted);
701
+ }
702
+ }
703
+ // Save local servers to project directory (atomic write prevents corruption)
704
+ const localPath = this.getServersFilePath(false);
705
+ await atomicWriteFile(localPath, JSON.stringify({
706
+ version: 4,
707
+ updatedAt: new Date().toISOString(),
708
+ servers: localServers,
709
+ monitoredPorts, // Port monitoring stays in local config
710
+ pendingStartups: localPendingStartups,
711
+ }, null, 2));
712
+ // Save global servers to ~/.cdp-tools/ (atomic write prevents corruption)
713
+ const globalPath = this.getServersFilePath(true);
714
+ await atomicWriteFile(globalPath, JSON.stringify({
715
+ version: 4,
716
+ updatedAt: new Date().toISOString(),
717
+ servers: globalServers,
718
+ monitoredPorts: [], // Global doesn't track port monitoring
719
+ pendingStartups: globalPendingStartups,
720
+ }, null, 2));
721
+ }
722
+ getRunnerCommand(runner) {
723
+ if (runner instanceof NativeRunner)
724
+ return runner.getCommand();
725
+ if (runner instanceof DockerRunner)
726
+ return runner.getCommand();
727
+ if (runner instanceof DockerComposeRunner)
728
+ return runner.getCommand();
729
+ return '';
730
+ }
731
+ getRunnerCwd(runner) {
732
+ if (runner instanceof NativeRunner)
733
+ return runner.getCwd();
734
+ if (runner instanceof DockerRunner)
735
+ return runner.getCwd();
736
+ if (runner instanceof DockerComposeRunner)
737
+ return runner.getCwd();
738
+ return '';
739
+ }
740
+ /**
741
+ * Find the managed server whose command's --inspect/--inspect-brk port
742
+ * matches `port` - i.e. the server a CDP debugger connection at that port
743
+ * actually belongs to. This is deliberately NOT the server's own detected
744
+ * app/service port (e.g. an HTTP port parsed from "listening on port
745
+ * 3000") - a Node process's inspector port and its own app port are
746
+ * normally different numbers, so matching on the app port would almost
747
+ * never find anything real.
748
+ */
749
+ async getManagedServerByInspectorPort(port) {
750
+ return this.findManagedServerByInspectorPortSync(port);
751
+ }
752
+ /** Sync core of getManagedServerByInspectorPort() - reused by watch-restart lookups, which run in a sync pre-execution hot path (checkBreakpointPause) and can't await. */
753
+ findManagedServerByInspectorPortSync(port) {
754
+ for (const managed of this.servers.values()) {
755
+ const command = this.getRunnerCommand(managed.runner);
756
+ if (extractInspectorPort(command) === port) {
757
+ return { id: managed.id, command };
758
+ }
759
+ }
760
+ return null;
761
+ }
762
+ async isPortInUse(port) {
763
+ return new Promise((resolve) => {
764
+ const socket = new net.Socket();
765
+ socket.setTimeout(300);
766
+ socket.on('connect', () => {
767
+ socket.destroy();
768
+ resolve(true);
769
+ });
770
+ socket.on('timeout', () => {
771
+ socket.destroy();
772
+ resolve(false);
773
+ });
774
+ socket.on('error', () => {
775
+ socket.destroy();
776
+ resolve(false);
777
+ });
778
+ socket.connect(port, 'localhost');
779
+ });
780
+ }
781
+ /**
782
+ * Find PIDs of processes listening on a port using lsof
783
+ */
784
+ async findProcessesOnPort(port) {
785
+ return new Promise((resolve) => {
786
+ const { exec } = require('child_process');
787
+ exec(`lsof -ti :${port}`, (error, stdout) => {
788
+ if (error || !stdout.trim()) {
789
+ resolve([]);
790
+ return;
791
+ }
792
+ const pids = stdout.trim().split('\n').map(p => parseInt(p, 10)).filter(p => !isNaN(p) && p > 0);
793
+ resolve(pids);
794
+ });
795
+ });
796
+ }
797
+ /**
798
+ * Get the working directory of a process by PID
799
+ */
800
+ async getProcessCwd(pid) {
801
+ return new Promise((resolve) => {
802
+ const { exec } = require('child_process');
803
+ // Use lsof to get the cwd of the process (works on macOS and Linux)
804
+ exec(`lsof -p ${pid} | grep cwd | awk '{print $NF}'`, (error, stdout) => {
805
+ if (error || !stdout.trim()) {
806
+ resolve(null);
807
+ return;
808
+ }
809
+ resolve(stdout.trim());
810
+ });
811
+ });
812
+ }
813
+ /**
814
+ * Kill orphan processes holding a port, but only if they're in the expected directory
815
+ * Returns: { killed: PIDs killed, foreign: PIDs that are from a different directory }
816
+ */
817
+ async killOrphanProcessesOnPort(port, expectedCwd, expectedPid) {
818
+ const pids = await this.findProcessesOnPort(port);
819
+ const killed = [];
820
+ const foreign = [];
821
+ for (const pid of pids) {
822
+ // Skip if this is the expected process
823
+ if (expectedPid && pid === expectedPid)
824
+ continue;
825
+ // Check if process is running from the expected directory
826
+ const processCwd = await this.getProcessCwd(pid);
827
+ if (!processCwd || !processCwd.startsWith(expectedCwd)) {
828
+ // Process is from a different directory - don't kill it
829
+ await debugLog('ServerManager', `Process ${pid} on port ${port} is from different directory (${processCwd}), not killing`);
830
+ foreign.push(pid);
831
+ continue;
832
+ }
833
+ try {
834
+ await debugLog('ServerManager', `Killing orphan process ${pid} on port ${port} (cwd: ${processCwd})`);
835
+ process.kill(pid, 'SIGTERM');
836
+ killed.push(pid);
837
+ // Wait briefly then force kill if needed
838
+ await new Promise(resolve => setTimeout(resolve, 500));
839
+ try {
840
+ process.kill(pid, 0); // Check if still alive
841
+ process.kill(pid, 'SIGKILL');
842
+ await debugLog('ServerManager', `Force killed orphan process ${pid}`);
843
+ }
844
+ catch {
845
+ // Process already dead
846
+ }
847
+ }
848
+ catch (err) {
849
+ await debugLog('ServerManager', `Failed to kill process ${pid}: ${err}`);
850
+ }
851
+ }
852
+ return { killed, foreign };
853
+ }
854
+ async waitForPortRelease(port, maxAttempts = 10) {
855
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
856
+ const inUse = await this.isPortInUse(port);
857
+ if (!inUse) {
858
+ await debugLog('ServerManager', `Port ${port} released after ${attempt + 1} attempts`);
859
+ return true;
860
+ }
861
+ const delay = Math.min(100 * Math.pow(2, attempt), 3000);
862
+ await debugLog('ServerManager', `Port ${port} still in use, waiting ${delay}ms (attempt ${attempt + 1}/${maxAttempts})`);
863
+ await new Promise(resolve => setTimeout(resolve, delay));
864
+ }
865
+ await debugLog('ServerManager', `Port ${port} still in use after ${maxAttempts} attempts`);
866
+ return false;
867
+ }
868
+ /**
869
+ * Start a server
870
+ */
871
+ async startServer(options) {
872
+ const { command, cwd, id: serverId, autoRun, env, port, monitorPort, global: isGlobal, watch, watchPaths } = options;
873
+ // Validate server ID for security (prevents command injection via container names)
874
+ validateServerId(serverId);
875
+ // Auto-detect runner type if not specified
876
+ const runnerType = options.runner ?? detectRunnerType(command);
877
+ // Validate cwd exists
878
+ if (!fs.existsSync(cwd)) {
879
+ throw new Error(`Working directory does not exist: ${cwd}`);
880
+ }
881
+ // Check if already running
882
+ const existing = this.servers.get(serverId);
883
+ if (existing) {
884
+ const isRunning = await existing.runner.isRunning();
885
+ if (isRunning) {
886
+ throw new Error(`Server "${serverId}" is already running. Stop it first or use restart.`);
887
+ }
888
+ this.servers.delete(serverId);
889
+ }
890
+ // Check port availability
891
+ const existingStatus = existing ? await existing.runner.getStatus() : null;
892
+ const portToCheck = port || existingStatus?.port;
893
+ if (portToCheck) {
894
+ const inUse = await this.isPortInUse(portToCheck);
895
+ if (inUse) {
896
+ throw new Error(`Port ${portToCheck} is already in use.`);
897
+ }
898
+ }
899
+ await debugLog('ServerManager', `Starting server: ${command} (runner: ${runnerType}, global: ${isGlobal ?? false})`);
900
+ // Create and start runner
901
+ const runner = createRunner(runnerType, serverId);
902
+ // Set global flag on native runner for log storage location
903
+ if (runner instanceof NativeRunner) {
904
+ runner.setGlobal(isGlobal ?? false);
905
+ }
906
+ const result = await runner.start({ command, cwd, id: serverId, env, port });
907
+ const resolvedWatchPaths = watchPaths && watchPaths.length > 0 ? watchPaths : [cwd];
908
+ const managed = {
909
+ id: serverId,
910
+ runner,
911
+ autoRun: autoRun ?? false,
912
+ monitorPort: monitorPort ?? true, // Default to monitoring server port
913
+ global: isGlobal ?? false,
914
+ watch: watch ?? false,
915
+ watchPaths: resolvedWatchPaths,
916
+ };
917
+ this.servers.set(serverId, managed);
918
+ if (watch) {
919
+ this.startWatcher(managed, resolvedWatchPaths);
920
+ }
921
+ const autoRestartMatch = detectAutoRestartCommand(command);
922
+ if (autoRestartMatch) {
923
+ await debugLog('ServerManager', `Server ${serverId} command looks auto-restarting (matched: ${autoRestartMatch}) - flagging as risky to pair with an attached breakpoint debugger`);
924
+ }
925
+ // Create pending startup entry for timeout tracking
926
+ const now = new Date();
927
+ const timeoutMs = 30000; // 30 seconds
928
+ this.pendingStartups.set(serverId, {
929
+ serverId,
930
+ startedAt: now,
931
+ timeoutAt: new Date(now.getTime() + timeoutMs),
932
+ acknowledged: false,
933
+ });
934
+ // Start port detection in background
935
+ this.detectPortInBackgroundWithTimeout(serverId, timeoutMs);
936
+ await this.saveState();
937
+ // Starting a server is what makes this session its owner. Without a claim
938
+ // nothing can tell it apart from a server another window is using, so
939
+ // nobody may ever stop it (issue #139).
940
+ await this.claims.claim(serverId, cwd, isGlobal ?? false);
941
+ await debugLog('ServerManager', `Server started: ${serverId} (${runnerType}, PID: ${result.pid})`);
942
+ let autoRestartWarning;
943
+ if (autoRestartMatch && watch) {
944
+ autoRestartWarning = `Command matches "${autoRestartMatch}" AND cdp-tools' own watch mode is also enabled for this server - that's redundant and risky, since both could try to restart the process at once. Recommend removing "${autoRestartMatch}" from the command and relying on cdp-tools' watch mode instead, which already coordinates safely with an attached breakpoint debugger.`;
945
+ }
946
+ else if (autoRestartMatch) {
947
+ autoRestartWarning = `Command matches "${autoRestartMatch}", which auto-restarts its own process on file changes. Attaching a CDP debugger and pausing at a breakpoint while this is running is a known-bad combination (can cause EADDRINUSE crash-loops and ambiguous failed-but-still-listening states) - the restart happens entirely outside anything cdp-tools tracks. Prefer disabling auto-restart and using cdp-tools' own watch mode instead (server({ action: 'start', watch: true })), which coordinates safely with a paused debugger, or call server({ action: 'restart' }) explicitly.`;
948
+ }
949
+ return {
950
+ id: serverId,
951
+ pid: result.pid,
952
+ runnerType,
953
+ containerId: result.containerId,
954
+ autoRestartWarning,
955
+ };
956
+ }
957
+ /**
958
+ * Background port detection with timeout management
959
+ * @param serverId The server to detect port for
960
+ * @param timeoutMs How long to wait before triggering blocking (default 30s)
961
+ */
962
+ async detectPortInBackgroundWithTimeout(serverId, timeoutMs = 30000) {
963
+ const managed = this.servers.get(serverId);
964
+ if (!managed)
965
+ return;
966
+ const iterations = Math.ceil(timeoutMs / 1000);
967
+ const checkInterval = 1000; // Check every second
968
+ for (let i = 0; i < iterations; i++) {
969
+ await new Promise(resolve => setTimeout(resolve, checkInterval));
970
+ const current = this.servers.get(serverId);
971
+ if (!current) {
972
+ // Server was removed, clean up pending state
973
+ this.pendingStartups.delete(serverId);
974
+ await this.saveState();
975
+ break;
976
+ }
977
+ // Check if server is still running
978
+ const isRunning = await current.runner.isRunning();
979
+ if (!isRunning) {
980
+ // Server died during startup - trigger blocking
981
+ const pending = this.pendingStartups.get(serverId);
982
+ if (pending) {
983
+ pending.reason = 'died';
984
+ pending.acknowledged = false;
985
+ await this.saveState();
986
+ await debugLog('ServerManager', `Server died during startup: ${serverId}`);
987
+ }
988
+ return; // Exit detection, blocking will be triggered by getPendingStartupFailures
989
+ }
990
+ // Check if port already in status
991
+ const status = await current.runner.getStatus();
992
+ if (status.port) {
993
+ // Port already detected - remove from pending and set up monitoring
994
+ this.pendingStartups.delete(serverId);
995
+ if (current.monitorPort && !this.getPortMonitor().isMonitoring(status.port)) {
996
+ await this.getPortMonitor().startMonitoring(status.port, 'block', `Server: ${serverId}`);
997
+ await debugLog('ServerManager', `Auto-started monitoring for port ${status.port}`);
998
+ }
999
+ await this.saveState();
1000
+ return;
1001
+ }
1002
+ // Try to detect port from logs
1003
+ const port = await current.runner.detectPort();
1004
+ if (port) {
1005
+ // Port detected - remove from pending and set up monitoring
1006
+ this.pendingStartups.delete(serverId);
1007
+ await debugLog('ServerManager', `Detected port ${port} for ${serverId}`);
1008
+ if (current.monitorPort && !this.getPortMonitor().isMonitoring(port)) {
1009
+ await this.getPortMonitor().startMonitoring(port, 'block', `Server: ${serverId}`);
1010
+ await debugLog('ServerManager', `Auto-started monitoring for port ${port}`);
1011
+ }
1012
+ await this.saveState();
1013
+ return;
1014
+ }
1015
+ }
1016
+ // Timeout reached without port detection - trigger blocking
1017
+ const pending = this.pendingStartups.get(serverId);
1018
+ if (pending && !pending.reason) {
1019
+ pending.reason = 'timeout';
1020
+ pending.acknowledged = false;
1021
+ await this.saveState();
1022
+ await debugLog('ServerManager', `Port detection timeout for ${serverId}`);
1023
+ }
1024
+ }
1025
+ /**
1026
+ * @deprecated Use detectPortInBackgroundWithTimeout instead
1027
+ */
1028
+ async detectPortInBackground(serverId) {
1029
+ await this.detectPortInBackgroundWithTimeout(serverId, 30000);
1030
+ }
1031
+ // ============================================================================
1032
+ // Pending Startup Management
1033
+ // ============================================================================
1034
+ /**
1035
+ * Get pending startup failures that should trigger blocking
1036
+ * Returns only startups that have timed out or died and are not acknowledged
1037
+ */
1038
+ getPendingStartupFailures() {
1039
+ const failures = [];
1040
+ for (const [, pending] of this.pendingStartups) {
1041
+ // Only return failures (has reason) that are not acknowledged
1042
+ if (pending.reason && !pending.acknowledged) {
1043
+ failures.push({
1044
+ serverId: pending.serverId,
1045
+ startedAt: pending.startedAt,
1046
+ reason: pending.reason,
1047
+ });
1048
+ }
1049
+ }
1050
+ return failures;
1051
+ }
1052
+ /**
1053
+ * Check if a server has a pending startup (regardless of state)
1054
+ */
1055
+ hasPendingStartup(serverId) {
1056
+ return this.pendingStartups.has(serverId);
1057
+ }
1058
+ /**
1059
+ * Get pending startup status for a server
1060
+ */
1061
+ getPendingStartup(serverId) {
1062
+ return this.pendingStartups.get(serverId);
1063
+ }
1064
+ /**
1065
+ * Acknowledge a pending startup failure
1066
+ * Clears the blocking state and starts background health monitoring
1067
+ */
1068
+ async acknowledgeStartup(serverId) {
1069
+ const pending = this.pendingStartups.get(serverId);
1070
+ if (!pending) {
1071
+ return false;
1072
+ }
1073
+ // Clear pending state - no longer blocking
1074
+ this.pendingStartups.delete(serverId);
1075
+ await this.saveState();
1076
+ await debugLog('ServerManager', `Acknowledged pending startup: ${serverId}`);
1077
+ // Start background health monitoring
1078
+ // This will detect port (and set up monitoring) or detect server death (and re-block)
1079
+ this.monitorServerHealth(serverId);
1080
+ return true;
1081
+ }
1082
+ /**
1083
+ * Monitor server health after acknowledgment
1084
+ * Checks every 5 seconds for:
1085
+ * - Port detection (sets up port monitoring when found)
1086
+ * - Server death (re-triggers blocking)
1087
+ * Runs until port is found, server dies, or server is removed
1088
+ */
1089
+ async monitorServerHealth(serverId) {
1090
+ const checkInterval = 5000; // Check every 5 seconds
1091
+ while (true) {
1092
+ await new Promise(resolve => setTimeout(resolve, checkInterval));
1093
+ const managed = this.servers.get(serverId);
1094
+ if (!managed) {
1095
+ // Server was removed - clean exit
1096
+ return;
1097
+ }
1098
+ // Check if server is still running
1099
+ const isRunning = await managed.runner.isRunning();
1100
+ if (!isRunning) {
1101
+ // Server died - create blocking entry
1102
+ const now = new Date();
1103
+ this.pendingStartups.set(serverId, {
1104
+ serverId,
1105
+ startedAt: now,
1106
+ timeoutAt: now,
1107
+ acknowledged: false,
1108
+ reason: 'died',
1109
+ });
1110
+ await this.saveState();
1111
+ await debugLog('ServerManager', `Server died (health check): ${serverId}`);
1112
+ return;
1113
+ }
1114
+ // Try to detect port if not already monitoring
1115
+ const status = await managed.runner.getStatus();
1116
+ let port = status.port ?? undefined;
1117
+ if (!port) {
1118
+ port = (await managed.runner.detectPort()) ?? undefined;
1119
+ }
1120
+ if (port && managed.monitorPort && !this.getPortMonitor().isMonitoring(port)) {
1121
+ // Port found - set up monitoring, then port monitor takes over
1122
+ await this.getPortMonitor().startMonitoring(port, 'block', `Server: ${serverId}`);
1123
+ await debugLog('ServerManager', `Health check found port ${port} - monitoring started for ${serverId}`);
1124
+ await this.saveState();
1125
+ return; // Port monitor will handle it from here
1126
+ }
1127
+ // If port is being monitored, we can stop health checking
1128
+ if (port && this.getPortMonitor().isMonitoring(port)) {
1129
+ await debugLog('ServerManager', `Port ${port} already monitored, stopping health check for ${serverId}`);
1130
+ return;
1131
+ }
1132
+ }
1133
+ }
1134
+ /**
1135
+ * Extend the startup timeout by another 30 seconds
1136
+ * Resets the acknowledged flag and reason, resumes detection
1137
+ */
1138
+ async extendStartupTimeout(serverId) {
1139
+ const pending = this.pendingStartups.get(serverId);
1140
+ if (!pending) {
1141
+ return false;
1142
+ }
1143
+ // Check if server is still running
1144
+ const managed = this.servers.get(serverId);
1145
+ if (!managed) {
1146
+ return false;
1147
+ }
1148
+ const isRunning = await managed.runner.isRunning();
1149
+ if (!isRunning) {
1150
+ // Server died, can't extend
1151
+ pending.reason = 'died';
1152
+ pending.acknowledged = false;
1153
+ await this.saveState();
1154
+ return false;
1155
+ }
1156
+ // Reset timeout
1157
+ const now = new Date();
1158
+ const timeoutMs = 30000;
1159
+ pending.timeoutAt = new Date(now.getTime() + timeoutMs);
1160
+ pending.acknowledged = false;
1161
+ pending.reason = undefined;
1162
+ await this.saveState();
1163
+ await debugLog('ServerManager', `Extended startup timeout for ${serverId}`);
1164
+ // Resume background detection
1165
+ this.detectPortInBackgroundWithTimeout(serverId, timeoutMs);
1166
+ return true;
1167
+ }
1168
+ /**
1169
+ * Remove pending startup entry (called when server is stopped or removed)
1170
+ */
1171
+ removePendingStartup(serverId) {
1172
+ this.pendingStartups.delete(serverId);
1173
+ }
1174
+ /**
1175
+ * Stop a server
1176
+ */
1177
+ async stopServer(serverId) {
1178
+ const managed = this.servers.get(serverId);
1179
+ if (!managed) {
1180
+ throw new Error(`Server "${serverId}" not found. Use list action to see servers.`);
1181
+ }
1182
+ // Clean up pending startup state
1183
+ this.removePendingStartup(serverId);
1184
+ // Stop watching and drop any queued/in-flight watch-restart state - a
1185
+ // leaked watcher could otherwise fire a restart on an already-stopped
1186
+ // server.
1187
+ this.stopWatcher(managed);
1188
+ const isRunning = await managed.runner.isRunning();
1189
+ if (!isRunning) {
1190
+ await this.saveState();
1191
+ return;
1192
+ }
1193
+ await debugLog('ServerManager', `Stopping server ${serverId}`);
1194
+ await managed.runner.stop();
1195
+ this.claims.release(serverId, managed.global ?? false);
1196
+ await this.saveState();
1197
+ await debugLog('ServerManager', `Server ${serverId} stopped`);
1198
+ }
1199
+ /**
1200
+ * Restart a server
1201
+ * Reloads config from persisted state to pick up any manual edits (e.g., port changes)
1202
+ */
1203
+ async restartServer(serverId) {
1204
+ const managed = this.servers.get(serverId);
1205
+ if (!managed) {
1206
+ throw new Error(`Server "${serverId}" not found.`);
1207
+ }
1208
+ // Reload config from persisted state to pick up any manual edits
1209
+ const persistedConfig = await this.reloadServerConfig(serverId);
1210
+ // Use persisted config values if available, otherwise fall back to in-memory
1211
+ const command = persistedConfig?.command ?? this.getRunnerCommand(managed.runner);
1212
+ const cwd = persistedConfig?.cwd ?? this.getRunnerCwd(managed.runner);
1213
+ const port = persistedConfig?.port;
1214
+ const autoRun = persistedConfig?.autoRun ?? managed.autoRun;
1215
+ const monitorPort = persistedConfig?.monitorPort ?? managed.monitorPort;
1216
+ const runnerType = persistedConfig?.type ?? managed.runner.type;
1217
+ const watch = persistedConfig?.watch ?? managed.watch ?? false;
1218
+ const watchPaths = persistedConfig?.watchPaths ?? managed.watchPaths;
1219
+ // Get current status for the old port (to clean up if different from new port)
1220
+ const currentStatus = await managed.runner.getStatus();
1221
+ const oldPort = currentStatus.port;
1222
+ await this.stopServer(serverId);
1223
+ // Wait for old port to be released (if server was running)
1224
+ if (oldPort) {
1225
+ let released = await this.waitForPortRelease(oldPort, 3);
1226
+ if (!released) {
1227
+ // Kill any orphan processes holding the port, but only if they're from the same directory
1228
+ const { killed, foreign } = await this.killOrphanProcessesOnPort(oldPort, cwd, currentStatus.pid);
1229
+ if (foreign.length > 0 && oldPort === port) {
1230
+ // Foreign process on the port we want to use - error
1231
+ throw new Error(`Port ${oldPort} is in use by another application (PID: ${foreign.join(', ')}). Stop that process first or use a different port.`);
1232
+ }
1233
+ if (killed.length > 0) {
1234
+ await debugLog('ServerManager', `Killed ${killed.length} orphan process(es) on port ${oldPort}: ${killed.join(', ')}`);
1235
+ released = await this.waitForPortRelease(oldPort, 5);
1236
+ }
1237
+ }
1238
+ // Only error if we couldn't release the port AND it's the same port we want to use
1239
+ if (!released && oldPort === port) {
1240
+ throw new Error(`Port ${oldPort} is still in use after stopping server.`);
1241
+ }
1242
+ }
1243
+ else {
1244
+ await new Promise(resolve => setTimeout(resolve, 500));
1245
+ }
1246
+ // Check if new port (from config) is available
1247
+ if (port && port !== oldPort) {
1248
+ const newPortInUse = await this.isPortInUse(port);
1249
+ if (newPortInUse) {
1250
+ const pids = await this.findProcessesOnPort(port);
1251
+ throw new Error(`Port ${port} is already in use (PID: ${pids.join(', ')}). Choose a different port.`);
1252
+ }
1253
+ }
1254
+ return await this.startServer({
1255
+ command,
1256
+ cwd,
1257
+ id: serverId,
1258
+ autoRun,
1259
+ runner: runnerType,
1260
+ monitorPort,
1261
+ watch,
1262
+ watchPaths,
1263
+ });
1264
+ }
1265
+ getOrCreateWatchState(serverId) {
1266
+ let state = this.watchState.get(serverId);
1267
+ if (!state) {
1268
+ state = { restarting: false, restartQueued: false };
1269
+ this.watchState.set(serverId, state);
1270
+ }
1271
+ return state;
1272
+ }
1273
+ startWatcher(managed, watchPaths) {
1274
+ managed.watcher?.stop();
1275
+ managed.watcher = new ServerFileWatcher({
1276
+ paths: watchPaths,
1277
+ onChange: () => {
1278
+ void this.requestWatchRestart(managed.id);
1279
+ },
1280
+ });
1281
+ managed.watcher.start();
1282
+ }
1283
+ stopWatcher(managed) {
1284
+ managed.watcher?.stop();
1285
+ managed.watcher = undefined;
1286
+ this.watchState.delete(managed.id);
1287
+ }
1288
+ /**
1289
+ * Single entry point for every watch-triggered restart (file-change,
1290
+ * resume-triggered auto-fire, and its own re-entrant re-check once an
1291
+ * in-flight restart finishes). Always re-checks pause state fresh rather
1292
+ * than trusting a cached value - see issue #88.
1293
+ */
1294
+ async requestWatchRestart(serverId) {
1295
+ const managed = this.servers.get(serverId);
1296
+ if (!managed) {
1297
+ return;
1298
+ }
1299
+ const state = this.getOrCreateWatchState(serverId);
1300
+ if (state.restarting) {
1301
+ // Queue one more pass for after the in-flight restart finishes, rather
1302
+ // than piggybacking on it - the file that triggered THIS call might not
1303
+ // be reflected in whatever the in-flight restart already read from disk.
1304
+ state.restartQueued = true;
1305
+ return;
1306
+ }
1307
+ const command = this.getRunnerCommand(managed.runner);
1308
+ const inspectorPort = extractInspectorPort(command);
1309
+ const isPaused = inspectorPort !== null && this.pauseChecker ? this.pauseChecker(inspectorPort) : false;
1310
+ if (isPaused) {
1311
+ state.pendingRestart = { queuedAt: new Date() };
1312
+ return;
1313
+ }
1314
+ try {
1315
+ await this.performGuardedRestart(serverId);
1316
+ }
1317
+ catch (err) {
1318
+ await debugLog('ServerManager', `Watch-triggered restart of ${serverId} failed: ${err}`);
1319
+ }
1320
+ }
1321
+ /**
1322
+ * Explicit, forced restart (bypasses the pause-check entirely - the caller
1323
+ * has already decided to end any paused debug session). Shares the same
1324
+ * in-flight guard as requestWatchRestart(): if a watch-triggered restart is
1325
+ * already running, piggybacks on its result instead of racing it.
1326
+ */
1327
+ async forceRestart(serverId) {
1328
+ const state = this.getOrCreateWatchState(serverId);
1329
+ state.pendingRestart = undefined;
1330
+ if (state.restarting && state.inFlight) {
1331
+ return state.inFlight;
1332
+ }
1333
+ return await this.performGuardedRestart(serverId);
1334
+ }
1335
+ /** Shared restart guard used by both requestWatchRestart() and forceRestart(). */
1336
+ async performGuardedRestart(serverId) {
1337
+ const state = this.getOrCreateWatchState(serverId);
1338
+ state.restarting = true;
1339
+ state.pendingRestart = undefined;
1340
+ const promise = this.restartServer(serverId).finally(() => {
1341
+ state.restarting = false;
1342
+ state.inFlight = undefined;
1343
+ if (state.restartQueued) {
1344
+ state.restartQueued = false;
1345
+ void this.requestWatchRestart(serverId);
1346
+ }
1347
+ });
1348
+ state.inFlight = promise;
1349
+ return promise;
1350
+ }
1351
+ /** For checkBreakpointPause's lookup (via a paused connection's inspector port). */
1352
+ getPendingRestartByInspectorPort(port) {
1353
+ const managed = this.findManagedServerByInspectorPortSync(port);
1354
+ if (!managed) {
1355
+ return null;
1356
+ }
1357
+ const state = this.watchState.get(managed.id);
1358
+ return state?.pendingRestart ? { serverId: managed.id, queuedAt: state.pendingRestart.queuedAt } : null;
1359
+ }
1360
+ /** Discard a queued watch-restart without performing it - keep debugging. */
1361
+ cancelPendingRestart(serverId) {
1362
+ const state = this.watchState.get(serverId);
1363
+ if (!state?.pendingRestart) {
1364
+ return false;
1365
+ }
1366
+ state.pendingRestart = undefined;
1367
+ return true;
1368
+ }
1369
+ /**
1370
+ * Re-check a port's pause state and let its queued watch-restart (if any)
1371
+ * fire now that the debugger has resumed. Call this after any resume that
1372
+ * un-pauses a connection - requestWatchRestart() only re-fires on the next
1373
+ * file change otherwise, so without this hook a restart deferred by a
1374
+ * pause would sit queued forever if no further edits happen.
1375
+ */
1376
+ retryPendingRestartByInspectorPort(port) {
1377
+ const managed = this.findManagedServerByInspectorPortSync(port);
1378
+ if (managed && this.watchState.get(managed.id)?.pendingRestart) {
1379
+ void this.requestWatchRestart(managed.id);
1380
+ }
1381
+ }
1382
+ /**
1383
+ * Set autoRun flag
1384
+ */
1385
+ async setAutoRun(serverId, autoRun) {
1386
+ const managed = this.servers.get(serverId);
1387
+ if (!managed) {
1388
+ throw new Error(`Server "${serverId}" not found.`);
1389
+ }
1390
+ managed.autoRun = autoRun;
1391
+ await this.saveState();
1392
+ await debugLog('ServerManager', `Set autoRun=${autoRun} for ${serverId}`);
1393
+ }
1394
+ /**
1395
+ * Get status of servers
1396
+ */
1397
+ async getStatus(serverId) {
1398
+ const formatUptime = (startedAt) => {
1399
+ const seconds = Math.floor((Date.now() - startedAt.getTime()) / 1000);
1400
+ if (seconds < 60)
1401
+ return `${seconds}s`;
1402
+ if (seconds < 3600)
1403
+ return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
1404
+ const hours = Math.floor(seconds / 3600);
1405
+ const minutes = Math.floor((seconds % 3600) / 60);
1406
+ return `${hours}h ${minutes}m`;
1407
+ };
1408
+ const getServerStatus = async (managed) => {
1409
+ const status = await managed.runner.getStatus();
1410
+ return {
1411
+ id: managed.id,
1412
+ command: this.getRunnerCommand(managed.runner),
1413
+ cwd: this.getRunnerCwd(managed.runner),
1414
+ pid: status.pid,
1415
+ containerId: status.containerId,
1416
+ startedAt: status.startedAt ?? new Date(),
1417
+ uptime: status.startedAt ? formatUptime(status.startedAt) : '0s',
1418
+ port: status.port,
1419
+ running: status.running,
1420
+ autoRun: managed.autoRun,
1421
+ runnerType: managed.runner.type,
1422
+ global: managed.global ?? false,
1423
+ watch: managed.watch ?? false,
1424
+ };
1425
+ };
1426
+ if (serverId) {
1427
+ const managed = this.servers.get(serverId);
1428
+ if (!managed)
1429
+ return [];
1430
+ return [await getServerStatus(managed)];
1431
+ }
1432
+ const results = [];
1433
+ for (const managed of this.servers.values()) {
1434
+ results.push(await getServerStatus(managed));
1435
+ }
1436
+ return results;
1437
+ }
1438
+ /**
1439
+ * Get log stats (for native runner only)
1440
+ */
1441
+ getLogStats() {
1442
+ const stats = [];
1443
+ for (const [serverId, managed] of this.servers) {
1444
+ if (managed.runner instanceof NativeRunner) {
1445
+ const runnerStats = managed.runner.getLogStats();
1446
+ stats.push({
1447
+ serverId,
1448
+ newStdout: runnerStats.newStdout,
1449
+ newStderr: runnerStats.newStderr,
1450
+ });
1451
+ }
1452
+ else {
1453
+ stats.push({
1454
+ serverId,
1455
+ newStdout: 0,
1456
+ newStderr: 0,
1457
+ });
1458
+ }
1459
+ }
1460
+ return stats;
1461
+ }
1462
+ /**
1463
+ * Get logs from a server
1464
+ */
1465
+ async getLogs(serverId, options = {}) {
1466
+ const managed = this.servers.get(serverId);
1467
+ if (!managed) {
1468
+ throw new Error(`Server "${serverId}" not found`);
1469
+ }
1470
+ const { type = 'all', lines } = options;
1471
+ return await managed.runner.getLogs({ type, lines });
1472
+ }
1473
+ /**
1474
+ * Clear logs (native runner only)
1475
+ */
1476
+ async clearLogs(serverId) {
1477
+ const managed = this.servers.get(serverId);
1478
+ if (!managed) {
1479
+ throw new Error(`Server "${serverId}" not found`);
1480
+ }
1481
+ if (managed.runner instanceof NativeRunner) {
1482
+ return await managed.runner.clearLogs();
1483
+ }
1484
+ throw new Error(`clearLogs is only supported for native runner, not ${managed.runner.type}`);
1485
+ }
1486
+ /**
1487
+ * Get log access info (file paths for native, commands for docker)
1488
+ */
1489
+ getLogAccess(serverId) {
1490
+ const managed = this.servers.get(serverId);
1491
+ if (!managed) {
1492
+ throw new Error(`Server "${serverId}" not found`);
1493
+ }
1494
+ if (managed.runner.getLogAccess) {
1495
+ return managed.runner.getLogAccess();
1496
+ }
1497
+ return null;
1498
+ }
1499
+ /**
1500
+ * Stop every server this session owns outright - one no live session other
1501
+ * than this one claims. Used when the session is going away for good (an
1502
+ * idle suspend, or a client that closed), which is the only time it is safe
1503
+ * to take a dev server down.
1504
+ *
1505
+ * A server another window is still using is left alone, and so is one whose
1506
+ * ownership cannot be established: over-stopping destroys running work,
1507
+ * under-stopping leaves a process for the next `initialize()` to collect.
1508
+ */
1509
+ async stopOwnedServers() {
1510
+ const stopped = [];
1511
+ const keptForOthers = [];
1512
+ for (const [serverId, managed] of this.servers) {
1513
+ const isGlobal = managed.global ?? false;
1514
+ try {
1515
+ if (!(await managed.runner.isRunning()))
1516
+ continue;
1517
+ const serverCwd = this.getRunnerCwd(managed.runner) ?? process.cwd();
1518
+ if (!this.claims.mayStop(serverId, serverCwd, isGlobal)) {
1519
+ keptForOthers.push(serverId);
1520
+ // Drop only OUR claim: the server keeps running for whoever else
1521
+ // holds one, and stays collectable once they are gone too.
1522
+ this.claims.release(serverId, isGlobal);
1523
+ continue;
1524
+ }
1525
+ await this.stopServer(serverId);
1526
+ stopped.push(serverId);
1527
+ }
1528
+ catch (error) {
1529
+ await debugLog('ServerManager', `Failed to release ${serverId}: ${error}`);
1530
+ keptForOthers.push(serverId);
1531
+ }
1532
+ }
1533
+ return { stopped, keptForOthers };
1534
+ }
1535
+ /**
1536
+ * Server ids whose every claim was dead, per storage scope, with those dead
1537
+ * claims deleted as a side effect.
1538
+ *
1539
+ * Must be read BEFORE this session claims anything during recovery -
1540
+ * otherwise our own fresh claim makes an orphan look owned, and nothing is
1541
+ * ever collected.
1542
+ *
1543
+ * A server with no claim file at all is absent from this: it predates
1544
+ * claims, or was started outside cdp-tools, and neither is ours to kill.
1545
+ */
1546
+ findAbandonedServerIds() {
1547
+ const byScope = new Map();
1548
+ for (const isGlobal of [false, true]) {
1549
+ const { removed, unclaimedServerIds } = this.claims.collectDeadClaims(isGlobal);
1550
+ if (removed > 0) {
1551
+ void debugLog('ServerClaims', `Removed ${removed} dead claim(s) (global=${isGlobal})`);
1552
+ }
1553
+ byScope.set(isGlobal, new Set(unclaimedServerIds));
1554
+ }
1555
+ return byScope;
1556
+ }
1557
+ /**
1558
+ * Stop all servers
1559
+ */
1560
+ async stopAll() {
1561
+ const stopped = [];
1562
+ for (const serverId of this.servers.keys()) {
1563
+ try {
1564
+ await this.stopServer(serverId);
1565
+ stopped.push(serverId);
1566
+ }
1567
+ catch (error) {
1568
+ await debugLog('ServerManager', `Failed to stop ${serverId}: ${error}`);
1569
+ }
1570
+ }
1571
+ return stopped;
1572
+ }
1573
+ /**
1574
+ * Remove a server from config
1575
+ */
1576
+ async removeServer(serverId) {
1577
+ const managed = this.servers.get(serverId);
1578
+ if (!managed) {
1579
+ throw new Error(`Server "${serverId}" not found.`);
1580
+ }
1581
+ const isRunning = await managed.runner.isRunning();
1582
+ if (isRunning) {
1583
+ await this.stopServer(serverId);
1584
+ }
1585
+ else {
1586
+ // stopServer() (which also closes the watcher) is only called above
1587
+ // when running - make sure a leaked watcher/pending state can't
1588
+ // outlive removal either way.
1589
+ this.stopWatcher(managed);
1590
+ }
1591
+ this.servers.delete(serverId);
1592
+ await this.saveState();
1593
+ await debugLog('ServerManager', `Server ${serverId} removed from config`);
1594
+ }
1595
+ /**
1596
+ * Get running server IDs
1597
+ */
1598
+ async getRunningServerIds() {
1599
+ const running = [];
1600
+ for (const [id, managed] of this.servers) {
1601
+ if (await managed.runner.isRunning()) {
1602
+ running.push(id);
1603
+ }
1604
+ }
1605
+ return running;
1606
+ }
1607
+ /**
1608
+ * Cleanup - refresh status of all servers (no longer removes stopped servers)
1609
+ * Stopped servers remain in config and can be manually restarted
1610
+ */
1611
+ async cleanup() {
1612
+ // Pick up servers another session started after this one loaded its state.
1613
+ // Without this a second editor window can neither see nor stop them - its
1614
+ // map was built once at startup - and, more importantly, it never claims
1615
+ // them, so it has no say in whether they are collected.
1616
+ await this.adoptNewlyPersistedServers();
1617
+ // Servers are only removed via the explicit 'remove' action
1618
+ return 0;
1619
+ }
1620
+ /**
1621
+ * Add running servers that appeared in persisted state since this session
1622
+ * loaded, claiming each one: seeing a server is enough to depend on it.
1623
+ */
1624
+ async adoptNewlyPersistedServers() {
1625
+ let persisted;
1626
+ try {
1627
+ const [local, global] = await Promise.all([this.loadState(false), this.loadState(true)]);
1628
+ persisted = [
1629
+ ...local.servers.map(server => ({ ...server, global: false })),
1630
+ ...global.servers.map(server => ({ ...server, global: true })),
1631
+ ];
1632
+ }
1633
+ catch {
1634
+ return; // Unreadable state is not worth failing a list over.
1635
+ }
1636
+ for (const server of persisted) {
1637
+ if (this.servers.has(server.id))
1638
+ continue;
1639
+ try {
1640
+ const runner = createRunner(server.type || 'native', server.id);
1641
+ runner.restore(server);
1642
+ if (!(await runner.isRunning()))
1643
+ continue;
1644
+ this.servers.set(server.id, {
1645
+ id: server.id,
1646
+ runner,
1647
+ autoRun: server.autoRun,
1648
+ monitorPort: server.monitorPort,
1649
+ global: server.global,
1650
+ watch: server.watch,
1651
+ watchPaths: server.watchPaths,
1652
+ });
1653
+ await this.claims.claim(server.id, server.cwd, server.global);
1654
+ await debugLog('ServerManager', `Adopted server started elsewhere: ${server.id}`);
1655
+ }
1656
+ catch (error) {
1657
+ await debugLog('ServerManager', `Could not adopt ${server.id}: ${error}`);
1658
+ }
1659
+ }
1660
+ }
1661
+ /**
1662
+ * Reload a server's config from persisted state (for picking up manual edits)
1663
+ */
1664
+ async reloadServerConfig(serverId) {
1665
+ const localPersisted = await this.loadState(false);
1666
+ const globalPersisted = await this.loadState(true);
1667
+ const allServers = [
1668
+ ...localPersisted.servers.map(s => ({ ...s, global: false })),
1669
+ ...globalPersisted.servers.map(s => ({ ...s, global: true })),
1670
+ ];
1671
+ return allServers.find(s => s.id === serverId) || null;
1672
+ }
1673
+ }
1674
+ //# sourceMappingURL=server-manager.js.map