browser-debugger-cli 0.13.0 → 0.14.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 (112) hide show
  1. package/.claude/skills/bdg/SKILL.md +100 -186
  2. package/README.md +4 -4
  3. package/dist/commands/console.js +5 -1
  4. package/dist/commands/dom/a11y.d.ts +1 -1
  5. package/dist/commands/dom/a11y.js +20 -20
  6. package/dist/commands/dom/eval.d.ts +2 -1
  7. package/dist/commands/dom/eval.js +21 -3
  8. package/dist/commands/dom/formInteraction.js +1 -1
  9. package/dist/commands/dom/get.js +25 -7
  10. package/dist/commands/dom/index.js +7 -2
  11. package/dist/commands/dom/query.d.ts +2 -1
  12. package/dist/commands/dom/query.js +5 -3
  13. package/dist/commands/dom/screenshot.js +1 -0
  14. package/dist/commands/helpJson.js +1 -1
  15. package/dist/commands/network/list.js +46 -3
  16. package/dist/commands/optionBehaviors.d.ts +25 -2
  17. package/dist/commands/optionBehaviors.js +55 -42
  18. package/dist/commands/peek.js +3 -0
  19. package/dist/commands/shared/CommandRunner.js +13 -13
  20. package/dist/commands/shared/daemonErrorHandler.js +2 -2
  21. package/dist/commands/shared/dataFetcher.d.ts +4 -2
  22. package/dist/commands/shared/dataFetcher.js +11 -3
  23. package/dist/commands/shared/handleValidationError.js +3 -3
  24. package/dist/commands/shared/optionTypes.d.ts +14 -3
  25. package/dist/commands/shared/startHelpers.js +3 -3
  26. package/dist/connection/chromeIdentity.d.ts +8 -2
  27. package/dist/connection/chromeIdentity.js +85 -13
  28. package/dist/constants.d.ts +29 -1
  29. package/dist/constants.js +35 -1
  30. package/dist/daemon/SessionController.js +2 -0
  31. package/dist/daemon/session/Session.d.ts +2 -1
  32. package/dist/daemon/session/Session.js +10 -2
  33. package/dist/daemon/session/TelemetryStore.d.ts +7 -0
  34. package/dist/daemon/session/TelemetryStore.js +6 -0
  35. package/dist/daemon/session/commandRegistry.js +23 -5
  36. package/dist/daemon/session/matchedStylesReset.d.ts +26 -0
  37. package/dist/daemon/session/matchedStylesReset.js +46 -0
  38. package/dist/daemon/session/plugins.js +1 -0
  39. package/dist/daemon/session/triggeredRequests.d.ts +0 -5
  40. package/dist/daemon/session/triggeredRequests.js +13 -7
  41. package/dist/daemon.js +742 -460
  42. package/dist/errors/messages.d.ts +8 -0
  43. package/dist/errors/messages.js +10 -0
  44. package/dist/index.js +710 -518
  45. package/dist/ipc/protocol/commands.d.ts +4 -0
  46. package/dist/ipc/protocol/inspectTypes.d.ts +5 -2
  47. package/dist/ipc/session/types.d.ts +5 -1
  48. package/dist/program.d.ts +14 -0
  49. package/dist/program.js +53 -0
  50. package/dist/runtime/dom/elementGeometry.d.ts +23 -0
  51. package/dist/runtime/dom/elementGeometry.js +17 -15
  52. package/dist/runtime/dom/elementInfo.d.ts +6 -4
  53. package/dist/runtime/dom/elementInfo.js +7 -4
  54. package/dist/runtime/dom/frameScopedConnection.d.ts +7 -0
  55. package/dist/runtime/dom/frameScopedConnection.js +2 -2
  56. package/dist/runtime/dom/inspect.d.ts +17 -3
  57. package/dist/runtime/dom/inspect.js +40 -26
  58. package/dist/runtime/dom/inspectModel.d.ts +3 -3
  59. package/dist/runtime/dom/inspectRules.d.ts +29 -3
  60. package/dist/runtime/dom/inspectRules.js +205 -11
  61. package/dist/runtime/dom/layout.d.ts +0 -2
  62. package/dist/runtime/dom/layout.js +1 -2
  63. package/dist/runtime/dom/reactEventHelpers.d.ts +4 -1
  64. package/dist/runtime/dom/reactEventHelpers.js +9 -2
  65. package/dist/runtime/dom/targetNode.d.ts +10 -6
  66. package/dist/runtime/dom/targetNode.js +15 -8
  67. package/dist/telemetry/a11y.d.ts +15 -1
  68. package/dist/telemetry/a11y.js +83 -0
  69. package/dist/telemetry/har/builder.js +1 -1
  70. package/dist/telemetry/network.d.ts +13 -16
  71. package/dist/telemetry/network.js +30 -52
  72. package/dist/telemetry/networkRetention.d.ts +83 -0
  73. package/dist/telemetry/networkRetention.js +117 -0
  74. package/dist/types.d.ts +26 -0
  75. package/dist/ui/OutputBuilder.d.ts +10 -0
  76. package/dist/ui/OutputBuilder.js +12 -0
  77. package/dist/ui/formatters/a11y.d.ts +5 -7
  78. package/dist/ui/formatters/a11y.js +7 -61
  79. package/dist/ui/formatters/console/chronological.js +4 -4
  80. package/dist/ui/formatters/console/follow.d.ts +4 -2
  81. package/dist/ui/formatters/console/follow.js +6 -3
  82. package/dist/ui/formatters/console/json.d.ts +3 -6
  83. package/dist/ui/formatters/console/json.js +9 -13
  84. package/dist/ui/formatters/console/shared.d.ts +17 -2
  85. package/dist/ui/formatters/console/shared.js +17 -0
  86. package/dist/ui/formatters/console/summarize.d.ts +2 -2
  87. package/dist/ui/formatters/console/summarize.js +22 -7
  88. package/dist/ui/formatters/console.d.ts +1 -1
  89. package/dist/ui/formatters/console.js +1 -5
  90. package/dist/ui/formatters/details.js +1 -1
  91. package/dist/ui/formatters/dom.d.ts +13 -4
  92. package/dist/ui/formatters/dom.js +25 -7
  93. package/dist/ui/formatters/layout.js +2 -1
  94. package/dist/ui/formatters/longValues.d.ts +14 -0
  95. package/dist/ui/formatters/longValues.js +23 -0
  96. package/dist/ui/formatters/networkList.d.ts +8 -2
  97. package/dist/ui/formatters/networkList.js +11 -2
  98. package/dist/ui/formatters/preview.d.ts +4 -1
  99. package/dist/ui/formatters/preview.js +55 -13
  100. package/dist/ui/formatters/status.js +7 -0
  101. package/dist/ui/formatters/triggeredRequests.js +2 -1
  102. package/dist/ui/messages/chrome.d.ts +20 -1
  103. package/dist/ui/messages/chrome.js +29 -3
  104. package/dist/ui/messages/commands.d.ts +29 -8
  105. package/dist/ui/messages/commands.js +36 -8
  106. package/dist/ui/messages/networkMessages.d.ts +24 -0
  107. package/dist/ui/messages/networkMessages.js +45 -0
  108. package/dist/utils/http.d.ts +9 -2
  109. package/dist/utils/http.js +4 -3
  110. package/dist/utils/strings.d.ts +19 -0
  111. package/dist/utils/strings.js +16 -0
  112. package/package.json +2 -2
package/dist/daemon.js CHANGED
@@ -5013,9 +5013,12 @@ var OBJECT_EXPANSION_MAX_DEPTH = 3;
5013
5013
  var OBJECT_EXPANSION_MAX_PROPERTIES = 10;
5014
5014
  var OBJECT_EXPANSION_FAILURE_THRESHOLD = 5;
5015
5015
  var MAX_RESPONSE_SIZE = 5 * 1024 * 1024;
5016
+ var MAX_TOTAL_BODY_BYTES = 100 * 1024 * 1024;
5016
5017
  var CHROME_NETWORK_BUFFER_TOTAL = 50 * 1024 * 1024;
5017
5018
  var CHROME_NETWORK_BUFFER_PER_RESOURCE = 10 * 1024 * 1024;
5018
5019
  var CHROME_POST_DATA_LIMIT = 1 * 1024 * 1024;
5020
+ var LAYOUT_ELEMENT_LIMIT = 100;
5021
+ var MAX_TRIGGERED_REQUESTS = 50;
5019
5022
  var DEFAULT_PAGE_READINESS_TIMEOUT_MS = 2e3;
5020
5023
  var MAX_JSONL_BUFFER_SIZE = 256 * 1024 * 1024;
5021
5024
 
