browser-debugger-cli 0.9.0 → 0.11.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 (139) hide show
  1. package/.claude/skills/bdg/SKILL.md +268 -0
  2. package/README.md +15 -1
  3. package/dist/commands/dom/a11y.js +2 -1
  4. package/dist/commands/dom/formInteraction.js +56 -25
  5. package/dist/commands/dom/helpers/keyAttributes.d.ts +20 -0
  6. package/dist/commands/dom/helpers/keyAttributes.js +54 -0
  7. package/dist/commands/dom/helpers/query.d.ts +1 -1
  8. package/dist/commands/dom/helpers/query.js +66 -19
  9. package/dist/commands/dom/helpers/runElementCommand.js +4 -3
  10. package/dist/commands/dom/helpers/screenshot.js +85 -12
  11. package/dist/commands/dom/index.d.ts +1 -0
  12. package/dist/commands/dom/index.js +8 -3
  13. package/dist/commands/dom/inspect.d.ts +15 -0
  14. package/dist/commands/dom/inspect.js +82 -0
  15. package/dist/commands/dom/layout.js +2 -2
  16. package/dist/commands/dom/listeners.js +2 -2
  17. package/dist/commands/dom/semanticUtils.d.ts +14 -1
  18. package/dist/commands/dom/semanticUtils.js +44 -3
  19. package/dist/commands/installSkill.d.ts +20 -0
  20. package/dist/commands/installSkill.js +87 -0
  21. package/dist/commands/network/list.js +13 -2
  22. package/dist/commands/optionBehaviors.js +48 -6
  23. package/dist/commands/page.d.ts +1 -1
  24. package/dist/commands/page.js +62 -3
  25. package/dist/commands/shared/commonOptions.d.ts +4 -0
  26. package/dist/commands/shared/commonOptions.js +9 -0
  27. package/dist/commands/shared/optionTypes.d.ts +21 -0
  28. package/dist/commands/shared/startHelpers.d.ts +66 -0
  29. package/dist/commands/shared/startHelpers.js +91 -10
  30. package/dist/commands/shared/validation.d.ts +11 -0
  31. package/dist/commands/shared/validation.js +16 -0
  32. package/dist/commands.js +3 -0
  33. package/dist/daemon/launcher.d.ts +8 -1
  34. package/dist/daemon/launcher.js +3 -1
  35. package/dist/daemon/session/Session.d.ts +7 -0
  36. package/dist/daemon/session/Session.js +23 -1
  37. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  38. package/dist/daemon/session/commandRegistry.js +65 -9
  39. package/dist/daemon/session/interactions.d.ts +18 -5
  40. package/dist/daemon/session/interactions.js +22 -12
  41. package/dist/daemon.js +3565 -329
  42. package/dist/errors/messages.d.ts +85 -0
  43. package/dist/errors/messages.js +128 -1
  44. package/dist/index.js +2151 -960
  45. package/dist/ipc/client.d.ts +9 -0
  46. package/dist/ipc/client.js +13 -0
  47. package/dist/ipc/protocol/commands.d.ts +56 -1
  48. package/dist/ipc/protocol/commands.js +2 -0
  49. package/dist/ipc/protocol/domTypes.d.ts +35 -2
  50. package/dist/ipc/protocol/inspectTypes.d.ts +388 -0
  51. package/dist/ipc/protocol/inspectTypes.js +10 -0
  52. package/dist/runtime/dom/actionEffects.d.ts +94 -15
  53. package/dist/runtime/dom/actionEffects.js +173 -27
  54. package/dist/runtime/dom/actionEffectsScripts.d.ts +52 -14
  55. package/dist/runtime/dom/actionEffectsScripts.js +224 -32
  56. package/dist/runtime/dom/elementInfo.d.ts +26 -0
  57. package/dist/runtime/dom/elementInfo.js +65 -0
  58. package/dist/runtime/dom/eventListeners.js +14 -4
  59. package/dist/runtime/dom/formFillHelpers/fill.d.ts +3 -4
  60. package/dist/runtime/dom/formFillHelpers/fill.js +77 -28
  61. package/dist/runtime/dom/frameSelection.d.ts +11 -0
  62. package/dist/runtime/dom/frameSelection.js +20 -1
  63. package/dist/runtime/dom/frames.d.ts +38 -5
  64. package/dist/runtime/dom/frames.js +136 -21
  65. package/dist/runtime/dom/inspect.d.ts +28 -0
  66. package/dist/runtime/dom/inspect.js +557 -0
  67. package/dist/runtime/dom/inspectAllStyles.d.ts +62 -0
  68. package/dist/runtime/dom/inspectAllStyles.js +385 -0
  69. package/dist/runtime/dom/inspectCascade.d.ts +94 -0
  70. package/dist/runtime/dom/inspectCascade.js +371 -0
  71. package/dist/runtime/dom/inspectCascadeModel.d.ts +39 -0
  72. package/dist/runtime/dom/inspectCascadeModel.js +232 -0
  73. package/dist/runtime/dom/inspectHints.d.ts +62 -0
  74. package/dist/runtime/dom/inspectHints.js +305 -0
  75. package/dist/runtime/dom/inspectLayoutModel.d.ts +87 -0
  76. package/dist/runtime/dom/inspectLayoutModel.js +346 -0
  77. package/dist/runtime/dom/inspectModel.d.ts +74 -0
  78. package/dist/runtime/dom/inspectModel.js +184 -0
  79. package/dist/runtime/dom/inspectPaintModel.d.ts +157 -0
  80. package/dist/runtime/dom/inspectPaintModel.js +461 -0
  81. package/dist/runtime/dom/inspectRules.d.ts +37 -0
  82. package/dist/runtime/dom/inspectRules.js +101 -0
  83. package/dist/runtime/dom/inspectScripts.d.ts +132 -0
  84. package/dist/runtime/dom/inspectScripts.js +263 -0
  85. package/dist/runtime/dom/inspectTree.d.ts +40 -0
  86. package/dist/runtime/dom/inspectTree.js +134 -0
  87. package/dist/runtime/dom/inspectVariables.d.ts +33 -0
  88. package/dist/runtime/dom/inspectVariables.js +94 -0
  89. package/dist/runtime/dom/inspectWhyModel.d.ts +20 -0
  90. package/dist/runtime/dom/inspectWhyModel.js +134 -0
  91. package/dist/runtime/dom/layout.d.ts +5 -1
  92. package/dist/runtime/dom/layout.js +10 -3
  93. package/dist/runtime/dom/listenerPageScripts.d.ts +11 -5
  94. package/dist/runtime/dom/listenerPageScripts.js +95 -9
  95. package/dist/runtime/dom/listenerSummary.d.ts +4 -0
  96. package/dist/runtime/dom/listenerSummary.js +26 -9
  97. package/dist/runtime/dom/reactEventHelpers.d.ts +5 -0
  98. package/dist/runtime/dom/reactEventHelpers.js +12 -4
  99. package/dist/runtime/page/emulation.d.ts +20 -0
  100. package/dist/runtime/page/emulation.js +37 -0
  101. package/dist/telemetry/a11y.d.ts +10 -0
  102. package/dist/telemetry/a11y.js +78 -1
  103. package/dist/telemetry/console.d.ts +1 -0
  104. package/dist/telemetry/console.js +100 -5
  105. package/dist/telemetry/network.js +3 -1
  106. package/dist/types.d.ts +40 -0
  107. package/dist/ui/formatters/details.d.ts +8 -0
  108. package/dist/ui/formatters/details.js +59 -3
  109. package/dist/ui/formatters/dom.d.ts +2 -1
  110. package/dist/ui/formatters/dom.js +25 -9
  111. package/dist/ui/formatters/inspect.d.ts +39 -0
  112. package/dist/ui/formatters/inspect.js +596 -0
  113. package/dist/ui/formatters/installSkill.d.ts +11 -0
  114. package/dist/ui/formatters/installSkill.js +31 -0
  115. package/dist/ui/formatters/keyAttributes.d.ts +19 -0
  116. package/dist/ui/formatters/keyAttributes.js +84 -0
  117. package/dist/ui/formatters/layout.js +2 -2
  118. package/dist/ui/formatters/networkHeaders.d.ts +13 -0
  119. package/dist/ui/formatters/networkHeaders.js +23 -3
  120. package/dist/ui/formatters/networkList.d.ts +29 -1
  121. package/dist/ui/formatters/networkList.js +86 -20
  122. package/dist/ui/formatters/status.js +1 -1
  123. package/dist/ui/formatting.d.ts +9 -0
  124. package/dist/ui/formatting.js +6 -3
  125. package/dist/ui/messages/commands.d.ts +123 -7
  126. package/dist/ui/messages/commands.js +181 -10
  127. package/dist/ui/messages/networkMessages.d.ts +14 -0
  128. package/dist/ui/messages/networkMessages.js +18 -0
  129. package/dist/ui/messages/session.d.ts +14 -0
  130. package/dist/ui/messages/session.js +20 -0
  131. package/dist/utils/async.d.ts +9 -0
  132. package/dist/utils/async.js +17 -0
  133. package/dist/utils/color.d.ts +84 -0
  134. package/dist/utils/color.js +376 -0
  135. package/dist/utils/cssValues.d.ts +109 -0
  136. package/dist/utils/cssValues.js +236 -0
  137. package/dist/utils/selectorFilters.d.ts +12 -0
  138. package/dist/utils/selectorFilters.js +29 -0
  139. package/package.json +2 -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
  ),
@@ -3261,6 +3358,18 @@ function formInIframeError(iframeUrl, crossOrigin) {
3261
3358
  suggestion: crossOrigin ? "Cross-origin iframes cannot be read or controlled; open the iframe URL directly: bdg <iframe url>" : 'dom form lists forms of the main document only; its fields are reachable directly: bdg dom query "input, select, textarea", then bdg dom fill <index> <value>'
3262
3359
  };
3263
3360
  }
3361
+ function skillSourceMissingError(source) {
3362
+ return {
3363
+ message: `The bdg skill is missing from the installed package (${source})`,
3364
+ suggestion: "Reinstall bdg: npm install -g browser-debugger-cli"
3365
+ };
3366
+ }
3367
+ function skillWriteFailedError(path6, reason) {
3368
+ return {
3369
+ message: `Could not write ${path6}: ${reason}`,
3370
+ suggestion: "Check the permissions of that directory, or install for the other agent only (--claude or --agents)"
3371
+ };
3372
+ }
3264
3373
  var LAUNCHED_CHROME_DESCRIPTION, A11Y_NAME_QUERY_EXAMPLE, SCOPED_FILTER_EXAMPLES, PLAYWRIGHT_ONLY_SYNTAX, MALFORMED_SELECTOR_DETAILS, SESSION_DIR_SUGGESTION, SCROLL_PROBLEMS, VIA_LABEL_SUFFIX, NAME_QUERY_PLACEHOLDER, LABEL_WITHOUT_CONTROL;
