browser-debugger-cli 0.10.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 (103) hide show
  1. package/.claude/skills/bdg/SKILL.md +268 -0
  2. package/README.md +148 -74
  3. package/dist/commands/css.d.ts +13 -0
  4. package/dist/commands/css.js +53 -0
  5. package/dist/commands/dom/audit.d.ts +14 -0
  6. package/dist/commands/dom/audit.js +87 -0
  7. package/dist/commands/dom/formInteraction.js +36 -6
  8. package/dist/commands/dom/helpers/keyAttributes.d.ts +3 -2
  9. package/dist/commands/dom/helpers/keyAttributes.js +6 -4
  10. package/dist/commands/dom/helpers/screenshot.d.ts +1 -0
  11. package/dist/commands/dom/helpers/screenshot.js +158 -38
  12. package/dist/commands/dom/index.js +4 -1
  13. package/dist/commands/dom/screenshot.js +10 -6
  14. package/dist/commands/dom/wait.js +5 -3
  15. package/dist/commands/helpJson.js +1 -1
  16. package/dist/commands/installSkill.d.ts +20 -0
  17. package/dist/commands/installSkill.js +87 -0
  18. package/dist/commands/optionBehaviors.js +21 -6
  19. package/dist/commands/page.js +7 -4
  20. package/dist/commands/peek.d.ts +7 -0
  21. package/dist/commands/peek.js +65 -23
  22. package/dist/commands/shared/optionTypes.d.ts +5 -1
  23. package/dist/commands/start.d.ts +13 -0
  24. package/dist/commands/start.js +19 -2
  25. package/dist/commands/tail.d.ts +7 -1
  26. package/dist/commands/tail.js +13 -62
  27. package/dist/commands.js +5 -0
  28. package/dist/daemon/session/commandRegistry.js +7 -1
  29. package/dist/daemon/session/plugins.js +4 -52
  30. package/dist/daemon.js +7986 -6848
  31. package/dist/errors/messages.d.ts +50 -4
  32. package/dist/errors/messages.js +94 -5
  33. package/dist/index.js +709 -190
  34. package/dist/ipc/client.d.ts +4 -0
  35. package/dist/ipc/client.js +8 -0
  36. package/dist/ipc/protocol/auditTypes.d.ts +129 -0
  37. package/dist/ipc/protocol/auditTypes.js +6 -0
  38. package/dist/ipc/protocol/commands.d.ts +23 -0
  39. package/dist/ipc/protocol/commands.js +2 -0
  40. package/dist/ipc/protocol/domTypes.d.ts +4 -0
  41. package/dist/ipc/protocol/inspectTypes.d.ts +71 -8
  42. package/dist/runtime/css/search.d.ts +39 -0
  43. package/dist/runtime/css/search.js +122 -0
  44. package/dist/runtime/dom/actionEffects.d.ts +4 -1
  45. package/dist/runtime/dom/actionEffects.js +8 -4
  46. package/dist/runtime/dom/audit.d.ts +19 -0
  47. package/dist/runtime/dom/audit.js +36 -0
  48. package/dist/runtime/dom/auditModel.d.ts +45 -0
  49. package/dist/runtime/dom/auditModel.js +215 -0
  50. package/dist/runtime/dom/auditScripts.d.ts +107 -0
  51. package/dist/runtime/dom/auditScripts.js +112 -0
  52. package/dist/runtime/dom/elementGeometry.d.ts +8 -2
  53. package/dist/runtime/dom/elementGeometry.js +24 -8
  54. package/dist/runtime/dom/elementInfo.d.ts +3 -2
  55. package/dist/runtime/dom/elementInfo.js +8 -2
  56. package/dist/runtime/dom/formFillHelpers/fill.js +2 -2
  57. package/dist/runtime/dom/inspect.d.ts +7 -0
  58. package/dist/runtime/dom/inspect.js +88 -23
  59. package/dist/runtime/dom/inspectAllStyles.d.ts +16 -4
  60. package/dist/runtime/dom/inspectAllStyles.js +89 -7
  61. package/dist/runtime/dom/inspectCascade.d.ts +19 -2
  62. package/dist/runtime/dom/inspectCascade.js +214 -44
  63. package/dist/runtime/dom/inspectCascadeModel.d.ts +8 -0
  64. package/dist/runtime/dom/inspectCascadeModel.js +108 -34
  65. package/dist/runtime/dom/inspectHints.d.ts +26 -3
  66. package/dist/runtime/dom/inspectHints.js +125 -9
  67. package/dist/runtime/dom/inspectModel.d.ts +3 -0
  68. package/dist/runtime/dom/inspectModel.js +30 -7
  69. package/dist/runtime/dom/inspectPaintModel.d.ts +48 -22
  70. package/dist/runtime/dom/inspectPaintModel.js +180 -68
  71. package/dist/runtime/dom/inspectRules.d.ts +19 -0
  72. package/dist/runtime/dom/inspectRules.js +21 -5
  73. package/dist/runtime/dom/inspectScripts.d.ts +85 -12
  74. package/dist/runtime/dom/inspectScripts.js +314 -28
  75. package/dist/runtime/dom/inspectTree.js +10 -2
  76. package/dist/runtime/dom/inspectWhyModel.d.ts +2 -1
  77. package/dist/runtime/dom/inspectWhyModel.js +52 -10
  78. package/dist/runtime/dom/layout.js +31 -9
  79. package/dist/runtime/dom/reactEventHelpers.d.ts +7 -0
  80. package/dist/runtime/dom/reactEventHelpers.js +27 -9
  81. package/dist/runtime/page/emulation.d.ts +13 -4
  82. package/dist/runtime/page/emulation.js +69 -4
  83. package/dist/runtime/page/userAgent.d.ts +17 -0
  84. package/dist/runtime/page/userAgent.js +57 -0
  85. package/dist/types.d.ts +12 -0
  86. package/dist/ui/formatters/audit.d.ts +19 -0
  87. package/dist/ui/formatters/audit.js +106 -0
  88. package/dist/ui/formatters/dom.d.ts +1 -1
  89. package/dist/ui/formatters/dom.js +6 -3
  90. package/dist/ui/formatters/inspect.js +42 -15
  91. package/dist/ui/formatters/installSkill.d.ts +11 -0
  92. package/dist/ui/formatters/installSkill.js +31 -0
  93. package/dist/ui/formatters/status.js +1 -1
  94. package/dist/ui/messages/commands.d.ts +44 -7
  95. package/dist/ui/messages/commands.js +83 -11
  96. package/dist/ui/messages/preview.d.ts +6 -0
  97. package/dist/ui/messages/preview.js +9 -1
  98. package/dist/utils/cssValues.js +36 -4
  99. package/dist/utils/decisionTrees.js +0 -5
  100. package/dist/utils/suggestions.d.ts +4 -2
  101. package/dist/utils/suggestions.js +7 -5
  102. package/dist/utils/taskMappings.js +1 -1
  103. package/package.json +4 -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",
@@ -3358,6 +3410,27 @@ function formInIframeError(iframeUrl, crossOrigin) {
3358
3410
  suggestion: crossOrigin ? "Cross-origin iframes cannot be read or controlled; open the iframe URL directly: bdg <iframe url>" : 'dom form lists forms of the main document only; its fields are reachable directly: bdg dom query "input, select, textarea", then bdg dom fill <index> <value>'
3359
3411
  };
3360
3412
  }
