browser-debugger-cli 0.11.0 → 0.12.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 (98) hide show
  1. package/README.md +142 -79
  2. package/dist/commands/css.d.ts +13 -0
  3. package/dist/commands/css.js +53 -0
  4. package/dist/commands/dom/audit.d.ts +14 -0
  5. package/dist/commands/dom/audit.js +87 -0
  6. package/dist/commands/dom/formInteraction.js +36 -6
  7. package/dist/commands/dom/helpers/keyAttributes.d.ts +3 -2
  8. package/dist/commands/dom/helpers/keyAttributes.js +6 -4
  9. package/dist/commands/dom/helpers/screenshot.d.ts +1 -0
  10. package/dist/commands/dom/helpers/screenshot.js +158 -38
  11. package/dist/commands/dom/index.js +4 -1
  12. package/dist/commands/dom/screenshot.js +10 -6
  13. package/dist/commands/dom/wait.js +5 -3
  14. package/dist/commands/helpJson.js +1 -1
  15. package/dist/commands/optionBehaviors.js +21 -6
  16. package/dist/commands/page.js +7 -4
  17. package/dist/commands/peek.d.ts +7 -0
  18. package/dist/commands/peek.js +65 -23
  19. package/dist/commands/shared/optionTypes.d.ts +5 -1
  20. package/dist/commands/start.d.ts +13 -0
  21. package/dist/commands/start.js +19 -2
  22. package/dist/commands/tail.d.ts +7 -1
  23. package/dist/commands/tail.js +13 -62
  24. package/dist/commands.js +2 -0
  25. package/dist/daemon/session/commandRegistry.js +7 -1
  26. package/dist/daemon/session/plugins.js +4 -52
  27. package/dist/daemon.js +7986 -6848
  28. package/dist/errors/messages.d.ts +34 -4
  29. package/dist/errors/messages.js +68 -5
  30. package/dist/index.js +610 -186
  31. package/dist/ipc/client.d.ts +4 -0
  32. package/dist/ipc/client.js +8 -0
  33. package/dist/ipc/protocol/auditTypes.d.ts +129 -0
  34. package/dist/ipc/protocol/auditTypes.js +6 -0
  35. package/dist/ipc/protocol/commands.d.ts +23 -0
  36. package/dist/ipc/protocol/commands.js +2 -0
  37. package/dist/ipc/protocol/domTypes.d.ts +4 -0
  38. package/dist/ipc/protocol/inspectTypes.d.ts +71 -8
  39. package/dist/runtime/css/search.d.ts +39 -0
  40. package/dist/runtime/css/search.js +122 -0
  41. package/dist/runtime/dom/actionEffects.d.ts +4 -1
  42. package/dist/runtime/dom/actionEffects.js +8 -4
  43. package/dist/runtime/dom/audit.d.ts +19 -0
  44. package/dist/runtime/dom/audit.js +36 -0
  45. package/dist/runtime/dom/auditModel.d.ts +45 -0
  46. package/dist/runtime/dom/auditModel.js +215 -0
  47. package/dist/runtime/dom/auditScripts.d.ts +107 -0
  48. package/dist/runtime/dom/auditScripts.js +112 -0
  49. package/dist/runtime/dom/elementGeometry.d.ts +8 -2
  50. package/dist/runtime/dom/elementGeometry.js +24 -8
  51. package/dist/runtime/dom/elementInfo.d.ts +3 -2
  52. package/dist/runtime/dom/elementInfo.js +8 -2
  53. package/dist/runtime/dom/formFillHelpers/fill.js +2 -2
  54. package/dist/runtime/dom/inspect.d.ts +7 -0
  55. package/dist/runtime/dom/inspect.js +88 -23
  56. package/dist/runtime/dom/inspectAllStyles.d.ts +16 -4
  57. package/dist/runtime/dom/inspectAllStyles.js +89 -7
  58. package/dist/runtime/dom/inspectCascade.d.ts +19 -2
  59. package/dist/runtime/dom/inspectCascade.js +214 -44
  60. package/dist/runtime/dom/inspectCascadeModel.d.ts +8 -0
  61. package/dist/runtime/dom/inspectCascadeModel.js +108 -34
  62. package/dist/runtime/dom/inspectHints.d.ts +26 -3
  63. package/dist/runtime/dom/inspectHints.js +125 -9
  64. package/dist/runtime/dom/inspectModel.d.ts +3 -0
  65. package/dist/runtime/dom/inspectModel.js +30 -7
  66. package/dist/runtime/dom/inspectPaintModel.d.ts +48 -22
  67. package/dist/runtime/dom/inspectPaintModel.js +180 -68
  68. package/dist/runtime/dom/inspectRules.d.ts +19 -0
  69. package/dist/runtime/dom/inspectRules.js +21 -5
  70. package/dist/runtime/dom/inspectScripts.d.ts +85 -12
  71. package/dist/runtime/dom/inspectScripts.js +314 -28
  72. package/dist/runtime/dom/inspectTree.js +10 -2
  73. package/dist/runtime/dom/inspectWhyModel.d.ts +2 -1
  74. package/dist/runtime/dom/inspectWhyModel.js +52 -10
  75. package/dist/runtime/dom/layout.js +31 -9
  76. package/dist/runtime/dom/reactEventHelpers.d.ts +7 -0
  77. package/dist/runtime/dom/reactEventHelpers.js +27 -9
  78. package/dist/runtime/page/emulation.d.ts +13 -4
  79. package/dist/runtime/page/emulation.js +69 -4
  80. package/dist/runtime/page/userAgent.d.ts +17 -0
  81. package/dist/runtime/page/userAgent.js +57 -0
  82. package/dist/types.d.ts +4 -0
  83. package/dist/ui/formatters/audit.d.ts +19 -0
  84. package/dist/ui/formatters/audit.js +106 -0
  85. package/dist/ui/formatters/dom.d.ts +1 -1
  86. package/dist/ui/formatters/dom.js +6 -3
  87. package/dist/ui/formatters/inspect.js +42 -15
  88. package/dist/ui/formatters/status.js +1 -1
  89. package/dist/ui/messages/commands.d.ts +44 -7
  90. package/dist/ui/messages/commands.js +83 -11
  91. package/dist/ui/messages/preview.d.ts +6 -0
  92. package/dist/ui/messages/preview.js +9 -1
  93. package/dist/utils/cssValues.js +36 -4
  94. package/dist/utils/decisionTrees.js +0 -5
  95. package/dist/utils/suggestions.d.ts +4 -2
  96. package/dist/utils/suggestions.js +7 -5
  97. package/dist/utils/taskMappings.js +1 -1
  98. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -567,13 +567,13 @@ var require_common = __commonJS({
567
567
  }
568
568
  }
569
569
  }
