browser-commander 0.20.0 → 0.21.1

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 (169) hide show
  1. package/.jscpd.json +2 -1
  2. package/.prettierignore +1 -0
  3. package/CHANGELOG.md +159 -0
  4. package/README.md +216 -32
  5. package/bin/browser-commander.js +16 -0
  6. package/eslint.config.js +10 -0
  7. package/examples/react-test-app/src/App.jsx +24 -23
  8. package/extension/background.js +14 -0
  9. package/extension/manifest.json +12 -0
  10. package/extension/relay-handler.js +353 -0
  11. package/package.json +21 -4
  12. package/src/browser/attach/differences.js +131 -0
  13. package/src/browser/attach/extension-relay.js +402 -0
  14. package/src/browser/attach/index.js +81 -0
  15. package/src/browser/attach/snapshot-launch.js +146 -0
  16. package/src/browser/attach/snapshot.js +374 -0
  17. package/src/browser/attach/websocket.js +615 -0
  18. package/src/browser/browser-cookie-cache.js +26 -19
  19. package/src/browser/browser-cookie-credentials.js +6 -9
  20. package/src/browser/browser-cookies.js +5 -2
  21. package/src/browser/cdp-session.js +94 -0
  22. package/src/browser/connector.js +42 -4
  23. package/src/browser/debugging-port.js +155 -0
  24. package/src/browser/launch-options.js +63 -52
  25. package/src/browser/launch-restrictions.json +147 -0
  26. package/src/browser/launcher.js +329 -133
  27. package/src/browser/media.js +5 -0
  28. package/src/browser/migration/bookmarks.js +73 -0
  29. package/src/browser/migration/chromium-crypto.js +89 -0
  30. package/src/browser/migration/cookies.js +155 -0
  31. package/src/browser/migration/extensions.js +187 -0
  32. package/src/browser/migration/firefox-bookmarks.js +144 -0
  33. package/src/browser/migration/firefox-der.js +84 -0
  34. package/src/browser/migration/firefox-nss.js +281 -0
  35. package/src/browser/migration/firefox.js +334 -0
  36. package/src/browser/migration/fs-utils.js +51 -0
  37. package/src/browser/migration/history.js +103 -0
  38. package/src/browser/migration/index.js +307 -0
  39. package/src/browser/migration/os-crypt-keys.js +106 -0
  40. package/src/browser/migration/passwords.js +171 -0
  41. package/src/browser/migration/preferences.js +132 -0
  42. package/src/browser/migration/sqlite-snapshot.js +138 -0
  43. package/src/browser/open-in-user-browser.js +112 -0
  44. package/src/browser/parity.js +439 -0
  45. package/src/browser/profile-directory.js +205 -0
  46. package/src/browser/real-browser.js +545 -306
  47. package/src/browser/restrictions.js +148 -0
  48. package/src/browser/system-browser.js +232 -0
  49. package/src/browser/webdriver.js +572 -0
  50. package/src/cli/args.js +300 -0
  51. package/src/cli/commands.js +332 -0
  52. package/src/cli/dispatcher.js +99 -0
  53. package/src/cli/handle-methods.js +136 -0
  54. package/src/cli/handles.js +273 -0
  55. package/src/cli/high-level-methods.js +310 -0
  56. package/src/cli/main.js +67 -0
  57. package/src/cli/modules.js +100 -0
  58. package/src/cli/rpc-error.js +77 -0
  59. package/src/cli/script.js +77 -0
  60. package/src/cli/serve.js +144 -0
  61. package/src/cli/sessions.js +199 -0
  62. package/src/core/constants.js +7 -1
  63. package/src/core/engine-adapter.js +107 -1
  64. package/src/core/engine-detection.js +18 -3
  65. package/src/core/navigation-safety.js +4 -0
  66. package/src/core/webdriver-conversions.js +347 -0
  67. package/src/core/webdriver-events.js +229 -0
  68. package/src/core/webdriver-page.js +716 -0
  69. package/src/downloads/attach.js +6 -0
  70. package/src/elements/selectors.js +3 -2
  71. package/src/elements/visibility.js +4 -0
  72. package/src/exports.js +61 -2
  73. package/src/factory.js +19 -4
  74. package/src/fingerprint/apply.js +6 -0
  75. package/src/fingerprint/automation-parity.js +7 -8
  76. package/src/fingerprint/limitations.js +1 -1
  77. package/src/fingerprint/limitations.json +45 -2
  78. package/src/parity/harness.js +303 -0
  79. package/src/parity/probe.js +838 -0
  80. package/src/parity/version-page.js +144 -0
  81. package/src/traces/bundle.js +3 -2
  82. package/src/traces/observers.js +14 -5
  83. package/src/traces/recorder.js +3 -0
  84. package/src/traces/viewer.js +16 -8
  85. package/src/utilities/subprocess.js +199 -0
  86. package/tests/e2e/api-coverage.e2e.test.js +174 -0
  87. package/tests/e2e/cli.e2e.test.js +160 -0
  88. package/tests/e2e/fingerprint-parity.e2e.test.js +104 -46
  89. package/tests/e2e/google-signin-probe.e2e.test.js +112 -0
  90. package/tests/e2e/measure-parity.e2e.test.js +118 -0
  91. package/tests/e2e/playwright.e2e.test.js +26 -17
  92. package/tests/e2e/puppeteer.e2e.test.js +18 -23
  93. package/tests/e2e/webdriver.e2e.test.js +239 -0
  94. package/tests/fixtures/firefox-nss-fixtures.mjs +211 -0
  95. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.html +2 -0
  96. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.png +1 -0
  97. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.state.json +54 -0
  98. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.html +2 -0
  99. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.png +1 -0
  100. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.state.json +60 -0
  101. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.html +2 -0
  102. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.png +1 -0
  103. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.state.json +60 -0
  104. package/tests/fixtures/traces/conformance/expected/bundle/events.ndjson +18 -0
  105. package/tests/fixtures/traces/conformance/expected/bundle/manifest.json +77 -0
  106. package/tests/fixtures/traces/conformance/expected/bundle/mutations/0001.ndjson +2 -0
  107. package/tests/fixtures/traces/conformance/expected/bundle/mutations/0002.ndjson +1 -0
  108. package/tests/fixtures/traces/conformance/expected/bundle/viewer.html +309 -0
  109. package/tests/fixtures/traces/conformance/expected/redaction.json +86 -0
  110. package/tests/fixtures/traces/conformance/expected/trace.lino +27 -0
  111. package/tests/fixtures/traces/conformance/scenario.json +249 -0
  112. package/tests/helpers/cli-fakes.js +214 -0
  113. package/tests/helpers/declared-api.js +80 -0
  114. package/tests/helpers/e2e-browser.js +40 -14
  115. package/tests/helpers/fake-chrome.js +124 -0
  116. package/tests/helpers/migration-fixtures.js +242 -0
  117. package/tests/helpers/temp-directory.js +24 -0
  118. package/tests/helpers/webdriver-mocks.js +183 -0
  119. package/tests/unit/browser/attach/extension-relay.test.js +360 -0
  120. package/tests/unit/browser/attach/snapshot-launch.test.js +321 -0
  121. package/tests/unit/browser/attach/snapshot.test.js +409 -0
  122. package/tests/unit/browser/attach/websocket.test.js +240 -0
  123. package/tests/unit/browser/browser-cookie-lock.test.js +102 -0
  124. package/tests/unit/browser/browser-cookies.test.js +2 -32
  125. package/tests/unit/browser/cdp-session.test.js +162 -0
  126. package/tests/unit/browser/connector.test.js +23 -0
  127. package/tests/unit/browser/launch-building-blocks.test.js +361 -0
  128. package/tests/unit/browser/launch-options.test.js +41 -22
  129. package/tests/unit/browser/launcher.test.js +281 -102
  130. package/tests/unit/browser/media.test.js +13 -1
  131. package/tests/unit/browser/migration/bookmarks.test.js +69 -0
  132. package/tests/unit/browser/migration/chromium-crypto.test.js +94 -0
  133. package/tests/unit/browser/migration/cookies.test.js +94 -0
  134. package/tests/unit/browser/migration/extensions.test.js +104 -0
  135. package/tests/unit/browser/migration/firefox-nss.test.js +113 -0
  136. package/tests/unit/browser/migration/firefox.test.js +139 -0
  137. package/tests/unit/browser/migration/history.test.js +48 -0
  138. package/tests/unit/browser/migration/index.test.js +117 -0
  139. package/tests/unit/browser/migration/os-crypt-keys.test.js +95 -0
  140. package/tests/unit/browser/migration/passwords.test.js +144 -0
  141. package/tests/unit/browser/migration/preferences.test.js +77 -0
  142. package/tests/unit/browser/migration/sqlite-snapshot.test.js +75 -0
  143. package/tests/unit/browser/open-in-user-browser.test.js +123 -0
  144. package/tests/unit/browser/parity.test.js +281 -0
  145. package/tests/unit/browser/real-browser.test.js +458 -33
  146. package/tests/unit/browser/storage-state.test.js +2 -0
  147. package/tests/unit/browser/webdriver.test.js +473 -0
  148. package/tests/unit/cli/bin.test.js +69 -0
  149. package/tests/unit/cli/declared-api.test.js +49 -0
  150. package/tests/unit/cli/dispatcher.test.js +429 -0
  151. package/tests/unit/cli/handles.test.js +170 -0
  152. package/tests/unit/cli/main.test.js +437 -0
  153. package/tests/unit/cli/serve.test.js +167 -0
  154. package/tests/unit/core/navigation-manager.test.js +11 -1
  155. package/tests/unit/core/webdriver-conversions.test.js +235 -0
  156. package/tests/unit/core/webdriver-page.test.js +534 -0
  157. package/tests/unit/fingerprint/automation-parity.test.js +17 -4
  158. package/tests/unit/fingerprint/limitations.test.js +1 -1
  159. package/tests/unit/parity/harness.test.js +138 -0
  160. package/tests/unit/playwright-protocol-coverage.test.js +231 -0
  161. package/tests/unit/puppeteer-bindings-coverage.test.js +214 -0
  162. package/tests/unit/scripts/ci-timeout-budgets.test.js +27 -0
  163. package/tests/unit/scripts/feature-parity.test.js +327 -0
  164. package/tests/unit/traces/conformance.test.js +92 -0
  165. package/tests/unit/traces/reader.test.js +1 -0
  166. package/tests/unit/traces/recorder.test.js +1 -0
  167. package/tests/unit/traces/viewer.test.js +78 -0
  168. package/tests/unit/utilities/command-stream-only.test.js +28 -0
  169. package/tests/unit/utilities/subprocess.test.js +80 -0