3413
+ function skillSourceMissingError(source) {
3414
+ return {
3415
+ message: `The bdg skill is missing from the installed package (${source})`,
3416
+ suggestion: "Reinstall bdg: npm install -g browser-debugger-cli"
3417
+ };
3418
+ }
3419
+ function skillWriteFailedError(path6, reason) {
3420
+ return {
3421
+ message: `Could not write ${path6}: ${reason}`,
3422
+ suggestion: "Check the permissions of that directory, or install for the other agent only (--claude or --agents)"
3423
+ };
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
+ }
3361
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;
3362
3435
  var init_messages = __esm({
3363
3436
  "src/errors/messages.ts"() {
@@ -3487,11 +3560,14 @@ function findSimilarNames(name, candidates) {
3487
3560
  const maxDistance2 = Math.max(2, Math.floor(name.length / 5));
3488
3561
  const minAffix = Math.max(4, Math.ceil(name.length / 3));
3489
3562
  const typos = findMatches(name, others, maxDistance2, false).map((match) => match.value);
3490
- 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);
3491
3564
  const ranked = [
3492
3565
  ...typos,
3493
3566
  ...byAffix((candidate) => commonSuffixLength(name, candidate)),
3494
- ...byAffix((candidate) => commonPrefixLength(name, candidate))
3567
+ ...byAffix(
3568
+ (candidate) => commonPrefixLength(name, candidate),
3569
+ Math.max(minAffix, Math.ceil(name.length / 2))
3570
+ )
3495
3571
  ];
3496
3572
  return [...new Set(ranked)].slice(0, MAX_SIMILAR_NAMES);
3497
3573
  }
@@ -3962,6 +4038,12 @@ async function domListeners(params) {
3962
4038
  async function domLayout(params) {
3963
4039
  return sendCommand("dom_layout", params);
3964
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
+ }
3965
4047
  async function domInspect(params) {
3966
4048
  return sendCommand("dom_inspect", params);
3967
4049
  }
@@ -4150,7 +4232,12 @@ var init_elementInfo = __esm({
4150
4232
  doc = frame.ownerDocument;
4151
4233
  }
4152
4234
  const root = el.getRootNode();
4153
- 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
+ }
4154
4241
  return parts.join(' > ');
4155
4242
  }`;
4156
4243
  }
@@ -4159,8 +4246,9 @@ var init_elementInfo = __esm({
4159
4246
  // src/commands/dom/helpers/keyAttributes.ts
4160
4247
  function keyAttributes(tag, attributes, state = {}) {
4161
4248
  const names = ATTRIBUTES_BY_TAG[tag];
4162
- if (!names) return void 0;
4163
- const result = {};
4249
+ const part = attributes["part"];
4250
+ if (!names) return part ? { part } : void 0;
4251
+ const result = part ? { part } : {};
4164
4252
  const type = state.type ?? attributes["type"];
4165
4253
  if (type) result["type"] = type;
4166
4254
  for (const name of names) {
@@ -4227,8 +4315,9 @@ function axisScroll(start, size, viewSize, range, aim) {
4227
4315
  const scroll = Math.round(Math.min(range.forward, Math.max(-range.back, target))) || 0;
4228
4316
  return visibleLength(start, size, viewSize, scroll) >= fully ? scroll : null;
4229
4317
  }
4230
- function classifyViewportPosition(geometry, viewport) {
4231
- 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;
4232
4321
  const rect = edgesOf(geometry.rect);
4233
4322
  const screen = edgesOf({ x: 0, y: 0, ...viewport });
4234
4323
  const view = geometry.clip ? overlap(screen, edgesOf(geometry.clip)) : screen;
@@ -4473,7 +4562,20 @@ var init_elementGeometry = __esm({
4473
4562
  }
4474
4563
  return 'not rendered';
4475
4564
  };
4476
- 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) => {
4477
4579
  if (style.display === 'none') return 'display: none';
4478
4580
  if (style.display === 'contents') return 'display: contents (no box of its own)';
4479
4581
  if (el.getClientRects().length === 0) {
@@ -4482,7 +4584,7 @@ var init_elementGeometry = __esm({
4482
4584
  const skipped = skippedReason(el);
4483
4585
  if (skipped) return skipped;
4484
4586
  if (style.visibility !== 'visible') return 'visibility: ' + style.visibility;
4485
- if (box.width === 0 || box.height === 0) return 'zero size';
4587
+ if ((box.width === 0 || box.height === 0) && !content) return 'zero size';
4486
4588
  return null;
4487
4589
  };
4488
4590
  const isSticky = () => {
@@ -4505,7 +4607,8 @@ var init_elementGeometry = __esm({
4505
4607
  let clipper = own.clipper;
4506
4608
  let fixed = own.fixed;
4507
4609
  let fixedBy = own.fixedBy;
4508
- 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;
4509
4612
  let x = 0;
4510
4613
  let y = 0;
4511
4614
  for (let view = el.ownerDocument.defaultView; view && view.frameElement; view = view.parent) {
@@ -4517,6 +4620,7 @@ var init_elementGeometry = __esm({
4517
4620
  overlay = { right: !!unframed && unframed.right <= clip.right && overlay.right, bottom: !!unframed && unframed.bottom <= clip.bottom && overlay.bottom };
4518
4621
  const offset = frameOffset(frame);
4519
4622
  rect = shift(rect, offset.x, offset.y);
4623
+ content = shift(content, offset.x, offset.y);
4520
4624
  const outer = ancestorClip(frame, rect, describe);
4521
4625
  const inner = shift(clip, offset.x, offset.y);
4522
4626
  clip = intersect(inner, outer.clip);
@@ -4535,7 +4639,7 @@ var init_elementGeometry = __esm({
4535
4639
  const invisible = hidden ? null : invisibleReason(el, describe);
4536
4640
  const lock = scrollLock(top, describe);
4537
4641
  if (lock && fixedBy === top.document.body) fixed = false;
4538
- 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 } };
4539
4643
  }`;
4540
4644
  IN_VIEW_SLACK = 1;
4541
4645
  }
@@ -5538,18 +5642,34 @@ var init_errors5 = __esm({
5538
5642
  }
5539
5643
  });
5540
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
+
5541
5653
  // src/runtime/page/emulation.ts
5542
5654
  function viewportOverride(viewport, deviceScaleFactor = 0) {
5543
- 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
+ };
5544
5662
  }
5545
- var log16;
5663
+ var log16, MOBILE_PIXEL_RATIO;
5546
5664
  var init_emulation = __esm({
5547
5665
  "src/runtime/page/emulation.ts"() {
5548
5666
  "use strict";
5549
5667
  init_elementGeometry();
5668
+ init_userAgent();
5550
5669
  init_logging();
5551
5670
  init_errors();
5552
5671
  log16 = createLogger("session");
5672
+ MOBILE_PIXEL_RATIO = 3;
5553
5673
  }
5554
5674
  });
5555
5675
 
@@ -5726,13 +5846,18 @@ async function useUnitPixelRatio(devicePixelRatio, viewport) {
5726
5846
  const sessionViewport = readSessionMetadata()?.viewport;
5727
5847
  const size = sessionViewport ?? await windowSize(viewport);
5728
5848
  await callCDP("Emulation.setDeviceMetricsOverride", viewportOverride(size, 1));
5729
- return async () => {
5730
- if (sessionViewport) {
5731
- await callCDP("Emulation.setDeviceMetricsOverride", viewportOverride(sessionViewport));
5732
- } else {
5733
- await callCDP("Emulation.clearDeviceMetricsOverride", {});
5734
- }
5735
- };
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
+ }
5736
5861
  }
