browser-debugger-cli 0.9.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 (133) hide show
  1. package/README.md +4 -1
  2. package/dist/commands/dom/a11y.js +2 -1
  3. package/dist/commands/dom/formInteraction.js +56 -25
  4. package/dist/commands/dom/helpers/keyAttributes.d.ts +20 -0
  5. package/dist/commands/dom/helpers/keyAttributes.js +54 -0
  6. package/dist/commands/dom/helpers/query.d.ts +1 -1
  7. package/dist/commands/dom/helpers/query.js +66 -19
  8. package/dist/commands/dom/helpers/runElementCommand.js +4 -3
  9. package/dist/commands/dom/helpers/screenshot.js +85 -12
  10. package/dist/commands/dom/index.d.ts +1 -0
  11. package/dist/commands/dom/index.js +8 -3
  12. package/dist/commands/dom/inspect.d.ts +15 -0
  13. package/dist/commands/dom/inspect.js +82 -0
  14. package/dist/commands/dom/layout.js +2 -2
  15. package/dist/commands/dom/listeners.js +2 -2
  16. package/dist/commands/dom/semanticUtils.d.ts +14 -1
  17. package/dist/commands/dom/semanticUtils.js +44 -3
  18. package/dist/commands/network/list.js +13 -2
  19. package/dist/commands/optionBehaviors.js +48 -6
  20. package/dist/commands/page.d.ts +1 -1
  21. package/dist/commands/page.js +62 -3
  22. package/dist/commands/shared/commonOptions.d.ts +4 -0
  23. package/dist/commands/shared/commonOptions.js +9 -0
  24. package/dist/commands/shared/optionTypes.d.ts +21 -0
  25. package/dist/commands/shared/startHelpers.d.ts +66 -0
  26. package/dist/commands/shared/startHelpers.js +91 -10
  27. package/dist/commands/shared/validation.d.ts +11 -0
  28. package/dist/commands/shared/validation.js +16 -0
  29. package/dist/daemon/launcher.d.ts +8 -1
  30. package/dist/daemon/launcher.js +3 -1
  31. package/dist/daemon/session/Session.d.ts +7 -0
  32. package/dist/daemon/session/Session.js +23 -1
  33. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  34. package/dist/daemon/session/commandRegistry.js +65 -9
  35. package/dist/daemon/session/interactions.d.ts +18 -5
  36. package/dist/daemon/session/interactions.js +22 -12
  37. package/dist/daemon.js +3565 -329
  38. package/dist/errors/messages.d.ts +69 -0
  39. package/dist/errors/messages.js +102 -1
  40. package/dist/index.js +2416 -1320
  41. package/dist/ipc/client.d.ts +9 -0
  42. package/dist/ipc/client.js +13 -0
  43. package/dist/ipc/protocol/commands.d.ts +56 -1
  44. package/dist/ipc/protocol/commands.js +2 -0
  45. package/dist/ipc/protocol/domTypes.d.ts +35 -2
  46. package/dist/ipc/protocol/inspectTypes.d.ts +388 -0
  47. package/dist/ipc/protocol/inspectTypes.js +10 -0
  48. package/dist/runtime/dom/actionEffects.d.ts +94 -15
  49. package/dist/runtime/dom/actionEffects.js +173 -27
  50. package/dist/runtime/dom/actionEffectsScripts.d.ts +52 -14
  51. package/dist/runtime/dom/actionEffectsScripts.js +224 -32
  52. package/dist/runtime/dom/elementInfo.d.ts +26 -0
  53. package/dist/runtime/dom/elementInfo.js +65 -0
  54. package/dist/runtime/dom/eventListeners.js +14 -4
  55. package/dist/runtime/dom/formFillHelpers/fill.d.ts +3 -4
  56. package/dist/runtime/dom/formFillHelpers/fill.js +77 -28
  57. package/dist/runtime/dom/frameSelection.d.ts +11 -0
  58. package/dist/runtime/dom/frameSelection.js +20 -1
  59. package/dist/runtime/dom/frames.d.ts +38 -5
  60. package/dist/runtime/dom/frames.js +136 -21
  61. package/dist/runtime/dom/inspect.d.ts +28 -0
  62. package/dist/runtime/dom/inspect.js +557 -0
  63. package/dist/runtime/dom/inspectAllStyles.d.ts +62 -0
  64. package/dist/runtime/dom/inspectAllStyles.js +385 -0
  65. package/dist/runtime/dom/inspectCascade.d.ts +94 -0
  66. package/dist/runtime/dom/inspectCascade.js +371 -0
  67. package/dist/runtime/dom/inspectCascadeModel.d.ts +39 -0
  68. package/dist/runtime/dom/inspectCascadeModel.js +232 -0
  69. package/dist/runtime/dom/inspectHints.d.ts +62 -0
  70. package/dist/runtime/dom/inspectHints.js +305 -0
  71. package/dist/runtime/dom/inspectLayoutModel.d.ts +87 -0
  72. package/dist/runtime/dom/inspectLayoutModel.js +346 -0
  73. package/dist/runtime/dom/inspectModel.d.ts +74 -0
  74. package/dist/runtime/dom/inspectModel.js +184 -0
  75. package/dist/runtime/dom/inspectPaintModel.d.ts +157 -0
  76. package/dist/runtime/dom/inspectPaintModel.js +461 -0
  77. package/dist/runtime/dom/inspectRules.d.ts +37 -0
  78. package/dist/runtime/dom/inspectRules.js +101 -0
  79. package/dist/runtime/dom/inspectScripts.d.ts +132 -0
  80. package/dist/runtime/dom/inspectScripts.js +263 -0
  81. package/dist/runtime/dom/inspectTree.d.ts +40 -0
  82. package/dist/runtime/dom/inspectTree.js +134 -0
  83. package/dist/runtime/dom/inspectVariables.d.ts +33 -0
  84. package/dist/runtime/dom/inspectVariables.js +94 -0
  85. package/dist/runtime/dom/inspectWhyModel.d.ts +20 -0
  86. package/dist/runtime/dom/inspectWhyModel.js +134 -0
  87. package/dist/runtime/dom/layout.d.ts +5 -1
  88. package/dist/runtime/dom/layout.js +10 -3
  89. package/dist/runtime/dom/listenerPageScripts.d.ts +11 -5
  90. package/dist/runtime/dom/listenerPageScripts.js +95 -9
  91. package/dist/runtime/dom/listenerSummary.d.ts +4 -0
  92. package/dist/runtime/dom/listenerSummary.js +26 -9
  93. package/dist/runtime/dom/reactEventHelpers.d.ts +5 -0
  94. package/dist/runtime/dom/reactEventHelpers.js +12 -4
  95. package/dist/runtime/page/emulation.d.ts +20 -0
  96. package/dist/runtime/page/emulation.js +37 -0
  97. package/dist/telemetry/a11y.d.ts +10 -0
  98. package/dist/telemetry/a11y.js +78 -1
  99. package/dist/telemetry/console.d.ts +1 -0
  100. package/dist/telemetry/console.js +100 -5
  101. package/dist/telemetry/network.js +3 -1
  102. package/dist/types.d.ts +32 -0
  103. package/dist/ui/formatters/details.d.ts +8 -0
  104. package/dist/ui/formatters/details.js +59 -3
  105. package/dist/ui/formatters/dom.d.ts +2 -1
  106. package/dist/ui/formatters/dom.js +25 -9
  107. package/dist/ui/formatters/inspect.d.ts +39 -0
  108. package/dist/ui/formatters/inspect.js +596 -0
  109. package/dist/ui/formatters/keyAttributes.d.ts +19 -0
  110. package/dist/ui/formatters/keyAttributes.js +84 -0
  111. package/dist/ui/formatters/layout.js +2 -2
  112. package/dist/ui/formatters/networkHeaders.d.ts +13 -0
  113. package/dist/ui/formatters/networkHeaders.js +23 -3
  114. package/dist/ui/formatters/networkList.d.ts +29 -1
  115. package/dist/ui/formatters/networkList.js +86 -20
  116. package/dist/ui/formatters/status.js +1 -1
  117. package/dist/ui/formatting.d.ts +9 -0
  118. package/dist/ui/formatting.js +6 -3
  119. package/dist/ui/messages/commands.d.ts +123 -7
  120. package/dist/ui/messages/commands.js +181 -10
  121. package/dist/ui/messages/networkMessages.d.ts +14 -0
  122. package/dist/ui/messages/networkMessages.js +18 -0
  123. package/dist/ui/messages/session.d.ts +14 -0
  124. package/dist/ui/messages/session.js +20 -0
  125. package/dist/utils/async.d.ts +9 -0
  126. package/dist/utils/async.js +17 -0
  127. package/dist/utils/color.d.ts +84 -0
  128. package/dist/utils/color.js +376 -0
  129. package/dist/utils/cssValues.d.ts +109 -0
  130. package/dist/utils/cssValues.js +236 -0
  131. package/dist/utils/selectorFilters.d.ts +12 -0
  132. package/dist/utils/selectorFilters.js +29 -0
  133. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2197,6 +2197,7 @@ function truncateUrl(url, maxLength = 60) {
2197
2197
  return `${host}/${rest.substring(0, start)}\u2026${rest.substring(rest.length - (room - start))}`;
2198
2198
  }
2199
2199
  function cutMiddle(text, maxLength) {
2200
+ if (text.length <= maxLength) return text;
2200
2201
  const head = Math.ceil((maxLength - 1) / 2);
2201
2202
  return `${text.substring(0, head)}\u2026${text.substring(text.length - (maxLength - 1 - head))}`;
2202
2203
  }
@@ -2386,9 +2387,25 @@ function formReadinessMessage(summary) {
2386
2387
  if (summary.requiredTotal > 0 || summary.emptyFieldLabels.length === 0) return "READY to submit";
2387
2388
  return `READY to submit (no field is marked required; empty: ${fieldLabelList(summary.emptyFieldLabels)})`;
2388
2389
  }
