browser-debugger-cli 0.8.0 → 0.10.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 (304) hide show
  1. package/README.md +7 -1
  2. package/dist/cdp/schema.d.ts +4 -1
  3. package/dist/cdp/schema.js +48 -7
  4. package/dist/commands/cdp.js +3 -2
  5. package/dist/commands/cleanup.d.ts +11 -0
  6. package/dist/commands/cleanup.js +161 -57
  7. package/dist/commands/console.d.ts +20 -1
  8. package/dist/commands/console.js +57 -17
  9. package/dist/commands/details.js +3 -2
  10. package/dist/commands/dom/DomElementResolver.d.ts +10 -3
  11. package/dist/commands/dom/DomElementResolver.js +35 -17
  12. package/dist/commands/dom/a11y.d.ts +10 -0
  13. package/dist/commands/dom/a11y.js +29 -6
  14. package/dist/commands/dom/eval.d.ts +3 -1
  15. package/dist/commands/dom/eval.js +29 -4
  16. package/dist/commands/dom/form.js +16 -62
  17. package/dist/commands/dom/formInteraction.js +189 -119
  18. package/dist/commands/dom/formSummary.d.ts +49 -0
  19. package/dist/commands/dom/formSummary.js +180 -0
  20. package/dist/commands/dom/frames.d.ts +2 -1
  21. package/dist/commands/dom/frames.js +17 -2
  22. package/dist/commands/dom/get.d.ts +6 -5
  23. package/dist/commands/dom/get.js +92 -82
  24. package/dist/commands/dom/helpers/index.d.ts +1 -1
  25. package/dist/commands/dom/helpers/index.js +1 -1
  26. package/dist/commands/dom/helpers/keyAttributes.d.ts +20 -0
  27. package/dist/commands/dom/helpers/keyAttributes.js +54 -0
  28. package/dist/commands/dom/helpers/query.d.ts +44 -17
  29. package/dist/commands/dom/helpers/query.js +300 -106
  30. package/dist/commands/dom/helpers/runElementCommand.d.ts +10 -2
  31. package/dist/commands/dom/helpers/runElementCommand.js +98 -30
  32. package/dist/commands/dom/helpers/screenshot.d.ts +4 -1
  33. package/dist/commands/dom/helpers/screenshot.js +239 -51
  34. package/dist/commands/dom/index.d.ts +4 -1
  35. package/dist/commands/dom/index.js +22 -7
  36. package/dist/commands/dom/inspect.d.ts +15 -0
  37. package/dist/commands/dom/inspect.js +82 -0
  38. package/dist/commands/dom/layout.d.ts +14 -0
  39. package/dist/commands/dom/layout.js +54 -0
  40. package/dist/commands/dom/listeners.d.ts +5 -1
  41. package/dist/commands/dom/listeners.js +15 -5
  42. package/dist/commands/dom/query.js +2 -3
  43. package/dist/commands/dom/screenshot.d.ts +12 -2
  44. package/dist/commands/dom/screenshot.js +27 -3
  45. package/dist/commands/dom/semanticUtils.d.ts +16 -10
  46. package/dist/commands/dom/semanticUtils.js +53 -16
  47. package/dist/commands/dom/wait.d.ts +13 -0
  48. package/dist/commands/dom/wait.js +83 -0
  49. package/dist/commands/helpJson.js +2 -2
  50. package/dist/commands/network/list.js +17 -13
  51. package/dist/commands/optionBehaviors.js +154 -21
  52. package/dist/commands/page.d.ts +3 -2
  53. package/dist/commands/page.js +100 -5
  54. package/dist/commands/peek.js +4 -11
  55. package/dist/commands/sessions.d.ts +8 -0
  56. package/dist/commands/sessions.js +19 -0
  57. package/dist/commands/shared/CommandRunner.js +4 -4
  58. package/dist/commands/shared/commonOptions.d.ts +4 -0
  59. package/dist/commands/shared/commonOptions.js +9 -0
  60. package/dist/commands/shared/dataFetcher.js +2 -2
  61. package/dist/commands/shared/followMode.d.ts +21 -1
  62. package/dist/commands/shared/followMode.js +29 -2
  63. package/dist/commands/shared/handleValidationError.d.ts +2 -2
  64. package/dist/commands/shared/handleValidationError.js +12 -3
  65. package/dist/commands/shared/optionTypes.d.ts +61 -5
  66. package/dist/commands/shared/startHelpers.d.ts +66 -0
  67. package/dist/commands/shared/startHelpers.js +103 -13
  68. package/dist/commands/shared/validation.d.ts +14 -2
  69. package/dist/commands/shared/validation.js +20 -3
  70. package/dist/commands/start.d.ts +63 -0
  71. package/dist/commands/start.js +115 -15
  72. package/dist/commands/status.js +29 -7
  73. package/dist/commands/stop.js +7 -6
  74. package/dist/commands/tail.js +4 -11
  75. package/dist/commands/types.d.ts +2 -0
  76. package/dist/commands.js +2 -0
  77. package/dist/connection/chromeIdentity.d.ts +65 -0
  78. package/dist/connection/chromeIdentity.js +143 -0
  79. package/dist/connection/launcher/profilePreferences.d.ts +47 -0
  80. package/dist/connection/launcher/profilePreferences.js +151 -0
  81. package/dist/connection/launcher.d.ts +21 -2
  82. package/dist/connection/launcher.js +42 -16
  83. package/dist/connection/portReservation.d.ts +14 -4
  84. package/dist/connection/portReservation.js +21 -6
  85. package/dist/connection/startupExit.d.ts +8 -0
  86. package/dist/connection/startupExit.js +15 -6
  87. package/dist/constants.d.ts +6 -2
  88. package/dist/constants.js +9 -2
  89. package/dist/daemon/SessionController.js +23 -7
  90. package/dist/daemon/errors.d.ts +1 -1
  91. package/dist/daemon/errors.js +1 -1
  92. package/dist/daemon/launcher.d.ts +10 -2
  93. package/dist/daemon/launcher.js +8 -7
  94. package/dist/daemon/server/SocketServer.js +1 -2
  95. package/dist/daemon/session/Session.d.ts +20 -0
  96. package/dist/daemon/session/Session.js +80 -9
  97. package/dist/daemon/session/chromeConnection.d.ts +9 -0
  98. package/dist/daemon/session/chromeConnection.js +45 -8
  99. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  100. package/dist/daemon/session/commandRegistry.js +113 -67
  101. package/dist/daemon/session/interactions.d.ts +48 -9
  102. package/dist/daemon/session/interactions.js +46 -9
  103. package/dist/daemon/session/triggeredRequests.d.ts +67 -0
  104. package/dist/daemon/session/triggeredRequests.js +157 -0
  105. package/dist/daemon/session/types.d.ts +5 -1
  106. package/dist/daemon.js +10630 -3601
  107. package/dist/errors/messages.d.ts +456 -24
  108. package/dist/errors/messages.js +862 -67
  109. package/dist/index.js +6915 -3401
  110. package/dist/ipc/client.d.ts +21 -1
  111. package/dist/ipc/client.js +35 -3
  112. package/dist/ipc/protocol/commands.d.ts +145 -5
  113. package/dist/ipc/protocol/commands.js +4 -0
  114. package/dist/ipc/protocol/domTypes.d.ts +291 -7
  115. package/dist/ipc/protocol/inspectTypes.d.ts +388 -0
  116. package/dist/ipc/protocol/inspectTypes.js +10 -0
  117. package/dist/ipc/session/lifecycle.d.ts +8 -1
  118. package/dist/ipc/session/queries.d.ts +5 -1
  119. package/dist/ipc/session/types.d.ts +5 -0
  120. package/dist/ipc/transport/index.d.ts +2 -1
  121. package/dist/ipc/transport/index.js +2 -2
  122. package/dist/runtime/dom/actionEffects.d.ts +185 -0
  123. package/dist/runtime/dom/actionEffects.js +402 -0
  124. package/dist/runtime/dom/actionEffectsScripts.d.ts +90 -0
  125. package/dist/runtime/dom/actionEffectsScripts.js +426 -0
  126. package/dist/runtime/dom/elementGeometry.d.ts +170 -0
  127. package/dist/runtime/dom/elementGeometry.js +553 -0
  128. package/dist/runtime/dom/elementInfo.d.ts +103 -0
  129. package/dist/runtime/dom/elementInfo.js +256 -0
  130. package/dist/runtime/dom/evalHelpers.d.ts +51 -6
  131. package/dist/runtime/dom/evalHelpers.js +136 -26
  132. package/dist/runtime/dom/eventListeners.d.ts +2 -1
  133. package/dist/runtime/dom/eventListeners.js +184 -47
  134. package/dist/runtime/dom/formDiscovery.d.ts +1 -1
  135. package/dist/runtime/dom/formDiscovery.js +116 -16
  136. package/dist/runtime/dom/formFillHelpers/fill.d.ts +9 -0
  137. package/dist/runtime/dom/formFillHelpers/fill.js +178 -14
  138. package/dist/runtime/dom/formFillHelpers/index.d.ts +2 -2
  139. package/dist/runtime/dom/formFillHelpers/index.js +2 -2
  140. package/dist/runtime/dom/formFillHelpers/pressKey.js +14 -3
  141. package/dist/runtime/dom/formFillHelpers/scroll.d.ts +3 -0
  142. package/dist/runtime/dom/formFillHelpers/scroll.js +60 -18
  143. package/dist/runtime/dom/formFillHelpers/shared.d.ts +17 -0
  144. package/dist/runtime/dom/formFillHelpers/shared.js +25 -1
  145. package/dist/runtime/dom/formFillHelpers/stability.d.ts +20 -6
  146. package/dist/runtime/dom/formFillHelpers/stability.js +50 -19
  147. package/dist/runtime/dom/formSubmitHelpers.d.ts +3 -0
  148. package/dist/runtime/dom/formSubmitHelpers.js +89 -15
  149. package/dist/runtime/dom/frameLayout.d.ts +60 -0
  150. package/dist/runtime/dom/frameLayout.js +140 -0
  151. package/dist/runtime/dom/frameOrigin.d.ts +50 -0
  152. package/dist/runtime/dom/frameOrigin.js +62 -0
  153. package/dist/runtime/dom/frameScopedConnection.d.ts +92 -0
  154. package/dist/runtime/dom/frameScopedConnection.js +252 -0
  155. package/dist/runtime/dom/frameSelection.d.ts +12 -1
  156. package/dist/runtime/dom/frameSelection.js +22 -3
  157. package/dist/runtime/dom/frames.d.ts +61 -5
  158. package/dist/runtime/dom/frames.js +329 -75
  159. package/dist/runtime/dom/inspect.d.ts +28 -0
  160. package/dist/runtime/dom/inspect.js +557 -0
  161. package/dist/runtime/dom/inspectAllStyles.d.ts +62 -0
  162. package/dist/runtime/dom/inspectAllStyles.js +385 -0
  163. package/dist/runtime/dom/inspectCascade.d.ts +94 -0
  164. package/dist/runtime/dom/inspectCascade.js +371 -0
  165. package/dist/runtime/dom/inspectCascadeModel.d.ts +39 -0
  166. package/dist/runtime/dom/inspectCascadeModel.js +232 -0
  167. package/dist/runtime/dom/inspectHints.d.ts +62 -0
  168. package/dist/runtime/dom/inspectHints.js +305 -0
  169. package/dist/runtime/dom/inspectLayoutModel.d.ts +87 -0
  170. package/dist/runtime/dom/inspectLayoutModel.js +346 -0
  171. package/dist/runtime/dom/inspectModel.d.ts +74 -0
  172. package/dist/runtime/dom/inspectModel.js +184 -0
  173. package/dist/runtime/dom/inspectPaintModel.d.ts +157 -0
  174. package/dist/runtime/dom/inspectPaintModel.js +461 -0
  175. package/dist/runtime/dom/inspectRules.d.ts +37 -0
  176. package/dist/runtime/dom/inspectRules.js +101 -0
  177. package/dist/runtime/dom/inspectScripts.d.ts +132 -0
  178. package/dist/runtime/dom/inspectScripts.js +263 -0
  179. package/dist/runtime/dom/inspectTree.d.ts +40 -0
  180. package/dist/runtime/dom/inspectTree.js +134 -0
  181. package/dist/runtime/dom/inspectVariables.d.ts +33 -0
  182. package/dist/runtime/dom/inspectVariables.js +94 -0
  183. package/dist/runtime/dom/inspectWhyModel.d.ts +20 -0
  184. package/dist/runtime/dom/inspectWhyModel.js +134 -0
  185. package/dist/runtime/dom/layout.d.ts +71 -0
  186. package/dist/runtime/dom/layout.js +340 -0
  187. package/dist/runtime/dom/listenerPageScripts.d.ts +72 -0
  188. package/dist/runtime/dom/listenerPageScripts.js +365 -0
  189. package/dist/runtime/dom/listenerSummary.d.ts +136 -11
  190. package/dist/runtime/dom/listenerSummary.js +361 -22
  191. package/dist/runtime/dom/pageActivity.d.ts +41 -0
  192. package/dist/runtime/dom/pageActivity.js +123 -0
  193. package/dist/runtime/dom/reactEventHelpers.d.ts +63 -2
  194. package/dist/runtime/dom/reactEventHelpers.js +220 -41
  195. package/dist/runtime/dom/targetNode.d.ts +80 -27
  196. package/dist/runtime/dom/targetNode.js +249 -33
  197. package/dist/runtime/dom/wait.d.ts +25 -0
  198. package/dist/runtime/dom/wait.js +199 -0
  199. package/dist/runtime/dom/waitCondition.d.ts +71 -0
  200. package/dist/runtime/dom/waitCondition.js +75 -0
  201. package/dist/runtime/page/emulation.d.ts +71 -0
  202. package/dist/runtime/page/emulation.js +117 -0
  203. package/dist/runtime/page/loadingState.d.ts +36 -0
  204. package/dist/runtime/page/loadingState.js +86 -0
  205. package/dist/runtime/page/navigation.d.ts +46 -2
  206. package/dist/runtime/page/navigation.js +69 -33
  207. package/dist/session/QueryCacheManager.d.ts +11 -1
  208. package/dist/session/QueryCacheManager.js +25 -3
  209. package/dist/session/chromeOwners.d.ts +34 -0
  210. package/dist/session/chromeOwners.js +51 -0
  211. package/dist/session/cleanup/staleSession.d.ts +11 -1
  212. package/dist/session/cleanup/staleSession.js +17 -6
  213. package/dist/session/cleanup/userCommands.js +2 -4
  214. package/dist/session/metadata.d.ts +5 -1
  215. package/dist/session/metadata.js +2 -1
  216. package/dist/session/paths.d.ts +77 -3
  217. package/dist/session/paths.js +111 -5
  218. package/dist/session/port.d.ts +31 -7
  219. package/dist/session/port.js +50 -43
  220. package/dist/session/portClaims.d.ts +66 -0
  221. package/dist/session/portClaims.js +284 -0
  222. package/dist/session/sessionList.d.ts +58 -0
  223. package/dist/session/sessionList.js +199 -0
  224. package/dist/session/sessionName.d.ts +46 -0
  225. package/dist/session/sessionName.js +97 -0
  226. package/dist/telemetry/a11y.d.ts +18 -3
  227. package/dist/telemetry/a11y.js +170 -29
  228. package/dist/telemetry/console.d.ts +1 -0
  229. package/dist/telemetry/console.js +100 -5
  230. package/dist/telemetry/network.js +3 -1
  231. package/dist/telemetry/requestKinds.d.ts +32 -0
  232. package/dist/telemetry/requestKinds.js +61 -0
  233. package/dist/telemetry/requestState.d.ts +31 -0
  234. package/dist/telemetry/requestState.js +38 -0
  235. package/dist/types.d.ts +112 -3
  236. package/dist/ui/formatters/a11y.js +3 -0
  237. package/dist/ui/formatters/console/chronological.d.ts +8 -0
  238. package/dist/ui/formatters/console/chronological.js +17 -4
  239. package/dist/ui/formatters/console/json.js +3 -4
  240. package/dist/ui/formatters/console/shared.d.ts +12 -0
  241. package/dist/ui/formatters/console.d.ts +2 -2
  242. package/dist/ui/formatters/console.js +1 -1
  243. package/dist/ui/formatters/details.d.ts +8 -0
  244. package/dist/ui/formatters/details.js +61 -4
  245. package/dist/ui/formatters/dom.d.ts +27 -14
  246. package/dist/ui/formatters/dom.js +88 -59
  247. package/dist/ui/formatters/form.js +29 -18
  248. package/dist/ui/formatters/inspect.d.ts +39 -0
  249. package/dist/ui/formatters/inspect.js +596 -0
  250. package/dist/ui/formatters/keyAttributes.d.ts +19 -0
  251. package/dist/ui/formatters/keyAttributes.js +84 -0
  252. package/dist/ui/formatters/layout.d.ts +31 -0
  253. package/dist/ui/formatters/layout.js +53 -0
  254. package/dist/ui/formatters/listeners.d.ts +3 -2
  255. package/dist/ui/formatters/listeners.js +73 -9
  256. package/dist/ui/formatters/networkHeaders.d.ts +13 -0
  257. package/dist/ui/formatters/networkHeaders.js +36 -3
  258. package/dist/ui/formatters/networkList.d.ts +29 -1
  259. package/dist/ui/formatters/networkList.js +86 -20
  260. package/dist/ui/formatters/preview.js +2 -1
  261. package/dist/ui/formatters/requestStatus.d.ts +1 -17
  262. package/dist/ui/formatters/requestStatus.js +2 -30
  263. package/dist/ui/formatters/sessions.d.ts +12 -0
  264. package/dist/ui/formatters/sessions.js +40 -0
  265. package/dist/ui/formatters/status.d.ts +21 -2
  266. package/dist/ui/formatters/status.js +47 -10
  267. package/dist/ui/formatters/triggeredRequests.d.ts +36 -0
  268. package/dist/ui/formatters/triggeredRequests.js +65 -0
  269. package/dist/ui/formatting.d.ts +19 -0
  270. package/dist/ui/formatting.js +31 -36
  271. package/dist/ui/messages/chrome.d.ts +9 -0
  272. package/dist/ui/messages/chrome.js +17 -5
  273. package/dist/ui/messages/commands.d.ts +504 -14
  274. package/dist/ui/messages/commands.js +835 -21
  275. package/dist/ui/messages/consoleMessages.d.ts +10 -0
  276. package/dist/ui/messages/consoleMessages.js +17 -0
  277. package/dist/ui/messages/hints.js +2 -1
  278. package/dist/ui/messages/networkMessages.d.ts +14 -0
  279. package/dist/ui/messages/networkMessages.js +18 -0
  280. package/dist/ui/messages/preview.js +5 -4
  281. package/dist/ui/messages/session.d.ts +30 -21
  282. package/dist/ui/messages/session.js +48 -26
  283. package/dist/ui/messages/sessionCommand.d.ts +43 -0
  284. package/dist/ui/messages/sessionCommand.js +52 -0
  285. package/dist/utils/async.d.ts +17 -0
  286. package/dist/utils/async.js +36 -0
  287. package/dist/utils/color.d.ts +84 -0
  288. package/dist/utils/color.js +376 -0
  289. package/dist/utils/cssValues.d.ts +109 -0
  290. package/dist/utils/cssValues.js +236 -0
  291. package/dist/utils/http.d.ts +22 -1
  292. package/dist/utils/http.js +28 -9
  293. package/dist/utils/selectorFilters.d.ts +48 -8
  294. package/dist/utils/selectorFilters.js +296 -53
  295. package/dist/utils/shellDetection.d.ts +8 -2
  296. package/dist/utils/shellDetection.js +120 -33
  297. package/dist/utils/suggestions.d.ts +26 -0
  298. package/dist/utils/suggestions.js +73 -0
  299. package/dist/utils/taskMappings.js +10 -0
  300. package/dist/utils/url.d.ts +12 -2
  301. package/dist/utils/url.js +69 -7
  302. package/package.json +1 -1
  303. package/dist/ui/formatters/sessionFormatters.d.ts +0 -58
  304. package/dist/ui/formatters/sessionFormatters.js +0 -121
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Identity of the Chrome bdg launched.
3
+ *
4
+ * chrome-launcher only waits until something answers on 127.0.0.1:<port>. That
5
+ * may be another process: when 127.0.0.1:<port> is taken, Chrome listens on
6
+ * [::1]:<port> instead and the readiness check is answered by the other one.
7
+ *
8
+ * Chrome prints `DevTools listening on ws://<host>:<port>/devtools/browser/<id>`
9
+ * to stderr (chrome-launcher writes it to `chrome-err.log` in the profile).
10
+ * The browser path is unique per browser, so comparing it with the one
11
+ * 127.0.0.1:<port> reports tells whether that is the launched Chrome.
12
+ * (Chrome's `DevToolsActivePort` file holds the same, but Chrome writes it only
13
+ * for `--remote-debugging-port=0`.)
14
+ *
15
+ * A Chrome whose output lacks the line (stderr redirected, a Chromium build
16
+ * that does not print it) is accepted with a warning when its browser answers
17
+ * on 127.0.0.1 and nothing listens on [::1]: the port was free on both right
18
+ * before the launch, and a second listener is how a conflict shows.
19
+ */
20
+ import { createLogger } from '../ui/logging/index.js';
21
+ import { delay } from '../utils/async.js';
22
+ import { fetchBrowserWsUrl } from '../utils/http.js';
23
+ import { isProcessAlive } from '../utils/process.js';
24
+ import { ChromeLaunchError } from './errors.js';
25
+ import { acceptsConnections } from './portReservation.js';
26
+ import { readStartupLines } from './startupExit.js';
27
+ /** Chrome's stderr line announcing its debugging endpoint */
28
+ const LISTENING_PATTERN = /^DevTools listening on (ws:\/\/\S+)/;
29
+ /** How long to wait for Chrome to announce its endpoint after the port answered */
30
+ const ENDPOINT_WAIT_MS = 10000;
31
+ const ENDPOINT_POLL_MS = 50;
32
+ const log = createLogger('chrome');
33
+ /**
34
+ * Find the endpoint Chrome announced in its output (the last announcement).
35
+ *
36
+ * @param lines - Chrome's output lines
37
+ * @returns Endpoint, or null if there is no complete announcement
38
+ */
39
+ export function parseDevToolsListening(lines) {
40
+ for (const line of [...lines].reverse()) {
41
+ const match = LISTENING_PATTERN.exec(line.trim());
42
+ if (!match?.[1])
43
+ continue;
44
+ try {
45
+ const url = new URL(match[1]);
46
+ const port = Number(url.port);
47
+ if (!port || !url.pathname.startsWith('/devtools/browser/'))
48
+ continue;
49
+ return { host: url.hostname, port, browserPath: url.pathname };
50
+ }
51
+ catch (error) {
52
+ log.debug(`Ignoring malformed DevTools announcement: ${String(error)}`);
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+ /**
58
+ * Wait for Chrome to announce its endpoint.
59
+ *
60
+ * @param logs - Chrome's log positions from before the launch
61
+ * @param isRunning - Whether Chrome is still running (waiting stops when not)
62
+ * @param timeoutMs - Longest wait
63
+ * @returns Endpoint, or null if Chrome exited or announced none in time
64
+ */
65
+ export async function waitForDevToolsEndpoint(logs, isRunning, timeoutMs = ENDPOINT_WAIT_MS) {
66
+ const deadline = Date.now() + timeoutMs;
67
+ for (;;) {
68
+ const endpoint = parseDevToolsListening(readStartupLines(logs));
69
+ if (endpoint || !isRunning() || Date.now() >= deadline)
70
+ return endpoint;
71
+ await delay(ENDPOINT_POLL_MS);
72
+ }
73
+ }
74
+ /**
75
+ * Check that the Chrome answering on 127.0.0.1:<port> is the one just
76
+ * launched, so bdg never drives another session's browser.
77
+ *
78
+ * @param options - Chrome's log positions from before the launch, requested
79
+ * port (free on 127.0.0.1 and ::1 right before the launch), Chrome's PID and
80
+ * longest wait for its announcement
81
+ * @throws ChromeLaunchError: CHROME_DIED_AFTER_LAUNCH if Chrome exits first;
82
+ * PORT_IN_USE if another browser or process answers on the port;
83
+ * CHROME_LAUNCH_FAILED if Chrome announced nothing and nothing answers
84
+ */
85
+ export async function verifyLaunchedChrome(options) {
86
+ const { logs, port, pid, timeoutMs } = options;
87
+ const isRunning = () => isProcessAlive(pid);
88
+ const endpoint = await waitForDevToolsEndpoint(logs, isRunning, timeoutMs);
89
+ if (!endpoint && !isRunning()) {
90
+ throw new ChromeLaunchError(`Chrome died immediately after launch (PID: ${pid})`, {
91
+ issue: { code: 'CHROME_DIED_AFTER_LAUNCH', context: { port, pid } },
92
+ });
93
+ }
94
+ if (!endpoint)
95
+ return acceptUnannouncedChrome(logs, port);
96
+ if (endpoint.port !== port) {
97
+ throw portTakenError(port, `Chrome listens on port ${endpoint.port} instead`);
98
+ }
99
+ const answering = await fetchBrowserWsUrl(port, log);
100
+ if (!answering || new URL(answering).pathname !== endpoint.browserPath) {
101
+ throw portTakenError(port, `another process answers on 127.0.0.1 (Chrome listens on ${endpoint.host})`);
102
+ }
103
+ log.debug(`Chrome on port ${port} is the launched one (${endpoint.browserPath})`);
104
+ }
105
+ /**
106
+ * Accept a Chrome that did not announce its endpoint, if a browser answers on
107
+ * 127.0.0.1:<port> and nothing listens on [::1]:<port> (two listeners mean a
108
+ * conflict).
109
+ *
110
+ * @param logs - Chrome's log positions (the stderr log is named in messages)
111
+ * @param port - Requested port
112
+ * @throws ChromeLaunchError: PORT_IN_USE for a second listener;
113
+ * CHROME_LAUNCH_FAILED if no browser answers
114
+ */
115
+ async function acceptUnannouncedChrome(logs, port) {
116
+ const errLog = logs.files[0]?.file ?? 'chrome-err.log';
117
+ const missing = `Chrome did not print "DevTools listening on" to ${errLog}`;
118
+ if (!(await fetchBrowserWsUrl(port, log))) {
119
+ throw new ChromeLaunchError(`${missing}, and no browser answers on port ${port}`, {
120
+ issue: {
121
+ code: 'CHROME_LAUNCH_FAILED',
122
+ context: { port, reason: `${missing}, and no browser answers on port ${port}` },
123
+ },
124
+ });
125
+ }
126
+ if (await acceptsConnections(port, '::1')) {
127
+ throw portTakenError(port, `${missing}, and both 127.0.0.1 and [::1] answer`);
128
+ }
129
+ log.info(`Warning: ${missing}; using the browser on 127.0.0.1:${port}, which was free before the launch`);
130
+ }
131
+ /**
132
+ * The error for a port another process answers on.
133
+ *
134
+ * @param port - Requested port
135
+ * @param reason - What was found
136
+ * @returns Launch error with the PORT_IN_USE issue
137
+ */
138
+ function portTakenError(port, reason) {
139
+ return new ChromeLaunchError(`Port ${port} is already in use: ${reason}`, {
140
+ issue: { code: 'PORT_IN_USE', context: { port, reason } },
141
+ });
142
+ }
143
+ //# sourceMappingURL=chromeIdentity.js.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Chrome profile preferences writer.
3
+ *
4
+ * Chrome reads `<user-data-dir>/Default/Preferences` as nested JSON and looks
5
+ * preferences up by path (`profile.password_manager_enabled` is
6
+ * `{"profile": {"password_manager_enabled": ...}}`). bdg's preferences use
7
+ * dotted names, so they are expanded into that structure and deep-merged into
8
+ * the existing file: persistent profiles reused across sessions get them too,
9
+ * and everything else Chrome stored there is kept.
10
+ */
11
+ import type { Logger } from '../types.js';
12
+ /** JSON object as stored in the Preferences file */
13
+ type PrefsObject = Record<string, unknown>;
14
+ /**
15
+ * Merge preferences into an existing Preferences object.
16
+ *
17
+ * Dotted names become nested paths, and a literal dotted top-level key (as
18
+ * earlier bdg versions wrote, which Chrome ignored) is removed.
19
+ *
20
+ * @param existing - Current Preferences content
21
+ * @param prefs - Preferences to apply, dotted or nested
22
+ * @returns Merged Preferences (a new object; `existing` is not modified)
23
+ */
24
+ export declare function mergePreferences(existing: PrefsObject, prefs: PrefsObject): PrefsObject;
25
+ /**
26
+ * Whether a running Chrome has the profile open, judged by its
27
+ * `SingletonLock` symlink (`<host>-<pid>`, macOS and Linux).
28
+ *
29
+ * @param userDataDir - Chrome user data directory
30
+ * @returns True if the lock names a live process on this host
31
+ */
32
+ export declare function profileInUse(userDataDir: string): boolean;
33
+ /**
34
+ * Apply preferences to the default profile in a Chrome user data directory.
35
+ *
36
+ * Failures are logged (visible in daemon.log), not thrown: Chrome still
37
+ * starts, only without them. An unreadable Preferences file and a profile
38
+ * that a running Chrome has open are left alone, and the file is not
39
+ * rewritten when nothing changes.
40
+ *
41
+ * @param userDataDir - Chrome user data directory
42
+ * @param prefs - Preferences to apply, dotted or nested
43
+ * @param logger - Logger for failures
44
+ */
45
+ export declare function writeProfilePreferences(userDataDir: string, prefs: PrefsObject, logger: Logger): void;
46
+ export {};
47
+ //# sourceMappingURL=profilePreferences.d.ts.map
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Chrome profile preferences writer.
3
+ *
4
+ * Chrome reads `<user-data-dir>/Default/Preferences` as nested JSON and looks
5
+ * preferences up by path (`profile.password_manager_enabled` is
6
+ * `{"profile": {"password_manager_enabled": ...}}`). bdg's preferences use
7
+ * dotted names, so they are expanded into that structure and deep-merged into
8
+ * the existing file: persistent profiles reused across sessions get them too,
9
+ * and everything else Chrome stored there is kept.
10
+ */
11
+ import * as fs from 'fs';
12
+ import * as os from 'os';
13
+ import * as path from 'path';
14
+ import { chromePrefsNotAppliedMessage } from '../../ui/messages/chrome.js';
15
+ import { getErrorMessage } from '../../utils/errors.js';
16
+ import { isProcessAlive } from '../../utils/process.js';
17
+ /**
18
+ * Whether a value is a plain JSON object (not an array or null).
19
+ *
20
+ * @param value - Value to check
21
+ * @returns True for objects that can be merged into
22
+ */
23
+ function isPrefsObject(value) {
24
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
25
+ }
26
+ /**
27
+ * Set a preference at a nested path, merging objects with what is there.
28
+ *
29
+ * @param target - Preferences object to modify
30
+ * @param keys - Path segments, e.g. `['profile', 'exit_type']`
31
+ * @param value - Value to set
32
+ */
33
+ function setAtPath(target, keys, value) {
34
+ const [key, ...rest] = keys;
35
+ if (key === undefined)
36
+ return;
37
+ const current = target[key];
38
+ if (rest.length > 0) {
39
+ const child = isPrefsObject(current) ? current : {};
40
+ target[key] = child;
41
+ setAtPath(child, rest, value);
42
+ return;
43
+ }
44
+ if (isPrefsObject(current) && isPrefsObject(value)) {
45
+ for (const [childKey, childValue] of Object.entries(value)) {
46
+ setAtPath(current, [childKey], childValue);
47
+ }
48
+ return;
49
+ }
50
+ target[key] = value;
51
+ }
52
+ /**
53
+ * Merge preferences into an existing Preferences object.
54
+ *
55
+ * Dotted names become nested paths, and a literal dotted top-level key (as
56
+ * earlier bdg versions wrote, which Chrome ignored) is removed.
57
+ *
58
+ * @param existing - Current Preferences content
59
+ * @param prefs - Preferences to apply, dotted or nested
60
+ * @returns Merged Preferences (a new object; `existing` is not modified)
61
+ */
62
+ export function mergePreferences(existing, prefs) {
63
+ const merged = structuredClone(existing);
64
+ for (const [name, value] of Object.entries(prefs)) {
65
+ if (name.includes('.'))
66
+ delete merged[name];
67
+ setAtPath(merged, name.split('.'), value);
68
+ }
69
+ return merged;
70
+ }
71
+ /**
72
+ * Read the profile's Preferences file.
73
+ *
74
+ * @param file - Preferences file path
75
+ * @returns Its content, `{}` if missing
76
+ * @throws Error if it is not valid JSON or not a JSON object
77
+ */
78
+ function readPreferencesFile(file) {
79
+ if (!fs.existsSync(file))
80
+ return {};
81
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
82
+ if (!isPrefsObject(parsed))
83
+ throw new Error('not a JSON object');
84
+ return parsed;
85
+ }
86
+ /**
87
+ * Whether a running Chrome has the profile open, judged by its
88
+ * `SingletonLock` symlink (`<host>-<pid>`, macOS and Linux).
89
+ *
90
+ * @param userDataDir - Chrome user data directory
91
+ * @returns True if the lock names a live process on this host
92
+ */
93
+ export function profileInUse(userDataDir) {
94
+ let target;
95
+ try {
96
+ target = fs.readlinkSync(path.join(userDataDir, 'SingletonLock'));
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ const separator = target.lastIndexOf('-');
102
+ const pid = Number(target.slice(separator + 1));
103
+ return (target.slice(0, separator) === os.hostname() && Number.isInteger(pid) && isProcessAlive(pid));
104
+ }
105
+ /**
106
+ * Replace a file atomically: write a temporary file next to it, then rename.
107
+ *
108
+ * @param file - File to replace
109
+ * @param content - New content
110
+ */
111
+ function writeFileAtomic(file, content) {
112
+ const temp = `${file}.bdg-${process.pid}.tmp`;
113
+ try {
114
+ fs.writeFileSync(temp, content, 'utf8');
115
+ fs.renameSync(temp, file);
116
+ }
117
+ finally {
118
+ fs.rmSync(temp, { force: true });
119
+ }
120
+ }
121
+ /**
122
+ * Apply preferences to the default profile in a Chrome user data directory.
123
+ *
124
+ * Failures are logged (visible in daemon.log), not thrown: Chrome still
125
+ * starts, only without them. An unreadable Preferences file and a profile
126
+ * that a running Chrome has open are left alone, and the file is not
127
+ * rewritten when nothing changes.
128
+ *
129
+ * @param userDataDir - Chrome user data directory
130
+ * @param prefs - Preferences to apply, dotted or nested
131
+ * @param logger - Logger for failures
132
+ */
133
+ export function writeProfilePreferences(userDataDir, prefs, logger) {
134
+ const file = path.join(userDataDir, 'Default', 'Preferences');
135
+ if (profileInUse(userDataDir)) {
136
+ logger.info(chromePrefsNotAppliedMessage(file, 'the profile is in use by a running Chrome'));
137
+ return;
138
+ }
139
+ try {
140
+ const existing = readPreferencesFile(file);
141
+ const content = JSON.stringify(mergePreferences(existing, prefs));
142
+ if (content === JSON.stringify(existing))
143
+ return;
144
+ fs.mkdirSync(path.dirname(file), { recursive: true });
145
+ writeFileAtomic(file, content);
146
+ }
147
+ catch (error) {
148
+ logger.info(chromePrefsNotAppliedMessage(file, getErrorMessage(error)));
149
+ }
150
+ }
151
+ //# sourceMappingURL=profilePreferences.js.map
@@ -7,7 +7,11 @@ import type { Options as ChromeLaunchOptions } from 'chrome-launcher';
7
7
  export interface LaunchOptions extends Pick<ChromeLaunchOptions, 'logLevel' | 'connectionPollInterval' | 'maxConnectionRetries' | 'portStrictMode' | 'envVars' | 'handleSIGINT' | 'ignoreDefaultFlags' | 'chromeFlags' | 'chromePath'> {
8
8
  /** Remote debugging port (defaults to 9222 when omitted) */
9
9
  port?: number;
10
- /** Directory for Chrome profile data. Falls back to persistent ~/.bdg/chrome-profile directory */
10
+ /**
11
+ * Directory for Chrome profile data, chosen by the user: bdg's default
12
+ * preferences are not written into it. Falls back to the bdg-managed
13
+ * persistent profile in the session directory (~/.bdg/chrome-profile)
14
+ */
11
15
  userDataDir?: string | undefined;
12
16
  /** Base directory for creating user data dir (defaults to OS temp dir, injectable for testing) */
13
17
  baseDir?: string | undefined;
@@ -23,7 +27,7 @@ export interface LaunchOptions extends Pick<ChromeLaunchOptions, 'logLevel' | 'c
23
27
  prefsFile?: string | undefined;
24
28
  /** Override Chrome binary detection with an explicit path */
25
29
  chromePath?: string;
26
- /** bdg session directory, recorded as a marker flag on the Chrome command line */
30
+ /** bdg session directory: recorded as a marker flag on the Chrome command line, and holds the default profile */
27
31
  sessionDir?: string | undefined;
28
32
  }
29
33
  /**
@@ -46,4 +50,19 @@ export interface LaunchOptions extends Pick<ChromeLaunchOptions, 'logLevel' | 'c
46
50
  * Uses chrome-launcher for cross-platform Chrome detection and launching.
47
51
  */
48
52
  export declare function launchChrome(options?: LaunchOptions): Promise<LaunchedChrome>;
53
+ /**
54
+ * Preferences for the launched profile: bdg's defaults (no translate prompt,
55
+ * no password manager or leak check, whose bubbles capture input) for
56
+ * profiles bdg manages, with the caller's preferences on top. A profile the
57
+ * user chose (`--user-data-dir`) gets only preferences the caller passed.
58
+ *
59
+ * File-based preferences take precedence over inline preferences because
60
+ * files allow for complex, reusable configurations that can be version
61
+ * controlled and shared across team members or CI environments.
62
+ *
63
+ * @param options - Launch options containing prefs or prefsFile
64
+ * @returns Merged preferences (empty when there is nothing to apply)
65
+ * @throws ChromeLaunchError if prefs file cannot be read or parsed, or prefs are not JSON
66
+ */
67
+ export declare function resolveChromePrefs(options: LaunchOptions): Record<string, unknown>;
49
68
  //# sourceMappingURL=launcher.d.ts.map
@@ -6,10 +6,12 @@ import { BDG_CHROME_PREFS, DEFAULT_CDP_PORT, CHROME_PROFILE_DIR, DEFAULT_CHROME_
6
6
  import { getErrorMessage } from '../utils/errors.js';
7
7
  import { filterDefined } from '../utils/objects.js';
8
8
  import { isProcessAlive } from '../utils/process.js';
9
+ import { verifyLaunchedChrome } from './chromeIdentity.js';
9
10
  import { ChromeLaunchError } from './errors.js';
10
11
  import { resolveChromeBinary } from './launcher/binaryResolver.js';
11
12
  import { buildChromeFlags } from './launcher/flagsBuilder.js';
12
13
  import { loadChromePrefs, ensureJSONCompatiblePrefs } from './launcher/preferencesLoader.js';
14
+ import { writeProfilePreferences } from './launcher/profilePreferences.js';
13
15
  import { reservePort } from './portReservation.js';
14
16
  import { markStartupLogs, watchStartupExit } from './startupExit.js';
15
17
  /**
@@ -57,7 +59,7 @@ export async function launchChrome(options = {}) {
57
59
  }
58
60
  const reservation = await reservePort(port);
59
61
  reservation.release();
60
- const userDataDir = options.userDataDir ?? getPersistentUserDataDir(options.baseDir);
62
+ const userDataDir = options.userDataDir ?? getPersistentUserDataDir(options.baseDir ?? options.sessionDir);
61
63
  if (!fs.existsSync(userDataDir)) {
62
64
  try {
63
65
  fs.mkdirSync(userDataDir, { recursive: true });
@@ -74,9 +76,11 @@ export async function launchChrome(options = {}) {
74
76
  }
75
77
  logger.info(`Launching Chrome on port ${port}...`);
76
78
  logger.debug(`User data directory: ${userDataDir}`);
77
- const chromeOptions = buildChromeOptions(options);
79
+ applyProfilePreferences(userDataDir, options, logger);
80
+ const chromeOptions = buildChromeOptions({ ...options, port });
78
81
  const launcher = new chromeLauncher.Launcher(chromeOptions);
79
- const startup = watchStartupExit(() => launcher.chromeProcess, markStartupLogs(userDataDir), userDataDir);
82
+ const logs = markStartupLogs(userDataDir);
83
+ const startup = watchStartupExit(() => launcher.chromeProcess, logs, userDataDir);
80
84
  try {
81
85
  const launchStart = Date.now();
82
86
  logger.info('Waiting for Chrome to be ready...');
@@ -96,10 +100,11 @@ export async function launchChrome(options = {}) {
96
100
  },
97
101
  });
98
102
  }
103
+ await verifyLaunchedChrome({ logs, port, pid: chromeProcessPid });
99
104
  logger.info(`Chrome launched successfully (PID: ${chromeProcessPid}, ${launchDurationMs}ms)`);
100
105
  return {
101
106
  pid: chromeProcessPid,
102
- port: launcher.port ?? port,
107
+ port,
103
108
  userDataDir: launcher.userDataDir,
104
109
  kill: async () => {
105
110
  return Promise.resolve().then(() => {
@@ -128,7 +133,9 @@ export async function launchChrome(options = {}) {
128
133
  /**
129
134
  * Get the default persistent user-data-dir path.
130
135
  *
131
- * Uses the session directory ($BDG_SESSION_DIR) to store Chrome profile data,
136
+ * Uses the session directory (`~/.bdg`, `$BDG_SESSION_DIR`, or
137
+ * `<base>/sessions/<name>` for a named session, passed as `sessionDir`) to
138
+ * store Chrome profile data,
132
139
  * ensuring each session has its own isolated Chrome profile. This prevents
133
140
  * SingletonLock conflicts when multiple agents run concurrently.
134
141
  *
@@ -136,7 +143,7 @@ export async function launchChrome(options = {}) {
136
143
  * For shared login state across sessions, use --user-data-dir to specify a
137
144
  * shared location explicitly.
138
145
  *
139
- * @param baseDir - Optional base directory (defaults to session dir). Allows injection for testing or custom locations.
146
+ * @param baseDir - Optional base directory (defaults to `~/.bdg` or `$BDG_SESSION_DIR`). Allows injection for testing or custom locations.
140
147
  * @returns Absolute path to session-isolated user-data-dir
141
148
  * @throws Error if user data directory cannot be created due to permission issues
142
149
  */
@@ -168,32 +175,52 @@ function getPersistentUserDataDir(baseDir) {
168
175
  return userDataDir;
169
176
  }
170
177
  /**
171
- * Load Chrome preferences from options.
178
+ * Preferences for the launched profile: bdg's defaults (no translate prompt,
179
+ * no password manager or leak check, whose bubbles capture input) for
180
+ * profiles bdg manages, with the caller's preferences on top. A profile the
181
+ * user chose (`--user-data-dir`) gets only preferences the caller passed.
172
182
  *
173
183
  * File-based preferences take precedence over inline preferences because
174
184
  * files allow for complex, reusable configurations that can be version
175
185
  * controlled and shared across team members or CI environments.
176
186
  *
177
187
  * @param options - Launch options containing prefs or prefsFile
178
- * @returns Chrome preferences object or undefined if no preferences specified
179
- * @throws ChromeLaunchError if prefs file cannot be read, parsed, or doesn't exist
188
+ * @returns Merged preferences (empty when there is nothing to apply)
189
+ * @throws ChromeLaunchError if prefs file cannot be read or parsed, or prefs are not JSON
180
190
  */
191
+ export function resolveChromePrefs(options) {
192
+ const defaults = options.userDataDir === undefined ? BDG_CHROME_PREFS : {};
193
+ const prefs = { ...defaults, ...loadChromePrefs(options) };
194
+ ensureJSONCompatiblePrefs(prefs);
195
+ return prefs;
196
+ }
197
+ /**
198
+ * Write the launch preferences into the profile, if there are any.
199
+ *
200
+ * @param userDataDir - Chrome user data directory
201
+ * @param options - Launch options
202
+ * @param logger - Logger for failures
203
+ * @throws ChromeLaunchError if prefs file cannot be read or parsed, or prefs are not JSON
204
+ */
205
+ function applyProfilePreferences(userDataDir, options, logger) {
206
+ const prefs = resolveChromePrefs(options);
207
+ if (Object.keys(prefs).length > 0)
208
+ writeProfilePreferences(userDataDir, prefs, logger);
209
+ }
181
210
  /**
182
211
  * Build chrome-launcher options from bdg launch options.
183
212
  *
184
213
  * Maps LaunchOptions to chrome-launcher API format using a clean utility approach
185
- * that filters out undefined values automatically. User preferences override bdg
186
- * defaults to allow customization while maintaining sensible base configuration.
214
+ * that filters out undefined values automatically. Preferences are not passed:
215
+ * chrome-launcher would replace whole nested sections of an existing profile's
216
+ * Preferences, so `writeProfilePreferences` merges them instead.
187
217
  *
188
218
  * @param options - bdg launch options to convert
189
219
  * @returns chrome-launcher compatible options object
190
- * @throws ChromeLaunchError if preference loading fails
191
220
  */
192
221
  function buildChromeOptions(options) {
193
- const userPrefs = loadChromePrefs(options);
194
- const userDataDir = options.userDataDir ?? getPersistentUserDataDir(options.baseDir);
222
+ const userDataDir = options.userDataDir ?? getPersistentUserDataDir(options.baseDir ?? options.sessionDir);
195
223
  const chromePathOverride = resolveChromeBinary(options);
196
- const mergedPrefs = userPrefs ? { ...BDG_CHROME_PREFS, ...userPrefs } : BDG_CHROME_PREFS;
197
224
  return {
198
225
  logLevel: options.logLevel ?? DEFAULT_CHROME_LOG_LEVEL,
199
226
  handleSIGINT: options.handleSIGINT ?? DEFAULT_CHROME_HANDLE_SIGINT,
@@ -206,7 +233,6 @@ function buildChromeOptions(options) {
206
233
  connectionPollInterval: options.connectionPollInterval ?? CHROME_READY_POLL_MS,
207
234
  maxConnectionRetries: options.maxConnectionRetries ?? CHROME_READY_POLL_ATTEMPTS,
208
235
  portStrictMode: options.portStrictMode,
209
- prefs: ensureJSONCompatiblePrefs(mergedPrefs),
210
236
  envVars: options.envVars,
211
237
  chromePath: chromePathOverride,
212
238
  }),
@@ -12,14 +12,24 @@ export interface PortReservation {
12
12
  release: () => void;
13
13
  }
14
14
  /**
15
- * Whether something already accepts connections on 127.0.0.1:port. A
16
- * listener on all interfaces (`0.0.0.0`/`::`) does not stop bdg from binding
17
- * 127.0.0.1 on macOS, but Chrome would then fail to listen, so it is checked
18
- * by connecting.
15
+ * Whether something already accepts connections on host:port.
19
16
  *
20
17
  * @param port - Port to check
18
+ * @param host - Address to connect to
21
19
  * @returns True if a connection was accepted
22
20
  */
21
+ export declare function acceptsConnections(port: number, host: string): Promise<boolean>;
22
+ /**
23
+ * Whether something already accepts connections on the port, on 127.0.0.1 or
24
+ * ::1. A listener on all interfaces (`0.0.0.0`/`::`) does not stop bdg from
25
+ * binding 127.0.0.1 on macOS, but Chrome would then fail to listen; a Chrome
26
+ * on [::1] (one that found 127.0.0.1 taken) does not stop it either, but a
27
+ * second Chrome would be reached through the first one's port. Both are found
28
+ * by connecting.
29
+ *
30
+ * @param port - Port to check
31
+ * @returns True if a connection was accepted on either address
32
+ */
23
33
  export declare function isPortAnswering(port: number): Promise<boolean>;
24
34
  /**
25
35
  * Atomically reserve a port to prevent race conditions during Chrome launch.
@@ -8,18 +8,18 @@ import * as net from 'net';
8
8
  import { ChromeLaunchError } from './errors.js';
9
9
  /** How long a connection attempt may take when checking a port */
10
10
  const CONNECT_CHECK_MS = 500;
11
+ /** Loopback addresses Chrome may listen on (it falls back to IPv6 when IPv4 is taken) */
12
+ const LOOPBACK_HOSTS = ['127.0.0.1', '::1'];
11
13
  /**
12
- * Whether something already accepts connections on 127.0.0.1:port. A
13
- * listener on all interfaces (`0.0.0.0`/`::`) does not stop bdg from binding
14
- * 127.0.0.1 on macOS, but Chrome would then fail to listen, so it is checked
15
- * by connecting.
14
+ * Whether something already accepts connections on host:port.
16
15
  *
17
16
  * @param port - Port to check
17
+ * @param host - Address to connect to
18
18
  * @returns True if a connection was accepted
19
19
  */
20
- export function isPortAnswering(port) {
20
+ export function acceptsConnections(port, host) {
21
21
  return new Promise((resolve) => {
22
- const socket = net.connect({ port, host: '127.0.0.1' });
22
+ const socket = net.connect({ port, host });
23
23
  const finish = (answering) => {
24
24
  socket.destroy();
25
25
  resolve(answering);
@@ -29,6 +29,21 @@ export function isPortAnswering(port) {
29
29
  socket.once('error', () => finish(false));
30
30
  });
31
31
  }
32
+ /**
33
+ * Whether something already accepts connections on the port, on 127.0.0.1 or
34
+ * ::1. A listener on all interfaces (`0.0.0.0`/`::`) does not stop bdg from
35
+ * binding 127.0.0.1 on macOS, but Chrome would then fail to listen; a Chrome
36
+ * on [::1] (one that found 127.0.0.1 taken) does not stop it either, but a
37
+ * second Chrome would be reached through the first one's port. Both are found
38
+ * by connecting.
39
+ *
40
+ * @param port - Port to check
41
+ * @returns True if a connection was accepted on either address
42
+ */
43
+ export async function isPortAnswering(port) {
44
+ const answers = await Promise.all(LOOPBACK_HOSTS.map((host) => acceptsConnections(port, host)));
45
+ return answers.some(Boolean);
46
+ }
32
47
  /**
33
48
  * The error for a port another process uses.
34
49
  *
@@ -22,6 +22,14 @@ export interface StartupLogs {
22
22
  * @returns Log positions
23
23
  */
24
24
  export declare function markStartupLogs(userDataDir: string): StartupLogs;
25
+ /**
26
+ * Chrome's output (stderr, then stdout) since {@link markStartupLogs}. The
27
+ * marked offsets are byte sizes, so the files are cut before decoding.
28
+ *
29
+ * @param logs - Log positions
30
+ * @returns Output lines
31
+ */
32
+ export declare function readStartupLines(logs: StartupLogs): string[];
25
33
  /**
26
34
  * Reject when the launched Chrome exits before it is ready.
27
35
  *
@@ -31,18 +31,27 @@ export function markStartupLogs(userDataDir) {
31
31
  };
32
32
  }
33
33
  /**
34
- * Chrome's output since {@link markStartupLogs}, last lines first trimmed.
34
+ * Chrome's output (stderr, then stdout) since {@link markStartupLogs}. The
35
+ * marked offsets are byte sizes, so the files are cut before decoding.
35
36
  *
36
37
  * @param logs - Log positions
37
- * @returns Up to {@link OUTPUT_LINES} non-empty lines
38
+ * @returns Output lines
38
39
  */
39
- function readStartupOutput(logs) {
40
- const lines = logs.files.flatMap(({ file, offset }) => {
40
+ export function readStartupLines(logs) {
41
+ return logs.files.flatMap(({ file, offset }) => {
41
42
  if (!fs.existsSync(file))
42
43
  return [];
43
- return fs.readFileSync(file, 'utf8').slice(offset).split('\n');
44
+ return fs.readFileSync(file).subarray(offset).toString('utf8').split('\n');
44
45
  });
45
- return lines
46
+ }
47
+ /**
48
+ * Chrome's output since {@link markStartupLogs}, last lines first trimmed.
49
+ *
50
+ * @param logs - Log positions
51
+ * @returns Up to {@link OUTPUT_LINES} non-empty lines
52
+ */
53
+ function readStartupOutput(logs) {
54
+ return readStartupLines(logs)
46
55
  .map((line) => line.trim())
47
56
  .filter(Boolean)
48
57
  .slice(-OUTPUT_LINES);
@@ -46,8 +46,12 @@ export declare const BDG_CHROME_FLAGS: string[];
46
46
  export declare const DOCKER_CHROME_FLAGS: string[];
47
47
  /**
48
48
  * BDG-specific Chrome preferences for automation
49
- * These preferences are automatically merged with user preferences when launching Chrome
50
- * User preferences take precedence over these defaults
49
+ * Written into the launched profile's Default/Preferences (dotted names become nested paths)
50
+ * before every launch, so persistent profiles get them too. User preferences take precedence.
51
+ *
52
+ * The password manager and its leak check are off: after a login their bubble
53
+ * (e.g. "Change your password") captures all input in headless Chrome, so
54
+ * later clicks silently never reach the page.
51
55
  *
52
56
  * Note: Crash/restore popup suppression is handled by Chrome flags (--disable-session-crashed-bubble, --disable-infobars)
53
57
  * which are more reliable than preference-based approaches.