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