package/.jscpd.json CHANGED
@@ -11,7 +11,8 @@
11
11
  "**/.changeset/**",
12
12
  "**/package-lock.json",
13
13
  "**/pnpm-lock.yaml",
14
- "**/yarn.lock"
14
+ "**/yarn.lock",
15
+ "**/tests/fixtures/traces/conformance/**"
15
16
  ],
16
17
  "reporters": ["console", "html"],
17
18
  "output": "./reports/jscpd"
package/.prettierignore CHANGED
@@ -7,3 +7,4 @@ package-lock.json
7
7
  .eslintcache
8
8
  CLAUDE.md
9
9
  reports
10
+ tests/fixtures/traces/conformance/expected
package/CHANGELOG.md CHANGED
@@ -1,5 +1,164 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.21.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 4caa385: Suppress Chromium's default-browser prompt through profile settings on fresh and snapshot launches, and expose configurable Preferences and Local State values.
8
+
9
+ Update the locked brace-expansion dependency to 5.0.12 to resolve its CPU and stack exhaustion advisories.
10
+
11
+ Retry transient Windows credential-lock EPERM within the existing bounded deadline, preserving the underlying error when contention persists.
12
+
13
+ Accept a `monotonic` clock in `startTrace` and use the injected `now` for interaction durations, so a trace recorded with a fixed clock is byte-for-byte reproducible; the Python and Rust recorders are checked against such a recording.
14
+
15
+ Escape recorded text in the offline trace viewer. A recorded page's URL, title, form control values and event fields were inserted into the viewer as HTML, so a page could run script in the viewer of whoever opened its trace.
16
+
17
+ ## 0.21.0
18
+
19
+ ### Minor Changes
20
+
21
+ - 1d398f4: Run a real browser by default, measure parity, migrate profiles and drive
22
+ every engine from one CLI (#101, #102, #103, #104, #105).
23
+
24
+ Launch the installed browser the way a person would, and make every restriction
25
+ opt-in (#101, #103).
26
+
27
+ `launchBrowser()` now starts the installed Chrome (or `channel`/
28
+ `executablePath`) itself and attaches over CDP. The whole command line is
29
+ `--user-data-dir=<fresh temporary profile> --remote-debugging-port=<reserved
30
+ port> about:blank` (a URL in `args` replaces `about:blank`): no `--enable-automation`, no `--disable-blink-features`, so
31
+ `navigator.webdriver` is `false` and Chrome shows neither the "controlled by
32
+ automated test software" nor the unsupported-flag infobar. The temporary
33
+ profile is seeded with the `First Run` sentinel and `Local State` entries that
34
+ keep the "What's new" tab and Microsoft Edge's first-run tab from stealing the
35
+ foreground, and it is deleted on close. `launch: 'engine'` keeps the Playwright/Puppeteer launcher.
36
+
37
+ The debugging port is reserved on loopback and its ownership is proven from the
38
+ browser's own `DevTools listening on` line, so a port lost to another process is
39
+ detected and the launch retried on a new port. Subprocesses are started through
40
+ `command-stream`.
41
+
42
+ Everything the library used to add on its own - `CHROME_ARGS`, the Google API
43
+ key override, the Translate preference, `--start-maximized` and `slowMo: 150` -
44
+ is gone from the defaults and available as named `restrictions` from the shared
45
+ `launch-restrictions.json` (`restrictions: ['legacy-defaults']` restores the old
46
+ switches). The host `process.env` is never modified; restriction environment
47
+ reaches the browser process only. `LAUNCH_MODES`, `LAUNCH_RESTRICTIONS`,
48
+ `LAUNCH_RESTRICTION_PRESETS`, `resolveRestrictions`, `mergeFeatureSwitches`,
49
+ `resolveLaunchExecutable`, the profile-directory helpers,
50
+ `reserveLoopbackPort` and `PortRaceError` are exported.
51
+
52
+ Measure how far a driven browser is from the same browser started by hand
53
+ (#103).
54
+
55
+ `measureParity()` starts the binary the way a person would, reads its
56
+ chrome://version command line and runs the environment probe in it, then does
57
+ the same for the browser Browser Commander launches (or for a `session` you
58
+ pass). Every difference in the command line, in the feature switches and in
59
+ anything a page can read is tagged with the limitations-catalogue entry that
60
+ explains it or marked `requested` when it follows from an option you set; a
61
+ report with an unexplained difference is not `ok`. `parseSwitches`,
62
+ `compareCommandLines`, `classifyDifferences` and `readBrowserVersionPage` are
63
+ exported too. The parity e2e suite measures every installed Chrome-family
64
+ browser (Chrome, Chromium, Edge, Brave), headful and headless, through both
65
+ engines.
66
+
67
+ `--headless` is no longer treated as an AutomationControlled trigger, so a
68
+ headless launch no longer adds `--disable-blink-features=AutomationControlled`:
69
+ Chrome 153 keeps `navigator.webdriver` false for every `--headless` form, and
70
+ the extra switch was the one difference `measureParity()` found headless.
71
+
72
+ Add opt-in profile migration and a no-automation "open in the user's browser"
73
+ mode (#102).
74
+
75
+ The real-browser launch stays clean by default - so signing in with a Google
76
+ account works (a nonexistent address shows "Couldn't find your Google Account",
77
+ not "This browser or app may not be secure"). New APIs close the gap to the
78
+ user's real browser:
79
+
80
+ - `migrateProfile({ from, to, include, domains, targetBrowser })` copies data
81
+ from an installed browser's main profile into a dedicated target profile. It
82
+ is strictly read-only on the source: SQLite databases (`History`, `Top Sites`,
83
+ cookies, Firefox `places.sqlite`) are copied through the SQLite Online Backup
84
+ API so a running browser is never disturbed, and JSON (`Bookmarks`,
85
+ `Preferences`) is copied as is. Data classes are `cookies`, `bookmarks`,
86
+ `history`, `passwords`, `preferences` and `extensions` (`ALL_DATA_CLASSES`).
87
+ Chromium passwords are re-encrypted with the target profile's OS-keystore key;
88
+ Firefox uses its NSS `key4.db` path. The report explains what was skipped and
89
+ why: DBSC-bound Google cookies (`dbsc-bound`), Windows app-bound `v20`
90
+ (`app-bound-v20`), migrated extensions whose `Secure Preferences` MAC cannot be
91
+ forged (`mac-will-not-validate`), and Firefox primary passwords
92
+ (`primary-password-set`).
93
+ - `launchRealBrowser({ migrateFrom })` runs that migration before launch, seeds
94
+ the migrated cookies automatically, and returns a `migration` report on the
95
+ session.
96
+ - `openInUserBrowser(url)` opens a URL in the user's own default browser with no
97
+ automation (macOS `open`, Linux `xdg-open`, Windows `explorer.exe`) for flows that
98
+ only need to show a page, such as an OAuth or CLI web-login screen.
99
+
100
+ `migrateProfile`, `ALL_DATA_CLASSES`, `openInUserBrowser`, `buildOpenCommand`
101
+ and `validateOpenUrl` are exported.
102
+
103
+ Add the `browser-commander` command line and its `serve --stdio` JSON-RPC
104
+ bridge (#104).
105
+
106
+ The package now installs a `browser-commander` binary. Its commands are
107
+ `version`, `launch [--keep-open]`, `open`, `goto`, `click`, `fill`, `eval`,
108
+ `screenshot`, `pdf`, `trace start|stop|view`, `cookies import`,
109
+ `profile migrate`, `doctor`, `run <script.json>` and `serve --stdio`. Each
110
+ command prints exactly one JSON document to stdout. It exits `0` on success,
111
+ `1` on error, `2` when `doctor` finds an unlisted difference, and `64` on a
112
+ usage error. Page commands either attach to a browser with `--cdp-endpoint` or
113
+ launch a temporary one. The Rust and Python CLIs follow the same contract,
114
+ `docs/cli-and-bridge.md`.
115
+
116
+ `run` and `serve --stdio` share one dispatcher. It has the high-level methods
117
+ (`session.launch`, `page.goto`, …) and generic handle methods
118
+ (`handle.root`, `handle.call`, `handle.get`, `handle.describe`,
119
+ `handle.dispose`, `events.subscribe`/`unsubscribe`). The handle methods reach
120
+ every public Playwright and Puppeteer method, and an e2e suite checks this
121
+ against the engines' shipped `.d.ts` files. Values that are not JSON are
122
+ tagged (`$handle`, `$binary`, `$function`, `$date`, …), and handle ids are
123
+ deterministic. The shared CLI contract checks equivalent results through all
124
+ three language entry points. The Rust crate's existing 27-operation
125
+ `node_engine_bridge.js` remains available for its typed adapter; callers can
126
+ use the generic CLI bridge to reach methods beyond that adapter.
127
+
128
+ `createCdpSession(page)` and `commander.createCdpSession()` return one raw CDP
129
+ surface (`send`, `on`, `once`, `off`, `detach`) for Playwright and Puppeteer
130
+ pages. The cookie credential tools now run through `command-stream`, like the
131
+ browser launch.
132
+
133
+ Add a `selenium` engine that drives Chrome and Firefox over W3C WebDriver and
134
+ WebDriver BiDi (#104).
135
+
136
+ `selenium-webdriver` is an optional peer dependency. `makeBrowserCommander()`
137
+ detects a selenium `WebDriver` and runs every commander operation through a
138
+ Puppeteer-shaped `WebDriverPage` facade and a `SeleniumAdapter` that uses
139
+ WebDriver's own Element Click, Element Clear and Element Send Keys. PDFs come
140
+ from the W3C Print Page command; options it cannot honour (header/footer
141
+ templates, `preferCSSPageSize`) are refused rather than dropped.
142
+
143
+ `launchWebDriver({ browser, executablePath, driverPath, headless, userDataDir,
144
+ bidi })` finds a driver (`driverPath`, then `PATH`, then the Selenium Manager
145
+ bundled with `selenium-webdriver`), starts it on a reserved loopback port, waits
146
+ for `/status`, and opens a session with a temporary profile that `close()`
147
+ removes. Chrome gets the real-browser command line and all 19 switches
148
+ chromedriver would add on its own are excluded, so `navigator.webdriver` is
149
+ `false` headful and headless (measured with Chrome and chromedriver 153).
150
+ `connectWebDriver({ serverUrl, capabilities })` opens a session on a server that
151
+ is already running.
152
+
153
+ With `bidi: true` the session asks for `webSocketUrl`, and console, dialog,
154
+ request and frame-navigation events, preload scripts and full-page screenshots
155
+ work through WebDriver BiDi. The adapter adds `onConsoleMessage()`,
156
+ `onNavigation()`, `onBidiEvent()` and `navigate()` helpers. Media emulation,
157
+ fingerprint overrides and managed downloads need CDP and throw a clear error on
158
+ this engine. Four new shared limitations (`webdriver-driver-launch-switches`,
159
+ `webdriver-no-cdp-emulation`, `webdriver-classic-has-no-events`,
160
+ `webdriver-print-options`) record the gaps for JavaScript, Python and Rust.
161
+
3
162
  ## 0.20.0
4
163
 
5
164
  ### Minor Changes
package/README.md CHANGED
@@ -8,7 +8,7 @@ A universal browser automation library for JavaScript/TypeScript that supports b
8
8
  npm install browser-commander
9
9
  ```
10
10
 
11
- You'll also need either Playwright or Puppeteer:
11
+ You'll also need Playwright, Puppeteer or Selenium WebDriver:
12
12
 
13
13
  ```bash
14
14
  # With Playwright
@@ -16,6 +16,9 @@ npm install playwright
16
16
 
17
17
  # Or with Puppeteer
18
18
  npm install puppeteer
19
+
20
+ # Or with Selenium WebDriver (W3C WebDriver + WebDriver BiDi)
21
+ npm install selenium-webdriver
19
22
  ```
20
23
 
21
24
  ## Documentation
@@ -93,26 +96,31 @@ await commander.destroy();
93
96
  await browser.close();
94
97
  ```
95
98
 
96
- To reuse an installed Chrome-family browser instead of downloading the
97
- engine's bundled Chromium, select a browser channel or provide its executable
98
- path. Both options are passed directly to Playwright or Puppeteer:
99
+ `launchBrowser()` starts the installed Chrome the way a person would and then
100
+ attaches to it. The whole command line is
101
+ `--user-data-dir=<fresh temporary profile> --remote-debugging-port=<reserved port> about:blank`:
102
+ no automation switches, so `navigator.webdriver` is `false`, there is no
103
+ "controlled by automated test software" or unsupported-flag infobar, and
104
+ extensions, sync, translation and every other browser feature behave as in a
105
+ hand-started Chrome. The temporary profile is deleted by `browser.close()`.
99
106
 
100
107
  ```javascript
101
108
  const { browser, page } = await launchBrowser({
102
109
  engine: 'playwright',
103
- channel: 'chrome',
104
- // Or: executablePath: '/usr/bin/google-chrome',
105
- headless: true,
110
+ channel: 'msedge', // or executablePath: '/usr/bin/google-chrome'
111
+ userDataDir: '/tmp/my-profile', // keep the profile instead of a temporary one
106
112
  extraArgs: ['--lang=en-US'],
107
- // Per-flag escape hatch; `args` remains a compatible append-only alias.
108
- ignoreDefaultArgs: ['--disable-infobars'],
113
+ // Opt-in restrictions, by name (see the table linked below).
114
+ restrictions: ['no-sync', 'no-translate'],
109
115
  });
110
116
  ```
111
117
 
112
- Browser Commander applies the documented
113
- [automation-friendly launch defaults](../docs/feature-parity.md#automation-friendly-launch-defaults),
114
- including `--password-store=basic` to avoid an additional OS credential
115
- dialog. `ignoreDefaultArgs: true` omits every Browser Commander default.
118
+ Every switch Browser Commander used to add on its own is available as a named
119
+ [launch restriction](../docs/feature-parity.md#launch-command-line-and-opt-in-restrictions);
120
+ `restrictions: ['legacy-defaults']` restores the old `CHROME_ARGS`. Pass
121
+ `launch: 'engine'` to use Playwright's `launchPersistentContext()` or
122
+ `puppeteer.launch()` instead; the engine then adds its own switches, and
123
+ `ignoreDefaultArgs` removes them.
116
124
 
117
125
  Attach to a Chrome-family browser that is already listening for CDP connections:
118
126
 
@@ -127,11 +135,13 @@ const commander = makeBrowserCommander({ page });
127
135
  ```
128
136
 
129
137
  Attachment cannot change the existing process's launch arguments. Start it
130
- with a loopback remote-debugging port, a dedicated `--user-data-dir`, and the
131
- automation-friendly defaults above, or use `launchRealBrowser()`.
138
+ with a fixed `--remote-debugging-port` and a dedicated `--user-data-dir`, or
139
+ let `launchBrowser()`/`launchRealBrowser()` start it.
132
140
 
133
- `launchRealBrowser()` can find and start a genuine installed Chrome,
134
- Edge, Brave, or Chromium with a loopback CDP endpoint and then attach to it. It
141
+ `launchRealBrowser()` is the lower-level helper behind the default launch: it
142
+ finds and starts an installed Chrome, Edge, Brave, or Chromium on a reserved
143
+ loopback port, confirms from the browser's own `DevTools listening on` line
144
+ that the port is really its own (retrying on a port race), and attaches. It
135
145
  always uses a dedicated profile; Chrome 136 and newer do not honor remote
136
146
  debugging switches for the default profile. See the
137
147
  [Chrome remote-debugging security change](https://developer.chrome.com/blog/remote-debugging-port).
@@ -144,7 +154,7 @@ const connection = await launchRealBrowser({
144
154
  channel: 'chrome',
145
155
  userDataDir: '/tmp/my-automation-profile',
146
156
  extraArgs: ['--lang=en-US'],
147
- ignoreDefaultArgs: ['--disable-infobars'],
157
+ restrictions: ['no-default-browser-check'],
148
158
  seedCookies: [
149
159
  { name: 'session', value: 'saved', url: 'https://example.com' },
150
160
  ],
@@ -188,8 +198,8 @@ scripts touch Keychain, libsecret/KWallet, or DPAPI at most once per TTL window.
188
198
  Set `refresh: true` to force a new read, customize `cache.dir`/`ttlMinutes`, or
189
199
  set `cache: false` to opt out of disk caching.
190
200
  `launchAndConnectRealBrowser()` remains available as a descriptive alias.
191
- Remote-debugging, loopback, and profile arguments remain managed even when all
192
- optional defaults are ignored; `headless: true` adds `--headless=new`.
201
+ The remote-debugging and profile switches are always managed;
202
+ `headless: true` adds `--headless=new`.
193
203
 
194
204
  Reuse a saved authenticated session by passing Playwright-compatible storage
195
205
  state as a JSON file path or object. Cookies and localStorage are restored for
@@ -371,17 +381,27 @@ commander.pageTrigger({
371
381
  ### launchBrowser(options)
372
382
 
373
383
  ```javascript
374
- const { browser, page } = await launchBrowser({
384
+ const { browser, page, close } = await launchBrowser({
375
385
  engine: 'playwright', // 'playwright' or 'puppeteer'
386
+ launch: 'real', // 'real' (start the installed browser, attach) or 'engine'
387
+ channel: 'chrome', // or executablePath; defaults to the installed Chrome
376
388
  headless: false, // Run in headless mode
377
- userDataDir: '~/.hh-apply/playwright-data', // Browser profile directory
378
- slowMo: 150, // Slow down operations (ms)
389
+ userDataDir: undefined, // Keep a profile; default is a fresh temporary one
390
+ restrictions: [], // Opt-in restrictions such as 'no-extensions', 'no-sync'
391
+ slowMo: 0, // Slow down operations (ms)
379
392
  verbose: false, // Enable debug logging
380
- args: ['--no-sandbox', '--disable-setuid-sandbox'], // Custom Chrome args to append
393
+ args: ['--no-sandbox'], // Custom Chrome args to append
381
394
  storageState: './session-state.json', // Saved cookies and localStorage, as a path or object
382
395
  });
383
396
  ```
384
397
 
398
+ With Playwright, `browser` is the attached browser's default context, so
399
+ `browser.pages()`, `browser.newPage()` and `browser.close()` work as they did
400
+ with the persistent context. `close()` (and `browser.close()`) closes the
401
+ browser and deletes a temporary profile. The result also carries `launch`,
402
+ `userDataDir`, `temporaryProfile` and the exact `args` the browser was started
403
+ with.
404
+
385
405
  The `args` option allows passing custom Chrome arguments, which is useful for headless server environments (Docker, CI/CD) that require flags like `--no-sandbox`.
386
406
 
387
407
  The `storageState` option accepts a Playwright-compatible JSON path or object.
@@ -414,15 +434,84 @@ Playwright accepts `timeout` and Puppeteer accepts `protocolTimeout`.
414
434
  Start an installed browser and connect through `connectBrowser()`. Use
415
435
  `channel` (`chrome`, `chrome-beta`, `chrome-dev`, `chrome-canary`, `msedge`,
416
436
  `msedge-beta`, `msedge-dev`, `msedge-canary`, `brave`, or `chromium`) or an
417
- explicit `executablePath`. The helper defaults to
418
- a managed directory under `~/.browser-commander/real-browser/`, rejects known
419
- default browser-profile paths, protects its remote-debugging arguments, and
420
- returns the spawned `browserProcess`, resolved `cdpEndpoint`, executable path,
421
- and profile path alongside `{ browser, page }`.
437
+ explicit `executablePath`. Without `userDataDir` it creates a fresh temporary
438
+ profile that `close()` deletes; it rejects known default browser-profile paths,
439
+ protects its remote-debugging switches, reserves a loopback port (or uses
440
+ `remoteDebuggingPort`, never `0`), and returns the spawned `browserProcess`,
441
+ `cdpEndpoint`, `remoteDebuggingPort`, `executablePath`, `userDataDir`,
442
+ `temporaryProfile` and `args` alongside `{ browser, page, close }`.
422
443
  `launchAndConnectRealBrowser()` is an alias with identical behavior.
423
- `extraArgs` appends custom switches, while `ignoreDefaultArgs` accepts a list
424
- of defaults to omit (or `true` to omit all optional defaults). The older
425
- `args` append option remains supported.
444
+ `restrictions` opts into named restrictions, `args`/`extraArgs` append custom
445
+ switches, and `env` adds environment variables for the browser process only.
446
+
447
+ The launch is clean by default, so signing in with a Google account works
448
+ (a nonexistent address shows "Couldn't find your Google Account" rather than
449
+ "This browser or app may not be secure"). Pass `migrateFrom` to opt into a
450
+ read-only migration from an installed browser's main profile before the launch;
451
+ the migrated cookies are seeded automatically and the returned session carries a
452
+ `migration` report (see `migrateProfile()` below):
453
+
454
+ ```javascript
455
+ const session = await launchRealBrowser({
456
+ channel: 'chrome',
457
+ userDataDir, // dedicated; clean by default
458
+ migrateFrom: {
459
+ browser: 'chrome', // chrome | edge | brave | chromium | firefox
460
+ profile: 'Default',
461
+ include: ['cookies', 'bookmarks', 'history', 'passwords', 'preferences'],
462
+ domains: ['npmjs.com', 'github.com'], // optional cookie filter
463
+ },
464
+ });
465
+ console.log(session.migration.migrated, session.migration.skipped);
466
+ ```
467
+
468
+ ### launchWebDriver(options) / connectWebDriver(options)
469
+
470
+ The `selenium` engine drives Chrome or Firefox over W3C WebDriver through
471
+ `selenium-webdriver` (an optional peer dependency). `launchWebDriver()` finds a
472
+ driver (`driverPath`, then `PATH`, then the Selenium Manager bundled with
473
+ `selenium-webdriver`, which downloads a chromedriver/geckodriver matching the
474
+ browser), starts it on a reserved loopback port, waits for `/status`, and opens
475
+ a session with a fresh temporary profile that `close()` deletes:
476
+
477
+ ```javascript
478
+ import { launchWebDriver, makeBrowserCommander } from 'browser-commander';
479
+
480
+ const { driver, page, close } = await launchWebDriver({
481
+ browser: 'chrome', // or 'firefox'
482
+ headless: false,
483
+ bidi: true, // WebDriver BiDi: console/dialog/network events, preload scripts
484
+ // executablePath, driverPath, userDataDir, args, restrictions, env
485
+ });
486
+ const commander = makeBrowserCommander({ page }); // commander.engine === 'selenium'
487
+
488
+ page.on('console', (message) => console.log(message.text())); // needs bidi
489
+ await commander.goto({ url: 'https://example.com' });
490
+ await commander.fill({ selector: 'input[name="q"]', text: 'hello' });
491
+ await commander.click({ selector: 'button[type="submit"]' });
492
+ const pdf = await page.pdf({ format: 'A4' }); // W3C Print Page
493
+ await close();
494
+ ```
495
+
496
+ `page` is a `WebDriverPage`: a Puppeteer-shaped facade over the `driver`
497
+ (`evaluate`, `$`, `$$`, `keyboard`, `mouse`, `screenshot`, `pdf`, `cookies`,
498
+ `on`), so the rest of Browser Commander runs unchanged. `makeBrowserCommander()`
499
+ also accepts a bare selenium `WebDriver` and wraps it. On the adapter
500
+ (`createEngineAdapter(page, 'selenium')`), `onConsoleMessage(handler)`,
501
+ `onNavigation(handler)`, `onBidiEvent(method, handler)` and
502
+ `navigate(url, {wait})` expose BiDi `log.entryAdded` and `browsingContext`
503
+ directly; they throw when the session has no BiDi.
504
+
505
+ Chrome gets the same command line as `launchRealBrowser()`
506
+ (`--user-data-dir` and a fixed `--remote-debugging-port`), and every switch
507
+ chromedriver would add on its own - `--enable-automation`,
508
+ `--remote-debugging-port=0`, `--password-store=basic`, `--test-type=webdriver`
509
+ and 16 more - is excluded, so `navigator.webdriver` is `false` headful and
510
+ headless. `connectWebDriver({ serverUrl, capabilities })` opens a session on a
511
+ server that is already running (a Selenium Grid, a cloud provider); pass
512
+ `capabilities: { webSocketUrl: true }` for BiDi. Media emulation, fingerprint
513
+ overrides and managed downloads need CDP and throw on this engine; see
514
+ [docs/feature-parity.md](../docs/feature-parity.md#webdriver-engine).
426
515
 
427
516
  ### listBrowserProfiles(options)
428
517
 
@@ -466,6 +555,75 @@ cookies is acceptable.
466
555
  Treat imported cookies like passwords: keep cache directories private, use a
467
556
  short TTL, never commit them, and seed only a dedicated automation profile.
468
557
 
558
+ ### migrateProfile(options)
559
+
560
+ Copy data from an installed browser's main profile into a dedicated target
561
+ profile directory (usually the `Default` folder of a `launchRealBrowser()`
562
+ `userDataDir`). This is the same migration `launchRealBrowser({ migrateFrom })`
563
+ runs, exposed directly. It is **opt-in and strictly read-only** on the source:
564
+ SQLite databases (`History`, `Top Sites`, cookies, Firefox `places.sqlite`) are
565
+ copied through the SQLite Online Backup API so a running browser is never
566
+ disturbed, and JSON files (`Bookmarks`, `Preferences`) are copied as is.
567
+
568
+ ```javascript
569
+ import { migrateProfile } from 'browser-commander';
570
+
571
+ const report = await migrateProfile({
572
+ from: { browser: 'chrome', profile: 'Default' },
573
+ to: '/path/to/dedicated/User Data/Default',
574
+ targetBrowser: 'chrome', // launching channel, for key derivation
575
+ include: [
576
+ 'cookies',
577
+ 'bookmarks',
578
+ 'history',
579
+ 'passwords',
580
+ 'preferences',
581
+ 'extensions',
582
+ ],
583
+ domains: ['github.com'], // optional cookie filter
584
+ });
585
+ ```
586
+
587
+ The report is `{ source, target, migrated, skipped, warnings, cookies }`:
588
+ `migrated` counts each data class, `cookies` holds the seedable Playwright-shape
589
+ cookies, and `skipped`/`warnings` explain everything that could not be migrated
590
+ verbatim, for example:
591
+
592
+ - **DBSC-bound Google cookies** (`__Secure-1PSIDTS`/`3PSIDTS`/`SIDTS`) are
593
+ reported `dbsc-bound`: their Device Bound Session Credentials key cannot leave
594
+ the source profile, so those sessions expire quickly and are not copied.
595
+ - **Windows app-bound `v20`** passwords/cookies are reported `app-bound-v20`
596
+ because they require the browser's privileged elevation service.
597
+ - **Migrated extensions** are reported `mac-will-not-validate`: Chromium's
598
+ `Secure Preferences` MAC cannot be forged externally, so migrated extensions
599
+ are likely disabled until re-enabled in the browser.
600
+ - **Firefox with a primary password** is reported `primary-password-set`.
601
+
602
+ Data classes: `cookies`, `bookmarks`, `history`, `passwords`, `preferences`,
603
+ `extensions` (exported as `ALL_DATA_CLASSES`). Chromium sources
604
+ (`chrome`/`edge`/`brave`/`chromium`) re-encrypt passwords with the target
605
+ profile's OS-keystore key; Firefox uses its own NSS (`key4.db`) path for
606
+ `cookies.sqlite`, `places.sqlite` and `logins.json`.
607
+
608
+ ### openInUserBrowser(url)
609
+
610
+ Open a URL in the user's own default browser with **no automation** - no CDP,
611
+ no dedicated profile, no `navigator.webdriver`. Use it when a consumer only has
612
+ to show a page where the user is already signed in (an OAuth consent screen or a
613
+ CLI web-login page). It shells out to the platform opener (macOS `open`, Linux
614
+ `xdg-open`, Windows `explorer.exe`) after validating the URL:
615
+
616
+ ```javascript
617
+ import { openInUserBrowser } from 'browser-commander';
618
+
619
+ await openInUserBrowser('https://github.com/login/device');
620
+ ```
621
+
622
+ Only a browser-safe scheme set is accepted (`http:`, `https:`, `file:`, `ftp:`,
623
+ `about:`, `chrome:`, `edge:`, `view-source:`); `validateOpenUrl()` and
624
+ `buildOpenCommand()` are exported for callers that want to validate or build the
625
+ command separately.
626
+
469
627
  ### saveStorageState(page, filePath)
470
628
 
471
629
  Save the current cookies and localStorage in Playwright's portable storage
@@ -852,6 +1010,32 @@ await commander.page.pdf({ format: 'A4' });
852
1010
 
853
1011
  This is the **official extensibility mechanism** while awaiting browser-commander to add first-class support for these APIs. Please [report missing APIs](https://github.com/link-foundation/browser-commander/issues) so they can be added.
854
1012
 
1013
+ ## Command Line
1014
+
1015
+ The package installs a `browser-commander` command. The Rust and Python packages ship the same command, and the contract all three follow is [docs/cli-and-bridge.md](../docs/cli-and-bridge.md). Every command prints exactly one JSON document to stdout. The exit code is `0` on success and `1` on error. `doctor` exits `2` when it finds an unlisted difference, and a usage error exits `64`.
1016
+
1017
+ ```bash
1018
+ npx browser-commander version
1019
+ npx browser-commander goto https://example.com --headless
1020
+
1021
+ # Keep one browser running and drive it from later commands
1022
+ npx browser-commander launch --keep-open &
1023
+ npx browser-commander fill '#q' 'hello' --cdp-endpoint http://127.0.0.1:9222
1024
+ npx browser-commander eval 'document.title' --cdp-endpoint http://127.0.0.1:9222
1025
+
1026
+ # Run a JSON command script (see tests/cli-contract/basic.json)
1027
+ npx browser-commander run script.json --engine puppeteer
1028
+
1029
+ # JSON-RPC 2.0 over stdin/stdout, one message per line
1030
+ echo '{"jsonrpc":"2.0","id":1,"method":"version"}' | npx browser-commander serve --stdio
1031
+ ```
1032
+
1033
+ The other commands are `open`, `click`, `screenshot`, `pdf`, `trace start|stop|view`, `cookies import`, `profile migrate` and `doctor`. Use the `=` form for values that start with `--`, for example `--arg=--lang=de`.
1034
+
1035
+ `serve --stdio` exposes the high-level methods (`session.launch`, `page.goto`, …). It also exposes generic handle methods (`handle.root`, `handle.call`, `handle.get`, `handle.describe`, `events.subscribe`, …), which reach every public Playwright and Puppeteer method. The Rust and Python ports use this bridge for the engines they do not implement natively.
1036
+
1037
+ For raw Chrome DevTools Protocol access from JavaScript, `createCdpSession(page)` (or `commander.createCdpSession()`) returns the same `send`/`on`/`once`/`off`/`detach` surface for Playwright and Puppeteer pages.
1038
+
855
1039
  ## Debugging
856
1040
 
857
1041
  Enable verbose mode for detailed logs:
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The `browser-commander` command (issue #104). See docs/cli-and-bridge.md.
4
+ *
5
+ * Stdout carries exactly one JSON document (or JSON-RPC lines for
6
+ * `serve --stdio`), so library logging is moved to stderr.
7
+ */
8
+ import { runCli } from '../src/cli/main.js';
9
+
10
+ for (const method of ['log', 'info', 'debug']) {
11
+ console[method] = (...args) => console.error(...args);
12
+ }
13
+
14
+ const exitCode = await runCli(process.argv.slice(2));
15
+ // Engines can keep sockets or timers alive; exit once stdout is flushed.
16
+ process.stdout.write('', () => process.exit(exitCode));
package/eslint.config.js CHANGED
@@ -131,6 +131,16 @@ export default [
131
131
  'no-unused-vars': 'off',
132
132
  },
133
133
  },
134
+ {
135
+ // The environment probe is source text too: `measureParity()` wraps it as
136
+ // `(<file contents>)()` for `page.evaluate` and a plain `<script>` tag, and
137
+ // its length is the surface of the browser API it measures.
138
+ files: ['src/parity/probe.js'],
139
+ rules: {
140
+ 'no-unused-vars': 'off',
141
+ 'max-lines-per-function': 'off',
142
+ },
143
+ },
134
144
  {
135
145
  // Test files have different requirements
136
146
  files: ['tests/**/*.js', '**/*.test.js'],
@@ -1,18 +1,20 @@
1
- import React, { useState } from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
+
3
+ const createEmptyFormData = () => ({
4
+ name: '',
5
+ email: '',
6
+ password: '',
7
+ bio: '',
8
+ gender: '',
9
+ interests: [],
10
+ country: '',
11
+ newsletter: false,
12
+ terms: false,
13
+ });
2
14
 
3
15
  function App() {
4
16
  // Form state
5
- const [formData, setFormData] = useState({
6
- name: '',
7
- email: '',
8
- password: '',
9
- bio: '',
10
- gender: '',
11
- interests: [],
12
- country: '',
13
- newsletter: false,
14
- terms: false,
15
- });
17
+ const [formData, setFormData] = useState(createEmptyFormData);
16
18
 
17
19
  const [submitResult, setSubmitResult] = useState(null);
18
20
  const [counter, setCounter] = useState(0);
@@ -23,6 +25,15 @@ function App() {
23
25
  const [dynamicContent, setDynamicContent] = useState(null);
24
26
  const [isLoading, setIsLoading] = useState(false);
25
27
 
28
+ useEffect(() => {
29
+ if (!showModal) return;
30
+ const closeOnEscape = (event) => {
31
+ if (event.key === 'Escape') setShowModal(false);
32
+ };
33
+ document.addEventListener('keydown', closeOnEscape);
34
+ return () => document.removeEventListener('keydown', closeOnEscape);
35
+ }, [showModal]);
36
+
26
37
  const handleInputChange = (e) => {
27
38
  const { name, value, type, checked } = e.target;
28
39
 
@@ -56,17 +67,7 @@ function App() {
56
67
  };
57
68
 
58
69
  const handleReset = () => {
59
- setFormData({
60
- name: '',
61
- email: '',
62
- password: '',
63
- bio: '',
64
- gender: '',
65
- interests: [],
66
- country: '',
67
- newsletter: false,
68
- terms: false,
69
- });
70
+ setFormData(createEmptyFormData());
70
71
  setSubmitResult(null);
71
72
  };
72
73
 
@@ -0,0 +1,14 @@
1
+ /* global chrome, WebSocket */
2
+ import { createRelayClient } from './relay-handler.js';
3
+
4
+ /**
5
+ * Service worker of the Browser Commander Relay extension (issue #102,
6
+ * addendum, mode 2). All logic lives in relay-handler.js; this file only
7
+ * wires it to the real `chrome` namespace. `start()` runs synchronously at top
8
+ * level because MV3 only delivers events to listeners registered then.
9
+ */
10
+ createRelayClient({
11
+ chrome,
12
+ WebSocket,
13
+ userAgent: navigator.userAgent,
14
+ }).start();
@@ -0,0 +1,12 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "Browser Commander Relay",
4
+ "version": "0.20.0",
5
+ "description": "Lets Browser Commander on this computer drive tabs of this browser through chrome.debugger, over a loopback-only connection.",
6
+ "minimum_chrome_version": "116",
7
+ "permissions": ["debugger", "tabs", "alarms", "storage"],
8
+ "background": {
9
+ "service_worker": "background.js",
10
+ "type": "module"
11
+ }
12
+ }