browser-commander 0.14.0 → 0.15.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b3560c8: Apply automation-friendly Chromium launch defaults, including `--password-store=basic`, and add append/opt-out launch argument controls.
8
+
3
9
  ## 0.14.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -103,9 +103,17 @@ const { browser, page } = await launchBrowser({
103
103
  channel: 'chrome',
104
104
  // Or: executablePath: '/usr/bin/google-chrome',
105
105
  headless: true,
106
+ extraArgs: ['--lang=en-US'],
107
+ // Per-flag escape hatch; `args` remains a compatible append-only alias.
108
+ ignoreDefaultArgs: ['--disable-infobars'],
106
109
  });
107
110
  ```
108
111
 
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.
116
+
109
117
  Attach to a Chrome-family browser that is already listening for CDP connections:
110
118
 
111
119
  ```javascript
@@ -118,6 +126,10 @@ const { browser, page } = await connectBrowser({
118
126
  const commander = makeBrowserCommander({ page });
119
127
  ```
120
128
 
129
+ 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()`.
132
+
121
133
  `launchRealBrowser()` can find and start a genuine installed Chrome,
122
134
  Edge, Brave, or Chromium with a loopback CDP endpoint and then attach to it. It
123
135
  always uses a dedicated profile; Chrome 136 and newer do not honor remote
@@ -131,6 +143,8 @@ const connection = await launchRealBrowser({
131
143
  engine: 'puppeteer',
132
144
  channel: 'chrome',
133
145
  userDataDir: '/tmp/my-automation-profile',
146
+ extraArgs: ['--lang=en-US'],
147
+ ignoreDefaultArgs: ['--disable-infobars'],
134
148
  seedCookies: [
135
149
  { name: 'session', value: 'saved', url: 'https://example.com' },
136
150
  ],
@@ -141,6 +155,8 @@ await connection.browser.close();
141
155
  Cookie seeding copies only cookies you explicitly provide; the helper does not
142
156
  read, decrypt, or expose cookies from a browser's default profile.
143
157
  `launchAndConnectRealBrowser()` remains available as a descriptive alias.
158
+ Remote-debugging, loopback, and profile arguments remain managed even when all
159
+ optional defaults are ignored; `headless: true` adds `--headless=new`.
144
160
 
145
161
  Reuse a saved authenticated session by passing Playwright-compatible storage
146
162
  state as a JSON file path or object. Cookies and localStorage are restored for
@@ -371,6 +387,9 @@ default browser-profile paths, protects its remote-debugging arguments, and
371
387
  returns the spawned `browserProcess`, resolved `cdpEndpoint`, executable path,
372
388
  and profile path alongside `{ browser, page }`.
373
389
  `launchAndConnectRealBrowser()` is an alias with identical behavior.
390
+ `extraArgs` appends custom switches, while `ignoreDefaultArgs` accepts a list
391
+ of defaults to omit (or `true` to omit all optional defaults). The older
392
+ `args` append option remains supported.
374
393
 
375
394
  ### saveStorageState(page, filePath)
376
395
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-commander",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Universal browser automation library that supports both Playwright and Puppeteer with a unified API",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -1,3 +1,47 @@
1
+ import { CHROME_ARGS } from '../core/constants.js';
2
+
3
+ function stringArray(value, name) {
4
+ if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
5
+ throw new TypeError(`${name} must be an array of strings`);
6
+ }
7
+ return value;
8
+ }
9
+
10
+ /** Resolve Browser Commander defaults, compatibility args, and extra args. */
11
+ export function resolveChromeArgs({
12
+ args = [],
13
+ extraArgs = [],
14
+ ignoreDefaultArgs = [],
15
+ } = {}) {
16
+ stringArray(args, 'args');
17
+ stringArray(extraArgs, 'extraArgs');
18
+ if (
19
+ typeof ignoreDefaultArgs !== 'boolean' &&
20
+ !Array.isArray(ignoreDefaultArgs)
21
+ ) {
22
+ throw new TypeError('ignoreDefaultArgs must be a boolean or string array');
23
+ }
24
+ if (Array.isArray(ignoreDefaultArgs)) {
25
+ stringArray(ignoreDefaultArgs, 'ignoreDefaultArgs');
26
+ }
27
+
28
+ const normalizedIgnoreDefaultArgs =
29
+ ignoreDefaultArgs === false ? [] : ignoreDefaultArgs;
30
+ const ignored = new Set(
31
+ normalizedIgnoreDefaultArgs === true
32
+ ? CHROME_ARGS
33
+ : normalizedIgnoreDefaultArgs
34
+ );
35
+ return {
36
+ args: [
37
+ ...CHROME_ARGS.filter((argument) => !ignored.has(argument)),
38
+ ...args,
39
+ ...extraArgs,
40
+ ],
41
+ ignoreDefaultArgs: normalizedIgnoreDefaultArgs,
42
+ };
43
+ }
44
+
1
45
  function setBrowserSelectionOptions(options, { channel, executablePath }) {
2
46
  if (channel !== undefined) {
3
47
  options.channel = channel;
@@ -15,14 +59,21 @@ export function buildPlaywrightLaunchOptions({
15
59
  colorScheme,
16
60
  channel,
17
61
  executablePath,
62
+ ignoreDefaultArgs = [],
18
63
  }) {
64
+ const normalizedIgnoreDefaultArgs =
65
+ ignoreDefaultArgs === false ? [] : ignoreDefaultArgs;
66
+ const engineIgnoreDefaultArgs =
67
+ normalizedIgnoreDefaultArgs === true
68
+ ? true
69
+ : [...new Set(['--enable-automation', ...normalizedIgnoreDefaultArgs])];
19
70
  const options = {
20
71
  headless,
21
72
  slowMo,
22
73
  chromiumSandbox: true,
23
74
  viewport: null,
24
75
  args: chromeArgs,
25
- ignoreDefaultArgs: ['--enable-automation'],
76
+ ignoreDefaultArgs: engineIgnoreDefaultArgs,
26
77
  };
27
78
 
28
79
  if (colorScheme !== undefined) {
@@ -38,14 +89,21 @@ export function buildPuppeteerLaunchOptions({
38
89
  userDataDir,
39
90
  channel,
40
91
  executablePath,
92
+ ignoreDefaultArgs = [],
41
93
  }) {
42
- return setBrowserSelectionOptions(
43
- {
44
- headless,
45
- defaultViewport: null,
46
- args: ['--start-maximized', ...chromeArgs],
47
- userDataDir,
48
- },
49
- { channel, executablePath }
50
- );
94
+ const normalizedIgnoreDefaultArgs =
95
+ ignoreDefaultArgs === false ? [] : ignoreDefaultArgs;
96
+ const options = {
97
+ headless,
98
+ defaultViewport: null,
99
+ args: ['--start-maximized', ...chromeArgs],
100
+ userDataDir,
101
+ };
102
+ if (
103
+ normalizedIgnoreDefaultArgs === true ||
104
+ normalizedIgnoreDefaultArgs.length > 0
105
+ ) {
106
+ options.ignoreDefaultArgs = normalizedIgnoreDefaultArgs;
107
+ }
108
+ return setBrowserSelectionOptions(options, { channel, executablePath });
51
109
  }
@@ -1,11 +1,11 @@
1
1
  import path from 'path';
2
2
  import os from 'os';
3
- import { CHROME_ARGS } from '../core/constants.js';
4
3
  import { disableTranslateInPreferences } from '../core/preferences.js';
5
4
  import { emulateMedia } from './media.js';
6
5
  import {
7
6
  buildPlaywrightLaunchOptions,
8
7
  buildPuppeteerLaunchOptions,
8
+ resolveChromeArgs,
9
9
  } from './launch-options.js';
10
10
  import {
11
11
  loadStorageState,
@@ -22,6 +22,8 @@ import {
22
22
  * @param {number} options.slowMo - Slow down operations by ms (default: 150 for Playwright, 0 for Puppeteer)
23
23
  * @param {boolean} options.verbose - Enable verbose logging (default: false)
24
24
  * @param {string[]} options.args - Custom Chrome arguments to append to the default CHROME_ARGS
25
+ * @param {string[]} [options.extraArgs] - Additional Chrome arguments appended after legacy args
26
+ * @param {boolean|string[]} [options.ignoreDefaultArgs] - Browser Commander defaults to omit, or true for all
25
27
  * @param {string|null} [options.colorScheme] - Emulate color scheme: 'light', 'dark', 'no-preference', or null to reset
26
28
  * @param {string} [options.channel] - Installed browser channel, such as 'chrome', 'chrome-beta', or 'msedge'
27
29
  * @param {string} [options.executablePath] - Explicit path to a Chrome or Chromium executable
@@ -36,14 +38,20 @@ export async function launchBrowser(options = {}) {
36
38
  slowMo = engine === 'playwright' ? 150 : 0,
37
39
  verbose = false,
38
40
  args = [],
41
+ extraArgs = [],
42
+ ignoreDefaultArgs = [],
39
43
  colorScheme,
40
44
  channel,
41
45
  executablePath,
42
46
  storageState,
43
47
  } = options;
44
48
 
45
- // Combine default CHROME_ARGS with custom args
46
- const chromeArgs = [...CHROME_ARGS, ...args];
49
+ const resolvedChromeArgs = resolveChromeArgs({
50
+ args,
51
+ extraArgs,
52
+ ignoreDefaultArgs,
53
+ });
54
+ const chromeArgs = resolvedChromeArgs.args;
47
55
 
48
56
  if (!['playwright', 'puppeteer'].includes(engine)) {
49
57
  throw new Error(
@@ -77,6 +85,7 @@ export async function launchBrowser(options = {}) {
77
85
  colorScheme,
78
86
  channel,
79
87
  executablePath,
88
+ ignoreDefaultArgs: resolvedChromeArgs.ignoreDefaultArgs,
80
89
  });
81
90
  browser = await chromium.launchPersistentContext(
82
91
  userDataDir,
@@ -96,6 +105,7 @@ export async function launchBrowser(options = {}) {
96
105
  userDataDir,
97
106
  channel,
98
107
  executablePath,
108
+ ignoreDefaultArgs: resolvedChromeArgs.ignoreDefaultArgs,
99
109
  })
100
110
  );
101
111
  const pages = await browser.pages();
@@ -5,6 +5,7 @@ import os from 'node:os';
5
5
  import path from 'node:path';
6
6
 
7
7
  import { connectBrowser } from './connector.js';
8
+ import { resolveChromeArgs } from './launch-options.js';
8
9
 
9
10
  const MANAGED_ARGUMENTS = [
10
11
  '--remote-debugging-address',
@@ -240,6 +241,8 @@ export function buildRealBrowserArgs({
240
241
  remoteDebuggingPort = 0,
241
242
  headless = false,
242
243
  args = [],
244
+ extraArgs = [],
245
+ ignoreDefaultArgs = [],
243
246
  }) {
244
247
  if (
245
248
  !Number.isInteger(remoteDebuggingPort) ||
@@ -250,7 +253,8 @@ export function buildRealBrowserArgs({
250
253
  'remoteDebuggingPort must be an integer from 0 to 65535'
251
254
  );
252
255
  }
253
- const conflictingArgument = args.find((argument) =>
256
+ const customArgs = [...args, ...extraArgs];
257
+ const conflictingArgument = customArgs.find((argument) =>
254
258
  MANAGED_ARGUMENTS.some(
255
259
  (managed) => argument === managed || argument.startsWith(`${managed}=`)
256
260
  )
@@ -261,14 +265,15 @@ export function buildRealBrowserArgs({
261
265
  );
262
266
  }
263
267
 
268
+ const defaultArgs = resolveChromeArgs({ ignoreDefaultArgs }).args;
269
+
264
270
  return [
265
271
  '--remote-debugging-address=127.0.0.1',
266
272
  `--remote-debugging-port=${remoteDebuggingPort}`,
267
273
  `--user-data-dir=${userDataDir}`,
268
- '--no-first-run',
269
- '--no-default-browser-check',
274
+ ...defaultArgs,
270
275
  ...(headless ? ['--headless=new'] : []),
271
- ...args,
276
+ ...customArgs,
272
277
  ];
273
278
  }
274
279
 
@@ -353,6 +358,8 @@ export async function waitForCdpEndpoint({
353
358
  * @param {number} [options.remoteDebuggingPort=0] - Loopback CDP port; zero lets Chrome choose
354
359
  * @param {boolean} [options.headless=false] - Run the installed browser headlessly
355
360
  * @param {string[]} [options.args] - Additional browser arguments
361
+ * @param {string[]} [options.extraArgs] - Additional browser arguments appended after legacy args
362
+ * @param {boolean|string[]} [options.ignoreDefaultArgs] - Browser Commander defaults to omit, or true for all
356
363
  * @param {number} [options.startupTimeout=30000] - CDP readiness timeout in milliseconds
357
364
  * @param {Object[]} [options.seedCookies] - Cookies to seed after connecting
358
365
  * @param {boolean} [options.verbose=false] - Show browser and connection logs
@@ -384,6 +391,8 @@ export async function launchAndConnectRealBrowserWithDependencies(
384
391
  remoteDebuggingPort = 0,
385
392
  headless = false,
386
393
  args = [],
394
+ extraArgs = [],
395
+ ignoreDefaultArgs = [],
387
396
  startupTimeout = 30_000,
388
397
  verbose = false,
389
398
  cdpEndpoint,
@@ -409,6 +418,8 @@ export async function launchAndConnectRealBrowserWithDependencies(
409
418
  remoteDebuggingPort,
410
419
  headless,
411
420
  args,
421
+ extraArgs,
422
+ ignoreDefaultArgs,
412
423
  });
413
424
  const spawnBrowser = dependencies.spawnBrowser ?? spawn;
414
425
  const browserProcess = spawnBrowser(resolvedExecutablePath, browserArgs, {
@@ -5,6 +5,7 @@ export const CHROME_ARGS = [
5
5
  '--disable-session-crashed-bubble',
6
6
  '--hide-crash-restore-bubble',
7
7
  '--disable-infobars',
8
+ '--password-store=basic',
8
9
  '--no-first-run',
9
10
  '--no-default-browser-check',
10
11
  '--disable-crash-restore',
@@ -4,9 +4,64 @@ import assert from 'node:assert';
4
4
  import {
5
5
  buildPlaywrightLaunchOptions,
6
6
  buildPuppeteerLaunchOptions,
7
+ resolveChromeArgs,
7
8
  } from '../../../src/browser/launch-options.js';
8
9
 
9
10
  describe('browser launch options', () => {
11
+ it('applies safe defaults, appends extra arguments, and supports per-flag opt-out', () => {
12
+ const options = resolveChromeArgs({
13
+ args: ['--legacy-arg'],
14
+ extraArgs: ['--lang=en-US'],
15
+ ignoreDefaultArgs: ['--no-default-browser-check'],
16
+ });
17
+
18
+ assert.ok(options.args.includes('--password-store=basic'));
19
+ assert.ok(options.args.includes('--no-first-run'));
20
+ assert.equal(options.args.includes('--no-default-browser-check'), false);
21
+ assert.deepEqual(options.args.slice(-2), ['--legacy-arg', '--lang=en-US']);
22
+ assert.deepEqual(options.ignoreDefaultArgs, ['--no-default-browser-check']);
23
+ });
24
+
25
+ it('can ignore every Browser Commander default', () => {
26
+ const options = resolveChromeArgs({
27
+ extraArgs: ['--lang=en-US'],
28
+ ignoreDefaultArgs: true,
29
+ });
30
+
31
+ assert.deepEqual(options.args, ['--lang=en-US']);
32
+ assert.equal(options.ignoreDefaultArgs, true);
33
+ });
34
+
35
+ it('can opt out of the password-store default specifically', () => {
36
+ const options = resolveChromeArgs({
37
+ ignoreDefaultArgs: ['--password-store=basic'],
38
+ });
39
+
40
+ assert.equal(options.args.includes('--password-store=basic'), false);
41
+ assert.ok(options.args.includes('--no-first-run'));
42
+ });
43
+
44
+ it('forwards ignored defaults to both browser engines', () => {
45
+ const playwright = buildPlaywrightLaunchOptions({
46
+ headless: false,
47
+ slowMo: 150,
48
+ chromeArgs: [],
49
+ ignoreDefaultArgs: ['--no-first-run'],
50
+ });
51
+ const puppeteer = buildPuppeteerLaunchOptions({
52
+ headless: false,
53
+ chromeArgs: [],
54
+ userDataDir: '/tmp/browser-commander-test',
55
+ ignoreDefaultArgs: ['--no-first-run'],
56
+ });
57
+
58
+ assert.deepEqual(playwright.ignoreDefaultArgs, [
59
+ '--enable-automation',
60
+ '--no-first-run',
61
+ ]);
62
+ assert.deepEqual(puppeteer.ignoreDefaultArgs, ['--no-first-run']);
63
+ });
64
+
10
65
  it('forwards channel and executablePath to Playwright', () => {
11
66
  const options = buildPlaywrightLaunchOptions({
12
67
  headless: true,
@@ -44,13 +44,32 @@ describe('launchAndConnectRealBrowser', () => {
44
44
  '--remote-debugging-address=127.0.0.1',
45
45
  '--remote-debugging-port=9333',
46
46
  '--user-data-dir=/tmp/browser-commander-dedicated',
47
+ '--disable-session-crashed-bubble',
48
+ '--hide-crash-restore-bubble',
49
+ '--disable-infobars',
50
+ '--password-store=basic',
47
51
  '--no-first-run',
48
52
  '--no-default-browser-check',
53
+ '--disable-crash-restore',
49
54
  '--headless=new',
50
55
  '--lang=en-US',
51
56
  ]);
52
57
  });
53
58
 
59
+ it('supports additive arguments and per-default opt-out', () => {
60
+ const args = buildRealBrowserArgs({
61
+ userDataDir: '/tmp/browser-commander-dedicated',
62
+ args: ['--legacy-arg'],
63
+ extraArgs: ['--lang=en-US'],
64
+ ignoreDefaultArgs: ['--no-first-run'],
65
+ });
66
+
67
+ assert.ok(args.includes('--password-store=basic'));
68
+ assert.equal(args.includes('--no-first-run'), false);
69
+ assert.ok(args.includes('--no-default-browser-check'));
70
+ assert.deepEqual(args.slice(-2), ['--legacy-arg', '--lang=en-US']);
71
+ });
72
+
54
73
  it('rejects custom arguments that could bypass protected CDP settings', () => {
55
74
  for (const argument of [
56
75
  '--remote-debugging-address=0.0.0.0',
@@ -10,6 +10,7 @@ describe('constants', () => {
10
10
 
11
11
  it('should contain expected browser arguments', () => {
12
12
  assert.ok(CHROME_ARGS.includes('--disable-infobars'));
13
+ assert.ok(CHROME_ARGS.includes('--password-store=basic'));
13
14
  assert.ok(CHROME_ARGS.includes('--no-first-run'));
14
15
  assert.ok(CHROME_ARGS.includes('--no-default-browser-check'));
15
16
  });