3265
3374
  var init_messages = __esm({
3266
3375
  "src/errors/messages.ts"() {
@@ -3853,6 +3962,9 @@ async function domScroll(params) {
3853
3962
  async function pageNavigate(params) {
3854
3963
  return sendCommand("page_navigate", params);
3855
3964
  }
3965
+ async function pageEmulate(params) {
3966
+ return sendCommand("page_emulate", params);
3967
+ }
3856
3968
  async function domFormDiscover() {
3857
3969
  return sendCommand("dom_form_discover", {});
3858
3970
  }
@@ -3862,6 +3974,9 @@ async function domListeners(params) {
3862
3974
  async function domLayout(params) {
3863
3975
  return sendCommand("dom_layout", params);
3864
3976
  }
3977
+ async function domInspect(params) {
3978
+ return sendCommand("dom_inspect", params);
3979
+ }
3865
3980
  async function domWait(params) {
3866
3981
  return sendCommand(
3867
3982
  "dom_wait",
@@ -3888,7 +4003,7 @@ function textPreview2(text, length = PREVIEW_LENGTH) {
3888
4003
  const characters = Array.from(collapsed);
3889
4004
  return characters.length > length ? characters.slice(0, length).join("") + "..." : collapsed;
3890
4005
  }
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;
4006
+ 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
4007
  var init_elementInfo = __esm({
3893
4008
  "src/runtime/dom/elementInfo.ts"() {
3894
4009
  "use strict";
@@ -3933,6 +4048,48 @@ var init_elementInfo = __esm({
3933
4048
  if (!parent || rendered(parent)) start += walker.currentNode.data;
3934
4049
  }
3935
4050
  return withoutDecorations(el, start);
4051
+ }`;
4052
+ MASKED_VALUE = "\u2022\u2022\u2022\u2022";
4053
+ SENSITIVE_FIELD_JS = `(el) => {
4054
+ const autocomplete = el.getAttribute('autocomplete') || '';
4055
+ if (/(^|\\s)(cc-[a-z-]+|one-time-code|current-password|new-password)(\\s|$)/i.test(autocomplete)) return true;
4056
+ if (el.type === 'password' || /^password$/i.test(el.getAttribute('type') || '')) return true;
4057
+ const names = [el.getAttribute('name'), el.id, autocomplete].join(' ');
4058
+ if (/passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc/i.test(names)) return true;
4059
+ try {
4060
+ const security = el.ownerDocument.defaultView.getComputedStyle(el).getPropertyValue('-webkit-text-security');
4061
+ return Boolean(security) && security !== 'none';
4062
+ } catch (e) {
4063
+ return false;
4064
+ }
4065
+ }`;
4066
+ ELEMENT_STATE_JS = `(el) => {
4067
+ const isSensitive = ${SENSITIVE_FIELD_JS};
4068
+ const mask = (value) => (value ? '${MASKED_VALUE}' : '');
4069
+ const guarded = (state) => {
4070
+ if (!isSensitive(el)) return state;
4071
+ const result = { ...state, sensitive: true };
4072
+ if ('value' in result) result.value = mask(result.value);
4073
+ if ('selected' in result) result.selected = mask(result.selected);
4074
+ return result;
4075
+ };
4076
+ switch (el.localName) {
4077
+ case 'input':
4078
+ if (el.type === 'hidden') return { type: 'hidden' };
4079
+ return guarded(
4080
+ /^(checkbox|radio)$/.test(el.type)
4081
+ ? { type: el.type, checked: el.checked, value: el.getAttribute('value') || '' }
4082
+ : { type: el.type, value: el.value }
4083
+ );
4084
+ case 'textarea':
4085
+ return guarded({ value: el.value });
4086
+ case 'select':
4087
+ return guarded({ selected: Array.from(el.selectedOptions || [], (option) => option.label).join(', ') });
4088
+ case 'button':
4089
+ return el.form ? { type: el.type } : {};
4090
+ default:
4091
+ return {};
4092
+ }
3936
4093
  }`;
3937
4094
  ELEMENT_DESCRIPTION_JS = `(node) => node.tagName.toLowerCase() +
3938
4095
  (node.id ? '#' + node.id : '') +
@@ -4011,6 +4168,42 @@ var init_elementInfo = __esm({
4011
4168
  }
4012
4169
  });
4013
4170
 
4171
+ // src/commands/dom/helpers/keyAttributes.ts
4172
+ function keyAttributes(tag, attributes, state = {}) {
4173
+ const names = ATTRIBUTES_BY_TAG[tag];
4174
+ if (!names) return void 0;
4175
+ const result = {};
4176
+ const type = state.type ?? attributes["type"];
4177
+ if (type) result["type"] = type;
4178
+ for (const name of names) {
4179
+ const value = attributes[name];
4180
+ if (value) result[name] = value;
4181
+ }
4182
+ if (type === "hidden") return result;
4183
+ const secret = state.sensitive === true || type === "password";
4184
+ if (state.value) result["value"] = secret ? MASKED_VALUE : state.value;
4185
+ if (state.checked !== void 0) result["checked"] = state.checked;
4186
+ if (state.selected) result["selected"] = secret ? MASKED_VALUE : state.selected;
4187
+ return Object.keys(result).length > 0 ? result : void 0;
4188
+ }
4189
+ var ATTRIBUTES_BY_TAG;
4190
+ var init_keyAttributes = __esm({
4191
+ "src/commands/dom/helpers/keyAttributes.ts"() {
4192
+ "use strict";
4193
+ init_elementInfo();
4194
+ ATTRIBUTES_BY_TAG = {
4195
+ img: ["src", "alt"],
4196
+ a: ["href"],
4197
+ input: ["name", "placeholder"],
4198
+ textarea: ["name", "placeholder"],
4199
+ button: ["name"],
4200
+ select: ["name"],
4201
+ iframe: ["src"],
4202
+ form: ["action", "method"]
4203
+ };
4204
+ }
4205
+ });
4206
+
4014
4207
  // src/runtime/dom/elementGeometry.ts
4015
4208
  function edgesOf(box) {
4016
4209
  return { left: box.x, top: box.y, right: box.x + box.width, bottom: box.y + box.height };
@@ -4397,6 +4590,23 @@ function splitSelectorList(selector) {
4397
4590
  parts.push(selector.slice(partStart));
4398
4591
  return parts;
4399
4592
  }
4593
+ function leadingCompounds(selector) {
4594
+ if (splitSelectorList(selector).length > 1) return [];
4595
+ const text = selector.trim();
4596
+ const found = [];
4597
+ let start = 0;
4598
+ scan(text, (index, depth) => {
4599
+ const char = text[index] ?? "";
4600
+ if (depth > 0 || !/[\s>+~]/.test(char)) return index;
4601
+ const compound = text.slice(start, index).trim();
4602
+ let next = index;
4603
+ while (next + 1 < text.length && /[\s>+~]/.test(text[next + 1] ?? "")) next++;
4604
+ if (compound) found.push({ compound, rest: text.slice(next + 1).trim() });
4605
+ start = next + 1;
4606
+ return next;
4607
+ });
4608
+ return found;
4609
+ }
4400
4610
  function scan(text, visit) {
4401
4611
  let depth = 0;
4402
4612
  for (let i = 0; i < text.length; i++) {
@@ -4864,15 +5074,24 @@ async function withSelection(selector, use) {
4864
5074
  async function noMatchesError(selector) {
4865
5075
  return noNodesFoundError(selector, await noMatchContext(selector));
4866
5076
  }
5077
+ function shadowHostJS(compounds) {
5078
+ const plain = compounds.slice(0, MAX_HOST_COMPOUNDS).map((entry) => [withoutFilters(entry.compound), withoutFilters(entry.rest)]);
5079
+ 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; } })`;
5080
+ }
5081
+ function withoutFilters(selector) {
5082
+ return selector.replace(/:visible\b|:(has-text|text-is)\((?:"[^"]*"|'[^']*'|[^)"'])*\)/g, "").trim() || "*";
5083
+ }
4867
5084
  async function noMatchContext(selector) {
4868
5085
  const parts = parseSelectorFilters(selector);
4869
5086
  const unfiltered = parts && withoutVisibleFilters(parts);
4870
5087
  const single = parseSingleNameSelector(selector);
4871
5088
  const hidden = unfiltered ? `(() => { try { return (${DEEP_QUERY_JS})(${JSON.stringify(selector)}, ${JSON.stringify(unfiltered)}).length; } catch (e) { return 0; } })()` : "0";
4872
5089
  const names = single ? pageNamesJS(single.kind) : "[]";
5090
+ const compounds = leadingCompounds(selector);
5091
+ const shadowHost = compounds.length > 0 ? shadowHostJS(compounds) : "-1";
4873
5092
  try {
4874
5093
  const evaluated = await callCDP("Runtime.evaluate", {
4875
- expression: `({ hidden: ${hidden}, readyState: document.readyState, unsearched: ${UNSEARCHED_CONTENT_JS}, names: ${names} })`,
5094
+ expression: `({ hidden: ${hidden}, readyState: document.readyState, unsearched: ${UNSEARCHED_CONTENT_JS}, names: ${names}, shadowHost: ${shadowHost} })`,
4876
5095
  returnByValue: true
4877
5096
  });
4878
5097
  const { result } = evaluated.data?.result ?? {};
@@ -4893,7 +5112,8 @@ async function noMatchContext(selector) {
4893
5112
  embeds: value.unsearched.embeds === true
4894
5113
  }
4895
5114
  },
4896
- ...similar && { similar }
5115
+ ...similar && { similar },
5116
+ ...typeof value.shadowHost === "number" && compounds[value.shadowHost] && { shadowHost: compounds[value.shadowHost] }
4897
5117
  };
4898
5118
  } catch (error) {
4899
5119
  log15.debug(`Could not read the page after no match: ${getErrorMessage(error)}`);
@@ -4924,6 +5144,7 @@ async function elementsWithDetails(arrayObjectId) {
4924
5144
  backendNodeId,
4925
5145
  context: details?.context ?? "",
4926
5146
  text: details?.text ?? "",
5147
+ state: details?.state ?? {},
4927
5148
  ...viewportHint(details?.geometry, viewport)
4928
5149
  }
4929
5150
  ];
@@ -4994,17 +5215,20 @@ async function queryDOMElements(selector) {
4994
5215
  log15.debug(`Querying ${elements.length} elements with selector: ${selector}`);
4995
5216
  }
4996
5217
  const nodes = await mapConcurrently(elements, async (element, index) => {
4997
- const { backendNodeId, context, text, inViewport, clippedBy } = element;
5218
+ const { backendNodeId, context, text, state, inViewport, clippedBy } = element;
4998
5219
  const desc = await describeNode({ backendNodeId });
4999
5220
  if (!desc) return { index, nodeId: 0 };
5000
5221
  const attributes = unpackAttributes(desc.attributes);
5001
5222
  const classes = attributes["class"]?.split(/\s+/).filter(Boolean);
5002
5223
  const preview = textPreview2(text);
5224
+ const tag = desc.nodeName.toLowerCase();
5225
+ const keys = keyAttributes(tag, attributes, state);
5003
5226
  return {
5004
5227
  index,
5005
5228
  nodeId: desc.backendNodeId,
5006
- tag: desc.nodeName.toLowerCase(),
5229
+ tag,
5007
5230
  ...identifyingAttributes(attributes, desc.nodeName),
5231
+ ...keys && { attributes: keys },
5008
5232
  ...classes && { classes },
5009
5233
  ...preview && { preview },
5010
5234
  ...context && { context },
@@ -5023,20 +5247,23 @@ function identifyingAttributes(attributes, nodeName) {
5023
5247
  ...value !== void 0 && { value }
5024
5248
  };
5025
5249
  }
5026
- async function elementText(ref, full) {
5250
+ async function elementTextAndState(ref, full) {
5027
5251
  const objectGroup = `bdg-text-${process.pid}-${++queryCount}`;
5028
5252
  const resolved = await callCDP("DOM.resolveNode", { ...ref, objectGroup });
5029
5253
  const objectId = resolved.data?.result?.object.objectId;
5030
- if (!objectId) return "";
5254
+ if (!objectId) return { text: "", state: {} };
5031
5255
  try {
5032
5256
  const response = await callCDP("Runtime.callFunctionOn", {
5033
5257
  objectId,
5034
- functionDeclaration: `function (full) { return (${ELEMENT_TEXT_JS})(this, full); }`,
5258
+ functionDeclaration: `function (full) { return { text: (${ELEMENT_TEXT_JS})(this, full), state: (${ELEMENT_STATE_JS})(this) }; }`,
5035
5259
  arguments: [{ value: full }],
5036
5260
  returnByValue: true
5037
5261
  });
5038
5262
  const value = response.data?.result?.result?.value;
5039
- return typeof value === "string" ? value : "";
5263
+ return {
5264
+ text: typeof value?.text === "string" ? value.text : "",
5265
+ state: value?.state ?? {}
5266
+ };
5040
5267
  } finally {
5041
5268
  await callCDP("Runtime.releaseObjectGroup", { objectGroup });
5042
5269
  }
@@ -5049,13 +5276,18 @@ async function getDomContext(ref, options = {}) {
5049
5276
  return null;
5050
5277
  }
5051
5278
  const full = options.full === true;
5052
- const classes = unpackAttributes(desc.attributes)["class"]?.split(/\s+/).filter(Boolean);
5053
- const text = await elementText(ref, full);
5279
+ const attributes = unpackAttributes(desc.attributes);
5280
+ const classes = attributes["class"]?.split(/\s+/).filter(Boolean);
5281
+ const { text, state } = await elementTextAndState(ref, full);
5054
5282
  const preview = textPreview2(text);
5055
5283
  const longer = textPreview2(text, full ? Number.POSITIVE_INFINITY : ELEMENT_TEXT_LENGTH);
5284
+ const tag = desc.nodeName.toLowerCase();
5285
+ const keys = keyAttributes(tag, attributes, state);
5056
5286
  return {
5057
- tag: desc.nodeName.toLowerCase(),
5287
+ tag,
5058
5288
  ...classes && classes.length > 0 && { classes },
5289
+ ...keys && { attributes: keys },
5290
+ ...state.sensitive && { sensitive: true },
5059
5291
  ...preview && { preview },
5060
5292
  ...longer !== preview && { text: longer },
5061
5293
  ...!preview && await childElements(ref)
@@ -5182,10 +5414,11 @@ async function assertNodeAttached(backendNodeId, source) {
5182
5414
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.STALE_CACHE);
5183
5415
  }
5184
5416
  }
5185
- var log15, CDP_CONCURRENCY_LIMIT, queryCount, VIEWPORT_HINT_LIMIT, ELEMENT_DETAILS_FUNCTION, CHILDREN_LISTED;
5417
+ var log15, CDP_CONCURRENCY_LIMIT, queryCount, MAX_HOST_COMPOUNDS, VIEWPORT_HINT_LIMIT, ELEMENT_DETAILS_FUNCTION, CHILDREN_LISTED;
5186
5418
  var init_query = __esm({
5187
5419
  "src/commands/dom/helpers/query.ts"() {
5188
5420
  "use strict";
5421
+ init_keyAttributes();
5189
5422
  init_errors3();
5190
5423
  init_messages();
5191
5424
  init_client();
@@ -5202,14 +5435,16 @@ var init_query = __esm({
5202
5435
  log15 = createLogger("dom");
5203
5436
  CDP_CONCURRENCY_LIMIT = 10;
5204
5437
  queryCount = 0;
5438
+ MAX_HOST_COMPOUNDS = 4;
5205
5439
  VIEWPORT_HINT_LIMIT = 100;
5206
5440
  ELEMENT_DETAILS_FUNCTION = `function () {
5207
5441
  const contextOf = ${ELEMENT_CONTEXT_JS};
5208
5442
  const textOf = ${ELEMENT_TEXT_JS};
5209
5443
  const geometryOf = ${ELEMENT_GEOMETRY_JS};
5444
+ const stateOf = ${ELEMENT_STATE_JS};
5210
5445
  const read = (el, index) => {
5211
5446
  try {
5212
- return { context: contextOf(el), text: textOf(el), geometry: index < ${VIEWPORT_HINT_LIMIT} ? geometryOf(el) : null };
5447
+ return { context: contextOf(el), text: textOf(el), state: stateOf(el), geometry: index < ${VIEWPORT_HINT_LIMIT} ? geometryOf(el) : null };
5213
5448
  } catch (e) {
5214
5449
  return {};
5215
5450
  }
@@ -5486,13 +5721,22 @@ async function restoreScrollPosition(position) {
5486
5721
  returnByValue: true
5487
5722
  });
5488
5723
  }
5724
+ async function windowSize(viewport) {
5725
+ const response = await callCDP("Runtime.evaluate", {
5726
+ expression: "[window.innerWidth, window.innerHeight]",
5727
+ returnByValue: true
5728
+ });
5729
+ const value = response.data?.result?.result?.value;
5730
+ const [width, height] = Array.isArray(value) ? value : [];
5731
+ return {
5732
+ width: Math.round(width ?? viewport.clientWidth),
5733
+ height: Math.round(height ?? viewport.clientHeight)
5734
+ };
5735
+ }
5489
5736
  async function useUnitPixelRatio(devicePixelRatio, viewport) {
5490
5737
  if (devicePixelRatio === 1) return () => Promise.resolve();
5491
5738
  const sessionViewport = readSessionMetadata()?.viewport;
5492
- const size = sessionViewport ?? {
5493
- width: Math.round(viewport.clientWidth),
5494
- height: Math.round(viewport.clientHeight)
5495
- };
5739
+ const size = sessionViewport ?? await windowSize(viewport);
5496
5740
  await callCDP("Emulation.setDeviceMetricsOverride", viewportOverride(size, 1));
5497
5741
  return async () => {
5498
5742
  if (sessionViewport) {
@@ -5636,6 +5880,28 @@ async function capturePageScreenshot(outputPath, options = {}) {
5636
5880
  }
5637
5881
  return result;
5638
5882
  }
5883
+ async function visibleViewport() {
5884
+ const metrics = (await callCDP("Page.getLayoutMetrics", {})).data?.result;
5885
+ const view = metrics?.cssVisualViewport ?? metrics?.visualViewport;
5886
+ return { width: view?.clientWidth ?? 0, height: view?.clientHeight ?? 0 };
5887
+ }
5888
+ function insideView(area, view) {
5889
+ return area.x >= 0 && area.y >= 0 && area.x + area.width <= view.width && area.y + area.height <= view.height;
5890
+ }
5891
+ async function measureInView(ref) {
5892
+ const view = await visibleViewport();
5893
+ let box = await getElementBounds(ref);
5894
+ let bounds = await captureArea(ref, box);
5895
+ const fits = bounds.width <= view.width && bounds.height <= view.height;
5896
+ if (fits && !insideView(bounds, view)) {
5897
+ const dx = bounds.x + bounds.width / 2 - view.width / 2;
5898
+ const dy = bounds.y + bounds.height / 2 - view.height / 2;
5899
+ await callCDP("Runtime.evaluate", { expression: `window.scrollBy(${dx}, ${dy})` });
5900
+ box = await getElementBounds(ref);
5901
+ bounds = await captureArea(ref, box);
5902
+ }
5903
+ return { box, bounds, inView: insideView(bounds, view) };
5904
+ }
5639
5905
  async function captureArea(ref, bounds) {
5640
5906
  const objectGroup = `bdg-shot-${process.pid}`;
5641
5907
  try {
@@ -5667,8 +5933,6 @@ async function captureArea(ref, bounds) {
5667
5933
  }
5668
5934
  }
5669
5935
  async function captureElementScreenshot(outputPath, ref, options = {}) {
5670
- const box = await getElementBounds(ref);
5671
- const bounds = await captureArea(ref, box);
5672
5936
  const format = options.format ?? "png";
5673
5937
  const quality = format === "jpeg" ? options.quality ?? 90 : void 0;
5674
5938
  const noResize = options.noResize ?? false;
@@ -5677,6 +5941,20 @@ async function captureElementScreenshot(outputPath, ref, options = {}) {
5677
5941
  returnByValue: true
5678
5942
  });
5679
5943
  const devicePixelRatio = dprResponse.data?.result?.result?.value ?? 1;
5944
+ const before = (await callCDP("Page.getLayoutMetrics", {})).data?.result;
5945
+ const restoreMetrics = await useUnitPixelRatio(
5946
+ devicePixelRatio,
5947
+ before?.visualViewport ?? { clientWidth: 800, clientHeight: 600 }
5948
+ );
5949
+ let box;
5950
+ let bounds;
5951
+ let inView;
5952
+ try {
5953
+ ({ box, bounds, inView } = await measureInView(ref));
5954
+ } catch (error) {
5955
+ await restoreMetrics();
5956
+ throw error;
5957
+ }
5680
5958
  const originalWidth = bounds.width;
5681
5959
  const originalHeight = bounds.height;
5682
5960
  const resized = shouldResize(originalWidth, originalHeight, noResize);
@@ -5685,7 +5963,6 @@ async function captureElementScreenshot(outputPath, ref, options = {}) {
5685
5963
  const finalHeight = Math.round(originalHeight * scale);
5686
5964
  const metricsResponse = await callCDP("Page.getLayoutMetrics", {});
5687
5965
  const metricsResult = metricsResponse.data?.result;
5688
- const viewport = metricsResult?.visualViewport ?? { clientWidth: 800, clientHeight: 600 };
5689
5966
  const scroll = metricsResult?.cssLayoutViewport ?? { pageX: 0, pageY: 0 };
5690
5967
  const onPage = (area) => ({
5691
5968
  ...area,
@@ -5693,14 +5970,13 @@ async function captureElementScreenshot(outputPath, ref, options = {}) {
5693
5970
  y: area.y + scroll.pageY
5694
5971
  });
5695
5972
  const clip = onPage(bounds);
5696
- const restoreMetrics = await useUnitPixelRatio(devicePixelRatio, viewport);
5697
5973
  let screenshotResult;
5698
5974
  try {
5699
5975
  const screenshotResponse = await callCDP("Page.captureScreenshot", {
5700
5976
  format,
5701
5977
  ...quality !== void 0 && { quality },
5702
5978
  clip: { ...clip, scale },
5703
- captureBeyondViewport: true
5979
+ captureBeyondViewport: !inView
5704
5980
  });
5705
5981
  screenshotResult = screenshotResponse.data?.result;
5706
5982
  } finally {
@@ -9193,6 +9469,8 @@ init_schema();
9193
9469
  init_screenshotResize();
9194
9470
  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
9471
  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';
9472
+ 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';
9473
+ 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
9474
  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
9475
  var OPTION_BEHAVIORS = {
9198
9476
  "screenshot:--selector": {
@@ -9252,7 +9530,7 @@ var OPTION_BEHAVIORS = {
9252
9530
  "eval:--frame": {
9253
9531
  default: "Evaluates in the page's main frame",
9254
9532
  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)."
9533
+ 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
9534
  },
9257
9535
  "console:-H": {
9258
9536
  default: "Shows messages from current page load only (most recent navigation)",
@@ -9294,7 +9572,7 @@ var OPTION_BEHAVIORS = {
9294
9572
  "click:--no-wait": {
9295
9573
  default: "Waits for network stability after click (150ms idle, up to 2s)",
9296
9574
  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`,
9575
+ 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
9576
  tokenImpact: "A click that navigates lists the whole page load in JSON triggeredRequests"
9299
9577
  },
9300
9578
  "click:--double": {
@@ -9305,10 +9583,19 @@ var OPTION_BEHAVIORS = {
9305
9583
  default: "Left click",
9306
9584
  whenEnabled: "Right-click: the page gets contextmenu (custom context menus open); cannot be combined with --double"
9307
9585
  },
9586
+ "click:--strict": {
9587
+ default: 'A covered, hidden or zero-size element is clicked with DOM events (method "dom", with a warning, exit 0)',
9588
+ 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)",
9589
+ automaticBehavior: "Applies to --double and --right too. Nothing is dispatched when the element is unreachable, so the page is unchanged"
9590
+ },
9308
9591
  "hover:--no-wait": {
9309
9592
  default: "Waits for network stability after moving the mouse (menus may load content)",
9310
9593
  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}`
9594
+ 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`
9595
+ },
9596
+ "hover:--strict": {
9597
+ default: 'A covered, hidden or zero-size element gets synthetic mouseover/mouseenter events (method "dom", with a warning)',
9598
+ whenEnabled: "Refuses with exit 90 when a real mouse cannot reach the element, naming what covers it and suggesting bdg dom layout <selector>"
9312
9599
  },
9313
9600
  "navigate:--no-wait": {
9314
9601
  default: "Waits until the new page has loaded and the network and DOM are idle (up to 15 s)",
@@ -9318,7 +9605,7 @@ var OPTION_BEHAVIORS = {
9318
9605
  "pressKey:--no-wait": {
9319
9606
  default: "Waits for network stability after key press (150ms idle, up to 2s)",
9320
9607
  whenDisabled: NO_WAIT_TRIGGERED_REQUESTS,
9321
- automaticBehavior: `${TRIGGERED_REQUESTS_BEHAVIOR}. ${ACTION_EFFECTS_BEHAVIOR}`
9608
+ 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
9609
  },
9323
9610
  "pressKey:--times": {
9324
9611
  default: "Presses key once",
@@ -9363,7 +9650,7 @@ var OPTION_BEHAVIORS = {
9363
9650
  "listeners:--type": {
9364
9651
  default: "Lists listeners of every event type on the element, its ancestors (through open shadow roots), its document and window",
9365
9652
  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`,
9653
+ 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
9654
  tokenImpact: "Framework roots are already collapsed; --type keeps the output short on pages with many listeners"
9368
9655
  },
9369
9656
  "listeners:--all": {
@@ -9377,6 +9664,35 @@ var OPTION_BEHAVIORS = {
9377
9664
  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
9665
  tokenImpact: 'About one line per element; a cheap alternative to screenshots for "where is it?"'
9379
9666
  },
9667
+ "inspect:--index": {
9668
+ 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)",
9669
+ whenEnabled: "Inspects the nth match (0-based); out of range exits 81",
9670
+ 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`,
9671
+ 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"
9672
+ },
9673
+ "inspect:--all": {
9674
+ default: "Shows the curated groups (the properties that define the look)",
9675
+ 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",
9676
+ tokenImpact: "About 80 tokens instead of 60\u201380"
9677
+ },
9678
+ "inspect:--props": {
9679
+ default: "Shows the curated groups",
9680
+ 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'
9681
+ },
9682
+ "inspect:--rules": {
9683
+ default: "Shows the values, not where they come from",
9684
+ 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",
9685
+ 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",
9686
+ tokenImpact: "About 15\u201325 tokens per row, 5\u201320 rows"
9687
+ },
9688
+ "inspect:--why": {
9689
+ default: "Not shown",
9690
+ 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"
9691
+ },
9692
+ "inspect:--no-hints": {
9693
+ 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",
9694
+ whenEnabled: "Skips the hints and does not read the matched rules"
9695
+ },
9380
9696
  "scroll:--down": {
9381
9697
  whenEnabled: "Scrolls page down by specified pixel amount"
9382
9698
  },
@@ -9458,7 +9774,7 @@ var OPTION_BEHAVIORS = {
9458
9774
  "bdg:--viewport": {
9459
9775
  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
9776
  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"
9777
+ 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
9778
  },
9463
9779
  "bdg:--color-scheme": {
9464
9780
  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 +10429,14 @@ init_logging();
10113
10429
  function delay2(ms) {
10114
10430
  return new Promise((resolve3) => setTimeout(resolve3, ms));
10115
10431
  }
10432
+ async function waitUntil(condition, timeoutMs, pollMs = 25) {
10433
+ const deadline = Date.now() + timeoutMs;
10434
+ while (!condition()) {
10435
+ if (Date.now() >= deadline) return false;
10436
+ await delay2(Math.min(pollMs, Math.max(0, deadline - Date.now())));
10437
+ }
10438
+ return true;
10439
+ }
10116
10440
 
10117
10441
  // src/utils/file.ts
10118
10442
  init_logging();
@@ -10892,6 +11216,12 @@ function showBothSectionsWhenBothRequested(options) {
10892
11216
  options.console = false;
10893
11217
  }
10894
11218
  }
11219
+ var SELECTOR_SCOPE = "searches open shadow roots and same-origin iframes";
11220
+ var SELECTOR_OR_INDEX_ARGUMENT = `CSS selector (${SELECTOR_SCOPE}) or numeric index from query results (0-based)`;
11221
+ var SELECTOR_SCOPE_HELP = `
11222
+ Selectors search the page, open shadow roots and same-origin iframes (nested ones
11223
+ included). They cannot reach into closed shadow roots or cross-origin iframes:
11224
+ use bdg dom eval --frame <frame> for those (see bdg dom frames).`;
10895
11225
 
10896
11226
  // src/commands/cdp.ts
10897
11227
  init_errors3();
@@ -10919,9 +11249,11 @@ var COMMANDS = {
10919
11249
  dom_press_key: defineCommand(),
10920
11250
  dom_scroll: defineCommand(),
10921
11251
  page_navigate: defineCommand(),
11252
+ page_emulate: defineCommand(),
10922
11253
  dom_form_discover: defineCommand(),
10923
11254
  dom_listeners: defineCommand(),
10924
11255
  dom_layout: defineCommand(),
11256
+ dom_inspect: defineCommand(),
10925
11257
  dom_wait: defineCommand()
10926
11258
  };
10927
11259
 
@@ -11940,6 +12272,15 @@ function eventTypesOption(value, previous = []) {
11940
12272
  if (types.length > 0) return [.../* @__PURE__ */ new Set([...previous, ...types])];
11941
12273
  throw new InvalidArgumentError("Give at least one event type, e.g. click or click,keydown.");
11942
12274
  }
12275
+ function cssPropertiesOption(value, previous = []) {
12276
+ const names = parseCommaSeparated(value).map(
12277
+ (name) => name.startsWith("--") ? name : name.toLowerCase()
12278
+ );
12279
+ if (names.length > 0) return [.../* @__PURE__ */ new Set([...previous, ...names])];
12280
+ throw new InvalidArgumentError(
12281
+ "Give at least one CSS property, e.g. color or padding-top,color."
12282
+ );
12283
+ }
11943
12284
  function parseCommaSeparated(value) {
11944
12285
  return value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
11945
12286
  }
@@ -12607,10 +12948,71 @@ function formatRequestStatus(request) {
12607
12948
  return request.errorText && request.errorText !== BODY_ABORTED ? `${request.status} (${request.errorText})` : String(request.status);
12608
12949
  }
12609
12950
 
12951
+ // src/ui/formatters/networkHeaders.ts
12952
+ init_formatting();
12953
+
12954
+ // src/ui/messages/networkMessages.ts
12955
+ function followingNetworkMessage() {
12956
+ return "Following network requests... (Ctrl+C to stop)";
12957
+ }
12958
+ function stoppedFollowingNetworkMessage() {
12959
+ return "\nStopped following network requests.";
12960
+ }
12961
+ function headerRepeatedNote(count) {
12962
+ return `(sent ${count} times)`;
12963
+ }
12964
+ function localProxyNote() {
12965
+ return "(loopback; likely a local proxy)";
12966
+ }
12967
+
12968
+ // src/ui/formatters/networkHeaders.ts
12969
+ function formatNetworkHeaders(data) {
12970
+ const fmt = new OutputFormatter();
12971
+ fmt.text("Network Request Headers").separator("\u2501", 60).blank();
12972
+ fmt.text("URL:").text(` ${data.url}`).blank();
12973
+ fmt.text(`Status: ${requestStatusLine(data)}`).blank();
12974
+ if (Object.keys(data.responseHeaders).length > 0) {
12975
+ fmt.text("Response Headers:");
12976
+ formatHeaderSection(fmt, data.responseHeaders);
12977
+ fmt.blank();
12978
+ }
12979
+ if (Object.keys(data.requestHeaders).length > 0) {
12980
+ fmt.text("Request Headers:");
12981
+ formatHeaderSection(fmt, data.requestHeaders);
12982
+ fmt.blank();
12983
+ }
12984
+ if (Object.keys(data.responseHeaders).length === 0 && Object.keys(data.requestHeaders).length === 0) {
12985
+ fmt.text("No headers found").blank();
12986
+ }
12987
+ fmt.text(`Request ID: ${data.requestId}`);
12988
+ return fmt.build();
12989
+ }
12990
+ function headerValueLines(name, value) {
12991
+ if (name.toLowerCase() === "set-cookie") return value.split("\n");
12992
+ const counts = /* @__PURE__ */ new Map();
12993
+ for (const line of value.split("\n")) counts.set(line, (counts.get(line) ?? 0) + 1);
12994
+ return [...counts].map(
12995
+ ([line, count]) => count > 1 ? `${line} ${headerRepeatedNote(count)}` : line
12996
+ );
12997
+ }
12998
+ function formatHeaderSection(fmt, headers) {
12999
+ const entries = Object.entries(headers).sort(([a], [b]) => a.localeCompare(b));
13000
+ const keyWidth = Math.max(...entries.map(([k]) => k.length)) + 4;
13001
+ entries.forEach(([key, value]) => {
13002
+ headerValueLines(key, value).forEach((line) => fmt.keyValue(` ${key}`, line, keyWidth));
13003
+ });
13004
+ }
13005
+ function requestStatusLine(data) {
13006
+ const status = formatRequestStatus(data);
13007
+ const text = data.statusText && /^\d+$/.test(status) ? ` ${data.statusText}` : "";
13008
+ return [data.method, `${status}${text}`].filter(Boolean).join(" ");
13009
+ }
13010
+
12610
13011
  // src/ui/formatters/details.ts
12611
13012
  init_formatting();
12612
13013
  init_sessionCommand();
12613
13014
  init_strings();
13015
+ init_url();
12614
13016
  var MESSAGE_PREVIEW_LENGTH = 200;
12615
13017
  var BINARY_OPCODE = 2;
12616
13018
  function formatWebSocketMessage(frame) {
@@ -12652,6 +13054,34 @@ function formatBytes(bytes) {
12652
13054
  if (bytes < 1024) return `${bytes} B`;
12653
13055
  return bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1024 / 1024).toFixed(1)} MB`;
12654
13056
  }
13057
+ function isLoopback(ip) {
13058
+ const bare = ip.replace(/^\[|\]$/g, "").toLowerCase();
13059
+ return /^(::ffff:)?127\./.test(bare) || bare === "::1";
13060
+ }
13061
+ var DEFAULT_PORTS = {
13062
+ "http:": "80",
13063
+ "https:": "443",
13064
+ "ws:": "80",
13065
+ "wss:": "443"
13066
+ };
13067
+ function looksLikeLocalProxy(request) {
13068
+ const url = safeParseUrl(request.url);
13069
+ if (!url || request.serverPort === void 0 || !isLoopback(request.serverIPAddress ?? "")) {
13070
+ return false;
13071
+ }
13072
+ const hostname = url.hostname.toLowerCase();
13073
+ if (hostname === "localhost" || hostname.endsWith(".localhost") || isLoopback(hostname)) {
13074
+ return false;
13075
+ }
13076
+ const urlPort = url.port || DEFAULT_PORTS[url.protocol];
13077
+ return urlPort !== void 0 && String(request.serverPort) !== urlPort;
13078
+ }
13079
+ function remoteAddress(request) {
13080
+ const ip = request.serverIPAddress ?? "";
13081
+ const host = ip.includes(":") && !ip.startsWith("[") ? `[${ip}]` : ip;
13082
+ const address = request.serverPort ? `${host}:${request.serverPort}` : host;
13083
+ return looksLikeLocalProxy(request) ? `${address} ${localProxyNote()}` : address;
13084
+ }
12655
13085
  function requestSummaryRows(request) {
12656
13086
  const rows = [
12657
13087
  ["Request ID", request.requestId],
@@ -12670,13 +13100,15 @@ function requestSummaryRows(request) {
12670
13100
  rows.push(["Size", sizes.join(", ")]);
12671
13101
  }
12672
13102
  if (request.fromCache) rows.push(["From Cache", "yes"]);
12673
- if (request.serverIPAddress) rows.push(["Remote Address", request.serverIPAddress]);
13103
+ if (request.serverIPAddress) rows.push(["Remote Address", remoteAddress(request)]);
12674
13104
  if (request.blockedReason) rows.push(["Blocked", request.blockedReason]);
12675
13105
  return rows;
12676
13106
  }
12677
13107
  function addHeaders(fmt, title, headers) {
12678
13108
  fmt.text(title).separator("\u2501", 70);
12679
- Object.entries(headers).forEach(([key, value]) => fmt.text(` ${key}: ${value}`));
13109
+ Object.entries(headers).forEach(
13110
+ ([key, value]) => headerValueLines(key, value).forEach((line) => fmt.text(` ${key}: ${line}`))
13111
+ );
12680
13112
  fmt.blank();
12681
13113
  }
12682
13114
  function describeResponseBody(request) {
@@ -13065,7 +13497,12 @@ async function withNotFoundContext(failure, selector, searched) {
13065
13497
  const context = await noMatchContext(selector);
13066
13498
  const note = searched ? unreachableElementsNote(selector, context.unsearched) : "";
13067
13499
  const suggestion = withLoadingHint(
13068
- joinLines(context.similar, failure.errorContext?.suggestion, note ? note : void 0),
13500
+ joinLines(
13501
+ context.similar,
13502
+ context.shadowHost && shadowBoundaryLine(context.shadowHost),
13503
+ failure.errorContext?.suggestion,
13504
+ note ? note : void 0
13505
+ ),
13069
13506
  context.readyState,
13070
13507
  selector
13071
13508
  );
@@ -13211,6 +13648,7 @@ init_formatting();
13211
13648
  init_commands();
13212
13649
  init_sessionCommand();
13213
13650
  init_exitCodes();
13651
+ 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
13652
  function modifiersOption(value) {
13215
13653
  const [unknown] = findUnknownModifiers(value);
13216
13654
  if (unknown !== void 0) {
@@ -13224,7 +13662,7 @@ function registerFormInteractionCommands(program2) {
13224
13662
  const err = internalError("DOM command group not found");
13225
13663
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.SOFTWARE_ERROR);
13226
13664
  }
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) => {
13665
+ 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
13666
  await runCommand(
13229
13667
  () => runElementCommand({
13230
13668
  selectorOrIndex,
@@ -13245,14 +13683,14 @@ function registerFormInteractionCommands(program2) {
13245
13683
  formatFillOutput
13246
13684
  );
13247
13685
  });
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) => {
13686
+ 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
13687
  const action = options.double ? "double" : options.right ? "right" : "click";
13250
13688
  await runPointerCommand(selectorOrIndex, options, action);
13251
13689
  });
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) => {
13690
+ 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
13691
  await runPointerCommand(selectorOrIndex, options, "hover");
13254
13692
  });
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(
13693
+ 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
13694
  "--wait-network <ms>",
13257
13695
  "Wait for network idle after submit (milliseconds)",
13258
13696
  integerOption(0),
@@ -13279,7 +13717,7 @@ function registerFormInteractionCommands(program2) {
13279
13717
  formatSubmitOutput
13280
13718
  );
13281
13719
  });
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(
13720
+ 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
13721
  "--modifiers <mods>",
13284
13722
  "Modifier keys: shift,ctrl,alt,meta (comma-separated; aliases cmd, control, option)",
13285
13723
  modifiersOption
@@ -13381,7 +13819,8 @@ async function runPointerCommand(selectorOrIndex, options, action) {
13381
13819
  buildRequest: (target) => ({
13382
13820
  ...target,
13383
13821
  wait: options.wait !== false,
13384
- ...action !== "click" && { action }
13822
+ ...action !== "click" && { action },
13823
+ ...options.strict && { strict: true }
13385
13824
  }),
13386
13825
  call: domClick,
13387
13826
  command: action === "hover" ? "hover" : "click",
@@ -13392,18 +13831,26 @@ async function runPointerCommand(selectorOrIndex, options, action) {
13392
13831
  formatClickOutput
13393
13832
  );
13394
13833
  }
13395
- function formatActionOutput(done, details, result, keyWidth = 15) {
13834
+ function formatActionOutput(done, details, result, options = {}) {
13835
+ const keyWidth = options.keyWidth ?? 15;
13396
13836
  const fmt = new OutputFormatter();
13397
- fmt.text(actionStatusLine(done, result.warning !== void 0, result.effect === "none"));
13837
+ const stillChanging = result.settled === false && result.pending !== void 0;
13838
+ fmt.text(
13839
+ actionStatusLine(done, {
13840
+ warned: result.warning !== void 0,
13841
+ noEffect: result.effect === "none",
13842
+ stillChanging
13843
+ })
13844
+ );
13398
13845
  if (result.warning) fmt.text(`\u26A0 Warning: ${result.warning}`);
13846
+ if (stillChanging && result.pending) {
13847
+ fmt.text(`\u26A0 ${stillChangingNote(options.action ?? "action", result.pending)}`);
13848
+ }
13399
13849
  fmt.blank();
13400
13850
  fmt.keyValueList(details, keyWidth);
13401
13851
  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
- });
13852
+ listRows(fmt, "New text", (result.messages ?? []).map(newMessageText), keyWidth);
13853
+ listRows(fmt, "Shown", (result.shown ?? []).map(shownElementText), keyWidth);
13407
13854
  const omitted = result.triggeredRequestsOmitted;
13408
13855
  const requests = formatTriggeredRequestLines(result.triggeredRequests ?? [], omitted);
13409
13856
  if (requests.length > 0) {
@@ -13415,6 +13862,12 @@ function formatActionOutput(done, details, result, keyWidth = 15) {
13415
13862
  }
13416
13863
  return fmt;
13417
13864
  }
13865
+ function listRows(fmt, label, texts, keyWidth) {
13866
+ texts.forEach((text, index) => {
13867
+ if (index === 0) fmt.keyValue(label, text, keyWidth);
13868
+ else fmt.text(" ".repeat(keyWidth) + text);
13869
+ });
13870
+ }
13418
13871
  function elementRow(result) {
13419
13872
  const source = result.indexSource ? ` (${indexSourceText(result.indexSource)})` : "";
13420
13873
  if (result.element === void 0) {
@@ -13445,7 +13898,8 @@ function formatClickOutput(result) {
13445
13898
  elementRow(result),
13446
13899
  ["Method", result.method === "dom" ? "DOM events" : "mouse events"]
13447
13900
  ],
13448
- result
13901
+ result,
13902
+ { action: POINTER_ACTION_NOUN[result.action ?? "click"] }
13449
13903
  ).build();
13450
13904
  }
13451
13905
  function formatSubmitOutput(result) {
@@ -13459,7 +13913,10 @@ function formatSubmitOutput(result) {
13459
13913
  if (result.navigationOccurred === false) details.push(["Navigation", "no"]);
13460
13914
  if (result.navigationOccurred && !result.navigation) details.push(["Navigation", "yes"]);
13461
13915
  if (result.waitTimeMs !== void 0) details.push(["Wait Time", `${result.waitTimeMs}ms`]);
13462
- const fmt = formatActionOutput("Form Submitted", details, result, 20);
13916
+ const fmt = formatActionOutput("Form Submitted", details, result, {
13917
+ keyWidth: 20,
13918
+ action: "submit"
13919
+ });
13463
13920
  fmt.hints("Next steps:", [
13464
13921
  `${sessionCommand("bdg network list --last 10").padEnd(32)} Check network requests`,
13465
13922
  `${sessionCommand("bdg console --last 5").padEnd(32)} Check console messages`,
@@ -13475,7 +13932,7 @@ function formatPressKeyOutput(result) {
13475
13932
  ];
13476
13933
  if (result.times && result.times > 1) details.push(["Times", result.times.toString()]);
13477
13934
  if (result.modifiers?.length) details.push(["Modifiers", result.modifiers.join("+")]);
13478
- return formatActionOutput("Key Pressed", details, result).build();
13935
+ return formatActionOutput("Key Pressed", details, result, { action: "key press" }).build();
13479
13936
  }
13480
13937
  function formatScrollOutput(result) {
13481
13938
  const details = [["Scroll Type", result.scrollType]];
@@ -13494,36 +13951,245 @@ function formatScrollOutput(result) {
13494
13951
 
13495
13952
  // src/commands/dom/a11y.ts
13496
13953
  init_helpers();
13497
- init_errors3();
13498
- init_messages();
13499
13954
 
13500
- // src/telemetry/a11y.ts
13501
- init_errors3();
13502
- init_messages();
13503
- init_client();
13955
+ // src/commands/dom/semanticUtils.ts
13956
+ init_elementInfo();
13504
13957
 
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)]);
13958
+ // src/telemetry/roleInference.ts
13959
+ var IMPLICIT_ARIA_ROLES = {
13960
+ // Interactive elements
13961
+ a: "link",
13962
+ button: "button",
13963
+ input: "textbox",
13964
+ select: "combobox",
13965
+ textarea: "textbox",
13966
+ option: "option",
13967
+ // Headings
13968
+ h1: "heading",
13969
+ h2: "heading",
13970
+ h3: "heading",
13971
+ h4: "heading",
13972
+ h5: "heading",
13973
+ h6: "heading",
13974
+ // Landmarks
13975
+ nav: "navigation",
13976
+ main: "main",
13977
+ header: "banner",
13978
+ footer: "contentinfo",
13979
+ aside: "complementary",
13980
+ section: "region",
13981
+ article: "article",
13982
+ form: "form",
13983
+ search: "search",
13984
+ // Media
13985
+ img: "image",
13986
+ figure: "figure",
13987
+ figcaption: "caption",
13988
+ video: "video",
13989
+ audio: "audio",
13990
+ // Lists
13991
+ ul: "list",
13992
+ ol: "list",
13993
+ li: "listitem",
13994
+ dl: "list",
13995
+ dt: "term",
13996
+ dd: "definition",
13997
+ menu: "menu",
13998
+ menuitem: "menuitem",
13999
+ // Tables
14000
+ table: "table",
14001
+ thead: "rowgroup",
14002
+ tbody: "rowgroup",
14003
+ tfoot: "rowgroup",
14004
+ tr: "row",
14005
+ th: "columnheader",
14006
+ td: "cell",
14007
+ // Other semantic elements
14008
+ dialog: "dialog",
14009
+ details: "group",
14010
+ summary: "button",
14011
+ progress: "progressbar",
14012
+ meter: "meter",
14013
+ output: "status",
14014
+ address: "group",
14015
+ blockquote: "blockquote",
14016
+ code: "code",
14017
+ pre: "group",
14018
+ hr: "separator"
14019
+ };
14020
+ var DEFAULT_ROLE = "generic";
14021
+ var MAX_NAME_LENGTH = 100;
14022
+ function inferRoleFromTag(tag) {
14023
+ const normalizedTag = tag.toLowerCase();
14024
+ return IMPLICIT_ARIA_ROLES[normalizedTag] ?? DEFAULT_ROLE;
13512
14025
  }
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
- );
13526
- }
14026
+ function extractHeadingLevel(tag) {
14027
+ const match = tag.toLowerCase().match(/^h([1-6])$/);
14028
+ const level = match?.[1];
14029
+ if (!level) {
14030
+ return null;
14031
+ }
14032
+ return parseInt(level, 10);
14033
+ }
14034
+ function synthesizeA11yNode(domContext, nodeId) {
14035
+ const role = inferRoleFromTag(domContext.tag);
14036
+ const node = {
14037
+ nodeId: String(nodeId),
14038
+ role,
14039
+ inferred: true,
14040
+ backendDOMNodeId: nodeId
14041
+ };
14042
+ if (domContext.preview) {
14043
+ const name = domContext.preview.length > MAX_NAME_LENGTH ? domContext.preview.slice(0, MAX_NAME_LENGTH - 3) + "..." : domContext.preview;
14044
+ node.name = name;
14045
+ }
14046
+ const headingLevel = extractHeadingLevel(domContext.tag);
14047
+ if (headingLevel !== null) {
14048
+ node.properties = { level: headingLevel };
14049
+ }
14050
+ return node;
14051
+ }
14052
+
14053
+ // src/ui/formatters/keyAttributes.ts
14054
+ init_formatting();
14055
+ init_url();
14056
+ var MAX_VALUE_LENGTH = 40;
14057
+ function fileName(src) {
14058
+ if (/^(data|blob):/i.test(src)) return src;
14059
+ const path6 = src.split(/[?#]/)[0] ?? src;
14060
+ const name = path6.substring(path6.lastIndexOf("/") + 1);
14061
+ return name && name !== path6 ? `\u2026/${name}` : src;
14062
+ }
14063
+ function hostOf(src) {
14064
+ const parsed = /^https?:/i.test(src) ? safeParseUrl(src) : null;
14065
+ if (!parsed) return src;
14066
+ const more = parsed.pathname.length > 1 || parsed.search ? "/\u2026" : "";
14067
+ return `//${parsed.host}${more}`;
14068
+ }
14069
+ function shortUrl(url) {
14070
+ return /^https?:/i.test(url) ? `//${truncateUrl(url, MAX_VALUE_LENGTH - 2)}` : url;
14071
+ }
14072
+ function shortValue(tag, name, value) {
14073
+ let short = value;
14074
+ if (name === "src") short = tag === "iframe" ? hostOf(value) : fileName(value);
14075
+ else if (name === "href" || name === "action") short = shortUrl(value);
14076
+ return cutMiddle(escapeControlChars(short.replace(/\s+/g, " ").trim()), MAX_VALUE_LENGTH);
14077
+ }
14078
+ function keyAttributeItems(tag, attributes, leaveOut = /* @__PURE__ */ new Set()) {
14079
+ return Object.entries(attributes ?? {}).flatMap(([name, value]) => {
14080
+ if (leaveOut.has(name) || value === false) return [];
14081
+ if (value === true) return [name];
14082
+ return [`${name}="${shortValue(tag, name, value)}"`];
14083
+ });
14084
+ }
14085
+
14086
+ // src/commands/dom/semanticUtils.ts
14087
+ init_formatting();
14088
+ init_commands();
14089
+ function capitalize(str) {
14090
+ return str.charAt(0).toUpperCase() + str.slice(1);
14091
+ }
14092
+ function buildRoleText(node) {
14093
+ if (node.role.toLowerCase() === "heading" && node.properties?.["level"] !== void 0) {
14094
+ const level = node.properties["level"];
14095
+ const levelNum = typeof level === "number" ? level : Number(level);
14096
+ if (!isNaN(levelNum)) {
14097
+ return `[Heading L${levelNum}]`;
14098
+ }
14099
+ }
14100
+ return `[${capitalize(node.role)}]`;
14101
+ }
14102
+ function buildContextText(node, domContext) {
14103
+ if (node.name) {
14104
+ return ` "${node.name}"`;
14105
+ }
14106
+ if (domContext) {
14107
+ const tagPart = `<${domContext.tag}`;
14108
+ const classPart = domContext.classes && domContext.classes.length > 0 ? `.${domContext.classes.slice(0, 3).join(".")}` : "";
14109
+ const previewPart = domContext.preview && !domContext.text ? ` "${domContext.preview}"` : "";
14110
+ return ` ${tagPart}${classPart}>${previewPart}`;
14111
+ }
14112
+ return "";
14113
+ }
14114
+ function buildKeyAttributesText(node, domContext) {
14115
+ if (!domContext?.attributes) return "";
14116
+ const shown = Object.entries(domContext.attributes).filter(([, value]) => Boolean(node.name) && value === node.name).map(([name]) => name);
14117
+ if (node.value !== void 0 && node.value !== "") shown.push("value");
14118
+ const items = keyAttributeItems(domContext.tag, domContext.attributes, new Set(shown));
14119
+ return items.length > 0 ? ` ${items.join(" ")}` : "";
14120
+ }
14121
+ function buildPropertiesText(node) {
14122
+ const props = [];
14123
+ if (node.value !== void 0 && node.value !== "") props.push(`value: "${node.value}"`);
14124
+ const checked = node.properties?.["checked"];
14125
+ if (checked !== void 0) {
14126
+ props.push(checked === "mixed" ? "partly checked" : checked ? "checked" : "unchecked");
14127
+ }
14128
+ const expanded = node.properties?.["expanded"];
14129
+ if (expanded !== void 0) props.push(expanded ? "expanded" : "collapsed");
14130
+ if (node.focusable) props.push("focusable");
14131
+ if (node.focused) props.push("focused");
14132
+ if (node.disabled) props.push("disabled");
14133
+ if (node.required) props.push("required");
14134
+ return props.length > 0 ? ` (${props.join(", ")})` : "";
14135
+ }
14136
+ function formatSemanticNodeWithContext(data) {
14137
+ const { node, domContext } = data;
14138
+ const roleText = buildRoleText(node);
14139
+ const contextText = buildContextText(node, domContext);
14140
+ const keysText = buildKeyAttributesText(node, domContext);
14141
+ const propsText = buildPropertiesText(node);
14142
+ const inferredText = node.inferred ? " (inferred from DOM)" : "";
14143
+ const line = `${roleText}${contextText}${keysText}${propsText}${inferredText}`;
14144
+ if (domContext?.text) return joinLines(line, elementTextLine(domContext.text));
14145
+ if (domContext?.childCount !== void 0 && !node.name) {
14146
+ return joinLines(line, emptyElementLine(domContext.children ?? [], domContext.childCount));
14147
+ }
14148
+ return line;
14149
+ }
14150
+ function resolveNodeWithFallback(a11yNode, domContext, nodeId) {
14151
+ if (a11yNode) return withSecretMasked(a11yNode, domContext);
14152
+ if (domContext && nodeId) return synthesizeA11yNode(domContext, nodeId);
14153
+ return null;
14154
+ }
14155
+ function withSecretMasked(node, domContext) {
14156
+ if (!domContext?.sensitive || !node.value) return node;
14157
+ return { ...node, value: MASKED_VALUE };
14158
+ }
14159
+
14160
+ // src/commands/dom/a11y.ts
14161
+ init_errors3();
14162
+ init_messages();
14163
+
14164
+ // src/telemetry/a11y.ts
14165
+ init_errors3();
14166
+ init_messages();
14167
+ init_client();
14168
+ init_elementInfo();
14169
+
14170
+ // src/runtime/dom/frameLayout.ts
14171
+ init_frameScopedConnection();
14172
+ init_logging();
14173
+ init_errors();
14174
+ var log19 = createLogger("dom");
14175
+ function childFrameIds(tree) {
14176
+ return (tree?.childFrames ?? []).flatMap((child) => [child.frame.id, ...childFrameIds(child)]);
14177
+ }
14178
+
14179
+ // src/telemetry/a11y.ts
14180
+ init_concurrency();
14181
+ init_exitCodes();
14182
+ init_levenshtein();
14183
+ function buildTreeFromRawNodes(rawNodes) {
14184
+ const rawById = new Map(rawNodes.map((rawNode) => [rawNode.nodeId, rawNode]));
14185
+ const rootRaw = rawNodes.find((rawNode) => !rawNode.ignored);
14186
+ if (!rootRaw) {
14187
+ throw new CommandError(
14188
+ "No root node found in accessibility tree",
14189
+ { suggestion: "The page may not be fully loaded. Wait and retry." },
14190
+ EXIT_CODES.SOFTWARE_ERROR
14191
+ );
14192
+ }
13527
14193
  const nodes = /* @__PURE__ */ new Map();
13528
14194
  const visit = (rawNode) => {
13529
14195
  if (nodes.has(rawNode.nodeId)) return;
@@ -13562,11 +14228,60 @@ async function collectA11yTree() {
13562
14228
  EXIT_CODES.SOFTWARE_ERROR
13563
14229
  );
13564
14230
  }
13565
- return buildTreeFromRawNodes([...result.nodes, ...await collectFrameNodes(result.nodes)]);
14231
+ const tree = buildTreeFromRawNodes([
14232
+ ...result.nodes,
14233
+ ...await collectFrameNodes(result.nodes)
14234
+ ]);
14235
+ return maskSecretValues(tree, await sensitiveNodeIds(tree));
13566
14236
  } finally {
13567
14237
  await callCDP("Accessibility.disable", {});
13568
14238
  }
13569
14239
  }
14240
+ var SENSITIVE_CHECK_CONCURRENCY = 10;
14241
+ async function sensitiveNodeIds(tree) {
14242
+ const candidates = [...tree.nodes.values()].filter(
14243
+ (node) => node.value !== void 0 && node.backendDOMNodeId !== void 0
14244
+ );
14245
+ const limiter = new ConcurrencyLimiter(SENSITIVE_CHECK_CONCURRENCY);
14246
+ const checked = await Promise.all(
14247
+ candidates.map(
14248
+ (node) => limiter.run(
14249
+ async () => await isSensitiveField(node.backendDOMNodeId ?? 0) ? node.nodeId : null
14250
+ )
14251
+ )
14252
+ );
14253
+ return new Set(checked.filter((id) => id !== null));
14254
+ }
14255
+ async function isSensitiveField(backendNodeId) {
14256
+ const objectGroup = "bdg-a11y-secret";
14257
+ const resolved = await callCDP("DOM.resolveNode", { backendNodeId, objectGroup });
14258
+ const objectId = resolved.data?.result?.object?.objectId;
14259
+ if (!objectId) return true;
14260
+ try {
14261
+ const response = await callCDP("Runtime.callFunctionOn", {
14262
+ objectId,
14263
+ functionDeclaration: `function () { return this.nodeType !== 1 || (${SENSITIVE_FIELD_JS})(this); }`,
14264
+ returnByValue: true
14265
+ });
14266
+ const value = response.data?.result?.result?.value;
14267
+ return value !== false;
14268
+ } finally {
14269
+ await callCDP("Runtime.releaseObjectGroup", { objectGroup });
14270
+ }
14271
+ }
14272
+ function maskSecretValues(tree, secretIds) {
14273
+ const hide = (nodeId, inside) => {
14274
+ const node = tree.nodes.get(nodeId);
14275
+ if (!node) return;
14276
+ const { name: _name, value: _value, ...rest } = node;
14277
+ const masked = inside ? rest : { ...node, ...node.value !== void 0 && { value: MASKED_VALUE } };
14278
+ tree.nodes.set(nodeId, masked);
14279
+ if (tree.root.nodeId === nodeId) tree.root = masked;
14280
+ node.childIds?.forEach((childId) => hide(childId, true));
14281
+ };
14282
+ secretIds.forEach((nodeId) => hide(nodeId, false));
14283
+ return tree;
14284
+ }
13570
14285
  async function collectFrameNodes(pageNodes) {
13571
14286
  const tree = (await callCDP("Page.getFrameTree", {})).data?.result;
13572
14287
  const known = [...pageNodes];
@@ -13913,7 +14628,7 @@ function formatA11yNodeWithContext(data) {
13913
14628
  }
13914
14629
  function formatA11yNodeOneLine(node) {
13915
14630
  const parts = [];
13916
- parts.push(`[${capitalize(node.role)}]`);
14631
+ parts.push(`[${capitalize2(node.role)}]`);
13917
14632
  if (node.name) {
13918
14633
  parts.push(`"${node.name}"`);
13919
14634
  }
@@ -13942,7 +14657,7 @@ function formatA11yNodeOneLine(node) {
13942
14657
  }
13943
14658
  return parts.join(" ");
13944
14659
  }
13945
- function capitalize(str) {
14660
+ function capitalize2(str) {
13946
14661
  return str.charAt(0).toUpperCase() + str.slice(1);
13947
14662
  }
13948
14663
  function truncate(str, maxLen) {
@@ -14051,7 +14766,7 @@ async function handleA11yDescribe(selectorOrIndex, options) {
14051
14766
  if (domNodeId) {
14052
14767
  domContext = await getDomContext({ backendNodeId: domNodeId });
14053
14768
  }
14054
- return { node, domContext };
14769
+ return { node: withSecretMasked(node, domContext), domContext };
14055
14770
  }
14056
14771
  if (options.json) {
14057
14772
  await runJsonCommand(fetchA11yNodeData);
@@ -14118,13 +14833,7 @@ function formatDomQuery(data) {
14118
14833
  const { count, nodes, selector } = data;
14119
14834
  const fmt = new OutputFormatter();
14120
14835
  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("");
14836
+ const attributes = queryTagAttributes(node).map((item) => ` ${item}`).join("");
14128
14837
  const context = node.context ? ` (in ${node.context})` : "";
14129
14838
  const preview = node.preview ? ` ${node.preview}` : "";
14130
14839
  const position = node.inViewport && viewportPositionHint(node.inViewport, node.clippedBy);
@@ -14134,6 +14843,18 @@ function formatDomQuery(data) {
14134
14843
  const exampleIndex = nodes[0]?.index ?? 0;
14135
14844
  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
14845
  }
14846
+ function queryTagAttributes(node) {
14847
+ const identifying = {
14848
+ ...node.name && { name: node.name },
14849
+ ...node.type && { type: node.type },
14850
+ ...node.value !== void 0 && { value: node.value }
14851
+ };
14852
+ return [
14853
+ ...node.id ? [`id="${node.id}"`] : [],
14854
+ ...keyAttributeItems(node.tag ?? "", { ...identifying, ...node.attributes }),
14855
+ ...node.classes?.length ? [`class="${node.classes.join(" ")}"`] : []
14856
+ ];
14857
+ }
14137
14858
  function formatDomGet(data) {
14138
14859
  const { nodes } = data;
14139
14860
  if (nodes.length === 1) {
@@ -14820,192 +15541,33 @@ async function withReadyState(frames) {
14820
15541
 
14821
15542
  // src/commands/dom/get.ts
14822
15543
  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;
15544
+ init_errors3();
15545
+ init_messages();
15546
+ init_exitCodes();
15547
+ var DOM_GET_DEFAULT_SELECTOR = "body";
15548
+ async function targetNodeId(selectorOrIndex, index) {
15549
+ const resolver = DomElementResolver.getInstance();
15550
+ if (resolver.isNumericIndex(selectorOrIndex)) {
15551
+ return (await resolver.getNodeIdForIndex(Number(selectorOrIndex))).nodeId;
15552
+ }
15553
+ return selectMatch(selectorOrIndex, index ?? 0);
14891
15554
  }
14892
- function extractHeadingLevel(tag) {
14893
- const match = tag.toLowerCase().match(/^h([1-6])$/);
14894
- const level = match?.[1];
14895
- if (!level) {
14896
- return null;
15555
+ async function semanticElement(backendNodeId, full) {
15556
+ const ref = { backendNodeId };
15557
+ const [a11yNode, domContext] = await Promise.all([
15558
+ resolveA11yNode(ref),
15559
+ getDomContext(ref, { full })
15560
+ ]);
15561
+ const node = resolveNodeWithFallback(a11yNode, domContext, backendNodeId);
15562
+ if (!node) {
15563
+ const err = nodeIdNotFoundError(backendNodeId);
15564
+ throw new CommandError(
15565
+ err.message,
15566
+ { suggestion: err.suggestion },
15567
+ EXIT_CODES.RESOURCE_NOT_FOUND
15568
+ );
14897
15569
  }
14898
- return parseInt(level, 10);
14899
- }
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
- );
15007
- }
15008
- return { node, domContext };
15570
+ return { node, domContext };
15009
15571
  }
15010
15572
  async function handleRawGet(selectorOrIndex, options) {
15011
15573
  await runCommand(
@@ -15078,6 +15640,441 @@ async function handleDomGet(selectorOrIndex, options) {
15078
15640
  else await handleSemanticGet(target, options);
15079
15641
  }
15080
15642
 
15643
+ // src/commands/dom/inspect.ts
15644
+ init_client();
15645
+
15646
+ // src/utils/color.ts
15647
+ var D50_WHITE = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585];
15648
+
15649
+ // src/utils/cssValues.ts
15650
+ function sidesShorthand(sides) {
15651
+ const [top, right, bottom, left] = sides.map(String);
15652
+ if (top === right && right === bottom && bottom === left) return `${top}`;
15653
+ if (top === bottom && right === left) return `${top} ${right}`;
15654
+ if (right === left) return `${top} ${right} ${bottom}`;
15655
+ return `${top} ${right} ${bottom} ${left}`;
15656
+ }
15657
+ function shadowText(layer, withSpread = true) {
15658
+ const lengths = [layer.x, layer.y, layer.blur, ...withSpread ? [layer.spread] : []];
15659
+ return `${layer.inset ? "inset " : ""}${layer.color} ${lengths.join(" ")}`;
15660
+ }
15661
+
15662
+ // src/runtime/dom/inspectLayoutModel.ts
15663
+ function containerKind(display) {
15664
+ if (display === void 0) return void 0;
15665
+ if (/(^|-)flex$/.test(display) || display === "-webkit-box") return "flex";
15666
+ return /(^|-)grid$/.test(display) ? "grid" : void 0;
15667
+ }
15668
+
15669
+ // src/runtime/dom/inspectTree.ts
15670
+ var DEFAULT_TREE_DEPTH = 2;
15671
+ var DEFAULT_TREE_LIMIT = 20;
15672
+
15673
+ // src/ui/formatters/inspect.ts
15674
+ init_formatting();
15675
+ init_commands();
15676
+ init_strings();
15677
+ var PARENT_LABEL_MAX = 40;
15678
+ var LABEL_WIDTH = 7;
15679
+ var ALL_LINE_WIDTH = 100;
15680
+ function groupLine(label, parts) {
15681
+ const shown = parts.filter((part) => Boolean(part));
15682
+ return shown.length > 0 ? `${label.padEnd(LABEL_WIDTH)}${shown.join(" \xB7 ")}` : void 0;
15683
+ }
15684
+ function inspectHeader(data) {
15685
+ const display = data.layout?.display ?? data.all?.["display"];
15686
+ const kind = containerKind(display);
15687
+ return [
15688
+ data.element,
15689
+ data.content !== void 0 && `"${data.content}"`,
15690
+ data.placeholder !== void 0 && `placeholder "${data.placeholder}"`,
15691
+ data.rect && `${data.rect.w}x${data.rect.h} @${data.rect.x},${data.rect.y}`,
15692
+ kind && `[${kind}]`,
15693
+ ...inspectVisibilityBadges(data.visibility),
15694
+ data.context && `in ${data.context}`,
15695
+ data.theme === "dark" && inspectDarkThemeBadge(),
15696
+ data.animating && inspectAnimatingBadge(data.animating)
15697
+ ].filter(Boolean).join(" ");
15698
+ }
15699
+ function zero(sides) {
15700
+ return sides.every((side) => side === 0);
15701
+ }
15702
+ function limitParts(kind, limits) {
15703
+ if (!limits) return [];
15704
+ return [
15705
+ limits.w !== void 0 ? `${kind}-w ${limits.w}` : "",
15706
+ limits.h !== void 0 ? `${kind}-h ${limits.h}` : ""
15707
+ ].filter(Boolean);
15708
+ }
15709
+ function boxLine(box) {
15710
+ const padded = !zero(box.padding) || !zero(box.border);
15711
+ return groupLine("box", [
15712
+ !zero(box.margin) && `m ${sidesShorthand(box.margin)}`,
15713
+ !zero(box.padding) && `p ${sidesShorthand(box.padding)}`,
15714
+ !zero(box.border) && `b ${sidesShorthand(box.border)}`,
15715
+ padded && box.sizing === "content-box" && "sizing content-box",
15716
+ ...limitParts("min", box.min),
15717
+ ...limitParts("max", box.max),
15718
+ box.overflow && `overflow ${box.overflow}`,
15719
+ box.scroll && `scroll ${box.scroll.w}x${box.scroll.h}`
15720
+ ]);
15721
+ }
15722
+ function containerParts(container) {
15723
+ const gap = Array.isArray(container.gap) ? container.gap.join(" ") : container.gap;
15724
+ return [
15725
+ container.wrap && `wrap ${container.wrap}`,
15726
+ container.columns && `cols ${container.columns}`,
15727
+ container.rows && `rows ${container.rows}`,
15728
+ container.justify && `justify ${container.justify}`,
15729
+ container.align && `align ${container.align}`,
15730
+ gap !== void 0 && `gap ${gap}`
15731
+ ].filter((part) => Boolean(part));
15732
+ }
15733
+ function displayWords(display, direction) {
15734
+ return direction ? `${display} ${direction}` : display;
15735
+ }
15736
+ function layoutLine(layout) {
15737
+ return groupLine("layout", [
15738
+ displayWords(layout.display, layout.direction),
15739
+ layout.position && `position ${layout.position}`,
15740
+ layout.inset && `inset ${sidesShorthand(layout.inset)}`,
15741
+ layout.z !== void 0 && `z ${layout.z}`,
15742
+ ...containerParts(layout),
15743
+ layout.flex && `flex ${layout.flex}`,
15744
+ layout.self && `self ${layout.self}`,
15745
+ layout.order !== void 0 && `order ${layout.order}`,
15746
+ layout.area && `area ${layout.area}`,
15747
+ layout.valign && `valign ${layout.valign}`,
15748
+ layout.float && `float ${layout.float}`
15749
+ ]);
15750
+ }
15751
+ function parentLine(layout) {
15752
+ const parent = layout.parent;
15753
+ const offsets = layout.inParent;
15754
+ const gaps = Object.entries(layout.siblings ?? {}).map(([side, gap]) => `${side[0]}${gap}`);
15755
+ const parentWords = parent && [
15756
+ truncateByLength(parent.element, PARENT_LABEL_MAX),
15757
+ displayWords(parent.display, parent.direction),
15758
+ ...containerParts(parent),
15759
+ parent.textAlign && `text-align ${parent.textAlign}`
15760
+ ];
15761
+ return groupLine("parent", [
15762
+ parentWords?.filter(Boolean).join(" "),
15763
+ offsets && `in-parent l${offsets.left} t${offsets.top} r${offsets.right} b${offsets.bottom}`,
15764
+ gaps.length > 0 && `sib ${gaps.join(" ")}`
15765
+ ]);
15766
+ }
15767
+ function fontParts(text) {
15768
+ const loaded = [
15769
+ text.rendered && `rendered "${text.rendered}"`,
15770
+ text.webfont && "webfont loaded"
15771
+ ].filter(Boolean);
15772
+ const family = text.family && [text.family, loaded.length > 0 && `(${loaded.join(", ")})`];
15773
+ const size = text.size !== void 0 && (text.lineHeight !== void 0 ? `${text.size}/${text.lineHeight}` : `${text.size}`);
15774
+ if (family && text.weight !== void 0 && size) {
15775
+ return [
15776
+ [...family, text.weight, text.style, size].filter((part) => part !== void 0 && part !== false && part !== "").join(" ")
15777
+ ];
15778
+ }
15779
+ return [
15780
+ family && `font ${family.filter(Boolean).join(" ")}`,
15781
+ text.weight !== void 0 && `weight ${text.weight}`,
15782
+ text.style && `style ${text.style}`,
15783
+ size && `size ${size}`,
15784
+ !size && text.lineHeight !== void 0 && `line-height ${text.lineHeight}`
15785
+ ].filter((part) => Boolean(part));
15786
+ }
15787
+ function contrastText(contrast) {
15788
+ if (!contrast) return void 0;
15789
+ return [
15790
+ `contrast ${contrast.ratio} ${contrast.level}`,
15791
+ contrast.inherited && `on ${contrast.background}`,
15792
+ contrast.overImage && "(over image)",
15793
+ contrast.opacity !== void 0 && `(faded: opacity ${contrast.opacity})`
15794
+ ].filter(Boolean).join(" ");
15795
+ }
15796
+ function textLine(text) {
15797
+ return groupLine("text", [
15798
+ ...fontParts(text),
15799
+ text.color && `color ${text.color}`,
15800
+ contrastText(text.contrast),
15801
+ text.align && `align ${text.align}`,
15802
+ text.transform && `transform ${text.transform}`,
15803
+ text.tracking !== void 0 && `tracking ${text.tracking}`,
15804
+ text.decoration && `decoration ${text.decoration}`,
15805
+ text.whiteSpace && `ws ${text.whiteSpace}`,
15806
+ text.overflow && `text-overflow ${text.overflow}`,
15807
+ text.clamp && `clamp ${text.clamp}`,
15808
+ text.shadow && `shadow ${text.shadow}`,
15809
+ text.features && `features ${text.features}`
15810
+ ]);
15811
+ }
15812
+ function fillText(fill) {
15813
+ if (fill.type === "solid") return `bg ${fill.color}`;
15814
+ if (fill.type === "gradient") return `bg-image ${fill.value}`;
15815
+ return `bg-image ${fill.value}${fill.size ? ` size ${fill.size}` : ""}`;
15816
+ }
15817
+ function fillLine(data) {
15818
+ return groupLine("fill", [
15819
+ ...(data.fills ?? []).map(fillText),
15820
+ data.opacity !== void 0 && `opacity ${data.opacity}`,
15821
+ data.blend && `blend ${data.blend}`
15822
+ ]);
15823
+ }
15824
+ function borderLine(data) {
15825
+ const outline = data.outline;
15826
+ return groupLine("border", [
15827
+ ...(data.strokes ?? []).map(
15828
+ (s) => `${s.side === "all" ? "" : `${s.side} `}${s.width} ${s.style} ${s.color}`
15829
+ ),
15830
+ data.radius && `radius ${sidesShorthand(data.radius)}`,
15831
+ outline && `outline ${outline.width} ${outline.style} ${outline.color}${outline.offset ? ` offset ${outline.offset}` : ""}`
15832
+ ]);
15833
+ }
15834
+ function fxLine(data) {
15835
+ const shadows = (data.effects ?? []).map(
15836
+ (e) => shadowText({ ...e, inset: e.type === "inner-shadow" })
15837
+ );
15838
+ const fx = data.fx ?? {};
15839
+ return groupLine("fx", [
15840
+ shadows.length > 0 && `shadow ${shadows.join(", ")}`,
15841
+ fx.transform && `transform ${fx.transform}`,
15842
+ fx.filter && `filter ${fx.filter}`,
15843
+ fx.backdrop && `backdrop ${fx.backdrop}`,
15844
+ fx.clip && `clip ${fx.clip}`,
15845
+ fx.mask && `mask ${fx.mask}`,
15846
+ fx.animation && `animation ${fx.animation}`
15847
+ ]);
15848
+ }
15849
+ function stateLine(data) {
15850
+ const state = data.state ?? {};
15851
+ return groupLine("state", [
15852
+ state.cursor && `cursor ${state.cursor}`,
15853
+ state.pointerEvents && `pointer-events ${state.pointerEvents}`,
15854
+ state.visibility && `visibility ${state.visibility}`,
15855
+ state.userSelect && `select ${state.userSelect}`,
15856
+ state.appearance && `appearance ${state.appearance}`
15857
+ ]);
15858
+ }
15859
+ function pseudoText(pseudo) {
15860
+ const shadows = (pseudo.effects ?? []).map(
15861
+ (e) => shadowText({ ...e, inset: e.type === "inner-shadow" })
15862
+ );
15863
+ return [
15864
+ pseudo.type,
15865
+ pseudo.content !== void 0 && `content ${pseudo.content}`,
15866
+ pseudo.display,
15867
+ pseudo.position,
15868
+ pseudo.size && `${pseudo.size.w}x${pseudo.size.h}`,
15869
+ pseudo.color && `color ${pseudo.color}`,
15870
+ pseudo.fontStyle,
15871
+ pseudo.fontWeight !== void 0 && `weight ${pseudo.fontWeight}`,
15872
+ pseudo.contrast && `\xB7 ${contrastText(pseudo.contrast)}`,
15873
+ ...(pseudo.fills ?? []).map(fillText),
15874
+ pseudo.radius && `radius ${pseudo.radius}`,
15875
+ shadows.length > 0 && `shadow ${shadows.join(", ")}`,
15876
+ pseudo.transform && `transform ${pseudo.transform}`,
15877
+ pseudo.opacity !== void 0 && `opacity ${pseudo.opacity}`
15878
+ ].filter(Boolean).join(" ");
15879
+ }
15880
+ function pseudoLines(pseudo) {
15881
+ return (pseudo ?? []).map(
15882
+ (entry, i) => `${(i === 0 ? "pseudo" : "").padEnd(LABEL_WIDTH)}${pseudoText(entry)}`
15883
+ );
15884
+ }
15885
+ function treeRow(node) {
15886
+ return [
15887
+ node.element,
15888
+ node.count !== void 0 && `\xD7${node.count}`,
15889
+ `${node.w}x${node.h}`,
15890
+ node.layout && `[${node.layout}]`,
15891
+ node.text && `"${node.text}"`,
15892
+ node.childCount !== void 0 && `(${node.childCount})`
15893
+ ].filter(Boolean).join(" ");
15894
+ }
15895
+ function treeLines2(nodes, hidden, depth) {
15896
+ const indent = " ".repeat(depth);
15897
+ const lines = nodes.flatMap((node) => [
15898
+ `${indent}${treeRow(node)}`,
15899
+ ...treeLines2(node.children ?? [], node.hiddenChildren, depth + 1)
15900
+ ]);
15901
+ return hidden ? [...lines, `${indent}(+${hidden} not rendered)`] : lines;
15902
+ }
15903
+ function labelledLines(label, lines) {
15904
+ return lines.map((line, i) => `${i === 0 ? label.padEnd(6) : "".padEnd(6)} ${line}`);
15905
+ }
15906
+ function ruleScope(entry) {
15907
+ return [entry.condition && ` @${entry.condition}`, entry.layer && ` layer ${entry.layer}`].filter(Boolean).join("");
15908
+ }
15909
+ function cascadeBlock(data) {
15910
+ const hints = (data.hints ?? []).map(
15911
+ (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}`
15912
+ );
15913
+ return [
15914
+ ...labelledLines("hints", data.hints?.length === 0 ? ["none"] : hints),
15915
+ ...labelledLines("rules", (data.rules ?? []).map(ruleLine)),
15916
+ ...labelledLines(
15917
+ "why",
15918
+ (data.why ?? []).flatMap((why) => whyLines(why, data))
15919
+ ),
15920
+ ...data.cascade ? [inspectCascadeNote(data.cascade)] : []
15921
+ ];
15922
+ }
15923
+ function ruleLine(rule) {
15924
+ return [
15925
+ `${rule.property} ${truncateByLength(rule.value, CASCADE_VALUE_WIDTH)}`,
15926
+ rule.computed !== void 0 && ` = ${truncateByLength(rule.computed, CASCADE_VALUE_WIDTH)}`,
15927
+ `${rule.important ? " !important" : ""} \u2190 ${rule.source}`,
15928
+ ruleScope(rule),
15929
+ rule.inherited !== void 0 && ` (inherited from ${levelsUp(rule.inherited)})`,
15930
+ rule.overrides && ` over ${rule.overrides.join(", ")}`
15931
+ ].filter(Boolean).join("");
15932
+ }
15933
+ function whyLines(why, data) {
15934
+ const entries = why.chain.map((entry) => {
15935
+ const value = `${entry.via ? `${entry.via}: ` : ""}${entry.value}`;
15936
+ const resolved = entry.unset ? ` = invalid: ${entry.unset.join(", ")} not set` : entry.resolved !== void 0 ? ` = ${truncateByLength(entry.resolved, CASCADE_VALUE_WIDTH)}` : "";
15937
+ return [
15938
+ ` ${entry.status === "overridden" ? "\u2717" : "\u2713"} `,
15939
+ truncateByLength(value, CASCADE_VALUE_WIDTH) + resolved,
15940
+ entry.important ? " !important" : "",
15941
+ ` ${entry.source}${entry.specificity ? ` [${entry.specificity.join(",")}]` : ""}${ruleScope(entry)}`,
15942
+ entry.status === "inherited" ? " (inherited)" : ""
15943
+ ].join("");
15944
+ });
15945
+ const variables = (why.variables ?? []).map(
15946
+ (variable) => ` ${variable.name}: ${truncateByLength(variable.value, CASCADE_VALUE_WIDTH)} ${variable.source}${variable.inherited !== void 0 ? ` (inherited from ${levelsUp(variable.inherited)})` : ""}`
15947
+ );
15948
+ return [
15949
+ `${why.property} = ${why.computed}${midTransition(why.property, data.animating) ? ` ${inspectMidTransitionNote()}` : ""}`,
15950
+ ...entries.slice(0, 1),
15951
+ ...variables,
15952
+ ...entries.slice(1),
15953
+ ...why.chain.length === 0 ? [" no author declaration: the default or inherited value"] : []
15954
+ ];
15955
+ }
15956
+ function midTransition(property, animating) {
15957
+ return (animating ?? []).some(
15958
+ (name) => name === "all" || name === property || property.startsWith(`${name}-`) || name.startsWith(`${property}-`)
15959
+ );
15960
+ }
15961
+ function levelsUp(levels) {
15962
+ return levels === 1 ? "the parent" : `${levels} levels up`;
15963
+ }
15964
+ var CASCADE_VALUE_WIDTH = 60;
15965
+ function treeBlock(data) {
15966
+ if (!data.children && !data.hiddenChildren) return [];
15967
+ return [
15968
+ "tree",
15969
+ ...treeLines2(data.children ?? [], data.hiddenChildren, 1),
15970
+ ...data.moreRows ? [` \u2026 +${data.moreRows} more`] : []
15971
+ ];
15972
+ }
15973
+ function allLines(all) {
15974
+ const lines = [];
15975
+ let current = [];
15976
+ for (const [name, value] of Object.entries(all)) {
15977
+ current.push(`${name} ${value}`);
15978
+ if (current.join(" \xB7 ").length > ALL_LINE_WIDTH) {
15979
+ lines.push(current.join(" \xB7 "));
15980
+ current = [];
15981
+ }
15982
+ }
15983
+ if (current.length > 0) lines.push(current.join(" \xB7 "));
15984
+ return lines.map((line, i) => `${(i === 0 ? "all" : "").padEnd(LABEL_WIDTH)}${line}`);
15985
+ }
15986
+ function propLines(props) {
15987
+ if (Object.keys(props).length === 0) return ["(no matching custom properties)"];
15988
+ return Object.entries(props).map(
15989
+ ([name, prop]) => `${name}: ${prop.computed || (name.startsWith("--") ? "(not set)" : "(empty)")}${prop.value !== prop.computed ? ` = ${prop.value}` : ""}`
15990
+ );
15991
+ }
15992
+ function groupLines(data) {
15993
+ return [
15994
+ data.box && boxLine(data.box),
15995
+ data.layout && layoutLine(data.layout),
15996
+ data.layout && parentLine(data.layout),
15997
+ data.text && textLine(data.text),
15998
+ fillLine(data),
15999
+ borderLine(data),
16000
+ fxLine(data),
16001
+ stateLine(data),
16002
+ ...pseudoLines(data.pseudo)
16003
+ ];
16004
+ }
16005
+ function formatInspect(data) {
16006
+ const body = data.props ? propLines(data.props) : data.all ? allLines(data.all) : groupLines(data);
16007
+ const note = data.picked ? multipleMatchesWarning(data.count, inspectedMatchAction(data.picked, data.index)) : void 0;
16008
+ if (data.why) return joinLines(inspectHeader(data), ...cascadeBlock(data), note);
16009
+ return joinLines(inspectHeader(data), ...body, ...cascadeBlock(data), ...treeBlock(data), note);
16010
+ }
16011
+
16012
+ // src/commands/dom/inspect.ts
16013
+ init_commands();
16014
+ function registerInspectCommand(dom) {
16015
+ dom.command("inspect").description(
16016
+ "What one element looks like without a screenshot: box, layout, font (rendered, contrast), colors, borders, effects, pseudo-elements and child tree"
16017
+ ).argument("<selectorOrIndex>", SELECTOR_OR_INDEX_ARGUMENT).option(
16018
+ "--index <n>",
16019
+ "Which match to inspect (0-based; default: the first rendered one)",
16020
+ integerOption(0)
16021
+ ).option(
16022
+ "--tree <depth>",
16023
+ `Child tree depth (default ${DEFAULT_TREE_DEPTH}; 0 for none)`,
16024
+ integerOption(0, 10)
16025
+ ).option(
16026
+ "--tree-limit <n>",
16027
+ `Child tree rows at most (default ${DEFAULT_TREE_LIMIT})`,
16028
+ integerOption(1, 500)
16029
+ ).addOption(
16030
+ new Option(
16031
+ "--all",
16032
+ "Every computed property that is not its default, collapsed into shorthands, instead of the groups"
16033
+ ).conflicts("props")
16034
+ ).addHelpText("after", INSPECT_OUTPUT_LEGEND).option(
16035
+ "--props <names>",
16036
+ "Only these properties, computed and normalized (comma-separated, e.g. padding,color,--brand; '--*' or '--bs-btn-*' lists custom properties)",
16037
+ cssPropertiesOption
16038
+ ).addOption(
16039
+ new Option(
16040
+ "--rules",
16041
+ "Also show which CSS rule sets each shown property (selector, file:line, what it overrides); with --props, those properties"
16042
+ ).conflicts("all")
16043
+ ).addOption(
16044
+ new Option(
16045
+ "--why <property>",
16046
+ "Every declaration of one property: the one that applies and those it overrides (e.g. --why color)"
16047
+ ).conflicts("all").argParser(propertyOption)
16048
+ ).option("--no-hints", "Skip the check for declarations that have no effect").addOption(jsonOption()).action(async (selectorOrIndex, options) => {
16049
+ await runCommand(() => inspectTarget(selectorOrIndex, options), options, formatInspect);
16050
+ });
16051
+ }
16052
+ function propertyOption(value) {
16053
+ return value.startsWith("--") ? value.trim() : value.trim().toLowerCase();
16054
+ }
16055
+ async function inspectTarget(selectorOrIndex, options) {
16056
+ return runElementCommand({
16057
+ selectorOrIndex,
16058
+ index: options.index,
16059
+ buildRequest: (target) => ({
16060
+ ...target,
16061
+ ...filterDefined({
16062
+ tree: options.tree,
16063
+ treeLimit: options.treeLimit,
16064
+ all: options.all,
16065
+ props: options.props,
16066
+ rules: options.rules,
16067
+ why: options.why,
16068
+ ...options.hints === false && { hints: false }
16069
+ })
16070
+ }),
16071
+ call: domInspect,
16072
+ command: "inspect",
16073
+ action: "inspect the element",
16074
+ failureSuggestion: 'Verify the selector matches an element: bdg dom query "<selector>"'
16075
+ });
16076
+ }
16077
+
15081
16078
  // src/commands/dom/layout.ts
15082
16079
  init_client();
15083
16080
 
@@ -15086,13 +16083,13 @@ init_messages();
15086
16083
  init_formatting();
15087
16084
  init_commands();
15088
16085
  var LAYOUT_DISPLAY_LIMIT = 20;
15089
- function layoutLine(element, viewport) {
16086
+ function layoutLine2(element, viewport) {
15090
16087
  const { bounds } = element;
15091
16088
  return [
15092
16089
  `[${element.index}] ${element.element}${element.text ? ` "${element.text}"` : ""}`,
15093
16090
  element.inViewport !== "hidden" && `${bounds.x},${bounds.y} ${bounds.width}\xD7${bounds.height}`,
15094
16091
  layoutPositionLabel(element, viewport),
15095
- element.coveredBy && `covered by ${element.coveredBy}`,
16092
+ element.coveredBy && coverText(element.coveredBy, element.coverTransparent),
15096
16093
  element.inert && "inert",
15097
16094
  element.invisible,
15098
16095
  element.context && element.context !== element.clippedBy && `in ${element.context}`
@@ -15103,14 +16100,14 @@ function formatLayout(data) {
15103
16100
  const more = data.elements.length - shown.length + (data.omitted ?? 0);
15104
16101
  return new OutputFormatter().text(pageLayoutLine(data.page)).text(
15105
16102
  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();
16103
+ ).list(shown.map((element) => layoutLine2(element, data.page.viewport))).list(more > 0 ? [moreMatchesNote(more, data.omitted ? data.elements.length : void 0)] : []).build();
15107
16104
  }
15108
16105
 
15109
16106
  // src/commands/dom/layout.ts
15110
16107
  function registerLayoutCommand(dom) {
15111
16108
  dom.command("layout").description(
15112
16109
  "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) => {
16110
+ ).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
16111
  await runCommand(() => measureLayout(selectorOrIndex, options), options, formatLayout);
15115
16112
  });
15116
16113
  }
@@ -15295,7 +16292,7 @@ function appendListenerGroups(fmt, listeners) {
15295
16292
 
15296
16293
  // src/commands/dom/listeners.ts
15297
16294
  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(
16295
+ 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
16296
  "--type <types>",
15300
16297
  "Only these event types (comma-separated, e.g. click,keydown; repeatable)",
15301
16298
  eventTypesOption
@@ -15638,25 +16635,28 @@ function registerDomCommands(program2) {
15638
16635
  registerFormCommand(dom);
15639
16636
  registerListenersCommand(dom);
15640
16637
  registerLayoutCommand(dom);
16638
+ registerInspectCommand(dom);
15641
16639
  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) => {
16640
+ 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
16641
  await handleDomQuery(selector, options);
15644
16642
  });
15645
16643
  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
16644
  "--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)"
16645
+ "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
16646
  ).option("-j, --json", "Output as JSON").action(async (script, options) => {
15649
16647
  await handleDomEval(script, options);
15650
16648
  });
15651
16649
  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
16650
  await handleDomEval(script, options);
15653
16651
  });
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) => {
16652
+ dom.command("frames").description(
16653
+ "List the page's iframes in document order (nested and cross-origin ones included) for eval --frame"
16654
+ ).option("-j, --json", "Output as JSON").action(async (options) => {
15655
16655
  await handleDomFrames(options);
15656
16656
  });
15657
16657
  dom.command("get").description("Get semantic accessibility structure (default) or raw HTML (--raw)").argument(
15658
16658
  "[selectorOrIndex]",
15659
- `CSS selector or numeric index from query results (0-based; e.g. ".error", "#app", 0); default: ${DOM_GET_DEFAULT_SELECTOR}`
16659
+ `${SELECTOR_OR_INDEX_ARGUMENT} (e.g. ".error", "#app", 0); default: ${DOM_GET_DEFAULT_SELECTOR}`
15660
16660
  ).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
16661
  "--node-id <id>",
15662
16662
  "Get the element with this node id (from dom query/get --raw or a11y describe; implies --raw)",
@@ -15682,6 +16682,87 @@ function registerDomCommands(program2) {
15682
16682
  );
15683
16683
  }
15684
16684
 
16685
+ // src/commands/installSkill.ts
16686
+ import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "fs";
16687
+ import { homedir as homedir3 } from "os";
16688
+ import { dirname as dirname5, join as join6 } from "path";
16689
+ init_errors3();
16690
+ init_messages();
16691
+
16692
+ // src/ui/formatters/installSkill.ts
16693
+ init_formatting();
16694
+ import { homedir as homedir2 } from "os";
16695
+ function tildePath(path6) {
16696
+ const home = homedir2();
16697
+ return path6.startsWith(`${home}/`) ? `~${path6.slice(home.length)}` : path6;
16698
+ }
16699
+ function formatInstalledSkills(data) {
16700
+ const fmt = new OutputFormatter().text("bdg skill:");
16701
+ for (const skill of data.skills) {
16702
+ fmt.text(` ${skill.target.padEnd(6)} ${skill.status.padEnd(9)} ${tildePath(skill.path)}`);
16703
+ }
16704
+ return fmt.hints("Next:", [
16705
+ "Start a new agent session to load it (running sessions keep the old list)",
16706
+ "After upgrading bdg, run bdg install-skill again"
16707
+ ]).build();
16708
+ }
16709
+
16710
+ // src/commands/installSkill.ts
16711
+ init_errors();
16712
+ init_exitCodes();
16713
+ var SKILL_SOURCE_PATH = join6(PACKAGE_ROOT, ".claude", "skills", "bdg", "SKILL.md");
16714
+ var SKILL_ROOTS = {
16715
+ claude: join6(".claude", "skills"),
16716
+ agents: join6(".agents", "skills")
16717
+ };
16718
+ function installSkill(targets, home = homedir3(), source = SKILL_SOURCE_PATH) {
16719
+ if (!existsSync10(source)) {
16720
+ const err = skillSourceMissingError(source);
16721
+ throw new CommandError(
16722
+ err.message,
16723
+ { suggestion: err.suggestion },
16724
+ EXIT_CODES.RESOURCE_NOT_FOUND
16725
+ );
16726
+ }
16727
+ const content = readFileSync8(source, "utf-8");
16728
+ return targets.map(
16729
+ (target) => writeSkill(target, join6(home, SKILL_ROOTS[target], "bdg", "SKILL.md"), content)
16730
+ );
16731
+ }
16732
+ function writeSkill(target, path6, content) {
16733
+ const existing = existsSync10(path6) ? readFileSync8(path6, "utf-8") : void 0;
16734
+ if (existing === content) {
16735
+ return { target, path: path6, status: "unchanged" };
16736
+ }
16737
+ try {
16738
+ mkdirSync3(dirname5(path6), { recursive: true });
16739
+ writeFileSync2(path6, content);
16740
+ } catch (caught) {
16741
+ const err = skillWriteFailedError(path6, getErrorMessage(caught));
16742
+ throw new CommandError(
16743
+ err.message,
16744
+ { suggestion: err.suggestion },
16745
+ EXIT_CODES.PERMISSION_DENIED
16746
+ );
16747
+ }
16748
+ return { target, path: path6, status: existing === void 0 ? "installed" : "updated" };
16749
+ }
16750
+ function selectedTargets(options) {
16751
+ const picked = Object.keys(SKILL_ROOTS).filter((target) => options[target]);
16752
+ return picked.length > 0 ? picked : Object.keys(SKILL_ROOTS);
16753
+ }
16754
+ function registerInstallSkillCommand(program2) {
16755
+ program2.command("install-skill").description(
16756
+ "Install the bdg agent skill for Claude Code (~/.claude/skills) and agents reading ~/.agents/skills (Codex, Gemini CLI, ...); re-run after upgrading bdg"
16757
+ ).option("--claude", "Only ~/.claude/skills (Claude Code)").option("--agents", "Only ~/.agents/skills (Codex, Gemini CLI and other agents)").addOption(jsonOption()).action(async (options) => {
16758
+ await runCommand(
16759
+ (opts) => Promise.resolve({ success: true, data: { skills: installSkill(selectedTargets(opts)) } }),
16760
+ options,
16761
+ formatInstalledSkills
16762
+ );
16763
+ });
16764
+ }
16765
+
15685
16766
  // src/commands/network/har.ts
15686
16767
  import * as fs13 from "fs";
15687
16768
  import * as path5 from "path";
@@ -16307,6 +17388,7 @@ init_commands();
16307
17388
  // src/ui/messages/session.ts
16308
17389
  init_formatting();
16309
17390
  init_commands();
17391
+ init_sessionCommand();
16310
17392
  function startNotices(options) {
16311
17393
  const { documentStatus, loading, autoStopAt, session } = options;
16312
17394
  return [
@@ -16353,6 +17435,13 @@ function lastSessionEndText(end) {
16353
17435
  const at = new Date(end.endedAt).toLocaleTimeString();
16354
17436
  return `The last session ended at ${at}: ${why[end.reason] ?? end.reason}`;
16355
17437
  }
17438
+ function daemonStillExitingHint(pid, waitedMs) {
17439
+ const daemon = pid === void 0 ? "The daemon" : `The daemon (PID ${pid})`;
17440
+ return `${daemon} was still shutting down after ${waitedMs / 1e3}s`;
17441
+ }
17442
+ function daemonStillExitingSuggestion() {
17443
+ return `check with bdg sessions, or end it with ${sessionCommand("bdg cleanup --force")}`;
17444
+ }
16356
17445
 
16357
17446
  // src/ui/formatters/status.ts
16358
17447
  init_sessionCommand();
@@ -16438,7 +17527,7 @@ function appearanceLines(metadata, pageState) {
16438
17527
  ...viewport ? [
16439
17528
  [
16440
17529
  "Viewport",
16441
- `${viewport.width}\xD7${viewport.height}${metadata.viewport ? ` (--viewport ${metadata.viewport.width}x${metadata.viewport.height})` : ""}`
17530
+ `${viewport.width}\xD7${viewport.height}${metadata.viewport ? ` (emulated ${metadata.viewport.width}x${metadata.viewport.height})` : ""}`
16442
17531
  ]
16443
17532
  ] : [],
16444
17533
  ...scheme ? [
@@ -16691,49 +17780,13 @@ function formatPreviewVerbose(output, options) {
16691
17780
  return fmt.build();
16692
17781
  }
16693
17782
 
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
- // src/commands/network/shared.ts
16731
- init_exitCodes();
16732
- async function fetchFromLiveSession() {
16733
- const response = await getHARData();
16734
- validateIPCResponse(response);
16735
- if (!response.data?.requests) {
16736
- throw new Error("No network data in response");
17783
+ // src/commands/network/shared.ts
17784
+ init_exitCodes();
17785
+ async function fetchFromLiveSession() {
17786
+ const response = await getHARData();
17787
+ validateIPCResponse(response);
17788
+ if (!response.data?.requests) {
17789
+ throw new Error("No network data in response");
16737
17790
  }
16738
17791
  return response.data.requests;
16739
17792
  }
@@ -17013,19 +18066,56 @@ function formatDuration2(ms) {
17013
18066
  if (ms === void 0) return "-";
17014
18067
  return ms < 1e3 ? `${Math.round(ms)}ms` : `${(ms / 1e3).toFixed(1)}s`;
17015
18068
  }
17016
- function formatColumnHeader(idWidth) {
17017
- return `${"[ID]".padEnd(idWidth)} STS METH TYP ${"SIZE".padStart(8)} ${"TIME".padStart(6)} URL`;
18069
+ function pageStartOf(requests) {
18070
+ const navigationIds = requests.flatMap(
18071
+ (r) => r.navigationId === void 0 ? [] : [r.navigationId]
18072
+ );
18073
+ const current = navigationIds.length > 0 ? Math.max(...navigationIds) : void 0;
18074
+ const page = requests.filter((r) => r.navigationId === current);
18075
+ const documents = page.filter((r) => r.resourceType === "Document");
18076
+ const first = (documents.length > 0 ? documents : page).reduce(
18077
+ (earliest, r) => earliest === void 0 || r.timestamp < earliest.timestamp ? r : earliest,
18078
+ void 0
18079
+ );
18080
+ return first && {
18081
+ timestamp: first.timestamp,
18082
+ ...first.sentTime !== void 0 && { sentTime: first.sentTime }
18083
+ };
18084
+ }
18085
+ function startOffsetMs(request, pageStart) {
18086
+ if (request.sentTime !== void 0 && pageStart.sentTime !== void 0) {
18087
+ return (request.sentTime - pageStart.sentTime) * 1e3;
18088
+ }
18089
+ return request.timestamp - pageStart.timestamp;
18090
+ }
18091
+ function formatStartOffset(request, pageStart) {
18092
+ if (!pageStart) return "-";
18093
+ const ms = startOffsetMs(request, pageStart);
18094
+ const seconds = Math.abs(ms) / 1e3;
18095
+ const sign = ms < -50 ? "-" : "+";
18096
+ if (seconds < 99.95) return `${sign}${seconds.toFixed(1)}s`;
18097
+ if (seconds < 999.5) return `${sign}${Math.round(seconds)}s`;
18098
+ return `${sign}${Math.floor(seconds / 60)}m`;
17018
18099
  }
17019
- function formatRequestLine(request, verbose, idWidth) {
17020
- const id = `[${request.requestId}]`.padEnd(idWidth);
18100
+ function formatColumnHeader(widths) {
18101
+ return `${"[ID]".padEnd(widths.id)} ${"START".padStart(6)} STS ${"METH".padEnd(widths.method)} TYP ${"SIZE".padStart(8)} ${"TIME".padStart(6)} URL`;
18102
+ }
18103
+ function formatRequestLine(request, options, widths) {
18104
+ const id = `[${request.requestId}]`.padEnd(widths.id);
18105
+ const start = formatStartOffset(request, options.pageStart).padStart(6);
17021
18106
  const status = formatStatus(request).padEnd(3);
17022
- const method = request.method.padEnd(4);
18107
+ const method = request.method.padEnd(widths.method);
17023
18108
  const type = getResourceTypeAbbr(request.resourceType, request.mimeType).padEnd(3);
17024
18109
  const size = formatSize(request.encodedDataLength).padStart(8);
17025
18110
  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}`;
18111
+ const url = options.verbose ? request.url : truncateUrl(request.url, 50);
18112
+ return `${id} ${start} ${status} ${method} ${type} ${size} ${time} ${url}`;
18113
+ }
18114
+ function columnWidths(requests, minIdWidth) {
18115
+ return {
18116
+ id: Math.max(minIdWidth, ...requests.map((r) => r.requestId.length + 2)),
18117
+ method: Math.max("METH".length, ...requests.map((r) => r.method.length))
18118
+ };
17029
18119
  }
17030
18120
  function buildHeader(showingCount, filteredCount, totalCount) {
17031
18121
  const shown = filteredCount > showingCount ? `last ${showingCount} of ` : "";
@@ -17044,40 +18134,30 @@ function formatNetworkListHuman(requests, options) {
17044
18134
  fmt.text("No matching requests found.");
17045
18135
  return fmt.build();
17046
18136
  }
17047
- const idWidth = Math.max("[ID]".length, ...requests.map((r) => r.requestId.length + 2));
17048
- fmt.text(formatColumnHeader(idWidth));
18137
+ const widths = columnWidths(requests, "[ID]".length);
18138
+ fmt.text(formatColumnHeader(widths));
17049
18139
  fmt.separator("\u2500", SEPARATOR_WIDTH2);
17050
- const verbose = options.verbose ?? false;
17051
18140
  for (const request of requests) {
17052
- fmt.text(formatRequestLine(request, verbose, idWidth));
18141
+ fmt.text(formatRequestLine(request, options, widths));
17053
18142
  }
17054
18143
  return fmt.build();
17055
18144
  }
17056
18145
  var FOLLOW_ID_WIDTH = 14;
17057
18146
  function formatNetworkFollowRows(requests, options = {}) {
17058
18147
  const fmt = new OutputFormatter();
17059
- const idWidth = Math.max(FOLLOW_ID_WIDTH, ...requests.map((r) => r.requestId.length + 2));
18148
+ const widths = columnWidths(requests, FOLLOW_ID_WIDTH);
17060
18149
  if (options.header) {
17061
18150
  fmt.text("Streaming network requests... (Ctrl+C to stop)");
17062
- fmt.text(formatColumnHeader(idWidth));
18151
+ fmt.text(formatColumnHeader(widths));
17063
18152
  fmt.separator("\u2500", SEPARATOR_WIDTH2);
17064
18153
  }
17065
- for (const request of requests)
17066
- fmt.text(formatRequestLine(request, options.verbose ?? false, idWidth));
18154
+ for (const request of requests) fmt.text(formatRequestLine(request, options, widths));
17067
18155
  return fmt.build();
17068
18156
  }
17069
18157
  function formatNetworkList(requests, options) {
17070
18158
  return formatNetworkListHuman(requests, options);
17071
18159
  }
17072
18160
 
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
18161
  // src/commands/network/list.ts
17082
18162
  init_exitCodes();
17083
18163
  var MIN_LAST2 = 0;
@@ -17132,7 +18212,8 @@ function buildFormatOptions2(options, result, lastLimit) {
17132
18212
  verbose: options.verbose ?? false,
17133
18213
  last: lastLimit,
17134
18214
  totalCount: result.totalCount,
17135
- filteredCount: result.filteredCount
18215
+ filteredCount: result.filteredCount,
18216
+ ...result.pageStart && { pageStart: result.pageStart }
17136
18217
  };
17137
18218
  }
17138
18219
  async function runFollowMode2(options, resourceTypes, lastN) {
@@ -17161,9 +18242,11 @@ async function runFollowMode2(options, resourceTypes, lastN) {
17161
18242
  console.log(JSON.stringify(buildSuccessResponse(data), null, 2));
17162
18243
  }
17163
18244
  } else {
18245
+ const pageStart = pageStartOf(result.data);
17164
18246
  const text = formatNetworkFollowRows(fresh, {
17165
18247
  header: !started,
17166
- verbose: options.verbose ?? false
18248
+ verbose: options.verbose ?? false,
18249
+ ...pageStart && { pageStart }
17167
18250
  });
17168
18251
  if (text) console.log(text);
17169
18252
  }
@@ -17176,6 +18259,11 @@ async function runFollowMode2(options, resourceTypes, lastN) {
17176
18259
  intervalMs: FOLLOW_INTERVAL
17177
18260
  });
17178
18261
  }
18262
+ var COLUMNS_HELP = `Columns:
18263
+ START When the request started, from the start of the current page (its document
18264
+ request): +1.2s. Requests of earlier pages are negative. --json has the
18265
+ absolute time (timestamp, epoch ms) and data.pageStart.
18266
+ TIME How long it took (to its last byte or failure); - while pending`;
17179
18267
  function formatPresetHelp() {
17180
18268
  return Object.entries(FILTER_PRESETS).map(([name, preset]) => ` ${name.padEnd(12)} ${preset.description}`).join("\n");
17181
18269
  }
@@ -17195,11 +18283,16 @@ function registerListCommand(networkCmd) {
17195
18283
  "--type <types>",
17196
18284
  "Filter by resource type (comma-separated: Document,XHR,Fetch,etc.)"
17197
18285
  )
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", `
18286
+ ).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(
18287
+ "after",
18288
+ `
18289
+ ${COLUMNS_HELP}
18290
+
17199
18291
  ${getFilterHelpText()}
17200
18292
 
17201
18293
  Presets:
17202
- ${formatPresetHelp()}`).action(async (options) => {
18294
+ ${formatPresetHelp()}`
18295
+ ).action(async (options) => {
17203
18296
  let resourceTypes;
17204
18297
  let lastN;
17205
18298
  try {
@@ -17229,12 +18322,14 @@ ${formatPresetHelp()}`).action(async (options) => {
17229
18322
  return createErrorResult(result.error, result.exitCode, result.suggestion);
17230
18323
  }
17231
18324
  const filtered = filterRequests(result.data, options, resourceTypes);
18325
+ const pageStart = pageStartOf(result.data);
17232
18326
  return {
17233
18327
  success: true,
17234
18328
  data: {
17235
18329
  requests: lastN === 0 ? filtered : filtered.slice(-lastN),
17236
18330
  totalCount: result.data.length,
17237
- filteredCount: filtered.length
18331
+ filteredCount: filtered.length,
18332
+ ...pageStart && { pageStart }
17238
18333
  }
17239
18334
  };
17240
18335
  },
@@ -17254,635 +18349,241 @@ function registerNetworkCommands(program2) {
17254
18349
  registerDocumentCommand(networkCmd);
17255
18350
  }
17256
18351
 
17257
- // src/commands/page.ts
18352
+ // src/commands/start.ts
18353
+ import fs15 from "node:fs";
18354
+ import os3 from "node:os";
18355
+
18356
+ // src/daemon/errors.ts
18357
+ init_exitCodes();
18358
+ var DaemonError = class _DaemonError extends Error {
18359
+ exitCode;
18360
+ code;
18361
+ constructor(message, code, exitCode = EXIT_CODES.SOFTWARE_ERROR) {
18362
+ super(message);
18363
+ this.name = "DaemonError";
18364
+ this.exitCode = exitCode;
18365
+ if (code !== void 0) {
18366
+ this.code = code;
18367
+ }
18368
+ if (Error.captureStackTrace) {
18369
+ Error.captureStackTrace(this, _DaemonError);
18370
+ }
18371
+ }
18372
+ };
18373
+ var DaemonStartupError = class extends DaemonError {
18374
+ name = "DaemonStartupError";
18375
+ constructor(message, code) {
18376
+ super(message, code, EXIT_CODES.SOFTWARE_ERROR);
18377
+ }
18378
+ };
18379
+ var SessionDirError = class extends DaemonError {
18380
+ /**
18381
+ * @param message - What is wrong with the directory
18382
+ * @param suggestion - How to fix it
18383
+ * @param exitCode - 82 for permissions, 81 for a too-long path, 103 otherwise
18384
+ */
18385
+ constructor(message, suggestion, exitCode = EXIT_CODES.SESSION_FILE_ERROR) {
18386
+ super(message, "SESSION_DIR_UNUSABLE", exitCode);
18387
+ this.suggestion = suggestion;
18388
+ }
18389
+ suggestion;
18390
+ name = "SessionDirError";
18391
+ };
18392
+
18393
+ // src/daemon/launcher.ts
18394
+ import { spawn } from "child_process";
18395
+ import fs14 from "fs";
18396
+ import { join as join8 } from "path";
17258
18397
  init_messages();
17259
- init_client();
17260
- init_formatting();
17261
- init_commands();
18398
+ init_paths();
18399
+ init_logging();
18400
+ init_errors();
17262
18401
  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();
17276
- }
17277
- function isScriptUrl(url) {
17278
- const [scheme = ""] = url.trim().split(":", 1);
17279
- return url.includes(":") && scheme.toLowerCase() === "javascript";
17280
- }
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
- );
17322
- }
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()
17346
- );
17347
- }
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);
18402
+ var log24 = createLogger("launcher");
18403
+ var DAEMON_READY_TIMEOUT_MS = 5e3;
18404
+ var DAEMON_READY_POLL_MS = 20;
18405
+ async function launchDaemon() {
18406
+ if (await isDaemonAlive()) {
18407
+ log24.debug("Daemon already running");
18408
+ return void 0;
18409
+ }
18410
+ if (!fs14.existsSync(DAEMON_SCRIPT_PATH)) {
18411
+ throw new DaemonStartupError(
18412
+ `Daemon script not found at ${DAEMON_SCRIPT_PATH}. Did you run 'npm run build'?`,
18413
+ "DAEMON_SCRIPT_NOT_FOUND"
18414
+ );
18415
+ }
18416
+ assertUsableSessionDir();
18417
+ const logPath = join8(getSessionDir(), "daemon.log");
18418
+ rotateLog(logPath);
18419
+ const logFd = fs14.openSync(logPath, "a");
18420
+ log24.debug(`Starting daemon: ${DAEMON_SCRIPT_PATH}`);
18421
+ const daemon = spawn(process.execPath, [DAEMON_SCRIPT_PATH], {
18422
+ detached: true,
18423
+ stdio: ["ignore", logFd, logFd]
17352
18424
  });
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);
18425
+ fs14.closeSync(logFd);
18426
+ let exited = false;
18427
+ daemon.once("exit", () => {
18428
+ exited = true;
17358
18429
  });
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
- }
18430
+ daemon.unref();
18431
+ await waitForDaemonReady(() => exited);
18432
+ return { pid: daemon.pid, hasExited: () => exited };
18433
+ }
18434
+ function assertUsableSessionDir() {
18435
+ const dir = getSessionDir();
18436
+ const fail = (err, exitCode) => {
18437
+ throw new SessionDirError(err.message, err.suggestion, exitCode);
18438
+ };
18439
+ if (fs14.existsSync(dir) && !fs14.statSync(dir).isDirectory()) fail(sessionDirIsFileError(dir));
18440
+ const socketPath = getSessionFilePath("DAEMON_SOCKET");
18441
+ if (Buffer.byteLength(socketPath) > MAX_DAEMON_SOCKET_PATH_BYTES) {
18442
+ fail(
18443
+ socketPathTooLongError(socketPath, MAX_DAEMON_SOCKET_PATH_BYTES),
18444
+ EXIT_CODES.INVALID_ARGUMENTS
18445
+ );
18446
+ }
18447
+ try {
18448
+ ensureSessionDir();
18449
+ fs14.accessSync(dir, fs14.constants.W_OK);
18450
+ } catch (error) {
18451
+ const code = error.code;
18452
+ const denied = code === "EACCES" || code === "EPERM" || code === "EROFS";
18453
+ fail(
18454
+ sessionDirNotWritableError(dir, code ?? getErrorMessage(error)),
18455
+ denied ? EXIT_CODES.PERMISSION_DENIED : EXIT_CODES.SESSION_FILE_ERROR
17364
18456
  );
17365
18457
  }
17366
18458
  }
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 };
17379
- }
17380
- function peekSection(options) {
17381
- if (options.network) return "network";
17382
- return options.console ? "console" : void 0;
17383
- }
17384
- async function fetchAndFilterPreview(lastN, resourceTypes, only) {
17385
- const result = await fetchPreviewOutput({
17386
- lastN: resourceTypes.length > 0 ? 0 : lastN,
17387
- ...only && { only }
17388
- });
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 } };
17394
- }
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;
18459
+ var MAX_LOG_BYTES = 5 * 1024 * 1024;
18460
+ function rotateLog(logPath) {
18461
+ try {
18462
+ if (fs14.statSync(logPath).size > MAX_LOG_BYTES) {
18463
+ fs14.renameSync(logPath, `${logPath}.1`);
18464
+ }
18465
+ } catch (error) {
18466
+ log24.debug(`Daemon log not rotated: ${getErrorMessage(error)}`);
17409
18467
  }
17410
- return options;
17411
18468
  }
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 });
18469
+ async function waitForDaemonReady(hasExited) {
18470
+ const deadline = Date.now() + DAEMON_READY_TIMEOUT_MS;
18471
+ while (Date.now() < deadline) {
18472
+ if (await isDaemonAlive()) {
18473
+ log24.debug("Daemon is ready");
18474
+ return;
17417
18475
  }
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
- };
17428
- await setupFollowMode(showPreview, {
17429
- startMessage: followingPreviewMessage,
17430
- stopMessage: stoppedFollowingPreviewMessage,
17431
- intervalMs: 1e3
17432
- });
17433
- }
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.'
18476
+ if (hasExited() && !await isDaemonAlive()) {
18477
+ throw new DaemonStartupError(
18478
+ `Daemon exited during startup. See ${join8(getSessionDir(), "daemon.log")}`,
18479
+ "DAEMON_EXITED"
17443
18480
  );
17444
18481
  }
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);
17453
- }
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
17461
- };
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
- });
17490
- }
17491
-
17492
- // src/session/sessionList.ts
17493
- init_client();
17494
- import * as fs15 from "fs";
17495
- init_paths();
17496
-
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) {
17503
- try {
17504
- const port = parseInt(fs14.readFileSync(portPath, "utf-8").trim(), 10);
17505
- return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
17506
- } catch (error) {
17507
- logDebugError(log24, `read ${portPath}`, error);
17508
- return null;
18482
+ await delay2(DAEMON_READY_POLL_MS);
17509
18483
  }
18484
+ throw new DaemonStartupError(
18485
+ `Daemon failed to start within ${DAEMON_READY_TIMEOUT_MS / 1e3} seconds`,
18486
+ "DAEMON_START_TIMEOUT"
18487
+ );
17510
18488
  }
17511
18489
 
17512
- // src/session/sessionName.ts
17513
- init_errors3();
18490
+ // src/commands/shared/startHelpers.ts
17514
18491
  init_messages();
18492
+ init_client();
18493
+ init_transport();
18494
+ init_errors2();
17515
18495
  init_paths();
18496
+ init_formatting();
18497
+ init_logging();
18498
+ init_sessionCommand();
18499
+ init_errors();
17516
18500
  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);
17523
- }
17524
- function normalizeSessionName(name) {
17525
- return name.toLowerCase();
17526
- }
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));
18501
+ var log25 = createLogger("bdg");
18502
+ var SPAWNED_DAEMON_EXIT_WAIT_MS = 3e3;
18503
+ var DEFAULT_START_DEPS = {
18504
+ launch: launchDaemon,
18505
+ send: startSession,
18506
+ exitWaitMs: SPAWNED_DAEMON_EXIT_WAIT_MS
18507
+ };
18508
+ async function startSessionViaDaemon(url, options, telemetry) {
18509
+ process.once("SIGINT", () => reportStartOutcome(interruptedOutcome("SIGINT"), options));
18510
+ process.once("SIGTERM", () => reportStartOutcome(interruptedOutcome("SIGTERM"), options));
18511
+ reportStartOutcome(await attemptStart(url, options, telemetry), options);
18512
+ }
18513
+ async function attemptStart(url, options, telemetry, deps = DEFAULT_START_DEPS) {
18514
+ const spawned = [];
18515
+ const attempt = async () => {
18516
+ const outcome2 = await requestSession(url, options, telemetry, deps);
18517
+ if (!outcome2.ok && outcome2.spawned) spawned.push(outcome2.spawned);
18518
+ return outcome2;
18519
+ };
18520
+ let outcome = await attempt();
18521
+ const deadline = Date.now() + SHUTDOWN_WAIT_MS;
18522
+ while (isShuttingDown(outcome) && Date.now() < deadline) {
18523
+ await delay2(SHUTDOWN_POLL_MS);
18524
+ outcome = await attempt();
17538
18525
  }
18526
+ return afterSpawnedDaemonExit(outcome, spawned, deps.exitWaitMs);
17539
18527
  }
17540
- function namedSocketPath(name) {
17541
- return sessionFilePathIn(getNamedSessionDir(name), "DAEMON_SOCKET");
17542
- }
17543
- function throwInvalid(err) {
17544
- throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
17545
- }
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;
18528
+ async function afterSpawnedDaemonExit(outcome, spawned, waitMs) {
18529
+ if (outcome.ok) return outcome;
18530
+ const { spawned: _spawned, daemonExiting: _exiting, ...failure } = outcome;
18531
+ if (spawned.length === 0) return failure;
18532
+ if (await waitUntil(() => spawned.every((daemon) => daemon.hasExited()), waitMs)) {
18533
+ return failure;
17551
18534
  }
17552
- validateSessionName(name);
17553
- process.env[SESSION_NAME_ENV] = normalizeSessionName(name);
17554
- }
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;
18535
+ const running = spawned.filter((daemon) => !daemon.hasExited()).pop();
18536
+ const suggestion = daemonStillExitingSuggestion();
18537
+ const previous = failure.details?.["suggestion"];
17564
18538
  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 }
18539
+ ...failure,
18540
+ human: joinLines(
18541
+ failure.human,
18542
+ `${daemonStillExitingHint(running?.pid, waitMs)}; ${suggestion}`
18543
+ ),
18544
+ details: {
18545
+ ...failure.details,
18546
+ daemonStillRunning: true,
18547
+ ...running?.pid !== void 0 && { daemonPid: running.pid },
18548
+ suggestion: typeof previous === "string" && previous ? `${previous}; ${suggestion}` : suggestion
18549
+ }
17571
18550
  };
17572
18551
  }
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" };
17585
- }
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);
18552
+ function interruptedOutcome(signal) {
18553
+ const message = `Start cancelled (${signal === "SIGINT" ? "interrupted" : "terminated"})`;
18554
+ const exitCode = signal === "SIGINT" ? EXIT_CODES.INTERRUPTED : EXIT_CODES.TERMINATED;
18555
+ return { ok: false, error: message, human: message, exitCode };
17590
18556
  }
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
- };
18557
+ var SHUTDOWN_WAIT_MS = 15e3;
18558
+ var SHUTDOWN_POLL_MS = 200;
18559
+ function isShuttingDown(outcome) {
18560
+ return !outcome.ok && (outcome.retryable === true || outcome.errorCode === "SESSION_SHUTTING_DOWN" /* SESSION_SHUTTING_DOWN */);
17604
18561
  }