570
- function matchesTemplate(search, template) {
570
+ function matchesTemplate(search2, template) {
571
571
  let searchIndex = 0;
572
572
  let templateIndex = 0;
573
573
  let starIndex = -1;
574
574
  let matchIndex2 = 0;
575
- while (searchIndex < search.length) {
576
- if (templateIndex < template.length && (template[templateIndex] === search[searchIndex] || template[templateIndex] === "*")) {
575
+ while (searchIndex < search2.length) {
576
+ if (templateIndex < template.length && (template[templateIndex] === search2[searchIndex] || template[templateIndex] === "*")) {
577
577
  if (template[templateIndex] === "*") {
578
578
  starIndex = templateIndex;
579
579
  matchIndex2 = searchIndex;
@@ -2412,6 +2412,9 @@ function pageNavigationText(navigation) {
2412
2412
  const status = navigation.status === void 0 ? "" : ` (${navigation.status})`;
2413
2413
  return `navigated to ${navigation.url}${status}`;
2414
2414
  }
2415
+ function moreMessagesText(count) {
2416
+ return `(+${count} more)`;
2417
+ }
2415
2418
  function newMessageText(message) {
2416
2419
  return `"${message.text}" (${message.element})`;
2417
2420
  }
@@ -2508,8 +2511,11 @@ function coverText(cover, transparent) {
2508
2511
  function inspectCascadeNote(reason) {
2509
2512
  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
2513
  }
2511
- function inspectDarkThemeBadge() {
2512
- return "[dark theme from system; --color-scheme light for light]";
2514
+ function inspectPseudoOfNote(pseudo) {
2515
+ return `Inspected the element of ${pseudo}: pseudo-elements cannot be selected; ${pseudo} is on the pseudo line (content, size, position, inset, colors)`;
2516
+ }
2517
+ function inspectDarkThemeBadge(emulated = false) {
2518
+ return emulated ? "[dark theme, emulated; bdg page emulate --color-scheme light for light]" : "[dark theme from system; --color-scheme light for light]";
2513
2519
  }
2514
2520
  function inspectVisibilityBadges(visibility) {
2515
2521
  const reason = (text) => text ? `: ${text}` : "";
@@ -2636,8 +2642,16 @@ function emptyElementLine(children, count) {
2636
2642
  const more = count > children.length ? `, \u2026 ${count - children.length} more` : "";
2637
2643
  return `No text; holds ${pluralize(count, "element")}: ${children.join(", ")}${more} (see its HTML with --raw)`;
2638
2644
  }
2639
- function screenshotGrownNote(box, captured) {
2640
- return `grown from ${box.width}\xD7${box.height} to ${captured.width}\xD7${captured.height} to include content overflowing the element`;
2645
+ function screenshotScaledNote(originalWidth, originalHeight, width, height) {
2646
+ return `scaled from ${originalWidth}\xD7${originalHeight} to ${width}\xD7${height}; --no-resize for full size`;
2647
+ }
2648
+ function screenshotGrownNote(box, captured, padding = 0) {
2649
+ const painted = { width: captured.width - 2 * padding, height: captured.height - 2 * padding };
2650
+ const grew = painted.width > box.width + 0.5 || painted.height > box.height + 0.5;
2651
+ const ink = `grown from ${box.width}\xD7${box.height} to ${painted.width}\xD7${painted.height} to include what it paints outside its box (overflowing content, shadows, outline)`;
2652
+ const pad = `${padding}px of page around it (--padding)`;
2653
+ if (!padding) return ink;
2654
+ return grew ? `${ink}, plus ${pad}` : `with ${pad}: ${captured.width}\xD7${captured.height}`;
2641
2655
  }
2642
2656
  function queryNextSteps(index) {
2643
2657
  return `Next: bdg dom get ${index} (text), bdg dom get ${index} --raw (HTML), bdg dom layout ${index} (position)`;
@@ -2689,14 +2703,15 @@ function startCommandHelpMessage() {
2689
2703
  function pageEmulateNothingError() {
2690
2704
  return {
2691
2705
  message: "Nothing to emulate",
2692
- suggestion: "Give --viewport <WxH>, --color-scheme light|dark, or --reset, e.g. bdg page emulate --viewport 900x700"
2706
+ suggestion: "Give --viewport <WxH>, --mobile, --color-scheme light|dark, or --reset, e.g. bdg page emulate --viewport 900x700"
2693
2707
  };
2694
2708
  }
2695
2709
  function pageEmulationLines(result) {
2696
2710
  const size = (v) => `${v.width}x${v.height}`;
2697
2711
  const { emulated } = result;
2712
+ const phone = emulated.viewport?.mobile ? " (phone: mobile layout, touch, mobile user agent)" : "";
2698
2713
  return [
2699
- ["Viewport", emulated.viewport ? size(emulated.viewport) : "the browser window"],
2714
+ ["Viewport", emulated.viewport ? `${size(emulated.viewport)}${phone}` : "the browser window"],
2700
2715
  ...result.viewport ? [["Layout", `${size(result.viewport)} (without scrollbars)`]] : [],
2701
2716
  [
2702
2717
  "Scheme",
@@ -2710,7 +2725,7 @@ function inspectAnimatingBadge(animating) {
2710
2725
  function inspectMidTransitionNote() {
2711
2726
  return "(mid-transition: inspect again for the final value)";
2712
2727
  }
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;
2728
+ var REQUIRED_FIELD_EMPTY_REASON, NO_VISIBLE_EFFECT, HOVER_OFF_DONE, HOVER_USAGE, 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, AUDIT_HELP_EXAMPLES, CSS_SEARCH_HELP_EXAMPLES;
2714
2729
  var init_commands = __esm({
2715
2730
  "src/ui/messages/commands.ts"() {
2716
2731
  "use strict";
@@ -2720,6 +2735,8 @@ var init_commands = __esm({
2720
2735
  init_strings();
2721
2736
  REQUIRED_FIELD_EMPTY_REASON = "Required field is empty";
2722
2737
  NO_VISIBLE_EFFECT = "no visible effect observed: no DOM change, requests or navigation within 300 ms";
2738
+ HOVER_OFF_DONE = "\u2713 Mouse moved off the page (hover styles and menus that close on mouseleave are gone)";
2739
+ HOVER_USAGE = "bdg dom hover <selector|index>, or bdg dom hover --off to move the mouse away";
2723
2740
  LAYOUT_REASONS = {
2724
2741
  /** Hidden: an `<option>` of a closed `<select>` has no box */
2725
2742
  option: "not rendered (an <option> is shown by its <select>)",
@@ -2748,8 +2765,14 @@ Output notation:
2748
2765
  in-parent distances to the parent's content edges (l t r b); sib: gaps to the sibling on each side
2749
2766
  scroll WxH the content (pseudo-elements too) is larger than the box
2750
2767
  16/24 font size / line height; 'webfont loaded' = drawn with a downloaded font;
2751
- (rendered "X") = drawn with another font than declared (a fallback)
2768
+ (rendered "X") = drawn with another font than declared (a fallback);
2769
+ (resolves to "X") = the font a generic family (sans-serif, system-ui) became
2752
2770
  contrast 4.47 WCAG ratio, rounded down, against the background behind the text
2771
+ text in X the text is drawn by descendant X (the one with most of it): its font, color, contrast
2772
+ truncated the text is cut off (overflow clip, ellipsis or line clamp); a "\u2026" in the header is
2773
+ only bdg shortening the text
2774
+ .a.b(+3) the first two classes and how many more the element has
2775
+ sizing content-box padding and border add to the CSS size (shown only then; border-box is not)
2753
2776
  (+N not rendered) children with display: none (or not in the layout)
2754
2777
  hints declarations on this element that have no effect, why, the fix and where they are
2755
2778
  ('none': checked, nothing found)
@@ -2803,6 +2826,21 @@ Sessions follow the system color scheme; start with --color-scheme light|dark to
2803
2826
  " bdg dom wait --load # the page finished loading"
2804
2827
  );
2805
2828
  FRAME_URL_MAX_LENGTH = 100;
2829
+ AUDIT_HELP_EXAMPLES = `
2830
+ Examples:
2831
+ bdg dom audit All checks
2832
+ bdg dom audit contrast --level AAA Text below WCAG AAA, weakest first
2833
+ bdg dom audit overflow What scrolls sideways, cut-off text, scaled images
2834
+ bdg dom audit layers animations Fixed/sticky elements and running animations
2835
+
2836
+ Follow up on a finding with bdg dom inspect <element> (e.g. --why color).`;
2837
+ CSS_SEARCH_HELP_EXAMPLES = `
2838
+ Examples:
2839
+ bdg css search -- --brand Where a custom property is set and used (-- before a text
2840
+ that starts with -; options go before it:
2841
+ bdg css search --limit 50 -- --brand)
2842
+ bdg css search "oklch(" Rules that use oklch colors
2843
+ bdg css search ".btn-primary" Rules of a class, in every stylesheet`;
2806
2844
  }
2807
2845
  });
2808
2846
 
@@ -3248,6 +3286,7 @@ function noNodesFoundError(selector, context = {}) {
3248
3286
  joinLines(
3249
3287
  context.similar,
3250
3288
  context.shadowHost && shadowBoundaryLine(context.shadowHost),
3289
+ shadowPseudoLine(selector),
3251
3290
  `${hidden}Verify the CSS selector is correct.`,
3252
3291
  note ? note : void 0
3253
3292
  ),
@@ -3256,6 +3295,13 @@ function noNodesFoundError(selector, context = {}) {
3256
3295
  )
3257
3296
  };
3258
3297
  }
3298
+ function shadowPseudoLine(selector) {
3299
+ const part = /::part\(\s*([^)\s]+)\s*\)/.exec(selector)?.[1];
3300
+ if (part) {
3301
+ return `::part() styles a shadow part and selects nothing; bdg searches open shadow roots, so select the part itself: [part~="${part}"]`;
3302
+ }
3303
+ return /::slotted\(/.test(selector) ? "::slotted() selects nothing; select the slotted element itself (it is in the light DOM)" : void 0;
3304
+ }
3259
3305
  function shellQuote(value) {
3260
3306
  return `'${value.split("'").join(`'\\''`)}'`;
3261
3307
  }
@@ -3285,6 +3331,12 @@ function elementZeroDimensionsError() {
3285
3331
  suggestion: "It may be collapsed or empty; capture a visible ancestor instead"
3286
3332
  };
3287
3333
  }
3334
+ function hoverOffWithTargetError() {
3335
+ return {
3336
+ message: "--off moves the mouse off the page; it takes no element",
3337
+ suggestion: "Use bdg dom hover --off alone, or bdg dom hover <selector> to hover an element"
3338
+ };
3339
+ }
3288
3340
  function missingArgumentError(usage) {
3289
3341
  return {
3290
3342
  message: "Missing required argument or flag",
@@ -3370,6 +3422,15 @@ function skillWriteFailedError(path6, reason) {
3370
3422
  suggestion: "Check the permissions of that directory, or install for the other agent only (--claude or --agents)"
3371
3423
  };
3372
3424
  }
3425
+ function intervalWithoutFollowError() {
3426
+ return {
3427
+ message: "--interval sets how often --follow refreshes",
3428
+ suggestion: "Add --follow, e.g. bdg peek --follow --interval 500"
3429
+ };
3430
+ }
3431
+ function unknownAuditCheckMessage(value, similar, checks) {
3432
+ return `Unknown check "${value}"${similar.length > 0 ? `; did you mean ${similar[0]}?` : ""} (checks: ${checks.join(", ")})`;
3433
+ }
3373
3434
  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;
3374
3435
  var init_messages = __esm({
3375
3436
  "src/errors/messages.ts"() {
@@ -3499,11 +3560,14 @@ function findSimilarNames(name, candidates) {
3499
3560
  const maxDistance2 = Math.max(2, Math.floor(name.length / 5));
3500
3561
  const minAffix = Math.max(4, Math.ceil(name.length / 3));
3501
3562
  const typos = findMatches(name, others, maxDistance2, false).map((match) => match.value);
3502
- const byAffix = (length) => others.map((candidate) => ({ candidate, length: length(candidate) })).filter((entry) => entry.length >= minAffix).sort((a, b) => b.length - a.length).map((entry) => entry.candidate);
3563
+ const byAffix = (length, min = minAffix) => others.map((candidate) => ({ candidate, length: length(candidate) })).filter((entry) => entry.length >= min).sort((a, b) => b.length - a.length).map((entry) => entry.candidate);
3503
3564
  const ranked = [
3504
3565
  ...typos,
3505
3566
  ...byAffix((candidate) => commonSuffixLength(name, candidate)),
3506
- ...byAffix((candidate) => commonPrefixLength(name, candidate))
3567
+ ...byAffix(
3568
+ (candidate) => commonPrefixLength(name, candidate),
3569
+ Math.max(minAffix, Math.ceil(name.length / 2))
3570
+ )
3507
3571
  ];
3508
3572
  return [...new Set(ranked)].slice(0, MAX_SIMILAR_NAMES);
3509
3573
  }
@@ -3974,6 +4038,12 @@ async function domListeners(params) {
3974
4038
  async function domLayout(params) {
3975
4039
  return sendCommand("dom_layout", params);
3976
4040
  }
4041
+ async function domAudit(params) {
4042
+ return sendCommand("dom_audit", params);
4043
+ }
4044
+ async function cssSearch(params) {
4045
+ return sendCommand("css_search", params);
4046
+ }
3977
4047
  async function domInspect(params) {
3978
4048
  return sendCommand("dom_inspect", params);
3979
4049
  }
@@ -4162,7 +4232,12 @@ var init_elementInfo = __esm({
4162
4232
  doc = frame.ownerDocument;
4163
4233
  }
4164
4234
  const root = el.getRootNode();
4165
- if (root.host) parts.push('shadow root of <' + describe(root.host) + '>');
4235
+ if (root.host) {
4236
+ const host = root.host;
4237
+ const label = describe(host) + (!host.id && host.classList.length ? '.' + host.classList[0] : '');
4238
+ const text = (host.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 30);
4239
+ parts.push('shadow root of <' + label + (text ? ' "' + text + '"' : '') + '>');
4240
+ }
4166
4241
  return parts.join(' > ');
4167
4242
  }`;
4168
4243
  }
@@ -4171,8 +4246,9 @@ var init_elementInfo = __esm({
4171
4246
  // src/commands/dom/helpers/keyAttributes.ts
4172
4247
  function keyAttributes(tag, attributes, state = {}) {
4173
4248
  const names = ATTRIBUTES_BY_TAG[tag];
4174
- if (!names) return void 0;
4175
- const result = {};
4249
+ const part = attributes["part"];
4250
+ if (!names) return part ? { part } : void 0;
4251
+ const result = part ? { part } : {};
4176
4252
  const type = state.type ?? attributes["type"];
4177
4253
  if (type) result["type"] = type;
4178
4254
  for (const name of names) {
@@ -4239,8 +4315,9 @@ function axisScroll(start, size, viewSize, range, aim) {
4239
4315
  const scroll = Math.round(Math.min(range.forward, Math.max(-range.back, target))) || 0;
4240
4316
  return visibleLength(start, size, viewSize, scroll) >= fully ? scroll : null;
4241
4317
  }
4242
- function classifyViewportPosition(geometry, viewport) {
4243
- if (geometry.hidden) return { inViewport: "hidden", hiddenReason: geometry.hidden };
4318
+ function classifyViewportPosition(measured, viewport) {
4319
+ if (measured.hidden) return { inViewport: "hidden", hiddenReason: measured.hidden };
4320
+ const geometry = measured.content ? { ...measured, rect: measured.content } : measured;
4244
4321
  const rect = edgesOf(geometry.rect);
4245
4322
  const screen = edgesOf({ x: 0, y: 0, ...viewport });
4246
4323
  const view = geometry.clip ? overlap(screen, edgesOf(geometry.clip)) : screen;
@@ -4485,7 +4562,20 @@ var init_elementGeometry = __esm({
4485
4562
  }
4486
4563
  return 'not rendered';
4487
4564
  };
4488
- const hiddenReason = (style, box) => {
4565
+ const shownChildren = (node) => {
4566
+ const s = styleOf(node);
4567
+ if (s.overflowX !== 'visible' || s.overflowY !== 'visible') return null;
4568
+ let union = null;
4569
+ for (const c of Array.from(node.children).slice(0, 50)) {
4570
+ const r = c.getBoundingClientRect();
4571
+ if (r.width <= 0 || r.height <= 0 || styleOf(c).visibility !== 'visible') continue;
4572
+ union = union
4573
+ ? { left: Math.min(union.left, r.left), top: Math.min(union.top, r.top), right: Math.max(union.right, r.right), bottom: Math.max(union.bottom, r.bottom) }
4574
+ : { left: r.left, top: r.top, right: r.right, bottom: r.bottom };
4575
+ }
4576
+ return union;
4577
+ };
4578
+ const hiddenReason = (style, box, content) => {
4489
4579
  if (style.display === 'none') return 'display: none';
4490
4580
  if (style.display === 'contents') return 'display: contents (no box of its own)';
4491
4581
  if (el.getClientRects().length === 0) {
@@ -4494,7 +4584,7 @@ var init_elementGeometry = __esm({
4494
4584
  const skipped = skippedReason(el);
4495
4585
  if (skipped) return skipped;
4496
4586
  if (style.visibility !== 'visible') return 'visibility: ' + style.visibility;
4497
- if (box.width === 0 || box.height === 0) return 'zero size';
4587
+ if ((box.width === 0 || box.height === 0) && !content) return 'zero size';
4498
4588
  return null;
4499
4589
  };
4500
4590
  const isSticky = () => {
@@ -4517,7 +4607,8 @@ var init_elementGeometry = __esm({
4517
4607
  let clipper = own.clipper;
4518
4608
  let fixed = own.fixed;
4519
4609
  let fixedBy = own.fixedBy;
4520
- let hidden = hiddenReason(styleOf(el), box) || own.collapsed;
4610
+ let content = box.width === 0 || box.height === 0 ? shownChildren(el) : null;
4611
+ let hidden = hiddenReason(styleOf(el), box, content) || own.collapsed;
4521
4612
  let x = 0;
4522
4613
  let y = 0;
4523
4614
  for (let view = el.ownerDocument.defaultView; view && view.frameElement; view = view.parent) {
@@ -4529,6 +4620,7 @@ var init_elementGeometry = __esm({
4529
4620
  overlay = { right: !!unframed && unframed.right <= clip.right && overlay.right, bottom: !!unframed && unframed.bottom <= clip.bottom && overlay.bottom };
4530
4621
  const offset = frameOffset(frame);
4531
4622
  rect = shift(rect, offset.x, offset.y);
4623
+ content = shift(content, offset.x, offset.y);
4532
4624
  const outer = ancestorClip(frame, rect, describe);
4533
4625
  const inner = shift(clip, offset.x, offset.y);
4534
4626
  clip = intersect(inner, outer.clip);
@@ -4547,7 +4639,7 @@ var init_elementGeometry = __esm({
4547
4639
  const invisible = hidden ? null : invisibleReason(el, describe);
4548
4640
  const lock = scrollLock(top, describe);
4549
4641
  if (lock && fixedBy === top.document.body) fixed = false;
4550
- return { rect: toBox(rect), clip: toBox(clip), clipOverlay: overlay, clipper: clipper, hidden: hidden, invisible: invisible, inert: isInert(), fixed: fixed, sticky: !fixed && isSticky(), pageScroll: scrollRange(top), scrollLock: lock, offset: { x: x, y: y } };
4642
+ return { rect: toBox(rect), content: toBox(content), clip: toBox(clip), clipOverlay: overlay, clipper: clipper, hidden: hidden, invisible: invisible, inert: isInert(), fixed: fixed, sticky: !fixed && isSticky(), pageScroll: scrollRange(top), scrollLock: lock, offset: { x: x, y: y } };
4551
4643
  }`;
4552
4644
  IN_VIEW_SLACK = 1;
4553
4645
  }
@@ -5550,18 +5642,34 @@ var init_errors5 = __esm({
5550
5642
  }
5551
5643
  });
5552
5644
 
5645
+ // src/runtime/page/userAgent.ts
5646
+ var init_userAgent = __esm({
5647
+ "src/runtime/page/userAgent.ts"() {
5648
+ "use strict";
5649
+ init_errors();
5650
+ }
5651
+ });
5652
+
5553
5653
  // src/runtime/page/emulation.ts
5554
5654
  function viewportOverride(viewport, deviceScaleFactor = 0) {
5555
- return { width: viewport.width, height: viewport.height, deviceScaleFactor, mobile: false };
5655
+ const mobile = viewport.mobile === true;
5656
+ return {
5657
+ width: viewport.width,
5658
+ height: viewport.height,
5659
+ deviceScaleFactor: deviceScaleFactor || (mobile ? MOBILE_PIXEL_RATIO : 0),
5660
+ mobile
5661
+ };
5556
5662
  }
5557
- var log16;
5663
+ var log16, MOBILE_PIXEL_RATIO;
5558
5664
  var init_emulation = __esm({
5559
5665
  "src/runtime/page/emulation.ts"() {
5560
5666
  "use strict";
5561
5667
  init_elementGeometry();
5668
+ init_userAgent();
5562
5669
  init_logging();
5563
5670
  init_errors();
5564
5671
  log16 = createLogger("session");
5672
+ MOBILE_PIXEL_RATIO = 3;
5565
5673
  }
5566
5674
  });
5567
5675
 
@@ -5738,13 +5846,18 @@ async function useUnitPixelRatio(devicePixelRatio, viewport) {
5738
5846
  const sessionViewport = readSessionMetadata()?.viewport;
5739
5847
  const size = sessionViewport ?? await windowSize(viewport);
5740
5848
  await callCDP("Emulation.setDeviceMetricsOverride", viewportOverride(size, 1));
5741
- return async () => {
5742
- if (sessionViewport) {
5743
- await callCDP("Emulation.setDeviceMetricsOverride", viewportOverride(sessionViewport));
5744
- } else {
5745
- await callCDP("Emulation.clearDeviceMetricsOverride", {});
5746
- }
5747
- };
5849
+ return restoreSessionMetrics;
5850
+ }
5851
+ async function restoreSessionMetrics() {
5852
+ const sessionViewport = readSessionMetadata()?.viewport;
5853
+ if (!sessionViewport) {
5854
+ await callCDP("Emulation.clearDeviceMetricsOverride", {});
5855
+ return;
5856
+ }
5857
+ await callCDP("Emulation.setDeviceMetricsOverride", viewportOverride(sessionViewport));
5858
+ if (sessionViewport.mobile) {
5859
+ await callCDP("Emulation.setTouchEmulationEnabled", { enabled: true, maxTouchPoints: 5 });
5860
+ }
5748
5861
  }
5749
5862
  async function getElementBounds(ref) {
5750
5863
  const response = await callCDP("DOM.getBoxModel", ref);
@@ -5888,21 +6001,61 @@ async function visibleViewport() {
5888
6001
  function insideView(area, view) {
5889
6002
  return area.x >= 0 && area.y >= 0 && area.x + area.width <= view.width && area.y + area.height <= view.height;
5890
6003
  }
5891
- async function measureInView(ref) {
6004
+ async function measureInView(ref, padding) {
5892
6005
  const view = await visibleViewport();
5893
6006
  let box = await getElementBounds(ref);
5894
- let bounds = await captureArea(ref, box);
6007
+ let bounds = await captureArea(ref, box, padding);
5895
6008
  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})` });
6009
+ if (!fits) {
6010
+ await keepLayoutWithoutScrollbars(view);
6011
+ box = await getElementBounds(ref);
6012
+ return { box, bounds: await captureArea(ref, box, padding), inView: false };
6013
+ }
6014
+ if (insideView(bounds, view)) return { box, bounds, inView: true };
6015
+ const scrolledFrom = await scrollPosition();
6016
+ const dx = bounds.x + bounds.width / 2 - view.width / 2;
6017
+ const dy = bounds.y + bounds.height / 2 - view.height / 2;
6018
+ await callCDP("Runtime.evaluate", { expression: `window.scrollBy(${dx}, ${dy})` });
6019
+ try {
5900
6020
  box = await getElementBounds(ref);
5901
- bounds = await captureArea(ref, box);
6021
+ bounds = await captureArea(ref, box, padding);
6022
+ } catch (error) {
6023
+ await restoreScrollPosition(scrolledFrom);
6024
+ throw error;
5902
6025
  }
5903
- return { box, bounds, inView: insideView(bounds, view) };
6026
+ return { box, bounds, inView: insideView(bounds, view), scrolledFrom };
6027
+ }
6028
+ async function keepLayoutWithoutScrollbars(view) {
6029
+ const phone = readSessionMetadata()?.viewport?.mobile;
6030
+ await callCDP("Emulation.setScrollbarsHidden", { hidden: true });
6031
+ await callCDP(
6032
+ "Emulation.setDeviceMetricsOverride",
6033
+ viewportOverride({ ...view, ...phone && { mobile: true } }, 1)
6034
+ );
6035
+ }
6036
+ async function restoreViewport() {
6037
+ await callCDP("Emulation.setScrollbarsHidden", { hidden: false });
6038
+ await restoreSessionMetrics();
5904
6039
  }
5905
- async function captureArea(ref, bounds) {
6040
+ async function scrollPosition() {
6041
+ const response = await callCDP("Runtime.evaluate", {
6042
+ expression: "[window.scrollX, window.scrollY]",
6043
+ returnByValue: true
6044
+ });
6045
+ const value = response.data?.result?.result?.value;
6046
+ const [x, y] = Array.isArray(value) ? value : [];
6047
+ return { x: x ?? 0, y: y ?? 0 };
6048
+ }
6049
+ async function captureArea(ref, bounds, padding) {
6050
+ const area = await paintedArea(ref, bounds);
6051
+ return padding > 0 ? {
6052
+ x: area.x - padding,
6053
+ y: area.y - padding,
6054
+ width: area.width + 2 * padding,
6055
+ height: area.height + 2 * padding
6056
+ } : area;
6057
+ }
6058
+ async function paintedArea(ref, bounds) {
5906
6059
  const objectGroup = `bdg-shot-${process.pid}`;
5907
6060
  try {
5908
6061
  const resolved = await callCDP("DOM.resolveNode", { ...ref, objectGroup });
@@ -5946,15 +6099,18 @@ async function captureElementScreenshot(outputPath, ref, options = {}) {
5946
6099
  devicePixelRatio,
5947
6100
  before?.visualViewport ?? { clientWidth: 800, clientHeight: 600 }
5948
6101
  );
5949
- let box;
5950
- let bounds;
5951
- let inView;
6102
+ const restore = async (wide, scrolledFrom2) => {
6103
+ await (wide ? restoreViewport() : restoreMetrics());
6104
+ if (scrolledFrom2) await restoreScrollPosition(scrolledFrom2);
6105
+ };
6106
+ let measured;
5952
6107
  try {
5953
- ({ box, bounds, inView } = await measureInView(ref));
6108
+ measured = await measureInView(ref, options.padding ?? 0);
5954
6109
  } catch (error) {
5955
- await restoreMetrics();
6110
+ await restoreViewport();
5956
6111
  throw error;
5957
6112
  }
6113
+ const { box, bounds, inView, scrolledFrom } = measured;
5958
6114
  const originalWidth = bounds.width;
5959
6115
  const originalHeight = bounds.height;
5960
6116
  const resized = shouldResize(originalWidth, originalHeight, noResize);
@@ -5980,7 +6136,7 @@ async function captureElementScreenshot(outputPath, ref, options = {}) {
5980
6136
  });
5981
6137
  screenshotResult = screenshotResponse.data?.result;
5982
6138
  } finally {
5983
- await restoreMetrics();
6139
+ await restore(!inView, scrolledFrom);
5984
6140
  }
5985
6141
  if (!screenshotResult?.data) {
5986
6142
  throw new CDPConnectionError("No screenshot data returned", new Error("Empty response"));
@@ -5997,7 +6153,8 @@ async function captureElementScreenshot(outputPath, ref, options = {}) {
5997
6153
  finalTokens: calculateImageTokens(finalWidth, finalHeight),
5998
6154
  element: {
5999
6155
  bounds: roundBounds(onPage(box)),
6000
- ...bounds !== box && { captured: roundBounds(clip) }
6156
+ ...bounds !== box && { captured: roundBounds(clip) },
6157
+ ...options.padding && { padding: options.padding }
6001
6158
  }
6002
6159
  };
6003
6160
  if (quality !== void 0) {
@@ -6063,8 +6220,37 @@ var init_screenshot = __esm({
6063
6220
  if (!clips(style)) walk(child);
6064
6221
  }
6065
6222
  };
6066
- if (!clips(view.getComputedStyle(this))) walk(this);
6067
- return { left: own.left - reach.left, top: own.top - reach.top, right: reach.right - own.right, bottom: reach.bottom - own.bottom };
6223
+ const ownStyle = view.getComputedStyle(this);
6224
+ if (!clips(ownStyle)) {
6225
+ walk(this);
6226
+ const scaleX = this.offsetWidth ? own.width / this.offsetWidth : 1;
6227
+ const scaleY = this.offsetHeight ? own.height / this.offsetHeight : 1;
6228
+ const wider = Math.max(0, this.scrollWidth - this.clientWidth) * scaleX;
6229
+ const taller = Math.max(0, this.scrollHeight - this.clientHeight) * scaleY;
6230
+ if (ownStyle.direction === 'rtl') reach.left = Math.min(reach.left, own.left - wider);
6231
+ else reach.right = Math.max(reach.right, own.right + wider);
6232
+ reach.bottom = Math.max(reach.bottom, own.bottom + taller);
6233
+ }
6234
+ const ink = { left: 0, top: 0, right: 0, bottom: 0 };
6235
+ const grow = (side, amount) => { ink[side] = Math.max(ink[side], amount); };
6236
+ for (const layer of ownStyle.boxShadow === 'none' ? [] : ownStyle.boxShadow.split(/,(?![^(]*\\))/)) {
6237
+ if (/\\binset\\b/.test(layer)) continue;
6238
+ const [x = 0, y = 0, blur = 0, spread = 0] = (layer.replace(/(rgba?|hsla?|color|oklch|lab|lch)\\([^)]*\\)/g, '').match(/-?[\\d.]+px/g) || []).map(parseFloat);
6239
+ grow('left', blur + spread - x);
6240
+ grow('right', blur + spread + x);
6241
+ grow('top', blur + spread - y);
6242
+ grow('bottom', blur + spread + y);
6243
+ }
6244
+ if (ownStyle.outlineStyle !== 'none') {
6245
+ const outline = parseFloat(ownStyle.outlineWidth) + parseFloat(ownStyle.outlineOffset);
6246
+ ['left', 'top', 'right', 'bottom'].forEach((side) => grow(side, outline));
6247
+ }
6248
+ return {
6249
+ left: Math.max(own.left - reach.left, ink.left),
6250
+ top: Math.max(own.top - reach.top, ink.top),
6251
+ right: Math.max(reach.right - own.right, ink.right),
6252
+ bottom: Math.max(reach.bottom - own.bottom, ink.bottom)
6253
+ };
6068
6254
  }`;
6069
6255
  OVERFLOW_SLACK = 1;
6070
6256
  }
@@ -8052,18 +8238,18 @@ Expecting one of '${allowedValues.join("', '")}'`);
8052
8238
  *
8053
8239
  * @private
8054
8240
  */
8055
- _prepareUserArgs(argv, parseOptions3) {
8241
+ _prepareUserArgs(argv, parseOptions2) {
8056
8242
  if (argv !== void 0 && !Array.isArray(argv)) {
8057
8243
  throw new Error("first parameter to parse must be array or undefined");
8058
8244
  }
8059
- parseOptions3 = parseOptions3 || {};
8060
- if (argv === void 0 && parseOptions3.from === void 0) {
8245
+ parseOptions2 = parseOptions2 || {};
8246
+ if (argv === void 0 && parseOptions2.from === void 0) {
8061
8247
  if (process2.versions?.electron) {
8062
- parseOptions3.from = "electron";
8248
+ parseOptions2.from = "electron";
8063
8249
  }
8064
8250
  const execArgv = process2.execArgv ?? [];
8065
8251
  if (execArgv.includes("-e") || execArgv.includes("--eval") || execArgv.includes("-p") || execArgv.includes("--print")) {
8066
- parseOptions3.from = "eval";
8252
+ parseOptions2.from = "eval";
8067
8253
  }
8068
8254
  }
8069
8255
  if (argv === void 0) {
@@ -8071,7 +8257,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
8071
8257
  }
8072
8258
  this.rawArgs = argv.slice();
8073
8259
  let userArgs;
8074
- switch (parseOptions3.from) {
8260
+ switch (parseOptions2.from) {
8075
8261
  case void 0:
8076
8262
  case "node":
8077
8263
  this._scriptPath = argv[1];
@@ -8093,7 +8279,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
8093
8279
  break;
8094
8280
  default:
8095
8281
  throw new Error(
8096
- `unexpected parse option { from: '${parseOptions3.from}' }`
8282
+ `unexpected parse option { from: '${parseOptions2.from}' }`
8097
8283
  );
8098
8284
  }
8099
8285
  if (!this._name && this._scriptPath)
@@ -8123,9 +8309,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
8123
8309
  * @param {string} [parseOptions.from] - where the args are from: 'node', 'user', 'electron'
8124
8310
  * @return {Command} `this` command for chaining
8125
8311
  */
8126
- parse(argv, parseOptions3) {
8312
+ parse(argv, parseOptions2) {
8127
8313
  this._prepareForParse();
8128
- const userArgs = this._prepareUserArgs(argv, parseOptions3);
8314
+ const userArgs = this._prepareUserArgs(argv, parseOptions2);
8129
8315
  this._parseCommand([], userArgs);
8130
8316
  return this;
8131
8317
  }
@@ -8149,9 +8335,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
8149
8335
  * @param {string} parseOptions.from - where the args are from: 'node', 'user', 'electron'
8150
8336
  * @return {Promise}
8151
8337
  */
8152
- async parseAsync(argv, parseOptions3) {
8338
+ async parseAsync(argv, parseOptions2) {
8153
8339
  this._prepareForParse();
8154
- const userArgs = this._prepareUserArgs(argv, parseOptions3);
8340
+ const userArgs = this._prepareUserArgs(argv, parseOptions2);
8155
8341
  await this._parseCommand([], userArgs);
8156
8342
  return this;
8157
8343
  }
@@ -9468,15 +9654,19 @@ init_schema();
9468
9654
  // src/commands/optionBehaviors.ts
9469
9655
  init_screenshotResize();
9470
9656
  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";
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';
9657
+ 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, with "(+N more)" and moreMessages for the rest; 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
9658
  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
9659
  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`;
9474
9660
  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)";
9475
9661
  var OPTION_BEHAVIORS = {
9476
9662
  "screenshot:--selector": {
9477
9663
  default: "Captures the page (full page unless --no-full-page)",
9478
- whenEnabled: 'Captures one element; the selector (or a query index) can also be given as the second argument: bdg dom screenshot out.png "#sel". Both given and naming different elements exits 81',
9479
- automaticBehavior: "The capture covers the border box plus content overflowing it (uncleared floats, positioned children; not what an overflow: hidden ancestor cuts off, nor fixed descendants); JSON element.bounds is the border box and element.captured the larger area when it grew, which human output notes"
9664
+ whenEnabled: 'Captures one element; the selector (or a query index) can also be given as the second argument: bdg dom screenshot out.png "#sel". With --index it picks that match of the selector (--selector ".item" --index 2). A positional and an option naming different elements exits 81',
9665
+ automaticBehavior: "The capture covers the border box plus what overflows it: uncleared floats, positioned children, text past a tight line height, and the element's own box shadows and outline (a focus ring); not what an overflow: hidden ancestor cuts off, nor fixed descendants. JSON element.bounds is the border box and element.captured the larger area when it grew, which human output notes. An element smaller than the viewport is scrolled into view for the capture and the page scroll put back afterwards; a larger one is captured with the page laid out at its width without scrollbars, so it does not shift"
9666
+ },
9667
+ "screenshot:--padding": {
9668
+ default: "The element capture is its painted area (border box, overflowing content, shadows, outline)",
9669
+ whenEnabled: "Adds that many CSS px of the page around the element capture on every side (0-500); without an element it exits 81"
9480
9670
  },
9481
9671
  "screenshot:--no-resize": {
9482
9672
  default: `Images auto-resized to max ${MAX_EDGE_PX}px longest edge for Claude Vision optimization (~1,600 tokens)`,
@@ -9658,10 +9848,16 @@ var OPTION_BEHAVIORS = {
9658
9848
  whenEnabled: "Lists every listener of framework roots individually",
9659
9849
  tokenImpact: "On React pages --all adds a row per event type and phase (about 140 rows, 60 KB of JSON)"
9660
9850
  },
9851
+ "audit:--level": {
9852
+ default: "Text must reach WCAG AA: 4.5, or 3 for large text (24px, or 18.66px bold); every text-drawing element is checked, composited like dom inspect",
9853
+ whenEnabled: "--level AAA asks 7, or 4.5 for large text",
9854
+ automaticBehavior: "One walk over the rendered elements (open shadow roots included, at most 20000; capped says when it stopped). Findings are sorted weakest first; --limit (default 20) lists that many per check and the rest are counted. Overflow leaves out content inside horizontal scrollers and visually-hidden 1px text; identical findings are grouped (\xD7N)",
9855
+ tokenImpact: "About one line per finding; --limit bounds it"
9856
+ },
9661
9857
  "layout:--index": {
9662
9858
  default: "Reports every match of the selector (human output lists the first 20, JSON up to 100 plus an omitted count); a numeric argument reports that cached query element",
9663
9859
  whenEnabled: "Reports only the nth match (0-based); out of range exits 81",
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',
9860
+ 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 first element painted above it at the center of the largest visible box that paints there (the background of a sticky header rather than the transparent logo on it; inside a shadow host, what its shadow root paints), else the topmost one with coverTransparent (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',
9665
9861
  tokenImpact: 'About one line per element; a cheap alternative to screenshots for "where is it?"'
9666
9862
  },
9667
9863
  "inspect:--index": {
@@ -9736,11 +9932,11 @@ var OPTION_BEHAVIORS = {
9736
9932
  },
9737
9933
  "peek:-f": {
9738
9934
  default: "Shows snapshot of current data",
9739
- whenEnabled: "Continuous monitoring - refreshes every second (like tail -f)"
9935
+ whenEnabled: "Continuous monitoring (like tail -f): refreshes every second, or every --interval ms (100-60000). Replaces the deprecated bdg tail"
9740
9936
  },
9741
9937
  "peek:--follow": {
9742
9938
  default: "Shows snapshot of current data",
9743
- whenEnabled: "Continuous monitoring - refreshes every second (like tail -f)"
9939
+ whenEnabled: "Continuous monitoring (like tail -f): refreshes every second, or every --interval ms (100-60000). Replaces the deprecated bdg tail"
9744
9940
  },
9745
9941
  "peek:-v": {
9746
9942
  default: "Compact output (truncated URLs, no resource types)",
@@ -9776,6 +9972,11 @@ var OPTION_BEHAVIORS = {
9776
9972
  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",
9777
9973
  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"
9778
9974
  },
9975
+ "bdg:--mobile": {
9976
+ default: "A desktop viewport: classic scrollbars take ~15px of the width, no touch, a desktop user agent",
9977
+ whenEnabled: "Emulates a phone for the whole session: a mobile viewport (390x844 unless --viewport) at pixel ratio 3 with mobile layout (meta viewport, overlay scrollbars, so 100vw fits), touch (pointer: coarse, maxTouchPoints 5) and an Android Chrome user agent with mobile client hints; bdg page emulate --mobile turns it on mid-session, --viewport WxH without --mobile or --reset turns it off",
9978
+ automaticBehavior: 'Screenshots keep the mobile layout and are taken at pixel ratio 1 (CSS px = image px); bdg status shows "(emulated 390x844, phone)"'
9979
+ },
9779
9980
  "bdg:--color-scheme": {
9780
9981
  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",
9781
9982
  whenEnabled: "Emulates prefers-color-scheme: light or dark for the whole session (Emulation.setEmulatedMedia); other values exit 81 with a suggestion",
@@ -10631,11 +10832,6 @@ var DECISION_TREES = {
10631
10832
  yesCommand: "peek --follow",
10632
10833
  noAction: "next"
10633
10834
  },
10634
- {
10635
- question: "Need continuous monitoring (like tail -f)?",
10636
- yesCommand: "tail",
10637
- noAction: "next"
10638
- },
10639
10835
  {
10640
10836
  question: "Need quick preview of recent data?",
10641
10837
  yesCommand: "peek",
@@ -10784,7 +10980,7 @@ var TASK_MAPPINGS = {
10784
10980
  cdpAlternative: "Multiple IPC queries to session state"
10785
10981
  },
10786
10982
  live_monitoring: {
10787
- commands: ["peek --follow", "tail"],
10983
+ commands: ["peek --follow"],
10788
10984
  description: "Monitor data collection in real-time",
10789
10985
  cdpAlternative: "CDP event subscriptions with custom handler"
10790
10986
  },
@@ -10891,7 +11087,7 @@ function convertCommand(command) {
10891
11087
  }
10892
11088
  function generateRuntimeState() {
10893
11089
  const sessionActive = readLiveDaemonPid() !== null;
10894
- const availableCommands = sessionActive ? ["peek", "tail", "details", "dom", "network", "console", "cdp", "status", "sessions", "stop"] : ["bdg <url>", "sessions", "cleanup", "--help", "--version"];
11090
+ const availableCommands = sessionActive ? ["peek", "details", "dom", "network", "console", "cdp", "status", "sessions", "stop"] : ["bdg <url>", "sessions", "cleanup", "--help", "--version"];
10895
11091
  return {
10896
11092
  sessionActive,
10897
11093
  daemonRunning: sessionActive,
@@ -11253,6 +11449,8 @@ var COMMANDS = {
11253
11449
  dom_form_discover: defineCommand(),
11254
11450
  dom_listeners: defineCommand(),
11255
11451
  dom_layout: defineCommand(),
11452
+ dom_audit: defineCommand(),
11453
+ css_search: defineCommand(),
11256
11454
  dom_inspect: defineCommand(),
11257
11455
  dom_wait: defineCommand()
11258
11456
  };
@@ -11864,7 +12062,7 @@ function verboseCommandsMessage() {
11864
12062
  return [
11865
12063
  "Commands:",
11866
12064
  ` Full preview: ${sessionCommand("bdg peek --last 50")}`,
11867
- ` Watch live: ${sessionCommand("bdg tail")}`,
12065
+ ` Watch live: ${sessionCommand("bdg peek --follow")}`,
11868
12066
  ` End session: ${sessionCommand("bdg stop")}`
11869
12067
  ].join("\n");
11870
12068
  }
@@ -11881,6 +12079,9 @@ function connectionLostRetryMessage(timestamp, retryLabel) {
11881
12079
  function connectionLostStopHintMessage() {
11882
12080
  return "Press Ctrl+C to stop";
11883
12081
  }
12082
+ function tailDeprecatedNotice() {
12083
+ return 'Note: "bdg tail" is deprecated and will be removed; use "bdg peek --follow" (same options: --last, --network, --console, --interval, --verbose)';
12084
+ }
11884
12085
 
11885
12086
  // src/commands/shared/daemonErrorHandler.ts
11886
12087
  init_exitCodes();
@@ -13687,9 +13888,17 @@ function registerFormInteractionCommands(program2) {
13687
13888
  const action = options.double ? "double" : options.right ? "right" : "click";
13688
13889
  await runPointerCommand(selectorOrIndex, options, action);
13689
13890
  });
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) => {
13691
- await runPointerCommand(selectorOrIndex, options, "hover");
13692
- });
13891
+ domCommand.command("hover").description(
13892
+ "Move the mouse over an element (shows hover menus and tooltips); --off moves it off the page"
13893
+ ).argument("[selectorOrIndex]", SELECTOR_OR_INDEX_ARGUMENT).option("--index <n>", "Element index if selector matches multiple (0-based)", integerOption(0)).option("--off", "Move the mouse off the page instead (closes menus that open on hover)").option("--strict", STRICT_OPTION_HELP).option("--no-wait", "Skip waiting for network stability after hovering").addOption(jsonOption()).action(
13894
+ async (selectorOrIndex, options) => {
13895
+ if (options.off || selectorOrIndex === void 0) {
13896
+ await hoverOff(selectorOrIndex, options);
13897
+ return;
13898
+ }
13899
+ await runPointerCommand(selectorOrIndex, options, "hover");
13900
+ }
13901
+ );
13693
13902
  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(
13694
13903
  "--wait-network <ms>",
13695
13904
  "Wait for network idle after submit (milliseconds)",
@@ -13807,6 +14016,25 @@ function scrollOptionsProblem(selector, options) {
13807
14016
  if (!selector && vertical + horizontal === 0) return "no-target";
13808
14017
  return null;
13809
14018
  }
14019
+ async function hoverOff(selectorOrIndex, options) {
14020
+ await runCommand(
14021
+ async () => {
14022
+ if (!options.off || selectorOrIndex !== void 0 || options.index !== void 0) {
14023
+ const err = options.off ? hoverOffWithTargetError() : missingArgumentError(HOVER_USAGE);
14024
+ return {
14025
+ success: false,
14026
+ error: err.message,
14027
+ exitCode: EXIT_CODES.INVALID_ARGUMENTS,
14028
+ errorContext: { suggestion: err.suggestion }
14029
+ };
14030
+ }
14031
+ await callCDP("Input.dispatchMouseEvent", { type: "mouseMoved", x: -1, y: -1 });
14032
+ return { success: true, data: { pointer: "off" } };
14033
+ },
14034
+ options,
14035
+ () => HOVER_OFF_DONE
14036
+ );
14037
+ }
13810
14038
  async function runPointerCommand(selectorOrIndex, options, action) {
13811
14039
  await runCommand(
13812
14040
  () => options.double && options.right ? Promise.resolve({
@@ -13849,7 +14077,15 @@ function formatActionOutput(done, details, result, options = {}) {
13849
14077
  fmt.blank();
13850
14078
  fmt.keyValueList(details, keyWidth);
13851
14079
  if (result.navigation) fmt.keyValue("Page", pageNavigationText(result.navigation), keyWidth);
13852
- listRows(fmt, "New text", (result.messages ?? []).map(newMessageText), keyWidth);
14080
+ listRows(
14081
+ fmt,
14082
+ "New text",
14083
+ [
14084
+ ...(result.messages ?? []).map(newMessageText),
14085
+ ...result.moreMessages ? [moreMessagesText(result.moreMessages)] : []
14086
+ ],
14087
+ keyWidth
14088
+ );
13853
14089
  listRows(fmt, "Shown", (result.shown ?? []).map(shownElementText), keyWidth);
13854
14090
  const omitted = result.triggeredRequestsOmitted;
13855
14091
  const requests = formatTriggeredRequestLines(result.triggeredRequests ?? [], omitted);
@@ -14784,19 +15020,19 @@ function registerA11yCommands(domCmd) {
14784
15020
  const a11y = domCmd.command("a11y").description("Accessibility tree inspection and semantic queries").argument(
14785
15021
  "[search]",
14786
15022
  'Quick search: index (describe), CSS selector (#id, .class), pattern with ":" (query), or name search'
14787
- ).enablePositionalOptions().addOption(jsonOption()).action(async (search, options) => {
14788
- if (!search) {
15023
+ ).enablePositionalOptions().addOption(jsonOption()).action(async (search2, options) => {
15024
+ if (!search2) {
14789
15025
  return a11y.help({ error: true });
14790
15026
  }
14791
- const isNumericIndex = /^\d+$/.test(search);
14792
- const isCssSelector = /^[#.[]/u.test(search);
14793
- const isPatternQuery = /^(role|name|description|desc)\s*[:=]/i.test(search);
15027
+ const isNumericIndex = /^\d+$/.test(search2);
15028
+ const isCssSelector = /^[#.[]/u.test(search2);
15029
+ const isPatternQuery = /^(role|name|description|desc)\s*[:=]/i.test(search2);
14794
15030
  if (isNumericIndex || isCssSelector) {
14795
- await handleA11yDescribe(search, options);
15031
+ await handleA11yDescribe(search2, options);
14796
15032
  } else if (isPatternQuery) {
14797
- await handleA11yQuery(search, options);
15033
+ await handleA11yQuery(search2, options);
14798
15034
  } else {
14799
- await handleA11yQuery(`name:${quoteQueryValue(search)}`, options);
15035
+ await handleA11yQuery(`name:${quoteQueryValue(search2)}`, options);
14800
15036
  }
14801
15037
  });
14802
15038
  a11y.command("tree").description("Dump full accessibility tree (filters ignored nodes)").addOption(jsonOption()).action(async (options) => {
@@ -14900,7 +15136,10 @@ function formatDomScreenshot(data) {
14900
15136
  output += " (viewport only - page too tall)";
14901
15137
  }
14902
15138
  if (data.element?.captured) {
14903
- output += ` (${screenshotGrownNote(data.element.bounds, data.element.captured)})`;
15139
+ output += ` (${screenshotGrownNote(data.element.bounds, data.element.captured, data.element.padding)})`;
15140
+ }
15141
+ if (data.resized && data.originalWidth !== void 0 && data.originalHeight !== void 0) {
15142
+ output += ` (${screenshotScaledNote(data.originalWidth, data.originalHeight, data.width, data.height)})`;
14904
15143
  }
14905
15144
  return output;
14906
15145
  }
@@ -15688,11 +15927,11 @@ function inspectHeader(data) {
15688
15927
  data.element,
15689
15928
  data.content !== void 0 && `"${data.content}"`,
15690
15929
  data.placeholder !== void 0 && `placeholder "${data.placeholder}"`,
15691
- data.rect && `${data.rect.w}x${data.rect.h} @${data.rect.x},${data.rect.y}`,
15930
+ data.rect && `${data.rect.w}x${data.rect.h}${data.rect.screen ? ` (${data.rect.screen.w}x${data.rect.screen.h} on screen)` : ""} @${data.rect.x},${data.rect.y}${data.rect.in ? " (fixed: viewport position)" : ""}`,
15692
15931
  kind && `[${kind}]`,
15693
15932
  ...inspectVisibilityBadges(data.visibility),
15694
15933
  data.context && `in ${data.context}`,
15695
- data.theme === "dark" && inspectDarkThemeBadge(),
15934
+ data.theme === "dark" && inspectDarkThemeBadge(data.themeFrom === "emulation"),
15696
15935
  data.animating && inspectAnimatingBadge(data.animating)
15697
15936
  ].filter(Boolean).join(" ");
15698
15937
  }
@@ -15767,6 +16006,7 @@ function parentLine(layout) {
15767
16006
  function fontParts(text) {
15768
16007
  const loaded = [
15769
16008
  text.rendered && `rendered "${text.rendered}"`,
16009
+ text.resolved && `resolves to "${text.resolved}"`,
15770
16010
  text.webfont && "webfont loaded"
15771
16011
  ].filter(Boolean);
15772
16012
  const family = text.family && [text.family, loaded.length > 0 && `(${loaded.join(", ")})`];
@@ -15787,14 +16027,16 @@ function fontParts(text) {
15787
16027
  function contrastText(contrast) {
15788
16028
  if (!contrast) return void 0;
15789
16029
  return [
15790
- `contrast ${contrast.ratio} ${contrast.level}`,
15791
- contrast.inherited && `on ${contrast.background}`,
16030
+ contrast.approximate ? `contrast \u2248${contrast.ratio} ${contrast.level}` : `contrast ${contrast.ratio} ${contrast.level}`,
16031
+ `on ${contrast.background}`,
15792
16032
  contrast.overImage && "(over image)",
15793
- contrast.opacity !== void 0 && `(faded: opacity ${contrast.opacity})`
16033
+ contrast.opacity !== void 0 && `(faded: opacity ${contrast.opacity})`,
16034
+ contrast.approximate && `(approximate: ${contrast.approximate.join(", ")})`
15794
16035
  ].filter(Boolean).join(" ");
15795
16036
  }
15796
16037
  function textLine(text) {
15797
16038
  return groupLine("text", [
16039
+ text.holder && `in ${text.holder}`,
15798
16040
  ...fontParts(text),
15799
16041
  text.color && `color ${text.color}`,
15800
16042
  contrastText(text.contrast),
@@ -15805,17 +16047,25 @@ function textLine(text) {
15805
16047
  text.whiteSpace && `ws ${text.whiteSpace}`,
15806
16048
  text.overflow && `text-overflow ${text.overflow}`,
15807
16049
  text.clamp && `clamp ${text.clamp}`,
16050
+ text.truncated && "truncated",
16051
+ text.gradientFill && "filled by its background (background-clip: text; no contrast)",
15808
16052
  text.shadow && `shadow ${text.shadow}`,
15809
16053
  text.features && `features ${text.features}`
15810
16054
  ]);
15811
16055
  }
15812
16056
  function fillText(fill) {
15813
16057
  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}` : ""}`;
16058
+ return [
16059
+ `bg-image ${fill.value}`,
16060
+ fill.size && `size ${fill.size}`,
16061
+ fill.position && `at ${fill.position}`
16062
+ ].filter(Boolean).join(" ");
15816
16063
  }
15817
16064
  function fillLine(data) {
16065
+ const paint = data.paint;
15818
16066
  return groupLine("fill", [
16067
+ paint && `fill ${paint.fill}`,
16068
+ paint && `stroke ${paint.stroke}${paint.strokeWidth !== void 0 ? ` ${paint.strokeWidth}` : ""}`,
15819
16069
  ...(data.fills ?? []).map(fillText),
15820
16070
  data.opacity !== void 0 && `opacity ${data.opacity}`,
15821
16071
  data.blend && `blend ${data.blend}`
@@ -15865,6 +16115,7 @@ function pseudoText(pseudo) {
15865
16115
  pseudo.content !== void 0 && `content ${pseudo.content}`,
15866
16116
  pseudo.display,
15867
16117
  pseudo.position,
16118
+ pseudo.inset && `inset ${pseudo.inset}`,
15868
16119
  pseudo.size && `${pseudo.size.w}x${pseudo.size.h}`,
15869
16120
  pseudo.color && `color ${pseudo.color}`,
15870
16121
  pseudo.fontStyle,
@@ -15886,7 +16137,9 @@ function treeRow(node) {
15886
16137
  return [
15887
16138
  node.element,
15888
16139
  node.count !== void 0 && `\xD7${node.count}`,
15889
- `${node.w}x${node.h}`,
16140
+ node.contents ? "(contents)" : `${node.w}x${node.h}`,
16141
+ node.shadow && "(shadow root)",
16142
+ node.via && `via ${node.via}`,
15890
16143
  node.layout && `[${node.layout}]`,
15891
16144
  node.text && `"${node.text}"`,
15892
16145
  node.childCount !== void 0 && `(${node.childCount})`
@@ -15908,7 +16161,7 @@ function ruleScope(entry) {
15908
16161
  }
15909
16162
  function cascadeBlock(data) {
15910
16163
  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}`
16164
+ (hint) => `${hint.property}: ${hint.value} ${hint.kind === "not-inherited" ? "is the browser's" : hint.only ? `has no effect on ${hint.only.join(", ")}` : "has no effect"}: ${hint.reason} \u2192 ${hint.fix} \xB7 in ${hint.source}`
15912
16165
  );
15913
16166
  return [
15914
16167
  ...labelledLines("hints", data.hints?.length === 0 ? ["none"] : hints),
@@ -15939,7 +16192,8 @@ function whyLines(why, data) {
15939
16192
  truncateByLength(value, CASCADE_VALUE_WIDTH) + resolved,
15940
16193
  entry.important ? " !important" : "",
15941
16194
  ` ${entry.source}${entry.specificity ? ` [${entry.specificity.join(",")}]` : ""}${ruleScope(entry)}`,
15942
- entry.status === "inherited" ? " (inherited)" : ""
16195
+ entry.status === "inherited" ? " (inherited)" : "",
16196
+ entry.note ? ` (${entry.note})` : ""
15943
16197
  ].join("");
15944
16198
  });
15945
16199
  const variables = (why.variables ?? []).map(
@@ -15948,8 +16202,12 @@ function whyLines(why, data) {
15948
16202
  return [
15949
16203
  `${why.property} = ${why.computed}${midTransition(why.property, data.animating) ? ` ${inspectMidTransitionNote()}` : ""}`,
15950
16204
  ...entries.slice(0, 1),
16205
+ ...why.chain[0]?.rule ? [` in ${truncateByLength(why.chain[0].rule, RULE_LINE_WIDTH)}`] : [],
15951
16206
  ...variables,
15952
16207
  ...entries.slice(1),
16208
+ ...(why.inactive ?? []).map(
16209
+ (rule) => ` - ${truncateByLength(rule.value, CASCADE_VALUE_WIDTH)} ${rule.selector} ${rule.condition} (does not apply now)`
16210
+ ),
15953
16211
  ...why.chain.length === 0 ? [" no author declaration: the default or inherited value"] : []
15954
16212
  ];
15955
16213
  }
@@ -15961,6 +16219,7 @@ function midTransition(property, animating) {
15961
16219
  function levelsUp(levels) {
15962
16220
  return levels === 1 ? "the parent" : `${levels} levels up`;
15963
16221
  }
16222
+ var RULE_LINE_WIDTH = 120;
15964
16223
  var CASCADE_VALUE_WIDTH = 60;
15965
16224
  function treeBlock(data) {
15966
16225
  if (!data.children && !data.hiddenChildren) return [];
@@ -16005,8 +16264,16 @@ function groupLines(data) {
16005
16264
  function formatInspect(data) {
16006
16265
  const body = data.props ? propLines(data.props) : data.all ? allLines(data.all) : groupLines(data);
16007
16266
  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);
16267
+ const pseudoNote = data.pseudoOf && inspectPseudoOfNote(data.pseudoOf);
16268
+ if (data.why) return joinLines(inspectHeader(data), ...cascadeBlock(data), note, pseudoNote);
16269
+ return joinLines(
16270
+ inspectHeader(data),
16271
+ ...body,
16272
+ ...cascadeBlock(data),
16273
+ ...treeBlock(data),
16274
+ note,
16275
+ pseudoNote
16276
+ );
16010
16277
  }
16011
16278
 
16012
16279
  // src/commands/dom/inspect.ts
@@ -16075,6 +16342,125 @@ async function inspectTarget(selectorOrIndex, options) {
16075
16342
  });
16076
16343
  }
16077
16344
 
16345
+ // src/commands/dom/audit.ts
16346
+ init_messages();
16347
+ init_client();
16348
+
16349
+ // src/ipc/protocol/auditTypes.ts
16350
+ var AUDIT_CHECKS = ["contrast", "overflow", "layers", "animations"];
16351
+
16352
+ // src/ui/formatters/audit.ts
16353
+ init_strings();
16354
+ var TEXT_WIDTH = 50;
16355
+ function formatAudit(data) {
16356
+ const sections = [
16357
+ data.contrast && contrastSection(data.contrast),
16358
+ data.overflow && overflowSection(data.overflow),
16359
+ data.layers && layersSection(data.layers),
16360
+ data.animations && animationsSection(data.animations)
16361
+ ].filter((section2) => section2 !== void 0);
16362
+ const capped = data.capped ? [`(stopped after ${data.walked} elements; the page has more)`] : [];
16363
+ return [...sections.flatMap((lines) => [...lines, ""]), ...capped].join("\n").trimEnd();
16364
+ }
16365
+ function contrastSection(contrast) {
16366
+ const head = `Contrast (${contrast.level}): ${contrast.failing} of ${contrast.checked} text elements below`;
16367
+ const rows = contrast.items.map(
16368
+ (item) => ` ${item.ratio.toFixed(2).padStart(5)} ${item.color} on ${item.background} ${item.element} "${truncateByLength(item.text, TEXT_WIDTH)}" ${item.size}px${item.weight >= 700 ? " bold" : ""}${item.inView ? "" : " (out of view)"}${item.approximate ? ` (approximate: ${item.approximate.join(", ")})` : ""}`
16369
+ );
16370
+ const more = contrast.failing - contrast.items.length;
16371
+ return [head, ...rows, ...more > 0 ? [` (+${more} more; --limit to list them)`] : []];
16372
+ }
16373
+ function overflowSection(overflow) {
16374
+ const head = overflow.scrollsSideways ? `Overflow: the page is ${overflow.pageWidth} wide in a ${overflow.viewportWidth} viewport (it scrolls sideways)` : `Overflow: nothing makes the page scroll sideways (${overflow.viewportWidth} wide)`;
16375
+ return [
16376
+ head,
16377
+ ...overflow.wide.map(
16378
+ (item) => ` past the right edge: ${item.element} (to ${item.right}, ${item.width} wide)`
16379
+ ),
16380
+ ...overflow.truncated.map(
16381
+ (item) => ` cut off (${item.kind}): ${item.element}${times(item.count)} "${truncateByLength(item.text, TEXT_WIDTH)}"`
16382
+ ),
16383
+ ...overflow.scrollers.map(
16384
+ (item) => ` scrolls sideways inside: ${item.element} (${item.scrollWidth} of content in ${item.width})`
16385
+ ),
16386
+ ...overflow.images.map(
16387
+ (image) => ` image ${[image.upscaled && `upscaled ${image.scale}x${overflow.pixelRatio > 1 ? ` at pixel ratio ${overflow.pixelRatio}` : ""}`, image.distorted && "distorted"].filter(Boolean).join(", ")}: ${image.element}${times(image.count)} ${image.natural.w}x${image.natural.h} pixels drawn at ${image.rendered.w}x${image.rendered.h} CSS px`
16388
+ )
16389
+ ];
16390
+ }
16391
+ function times(count) {
16392
+ return count ? ` \xD7${count}` : "";
16393
+ }
16394
+ function layersSection(layers) {
16395
+ if (layers.length === 0) return ["Layers: no fixed or sticky elements"];
16396
+ return [
16397
+ `Layers: ${layers.length} fixed or sticky`,
16398
+ ...layers.map(
16399
+ (layer) => ` ${layer.position} z ${layer.zIndex} ${layer.element} ${layer.rect.w}x${layer.rect.h} at ${layer.rect.x},${layer.rect.y} of the viewport${layer.inView ? "" : " (not in view now)"}`
16400
+ )
16401
+ ];
16402
+ }
16403
+ function animationsSection(animations) {
16404
+ if (animations.length === 0) return ["Animations: none running"];
16405
+ return [
16406
+ `Animations: ${animations.length} running`,
16407
+ ...animations.map(
16408
+ (animation) => ` ${animation.name} on ${animation.element}${times(animation.count)} (${animation.type}, ${typeof animation.duration === "number" ? `${animation.duration}ms` : animation.duration}, ${animation.iterations === "infinite" ? "infinite" : `${animation.iterations}x`}${animation.scrollDriven ? ", scroll-driven" : ""})`
16409
+ )
16410
+ ];
16411
+ }
16412
+ function formatCssSearch(data) {
16413
+ if (data.total === 0) return `"${data.query}" is not in the page's ${data.sheets} stylesheets`;
16414
+ const more = data.total - data.matches.length;
16415
+ return [
16416
+ `"${data.query}": ${data.total} matches in ${data.sheets} stylesheets`,
16417
+ ...data.matches.flatMap((match) => [` ${match.source}`, ` ${match.text}`]),
16418
+ ...more > 0 ? [` (+${more} more; --limit to list them)`] : []
16419
+ ].join("\n");
16420
+ }
16421
+
16422
+ // src/commands/dom/audit.ts
16423
+ init_commands();
16424
+ init_exitCodes();
16425
+ init_suggestions();
16426
+ function registerAuditCommand(dom) {
16427
+ dom.command("audit").description(
16428
+ "Page-wide checks: text below WCAG contrast, sideways scroll and cut-off text, scaled images, fixed/sticky layers, animations"
16429
+ ).argument("[checks...]", `Checks to run: ${AUDIT_CHECKS.join(", ")} (default: all)`, checkList).option("--level <level>", "WCAG level text must reach: AA or AAA (default: AA)", levelOption).option("--limit <n>", "Findings listed per check (default: 20)", integerOption(1, 500)).addOption(jsonOption()).addHelpText("after", AUDIT_HELP_EXAMPLES).action(async (checks, options) => {
16430
+ await runCommand(() => audit(checks, options), options, formatAudit);
16431
+ });
16432
+ }
16433
+ function checkList(value, previous = []) {
16434
+ const check = AUDIT_CHECKS.find((name) => name === value.trim().toLowerCase());
16435
+ if (!check) {
16436
+ throw new InvalidArgumentError(
16437
+ unknownAuditCheckMessage(value, findSimilar(value, [...AUDIT_CHECKS]), AUDIT_CHECKS)
16438
+ );
16439
+ }
16440
+ return previous.includes(check) ? previous : [...previous, check];
16441
+ }
16442
+ function levelOption(value) {
16443
+ const level = value.trim().toUpperCase();
16444
+ if (level === "AA" || level === "AAA") return level;
16445
+ throw new InvalidArgumentError("Use AA or AAA");
16446
+ }
16447
+ async function audit(checks, options) {
16448
+ const response = await domAudit({
16449
+ checks: checks && checks.length > 0 ? checks : [...AUDIT_CHECKS],
16450
+ ...options.level && { level: options.level },
16451
+ ...options.limit !== void 0 && { limit: options.limit }
16452
+ });
16453
+ if (response.status === "error" || !response.data) {
16454
+ return {
16455
+ success: false,
16456
+ error: response.error ?? "Failed to audit the page",
16457
+ exitCode: response.exitCode ?? EXIT_CODES.CDP_CONNECTION_FAILURE,
16458
+ ...response.suggestion && { errorContext: { suggestion: response.suggestion } }
16459
+ };
16460
+ }
16461
+ return { success: true, data: response.data };
16462
+ }
16463
+
16078
16464
  // src/commands/dom/layout.ts
16079
16465
  init_client();
16080
16466
 
@@ -16394,13 +16780,17 @@ function buildElementScreenshotOptions(options) {
16394
16780
  return filterDefined({
16395
16781
  format: options.format,
16396
16782
  quality: options.quality,
16397
- noResize: options.resize === false
16783
+ noResize: options.resize === false,
16784
+ padding: options.padding
16398
16785
  });
16399
16786
  }
16400
16787
  function hasElementTarget(options) {
16401
16788
  return options.selector !== void 0 || options.index !== void 0;
16402
16789
  }
16403
16790
  async function resolveElementNodeId(options) {
16791
+ if (options.selector !== void 0 && options.index !== void 0) {
16792
+ return { backendNodeId: await selectMatch(options.selector, options.index) };
16793
+ }
16404
16794
  if (options.index !== void 0) {
16405
16795
  const resolver = DomElementResolver.getInstance();
16406
16796
  const node = await resolver.getNodeIdForIndex(options.index);
@@ -16535,8 +16925,8 @@ function reportSequenceError(error, captured, json) {
16535
16925
  }
16536
16926
  function assertScreenshotOptions(outputPath, options) {
16537
16927
  let message;
16538
- if (options.selector !== void 0 && options.index !== void 0) {
16539
- message = conflictingOptionsMessage("--selector", "--index");
16928
+ if (options.padding !== void 0 && !hasElementTarget(options)) {
16929
+ message = "--padding applies to element captures; name an element (selector or index)";
16540
16930
  } else if (options.quality !== void 0 && !options.follow && resolveImageFormat(outputPath, options.format) === "png") {
16541
16931
  message = "--quality applies to JPEG only; this screenshot is a PNG";
16542
16932
  }
@@ -16586,7 +16976,7 @@ function registerWaitCommand(dom) {
16586
16976
  "Wait until elements appear, become visible, contain a text or are gone (or the page loads)"
16587
16977
  ).argument(
16588
16978
  "[selector]",
16589
- "CSS selector (:has-text, :visible allowed; shadow DOM and same-origin iframes searched); optional with --load"
16979
+ "CSS selector (:has-text, :visible allowed; shadow DOM and same-origin iframes searched); without one, waits for the page to load (--load)"
16590
16980
  ).option("--text <text>", "A match must contain this text (case-insensitive)").option("--visible", "Only count visible matches").option("--gone", "Wait until no element matches (none visible, with --visible)").option("--load", "Also wait for the page to finish loading (document.readyState complete)").option(
16591
16981
  "--timeout <ms>",
16592
16982
  "Give up after this many milliseconds (exit 102)",
@@ -16597,7 +16987,8 @@ function registerWaitCommand(dom) {
16597
16987
  });
16598
16988
  }
16599
16989
  async function waitFor(selector, options) {
16600
- const needsSelector = options.text !== void 0 || options.gone === true || !options.load;
16990
+ const load = options.load === true || selector === void 0 && options.text === void 0 && !options.gone && !options.visible;
16991
+ const needsSelector = options.text !== void 0 || options.gone === true || !load;
16601
16992
  if (selector === void 0 && needsSelector) {
16602
16993
  const err = waitTargetRequiredError();
16603
16994
  return {
@@ -16611,7 +17002,7 @@ async function waitFor(selector, options) {
16611
17002
  ...filterDefined({ selector, text: options.text }),
16612
17003
  ...options.gone && { gone: true },
16613
17004
  ...options.visible && { visible: true },
16614
- ...options.load && { load: true },
17005
+ ...load && { load: true },
16615
17006
  timeout: options.timeout
16616
17007
  });
16617
17008
  if (response.status === "error" || !response.data) {
@@ -16635,6 +17026,7 @@ function registerDomCommands(program2) {
16635
17026
  registerFormCommand(dom);
16636
17027
  registerListenersCommand(dom);
16637
17028
  registerLayoutCommand(dom);
17029
+ registerAuditCommand(dom);
16638
17030
  registerInspectCommand(dom);
16639
17031
  registerWaitCommand(dom);
16640
17032
  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) => {
@@ -16669,8 +17061,12 @@ function registerDomCommands(program2) {
16669
17061
  "Element to capture: CSS selector or index from a query (same as --selector / --index)"
16670
17062
  ).option("--selector <selector>", "CSS selector for element capture").option(
16671
17063
  "--index <number>",
16672
- "Cached element index (0-based) from previous query",
17064
+ "Cached element index (0-based) from a previous query; with --selector, which match",
16673
17065
  integerOption(0)
17066
+ ).option(
17067
+ "--padding <px>",
17068
+ "Element capture: extra space around it (shadows and focus rings are included anyway)",
17069
+ integerOption(0, 500)
16674
17070
  ).option(
16675
17071
  "--format <format>",
16676
17072
  "Image format: png or jpeg/jpg (default: from the file extension, else png)",
@@ -17527,7 +17923,7 @@ function appearanceLines(metadata, pageState) {
17527
17923
  ...viewport ? [
17528
17924
  [
17529
17925
  "Viewport",
17530
- `${viewport.width}\xD7${viewport.height}${metadata.viewport ? ` (emulated ${metadata.viewport.width}x${metadata.viewport.height})` : ""}`
17926
+ `${viewport.width}\xD7${viewport.height}${metadata.viewport ? ` (emulated ${metadata.viewport.width}x${metadata.viewport.height}${metadata.viewport.mobile ? ", phone" : ""})` : ""}`
17531
17927
  ]
17532
17928
  ] : [],
17533
17929
  ...scheme ? [
@@ -18349,6 +18745,37 @@ function registerNetworkCommands(program2) {
18349
18745
  registerDocumentCommand(networkCmd);
18350
18746
  }
18351
18747
 
18748
+ // src/commands/css.ts
18749
+ init_client();
18750
+ init_commands();
18751
+ init_exitCodes();
18752
+ function registerCssCommands(program2) {
18753
+ const css = program2.command("css").description("The page's stylesheets: search <text>");
18754
+ css.command("search").description(
18755
+ "Find text in every stylesheet of the page (cross-origin ones too), with the rule and file:line"
18756
+ ).argument(
18757
+ "<text>",
18758
+ "Text to find (case-insensitive), e.g. oklch( or .btn-primary; put -- before one that starts with - (-- --brand)"
18759
+ ).option("--limit <n>", "Matches listed (default: 20)", integerOption(1, 500)).addOption(jsonOption()).addHelpText("after", CSS_SEARCH_HELP_EXAMPLES).action(async (text, options) => {
18760
+ await runCommand(() => search(text, options), options, formatCssSearch);
18761
+ });
18762
+ }
18763
+ async function search(text, options) {
18764
+ const response = await cssSearch({
18765
+ query: text,
18766
+ ...options.limit !== void 0 && { limit: options.limit }
18767
+ });
18768
+ if (response.status === "error" || !response.data) {
18769
+ return {
18770
+ success: false,
18771
+ error: response.error ?? "Failed to search the stylesheets",
18772
+ exitCode: response.exitCode ?? EXIT_CODES.CDP_CONNECTION_FAILURE,
18773
+ ...response.suggestion && { errorContext: { suggestion: response.suggestion } }
18774
+ };
18775
+ }
18776
+ return { success: true, data: response.data };
18777
+ }
18778
+
18352
18779
  // src/commands/start.ts
18353
18780
  import fs15 from "node:fs";
18354
18781
  import os3 from "node:os";
@@ -18822,8 +19249,17 @@ function applyCollectorOptions(command) {
18822
19249
  ).option(
18823
19250
  "--color-scheme <scheme>",
18824
19251
  "Emulate prefers-color-scheme for the session: light or dark (default: the system setting)"
19252
+ ).option(
19253
+ "--mobile",
19254
+ `Emulate a phone for the session: mobile viewport (${MOBILE_VIEWPORT.width}x${MOBILE_VIEWPORT.height} unless --viewport), touch, mobile user agent`
18825
19255
  );
18826
19256
  }
19257
+ var MOBILE_VIEWPORT = { width: 390, height: 844 };
19258
+ function requestedViewport(viewport, mobile) {
19259
+ const size = viewport !== void 0 ? parseViewport(viewport) : void 0;
19260
+ if (!mobile) return size;
19261
+ return { ...size ?? MOBILE_VIEWPORT, mobile: true };
19262
+ }
18827
19263
  var MAX_VIEWPORT_SIDE = 1e4;
18828
19264
  var COLOR_SCHEMES = ["light", "dark"];
18829
19265
  function parseViewport(value) {
@@ -18871,7 +19307,7 @@ function buildSessionOptions(options) {
18871
19307
  quiet: options.quiet ?? false,
18872
19308
  json: options.json ?? false,
18873
19309
  chromeFlags,
18874
- viewport: options.viewport !== void 0 ? parseViewport(options.viewport) : void 0,
19310
+ viewport: requestedViewport(options.viewport, options.mobile),
18875
19311
  colorScheme: options.colorScheme !== void 0 ? parseColorScheme(options.colorScheme) : void 0
18876
19312
  };
18877
19313
  }
@@ -19123,7 +19559,7 @@ async function showPageInfo(options) {
19123
19559
  }
19124
19560
  function emulationRequest(options) {
19125
19561
  if (options.reset) return { reset: true };
19126
- if (options.viewport === void 0 && options.colorScheme === void 0) {
19562
+ if (options.viewport === void 0 && options.colorScheme === void 0 && !options.mobile) {
19127
19563
  const err = pageEmulateNothingError();
19128
19564
  throw new CommandError(
19129
19565
  err.message,
@@ -19131,8 +19567,9 @@ function emulationRequest(options) {
19131
19567
  EXIT_CODES.INVALID_ARGUMENTS
19132
19568
  );
19133
19569
  }
19570
+ const viewport = requestedViewport(options.viewport, options.mobile);
19134
19571
  return {
19135
- ...options.viewport !== void 0 && { viewport: parseViewport(options.viewport) },
19572
+ ...viewport && { viewport },
19136
19573
  ...options.colorScheme !== void 0 && {
19137
19574
  colorScheme: parseColorScheme(options.colorScheme)
19138
19575
  }
@@ -19169,10 +19606,17 @@ function registerPageCommands(program2) {
19169
19606
  ).action(async (url, options) => {
19170
19607
  await runPageAction("navigate", options, url);
19171
19608
  });
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(
19609
+ page.command("emulate").description(PAGE_EMULATE_DESCRIPTION).option(
19610
+ "--viewport <WxH>",
19611
+ "Viewport size in CSS px, e.g. 900x700 (a desktop one unless --mobile)"
19612
+ ).option("--color-scheme <scheme>", "Emulate prefers-color-scheme: light or dark").option(
19613
+ "--mobile",
19614
+ "Emulate a phone: mobile viewport (390x844 unless --viewport), touch, mobile user agent"
19615
+ ).addOption(
19173
19616
  new Option("--reset", "Back to the browser window size and the system setting").conflicts([
19174
19617
  "viewport",
19175
- "colorScheme"
19618
+ "colorScheme",
19619
+ "mobile"
19176
19620
  ])
19177
19621
  ).addOption(jsonOption()).action(async (options) => {
19178
19622
  await emulate(options);
@@ -19187,6 +19631,9 @@ function registerPageCommands(program2) {
19187
19631
  }
19188
19632
 
19189
19633
  // src/commands/peek.ts
19634
+ init_errors3();
19635
+ init_messages();
19636
+ init_exitCodes();
19190
19637
  function parseOptions(options) {
19191
19638
  const lastN = positiveIntRule({
19192
19639
  name: "--last",
@@ -19196,7 +19643,21 @@ function parseOptions(options) {
19196
19643
  allowZeroForAll: true
19197
19644
  }).validate(options.last);
19198
19645
  const resourceTypes = resourceTypeRule().validate(options.type);
19199
- return { lastN, resourceTypes };
19646
+ if (options.interval !== void 0 && !options.follow) {
19647
+ const err = intervalWithoutFollowError();
19648
+ throw new CommandError(
19649
+ err.message,
19650
+ { suggestion: err.suggestion },
19651
+ EXIT_CODES.INVALID_ARGUMENTS
19652
+ );
19653
+ }
19654
+ const interval = positiveIntRule({
19655
+ name: "--interval",
19656
+ min: 100,
19657
+ max: 6e4,
19658
+ default: 1e3
19659
+ }).validate(options.interval);
19660
+ return { lastN, resourceTypes, interval };
19200
19661
  }
19201
19662
  function peekSection(options) {
19202
19663
  if (options.network) return "network";
@@ -19230,11 +19691,12 @@ function createPreviewOptions(base, resourceTypes, unfilteredCount) {
19230
19691
  }
19231
19692
  return options;
19232
19693
  }
19233
- async function runFollowMode3(options, lastN, resourceTypes, baseOptions) {
19694
+ async function runFollowMode3(options, parsed, baseOptions) {
19695
+ const { lastN, resourceTypes, interval } = parsed;
19234
19696
  const showPreview = async () => {
19235
19697
  const result = await fetchAndFilterPreview(lastN, resourceTypes, peekSection(options));
19236
19698
  if (!result.success) {
19237
- return followFetchFailure(result, { json: options.json, retryIntervalMs: 1e3 });
19699
+ return followFetchFailure(result, { json: options.json, retryIntervalMs: interval });
19238
19700
  }
19239
19701
  noteFollowConnected();
19240
19702
  if (!options.json) console.clear();
@@ -19249,11 +19711,34 @@ async function runFollowMode3(options, lastN, resourceTypes, baseOptions) {
19249
19711
  await setupFollowMode(showPreview, {
19250
19712
  startMessage: followingPreviewMessage,
19251
19713
  stopMessage: stoppedFollowingPreviewMessage,
19252
- intervalMs: 1e3
19714
+ intervalMs: interval
19253
19715
  });
19254
19716
  }
19717
+ function parsePeekOptions(options) {
19718
+ try {
19719
+ return parseOptions(options);
19720
+ } catch (error) {
19721
+ handleValidationError(error, options.json ?? false);
19722
+ }
19723
+ }
19724
+ function previewDisplayOptions(options, lastN) {
19725
+ return {
19726
+ json: options.json,
19727
+ network: options.network,
19728
+ console: options.console,
19729
+ last: lastN,
19730
+ verbose: options.verbose,
19731
+ follow: options.follow
19732
+ };
19733
+ }
19734
+ async function followPreview(options) {
19735
+ const following = { ...options, follow: true };
19736
+ showBothSectionsWhenBothRequested(following);
19737
+ const parsed = parsePeekOptions(following);
19738
+ await runFollowMode3(following, parsed, previewDisplayOptions(following, parsed.lastN));
19739
+ }
19255
19740
  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(
19741
+ 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("--interval <ms>", "Refresh interval of --follow in ms, 100-60000 (default: 1000)").option("--last <count>", "Show last N items, 0 for all", "10").option(
19257
19742
  "--type <types>",
19258
19743
  "Filter network requests by resource type (comma-separated: Document,XHR,Fetch,etc.)"
19259
19744
  ).action(async (options) => {
@@ -19263,25 +19748,11 @@ function registerPeekCommand(program2) {
19263
19748
  'Note: "bdg peek --network" is deprecated. Use "bdg network list" for enhanced filtering.'
19264
19749
  );
19265
19750
  }
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
- };
19751
+ const parsed = parsePeekOptions(options);
19752
+ const { lastN, resourceTypes } = parsed;
19753
+ const baseOptions = previewDisplayOptions(options, lastN);
19283
19754
  if (options.follow) {
19284
- await runFollowMode3(options, lastN, resourceTypes, baseOptions);
19755
+ await runFollowMode3(options, parsed, baseOptions);
19285
19756
  return;
19286
19757
  }
19287
19758
  await runCommand(
@@ -19750,62 +20221,14 @@ function registerStopCommand(program2) {
19750
20221
  }
19751
20222
 
19752
20223
  // src/commands/tail.ts
19753
- function parseOptions2(options) {
19754
- const lastRule = positiveIntRule({
19755
- name: "--last",
19756
- min: 1,
19757
- max: MAX_LAST_ITEMS,
19758
- default: 10,
19759
- allowZeroForAll: true
19760
- });
19761
- const intervalRule = positiveIntRule({ name: "--interval", min: 100, max: 6e4, default: 1e3 });
19762
- return {
19763
- lastN: lastRule.validate(options.last),
19764
- interval: intervalRule.validate(options.interval)
19765
- };
19766
- }
19767
- function createPreviewOptions2(options, lastN) {
19768
- return {
19769
- json: options.json,
19770
- network: options.network,
19771
- console: options.console,
19772
- last: lastN,
19773
- verbose: options.verbose,
19774
- follow: true,
19775
- viewedAt: /* @__PURE__ */ new Date()
19776
- };
19777
- }
19778
20224
  function registerTailCommand(program2) {
19779
- program2.command("tail").description("Continuously monitor session data (like tail -f)").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(
20225
+ program2.command("tail").description('Deprecated: use "bdg peek --follow" (same options)').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(
19780
20226
  "--last <count>",
19781
20227
  "Show last N items (network requests + console messages), 0 for all",
19782
20228
  "10"
19783
20229
  ).option("--interval <ms>", "Update interval in milliseconds", "1000").action(async (options) => {
19784
- showBothSectionsWhenBothRequested(options);
19785
- let lastN;
19786
- let interval;
19787
- try {
19788
- const parsed = parseOptions2(options);
19789
- lastN = parsed.lastN;
19790
- interval = parsed.interval;
19791
- } catch (error) {
19792
- handleValidationError(error, options.json ?? false);
19793
- }
19794
- const showPreview = async () => {
19795
- const result = await fetchPreviewOutput({ lastN });
19796
- if (!result.success) {
19797
- return followFetchFailure(result, { json: options.json, retryIntervalMs: interval });
19798
- }
19799
- noteFollowConnected();
19800
- if (!options.json) console.clear();
19801
- console.log(formatPreview(result.data, createPreviewOptions2(options, lastN)));
19802
- return void 0;
19803
- };
19804
- await setupFollowMode(showPreview, {
19805
- startMessage: followingPreviewMessage,
19806
- stopMessage: stoppedFollowingPreviewMessage,
19807
- intervalMs: interval
19808
- });
20230
+ console.error(tailDeprecatedNotice());
20231
+ await followPreview({ ...options, follow: true });
19809
20232
  });
19810
20233
  }
19811
20234
 
@@ -19829,6 +20252,7 @@ var commandRegistry = [
19829
20252
  registerDomCommands,
19830
20253
  registerFormInteractionCommands,
19831
20254
  registerPageCommands,
20255
+ registerCssCommands,
19832
20256
  addCommandGroup("CDP Commands:"),
19833
20257
  registerCdpCommand,
19834
20258
  addCommandGroup("Network Commands:"),