browser-debugger-cli 0.14.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.
Files changed (167) hide show
  1. package/.claude/skills/bdg/SKILL.md +3 -2
  2. package/dist/cdp/methodTarget.d.ts +92 -0
  3. package/dist/cdp/methodTarget.js +159 -0
  4. package/dist/cdp/protocol.d.ts +16 -1
  5. package/dist/cdp/protocol.js +21 -0
  6. package/dist/cdp/schema.d.ts +55 -1
  7. package/dist/cdp/schema.js +134 -25
  8. package/dist/cdp/types.d.ts +3 -1
  9. package/dist/commands/cdp.d.ts +38 -1
  10. package/dist/commands/cdp.js +201 -133
  11. package/dist/commands/cleanup.js +21 -4
  12. package/dist/commands/dom/eval.d.ts +2 -1
  13. package/dist/commands/dom/eval.js +6 -21
  14. package/dist/commands/dom/formInteraction.js +8 -4
  15. package/dist/commands/dom/helpers/evalResult.d.ts +36 -0
  16. package/dist/commands/dom/helpers/evalResult.js +59 -0
  17. package/dist/commands/dom/helpers/index.d.ts +4 -4
  18. package/dist/commands/dom/helpers/index.js +3 -3
  19. package/dist/commands/dom/helpers/query.d.ts +2 -2
  20. package/dist/commands/dom/helpers/query.js +2 -2
  21. package/dist/commands/dom/helpers/screenshot.d.ts +21 -26
  22. package/dist/commands/dom/helpers/screenshot.js +50 -668
  23. package/dist/commands/dom/screenshot.js +56 -36
  24. package/dist/commands/helpJson.d.ts +1 -1
  25. package/dist/commands/helpJson.js +3 -3
  26. package/dist/commands/helpTopic.js +10 -4
  27. package/dist/commands/network/har.js +18 -14
  28. package/dist/commands/optionBehaviors.js +24 -9
  29. package/dist/commands/shared/CommandRunner.d.ts +5 -0
  30. package/dist/commands/shared/CommandRunner.js +18 -3
  31. package/dist/commands/shared/interrupt.d.ts +40 -0
  32. package/dist/commands/shared/interrupt.js +73 -0
  33. package/dist/commands/shared/optionTypes.d.ts +3 -0
  34. package/dist/commands/shared/outputFile.d.ts +2 -1
  35. package/dist/commands/shared/outputFile.js +7 -4
  36. package/dist/commands/shared/startHelpers.d.ts +26 -3
  37. package/dist/commands/shared/startHelpers.js +145 -23
  38. package/dist/commands/status.js +3 -1
  39. package/dist/commands/stop.js +2 -1
  40. package/dist/commands/types.d.ts +5 -0
  41. package/dist/connection/cdp.js +1 -16
  42. package/dist/connection/chromeIdentity.d.ts +24 -5
  43. package/dist/connection/chromeIdentity.js +53 -22
  44. package/dist/connection/launcher/flagsBuilder.d.ts +46 -0
  45. package/dist/connection/launcher/flagsBuilder.js +107 -23
  46. package/dist/connection/launcher.d.ts +35 -2
  47. package/dist/connection/launcher.js +99 -12
  48. package/dist/connection/typed-cdp.d.ts +3 -2
  49. package/dist/constants.d.ts +3 -5
  50. package/dist/constants.js +3 -5
  51. package/dist/daemon/SessionController.d.ts +10 -5
  52. package/dist/daemon/SessionController.js +15 -8
  53. package/dist/daemon/ipcServer.js +1 -1
  54. package/dist/daemon/launcher.d.ts +22 -3
  55. package/dist/daemon/launcher.js +45 -8
  56. package/dist/daemon/session/Session.d.ts +5 -1
  57. package/dist/daemon/session/Session.js +9 -8
  58. package/dist/daemon/session/TelemetryStore.d.ts +5 -0
  59. package/dist/daemon/session/TelemetryStore.js +4 -0
  60. package/dist/daemon/session/captureGate.d.ts +59 -0
  61. package/dist/daemon/session/captureGate.js +96 -0
  62. package/dist/daemon/session/chromeConnection.d.ts +16 -1
  63. package/dist/daemon/session/chromeConnection.js +34 -4
  64. package/dist/daemon/session/collectors.d.ts +15 -0
  65. package/dist/daemon/session/collectors.js +39 -2
  66. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  67. package/dist/daemon/session/commandRegistry.js +48 -13
  68. package/dist/daemon/session/downloads.d.ts +32 -0
  69. package/dist/daemon/session/downloads.js +96 -0
  70. package/dist/daemon/session/interactions.d.ts +3 -2
  71. package/dist/daemon/session/interactions.js +7 -2
  72. package/dist/daemon/session/plugins.js +6 -0
  73. package/dist/daemon.js +18520 -17014
  74. package/dist/errors/CommandError.d.ts +2 -0
  75. package/dist/errors/issues.d.ts +1 -1
  76. package/dist/errors/messages.d.ts +81 -0
  77. package/dist/errors/messages.js +198 -6
  78. package/dist/index.js +1446 -1078
  79. package/dist/ipc/client.d.ts +20 -2
  80. package/dist/ipc/client.js +32 -6
  81. package/dist/ipc/protocol/commands.d.ts +36 -2
  82. package/dist/ipc/protocol/commands.js +1 -0
  83. package/dist/ipc/protocol/domTypes.d.ts +24 -1
  84. package/dist/ipc/session/queries.d.ts +3 -0
  85. package/dist/ipc/session/types.d.ts +5 -0
  86. package/dist/ipc/transport/IPCError.d.ts +9 -0
  87. package/dist/ipc/transport/IPCError.js +12 -0
  88. package/dist/ipc/transport/errors.d.ts +2 -1
  89. package/dist/ipc/transport/errors.js +4 -1
  90. package/dist/ipc/transport/index.d.ts +10 -2
  91. package/dist/ipc/transport/index.js +29 -4
  92. package/dist/runtime/dom/actionEffects.d.ts +48 -9
  93. package/dist/runtime/dom/actionEffects.js +269 -34
  94. package/dist/runtime/dom/actionEffectsScripts.d.ts +45 -0
  95. package/dist/runtime/dom/actionEffectsScripts.js +101 -2
  96. package/dist/runtime/dom/captureArea.d.ts +35 -0
  97. package/dist/runtime/dom/captureArea.js +203 -0
  98. package/dist/runtime/dom/elementInfo.d.ts +13 -4
  99. package/dist/runtime/dom/elementInfo.js +12 -3
  100. package/dist/runtime/dom/evalHelpers.d.ts +24 -4
  101. package/dist/runtime/dom/evalHelpers.js +40 -12
  102. package/dist/runtime/dom/formDiscovery.d.ts +1 -1
  103. package/dist/runtime/dom/frames.d.ts +2 -1
  104. package/dist/runtime/dom/frames.js +3 -1
  105. package/dist/runtime/page/bdgWorld.d.ts +9 -0
  106. package/dist/runtime/page/bdgWorld.js +11 -0
  107. package/dist/runtime/page/captureEmulation.d.ts +119 -0
  108. package/dist/runtime/page/captureEmulation.js +189 -0
  109. package/dist/runtime/page/captureScroll.d.ts +24 -0
  110. package/dist/runtime/page/captureScroll.js +124 -0
  111. package/dist/runtime/page/emulation.js +6 -5
  112. package/dist/runtime/page/screenshot.d.ts +41 -0
  113. package/dist/runtime/page/screenshot.js +394 -0
  114. package/dist/runtime/page/userAgent.d.ts +86 -2
  115. package/dist/runtime/page/userAgent.js +154 -33
  116. package/dist/session/paths.d.ts +52 -3
  117. package/dist/session/paths.js +179 -7
  118. package/dist/session/portClaims.d.ts +0 -8
  119. package/dist/session/portClaims.js +1 -22
  120. package/dist/session/sessionList.d.ts +5 -1
  121. package/dist/session/sessionList.js +5 -1
  122. package/dist/telemetry/downloads.d.ts +127 -0
  123. package/dist/telemetry/downloads.js +265 -0
  124. package/dist/telemetry/har/builder.d.ts +12 -1
  125. package/dist/telemetry/har/builder.js +32 -9
  126. package/dist/telemetry/har/sanitize.d.ts +28 -0
  127. package/dist/telemetry/har/sanitize.js +184 -0
  128. package/dist/telemetry/har/sanitizeBody.d.ts +78 -0
  129. package/dist/telemetry/har/sanitizeBody.js +541 -0
  130. package/dist/telemetry/har/types.d.ts +2 -0
  131. package/dist/telemetry/network.d.ts +4 -4
  132. package/dist/telemetry/network.js +38 -4
  133. package/dist/telemetry/networkRetention.d.ts +35 -14
  134. package/dist/telemetry/networkRetention.js +62 -26
  135. package/dist/types.d.ts +9 -14
  136. package/dist/ui/OutputBuilder.d.ts +3 -2
  137. package/dist/ui/OutputBuilder.js +4 -3
  138. package/dist/ui/formatters/cdp.d.ts +32 -9
  139. package/dist/ui/formatters/cdp.js +77 -6
  140. package/dist/ui/formatters/details.js +7 -15
  141. package/dist/ui/formatters/preview.d.ts +2 -0
  142. package/dist/ui/formatters/preview.js +7 -1
  143. package/dist/ui/formatters/sessions.d.ts +3 -2
  144. package/dist/ui/formatters/sessions.js +10 -3
  145. package/dist/ui/formatters/status.js +6 -1
  146. package/dist/ui/formatting.d.ts +7 -0
  147. package/dist/ui/formatting.js +13 -0
  148. package/dist/ui/logging/logger.d.ts +1 -1
  149. package/dist/ui/messages/chrome.d.ts +27 -6
  150. package/dist/ui/messages/chrome.js +78 -12
  151. package/dist/ui/messages/commands.d.ts +71 -3
  152. package/dist/ui/messages/commands.js +98 -3
  153. package/dist/ui/messages/networkMessages.d.ts +50 -5
  154. package/dist/ui/messages/networkMessages.js +50 -6
  155. package/dist/ui/messages/session.d.ts +8 -0
  156. package/dist/ui/messages/session.js +10 -0
  157. package/dist/utils/async.d.ts +3 -2
  158. package/dist/utils/async.js +16 -3
  159. package/dist/utils/atomicFile.d.ts +2 -1
  160. package/dist/utils/atomicFile.js +5 -2
  161. package/dist/utils/directories.d.ts +41 -0
  162. package/dist/utils/directories.js +48 -0
  163. package/dist/utils/http.d.ts +11 -4
  164. package/dist/utils/http.js +5 -3
  165. package/package.json +18 -4
  166. /package/dist/{commands/dom → runtime/page}/screenshotResize.d.ts +0 -0
  167. /package/dist/{commands/dom → runtime/page}/screenshotResize.js +0 -0