17605
- function leftoverPort(dir) {
18562
+ async function requestSession(url, options, telemetry, deps) {
18563
+ let spawned;
17606
18564
  try {
17607
- const meta = JSON.parse(fs15.readFileSync(sessionFilePathIn(dir, "METADATA"), "utf8"));
17608
- if (typeof meta.port === "number") return meta.port;
18565
+ spawned = await deps.launch();
17609
18566
  } catch (error) {
17610
- logDebugError(log25, `read the metadata in ${dir}`, error);
18567
+ if (error instanceof SessionDirError) {
18568
+ return {
18569
+ ok: false,
18570
+ error: error.message,
18571
+ human: joinLines(genericError(error.message), error.suggestion),
18572
+ exitCode: error.exitCode,
18573
+ details: { suggestion: error.suggestion }
18574
+ };
18575
+ }
18576
+ const message = `Failed to start daemon: ${getErrorMessage(error)}`;
18577
+ const exitCode = error instanceof DaemonError ? error.exitCode : EXIT_CODES.SOFTWARE_ERROR;
18578
+ return { ok: false, error: message, human: genericError(message), exitCode };
17611
18579
  }
17612
- return readPortFile(sessionFilePathIn(dir, "PORT"));
18580
+ const outcome = await sendStart(url, options, telemetry, deps.send);
18581
+ return !outcome.ok && outcome.daemonExiting && spawned ? { ...outcome, spawned } : outcome;
17613
18582
  }
17614
- function isSelectable({ name }) {
17615
- return name === null || isValidSessionName(name) && name === normalizeSessionName(name);
17616
- }
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
- }
17626
- }
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;
17632
- }
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) };
17637
- }
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
- })
17648
- );
17649
- return sessions.filter((session) => session !== null);
17650
- }
17651
-
17652
- // src/ui/formatters/sessions.ts
17653
- 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))
17670
- );
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
- }
17680
- return fmt.build();
17681
- }
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
- });
17694
- }
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);
17776
- }
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),
17787
- EXIT_CODES.INVALID_ARGUMENTS
17788
- );
17789
- }
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
- );
17824
- }
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"
17830
- );
17831
- }
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);
17853
- }
17854
- reportStartOutcome(outcome, options);
17855
- }
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 };
17860
- }
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 */);
17865
- }
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
- }
18583
+ async function sendStart(url, options, telemetry, send) {
17883
18584
  try {
17884
- log27.debug("Connecting to daemon...");
17885
- const response = await startSession(
18585
+ log25.debug("Connecting to daemon...");
18586
+ const response = await send(
17886
18587
  url,
17887
18588
  filterDefined({
17888
18589
  port: options.port,
@@ -17898,7 +18599,9 @@ async function requestSession(url, options, telemetry) {
17898
18599
  colorScheme: options.colorScheme
17899
18600
  })
17900
18601
  );
17901
- if (response.status === "error") return describeStartFailure(response, options);
18602
+ if (response.status === "error") {
18603
+ return { ...describeStartFailure(response, options), daemonExiting: true };
18604
+ }
17902
18605
  if (!response.data) {
17903
18606
  const message = "Invalid response from daemon: missing data";
17904
18607
  return {
@@ -17926,7 +18629,8 @@ async function requestSession(url, options, telemetry) {
17926
18629
  error: `${noActiveSessionMessage()} (daemon not running)`,
17927
18630
  human: daemonNotRunningError({ suggestStatus: true, suggestRetry: true }),
17928
18631
  exitCode: EXIT_CODES.RESOURCE_NOT_FOUND,
17929
- retryable: true
18632
+ retryable: true,
18633
+ daemonExiting: true
17930
18634
  };
17931
18635
  }
17932
18636
  const message = getErrorMessage(error);
@@ -18270,7 +18974,7 @@ function assertUserDataDir(value) {
18270
18974
  let reason;
18271
18975
  if (!value.trim()) reason = "the path is empty";
18272
18976
  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";
18977
+ else if (fs15.existsSync(resolved) && !fs15.statSync(resolved).isDirectory()) reason = "it is a file";
18274
18978
  if (reason === void 0) return;
18275
18979
  const err = invalidUserDataDirError(value, reason);
18276
18980
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
@@ -18325,6 +19029,491 @@ function assertValidChromeWsUrl(url) {
18325
19029
  );
18326
19030
  }
18327
19031
 
19032
+ // src/commands/page.ts
19033
+ init_errors3();
19034
+ init_messages();
19035
+ init_client();
19036
+ init_formatting();
19037
+ init_commands();
19038
+ init_exitCodes();
19039
+ init_url();
19040
+ function formatPageResult(result) {
19041
+ const fmt = new OutputFormatter().text(`\u2713 ${PAGE_ACTION_DONE[result.action]}`).keyValueList(
19042
+ [
19043
+ ["URL", result.url],
19044
+ ["Title", result.title],
19045
+ ...result.status !== void 0 ? [["Status", String(result.status)]] : []
19046
+ ],
19047
+ 8
19048
+ );
19049
+ if (result.warning) fmt.text(`\u26A0 ${result.warning}`);
19050
+ if (result.loading) fmt.text(`\u26A0 ${pageLoadingWarning(result.loading)}`);
19051
+ return fmt.build();
19052
+ }
19053
+ function isScriptUrl(url) {
19054
+ const [scheme = ""] = url.trim().split(":", 1);
19055
+ return url.includes(":") && scheme.toLowerCase() === "javascript";
19056
+ }
19057
+ async function runPageAction(action, options, url) {
19058
+ await runCommand(
19059
+ async () => {
19060
+ if (url !== void 0 && isScriptUrl(url)) {
19061
+ const err = javascriptNavigationError();
19062
+ return {
19063
+ success: false,
19064
+ error: err.message,
19065
+ exitCode: EXIT_CODES.INVALID_URL,
19066
+ errorContext: { suggestion: err.suggestion }
19067
+ };
19068
+ }
19069
+ if (url !== void 0) {
19070
+ const check = validateUrl(url);
19071
+ if (!check.valid) {
19072
+ return {
19073
+ success: false,
19074
+ error: check.error,
19075
+ exitCode: EXIT_CODES.INVALID_URL,
19076
+ ...check.suggestion && { errorContext: { suggestion: check.suggestion } }
19077
+ };
19078
+ }
19079
+ }
19080
+ const response = await pageNavigate({
19081
+ action,
19082
+ ...url !== void 0 && { url },
19083
+ wait: options.wait !== false
19084
+ });
19085
+ if (response.status === "error" || !response.data) {
19086
+ return {
19087
+ success: false,
19088
+ error: response.error ?? `Failed to ${action}`,
19089
+ exitCode: response.exitCode ?? EXIT_CODES.SOFTWARE_ERROR,
19090
+ ...response.suggestion && { errorContext: { suggestion: response.suggestion } }
19091
+ };
19092
+ }
19093
+ return { success: true, data: response.data };
19094
+ },
19095
+ options,
19096
+ formatPageResult
19097
+ );
19098
+ }
19099
+ async function showPageInfo(options) {
19100
+ await runCommand(
19101
+ async () => {
19102
+ const response = await getStatus();
19103
+ if (response.status === "error") {
19104
+ return {
19105
+ success: false,
19106
+ error: response.error ?? "Failed to read the page",
19107
+ exitCode: EXIT_CODES.SOFTWARE_ERROR
19108
+ };
19109
+ }
19110
+ const page = response.data?.sessionPid ? response.data.pageState : void 0;
19111
+ if (!page) throw noActiveSessionError();
19112
+ return { success: true, data: { url: page.url, title: page.title } };
19113
+ },
19114
+ options,
19115
+ (page) => new OutputFormatter().keyValueList(
19116
+ [
19117
+ ["URL", page.url],
19118
+ ["Title", page.title]
19119
+ ],
19120
+ 8
19121
+ ).build()
19122
+ );
19123
+ }
19124
+ function emulationRequest(options) {
19125
+ if (options.reset) return { reset: true };
19126
+ if (options.viewport === void 0 && options.colorScheme === void 0) {
19127
+ const err = pageEmulateNothingError();
19128
+ throw new CommandError(
19129
+ err.message,
19130
+ { suggestion: err.suggestion },
19131
+ EXIT_CODES.INVALID_ARGUMENTS
19132
+ );
19133
+ }
19134
+ return {
19135
+ ...options.viewport !== void 0 && { viewport: parseViewport(options.viewport) },
19136
+ ...options.colorScheme !== void 0 && {
19137
+ colorScheme: parseColorScheme(options.colorScheme)
19138
+ }
19139
+ };
19140
+ }
19141
+ async function emulate(options) {
19142
+ await runCommand(
19143
+ async () => {
19144
+ const response = await pageEmulate(emulationRequest(options));
19145
+ if (response.status === "error" || !response.data) {
19146
+ return {
19147
+ success: false,
19148
+ error: response.error ?? "Failed to change the emulation",
19149
+ exitCode: response.exitCode ?? EXIT_CODES.SOFTWARE_ERROR,
19150
+ ...response.suggestion && { errorContext: { suggestion: response.suggestion } }
19151
+ };
19152
+ }
19153
+ return { success: true, data: response.data };
19154
+ },
19155
+ options,
19156
+ (result) => new OutputFormatter().text("\u2713 Page emulation changed").keyValueList(pageEmulationLines(result), 10).build()
19157
+ );
19158
+ }
19159
+ function registerPageCommands(program2) {
19160
+ const page = program2.command("page").description(
19161
+ "The session page: info (URL and title), navigate <url>, reload, back, forward, emulate (viewport, color scheme)"
19162
+ );
19163
+ page.command("info").description(PAGE_INFO_DESCRIPTION).addOption(jsonOption()).action(async (options) => {
19164
+ await showPageInfo(options);
19165
+ });
19166
+ const withCommon = (command) => command.option("--no-wait", "Return without waiting for the page to load").addOption(jsonOption());
19167
+ withCommon(
19168
+ page.command("navigate").description("Load a URL in the session page").argument("<url>", "URL to load")
19169
+ ).action(async (url, options) => {
19170
+ await runPageAction("navigate", options, url);
19171
+ });
19172
+ 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(
19173
+ new Option("--reset", "Back to the browser window size and the system setting").conflicts([
19174
+ "viewport",
19175
+ "colorScheme"
19176
+ ])
19177
+ ).addOption(jsonOption()).action(async (options) => {
19178
+ await emulate(options);
19179
+ });
19180
+ for (const action of ["reload", "back", "forward"]) {
19181
+ withCommon(page.command(action).description(PAGE_ACTION_DESCRIPTIONS[action])).action(
19182
+ async (options) => {
19183
+ await runPageAction(action, options);
19184
+ }
19185
+ );
19186
+ }
19187
+ }
19188
+
19189
+ // src/commands/peek.ts
19190
+ function parseOptions(options) {
19191
+ const lastN = positiveIntRule({
19192
+ name: "--last",
19193
+ min: 1,
19194
+ max: MAX_LAST_ITEMS,
19195
+ default: 10,
19196
+ allowZeroForAll: true
19197
+ }).validate(options.last);
19198
+ const resourceTypes = resourceTypeRule().validate(options.type);
19199
+ return { lastN, resourceTypes };
19200
+ }
19201
+ function peekSection(options) {
19202
+ if (options.network) return "network";
19203
+ return options.console ? "console" : void 0;
19204
+ }
19205
+ async function fetchAndFilterPreview(lastN, resourceTypes, only) {
19206
+ const result = await fetchPreviewOutput({
19207
+ lastN: resourceTypes.length > 0 ? 0 : lastN,
19208
+ ...only && { only }
19209
+ });
19210
+ if (!result.success) return result;
19211
+ const output = result.data;
19212
+ const unfilteredNetworkCount = output.totals?.network ?? output.data.network?.length ?? 0;
19213
+ if (resourceTypes.length === 0 || !output.data.network) {
19214
+ return { success: true, data: { output, unfilteredNetworkCount } };
19215
+ }
19216
+ const network = filterByResourceType(output.data.network, resourceTypes);
19217
+ const filteredOutput = {
19218
+ ...output,
19219
+ data: { ...output.data, network },
19220
+ ...output.totals && { totals: { ...output.totals, network: network.length } }
19221
+ };
19222
+ return { success: true, data: { output: filteredOutput, unfilteredNetworkCount } };
19223
+ }
19224
+ function createPreviewOptions(base, resourceTypes, unfilteredCount) {
19225
+ const options = { ...base };
19226
+ if (base.follow) options.viewedAt = /* @__PURE__ */ new Date();
19227
+ if (resourceTypes.length > 0) {
19228
+ options.filteredTypes = resourceTypes;
19229
+ options.unfilteredNetworkCount = unfilteredCount;
19230
+ }
19231
+ return options;
19232
+ }
19233
+ async function runFollowMode3(options, lastN, resourceTypes, baseOptions) {
19234
+ const showPreview = async () => {
19235
+ const result = await fetchAndFilterPreview(lastN, resourceTypes, peekSection(options));
19236
+ if (!result.success) {
19237
+ return followFetchFailure(result, { json: options.json, retryIntervalMs: 1e3 });
19238
+ }
19239
+ noteFollowConnected();
19240
+ if (!options.json) console.clear();
19241
+ const previewOptions = createPreviewOptions(
19242
+ baseOptions,
19243
+ resourceTypes,
19244
+ result.data.unfilteredNetworkCount
19245
+ );
19246
+ console.log(formatPreview(result.data.output, previewOptions));
19247
+ return void 0;
19248
+ };
19249
+ await setupFollowMode(showPreview, {
19250
+ startMessage: followingPreviewMessage,
19251
+ stopMessage: stoppedFollowingPreviewMessage,
19252
+ intervalMs: 1e3
19253
+ });
19254
+ }
19255
+ function registerPeekCommand(program2) {
19256
+ 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(
19257
+ "--type <types>",
19258
+ "Filter network requests by resource type (comma-separated: Document,XHR,Fetch,etc.)"
19259
+ ).action(async (options) => {
19260
+ showBothSectionsWhenBothRequested(options);
19261
+ if (options.network && !options.json) {
19262
+ console.error(
19263
+ 'Note: "bdg peek --network" is deprecated. Use "bdg network list" for enhanced filtering.'
19264
+ );
19265
+ }
19266
+ let lastN;
19267
+ let resourceTypes;
19268
+ try {
19269
+ const parsed = parseOptions(options);
19270
+ lastN = parsed.lastN;
19271
+ resourceTypes = parsed.resourceTypes;
19272
+ } catch (error) {
19273
+ handleValidationError(error, options.json ?? false);
19274
+ }
19275
+ const baseOptions = {
19276
+ json: options.json,
19277
+ network: options.network,
19278
+ console: options.console,
19279
+ last: lastN,
19280
+ verbose: options.verbose,
19281
+ follow: options.follow
19282
+ };
19283
+ if (options.follow) {
19284
+ await runFollowMode3(options, lastN, resourceTypes, baseOptions);
19285
+ return;
19286
+ }
19287
+ await runCommand(
19288
+ async () => {
19289
+ const result = await fetchAndFilterPreview(lastN, resourceTypes, peekSection(options));
19290
+ if (!result.success) {
19291
+ return createErrorResult(result.error, result.exitCode, result.suggestion);
19292
+ }
19293
+ const { output } = result.data;
19294
+ return {
19295
+ success: true,
19296
+ data: options.json ? buildPreviewJsonData(output, baseOptions) : output
19297
+ };
19298
+ },
19299
+ options,
19300
+ (data) => {
19301
+ const output = data;
19302
+ const previewOptions = createPreviewOptions(
19303
+ baseOptions,
19304
+ resourceTypes,
19305
+ output.data.network?.length ?? 0
19306
+ );
19307
+ return formatPreview(output, previewOptions);
19308
+ }
19309
+ );
19310
+ });
19311
+ }
19312
+
19313
+ // src/session/sessionList.ts
19314
+ init_client();
19315
+ import * as fs17 from "fs";
19316
+ init_paths();
19317
+
19318
+ // src/session/portClaims.ts
19319
+ import * as fs16 from "fs";
19320
+ init_paths();
19321
+ init_logging();
19322
+ var log26 = createLogger("session");
19323
+ function readPortFile(portPath) {
19324
+ try {
19325
+ const port = parseInt(fs16.readFileSync(portPath, "utf-8").trim(), 10);
19326
+ return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
19327
+ } catch (error) {
19328
+ logDebugError(log26, `read ${portPath}`, error);
19329
+ return null;
19330
+ }
19331
+ }
19332
+
19333
+ // src/session/sessionName.ts
19334
+ init_errors3();
19335
+ init_messages();
19336
+ init_paths();
19337
+ init_exitCodes();
19338
+ var MAX_SESSION_NAME_LENGTH = 40;
19339
+ var SESSION_NAME_PATTERN = new RegExp(
19340
+ `^[A-Za-z0-9][A-Za-z0-9_-]{0,${MAX_SESSION_NAME_LENGTH - 1}}$`
19341
+ );
19342
+ function isValidSessionName(name) {
19343
+ return SESSION_NAME_PATTERN.test(name);
19344
+ }
19345
+ function normalizeSessionName(name) {
19346
+ return name.toLowerCase();
19347
+ }
19348
+ function validateSessionName(name) {
19349
+ if (!isValidSessionName(name)) {
19350
+ throwInvalid(invalidSessionNameError(name, MAX_SESSION_NAME_LENGTH));
19351
+ }
19352
+ const shortest = namedSocketPath("a");
19353
+ if (Buffer.byteLength(shortest) > MAX_DAEMON_SOCKET_PATH_BYTES) {
19354
+ throwInvalid(socketPathTooLongError(shortest, MAX_DAEMON_SOCKET_PATH_BYTES));
19355
+ }
19356
+ const socketPath = namedSocketPath(name);
19357
+ if (Buffer.byteLength(socketPath) > MAX_DAEMON_SOCKET_PATH_BYTES) {
19358
+ throwInvalid(sessionNameSocketTooLongError(name, socketPath, MAX_DAEMON_SOCKET_PATH_BYTES));
19359
+ }
19360
+ }
19361
+ function namedSocketPath(name) {
19362
+ return sessionFilePathIn(getNamedSessionDir(name), "DAEMON_SOCKET");
19363
+ }
19364
+ function throwInvalid(err) {
19365
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
19366
+ }
19367
+ function selectSession(optionValue) {
19368
+ const name = optionValue ?? process.env[SESSION_NAME_ENV]?.trim();
19369
+ if (name === void 0 || optionValue === void 0 && name === "") {
19370
+ delete process.env[SESSION_NAME_ENV];
19371
+ return;
19372
+ }
19373
+ validateSessionName(name);
19374
+ process.env[SESSION_NAME_ENV] = normalizeSessionName(name);
19375
+ }
19376
+
19377
+ // src/session/sessionList.ts
19378
+ init_logging();
19379
+ init_sessionCommand();
19380
+ var log27 = createLogger("session");
19381
+ function toRunningSession(name, data) {
19382
+ const meta = data.sessionMetadata;
19383
+ const state = meta ? "active" : data.ending ? "ending" : "starting";
19384
+ const url = data.pageState?.url ?? data.starting?.url;
19385
+ return {
19386
+ name,
19387
+ state,
19388
+ ...url !== void 0 && { url },
19389
+ ...meta && { port: meta.port },
19390
+ daemonPid: data.daemonPid,
19391
+ ...meta?.chromePid && { chromePid: meta.chromePid }
19392
+ };
19393
+ }
19394
+ async function describeSession(entry) {
19395
+ const { name, dir } = entry;
19396
+ const socketPath = sessionFilePathIn(dir, "DAEMON_SOCKET");
19397
+ const probe = await probeDaemonSocket(socketPath);
19398
+ if (probe !== "alive") return describeWithoutSocket(entry, probe);
19399
+ try {
19400
+ const response = await getStatus(socketPath);
19401
+ if (response.status === "ok" && response.data) return toRunningSession(name, response.data);
19402
+ } catch (error) {
19403
+ logDebugError(log27, `get status of ${dir}`, error);
19404
+ }
19405
+ return { name, state: "unresponsive" };
19406
+ }
19407
+ function describeWithoutSocket(entry, probe) {
19408
+ const daemonPid = readLiveDaemonPid(entry.dir);
19409
+ if (daemonPid !== null) return { name: entry.name, state: "starting", daemonPid };
19410
+ return describeLeftovers(entry, probe);
19411
+ }
19412
+ function describeLeftovers({ name, dir }, probe) {
19413
+ const chromePid = readPidFromFile(sessionFilePathIn(dir, "CHROME_PID"));
19414
+ const orphan = chromePid !== null && isProcessAlive(chromePid) && isSessionChrome(chromePid, dir);
19415
+ const leftover = probe === "stale" || SESSION_STATE_FILES.some((type) => fs17.existsSync(sessionFilePathIn(dir, type)));
19416
+ if (!orphan && !leftover) return null;
19417
+ const port = leftoverPort(dir);
19418
+ return {
19419
+ name,
19420
+ state: orphan ? "crashed" : "stale",
19421
+ ...port !== null && { port },
19422
+ ...orphan && { chromePid },
19423
+ cleanup: sessionCommand("bdg cleanup", name)
19424
+ };
19425
+ }
19426
+ function leftoverPort(dir) {
19427
+ try {
19428
+ const meta = JSON.parse(fs17.readFileSync(sessionFilePathIn(dir, "METADATA"), "utf8"));
19429
+ if (typeof meta.port === "number") return meta.port;
19430
+ } catch (error) {
19431
+ logDebugError(log27, `read the metadata in ${dir}`, error);
19432
+ }
19433
+ return readPortFile(sessionFilePathIn(dir, "PORT"));
19434
+ }
19435
+ function isSelectable({ name }) {
19436
+ return name === null || isValidSessionName(name) && name === normalizeSessionName(name);
19437
+ }
19438
+ function isSameDir(a, b) {
19439
+ try {
19440
+ const first = fs17.statSync(a);
19441
+ const second = fs17.statSync(b);
19442
+ return first.ino === second.ino && first.dev === second.dev;
19443
+ } catch (error) {
19444
+ logDebugError(log27, `compare ${a} with ${b}`, error);
19445
+ return false;
19446
+ }
19447
+ }
19448
+ function selectableEntry(entry) {
19449
+ if (isSelectable(entry) || entry.name === null) return entry;
19450
+ const lower = normalizeSessionName(entry.name);
19451
+ if (!isValidSessionName(lower)) return null;
19452
+ return isSameDir(entry.dir, getNamedSessionDir(lower)) ? { name: lower, dir: entry.dir } : null;
19453
+ }
19454
+ async function describeUnselectable(entry) {
19455
+ const socketPath = sessionFilePathIn(entry.dir, "DAEMON_SOCKET");
19456
+ if (await probeDaemonSocket(socketPath) === "alive") return describeSession(entry);
19457
+ return { name: entry.name, state: "stale", cleanup: removeDirCommand(entry.dir) };
19458
+ }
19459
+ async function listRunningSessions() {
19460
+ const listed = /* @__PURE__ */ new Set();
19461
+ const sessions = await Promise.all(
19462
+ listSessionDirs().map((entry) => {
19463
+ const selectable = selectableEntry(entry);
19464
+ if (selectable === null) return describeUnselectable(entry);
19465
+ if (listed.has(selectable.name)) return Promise.resolve(null);
19466
+ listed.add(selectable.name);
19467
+ return describeSession(selectable);
19468
+ })
19469
+ );
19470
+ return sessions.filter((session) => session !== null);
19471
+ }
19472
+
19473
+ // src/ui/formatters/sessions.ts
19474
+ init_formatting();
19475
+ var DEFAULT_SESSION_LABEL = "(default)";
19476
+ function formatSessionList(data) {
19477
+ const fmt = new OutputFormatter();
19478
+ if (data.sessions.length === 0) {
19479
+ return fmt.text("No running sessions").hints("Suggestions:", ["Start one: bdg <url> [--session <name>]"]).build();
19480
+ }
19481
+ const rows = data.sessions.map((session) => [
19482
+ session.name ?? DEFAULT_SESSION_LABEL,
19483
+ session.state,
19484
+ session.port?.toString() ?? "-",
19485
+ session.daemonPid?.toString() ?? "-",
19486
+ session.url ?? "-"
19487
+ ]);
19488
+ const header = ["SESSION", "STATE", "PORT", "PID", "URL"];
19489
+ const widths = header.map(
19490
+ (title, column) => Math.max(title.length, ...rows.map((row) => row[column]?.length ?? 0))
19491
+ );
19492
+ for (const row of [header, ...rows]) {
19493
+ fmt.text(
19494
+ row.map((cell, column) => cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd()
19495
+ );
19496
+ }
19497
+ const cleanups = data.sessions.flatMap((session) => session.cleanup ? [session.cleanup] : []);
19498
+ if (cleanups.length > 0) {
19499
+ fmt.hints("Crashed or stale sessions; clean up with:", cleanups);
19500
+ }
19501
+ return fmt.build();
19502
+ }
19503
+
19504
+ // src/commands/sessions.ts
19505
+ function registerSessionsCommand(program2) {
19506
+ program2.command("sessions").description(
19507
+ "List sessions (default and named) with their state, URL, port and PID, including crashed ones to clean up"
19508
+ ).addOption(jsonOption()).action(async (options) => {
19509
+ await runCommand(
19510
+ async () => ({ success: true, data: { sessions: await listRunningSessions() } }),
19511
+ options,
19512
+ formatSessionList
19513
+ );
19514
+ });
19515
+ }
19516
+
18328
19517
  // src/commands/status.ts
18329
19518
  init_errors3();
18330
19519
  init_messages();
@@ -18645,7 +19834,9 @@ var commandRegistry = [
18645
19834
  addCommandGroup("Network Commands:"),
18646
19835
  registerNetworkCommands,
18647
19836
  addCommandGroup("Console Commands:"),
18648
- registerConsoleCommand
19837
+ registerConsoleCommand,
19838
+ addCommandGroup("Agent Setup:"),
19839
+ registerInstallSkillCommand
18649
19840
  ];
18650
19841
 
18651
19842
  // src/index.ts