2389
- function actionStatusLine(done, warned, noEffect = false) {
2390
- if (noEffect) return `\u26A0 ${done} (${NO_VISIBLE_EFFECT})`;
2391
- return warned ? `\u26A0 ${done} (with warnings)` : `\u2713 ${done}`;
2390
+ function actionStatusLine(done, state) {
2391
+ if (state.noEffect) return `\u26A0 ${done} (${NO_VISIBLE_EFFECT})`;
2392
+ const notes = [state.warned && "with warnings", state.stillChanging && "page still changing"];
2393
+ const shown = notes.filter((note) => typeof note === "string");
2394
+ return shown.length > 0 ? `\u26A0 ${done} (${shown.join("; ")})` : `\u2713 ${done}`;
2395
+ }
2396
+ function stillChangingNote(action, pending) {
2397
+ const parts = [
2398
+ pending.requests !== void 0 && `${pluralize(pending.requests, "request")} pending`,
2399
+ pending.navigation && "a new page still loading",
2400
+ pending.loading !== void 0 && `loading indicator ${pending.loading} shown`,
2401
+ pending.domChanging && "DOM still changing",
2402
+ pending.busy && "page busy running a script"
2403
+ ].filter((part) => typeof part === "string");
2404
+ const wait = sessionCommand("bdg dom wait <selector>");
2405
+ return `The page was still changing when the ${action} returned (${parts.join(", ")}); wait for the result with ${wait}`;
2406
+ }
2407
+ function shownElementText(element) {
2408
+ return `${element.element} "${element.text}"`;
2392
2409
  }
2393
2410
  function pageNavigationText(navigation) {
2394
2411
  if (navigation.sameDocument) return `URL changed to ${navigation.url} (same document)`;
@@ -2466,7 +2483,7 @@ function layoutPositionLabel(element, viewport) {
2466
2483
  return note ? `${label} (${note})` : label;
2467
2484
  }
2468
2485
  function colorSchemeLabel(scheme, emulated) {
2469
- const source = emulated ? "emulated with --color-scheme" : "from the system setting";
2486
+ const source = emulated ? "emulated" : "from the system setting";
2470
2487
  return `prefers-color-scheme: ${scheme} (${source})`;
2471
2488
  }
2472
2489
  function sessionActiveLine(page) {
@@ -2485,6 +2502,32 @@ function layoutHeadline(count, listed, selector) {
2485
2502
  function indexLayoutHeadline(target) {
2486
2503
  return `Element at ${target} (page x,y and size in CSS px):`;
2487
2504
  }
2505
+ function coverText(cover, transparent) {
2506
+ return transparent ? `under transparent ${cover} (clicks land on it)` : `covered by ${cover}`;
2507
+ }
2508
+ function inspectCascadeNote(reason) {
2509
+ return reason === "timeout" ? "CSS rules not read: the page's stylesheets took too long (hints wait 1 s; --rules and --why 5 s)" : "CSS rules not read: Chrome could not report the rules matching this element";
2510
+ }
2511
+ function inspectDarkThemeBadge() {
2512
+ return "[dark theme from system; --color-scheme light for light]";
2513
+ }
2514
+ function inspectVisibilityBadges(visibility) {
2515
+ const reason = (text) => text ? `: ${text}` : "";
2516
+ if (visibility.notRendered) {
2517
+ return [`[not rendered${reason(visibility.hidden?.replace(/^not rendered \((.*)\)$/, "$1"))}]`];
2518
+ }
2519
+ return [
2520
+ visibility.hidden && `[hidden: ${visibility.hidden}]`,
2521
+ visibility.offscreen && `[offscreen: ${visibility.offscreen}]`,
2522
+ visibility.coveredBy && `[${coverText(visibility.coveredBy, visibility.coverTransparent)}]`
2523
+ ].filter((badge) => Boolean(badge));
2524
+ }
2525
+ function inspectedMatchAction(picked, index) {
2526
+ return picked === "first-visible" ? `inspected the first visible one ([${index}])` : "inspected the first";
2527
+ }
2528
+ function multipleMatchesWarning(count, action) {
2529
+ return `${count} elements match; ${action} (use --index or a more specific selector)`;
2530
+ }
2488
2531
  function listenersHeadline(element, counts, context = {}) {
2489
2532
  const index = context.index === void 0 ? "" : ` [${context.index}]`;
2490
2533
  const frame = context.frame ? ` in ${context.frame}` : "";
@@ -2643,7 +2686,31 @@ function startCommandHelpMessage() {
2643
2686
  ""
2644
2687
  );
2645
2688
  }
2646
- var REQUIRED_FIELD_EMPTY_REASON, NO_VISIBLE_EFFECT, LAYOUT_REASONS, SCROLL_LOCKED_PREFIX, VIEWPORT_POSITION_HINTS, COLLAPSED_LISTENERS_HEADING, NO_LISTENERS_HINT, NOTE_TYPES_SHOWN, POINTER_ACTION_DONE, PAGE_ACTION_DONE, PAGE_INFO_DESCRIPTION, PAGE_ACTION_DESCRIPTIONS, CLICK_RESULT_WAIT_HELP, WAIT_HELP_EXAMPLES, FRAME_URL_MAX_LENGTH;
2689
+ function pageEmulateNothingError() {
2690
+ return {
2691
+ message: "Nothing to emulate",
2692
+ suggestion: "Give --viewport <WxH>, --color-scheme light|dark, or --reset, e.g. bdg page emulate --viewport 900x700"
2693
+ };
2694
+ }
2695
+ function pageEmulationLines(result) {
2696
+ const size = (v) => `${v.width}x${v.height}`;
2697
+ const { emulated } = result;
2698
+ return [
2699
+ ["Viewport", emulated.viewport ? size(emulated.viewport) : "the browser window"],
2700
+ ...result.viewport ? [["Layout", `${size(result.viewport)} (without scrollbars)`]] : [],
2701
+ [
2702
+ "Scheme",
2703
+ emulated.colorScheme ?? `system setting${result.colorScheme ? ` (${result.colorScheme})` : ""}`
2704
+ ]
2705
+ ];
2706
+ }
2707
+ function inspectAnimatingBadge(animating) {
2708
+ return `[animating: ${animating.join(", ")}; values are mid-way, inspect again]`;
2709
+ }
2710
+ function inspectMidTransitionNote() {
2711
+ return "(mid-transition: inspect again for the final value)";
2712
+ }
2713
+ var REQUIRED_FIELD_EMPTY_REASON, NO_VISIBLE_EFFECT, LAYOUT_REASONS, SCROLL_LOCKED_PREFIX, VIEWPORT_POSITION_HINTS, INSPECT_OUTPUT_LEGEND, COLLAPSED_LISTENERS_HEADING, NO_LISTENERS_HINT, NOTE_TYPES_SHOWN, POINTER_ACTION_DONE, POINTER_ACTION_NOUN, PAGE_ACTION_DONE, PAGE_INFO_DESCRIPTION, PAGE_EMULATE_DESCRIPTION, PAGE_ACTION_DESCRIPTIONS, CLICK_RESULT_WAIT_HELP, WAIT_HELP_EXAMPLES, FRAME_URL_MAX_LENGTH;
2647
2714
  var init_commands = __esm({
2648
2715
  "src/ui/messages/commands.ts"() {
2649
2716
  "use strict";
@@ -2674,6 +2741,24 @@ var init_commands = __esm({
2674
2741
  right: "right of viewport",
2675
2742
  hidden: "hidden"
2676
2743
  };
2744
+ INSPECT_OUTPUT_LEGEND = `
2745
+ Output notation:
2746
+ WxH @x,y rendered border box size and page position (CSS px, no unit)
2747
+ m / p / b margin / padding / border widths, 1-4 values in CSS order (top right bottom left)
2748
+ in-parent distances to the parent's content edges (l t r b); sib: gaps to the sibling on each side
2749
+ scroll WxH the content (pseudo-elements too) is larger than the box
2750
+ 16/24 font size / line height; 'webfont loaded' = drawn with a downloaded font;
2751
+ (rendered "X") = drawn with another font than declared (a fallback)
2752
+ contrast 4.47 WCAG ratio, rounded down, against the background behind the text
2753
+ (+N not rendered) children with display: none (or not in the layout)
2754
+ hints declarations on this element that have no effect, why, the fix and where they are
2755
+ ('none': checked, nothing found)
2756
+ \u2190 sel (file:N) --rules: the declaration that sets the value (file:line, or file:line:column in
2757
+ minified files); 'over X': rules it beats; '= v': the value of a var() expression
2758
+ \u2713 / \u2717 --why: the winning declaration / ones it beats, highest precedence first;
2759
+ [0,2,0]: selector specificity (ids, classes, types); indented --name lines: where
2760
+ the winner's custom properties are set
2761
+ Sessions follow the system color scheme; start with --color-scheme light|dark to choose.`;
2677
2762
  COLLAPSED_LISTENERS_HEADING = "Framework roots (one line per node; --all lists each listener):";
2678
2763
  NO_LISTENERS_HINT = "Inline on\u2026 attributes and on\u2026 properties are included, and so are handlers frameworks delegate to ancestors (React, jQuery)";
2679
2764
  NOTE_TYPES_SHOWN = 5;
@@ -2683,6 +2768,12 @@ var init_commands = __esm({
2683
2768
  right: "Right-clicked",
2684
2769
  hover: "Hovered"
2685
2770
  };
2771
+ POINTER_ACTION_NOUN = {
2772
+ click: "click",
2773
+ double: "double-click",
2774
+ right: "right-click",
2775
+ hover: "hover"
2776
+ };
2686
2777
  PAGE_ACTION_DONE = {
2687
2778
  navigate: "Navigated",
2688
2779
  reload: "Reloaded",
@@ -2690,6 +2781,7 @@ var init_commands = __esm({
2690
2781
  forward: "Went forward"
2691
2782
  };
2692
2783
  PAGE_INFO_DESCRIPTION = "Show the URL and title of the session page";
2784
+ PAGE_EMULATE_DESCRIPTION = "Change the viewport or color scheme mid-session (like --viewport and --color-scheme at start), or --reset both";
2693
2785
  PAGE_ACTION_DESCRIPTIONS = {
2694
2786
  reload: "Reload the page",
2695
2787
  back: "Go back one page (like the browser button)",
@@ -2698,7 +2790,8 @@ var init_commands = __esm({
2698
2790
  CLICK_RESULT_WAIT_HELP = joinLines(
2699
2791
  "",
2700
2792
  "Waits only for the requests the action starts (150 ms idle, up to 2 s), not for",
2701
- "results the page shows later (timers, spinners, animations). Wait for those with:",
2793
+ "results the page shows later (timers, spinners, animations); the result says",
2794
+ '"page still changing" when it saw such work pending. Wait for those with:',
2702
2795
  " bdg dom wait '#result' --visible # or --text 'Saved', or '.spinner' --gone"
2703
2796
  );
2704
2797
  WAIT_HELP_EXAMPLES = joinLines(
@@ -3142,6 +3235,9 @@ function similarSelectorsLine(kind, names) {
3142
3235
  const plural = names.length === 1 ? what : kind === "id" ? "ids" : "classes";
3143
3236
  return `Did you mean ${names.map((name) => sigil + name).join(", ")}? (similar ${plural} on the page)`;
3144
3237
  }
3238
+ function shadowBoundaryLine(host) {
3239
+ return `${host.compound} hosts a shadow root, which a selector cannot cross into; bdg searches open shadow roots itself: use "${host.rest}"`;
3240
+ }
3145
3241
  function noNodesFoundError(selector, context = {}) {
3146
3242
  const hiddenMatches = context.hidden ?? 0;
3147
3243
  const hidden = hiddenMatches > 0 ? hiddenMatches === 1 ? "1 element matches without :visible but is hidden (check with bdg dom layout). " : `${hiddenMatches} elements match without :visible but are hidden (check with bdg dom layout). ` : "";
@@ -3151,6 +3247,7 @@ function noNodesFoundError(selector, context = {}) {
3151
3247
  suggestion: withLoadingHint(
3152
3248
  joinLines(
3153
3249
  context.similar,
3250
+ context.shadowHost && shadowBoundaryLine(context.shadowHost),
3154
3251
  `${hidden}Verify the CSS selector is correct.`,
3155
3252
  note ? note : void 0
3156
3253
  ),
@@ -3853,6 +3950,9 @@ async function domScroll(params) {
3853
3950
  async function pageNavigate(params) {
3854
3951
  return sendCommand("page_navigate", params);
3855
3952
  }
3953
+ async function pageEmulate(params) {
3954
+ return sendCommand("page_emulate", params);
3955
+ }
3856
3956
  async function domFormDiscover() {
3857
3957
  return sendCommand("dom_form_discover", {});
3858
3958
  }
@@ -3862,6 +3962,9 @@ async function domListeners(params) {
3862
3962
  async function domLayout(params) {
3863
3963
  return sendCommand("dom_layout", params);
3864
3964
  }
3965
+ async function domInspect(params) {
3966
+ return sendCommand("dom_inspect", params);
3967
+ }
3865
3968
  async function domWait(params) {
3866
3969
  return sendCommand(
3867
3970
  "dom_wait",
@@ -3888,7 +3991,7 @@ function textPreview2(text, length = PREVIEW_LENGTH) {
3888
3991
  const characters = Array.from(collapsed);
3889
3992
  return characters.length > length ? characters.slice(0, length).join("") + "..." : collapsed;
3890
3993
  }
3891
- var PREVIEW_LENGTH, ELEMENT_TEXT_LENGTH, WITHOUT_DECORATIONS_JS, ELEMENT_TEXT_JS, ELEMENT_DESCRIPTION_JS, SIBLING_POSITION_JS, ELEMENT_IDENTITY_JS, ELEMENT_CONTEXT_JS;
3994
+ var PREVIEW_LENGTH, ELEMENT_TEXT_LENGTH, WITHOUT_DECORATIONS_JS, ELEMENT_TEXT_JS, MASKED_VALUE, SENSITIVE_FIELD_JS, ELEMENT_STATE_JS, ELEMENT_DESCRIPTION_JS, SIBLING_POSITION_JS, ELEMENT_IDENTITY_JS, ELEMENT_CONTEXT_JS;
3892
3995
  var init_elementInfo = __esm({
3893
3996
  "src/runtime/dom/elementInfo.ts"() {
3894
3997
  "use strict";
@@ -3933,6 +4036,48 @@ var init_elementInfo = __esm({
3933
4036
  if (!parent || rendered(parent)) start += walker.currentNode.data;
3934
4037
  }
3935
4038
  return withoutDecorations(el, start);
4039
+ }`;
4040
+ MASKED_VALUE = "\u2022\u2022\u2022\u2022";
4041
+ SENSITIVE_FIELD_JS = `(el) => {
4042
+ const autocomplete = el.getAttribute('autocomplete') || '';
4043
+ if (/(^|\\s)(cc-[a-z-]+|one-time-code|current-password|new-password)(\\s|$)/i.test(autocomplete)) return true;
4044
+ if (el.type === 'password' || /^password$/i.test(el.getAttribute('type') || '')) return true;
4045
+ const names = [el.getAttribute('name'), el.id, autocomplete].join(' ');
4046
+ if (/passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc/i.test(names)) return true;
4047
+ try {
4048
+ const security = el.ownerDocument.defaultView.getComputedStyle(el).getPropertyValue('-webkit-text-security');
4049
+ return Boolean(security) && security !== 'none';
4050
+ } catch (e) {
4051
+ return false;
4052
+ }
4053
+ }`;
4054
+ ELEMENT_STATE_JS = `(el) => {
4055
+ const isSensitive = ${SENSITIVE_FIELD_JS};
4056
+ const mask = (value) => (value ? '${MASKED_VALUE}' : '');
4057
+ const guarded = (state) => {
4058
+ if (!isSensitive(el)) return state;
4059
+ const result = { ...state, sensitive: true };
4060
+ if ('value' in result) result.value = mask(result.value);
4061
+ if ('selected' in result) result.selected = mask(result.selected);
4062
+ return result;
4063
+ };
4064
+ switch (el.localName) {
4065
+ case 'input':
4066
+ if (el.type === 'hidden') return { type: 'hidden' };
4067
+ return guarded(
4068
+ /^(checkbox|radio)$/.test(el.type)
4069
+ ? { type: el.type, checked: el.checked, value: el.getAttribute('value') || '' }
4070
+ : { type: el.type, value: el.value }
4071
+ );
4072
+ case 'textarea':
4073
+ return guarded({ value: el.value });
4074
+ case 'select':
4075
+ return guarded({ selected: Array.from(el.selectedOptions || [], (option) => option.label).join(', ') });
4076
+ case 'button':
4077
+ return el.form ? { type: el.type } : {};
4078
+ default:
4079
+ return {};
4080
+ }
3936
4081
  }`;
3937
4082
  ELEMENT_DESCRIPTION_JS = `(node) => node.tagName.toLowerCase() +
3938
4083
  (node.id ? '#' + node.id : '') +
@@ -4011,6 +4156,42 @@ var init_elementInfo = __esm({
4011
4156
  }
4012
4157
  });
4013
4158
 
4159
+ // src/commands/dom/helpers/keyAttributes.ts
4160
+ function keyAttributes(tag, attributes, state = {}) {
4161
+ const names = ATTRIBUTES_BY_TAG[tag];
4162
+ if (!names) return void 0;
4163
+ const result = {};
4164
+ const type = state.type ?? attributes["type"];
4165
+ if (type) result["type"] = type;
4166
+ for (const name of names) {
4167
+ const value = attributes[name];
4168
+ if (value) result[name] = value;
4169
+ }
4170
+ if (type === "hidden") return result;
4171
+ const secret = state.sensitive === true || type === "password";
4172
+ if (state.value) result["value"] = secret ? MASKED_VALUE : state.value;
4173
+ if (state.checked !== void 0) result["checked"] = state.checked;
4174
+ if (state.selected) result["selected"] = secret ? MASKED_VALUE : state.selected;
4175
+ return Object.keys(result).length > 0 ? result : void 0;
4176
+ }
4177
+ var ATTRIBUTES_BY_TAG;
4178
+ var init_keyAttributes = __esm({
4179
+ "src/commands/dom/helpers/keyAttributes.ts"() {
4180
+ "use strict";
4181
+ init_elementInfo();
4182
+ ATTRIBUTES_BY_TAG = {
4183
+ img: ["src", "alt"],
4184
+ a: ["href"],
4185
+ input: ["name", "placeholder"],
4186
+ textarea: ["name", "placeholder"],
4187
+ button: ["name"],
4188
+ select: ["name"],
4189
+ iframe: ["src"],
4190
+ form: ["action", "method"]
4191
+ };
4192
+ }
4193
+ });
4194
+
4014
4195
  // src/runtime/dom/elementGeometry.ts
4015
4196
  function edgesOf(box) {
4016
4197
  return { left: box.x, top: box.y, right: box.x + box.width, bottom: box.y + box.height };
@@ -4397,6 +4578,23 @@ function splitSelectorList(selector) {
4397
4578
  parts.push(selector.slice(partStart));
4398
4579
  return parts;
4399
4580
  }
4581
+ function leadingCompounds(selector) {
4582
+ if (splitSelectorList(selector).length > 1) return [];
4583
+ const text = selector.trim();
4584
+ const found = [];
4585
+ let start = 0;
4586
+ scan(text, (index, depth) => {
4587
+ const char = text[index] ?? "";
4588
+ if (depth > 0 || !/[\s>+~]/.test(char)) return index;
4589
+ const compound = text.slice(start, index).trim();
4590
+ let next = index;
4591
+ while (next + 1 < text.length && /[\s>+~]/.test(text[next + 1] ?? "")) next++;
4592
+ if (compound) found.push({ compound, rest: text.slice(next + 1).trim() });
4593
+ start = next + 1;
4594
+ return next;
4595
+ });
4596
+ return found;
4597
+ }
4400
4598
  function scan(text, visit) {
4401
4599
  let depth = 0;
4402
4600
  for (let i = 0; i < text.length; i++) {
@@ -4864,15 +5062,24 @@ async function withSelection(selector, use) {
4864
5062
  async function noMatchesError(selector) {
4865
5063
  return noNodesFoundError(selector, await noMatchContext(selector));
4866
5064
  }
5065
+ function shadowHostJS(compounds) {
5066
+ const plain = compounds.slice(0, MAX_HOST_COMPOUNDS).map((entry) => [withoutFilters(entry.compound), withoutFilters(entry.rest)]);
5067
+ return `${JSON.stringify(plain)}.findIndex(([host, rest]) => { try { const deep = ${DEEP_QUERY_JS}; return deep(host, null).some((el) => el.shadowRoot) && deep(rest, null).length > 0; } catch (e) { return false; } })`;
5068
+ }
5069
+ function withoutFilters(selector) {
5070
+ return selector.replace(/:visible\b|:(has-text|text-is)\((?:"[^"]*"|'[^']*'|[^)"'])*\)/g, "").trim() || "*";
5071
+ }
4867
5072
  async function noMatchContext(selector) {
4868
5073
  const parts = parseSelectorFilters(selector);
4869
5074
  const unfiltered = parts && withoutVisibleFilters(parts);
4870
5075
  const single = parseSingleNameSelector(selector);
4871
5076
  const hidden = unfiltered ? `(() => { try { return (${DEEP_QUERY_JS})(${JSON.stringify(selector)}, ${JSON.stringify(unfiltered)}).length; } catch (e) { return 0; } })()` : "0";
4872
5077
  const names = single ? pageNamesJS(single.kind) : "[]";
5078
+ const compounds = leadingCompounds(selector);
5079
+ const shadowHost = compounds.length > 0 ? shadowHostJS(compounds) : "-1";
4873
5080
  try {
4874
5081
  const evaluated = await callCDP("Runtime.evaluate", {
4875
- expression: `({ hidden: ${hidden}, readyState: document.readyState, unsearched: ${UNSEARCHED_CONTENT_JS}, names: ${names} })`,
5082
+ expression: `({ hidden: ${hidden}, readyState: document.readyState, unsearched: ${UNSEARCHED_CONTENT_JS}, names: ${names}, shadowHost: ${shadowHost} })`,
4876
5083
  returnByValue: true
4877
5084
  });
4878
5085
  const { result } = evaluated.data?.result ?? {};
@@ -4893,7 +5100,8 @@ async function noMatchContext(selector) {
4893
5100
  embeds: value.unsearched.embeds === true
4894
5101
  }
4895
5102
  },
4896
- ...similar && { similar }
5103
+ ...similar && { similar },
5104
+ ...typeof value.shadowHost === "number" && compounds[value.shadowHost] && { shadowHost: compounds[value.shadowHost] }
4897
5105
  };
4898
5106
  } catch (error) {
4899
5107
  log15.debug(`Could not read the page after no match: ${getErrorMessage(error)}`);
@@ -4924,6 +5132,7 @@ async function elementsWithDetails(arrayObjectId) {
4924
5132
  backendNodeId,
4925
5133
  context: details?.context ?? "",
4926
5134
  text: details?.text ?? "",
5135
+ state: details?.state ?? {},
4927
5136
  ...viewportHint(details?.geometry, viewport)
4928
5137
  }
4929
5138
  ];
@@ -4994,17 +5203,20 @@ async function queryDOMElements(selector) {
4994
5203
  log15.debug(`Querying ${elements.length} elements with selector: ${selector}`);
4995
5204
  }
4996
5205
  const nodes = await mapConcurrently(elements, async (element, index) => {
4997
- const { backendNodeId, context, text, inViewport, clippedBy } = element;
5206
+ const { backendNodeId, context, text, state, inViewport, clippedBy } = element;
4998
5207
  const desc = await describeNode({ backendNodeId });
4999
5208
  if (!desc) return { index, nodeId: 0 };
5000
5209
  const attributes = unpackAttributes(desc.attributes);
5001
5210
  const classes = attributes["class"]?.split(/\s+/).filter(Boolean);
5002
5211
  const preview = textPreview2(text);
5212
+ const tag = desc.nodeName.toLowerCase();
5213
+ const keys = keyAttributes(tag, attributes, state);
5003
5214
  return {
5004
5215
  index,
5005
5216
  nodeId: desc.backendNodeId,
5006
- tag: desc.nodeName.toLowerCase(),
5217
+ tag,
5007
5218
  ...identifyingAttributes(attributes, desc.nodeName),
5219
+ ...keys && { attributes: keys },
5008
5220
  ...classes && { classes },
5009
5221
  ...preview && { preview },
5010
5222
  ...context && { context },
@@ -5023,20 +5235,23 @@ function identifyingAttributes(attributes, nodeName) {
5023
5235
  ...value !== void 0 && { value }
5024
5236
  };
5025
5237
  }
5026
- async function elementText(ref, full) {
5238
+ async function elementTextAndState(ref, full) {
5027
5239
  const objectGroup = `bdg-text-${process.pid}-${++queryCount}`;
5028
5240
  const resolved = await callCDP("DOM.resolveNode", { ...ref, objectGroup });
5029
5241
  const objectId = resolved.data?.result?.object.objectId;
5030
- if (!objectId) return "";
5242
+ if (!objectId) return { text: "", state: {} };
5031
5243
  try {
5032
5244
  const response = await callCDP("Runtime.callFunctionOn", {
5033
5245
  objectId,
5034
- functionDeclaration: `function (full) { return (${ELEMENT_TEXT_JS})(this, full); }`,
5246
+ functionDeclaration: `function (full) { return { text: (${ELEMENT_TEXT_JS})(this, full), state: (${ELEMENT_STATE_JS})(this) }; }`,
5035
5247
  arguments: [{ value: full }],
5036
5248
  returnByValue: true
5037
5249
  });
5038
5250
  const value = response.data?.result?.result?.value;
5039
- return typeof value === "string" ? value : "";
5251
+ return {
5252
+ text: typeof value?.text === "string" ? value.text : "",
5253
+ state: value?.state ?? {}
5254
+ };
5040
5255
  } finally {
5041
5256
  await callCDP("Runtime.releaseObjectGroup", { objectGroup });
5042
5257
  }
@@ -5049,13 +5264,18 @@ async function getDomContext(ref, options = {}) {
5049
5264
  return null;
5050
5265
  }
5051
5266
  const full = options.full === true;
5052
- const classes = unpackAttributes(desc.attributes)["class"]?.split(/\s+/).filter(Boolean);
5053
- const text = await elementText(ref, full);
5267
+ const attributes = unpackAttributes(desc.attributes);
5268
+ const classes = attributes["class"]?.split(/\s+/).filter(Boolean);
5269
+ const { text, state } = await elementTextAndState(ref, full);
5054
5270
  const preview = textPreview2(text);
5055
5271
  const longer = textPreview2(text, full ? Number.POSITIVE_INFINITY : ELEMENT_TEXT_LENGTH);
5272
+ const tag = desc.nodeName.toLowerCase();
5273
+ const keys = keyAttributes(tag, attributes, state);
5056
5274
  return {
5057
- tag: desc.nodeName.toLowerCase(),
5275
+ tag,
5058
5276
  ...classes && classes.length > 0 && { classes },
5277
+ ...keys && { attributes: keys },
5278
+ ...state.sensitive && { sensitive: true },
5059
5279
  ...preview && { preview },
5060
5280
  ...longer !== preview && { text: longer },
5061
5281
  ...!preview && await childElements(ref)
@@ -5182,10 +5402,11 @@ async function assertNodeAttached(backendNodeId, source) {
5182
5402
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.STALE_CACHE);
5183
5403
  }
5184
5404
  }
5185
- var log15, CDP_CONCURRENCY_LIMIT, queryCount, VIEWPORT_HINT_LIMIT, ELEMENT_DETAILS_FUNCTION, CHILDREN_LISTED;
5405
+ var log15, CDP_CONCURRENCY_LIMIT, queryCount, MAX_HOST_COMPOUNDS, VIEWPORT_HINT_LIMIT, ELEMENT_DETAILS_FUNCTION, CHILDREN_LISTED;
5186
5406
  var init_query = __esm({
5187
5407
  "src/commands/dom/helpers/query.ts"() {
5188
5408
  "use strict";
5409
+ init_keyAttributes();
5189
5410
  init_errors3();
5190
5411
  init_messages();
5191
5412
  init_client();
@@ -5202,14 +5423,16 @@ var init_query = __esm({
5202
5423
  log15 = createLogger("dom");
5203
5424
  CDP_CONCURRENCY_LIMIT = 10;
5204
5425
  queryCount = 0;
5426
+ MAX_HOST_COMPOUNDS = 4;
5205
5427
  VIEWPORT_HINT_LIMIT = 100;
5206
5428
  ELEMENT_DETAILS_FUNCTION = `function () {
5207
5429
  const contextOf = ${ELEMENT_CONTEXT_JS};
5208
5430
  const textOf = ${ELEMENT_TEXT_JS};
5209
5431
  const geometryOf = ${ELEMENT_GEOMETRY_JS};
5432
+ const stateOf = ${ELEMENT_STATE_JS};
5210
5433
  const read = (el, index) => {
5211
5434
  try {
5212
- return { context: contextOf(el), text: textOf(el), geometry: index < ${VIEWPORT_HINT_LIMIT} ? geometryOf(el) : null };
5435
+ return { context: contextOf(el), text: textOf(el), state: stateOf(el), geometry: index < ${VIEWPORT_HINT_LIMIT} ? geometryOf(el) : null };
5213
5436
  } catch (e) {
5214
5437
  return {};
5215
5438
  }
@@ -5486,13 +5709,22 @@ async function restoreScrollPosition(position) {
5486
5709
  returnByValue: true
5487
5710
  });
5488
5711
  }
5712
+ async function windowSize(viewport) {
5713
+ const response = await callCDP("Runtime.evaluate", {
5714
+ expression: "[window.innerWidth, window.innerHeight]",
5715
+ returnByValue: true
5716
+ });
5717
+ const value = response.data?.result?.result?.value;
5718
+ const [width, height] = Array.isArray(value) ? value : [];
5719
+ return {
5720
+ width: Math.round(width ?? viewport.clientWidth),
5721
+ height: Math.round(height ?? viewport.clientHeight)
5722
+ };
5723
+ }
5489
5724
  async function useUnitPixelRatio(devicePixelRatio, viewport) {
5490
5725
  if (devicePixelRatio === 1) return () => Promise.resolve();
5491
5726
  const sessionViewport = readSessionMetadata()?.viewport;
5492
- const size = sessionViewport ?? {
5493
- width: Math.round(viewport.clientWidth),
5494
- height: Math.round(viewport.clientHeight)
5495
- };
5727
+ const size = sessionViewport ?? await windowSize(viewport);
5496
5728
  await callCDP("Emulation.setDeviceMetricsOverride", viewportOverride(size, 1));
5497
5729
  return async () => {
5498
5730
  if (sessionViewport) {
@@ -5636,6 +5868,28 @@ async function capturePageScreenshot(outputPath, options = {}) {
5636
5868
  }
5637
5869
  return result;
5638
5870
  }
5871
+ async function visibleViewport() {
5872
+ const metrics = (await callCDP("Page.getLayoutMetrics", {})).data?.result;
5873
+ const view = metrics?.cssVisualViewport ?? metrics?.visualViewport;
5874
+ return { width: view?.clientWidth ?? 0, height: view?.clientHeight ?? 0 };
5875
+ }
5876
+ function insideView(area, view) {
5877
+ return area.x >= 0 && area.y >= 0 && area.x + area.width <= view.width && area.y + area.height <= view.height;
5878
+ }
5879
+ async function measureInView(ref) {
5880
+ const view = await visibleViewport();
5881
+ let box = await getElementBounds(ref);
5882
+ let bounds = await captureArea(ref, box);
5883
+ const fits = bounds.width <= view.width && bounds.height <= view.height;
5884
+ if (fits && !insideView(bounds, view)) {
5885
+ const dx = bounds.x + bounds.width / 2 - view.width / 2;
5886
+ const dy = bounds.y + bounds.height / 2 - view.height / 2;
5887
+ await callCDP("Runtime.evaluate", { expression: `window.scrollBy(${dx}, ${dy})` });
5888
+ box = await getElementBounds(ref);
5889
+ bounds = await captureArea(ref, box);
5890
+ }
5891
+ return { box, bounds, inView: insideView(bounds, view) };
5892
+ }
5639
5893
  async function captureArea(ref, bounds) {
5640
5894
  const objectGroup = `bdg-shot-${process.pid}`;
5641
5895
  try {
@@ -5667,8 +5921,6 @@ async function captureArea(ref, bounds) {
5667
5921
  }
5668
5922
  }
5669
5923
  async function captureElementScreenshot(outputPath, ref, options = {}) {
5670
- const box = await getElementBounds(ref);
5671
- const bounds = await captureArea(ref, box);
5672
5924
  const format = options.format ?? "png";
5673
5925
  const quality = format === "jpeg" ? options.quality ?? 90 : void 0;
5674
5926
  const noResize = options.noResize ?? false;
@@ -5677,6 +5929,20 @@ async function captureElementScreenshot(outputPath, ref, options = {}) {
5677
5929
  returnByValue: true
5678
5930
  });
5679
5931
  const devicePixelRatio = dprResponse.data?.result?.result?.value ?? 1;
5932
+ const before = (await callCDP("Page.getLayoutMetrics", {})).data?.result;
5933
+ const restoreMetrics = await useUnitPixelRatio(
5934
+ devicePixelRatio,
5935
+ before?.visualViewport ?? { clientWidth: 800, clientHeight: 600 }
5936
+ );
5937
+ let box;
5938
+ let bounds;
5939
+ let inView;
5940
+ try {
5941
+ ({ box, bounds, inView } = await measureInView(ref));
5942
+ } catch (error) {
5943
+ await restoreMetrics();
5944
+ throw error;
5945
+ }
5680
5946
  const originalWidth = bounds.width;
5681
5947
  const originalHeight = bounds.height;
5682
5948
  const resized = shouldResize(originalWidth, originalHeight, noResize);
@@ -5685,7 +5951,6 @@ async function captureElementScreenshot(outputPath, ref, options = {}) {
5685
5951
  const finalHeight = Math.round(originalHeight * scale);
5686
5952
  const metricsResponse = await callCDP("Page.getLayoutMetrics", {});
5687
5953
  const metricsResult = metricsResponse.data?.result;
5688
- const viewport = metricsResult?.visualViewport ?? { clientWidth: 800, clientHeight: 600 };
5689
5954
  const scroll = metricsResult?.cssLayoutViewport ?? { pageX: 0, pageY: 0 };
5690
5955
  const onPage = (area) => ({
5691
5956
  ...area,
@@ -5693,14 +5958,13 @@ async function captureElementScreenshot(outputPath, ref, options = {}) {
5693
5958
  y: area.y + scroll.pageY
5694
5959
  });
5695
5960
  const clip = onPage(bounds);
5696
- const restoreMetrics = await useUnitPixelRatio(devicePixelRatio, viewport);
5697
5961
  let screenshotResult;
5698
5962
  try {
5699
5963
  const screenshotResponse = await callCDP("Page.captureScreenshot", {
5700
5964
  format,
5701
5965
  ...quality !== void 0 && { quality },
5702
5966
  clip: { ...clip, scale },
5703
- captureBeyondViewport: true
5967
+ captureBeyondViewport: !inView
5704
5968
  });
5705
5969
  screenshotResult = screenshotResponse.data?.result;
5706
5970
  } finally {
@@ -9193,6 +9457,8 @@ init_schema();
9193
9457
  init_screenshotResize();
9194
9458
  var TRIGGERED_REQUESTS_BEHAVIOR = "Requests (and WebSocket connections) that start after the action begins are returned as triggeredRequests (method, url, status, durationMs; pending when still running at return, loading when the response arrived but its body is still streaming; with resourceType; human output lists documents, XHR/fetch and WebSockets first (up to 10) and counts static assets on one line; absent when network telemetry is off). Attribution is by time: requests a page timer or poller starts meanwhile are listed too, whether or not the action caused them";
9195
9459
  var ACTION_EFFECTS_BEHAVIOR = 'The result also says what changed on the page: a navigation (Page: navigated to <url> (status), or URL changed to <url> (same document); JSON navigation { url, sameDocument, status }), and messages that appeared or changed in alert/status/aria-live elements or flash/error/toast-like classes (New text: "\u2026" (element); JSON messages [{ text, element }], at most 3; after a navigation every message on the new page counts; texts of only digits and time units, such as clocks and counters, are left out, but other text that changes on its own, such as a rotating banner, can show up). Both are absent when nothing changed. Cost: one page script sent before the action without waiting for it and one read after it, a few ms; when the page does not answer (a pending navigation) bdg waits at most 200 ms for the snapshot and 250 ms per read, and the navigation is still reported from CDP events';
9460
+ var STILL_CHANGING_BEHAVIOR = 'When the page was still changing as the action returned, the status line says (page still changing), a note below it says what was pending and suggests bdg dom wait <selector>, and JSON has settled: false with pending { requests (document, fetch/XHR and script requests still running), navigation (a new page still loading), loading (a loading indicator that appeared, e.g. "div#loading"), domChanging (DOM changes kept coming in bursts over a second look 250 ms later; a single render, ticking text and style animations do not count), busy (the page did not answer within 250 ms: a long script) }; absent when the page looked settled (exit code stays 0). A result a timer renders later, with no DOM change, request or loading indicator before it, is not detected. Cost: nothing extra, except 250 ms plus one read when the DOM looked busy. Not checked with --no-wait';
9461
+ var SHOWN_BEHAVIOR = `Elements the action showed are listed (Shown: <element> "<text>"; JSON shown [{ text, element }], at most 3, outermost first): elements with visible text added inside the target's form, search box, dialog or combobox (else its grandparent, or its parent when that is the body), and popups and messages added anywhere (tooltip, menu, listbox, dialog, alert, status roles, popover, aria-live, message-like classes); widgets elsewhere on the page and re-rendered elements whose text was there before do not count`;
9196
9462
  var NO_WAIT_TRIGGERED_REQUESTS = "Returns immediately without waiting for network; triggeredRequests lists only requests bdg saw start before returning (often none yet; check bdg network list later)";
9197
9463
  var OPTION_BEHAVIORS = {
9198
9464
  "screenshot:--selector": {
@@ -9252,7 +9518,7 @@ var OPTION_BEHAVIORS = {
9252
9518
  "eval:--frame": {
9253
9519
  default: "Evaluates in the page's main frame",
9254
9520
  whenEnabled: "Evaluates in one iframe's main world (its own globals), including cross-origin (out-of-process) iframes; output gains a frame field (its URL)",
9255
- automaticBehavior: "The value is matched as: a 0-based index (bdg dom frames order, main page not counted), else an exact name/id attribute, else a case-insensitive part of the name, id or URL. Several matches fail with 81 listing them; none fails with 83 listing all frames. Frames are looked up on every call (a reloaded iframe is found again)."
9521
+ automaticBehavior: "The value is matched as: a 0-based index (bdg dom frames order: document order of the <iframe> elements, nested ones depth-first, main page not counted), else an exact name/id attribute, else a case-insensitive part of the name, id or URL. Several matches fail with 81 listing them; none fails with 83 listing all frames. Frames are looked up on every call (a reloaded iframe is found again). An index that names another frame than in the last bdg dom frames listing (iframes added, removed or moved, or the page navigated) fails with 87 STALE_CACHE: re-run bdg dom frames or pick the frame by name."
9256
9522
  },
9257
9523
  "console:-H": {
9258
9524
  default: "Shows messages from current page load only (most recent navigation)",
@@ -9294,7 +9560,7 @@ var OPTION_BEHAVIORS = {
9294
9560
  "click:--no-wait": {
9295
9561
  default: "Waits for network stability after click (150ms idle, up to 2s)",
9296
9562
  whenDisabled: NO_WAIT_TRIGGERED_REQUESTS,
9297
- automaticBehavior: `Network wait helps ensure AJAX requests triggered by click complete. ${TRIGGERED_REQUESTS_BEHAVIOR}. The click itself uses real mouse events in the visible part of the element (method "mouse"); if the element is covered or has no size it falls back to DOM events (method "dom", with a warning). Results the page shows later without requests (timers, spinners) are not waited for: use bdg dom wait <selector> --visible. ${ACTION_EFFECTS_BEHAVIOR}. A click with no DOM change, no request and no navigation (checked again 300 ms later, which adds 300 ms plus at most 250 ms for the read) is reported as \u26A0 Element Clicked (no visible effect observed: no DOM change, requests or navigation within 300 ms) and effect: "none" in JSON (exit code stays 0); not claimed with --no-wait, for hover or right-click, after a copy or cut, or when the click hit a form control, label, media, iframe, popover button, a mailto:/tel:/javascript: or other non-http link, a link to another window or a custom element with a closed shadow root, or moved focus to an element that is not a button or link. Effects outside the DOM (CSS :hover/:focus-within styles, canvas, clipboard without a copy event) are not seen`,
9563
+ automaticBehavior: `Network wait helps ensure AJAX requests triggered by click complete. ${TRIGGERED_REQUESTS_BEHAVIOR}. The click itself uses real mouse events in the visible part of the element (method "mouse"); if the element is covered or has no size it falls back to DOM events (method "dom", with a warning; --strict refuses instead). Results the page shows later (timers, spinners, slow renders) are not waited for but reported as pending work: use bdg dom wait <selector> --visible. ${ACTION_EFFECTS_BEHAVIOR}. ${STILL_CHANGING_BEHAVIOR}. A click with no DOM change, no request and no navigation (checked again 300 ms later, which adds 300 ms plus at most 250 ms for the read) is reported as \u26A0 Element Clicked (no visible effect observed: no DOM change, requests or navigation within 300 ms) and effect: "none" in JSON (exit code stays 0); not claimed with --no-wait, for hover or right-click, after a copy or cut, or when the click hit a form control, label, media, iframe, popover button, a mailto:/tel:/javascript: or other non-http link, a link to another window or a custom element with a closed shadow root, or moved focus to an element that is not a button or link. Effects outside the DOM (CSS :hover/:focus-within styles, canvas, clipboard without a copy event) are not seen`,
9298
9564
  tokenImpact: "A click that navigates lists the whole page load in JSON triggeredRequests"
9299
9565
  },
9300
9566
  "click:--double": {
@@ -9305,10 +9571,19 @@ var OPTION_BEHAVIORS = {
9305
9571
  default: "Left click",
9306
9572
  whenEnabled: "Right-click: the page gets contextmenu (custom context menus open); cannot be combined with --double"
9307
9573
  },
9574
+ "click:--strict": {
9575
+ default: 'A covered, hidden or zero-size element is clicked with DOM events (method "dom", with a warning, exit 0)',
9576
+ whenEnabled: "Refuses with exit 90 (RESOURCE_CONFLICT: the page state blocks the request) when a real mouse cannot reach the element, naming what covers it and suggesting bdg dom layout <selector>; also when the mouse press never reached the element (it is released, no further presses for --double)",
9577
+ automaticBehavior: "Applies to --double and --right too. Nothing is dispatched when the element is unreachable, so the page is unchanged"
9578
+ },
9308
9579
  "hover:--no-wait": {
9309
9580
  default: "Waits for network stability after moving the mouse (menus may load content)",
9310
9581
  whenDisabled: NO_WAIT_TRIGGERED_REQUESTS,
9311
- automaticBehavior: `The mouse stays over the element afterwards, so hover menus stay open until the next mouse action. ${TRIGGERED_REQUESTS_BEHAVIOR}. ${ACTION_EFFECTS_BEHAVIOR}`
9582
+ automaticBehavior: `The mouse stays over the element afterwards, so hover menus stay open until the next mouse action. ${TRIGGERED_REQUESTS_BEHAVIOR}. ${ACTION_EFFECTS_BEHAVIOR}. ${SHOWN_BEHAVIOR}; for a hover also elements around it (its parent's subtree) and tooltips, menus, listboxes, dialogs and popovers anywhere that were hidden before, so captions shown by CSS :hover count (hidden elements noted by identity right before the mouse moves: up to 1500, within 8 ms). A hover never claims "no visible effect" and does not check whether the page was still changing`
9583
+ },
9584
+ "hover:--strict": {
9585
+ default: 'A covered, hidden or zero-size element gets synthetic mouseover/mouseenter events (method "dom", with a warning)',
9586
+ whenEnabled: "Refuses with exit 90 when a real mouse cannot reach the element, naming what covers it and suggesting bdg dom layout <selector>"
9312
9587
  },
9313
9588
  "navigate:--no-wait": {
9314
9589
  default: "Waits until the new page has loaded and the network and DOM are idle (up to 15 s)",
@@ -9318,7 +9593,7 @@ var OPTION_BEHAVIORS = {
9318
9593
  "pressKey:--no-wait": {
9319
9594
  default: "Waits for network stability after key press (150ms idle, up to 2s)",
9320
9595
  whenDisabled: NO_WAIT_TRIGGERED_REQUESTS,
9321
- automaticBehavior: `${TRIGGERED_REQUESTS_BEHAVIOR}. ${ACTION_EFFECTS_BEHAVIOR}`
9596
+ automaticBehavior: `${TRIGGERED_REQUESTS_BEHAVIOR}. ${ACTION_EFFECTS_BEHAVIOR}. ${SHOWN_BEHAVIOR}, such as the item Enter added to a list. ${STILL_CHANGING_BEHAVIOR}. A key press never claims "no visible effect"`
9322
9597
  },
9323
9598
  "pressKey:--times": {
9324
9599
  default: "Presses key once",
@@ -9363,7 +9638,7 @@ var OPTION_BEHAVIORS = {
9363
9638
  "listeners:--type": {
9364
9639
  default: "Lists listeners of every event type on the element, its ancestors (through open shadow roots), its document and window",
9365
9640
  whenEnabled: "Lists only these event types (comma-separated or repeated, case-sensitive like addEventListener); when none match, typeSuggestions names close types (Click, onclick \u2192 click)",
9366
- automaticBehavior: `Listeners are grouped by type, the element's own handlers first (types handled on the element before delegated ones; nearest first, no-ops last); JSON line/column numbers are 0-based (human output shows them 1-based like DevTools). React's on\u2026 props that run for the element (its own and its React parents', through portals; onFocus/onBlur as focusin/focusout; parents' props for non-bubbling events left out) are listed with source and location (framework: "React", reactProp: "onClick"; at most 50 of the requested types, reactHandlersSkipped counts the rest). jQuery handlers are shown instead of jQuery's dispatcher (framework: "jQuery", delegateSelector for delegates the element matches; a dispatcher with no handler for the element is omitted; at most 50 are resolved, jqueryHandlersSkipped counts the rest). Empty handlers (React's onclick placeholder) are marked noop and do not count as the element's own handler. The Debugger domain is not enabled`,
9641
+ automaticBehavior: `Listeners are grouped by type, the element's own handlers first (types handled on the element before delegated ones; nearest first, no-ops last); JSON line/column numbers are 0-based (human output shows them 1-based like DevTools). React's on\u2026 props that run for the element (its own and its React parents', through portals and from a nested root into the outer root; onFocus/onBlur as focusin/focusout; parents' props for non-bubbling events left out) are listed with source and location (framework: "React", reactProp: "onClick"; at most 50 of the requested types, reactHandlersSkipped counts the rest). jQuery handlers are shown instead of jQuery's dispatcher (framework: "jQuery", delegateSelector for delegates the element matches; a dispatcher with no handler for the element is omitted; at most 50 are resolved, jqueryHandlersSkipped counts the rest). Preact's event proxy is replaced by the handler Preact runs (framework: "Preact"). Empty handlers (React's onclick placeholder) are marked noop and do not count as the element's own handler. The Debugger domain is not enabled`,
9367
9642
  tokenImpact: "Framework roots are already collapsed; --type keeps the output short on pages with many listeners"
9368
9643
  },
9369
9644
  "listeners:--all": {
@@ -9377,6 +9652,35 @@ var OPTION_BEHAVIORS = {
9377
9652
  automaticBehavior: 'Coordinates are CSS px: bounds relative to the top-level page (iframe offsets and page scroll included), viewport relative to the visible area. Iframes and overflow containers (scroll lists, overflow: hidden) clip what counts as visible (clippedBy names the one cutting it off). scrollBy brings the whole element into view and is limited to how far the page can scroll: for an element out of view it centres it (aligns its start when it is larger than the viewport; human output says "to centre it"), for a partly visible one it is the smallest scroll that shows all of it (the part cut off at the top or bottom; the start of one larger than the viewport; "partly visible (87%); scroll up 5px to see all of it"). Elements a page script moves on scroll (floating menus) may move again after it; fixed and sticky elements (page scroll does not move them, or only until they stick) and ones beyond that range get offScreenReason instead, which says "page scrolling is locked (\u2026)" when the page cannot scroll because body/html is position: fixed or overflow: hidden, so in-flow content is not called fixed; a visible dialog (dialog[open], [aria-modal=true], [role=dialog|alertdialog]) is named as the likely cause ("likely by dialog div#consent"). page.viewport is the layout viewport without scrollbars, as dom scroll reports it; page.colorScheme is the prefers-color-scheme media feature the page sees (not the theme it renders). Content in a closed <details> or under content-visibility: hidden is hidden. coveredBy is the topmost element at the center of the largest visible box (none for pointer-events: none, nor for an element of the same click target: an overlay inside the link, button or label the element is in, a link to the same URL, or the textless absolutely positioned overlay link spanning the card that holds plain content); inert elements are flagged, not hidden',
9378
9653
  tokenImpact: 'About one line per element; a cheap alternative to screenshots for "where is it?"'
9379
9654
  },
9655
+ "inspect:--index": {
9656
+ default: "Inspects the first rendered match (the first when none is rendered) and notes how many matched; a numeric argument inspects that cached element (from dom query, dom form or dom a11y query)",
9657
+ whenEnabled: "Inspects the nth match (0-based); out of range exits 81",
9658
+ automaticBehavior: `Answers "what does it look like" without a screenshot, grouped like Figma Dev Mode: header (element, text, size and page position, [flex]/[grid], [not rendered]/[hidden]/[offscreen]/[covered by \u2026], prefers-color-scheme), box (margin, padding, border widths, box-sizing, overflow, scroll size), layout (display, position, flex/grid container and item settings), parent (its display and layout, distances to its content edges, gaps to the neighbouring siblings), text (first font family \u2192 the font Chrome rendered, (webfont) or local; weight size/line-height; color; WCAG contrast against the composited background; only for elements with text), fill, border (sides, radius, outline), fx (shadow, transform, filter, opacity, blend), state (cursor, pointer-events, user-select, appearance), pseudo (::before/::after with content, ::placeholder) and a child tree (depth 2, 20 rows, identical siblings grouped). Values that change nothing (0, none, transparent, normal) are left out; colors are hex (lab/oklch from Tailwind converted), lengths px without the unit, rounded to 0.1. Secrets are never shown. JSON uses Figma-aligned names (rect, box, layout.sizing hug/fill/fixed, text, fills, strokes, radius, effects, children). Also by default: hints, the element's own declarations that have no effect (justify-content on a block, width on an inline element, top on a static one, var() of an unset custom property) with the reason, the fix and the rule's file:line`,
9659
+ tokenImpact: "About 60\u201380 tokens for the styles and 30\u201370 more for the child tree (--tree 0 drops it), against about 1,500 for a screenshot or 3,000+ for raw computed styles"
9660
+ },
9661
+ "inspect:--all": {
9662
+ default: "Shows the curated groups (the properties that define the look)",
9663
+ whenEnabled: "Lists every computed property that differs from the default of the same element type, longhands collapsed into shorthands, noise (logical duplicates, currentColor echoes, custom properties) dropped",
9664
+ tokenImpact: "About 80 tokens instead of 60\u201380"
9665
+ },
9666
+ "inspect:--props": {
9667
+ default: "Shows the curated groups",
9668
+ whenEnabled: 'Shows only the named properties (custom properties like --brand included, "(not set)" when no rule sets one; --* lists every custom property the element has, --bs-btn-* those with a prefix), each computed and normalized; an unknown name exits 81 with a suggestion'
9669
+ },
9670
+ "inspect:--rules": {
9671
+ default: "Shows the values, not where they come from",
9672
+ whenEnabled: "Adds a rules group: for each shown property the page's CSS sets, the value as written (with the computed value when it uses var()), selector, file:line (column for minified files), @media/@container condition, cascade layer, how many ancestors up it is inherited from, and the rules it beats. Sides one declaration sets are one row; browser defaults are left out. With --props, only those properties",
9673
+ automaticBehavior: "The cascade is computed by bdg from CSS.getMatchedStylesForNode (origin, !important, style attribute, layers, specificity and order); reading it waits up to 5 s, then the output notes the cascade was not read",
9674
+ tokenImpact: "About 15\u201325 tokens per row, 5\u201320 rows"
9675
+ },
9676
+ "inspect:--why": {
9677
+ default: "Not shown",
9678
+ whenEnabled: "Adds why <property> = computed value, then every declaration of it on the element, highest precedence first: \u2713 the winner (or the inherited ancestor's), \u2717 the ones it beats, browser defaults included. Each rule shows its selector specificity [ids,classes,types]. var() values are shown substituted (or invalid: --x not set), with where the winner's custom properties are set, followed up to :root. Logical names map to physical ones (margin-inline-start \u2192 margin-left); a shorthand (padding, border) gives one answer when one declaration sets all its sides, else one per side"
9679
+ },
9680
+ "inspect:--no-hints": {
9681
+ default: "Hints at the element's own author declarations that have no effect (flex/grid properties without flex or grid, item properties without a flex or grid parent, offsets on static elements, sizes on inline ones, var() of an unset custom property, form controls in the browser's font), within a 1 s budget; hints none when nothing was found",
9682
+ whenEnabled: "Skips the hints and does not read the matched rules"
9683
+ },
9380
9684
  "scroll:--down": {
9381
9685
  whenEnabled: "Scrolls page down by specified pixel amount"
9382
9686
  },
@@ -9458,7 +9762,7 @@ var OPTION_BEHAVIORS = {
9458
9762
  "bdg:--viewport": {
9459
9763
  default: "A launched Chrome opens a 1920x1080 window (the viewport is smaller by the scrollbar, and in a visible window by the browser UI); an attached Chrome keeps its window",
9460
9764
  whenEnabled: "The page gets exactly that viewport (CSS px, e.g. 1280x800) for the whole session, through navigations and reloads (Emulation.setDeviceMetricsOverride at the display pixel ratio); a launched Chrome also opens its window at that size, so tabs the page opens get it too",
9461
- automaticBehavior: "Works with --chrome-ws-url: the override belongs to the session, and Chrome drops it when the session ends, so the attached browser gets its own size back. bdg status shows the resulting layout viewport without the scrollbar (Viewport: 1265\xD7800 (--viewport 1280x800)). Invalid sizes (not WxH, a side outside 1-10000) exit 81"
9765
+ automaticBehavior: "Works with --chrome-ws-url: the override belongs to the session, and Chrome drops it when the session ends, so the attached browser gets its own size back. bdg status shows the resulting layout viewport without the scrollbar (Viewport: 1265\xD7800 (emulated 1280x800)). Invalid sizes (not WxH, a side outside 1-10000) exit 81"
9462
9766
  },
9463
9767
  "bdg:--color-scheme": {
9464
9768
  default: "The page sees the system setting for prefers-color-scheme (headless Chrome follows the OS, so a dark OS renders dark pages); bdg status and dom layout show which one",
@@ -10113,6 +10417,14 @@ init_logging();
10113
10417
  function delay2(ms) {
10114
10418
  return new Promise((resolve3) => setTimeout(resolve3, ms));
10115
10419
  }
10420
+ async function waitUntil(condition, timeoutMs, pollMs = 25) {
10421
+ const deadline = Date.now() + timeoutMs;
10422
+ while (!condition()) {
10423
+ if (Date.now() >= deadline) return false;
10424
+ await delay2(Math.min(pollMs, Math.max(0, deadline - Date.now())));
10425
+ }
10426
+ return true;
10427
+ }
10116
10428
 
10117
10429
  // src/utils/file.ts
10118
10430
  init_logging();
@@ -10892,6 +11204,12 @@ function showBothSectionsWhenBothRequested(options) {
10892
11204
  options.console = false;
10893
11205
  }
10894
11206
  }
11207
+ var SELECTOR_SCOPE = "searches open shadow roots and same-origin iframes";
11208
+ var SELECTOR_OR_INDEX_ARGUMENT = `CSS selector (${SELECTOR_SCOPE}) or numeric index from query results (0-based)`;
11209
+ var SELECTOR_SCOPE_HELP = `
11210
+ Selectors search the page, open shadow roots and same-origin iframes (nested ones
11211
+ included). They cannot reach into closed shadow roots or cross-origin iframes:
11212
+ use bdg dom eval --frame <frame> for those (see bdg dom frames).`;
10895
11213
 
10896
11214
  // src/commands/cdp.ts
10897
11215
  init_errors3();
@@ -10919,9 +11237,11 @@ var COMMANDS = {
10919
11237
  dom_press_key: defineCommand(),
10920
11238
  dom_scroll: defineCommand(),
10921
11239
  page_navigate: defineCommand(),
11240
+ page_emulate: defineCommand(),
10922
11241
  dom_form_discover: defineCommand(),
10923
11242
  dom_listeners: defineCommand(),
10924
11243
  dom_layout: defineCommand(),
11244
+ dom_inspect: defineCommand(),
10925
11245
  dom_wait: defineCommand()
10926
11246
  };
10927
11247
 
@@ -11940,6 +12260,15 @@ function eventTypesOption(value, previous = []) {
11940
12260
  if (types.length > 0) return [.../* @__PURE__ */ new Set([...previous, ...types])];
11941
12261
  throw new InvalidArgumentError("Give at least one event type, e.g. click or click,keydown.");
11942
12262
  }
12263
+ function cssPropertiesOption(value, previous = []) {
12264
+ const names = parseCommaSeparated(value).map(
12265
+ (name) => name.startsWith("--") ? name : name.toLowerCase()
12266
+ );
12267
+ if (names.length > 0) return [.../* @__PURE__ */ new Set([...previous, ...names])];
12268
+ throw new InvalidArgumentError(
12269
+ "Give at least one CSS property, e.g. color or padding-top,color."
12270
+ );
12271
+ }
11943
12272
  function parseCommaSeparated(value) {
11944
12273
  return value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
11945
12274
  }
@@ -12607,10 +12936,71 @@ function formatRequestStatus(request) {
12607
12936
  return request.errorText && request.errorText !== BODY_ABORTED ? `${request.status} (${request.errorText})` : String(request.status);
12608
12937
  }
12609
12938
 
12939
+ // src/ui/formatters/networkHeaders.ts
12940
+ init_formatting();
12941
+
12942
+ // src/ui/messages/networkMessages.ts
12943
+ function followingNetworkMessage() {
12944
+ return "Following network requests... (Ctrl+C to stop)";
12945
+ }
12946
+ function stoppedFollowingNetworkMessage() {
12947
+ return "\nStopped following network requests.";
12948
+ }
12949
+ function headerRepeatedNote(count) {
12950
+ return `(sent ${count} times)`;
12951
+ }
12952
+ function localProxyNote() {
12953
+ return "(loopback; likely a local proxy)";
12954
+ }
12955
+
12956
+ // src/ui/formatters/networkHeaders.ts
12957
+ function formatNetworkHeaders(data) {
12958
+ const fmt = new OutputFormatter();
12959
+ fmt.text("Network Request Headers").separator("\u2501", 60).blank();
12960
+ fmt.text("URL:").text(` ${data.url}`).blank();
12961
+ fmt.text(`Status: ${requestStatusLine(data)}`).blank();
12962
+ if (Object.keys(data.responseHeaders).length > 0) {
12963
+ fmt.text("Response Headers:");
12964
+ formatHeaderSection(fmt, data.responseHeaders);
12965
+ fmt.blank();
12966
+ }
12967
+ if (Object.keys(data.requestHeaders).length > 0) {
12968
+ fmt.text("Request Headers:");
12969
+ formatHeaderSection(fmt, data.requestHeaders);
12970
+ fmt.blank();
12971
+ }
12972
+ if (Object.keys(data.responseHeaders).length === 0 && Object.keys(data.requestHeaders).length === 0) {
12973
+ fmt.text("No headers found").blank();
12974
+ }
12975
+ fmt.text(`Request ID: ${data.requestId}`);
12976
+ return fmt.build();
12977
+ }
12978
+ function headerValueLines(name, value) {
12979
+ if (name.toLowerCase() === "set-cookie") return value.split("\n");
12980
+ const counts = /* @__PURE__ */ new Map();
12981
+ for (const line of value.split("\n")) counts.set(line, (counts.get(line) ?? 0) + 1);
12982
+ return [...counts].map(
12983
+ ([line, count]) => count > 1 ? `${line} ${headerRepeatedNote(count)}` : line
12984
+ );
12985
+ }
12986
+ function formatHeaderSection(fmt, headers) {
12987
+ const entries = Object.entries(headers).sort(([a], [b]) => a.localeCompare(b));
12988
+ const keyWidth = Math.max(...entries.map(([k]) => k.length)) + 4;
12989
+ entries.forEach(([key, value]) => {
12990
+ headerValueLines(key, value).forEach((line) => fmt.keyValue(` ${key}`, line, keyWidth));
12991
+ });
12992
+ }
12993
+ function requestStatusLine(data) {
12994
+ const status = formatRequestStatus(data);
12995
+ const text = data.statusText && /^\d+$/.test(status) ? ` ${data.statusText}` : "";
12996
+ return [data.method, `${status}${text}`].filter(Boolean).join(" ");
12997
+ }
12998
+
12610
12999
  // src/ui/formatters/details.ts
12611
13000
  init_formatting();
12612
13001
  init_sessionCommand();
12613
13002
  init_strings();
13003
+ init_url();
12614
13004
  var MESSAGE_PREVIEW_LENGTH = 200;
12615
13005
  var BINARY_OPCODE = 2;
12616
13006
  function formatWebSocketMessage(frame) {
@@ -12652,6 +13042,34 @@ function formatBytes(bytes) {
12652
13042
  if (bytes < 1024) return `${bytes} B`;
12653
13043
  return bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1024 / 1024).toFixed(1)} MB`;
12654
13044
  }
13045
+ function isLoopback(ip) {
13046
+ const bare = ip.replace(/^\[|\]$/g, "").toLowerCase();
13047
+ return /^(::ffff:)?127\./.test(bare) || bare === "::1";
13048
+ }
13049
+ var DEFAULT_PORTS = {
13050
+ "http:": "80",
13051
+ "https:": "443",
13052
+ "ws:": "80",
13053
+ "wss:": "443"
13054
+ };
13055
+ function looksLikeLocalProxy(request) {
13056
+ const url = safeParseUrl(request.url);
13057
+ if (!url || request.serverPort === void 0 || !isLoopback(request.serverIPAddress ?? "")) {
13058
+ return false;
13059
+ }
13060
+ const hostname = url.hostname.toLowerCase();
13061
+ if (hostname === "localhost" || hostname.endsWith(".localhost") || isLoopback(hostname)) {
13062
+ return false;
13063
+ }
13064
+ const urlPort = url.port || DEFAULT_PORTS[url.protocol];
13065
+ return urlPort !== void 0 && String(request.serverPort) !== urlPort;
13066
+ }
13067
+ function remoteAddress(request) {
13068
+ const ip = request.serverIPAddress ?? "";
13069
+ const host = ip.includes(":") && !ip.startsWith("[") ? `[${ip}]` : ip;
13070
+ const address = request.serverPort ? `${host}:${request.serverPort}` : host;
13071
+ return looksLikeLocalProxy(request) ? `${address} ${localProxyNote()}` : address;
13072
+ }
12655
13073
  function requestSummaryRows(request) {
12656
13074
  const rows = [
12657
13075
  ["Request ID", request.requestId],
@@ -12670,13 +13088,15 @@ function requestSummaryRows(request) {
12670
13088
  rows.push(["Size", sizes.join(", ")]);
12671
13089
  }
12672
13090
  if (request.fromCache) rows.push(["From Cache", "yes"]);
12673
- if (request.serverIPAddress) rows.push(["Remote Address", request.serverIPAddress]);
13091
+ if (request.serverIPAddress) rows.push(["Remote Address", remoteAddress(request)]);
12674
13092
  if (request.blockedReason) rows.push(["Blocked", request.blockedReason]);
12675
13093
  return rows;
12676
13094
  }
12677
13095
  function addHeaders(fmt, title, headers) {
12678
13096
  fmt.text(title).separator("\u2501", 70);
12679
- Object.entries(headers).forEach(([key, value]) => fmt.text(` ${key}: ${value}`));
13097
+ Object.entries(headers).forEach(
13098
+ ([key, value]) => headerValueLines(key, value).forEach((line) => fmt.text(` ${key}: ${line}`))
13099
+ );
12680
13100
  fmt.blank();
12681
13101
  }
12682
13102
  function describeResponseBody(request) {
@@ -13065,7 +13485,12 @@ async function withNotFoundContext(failure, selector, searched) {
13065
13485
  const context = await noMatchContext(selector);
13066
13486
  const note = searched ? unreachableElementsNote(selector, context.unsearched) : "";
13067
13487
  const suggestion = withLoadingHint(
13068
- joinLines(context.similar, failure.errorContext?.suggestion, note ? note : void 0),
13488
+ joinLines(
13489
+ context.similar,
13490
+ context.shadowHost && shadowBoundaryLine(context.shadowHost),
13491
+ failure.errorContext?.suggestion,
13492
+ note ? note : void 0
13493
+ ),
13069
13494
  context.readyState,
13070
13495
  selector
13071
13496
  );
@@ -13211,6 +13636,7 @@ init_formatting();
13211
13636
  init_commands();
13212
13637
  init_sessionCommand();
13213
13638
  init_exitCodes();
13639
+ var STRICT_OPTION_HELP = "Fail (exit 90) instead of using DOM events when a real mouse cannot reach the element (covered, hidden, zero-size)";
13214
13640
  function modifiersOption(value) {
13215
13641
  const [unknown] = findUnknownModifiers(value);
13216
13642
  if (unknown !== void 0) {
@@ -13224,7 +13650,7 @@ function registerFormInteractionCommands(program2) {
13224
13650
  const err = internalError("DOM command group not found");
13225
13651
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.SOFTWARE_ERROR);
13226
13652
  }
13227
- domCommand.command("fill").description("Fill a form field with a value (React-compatible, waits for stability)").argument("<selectorOrIndex>", "CSS selector or numeric index from query results (0-based)").argument("<value>", 'Value to fill (file inputs: paths separated by commas, "" clears)').option("--index <n>", "Element index if selector matches multiple (0-based)", integerOption(0)).option("--no-blur", "Do not blur after filling (keeps focus on element)").option("--no-wait", "Skip waiting for network stability after fill").addOption(jsonOption()).action(async (selectorOrIndex, value, options) => {
13653
+ domCommand.command("fill").description("Fill a form field with a value (React-compatible, waits for stability)").argument("<selectorOrIndex>", SELECTOR_OR_INDEX_ARGUMENT).argument("<value>", 'Value to fill (file inputs: paths separated by commas, "" clears)').option("--index <n>", "Element index if selector matches multiple (0-based)", integerOption(0)).option("--no-blur", "Do not blur after filling (keeps focus on element)").option("--no-wait", "Skip waiting for network stability after fill").addOption(jsonOption()).action(async (selectorOrIndex, value, options) => {
13228
13654
  await runCommand(
13229
13655
  () => runElementCommand({
13230
13656
  selectorOrIndex,
@@ -13245,14 +13671,14 @@ function registerFormInteractionCommands(program2) {
13245
13671
  formatFillOutput
13246
13672
  );
13247
13673
  });
13248
- domCommand.command("click").description("Click an element and wait for stability (accepts selector or index)").argument("<selectorOrIndex>", "CSS selector or numeric index from query results (0-based)").option("--index <n>", "Element index if selector matches multiple (0-based)", integerOption(0)).option("--double", "Double-click").option("--right", "Right-click (opens the context menu)").option("--no-wait", "Skip waiting for network stability after click").addOption(jsonOption()).addHelpText("after", CLICK_RESULT_WAIT_HELP).action(async (selectorOrIndex, options) => {
13674
+ domCommand.command("click").description("Click an element and wait for stability (accepts selector or index)").argument("<selectorOrIndex>", SELECTOR_OR_INDEX_ARGUMENT).option("--index <n>", "Element index if selector matches multiple (0-based)", integerOption(0)).option("--double", "Double-click").option("--right", "Right-click (opens the context menu)").option("--strict", STRICT_OPTION_HELP).option("--no-wait", "Skip waiting for network stability after click").addOption(jsonOption()).addHelpText("after", CLICK_RESULT_WAIT_HELP).action(async (selectorOrIndex, options) => {
13249
13675
  const action = options.double ? "double" : options.right ? "right" : "click";
13250
13676
  await runPointerCommand(selectorOrIndex, options, action);
13251
13677
  });
13252
- domCommand.command("hover").description("Move the mouse over an element (shows hover menus and tooltips)").argument("<selectorOrIndex>", "CSS selector or numeric index from query results (0-based)").option("--index <n>", "Element index if selector matches multiple (0-based)", integerOption(0)).option("--no-wait", "Skip waiting for network stability after hovering").addOption(jsonOption()).action(async (selectorOrIndex, options) => {
13678
+ domCommand.command("hover").description("Move the mouse over an element (shows hover menus and tooltips)").argument("<selectorOrIndex>", SELECTOR_OR_INDEX_ARGUMENT).option("--index <n>", "Element index if selector matches multiple (0-based)", integerOption(0)).option("--strict", STRICT_OPTION_HELP).option("--no-wait", "Skip waiting for network stability after hovering").addOption(jsonOption()).action(async (selectorOrIndex, options) => {
13253
13679
  await runPointerCommand(selectorOrIndex, options, "hover");
13254
13680
  });
13255
- domCommand.command("submit").description("Submit a form by clicking submit button and waiting for completion").argument("<selectorOrIndex>", "CSS selector or numeric index from query results (0-based)").option("--index <n>", "Element index if selector matches multiple (0-based)", integerOption(0)).option("--wait-navigation", "Wait for page navigation after submit").option(
13681
+ domCommand.command("submit").description("Submit a form by clicking submit button and waiting for completion").argument("<selectorOrIndex>", SELECTOR_OR_INDEX_ARGUMENT).option("--index <n>", "Element index if selector matches multiple (0-based)", integerOption(0)).option("--wait-navigation", "Wait for page navigation after submit").option(
13256
13682
  "--wait-network <ms>",
13257
13683
  "Wait for network idle after submit (milliseconds)",
13258
13684
  integerOption(0),
@@ -13279,7 +13705,7 @@ function registerFormInteractionCommands(program2) {
13279
13705
  formatSubmitOutput
13280
13706
  );
13281
13707
  });
13282
- domCommand.command("pressKey").description("Press a key on an element (for Enter-to-submit, keyboard navigation)").argument("<selectorOrIndex>", "CSS selector or numeric index from query results (0-based)").argument("<key>", "Key to press (Enter, Tab, Escape, Space, ArrowUp, etc.)").option("--index <n>", "Element index if selector matches multiple (0-based)", integerOption(0)).option("--times <n>", "Press key multiple times (default: 1)", integerOption(1, 1e3)).option(
13708
+ domCommand.command("pressKey").description("Press a key on an element (for Enter-to-submit, keyboard navigation)").argument("<selectorOrIndex>", SELECTOR_OR_INDEX_ARGUMENT).argument("<key>", "Key to press (Enter, Tab, Escape, Space, ArrowUp, etc.)").option("--index <n>", "Element index if selector matches multiple (0-based)", integerOption(0)).option("--times <n>", "Press key multiple times (default: 1)", integerOption(1, 1e3)).option(
13283
13709
  "--modifiers <mods>",
13284
13710
  "Modifier keys: shift,ctrl,alt,meta (comma-separated; aliases cmd, control, option)",
13285
13711
  modifiersOption
@@ -13381,7 +13807,8 @@ async function runPointerCommand(selectorOrIndex, options, action) {
13381
13807
  buildRequest: (target) => ({
13382
13808
  ...target,
13383
13809
  wait: options.wait !== false,
13384
- ...action !== "click" && { action }
13810
+ ...action !== "click" && { action },
13811
+ ...options.strict && { strict: true }
13385
13812
  }),
13386
13813
  call: domClick,
13387
13814
  command: action === "hover" ? "hover" : "click",
@@ -13392,18 +13819,26 @@ async function runPointerCommand(selectorOrIndex, options, action) {
13392
13819
  formatClickOutput
13393
13820
  );
13394
13821
  }
13395
- function formatActionOutput(done, details, result, keyWidth = 15) {
13822
+ function formatActionOutput(done, details, result, options = {}) {
13823
+ const keyWidth = options.keyWidth ?? 15;
13396
13824
  const fmt = new OutputFormatter();
13397
- fmt.text(actionStatusLine(done, result.warning !== void 0, result.effect === "none"));
13825
+ const stillChanging = result.settled === false && result.pending !== void 0;
13826
+ fmt.text(
13827
+ actionStatusLine(done, {
13828
+ warned: result.warning !== void 0,
13829
+ noEffect: result.effect === "none",
13830
+ stillChanging
13831
+ })
13832
+ );
13398
13833
  if (result.warning) fmt.text(`\u26A0 Warning: ${result.warning}`);
13834
+ if (stillChanging && result.pending) {
13835
+ fmt.text(`\u26A0 ${stillChangingNote(options.action ?? "action", result.pending)}`);
13836
+ }
13399
13837
  fmt.blank();
13400
13838
  fmt.keyValueList(details, keyWidth);
13401
13839
  if (result.navigation) fmt.keyValue("Page", pageNavigationText(result.navigation), keyWidth);
13402
- (result.messages ?? []).forEach((message, index) => {
13403
- const text = newMessageText(message);
13404
- if (index === 0) fmt.keyValue("New text", text, keyWidth);
13405
- else fmt.text(" ".repeat(keyWidth) + text);
13406
- });
13840
+ listRows(fmt, "New text", (result.messages ?? []).map(newMessageText), keyWidth);
13841
+ listRows(fmt, "Shown", (result.shown ?? []).map(shownElementText), keyWidth);
13407
13842
  const omitted = result.triggeredRequestsOmitted;
13408
13843
  const requests = formatTriggeredRequestLines(result.triggeredRequests ?? [], omitted);
13409
13844
  if (requests.length > 0) {
@@ -13415,6 +13850,12 @@ function formatActionOutput(done, details, result, keyWidth = 15) {
13415
13850
  }
13416
13851
  return fmt;
13417
13852
  }
13853
+ function listRows(fmt, label, texts, keyWidth) {
13854
+ texts.forEach((text, index) => {
13855
+ if (index === 0) fmt.keyValue(label, text, keyWidth);
13856
+ else fmt.text(" ".repeat(keyWidth) + text);
13857
+ });
13858
+ }
13418
13859
  function elementRow(result) {
13419
13860
  const source = result.indexSource ? ` (${indexSourceText(result.indexSource)})` : "";
13420
13861
  if (result.element === void 0) {
@@ -13445,7 +13886,8 @@ function formatClickOutput(result) {
13445
13886
  elementRow(result),
13446
13887
  ["Method", result.method === "dom" ? "DOM events" : "mouse events"]
13447
13888
  ],
13448
- result
13889
+ result,
13890
+ { action: POINTER_ACTION_NOUN[result.action ?? "click"] }
13449
13891
  ).build();
13450
13892
  }
13451
13893
  function formatSubmitOutput(result) {
@@ -13459,7 +13901,10 @@ function formatSubmitOutput(result) {
13459
13901
  if (result.navigationOccurred === false) details.push(["Navigation", "no"]);
13460
13902
  if (result.navigationOccurred && !result.navigation) details.push(["Navigation", "yes"]);
13461
13903
  if (result.waitTimeMs !== void 0) details.push(["Wait Time", `${result.waitTimeMs}ms`]);
13462
- const fmt = formatActionOutput("Form Submitted", details, result, 20);
13904
+ const fmt = formatActionOutput("Form Submitted", details, result, {
13905
+ keyWidth: 20,
13906
+ action: "submit"
13907
+ });
13463
13908
  fmt.hints("Next steps:", [
13464
13909
  `${sessionCommand("bdg network list --last 10").padEnd(32)} Check network requests`,
13465
13910
  `${sessionCommand("bdg console --last 5").padEnd(32)} Check console messages`,
@@ -13475,7 +13920,7 @@ function formatPressKeyOutput(result) {
13475
13920
  ];
13476
13921
  if (result.times && result.times > 1) details.push(["Times", result.times.toString()]);
13477
13922
  if (result.modifiers?.length) details.push(["Modifiers", result.modifiers.join("+")]);
13478
- return formatActionOutput("Key Pressed", details, result).build();
13923
+ return formatActionOutput("Key Pressed", details, result, { action: "key press" }).build();
13479
13924
  }
13480
13925
  function formatScrollOutput(result) {
13481
13926
  const details = [["Scroll Type", result.scrollType]];
@@ -13494,38 +13939,247 @@ function formatScrollOutput(result) {
13494
13939
 
13495
13940
  // src/commands/dom/a11y.ts
13496
13941
  init_helpers();
13497
- init_errors3();
13498
- init_messages();
13499
13942
 
13500
- // src/telemetry/a11y.ts
13501
- init_errors3();
13502
- init_messages();
13503
- init_client();
13943
+ // src/commands/dom/semanticUtils.ts
13944
+ init_elementInfo();
13504
13945
 
13505
- // src/runtime/dom/frameLayout.ts
13506
- init_frameScopedConnection();
13507
- init_logging();
13508
- init_errors();
13509
- var log19 = createLogger("dom");
13510
- function childFrameIds(tree) {
13511
- return (tree?.childFrames ?? []).flatMap((child) => [child.frame.id, ...childFrameIds(child)]);
13946
+ // src/telemetry/roleInference.ts
13947
+ var IMPLICIT_ARIA_ROLES = {
13948
+ // Interactive elements
13949
+ a: "link",
13950
+ button: "button",
13951
+ input: "textbox",
13952
+ select: "combobox",
13953
+ textarea: "textbox",
13954
+ option: "option",
13955
+ // Headings
13956
+ h1: "heading",
13957
+ h2: "heading",
13958
+ h3: "heading",
13959
+ h4: "heading",
13960
+ h5: "heading",
13961
+ h6: "heading",
13962
+ // Landmarks
13963
+ nav: "navigation",
13964
+ main: "main",
13965
+ header: "banner",
13966
+ footer: "contentinfo",
13967
+ aside: "complementary",
13968
+ section: "region",
13969
+ article: "article",
13970
+ form: "form",
13971
+ search: "search",
13972
+ // Media
13973
+ img: "image",
13974
+ figure: "figure",
13975
+ figcaption: "caption",
13976
+ video: "video",
13977
+ audio: "audio",
13978
+ // Lists
13979
+ ul: "list",
13980
+ ol: "list",
13981
+ li: "listitem",
13982
+ dl: "list",
13983
+ dt: "term",
13984
+ dd: "definition",
13985
+ menu: "menu",
13986
+ menuitem: "menuitem",
13987
+ // Tables
13988
+ table: "table",
13989
+ thead: "rowgroup",
13990
+ tbody: "rowgroup",
13991
+ tfoot: "rowgroup",
13992
+ tr: "row",
13993
+ th: "columnheader",
13994
+ td: "cell",
13995
+ // Other semantic elements
13996
+ dialog: "dialog",
13997
+ details: "group",
13998
+ summary: "button",
13999
+ progress: "progressbar",
14000
+ meter: "meter",
14001
+ output: "status",
14002
+ address: "group",
14003
+ blockquote: "blockquote",
14004
+ code: "code",
14005
+ pre: "group",
14006
+ hr: "separator"
14007
+ };
14008
+ var DEFAULT_ROLE = "generic";
14009
+ var MAX_NAME_LENGTH = 100;
14010
+ function inferRoleFromTag(tag) {
14011
+ const normalizedTag = tag.toLowerCase();
14012
+ return IMPLICIT_ARIA_ROLES[normalizedTag] ?? DEFAULT_ROLE;
13512
14013
  }
13513
-
13514
- // src/telemetry/a11y.ts
13515
- init_exitCodes();
13516
- init_levenshtein();
13517
- function buildTreeFromRawNodes(rawNodes) {
13518
- const rawById = new Map(rawNodes.map((rawNode) => [rawNode.nodeId, rawNode]));
13519
- const rootRaw = rawNodes.find((rawNode) => !rawNode.ignored);
13520
- if (!rootRaw) {
13521
- throw new CommandError(
13522
- "No root node found in accessibility tree",
13523
- { suggestion: "The page may not be fully loaded. Wait and retry." },
13524
- EXIT_CODES.SOFTWARE_ERROR
13525
- );
14014
+ function extractHeadingLevel(tag) {
14015
+ const match = tag.toLowerCase().match(/^h([1-6])$/);
14016
+ const level = match?.[1];
14017
+ if (!level) {
14018
+ return null;
13526
14019
  }
13527
- const nodes = /* @__PURE__ */ new Map();
13528
- const visit = (rawNode) => {
14020
+ return parseInt(level, 10);
14021
+ }
14022
+ function synthesizeA11yNode(domContext, nodeId) {
14023
+ const role = inferRoleFromTag(domContext.tag);
14024
+ const node = {
14025
+ nodeId: String(nodeId),
14026
+ role,
14027
+ inferred: true,
14028
+ backendDOMNodeId: nodeId
14029
+ };
14030
+ if (domContext.preview) {
14031
+ const name = domContext.preview.length > MAX_NAME_LENGTH ? domContext.preview.slice(0, MAX_NAME_LENGTH - 3) + "..." : domContext.preview;
14032
+ node.name = name;
14033
+ }
14034
+ const headingLevel = extractHeadingLevel(domContext.tag);
14035
+ if (headingLevel !== null) {
14036
+ node.properties = { level: headingLevel };
14037
+ }
14038
+ return node;
14039
+ }
14040
+
14041
+ // src/ui/formatters/keyAttributes.ts
14042
+ init_formatting();
14043
+ init_url();
14044
+ var MAX_VALUE_LENGTH = 40;
14045
+ function fileName(src) {
14046
+ if (/^(data|blob):/i.test(src)) return src;
14047
+ const path6 = src.split(/[?#]/)[0] ?? src;
14048
+ const name = path6.substring(path6.lastIndexOf("/") + 1);
14049
+ return name && name !== path6 ? `\u2026/${name}` : src;
14050
+ }
14051
+ function hostOf(src) {
14052
+ const parsed = /^https?:/i.test(src) ? safeParseUrl(src) : null;
14053
+ if (!parsed) return src;
14054
+ const more = parsed.pathname.length > 1 || parsed.search ? "/\u2026" : "";
14055
+ return `//${parsed.host}${more}`;
14056
+ }
14057
+ function shortUrl(url) {
14058
+ return /^https?:/i.test(url) ? `//${truncateUrl(url, MAX_VALUE_LENGTH - 2)}` : url;
14059
+ }
14060
+ function shortValue(tag, name, value) {
14061
+ let short = value;
14062
+ if (name === "src") short = tag === "iframe" ? hostOf(value) : fileName(value);
14063
+ else if (name === "href" || name === "action") short = shortUrl(value);
14064
+ return cutMiddle(escapeControlChars(short.replace(/\s+/g, " ").trim()), MAX_VALUE_LENGTH);
14065
+ }
14066
+ function keyAttributeItems(tag, attributes, leaveOut = /* @__PURE__ */ new Set()) {
14067
+ return Object.entries(attributes ?? {}).flatMap(([name, value]) => {
14068
+ if (leaveOut.has(name) || value === false) return [];
14069
+ if (value === true) return [name];
14070
+ return [`${name}="${shortValue(tag, name, value)}"`];
14071
+ });
14072
+ }
14073
+
14074
+ // src/commands/dom/semanticUtils.ts
14075
+ init_formatting();
14076
+ init_commands();
14077
+ function capitalize(str) {
14078
+ return str.charAt(0).toUpperCase() + str.slice(1);
14079
+ }
14080
+ function buildRoleText(node) {
14081
+ if (node.role.toLowerCase() === "heading" && node.properties?.["level"] !== void 0) {
14082
+ const level = node.properties["level"];
14083
+ const levelNum = typeof level === "number" ? level : Number(level);
14084
+ if (!isNaN(levelNum)) {
14085
+ return `[Heading L${levelNum}]`;
14086
+ }
14087
+ }
14088
+ return `[${capitalize(node.role)}]`;
14089
+ }
14090
+ function buildContextText(node, domContext) {
14091
+ if (node.name) {
14092
+ return ` "${node.name}"`;
14093
+ }
14094
+ if (domContext) {
14095
+ const tagPart = `<${domContext.tag}`;
14096
+ const classPart = domContext.classes && domContext.classes.length > 0 ? `.${domContext.classes.slice(0, 3).join(".")}` : "";
14097
+ const previewPart = domContext.preview && !domContext.text ? ` "${domContext.preview}"` : "";
14098
+ return ` ${tagPart}${classPart}>${previewPart}`;
14099
+ }
14100
+ return "";
14101
+ }
14102
+ function buildKeyAttributesText(node, domContext) {
14103
+ if (!domContext?.attributes) return "";
14104
+ const shown = Object.entries(domContext.attributes).filter(([, value]) => Boolean(node.name) && value === node.name).map(([name]) => name);
14105
+ if (node.value !== void 0 && node.value !== "") shown.push("value");
14106
+ const items = keyAttributeItems(domContext.tag, domContext.attributes, new Set(shown));
14107
+ return items.length > 0 ? ` ${items.join(" ")}` : "";
14108
+ }
14109
+ function buildPropertiesText(node) {
14110
+ const props = [];
14111
+ if (node.value !== void 0 && node.value !== "") props.push(`value: "${node.value}"`);
14112
+ const checked = node.properties?.["checked"];
14113
+ if (checked !== void 0) {
14114
+ props.push(checked === "mixed" ? "partly checked" : checked ? "checked" : "unchecked");
14115
+ }
14116
+ const expanded = node.properties?.["expanded"];
14117
+ if (expanded !== void 0) props.push(expanded ? "expanded" : "collapsed");
14118
+ if (node.focusable) props.push("focusable");
14119
+ if (node.focused) props.push("focused");
14120
+ if (node.disabled) props.push("disabled");
14121
+ if (node.required) props.push("required");
14122
+ return props.length > 0 ? ` (${props.join(", ")})` : "";
14123
+ }
14124
+ function formatSemanticNodeWithContext(data) {
14125
+ const { node, domContext } = data;
14126
+ const roleText = buildRoleText(node);
14127
+ const contextText = buildContextText(node, domContext);
14128
+ const keysText = buildKeyAttributesText(node, domContext);
14129
+ const propsText = buildPropertiesText(node);
14130
+ const inferredText = node.inferred ? " (inferred from DOM)" : "";
14131
+ const line = `${roleText}${contextText}${keysText}${propsText}${inferredText}`;
14132
+ if (domContext?.text) return joinLines(line, elementTextLine(domContext.text));
14133
+ if (domContext?.childCount !== void 0 && !node.name) {
14134
+ return joinLines(line, emptyElementLine(domContext.children ?? [], domContext.childCount));
14135
+ }
14136
+ return line;
14137
+ }
14138
+ function resolveNodeWithFallback(a11yNode, domContext, nodeId) {
14139
+ if (a11yNode) return withSecretMasked(a11yNode, domContext);
14140
+ if (domContext && nodeId) return synthesizeA11yNode(domContext, nodeId);
14141
+ return null;
14142
+ }
14143
+ function withSecretMasked(node, domContext) {
14144
+ if (!domContext?.sensitive || !node.value) return node;
14145
+ return { ...node, value: MASKED_VALUE };
14146
+ }
14147
+
14148
+ // src/commands/dom/a11y.ts
14149
+ init_errors3();
14150
+ init_messages();
14151
+
14152
+ // src/telemetry/a11y.ts
14153
+ init_errors3();
14154
+ init_messages();
14155
+ init_client();
14156
+ init_elementInfo();
14157
+
14158
+ // src/runtime/dom/frameLayout.ts
14159
+ init_frameScopedConnection();
14160
+ init_logging();
14161
+ init_errors();
14162
+ var log19 = createLogger("dom");
14163
+ function childFrameIds(tree) {
14164
+ return (tree?.childFrames ?? []).flatMap((child) => [child.frame.id, ...childFrameIds(child)]);
14165
+ }
14166
+
14167
+ // src/telemetry/a11y.ts
14168
+ init_concurrency();
14169
+ init_exitCodes();
14170
+ init_levenshtein();
14171
+ function buildTreeFromRawNodes(rawNodes) {
14172
+ const rawById = new Map(rawNodes.map((rawNode) => [rawNode.nodeId, rawNode]));
14173
+ const rootRaw = rawNodes.find((rawNode) => !rawNode.ignored);
14174
+ if (!rootRaw) {
14175
+ throw new CommandError(
14176
+ "No root node found in accessibility tree",
14177
+ { suggestion: "The page may not be fully loaded. Wait and retry." },
14178
+ EXIT_CODES.SOFTWARE_ERROR
14179
+ );
14180
+ }
14181
+ const nodes = /* @__PURE__ */ new Map();
14182
+ const visit = (rawNode) => {
13529
14183
  if (nodes.has(rawNode.nodeId)) return;
13530
14184
  const node = parseA11yNode(rawNode);
13531
14185
  const childIds = visibleChildIds(rawNode, rawById);
@@ -13562,11 +14216,60 @@ async function collectA11yTree() {
13562
14216
  EXIT_CODES.SOFTWARE_ERROR
13563
14217
  );
13564
14218
  }
13565
- return buildTreeFromRawNodes([...result.nodes, ...await collectFrameNodes(result.nodes)]);
14219
+ const tree = buildTreeFromRawNodes([
14220
+ ...result.nodes,
14221
+ ...await collectFrameNodes(result.nodes)
14222
+ ]);
14223
+ return maskSecretValues(tree, await sensitiveNodeIds(tree));
13566
14224
  } finally {
13567
14225
  await callCDP("Accessibility.disable", {});
13568
14226
  }
13569
14227
  }
14228
+ var SENSITIVE_CHECK_CONCURRENCY = 10;
14229
+ async function sensitiveNodeIds(tree) {
14230
+ const candidates = [...tree.nodes.values()].filter(
14231
+ (node) => node.value !== void 0 && node.backendDOMNodeId !== void 0
14232
+ );
14233
+ const limiter = new ConcurrencyLimiter(SENSITIVE_CHECK_CONCURRENCY);
14234
+ const checked = await Promise.all(
14235
+ candidates.map(
14236
+ (node) => limiter.run(
14237
+ async () => await isSensitiveField(node.backendDOMNodeId ?? 0) ? node.nodeId : null
14238
+ )
14239
+ )
14240
+ );
14241
+ return new Set(checked.filter((id) => id !== null));
14242
+ }
14243
+ async function isSensitiveField(backendNodeId) {
14244
+ const objectGroup = "bdg-a11y-secret";
14245
+ const resolved = await callCDP("DOM.resolveNode", { backendNodeId, objectGroup });
14246
+ const objectId = resolved.data?.result?.object?.objectId;
14247
+ if (!objectId) return true;
14248
+ try {
14249
+ const response = await callCDP("Runtime.callFunctionOn", {
14250
+ objectId,
14251
+ functionDeclaration: `function () { return this.nodeType !== 1 || (${SENSITIVE_FIELD_JS})(this); }`,
14252
+ returnByValue: true
14253
+ });
14254
+ const value = response.data?.result?.result?.value;
14255
+ return value !== false;
14256
+ } finally {
14257
+ await callCDP("Runtime.releaseObjectGroup", { objectGroup });
14258
+ }
14259
+ }
14260
+ function maskSecretValues(tree, secretIds) {
14261
+ const hide = (nodeId, inside) => {
14262
+ const node = tree.nodes.get(nodeId);
14263
+ if (!node) return;
14264
+ const { name: _name, value: _value, ...rest } = node;
14265
+ const masked = inside ? rest : { ...node, ...node.value !== void 0 && { value: MASKED_VALUE } };
14266
+ tree.nodes.set(nodeId, masked);
14267
+ if (tree.root.nodeId === nodeId) tree.root = masked;
14268
+ node.childIds?.forEach((childId) => hide(childId, true));
14269
+ };
14270
+ secretIds.forEach((nodeId) => hide(nodeId, false));
14271
+ return tree;
14272
+ }
13570
14273
  async function collectFrameNodes(pageNodes) {
13571
14274
  const tree = (await callCDP("Page.getFrameTree", {})).data?.result;
13572
14275
  const known = [...pageNodes];
@@ -13913,7 +14616,7 @@ function formatA11yNodeWithContext(data) {
13913
14616
  }
13914
14617
  function formatA11yNodeOneLine(node) {
13915
14618
  const parts = [];
13916
- parts.push(`[${capitalize(node.role)}]`);
14619
+ parts.push(`[${capitalize2(node.role)}]`);
13917
14620
  if (node.name) {
13918
14621
  parts.push(`"${node.name}"`);
13919
14622
  }
@@ -13942,7 +14645,7 @@ function formatA11yNodeOneLine(node) {
13942
14645
  }
13943
14646
  return parts.join(" ");
13944
14647
  }
13945
- function capitalize(str) {
14648
+ function capitalize2(str) {
13946
14649
  return str.charAt(0).toUpperCase() + str.slice(1);
13947
14650
  }
13948
14651
  function truncate(str, maxLen) {
@@ -14051,7 +14754,7 @@ async function handleA11yDescribe(selectorOrIndex, options) {
14051
14754
  if (domNodeId) {
14052
14755
  domContext = await getDomContext({ backendNodeId: domNodeId });
14053
14756
  }
14054
- return { node, domContext };
14757
+ return { node: withSecretMasked(node, domContext), domContext };
14055
14758
  }
14056
14759
  if (options.json) {
14057
14760
  await runJsonCommand(fetchA11yNodeData);
@@ -14118,13 +14821,7 @@ function formatDomQuery(data) {
14118
14821
  const { count, nodes, selector } = data;
14119
14822
  const fmt = new OutputFormatter();
14120
14823
  const nodeLines = nodes.slice(0, QUERY_DISPLAY_LIMIT).map((node) => {
14121
- const attributes = [
14122
- node.id && ` id="${node.id}"`,
14123
- node.name && ` name="${node.name}"`,
14124
- node.type && ` type="${node.type}"`,
14125
- node.value !== void 0 && ` value="${node.value}"`,
14126
- node.classes?.length && ` class="${node.classes.join(" ")}"`
14127
- ].filter(Boolean).join("");
14824
+ const attributes = queryTagAttributes(node).map((item) => ` ${item}`).join("");
14128
14825
  const context = node.context ? ` (in ${node.context})` : "";
14129
14826
  const preview = node.preview ? ` ${node.preview}` : "";
14130
14827
  const position = node.inViewport && viewportPositionHint(node.inViewport, node.clippedBy);
@@ -14134,6 +14831,18 @@ function formatDomQuery(data) {
14134
14831
  const exampleIndex = nodes[0]?.index ?? 0;
14135
14832
  return fmt.text(`Found ${count} node${count === 1 ? "" : "s"} matching "${selector}":`).list(nodeLines).list(count > QUERY_DISPLAY_LIMIT ? [moreMatchesNote(count - QUERY_DISPLAY_LIMIT)] : []).tip(queryNextSteps(exampleIndex)).build();
14136
14833
  }
14834
+ function queryTagAttributes(node) {
14835
+ const identifying = {
14836
+ ...node.name && { name: node.name },
14837
+ ...node.type && { type: node.type },
14838
+ ...node.value !== void 0 && { value: node.value }
14839
+ };
14840
+ return [
14841
+ ...node.id ? [`id="${node.id}"`] : [],
14842
+ ...keyAttributeItems(node.tag ?? "", { ...identifying, ...node.attributes }),
14843
+ ...node.classes?.length ? [`class="${node.classes.join(" ")}"`] : []
14844
+ ];
14845
+ }
14137
14846
  function formatDomGet(data) {
14138
14847
  const { nodes } = data;
14139
14848
  if (nodes.length === 1) {
@@ -14820,190 +15529,31 @@ async function withReadyState(frames) {
14820
15529
 
14821
15530
  // src/commands/dom/get.ts
14822
15531
  init_helpers();
14823
-
14824
- // src/telemetry/roleInference.ts
14825
- var IMPLICIT_ARIA_ROLES = {
14826
- // Interactive elements
14827
- a: "link",
14828
- button: "button",
14829
- input: "textbox",
14830
- select: "combobox",
14831
- textarea: "textbox",
14832
- option: "option",
14833
- // Headings
14834
- h1: "heading",
14835
- h2: "heading",
14836
- h3: "heading",
14837
- h4: "heading",
14838
- h5: "heading",
14839
- h6: "heading",
14840
- // Landmarks
14841
- nav: "navigation",
14842
- main: "main",
14843
- header: "banner",
14844
- footer: "contentinfo",
14845
- aside: "complementary",
14846
- section: "region",
14847
- article: "article",
14848
- form: "form",
14849
- search: "search",
14850
- // Media
14851
- img: "image",
14852
- figure: "figure",
14853
- figcaption: "caption",
14854
- video: "video",
14855
- audio: "audio",
14856
- // Lists
14857
- ul: "list",
14858
- ol: "list",
14859
- li: "listitem",
14860
- dl: "list",
14861
- dt: "term",
14862
- dd: "definition",
14863
- menu: "menu",
14864
- menuitem: "menuitem",
14865
- // Tables
14866
- table: "table",
14867
- thead: "rowgroup",
14868
- tbody: "rowgroup",
14869
- tfoot: "rowgroup",
14870
- tr: "row",
14871
- th: "columnheader",
14872
- td: "cell",
14873
- // Other semantic elements
14874
- dialog: "dialog",
14875
- details: "group",
14876
- summary: "button",
14877
- progress: "progressbar",
14878
- meter: "meter",
14879
- output: "status",
14880
- address: "group",
14881
- blockquote: "blockquote",
14882
- code: "code",
14883
- pre: "group",
14884
- hr: "separator"
14885
- };
14886
- var DEFAULT_ROLE = "generic";
14887
- var MAX_NAME_LENGTH = 100;
14888
- function inferRoleFromTag(tag) {
14889
- const normalizedTag = tag.toLowerCase();
14890
- return IMPLICIT_ARIA_ROLES[normalizedTag] ?? DEFAULT_ROLE;
14891
- }
14892
- function extractHeadingLevel(tag) {
14893
- const match = tag.toLowerCase().match(/^h([1-6])$/);
14894
- const level = match?.[1];
14895
- if (!level) {
14896
- return null;
15532
+ init_errors3();
15533
+ init_messages();
15534
+ init_exitCodes();
15535
+ var DOM_GET_DEFAULT_SELECTOR = "body";
15536
+ async function targetNodeId(selectorOrIndex, index) {
15537
+ const resolver = DomElementResolver.getInstance();
15538
+ if (resolver.isNumericIndex(selectorOrIndex)) {
15539
+ return (await resolver.getNodeIdForIndex(Number(selectorOrIndex))).nodeId;
14897
15540
  }
14898
- return parseInt(level, 10);
15541
+ return selectMatch(selectorOrIndex, index ?? 0);
14899
15542
  }
14900
- function synthesizeA11yNode(domContext, nodeId) {
14901
- const role = inferRoleFromTag(domContext.tag);
14902
- const node = {
14903
- nodeId: String(nodeId),
14904
- role,
14905
- inferred: true,
14906
- backendDOMNodeId: nodeId
14907
- };
14908
- if (domContext.preview) {
14909
- const name = domContext.preview.length > MAX_NAME_LENGTH ? domContext.preview.slice(0, MAX_NAME_LENGTH - 3) + "..." : domContext.preview;
14910
- node.name = name;
14911
- }
14912
- const headingLevel = extractHeadingLevel(domContext.tag);
14913
- if (headingLevel !== null) {
14914
- node.properties = { level: headingLevel };
14915
- }
14916
- return node;
14917
- }
14918
-
14919
- // src/commands/dom/semanticUtils.ts
14920
- init_formatting();
14921
- init_commands();
14922
- function capitalize2(str) {
14923
- return str.charAt(0).toUpperCase() + str.slice(1);
14924
- }
14925
- function buildRoleText(node) {
14926
- if (node.role.toLowerCase() === "heading" && node.properties?.["level"] !== void 0) {
14927
- const level = node.properties["level"];
14928
- const levelNum = typeof level === "number" ? level : Number(level);
14929
- if (!isNaN(levelNum)) {
14930
- return `[Heading L${levelNum}]`;
14931
- }
14932
- }
14933
- return `[${capitalize2(node.role)}]`;
14934
- }
14935
- function buildContextText(node, domContext) {
14936
- if (node.name) {
14937
- return ` "${node.name}"`;
14938
- }
14939
- if (domContext) {
14940
- const tagPart = `<${domContext.tag}`;
14941
- const classPart = domContext.classes && domContext.classes.length > 0 ? `.${domContext.classes.slice(0, 3).join(".")}` : "";
14942
- const previewPart = domContext.preview && !domContext.text ? ` "${domContext.preview}"` : "";
14943
- return ` ${tagPart}${classPart}>${previewPart}`;
14944
- }
14945
- return "";
14946
- }
14947
- function buildPropertiesText(node) {
14948
- const props = [];
14949
- if (node.value !== void 0 && node.value !== "") props.push(`value: "${node.value}"`);
14950
- const checked = node.properties?.["checked"];
14951
- if (checked !== void 0) {
14952
- props.push(checked === "mixed" ? "partly checked" : checked ? "checked" : "unchecked");
14953
- }
14954
- const expanded = node.properties?.["expanded"];
14955
- if (expanded !== void 0) props.push(expanded ? "expanded" : "collapsed");
14956
- if (node.focusable) props.push("focusable");
14957
- if (node.focused) props.push("focused");
14958
- if (node.disabled) props.push("disabled");
14959
- if (node.required) props.push("required");
14960
- return props.length > 0 ? ` (${props.join(", ")})` : "";
14961
- }
14962
- function formatSemanticNodeWithContext(data) {
14963
- const { node, domContext } = data;
14964
- const roleText = buildRoleText(node);
14965
- const contextText = buildContextText(node, domContext);
14966
- const propsText = buildPropertiesText(node);
14967
- const inferredText = node.inferred ? " (inferred from DOM)" : "";
14968
- const line = `${roleText}${contextText}${propsText}${inferredText}`;
14969
- if (domContext?.text) return joinLines(line, elementTextLine(domContext.text));
14970
- if (domContext?.childCount !== void 0 && !node.name) {
14971
- return joinLines(line, emptyElementLine(domContext.children ?? [], domContext.childCount));
14972
- }
14973
- return line;
14974
- }
14975
- function resolveNodeWithFallback(a11yNode, domContext, nodeId) {
14976
- if (a11yNode) return a11yNode;
14977
- if (domContext && nodeId) return synthesizeA11yNode(domContext, nodeId);
14978
- return null;
14979
- }
14980
-
14981
- // src/commands/dom/get.ts
14982
- init_errors3();
14983
- init_messages();
14984
- init_exitCodes();
14985
- var DOM_GET_DEFAULT_SELECTOR = "body";
14986
- async function targetNodeId(selectorOrIndex, index) {
14987
- const resolver = DomElementResolver.getInstance();
14988
- if (resolver.isNumericIndex(selectorOrIndex)) {
14989
- return (await resolver.getNodeIdForIndex(Number(selectorOrIndex))).nodeId;
14990
- }
14991
- return selectMatch(selectorOrIndex, index ?? 0);
14992
- }
14993
- async function semanticElement(backendNodeId, full) {
14994
- const ref = { backendNodeId };
14995
- const [a11yNode, domContext] = await Promise.all([
14996
- resolveA11yNode(ref),
14997
- getDomContext(ref, { full })
14998
- ]);
14999
- const node = resolveNodeWithFallback(a11yNode, domContext, backendNodeId);
15000
- if (!node) {
15001
- const err = nodeIdNotFoundError(backendNodeId);
15002
- throw new CommandError(
15003
- err.message,
15004
- { suggestion: err.suggestion },
15005
- EXIT_CODES.RESOURCE_NOT_FOUND
15006
- );
15543
+ async function semanticElement(backendNodeId, full) {
15544
+ const ref = { backendNodeId };
15545
+ const [a11yNode, domContext] = await Promise.all([
15546
+ resolveA11yNode(ref),
15547
+ getDomContext(ref, { full })
15548
+ ]);
15549
+ const node = resolveNodeWithFallback(a11yNode, domContext, backendNodeId);
15550
+ if (!node) {
15551
+ const err = nodeIdNotFoundError(backendNodeId);
15552
+ throw new CommandError(
15553
+ err.message,
15554
+ { suggestion: err.suggestion },
15555
+ EXIT_CODES.RESOURCE_NOT_FOUND
15556
+ );
15007
15557
  }
15008
15558
  return { node, domContext };
15009
15559
  }
@@ -15078,6 +15628,441 @@ async function handleDomGet(selectorOrIndex, options) {
15078
15628
  else await handleSemanticGet(target, options);
15079
15629
  }
15080
15630
 
15631
+ // src/commands/dom/inspect.ts
15632
+ init_client();
15633
+
15634
+ // src/utils/color.ts
15635
+ var D50_WHITE = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585];
15636
+
15637
+ // src/utils/cssValues.ts
15638
+ function sidesShorthand(sides) {
15639
+ const [top, right, bottom, left] = sides.map(String);
15640
+ if (top === right && right === bottom && bottom === left) return `${top}`;
15641
+ if (top === bottom && right === left) return `${top} ${right}`;
15642
+ if (right === left) return `${top} ${right} ${bottom}`;
15643
+ return `${top} ${right} ${bottom} ${left}`;
15644
+ }
15645
+ function shadowText(layer, withSpread = true) {
15646
+ const lengths = [layer.x, layer.y, layer.blur, ...withSpread ? [layer.spread] : []];
15647
+ return `${layer.inset ? "inset " : ""}${layer.color} ${lengths.join(" ")}`;
15648
+ }
15649
+
15650
+ // src/runtime/dom/inspectLayoutModel.ts
15651
+ function containerKind(display) {
15652
+ if (display === void 0) return void 0;
15653
+ if (/(^|-)flex$/.test(display) || display === "-webkit-box") return "flex";
15654
+ return /(^|-)grid$/.test(display) ? "grid" : void 0;
15655
+ }
15656
+
15657
+ // src/runtime/dom/inspectTree.ts
15658
+ var DEFAULT_TREE_DEPTH = 2;
15659
+ var DEFAULT_TREE_LIMIT = 20;
15660
+
15661
+ // src/ui/formatters/inspect.ts
15662
+ init_formatting();
15663
+ init_commands();
15664
+ init_strings();
15665
+ var PARENT_LABEL_MAX = 40;
15666
+ var LABEL_WIDTH = 7;
15667
+ var ALL_LINE_WIDTH = 100;
15668
+ function groupLine(label, parts) {
15669
+ const shown = parts.filter((part) => Boolean(part));
15670
+ return shown.length > 0 ? `${label.padEnd(LABEL_WIDTH)}${shown.join(" \xB7 ")}` : void 0;
15671
+ }
15672
+ function inspectHeader(data) {
15673
+ const display = data.layout?.display ?? data.all?.["display"];
15674
+ const kind = containerKind(display);
15675
+ return [
15676
+ data.element,
15677
+ data.content !== void 0 && `"${data.content}"`,
15678
+ data.placeholder !== void 0 && `placeholder "${data.placeholder}"`,
15679
+ data.rect && `${data.rect.w}x${data.rect.h} @${data.rect.x},${data.rect.y}`,
15680
+ kind && `[${kind}]`,
15681
+ ...inspectVisibilityBadges(data.visibility),
15682
+ data.context && `in ${data.context}`,
15683
+ data.theme === "dark" && inspectDarkThemeBadge(),
15684
+ data.animating && inspectAnimatingBadge(data.animating)
15685
+ ].filter(Boolean).join(" ");
15686
+ }
15687
+ function zero(sides) {
15688
+ return sides.every((side) => side === 0);
15689
+ }
15690
+ function limitParts(kind, limits) {
15691
+ if (!limits) return [];
15692
+ return [
15693
+ limits.w !== void 0 ? `${kind}-w ${limits.w}` : "",
15694
+ limits.h !== void 0 ? `${kind}-h ${limits.h}` : ""
15695
+ ].filter(Boolean);
15696
+ }
15697
+ function boxLine(box) {
15698
+ const padded = !zero(box.padding) || !zero(box.border);
15699
+ return groupLine("box", [
15700
+ !zero(box.margin) && `m ${sidesShorthand(box.margin)}`,
15701
+ !zero(box.padding) && `p ${sidesShorthand(box.padding)}`,
15702
+ !zero(box.border) && `b ${sidesShorthand(box.border)}`,
15703
+ padded && box.sizing === "content-box" && "sizing content-box",
15704
+ ...limitParts("min", box.min),
15705
+ ...limitParts("max", box.max),
15706
+ box.overflow && `overflow ${box.overflow}`,
15707
+ box.scroll && `scroll ${box.scroll.w}x${box.scroll.h}`
15708
+ ]);
15709
+ }
15710
+ function containerParts(container) {
15711
+ const gap = Array.isArray(container.gap) ? container.gap.join(" ") : container.gap;
15712
+ return [
15713
+ container.wrap && `wrap ${container.wrap}`,
15714
+ container.columns && `cols ${container.columns}`,
15715
+ container.rows && `rows ${container.rows}`,
15716
+ container.justify && `justify ${container.justify}`,
15717
+ container.align && `align ${container.align}`,
15718
+ gap !== void 0 && `gap ${gap}`
15719
+ ].filter((part) => Boolean(part));
15720
+ }
15721
+ function displayWords(display, direction) {
15722
+ return direction ? `${display} ${direction}` : display;
15723
+ }
15724
+ function layoutLine(layout) {
15725
+ return groupLine("layout", [
15726
+ displayWords(layout.display, layout.direction),
15727
+ layout.position && `position ${layout.position}`,
15728
+ layout.inset && `inset ${sidesShorthand(layout.inset)}`,
15729
+ layout.z !== void 0 && `z ${layout.z}`,
15730
+ ...containerParts(layout),
15731
+ layout.flex && `flex ${layout.flex}`,
15732
+ layout.self && `self ${layout.self}`,
15733
+ layout.order !== void 0 && `order ${layout.order}`,
15734
+ layout.area && `area ${layout.area}`,
15735
+ layout.valign && `valign ${layout.valign}`,
15736
+ layout.float && `float ${layout.float}`
15737
+ ]);
15738
+ }
15739
+ function parentLine(layout) {
15740
+ const parent = layout.parent;
15741
+ const offsets = layout.inParent;
15742
+ const gaps = Object.entries(layout.siblings ?? {}).map(([side, gap]) => `${side[0]}${gap}`);
15743
+ const parentWords = parent && [
15744
+ truncateByLength(parent.element, PARENT_LABEL_MAX),
15745
+ displayWords(parent.display, parent.direction),
15746
+ ...containerParts(parent),
15747
+ parent.textAlign && `text-align ${parent.textAlign}`
15748
+ ];
15749
+ return groupLine("parent", [
15750
+ parentWords?.filter(Boolean).join(" "),
15751
+ offsets && `in-parent l${offsets.left} t${offsets.top} r${offsets.right} b${offsets.bottom}`,
15752
+ gaps.length > 0 && `sib ${gaps.join(" ")}`
15753
+ ]);
15754
+ }
15755
+ function fontParts(text) {
15756
+ const loaded = [
15757
+ text.rendered && `rendered "${text.rendered}"`,
15758
+ text.webfont && "webfont loaded"
15759
+ ].filter(Boolean);
15760
+ const family = text.family && [text.family, loaded.length > 0 && `(${loaded.join(", ")})`];
15761
+ const size = text.size !== void 0 && (text.lineHeight !== void 0 ? `${text.size}/${text.lineHeight}` : `${text.size}`);
15762
+ if (family && text.weight !== void 0 && size) {
15763
+ return [
15764
+ [...family, text.weight, text.style, size].filter((part) => part !== void 0 && part !== false && part !== "").join(" ")
15765
+ ];
15766
+ }
15767
+ return [
15768
+ family && `font ${family.filter(Boolean).join(" ")}`,
15769
+ text.weight !== void 0 && `weight ${text.weight}`,
15770
+ text.style && `style ${text.style}`,
15771
+ size && `size ${size}`,
15772
+ !size && text.lineHeight !== void 0 && `line-height ${text.lineHeight}`
15773
+ ].filter((part) => Boolean(part));
15774
+ }
15775
+ function contrastText(contrast) {
15776
+ if (!contrast) return void 0;
15777
+ return [
15778
+ `contrast ${contrast.ratio} ${contrast.level}`,
15779
+ contrast.inherited && `on ${contrast.background}`,
15780
+ contrast.overImage && "(over image)",
15781
+ contrast.opacity !== void 0 && `(faded: opacity ${contrast.opacity})`
15782
+ ].filter(Boolean).join(" ");
15783
+ }
15784
+ function textLine(text) {
15785
+ return groupLine("text", [
15786
+ ...fontParts(text),
15787
+ text.color && `color ${text.color}`,
15788
+ contrastText(text.contrast),
15789
+ text.align && `align ${text.align}`,
15790
+ text.transform && `transform ${text.transform}`,
15791
+ text.tracking !== void 0 && `tracking ${text.tracking}`,
15792
+ text.decoration && `decoration ${text.decoration}`,
15793
+ text.whiteSpace && `ws ${text.whiteSpace}`,
15794
+ text.overflow && `text-overflow ${text.overflow}`,
15795
+ text.clamp && `clamp ${text.clamp}`,
15796
+ text.shadow && `shadow ${text.shadow}`,
15797
+ text.features && `features ${text.features}`
15798
+ ]);
15799
+ }
15800
+ function fillText(fill) {
15801
+ if (fill.type === "solid") return `bg ${fill.color}`;
15802
+ if (fill.type === "gradient") return `bg-image ${fill.value}`;
15803
+ return `bg-image ${fill.value}${fill.size ? ` size ${fill.size}` : ""}`;
15804
+ }
15805
+ function fillLine(data) {
15806
+ return groupLine("fill", [
15807
+ ...(data.fills ?? []).map(fillText),
15808
+ data.opacity !== void 0 && `opacity ${data.opacity}`,
15809
+ data.blend && `blend ${data.blend}`
15810
+ ]);
15811
+ }
15812
+ function borderLine(data) {
15813
+ const outline = data.outline;
15814
+ return groupLine("border", [
15815
+ ...(data.strokes ?? []).map(
15816
+ (s) => `${s.side === "all" ? "" : `${s.side} `}${s.width} ${s.style} ${s.color}`
15817
+ ),
15818
+ data.radius && `radius ${sidesShorthand(data.radius)}`,
15819
+ outline && `outline ${outline.width} ${outline.style} ${outline.color}${outline.offset ? ` offset ${outline.offset}` : ""}`
15820
+ ]);
15821
+ }
15822
+ function fxLine(data) {
15823
+ const shadows = (data.effects ?? []).map(
15824
+ (e) => shadowText({ ...e, inset: e.type === "inner-shadow" })
15825
+ );
15826
+ const fx = data.fx ?? {};
15827
+ return groupLine("fx", [
15828
+ shadows.length > 0 && `shadow ${shadows.join(", ")}`,
15829
+ fx.transform && `transform ${fx.transform}`,
15830
+ fx.filter && `filter ${fx.filter}`,
15831
+ fx.backdrop && `backdrop ${fx.backdrop}`,
15832
+ fx.clip && `clip ${fx.clip}`,
15833
+ fx.mask && `mask ${fx.mask}`,
15834
+ fx.animation && `animation ${fx.animation}`
15835
+ ]);
15836
+ }
15837
+ function stateLine(data) {
15838
+ const state = data.state ?? {};
15839
+ return groupLine("state", [
15840
+ state.cursor && `cursor ${state.cursor}`,
15841
+ state.pointerEvents && `pointer-events ${state.pointerEvents}`,
15842
+ state.visibility && `visibility ${state.visibility}`,
15843
+ state.userSelect && `select ${state.userSelect}`,
15844
+ state.appearance && `appearance ${state.appearance}`
15845
+ ]);
15846
+ }
15847
+ function pseudoText(pseudo) {
15848
+ const shadows = (pseudo.effects ?? []).map(
15849
+ (e) => shadowText({ ...e, inset: e.type === "inner-shadow" })
15850
+ );
15851
+ return [
15852
+ pseudo.type,
15853
+ pseudo.content !== void 0 && `content ${pseudo.content}`,
15854
+ pseudo.display,
15855
+ pseudo.position,
15856
+ pseudo.size && `${pseudo.size.w}x${pseudo.size.h}`,
15857
+ pseudo.color && `color ${pseudo.color}`,
15858
+ pseudo.fontStyle,
15859
+ pseudo.fontWeight !== void 0 && `weight ${pseudo.fontWeight}`,
15860
+ pseudo.contrast && `\xB7 ${contrastText(pseudo.contrast)}`,
15861
+ ...(pseudo.fills ?? []).map(fillText),
15862
+ pseudo.radius && `radius ${pseudo.radius}`,
15863
+ shadows.length > 0 && `shadow ${shadows.join(", ")}`,
15864
+ pseudo.transform && `transform ${pseudo.transform}`,
15865
+ pseudo.opacity !== void 0 && `opacity ${pseudo.opacity}`
15866
+ ].filter(Boolean).join(" ");
15867
+ }
15868
+ function pseudoLines(pseudo) {
15869
+ return (pseudo ?? []).map(
15870
+ (entry, i) => `${(i === 0 ? "pseudo" : "").padEnd(LABEL_WIDTH)}${pseudoText(entry)}`
15871
+ );
15872
+ }
15873
+ function treeRow(node) {
15874
+ return [
15875
+ node.element,
15876
+ node.count !== void 0 && `\xD7${node.count}`,
15877
+ `${node.w}x${node.h}`,
15878
+ node.layout && `[${node.layout}]`,
15879
+ node.text && `"${node.text}"`,
15880
+ node.childCount !== void 0 && `(${node.childCount})`
15881
+ ].filter(Boolean).join(" ");
15882
+ }
15883
+ function treeLines2(nodes, hidden, depth) {
15884
+ const indent = " ".repeat(depth);
15885
+ const lines = nodes.flatMap((node) => [
15886
+ `${indent}${treeRow(node)}`,
15887
+ ...treeLines2(node.children ?? [], node.hiddenChildren, depth + 1)
15888
+ ]);
15889
+ return hidden ? [...lines, `${indent}(+${hidden} not rendered)`] : lines;
15890
+ }
15891
+ function labelledLines(label, lines) {
15892
+ return lines.map((line, i) => `${i === 0 ? label.padEnd(6) : "".padEnd(6)} ${line}`);
15893
+ }
15894
+ function ruleScope(entry) {
15895
+ return [entry.condition && ` @${entry.condition}`, entry.layer && ` layer ${entry.layer}`].filter(Boolean).join("");
15896
+ }
15897
+ function cascadeBlock(data) {
15898
+ const hints = (data.hints ?? []).map(
15899
+ (hint) => `${hint.property}: ${hint.value} ${hint.kind === "not-inherited" ? "is the browser's" : "has no effect"}: ${hint.reason} \u2192 ${hint.fix} \xB7 in ${hint.source}`
15900
+ );
15901
+ return [
15902
+ ...labelledLines("hints", data.hints?.length === 0 ? ["none"] : hints),
15903
+ ...labelledLines("rules", (data.rules ?? []).map(ruleLine)),
15904
+ ...labelledLines(
15905
+ "why",
15906
+ (data.why ?? []).flatMap((why) => whyLines(why, data))
15907
+ ),
15908
+ ...data.cascade ? [inspectCascadeNote(data.cascade)] : []
15909
+ ];
15910
+ }
15911
+ function ruleLine(rule) {
15912
+ return [
15913
+ `${rule.property} ${truncateByLength(rule.value, CASCADE_VALUE_WIDTH)}`,
15914
+ rule.computed !== void 0 && ` = ${truncateByLength(rule.computed, CASCADE_VALUE_WIDTH)}`,
15915
+ `${rule.important ? " !important" : ""} \u2190 ${rule.source}`,
15916
+ ruleScope(rule),
15917
+ rule.inherited !== void 0 && ` (inherited from ${levelsUp(rule.inherited)})`,
15918
+ rule.overrides && ` over ${rule.overrides.join(", ")}`
15919
+ ].filter(Boolean).join("");
15920
+ }
15921
+ function whyLines(why, data) {
15922
+ const entries = why.chain.map((entry) => {
15923
+ const value = `${entry.via ? `${entry.via}: ` : ""}${entry.value}`;
15924
+ const resolved = entry.unset ? ` = invalid: ${entry.unset.join(", ")} not set` : entry.resolved !== void 0 ? ` = ${truncateByLength(entry.resolved, CASCADE_VALUE_WIDTH)}` : "";
15925
+ return [
15926
+ ` ${entry.status === "overridden" ? "\u2717" : "\u2713"} `,
15927
+ truncateByLength(value, CASCADE_VALUE_WIDTH) + resolved,
15928
+ entry.important ? " !important" : "",
15929
+ ` ${entry.source}${entry.specificity ? ` [${entry.specificity.join(",")}]` : ""}${ruleScope(entry)}`,
15930
+ entry.status === "inherited" ? " (inherited)" : ""
15931
+ ].join("");
15932
+ });
15933
+ const variables = (why.variables ?? []).map(
15934
+ (variable) => ` ${variable.name}: ${truncateByLength(variable.value, CASCADE_VALUE_WIDTH)} ${variable.source}${variable.inherited !== void 0 ? ` (inherited from ${levelsUp(variable.inherited)})` : ""}`
15935
+ );
15936
+ return [
15937
+ `${why.property} = ${why.computed}${midTransition(why.property, data.animating) ? ` ${inspectMidTransitionNote()}` : ""}`,
15938
+ ...entries.slice(0, 1),
15939
+ ...variables,
15940
+ ...entries.slice(1),
15941
+ ...why.chain.length === 0 ? [" no author declaration: the default or inherited value"] : []
15942
+ ];
15943
+ }
15944
+ function midTransition(property, animating) {
15945
+ return (animating ?? []).some(
15946
+ (name) => name === "all" || name === property || property.startsWith(`${name}-`) || name.startsWith(`${property}-`)
15947
+ );
15948
+ }
15949
+ function levelsUp(levels) {
15950
+ return levels === 1 ? "the parent" : `${levels} levels up`;
15951
+ }
15952
+ var CASCADE_VALUE_WIDTH = 60;
15953
+ function treeBlock(data) {
15954
+ if (!data.children && !data.hiddenChildren) return [];
15955
+ return [
15956
+ "tree",
15957
+ ...treeLines2(data.children ?? [], data.hiddenChildren, 1),
15958
+ ...data.moreRows ? [` \u2026 +${data.moreRows} more`] : []
15959
+ ];
15960
+ }
15961
+ function allLines(all) {
15962
+ const lines = [];
15963
+ let current = [];
15964
+ for (const [name, value] of Object.entries(all)) {
15965
+ current.push(`${name} ${value}`);
15966
+ if (current.join(" \xB7 ").length > ALL_LINE_WIDTH) {
15967
+ lines.push(current.join(" \xB7 "));
15968
+ current = [];
15969
+ }
15970
+ }
15971
+ if (current.length > 0) lines.push(current.join(" \xB7 "));
15972
+ return lines.map((line, i) => `${(i === 0 ? "all" : "").padEnd(LABEL_WIDTH)}${line}`);
15973
+ }
15974
+ function propLines(props) {
15975
+ if (Object.keys(props).length === 0) return ["(no matching custom properties)"];
15976
+ return Object.entries(props).map(
15977
+ ([name, prop]) => `${name}: ${prop.computed || (name.startsWith("--") ? "(not set)" : "(empty)")}${prop.value !== prop.computed ? ` = ${prop.value}` : ""}`
15978
+ );
15979
+ }
15980
+ function groupLines(data) {
15981
+ return [
15982
+ data.box && boxLine(data.box),
15983
+ data.layout && layoutLine(data.layout),
15984
+ data.layout && parentLine(data.layout),
15985
+ data.text && textLine(data.text),
15986
+ fillLine(data),
15987
+ borderLine(data),
15988
+ fxLine(data),
15989
+ stateLine(data),
15990
+ ...pseudoLines(data.pseudo)
15991
+ ];
15992
+ }
15993
+ function formatInspect(data) {
15994
+ const body = data.props ? propLines(data.props) : data.all ? allLines(data.all) : groupLines(data);
15995
+ const note = data.picked ? multipleMatchesWarning(data.count, inspectedMatchAction(data.picked, data.index)) : void 0;
15996
+ if (data.why) return joinLines(inspectHeader(data), ...cascadeBlock(data), note);
15997
+ return joinLines(inspectHeader(data), ...body, ...cascadeBlock(data), ...treeBlock(data), note);
15998
+ }
15999
+
16000
+ // src/commands/dom/inspect.ts
16001
+ init_commands();
16002
+ function registerInspectCommand(dom) {
16003
+ dom.command("inspect").description(
16004
+ "What one element looks like without a screenshot: box, layout, font (rendered, contrast), colors, borders, effects, pseudo-elements and child tree"
16005
+ ).argument("<selectorOrIndex>", SELECTOR_OR_INDEX_ARGUMENT).option(
16006
+ "--index <n>",
16007
+ "Which match to inspect (0-based; default: the first rendered one)",
16008
+ integerOption(0)
16009
+ ).option(
16010
+ "--tree <depth>",
16011
+ `Child tree depth (default ${DEFAULT_TREE_DEPTH}; 0 for none)`,
16012
+ integerOption(0, 10)
16013
+ ).option(
16014
+ "--tree-limit <n>",
16015
+ `Child tree rows at most (default ${DEFAULT_TREE_LIMIT})`,
16016
+ integerOption(1, 500)
16017
+ ).addOption(
16018
+ new Option(
16019
+ "--all",
16020
+ "Every computed property that is not its default, collapsed into shorthands, instead of the groups"
16021
+ ).conflicts("props")
16022
+ ).addHelpText("after", INSPECT_OUTPUT_LEGEND).option(
16023
+ "--props <names>",
16024
+ "Only these properties, computed and normalized (comma-separated, e.g. padding,color,--brand; '--*' or '--bs-btn-*' lists custom properties)",
16025
+ cssPropertiesOption
16026
+ ).addOption(
16027
+ new Option(
16028
+ "--rules",
16029
+ "Also show which CSS rule sets each shown property (selector, file:line, what it overrides); with --props, those properties"
16030
+ ).conflicts("all")
16031
+ ).addOption(
16032
+ new Option(
16033
+ "--why <property>",
16034
+ "Every declaration of one property: the one that applies and those it overrides (e.g. --why color)"
16035
+ ).conflicts("all").argParser(propertyOption)
16036
+ ).option("--no-hints", "Skip the check for declarations that have no effect").addOption(jsonOption()).action(async (selectorOrIndex, options) => {
16037
+ await runCommand(() => inspectTarget(selectorOrIndex, options), options, formatInspect);
16038
+ });
16039
+ }
16040
+ function propertyOption(value) {
16041
+ return value.startsWith("--") ? value.trim() : value.trim().toLowerCase();
16042
+ }
16043
+ async function inspectTarget(selectorOrIndex, options) {
16044
+ return runElementCommand({
16045
+ selectorOrIndex,
16046
+ index: options.index,
16047
+ buildRequest: (target) => ({
16048
+ ...target,
16049
+ ...filterDefined({
16050
+ tree: options.tree,
16051
+ treeLimit: options.treeLimit,
16052
+ all: options.all,
16053
+ props: options.props,
16054
+ rules: options.rules,
16055
+ why: options.why,
16056
+ ...options.hints === false && { hints: false }
16057
+ })
16058
+ }),
16059
+ call: domInspect,
16060
+ command: "inspect",
16061
+ action: "inspect the element",
16062
+ failureSuggestion: 'Verify the selector matches an element: bdg dom query "<selector>"'
16063
+ });
16064
+ }
16065
+
15081
16066
  // src/commands/dom/layout.ts
15082
16067
  init_client();
15083
16068
 
@@ -15086,13 +16071,13 @@ init_messages();
15086
16071
  init_formatting();
15087
16072
  init_commands();
15088
16073
  var LAYOUT_DISPLAY_LIMIT = 20;
15089
- function layoutLine(element, viewport) {
16074
+ function layoutLine2(element, viewport) {
15090
16075
  const { bounds } = element;
15091
16076
  return [
15092
16077
  `[${element.index}] ${element.element}${element.text ? ` "${element.text}"` : ""}`,
15093
16078
  element.inViewport !== "hidden" && `${bounds.x},${bounds.y} ${bounds.width}\xD7${bounds.height}`,
15094
16079
  layoutPositionLabel(element, viewport),
15095
- element.coveredBy && `covered by ${element.coveredBy}`,
16080
+ element.coveredBy && coverText(element.coveredBy, element.coverTransparent),
15096
16081
  element.inert && "inert",
15097
16082
  element.invisible,
15098
16083
  element.context && element.context !== element.clippedBy && `in ${element.context}`
@@ -15103,14 +16088,14 @@ function formatLayout(data) {
15103
16088
  const more = data.elements.length - shown.length + (data.omitted ?? 0);
15104
16089
  return new OutputFormatter().text(pageLayoutLine(data.page)).text(
15105
16090
  data.indexSource ? indexLayoutHeadline(indexSourceText(data.indexSource)) : layoutHeadline(data.count, data.elements.length + (data.omitted ?? 0), data.selector)
15106
- ).list(shown.map((element) => layoutLine(element, data.page.viewport))).list(more > 0 ? [moreMatchesNote(more, data.omitted ? data.elements.length : void 0)] : []).build();
16091
+ ).list(shown.map((element) => layoutLine2(element, data.page.viewport))).list(more > 0 ? [moreMatchesNote(more, data.omitted ? data.elements.length : void 0)] : []).build();
15107
16092
  }
15108
16093
 
15109
16094
  // src/commands/dom/layout.ts
15110
16095
  function registerLayoutCommand(dom) {
15111
16096
  dom.command("layout").description(
15112
16097
  "Positions, sizes and visibility of elements (above/below the fold, hidden, covered) without a screenshot"
15113
- ).argument("<selectorOrIndex>", "CSS selector or numeric index from query results (0-based)").option("--index <n>", "Only this match of the selector (0-based)", integerOption(0)).addOption(jsonOption()).action(async (selectorOrIndex, options) => {
16098
+ ).argument("<selectorOrIndex>", SELECTOR_OR_INDEX_ARGUMENT).option("--index <n>", "Only this match of the selector (0-based)", integerOption(0)).addOption(jsonOption()).action(async (selectorOrIndex, options) => {
15114
16099
  await runCommand(() => measureLayout(selectorOrIndex, options), options, formatLayout);
15115
16100
  });
15116
16101
  }
@@ -15295,7 +16280,7 @@ function appendListenerGroups(fmt, listeners) {
15295
16280
 
15296
16281
  // src/commands/dom/listeners.ts
15297
16282
  function registerListenersCommand(dom) {
15298
- dom.command("listeners").description("List event listeners that run for an element (incl. delegated ones on ancestors)").argument("<selectorOrIndex>", "CSS selector or numeric index from query results (0-based)").option("--index <n>", "Element index if selector matches multiple (0-based)", integerOption(0)).option(
16283
+ dom.command("listeners").description("List event listeners that run for an element (incl. delegated ones on ancestors)").argument("<selectorOrIndex>", SELECTOR_OR_INDEX_ARGUMENT).option("--index <n>", "Element index if selector matches multiple (0-based)", integerOption(0)).option(
15299
16284
  "--type <types>",
15300
16285
  "Only these event types (comma-separated, e.g. click,keydown; repeatable)",
15301
16286
  eventTypesOption
@@ -15638,25 +16623,28 @@ function registerDomCommands(program2) {
15638
16623
  registerFormCommand(dom);
15639
16624
  registerListenersCommand(dom);
15640
16625
  registerLayoutCommand(dom);
16626
+ registerInspectCommand(dom);
15641
16627
  registerWaitCommand(dom);
15642
- dom.command("query").description("Find elements by CSS selector").argument("<selector>", 'CSS selector (e.g., ".error", "#app", "button")').option("-j, --json", "Output as JSON").action(async (selector, options) => {
16628
+ dom.command("query").description("Find elements by CSS selector").argument("<selector>", 'CSS selector (e.g., ".error", "#app", "button")').option("-j, --json", "Output as JSON").addHelpText("after", SELECTOR_SCOPE_HELP).action(async (selector, options) => {
15643
16629
  await handleDomQuery(selector, options);
15644
16630
  });
15645
16631
  dom.command("eval").description("Evaluate JavaScript expression in the page context").argument("<script>", 'JavaScript to execute (e.g., "document.title", "window.location.href")').option(
15646
16632
  "--frame <frame>",
15647
- "Evaluate in an iframe, cross-origin ones included: index, name/id attribute, or part of the name, id or URL (see dom frames)"
16633
+ "Evaluate in an iframe, cross-origin ones included: index (from dom frames; 87 when stale), name/id attribute, or part of the name, id or URL"
15648
16634
  ).option("-j, --json", "Output as JSON").action(async (script, options) => {
15649
16635
  await handleDomEval(script, options);
15650
16636
  });
15651
16637
  program2.command("eval", { hidden: true }).description("Shortcut for: bdg dom eval").argument("<script>", "JavaScript to execute").option("--frame <frame>", "Evaluate in an iframe (see dom frames)").option("-j, --json", "Output as JSON").action(async (script, options) => {
15652
16638
  await handleDomEval(script, options);
15653
16639
  });
15654
- dom.command("frames").description("List the page's iframes (nested and cross-origin ones included) for eval --frame").option("-j, --json", "Output as JSON").action(async (options) => {
16640
+ dom.command("frames").description(
16641
+ "List the page's iframes in document order (nested and cross-origin ones included) for eval --frame"
16642
+ ).option("-j, --json", "Output as JSON").action(async (options) => {
15655
16643
  await handleDomFrames(options);
15656
16644
  });
15657
16645
  dom.command("get").description("Get semantic accessibility structure (default) or raw HTML (--raw)").argument(
15658
16646
  "[selectorOrIndex]",
15659
- `CSS selector or numeric index from query results (0-based; e.g. ".error", "#app", 0); default: ${DOM_GET_DEFAULT_SELECTOR}`
16647
+ `${SELECTOR_OR_INDEX_ARGUMENT} (e.g. ".error", "#app", 0); default: ${DOM_GET_DEFAULT_SELECTOR}`
15660
16648
  ).option("--raw", "Output raw HTML with all filtering options").option("--full", "Show all of the element text (default: the first 500 characters)").option("--all", "Get all matches (only with --raw)").option("--index <n>", "Element index if selector matches multiple (0-based)", integerOption(0)).addOption(new Option("--nth <n>", "Alias of --index").argParser(integerOption(0)).hideHelp()).option(
15661
16649
  "--node-id <id>",
15662
16650
  "Get the element with this node id (from dom query/get --raw or a11y describe; implies --raw)",
@@ -16307,6 +17295,7 @@ init_commands();
16307
17295
  // src/ui/messages/session.ts
16308
17296
  init_formatting();
16309
17297
  init_commands();
17298
+ init_sessionCommand();
16310
17299
  function startNotices(options) {
16311
17300
  const { documentStatus, loading, autoStopAt, session } = options;
16312
17301
  return [
@@ -16353,6 +17342,13 @@ function lastSessionEndText(end) {
16353
17342
  const at = new Date(end.endedAt).toLocaleTimeString();
16354
17343
  return `The last session ended at ${at}: ${why[end.reason] ?? end.reason}`;
16355
17344
  }
17345
+ function daemonStillExitingHint(pid, waitedMs) {
17346
+ const daemon = pid === void 0 ? "The daemon" : `The daemon (PID ${pid})`;
17347
+ return `${daemon} was still shutting down after ${waitedMs / 1e3}s`;
17348
+ }
17349
+ function daemonStillExitingSuggestion() {
17350
+ return `check with bdg sessions, or end it with ${sessionCommand("bdg cleanup --force")}`;
17351
+ }
16356
17352
 
16357
17353
  // src/ui/formatters/status.ts
16358
17354
  init_sessionCommand();
@@ -16438,7 +17434,7 @@ function appearanceLines(metadata, pageState) {
16438
17434
  ...viewport ? [
16439
17435
  [
16440
17436
  "Viewport",
16441
- `${viewport.width}\xD7${viewport.height}${metadata.viewport ? ` (--viewport ${metadata.viewport.width}x${metadata.viewport.height})` : ""}`
17437
+ `${viewport.width}\xD7${viewport.height}${metadata.viewport ? ` (emulated ${metadata.viewport.width}x${metadata.viewport.height})` : ""}`
16442
17438
  ]
16443
17439
  ] : [],
16444
17440
  ...scheme ? [
@@ -16691,42 +17687,6 @@ function formatPreviewVerbose(output, options) {
16691
17687
  return fmt.build();
16692
17688
  }
16693
17689
 
16694
- // src/ui/formatters/networkHeaders.ts
16695
- init_formatting();
16696
- function formatNetworkHeaders(data) {
16697
- const fmt = new OutputFormatter();
16698
- fmt.text("Network Request Headers").separator("\u2501", 60).blank();
16699
- fmt.text("URL:").text(` ${data.url}`).blank();
16700
- fmt.text(`Status: ${requestStatusLine(data)}`).blank();
16701
- if (Object.keys(data.responseHeaders).length > 0) {
16702
- fmt.text("Response Headers:");
16703
- formatHeaderSection(fmt, data.responseHeaders);
16704
- fmt.blank();
16705
- }
16706
- if (Object.keys(data.requestHeaders).length > 0) {
16707
- fmt.text("Request Headers:");
16708
- formatHeaderSection(fmt, data.requestHeaders);
16709
- fmt.blank();
16710
- }
16711
- if (Object.keys(data.responseHeaders).length === 0 && Object.keys(data.requestHeaders).length === 0) {
16712
- fmt.text("No headers found").blank();
16713
- }
16714
- fmt.text(`Request ID: ${data.requestId}`);
16715
- return fmt.build();
16716
- }
16717
- function formatHeaderSection(fmt, headers) {
16718
- const entries = Object.entries(headers).sort(([a], [b]) => a.localeCompare(b));
16719
- const keyWidth = Math.max(...entries.map(([k]) => k.length)) + 4;
16720
- entries.forEach(([key, value]) => {
16721
- value.split("\n").forEach((line) => fmt.keyValue(` ${key}`, line, keyWidth));
16722
- });
16723
- }
16724
- function requestStatusLine(data) {
16725
- const status = formatRequestStatus(data);
16726
- const text = data.statusText && /^\d+$/.test(status) ? ` ${data.statusText}` : "";
16727
- return [data.method, `${status}${text}`].filter(Boolean).join(" ");
16728
- }
16729
-
16730
17690
  // src/commands/network/shared.ts
16731
17691
  init_exitCodes();
16732
17692
  async function fetchFromLiveSession() {
@@ -17013,19 +17973,56 @@ function formatDuration2(ms) {
17013
17973
  if (ms === void 0) return "-";
17014
17974
  return ms < 1e3 ? `${Math.round(ms)}ms` : `${(ms / 1e3).toFixed(1)}s`;
17015
17975
  }
17016
- function formatColumnHeader(idWidth) {
17017
- return `${"[ID]".padEnd(idWidth)} STS METH TYP ${"SIZE".padStart(8)} ${"TIME".padStart(6)} URL`;
17976
+ function pageStartOf(requests) {
17977
+ const navigationIds = requests.flatMap(
17978
+ (r) => r.navigationId === void 0 ? [] : [r.navigationId]
17979
+ );
17980
+ const current = navigationIds.length > 0 ? Math.max(...navigationIds) : void 0;
17981
+ const page = requests.filter((r) => r.navigationId === current);
17982
+ const documents = page.filter((r) => r.resourceType === "Document");
17983
+ const first = (documents.length > 0 ? documents : page).reduce(
17984
+ (earliest, r) => earliest === void 0 || r.timestamp < earliest.timestamp ? r : earliest,
17985
+ void 0
17986
+ );
17987
+ return first && {
17988
+ timestamp: first.timestamp,
17989
+ ...first.sentTime !== void 0 && { sentTime: first.sentTime }
17990
+ };
17991
+ }
17992
+ function startOffsetMs(request, pageStart) {
17993
+ if (request.sentTime !== void 0 && pageStart.sentTime !== void 0) {
17994
+ return (request.sentTime - pageStart.sentTime) * 1e3;
17995
+ }
17996
+ return request.timestamp - pageStart.timestamp;
17997
+ }
17998
+ function formatStartOffset(request, pageStart) {
17999
+ if (!pageStart) return "-";
18000
+ const ms = startOffsetMs(request, pageStart);
18001
+ const seconds = Math.abs(ms) / 1e3;
18002
+ const sign = ms < -50 ? "-" : "+";
18003
+ if (seconds < 99.95) return `${sign}${seconds.toFixed(1)}s`;
18004
+ if (seconds < 999.5) return `${sign}${Math.round(seconds)}s`;
18005
+ return `${sign}${Math.floor(seconds / 60)}m`;
17018
18006
  }
17019
- function formatRequestLine(request, verbose, idWidth) {
17020
- const id = `[${request.requestId}]`.padEnd(idWidth);
18007
+ function formatColumnHeader(widths) {
18008
+ return `${"[ID]".padEnd(widths.id)} ${"START".padStart(6)} STS ${"METH".padEnd(widths.method)} TYP ${"SIZE".padStart(8)} ${"TIME".padStart(6)} URL`;
18009
+ }
18010
+ function formatRequestLine(request, options, widths) {
18011
+ const id = `[${request.requestId}]`.padEnd(widths.id);
18012
+ const start = formatStartOffset(request, options.pageStart).padStart(6);
17021
18013
  const status = formatStatus(request).padEnd(3);
17022
- const method = request.method.padEnd(4);
18014
+ const method = request.method.padEnd(widths.method);
17023
18015
  const type = getResourceTypeAbbr(request.resourceType, request.mimeType).padEnd(3);
17024
18016
  const size = formatSize(request.encodedDataLength).padStart(8);
17025
18017
  const time = formatDuration2(request.duration).padStart(6);
17026
- const urlMaxLength = verbose ? 120 : 50;
17027
- const url = verbose ? request.url : truncateUrl(request.url, urlMaxLength);
17028
- return `${id} ${status} ${method} ${type} ${size} ${time} ${url}`;
18018
+ const url = options.verbose ? request.url : truncateUrl(request.url, 50);
18019
+ return `${id} ${start} ${status} ${method} ${type} ${size} ${time} ${url}`;
18020
+ }
18021
+ function columnWidths(requests, minIdWidth) {
18022
+ return {
18023
+ id: Math.max(minIdWidth, ...requests.map((r) => r.requestId.length + 2)),
18024
+ method: Math.max("METH".length, ...requests.map((r) => r.method.length))
18025
+ };
17029
18026
  }
17030
18027
  function buildHeader(showingCount, filteredCount, totalCount) {
17031
18028
  const shown = filteredCount > showingCount ? `last ${showingCount} of ` : "";
@@ -17044,40 +18041,30 @@ function formatNetworkListHuman(requests, options) {
17044
18041
  fmt.text("No matching requests found.");
17045
18042
  return fmt.build();
17046
18043
  }
17047
- const idWidth = Math.max("[ID]".length, ...requests.map((r) => r.requestId.length + 2));
17048
- fmt.text(formatColumnHeader(idWidth));
18044
+ const widths = columnWidths(requests, "[ID]".length);
18045
+ fmt.text(formatColumnHeader(widths));
17049
18046
  fmt.separator("\u2500", SEPARATOR_WIDTH2);
17050
- const verbose = options.verbose ?? false;
17051
18047
  for (const request of requests) {
17052
- fmt.text(formatRequestLine(request, verbose, idWidth));
18048
+ fmt.text(formatRequestLine(request, options, widths));
17053
18049
  }
17054
18050
  return fmt.build();
17055
18051
  }
17056
18052
  var FOLLOW_ID_WIDTH = 14;
17057
18053
  function formatNetworkFollowRows(requests, options = {}) {
17058
18054
  const fmt = new OutputFormatter();
17059
- const idWidth = Math.max(FOLLOW_ID_WIDTH, ...requests.map((r) => r.requestId.length + 2));
18055
+ const widths = columnWidths(requests, FOLLOW_ID_WIDTH);
17060
18056
  if (options.header) {
17061
18057
  fmt.text("Streaming network requests... (Ctrl+C to stop)");
17062
- fmt.text(formatColumnHeader(idWidth));
18058
+ fmt.text(formatColumnHeader(widths));
17063
18059
  fmt.separator("\u2500", SEPARATOR_WIDTH2);
17064
18060
  }
17065
- for (const request of requests)
17066
- fmt.text(formatRequestLine(request, options.verbose ?? false, idWidth));
18061
+ for (const request of requests) fmt.text(formatRequestLine(request, options, widths));
17067
18062
  return fmt.build();
17068
18063
  }
17069
18064
  function formatNetworkList(requests, options) {
17070
18065
  return formatNetworkListHuman(requests, options);
17071
18066
  }
17072
18067
 
17073
- // src/ui/messages/networkMessages.ts
17074
- function followingNetworkMessage() {
17075
- return "Following network requests... (Ctrl+C to stop)";
17076
- }
17077
- function stoppedFollowingNetworkMessage() {
17078
- return "\nStopped following network requests.";
17079
- }
17080
-
17081
18068
  // src/commands/network/list.ts
17082
18069
  init_exitCodes();
17083
18070
  var MIN_LAST2 = 0;
@@ -17132,7 +18119,8 @@ function buildFormatOptions2(options, result, lastLimit) {
17132
18119
  verbose: options.verbose ?? false,
17133
18120
  last: lastLimit,
17134
18121
  totalCount: result.totalCount,
17135
- filteredCount: result.filteredCount
18122
+ filteredCount: result.filteredCount,
18123
+ ...result.pageStart && { pageStart: result.pageStart }
17136
18124
  };
17137
18125
  }
17138
18126
  async function runFollowMode2(options, resourceTypes, lastN) {
@@ -17161,9 +18149,11 @@ async function runFollowMode2(options, resourceTypes, lastN) {
17161
18149
  console.log(JSON.stringify(buildSuccessResponse(data), null, 2));
17162
18150
  }
17163
18151
  } else {
18152
+ const pageStart = pageStartOf(result.data);
17164
18153
  const text = formatNetworkFollowRows(fresh, {
17165
18154
  header: !started,
17166
- verbose: options.verbose ?? false
18155
+ verbose: options.verbose ?? false,
18156
+ ...pageStart && { pageStart }
17167
18157
  });
17168
18158
  if (text) console.log(text);
17169
18159
  }
@@ -17176,6 +18166,11 @@ async function runFollowMode2(options, resourceTypes, lastN) {
17176
18166
  intervalMs: FOLLOW_INTERVAL
17177
18167
  });
17178
18168
  }
18169
+ var COLUMNS_HELP = `Columns:
18170
+ START When the request started, from the start of the current page (its document
18171
+ request): +1.2s. Requests of earlier pages are negative. --json has the
18172
+ absolute time (timestamp, epoch ms) and data.pageStart.
18173
+ TIME How long it took (to its last byte or failure); - while pending`;
17179
18174
  function formatPresetHelp() {
17180
18175
  return Object.entries(FILTER_PRESETS).map(([name, preset]) => ` ${name.padEnd(12)} ${preset.description}`).join("\n");
17181
18176
  }
@@ -17195,11 +18190,16 @@ function registerListCommand(networkCmd) {
17195
18190
  "--type <types>",
17196
18191
  "Filter by resource type (comma-separated: Document,XHR,Fetch,etc.)"
17197
18192
  )
17198
- ).addOption(networkLastOption).addOption(new Option("-f, --follow", "Stream network requests in real-time").default(false)).addOption(new Option("-v, --verbose", "Show full URLs and additional details").default(false)).addHelpText("after", `
18193
+ ).addOption(networkLastOption).addOption(new Option("-f, --follow", "Stream network requests in real-time").default(false)).addOption(new Option("-v, --verbose", "Show full URLs and additional details").default(false)).addHelpText(
18194
+ "after",
18195
+ `
18196
+ ${COLUMNS_HELP}
18197
+
17199
18198
  ${getFilterHelpText()}
17200
18199
 
17201
18200
  Presets:
17202
- ${formatPresetHelp()}`).action(async (options) => {
18201
+ ${formatPresetHelp()}`
18202
+ ).action(async (options) => {
17203
18203
  let resourceTypes;
17204
18204
  let lastN;
17205
18205
  try {
@@ -17229,12 +18229,14 @@ ${formatPresetHelp()}`).action(async (options) => {
17229
18229
  return createErrorResult(result.error, result.exitCode, result.suggestion);
17230
18230
  }
17231
18231
  const filtered = filterRequests(result.data, options, resourceTypes);
18232
+ const pageStart = pageStartOf(result.data);
17232
18233
  return {
17233
18234
  success: true,
17234
18235
  data: {
17235
18236
  requests: lastN === 0 ? filtered : filtered.slice(-lastN),
17236
18237
  totalCount: result.data.length,
17237
- filteredCount: filtered.length
18238
+ filteredCount: filtered.length,
18239
+ ...pageStart && { pageStart }
17238
18240
  }
17239
18241
  };
17240
18242
  },
@@ -17254,1075 +18256,1169 @@ function registerNetworkCommands(program2) {
17254
18256
  registerDocumentCommand(networkCmd);
17255
18257
  }
17256
18258
 
17257
- // src/commands/page.ts
17258
- init_messages();
17259
- init_client();
17260
- init_formatting();
17261
- init_commands();
18259
+ // src/commands/start.ts
18260
+ import fs15 from "node:fs";
18261
+ import os3 from "node:os";
18262
+
18263
+ // src/daemon/errors.ts
17262
18264
  init_exitCodes();
17263
- init_url();
17264
- function formatPageResult(result) {
17265
- const fmt = new OutputFormatter().text(`\u2713 ${PAGE_ACTION_DONE[result.action]}`).keyValueList(
17266
- [
17267
- ["URL", result.url],
17268
- ["Title", result.title],
17269
- ...result.status !== void 0 ? [["Status", String(result.status)]] : []
17270
- ],
17271
- 8
17272
- );
17273
- if (result.warning) fmt.text(`\u26A0 ${result.warning}`);
17274
- if (result.loading) fmt.text(`\u26A0 ${pageLoadingWarning(result.loading)}`);
17275
- return fmt.build();
18265
+ var DaemonError = class _DaemonError extends Error {
18266
+ exitCode;
18267
+ code;
18268
+ constructor(message, code, exitCode = EXIT_CODES.SOFTWARE_ERROR) {
18269
+ super(message);
18270
+ this.name = "DaemonError";
18271
+ this.exitCode = exitCode;
18272
+ if (code !== void 0) {
18273
+ this.code = code;
18274
+ }
18275
+ if (Error.captureStackTrace) {
18276
+ Error.captureStackTrace(this, _DaemonError);
18277
+ }
18278
+ }
18279
+ };
18280
+ var DaemonStartupError = class extends DaemonError {
18281
+ name = "DaemonStartupError";
18282
+ constructor(message, code) {
18283
+ super(message, code, EXIT_CODES.SOFTWARE_ERROR);
18284
+ }
18285
+ };
18286
+ var SessionDirError = class extends DaemonError {
18287
+ /**
18288
+ * @param message - What is wrong with the directory
18289
+ * @param suggestion - How to fix it
18290
+ * @param exitCode - 82 for permissions, 81 for a too-long path, 103 otherwise
18291
+ */
18292
+ constructor(message, suggestion, exitCode = EXIT_CODES.SESSION_FILE_ERROR) {
18293
+ super(message, "SESSION_DIR_UNUSABLE", exitCode);
18294
+ this.suggestion = suggestion;
18295
+ }
18296
+ suggestion;
18297
+ name = "SessionDirError";
18298
+ };
18299
+
18300
+ // src/daemon/launcher.ts
18301
+ import { spawn } from "child_process";
18302
+ import fs14 from "fs";
18303
+ import { join as join7 } from "path";
18304
+ init_messages();
18305
+ init_paths();
18306
+ init_logging();
18307
+ init_errors();
18308
+ init_exitCodes();
18309
+ var log24 = createLogger("launcher");
18310
+ var DAEMON_READY_TIMEOUT_MS = 5e3;
18311
+ var DAEMON_READY_POLL_MS = 20;
18312
+ async function launchDaemon() {
18313
+ if (await isDaemonAlive()) {
18314
+ log24.debug("Daemon already running");
18315
+ return void 0;
18316
+ }
18317
+ if (!fs14.existsSync(DAEMON_SCRIPT_PATH)) {
18318
+ throw new DaemonStartupError(
18319
+ `Daemon script not found at ${DAEMON_SCRIPT_PATH}. Did you run 'npm run build'?`,
18320
+ "DAEMON_SCRIPT_NOT_FOUND"
18321
+ );
18322
+ }
18323
+ assertUsableSessionDir();
18324
+ const logPath = join7(getSessionDir(), "daemon.log");
18325
+ rotateLog(logPath);
18326
+ const logFd = fs14.openSync(logPath, "a");
18327
+ log24.debug(`Starting daemon: ${DAEMON_SCRIPT_PATH}`);
18328
+ const daemon = spawn(process.execPath, [DAEMON_SCRIPT_PATH], {
18329
+ detached: true,
18330
+ stdio: ["ignore", logFd, logFd]
18331
+ });
18332
+ fs14.closeSync(logFd);
18333
+ let exited = false;
18334
+ daemon.once("exit", () => {
18335
+ exited = true;
18336
+ });
18337
+ daemon.unref();
18338
+ await waitForDaemonReady(() => exited);
18339
+ return { pid: daemon.pid, hasExited: () => exited };
17276
18340
  }
17277
- function isScriptUrl(url) {
17278
- const [scheme = ""] = url.trim().split(":", 1);
17279
- return url.includes(":") && scheme.toLowerCase() === "javascript";
18341
+ function assertUsableSessionDir() {
18342
+ const dir = getSessionDir();
18343
+ const fail = (err, exitCode) => {
18344
+ throw new SessionDirError(err.message, err.suggestion, exitCode);
18345
+ };
18346
+ if (fs14.existsSync(dir) && !fs14.statSync(dir).isDirectory()) fail(sessionDirIsFileError(dir));
18347
+ const socketPath = getSessionFilePath("DAEMON_SOCKET");
18348
+ if (Buffer.byteLength(socketPath) > MAX_DAEMON_SOCKET_PATH_BYTES) {
18349
+ fail(
18350
+ socketPathTooLongError(socketPath, MAX_DAEMON_SOCKET_PATH_BYTES),
18351
+ EXIT_CODES.INVALID_ARGUMENTS
18352
+ );
18353
+ }
18354
+ try {
18355
+ ensureSessionDir();
18356
+ fs14.accessSync(dir, fs14.constants.W_OK);
18357
+ } catch (error) {
18358
+ const code = error.code;
18359
+ const denied = code === "EACCES" || code === "EPERM" || code === "EROFS";
18360
+ fail(
18361
+ sessionDirNotWritableError(dir, code ?? getErrorMessage(error)),
18362
+ denied ? EXIT_CODES.PERMISSION_DENIED : EXIT_CODES.SESSION_FILE_ERROR
18363
+ );
18364
+ }
17280
18365
  }
17281
- async function runPageAction(action, options, url) {
17282
- await runCommand(
17283
- async () => {
17284
- if (url !== void 0 && isScriptUrl(url)) {
17285
- const err = javascriptNavigationError();
17286
- return {
17287
- success: false,
17288
- error: err.message,
17289
- exitCode: EXIT_CODES.INVALID_URL,
17290
- errorContext: { suggestion: err.suggestion }
17291
- };
17292
- }
17293
- if (url !== void 0) {
17294
- const check = validateUrl(url);
17295
- if (!check.valid) {
17296
- return {
17297
- success: false,
17298
- error: check.error,
17299
- exitCode: EXIT_CODES.INVALID_URL,
17300
- ...check.suggestion && { errorContext: { suggestion: check.suggestion } }
17301
- };
17302
- }
17303
- }
17304
- const response = await pageNavigate({
17305
- action,
17306
- ...url !== void 0 && { url },
17307
- wait: options.wait !== false
17308
- });
17309
- if (response.status === "error" || !response.data) {
17310
- return {
17311
- success: false,
17312
- error: response.error ?? `Failed to ${action}`,
17313
- exitCode: response.exitCode ?? EXIT_CODES.SOFTWARE_ERROR,
17314
- ...response.suggestion && { errorContext: { suggestion: response.suggestion } }
17315
- };
17316
- }
17317
- return { success: true, data: response.data };
17318
- },
17319
- options,
17320
- formatPageResult
17321
- );
18366
+ var MAX_LOG_BYTES = 5 * 1024 * 1024;
18367
+ function rotateLog(logPath) {
18368
+ try {
18369
+ if (fs14.statSync(logPath).size > MAX_LOG_BYTES) {
18370
+ fs14.renameSync(logPath, `${logPath}.1`);
18371
+ }
18372
+ } catch (error) {
18373
+ log24.debug(`Daemon log not rotated: ${getErrorMessage(error)}`);
18374
+ }
17322
18375
  }
17323
- async function showPageInfo(options) {
17324
- await runCommand(
17325
- async () => {
17326
- const response = await getStatus();
17327
- if (response.status === "error") {
17328
- return {
17329
- success: false,
17330
- error: response.error ?? "Failed to read the page",
17331
- exitCode: EXIT_CODES.SOFTWARE_ERROR
17332
- };
17333
- }
17334
- const page = response.data?.sessionPid ? response.data.pageState : void 0;
17335
- if (!page) throw noActiveSessionError();
17336
- return { success: true, data: { url: page.url, title: page.title } };
17337
- },
17338
- options,
17339
- (page) => new OutputFormatter().keyValueList(
17340
- [
17341
- ["URL", page.url],
17342
- ["Title", page.title]
17343
- ],
17344
- 8
17345
- ).build()
18376
+ async function waitForDaemonReady(hasExited) {
18377
+ const deadline = Date.now() + DAEMON_READY_TIMEOUT_MS;
18378
+ while (Date.now() < deadline) {
18379
+ if (await isDaemonAlive()) {
18380
+ log24.debug("Daemon is ready");
18381
+ return;
18382
+ }
18383
+ if (hasExited() && !await isDaemonAlive()) {
18384
+ throw new DaemonStartupError(
18385
+ `Daemon exited during startup. See ${join7(getSessionDir(), "daemon.log")}`,
18386
+ "DAEMON_EXITED"
18387
+ );
18388
+ }
18389
+ await delay2(DAEMON_READY_POLL_MS);
18390
+ }
18391
+ throw new DaemonStartupError(
18392
+ `Daemon failed to start within ${DAEMON_READY_TIMEOUT_MS / 1e3} seconds`,
18393
+ "DAEMON_START_TIMEOUT"
17346
18394
  );
17347
18395
  }
17348
- function registerPageCommands(program2) {
17349
- const page = program2.command("page").description("The session page: info (URL and title), navigate <url>, reload, back, forward");
17350
- page.command("info").description(PAGE_INFO_DESCRIPTION).addOption(jsonOption()).action(async (options) => {
17351
- await showPageInfo(options);
17352
- });
17353
- const withCommon = (command) => command.option("--no-wait", "Return without waiting for the page to load").addOption(jsonOption());
17354
- withCommon(
17355
- page.command("navigate").description("Load a URL in the session page").argument("<url>", "URL to load")
17356
- ).action(async (url, options) => {
17357
- await runPageAction("navigate", options, url);
17358
- });
17359
- for (const action of ["reload", "back", "forward"]) {
17360
- withCommon(page.command(action).description(PAGE_ACTION_DESCRIPTIONS[action])).action(
17361
- async (options) => {
17362
- await runPageAction(action, options);
17363
- }
17364
- );
18396
+
18397
+ // src/commands/shared/startHelpers.ts
18398
+ init_messages();
18399
+ init_client();
18400
+ init_transport();
18401
+ init_errors2();
18402
+ init_paths();
18403
+ init_formatting();
18404
+ init_logging();
18405
+ init_sessionCommand();
18406
+ init_errors();
18407
+ init_exitCodes();
18408
+ var log25 = createLogger("bdg");
18409
+ var SPAWNED_DAEMON_EXIT_WAIT_MS = 3e3;
18410
+ var DEFAULT_START_DEPS = {
18411
+ launch: launchDaemon,
18412
+ send: startSession,
18413
+ exitWaitMs: SPAWNED_DAEMON_EXIT_WAIT_MS
18414
+ };
18415
+ async function startSessionViaDaemon(url, options, telemetry) {
18416
+ process.once("SIGINT", () => reportStartOutcome(interruptedOutcome("SIGINT"), options));
18417
+ process.once("SIGTERM", () => reportStartOutcome(interruptedOutcome("SIGTERM"), options));
18418
+ reportStartOutcome(await attemptStart(url, options, telemetry), options);
18419
+ }
18420
+ async function attemptStart(url, options, telemetry, deps = DEFAULT_START_DEPS) {
18421
+ const spawned = [];
18422
+ const attempt = async () => {
18423
+ const outcome2 = await requestSession(url, options, telemetry, deps);
18424
+ if (!outcome2.ok && outcome2.spawned) spawned.push(outcome2.spawned);
18425
+ return outcome2;
18426
+ };
18427
+ let outcome = await attempt();
18428
+ const deadline = Date.now() + SHUTDOWN_WAIT_MS;
18429
+ while (isShuttingDown(outcome) && Date.now() < deadline) {
18430
+ await delay2(SHUTDOWN_POLL_MS);
18431
+ outcome = await attempt();
17365
18432
  }
18433
+ return afterSpawnedDaemonExit(outcome, spawned, deps.exitWaitMs);
17366
18434
  }
17367
-
17368
- // src/commands/peek.ts
17369
- function parseOptions(options) {
17370
- const lastN = positiveIntRule({
17371
- name: "--last",
17372
- min: 1,
17373
- max: MAX_LAST_ITEMS,
17374
- default: 10,
17375
- allowZeroForAll: true
17376
- }).validate(options.last);
17377
- const resourceTypes = resourceTypeRule().validate(options.type);
17378
- return { lastN, resourceTypes };
18435
+ async function afterSpawnedDaemonExit(outcome, spawned, waitMs) {
18436
+ if (outcome.ok) return outcome;
18437
+ const { spawned: _spawned, daemonExiting: _exiting, ...failure } = outcome;
18438
+ if (spawned.length === 0) return failure;
18439
+ if (await waitUntil(() => spawned.every((daemon) => daemon.hasExited()), waitMs)) {
18440
+ return failure;
18441
+ }
18442
+ const running = spawned.filter((daemon) => !daemon.hasExited()).pop();
18443
+ const suggestion = daemonStillExitingSuggestion();
18444
+ const previous = failure.details?.["suggestion"];
18445
+ return {
18446
+ ...failure,
18447
+ human: joinLines(
18448
+ failure.human,
18449
+ `${daemonStillExitingHint(running?.pid, waitMs)}; ${suggestion}`
18450
+ ),
18451
+ details: {
18452
+ ...failure.details,
18453
+ daemonStillRunning: true,
18454
+ ...running?.pid !== void 0 && { daemonPid: running.pid },
18455
+ suggestion: typeof previous === "string" && previous ? `${previous}; ${suggestion}` : suggestion
18456
+ }
18457
+ };
17379
18458
  }
17380
- function peekSection(options) {
17381
- if (options.network) return "network";
17382
- return options.console ? "console" : void 0;
18459
+ function interruptedOutcome(signal) {
18460
+ const message = `Start cancelled (${signal === "SIGINT" ? "interrupted" : "terminated"})`;
18461
+ const exitCode = signal === "SIGINT" ? EXIT_CODES.INTERRUPTED : EXIT_CODES.TERMINATED;
18462
+ return { ok: false, error: message, human: message, exitCode };
17383
18463
  }
17384
- async function fetchAndFilterPreview(lastN, resourceTypes, only) {
17385
- const result = await fetchPreviewOutput({
17386
- lastN: resourceTypes.length > 0 ? 0 : lastN,
17387
- ...only && { only }
18464
+ var SHUTDOWN_WAIT_MS = 15e3;
18465
+ var SHUTDOWN_POLL_MS = 200;
18466
+ function isShuttingDown(outcome) {
18467
+ return !outcome.ok && (outcome.retryable === true || outcome.errorCode === "SESSION_SHUTTING_DOWN" /* SESSION_SHUTTING_DOWN */);
18468
+ }
18469
+ async function requestSession(url, options, telemetry, deps) {
18470
+ let spawned;
18471
+ try {
18472
+ spawned = await deps.launch();
18473
+ } catch (error) {
18474
+ if (error instanceof SessionDirError) {
18475
+ return {
18476
+ ok: false,
18477
+ error: error.message,
18478
+ human: joinLines(genericError(error.message), error.suggestion),
18479
+ exitCode: error.exitCode,
18480
+ details: { suggestion: error.suggestion }
18481
+ };
18482
+ }
18483
+ const message = `Failed to start daemon: ${getErrorMessage(error)}`;
18484
+ const exitCode = error instanceof DaemonError ? error.exitCode : EXIT_CODES.SOFTWARE_ERROR;
18485
+ return { ok: false, error: message, human: genericError(message), exitCode };
18486
+ }
18487
+ const outcome = await sendStart(url, options, telemetry, deps.send);
18488
+ return !outcome.ok && outcome.daemonExiting && spawned ? { ...outcome, spawned } : outcome;
18489
+ }
18490
+ async function sendStart(url, options, telemetry, send) {
18491
+ try {
18492
+ log25.debug("Connecting to daemon...");
18493
+ const response = await send(
18494
+ url,
18495
+ filterDefined({
18496
+ port: options.port,
18497
+ timeout: options.timeout,
18498
+ telemetry: telemetry.length > 0 ? telemetry : void 0,
18499
+ includeAll: options.includeAll,
18500
+ userDataDir: options.userDataDir,
18501
+ maxBodySize: options.maxBodySize,
18502
+ headless: options.headless,
18503
+ chromeWsUrl: options.chromeWsUrl,
18504
+ chromeFlags: options.chromeFlags,
18505
+ viewport: options.viewport,
18506
+ colorScheme: options.colorScheme
18507
+ })
18508
+ );
18509
+ if (response.status === "error") {
18510
+ return { ...describeStartFailure(response, options), daemonExiting: true };
18511
+ }
18512
+ if (!response.data) {
18513
+ const message = "Invalid response from daemon: missing data";
18514
+ return {
18515
+ ok: false,
18516
+ error: message,
18517
+ human: invalidResponseError("missing data"),
18518
+ exitCode: EXIT_CODES.SOFTWARE_ERROR
18519
+ };
18520
+ }
18521
+ return { ok: true, data: response.data };
18522
+ } catch (error) {
18523
+ if (error instanceof IPCTimeoutError) {
18524
+ const timeout = timeoutError(error);
18525
+ return {
18526
+ ok: false,
18527
+ error: timeout.message,
18528
+ human: joinLines(genericError(timeout.message), String(timeout.metadata.suggestion)),
18529
+ exitCode: timeout.exitCode,
18530
+ details: { suggestion: timeout.metadata.suggestion }
18531
+ };
18532
+ }
18533
+ if (isConnectionError(error)) {
18534
+ return {
18535
+ ok: false,
18536
+ error: `${noActiveSessionMessage()} (daemon not running)`,
18537
+ human: daemonNotRunningError({ suggestStatus: true, suggestRetry: true }),
18538
+ exitCode: EXIT_CODES.RESOURCE_NOT_FOUND,
18539
+ retryable: true,
18540
+ daemonExiting: true
18541
+ };
18542
+ }
18543
+ const message = getErrorMessage(error);
18544
+ return {
18545
+ ok: false,
18546
+ error: message,
18547
+ human: genericError(message),
18548
+ exitCode: EXIT_CODES.SOFTWARE_ERROR
18549
+ };
18550
+ }
18551
+ }
18552
+ function omitDuration(existing) {
18553
+ const { duration: _duration, ...rest } = existing;
18554
+ return rest;
18555
+ }
18556
+ function describeStartFailure(response, options) {
18557
+ const exitCode = response.exitCode ?? getExitCodeForIPCError(response.errorCode);
18558
+ const existing = response.existingSession;
18559
+ const details = filterDefined({
18560
+ existingSession: existing && {
18561
+ ...omitDuration(existing),
18562
+ ...existing.duration !== void 0 && { durationMs: existing.duration * 1e3 }
18563
+ }
17388
18564
  });
17389
- if (!result.success) return result;
17390
- const output = result.data;
17391
- const unfilteredNetworkCount = output.totals?.network ?? output.data.network?.length ?? 0;
17392
- if (resourceTypes.length === 0 || !output.data.network) {
17393
- return { success: true, data: { output, unfilteredNetworkCount } };
18565
+ if (response.errorCode === "SESSION_ALREADY_RUNNING" /* SESSION_ALREADY_RUNNING */ && existing) {
18566
+ const { pid, targetUrl, duration } = existing;
18567
+ return {
18568
+ ok: false,
18569
+ error: response.message ?? "Session already running",
18570
+ human: sessionAlreadyRunningError(pid, duration ? duration * 1e3 : 0, targetUrl),
18571
+ exitCode,
18572
+ errorCode: response.errorCode,
18573
+ details: { ...details, suggestion: alreadyRunningSuggestion() }
18574
+ };
17394
18575
  }
17395
- const network = filterByResourceType(output.data.network, resourceTypes);
17396
- const filteredOutput = {
17397
- ...output,
17398
- data: { ...output.data, network },
17399
- ...output.totals && { totals: { ...output.totals, network: network.length } }
17400
- };
17401
- return { success: true, data: { output: filteredOutput, unfilteredNetworkCount } };
17402
- }
17403
- function createPreviewOptions(base, resourceTypes, unfilteredCount) {
17404
- const options = { ...base };
17405
- if (base.follow) options.viewedAt = /* @__PURE__ */ new Date();
17406
- if (resourceTypes.length > 0) {
17407
- options.filteredTypes = resourceTypes;
17408
- options.unfilteredNetworkCount = unfilteredCount;
18576
+ if (response.errorCode === "SESSION_TARGET_MISMATCH" /* SESSION_TARGET_MISMATCH */) {
18577
+ const current = response.existingSession?.targetUrl ?? LAUNCHED_CHROME_DESCRIPTION;
18578
+ const requested = options.chromeWsUrl ?? LAUNCHED_CHROME_DESCRIPTION;
18579
+ return {
18580
+ ok: false,
18581
+ error: response.message ?? "Session target mismatch",
18582
+ human: sessionTargetMismatchError(current, requested),
18583
+ exitCode,
18584
+ errorCode: response.errorCode,
18585
+ details
18586
+ };
17409
18587
  }
17410
- return options;
17411
- }
17412
- async function runFollowMode3(options, lastN, resourceTypes, baseOptions) {
17413
- const showPreview = async () => {
17414
- const result = await fetchAndFilterPreview(lastN, resourceTypes, peekSection(options));
17415
- if (!result.success) {
17416
- return followFetchFailure(result, { json: options.json, retryIntervalMs: 1e3 });
18588
+ const message = response.message ?? "Unknown error";
18589
+ const [error = message, ...hint] = message.split("\n");
18590
+ return {
18591
+ ok: false,
18592
+ error,
18593
+ human: genericError(message),
18594
+ exitCode,
18595
+ errorCode: response.errorCode,
18596
+ details: {
18597
+ ...details,
18598
+ ...hint.join("\n").trim() && { suggestion: hint.join("\n").trim() }
17417
18599
  }
17418
- noteFollowConnected();
17419
- if (!options.json) console.clear();
17420
- const previewOptions = createPreviewOptions(
17421
- baseOptions,
17422
- resourceTypes,
17423
- result.data.unfilteredNetworkCount
17424
- );
17425
- console.log(formatPreview(result.data.output, previewOptions));
17426
- return void 0;
17427
18600
  };
17428
- await setupFollowMode(showPreview, {
17429
- startMessage: followingPreviewMessage,
17430
- stopMessage: stoppedFollowingPreviewMessage,
17431
- intervalMs: 1e3
17432
- });
17433
18601
  }
17434
- function registerPeekCommand(program2) {
17435
- program2.command("peek").description("Preview collected data without stopping the session").addOption(jsonOption()).option("-v, --verbose", "Use verbose output with full URLs and formatting", false).option("-n, --network", "Show only network requests", false).option("-c, --console", "Show only console messages", false).option("-f, --follow", "Watch for updates (like tail -f)", false).option("--last <count>", "Show last N items, 0 for all", "10").option(
17436
- "--type <types>",
17437
- "Filter network requests by resource type (comma-separated: Document,XHR,Fetch,etc.)"
17438
- ).action(async (options) => {
17439
- showBothSectionsWhenBothRequested(options);
17440
- if (options.network && !options.json) {
17441
- console.error(
17442
- 'Note: "bdg peek --network" is deprecated. Use "bdg network list" for enhanced filtering.'
17443
- );
17444
- }
17445
- let lastN;
17446
- let resourceTypes;
17447
- try {
17448
- const parsed = parseOptions(options);
17449
- lastN = parsed.lastN;
17450
- resourceTypes = parsed.resourceTypes;
17451
- } catch (error) {
17452
- handleValidationError(error, options.json ?? false);
18602
+ function reportStartOutcome(outcome, options) {
18603
+ if (!outcome.ok) {
18604
+ if (options.json) {
18605
+ const envelope = OutputBuilder.buildJsonError(outcome.error, {
18606
+ exitCode: outcome.exitCode,
18607
+ ...outcome.details
18608
+ });
18609
+ console.log(JSON.stringify(envelope, null, 2));
18610
+ } else {
18611
+ console.error(escapeControlChars(outcome.human));
17453
18612
  }
17454
- const baseOptions = {
17455
- json: options.json,
17456
- network: options.network,
17457
- console: options.console,
17458
- last: lastN,
17459
- verbose: options.verbose,
17460
- follow: options.follow
18613
+ process.exit(outcome.exitCode);
18614
+ }
18615
+ const { data } = outcome;
18616
+ const autoStopAt = options.timeout !== void 0 ? new Date(Date.now() + options.timeout * 1e3) : void 0;
18617
+ const session = getSessionName() ?? void 0;
18618
+ if (options.json) {
18619
+ const result = {
18620
+ ...session && { session },
18621
+ targetUrl: data.targetUrl,
18622
+ ...data.targetTitle !== void 0 && { targetTitle: data.targetTitle },
18623
+ ...data.documentStatus !== void 0 && { documentStatus: data.documentStatus },
18624
+ ...data.loading && { loading: data.loading },
18625
+ port: data.port,
18626
+ ...data.chromePid > 0 ? { chromePid: data.chromePid } : { externalChrome: true },
18627
+ daemonPid: data.daemonPid,
18628
+ ...autoStopAt && { autoStopAt: autoStopAt.toISOString() }
17461
18629
  };
17462
- if (options.follow) {
17463
- await runFollowMode3(options, lastN, resourceTypes, baseOptions);
17464
- return;
17465
- }
17466
- await runCommand(
17467
- async () => {
17468
- const result = await fetchAndFilterPreview(lastN, resourceTypes, peekSection(options));
17469
- if (!result.success) {
17470
- return createErrorResult(result.error, result.exitCode, result.suggestion);
17471
- }
17472
- const { output } = result.data;
17473
- return {
17474
- success: true,
17475
- data: options.json ? buildPreviewJsonData(output, baseOptions) : output
17476
- };
17477
- },
17478
- options,
17479
- (data) => {
17480
- const output = data;
17481
- const previewOptions = createPreviewOptions(
17482
- baseOptions,
17483
- resourceTypes,
17484
- output.data.network?.length ?? 0
17485
- );
17486
- return formatPreview(output, previewOptions);
17487
- }
17488
- );
17489
- });
18630
+ console.log(JSON.stringify(buildSuccessResponse(result), null, 2));
18631
+ } else {
18632
+ const page = {
18633
+ url: data.targetUrl,
18634
+ ...data.documentStatus !== void 0 && { documentStatus: data.documentStatus },
18635
+ ...data.loading && { loading: data.loading },
18636
+ ...autoStopAt && { autoStopAt },
18637
+ ...session && { session }
18638
+ };
18639
+ const text = options.quiet ? [`Session started: ${data.targetUrl}`, ...startNotices(page)].join("\n") : landingPage(page);
18640
+ console.error(escapeControlChars(text));
18641
+ }
18642
+ process.exit(EXIT_CODES.SUCCESS);
17490
18643
  }
17491
18644
 
17492
- // src/session/sessionList.ts
17493
- init_client();
17494
- import * as fs15 from "fs";
17495
- init_paths();
18645
+ // src/commands/start.ts
18646
+ init_constants();
18647
+ init_errors3();
18648
+ init_messages();
18649
+ init_commands();
18650
+ init_exitCodes();
17496
18651
 
17497
- // src/session/portClaims.ts
17498
- import * as fs14 from "fs";
17499
- init_paths();
17500
- init_logging();
17501
- var log24 = createLogger("session");
17502
- function readPortFile(portPath) {
18652
+ // src/utils/http.ts
18653
+ init_constants();
18654
+ init_errors();
18655
+ var CDP_HTTP_TIMEOUT_MS = 5e3;
18656
+ async function probeDevToolsEndpoint(port, logger, options) {
18657
+ const url = `${options?.secure ? "https" : "http"}://${options?.host ?? HTTP_LOCALHOST}:${port}/json/version`;
18658
+ let response;
17503
18659
  try {
17504
- const port = parseInt(fs14.readFileSync(portPath, "utf-8").trim(), 10);
17505
- return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
18660
+ response = await fetch(url, { signal: AbortSignal.timeout(CDP_HTTP_TIMEOUT_MS) });
17506
18661
  } catch (error) {
17507
- logDebugError(log24, `read ${portPath}`, error);
17508
- return null;
18662
+ logger?.debug(`Chrome version request failed: ${getErrorMessage(error)} (${url})`);
18663
+ return { kind: "unreachable" };
18664
+ }
18665
+ try {
18666
+ const data = await response.json();
18667
+ const wsUrl = data?.webSocketDebuggerUrl;
18668
+ if (response.ok && typeof wsUrl === "string") return { kind: "devtools", wsUrl };
18669
+ } catch (error) {
18670
+ logger?.debug(`Chrome version response is not JSON: ${getErrorMessage(error)} (${url})`);
17509
18671
  }
18672
+ return { kind: "not-devtools" };
17510
18673
  }
17511
18674
 
17512
- // src/session/sessionName.ts
17513
- init_errors3();
17514
- init_messages();
17515
- init_paths();
17516
- init_exitCodes();
17517
- var MAX_SESSION_NAME_LENGTH = 40;
17518
- var SESSION_NAME_PATTERN = new RegExp(
17519
- `^[A-Za-z0-9][A-Za-z0-9_-]{0,${MAX_SESSION_NAME_LENGTH - 1}}$`
17520
- );
17521
- function isValidSessionName(name) {
17522
- return SESSION_NAME_PATTERN.test(name);
18675
+ // src/commands/start.ts
18676
+ init_suggestions();
18677
+ init_url();
18678
+ function hasDisplay() {
18679
+ const display = process.env["DISPLAY"];
18680
+ const wayland = process.env["WAYLAND_DISPLAY"];
18681
+ return display !== void 0 && display !== "" || wayland !== void 0 && wayland !== "";
18682
+ }
18683
+ function expandHome(value) {
18684
+ return value.startsWith("~/") ? value.replace(/^~/, os3.homedir()) : value;
18685
+ }
18686
+ function extractUserDataDirFromFlags(flags) {
18687
+ const rest = [];
18688
+ let userDataDir;
18689
+ for (let i = 0; i < flags.length; i++) {
18690
+ const flag = flags[i];
18691
+ if (flag.startsWith("--user-data-dir=")) {
18692
+ userDataDir = flag.slice("--user-data-dir=".length);
18693
+ continue;
18694
+ }
18695
+ if (flag === "--user-data-dir" && i + 1 < flags.length) {
18696
+ userDataDir = flags[i + 1];
18697
+ i++;
18698
+ continue;
18699
+ }
18700
+ rest.push(flag);
18701
+ }
18702
+ return {
18703
+ userDataDir: userDataDir ? expandHome(userDataDir) : void 0,
18704
+ rest
18705
+ };
18706
+ }
18707
+ function applyCollectorOptions(command) {
18708
+ const defaultHeadless = !hasDisplay();
18709
+ return command.option("-p, --port <number>", PORT_OPTION_DESCRIPTION).option(
18710
+ "-t, --timeout <seconds>",
18711
+ "Stop the session this many seconds after the page has loaded (unlimited if not specified)"
18712
+ ).option("-u, --user-data-dir <path>", "Chrome user data directory (defaults to session dir)").option(
18713
+ "-a, --all",
18714
+ "Include all data: no tracking/analytics filtering, and capture every response body (incl. binary)",
18715
+ false
18716
+ ).option("-m, --max-body-size <megabytes>", "Maximum response body size in MB", "5").addOption(new Option("--compact", "No effect; kept for compatibility").hideHelp()).option(
18717
+ "--headless",
18718
+ "Run Chrome without a window (default unless DISPLAY or WAYLAND_DISPLAY is set)",
18719
+ defaultHeadless
18720
+ ).option("--no-headless", "Show browser window").option(
18721
+ "--chrome-ws-url <url>",
18722
+ "Connect to an existing Chrome: its DevTools port (9222, host:port, http://host:port), or a WebSocket URL: browser (ws://host:port/devtools/browser/<id>, uses the first tab) or page (.../devtools/page/<id>)"
18723
+ ).option("-q, --quiet", "Quiet mode - minimal output for AI agents", false).addOption(jsonOption()).option(
18724
+ "--chrome-flags <flags>",
18725
+ 'Custom Chrome flags (space-separated, e.g., --chrome-flags="--ignore-certificate-errors --disable-web-security")'
18726
+ ).option(
18727
+ "--viewport <WxH>",
18728
+ "Viewport size in CSS px for the whole session, e.g. 1280x800 (default: 1920x1080 window)"
18729
+ ).option(
18730
+ "--color-scheme <scheme>",
18731
+ "Emulate prefers-color-scheme for the session: light or dark (default: the system setting)"
18732
+ );
17523
18733
  }
17524
- function normalizeSessionName(name) {
17525
- return name.toLowerCase();
18734
+ var MAX_VIEWPORT_SIDE = 1e4;
18735
+ var COLOR_SCHEMES = ["light", "dark"];
18736
+ function parseViewport(value) {
18737
+ const match = /^\s*(\d+)\s*[xX×,]\s*(\d+)\s*$/.exec(value);
18738
+ const width = Number(match?.[1]);
18739
+ const height = Number(match?.[2]);
18740
+ const valid2 = (side) => side >= 1 && side <= MAX_VIEWPORT_SIDE;
18741
+ if (match && valid2(width) && valid2(height)) return { width, height };
18742
+ const err = invalidViewportError(value, MAX_VIEWPORT_SIDE);
18743
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
17526
18744
  }
17527
- function validateSessionName(name) {
17528
- if (!isValidSessionName(name)) {
17529
- throwInvalid(invalidSessionNameError(name, MAX_SESSION_NAME_LENGTH));
17530
- }
17531
- const shortest = namedSocketPath("a");
17532
- if (Buffer.byteLength(shortest) > MAX_DAEMON_SOCKET_PATH_BYTES) {
17533
- throwInvalid(socketPathTooLongError(shortest, MAX_DAEMON_SOCKET_PATH_BYTES));
17534
- }
17535
- const socketPath = namedSocketPath(name);
17536
- if (Buffer.byteLength(socketPath) > MAX_DAEMON_SOCKET_PATH_BYTES) {
17537
- throwInvalid(sessionNameSocketTooLongError(name, socketPath, MAX_DAEMON_SOCKET_PATH_BYTES));
17538
- }
18745
+ function parseColorScheme(value) {
18746
+ const scheme = COLOR_SCHEMES.find((candidate) => candidate === value.trim().toLowerCase());
18747
+ if (scheme) return scheme;
18748
+ const err = invalidColorSchemeError(value, findSimilar(value, [...COLOR_SCHEMES]), COLOR_SCHEMES);
18749
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
17539
18750
  }
17540
- function namedSocketPath(name) {
17541
- return sessionFilePathIn(getNamedSessionDir(name), "DAEMON_SOCKET");
18751
+ function buildSessionOptions(options) {
18752
+ const maxBodySizeRule = positiveIntRule({
18753
+ name: "--max-body-size",
18754
+ min: 1,
18755
+ max: 100,
18756
+ required: false
18757
+ });
18758
+ const timeoutRule = positiveIntRule({ name: "--timeout", min: 1, max: 3600, required: false });
18759
+ const portRule = positiveIntRule({ name: "--port", min: 1024, max: 65535, required: false });
18760
+ const maxBodySizeMB = options.maxBodySize !== void 0 ? maxBodySizeRule.validate(options.maxBodySize) : void 0;
18761
+ const timeout = options.timeout !== void 0 ? timeoutRule.validate(options.timeout) : void 0;
18762
+ const envFlags = process.env["BDG_CHROME_FLAGS"]?.split(" ").filter(Boolean) ?? [];
18763
+ const cliFlags = options.chromeFlags?.split(" ").filter(Boolean) ?? [];
18764
+ const { userDataDir: flagUserDataDir, rest: combinedFlags } = extractUserDataDirFromFlags([
18765
+ ...envFlags,
18766
+ ...cliFlags
18767
+ ]);
18768
+ const userDataDir = options.userDataDir ? expandHome(options.userDataDir) : flagUserDataDir;
18769
+ const chromeFlags = combinedFlags.length > 0 ? combinedFlags : void 0;
18770
+ return {
18771
+ port: options.port !== void 0 ? portRule.validate(options.port) : void 0,
18772
+ timeout,
18773
+ userDataDir,
18774
+ includeAll: options.all ?? false,
18775
+ maxBodySize: maxBodySizeMB !== void 0 ? maxBodySizeMB * 1024 * 1024 : void 0,
18776
+ headless: options.headless ?? !hasDisplay(),
18777
+ chromeWsUrl: options.chromeWsUrl,
18778
+ quiet: options.quiet ?? false,
18779
+ json: options.json ?? false,
18780
+ chromeFlags,
18781
+ viewport: options.viewport !== void 0 ? parseViewport(options.viewport) : void 0,
18782
+ colorScheme: options.colorScheme !== void 0 ? parseColorScheme(options.colorScheme) : void 0
18783
+ };
17542
18784
  }
17543
- function throwInvalid(err) {
18785
+ var SESSION_TELEMETRY = ["dom", "network", "console"];
18786
+ var FLAG_WORDS = { version: "--version", help: "--help" };
18787
+ function assertNotCommandTypo(arg, commandNames) {
18788
+ if (!/^[a-z][a-z0-9_-]*$/i.test(arg) || arg.toLowerCase() === "localhost") return;
18789
+ const flag = FLAG_WORDS[arg.toLowerCase()];
18790
+ const err = unknownCommandError(arg, flag ? [flag] : findSimilar(arg, commandNames));
17544
18791
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
17545
18792
  }
17546
- function selectSession(optionValue) {
17547
- const name = optionValue ?? process.env[SESSION_NAME_ENV]?.trim();
17548
- if (name === void 0 || optionValue === void 0 && name === "") {
17549
- delete process.env[SESSION_NAME_ENV];
17550
- return;
18793
+ function assertNotGroupSubcommand(program2, argv) {
18794
+ const [first] = argv.slice(2).filter((arg) => !arg.startsWith("-"));
18795
+ if (first === void 0 || program2.commands.some((command) => command.name() === first)) return;
18796
+ const group = program2.commands.find(
18797
+ (command) => command.commands.some((sub) => sub.name() === first)
18798
+ );
18799
+ if (!group) return;
18800
+ const err = unknownCommandError(first, [`${group.name()} ${first}`]);
18801
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
18802
+ }
18803
+ function validateStartInput(url, options, program2) {
18804
+ if (url === void 0) {
18805
+ const err = missingStartUrlError();
18806
+ throw new CommandError(
18807
+ err.message,
18808
+ { suggestion: err.suggestion },
18809
+ EXIT_CODES.INVALID_ARGUMENTS
18810
+ );
17551
18811
  }
17552
- validateSessionName(name);
17553
- process.env[SESSION_NAME_ENV] = normalizeSessionName(name);
18812
+ assertNotCommandTypo(
18813
+ url,
18814
+ program2.commands.map((command) => command.name())
18815
+ );
18816
+ assertValidUrl(url);
18817
+ if (options.chromeWsUrl !== void 0) {
18818
+ assertValidChromeWsUrl(options.chromeWsUrl);
18819
+ assertNoLaunchOptions(options, program2);
18820
+ }
18821
+ if (options.userDataDir !== void 0) assertUserDataDir(options.userDataDir);
18822
+ if (options.chromeWsUrl === void 0)
18823
+ assertChromeFlags([
18824
+ ...process.env["BDG_CHROME_FLAGS"]?.split(" ") ?? [],
18825
+ ...options.chromeFlags?.split(" ") ?? []
18826
+ ]);
18827
+ return { url, sessionOptions: buildSessionOptions(options) };
17554
18828
  }
17555
-
17556
- // src/session/sessionList.ts
17557
- init_logging();
17558
- init_sessionCommand();
17559
- var log25 = createLogger("session");
17560
- function toRunningSession(name, data) {
17561
- const meta = data.sessionMetadata;
17562
- const state = meta ? "active" : data.ending ? "ending" : "starting";
17563
- const url = data.pageState?.url ?? data.starting?.url;
17564
- return {
17565
- name,
17566
- state,
17567
- ...url !== void 0 && { url },
17568
- ...meta && { port: meta.port },
17569
- daemonPid: data.daemonPid,
17570
- ...meta?.chromePid && { chromePid: meta.chromePid }
17571
- };
18829
+ function registerStartCommands(program2) {
18830
+ applyCollectorOptions(
18831
+ program2.argument("[url]", "Target URL (example.com or localhost:3000)")
18832
+ ).action(async (url, options) => {
18833
+ if (url === void 0 && !hasUserOptions(program2)) {
18834
+ console.error(startCommandHelpMessage());
18835
+ process.exit(0);
18836
+ }
18837
+ let validated;
18838
+ try {
18839
+ validated = validateStartInput(url, options, program2);
18840
+ validated.sessionOptions.chromeWsUrl = await resolveChromeWsUrl(
18841
+ validated.sessionOptions.chromeWsUrl
18842
+ );
18843
+ } catch (error) {
18844
+ handleValidationError(error, options.json ?? false);
18845
+ }
18846
+ await startSessionViaDaemon(validated.url, validated.sessionOptions, SESSION_TELEMETRY);
18847
+ });
17572
18848
  }
17573
- async function describeSession(entry) {
17574
- const { name, dir } = entry;
17575
- const socketPath = sessionFilePathIn(dir, "DAEMON_SOCKET");
17576
- const probe = await probeDaemonSocket(socketPath);
17577
- if (probe !== "alive") return describeWithoutSocket(entry, probe);
17578
- try {
17579
- const response = await getStatus(socketPath);
17580
- if (response.status === "ok" && response.data) return toRunningSession(name, response.data);
17581
- } catch (error) {
17582
- logDebugError(log25, `get status of ${dir}`, error);
17583
- }
17584
- return { name, state: "unresponsive" };
18849
+ async function resolveChromeWsUrl(value) {
18850
+ const endpoint = value === void 0 ? null : devToolsHttpEndpoint(value);
18851
+ if (endpoint === null) return value;
18852
+ const { hostname, port, protocol } = new URL(endpoint);
18853
+ const secure = protocol === "https:";
18854
+ const defaultPort = secure ? 443 : 80;
18855
+ const probe = await probeDevToolsEndpoint(Number(port) || defaultPort, void 0, {
18856
+ host: hostname,
18857
+ secure
18858
+ });
18859
+ if (probe.kind === "devtools") return atEndpoint(probe.wsUrl, endpoint);
18860
+ const err = probe.kind === "not-devtools" ? notDevToolsEndpointError(endpoint) : externalChromeUnreachableError(endpoint, false);
18861
+ throw new CommandError(
18862
+ err.message,
18863
+ { suggestion: err.suggestion },
18864
+ EXIT_CODES.CDP_CONNECTION_FAILURE
18865
+ );
17585
18866
  }
17586
- function describeWithoutSocket(entry, probe) {
17587
- const daemonPid = readLiveDaemonPid(entry.dir);
17588
- if (daemonPid !== null) return { name: entry.name, state: "starting", daemonPid };
17589
- return describeLeftovers(entry, probe);
18867
+ function atEndpoint(wsUrl, endpoint) {
18868
+ const resolved = new URL(wsUrl);
18869
+ const target = new URL(endpoint);
18870
+ resolved.protocol = target.protocol === "https:" ? "wss:" : "ws:";
18871
+ resolved.host = target.host;
18872
+ return resolved.toString();
17590
18873
  }
17591
- function describeLeftovers({ name, dir }, probe) {
17592
- const chromePid = readPidFromFile(sessionFilePathIn(dir, "CHROME_PID"));
17593
- const orphan = chromePid !== null && isProcessAlive(chromePid) && isSessionChrome(chromePid, dir);
17594
- const leftover = probe === "stale" || SESSION_STATE_FILES.some((type) => fs15.existsSync(sessionFilePathIn(dir, type)));
17595
- if (!orphan && !leftover) return null;
17596
- const port = leftoverPort(dir);
17597
- return {
17598
- name,
17599
- state: orphan ? "crashed" : "stale",
17600
- ...port !== null && { port },
17601
- ...orphan && { chromePid },
17602
- cleanup: sessionCommand("bdg cleanup", name)
17603
- };
18874
+ function hasUserOptions(program2) {
18875
+ return program2.options.some(
18876
+ (option) => program2.getOptionValueSource(option.attributeName()) === "cli"
18877
+ );
17604
18878
  }
17605
- function leftoverPort(dir) {
17606
- try {
17607
- const meta = JSON.parse(fs15.readFileSync(sessionFilePathIn(dir, "METADATA"), "utf8"));
17608
- if (typeof meta.port === "number") return meta.port;
17609
- } catch (error) {
17610
- logDebugError(log25, `read the metadata in ${dir}`, error);
17611
- }
17612
- return readPortFile(sessionFilePathIn(dir, "PORT"));
18879
+ function assertUserDataDir(value) {
18880
+ const resolved = expandHome(value);
18881
+ let reason;
18882
+ if (!value.trim()) reason = "the path is empty";
18883
+ else if (value.startsWith("-")) reason = "it looks like an option; -u needs a directory path";
18884
+ else if (fs15.existsSync(resolved) && !fs15.statSync(resolved).isDirectory()) reason = "it is a file";
18885
+ if (reason === void 0) return;
18886
+ const err = invalidUserDataDirError(value, reason);
18887
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
17613
18888
  }
17614
- function isSelectable({ name }) {
17615
- return name === null || isValidSessionName(name) && name === normalizeSessionName(name);
18889
+ function launchOptionConflicts(options, program2) {
18890
+ const headlessGiven = program2.getOptionValueSource("headless") === "cli";
18891
+ return [
18892
+ ...options.port !== void 0 ? ["--port"] : [],
18893
+ ...options.userDataDir !== void 0 ? ["--user-data-dir"] : [],
18894
+ ...headlessGiven ? [options.headless ? "--headless" : "--no-headless"] : []
18895
+ ];
17616
18896
  }
17617
- function isSameDir(a, b) {
17618
- try {
17619
- const first = fs15.statSync(a);
17620
- const second = fs15.statSync(b);
17621
- return first.ino === second.ino && first.dev === second.dev;
17622
- } catch (error) {
17623
- logDebugError(log25, `compare ${a} with ${b}`, error);
17624
- return false;
17625
- }
18897
+ function assertNoLaunchOptions(options, program2) {
18898
+ const conflicts = launchOptionConflicts(options, program2);
18899
+ if (conflicts.length === 0) return;
18900
+ const err = chromeWsUrlConflictError(conflicts);
18901
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
17626
18902
  }
17627
- function selectableEntry(entry) {
17628
- if (isSelectable(entry) || entry.name === null) return entry;
17629
- const lower = normalizeSessionName(entry.name);
17630
- if (!isValidSessionName(lower)) return null;
17631
- return isSameDir(entry.dir, getNamedSessionDir(lower)) ? { name: lower, dir: entry.dir } : null;
18903
+ var BDG_OPTION_WORDS = /* @__PURE__ */ new Set([
18904
+ "--json",
18905
+ "-j",
18906
+ "--port",
18907
+ "-p",
18908
+ "--no-headless",
18909
+ "-q",
18910
+ "--quiet"
18911
+ ]);
18912
+ function assertChromeFlags(flags) {
18913
+ const flag = flags.find(
18914
+ (value) => /^--remote-debugging-(port|pipe)\b/.test(value) || BDG_OPTION_WORDS.has(value)
18915
+ );
18916
+ if (flag === void 0) return;
18917
+ const err = invalidChromeFlagError(flag);
18918
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
17632
18919
  }
17633
- async function describeUnselectable(entry) {
17634
- const socketPath = sessionFilePathIn(entry.dir, "DAEMON_SOCKET");
17635
- if (await probeDaemonSocket(socketPath) === "alive") return describeSession(entry);
17636
- return { name: entry.name, state: "stale", cleanup: removeDirCommand(entry.dir) };
18920
+ function assertValidUrl(url) {
18921
+ const result = validateUrl(url);
18922
+ if (result.valid) return;
18923
+ throw new CommandError(
18924
+ result.error,
18925
+ result.suggestion ? { suggestion: result.suggestion } : {},
18926
+ EXIT_CODES.INVALID_URL
18927
+ );
17637
18928
  }
17638
- async function listRunningSessions() {
17639
- const listed = /* @__PURE__ */ new Set();
17640
- const sessions = await Promise.all(
17641
- listSessionDirs().map((entry) => {
17642
- const selectable = selectableEntry(entry);
17643
- if (selectable === null) return describeUnselectable(entry);
17644
- if (listed.has(selectable.name)) return Promise.resolve(null);
17645
- listed.add(selectable.name);
17646
- return describeSession(selectable);
17647
- })
18929
+ function assertValidChromeWsUrl(url) {
18930
+ const result = validateChromeWsUrl(url);
18931
+ if (result.valid) return;
18932
+ throw new CommandError(
18933
+ result.error,
18934
+ result.suggestion ? { suggestion: result.suggestion } : {},
18935
+ EXIT_CODES.INVALID_URL
17648
18936
  );
17649
- return sessions.filter((session) => session !== null);
17650
18937
  }
17651
18938
 
17652
- // src/ui/formatters/sessions.ts
18939
+ // src/commands/page.ts
18940
+ init_errors3();
18941
+ init_messages();
18942
+ init_client();
17653
18943
  init_formatting();
17654
- var DEFAULT_SESSION_LABEL = "(default)";
17655
- function formatSessionList(data) {
17656
- const fmt = new OutputFormatter();
17657
- if (data.sessions.length === 0) {
17658
- return fmt.text("No running sessions").hints("Suggestions:", ["Start one: bdg <url> [--session <name>]"]).build();
17659
- }
17660
- const rows = data.sessions.map((session) => [
17661
- session.name ?? DEFAULT_SESSION_LABEL,
17662
- session.state,
17663
- session.port?.toString() ?? "-",
17664
- session.daemonPid?.toString() ?? "-",
17665
- session.url ?? "-"
17666
- ]);
17667
- const header = ["SESSION", "STATE", "PORT", "PID", "URL"];
17668
- const widths = header.map(
17669
- (title, column) => Math.max(title.length, ...rows.map((row) => row[column]?.length ?? 0))
18944
+ init_commands();
18945
+ init_exitCodes();
18946
+ init_url();
18947
+ function formatPageResult(result) {
18948
+ const fmt = new OutputFormatter().text(`\u2713 ${PAGE_ACTION_DONE[result.action]}`).keyValueList(
18949
+ [
18950
+ ["URL", result.url],
18951
+ ["Title", result.title],
18952
+ ...result.status !== void 0 ? [["Status", String(result.status)]] : []
18953
+ ],
18954
+ 8
17670
18955
  );
17671
- for (const row of [header, ...rows]) {
17672
- fmt.text(
17673
- row.map((cell, column) => cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd()
17674
- );
17675
- }
17676
- const cleanups = data.sessions.flatMap((session) => session.cleanup ? [session.cleanup] : []);
17677
- if (cleanups.length > 0) {
17678
- fmt.hints("Crashed or stale sessions; clean up with:", cleanups);
17679
- }
18956
+ if (result.warning) fmt.text(`\u26A0 ${result.warning}`);
18957
+ if (result.loading) fmt.text(`\u26A0 ${pageLoadingWarning(result.loading)}`);
17680
18958
  return fmt.build();
17681
18959
  }
17682
-
17683
- // src/commands/sessions.ts
17684
- function registerSessionsCommand(program2) {
17685
- program2.command("sessions").description(
17686
- "List sessions (default and named) with their state, URL, port and PID, including crashed ones to clean up"
17687
- ).addOption(jsonOption()).action(async (options) => {
17688
- await runCommand(
17689
- async () => ({ success: true, data: { sessions: await listRunningSessions() } }),
17690
- options,
17691
- formatSessionList
17692
- );
17693
- });
18960
+ function isScriptUrl(url) {
18961
+ const [scheme = ""] = url.trim().split(":", 1);
18962
+ return url.includes(":") && scheme.toLowerCase() === "javascript";
17694
18963
  }
17695
-
17696
- // src/commands/start.ts
17697
- import fs17 from "node:fs";
17698
- import os3 from "node:os";
17699
-
17700
- // src/daemon/errors.ts
17701
- init_exitCodes();
17702
- var DaemonError = class _DaemonError extends Error {
17703
- exitCode;
17704
- code;
17705
- constructor(message, code, exitCode = EXIT_CODES.SOFTWARE_ERROR) {
17706
- super(message);
17707
- this.name = "DaemonError";
17708
- this.exitCode = exitCode;
17709
- if (code !== void 0) {
17710
- this.code = code;
17711
- }
17712
- if (Error.captureStackTrace) {
17713
- Error.captureStackTrace(this, _DaemonError);
17714
- }
17715
- }
17716
- };
17717
- var DaemonStartupError = class extends DaemonError {
17718
- name = "DaemonStartupError";
17719
- constructor(message, code) {
17720
- super(message, code, EXIT_CODES.SOFTWARE_ERROR);
17721
- }
17722
- };
17723
- var SessionDirError = class extends DaemonError {
17724
- /**
17725
- * @param message - What is wrong with the directory
17726
- * @param suggestion - How to fix it
17727
- * @param exitCode - 82 for permissions, 81 for a too-long path, 103 otherwise
17728
- */
17729
- constructor(message, suggestion, exitCode = EXIT_CODES.SESSION_FILE_ERROR) {
17730
- super(message, "SESSION_DIR_UNUSABLE", exitCode);
17731
- this.suggestion = suggestion;
17732
- }
17733
- suggestion;
17734
- name = "SessionDirError";
17735
- };
17736
-
17737
- // src/daemon/launcher.ts
17738
- import { spawn } from "child_process";
17739
- import fs16 from "fs";
17740
- import { join as join7 } from "path";
17741
- init_messages();
17742
- init_paths();
17743
- init_logging();
17744
- init_errors();
17745
- init_exitCodes();
17746
- var log26 = createLogger("launcher");
17747
- var DAEMON_READY_TIMEOUT_MS = 5e3;
17748
- var DAEMON_READY_POLL_MS = 20;
17749
- async function launchDaemon() {
17750
- if (await isDaemonAlive()) {
17751
- log26.debug("Daemon already running");
17752
- return;
17753
- }
17754
- if (!fs16.existsSync(DAEMON_SCRIPT_PATH)) {
17755
- throw new DaemonStartupError(
17756
- `Daemon script not found at ${DAEMON_SCRIPT_PATH}. Did you run 'npm run build'?`,
17757
- "DAEMON_SCRIPT_NOT_FOUND"
17758
- );
17759
- }
17760
- assertUsableSessionDir();
17761
- const logPath = join7(getSessionDir(), "daemon.log");
17762
- rotateLog(logPath);
17763
- const logFd = fs16.openSync(logPath, "a");
17764
- log26.debug(`Starting daemon: ${DAEMON_SCRIPT_PATH}`);
17765
- const daemon = spawn(process.execPath, [DAEMON_SCRIPT_PATH], {
17766
- detached: true,
17767
- stdio: ["ignore", logFd, logFd]
17768
- });
17769
- fs16.closeSync(logFd);
17770
- let exited = false;
17771
- daemon.once("exit", () => {
17772
- exited = true;
17773
- });
17774
- daemon.unref();
17775
- await waitForDaemonReady(() => exited);
18964
+ async function runPageAction(action, options, url) {
18965
+ await runCommand(
18966
+ async () => {
18967
+ if (url !== void 0 && isScriptUrl(url)) {
18968
+ const err = javascriptNavigationError();
18969
+ return {
18970
+ success: false,
18971
+ error: err.message,
18972
+ exitCode: EXIT_CODES.INVALID_URL,
18973
+ errorContext: { suggestion: err.suggestion }
18974
+ };
18975
+ }
18976
+ if (url !== void 0) {
18977
+ const check = validateUrl(url);
18978
+ if (!check.valid) {
18979
+ return {
18980
+ success: false,
18981
+ error: check.error,
18982
+ exitCode: EXIT_CODES.INVALID_URL,
18983
+ ...check.suggestion && { errorContext: { suggestion: check.suggestion } }
18984
+ };
18985
+ }
18986
+ }
18987
+ const response = await pageNavigate({
18988
+ action,
18989
+ ...url !== void 0 && { url },
18990
+ wait: options.wait !== false
18991
+ });
18992
+ if (response.status === "error" || !response.data) {
18993
+ return {
18994
+ success: false,
18995
+ error: response.error ?? `Failed to ${action}`,
18996
+ exitCode: response.exitCode ?? EXIT_CODES.SOFTWARE_ERROR,
18997
+ ...response.suggestion && { errorContext: { suggestion: response.suggestion } }
18998
+ };
18999
+ }
19000
+ return { success: true, data: response.data };
19001
+ },
19002
+ options,
19003
+ formatPageResult
19004
+ );
17776
19005
  }
17777
- function assertUsableSessionDir() {
17778
- const dir = getSessionDir();
17779
- const fail = (err, exitCode) => {
17780
- throw new SessionDirError(err.message, err.suggestion, exitCode);
17781
- };
17782
- if (fs16.existsSync(dir) && !fs16.statSync(dir).isDirectory()) fail(sessionDirIsFileError(dir));
17783
- const socketPath = getSessionFilePath("DAEMON_SOCKET");
17784
- if (Buffer.byteLength(socketPath) > MAX_DAEMON_SOCKET_PATH_BYTES) {
17785
- fail(
17786
- socketPathTooLongError(socketPath, MAX_DAEMON_SOCKET_PATH_BYTES),
19006
+ async function showPageInfo(options) {
19007
+ await runCommand(
19008
+ async () => {
19009
+ const response = await getStatus();
19010
+ if (response.status === "error") {
19011
+ return {
19012
+ success: false,
19013
+ error: response.error ?? "Failed to read the page",
19014
+ exitCode: EXIT_CODES.SOFTWARE_ERROR
19015
+ };
19016
+ }
19017
+ const page = response.data?.sessionPid ? response.data.pageState : void 0;
19018
+ if (!page) throw noActiveSessionError();
19019
+ return { success: true, data: { url: page.url, title: page.title } };
19020
+ },
19021
+ options,
19022
+ (page) => new OutputFormatter().keyValueList(
19023
+ [
19024
+ ["URL", page.url],
19025
+ ["Title", page.title]
19026
+ ],
19027
+ 8
19028
+ ).build()
19029
+ );
19030
+ }
19031
+ function emulationRequest(options) {
19032
+ if (options.reset) return { reset: true };
19033
+ if (options.viewport === void 0 && options.colorScheme === void 0) {
19034
+ const err = pageEmulateNothingError();
19035
+ throw new CommandError(
19036
+ err.message,
19037
+ { suggestion: err.suggestion },
17787
19038
  EXIT_CODES.INVALID_ARGUMENTS
17788
19039
  );
17789
19040
  }
17790
- try {
17791
- ensureSessionDir();
17792
- fs16.accessSync(dir, fs16.constants.W_OK);
17793
- } catch (error) {
17794
- const code = error.code;
17795
- const denied = code === "EACCES" || code === "EPERM" || code === "EROFS";
17796
- fail(
17797
- sessionDirNotWritableError(dir, code ?? getErrorMessage(error)),
17798
- denied ? EXIT_CODES.PERMISSION_DENIED : EXIT_CODES.SESSION_FILE_ERROR
17799
- );
17800
- }
17801
- }
17802
- var MAX_LOG_BYTES = 5 * 1024 * 1024;
17803
- function rotateLog(logPath) {
17804
- try {
17805
- if (fs16.statSync(logPath).size > MAX_LOG_BYTES) {
17806
- fs16.renameSync(logPath, `${logPath}.1`);
17807
- }
17808
- } catch (error) {
17809
- log26.debug(`Daemon log not rotated: ${getErrorMessage(error)}`);
17810
- }
17811
- }
17812
- async function waitForDaemonReady(hasExited) {
17813
- const deadline = Date.now() + DAEMON_READY_TIMEOUT_MS;
17814
- while (Date.now() < deadline) {
17815
- if (await isDaemonAlive()) {
17816
- log26.debug("Daemon is ready");
17817
- return;
17818
- }
17819
- if (hasExited() && !await isDaemonAlive()) {
17820
- throw new DaemonStartupError(
17821
- `Daemon exited during startup. See ${join7(getSessionDir(), "daemon.log")}`,
17822
- "DAEMON_EXITED"
17823
- );
19041
+ return {
19042
+ ...options.viewport !== void 0 && { viewport: parseViewport(options.viewport) },
19043
+ ...options.colorScheme !== void 0 && {
19044
+ colorScheme: parseColorScheme(options.colorScheme)
17824
19045
  }
17825
- await delay2(DAEMON_READY_POLL_MS);
17826
- }
17827
- throw new DaemonStartupError(
17828
- `Daemon failed to start within ${DAEMON_READY_TIMEOUT_MS / 1e3} seconds`,
17829
- "DAEMON_START_TIMEOUT"
19046
+ };
19047
+ }
19048
+ async function emulate(options) {
19049
+ await runCommand(
19050
+ async () => {
19051
+ const response = await pageEmulate(emulationRequest(options));
19052
+ if (response.status === "error" || !response.data) {
19053
+ return {
19054
+ success: false,
19055
+ error: response.error ?? "Failed to change the emulation",
19056
+ exitCode: response.exitCode ?? EXIT_CODES.SOFTWARE_ERROR,
19057
+ ...response.suggestion && { errorContext: { suggestion: response.suggestion } }
19058
+ };
19059
+ }
19060
+ return { success: true, data: response.data };
19061
+ },
19062
+ options,
19063
+ (result) => new OutputFormatter().text("\u2713 Page emulation changed").keyValueList(pageEmulationLines(result), 10).build()
17830
19064
  );
17831
19065
  }
17832
-
17833
- // src/commands/shared/startHelpers.ts
17834
- init_messages();
17835
- init_client();
17836
- init_transport();
17837
- init_errors2();
17838
- init_paths();
17839
- init_formatting();
17840
- init_logging();
17841
- init_sessionCommand();
17842
- init_errors();
17843
- init_exitCodes();
17844
- var log27 = createLogger("bdg");
17845
- async function startSessionViaDaemon(url, options, telemetry) {
17846
- process.once("SIGINT", () => reportStartOutcome(interruptedOutcome("SIGINT"), options));
17847
- process.once("SIGTERM", () => reportStartOutcome(interruptedOutcome("SIGTERM"), options));
17848
- let outcome = await requestSession(url, options, telemetry);
17849
- const deadline = Date.now() + SHUTDOWN_WAIT_MS;
17850
- while (isShuttingDown(outcome) && Date.now() < deadline) {
17851
- await delay2(SHUTDOWN_POLL_MS);
17852
- outcome = await requestSession(url, options, telemetry);
19066
+ function registerPageCommands(program2) {
19067
+ const page = program2.command("page").description(
19068
+ "The session page: info (URL and title), navigate <url>, reload, back, forward, emulate (viewport, color scheme)"
19069
+ );
19070
+ page.command("info").description(PAGE_INFO_DESCRIPTION).addOption(jsonOption()).action(async (options) => {
19071
+ await showPageInfo(options);
19072
+ });
19073
+ const withCommon = (command) => command.option("--no-wait", "Return without waiting for the page to load").addOption(jsonOption());
19074
+ withCommon(
19075
+ page.command("navigate").description("Load a URL in the session page").argument("<url>", "URL to load")
19076
+ ).action(async (url, options) => {
19077
+ await runPageAction("navigate", options, url);
19078
+ });
19079
+ page.command("emulate").description(PAGE_EMULATE_DESCRIPTION).option("--viewport <WxH>", "Viewport size in CSS px, e.g. 900x700").option("--color-scheme <scheme>", "Emulate prefers-color-scheme: light or dark").addOption(
19080
+ new Option("--reset", "Back to the browser window size and the system setting").conflicts([
19081
+ "viewport",
19082
+ "colorScheme"
19083
+ ])
19084
+ ).addOption(jsonOption()).action(async (options) => {
19085
+ await emulate(options);
19086
+ });
19087
+ for (const action of ["reload", "back", "forward"]) {
19088
+ withCommon(page.command(action).description(PAGE_ACTION_DESCRIPTIONS[action])).action(
19089
+ async (options) => {
19090
+ await runPageAction(action, options);
19091
+ }
19092
+ );
17853
19093
  }
17854
- reportStartOutcome(outcome, options);
17855
19094
  }
17856
- function interruptedOutcome(signal) {
17857
- const message = `Start cancelled (${signal === "SIGINT" ? "interrupted" : "terminated"})`;
17858
- const exitCode = signal === "SIGINT" ? EXIT_CODES.INTERRUPTED : EXIT_CODES.TERMINATED;
17859
- return { ok: false, error: message, human: message, exitCode };
19095
+
19096
+ // src/commands/peek.ts
19097
+ function parseOptions(options) {
19098
+ const lastN = positiveIntRule({
19099
+ name: "--last",
19100
+ min: 1,
19101
+ max: MAX_LAST_ITEMS,
19102
+ default: 10,
19103
+ allowZeroForAll: true
19104
+ }).validate(options.last);
19105
+ const resourceTypes = resourceTypeRule().validate(options.type);
19106
+ return { lastN, resourceTypes };
17860
19107
  }
17861
- var SHUTDOWN_WAIT_MS = 15e3;
17862
- var SHUTDOWN_POLL_MS = 200;
17863
- function isShuttingDown(outcome) {
17864
- return !outcome.ok && (outcome.retryable === true || outcome.errorCode === "SESSION_SHUTTING_DOWN" /* SESSION_SHUTTING_DOWN */);
19108
+ function peekSection(options) {
19109
+ if (options.network) return "network";
19110
+ return options.console ? "console" : void 0;
17865
19111
  }
17866
- async function requestSession(url, options, telemetry) {
17867
- try {
17868
- await launchDaemon();
17869
- } catch (error) {
17870
- if (error instanceof SessionDirError) {
17871
- return {
17872
- ok: false,
17873
- error: error.message,
17874
- human: joinLines(genericError(error.message), error.suggestion),
17875
- exitCode: error.exitCode,
17876
- details: { suggestion: error.suggestion }
17877
- };
17878
- }
17879
- const message = `Failed to start daemon: ${getErrorMessage(error)}`;
17880
- const exitCode = error instanceof DaemonError ? error.exitCode : EXIT_CODES.SOFTWARE_ERROR;
17881
- return { ok: false, error: message, human: genericError(message), exitCode };
17882
- }
17883
- try {
17884
- log27.debug("Connecting to daemon...");
17885
- const response = await startSession(
17886
- url,
17887
- filterDefined({
17888
- port: options.port,
17889
- timeout: options.timeout,
17890
- telemetry: telemetry.length > 0 ? telemetry : void 0,
17891
- includeAll: options.includeAll,
17892
- userDataDir: options.userDataDir,
17893
- maxBodySize: options.maxBodySize,
17894
- headless: options.headless,
17895
- chromeWsUrl: options.chromeWsUrl,
17896
- chromeFlags: options.chromeFlags,
17897
- viewport: options.viewport,
17898
- colorScheme: options.colorScheme
17899
- })
17900
- );
17901
- if (response.status === "error") return describeStartFailure(response, options);
17902
- if (!response.data) {
17903
- const message = "Invalid response from daemon: missing data";
17904
- return {
17905
- ok: false,
17906
- error: message,
17907
- human: invalidResponseError("missing data"),
17908
- exitCode: EXIT_CODES.SOFTWARE_ERROR
17909
- };
17910
- }
17911
- return { ok: true, data: response.data };
17912
- } catch (error) {
17913
- if (error instanceof IPCTimeoutError) {
17914
- const timeout = timeoutError(error);
17915
- return {
17916
- ok: false,
17917
- error: timeout.message,
17918
- human: joinLines(genericError(timeout.message), String(timeout.metadata.suggestion)),
17919
- exitCode: timeout.exitCode,
17920
- details: { suggestion: timeout.metadata.suggestion }
17921
- };
17922
- }
17923
- if (isConnectionError(error)) {
17924
- return {
17925
- ok: false,
17926
- error: `${noActiveSessionMessage()} (daemon not running)`,
17927
- human: daemonNotRunningError({ suggestStatus: true, suggestRetry: true }),
17928
- exitCode: EXIT_CODES.RESOURCE_NOT_FOUND,
17929
- retryable: true
17930
- };
17931
- }
17932
- const message = getErrorMessage(error);
17933
- return {
17934
- ok: false,
17935
- error: message,
17936
- human: genericError(message),
17937
- exitCode: EXIT_CODES.SOFTWARE_ERROR
17938
- };
19112
+ async function fetchAndFilterPreview(lastN, resourceTypes, only) {
19113
+ const result = await fetchPreviewOutput({
19114
+ lastN: resourceTypes.length > 0 ? 0 : lastN,
19115
+ ...only && { only }
19116
+ });
19117
+ if (!result.success) return result;
19118
+ const output = result.data;
19119
+ const unfilteredNetworkCount = output.totals?.network ?? output.data.network?.length ?? 0;
19120
+ if (resourceTypes.length === 0 || !output.data.network) {
19121
+ return { success: true, data: { output, unfilteredNetworkCount } };
17939
19122
  }
19123
+ const network = filterByResourceType(output.data.network, resourceTypes);
19124
+ const filteredOutput = {
19125
+ ...output,
19126
+ data: { ...output.data, network },
19127
+ ...output.totals && { totals: { ...output.totals, network: network.length } }
19128
+ };
19129
+ return { success: true, data: { output: filteredOutput, unfilteredNetworkCount } };
17940
19130
  }
17941
- function omitDuration(existing) {
17942
- const { duration: _duration, ...rest } = existing;
17943
- return rest;
19131
+ function createPreviewOptions(base, resourceTypes, unfilteredCount) {
19132
+ const options = { ...base };
19133
+ if (base.follow) options.viewedAt = /* @__PURE__ */ new Date();
19134
+ if (resourceTypes.length > 0) {
19135
+ options.filteredTypes = resourceTypes;
19136
+ options.unfilteredNetworkCount = unfilteredCount;
19137
+ }
19138
+ return options;
17944
19139
  }
17945
- function describeStartFailure(response, options) {
17946
- const exitCode = response.exitCode ?? getExitCodeForIPCError(response.errorCode);
17947
- const existing = response.existingSession;
17948
- const details = filterDefined({
17949
- existingSession: existing && {
17950
- ...omitDuration(existing),
17951
- ...existing.duration !== void 0 && { durationMs: existing.duration * 1e3 }
19140
+ async function runFollowMode3(options, lastN, resourceTypes, baseOptions) {
19141
+ const showPreview = async () => {
19142
+ const result = await fetchAndFilterPreview(lastN, resourceTypes, peekSection(options));
19143
+ if (!result.success) {
19144
+ return followFetchFailure(result, { json: options.json, retryIntervalMs: 1e3 });
17952
19145
  }
19146
+ noteFollowConnected();
19147
+ if (!options.json) console.clear();
19148
+ const previewOptions = createPreviewOptions(
19149
+ baseOptions,
19150
+ resourceTypes,
19151
+ result.data.unfilteredNetworkCount
19152
+ );
19153
+ console.log(formatPreview(result.data.output, previewOptions));
19154
+ return void 0;
19155
+ };
19156
+ await setupFollowMode(showPreview, {
19157
+ startMessage: followingPreviewMessage,
19158
+ stopMessage: stoppedFollowingPreviewMessage,
19159
+ intervalMs: 1e3
17953
19160
  });
17954
- if (response.errorCode === "SESSION_ALREADY_RUNNING" /* SESSION_ALREADY_RUNNING */ && existing) {
17955
- const { pid, targetUrl, duration } = existing;
17956
- return {
17957
- ok: false,
17958
- error: response.message ?? "Session already running",
17959
- human: sessionAlreadyRunningError(pid, duration ? duration * 1e3 : 0, targetUrl),
17960
- exitCode,
17961
- errorCode: response.errorCode,
17962
- details: { ...details, suggestion: alreadyRunningSuggestion() }
17963
- };
17964
- }
17965
- if (response.errorCode === "SESSION_TARGET_MISMATCH" /* SESSION_TARGET_MISMATCH */) {
17966
- const current = response.existingSession?.targetUrl ?? LAUNCHED_CHROME_DESCRIPTION;
17967
- const requested = options.chromeWsUrl ?? LAUNCHED_CHROME_DESCRIPTION;
17968
- return {
17969
- ok: false,
17970
- error: response.message ?? "Session target mismatch",
17971
- human: sessionTargetMismatchError(current, requested),
17972
- exitCode,
17973
- errorCode: response.errorCode,
17974
- details
17975
- };
17976
- }
17977
- const message = response.message ?? "Unknown error";
17978
- const [error = message, ...hint] = message.split("\n");
17979
- return {
17980
- ok: false,
17981
- error,
17982
- human: genericError(message),
17983
- exitCode,
17984
- errorCode: response.errorCode,
17985
- details: {
17986
- ...details,
17987
- ...hint.join("\n").trim() && { suggestion: hint.join("\n").trim() }
19161
+ }
19162
+ function registerPeekCommand(program2) {
19163
+ program2.command("peek").description("Preview collected data without stopping the session").addOption(jsonOption()).option("-v, --verbose", "Use verbose output with full URLs and formatting", false).option("-n, --network", "Show only network requests", false).option("-c, --console", "Show only console messages", false).option("-f, --follow", "Watch for updates (like tail -f)", false).option("--last <count>", "Show last N items, 0 for all", "10").option(
19164
+ "--type <types>",
19165
+ "Filter network requests by resource type (comma-separated: Document,XHR,Fetch,etc.)"
19166
+ ).action(async (options) => {
19167
+ showBothSectionsWhenBothRequested(options);
19168
+ if (options.network && !options.json) {
19169
+ console.error(
19170
+ 'Note: "bdg peek --network" is deprecated. Use "bdg network list" for enhanced filtering.'
19171
+ );
17988
19172
  }
17989
- };
17990
- }
17991
- function reportStartOutcome(outcome, options) {
17992
- if (!outcome.ok) {
17993
- if (options.json) {
17994
- const envelope = OutputBuilder.buildJsonError(outcome.error, {
17995
- exitCode: outcome.exitCode,
17996
- ...outcome.details
17997
- });
17998
- console.log(JSON.stringify(envelope, null, 2));
17999
- } else {
18000
- console.error(escapeControlChars(outcome.human));
19173
+ let lastN;
19174
+ let resourceTypes;
19175
+ try {
19176
+ const parsed = parseOptions(options);
19177
+ lastN = parsed.lastN;
19178
+ resourceTypes = parsed.resourceTypes;
19179
+ } catch (error) {
19180
+ handleValidationError(error, options.json ?? false);
18001
19181
  }
18002
- process.exit(outcome.exitCode);
18003
- }
18004
- const { data } = outcome;
18005
- const autoStopAt = options.timeout !== void 0 ? new Date(Date.now() + options.timeout * 1e3) : void 0;
18006
- const session = getSessionName() ?? void 0;
18007
- if (options.json) {
18008
- const result = {
18009
- ...session && { session },
18010
- targetUrl: data.targetUrl,
18011
- ...data.targetTitle !== void 0 && { targetTitle: data.targetTitle },
18012
- ...data.documentStatus !== void 0 && { documentStatus: data.documentStatus },
18013
- ...data.loading && { loading: data.loading },
18014
- port: data.port,
18015
- ...data.chromePid > 0 ? { chromePid: data.chromePid } : { externalChrome: true },
18016
- daemonPid: data.daemonPid,
18017
- ...autoStopAt && { autoStopAt: autoStopAt.toISOString() }
18018
- };
18019
- console.log(JSON.stringify(buildSuccessResponse(result), null, 2));
18020
- } else {
18021
- const page = {
18022
- url: data.targetUrl,
18023
- ...data.documentStatus !== void 0 && { documentStatus: data.documentStatus },
18024
- ...data.loading && { loading: data.loading },
18025
- ...autoStopAt && { autoStopAt },
18026
- ...session && { session }
19182
+ const baseOptions = {
19183
+ json: options.json,
19184
+ network: options.network,
19185
+ console: options.console,
19186
+ last: lastN,
19187
+ verbose: options.verbose,
19188
+ follow: options.follow
18027
19189
  };
18028
- const text = options.quiet ? [`Session started: ${data.targetUrl}`, ...startNotices(page)].join("\n") : landingPage(page);
18029
- console.error(escapeControlChars(text));
18030
- }
18031
- process.exit(EXIT_CODES.SUCCESS);
19190
+ if (options.follow) {
19191
+ await runFollowMode3(options, lastN, resourceTypes, baseOptions);
19192
+ return;
19193
+ }
19194
+ await runCommand(
19195
+ async () => {
19196
+ const result = await fetchAndFilterPreview(lastN, resourceTypes, peekSection(options));
19197
+ if (!result.success) {
19198
+ return createErrorResult(result.error, result.exitCode, result.suggestion);
19199
+ }
19200
+ const { output } = result.data;
19201
+ return {
19202
+ success: true,
19203
+ data: options.json ? buildPreviewJsonData(output, baseOptions) : output
19204
+ };
19205
+ },
19206
+ options,
19207
+ (data) => {
19208
+ const output = data;
19209
+ const previewOptions = createPreviewOptions(
19210
+ baseOptions,
19211
+ resourceTypes,
19212
+ output.data.network?.length ?? 0
19213
+ );
19214
+ return formatPreview(output, previewOptions);
19215
+ }
19216
+ );
19217
+ });
18032
19218
  }
18033
19219
 
18034
- // src/commands/start.ts
18035
- init_constants();
18036
- init_errors3();
18037
- init_messages();
18038
- init_commands();
18039
- init_exitCodes();
19220
+ // src/session/sessionList.ts
19221
+ init_client();
19222
+ import * as fs17 from "fs";
19223
+ init_paths();
18040
19224
 
18041
- // src/utils/http.ts
18042
- init_constants();
18043
- init_errors();
18044
- var CDP_HTTP_TIMEOUT_MS = 5e3;
18045
- async function probeDevToolsEndpoint(port, logger, options) {
18046
- const url = `${options?.secure ? "https" : "http"}://${options?.host ?? HTTP_LOCALHOST}:${port}/json/version`;
18047
- let response;
18048
- try {
18049
- response = await fetch(url, { signal: AbortSignal.timeout(CDP_HTTP_TIMEOUT_MS) });
18050
- } catch (error) {
18051
- logger?.debug(`Chrome version request failed: ${getErrorMessage(error)} (${url})`);
18052
- return { kind: "unreachable" };
18053
- }
19225
+ // src/session/portClaims.ts
19226
+ import * as fs16 from "fs";
19227
+ init_paths();
19228
+ init_logging();
19229
+ var log26 = createLogger("session");
19230
+ function readPortFile(portPath) {
18054
19231
  try {
18055
- const data = await response.json();
18056
- const wsUrl = data?.webSocketDebuggerUrl;
18057
- if (response.ok && typeof wsUrl === "string") return { kind: "devtools", wsUrl };
19232
+ const port = parseInt(fs16.readFileSync(portPath, "utf-8").trim(), 10);
19233
+ return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
18058
19234
  } catch (error) {
18059
- logger?.debug(`Chrome version response is not JSON: ${getErrorMessage(error)} (${url})`);
19235
+ logDebugError(log26, `read ${portPath}`, error);
19236
+ return null;
18060
19237
  }
18061
- return { kind: "not-devtools" };
18062
19238
  }
18063
19239
 
18064
- // src/commands/start.ts
18065
- init_suggestions();
18066
- init_url();
18067
- function hasDisplay() {
18068
- const display = process.env["DISPLAY"];
18069
- const wayland = process.env["WAYLAND_DISPLAY"];
18070
- return display !== void 0 && display !== "" || wayland !== void 0 && wayland !== "";
19240
+ // src/session/sessionName.ts
19241
+ init_errors3();
19242
+ init_messages();
19243
+ init_paths();
19244
+ init_exitCodes();
19245
+ var MAX_SESSION_NAME_LENGTH = 40;
19246
+ var SESSION_NAME_PATTERN = new RegExp(
19247
+ `^[A-Za-z0-9][A-Za-z0-9_-]{0,${MAX_SESSION_NAME_LENGTH - 1}}$`
19248
+ );
19249
+ function isValidSessionName(name) {
19250
+ return SESSION_NAME_PATTERN.test(name);
18071
19251
  }
18072
- function expandHome(value) {
18073
- return value.startsWith("~/") ? value.replace(/^~/, os3.homedir()) : value;
19252
+ function normalizeSessionName(name) {
19253
+ return name.toLowerCase();
18074
19254
  }
18075
- function extractUserDataDirFromFlags(flags) {
18076
- const rest = [];
18077
- let userDataDir;
18078
- for (let i = 0; i < flags.length; i++) {
18079
- const flag = flags[i];
18080
- if (flag.startsWith("--user-data-dir=")) {
18081
- userDataDir = flag.slice("--user-data-dir=".length);
18082
- continue;
18083
- }
18084
- if (flag === "--user-data-dir" && i + 1 < flags.length) {
18085
- userDataDir = flags[i + 1];
18086
- i++;
18087
- continue;
18088
- }
18089
- rest.push(flag);
19255
+ function validateSessionName(name) {
19256
+ if (!isValidSessionName(name)) {
19257
+ throwInvalid(invalidSessionNameError(name, MAX_SESSION_NAME_LENGTH));
19258
+ }
19259
+ const shortest = namedSocketPath("a");
19260
+ if (Buffer.byteLength(shortest) > MAX_DAEMON_SOCKET_PATH_BYTES) {
19261
+ throwInvalid(socketPathTooLongError(shortest, MAX_DAEMON_SOCKET_PATH_BYTES));
19262
+ }
19263
+ const socketPath = namedSocketPath(name);
19264
+ if (Buffer.byteLength(socketPath) > MAX_DAEMON_SOCKET_PATH_BYTES) {
19265
+ throwInvalid(sessionNameSocketTooLongError(name, socketPath, MAX_DAEMON_SOCKET_PATH_BYTES));
18090
19266
  }
18091
- return {
18092
- userDataDir: userDataDir ? expandHome(userDataDir) : void 0,
18093
- rest
18094
- };
18095
- }
18096
- function applyCollectorOptions(command) {
18097
- const defaultHeadless = !hasDisplay();
18098
- return command.option("-p, --port <number>", PORT_OPTION_DESCRIPTION).option(
18099
- "-t, --timeout <seconds>",
18100
- "Stop the session this many seconds after the page has loaded (unlimited if not specified)"
18101
- ).option("-u, --user-data-dir <path>", "Chrome user data directory (defaults to session dir)").option(
18102
- "-a, --all",
18103
- "Include all data: no tracking/analytics filtering, and capture every response body (incl. binary)",
18104
- false
18105
- ).option("-m, --max-body-size <megabytes>", "Maximum response body size in MB", "5").addOption(new Option("--compact", "No effect; kept for compatibility").hideHelp()).option(
18106
- "--headless",
18107
- "Run Chrome without a window (default unless DISPLAY or WAYLAND_DISPLAY is set)",
18108
- defaultHeadless
18109
- ).option("--no-headless", "Show browser window").option(
18110
- "--chrome-ws-url <url>",
18111
- "Connect to an existing Chrome: its DevTools port (9222, host:port, http://host:port), or a WebSocket URL: browser (ws://host:port/devtools/browser/<id>, uses the first tab) or page (.../devtools/page/<id>)"
18112
- ).option("-q, --quiet", "Quiet mode - minimal output for AI agents", false).addOption(jsonOption()).option(
18113
- "--chrome-flags <flags>",
18114
- 'Custom Chrome flags (space-separated, e.g., --chrome-flags="--ignore-certificate-errors --disable-web-security")'
18115
- ).option(
18116
- "--viewport <WxH>",
18117
- "Viewport size in CSS px for the whole session, e.g. 1280x800 (default: 1920x1080 window)"
18118
- ).option(
18119
- "--color-scheme <scheme>",
18120
- "Emulate prefers-color-scheme for the session: light or dark (default: the system setting)"
18121
- );
18122
- }
18123
- var MAX_VIEWPORT_SIDE = 1e4;
18124
- var COLOR_SCHEMES = ["light", "dark"];
18125
- function parseViewport(value) {
18126
- const match = /^\s*(\d+)\s*[xX×,]\s*(\d+)\s*$/.exec(value);
18127
- const width = Number(match?.[1]);
18128
- const height = Number(match?.[2]);
18129
- const valid2 = (side) => side >= 1 && side <= MAX_VIEWPORT_SIDE;
18130
- if (match && valid2(width) && valid2(height)) return { width, height };
18131
- const err = invalidViewportError(value, MAX_VIEWPORT_SIDE);
18132
- throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
18133
- }
18134
- function parseColorScheme(value) {
18135
- const scheme = COLOR_SCHEMES.find((candidate) => candidate === value.trim().toLowerCase());
18136
- if (scheme) return scheme;
18137
- const err = invalidColorSchemeError(value, findSimilar(value, [...COLOR_SCHEMES]), COLOR_SCHEMES);
18138
- throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
18139
- }
18140
- function buildSessionOptions(options) {
18141
- const maxBodySizeRule = positiveIntRule({
18142
- name: "--max-body-size",
18143
- min: 1,
18144
- max: 100,
18145
- required: false
18146
- });
18147
- const timeoutRule = positiveIntRule({ name: "--timeout", min: 1, max: 3600, required: false });
18148
- const portRule = positiveIntRule({ name: "--port", min: 1024, max: 65535, required: false });
18149
- const maxBodySizeMB = options.maxBodySize !== void 0 ? maxBodySizeRule.validate(options.maxBodySize) : void 0;
18150
- const timeout = options.timeout !== void 0 ? timeoutRule.validate(options.timeout) : void 0;
18151
- const envFlags = process.env["BDG_CHROME_FLAGS"]?.split(" ").filter(Boolean) ?? [];
18152
- const cliFlags = options.chromeFlags?.split(" ").filter(Boolean) ?? [];
18153
- const { userDataDir: flagUserDataDir, rest: combinedFlags } = extractUserDataDirFromFlags([
18154
- ...envFlags,
18155
- ...cliFlags
18156
- ]);
18157
- const userDataDir = options.userDataDir ? expandHome(options.userDataDir) : flagUserDataDir;
18158
- const chromeFlags = combinedFlags.length > 0 ? combinedFlags : void 0;
18159
- return {
18160
- port: options.port !== void 0 ? portRule.validate(options.port) : void 0,
18161
- timeout,
18162
- userDataDir,
18163
- includeAll: options.all ?? false,
18164
- maxBodySize: maxBodySizeMB !== void 0 ? maxBodySizeMB * 1024 * 1024 : void 0,
18165
- headless: options.headless ?? !hasDisplay(),
18166
- chromeWsUrl: options.chromeWsUrl,
18167
- quiet: options.quiet ?? false,
18168
- json: options.json ?? false,
18169
- chromeFlags,
18170
- viewport: options.viewport !== void 0 ? parseViewport(options.viewport) : void 0,
18171
- colorScheme: options.colorScheme !== void 0 ? parseColorScheme(options.colorScheme) : void 0
18172
- };
18173
19267
  }
18174
- var SESSION_TELEMETRY = ["dom", "network", "console"];
18175
- var FLAG_WORDS = { version: "--version", help: "--help" };
18176
- function assertNotCommandTypo(arg, commandNames) {
18177
- if (!/^[a-z][a-z0-9_-]*$/i.test(arg) || arg.toLowerCase() === "localhost") return;
18178
- const flag = FLAG_WORDS[arg.toLowerCase()];
18179
- const err = unknownCommandError(arg, flag ? [flag] : findSimilar(arg, commandNames));
18180
- throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
19268
+ function namedSocketPath(name) {
19269
+ return sessionFilePathIn(getNamedSessionDir(name), "DAEMON_SOCKET");
18181
19270
  }
18182
- function assertNotGroupSubcommand(program2, argv) {
18183
- const [first] = argv.slice(2).filter((arg) => !arg.startsWith("-"));
18184
- if (first === void 0 || program2.commands.some((command) => command.name() === first)) return;
18185
- const group = program2.commands.find(
18186
- (command) => command.commands.some((sub) => sub.name() === first)
18187
- );
18188
- if (!group) return;
18189
- const err = unknownCommandError(first, [`${group.name()} ${first}`]);
19271
+ function throwInvalid(err) {
18190
19272
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
18191
19273
  }
18192
- function validateStartInput(url, options, program2) {
18193
- if (url === void 0) {
18194
- const err = missingStartUrlError();
18195
- throw new CommandError(
18196
- err.message,
18197
- { suggestion: err.suggestion },
18198
- EXIT_CODES.INVALID_ARGUMENTS
18199
- );
19274
+ function selectSession(optionValue) {
19275
+ const name = optionValue ?? process.env[SESSION_NAME_ENV]?.trim();
19276
+ if (name === void 0 || optionValue === void 0 && name === "") {
19277
+ delete process.env[SESSION_NAME_ENV];
19278
+ return;
18200
19279
  }
18201
- assertNotCommandTypo(
18202
- url,
18203
- program2.commands.map((command) => command.name())
18204
- );
18205
- assertValidUrl(url);
18206
- if (options.chromeWsUrl !== void 0) {
18207
- assertValidChromeWsUrl(options.chromeWsUrl);
18208
- assertNoLaunchOptions(options, program2);
19280
+ validateSessionName(name);
19281
+ process.env[SESSION_NAME_ENV] = normalizeSessionName(name);
19282
+ }
19283
+
19284
+ // src/session/sessionList.ts
19285
+ init_logging();
19286
+ init_sessionCommand();
19287
+ var log27 = createLogger("session");
19288
+ function toRunningSession(name, data) {
19289
+ const meta = data.sessionMetadata;
19290
+ const state = meta ? "active" : data.ending ? "ending" : "starting";
19291
+ const url = data.pageState?.url ?? data.starting?.url;
19292
+ return {
19293
+ name,
19294
+ state,
19295
+ ...url !== void 0 && { url },
19296
+ ...meta && { port: meta.port },
19297
+ daemonPid: data.daemonPid,
19298
+ ...meta?.chromePid && { chromePid: meta.chromePid }
19299
+ };
19300
+ }
19301
+ async function describeSession(entry) {
19302
+ const { name, dir } = entry;
19303
+ const socketPath = sessionFilePathIn(dir, "DAEMON_SOCKET");
19304
+ const probe = await probeDaemonSocket(socketPath);
19305
+ if (probe !== "alive") return describeWithoutSocket(entry, probe);
19306
+ try {
19307
+ const response = await getStatus(socketPath);
19308
+ if (response.status === "ok" && response.data) return toRunningSession(name, response.data);
19309
+ } catch (error) {
19310
+ logDebugError(log27, `get status of ${dir}`, error);
18209
19311
  }
18210
- if (options.userDataDir !== void 0) assertUserDataDir(options.userDataDir);
18211
- if (options.chromeWsUrl === void 0)
18212
- assertChromeFlags([
18213
- ...process.env["BDG_CHROME_FLAGS"]?.split(" ") ?? [],
18214
- ...options.chromeFlags?.split(" ") ?? []
18215
- ]);
18216
- return { url, sessionOptions: buildSessionOptions(options) };
19312
+ return { name, state: "unresponsive" };
18217
19313
  }
18218
- function registerStartCommands(program2) {
18219
- applyCollectorOptions(
18220
- program2.argument("[url]", "Target URL (example.com or localhost:3000)")
18221
- ).action(async (url, options) => {
18222
- if (url === void 0 && !hasUserOptions(program2)) {
18223
- console.error(startCommandHelpMessage());
18224
- process.exit(0);
18225
- }
18226
- let validated;
18227
- try {
18228
- validated = validateStartInput(url, options, program2);
18229
- validated.sessionOptions.chromeWsUrl = await resolveChromeWsUrl(
18230
- validated.sessionOptions.chromeWsUrl
18231
- );
18232
- } catch (error) {
18233
- handleValidationError(error, options.json ?? false);
18234
- }
18235
- await startSessionViaDaemon(validated.url, validated.sessionOptions, SESSION_TELEMETRY);
18236
- });
19314
+ function describeWithoutSocket(entry, probe) {
19315
+ const daemonPid = readLiveDaemonPid(entry.dir);
19316
+ if (daemonPid !== null) return { name: entry.name, state: "starting", daemonPid };
19317
+ return describeLeftovers(entry, probe);
18237
19318
  }
18238
- async function resolveChromeWsUrl(value) {
18239
- const endpoint = value === void 0 ? null : devToolsHttpEndpoint(value);
18240
- if (endpoint === null) return value;
18241
- const { hostname, port, protocol } = new URL(endpoint);
18242
- const secure = protocol === "https:";
18243
- const defaultPort = secure ? 443 : 80;
18244
- const probe = await probeDevToolsEndpoint(Number(port) || defaultPort, void 0, {
18245
- host: hostname,
18246
- secure
18247
- });
18248
- if (probe.kind === "devtools") return atEndpoint(probe.wsUrl, endpoint);
18249
- const err = probe.kind === "not-devtools" ? notDevToolsEndpointError(endpoint) : externalChromeUnreachableError(endpoint, false);
18250
- throw new CommandError(
18251
- err.message,
18252
- { suggestion: err.suggestion },
18253
- EXIT_CODES.CDP_CONNECTION_FAILURE
18254
- );
19319
+ function describeLeftovers({ name, dir }, probe) {
19320
+ const chromePid = readPidFromFile(sessionFilePathIn(dir, "CHROME_PID"));
19321
+ const orphan = chromePid !== null && isProcessAlive(chromePid) && isSessionChrome(chromePid, dir);
19322
+ const leftover = probe === "stale" || SESSION_STATE_FILES.some((type) => fs17.existsSync(sessionFilePathIn(dir, type)));
19323
+ if (!orphan && !leftover) return null;
19324
+ const port = leftoverPort(dir);
19325
+ return {
19326
+ name,
19327
+ state: orphan ? "crashed" : "stale",
19328
+ ...port !== null && { port },
19329
+ ...orphan && { chromePid },
19330
+ cleanup: sessionCommand("bdg cleanup", name)
19331
+ };
18255
19332
  }
18256
- function atEndpoint(wsUrl, endpoint) {
18257
- const resolved = new URL(wsUrl);
18258
- const target = new URL(endpoint);
18259
- resolved.protocol = target.protocol === "https:" ? "wss:" : "ws:";
18260
- resolved.host = target.host;
18261
- return resolved.toString();
19333
+ function leftoverPort(dir) {
19334
+ try {
19335
+ const meta = JSON.parse(fs17.readFileSync(sessionFilePathIn(dir, "METADATA"), "utf8"));
19336
+ if (typeof meta.port === "number") return meta.port;
19337
+ } catch (error) {
19338
+ logDebugError(log27, `read the metadata in ${dir}`, error);
19339
+ }
19340
+ return readPortFile(sessionFilePathIn(dir, "PORT"));
18262
19341
  }
18263
- function hasUserOptions(program2) {
18264
- return program2.options.some(
18265
- (option) => program2.getOptionValueSource(option.attributeName()) === "cli"
18266
- );
19342
+ function isSelectable({ name }) {
19343
+ return name === null || isValidSessionName(name) && name === normalizeSessionName(name);
18267
19344
  }
18268
- function assertUserDataDir(value) {
18269
- const resolved = expandHome(value);
18270
- let reason;
18271
- if (!value.trim()) reason = "the path is empty";
18272
- else if (value.startsWith("-")) reason = "it looks like an option; -u needs a directory path";
18273
- else if (fs17.existsSync(resolved) && !fs17.statSync(resolved).isDirectory()) reason = "it is a file";
18274
- if (reason === void 0) return;
18275
- const err = invalidUserDataDirError(value, reason);
18276
- throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
19345
+ function isSameDir(a, b) {
19346
+ try {
19347
+ const first = fs17.statSync(a);
19348
+ const second = fs17.statSync(b);
19349
+ return first.ino === second.ino && first.dev === second.dev;
19350
+ } catch (error) {
19351
+ logDebugError(log27, `compare ${a} with ${b}`, error);
19352
+ return false;
19353
+ }
18277
19354
  }
18278
- function launchOptionConflicts(options, program2) {
18279
- const headlessGiven = program2.getOptionValueSource("headless") === "cli";
18280
- return [
18281
- ...options.port !== void 0 ? ["--port"] : [],
18282
- ...options.userDataDir !== void 0 ? ["--user-data-dir"] : [],
18283
- ...headlessGiven ? [options.headless ? "--headless" : "--no-headless"] : []
18284
- ];
19355
+ function selectableEntry(entry) {
19356
+ if (isSelectable(entry) || entry.name === null) return entry;
19357
+ const lower = normalizeSessionName(entry.name);
19358
+ if (!isValidSessionName(lower)) return null;
19359
+ return isSameDir(entry.dir, getNamedSessionDir(lower)) ? { name: lower, dir: entry.dir } : null;
18285
19360
  }
18286
- function assertNoLaunchOptions(options, program2) {
18287
- const conflicts = launchOptionConflicts(options, program2);
18288
- if (conflicts.length === 0) return;
18289
- const err = chromeWsUrlConflictError(conflicts);
18290
- throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
19361
+ async function describeUnselectable(entry) {
19362
+ const socketPath = sessionFilePathIn(entry.dir, "DAEMON_SOCKET");
19363
+ if (await probeDaemonSocket(socketPath) === "alive") return describeSession(entry);
19364
+ return { name: entry.name, state: "stale", cleanup: removeDirCommand(entry.dir) };
18291
19365
  }
18292
- var BDG_OPTION_WORDS = /* @__PURE__ */ new Set([
18293
- "--json",
18294
- "-j",
18295
- "--port",
18296
- "-p",
18297
- "--no-headless",
18298
- "-q",
18299
- "--quiet"
18300
- ]);
18301
- function assertChromeFlags(flags) {
18302
- const flag = flags.find(
18303
- (value) => /^--remote-debugging-(port|pipe)\b/.test(value) || BDG_OPTION_WORDS.has(value)
19366
+ async function listRunningSessions() {
19367
+ const listed = /* @__PURE__ */ new Set();
19368
+ const sessions = await Promise.all(
19369
+ listSessionDirs().map((entry) => {
19370
+ const selectable = selectableEntry(entry);
19371
+ if (selectable === null) return describeUnselectable(entry);
19372
+ if (listed.has(selectable.name)) return Promise.resolve(null);
19373
+ listed.add(selectable.name);
19374
+ return describeSession(selectable);
19375
+ })
18304
19376
  );
18305
- if (flag === void 0) return;
18306
- const err = invalidChromeFlagError(flag);
18307
- throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
19377
+ return sessions.filter((session) => session !== null);
18308
19378
  }
18309
- function assertValidUrl(url) {
18310
- const result = validateUrl(url);
18311
- if (result.valid) return;
18312
- throw new CommandError(
18313
- result.error,
18314
- result.suggestion ? { suggestion: result.suggestion } : {},
18315
- EXIT_CODES.INVALID_URL
19379
+
19380
+ // src/ui/formatters/sessions.ts
19381
+ init_formatting();
19382
+ var DEFAULT_SESSION_LABEL = "(default)";
19383
+ function formatSessionList(data) {
19384
+ const fmt = new OutputFormatter();
19385
+ if (data.sessions.length === 0) {
19386
+ return fmt.text("No running sessions").hints("Suggestions:", ["Start one: bdg <url> [--session <name>]"]).build();
19387
+ }
19388
+ const rows = data.sessions.map((session) => [
19389
+ session.name ?? DEFAULT_SESSION_LABEL,
19390
+ session.state,
19391
+ session.port?.toString() ?? "-",
19392
+ session.daemonPid?.toString() ?? "-",
19393
+ session.url ?? "-"
19394
+ ]);
19395
+ const header = ["SESSION", "STATE", "PORT", "PID", "URL"];
19396
+ const widths = header.map(
19397
+ (title, column) => Math.max(title.length, ...rows.map((row) => row[column]?.length ?? 0))
18316
19398
  );
19399
+ for (const row of [header, ...rows]) {
19400
+ fmt.text(
19401
+ row.map((cell, column) => cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd()
19402
+ );
19403
+ }
19404
+ const cleanups = data.sessions.flatMap((session) => session.cleanup ? [session.cleanup] : []);
19405
+ if (cleanups.length > 0) {
19406
+ fmt.hints("Crashed or stale sessions; clean up with:", cleanups);
19407
+ }
19408
+ return fmt.build();
18317
19409
  }
18318
- function assertValidChromeWsUrl(url) {
18319
- const result = validateChromeWsUrl(url);
18320
- if (result.valid) return;
18321
- throw new CommandError(
18322
- result.error,
18323
- result.suggestion ? { suggestion: result.suggestion } : {},
18324
- EXIT_CODES.INVALID_URL
18325
- );
19410
+
19411
+ // src/commands/sessions.ts
19412
+ function registerSessionsCommand(program2) {
19413
+ program2.command("sessions").description(
19414
+ "List sessions (default and named) with their state, URL, port and PID, including crashed ones to clean up"
19415
+ ).addOption(jsonOption()).action(async (options) => {
19416
+ await runCommand(
19417
+ async () => ({ success: true, data: { sessions: await listRunningSessions() } }),
19418
+ options,
19419
+ formatSessionList
19420
+ );
19421
+ });
18326
19422
  }
18327
19423
 
18328
19424
  // src/commands/status.ts