@@ -56,5 +56,51 @@ export declare function needsNoSandbox(): boolean;
56
56
  * @returns True if running in Docker, false otherwise
57
57
  */
58
58
  export declare function isDocker(): boolean;
59
+ /**
60
+ * Remove `HEADLESS` from this process's environment.
61
+ *
62
+ * chrome-launcher adds a bare `--headless` whenever the launching process has
63
+ * a non-empty `HEADLESS` variable (even `0` or `false`), whatever its
64
+ * `envVars` option says, which would make `--no-headless` launch headless.
65
+ * The daemon calls this before it launches Chrome.
66
+ *
67
+ * @param env - Environment to clean (defaults to `process.env`)
68
+ */
69
+ export declare function dropLauncherHeadlessEnv(env?: NodeJS.ProcessEnv): void;
70
+ /**
71
+ * Build Chrome flags array from launch options.
72
+ *
73
+ * Uses chrome-launcher default flags as base (unless ignoreDefaultFlags is true)
74
+ * and layers bdg-specific overrides on top. Headless mode uses the new headless
75
+ * implementation for better compatibility.
76
+ *
77
+ * When running in Docker, automatically adds GPU-disabling flags to work around
78
+ * graphics limitations in containerized environments.
79
+ *
80
+ * Custom flags are passed via the chromeFlags option. The BDG_CHROME_FLAGS env var
81
+ * is parsed by the CLI and merged into chromeFlags before reaching this function.
82
+ * Feature lists from all sources end up in one `--disable-features` (and one
83
+ * `--enable-features`) flag, and each flag appears once.
84
+ *
85
+ * @param options - Launch options containing flag preferences
86
+ * @returns Array of Chrome command-line flags
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * // Standard flags
91
+ * const flags = buildChromeFlags({ port: 9222 });
92
+ *
93
+ * // Headless with custom flags
94
+ * const flags = buildChromeFlags({
95
+ * port: 9222,
96
+ * headless: true,
97
+ * chromeFlags: ['--window-size=1920,1080']
98
+ * });
99
+ *
100
+ * // Docker environment (auto-detects)
101
+ * const flags = buildChromeFlags({ port: 9222 });
102
+ * // Includes --disable-gpu, --no-sandbox if in Docker
103
+ * ```
104
+ */
59
105
  export declare function buildChromeFlags(options: FlagsBuilderOptions): string[];