@@ -5533,7 +5536,13 @@ function evalPreviewWarning(replaced) {
5533
5536
 
5534
5537
  // src/daemon/session/TelemetryStore.ts
5535
5538
  var TelemetryStore = class {
5539
+ /**
5540
+ * Finished requests, oldest first: the newest are kept, so the session's
5541
+ * `networkEvictions.requestsDropped` oldest ones are gone
5542
+ */
5536
5543
  networkRequests = [];
5544
+ /** Requests dropped and response bodies evicted at the capture limits */
5545
+ networkEvictions = { requestsDropped: 0, bodiesEvicted: 0 };
5537
5546
  /** Requests still in flight, keyed by CDP requestId */
5538
5547
  pendingNetworkRequests = /* @__PURE__ */ new Map();
5539
5548
  consoleMessages = [];
@@ -6747,6 +6756,9 @@ function sessionEndedDuringCommandError() {
6747
6756
  suggestion: `Start a new session with: ${sessionCommand("bdg <url>")}`
6748
6757
  };
6749
6758
  }
6759
+ function unknownSessionCommandMessage(name) {
6760
+ return `Unknown session command: "${name}"`;
6761
+ }
6750
6762
  function navigationFailedError(url, errorText) {
6751
6763
  return {
6752
6764
  message: `Could not load ${url}: ${errorText}`,
@@ -7260,9 +7272,10 @@ async function fetchCDPTargets(port = DEFAULT_CDP_PORT, logger, options) {
7260
7272
  }
7261
7273
  async function probeDevToolsEndpoint(port, logger, options) {
7262
7274
  const url = `${options?.secure ? "https" : "http"}://${options?.host ?? HTTP_LOCALHOST}:${port}/json/version`;
7275
+ const timeoutMs = options?.timeoutMs ?? CDP_HTTP_TIMEOUT_MS;
7263
7276
  let response;
7264
7277
  try {
7265
- response = await fetch(url, { signal: AbortSignal.timeout(CDP_HTTP_TIMEOUT_MS) });
7278
+ response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
7266
7279
  } catch (error) {
7267
7280
  logger?.debug(`Chrome version request failed: ${getErrorMessage(error)} (${url})`);
7268
7281
  return { kind: "unreachable" };
@@ -8342,6 +8355,234 @@ function listProcesses() {
8342
8355
  return result.stdout.split("\n").map((line) => /^\s*(\d+)\s+(.*)$/.exec(line)).filter((match) => match !== null).map(([, pid, command]) => ({ pid: Number(pid), command: command ?? "" }));
8343
8356
  }
8344
8357
 
8358
+ // src/connection/diagnostics.ts
8359
+ var log4 = createLogger("diagnostics");
8360
+ var cachedDiagnostics = null;
8361
+ function getChromeDiagnostics() {
8362
+ if (cachedDiagnostics) {
8363
+ return cachedDiagnostics;
8364
+ }
8365
+ let defaultPath = null;
8366
+ try {
8367
+ defaultPath = getChromePath();
8368
+ } catch (error) {
8369
+ log4.debug(`Failed to get default Chrome path: ${getErrorMessage(error)}`);
8370
+ }
8371
+ let installations = [];
8372
+ try {
8373
+ installations = Launcher.getInstallations();
8374
+ } catch (error) {
8375
+ log4.debug(`Failed to get Chrome installations: ${getErrorMessage(error)}`);
8376
+ }
8377
+ cachedDiagnostics = {
8378
+ defaultPath,
8379
+ installations,
8380
+ installationCount: installations.length
8381
+ };
8382
+ return cachedDiagnostics;
8383
+ }
8384
+
8385
+ // src/ui/messages/chrome.ts
8386
+ function formatChromeNotice(notice) {
8387
+ const ctx = notice.context ?? {};
8388
+ switch (notice.code) {
8389
+ case "EXTERNAL_CHROME_CONNECTING":
8390
+ return chromeExternalConnectionMessage();
8391
+ case "EXTERNAL_CHROME_WS_URL":
8392
+ return chromeExternalWebSocketMessage(ctx["wsUrl"]);
8393
+ case "EXTERNAL_CHROME_NO_PID":
8394
+ return chromeExternalNoPidMessage();
8395
+ case "EXTERNAL_CHROME_SKIP_TERMINATION":
8396
+ return chromeExternalSkipTerminationMessage();
8397
+ }
8398
+ }
8399
+ function formatChromeIssue(issue) {
8400
+ const ctx = issue.context ?? {};
8401
+ switch (issue.code) {
8402
+ case "PORT_IN_USE":
8403
+ return portInUseError(ctx["port"], ctx["reason"]);
8404
+ case "INVALID_PORT":
8405
+ return invalidPortError(ctx["port"]);
8406
+ case "USER_DATA_DIR_CREATE_FAILED":
8407
+ return userDataDirError(ctx["userDataDir"], ctx["reason"] ?? "");
8408
+ case "CHROME_LAUNCH_FAILED":
8409
+ case "CHROME_DIED_AFTER_LAUNCH": {
8410
+ const port = ctx["port"];
8411
+ const reason = ctx["reason"];
8412
+ const pid = typeof ctx["pid"] === "number" ? ctx["pid"] : "unknown";
8413
+ const header2 = issue.code === "CHROME_DIED_AFTER_LAUNCH" ? `Chrome died immediately after launch (PID: ${pid})` : reason ? chromeLaunchFailedError(reason) : `Chrome failed to launch`;
8414
+ const diagnostics = getFormattedDiagnostics();
8415
+ return joinLines(
8416
+ header2,
8417
+ "",
8418
+ "Possible causes:",
8419
+ ` - Port ${port} conflict (check: lsof -ti:${port})`,
8420
+ ` - Chrome binary not found`,
8421
+ ` - Insufficient permissions`,
8422
+ ` - Chrome crashed on startup`,
8423
+ "",
8424
+ ...diagnostics,
8425
+ "",
8426
+ "Try:",
8427
+ ` - ${sessionCommand("bdg cleanup")}`,
8428
+ ` - See what uses the port: lsof -i :${port}`,
8429
+ ` - Use different port: ${sessionCommand(`bdg <url> --port ${port + 1}`)}`,
8430
+ ` - In a container where Chrome's sandbox fails: BDG_NO_SANDBOX=1 ${sessionCommand("bdg <url>")}`
8431
+ );
8432
+ }
8433
+ case "CHROME_EXITED_DURING_STARTUP":
8434
+ return chromeExitedDuringStartupError(
8435
+ ctx["exitCode"],
8436
+ ctx["output"] ?? [],
8437
+ ctx["userDataDir"],
8438
+ ctx["profileInUse"] === true
8439
+ );
8440
+ case "NO_PAGE_TARGET_FOUND":
8441
+ return noPageTargetFoundError(
8442
+ ctx["port"],
8443
+ ctx["availableTargets"]
8444
+ );
8445
+ case "CHROME_BINARY_NOT_FOUND": {
8446
+ const diagnostics = getFormattedDiagnostics();
8447
+ return joinLines(
8448
+ chromeBinaryOverrideNotFound(ctx["chromePath"], ctx["source"]),
8449
+ "",
8450
+ ...diagnostics
8451
+ );
8452
+ }
8453
+ case "CHROME_BINARY_IS_DIRECTORY":
8454
+ return chromeBinaryOverrideIsDirectory(ctx["chromePath"], ctx["source"]);
8455
+ case "CHROME_BINARY_NOT_EXECUTABLE": {
8456
+ const reason = ctx["reason"];
8457
+ const base = chromeBinaryOverrideNotExecutable(
8458
+ ctx["chromePath"],
8459
+ ctx["source"]
8460
+ );
8461
+ return reason ? `${base}
8462
+
8463
+ ${reason}` : base;
8464
+ }
8465
+ case "PREFS_FILE_NOT_FOUND":
8466
+ return prefsFileNotFoundError(ctx["file"]);
8467
+ case "PREFS_INVALID_FORMAT":
8468
+ return invalidPrefsFormatError(ctx["file"], ctx["actualType"]);
8469
+ case "PREFS_LOAD_FAILED":
8470
+ return prefsLoadError(ctx["file"], ctx["reason"] ?? "");
8471
+ case "PREFS_NOT_JSON_SERIALIZABLE":
8472
+ return `Chrome preferences must be JSON-serializable: ${ctx["reason"] ?? "unknown error"}`;
8473
+ }
8474
+ }
8475
+ function chromeExitedDuringStartupError(exitCode, output, userDataDir, profileInUse2) {
8476
+ if (profileInUse2) {
8477
+ return joinLines(
8478
+ `The Chrome profile ${userDataDir} is in use by another Chrome`,
8479
+ `Close that Chrome, or use another profile directory: ${sessionCommand("bdg <url> -u ./other-profile")}`
8480
+ );
8481
+ }
8482
+ return joinLines(
8483
+ `Chrome exited during startup (exit code ${exitCode ?? "none"})`,
8484
+ ...output.length > 0 ? ["Chrome said:", ...output.map((line) => ` ${line}`)] : [],
8485
+ "Check --chrome-flags and BDG_CHROME_FLAGS (an unknown flag or value can stop Chrome)"
8486
+ );
8487
+ }
8488
+ function getFormattedDiagnostics() {
8489
+ return formatDiagnosticsForError(getChromeDiagnostics());
8490
+ }
8491
+ function formatDiagnosticsForError(diagnostics) {
8492
+ const lines = [];
8493
+ if (diagnostics.installationCount === 0) {
8494
+ lines.push("Error: No Chrome installations detected\n");
8495
+ lines.push("Install Chrome from:");
8496
+ lines.push(" https://www.google.com/chrome/\n");
8497
+ } else {
8498
+ lines.push(`Found ${pluralize(diagnostics.installationCount, "Chrome installation")}:
8499
+ `);
8500
+ diagnostics.installations.forEach((path9, index) => {
8501
+ lines.push(` ${index + 1}. ${path9}`);
8502
+ });
8503
+ lines.push("");
8504
+ if (diagnostics.defaultPath) {
8505
+ lines.push(`Default binary: ${diagnostics.defaultPath}
8506
+ `);
8507
+ } else {
8508
+ lines.push("Default binary: Could not determine\n");
8509
+ }
8510
+ }
8511
+ return lines;
8512
+ }
8513
+ function invalidPortError(port) {
8514
+ return `Invalid port number: ${port}. Port must be between 1 and 65535.`;
8515
+ }
8516
+ function userDataDirError(dir, error) {
8517
+ return `Failed to create user data directory at ${dir}: ${error}`;
8518
+ }
8519
+ function chromeExternalConnectionMessage() {
8520
+ return "Connecting to existing Chrome instance...";
8521
+ }
8522
+ function chromeExternalWebSocketMessage(wsUrl) {
8523
+ return `WebSocket URL: ${wsUrl}`;
8524
+ }
8525
+ function chromeExternalNoPidMessage() {
8526
+ return "Using external Chrome (no PID - not managed by bdg)";
8527
+ }
8528
+ function chromeExternalSkipTerminationMessage() {
8529
+ return "Using external Chrome - skipping termination (not managed by bdg)";
8530
+ }
8531
+ function noPageTargetFoundError(port, availableTargets) {
8532
+ return joinLines(
8533
+ `Chrome started but opened no page to attach to (port ${port})`,
8534
+ `Retry: ${sessionCommand("bdg <url>")}. If it keeps failing, another program may use the port (lsof -i :${port}) or a crashed session may be left over: ${sessionCommand("bdg cleanup")} && ${sessionCommand("bdg <url>")}`,
8535
+ ...availableTargets ? ["", `Chrome targets:
8536
+ ${availableTargets}`] : []
8537
+ );
8538
+ }
8539
+ function prefsFileNotFoundError(file) {
8540
+ return `Chrome preferences file not found: ${file}`;
8541
+ }
8542
+ function invalidPrefsFormatError(file, type) {
8543
+ return `Invalid Chrome preferences format in ${file}: expected object, got ${type}`;
8544
+ }
8545
+ function prefsLoadError(file, error) {
8546
+ return `Failed to load Chrome preferences from ${file}: ${error}`;
8547
+ }
8548
+ function chromeLaunchFailedError(error) {
8549
+ return `Failed to launch Chrome: ${error}`;
8550
+ }
8551
+ function chromeBinaryOverrideNotFound(path9, source) {
8552
+ return `Chrome binary override (${source}) points to "${path9}", but that file does not exist.`;
8553
+ }
8554
+ function chromeBinaryOverrideNotExecutable(path9, source) {
8555
+ return `Chrome binary override (${source}) points to "${path9}", but it is not an executable file.
8556
+ Update the path to the Chrome binary (e.g. /Applications/Google Chrome.app/Contents/MacOS/Google Chrome) or unset the override to let bdg auto-detect Chrome.`;
8557
+ }
8558
+ function chromeBinaryOverrideIsDirectory(path9, source) {
8559
+ return `Chrome binary override (${source}) points to "${path9}", which is a directory, not an executable file.`;
8560
+ }
8561
+ function portInUseError(port, reason) {
8562
+ return joinLines(
8563
+ reason ? `Port ${port} is already in use: ${reason}.
8564
+ ` : `Port ${port} is already in use.
8565
+ `,
8566
+ "Another program (or a Chrome left from a previous session) is listening on it.\n",
8567
+ "Try:",
8568
+ ` - Use a different port: ${sessionCommand(`bdg <url> --port ${port + 1}`)}`,
8569
+ ` - If a bdg session holds it: find it with bdg sessions, then end that one: bdg stop --session <name> (bdg cleanup --force --session <name> if it is stuck)`,
8570
+ ` - See what uses the port: lsof -i :${port}`
8571
+ );
8572
+ }
8573
+ function portTakenByReason(answeredBy, chromeHost) {
8574
+ if (answeredBy === "nothing")
8575
+ return `something holds 127.0.0.1 (Chrome fell back to ${chromeHost})`;
8576
+ const other = answeredBy === "browser" ? "another browser" : "another process";
8577
+ return `${other} answers on 127.0.0.1 (Chrome listens on ${chromeHost})`;
8578
+ }
8579
+ function chromeNotAnsweringReason(port, waitedMs) {
8580
+ return `Chrome announced port ${port} but did not answer on 127.0.0.1 within ${(waitedMs / 1e3).toFixed(1)}s (slow start)`;
8581
+ }
8582
+ function chromePrefsNotAppliedMessage(file, reason) {
8583
+ return `Warning: Chrome preferences not applied to ${file} (${reason}); password manager bubbles may capture clicks`;
8584
+ }
8585
+
8345
8586
  // src/connection/portReservation.ts
8346
8587
  import * as net2 from "net";
8347
8588
  var CONNECT_CHECK_MS = 500;
@@ -8362,18 +8603,18 @@ async function isPortAnswering(port) {
8362
8603
  const answers = await Promise.all(LOOPBACK_HOSTS.map((host) => acceptsConnections(port, host)));
8363
8604
  return answers.some(Boolean);
8364
8605
  }
8365
- function portInUseError(port) {
8606
+ function portInUseError2(port) {
8366
8607
  return new ChromeLaunchError(`Port ${port} is already in use`, {
8367
8608
  issue: { code: "PORT_IN_USE", context: { port } }
8368
8609
  });
8369
8610
  }
8370
8611
  async function reservePort(port) {
8371
- if (await isPortAnswering(port)) throw portInUseError(port);
8612
+ if (await isPortAnswering(port)) throw portInUseError2(port);
8372
8613
  return new Promise((resolve6, reject2) => {
8373
8614
  const server2 = net2.createServer();
8374
8615
  server2.once("error", (err) => {
8375
8616
  if (err.code === "EADDRINUSE") {
8376
- reject2(portInUseError(port));
8617
+ reject2(portInUseError2(port));
8377
8618
  } else {
8378
8619
  reject2(err);
8379
8620
  }
@@ -8453,7 +8694,8 @@ function startupExitError(exitCode, output, userDataDir) {
8453
8694
  var LISTENING_PATTERN = /^DevTools listening on (ws:\/\/\S+)/;
8454
8695
  var ENDPOINT_WAIT_MS = 1e4;
8455
8696
  var ENDPOINT_POLL_MS = 50;
8456
- var log4 = createLogger("chrome");
8697
+ var MIN_ANSWER_WAIT_MS = 1e3;
8698
+ var log5 = createLogger("chrome");
8457
8699
  function parseDevToolsListening(lines) {
8458
8700
  for (const line of [...lines].reverse()) {
8459
8701
  const match = LISTENING_PATTERN.exec(line.trim());
@@ -8464,7 +8706,7 @@ function parseDevToolsListening(lines) {
8464
8706
  if (!port || !url.pathname.startsWith("/devtools/browser/")) continue;
8465
8707
  return { host: url.hostname, port, browserPath: url.pathname };
8466
8708
  } catch (error) {
8467
- log4.debug(`Ignoring malformed DevTools announcement: ${String(error)}`);
8709
+ log5.debug(`Ignoring malformed DevTools announcement: ${String(error)}`);
8468
8710
  }
8469
8711
  }
8470
8712
  return null;
@@ -8478,31 +8720,45 @@ async function waitForDevToolsEndpoint(logs, isRunning, timeoutMs = ENDPOINT_WAI
8478
8720
  }
8479
8721
  }
8480
8722
  async function verifyLaunchedChrome(options) {
8481
- const { logs, port, pid, timeoutMs } = options;
8723
+ const { logs, port, pid, timeoutMs = ENDPOINT_WAIT_MS } = options;
8724
+ const started = Date.now();
8482
8725
  const isRunning = () => isProcessAlive(pid);
8483
8726
  const endpoint = await waitForDevToolsEndpoint(logs, isRunning, timeoutMs);
8484
- if (!endpoint && !isRunning()) {
8485
- throw new ChromeLaunchError(`Chrome died immediately after launch (PID: ${pid})`, {
8486
- issue: { code: "CHROME_DIED_AFTER_LAUNCH", context: { port, pid } }
8487
- });
8488
- }
8727
+ if (!endpoint && !isRunning()) throw diedError(port, pid);
8489
8728
  if (!endpoint) return acceptUnannouncedChrome(logs, port);
8490
8729
  if (endpoint.port !== port) {
8491
8730
  throw portTakenError(port, `Chrome listens on port ${endpoint.port} instead`);
8492
8731
  }
8493
- const answering = await fetchBrowserWsUrl(port, log4);
8494
- if (!answering || new URL(answering).pathname !== endpoint.browserPath) {
8495
- throw portTakenError(
8496
- port,
8497
- `another process answers on 127.0.0.1 (Chrome listens on ${endpoint.host})`
8498
- );
8732
+ const onLoopback = endpoint.host === "127.0.0.1";
8733
+ const deadline = onLoopback ? started + timeoutMs : Date.now();
8734
+ const answer = await waitForAnswer(port, deadline, isRunning);
8735
+ if (answer.kind === "devtools" && new URL(answer.wsUrl).pathname === endpoint.browserPath) {
8736
+ log5.debug(`Chrome on port ${port} is the launched one (${endpoint.browserPath})`);
8737
+ return;
8738
+ }
8739
+ if (answer.kind === "unreachable" && !isRunning()) throw diedError(port, pid);
8740
+ if (answer.kind === "unreachable" && onLoopback) {
8741
+ throw slowStartError(port, Date.now() - started);
8742
+ }
8743
+ throw portTakenError(port, portTakenByReason(answerSource(answer), endpoint.host));
8744
+ }
8745
+ async function waitForAnswer(port, deadline, isRunning) {
8746
+ for (; ; ) {
8747
+ const remaining = Math.max(deadline - Date.now(), MIN_ANSWER_WAIT_MS);
8748
+ const timeoutMs = Math.min(CDP_HTTP_TIMEOUT_MS, remaining);
8749
+ const answer = await probeDevToolsEndpoint(port, void 0, { timeoutMs });
8750
+ if (answer.kind !== "unreachable" || !isRunning() || Date.now() >= deadline) return answer;
8751
+ await delay(ENDPOINT_POLL_MS);
8499
8752
  }
8500
- log4.debug(`Chrome on port ${port} is the launched one (${endpoint.browserPath})`);
8753
+ }
8754
+ function answerSource(answer) {
8755
+ if (answer.kind === "devtools") return "browser";
8756
+ return answer.kind === "not-devtools" ? "process" : "nothing";
8501
8757
  }
8502
8758
  async function acceptUnannouncedChrome(logs, port) {
8503
8759
  const errLog = logs.files[0]?.file ?? "chrome-err.log";
8504
8760
  const missing = `Chrome did not print "DevTools listening on" to ${errLog}`;
8505
- if (!await fetchBrowserWsUrl(port, log4)) {
8761
+ if (!await fetchBrowserWsUrl(port, log5)) {
8506
8762
  throw new ChromeLaunchError(`${missing}, and no browser answers on port ${port}`, {
8507
8763
  issue: {
8508
8764
  code: "CHROME_LAUNCH_FAILED",
@@ -8513,10 +8769,21 @@ async function acceptUnannouncedChrome(logs, port) {
8513
8769
  if (await acceptsConnections(port, "::1")) {
8514
8770
  throw portTakenError(port, `${missing}, and both 127.0.0.1 and [::1] answer`);
8515
8771
  }
8516
- log4.info(
8772
+ log5.info(
8517
8773
  `Warning: ${missing}; using the browser on 127.0.0.1:${port}, which was free before the launch`
8518
8774
  );
8519
8775
  }
8776
+ function diedError(port, pid) {
8777
+ return new ChromeLaunchError(`Chrome died immediately after launch (PID: ${pid})`, {
8778
+ issue: { code: "CHROME_DIED_AFTER_LAUNCH", context: { port, pid } }
8779
+ });
8780
+ }
8781
+ function slowStartError(port, waitedMs) {
8782
+ const reason = chromeNotAnsweringReason(port, waitedMs);
8783
+ return new ChromeLaunchError(reason, {
8784
+ issue: { code: "CHROME_LAUNCH_FAILED", context: { port, reason } }
8785
+ });
8786
+ }
8520
8787
  function portTakenError(port, reason) {
8521
8788
  return new ChromeLaunchError(`Port ${port} is already in use: ${reason}`, {
8522
8789
  issue: { code: "PORT_IN_USE", context: { port, reason } }
@@ -8550,367 +8817,147 @@ function resolveChromeBinary(options) {
8550
8817
  const stats = fs7.statSync(chromePath);
8551
8818
  if (stats.isDirectory()) {
8552
8819
  throw new ChromeLaunchError(
8553
- `Chrome binary override (${sourceLabel2}) points to "${chromePath}", which is a directory.`,
8554
- {
8555
- issue: {
8556
- code: "CHROME_BINARY_IS_DIRECTORY",
8557
- context: { chromePath, source: sourceLabel2 }
8558
- }
8559
- }
8560
- );
8561
- }
8562
- fs7.accessSync(chromePath, fs7.constants.X_OK);
8563
- } catch (error) {
8564
- if (error instanceof ChromeLaunchError) {
8565
- throw error;
8566
- }
8567
- throw new ChromeLaunchError(
8568
- `Chrome binary override (${sourceLabel2}) points to "${chromePath}", but it is not executable.`,
8569
- {
8570
- ...error instanceof Error && { cause: error },
8571
- issue: {
8572
- code: "CHROME_BINARY_NOT_EXECUTABLE",
8573
- context: { chromePath, source: sourceLabel2, reason: getErrorMessage(error) }
8574
- }
8575
- }
8576
- );
8577
- }
8578
- return chromePath;
8579
- }
8580
-
8581
- // src/connection/launcher/flagsBuilder.ts
8582
- import * as fs8 from "fs";
8583
- function chromeSessionMarkerFlag(sessionDir) {
8584
- return `--bdg-session-dir=${sessionDir}`;
8585
- }
8586
- var NO_SANDBOX_ENV = "BDG_NO_SANDBOX";
8587
- function needsNoSandbox() {
8588
- if (process.env[NO_SANDBOX_ENV] === "1") return true;
8589
- if (process.platform === "linux" && process.getuid?.() === 0) return true;
8590
- return fs8.existsSync("/.dockerenv");
8591
- }
8592
- function isDocker() {
8593
- try {
8594
- if (fs8.existsSync("/.dockerenv") || fs8.existsSync("/run/.containerenv")) {
8595
- return true;
8596
- }
8597
- if (fs8.existsSync("/proc/self/cgroup")) {
8598
- const cgroup = fs8.readFileSync("/proc/self/cgroup", "utf8");
8599
- return cgroup.includes("docker") || cgroup.includes("containerd");
8600
- }
8601
- return false;
8602
- } catch {
8603
- return false;
8604
- }
8605
- }
8606
- function defaultFlags() {
8607
- const flags = Launcher.defaultFlags();
8608
- return process.platform === "linux" ? [...flags, "--disable-setuid-sandbox"] : flags;
8609
- }
8610
- function buildChromeFlags(options) {
8611
- const baseFlags = options.ignoreDefaultFlags ? [] : defaultFlags();
8612
- const bdgFlags = [
8613
- ...options.sessionDir ? [chromeSessionMarkerFlag(options.sessionDir)] : [],
8614
- ...BDG_CHROME_FLAGS
8615
- ];
8616
- const dockerFlags = isDocker() ? DOCKER_CHROME_FLAGS : [];
8617
- const sandboxFlags = needsNoSandbox() ? ["--no-sandbox"] : [];
8618
- const customFlags = options.chromeFlags ?? [];
8619
- if (options.headless) {
8620
- return [
8621
- HEADLESS_FLAG,
8622
- ...baseFlags,
8623
- ...bdgFlags,
8624
- ...dockerFlags,
8625
- ...sandboxFlags,
8626
- ...customFlags
8627
- ];
8628
- }
8629
- return [...baseFlags, ...bdgFlags, ...dockerFlags, ...sandboxFlags, ...customFlags];
8630
- }
8631
-
8632
- // src/connection/launcher/preferencesLoader.ts
8633
- import * as fs9 from "fs";
8634
- function loadChromePrefs(options) {
8635
- if (options.prefsFile) {
8636
- const file = options.prefsFile;
8637
- if (!fs9.existsSync(file)) {
8638
- throw new ChromeLaunchError(`Chrome preferences file not found: ${file}`, {
8639
- issue: { code: "PREFS_FILE_NOT_FOUND", context: { file } }
8640
- });
8641
- }
8642
- try {
8643
- const prefsContent = fs9.readFileSync(file, "utf8");
8644
- const parsed = JSON.parse(prefsContent);
8645
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
8646
- throw new ChromeLaunchError(
8647
- `Invalid Chrome preferences format in ${file}: expected object, got ${typeof parsed}`,
8648
- {
8649
- issue: {
8650
- code: "PREFS_INVALID_FORMAT",
8651
- context: { file, actualType: Array.isArray(parsed) ? "array" : typeof parsed }
8652
- }
8653
- }
8654
- );
8655
- }
8656
- return parsed;
8657
- } catch (error) {
8658
- if (error instanceof ChromeLaunchError) {
8659
- throw error;
8660
- }
8661
- throw new ChromeLaunchError(`Failed to load Chrome preferences from ${file}`, {
8662
- ...error instanceof Error && { cause: error },
8663
- issue: {
8664
- code: "PREFS_LOAD_FAILED",
8665
- context: { file, reason: getErrorMessage(error) }
8666
- }
8667
- });
8668
- }
8669
- }
8670
- return options.prefs;
8671
- }
8672
- function ensureJSONCompatiblePrefs(prefs) {
8673
- if (prefs === void 0) {
8674
- return void 0;
8675
- }
8676
- try {
8677
- JSON.stringify(prefs);
8678
- } catch (error) {
8679
- throw new ChromeLaunchError("Chrome preferences must be JSON-serializable", {
8680
- ...error instanceof Error && { cause: error },
8681
- issue: {
8682
- code: "PREFS_NOT_JSON_SERIALIZABLE",
8683
- context: { reason: getErrorMessage(error) }
8684
- }
8685
- });
8686
- }
8687
- return prefs;
8688
- }
8689
-
8690
- // src/connection/launcher/profilePreferences.ts
8691
- import * as fs10 from "fs";
8692
- import * as os3 from "os";
8693
- import * as path5 from "path";
8694
-
8695
- // src/connection/diagnostics.ts
8696
- var log5 = createLogger("diagnostics");
8697
- var cachedDiagnostics = null;
8698
- function getChromeDiagnostics() {
8699
- if (cachedDiagnostics) {
8700
- return cachedDiagnostics;
8701
- }
8702
- let defaultPath = null;
8703
- try {
8704
- defaultPath = getChromePath();
8705
- } catch (error) {
8706
- log5.debug(`Failed to get default Chrome path: ${getErrorMessage(error)}`);
8707
- }
8708
- let installations = [];
8709
- try {
8710
- installations = Launcher.getInstallations();
8711
- } catch (error) {
8712
- log5.debug(`Failed to get Chrome installations: ${getErrorMessage(error)}`);
8713
- }
8714
- cachedDiagnostics = {
8715
- defaultPath,
8716
- installations,
8717
- installationCount: installations.length
8718
- };
8719
- return cachedDiagnostics;
8720
- }
8721
-
8722
- // src/ui/messages/chrome.ts
8723
- function formatChromeNotice(notice) {
8724
- const ctx = notice.context ?? {};
8725
- switch (notice.code) {
8726
- case "EXTERNAL_CHROME_CONNECTING":
8727
- return chromeExternalConnectionMessage();
8728
- case "EXTERNAL_CHROME_WS_URL":
8729
- return chromeExternalWebSocketMessage(ctx["wsUrl"]);
8730
- case "EXTERNAL_CHROME_NO_PID":
8731
- return chromeExternalNoPidMessage();
8732
- case "EXTERNAL_CHROME_SKIP_TERMINATION":
8733
- return chromeExternalSkipTerminationMessage();
8734
- }
8735
- }
8736
- function formatChromeIssue(issue) {
8737
- const ctx = issue.context ?? {};
8738
- switch (issue.code) {
8739
- case "PORT_IN_USE":
8740
- return portInUseError2(ctx["port"]);
8741
- case "INVALID_PORT":
8742
- return invalidPortError(ctx["port"]);
8743
- case "USER_DATA_DIR_CREATE_FAILED":
8744
- return userDataDirError(ctx["userDataDir"], ctx["reason"] ?? "");
8745
- case "CHROME_LAUNCH_FAILED":
8746
- case "CHROME_DIED_AFTER_LAUNCH": {
8747
- const port = ctx["port"];
8748
- const reason = ctx["reason"];
8749
- const pid = typeof ctx["pid"] === "number" ? ctx["pid"] : "unknown";
8750
- const header2 = issue.code === "CHROME_DIED_AFTER_LAUNCH" ? `Chrome died immediately after launch (PID: ${pid})` : reason ? chromeLaunchFailedError(reason) : `Chrome failed to launch`;
8751
- const diagnostics = getFormattedDiagnostics();
8752
- return joinLines(
8753
- header2,
8754
- "",
8755
- "Possible causes:",
8756
- ` - Port ${port} conflict (check: lsof -ti:${port})`,
8757
- ` - Chrome binary not found`,
8758
- ` - Insufficient permissions`,
8759
- ` - Chrome crashed on startup`,
8760
- "",
8761
- ...diagnostics,
8762
- "",
8763
- "Try:",
8764
- ` - ${sessionCommand("bdg cleanup")}`,
8765
- ` - See what uses the port: lsof -i :${port}`,
8766
- ` - Use different port: ${sessionCommand(`bdg <url> --port ${port + 1}`)}`,
8767
- ` - In a container where Chrome's sandbox fails: BDG_NO_SANDBOX=1 ${sessionCommand("bdg <url>")}`
8768
- );
8769
- }
8770
- case "CHROME_EXITED_DURING_STARTUP":
8771
- return chromeExitedDuringStartupError(
8772
- ctx["exitCode"],
8773
- ctx["output"] ?? [],
8774
- ctx["userDataDir"],
8775
- ctx["profileInUse"] === true
8776
- );
8777
- case "NO_PAGE_TARGET_FOUND":
8778
- return noPageTargetFoundError(
8779
- ctx["port"],
8780
- ctx["availableTargets"]
8781
- );
8782
- case "CHROME_BINARY_NOT_FOUND": {
8783
- const diagnostics = getFormattedDiagnostics();
8784
- return joinLines(
8785
- chromeBinaryOverrideNotFound(ctx["chromePath"], ctx["source"]),
8786
- "",
8787
- ...diagnostics
8788
- );
8789
- }
8790
- case "CHROME_BINARY_IS_DIRECTORY":
8791
- return chromeBinaryOverrideIsDirectory(ctx["chromePath"], ctx["source"]);
8792
- case "CHROME_BINARY_NOT_EXECUTABLE": {
8793
- const reason = ctx["reason"];
8794
- const base = chromeBinaryOverrideNotExecutable(
8795
- ctx["chromePath"],
8796
- ctx["source"]
8797
- );
8798
- return reason ? `${base}
8799
-
8800
- ${reason}` : base;
8801
- }
8802
- case "PREFS_FILE_NOT_FOUND":
8803
- return prefsFileNotFoundError(ctx["file"]);
8804
- case "PREFS_INVALID_FORMAT":
8805
- return invalidPrefsFormatError(ctx["file"], ctx["actualType"]);
8806
- case "PREFS_LOAD_FAILED":
8807
- return prefsLoadError(ctx["file"], ctx["reason"] ?? "");
8808
- case "PREFS_NOT_JSON_SERIALIZABLE":
8809
- return `Chrome preferences must be JSON-serializable: ${ctx["reason"] ?? "unknown error"}`;
8810
- }
8811
- }
8812
- function chromeExitedDuringStartupError(exitCode, output, userDataDir, profileInUse2) {
8813
- if (profileInUse2) {
8814
- return joinLines(
8815
- `The Chrome profile ${userDataDir} is in use by another Chrome`,
8816
- `Close that Chrome, or use another profile directory: ${sessionCommand("bdg <url> -u ./other-profile")}`
8817
- );
8818
- }
8819
- return joinLines(
8820
- `Chrome exited during startup (exit code ${exitCode ?? "none"})`,
8821
- ...output.length > 0 ? ["Chrome said:", ...output.map((line) => ` ${line}`)] : [],
8822
- "Check --chrome-flags and BDG_CHROME_FLAGS (an unknown flag or value can stop Chrome)"
8823
- );
8824
- }
8825
- function getFormattedDiagnostics() {
8826
- return formatDiagnosticsForError(getChromeDiagnostics());
8827
- }
8828
- function formatDiagnosticsForError(diagnostics) {
8829
- const lines = [];
8830
- if (diagnostics.installationCount === 0) {
8831
- lines.push("Error: No Chrome installations detected\n");
8832
- lines.push("Install Chrome from:");
8833
- lines.push(" https://www.google.com/chrome/\n");
8834
- } else {
8835
- lines.push(`Found ${pluralize(diagnostics.installationCount, "Chrome installation")}:
8836
- `);
8837
- diagnostics.installations.forEach((path9, index) => {
8838
- lines.push(` ${index + 1}. ${path9}`);
8839
- });
8840
- lines.push("");
8841
- if (diagnostics.defaultPath) {
8842
- lines.push(`Default binary: ${diagnostics.defaultPath}
8843
- `);
8844
- } else {
8845
- lines.push("Default binary: Could not determine\n");
8846
- }
8847
- }
8848
- return lines;
8849
- }
8850
- function invalidPortError(port) {
8851
- return `Invalid port number: ${port}. Port must be between 1 and 65535.`;
8852
- }
8853
- function userDataDirError(dir, error) {
8854
- return `Failed to create user data directory at ${dir}: ${error}`;
8855
- }
8856
- function chromeExternalConnectionMessage() {
8857
- return "Connecting to existing Chrome instance...";
8858
- }
8859
- function chromeExternalWebSocketMessage(wsUrl) {
8860
- return `WebSocket URL: ${wsUrl}`;
8861
- }
8862
- function chromeExternalNoPidMessage() {
8863
- return "Using external Chrome (no PID - not managed by bdg)";
8864
- }
8865
- function chromeExternalSkipTerminationMessage() {
8866
- return "Using external Chrome - skipping termination (not managed by bdg)";
8867
- }
8868
- function noPageTargetFoundError(port, availableTargets) {
8869
- return joinLines(
8870
- `Chrome started but opened no page to attach to (port ${port})`,
8871
- `Retry: ${sessionCommand("bdg <url>")}. If it keeps failing, another program may use the port (lsof -i :${port}) or a crashed session may be left over: ${sessionCommand("bdg cleanup")} && ${sessionCommand("bdg <url>")}`,
8872
- ...availableTargets ? ["", `Chrome targets:
8873
- ${availableTargets}`] : []
8874
- );
8875
- }
8876
- function prefsFileNotFoundError(file) {
8877
- return `Chrome preferences file not found: ${file}`;
8878
- }
8879
- function invalidPrefsFormatError(file, type) {
8880
- return `Invalid Chrome preferences format in ${file}: expected object, got ${type}`;
8820
+ `Chrome binary override (${sourceLabel2}) points to "${chromePath}", which is a directory.`,
8821
+ {
8822
+ issue: {
8823
+ code: "CHROME_BINARY_IS_DIRECTORY",
8824
+ context: { chromePath, source: sourceLabel2 }
8825
+ }
8826
+ }
8827
+ );
8828
+ }
8829
+ fs7.accessSync(chromePath, fs7.constants.X_OK);
8830
+ } catch (error) {
8831
+ if (error instanceof ChromeLaunchError) {
8832
+ throw error;
8833
+ }
8834
+ throw new ChromeLaunchError(
8835
+ `Chrome binary override (${sourceLabel2}) points to "${chromePath}", but it is not executable.`,
8836
+ {
8837
+ ...error instanceof Error && { cause: error },
8838
+ issue: {
8839
+ code: "CHROME_BINARY_NOT_EXECUTABLE",
8840
+ context: { chromePath, source: sourceLabel2, reason: getErrorMessage(error) }
8841
+ }
8842
+ }
8843
+ );
8844
+ }
8845
+ return chromePath;
8881
8846
  }
8882
- function prefsLoadError(file, error) {
8883
- return `Failed to load Chrome preferences from ${file}: ${error}`;
8847
+
8848
+ // src/connection/launcher/flagsBuilder.ts
8849
+ import * as fs8 from "fs";
8850
+ function chromeSessionMarkerFlag(sessionDir) {
8851
+ return `--bdg-session-dir=${sessionDir}`;
8884
8852
  }
8885
- function chromeLaunchFailedError(error) {
8886
- return `Failed to launch Chrome: ${error}`;
8853
+ var NO_SANDBOX_ENV = "BDG_NO_SANDBOX";
8854
+ function needsNoSandbox() {
8855
+ if (process.env[NO_SANDBOX_ENV] === "1") return true;
8856
+ if (process.platform === "linux" && process.getuid?.() === 0) return true;
8857
+ return fs8.existsSync("/.dockerenv");
8887
8858
  }
8888
- function chromeBinaryOverrideNotFound(path9, source) {
8889
- return `Chrome binary override (${source}) points to "${path9}", but that file does not exist.`;
8859
+ function isDocker() {
8860
+ try {
8861
+ if (fs8.existsSync("/.dockerenv") || fs8.existsSync("/run/.containerenv")) {
8862
+ return true;
8863
+ }
8864
+ if (fs8.existsSync("/proc/self/cgroup")) {
8865
+ const cgroup = fs8.readFileSync("/proc/self/cgroup", "utf8");
8866
+ return cgroup.includes("docker") || cgroup.includes("containerd");
8867
+ }
8868
+ return false;
8869
+ } catch {
8870
+ return false;
8871
+ }
8890
8872
  }
8891
- function chromeBinaryOverrideNotExecutable(path9, source) {
8892
- return `Chrome binary override (${source}) points to "${path9}", but it is not an executable file.
8893
- Update the path to the Chrome binary (e.g. /Applications/Google Chrome.app/Contents/MacOS/Google Chrome) or unset the override to let bdg auto-detect Chrome.`;
8873
+ function defaultFlags() {
8874
+ const flags = Launcher.defaultFlags();
8875
+ return process.platform === "linux" ? [...flags, "--disable-setuid-sandbox"] : flags;
8894
8876
  }
8895
- function chromeBinaryOverrideIsDirectory(path9, source) {
8896
- return `Chrome binary override (${source}) points to "${path9}", which is a directory, not an executable file.`;
8877
+ function buildChromeFlags(options) {
8878
+ const baseFlags = options.ignoreDefaultFlags ? [] : defaultFlags();
8879
+ const bdgFlags = [
8880
+ ...options.sessionDir ? [chromeSessionMarkerFlag(options.sessionDir)] : [],
8881
+ ...BDG_CHROME_FLAGS
8882
+ ];
8883
+ const dockerFlags = isDocker() ? DOCKER_CHROME_FLAGS : [];
8884
+ const sandboxFlags = needsNoSandbox() ? ["--no-sandbox"] : [];
8885
+ const customFlags = options.chromeFlags ?? [];
8886
+ if (options.headless) {
8887
+ return [
8888
+ HEADLESS_FLAG,
8889
+ ...baseFlags,
8890
+ ...bdgFlags,
8891
+ ...dockerFlags,
8892
+ ...sandboxFlags,
8893
+ ...customFlags
8894
+ ];
8895
+ }
8896
+ return [...baseFlags, ...bdgFlags, ...dockerFlags, ...sandboxFlags, ...customFlags];
8897
8897
  }
8898
- function portInUseError2(port) {
8899
- return joinLines(
8900
- `Port ${port} is already in use.
8901
- `,
8902
- "Another program (or a Chrome left from a previous session) is listening on it.\n",
8903
- "Try:",
8904
- ` - Use a different port: ${sessionCommand(`bdg <url> --port ${port + 1}`)}`,
8905
- ` - If a bdg session holds it: find it with bdg sessions, then end that one: bdg stop --session <name> (bdg cleanup --force --session <name> if it is stuck)`,
8906
- ` - See what uses the port: lsof -i :${port}`
8907
- );
8898
+
8899
+ // src/connection/launcher/preferencesLoader.ts
8900
+ import * as fs9 from "fs";
8901
+ function loadChromePrefs(options) {
8902
+ if (options.prefsFile) {
8903
+ const file = options.prefsFile;
8904
+ if (!fs9.existsSync(file)) {
8905
+ throw new ChromeLaunchError(`Chrome preferences file not found: ${file}`, {
8906
+ issue: { code: "PREFS_FILE_NOT_FOUND", context: { file } }
8907
+ });
8908
+ }
8909
+ try {
8910
+ const prefsContent = fs9.readFileSync(file, "utf8");
8911
+ const parsed = JSON.parse(prefsContent);
8912
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
8913
+ throw new ChromeLaunchError(
8914
+ `Invalid Chrome preferences format in ${file}: expected object, got ${typeof parsed}`,
8915
+ {
8916
+ issue: {
8917
+ code: "PREFS_INVALID_FORMAT",
8918
+ context: { file, actualType: Array.isArray(parsed) ? "array" : typeof parsed }
8919
+ }
8920
+ }
8921
+ );
8922
+ }
8923
+ return parsed;
8924
+ } catch (error) {
8925
+ if (error instanceof ChromeLaunchError) {
8926
+ throw error;
8927
+ }
8928
+ throw new ChromeLaunchError(`Failed to load Chrome preferences from ${file}`, {
8929
+ ...error instanceof Error && { cause: error },
8930
+ issue: {
8931
+ code: "PREFS_LOAD_FAILED",
8932
+ context: { file, reason: getErrorMessage(error) }
8933
+ }
8934
+ });
8935
+ }
8936
+ }
8937
+ return options.prefs;
8908
8938
  }
8909
- function chromePrefsNotAppliedMessage(file, reason) {
8910
- return `Warning: Chrome preferences not applied to ${file} (${reason}); password manager bubbles may capture clicks`;
8939
+ function ensureJSONCompatiblePrefs(prefs) {
8940
+ if (prefs === void 0) {
8941
+ return void 0;
8942
+ }
8943
+ try {
8944
+ JSON.stringify(prefs);
8945
+ } catch (error) {
8946
+ throw new ChromeLaunchError("Chrome preferences must be JSON-serializable", {
8947
+ ...error instanceof Error && { cause: error },
8948
+ issue: {
8949
+ code: "PREFS_NOT_JSON_SERIALIZABLE",
8950
+ context: { reason: getErrorMessage(error) }
8951
+ }
8952
+ });
8953
+ }
8954
+ return prefs;
8911
8955
  }
8912
8956
 
8913
8957
  // src/connection/launcher/profilePreferences.ts
8958
+ import * as fs10 from "fs";
8959
+ import * as os3 from "os";
8960
+ import * as path5 from "path";
8914
8961
  function isPrefsObject(value) {
8915
8962
  return typeof value === "object" && value !== null && !Array.isArray(value);
8916
8963
  }
@@ -10941,6 +10988,101 @@ var ExtraInfoTracker = class {
10941
10988
  }
10942
10989
  };
10943
10990
 
10991
+ // src/ui/messages/networkMessages.ts
10992
+ function megabytes(bytes) {
10993
+ return `${Math.round(bytes / (1024 * 1024))} MB`;
10994
+ }
10995
+ function bodyEvictedReason(budgetBytes) {
10996
+ return `evicted: total body budget (bdg keeps the newest ${megabytes(budgetBytes)} of response bodies)`;
10997
+ }
10998
+
10999
+ // src/telemetry/networkRetention.ts
11000
+ var RequestRetention = class {
11001
+ /**
11002
+ * @param requests - Finished requests, oldest first (updated in place)
11003
+ * @param limits - Request cap and body budget
11004
+ * @param evictions - Counters updated on each drop and eviction
11005
+ */
11006
+ constructor(requests, limits, evictions) {
11007
+ this.requests = requests;
11008
+ this.limits = limits;
11009
+ this.evictions = evictions;
11010
+ }
11011
+ requests;
11012
+ limits;
11013
+ evictions;
11014
+ bodySizes = /* @__PURE__ */ new Map();
11015
+ storedBodyBytes = 0;
11016
+ /**
11017
+ * Add a finished request, dropping the oldest one past the cap.
11018
+ *
11019
+ * @param request - Finished request
11020
+ * @returns The dropped request, if one was
11021
+ */
11022
+ add(request) {
11023
+ this.requests.push(request);
11024
+ if (this.requests.length <= this.limits.maxRequests) return void 0;
11025
+ const dropped = this.requests.shift();
11026
+ if (!dropped) return void 0;
11027
+ this.evictions.requestsDropped++;
11028
+ this.forgetBody(dropped);
11029
+ return dropped;
11030
+ }
11031
+ /**
11032
+ * Store a fetched response body on a kept request, then evict the oldest
11033
+ * bodies until the total fits the budget (a body larger than the whole
11034
+ * budget is evicted itself). Storing a body again replaces the earlier one.
11035
+ *
11036
+ * @param request - Request the body belongs to
11037
+ * @param body - Body as Chrome returned it
11038
+ * @param base64Encoded - Whether `body` is base64
11039
+ */
11040
+ storeBody(request, body, base64Encoded) {
11041
+ this.forgetBody(request);
11042
+ request.responseBody = body;
11043
+ if (base64Encoded) request.responseBodyBase64 = true;
11044
+ if (body) {
11045
+ request.decodedBodyLength = Buffer.byteLength(body, base64Encoded ? "base64" : "utf-8");
11046
+ }
11047
+ const size = Buffer.byteLength(body);
11048
+ this.bodySizes.set(request, size);
11049
+ this.storedBodyBytes += size;
11050
+ this.enforceBodyBudget();
11051
+ }
11052
+ /** Evict the oldest stored bodies while their total is over the budget. */
11053
+ enforceBodyBudget() {
11054
+ while (this.storedBodyBytes > this.limits.maxTotalBodyBytes) {
11055
+ const oldest = this.bodySizes.keys().next();
11056
+ if (oldest.done) return;
11057
+ const request = oldest.value;
11058
+ this.forgetBody(request);
11059
+ request.responseBody = skippedBodyPlaceholder(
11060
+ bodyEvictedReason(this.limits.maxTotalBodyBytes)
11061
+ );
11062
+ delete request.responseBodyBase64;
11063
+ this.evictions.bodiesEvicted++;
11064
+ }
11065
+ }
11066
+ /**
11067
+ * Stop counting a request's stored body.
11068
+ *
11069
+ * @param request - Request whose body is let go
11070
+ */
11071
+ forgetBody(request) {
11072
+ const size = this.bodySizes.get(request);
11073
+ if (size === void 0) return;
11074
+ this.bodySizes.delete(request);
11075
+ this.storedBodyBytes -= size;
11076
+ }
11077
+ };
11078
+ var SKIPPED_BODY_PATTERN = /^\[SKIPPED: (.*)\]$/s;
11079
+ function skippedBodyPlaceholder(reason) {
11080
+ return `[SKIPPED: ${reason}]`;
11081
+ }
11082
+ function skippedBodyReason(body) {
11083
+ return body === void 0 ? void 0 : SKIPPED_BODY_PATTERN.exec(body)?.[1];
11084
+ }
11085
+
10944
11086
  // src/telemetry/network.ts
10945
11087
  var log16 = createLogger("network");
10946
11088
  function shouldFilterRequest(url, includeAll, networkInclude, networkExclude) {
@@ -10952,19 +11094,12 @@ function shouldFilterRequest(url, includeAll, networkInclude, networkExclude) {
10952
11094
  }
10953
11095
  return false;
10954
11096
  }
10955
- function fetchResponseBody(cdp, requestId, request, pendingFetches, sessionId) {
11097
+ function fetchResponseBody(cdp, requestId, request, pendingFetches, retention, sessionId) {
10956
11098
  pendingFetches.add(requestId);
10957
11099
  void cdp.send("Network.getResponseBody", { requestId }, sessionId).then((response) => {
10958
11100
  if (!pendingFetches.has(requestId)) return;
10959
- const typedResponse = response;
10960
- request.responseBody = typedResponse.body;
10961
- if (typedResponse.base64Encoded) request.responseBodyBase64 = true;
10962
- if (typedResponse.body) {
10963
- request.decodedBodyLength = Buffer.byteLength(
10964
- typedResponse.body,
10965
- typedResponse.base64Encoded ? "base64" : "utf-8"
10966
- );
10967
- }
11101
+ const { body, base64Encoded } = response;
11102
+ retention.storeBody(request, body, base64Encoded);
10968
11103
  }).catch((error) => {
10969
11104
  log16.debug(
10970
11105
  `Failed to fetch response body for request ${requestId}: ${getErrorMessage(error)}`
@@ -10993,13 +11128,6 @@ function createNetworkRequest(params, getCurrentNavigationId) {
10993
11128
  ...params.type !== void 0 && { resourceType: params.type }
10994
11129
  };
10995
11130
  }
10996
- var SKIPPED_BODY_PATTERN = /^\[SKIPPED: (.*)\]$/s;
10997
- function skippedBodyPlaceholder(reason) {
10998
- return `[SKIPPED: ${reason}]`;
10999
- }
11000
- function skippedBodyReason(body) {
11001
- return body === void 0 ? void 0 : SKIPPED_BODY_PATTERN.exec(body)?.[1];
11002
- }
11003
11131
  function applyResponse(request, response, resourceType) {
11004
11132
  const { status, mimeType, headers, timing, remoteIPAddress, remotePort, connectionId } = response;
11005
11133
  request.status = status;
@@ -11090,6 +11218,9 @@ async function startNetworkCollection(cdp, requests, options = {}) {
11090
11218
  networkInclude = [],
11091
11219
  networkExclude = [],
11092
11220
  maxBodySize = MAX_RESPONSE_SIZE,
11221
+ maxRequests = MAX_NETWORK_REQUESTS,
11222
+ maxTotalBodyBytes = MAX_TOTAL_BODY_BYTES,
11223
+ evictions = { requestsDropped: 0, bodiesEvicted: 0 },
11093
11224
  getCurrentNavigationId
11094
11225
  } = options;
11095
11226
  const requestMap = options.pendingRequests ?? /* @__PURE__ */ new Map();
@@ -11098,6 +11229,11 @@ async function startNetworkCollection(cdp, requests, options = {}) {
11098
11229
  const extraInfo = new ExtraInfoTracker((requestId) => requestMap.get(requestId)?.request);
11099
11230
  const registry = new CDPHandlerRegistry();
11100
11231
  const typed = new TypedCDPConnection(cdp);
11232
+ const retention = new RequestRetention(requests, { maxRequests, maxTotalBodyBytes }, evictions);
11233
+ const record = (request) => {
11234
+ const dropped = retention.add(request);
11235
+ if (dropped) pendingFetches.delete(dropped.requestId);
11236
+ };
11101
11237
  let bodiesFetched = 0;
11102
11238
  let bodiesSkipped = 0;
11103
11239
  try {
@@ -11114,11 +11250,9 @@ async function startNetworkCollection(cdp, requests, options = {}) {
11114
11250
  const failRequest = (requestId, failure) => {
11115
11251
  const entry = requestMap.get(requestId);
11116
11252
  if (!entry) return;
11117
- if (requests.length < MAX_NETWORK_REQUESTS) {
11118
- applyFailure(entry.request, failure);
11119
- requests.push(entry.request);
11120
- extraInfo.complete(requestId, entry.request);
11121
- }
11253
+ applyFailure(entry.request, failure);
11254
+ record(entry.request);
11255
+ extraInfo.complete(requestId, entry.request);
11122
11256
  requestMap.delete(requestId);
11123
11257
  redirectHops.delete(requestId);
11124
11258
  };
@@ -11153,17 +11287,11 @@ async function startNetworkCollection(cdp, requests, options = {}) {
11153
11287
  const hop = completeRedirectHop(previous.request, params, redirectHops);
11154
11288
  extraInfo.applyResponse(params.requestId, hop);
11155
11289
  extraInfo.recordRedirectHop(params.requestId, hop);
11156
- if (requests.length < MAX_NETWORK_REQUESTS) requests.push(hop);
11290
+ record(hop);
11157
11291
  }
11158
11292
  if (shouldFilterRequest(params.request.url, includeAll, networkInclude, networkExclude)) {
11159
11293
  return;
11160
11294
  }
11161
- if (requestMap.size >= MAX_NETWORK_REQUESTS) {
11162
- log16.debug(
11163
- `Warning: Network request limit reached (${MAX_NETWORK_REQUESTS}), dropping new requests`
11164
- );
11165
- return;
11166
- }
11167
11295
  const request = createNetworkRequest(params, getCurrentNavigationId);
11168
11296
  extraInfo.applyRequest(params.requestId, request);
11169
11297
  if (params.type === "Document" && params.loaderId) {
@@ -11200,12 +11328,6 @@ async function startNetworkCollection(cdp, requests, options = {}) {
11200
11328
  registry.registerTyped(typed, "Network.loadingFinished", (params) => {
11201
11329
  const entry = requestMap.get(params.requestId);
11202
11330
  if (!entry) return;
11203
- if (requests.length >= MAX_NETWORK_REQUESTS) {
11204
- log16.debug(`Warning: Network request limit reached (${MAX_NETWORK_REQUESTS})`);
11205
- requestMap.delete(params.requestId);
11206
- redirectHops.delete(params.requestId);
11207
- return;
11208
- }
11209
11331
  const request = entry.request;
11210
11332
  if (params.encodedDataLength !== void 0) {
11211
11333
  request.encodedDataLength = params.encodedDataLength;
@@ -11225,12 +11347,12 @@ async function startNetworkCollection(cdp, requests, options = {}) {
11225
11347
  );
11226
11348
  if (decision.should) {
11227
11349
  bodiesFetched++;
11228
- fetchResponseBody(cdp, params.requestId, request, pendingFetches, entry.sessionId);
11350
+ fetchResponseBody(cdp, params.requestId, request, pendingFetches, retention, entry.sessionId);
11229
11351
  } else {
11230
11352
  bodiesSkipped++;
11231
11353
  request.responseBody = skippedBodyPlaceholder(decision.reason ?? "not captured");
11232
11354
  }
11233
- requests.push(request);
11355
+ record(request);
11234
11356
  extraInfo.complete(params.requestId, request);
11235
11357
  requestMap.delete(params.requestId);
11236
11358
  redirectHops.delete(params.requestId);
@@ -11482,6 +11604,7 @@ function createDefaultTelemetryPlugins() {
11482
11604
  fetchAllBodies: config.includeAll ?? false,
11483
11605
  getCurrentNavigationId: store.getCurrentNavigationId ?? void 0,
11484
11606
  pendingRequests: store.pendingNetworkRequests,
11607
+ evictions: store.networkEvictions,
11485
11608
  ...filterDefined({
11486
11609
  maxBodySize: config.maxBodySize
11487
11610
  })
@@ -11668,7 +11791,7 @@ var COMPOSED_JS = `(node) => {
11668
11791
  while (walker.nextNode()) if (composes(walker.currentNode)) return true;
11669
11792
  return false;
11670
11793
  }`;
11671
- var FLAT_TEXT_JS = `(el, limit) => {
11794
+ var FLAT_TEXT_JS = `(el, limit, fields) => {
11672
11795
  const composed = ${COMPOSED_JS};
11673
11796
  const view = el.ownerDocument.defaultView;
11674
11797
  let text = '';
@@ -11678,7 +11801,8 @@ var FLAT_TEXT_JS = `(el, limit) => {
11678
11801
  if (text.length >= limit) return;
11679
11802
  if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');
11680
11803
  if (node.nodeType !== 1) return;
11681
- if (/^(input|textarea|select)$/.test(node.localName) || node.isContentEditable) return;
11804
+ if (/^(input|textarea)$/.test(node.localName)) return;
11805
+ if (!fields && (node.localName === 'select' || node.isContentEditable)) return;
11682
11806
  const style = view.getComputedStyle(node);
11683
11807
  if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;
11684
11808
  if (node.localName === 'br') text += '\\n';
@@ -13090,14 +13214,15 @@ var BIND_MATCH_FUNCTION = `function (selector, i) {
13090
13214
  }`;
13091
13215
  var log22 = createLogger("dom");
13092
13216
  var FILTER_MATCHING_JS = `(shadowRoots) => {
13217
+ const composed = ${COMPOSED_JS};
13218
+ const flatText = ${FLAT_TEXT_JS};
13093
13219
  const skipped = /^(script|style|noscript|template)$/;
13094
13220
  const hiddenText = (el) => {
13095
- const walker = el.ownerDocument.createTreeWalker(el, 5, {
13096
- acceptNode: (node) => (node.nodeType === 1 && skipped.test(node.localName) ? 2 : 1)
13097
- });
13221
+ const nodes = el.localName === 'slot' ? el.assignedNodes({ flatten: true }) : (el.shadowRoot || el).childNodes;
13098
13222
  let text = '';
13099
- for (let node = walker.nextNode(); node; node = walker.nextNode()) {
13223
+ for (const node of nodes) {
13100
13224
  if (node.nodeType === 3) text += node.data;
13225
+ else if (node.nodeType === 1 && !skipped.test(node.localName)) text += hiddenText(node);
13101
13226
  }
13102
13227
  return text;
13103
13228
  };
@@ -13105,7 +13230,8 @@ var FILTER_MATCHING_JS = `(shadowRoots) => {
13105
13230
  if (el.localName === 'input' && /^(submit|button|reset)$/i.test(el.type)) return el.value;
13106
13231
  const rendered = typeof el.innerText === 'string' &&
13107
13232
  (typeof el.checkVisibility !== 'function' || el.checkVisibility({ visibilityProperty: true }));
13108
- return (rendered ? el.innerText : hiddenText(el)).replace(/\\s+/g, ' ').trim();
13233
+ const text = !rendered ? hiddenText(el) : composed(el) ? flatText(el, Infinity, true) : el.innerText;
13234
+ return text.replace(/\\s+/g, ' ').trim();
13109
13235
  };
13110
13236
  const passes = (el, filter) => {
13111
13237
  if (filter.kind === 'visible') {
@@ -13455,15 +13581,15 @@ var BODY_ABORTED = "net::ERR_ABORTED";
13455
13581
 
13456
13582
  // src/daemon/session/triggeredRequests.ts
13457
13583
  var LOCAL_URL_PATTERN = /^(data|blob):/i;
13458
- var MAX_TRIGGERED_REQUESTS = 50;
13459
13584
  function watchTriggeredRequests(store) {
13460
13585
  if (!store.activeTelemetry.includes("network")) return () => void 0;
13461
13586
  const startedAt = Date.now();
13462
- const firstFinished = store.networkRequests.length;
13587
+ const firstFinished = requestsFinished(store);
13463
13588
  const firstWebSocket = store.websocketConnections.length;
13464
13589
  return () => {
13590
+ const firstKept = Math.max(0, firstFinished - store.networkEvictions.requestsDropped);
13465
13591
  const started = [
13466
- ...store.networkRequests.slice(firstFinished).map((request) => ({ request })),
13592
+ ...store.networkRequests.slice(firstKept).map((request) => ({ request })),
13467
13593
  ...[...store.pendingNetworkRequests.values()].map((pending) => ({
13468
13594
  request: pending.request,
13469
13595
  inFlight: true
@@ -13479,6 +13605,9 @@ function watchTriggeredRequests(store) {
13479
13605
  };
13480
13606
  };
13481
13607
  }
13608
+ function requestsFinished(store) {
13609
+ return store.networkEvictions.requestsDropped + store.networkRequests.length;
13610
+ }
13482
13611
  function keepNotableFirst(requests, limit) {
13483
13612
  if (requests.length <= limit) return requests;
13484
13613
  const notable = requests.filter(isNotableRequest).slice(0, limit);
@@ -16041,12 +16170,113 @@ function trackStyleSheets(cdp) {
16041
16170
  function styleSheetHeaders(cdp) {
16042
16171
  return headersByConnection.get(cdp)?.values() ?? [];
16043
16172
  }
16044
- async function matchedStyles(cdp, nodeId, budgetMs) {
16045
- const request = cdp.send("CSS.getMatchedStylesForNode", { nodeId }).then((response) => response).catch((error) => {
16046
- log24.debug(`CSS.getMatchedStylesForNode failed: ${String(error)}`);
16047
- return "failed";
16173
+ var HINTS_BUDGET_MS = 1e3;
16174
+ var RULES_BUDGET_MS = 5e3;
16175
+ var KEEP_ANSWERS_SLOWER_THAN_MS = 300;
16176
+ var KEPT_ANSWER_TTL_MS = 5e3;
16177
+ var MAX_KEPT_ANSWERS = 4;
16178
+ var STYLESHEET_EVENTS = ["CSS.styleSheetAdded", "CSS.styleSheetChanged", "CSS.styleSheetRemoved"];
16179
+ var STYLE_CHANGE_EVENTS = [
16180
+ "CSS.mediaQueryResultChanged",
16181
+ "DOM.attributeModified",
16182
+ "DOM.attributeRemoved",
16183
+ "DOM.inlineStyleInvalidated",
16184
+ "DOM.childNodeInserted",
16185
+ "DOM.childNodeRemoved",
16186
+ "DOM.childNodeCountUpdated",
16187
+ "DOM.pseudoElementAdded",
16188
+ "DOM.pseudoElementRemoved"
16189
+ ];
16190
+ var documentStylesByConnection = /* @__PURE__ */ new WeakMap();
16191
+ function documentStyles(cdp) {
16192
+ const existing = documentStylesByConnection.get(cdp);
16193
+ if (existing) return existing;
16194
+ const state = { requests: /* @__PURE__ */ new Map(), slow: false, document: 0 };
16195
+ documentStylesByConnection.set(cdp, state);
16196
+ cdp.on("DOM.documentUpdated", (_params, sessionId) => {
16197
+ if (sessionId) return;
16198
+ state.document++;
16199
+ state.requests.clear();
16200
+ state.running = void 0;
16201
+ state.slow = false;
16202
+ });
16203
+ for (const event of STYLESHEET_EVENTS) {
16204
+ cdp.on(event, (_params, sessionId) => {
16205
+ if (sessionId) return;
16206
+ state.requests.clear();
16207
+ state.slow = false;
16208
+ });
16209
+ }
16210
+ for (const event of STYLE_CHANGE_EVENTS) {
16211
+ cdp.on(event, (_params, sessionId) => {
16212
+ if (!sessionId) state.requests.clear();
16213
+ });
16214
+ }
16215
+ return state;
16216
+ }
16217
+ function resetMatchedStyles(cdp) {
16218
+ documentStylesByConnection.get(cdp)?.requests.clear();
16219
+ }
16220
+ function settleRequest(state, nodeId, request, tookMs) {
16221
+ if (state.running === request.promise) state.running = void 0;
16222
+ if (request.document !== state.document) return;
16223
+ const answered = request.answer !== "failed";
16224
+ if (answered && tookMs <= HINTS_BUDGET_MS) state.slow = false;
16225
+ if (answered && tookMs > KEEP_ANSWERS_SLOWER_THAN_MS) {
16226
+ request.expiresAt = Date.now() + KEPT_ANSWER_TTL_MS;
16227
+ } else if (state.requests.get(nodeId) === request) {
16228
+ state.requests.delete(nodeId);
16229
+ }
16230
+ }
16231
+ function sendMatchedRequest(cdp, state, nodeId) {
16232
+ const sentAt = Date.now();
16233
+ const request = {
16234
+ document: state.document,
16235
+ promise: cdp.send("CSS.getMatchedStylesForNode", { nodeId }).then((response) => response).catch((error) => {
16236
+ log24.debug(`CSS.getMatchedStylesForNode failed: ${String(error)}`);
16237
+ return "failed";
16238
+ })
16239
+ };
16240
+ state.running = request.promise;
16241
+ void request.promise.then((answer) => {
16242
+ request.answer = answer;
16243
+ settleRequest(state, nodeId, request, Date.now() - sentAt);
16048
16244
  });
16049
- return await raceTimeout(request, budgetMs) ?? "timeout";
16245
+ state.requests.set(nodeId, request);
16246
+ for (const oldest of state.requests.keys()) {
16247
+ if (state.requests.size <= MAX_KEPT_ANSWERS) break;
16248
+ state.requests.delete(oldest);
16249
+ }
16250
+ return request;
16251
+ }
16252
+ function keptRequest(state, nodeId) {
16253
+ const kept = state.requests.get(nodeId);
16254
+ if (!kept) return void 0;
16255
+ state.requests.delete(nodeId);
16256
+ if (kept.expiresAt !== void 0 && kept.expiresAt <= Date.now()) return void 0;
16257
+ state.requests.set(nodeId, kept);
16258
+ return kept;
16259
+ }
16260
+ async function requestWhenFree(cdp, state, nodeId, deadline) {
16261
+ while (state.running) {
16262
+ const kept = state.requests.get(nodeId);
16263
+ if (kept) return kept;
16264
+ if (await raceTimeout(state.running, deadline - Date.now()) === void 0) return void 0;
16265
+ }
16266
+ return state.requests.get(nodeId) ?? sendMatchedRequest(cdp, state, nodeId);
16267
+ }
16268
+ async function matchedStyles(cdp, nodeId, budgetMs, options = {}) {
16269
+ const state = documentStyles(cdp);
16270
+ const kept = keptRequest(state, nodeId);
16271
+ if (kept?.answer) return kept.answer;
16272
+ if (options.skipWhenSlow && state.slow) return "skipped";
16273
+ const deadline = Date.now() + budgetMs;
16274
+ const request = kept ?? await requestWhenFree(cdp, state, nodeId, deadline);
16275
+ if (!request) return "timeout";
16276
+ const answer = await raceTimeout(request.promise, deadline - Date.now());
16277
+ if (answer) return answer;
16278
+ if (request.document === state.document) state.slow = true;
16279
+ return "timeout";
16050
16280
  }
16051
16281
  function fileName(url) {
16052
16282
  const path9 = url.split(/[?#]/)[0] ?? url;
@@ -16635,7 +16865,7 @@ var HOLDS_FIXED_JS = `(style) =>
16635
16865
  /transform|filter|perspective/.test(style.willChange) || /paint|layout|strict|content/.test(style.contain)`;
16636
16866
  var FIXED_TO_VIEWPORT_JS = `(n) => {
16637
16867
  const holdsFixed = ${HOLDS_FIXED_JS};
16638
- const parentOf = (node) => node.parentElement || (node.parentNode && node.parentNode.host) || null;
16868
+ const parentOf = ${FLAT_PARENT_JS};
16639
16869
  for (let p = parentOf(n); p && p !== n.ownerDocument.documentElement; p = parentOf(p)) {
16640
16870
  if (holdsFixed(p.ownerDocument.defaultView.getComputedStyle(p))) return false;
16641
16871
  }
@@ -16648,7 +16878,7 @@ var ANCESTOR_CLIP_JS = `(node, rect, describe) => {
16648
16878
  const fixedToViewport = ${FIXED_TO_VIEWPORT_JS};
16649
16879
  const reasons = ${JSON.stringify(LAYOUT_REASONS)};
16650
16880
  const styleOf = (n) => n.ownerDocument.defaultView.getComputedStyle(n);
16651
- const parentOf = (n) => n.parentElement || (n.parentNode && n.parentNode.host) || null;
16881
+ const parentOf = ${FLAT_PARENT_JS};
16652
16882
  const doc = node.ownerDocument;
16653
16883
  const rootStyle = styleOf(doc.documentElement);
16654
16884
  const bodyClips = rootStyle.overflowX !== 'visible' || rootStyle.overflowY !== 'visible';
@@ -16995,7 +17225,6 @@ function placeInOwnerFrame(raw, owner, mapping) {
16995
17225
 
16996
17226
  // src/runtime/dom/layout.ts
16997
17227
  var log26 = createLogger("dom");
16998
- var LAYOUT_ELEMENT_LIMIT = 100;
16999
17228
  var CLEAR_OF_SCROLLBAR_JS = `(start, end, edge, overlay) =>
17000
17229
  overlay && end > edge - 16 && edge - 16 > start ? edge - 16 : end`;
17001
17230
  var VIEWPORT_OVERLAY_JS = `(view, viewport) => {
@@ -17235,8 +17464,6 @@ function elementLayout(raw, page) {
17235
17464
  // src/runtime/dom/inspect.ts
17236
17465
  var log27 = createLogger("dom");
17237
17466
  var stylesEnabled = /* @__PURE__ */ new WeakSet();
17238
- var HINTS_BUDGET_MS = 1e3;
17239
- var RULES_BUDGET_MS = 5e3;
17240
17467
  var groupCounter2 = 0;
17241
17468
  var PICK_MATCH_JS = `function (i) {
17242
17469
  if (i !== null) return i;
@@ -17412,14 +17639,16 @@ async function callOn(cdp, objectId, functionDeclaration, args) {
17412
17639
  }
17413
17640
  async function readSources(cdp, objectId, params, objectGroup) {
17414
17641
  const related = await relatedNodes(cdp, objectId, objectGroup);
17415
- const [raw, measured, styles] = await Promise.all([
17642
+ const [raw, measured, { nodeId, ...styles }] = await Promise.all([
17416
17643
  readPage2(cdp, objectId, params),
17417
17644
  measure(cdp, params.selector, related.node),
17418
- readStyles(cdp, related, params)
17645
+ readStyles(cdp, related)
17419
17646
  ]);
17647
+ const matched = await readMatched(cdp, nodeId, params);
17420
17648
  return {
17421
17649
  raw,
17422
17650
  ...styles,
17651
+ ...matched && { matched },
17423
17652
  fonts: raw.textHolder ? styles.fonts.textHolder : styles.fonts.node,
17424
17653
  ...measured,
17425
17654
  ...params.rules && { rules: true },
@@ -17500,24 +17729,25 @@ async function nodeIdLookup(cdp, related) {
17500
17729
  );
17501
17730
  return (backendNodeId) => backendNodeId === void 0 ? void 0 : ids.get(backendNodeId);
17502
17731
  }
17503
- async function readStyles(cdp, related, params) {
17732
+ async function readStyles(cdp, related) {
17504
17733
  await enableStyleDomains(cdp);
17505
17734
  const nodeIdOf = await nodeIdLookup(cdp, related);
17506
17735
  const optionalStyle = (backendNodeId) => backendNodeId === void 0 ? Promise.resolve(void 0) : computedStyle(cdp, nodeIdOf(backendNodeId));
17507
- const [matched, style, parentStyle, holderStyle, nodeFonts, holderFonts, size, pseudo] = await Promise.all([
17508
- readMatched(cdp, nodeIdOf(related.node), params),
17509
- computedStyle(cdp, nodeIdOf(related.node)),
17510
- optionalStyle(related.parent),
17511
- optionalStyle(related.textHolder),
17512
- platformFonts(cdp, nodeIdOf(related.node)),
17513
- platformFonts(cdp, nodeIdOf(related.fontHolder)),
17514
- borderBoxSize(cdp, related.node),
17515
- Promise.all(
17516
- related.pseudo.map(
17517
- (p) => pseudoSource(cdp, p.type, p.backendNodeId, nodeIdOf(p.backendNodeId))
17736
+ const [style, parentStyle, holderStyle, nodeFonts, holderFonts, size, pseudo] = await Promise.all(
17737
+ [
17738
+ computedStyle(cdp, nodeIdOf(related.node)),
17739
+ optionalStyle(related.parent),
17740
+ optionalStyle(related.textHolder),
17741
+ platformFonts(cdp, nodeIdOf(related.node)),
17742
+ platformFonts(cdp, nodeIdOf(related.fontHolder)),
17743
+ borderBoxSize(cdp, related.node),
17744
+ Promise.all(
17745
+ related.pseudo.map(
17746
+ (p) => pseudoSource(cdp, p.type, p.backendNodeId, nodeIdOf(p.backendNodeId))
17747
+ )
17518
17748
  )
17519
- )
17520
- ]);
17749
+ ]
17750
+ );
17521
17751
  return {
17522
17752
  style,
17523
17753
  ...parentStyle && { parentStyle },
@@ -17525,16 +17755,20 @@ async function readStyles(cdp, related, params) {
17525
17755
  pseudo,
17526
17756
  fonts: { node: nodeFonts, textHolder: holderFonts },
17527
17757
  ...size && { size },
17528
- ...matched && { matched }
17758
+ nodeId: nodeIdOf(related.node)
17529
17759
  };
17530
17760
  }
17531
- async function readMatched(cdp, nodeId, params) {
17532
- const explicit = params.rules === true || params.why !== void 0;
17533
- const skipped = !explicit && (params.hints === false || params.props !== void 0 || params.all === true);
17534
- if (nodeId === void 0 || skipped) {
17535
- return void 0;
17761
+ function matchedStylesRead(params) {
17762
+ if (params.rules === true || params.why !== void 0) {
17763
+ return { budgetMs: RULES_BUDGET_MS, skipWhenSlow: false };
17536
17764
  }
17537
- return matchedStyles(cdp, nodeId, explicit ? RULES_BUDGET_MS : HINTS_BUDGET_MS);
17765
+ if (params.hints === false || params.props !== void 0 || params.all === true) return void 0;
17766
+ return { budgetMs: HINTS_BUDGET_MS, skipWhenSlow: true };
17767
+ }
17768
+ async function readMatched(cdp, nodeId, params) {
17769
+ const read = matchedStylesRead(params);
17770
+ if (nodeId === void 0 || !read) return void 0;
17771
+ return matchedStyles(cdp, nodeId, read.budgetMs, { skipWhenSlow: read.skipWhenSlow });
17538
17772
  }
17539
17773
  async function pseudoSource(cdp, type, backendNodeId, nodeId) {
17540
17774
  const [style, size] = await Promise.all([
@@ -20381,7 +20615,10 @@ var CLICK_ELEMENT_SCRIPT = `
20381
20615
  else if (el.closest('[inert]')) obstruction = 'inert (the page made it non-interactive)';
20382
20616
  else if (style.pointerEvents === 'none') obstruction = 'not clickable (pointer-events: none)';
20383
20617
  else if (!hasSize) obstruction = 'zero-size';
20384
- else if (!hittable) obstruction = 'covered by another element' + coveredBy();
20618
+ else if (!hittable) {
20619
+ const collapsed = (${ANCESTOR_CLIP_JS})(el, rect, describe).collapsed;
20620
+ obstruction = collapsed ? 'hidden (' + collapsed + ')' : 'covered by another element' + coveredBy();
20621
+ }
20385
20622
 
20386
20623
  if (action === 'hover') (${REVEAL_SNAPSHOT_JS})(el);
20387
20624
 
@@ -22277,6 +22514,13 @@ function webSocketAsRequest2(connection) {
22277
22514
  webSocket: { frames: connection.frames, ...closedTime !== void 0 && { closedTime } }
22278
22515
  };
22279
22516
  }
22517
+ function networkEvictionActivity(store) {
22518
+ const { requestsDropped, bodiesEvicted } = store.networkEvictions;
22519
+ return {
22520
+ ...requestsDropped > 0 && { networkRequestsDropped: requestsDropped },
22521
+ ...bodiesEvicted > 0 && { networkBodiesEvicted: bodiesEvicted }
22522
+ };
22523
+ }
22280
22524
  function allNetworkRequests(store) {
22281
22525
  return [
22282
22526
  ...store.networkRequests,
@@ -22403,6 +22647,7 @@ function createCommandRegistry(store, emulation) {
22403
22647
  const totalNetwork = allNetwork.length;
22404
22648
  const totalConsole = store.consoleMessages.length;
22405
22649
  const dropped = store.consoleDropped;
22650
+ const { requestsDropped, bodiesEvicted } = store.networkEvictions;
22406
22651
  const networkBounds = calculateSliceBounds(totalNetwork, lastN, offset);
22407
22652
  const consoleBounds = calculateSliceBounds(totalConsole, lastN, offset);
22408
22653
  const recentNetwork = params.only === "console" ? [] : allNetwork.slice(networkBounds.start, networkBounds.end).map((req) => mapNetworkRequestToPreview(req, params.withHeaders));
@@ -22423,6 +22668,8 @@ function createCommandRegistry(store, emulation) {
22423
22668
  totalNetwork,
22424
22669
  totalConsole,
22425
22670
  ...dropped > 0 && { droppedConsole: dropped },
22671
+ ...requestsDropped > 0 && { droppedNetwork: requestsDropped },
22672
+ ...bodiesEvicted > 0 && { evictedNetworkBodies: bodiesEvicted },
22426
22673
  hasMoreNetwork: networkBounds.start > 0,
22427
22674
  hasMoreConsole: consoleBounds.start > 0
22428
22675
  });
@@ -22457,12 +22704,13 @@ function createCommandRegistry(store, emulation) {
22457
22704
  ...store.pageCrashedAt === void 0 ? await pageAppearance(cdp) : { crashedAt: store.pageCrashedAt }
22458
22705
  },
22459
22706
  activeTelemetry: store.activeTelemetry,
22460
- activity: filterDefined({
22707
+ activity: {
22461
22708
  networkRequestsCaptured: store.networkRequests.length,
22462
22709
  consoleMessagesCaptured: store.consoleMessages.length,
22463
- lastNetworkRequestAt: lastNetworkRequest?.timestamp,
22464
- lastConsoleMessageAt: lastConsoleMessage?.timestamp
22465
- }),
22710
+ ...lastNetworkRequest && { lastNetworkRequestAt: lastNetworkRequest.timestamp },
22711
+ ...lastConsoleMessage && { lastConsoleMessageAt: lastConsoleMessage.timestamp },
22712
+ ...networkEvictionActivity(store)
22713
+ },
22466
22714
  navigationId: store.getCurrentNavigationId?.() ?? 0
22467
22715
  };
22468
22716
  return result;
@@ -22685,6 +22933,30 @@ function createCommandRegistry(store, emulation) {
22685
22933
  };
22686
22934
  }
22687
22935
 
22936
+ // src/daemon/session/matchedStylesReset.ts
22937
+ var KEEPS_MATCHED_STYLES = /* @__PURE__ */ new Set([
22938
+ "session_peek",
22939
+ "session_details",
22940
+ "session_status",
22941
+ "session_har_data",
22942
+ "session_network_headers",
22943
+ "dom_frames",
22944
+ "dom_form_discover",
22945
+ "dom_listeners",
22946
+ "dom_layout",
22947
+ "dom_audit",
22948
+ "css_search",
22949
+ "dom_inspect"
22950
+ ]);
22951
+ function withMatchedStylesReset(cdp, name, run) {
22952
+ if (KEEPS_MATCHED_STYLES.has(name)) return run();
22953
+ const reset = () => resetMatchedStyles(cdp);
22954
+ reset();
22955
+ const result = run();
22956
+ void result.then(reset, reset);
22957
+ return result;
22958
+ }
22959
+
22688
22960
  // src/daemon/session/teardown.ts
22689
22961
  var CHROME_EXIT_POLL_MS = 50;
22690
22962
  var CHROME_EXIT_POLL_ATTEMPTS = 100;
@@ -23163,7 +23435,8 @@ var Session = class _Session {
23163
23435
  * Execute a registered command against this session. After a renderer
23164
23436
  * crash only {@link RUN_ON_CRASHED_PAGE} commands run (`bdg cdp` only for
23165
23437
  * {@link CRASH_SAFE_CDP} methods); others fail with exit 107 instead of
23166
- * waiting for a page that cannot answer.
23438
+ * waiting for a page that cannot answer. Commands that may change the page
23439
+ * drop `dom inspect`'s kept matched rules ({@link withMatchedStylesReset}).
23167
23440
  *
23168
23441
  * @param name - Command name
23169
23442
  * @param params - Command parameters
@@ -23179,7 +23452,14 @@ var Session = class _Session {
23179
23452
  if (crashedAt !== void 0 && needsPage) {
23180
23453
  return Promise.reject(pageCrashedCommandError(crashedAt));
23181
23454
  }
23182
- return this.registry[name](this.cdp, params);
23455
+ if (!Object.hasOwn(this.registry, name)) {
23456
+ return Promise.reject(
23457
+ new CommandError(unknownSessionCommandMessage(name), {}, EXIT_CODES.INVALID_ARGUMENTS)
23458
+ );
23459
+ }
23460
+ const handler = this.registry[name];
23461
+ const cdp = this.cdp;
23462
+ return withMatchedStylesReset(cdp, name, () => handler(cdp, params));
23183
23463
  }
23184
23464
  /**
23185
23465
  * Summary of this session for start responses.
@@ -23759,7 +24039,9 @@ var SessionController = class {
23759
24039
  totals: {
23760
24040
  network: data.totalNetwork,
23761
24041
  console: data.totalConsole,
23762
- ...data.droppedConsole && { consoleDropped: data.droppedConsole }
24042
+ ...data.droppedConsole && { consoleDropped: data.droppedConsole },
24043
+ ...data.droppedNetwork && { networkDropped: data.droppedNetwork },
24044
+ ...data.evictedNetworkBodies && { networkBodiesEvicted: data.evictedNetworkBodies }
23763
24045
  },
23764
24046
  currentNavigationId: data.currentNavigationId,
23765
24047
  ...data.pageCrashedAt !== void 0 && { pageCrashedAt: data.pageCrashedAt },