browser-debugger-cli 0.15.0 → 0.16.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/.claude/skills/bdg/SKILL.md +2 -1
- package/dist/cdp/methodTarget.d.ts +92 -0
- package/dist/cdp/methodTarget.js +159 -0
- package/dist/cdp/protocol.d.ts +16 -1
- package/dist/cdp/protocol.js +21 -0
- package/dist/cdp/schema.d.ts +55 -1
- package/dist/cdp/schema.js +134 -25
- package/dist/cdp/types.d.ts +3 -1
- package/dist/commands/cdp.d.ts +38 -1
- package/dist/commands/cdp.js +200 -133
- package/dist/commands/cleanup.js +18 -4
- package/dist/commands/dom/formInteraction.js +8 -4
- package/dist/commands/dom/helpers/index.d.ts +4 -4
- package/dist/commands/dom/helpers/index.js +3 -3
- package/dist/commands/dom/helpers/query.d.ts +2 -2
- package/dist/commands/dom/helpers/query.js +2 -2
- package/dist/commands/dom/helpers/screenshot.d.ts +21 -26
- package/dist/commands/dom/helpers/screenshot.js +50 -668
- package/dist/commands/dom/screenshot.js +56 -36
- package/dist/commands/optionBehaviors.js +18 -8
- package/dist/commands/shared/CommandRunner.d.ts +5 -0
- package/dist/commands/shared/CommandRunner.js +18 -3
- package/dist/commands/shared/interrupt.d.ts +40 -0
- package/dist/commands/shared/interrupt.js +73 -0
- package/dist/commands/shared/optionTypes.d.ts +2 -0
- package/dist/commands/shared/startHelpers.d.ts +26 -3
- package/dist/commands/shared/startHelpers.js +145 -23
- package/dist/commands/types.d.ts +5 -0
- package/dist/connection/cdp.js +1 -16
- package/dist/connection/chromeIdentity.d.ts +24 -5
- package/dist/connection/chromeIdentity.js +53 -22
- package/dist/connection/launcher.d.ts +34 -1
- package/dist/connection/launcher.js +98 -10
- package/dist/connection/typed-cdp.d.ts +3 -2
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/daemon/SessionController.d.ts +10 -5
- package/dist/daemon/SessionController.js +15 -8
- package/dist/daemon/ipcServer.js +1 -1
- package/dist/daemon/launcher.d.ts +5 -0
- package/dist/daemon/launcher.js +8 -1
- package/dist/daemon/session/Session.d.ts +5 -1
- package/dist/daemon/session/Session.js +9 -8
- package/dist/daemon/session/TelemetryStore.d.ts +5 -0
- package/dist/daemon/session/TelemetryStore.js +4 -0
- package/dist/daemon/session/captureGate.d.ts +59 -0
- package/dist/daemon/session/captureGate.js +96 -0
- package/dist/daemon/session/chromeConnection.d.ts +16 -1
- package/dist/daemon/session/chromeConnection.js +34 -4
- package/dist/daemon/session/collectors.d.ts +15 -0
- package/dist/daemon/session/collectors.js +39 -2
- package/dist/daemon/session/commandRegistry.d.ts +14 -1
- package/dist/daemon/session/commandRegistry.js +46 -11
- package/dist/daemon/session/downloads.d.ts +32 -0
- package/dist/daemon/session/downloads.js +96 -0
- package/dist/daemon/session/interactions.d.ts +3 -2
- package/dist/daemon/session/interactions.js +7 -2
- package/dist/daemon/session/plugins.js +6 -0
- package/dist/daemon.js +12843 -11482
- package/dist/errors/CommandError.d.ts +2 -0
- package/dist/errors/issues.d.ts +1 -1
- package/dist/errors/messages.d.ts +58 -0
- package/dist/errors/messages.js +112 -0
- package/dist/index.js +999 -1020
- package/dist/ipc/client.d.ts +14 -1
- package/dist/ipc/client.js +21 -4
- package/dist/ipc/protocol/commands.d.ts +32 -2
- package/dist/ipc/protocol/commands.js +1 -0
- package/dist/ipc/protocol/domTypes.d.ts +24 -1
- package/dist/ipc/session/queries.d.ts +3 -0
- package/dist/ipc/session/types.d.ts +5 -0
- package/dist/ipc/transport/IPCError.d.ts +9 -0
- package/dist/ipc/transport/IPCError.js +12 -0
- package/dist/ipc/transport/errors.d.ts +2 -1
- package/dist/ipc/transport/errors.js +4 -1
- package/dist/ipc/transport/index.d.ts +4 -2
- package/dist/ipc/transport/index.js +13 -3
- package/dist/runtime/dom/actionEffects.d.ts +48 -9
- package/dist/runtime/dom/actionEffects.js +269 -34
- package/dist/runtime/dom/actionEffectsScripts.d.ts +45 -0
- package/dist/runtime/dom/actionEffectsScripts.js +101 -2
- package/dist/runtime/dom/captureArea.d.ts +35 -0
- package/dist/runtime/dom/captureArea.js +203 -0
- package/dist/runtime/dom/elementInfo.d.ts +10 -8
- package/dist/runtime/dom/elementInfo.js +8 -6
- package/dist/runtime/dom/formDiscovery.d.ts +1 -1
- package/dist/runtime/page/bdgWorld.d.ts +9 -0
- package/dist/runtime/page/bdgWorld.js +11 -0
- package/dist/runtime/page/captureEmulation.d.ts +119 -0
- package/dist/runtime/page/captureEmulation.js +189 -0
- package/dist/runtime/page/captureScroll.d.ts +24 -0
- package/dist/runtime/page/captureScroll.js +124 -0
- package/dist/runtime/page/screenshot.d.ts +41 -0
- package/dist/runtime/page/screenshot.js +394 -0
- package/dist/session/paths.d.ts +14 -0
- package/dist/session/paths.js +25 -0
- package/dist/telemetry/downloads.d.ts +127 -0
- package/dist/telemetry/downloads.js +265 -0
- package/dist/telemetry/har/builder.js +22 -7
- package/dist/telemetry/har/sanitize.d.ts +7 -3
- package/dist/telemetry/har/sanitize.js +52 -6
- package/dist/telemetry/har/sanitizeBody.d.ts +47 -7
- package/dist/telemetry/har/sanitizeBody.js +429 -56
- package/dist/telemetry/har/types.d.ts +2 -0
- package/dist/telemetry/network.d.ts +4 -4
- package/dist/telemetry/network.js +38 -4
- package/dist/telemetry/networkRetention.d.ts +35 -14
- package/dist/telemetry/networkRetention.js +62 -26
- package/dist/types.d.ts +9 -14
- package/dist/ui/OutputBuilder.d.ts +3 -2
- package/dist/ui/OutputBuilder.js +4 -3
- package/dist/ui/formatters/cdp.d.ts +32 -9
- package/dist/ui/formatters/cdp.js +77 -6
- package/dist/ui/formatters/details.js +7 -15
- package/dist/ui/formatters/preview.d.ts +2 -0
- package/dist/ui/formatters/preview.js +7 -1
- package/dist/ui/formatters/status.js +6 -1
- package/dist/ui/formatting.d.ts +7 -0
- package/dist/ui/formatting.js +13 -0
- package/dist/ui/logging/logger.d.ts +1 -1
- package/dist/ui/messages/chrome.d.ts +13 -0
- package/dist/ui/messages/chrome.js +26 -0
- package/dist/ui/messages/commands.d.ts +71 -3
- package/dist/ui/messages/commands.js +98 -3
- package/dist/ui/messages/networkMessages.d.ts +24 -5
- package/dist/ui/messages/networkMessages.js +31 -8
- package/dist/utils/async.d.ts +3 -2
- package/dist/utils/async.js +16 -3
- package/dist/utils/http.d.ts +11 -4
- package/dist/utils/http.js +5 -3
- package/package.json +18 -4
- /package/dist/{commands/dom → runtime/page}/screenshotResize.d.ts +0 -0
- /package/dist/{commands/dom → runtime/page}/screenshotResize.js +0 -0
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { extname } from 'path';
|
|
5
5
|
import { DomElementResolver } from './DomElementResolver.js';
|
|
6
|
-
import {
|
|
6
|
+
import { captureScreenshot, resolveSelector, screenshotInterrupted, selectMatch, } from './helpers/index.js';
|
|
7
7
|
import { runCommand } from '../shared/CommandRunner.js';
|
|
8
|
+
import { abortOnInterrupt, unlessInterrupted } from '../shared/interrupt.js';
|
|
8
9
|
import { assertFilePath, outputPathError } from '../shared/outputFile.js';
|
|
9
10
|
import { positiveIntRule } from '../shared/validation.js';
|
|
10
11
|
import { CommandError } from '../../errors/index.js';
|
|
@@ -47,34 +48,43 @@ export function resolveImageFormat(outputPath, requested) {
|
|
|
47
48
|
}
|
|
48
49
|
return requested ?? fromExtension ?? 'png';
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
51
|
+
/**
|
|
52
|
+
* What to capture, from the command's options: the element `backendNodeId`
|
|
53
|
+
* names (with its `--padding`), else the page (`--full-page`, `--scroll`).
|
|
54
|
+
*
|
|
55
|
+
* @param options - Command options
|
|
56
|
+
* @param backendNodeId - Element to capture, if any
|
|
57
|
+
* @returns Daemon request
|
|
58
|
+
*/
|
|
59
|
+
function buildScreenshotRequest(options, backendNodeId) {
|
|
60
|
+
const shared = {
|
|
61
|
+
format: options.format ?? 'png',
|
|
62
|
+
...filterDefined({ quality: options.quality }),
|
|
63
|
+
...(options.resize === false && { noResize: true }),
|
|
64
|
+
};
|
|
65
|
+
if (backendNodeId !== undefined) {
|
|
66
|
+
return { ...shared, backendNodeId, ...filterDefined({ padding: options.padding }) };
|
|
67
|
+
}
|
|
68
|
+
return { ...shared, ...filterDefined({ fullPage: options.fullPage, scroll: options.scroll }) };
|
|
66
69
|
}
|
|
67
70
|
function hasElementTarget(options) {
|
|
68
71
|
return options.selector !== undefined || options.index !== undefined;
|
|
69
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* The element to capture: the selector's match (`--index` picks one), or the
|
|
75
|
+
* cached query result at `--index`.
|
|
76
|
+
*
|
|
77
|
+
* @param options - Command options
|
|
78
|
+
* @returns Its backend node id
|
|
79
|
+
* @throws CommandError (81) when neither is given
|
|
80
|
+
*/
|
|
70
81
|
async function resolveElementNodeId(options) {
|
|
71
82
|
if (options.selector !== undefined && options.index !== undefined) {
|
|
72
|
-
return
|
|
83
|
+
return selectMatch(options.selector, options.index);
|
|
73
84
|
}
|
|
74
85
|
if (options.index !== undefined) {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
return { backendNodeId: node.nodeId };
|
|
86
|
+
const node = await DomElementResolver.getInstance().getNodeIdForIndex(options.index);
|
|
87
|
+
return node.nodeId;
|
|
78
88
|
}
|
|
79
89
|
if (options.selector !== undefined) {
|
|
80
90
|
return resolveSelector(options.selector);
|
|
@@ -120,32 +130,42 @@ function ensureDirectory(dirPath, fs) {
|
|
|
120
130
|
function formatFrameFilename(frameNumber, format) {
|
|
121
131
|
return `${String(frameNumber).padStart(3, '0')}.${format}`;
|
|
122
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Capture the page. Ctrl-C (or SIGTERM) cancels the capture and exits 130
|
|
135
|
+
* (143), with the error envelope under `--json`.
|
|
136
|
+
*
|
|
137
|
+
* @param outputPath - File to write
|
|
138
|
+
* @param options - Command options
|
|
139
|
+
*/
|
|
123
140
|
async function handlePageScreenshot(outputPath, options) {
|
|
141
|
+
const interrupt = abortOnInterrupt();
|
|
124
142
|
await runCommand(async () => {
|
|
125
|
-
const
|
|
126
|
-
const result = await
|
|
143
|
+
const request = buildScreenshotRequest(options);
|
|
144
|
+
const result = await captureScreenshot(outputPath, request, interrupt);
|
|
127
145
|
return { success: true, data: result };
|
|
128
146
|
}, options, formatDomScreenshot);
|
|
129
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Capture one element; interrupted like {@link handlePageScreenshot}, also
|
|
150
|
+
* while the element is looked up.
|
|
151
|
+
*
|
|
152
|
+
* @param outputPath - File to write
|
|
153
|
+
* @param options - Command options
|
|
154
|
+
*/
|
|
130
155
|
async function handleElementScreenshot(outputPath, options) {
|
|
156
|
+
const interrupt = abortOnInterrupt();
|
|
131
157
|
await runCommand(async () => {
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const
|
|
158
|
+
const lookup = resolveElementNodeId(options);
|
|
159
|
+
const backendNodeId = await unlessInterrupted(lookup, interrupt, screenshotInterrupted);
|
|
160
|
+
const request = buildScreenshotRequest(options, backendNodeId);
|
|
161
|
+
const shot = await captureScreenshot(outputPath, request, interrupt);
|
|
162
|
+
const elementResult = addElementInfo(shot, options);
|
|
136
163
|
return { success: true, data: elementResult };
|
|
137
164
|
}, options, formatDomScreenshot);
|
|
138
165
|
}
|
|
139
166
|
async function captureSequenceFrame(outputPath, options) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const elementOptions = buildElementScreenshotOptions(options);
|
|
143
|
-
await captureElementScreenshot(outputPath, nodeRef, elementOptions);
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
const pageOptions = buildPageScreenshotOptions(options);
|
|
147
|
-
await capturePageScreenshot(outputPath, pageOptions);
|
|
148
|
-
}
|
|
167
|
+
const backendNodeId = hasElementTarget(options) ? await resolveElementNodeId(options) : undefined;
|
|
168
|
+
await captureScreenshot(outputPath, buildScreenshotRequest(options, backendNodeId));
|
|
149
169
|
}
|
|
150
170
|
async function handleSequenceCapture(outputDir, options) {
|
|
151
171
|
const fs = await import('fs');
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @see docs/principles/SELF_DOCUMENTING_SYSTEMS.md
|
|
8
8
|
*/
|
|
9
|
-
import { MAX_EDGE_PX, PIXELS_PER_TOKEN, TALL_PAGE_THRESHOLD, } from '
|
|
9
|
+
import { MAX_EDGE_PX, PIXELS_PER_TOKEN, TALL_PAGE_THRESHOLD, } from '../runtime/page/screenshotResize.js';
|
|
10
10
|
/** What DOM actions report about the network requests they triggered */
|
|
11
11
|
const 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';
|
|
12
12
|
/** What every DOM action reports about the page besides its requests */
|
|
13
|
-
const 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: "…" (element); JSON messages [{ text, element }], at most 3, with "(+N more)" and moreMessages for the rest; after a navigation every message on the new page counts; texts of only digits and time units, such as clocks and counters, are left out, but other text that changes on its own, such as a rotating banner, can show up). Both are absent when nothing changed. Cost: one page script sent before the action without waiting for it and one read after it, a few ms; when the page does not answer (a pending navigation) bdg waits at most 200 ms for the snapshot and 250 ms per read, and the navigation is still reported from CDP events';
|
|
13
|
+
const 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: "…" (element); JSON messages [{ text, element }], at most 3, with "(+N more)" and moreMessages for the rest; after a navigation every message on the new page counts; texts of only digits and time units, such as clocks and counters, are left out, but other text that changes on its own, such as a rotating banner, can show up). Both are absent when nothing changed. Downloads that began meanwhile are listed (Download: <name> → <path> (<state>, <size>); JSON downloads [{ url, suggestedFilename, path, state: inProgress|completed|canceled, bytes }]); a Chrome bdg launched saves them into <session dir>/downloads (never ~/Downloads), named as suggested with (1), (2)… when taken, and one still running when the action returns is inProgress and appears at its path once complete; downloads of tabs the page opens (target=_blank, window.open) count too; when that directory cannot be created they are refused (canceled, with reason) instead of going to ~/Downloads, and if Chrome refuses the download behavior bdg status warns that downloads are not redirected; ones that begin after the action returned are listed by bdg status and bdg peek (Downloads: N (last: …)); files stay after stop and cleanup; with --chrome-ws-url (the exception: not a browser bdg owns) downloads go to the download folder of that Chrome (usually ~/Downloads), only downloads of the session page are tracked (not those of tabs or popups it opens), and bdg sets behavior default with events, which replaces one another CDP client set and which Chrome drops when the session connection closes. 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';
|
|
14
14
|
/** What click and pressKey report when the page was still changing as they returned */
|
|
15
|
-
const 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
|
|
15
|
+
const 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 came in bursts and, at a second look 250 ms later, again, with no quiet gap over 150 ms; time the page could not run its tasks, a long task or timers a starved renderer runs late, is not quiet, and a single burst more than 150 ms old with at most 75 ms of quiet time since also gets the second look (only that 250 ms; domChanging still needs a fresh change at it); a single render, ticking text and style animations do not count, and changes more than about 150 ms apart while the page could run look settled), busy (the page did not answer within 250 ms and ran a long task since the action began, or did not answer a check within 250 ms more: a long script; a page that answered late without a long task, a starved renderer, gets its answer waited for instead; a renderer descheduled in the middle of a task can still record a long task and is then busy; the check runs at most once per action, adding at most 250 ms, so an endless script is reported busy about 500 ms after the action) }; 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';
|
|
16
16
|
/** What hover and pressKey report about elements they showed */
|
|
17
17
|
const 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';
|
|
18
18
|
/** What `--no-wait` does to a DOM action's triggered requests */
|
|
@@ -331,9 +331,9 @@ const OPTION_BEHAVIORS = {
|
|
|
331
331
|
automaticBehavior: FOLLOW_BEHAVIOR,
|
|
332
332
|
},
|
|
333
333
|
'har:--include-sensitive': {
|
|
334
|
-
default: 'The HAR is sanitized: values become "[redacted]" for Authorization, Proxy-Authorization, Authentication, Cookie and Set-Cookie headers, X-*key/token/secret/auth headers and headers with an api-key/apikey/token/secret/jwt/subscription-key/session(-id) segment (www-authenticate is kept); every cookie value; query and fragment parameters named like credentials (plus code, sig, key) in the request URL, queryString, redirectURL and Location/Referer headers ("%5Bredacted%5D" in URLs); and password/token/secret/key/session/signature fields of JSON (primitives at any depth under such a name), form-urlencoded (also sniffed when the Content-Type says otherwise) and multipart request bodies.
|
|
334
|
+
default: 'The HAR is sanitized: values become "[redacted]" for Authorization, Proxy-Authorization, Authentication, Cookie and Set-Cookie headers, X-*key/token/secret/auth headers and headers with an api-key/apikey/token/secret/jwt/subscription-key/session(-id) segment (www-authenticate is kept); every cookie value; query and fragment parameters named like credentials (plus code, sig, key) in the request URL, queryString, redirectURL and Location/Referer headers ("%5Bredacted%5D" in URLs); and password/token/secret/key/session/signature/bearer/cookie/csrf/refresh/auth/sid/pin/ssn/card-number fields of JSON (primitives at any depth under such a name), form-urlencoded (also sniffed when the Content-Type says otherwise, user[password] names too) and multipart request and response bodies (an access_token/refresh_token/id_token login response too) and of WebSocket messages, also in truncated JSON, JSON encoded in string values (3 levels), socket.io and SockJS packets, server-sent events, NDJSON, base64 bodies with no, a generic, JSON, form or event-stream type and binary WebSocket messages that are UTF-8 text; any whole JWT (eyJ…) in a JSON string, form or URL value, multipart part or text body (only the JWT is replaced). camelCase names count as words (userPin). JSON is edited in place, not re-serialized: everything but the replaced values (64-bit numbers, formatting, duplicate keys, a BOM or )]}\' prefix) stays byte for byte. Header, cookie and parameter names, cookie attributes, headersSize, bodySize and content.size stay; log.comment and JSON sanitized: true say so',
|
|
335
335
|
whenEnabled: 'Writes every captured value (JSON sanitized: false); human output warns that the file holds credentials',
|
|
336
|
-
automaticBehavior: 'Matching is by name, so it over-redacts: harmless values under credential-looking names (tokenCount: 5, sessionLength) are replaced too, and credentials under other names are kept. Unlike Chrome DevTools, which drops these headers and empties cookies, names are kept so the HAR still shows a request was authenticated.
|
|
336
|
+
automaticBehavior: 'Matching is by name, so it over-redacts: harmless values under credential-looking names (tokenCount: 5, sessionLength, refreshInterval, cookieConsent) are replaced too, and credentials under other names are kept. Only JSON syntax is understood: single-quoted strings, unquoted keys, JSONP, STOMP passcode: header lines and bare values with spaces ({"token":abc def}, a non-JWT token after Bearer in text) are not (fully) redacted. A body the sanitizer fails on is replaced whole by [redacted]. Unlike Chrome DevTools, which drops these headers and empties cookies, names are kept so the HAR still shows a request was authenticated. Kept as captured: other binary (base64) bodies, binary WebSocket messages that are not UTF-8, and text that is not JSON or a form (apart from JWTs); a WebSocket message cut at 100 KB has _truncatedFrom. HAR files are written readable by their owner only (0600). network headers and network getCookies always show real values',
|
|
337
337
|
},
|
|
338
338
|
'peek:--verbose': {
|
|
339
339
|
default: 'Compact output (truncated URLs, no resource types)',
|
|
@@ -365,8 +365,8 @@ const OPTION_BEHAVIORS = {
|
|
|
365
365
|
whenEnabled: 'Alias for --force, kept for compatibility',
|
|
366
366
|
},
|
|
367
367
|
'cleanup:--purge': {
|
|
368
|
-
default: "A named session's directory (Chrome profile, ~60 MB; logs; port.txt) is kept for its next start",
|
|
369
|
-
whenEnabled: 'After cleaning up, deletes the directory of the session named by --session (exit 81 without --session); a running session is refused unless --force is given, and the directory is kept (exit 90) if the daemon still answers, cleanup reported a problem, or its Chrome has not exited',
|
|
368
|
+
default: "A named session's directory (Chrome profile, ~60 MB; logs; port.txt; downloads/) is kept for its next start",
|
|
369
|
+
whenEnabled: 'After cleaning up, deletes the directory of the session named by --session, downloaded files included (exit 81 without --session); a running session is refused unless --force is given, and the directory is kept (exit 90) if the daemon still answers, cleanup reported a problem, or its Chrome has not exited',
|
|
370
370
|
},
|
|
371
371
|
'bdg:--viewport': {
|
|
372
372
|
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',
|
|
@@ -386,12 +386,22 @@ const OPTION_BEHAVIORS = {
|
|
|
386
386
|
'bdg:--chrome-ws-url': {
|
|
387
387
|
default: 'bdg launches its own Chrome (closed on stop)',
|
|
388
388
|
whenEnabled: 'Attaches to a running Chrome instead; it keeps running after stop. --port, -u and --[no-]headless cannot be combined with it (exit 81)',
|
|
389
|
-
automaticBehavior: 'A port (9222), host:port or http://host:port is turned into the browser WebSocket URL via /json/version; a browser URL uses the first open tab. Refused with exit 90 when another running bdg session launched that Chrome or drives that tab (sessions of this BDG_SESSION_DIR, and of others that claimed a port)',
|
|
389
|
+
automaticBehavior: 'A port (9222), host:port or http://host:port is turned into the browser WebSocket URL via /json/version; a browser URL uses the first open tab. Refused with exit 90 when another running bdg session launched that Chrome or drives that tab (sessions of this BDG_SESSION_DIR, and of others that claimed a port). Downloads are not redirected: they go to the download folder of that Chrome (usually ~/Downloads), and only downloads of the session page are reported (not those of tabs or popups it opens)',
|
|
390
390
|
},
|
|
391
391
|
'stop:--kill-chrome': {
|
|
392
392
|
default: 'Chrome launched by bdg is always closed on stop; an attached Chrome (--chrome-ws-url) is left running',
|
|
393
393
|
whenEnabled: 'No additional effect; kept for compatibility',
|
|
394
394
|
},
|
|
395
|
+
'cdp:--send-anyway': {
|
|
396
|
+
default: 'A Domain.method 1 edit from a bundled method or domain (2 for names over 5 letters, case ignored) is taken for a typo: exit 81 with Did you mean',
|
|
397
|
+
whenEnabled: 'Sends such a name to Chrome as typed (for a method newer than the bundled protocol, e.g. next to a bundled sibling like getWindowBounds/setWindowBounds), with the not-in-bundled-protocol warning',
|
|
398
|
+
automaticBehavior: 'Blocked methods (Page.captureScreenshot, Page.close, Browser.close), type names and names that are not Domain.method are still refused; a well-formed name far from every bundled one is sent without the flag',
|
|
399
|
+
},
|
|
400
|
+
'cdp:--describe': {
|
|
401
|
+
default: 'Without --describe, a Domain.method is called (one missing from the bundled protocol is sent as typed, with a warning: only bundled methods are matched case-insensitively)',
|
|
402
|
+
whenEnabled: 'Describes a domain, a method (parameters with ? for optional, returns, example) or a protocol type (Domain.Type: enum values or object properties)',
|
|
403
|
+
automaticBehavior: 'Parameters referring to an enum type list its values inline (JSON enum, ref, refType); a redirected method (DOM.highlightNode) also shows the method implementing it and its parameters, which Chrome checks (JSON redirect, resolved: true); a redirect to a method the protocol lacks (Page.deleteCookie → Network.deleteCookie) is shown as unresolved (resolved: false). Example values work as typed: width 1280, height 800, x/y 100, deviceScaleFactor/scale 1, timeout 5000, url https://example.com, other numbers 1 (never 0, which often means off)',
|
|
404
|
+
},
|
|
395
405
|
'status:--verbose': {
|
|
396
406
|
default: 'Basic session status (daemon running, session active, URL)',
|
|
397
407
|
whenEnabled: 'Includes Chrome diagnostics and CDP connection details',
|
|
@@ -44,6 +44,11 @@ export interface CommandResult<T = unknown> {
|
|
|
44
44
|
errorContext?: Record<string, unknown>;
|
|
45
45
|
/** Optional hint message to display on stderr (for successful commands with guidance) */
|
|
46
46
|
hint?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Warning about how the command ran (success or failure): top-level
|
|
49
|
+
* `warning` in the JSON envelope, `Warning: …` on stderr otherwise
|
|
50
|
+
*/
|
|
51
|
+
warning?: string;
|
|
47
52
|
}
|
|
48
53
|
/**
|
|
49
54
|
* Handler function type.
|
|
@@ -80,6 +80,15 @@ function sessionEndedError() {
|
|
|
80
80
|
const err = sessionEndedDuringCommandError();
|
|
81
81
|
return new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.RESOURCE_NOT_FOUND);
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Print a command's warning on stderr (text output; `--json` has it in the envelope).
|
|
85
|
+
*
|
|
86
|
+
* @param warning - Warning, if any
|
|
87
|
+
*/
|
|
88
|
+
function printWarning(warning) {
|
|
89
|
+
if (warning)
|
|
90
|
+
console.error(escapeControlChars(`Warning: ${warning}`));
|
|
91
|
+
}
|
|
83
92
|
/**
|
|
84
93
|
* Run a command with consistent error handling, output formatting, and exit codes.
|
|
85
94
|
* Eliminates boilerplate try-catch and JSON output logic from command handlers.
|
|
@@ -114,6 +123,7 @@ export async function runCommand(handler, options, formatter) {
|
|
|
114
123
|
if (options.json) {
|
|
115
124
|
console.log(stringifyEnvelope(OutputBuilder.buildJsonError(result.error ?? 'Unknown error', {
|
|
116
125
|
...result.errorContext,
|
|
126
|
+
...(result.warning && { warning: result.warning }),
|
|
117
127
|
exitCode,
|
|
118
128
|
})));
|
|
119
129
|
}
|
|
@@ -126,14 +136,17 @@ export async function runCommand(handler, options, formatter) {
|
|
|
126
136
|
}
|
|
127
137
|
}
|
|
128
138
|
}
|
|
139
|
+
printWarning(result.warning);
|
|
129
140
|
}
|
|
130
141
|
process.exit(exitCode);
|
|
131
142
|
}
|
|
143
|
+
if (!options.json)
|
|
144
|
+
printWarning(result.warning);
|
|
132
145
|
if (result.hint && !options.quiet) {
|
|
133
146
|
console.error(escapeControlChars(result.hint));
|
|
134
147
|
}
|
|
135
148
|
if (options.json) {
|
|
136
|
-
console.log(stringifyEnvelope(buildSuccessResponse(result.data)));
|
|
149
|
+
console.log(stringifyEnvelope(buildSuccessResponse(result.data, result.warning)));
|
|
137
150
|
}
|
|
138
151
|
else if (formatter) {
|
|
139
152
|
const formattedOutput = formatter(result.data);
|
|
@@ -158,10 +171,12 @@ export async function runCommand(handler, options, formatter) {
|
|
|
158
171
|
})));
|
|
159
172
|
}
|
|
160
173
|
else {
|
|
174
|
+
const { warning, ...metadata } = error.metadata;
|
|
161
175
|
console.error(genericError(error.message));
|
|
162
|
-
for (const value of Object.values(
|
|
163
|
-
console.error(escapeControlChars(
|
|
176
|
+
for (const value of Object.values(metadata)) {
|
|
177
|
+
console.error(escapeControlChars(typeof value === 'string' ? value : JSON.stringify(value)));
|
|
164
178
|
}
|
|
179
|
+
printWarning(warning);
|
|
165
180
|
}
|
|
166
181
|
process.exit(error.exitCode);
|
|
167
182
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ctrl-C and SIGTERM for a command that cancels its daemon request instead
|
|
3
|
+
* of dying mid-request, so it can still report (the `--json` envelope) and
|
|
4
|
+
* exit with the shell's code.
|
|
5
|
+
*/
|
|
6
|
+
/** Signals that interrupt a command */
|
|
7
|
+
export type InterruptSignal = 'SIGINT' | 'SIGTERM';
|
|
8
|
+
/**
|
|
9
|
+
* The exit code of an interrupted command, as shells expect.
|
|
10
|
+
*
|
|
11
|
+
* @param signal - The signal
|
|
12
|
+
* @returns 130 for SIGINT, 143 for SIGTERM
|
|
13
|
+
*/
|
|
14
|
+
export declare function interruptExitCode(signal: InterruptSignal): number;
|
|
15
|
+
/**
|
|
16
|
+
* The signal that aborted an interrupt, from its reason.
|
|
17
|
+
*
|
|
18
|
+
* @param interrupt - Aborted interrupt (reason: the signal name)
|
|
19
|
+
* @returns The signal, SIGINT unless it was SIGTERM
|
|
20
|
+
*/
|
|
21
|
+
export declare function interruptSignal(interrupt: AbortSignal): InterruptSignal;
|
|
22
|
+
/**
|
|
23
|
+
* Abort on Ctrl-C or SIGTERM (reason: the signal name). A second signal
|
|
24
|
+
* exits at once.
|
|
25
|
+
*
|
|
26
|
+
* @returns Aborted on the first signal
|
|
27
|
+
*/
|
|
28
|
+
export declare function abortOnInterrupt(): AbortSignal;
|
|
29
|
+
/**
|
|
30
|
+
* Wait for work unless interrupted first: the first signal ends the wait at
|
|
31
|
+
* once with the interrupted error (the work is left to the exiting process).
|
|
32
|
+
*
|
|
33
|
+
* @param work - Work to wait for
|
|
34
|
+
* @param interrupt - Aborted on Ctrl-C or SIGTERM (reason: the signal)
|
|
35
|
+
* @param interruptedError - The error for an interrupt by a signal
|
|
36
|
+
* @returns The work's value
|
|
37
|
+
* @throws The interrupted error once interrupted, else the work's error
|
|
38
|
+
*/
|
|
39
|
+
export declare function unlessInterrupted<T>(work: Promise<T>, interrupt: AbortSignal, interruptedError: (signal: InterruptSignal) => Error): Promise<T>;
|
|
40
|
+
//# sourceMappingURL=interrupt.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ctrl-C and SIGTERM for a command that cancels its daemon request instead
|
|
3
|
+
* of dying mid-request, so it can still report (the `--json` envelope) and
|
|
4
|
+
* exit with the shell's code.
|
|
5
|
+
*/
|
|
6
|
+
import { EXIT_CODES } from '../../utils/exitCodes.js';
|
|
7
|
+
/**
|
|
8
|
+
* The exit code of an interrupted command, as shells expect.
|
|
9
|
+
*
|
|
10
|
+
* @param signal - The signal
|
|
11
|
+
* @returns 130 for SIGINT, 143 for SIGTERM
|
|
12
|
+
*/
|
|
13
|
+
export function interruptExitCode(signal) {
|
|
14
|
+
return signal === 'SIGINT' ? EXIT_CODES.INTERRUPTED : EXIT_CODES.TERMINATED;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The signal that aborted an interrupt, from its reason.
|
|
18
|
+
*
|
|
19
|
+
* @param interrupt - Aborted interrupt (reason: the signal name)
|
|
20
|
+
* @returns The signal, SIGINT unless it was SIGTERM
|
|
21
|
+
*/
|
|
22
|
+
export function interruptSignal(interrupt) {
|
|
23
|
+
return interrupt.reason === 'SIGTERM' ? 'SIGTERM' : 'SIGINT';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Abort on Ctrl-C or SIGTERM (reason: the signal name). A second signal
|
|
27
|
+
* exits at once.
|
|
28
|
+
*
|
|
29
|
+
* @returns Aborted on the first signal
|
|
30
|
+
*/
|
|
31
|
+
export function abortOnInterrupt() {
|
|
32
|
+
const interrupt = new AbortController();
|
|
33
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
34
|
+
process.on(signal, () => {
|
|
35
|
+
if (interrupt.signal.aborted)
|
|
36
|
+
process.exit(interruptExitCode(signal));
|
|
37
|
+
interrupt.abort(signal);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return interrupt.signal;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Wait for work unless interrupted first: the first signal ends the wait at
|
|
44
|
+
* once with the interrupted error (the work is left to the exiting process).
|
|
45
|
+
*
|
|
46
|
+
* @param work - Work to wait for
|
|
47
|
+
* @param interrupt - Aborted on Ctrl-C or SIGTERM (reason: the signal)
|
|
48
|
+
* @param interruptedError - The error for an interrupt by a signal
|
|
49
|
+
* @returns The work's value
|
|
50
|
+
* @throws The interrupted error once interrupted, else the work's error
|
|
51
|
+
*/
|
|
52
|
+
export async function unlessInterrupted(work, interrupt, interruptedError) {
|
|
53
|
+
const fail = () => interruptedError(interruptSignal(interrupt));
|
|
54
|
+
if (interrupt.aborted)
|
|
55
|
+
throw fail();
|
|
56
|
+
let onAbort = () => undefined;
|
|
57
|
+
const interrupted = new Promise((_, reject) => {
|
|
58
|
+
onAbort = () => reject(fail());
|
|
59
|
+
interrupt.addEventListener('abort', onAbort, { once: true });
|
|
60
|
+
});
|
|
61
|
+
try {
|
|
62
|
+
return await Promise.race([work, interrupted]);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (interrupt.aborted)
|
|
66
|
+
throw fail();
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
interrupt.removeEventListener('abort', onAbort);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=interrupt.js.map
|
|
@@ -129,6 +129,8 @@ export interface CdpMethodOptions {
|
|
|
129
129
|
describe?: boolean;
|
|
130
130
|
/** Search for methods */
|
|
131
131
|
search?: string;
|
|
132
|
+
/** Send a method that looks like a typo of a bundled one as typed */
|
|
133
|
+
sendAnyway?: boolean;
|
|
132
134
|
}
|
|
133
135
|
/** Options for stop command */
|
|
134
136
|
export type StopCommandOptions = BaseOptions & ChromeOptions;
|
|
@@ -15,6 +15,8 @@ import type { TelemetryType } from '../../types.js';
|
|
|
15
15
|
export type StartOutcome = {
|
|
16
16
|
ok: true;
|
|
17
17
|
data: StartSessionResponseData;
|
|
18
|
+
/** The daemon this attempt spawned (internal: stopped if the start is interrupted) */
|
|
19
|
+
spawned?: SpawnedDaemon;
|
|
18
20
|
} | {
|
|
19
21
|
ok: false;
|
|
20
22
|
/** Message for `--json` (no "Error:" prefix) */
|
|
@@ -44,6 +46,8 @@ export interface StartDeps {
|
|
|
44
46
|
launch: () => Promise<SpawnedDaemon | undefined>;
|
|
45
47
|
/** Sends `start_session_request` */
|
|
46
48
|
send: typeof sendStartSessionRequest;
|
|
49
|
+
/** Sends `stop_session_request` (stops a session whose start was interrupted) */
|
|
50
|
+
stop: () => Promise<unknown>;
|
|
47
51
|
/** How long a failed start waits for the daemons it spawned to exit */
|
|
48
52
|
exitWaitMs: number;
|
|
49
53
|
}
|
|
@@ -53,6 +57,19 @@ export interface StartDeps {
|
|
|
53
57
|
* Spawns the daemon if needed, sends `start_session_request`, then prints the
|
|
54
58
|
* result as a JSON envelope (`--json`) or human-readable text, and exits.
|
|
55
59
|
*
|
|
60
|
+
* Ctrl-C (or SIGTERM) cancels the start at any point until the outcome is
|
|
61
|
+
* printed: the connection is closed, so the daemon abandons the session (a
|
|
62
|
+
* daemon spawned but not yet asked is told to shut down, a session that has
|
|
63
|
+
* just started is stopped), and the command exits with 130 (143) once the
|
|
64
|
+
* daemon it spawned has exited (bounded, like a failed start), so a command
|
|
65
|
+
* run right after sees no session. A second signal exits at once.
|
|
66
|
+
*
|
|
67
|
+
* Race-free: {@link attemptStart} checks the interrupt last, synchronously,
|
|
68
|
+
* and only microtasks run between that check and the exit in
|
|
69
|
+
* {@link reportStartOutcome}; a signal handler runs as a macrotask, so a
|
|
70
|
+
* signal either lands before the check (the start is cancelled) or after the
|
|
71
|
+
* outcome has been printed.
|
|
72
|
+
*
|
|
56
73
|
* @param url - Target URL to navigate to
|
|
57
74
|
* @param options - Session configuration options
|
|
58
75
|
* @param telemetry - Array of telemetry types to enable
|
|
@@ -63,13 +80,19 @@ export declare function startSessionViaDaemon(url: string, options: SessionStart
|
|
|
63
80
|
* failure the daemon reported (or a dropped connection), it waits for every
|
|
64
81
|
* daemon the attempts spawned to exit ({@link afterSpawnedDaemonExit}).
|
|
65
82
|
*
|
|
83
|
+
* An interrupt cancels the start whenever it arrives: a start that succeeded
|
|
84
|
+
* meanwhile is stopped ({@link cancelStartedSession}), and a failure keeps its
|
|
85
|
+
* message but exits 130 (143), also when the signal came during the wait.
|
|
86
|
+
*
|
|
66
87
|
* @param url - Target URL
|
|
67
88
|
* @param options - Session options
|
|
68
89
|
* @param telemetry - Telemetry types
|
|
69
90
|
* @param deps - How to reach the daemon (tests replace it)
|
|
70
|
-
* @
|
|
91
|
+
* @param interrupt - Aborted (reason: the signal) on Ctrl-C or SIGTERM: the
|
|
92
|
+
* start is cancelled and the daemon it spawned waited for like after a failure
|
|
93
|
+
* @returns Start outcome, ready to report (the interrupt is checked last)
|
|
71
94
|
*/
|
|
72
|
-
export declare function attemptStart(url: string, options: SessionStartOptions, telemetry: TelemetryType[], deps?: StartDeps): Promise<StartOutcome>;
|
|
95
|
+
export declare function attemptStart(url: string, options: SessionStartOptions, telemetry: TelemetryType[], deps?: StartDeps, interrupt?: AbortSignal): Promise<StartOutcome>;
|
|
73
96
|
/**
|
|
74
97
|
* Let the daemons a failed start spawned finish exiting before the error is
|
|
75
98
|
* reported: a daemon removes its session files on the way out, and a command
|
|
@@ -82,5 +105,5 @@ export declare function attemptStart(url: string, options: SessionStartOptions,
|
|
|
82
105
|
* @param waitMs - Milliseconds to wait at most for all of them
|
|
83
106
|
* @returns The failure (without internal fields), with a hint when a daemon did not exit in time
|
|
84
107
|
*/
|
|
85
|
-
export declare function afterSpawnedDaemonExit(outcome: StartOutcome, spawned: SpawnedDaemon[], waitMs: number): Promise<StartOutcome>;
|
|
108
|
+
export declare function afterSpawnedDaemonExit(outcome: StartOutcome, spawned: Pick<SpawnedDaemon, 'pid' | 'hasExited'>[], waitMs: number): Promise<StartOutcome>;
|
|
86
109
|
//# sourceMappingURL=startHelpers.d.ts.map
|