60
106
  //# sourceMappingURL=flagsBuilder.d.ts.map
@@ -68,6 +68,103 @@ export function isDocker() {
68
68
  return false;
69
69
  }
70
70
  }
71
+ const DISABLE_FEATURES = '--disable-features=';
72
+ const ENABLE_FEATURES = '--enable-features=';
73
+ /**
74
+ * Flags that bdg merges into one: Chrome reads only the last occurrence of
75
+ * these comma-separated feature lists, and chrome-launcher's defaults, bdg and
76
+ * users all pass them. `--disable-blink-features`/`--enable-blink-features`
77
+ * are not merged (neither bdg nor the defaults pass them), nor is `--js-flags`
78
+ * (space-separated V8 flags, not a feature list).
79
+ */
80
+ const FEATURE_LIST_FLAGS = [DISABLE_FEATURES, ENABLE_FEATURES];
81
+ /**
82
+ * Bare name of a feature-list entry, without a `<Trial` or `:params` suffix.
83
+ *
84
+ * @param entry - Entry such as `Foo<Trial` or `Foo:param/1`
85
+ * @returns Feature name
86
+ */
87
+ function featureName(entry) {
88
+ return entry.split(/[<:]/)[0] ?? entry;
89
+ }
90
+ /**
91
+ * Remove disabled features that are also enabled.
92
+ *
93
+ * Chrome lets `--disable-features` win over `--enable-features`, so without
94
+ * this a user's `--enable-features=MediaRouter` could not re-enable a feature
95
+ * chrome-launcher disables by default.
96
+ *
97
+ * @param features - Merged values per feature-list flag (changed in place)
98
+ */
99
+ function dropReEnabledFeatures(features) {
100
+ const enabled = new Set([...(features.get(ENABLE_FEATURES) ?? [])].map(featureName));
101
+ const disabled = features.get(DISABLE_FEATURES);
102
+ disabled?.forEach((entry) => {
103
+ if (enabled.has(featureName(entry)))
104
+ disabled.delete(entry);
105
+ });
106
+ }
107
+ /**
108
+ * Merge every `--disable-features=` (and `--enable-features=`) flag into one
109
+ * and drop repeated flags.
110
+ *
111
+ * Chrome applies only the last occurrence of a feature-list flag, so bdg's or
112
+ * the user's list would otherwise switch chrome-launcher's defaults back on.
113
+ * The merged flag keeps the first one's position; its values keep their order
114
+ * without repeats, and an enabled feature is never also disabled. Only
115
+ * entries starting with `-` are deduplicated, so positional arguments such as
116
+ * URLs are passed as given.
117
+ *
118
+ * @param flags - Chrome flags in launch order
119
+ * @returns Flags with at most one flag per feature list and no repeated flags
120
+ */
121
+ function mergeFeatureFlags(flags) {
122
+ const features = new Map();
123
+ const merged = [];
124
+ for (const flag of flags) {
125
+ const prefix = FEATURE_LIST_FLAGS.find((p) => flag.startsWith(p));
126
+ if (prefix) {
127
+ if (!features.has(prefix))
128
+ merged.push(prefix);
129
+ const values = flag.slice(prefix.length).split(',').filter(Boolean);
130
+ features.set(prefix, new Set([...(features.get(prefix) ?? []), ...values]));
131
+ }
132
+ else if (!flag.startsWith('-') || !merged.includes(flag)) {
133
+ merged.push(flag);
134
+ }
135
+ }
136
+ dropReEnabledFeatures(features);
137
+ return merged.flatMap((flag) => {
138
+ const values = features.get(flag);
139
+ if (!values)
140
+ return [flag];
141
+ return values.size > 0 ? [flag + [...values].join(',')] : [];
142
+ });
143
+ }
144
+ /**
145
+ * Remove `HEADLESS` from this process's environment.
146
+ *
147
+ * chrome-launcher adds a bare `--headless` whenever the launching process has
148
+ * a non-empty `HEADLESS` variable (even `0` or `false`), whatever its
149
+ * `envVars` option says, which would make `--no-headless` launch headless.
150
+ * The daemon calls this before it launches Chrome.
151
+ *
152
+ * @param env - Environment to clean (defaults to `process.env`)
153
+ */
154
+ export function dropLauncherHeadlessEnv(env = process.env) {
155
+ delete env['HEADLESS'];
156
+ }
157
+ /**
158
+ * chrome-launcher's default flags (plus its Linux sandbox flag). bdg passes
159
+ * them itself and tells chrome-launcher to skip its own copy, so each flag
160
+ * appears once; chrome-launcher adds `--remote-debugging-port`.
161
+ *
162
+ * @returns Default flags
163
+ */
164
+ function defaultFlags() {
165
+ const flags = chromeLauncher.Launcher.defaultFlags();
166
+ return process.platform === 'linux' ? [...flags, '--disable-setuid-sandbox'] : flags;
167
+ }
71
168
  /**
72
169
  * Build Chrome flags array from launch options.
73
170
  *
@@ -80,6 +177,8 @@ export function isDocker() {
80
177
  *
81
178
  * Custom flags are passed via the chromeFlags option. The BDG_CHROME_FLAGS env var
82
179
  * is parsed by the CLI and merged into chromeFlags before reaching this function.
180
+ * Feature lists from all sources end up in one `--disable-features` (and one
181
+ * `--enable-features`) flag, and each flag appears once.
83
182
  *
84
183
  * @param options - Launch options containing flag preferences
85
184
  * @returns Array of Chrome command-line flags
@@ -101,17 +200,6 @@ export function isDocker() {
101
200
  * // Includes --disable-gpu, --no-sandbox if in Docker
102
201
  * ```
103
202
  */