5737
5862
  async function getElementBounds(ref) {
5738
5863
  const response = await callCDP("DOM.getBoxModel", ref);
@@ -5876,21 +6001,61 @@ async function visibleViewport() {
5876
6001
  function insideView(area, view) {
5877
6002
  return area.x >= 0 && area.y >= 0 && area.x + area.width <= view.width && area.y + area.height <= view.height;
5878
6003
  }
5879
- async function measureInView(ref) {
6004
+ async function measureInView(ref, padding) {
5880
6005
  const view = await visibleViewport();
5881
6006
  let box = await getElementBounds(ref);
5882
- let bounds = await captureArea(ref, box);
6007
+ let bounds = await captureArea(ref, box, padding);
5883
6008
  const fits = bounds.width <= view.width && bounds.height <= view.height;
5884
- if (fits && !insideView(bounds, view)) {
5885
- const dx = bounds.x + bounds.width / 2 - view.width / 2;
5886
- const dy = bounds.y + bounds.height / 2 - view.height / 2;
5887
- await callCDP("Runtime.evaluate", { expression: `window.scrollBy(${dx}, ${dy})` });
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 {
5888
6020
  box = await getElementBounds(ref);
5889
- bounds = await captureArea(ref, box);
6021
+ bounds = await captureArea(ref, box, padding);
6022
+ } catch (error) {
6023
+ await restoreScrollPosition(scrolledFrom);
6024
+ throw error;
5890
6025
  }
5891
- return { box, bounds, inView: insideView(bounds, view) };
6026
+ return { box, bounds, inView: insideView(bounds, view), scrolledFrom };
5892
6027
  }
5893
- async function captureArea(ref, bounds) {
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();
6039
+ }
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) {
5894
6059
  const objectGroup = `bdg-shot-${process.pid}`;
5895
6060
  try {
5896
6061
  const resolved = await callCDP("DOM.resolveNode", { ...ref, objectGroup });
@@ -5934,15 +6099,18 @@ async function captureElementScreenshot(outputPath, ref, options = {}) {
5934
6099
  devicePixelRatio,
5935
6100
  before?.visualViewport ?? { clientWidth: 800, clientHeight: 600 }
5936
6101
  );
5937
- let box;
5938
- let bounds;
5939
- let inView;
6102
+ const restore = async (wide, scrolledFrom2) => {
6103
+ await (wide ? restoreViewport() : restoreMetrics());
6104
+ if (scrolledFrom2) await restoreScrollPosition(scrolledFrom2);
6105
+ };
6106
+ let measured;
5940
6107
  try {
5941
- ({ box, bounds, inView } = await measureInView(ref));
6108
+ measured = await measureInView(ref, options.padding ?? 0);
5942
6109
  } catch (error) {
5943
- await restoreMetrics();
6110
+ await restoreViewport();
5944
6111
  throw error;
5945
6112
  }
6113
+ const { box, bounds, inView, scrolledFrom } = measured;
5946
6114
  const originalWidth = bounds.width;
5947
6115
  const originalHeight = bounds.height;
5948
6116
  const resized = shouldResize(originalWidth, originalHeight, noResize);
@@ -5968,7 +6136,7 @@ async function captureElementScreenshot(outputPath, ref, options = {}) {
5968
6136
  });
5969
6137
  screenshotResult = screenshotResponse.data?.result;
5970
6138
  } finally {
5971
- await restoreMetrics();
6139
+ await restore(!inView, scrolledFrom);
5972
6140
  }
5973
6141
  if (!screenshotResult?.data) {
5974
6142
  throw new CDPConnectionError("No screenshot data returned", new Error("Empty response"));
@@ -5985,7 +6153,8 @@ async function captureElementScreenshot(outputPath, ref, options = {}) {
5985
6153
  finalTokens: calculateImageTokens(finalWidth, finalHeight),
5986
6154
  element: {
5987
6155
  bounds: roundBounds(onPage(box)),
5988
- ...bounds !== box && { captured: roundBounds(clip) }
6156
+ ...bounds !== box && { captured: roundBounds(clip) },
6157
+ ...options.padding && { padding: options.padding }
5989
6158
  }
5990
6159
  };
5991
6160
  if (quality !== void 0) {
@@ -6051,8 +6220,37 @@ var init_screenshot = __esm({
6051
6220
  if (!clips(style)) walk(child);
6052
6221
  }
6053
6222
  };
6054
- if (!clips(view.getComputedStyle(this))) walk(this);
6055
- 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
+ };
6056
6254
  }`;
6057
6255
  OVERFLOW_SLACK = 1;
6058
6256
  }
@@ -8040,18 +8238,18 @@ Expecting one of '${allowedValues.join("', '")}'`);
8040
8238
  *
8041
8239
  * @private
8042
8240
  */
8043
- _prepareUserArgs(argv, parseOptions3) {
8241
+ _prepareUserArgs(argv, parseOptions2) {
8044
8242
  if (argv !== void 0 && !Array.isArray(argv)) {
8045
8243
  throw new Error("first parameter to parse must be array or undefined");
8046
8244
  }
8047
- parseOptions3 = parseOptions3 || {};
8048
- if (argv === void 0 && parseOptions3.from === void 0) {
8245
+ parseOptions2 = parseOptions2 || {};
8246
+ if (argv === void 0 && parseOptions2.from === void 0) {
8049
8247
  if (process2.versions?.electron) {
8050
- parseOptions3.from = "electron";
8248
+ parseOptions2.from = "electron";
8051
8249
  }
8052
8250
  const execArgv = process2.execArgv ?? [];
8053
8251
  if (execArgv.includes("-e") || execArgv.includes("--eval") || execArgv.includes("-p") || execArgv.includes("--print")) {
8054
- parseOptions3.from = "eval";
8252
+ parseOptions2.from = "eval";
8055
8253
  }
8056
8254
  }
8057
8255
  if (argv === void 0) {
@@ -8059,7 +8257,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
8059
8257
  }
8060
8258
  this.rawArgs = argv.slice();
8061
8259
  let userArgs;
8062
- switch (parseOptions3.from) {
8260
+ switch (parseOptions2.from) {
8063
8261
  case void 0:
8064
8262
  case "node":
8065
8263
  this._scriptPath = argv[1];
@@ -8081,7 +8279,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
8081
8279
  break;
8082
8280
  default:
8083
8281
  throw new Error(
8084
- `unexpected parse option { from: '${parseOptions3.from}' }`
8282
+ `unexpected parse option { from: '${parseOptions2.from}' }`
8085
8283
  );
8086
8284
  }
8087
8285
  if (!this._name && this._scriptPath)
@@ -8111,9 +8309,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
8111
8309
  * @param {string} [parseOptions.from] - where the args are from: 'node', 'user', 'electron'
8112
8310
  * @return {Command} `this` command for chaining
8113
8311
  */
8114
- parse(argv, parseOptions3) {
8312
+ parse(argv, parseOptions2) {
8115
8313
  this._prepareForParse();
8116
- const userArgs = this._prepareUserArgs(argv, parseOptions3);
8314
+ const userArgs = this._prepareUserArgs(argv, parseOptions2);
8117
8315
  this._parseCommand([], userArgs);
8118
8316
  return this;
8119
8317
  }
@@ -8137,9 +8335,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
8137
8335
  * @param {string} parseOptions.from - where the args are from: 'node', 'user', 'electron'
8138
8336
  * @return {Promise}
8139
8337
  */
8140
- async parseAsync(argv, parseOptions3) {
8338
+ async parseAsync(argv, parseOptions2) {
8141
8339
  this._prepareForParse();
8142
- const userArgs = this._prepareUserArgs(argv, parseOptions3);
8340
+ const userArgs = this._prepareUserArgs(argv, parseOptions2);
8143
8341
  await this._parseCommand([], userArgs);
8144
8342
  return this;
8145
8343
  }
@@ -9456,15 +9654,19 @@ init_schema();
9456
9654
  // src/commands/optionBehaviors.ts
9457
9655
  init_screenshotResize();
9458
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";
9459
- var ACTION_EFFECTS_BEHAVIOR = 'The result also says what changed on the page: a navigation (Page: navigated to <url> (status), or URL changed to <url> (same document); JSON navigation { url, sameDocument, status }), and messages that appeared or changed in alert/status/aria-live elements or flash/error/toast-like classes (New text: "\u2026" (element); JSON messages [{ text, element }], at most 3; after a navigation every message on the new page counts; texts of only digits and time units, such as clocks and counters, are left out, but other text that changes on its own, such as a rotating banner, can show up). Both are absent when nothing changed. Cost: one page script sent before the action without waiting for it and one read after it, a few ms; when the page does not answer (a pending navigation) bdg waits at most 200 ms for the snapshot and 250 ms per read, and the navigation is still reported from CDP events';
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';
9460
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';
9461
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`;
9462
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)";
9463
9661
  var OPTION_BEHAVIORS = {
9464
9662
  "screenshot:--selector": {
9465
9663
  default: "Captures the page (full page unless --no-full-page)",
9466
- 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',
9467
- 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"
9468
9670
  },
9469
9671
  "screenshot:--no-resize": {
9470
9672
  default: `Images auto-resized to max ${MAX_EDGE_PX}px longest edge for Claude Vision optimization (~1,600 tokens)`,
@@ -9646,10 +9848,16 @@ var OPTION_BEHAVIORS = {
9646
9848
  whenEnabled: "Lists every listener of framework roots individually",
9647
9849
  tokenImpact: "On React pages --all adds a row per event type and phase (about 140 rows, 60 KB of JSON)"
9648
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
+ },
9649
9857
  "layout:--index": {
9650
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",
9651
9859
  whenEnabled: "Reports only the nth match (0-based); out of range exits 81",
9652
- automaticBehavior: 'Coordinates are CSS px: bounds relative to the top-level page (iframe offsets and page scroll included), viewport relative to the visible area. Iframes and overflow containers (scroll lists, overflow: hidden) clip what counts as visible (clippedBy names the one cutting it off). scrollBy brings the whole element into view and is limited to how far the page can scroll: for an element out of view it centres it (aligns its start when it is larger than the viewport; human output says "to centre it"), for a partly visible one it is the smallest scroll that shows all of it (the part cut off at the top or bottom; the start of one larger than the viewport; "partly visible (87%); scroll up 5px to see all of it"). Elements a page script moves on scroll (floating menus) may move again after it; fixed and sticky elements (page scroll does not move them, or only until they stick) and ones beyond that range get offScreenReason instead, which says "page scrolling is locked (\u2026)" when the page cannot scroll because body/html is position: fixed or overflow: hidden, so in-flow content is not called fixed; a visible dialog (dialog[open], [aria-modal=true], [role=dialog|alertdialog]) is named as the likely cause ("likely by dialog div#consent"). page.viewport is the layout viewport without scrollbars, as dom scroll reports it; page.colorScheme is the prefers-color-scheme media feature the page sees (not the theme it renders). Content in a closed <details> or under content-visibility: hidden is hidden. coveredBy is the topmost element at the center of the largest visible box (none for pointer-events: none, nor for an element of the same click target: an overlay inside the link, button or label the element is in, a link to the same URL, or the textless absolutely positioned overlay link spanning the card that holds plain content); inert elements are flagged, not hidden',
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',
9653
9861
  tokenImpact: 'About one line per element; a cheap alternative to screenshots for "where is it?"'
9654
9862
  },
9655
9863
  "inspect:--index": {
@@ -9724,11 +9932,11 @@ var OPTION_BEHAVIORS = {
9724
9932
  },
9725
9933
  "peek:-f": {
9726
9934
  default: "Shows snapshot of current data",
9727
- 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"
9728
9936
  },
9729
9937
  "peek:--follow": {
9730
9938
  default: "Shows snapshot of current data",
9731
- 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"
9732
9940
  },
9733
9941
  "peek:-v": {
9734
9942
  default: "Compact output (truncated URLs, no resource types)",
@@ -9764,6 +9972,11 @@ var OPTION_BEHAVIORS = {
9764
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",
9765
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"
9766
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
+ },
9767
9980
  "bdg:--color-scheme": {
9768
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",
9769
9982
  whenEnabled: "Emulates prefers-color-scheme: light or dark for the whole session (Emulation.setEmulatedMedia); other values exit 81 with a suggestion",
@@ -10619,11 +10832,6 @@ var DECISION_TREES = {
10619
10832
  yesCommand: "peek --follow",
10620
10833
  noAction: "next"
10621
10834
  },
10622
- {
10623
- question: "Need continuous monitoring (like tail -f)?",
10624
- yesCommand: "tail",
10625
- noAction: "next"
10626
- },
10627
10835
  {
10628
10836
  question: "Need quick preview of recent data?",
10629
10837
  yesCommand: "peek",
@@ -10772,7 +10980,7 @@ var TASK_MAPPINGS = {
10772
10980
  cdpAlternative: "Multiple IPC queries to session state"
10773
10981
  },
10774
10982
  live_monitoring: {
10775
- commands: ["peek --follow", "tail"],
10983
+ commands: ["peek --follow"],
10776
10984
  description: "Monitor data collection in real-time",
10777
10985
  cdpAlternative: "CDP event subscriptions with custom handler"
10778
10986
  },
@@ -10879,7 +11087,7 @@ function convertCommand(command) {
10879
11087
  }
10880
11088
  function generateRuntimeState() {
10881
11089
  const sessionActive = readLiveDaemonPid() !== null;
10882
- 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"];
10883
11091
  return {
10884
11092
  sessionActive,
10885
11093
  daemonRunning: sessionActive,
@@ -11241,6 +11449,8 @@ var COMMANDS = {
11241
11449
  dom_form_discover: defineCommand(),
11242
11450
  dom_listeners: defineCommand(),
11243
11451
  dom_layout: defineCommand(),
11452
+ dom_audit: defineCommand(),
11453
+ css_search: defineCommand(),
11244
11454
  dom_inspect: defineCommand(),
11245
11455
  dom_wait: defineCommand()
11246
11456
  };
@@ -11852,7 +12062,7 @@ function verboseCommandsMessage() {
11852
12062
  return [
11853
12063
  "Commands:",
11854
12064
  ` Full preview: ${sessionCommand("bdg peek --last 50")}`,
11855
- ` Watch live: ${sessionCommand("bdg tail")}`,
12065
+ ` Watch live: ${sessionCommand("bdg peek --follow")}`,
11856
12066
  ` End session: ${sessionCommand("bdg stop")}`
11857
12067
  ].join("\n");
11858
12068
  }
@@ -11869,6 +12079,9 @@ function connectionLostRetryMessage(timestamp, retryLabel) {
11869
12079
  function connectionLostStopHintMessage() {
11870
12080
  return "Press Ctrl+C to stop";
11871
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
+ }
11872
12085
 
11873
12086
  // src/commands/shared/daemonErrorHandler.ts
11874
12087
  init_exitCodes();
@@ -13675,9 +13888,17 @@ function registerFormInteractionCommands(program2) {
13675
13888
  const action = options.double ? "double" : options.right ? "right" : "click";
13676
13889
  await runPointerCommand(selectorOrIndex, options, action);
13677
13890
  });
13678
- domCommand.command("hover").description("Move the mouse over an element (shows hover menus and tooltips)").argument("<selectorOrIndex>", SELECTOR_OR_INDEX_ARGUMENT).option("--index <n>", "Element index if selector matches multiple (0-based)", integerOption(0)).option("--strict", STRICT_OPTION_HELP).option("--no-wait", "Skip waiting for network stability after hovering").addOption(jsonOption()).action(async (selectorOrIndex, options) => {
13679
- await runPointerCommand(selectorOrIndex, options, "hover");
13680
- });
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
+ );
13681
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(
13682
13903
  "--wait-network <ms>",
13683
13904
  "Wait for network idle after submit (milliseconds)",
@@ -13795,6 +14016,25 @@ function scrollOptionsProblem(selector, options) {
13795
14016
  if (!selector && vertical + horizontal === 0) return "no-target";
13796
14017
  return null;
13797
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
+ }
13798
14038
  async function runPointerCommand(selectorOrIndex, options, action) {
13799
14039
  await runCommand(
13800
14040
  () => options.double && options.right ? Promise.resolve({
@@ -13837,7 +14077,15 @@ function formatActionOutput(done, details, result, options = {}) {
13837
14077
  fmt.blank();
13838
14078
  fmt.keyValueList(details, keyWidth);
13839
14079
  if (result.navigation) fmt.keyValue("Page", pageNavigationText(result.navigation), keyWidth);
13840
- 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
+ );
13841
14089
  listRows(fmt, "Shown", (result.shown ?? []).map(shownElementText), keyWidth);
13842
14090
  const omitted = result.triggeredRequestsOmitted;
13843
14091
  const requests = formatTriggeredRequestLines(result.triggeredRequests ?? [], omitted);
@@ -14772,19 +15020,19 @@ function registerA11yCommands(domCmd) {
14772
15020
  const a11y = domCmd.command("a11y").description("Accessibility tree inspection and semantic queries").argument(
14773
15021
  "[search]",
14774
15022
  'Quick search: index (describe), CSS selector (#id, .class), pattern with ":" (query), or name search'
14775
- ).enablePositionalOptions().addOption(jsonOption()).action(async (search, options) => {
14776
- if (!search) {
15023
+ ).enablePositionalOptions().addOption(jsonOption()).action(async (search2, options) => {
15024
+ if (!search2) {
14777
15025
  return a11y.help({ error: true });
14778
15026
  }
14779
- const isNumericIndex = /^\d+$/.test(search);
14780
- const isCssSelector = /^[#.[]/u.test(search);
14781
- 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);
14782
15030
  if (isNumericIndex || isCssSelector) {
14783
- await handleA11yDescribe(search, options);
15031
+ await handleA11yDescribe(search2, options);
14784
15032
  } else if (isPatternQuery) {
14785
- await handleA11yQuery(search, options);
15033
+ await handleA11yQuery(search2, options);
14786
15034
  } else {
14787
- await handleA11yQuery(`name:${quoteQueryValue(search)}`, options);
15035
+ await handleA11yQuery(`name:${quoteQueryValue(search2)}`, options);
14788
15036
  }
14789
15037
  });
14790
15038
  a11y.command("tree").description("Dump full accessibility tree (filters ignored nodes)").addOption(jsonOption()).action(async (options) => {
@@ -14888,7 +15136,10 @@ function formatDomScreenshot(data) {
14888
15136
  output += " (viewport only - page too tall)";
14889
15137
  }
14890
15138
  if (data.element?.captured) {
14891
- 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)})`;
14892
15143
  }
14893
15144
  return output;
14894
15145
  }
@@ -15676,11 +15927,11 @@ function inspectHeader(data) {
15676
15927
  data.element,
15677
15928
  data.content !== void 0 && `"${data.content}"`,
15678
15929
  data.placeholder !== void 0 && `placeholder "${data.placeholder}"`,
15679
- 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)" : ""}`,
15680
15931
  kind && `[${kind}]`,
15681
15932
  ...inspectVisibilityBadges(data.visibility),
15682
15933
  data.context && `in ${data.context}`,
15683
- data.theme === "dark" && inspectDarkThemeBadge(),
15934
+ data.theme === "dark" && inspectDarkThemeBadge(data.themeFrom === "emulation"),
15684
15935
  data.animating && inspectAnimatingBadge(data.animating)
15685
15936
  ].filter(Boolean).join(" ");
15686
15937
  }
@@ -15755,6 +16006,7 @@ function parentLine(layout) {
15755
16006
  function fontParts(text) {
15756
16007
  const loaded = [
15757
16008
  text.rendered && `rendered "${text.rendered}"`,
16009
+ text.resolved && `resolves to "${text.resolved}"`,
15758
16010
  text.webfont && "webfont loaded"
15759
16011
  ].filter(Boolean);
15760
16012
  const family = text.family && [text.family, loaded.length > 0 && `(${loaded.join(", ")})`];
@@ -15775,14 +16027,16 @@ function fontParts(text) {
15775
16027
  function contrastText(contrast) {
15776
16028
  if (!contrast) return void 0;
15777
16029
  return [
15778
- `contrast ${contrast.ratio} ${contrast.level}`,
15779
- contrast.inherited && `on ${contrast.background}`,
16030
+ contrast.approximate ? `contrast \u2248${contrast.ratio} ${contrast.level}` : `contrast ${contrast.ratio} ${contrast.level}`,
16031
+ `on ${contrast.background}`,
15780
16032
  contrast.overImage && "(over image)",
15781
- contrast.opacity !== void 0 && `(faded: opacity ${contrast.opacity})`
16033
+ contrast.opacity !== void 0 && `(faded: opacity ${contrast.opacity})`,
16034
+ contrast.approximate && `(approximate: ${contrast.approximate.join(", ")})`
15782
16035
  ].filter(Boolean).join(" ");
15783
16036
  }
15784
16037
  function textLine(text) {
15785
16038
  return groupLine("text", [
16039
+ text.holder && `in ${text.holder}`,
15786
16040
  ...fontParts(text),
15787
16041
  text.color && `color ${text.color}`,
15788
16042
  contrastText(text.contrast),
@@ -15793,17 +16047,25 @@ function textLine(text) {
15793
16047
  text.whiteSpace && `ws ${text.whiteSpace}`,
15794
16048
  text.overflow && `text-overflow ${text.overflow}`,
15795
16049
  text.clamp && `clamp ${text.clamp}`,
16050
+ text.truncated && "truncated",
16051
+ text.gradientFill && "filled by its background (background-clip: text; no contrast)",
15796
16052
  text.shadow && `shadow ${text.shadow}`,
15797
16053
  text.features && `features ${text.features}`
15798
16054
  ]);
15799
16055
  }
15800
16056
  function fillText(fill) {
15801
16057
  if (fill.type === "solid") return `bg ${fill.color}`;
15802
- if (fill.type === "gradient") return `bg-image ${fill.value}`;
15803
- return `bg-image ${fill.value}${fill.size ? ` size ${fill.size}` : ""}`;
16058
+ return [
16059
+ `bg-image ${fill.value}`,
16060
+ fill.size && `size ${fill.size}`,
16061
+ fill.position && `at ${fill.position}`
16062
+ ].filter(Boolean).join(" ");
15804
16063
  }
15805
16064
  function fillLine(data) {
16065
+ const paint = data.paint;
15806
16066
  return groupLine("fill", [
16067
+ paint && `fill ${paint.fill}`,
16068
+ paint && `stroke ${paint.stroke}${paint.strokeWidth !== void 0 ? ` ${paint.strokeWidth}` : ""}`,
15807
16069
  ...(data.fills ?? []).map(fillText),
15808
16070
  data.opacity !== void 0 && `opacity ${data.opacity}`,
15809
16071
  data.blend && `blend ${data.blend}`
@@ -15853,6 +16115,7 @@ function pseudoText(pseudo) {
15853
16115
  pseudo.content !== void 0 && `content ${pseudo.content}`,
15854
16116
  pseudo.display,
15855
16117
  pseudo.position,
16118
+ pseudo.inset && `inset ${pseudo.inset}`,
15856
16119
  pseudo.size && `${pseudo.size.w}x${pseudo.size.h}`,
15857
16120
  pseudo.color && `color ${pseudo.color}`,
15858
16121
  pseudo.fontStyle,
@@ -15874,7 +16137,9 @@ function treeRow(node) {
15874
16137
  return [
15875
16138
  node.element,
15876
16139
  node.count !== void 0 && `\xD7${node.count}`,
15877
- `${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}`,
15878
16143
  node.layout && `[${node.layout}]`,
15879
16144
  node.text && `"${node.text}"`,
15880
16145
  node.childCount !== void 0 && `(${node.childCount})`
@@ -15896,7 +16161,7 @@ function ruleScope(entry) {
15896
16161
  }
15897
16162
  function cascadeBlock(data) {
15898
16163
  const hints = (data.hints ?? []).map(
15899
- (hint) => `${hint.property}: ${hint.value} ${hint.kind === "not-inherited" ? "is the browser's" : "has no effect"}: ${hint.reason} \u2192 ${hint.fix} \xB7 in ${hint.source}`
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}`
15900
16165
  );
15901
16166
  return [
15902
16167
  ...labelledLines("hints", data.hints?.length === 0 ? ["none"] : hints),
@@ -15927,7 +16192,8 @@ function whyLines(why, data) {
15927
16192
  truncateByLength(value, CASCADE_VALUE_WIDTH) + resolved,
15928
16193
  entry.important ? " !important" : "",
15929
16194
  ` ${entry.source}${entry.specificity ? ` [${entry.specificity.join(",")}]` : ""}${ruleScope(entry)}`,
15930
- entry.status === "inherited" ? " (inherited)" : ""
16195
+ entry.status === "inherited" ? " (inherited)" : "",
16196
+ entry.note ? ` (${entry.note})` : ""
15931
16197
  ].join("");
15932
16198
  });
15933
16199
  const variables = (why.variables ?? []).map(
@@ -15936,8 +16202,12 @@ function whyLines(why, data) {
15936
16202
  return [
15937
16203
  `${why.property} = ${why.computed}${midTransition(why.property, data.animating) ? ` ${inspectMidTransitionNote()}` : ""}`,
15938
16204
  ...entries.slice(0, 1),
16205
+ ...why.chain[0]?.rule ? [` in ${truncateByLength(why.chain[0].rule, RULE_LINE_WIDTH)}`] : [],
15939
16206
  ...variables,
15940
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
+ ),
15941
16211
  ...why.chain.length === 0 ? [" no author declaration: the default or inherited value"] : []
15942
16212
  ];
15943
16213
  }
@@ -15949,6 +16219,7 @@ function midTransition(property, animating) {
15949
16219
  function levelsUp(levels) {
15950
16220
  return levels === 1 ? "the parent" : `${levels} levels up`;
15951
16221
  }
16222
+ var RULE_LINE_WIDTH = 120;
15952
16223
  var CASCADE_VALUE_WIDTH = 60;
15953
16224
  function treeBlock(data) {
15954
16225
  if (!data.children && !data.hiddenChildren) return [];
@@ -15993,8 +16264,16 @@ function groupLines(data) {
15993
16264
  function formatInspect(data) {
15994
16265
  const body = data.props ? propLines(data.props) : data.all ? allLines(data.all) : groupLines(data);
15995
16266
  const note = data.picked ? multipleMatchesWarning(data.count, inspectedMatchAction(data.picked, data.index)) : void 0;
15996
- if (data.why) return joinLines(inspectHeader(data), ...cascadeBlock(data), note);
15997
- return joinLines(inspectHeader(data), ...body, ...cascadeBlock(data), ...treeBlock(data), note);
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
+ );
15998
16277
  }
15999
16278
 
16000
16279
  // src/commands/dom/inspect.ts
@@ -16063,6 +16342,125 @@ async function inspectTarget(selectorOrIndex, options) {
16063
16342
  });
16064
16343
  }
16065
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
+
16066
16464
  // src/commands/dom/layout.ts
16067
16465
  init_client();
16068
16466
 
@@ -16382,13 +16780,17 @@ function buildElementScreenshotOptions(options) {
16382
16780
  return filterDefined({
16383
16781
  format: options.format,
16384
16782
  quality: options.quality,
16385
- noResize: options.resize === false
16783
+ noResize: options.resize === false,
16784
+ padding: options.padding
16386
16785
  });
16387
16786
  }
16388
16787
  function hasElementTarget(options) {
16389
16788
  return options.selector !== void 0 || options.index !== void 0;
16390
16789
  }
16391
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
+ }
16392
16794
  if (options.index !== void 0) {
16393
16795
  const resolver = DomElementResolver.getInstance();
16394
16796
  const node = await resolver.getNodeIdForIndex(options.index);
@@ -16523,8 +16925,8 @@ function reportSequenceError(error, captured, json) {
16523
16925
  }
16524
16926
  function assertScreenshotOptions(outputPath, options) {
16525
16927
  let message;
16526
- if (options.selector !== void 0 && options.index !== void 0) {
16527
- 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)";
16528
16930
  } else if (options.quality !== void 0 && !options.follow && resolveImageFormat(outputPath, options.format) === "png") {
16529
16931
  message = "--quality applies to JPEG only; this screenshot is a PNG";
16530
16932
  }
@@ -16574,7 +16976,7 @@ function registerWaitCommand(dom) {
16574
16976
  "Wait until elements appear, become visible, contain a text or are gone (or the page loads)"
16575
16977
  ).argument(
16576
16978
  "[selector]",
16577
- "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)"
16578
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(
16579
16981
  "--timeout <ms>",
16580
16982
  "Give up after this many milliseconds (exit 102)",
@@ -16585,7 +16987,8 @@ function registerWaitCommand(dom) {
16585
16987
  });
16586
16988
  }
16587
16989
  async function waitFor(selector, options) {
16588
- 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;
16589
16992
  if (selector === void 0 && needsSelector) {
16590
16993
  const err = waitTargetRequiredError();
16591
16994
  return {
@@ -16599,7 +17002,7 @@ async function waitFor(selector, options) {
16599
17002
  ...filterDefined({ selector, text: options.text }),
16600
17003
  ...options.gone && { gone: true },
16601
17004
  ...options.visible && { visible: true },
16602
- ...options.load && { load: true },
17005
+ ...load && { load: true },
16603
17006
  timeout: options.timeout
16604
17007
  });
16605
17008
  if (response.status === "error" || !response.data) {
@@ -16623,6 +17026,7 @@ function registerDomCommands(program2) {
16623
17026
  registerFormCommand(dom);
16624
17027
  registerListenersCommand(dom);
16625
17028
  registerLayoutCommand(dom);
17029
+ registerAuditCommand(dom);
16626
17030
  registerInspectCommand(dom);
16627
17031
  registerWaitCommand(dom);
16628
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) => {
@@ -16657,8 +17061,12 @@ function registerDomCommands(program2) {
16657
17061
  "Element to capture: CSS selector or index from a query (same as --selector / --index)"
16658
17062
  ).option("--selector <selector>", "CSS selector for element capture").option(
16659
17063
  "--index <number>",
16660
- "Cached element index (0-based) from previous query",
17064
+ "Cached element index (0-based) from a previous query; with --selector, which match",
16661
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)
16662
17070
  ).option(
16663
17071
  "--format <format>",
16664
17072
  "Image format: png or jpeg/jpg (default: from the file extension, else png)",
@@ -16670,6 +17078,87 @@ function registerDomCommands(program2) {
16670
17078
  );
16671
17079
  }
16672
17080
 
17081
+ // src/commands/installSkill.ts
17082
+ import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "fs";
17083
+ import { homedir as homedir3 } from "os";
17084
+ import { dirname as dirname5, join as join6 } from "path";
17085
+ init_errors3();
17086
+ init_messages();
17087
+
17088
+ // src/ui/formatters/installSkill.ts
17089
+ init_formatting();
17090
+ import { homedir as homedir2 } from "os";
17091
+ function tildePath(path6) {
17092
+ const home = homedir2();
17093
+ return path6.startsWith(`${home}/`) ? `~${path6.slice(home.length)}` : path6;
17094
+ }
17095
+ function formatInstalledSkills(data) {
17096
+ const fmt = new OutputFormatter().text("bdg skill:");
17097
+ for (const skill of data.skills) {
17098
+ fmt.text(` ${skill.target.padEnd(6)} ${skill.status.padEnd(9)} ${tildePath(skill.path)}`);
17099
+ }
17100
+ return fmt.hints("Next:", [
17101
+ "Start a new agent session to load it (running sessions keep the old list)",
17102
+ "After upgrading bdg, run bdg install-skill again"
17103
+ ]).build();
17104
+ }
17105
+
17106
+ // src/commands/installSkill.ts
17107
+ init_errors();
17108
+ init_exitCodes();
17109
+ var SKILL_SOURCE_PATH = join6(PACKAGE_ROOT, ".claude", "skills", "bdg", "SKILL.md");
17110
+ var SKILL_ROOTS = {
17111
+ claude: join6(".claude", "skills"),
17112
+ agents: join6(".agents", "skills")
17113
+ };
17114
+ function installSkill(targets, home = homedir3(), source = SKILL_SOURCE_PATH) {
17115
+ if (!existsSync10(source)) {
17116
+ const err = skillSourceMissingError(source);
17117
+ throw new CommandError(
17118
+ err.message,
17119
+ { suggestion: err.suggestion },
17120
+ EXIT_CODES.RESOURCE_NOT_FOUND
17121
+ );
17122
+ }
17123
+ const content = readFileSync8(source, "utf-8");
17124
+ return targets.map(
17125
+ (target) => writeSkill(target, join6(home, SKILL_ROOTS[target], "bdg", "SKILL.md"), content)
17126
+ );
17127
+ }
17128
+ function writeSkill(target, path6, content) {
17129
+ const existing = existsSync10(path6) ? readFileSync8(path6, "utf-8") : void 0;
17130
+ if (existing === content) {
17131
+ return { target, path: path6, status: "unchanged" };
17132
+ }
17133
+ try {
17134
+ mkdirSync3(dirname5(path6), { recursive: true });
17135
+ writeFileSync2(path6, content);
17136
+ } catch (caught) {
17137
+ const err = skillWriteFailedError(path6, getErrorMessage(caught));
17138
+ throw new CommandError(
17139
+ err.message,
17140
+ { suggestion: err.suggestion },
17141
+ EXIT_CODES.PERMISSION_DENIED
17142
+ );
17143
+ }
17144
+ return { target, path: path6, status: existing === void 0 ? "installed" : "updated" };
17145
+ }
17146
+ function selectedTargets(options) {
17147
+ const picked = Object.keys(SKILL_ROOTS).filter((target) => options[target]);
17148
+ return picked.length > 0 ? picked : Object.keys(SKILL_ROOTS);
17149
+ }
17150
+ function registerInstallSkillCommand(program2) {
17151
+ program2.command("install-skill").description(
17152
+ "Install the bdg agent skill for Claude Code (~/.claude/skills) and agents reading ~/.agents/skills (Codex, Gemini CLI, ...); re-run after upgrading bdg"
17153
+ ).option("--claude", "Only ~/.claude/skills (Claude Code)").option("--agents", "Only ~/.agents/skills (Codex, Gemini CLI and other agents)").addOption(jsonOption()).action(async (options) => {
17154
+ await runCommand(
17155
+ (opts) => Promise.resolve({ success: true, data: { skills: installSkill(selectedTargets(opts)) } }),
17156
+ options,
17157
+ formatInstalledSkills
17158
+ );
17159
+ });
17160
+ }
17161
+
16673
17162
  // src/commands/network/har.ts
16674
17163
  import * as fs13 from "fs";
16675
17164
  import * as path5 from "path";
@@ -17434,7 +17923,7 @@ function appearanceLines(metadata, pageState) {
17434
17923
  ...viewport ? [
17435
17924
  [
17436
17925
  "Viewport",
17437
- `${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" : ""})` : ""}`
17438
17927
  ]
17439
17928
  ] : [],
17440
17929
  ...scheme ? [
@@ -18256,6 +18745,37 @@ function registerNetworkCommands(program2) {
18256
18745
  registerDocumentCommand(networkCmd);
18257
18746
  }
18258
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
+
18259
18779
  // src/commands/start.ts
18260
18780
  import fs15 from "node:fs";
18261
18781
  import os3 from "node:os";
@@ -18300,7 +18820,7 @@ var SessionDirError = class extends DaemonError {
18300
18820
  // src/daemon/launcher.ts
18301
18821
  import { spawn } from "child_process";
18302
18822
  import fs14 from "fs";
18303
- import { join as join7 } from "path";
18823
+ import { join as join8 } from "path";
18304
18824
  init_messages();
18305
18825
  init_paths();
18306
18826
  init_logging();
@@ -18321,7 +18841,7 @@ async function launchDaemon() {
18321
18841
  );
18322
18842
  }
18323
18843
  assertUsableSessionDir();
18324
- const logPath = join7(getSessionDir(), "daemon.log");
18844
+ const logPath = join8(getSessionDir(), "daemon.log");
18325
18845
  rotateLog(logPath);
18326
18846
  const logFd = fs14.openSync(logPath, "a");
18327
18847
  log24.debug(`Starting daemon: ${DAEMON_SCRIPT_PATH}`);
@@ -18382,7 +18902,7 @@ async function waitForDaemonReady(hasExited) {
18382
18902
  }
18383
18903
  if (hasExited() && !await isDaemonAlive()) {
18384
18904
  throw new DaemonStartupError(
18385
- `Daemon exited during startup. See ${join7(getSessionDir(), "daemon.log")}`,
18905
+ `Daemon exited during startup. See ${join8(getSessionDir(), "daemon.log")}`,
18386
18906
  "DAEMON_EXITED"
18387
18907
  );
18388
18908
  }
@@ -18729,8 +19249,17 @@ function applyCollectorOptions(command) {
18729
19249
  ).option(
18730
19250
  "--color-scheme <scheme>",
18731
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`
18732
19255
  );
18733
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
+ }
18734
19263
  var MAX_VIEWPORT_SIDE = 1e4;
18735
19264
  var COLOR_SCHEMES = ["light", "dark"];
18736
19265
  function parseViewport(value) {
@@ -18778,7 +19307,7 @@ function buildSessionOptions(options) {
18778
19307
  quiet: options.quiet ?? false,
18779
19308
  json: options.json ?? false,
18780
19309
  chromeFlags,
18781
- viewport: options.viewport !== void 0 ? parseViewport(options.viewport) : void 0,
19310
+ viewport: requestedViewport(options.viewport, options.mobile),
18782
19311
  colorScheme: options.colorScheme !== void 0 ? parseColorScheme(options.colorScheme) : void 0
18783
19312
  };
18784
19313
  }
@@ -19030,7 +19559,7 @@ async function showPageInfo(options) {
19030
19559
  }
19031
19560
  function emulationRequest(options) {
19032
19561
  if (options.reset) return { reset: true };
19033
- if (options.viewport === void 0 && options.colorScheme === void 0) {
19562
+ if (options.viewport === void 0 && options.colorScheme === void 0 && !options.mobile) {
19034
19563
  const err = pageEmulateNothingError();
19035
19564
  throw new CommandError(
19036
19565
  err.message,
@@ -19038,8 +19567,9 @@ function emulationRequest(options) {
19038
19567
  EXIT_CODES.INVALID_ARGUMENTS
19039
19568
  );
19040
19569
  }
19570
+ const viewport = requestedViewport(options.viewport, options.mobile);
19041
19571
  return {
19042
- ...options.viewport !== void 0 && { viewport: parseViewport(options.viewport) },
19572
+ ...viewport && { viewport },
19043
19573
  ...options.colorScheme !== void 0 && {
19044
19574
  colorScheme: parseColorScheme(options.colorScheme)
19045
19575
  }
@@ -19076,10 +19606,17 @@ function registerPageCommands(program2) {
19076
19606
  ).action(async (url, options) => {
19077
19607
  await runPageAction("navigate", options, url);
19078
19608
  });
19079
- page.command("emulate").description(PAGE_EMULATE_DESCRIPTION).option("--viewport <WxH>", "Viewport size in CSS px, e.g. 900x700").option("--color-scheme <scheme>", "Emulate prefers-color-scheme: light or dark").addOption(
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(
19080
19616
  new Option("--reset", "Back to the browser window size and the system setting").conflicts([
19081
19617
  "viewport",
19082
- "colorScheme"
19618
+ "colorScheme",
19619
+ "mobile"
19083
19620
  ])
19084
19621
  ).addOption(jsonOption()).action(async (options) => {
19085
19622
  await emulate(options);
@@ -19094,6 +19631,9 @@ function registerPageCommands(program2) {
19094
19631
  }
19095
19632
 
19096
19633
  // src/commands/peek.ts
19634
+ init_errors3();
19635
+ init_messages();
19636
+ init_exitCodes();
19097
19637
  function parseOptions(options) {
19098
19638
  const lastN = positiveIntRule({
19099
19639
  name: "--last",
@@ -19103,7 +19643,21 @@ function parseOptions(options) {
19103
19643
  allowZeroForAll: true
19104
19644
  }).validate(options.last);
19105
19645
  const resourceTypes = resourceTypeRule().validate(options.type);
19106
- 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 };
19107
19661
  }
19108
19662
  function peekSection(options) {
19109
19663
  if (options.network) return "network";
@@ -19137,11 +19691,12 @@ function createPreviewOptions(base, resourceTypes, unfilteredCount) {
19137
19691
  }
19138
19692
  return options;
19139
19693
  }
19140
- async function runFollowMode3(options, lastN, resourceTypes, baseOptions) {
19694
+ async function runFollowMode3(options, parsed, baseOptions) {
19695
+ const { lastN, resourceTypes, interval } = parsed;
19141
19696
  const showPreview = async () => {
19142
19697
  const result = await fetchAndFilterPreview(lastN, resourceTypes, peekSection(options));
19143
19698
  if (!result.success) {
19144
- return followFetchFailure(result, { json: options.json, retryIntervalMs: 1e3 });
19699
+ return followFetchFailure(result, { json: options.json, retryIntervalMs: interval });
19145
19700
  }
19146
19701
  noteFollowConnected();
19147
19702
  if (!options.json) console.clear();
@@ -19156,11 +19711,34 @@ async function runFollowMode3(options, lastN, resourceTypes, baseOptions) {
19156
19711
  await setupFollowMode(showPreview, {
19157
19712
  startMessage: followingPreviewMessage,
19158
19713
  stopMessage: stoppedFollowingPreviewMessage,
19159
- intervalMs: 1e3
19714
+ intervalMs: interval
19160
19715
  });
19161
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
+ }
19162
19740
  function registerPeekCommand(program2) {
19163
- program2.command("peek").description("Preview collected data without stopping the session").addOption(jsonOption()).option("-v, --verbose", "Use verbose output with full URLs and formatting", false).option("-n, --network", "Show only network requests", false).option("-c, --console", "Show only console messages", false).option("-f, --follow", "Watch for updates (like tail -f)", false).option("--last <count>", "Show last N items, 0 for all", "10").option(
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(
19164
19742
  "--type <types>",
19165
19743
  "Filter network requests by resource type (comma-separated: Document,XHR,Fetch,etc.)"
19166
19744
  ).action(async (options) => {
@@ -19170,25 +19748,11 @@ function registerPeekCommand(program2) {
19170
19748
  'Note: "bdg peek --network" is deprecated. Use "bdg network list" for enhanced filtering.'
19171
19749
  );
19172
19750
  }
19173
- let lastN;
19174
- let resourceTypes;
19175
- try {
19176
- const parsed = parseOptions(options);
19177
- lastN = parsed.lastN;
19178
- resourceTypes = parsed.resourceTypes;
19179
- } catch (error) {
19180
- handleValidationError(error, options.json ?? false);
19181
- }
19182
- const baseOptions = {
19183
- json: options.json,
19184
- network: options.network,
19185
- console: options.console,
19186
- last: lastN,
19187
- verbose: options.verbose,
19188
- follow: options.follow
19189
- };
19751
+ const parsed = parsePeekOptions(options);
19752
+ const { lastN, resourceTypes } = parsed;
19753
+ const baseOptions = previewDisplayOptions(options, lastN);
19190
19754
  if (options.follow) {
19191
- await runFollowMode3(options, lastN, resourceTypes, baseOptions);
19755
+ await runFollowMode3(options, parsed, baseOptions);
19192
19756
  return;
19193
19757
  }
19194
19758
  await runCommand(
@@ -19657,62 +20221,14 @@ function registerStopCommand(program2) {
19657
20221
  }
19658
20222
 
19659
20223
  // src/commands/tail.ts
19660
- function parseOptions2(options) {
19661
- const lastRule = positiveIntRule({
19662
- name: "--last",
19663
- min: 1,
19664
- max: MAX_LAST_ITEMS,
19665
- default: 10,
19666
- allowZeroForAll: true
19667
- });
19668
- const intervalRule = positiveIntRule({ name: "--interval", min: 100, max: 6e4, default: 1e3 });
19669
- return {
19670
- lastN: lastRule.validate(options.last),
19671
- interval: intervalRule.validate(options.interval)
19672
- };
19673
- }
19674
- function createPreviewOptions2(options, lastN) {
19675
- return {
19676
- json: options.json,
19677
- network: options.network,
19678
- console: options.console,
19679
- last: lastN,
19680
- verbose: options.verbose,
19681
- follow: true,
19682
- viewedAt: /* @__PURE__ */ new Date()
19683
- };
19684
- }
19685
20224
  function registerTailCommand(program2) {
19686
- 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(
19687
20226
  "--last <count>",
19688
20227
  "Show last N items (network requests + console messages), 0 for all",
19689
20228
  "10"
19690
20229
  ).option("--interval <ms>", "Update interval in milliseconds", "1000").action(async (options) => {
19691
- showBothSectionsWhenBothRequested(options);
19692
- let lastN;
19693
- let interval;
19694
- try {
19695
- const parsed = parseOptions2(options);
19696
- lastN = parsed.lastN;
19697
- interval = parsed.interval;
19698
- } catch (error) {
19699
- handleValidationError(error, options.json ?? false);
19700
- }
19701
- const showPreview = async () => {
19702
- const result = await fetchPreviewOutput({ lastN });
19703
- if (!result.success) {
19704
- return followFetchFailure(result, { json: options.json, retryIntervalMs: interval });
19705
- }
19706
- noteFollowConnected();
19707
- if (!options.json) console.clear();
19708
- console.log(formatPreview(result.data, createPreviewOptions2(options, lastN)));
19709
- return void 0;
19710
- };
19711
- await setupFollowMode(showPreview, {
19712
- startMessage: followingPreviewMessage,
19713
- stopMessage: stoppedFollowingPreviewMessage,
19714
- intervalMs: interval
19715
- });
20230
+ console.error(tailDeprecatedNotice());
20231
+ await followPreview({ ...options, follow: true });
19716
20232
  });
19717
20233
  }
19718
20234
 
@@ -19736,12 +20252,15 @@ var commandRegistry = [
19736
20252
  registerDomCommands,
19737
20253
  registerFormInteractionCommands,
19738
20254
  registerPageCommands,
20255
+ registerCssCommands,
19739
20256
  addCommandGroup("CDP Commands:"),
19740
20257
  registerCdpCommand,
19741
20258
  addCommandGroup("Network Commands:"),
19742
20259
  registerNetworkCommands,
19743
20260
  addCommandGroup("Console Commands:"),
19744
- registerConsoleCommand
20261
+ registerConsoleCommand,
20262
+ addCommandGroup("Agent Setup:"),
20263
+ registerInstallSkillCommand
19745
20264
  ];
19746
20265
 
19747
20266
  // src/index.ts