104
- /**
105
- * chrome-launcher's default flags (plus its Linux sandbox flag). bdg passes
106
- * them itself and tells chrome-launcher to skip its own copy, so each flag
107
- * appears once; chrome-launcher adds `--remote-debugging-port`.
108
- *
109
- * @returns Default flags
110
- */
111
- function defaultFlags() {
112
- const flags = chromeLauncher.Launcher.defaultFlags();
113
- return process.platform === 'linux' ? [...flags, '--disable-setuid-sandbox'] : flags;
114
- }
115
203
  export function buildChromeFlags(options) {
116
204
  const baseFlags = options.ignoreDefaultFlags ? [] : defaultFlags();
117
205
  const bdgFlags = [
@@ -120,18 +208,14 @@ export function buildChromeFlags(options) {
120
208
  ];
121
209
  const dockerFlags = isDocker() ? DOCKER_CHROME_FLAGS : [];
122
210
  const sandboxFlags = needsNoSandbox() ? ['--no-sandbox'] : [];
123
- // Custom flags from CLI option (env var BDG_CHROME_FLAGS is parsed by CLI and passed here)
124
211
  const customFlags = options.chromeFlags ?? [];
125
- if (options.headless) {
126
- return [
127
- HEADLESS_FLAG,
128
- ...baseFlags,
129
- ...bdgFlags,
130
- ...dockerFlags,
131
- ...sandboxFlags,
132
- ...customFlags,
133
- ];
134
- }
135
- return [...baseFlags, ...bdgFlags, ...dockerFlags, ...sandboxFlags, ...customFlags];
212
+ return mergeFeatureFlags([
213
+ ...(options.headless ? [HEADLESS_FLAG] : []),
214
+ ...baseFlags,
215
+ ...bdgFlags,
216
+ ...dockerFlags,
217
+ ...sandboxFlags,
218
+ ...customFlags,
219
+ ]);
136
220
  }
137
221
  //# sourceMappingURL=flagsBuilder.js.map
@@ -1,10 +1,11 @@
1
1
  import type { LaunchedChrome, Logger } from './types.js';
2
2
  import type { Options as ChromeLaunchOptions } from 'chrome-launcher';
3
+ import { ChromeLaunchError } from './errors.js';
3
4
  /**
4
5
  * Options that control how Chrome is launched for CDP sessions.
5
6
  * Extended to support chrome-launcher advanced features.
6
7
  */
7
- export interface LaunchOptions extends Pick<ChromeLaunchOptions, 'logLevel' | 'connectionPollInterval' | 'maxConnectionRetries' | 'portStrictMode' | 'envVars' | 'handleSIGINT' | 'ignoreDefaultFlags' | 'chromeFlags' | 'chromePath'> {
8
+ export interface LaunchOptions extends Pick<ChromeLaunchOptions, 'logLevel' | 'connectionPollInterval' | 'maxConnectionRetries' | 'portStrictMode' | 'envVars' | 'ignoreDefaultFlags' | 'chromeFlags' | 'chromePath'> {
8
9
  /** Remote debugging port (defaults to 9222 when omitted) */
9
10
  port?: number;
10
11
  /**
@@ -29,6 +30,11 @@ export interface LaunchOptions extends Pick<ChromeLaunchOptions, 'logLevel' | 'c
29
30
  chromePath?: string;
30
31
  /** bdg session directory: recorded as a marker flag on the Chrome command line, and holds the default profile */
31
32
  sessionDir?: string | undefined;
33
+ /**
34
+ * Ends the launch when aborted (the session was stopped): bdg stops waiting
35
+ * and kills Chrome as soon as chrome-launcher has spawned it
36
+ */
37
+ signal?: AbortSignal | undefined;
32
38
  }
33
39
  /**
34
40
  * Launch Chrome with remote debugging enabled using chrome-launcher.
@@ -42,7 +48,7 @@ export interface LaunchOptions extends Pick<ChromeLaunchOptions, 'logLevel' | 'c
42
48
  *
43
49
  * @param options - Launch configuration options
44
50
  * @returns LaunchedChrome instance with PID and kill method
45
- * @throws ChromeLaunchError if Chrome fails to launch, process dies immediately, or CDP doesn't become available
51
+ * @throws ChromeLaunchError if Chrome fails to launch, process dies immediately, CDP doesn't become available, or `options.signal` aborts
46
52
  * @throws Error if user data directory cannot be created
47
53
  *
48
54
  * @remarks
@@ -50,6 +56,32 @@ export interface LaunchOptions extends Pick<ChromeLaunchOptions, 'logLevel' | 'c
50
56
  * Uses chrome-launcher for cross-platform Chrome detection and launching.
51
57
  */
52
58
  export declare function launchChrome(options?: LaunchOptions): Promise<LaunchedChrome>;
59
+ /**
60
+ * State of the launch when chrome-launcher rejected.
61
+ */
62
+ interface LaunchFailureContext {
63
+ /** Debugging port */
64
+ port: number;
65
+ /** Chrome's PID (0 if it was not spawned) */
66
+ pid: number;
67
+ /** Whether Chrome was still running when chrome-launcher rejected */
68
+ chromeAlive: boolean;
69
+ /** How long chrome-launcher waited for the port to open */
70
+ readyBudgetMs: number;
71
+ }
72
+ /**
73
+ * The error for a launch chrome-launcher rejected.
74
+ *
75
+ * A refused connection while Chrome is still running means Chrome did not
76
+ * open its port within the readiness budget (a slow start, e.g. the first
77
+ * one on a cold machine): the port was free right before the launch and
78
+ * nothing answers on it, so it is not a conflict, and Chrome did not crash.
79
+ *
80
+ * @param error - chrome-launcher's rejection
81
+ * @param context - Port, Chrome's PID and liveness, and the readiness budget
82
+ * @returns CHROME_PORT_NOT_OPENED for a slow start, else CHROME_LAUNCH_FAILED
83
+ */
84
+ export declare function launchFailedError(error: unknown, { port, pid, chromeAlive, readyBudgetMs }: LaunchFailureContext): ChromeLaunchError;
53
85
  /**
54
86
  * Preferences for the launched profile: bdg's defaults (no translate prompt,
55
87
  * no password manager or leak check, whose bubbles capture input) for
@@ -65,4 +97,5 @@ export declare function launchChrome(options?: LaunchOptions): Promise<LaunchedC
65
97
  * @throws ChromeLaunchError if prefs file cannot be read or parsed, or prefs are not JSON
66
98
  */
67
99
  export declare function resolveChromePrefs(options: LaunchOptions): Record<string, unknown>;
100
+ export {};
68
101
  //# sourceMappingURL=launcher.d.ts.map
@@ -2,12 +2,14 @@ import * as fs from 'fs';
2
2
  import * as os from 'os';
3
3
  import * as path from 'path';
4
4
  import * as chromeLauncher from 'chrome-launcher';
5
- import { BDG_CHROME_PREFS, DEFAULT_CDP_PORT, CHROME_PROFILE_DIR, DEFAULT_CHROME_LOG_LEVEL, DEFAULT_CHROME_HANDLE_SIGINT, } from '../constants.js';
5
+ import { BDG_CHROME_PREFS, DEFAULT_CDP_PORT, CHROME_PROFILE_DIR, DEFAULT_CHROME_LOG_LEVEL, } from '../constants.js';
6
+ import { chromePortNotOpenedMessage } from '../ui/messages/chrome.js';
7
+ import { delay } from '../utils/async.js';
6
8
  import { makeDirectory } from '../utils/directories.js';
7
9
  import { getErrorMessage } from '../utils/errors.js';
8
10
  import { filterDefined } from '../utils/objects.js';
9
11
  import { isProcessAlive } from '../utils/process.js';
10
- import { verifyLaunchedChrome } from './chromeIdentity.js';
12
+ import { launchAbortedError, throwIfLaunchAborted, verifyLaunchedChrome, } from './chromeIdentity.js';
11
13
  import { ChromeLaunchError } from './errors.js';
12
14
  import { resolveChromeBinary } from './launcher/binaryResolver.js';
13
15
  import { buildChromeFlags } from './launcher/flagsBuilder.js';
@@ -27,6 +29,8 @@ import { markStartupLogs, watchStartupExit } from './startupExit.js';
27
29
  const CHROME_READY_POLL_MS = 50;
28
30
  /** Checks before giving up: 25 s in all, as with chrome-launcher's defaults */
29
31
  const CHROME_READY_POLL_ATTEMPTS = 500;
32
+ /** How often an aborted launch checks whether chrome-launcher has spawned Chrome yet */
33
+ const SPAWN_POLL_MS = 10;
30
34
  const defaultLogger = {
31
35
  info: (msg) => console.error(msg),
32
36
  debug: () => { }, // No-op by default
@@ -43,7 +47,7 @@ const defaultLogger = {
43
47
  *
44
48
  * @param options - Launch configuration options
45
49
  * @returns LaunchedChrome instance with PID and kill method
46
- * @throws ChromeLaunchError if Chrome fails to launch, process dies immediately, or CDP doesn't become available
50
+ * @throws ChromeLaunchError if Chrome fails to launch, process dies immediately, CDP doesn't become available, or `options.signal` aborts
47
51
  * @throws Error if user data directory cannot be created
48
52
  *
49
53
  * @remarks
@@ -79,13 +83,14 @@ export async function launchChrome(options = {}) {
79
83
  logger.debug(`User data directory: ${userDataDir}`);
80
84
  applyProfilePreferences(userDataDir, options, logger);
81
85
  const chromeOptions = buildChromeOptions({ ...options, port });
86
+ throwIfLaunchAborted(options.signal);
82
87
  const launcher = new chromeLauncher.Launcher(chromeOptions);
83
88
  const logs = markStartupLogs(userDataDir);
84
89
  const startup = watchStartupExit(() => launcher.chromeProcess, logs, userDataDir);
85
90
  try {
86
91
  const launchStart = Date.now();
87
92
  logger.info('Waiting for Chrome to be ready...');
88
- await Promise.race([launcher.launch(), startup.exited]);
93
+ await launchUnlessAborted(launcher, startup.exited, options.signal);
89
94
  startup.stop();
90
95
  const launchDurationMs = Date.now() - launchStart;
91
96
  logger.info(`✓ Chrome ready (${launchDurationMs}ms)`);
@@ -101,7 +106,7 @@ export async function launchChrome(options = {}) {
101
106
  },
102
107
  });
103
108
  }
104
- await verifyLaunchedChrome({ logs, port, pid: chromeProcessPid });
109
+ await verifyLaunchedChrome({ logs, port, pid: chromeProcessPid, signal: options.signal });
105
110
  logger.info(`Chrome launched successfully (PID: ${chromeProcessPid}, ${launchDurationMs}ms)`);
106
111
  return {
107
112
  pid: chromeProcessPid,
@@ -117,20 +122,103 @@ export async function launchChrome(options = {}) {
117
122
  }
118
123
  catch (error) {
119
124
  startup.stop();
125
+ const pid = launcher.chromeProcess?.pid ?? 0;
126
+ const chromeAlive = pid > 0 && isProcessAlive(pid);
120
127
  launcher.kill();
121
128
  launcher.destroyTmp();
122
129
  if (error instanceof ChromeLaunchError) {
123
130
  throw error;
124
131
  }
125
- throw new ChromeLaunchError(`Failed to launch Chrome: ${getErrorMessage(error)}`, {
126
- ...(error instanceof Error && { cause: error }),
127
- issue: {
128
- code: 'CHROME_LAUNCH_FAILED',
129
- context: { port, reason: getErrorMessage(error) },
130
- },
132
+ throw launchFailedError(error, {
133
+ port,
134
+ pid,
135
+ chromeAlive,
136
+ readyBudgetMs: readyBudgetMs(chromeOptions),
131
137
  });
132
138
  }
133
139
  }
140
+ /**
141
+ * The error for a launch chrome-launcher rejected.
142
+ *
143
+ * A refused connection while Chrome is still running means Chrome did not
144
+ * open its port within the readiness budget (a slow start, e.g. the first
145
+ * one on a cold machine): the port was free right before the launch and
146
+ * nothing answers on it, so it is not a conflict, and Chrome did not crash.
147
+ *
148
+ * @param error - chrome-launcher's rejection
149
+ * @param context - Port, Chrome's PID and liveness, and the readiness budget
150
+ * @returns CHROME_PORT_NOT_OPENED for a slow start, else CHROME_LAUNCH_FAILED
151
+ */
152
+ export function launchFailedError(error, { port, pid, chromeAlive, readyBudgetMs }) {
153
+ const cause = error instanceof Error ? { cause: error } : {};
154
+ if (chromeAlive && isConnectionRefused(error)) {
155
+ return new ChromeLaunchError(chromePortNotOpenedMessage(pid, port, readyBudgetMs), {
156
+ ...cause,
157
+ issue: { code: 'CHROME_PORT_NOT_OPENED', context: { port, pid, waitedMs: readyBudgetMs } },
158
+ });
159
+ }
160
+ return new ChromeLaunchError(`Failed to launch Chrome: ${getErrorMessage(error)}`, {
161
+ ...cause,
162
+ issue: { code: 'CHROME_LAUNCH_FAILED', context: { port, reason: getErrorMessage(error) } },
163
+ });
164
+ }
165
+ /**
166
+ * Whether an error is a refused TCP connection.
167
+ *
168
+ * @param error - Error to check
169
+ * @returns True for ECONNREFUSED
170
+ */
171
+ function isConnectionRefused(error) {
172
+ return error?.code === 'ECONNREFUSED';
173
+ }
174
+ /**
175
+ * How long chrome-launcher waits for Chrome's debugging port to open.
176
+ *
177
+ * @param chromeOptions - chrome-launcher options
178
+ * @returns Poll interval times the number of polls
179
+ */
180
+ function readyBudgetMs(chromeOptions) {
181
+ return ((chromeOptions.connectionPollInterval ?? CHROME_READY_POLL_MS) *
182
+ (chromeOptions.maxConnectionRetries ?? CHROME_READY_POLL_ATTEMPTS));
183
+ }
184
+ /**
185
+ * Run chrome-launcher's launch (spawn Chrome, wait for its port to open),
186
+ * ending early when Chrome exits or `signal` aborts.
187
+ *
188
+ * chrome-launcher cannot be cancelled: after an abort its port poller keeps
189
+ * running, bdg just stops waiting for it. An abort can arrive before
190
+ * chrome-launcher has spawned Chrome (it first checks whether the port
191
+ * answers), so this waits until Chrome is spawned or the launch has ended
192
+ * without one; the caller's kill then reaches that Chrome.
193
+ *
194
+ * @param launcher - chrome-launcher instance, not launched yet
195
+ * @param exited - Rejects when Chrome exits during startup
196
+ * @param signal - The launch's abort signal
197
+ * @throws ChromeLaunchError if `signal` aborts (before or during the launch)
198
+ * @throws Error from chrome-launcher or `exited`
199
+ */
200
+ async function launchUnlessAborted(launcher, exited, signal) {
201
+ throwIfLaunchAborted(signal);
202
+ let settled = false;
203
+ const launching = launcher.launch().finally(() => {
204
+ settled = true;
205
+ });
206
+ let onAbort = () => { };
207
+ const aborted = new Promise((resolve) => {
208
+ onAbort = () => resolve('aborted');
209
+ signal?.addEventListener('abort', onAbort, { once: true });
210
+ });
211
+ try {
212
+ if ((await Promise.race([launching, exited, aborted])) !== 'aborted')
213
+ return;
214
+ }
215
+ finally {
216
+ signal?.removeEventListener('abort', onAbort);
217
+ }
218
+ while (!launcher.chromeProcess && !settled)
219
+ await delay(SPAWN_POLL_MS);
220
+ throw launchAbortedError();
221
+ }
134
222
  /**
135
223
  * Get the default persistent user-data-dir path.
136
224
  *
@@ -224,7 +312,6 @@ function buildChromeOptions(options) {
224
312
  const chromePathOverride = resolveChromeBinary(options);
225
313
  return {
226
314
  logLevel: options.logLevel ?? DEFAULT_CHROME_LOG_LEVEL,
227
- handleSIGINT: options.handleSIGINT ?? DEFAULT_CHROME_HANDLE_SIGINT,
228
315
  ignoreDefaultFlags: true,
229
316
  chromeFlags: buildChromeFlags(options),
230
317
  userDataDir,
@@ -18,9 +18,10 @@ import type { Protocol } from 'devtools-protocol/types/protocol.js';
18
18
  * Extract parameter type from a CDP command.
19
19
  *
20
20
  * If command has no parameters, returns empty object type.
21
- * If command has single parameter array, returns first element.
21
+ * If command has a single parameter (required, or optional when all its
22
+ * fields are), returns its type.
22
23
  */
23
- type CommandParams<T extends keyof ProtocolMapping.Commands> = ProtocolMapping.Commands[T]['paramsType'] extends [infer P] ? P : ProtocolMapping.Commands[T]['paramsType'] extends [] ? Record<string, never> : never;
24
+ type CommandParams<T extends keyof ProtocolMapping.Commands> = ProtocolMapping.Commands[T]['paramsType'] extends [] ? Record<string, never> : ProtocolMapping.Commands[T]['paramsType'] extends [(infer P)?] ? NonNullable<P> : never;
24
25
  /**
25
26
  * Extract return type from a CDP command.
26
27
  */
@@ -12,10 +12,6 @@ export declare const DEFAULT_CDP_PORT = 9222;
12
12
  * Default Chrome launcher log level for quiet operation
13
13
  */
14
14
  export declare const DEFAULT_CHROME_LOG_LEVEL = "silent";
15
- /**
16
- * Default SIGINT handling - bdg handles signals, not chrome-launcher
17
- */
18
- export declare const DEFAULT_CHROME_HANDLE_SIGINT = false;
19
15
  /**
20
16
  * Persistent Chrome profile directory path (relative to user home)
21
17
  */
@@ -90,7 +86,7 @@ export declare const OBJECT_EXPANSION_FAILURE_THRESHOLD = 5;
90
86
  */
91
87
  export declare const MAX_RESPONSE_SIZE: number;
92
88
  /**
93
- * Total size of the response bodies a session keeps (100MB)
89
+ * Total size of the request and response bodies a session keeps (100MB)
94
90
  * Past this the oldest bodies are replaced by a placeholder; their requests stay
95
91
  */
96
92
  export declare const MAX_TOTAL_BODY_BYTES: number;
@@ -124,6 +120,8 @@ export declare const CHROME_NETWORK_BUFFER_PER_RESOURCE: number;
124
120
  export declare const CHROME_POST_DATA_LIMIT: number;
125
121
  /** Matches `dom query` and `dom a11y query` list with `--json` and no `--limit` */
126
122
  export declare const QUERY_JSON_LIST_LIMIT = 100;
123
+ /** Elements of an array result `dom eval --json` lists (the rest are counted as omitted) */
124
+ export declare const EVAL_JSON_ARRAY_LIMIT = 100;
127
125
  /** Matches `dom layout` measures per command (the rest are counted as omitted) */
128
126
  export declare const LAYOUT_ELEMENT_LIMIT = 100;
129
127
  /**
package/dist/constants.js CHANGED
@@ -15,10 +15,6 @@ export const DEFAULT_CDP_PORT = 9222;
15
15
  * Default Chrome launcher log level for quiet operation
16
16
  */
17
17
  export const DEFAULT_CHROME_LOG_LEVEL = 'silent';
18
- /**
19
- * Default SIGINT handling - bdg handles signals, not chrome-launcher
20
- */
21
- export const DEFAULT_CHROME_HANDLE_SIGINT = false;
22
18
  /**
23
19
  * Persistent Chrome profile directory path (relative to user home)
24
20
  */
@@ -122,7 +118,7 @@ export const OBJECT_EXPANSION_FAILURE_THRESHOLD = 5;
122
118
  */
123
119
  export const MAX_RESPONSE_SIZE = 5 * 1024 * 1024; // 5MB
124
120
  /**
125
- * Total size of the response bodies a session keeps (100MB)
121
+ * Total size of the request and response bodies a session keeps (100MB)
126
122
  * Past this the oldest bodies are replaced by a placeholder; their requests stay
127
123
  */
128
124
  export const MAX_TOTAL_BODY_BYTES = 100 * 1024 * 1024;
@@ -165,6 +161,8 @@ export const CHROME_POST_DATA_LIMIT = 1 * 1024 * 1024; // 1MB
165
161
  // ============================================================================
166
162
  /** Matches `dom query` and `dom a11y query` list with `--json` and no `--limit` */
167
163
  export const QUERY_JSON_LIST_LIMIT = 100;
164
+ /** Elements of an array result `dom eval --json` lists (the rest are counted as omitted) */
165
+ export const EVAL_JSON_ARRAY_LIMIT = 100;
168
166
  /** Matches `dom layout` measures per command (the rest are counted as omitted) */
169
167
  export const LAYOUT_ELEMENT_LIMIT = 100;
170
168
  /**
@@ -41,7 +41,8 @@ export declare class SessionController {
41
41
  stopActiveSession(): Promise<boolean>;
42
42
  /**
43
43
  * Error fields for a request that needs the session when there is none:
44
- * still starting (85), or none at all.
44
+ * still starting (85), or none at all (also while an abandoned start is
45
+ * torn down).
45
46
  *
46
47
  * @returns Error message, plus exit code and suggestion while starting
47
48
  */
@@ -78,15 +79,19 @@ export declare class SessionController {
78
79
  * Execute a session command (dom_*, cdp_call, session_details, ...).
79
80
  *
80
81
  * @param request - Command request
82
+ * @param abandoned - Aborted when the requesting client disconnects
81
83
  * @returns Command response, forwarding exit code and suggestion on failure
82
84
  */
83
- command(request: ClientRequestUnion): Promise<unknown>;
85
+ command(request: ClientRequestUnion, abandoned?: AbortSignal): Promise<unknown>;
84
86
  /**
85
87
  * Start a session, or report the one already running.
86
88
  *
87
- * A start whose client disconnects (Ctrl-C) is abandoned: the session is
88
- * stopped, whether it is still launching or has just started, since nobody
89
- * learns that it exists.
89
+ * A start whose client disconnects (Ctrl-C, or a client killed without a
90
+ * clean disconnect) is abandoned: the session is stopped, whether it is
91
+ * still launching or has just started, since nobody learns that it exists.
92
+ * From then on the daemon reports itself shutting down (a new start gets
93
+ * the retryable `SESSION_SHUTTING_DOWN`, status shows the session ending),
94
+ * not starting, while Chrome is torn down.
90
95
  *
91
96
  * @param request - Start session request
92
97
  * @param abandoned - Aborted when the requesting client disconnects
@@ -149,12 +149,13 @@ export class SessionController {
149
149
  }
150
150
  /**
151
151
  * Error fields for a request that needs the session when there is none:
152
- * still starting (85), or none at all.
152
+ * still starting (85), or none at all (also while an abandoned start is
153
+ * torn down).
153
154
  *
154
155
  * @returns Error message, plus exit code and suggestion while starting
155
156
  */
156
157
  noSessionError() {
157
- if (!this.launching)
158
+ if (!this.launching || this.closing)
158
159
  return { error: noActiveSessionMessage() };
159
160
  return {
160
161
  error: STARTING_ERROR,
@@ -190,7 +191,7 @@ export class SessionController {
190
191
  };
191
192
  const base = { type: 'status_response', sessionId: request.sessionId };
192
193
  if (!this.session || this.session.stopRequested()) {
193
- if (this.launching) {
194
+ if (this.launching && !this.closing) {
194
195
  data.starting = { url: this.launching.url, since: this.launching.since };
195
196
  }
196
197
  if (this.session || this.closing)
@@ -250,6 +251,7 @@ export class SessionController {
250
251
  },
251
252
  currentNavigationId: data.currentNavigationId,
252
253
  ...(data.pageCrashedAt !== undefined && { pageCrashedAt: data.pageCrashedAt }),
254
+ ...(data.downloads && { downloads: data.downloads }),
253
255
  partial: true,
254
256
  },
255
257
  },
@@ -282,9 +284,10 @@ export class SessionController {
282
284
  * Execute a session command (dom_*, cdp_call, session_details, ...).
283
285
  *
284
286
  * @param request - Command request
287
+ * @param abandoned - Aborted when the requesting client disconnects
285
288
  * @returns Command response, forwarding exit code and suggestion on failure
286
289
  */
287
- async command(request) {
290
+ async command(request, abandoned) {
288
291
  const name = request.type.slice(0, -'_request'.length);
289
292
  const base = { type: `${name}_response`, sessionId: request.sessionId };
290
293
  if (!this.session) {
@@ -293,7 +296,7 @@ export class SessionController {
293
296
  const { sessionId: _sessionId, type: _type, ...params } = request;
294
297
  try {
295
298
  const execute = this.session.execute.bind(this.session);
296
- const data = await withTimeout(execute(name, params), commandTimeoutMs(name, params), 'Command');
299
+ const data = await withTimeout(execute(name, params, abandoned), commandTimeoutMs(name, params), 'Command');
297
300
  return { ...base, status: 'ok', data };
298
301
  }
299
302
  catch (error) {
@@ -303,9 +306,12 @@ export class SessionController {
303
306
  /**
304
307
  * Start a session, or report the one already running.
305
308
  *
306
- * A start whose client disconnects (Ctrl-C) is abandoned: the session is
307
- * stopped, whether it is still launching or has just started, since nobody
308
- * learns that it exists.
309
+ * A start whose client disconnects (Ctrl-C, or a client killed without a
310
+ * clean disconnect) is abandoned: the session is stopped, whether it is
311
+ * still launching or has just started, since nobody learns that it exists.
312
+ * From then on the daemon reports itself shutting down (a new start gets
313
+ * the retryable `SESSION_SHUTTING_DOWN`, status shows the session ending),
314
+ * not starting, while Chrome is torn down.
309
315
  *
310
316
  * @param request - Start session request
311
317
  * @param abandoned - Aborted when the requesting client disconnects
@@ -330,6 +336,7 @@ export class SessionController {
330
336
  this.launching = launching;
331
337
  const stopAbandoned = () => {
332
338
  log.info('Client disconnected during start; stopping the session');
339
+ this.closing = true;
333
340
  void launching.session?.stop('normal');
334
341
  };
335
342
  abandoned?.addEventListener('abort', stopAbandoned, { once: true });
@@ -196,7 +196,7 @@ export class IPCServer {
196
196
  */
197
197
  async route(message, disconnected) {
198
198
  if (isCommandRequest(message.type)) {
199
- return this.controller.command(message);
199
+ return this.controller.command(message, disconnected);
200
200
  }
201
201
  switch (message.type) {
202
202
  case 'handshake_request':