@guanzhu.me/pw-cli 0.0.17 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -72,6 +72,7 @@ Run a local script:
72
72
 
73
73
  ```bash
74
74
  pw-cli run-script ./scrape.js --url https://example.com
75
+ pw-cli run-script --extension ./scrape.js --url https://example.com
75
76
  ```
76
77
 
77
78
  `run-script` is intended for multi-step automation. Define an `async function main` that receives Playwright globals as a single object:
@@ -116,6 +117,13 @@ async function main({ page, args }) {
116
117
  pw-cli run-script ./scripts/extract-links.js --url https://example.com --output links.json
117
118
  ```
118
119
 
120
+ To drive an already-open Chrome/Edge browser through Playwright MCP Bridge, add `--extension`:
121
+
122
+ ```bash
123
+ pw-cli run-script --extension ./scripts/extract-links.js --url https://example.com
124
+ pw-cli run-script --extension=msedge ./scripts/extract-links.js --url https://example.com
125
+ ```
126
+
119
127
  Reuse a page that was opened through `pw-cli open`:
120
128
 
121
129
  ```bash
@@ -332,6 +340,7 @@ pw-cli run-code "await page.goto('https://example.com'); return await page.title
332
340
  echo "return await page.url()" | pw-cli run-code
333
341
  pw-cli run-script ./scripts/smoke.js --env prod
334
342
  pw-cli run-script ./scripts/extract-links.js --url https://example.com --output links.json
343
+ pw-cli run-script --extension ./scripts/extract-links.js --url https://example.com
335
344
  pw-cli click "//button[contains(., 'Submit')]"
336
345
  pw-cli queue add goto https://example.com
337
346
  pw-cli queue add snapshot
package/bin/pw-cli.js CHANGED
@@ -113,12 +113,16 @@ function getCommandAndSession(argv) {
113
113
  }
114
114
 
115
115
  function parsePwCliGlobalOptions(argv) {
116
- const options = { headless: false, profile: 'default', port: 9223 };
116
+ const options = { headless: false, profile: 'default', port: 9223, extension: false };
117
117
 
118
118
  for (let i = 0; i < argv.length; i++) {
119
119
  const arg = argv[i];
120
120
  if (arg === '--headless') {
121
121
  options.headless = true;
122
+ } else if (arg === '--extension') {
123
+ options.extension = true;
124
+ } else if (arg.startsWith('--extension=')) {
125
+ options.extension = arg.split('=')[1] || true;
122
126
  } else if (arg === '--profile' && argv[i + 1]) {
123
127
  options.profile = argv[++i];
124
128
  } else if (arg === '--port' && argv[i + 1]) {
@@ -260,6 +264,7 @@ Global options:
260
264
  --version print version
261
265
  -s, --session <name> choose browser session
262
266
  --headless used by pw-cli-managed browser launches
267
+ --extension[=browser] run scripts/code through Playwright MCP Bridge (default browser: chrome)
263
268
 
264
269
  Requirements:
265
270
  Node.js 18+
@@ -302,6 +307,7 @@ What the script receives:
302
307
 
303
308
  Example:
304
309
  pw-cli run-script ./scripts/extract-links.js --url https://example.com --output links.json
310
+ pw-cli run-script --extension ./scripts/extract-links.js --url https://example.com
305
311
  `;
306
312
  }
307
313
 
@@ -736,7 +742,7 @@ async function handleRunScript(rawArgv) {
736
742
  process.stderr.write(`pw-cli: ${err.message || err}\n`);
737
743
  process.exit(1);
738
744
  } finally {
739
- await conn.browser.close();
745
+ await (conn.close ? conn.close() : conn.browser.close());
740
746
  }
741
747
  process.exit(0);
742
748
  }
@@ -783,7 +789,7 @@ async function handleRunCode(rawArgv) {
783
789
  process.stderr.write(`pw-cli: ${err.message || err}\n`);
784
790
  process.exit(1);
785
791
  } finally {
786
- await conn.browser.close();
792
+ await (conn.close ? conn.close() : conn.browser.close());
787
793
  }
788
794
 
789
795
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanzhu.me/pw-cli",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "description": "Persistent Playwright browser CLI with headed defaults, profile support, queueing, and script execution",
5
5
  "bin": {
6
6
  "pw-cli": "./bin/pw-cli.js"
@@ -9,9 +9,12 @@
9
9
  "access": "public"
10
10
  },
11
11
  "scripts": {
12
+ "changeset": "changeset",
12
13
  "lint": "node scripts/check-syntax.js",
13
14
  "test": "node --test",
14
- "verify": "npm run lint && npm test"
15
+ "verify": "npm run lint && npm test",
16
+ "version-packages": "changeset version",
17
+ "release": "changeset publish"
15
18
  },
16
19
  "files": [
17
20
  "bin/",
@@ -23,8 +26,8 @@
23
26
  "node": ">=18"
24
27
  },
25
28
  "peerDependencies": {
26
- "playwright": ">=1.40.0",
27
- "@playwright/cli": ">=0.1.0"
29
+ "@playwright/cli": ">=0.1.0",
30
+ "playwright": ">=1.40.0"
28
31
  },
29
32
  "keywords": [
30
33
  "playwright",
@@ -42,5 +45,9 @@
42
45
  "bugs": {
43
46
  "url": "https://github.com/wn0x00/pw-cli/issues"
44
47
  },
45
- "license": "MIT"
48
+ "license": "MIT",
49
+ "devDependencies": {
50
+ "@changesets/cli": "^2.30.0",
51
+ "@types/node": "^25.5.2"
52
+ }
46
53
  }
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { spawn } = require('child_process');
4
+ const http = require('http');
4
5
  const path = require('path');
5
6
  const os = require('os');
6
7
  const fs = require('fs');
@@ -9,6 +10,27 @@ const { execSync } = require('child_process');
9
10
  const { readState, writeState, clearState, getProfileDir } = require('./state');
10
11
  const { probeCDP, findFreePort, sleep, fetchActivePageUrl } = require('./utils');
11
12
 
13
+ function loadPlaywrightUtilsBundle() {
14
+ const candidates = [
15
+ '../node_modules/@playwright/cli/node_modules/playwright-core/lib/utilsBundleImpl',
16
+ '../node_modules/@playwright/cli/node_modules/playwright-core/lib/utilsBundleImpl/index.js',
17
+ ];
18
+
19
+ for (const candidate of candidates) {
20
+ try {
21
+ return require(candidate);
22
+ } catch (error) {
23
+ if (error.code !== 'MODULE_NOT_FOUND') {
24
+ throw error;
25
+ }
26
+ }
27
+ }
28
+
29
+ throw new Error('Unable to load playwright-core utilsBundleImpl from @playwright/cli. Reinstall @playwright/cli or playwright.');
30
+ }
31
+
32
+ const { ws, wsServer } = loadPlaywrightUtilsBundle();
33
+
12
34
  const DAEMON_SCRIPT = path.join(__dirname, 'launch-daemon.js');
13
35
 
14
36
  // ---------------------------------------------------------------------------
@@ -61,6 +83,27 @@ function loadPlaywright() {
61
83
  throw new Error('playwright is not installed. Run: npm install -g playwright');
62
84
  }
63
85
 
86
+ function normalizeExtensionBrowser(extension) {
87
+ if (typeof extension === 'string' && extension.trim()) {
88
+ return extension.trim();
89
+ }
90
+ return 'chrome';
91
+ }
92
+
93
+ function buildExtensionConnectHeaders(extension) {
94
+ const browser = normalizeExtensionBrowser(extension);
95
+ const browserType = 'chromium';
96
+ const launchOptions = browser !== 'chromium' ? { channel: browser } : {};
97
+
98
+ return {
99
+ browserType,
100
+ headers: {
101
+ 'x-playwright-browser': browserType,
102
+ 'x-playwright-launch-options': JSON.stringify(launchOptions),
103
+ },
104
+ };
105
+ }
106
+
64
107
  function pickPage(pages, activeUrl) {
65
108
  if (!pages || pages.length === 0) return null;
66
109
  if (activeUrl) {
@@ -87,6 +130,385 @@ async function resolveContextAndPage(browser, cdpPort) {
87
130
  return { context, page };
88
131
  }
89
132
 
133
+ function getBrowserExecutableCandidates(browser) {
134
+ switch (browser) {
135
+ case 'msedge':
136
+ return process.platform === 'win32'
137
+ ? [
138
+ path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
139
+ path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
140
+ ]
141
+ : [];
142
+ case 'chromium':
143
+ case 'chrome':
144
+ default:
145
+ return process.platform === 'win32'
146
+ ? [
147
+ path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Google', 'Chrome', 'Application', 'chrome.exe'),
148
+ path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Google', 'Chrome', 'Application', 'chrome.exe'),
149
+ ]
150
+ : [];
151
+ }
152
+ }
153
+
154
+ function resolveExtensionExecutablePath(browser) {
155
+ if (process.env.PLAYWRIGHT_MCP_EXECUTABLE_PATH && fs.existsSync(process.env.PLAYWRIGHT_MCP_EXECUTABLE_PATH)) {
156
+ return process.env.PLAYWRIGHT_MCP_EXECUTABLE_PATH;
157
+ }
158
+
159
+ const candidates = getBrowserExecutableCandidates(browser);
160
+ for (const candidate of candidates) {
161
+ if (fs.existsSync(candidate)) {
162
+ return candidate;
163
+ }
164
+ }
165
+
166
+ throw new Error(`Unable to find executable for browser channel "${browser}". Set PLAYWRIGHT_MCP_EXECUTABLE_PATH or install ${browser}.`);
167
+ }
168
+
169
+ class ManualPromise {
170
+ constructor() {
171
+ this._settled = false;
172
+ this.promise = new Promise((resolve, reject) => {
173
+ this._resolve = value => {
174
+ if (this._settled) return;
175
+ this._settled = true;
176
+ resolve(value);
177
+ };
178
+ this._reject = error => {
179
+ if (this._settled) return;
180
+ this._settled = true;
181
+ reject(error);
182
+ };
183
+ });
184
+ }
185
+
186
+ resolve(value) {
187
+ this._resolve(value);
188
+ }
189
+
190
+ reject(error) {
191
+ this._reject(error);
192
+ }
193
+ }
194
+
195
+ class ExtensionConnection {
196
+ constructor(socket) {
197
+ this._socket = socket;
198
+ this._callbacks = new Map();
199
+ this._lastId = 0;
200
+ this._socket.on('message', this._onMessage.bind(this));
201
+ this._socket.on('close', this._onClose.bind(this));
202
+ this._socket.on('error', this._onError.bind(this));
203
+ }
204
+
205
+ send(method, params) {
206
+ if (this._socket.readyState !== ws.OPEN) {
207
+ throw new Error(`Unexpected WebSocket state: ${this._socket.readyState}`);
208
+ }
209
+
210
+ const id = ++this._lastId;
211
+ this._socket.send(JSON.stringify({ id, method, params }));
212
+
213
+ return new Promise((resolve, reject) => {
214
+ this._callbacks.set(id, {
215
+ resolve,
216
+ reject,
217
+ error: new Error(`Protocol error: ${method}`),
218
+ });
219
+ });
220
+ }
221
+
222
+ close(reason) {
223
+ if (this._socket.readyState === ws.OPEN) {
224
+ this._socket.close(1000, reason);
225
+ }
226
+ }
227
+
228
+ _onMessage(message) {
229
+ const object = JSON.parse(message.toString());
230
+
231
+ if (object.id && this._callbacks.has(object.id)) {
232
+ const callback = this._callbacks.get(object.id);
233
+ this._callbacks.delete(object.id);
234
+ if (object.error) {
235
+ callback.error.message = object.error;
236
+ callback.reject(callback.error);
237
+ } else {
238
+ callback.resolve(object.result);
239
+ }
240
+ return;
241
+ }
242
+
243
+ if (!object.id) {
244
+ this.onmessage?.(object.method, object.params);
245
+ }
246
+ }
247
+
248
+ _onClose() {
249
+ for (const callback of this._callbacks.values()) {
250
+ callback.reject(new Error('WebSocket closed'));
251
+ }
252
+ this._callbacks.clear();
253
+ this.onclose?.(this, 'WebSocket closed');
254
+ }
255
+
256
+ _onError() {
257
+ for (const callback of this._callbacks.values()) {
258
+ callback.reject(new Error('WebSocket error'));
259
+ }
260
+ this._callbacks.clear();
261
+ }
262
+ }
263
+
264
+ class CDPRelayServer {
265
+ constructor(server, browserChannel, userDataDir, executablePath) {
266
+ this._playwrightConnection = null;
267
+ this._extensionConnection = null;
268
+ this._nextSessionId = 1;
269
+ this._browserChannel = browserChannel;
270
+ this._userDataDir = userDataDir;
271
+ this._executablePath = executablePath;
272
+ const uuid = crypto.randomUUID();
273
+ const address = server.address();
274
+ this._wsHost = `ws://127.0.0.1:${address.port}`;
275
+ this._cdpPath = `/cdp/${uuid}`;
276
+ this._extensionPath = `/extension/${uuid}`;
277
+ this._resetExtensionConnection();
278
+ this._wss = new wsServer({ server });
279
+ this._wss.on('connection', this._onConnection.bind(this));
280
+ }
281
+
282
+ cdpEndpoint() {
283
+ return `${this._wsHost}${this._cdpPath}`;
284
+ }
285
+
286
+ extensionEndpoint() {
287
+ return `${this._wsHost}${this._extensionPath}`;
288
+ }
289
+
290
+ async ensureExtensionConnectionForMCPContext(clientName) {
291
+ if (this._extensionConnection) return;
292
+ this._connectBrowser(clientName);
293
+ await Promise.race([
294
+ this._extensionConnectionPromise.promise,
295
+ new Promise((_, reject) => setTimeout(() => {
296
+ reject(new Error('Extension connection timeout. Make sure the "Playwright MCP Bridge" extension is installed and enabled.'));
297
+ }, 5000)),
298
+ ]);
299
+ }
300
+
301
+ stop() {
302
+ this.closeConnections('Server stopped');
303
+ this._wss.close();
304
+ }
305
+
306
+ closeConnections(reason) {
307
+ this._closePlaywrightConnection(reason);
308
+ this._closeExtensionConnection(reason);
309
+ }
310
+
311
+ _connectBrowser(clientName) {
312
+ const relayUrl = `${this._wsHost}${this._extensionPath}`;
313
+ const url = new URL('chrome-extension://mmlmfjhmonkocbjadbfplnigmagldckm/connect.html');
314
+ url.searchParams.set('mcpRelayUrl', relayUrl);
315
+ url.searchParams.set('client', JSON.stringify({ name: clientName }));
316
+ url.searchParams.set('protocolVersion', '1');
317
+
318
+ const executablePath = this._executablePath || resolveExtensionExecutablePath(this._browserChannel);
319
+ const args = [];
320
+ if (this._userDataDir) {
321
+ args.push(`--user-data-dir=${this._userDataDir}`);
322
+ }
323
+ args.push(url.toString());
324
+
325
+ const child = spawn(executablePath, args, {
326
+ windowsHide: true,
327
+ detached: true,
328
+ shell: false,
329
+ stdio: 'ignore',
330
+ });
331
+ child.unref();
332
+ }
333
+
334
+ _onConnection(socket, request) {
335
+ const url = new URL(`http://localhost${request.url}`);
336
+ if (url.pathname === this._cdpPath) {
337
+ this._handlePlaywrightConnection(socket);
338
+ return;
339
+ }
340
+ if (url.pathname === this._extensionPath) {
341
+ this._handleExtensionConnection(socket);
342
+ return;
343
+ }
344
+ socket.close(4004, 'Invalid path');
345
+ }
346
+
347
+ _handlePlaywrightConnection(socket) {
348
+ if (this._playwrightConnection) {
349
+ socket.close(1000, 'Another CDP client already connected');
350
+ return;
351
+ }
352
+ this._playwrightConnection = socket;
353
+ socket.on('message', async data => {
354
+ const message = JSON.parse(data.toString());
355
+ try {
356
+ const result = await this._handleCDPCommand(message.method, message.params, message.sessionId);
357
+ this._sendToPlaywright({ id: message.id, sessionId: message.sessionId, result });
358
+ } catch (error) {
359
+ this._sendToPlaywright({
360
+ id: message.id,
361
+ sessionId: message.sessionId,
362
+ error: { message: error.message },
363
+ });
364
+ }
365
+ });
366
+ socket.on('close', () => {
367
+ if (this._playwrightConnection !== socket) return;
368
+ this._playwrightConnection = null;
369
+ this._closeExtensionConnection('Playwright client disconnected');
370
+ });
371
+ }
372
+
373
+ _handleExtensionConnection(socket) {
374
+ if (this._extensionConnection) {
375
+ socket.close(1000, 'Another extension connection already established');
376
+ return;
377
+ }
378
+ this._extensionConnection = new ExtensionConnection(socket);
379
+ this._extensionConnection.onclose = current => {
380
+ if (this._extensionConnection !== current) return;
381
+ this._resetExtensionConnection();
382
+ this._closePlaywrightConnection('Extension disconnected');
383
+ };
384
+ this._extensionConnection.onmessage = this._handleExtensionMessage.bind(this);
385
+ this._extensionConnectionPromise.resolve();
386
+ }
387
+
388
+ _handleExtensionMessage(method, params) {
389
+ if (method !== 'forwardCDPEvent') return;
390
+ const sessionId = params.sessionId || this._connectedTabInfo?.sessionId;
391
+ this._sendToPlaywright({
392
+ sessionId,
393
+ method: params.method,
394
+ params: params.params,
395
+ });
396
+ }
397
+
398
+ async _handleCDPCommand(method, params, sessionId) {
399
+ switch (method) {
400
+ case 'Browser.getVersion':
401
+ return {
402
+ protocolVersion: '1.3',
403
+ product: 'Chrome/Extension-Bridge',
404
+ userAgent: 'CDP-Bridge-Server/1.0.0',
405
+ };
406
+ case 'Browser.setDownloadBehavior':
407
+ return {};
408
+ case 'Target.setAutoAttach': {
409
+ if (sessionId) break;
410
+ const { targetInfo } = await this._extensionConnection.send('attachToTab', {});
411
+ this._connectedTabInfo = {
412
+ targetInfo,
413
+ sessionId: `pw-tab-${this._nextSessionId++}`,
414
+ };
415
+ this._sendToPlaywright({
416
+ method: 'Target.attachedToTarget',
417
+ params: {
418
+ sessionId: this._connectedTabInfo.sessionId,
419
+ targetInfo: {
420
+ ...this._connectedTabInfo.targetInfo,
421
+ attached: true,
422
+ },
423
+ waitingForDebugger: false,
424
+ },
425
+ });
426
+ return {};
427
+ }
428
+ case 'Target.getTargetInfo':
429
+ return this._connectedTabInfo?.targetInfo;
430
+ default:
431
+ return this._forwardToExtension(method, params, sessionId);
432
+ }
433
+ }
434
+
435
+ async _forwardToExtension(method, params, sessionId) {
436
+ if (!this._extensionConnection) {
437
+ throw new Error('Extension not connected');
438
+ }
439
+ if (this._connectedTabInfo?.sessionId === sessionId) {
440
+ sessionId = undefined;
441
+ }
442
+ return this._extensionConnection.send('forwardCDPCommand', { sessionId, method, params });
443
+ }
444
+
445
+ _sendToPlaywright(message) {
446
+ if (this._playwrightConnection?.readyState === ws.OPEN) {
447
+ this._playwrightConnection.send(JSON.stringify(message));
448
+ }
449
+ }
450
+
451
+ _closeExtensionConnection(reason) {
452
+ this._extensionConnection?.close(reason);
453
+ this._extensionConnectionPromise.reject(new Error(reason));
454
+ this._resetExtensionConnection();
455
+ }
456
+
457
+ _resetExtensionConnection() {
458
+ this._connectedTabInfo = undefined;
459
+ this._extensionConnection = null;
460
+ this._extensionConnectionPromise = new ManualPromise();
461
+ }
462
+
463
+ _closePlaywrightConnection(reason) {
464
+ if (this._playwrightConnection?.readyState === ws.OPEN) {
465
+ this._playwrightConnection.close(1000, reason);
466
+ }
467
+ this._playwrightConnection = null;
468
+ }
469
+ }
470
+
471
+ async function startExtensionRelay(extension) {
472
+ const browser = normalizeExtensionBrowser(extension);
473
+ if (!['chrome', 'chromium', 'msedge'].includes(browser)) {
474
+ throw new Error(`--extension currently supports Chromium-based channels only (received "${browser}")`);
475
+ }
476
+
477
+ const server = http.createServer();
478
+ await new Promise((resolve, reject) => {
479
+ server.once('error', reject);
480
+ server.listen(0, '127.0.0.1', resolve);
481
+ });
482
+ const relay = new CDPRelayServer(server, browser, null, null);
483
+ await relay.ensureExtensionConnectionForMCPContext('pw-cli');
484
+ return { relay, server };
485
+ }
486
+
487
+ async function getExtensionConnection(extension) {
488
+ const playwright = loadPlaywright();
489
+ const { relay, server } = await startExtensionRelay(extension);
490
+
491
+ try {
492
+ const browser = await playwright.chromium.connectOverCDP(relay.cdpEndpoint());
493
+ const { context, page } = await resolveContextAndPage(browser, null);
494
+ return {
495
+ browser,
496
+ context,
497
+ page,
498
+ playwright,
499
+ close: async () => {
500
+ relay.stop();
501
+ await new Promise(resolve => server.close(resolve));
502
+ await browser.close();
503
+ },
504
+ };
505
+ } catch (error) {
506
+ relay.stop();
507
+ await new Promise(resolve => server.close(resolve));
508
+ throw error;
509
+ }
510
+ }
511
+
90
512
  // ---------------------------------------------------------------------------
91
513
  // Our own CDP-based browser launcher (fallback when playwright-cli not running)
92
514
  // ---------------------------------------------------------------------------
@@ -147,7 +569,11 @@ async function launchBrowser({ headless = false, profile = 'default', port: pref
147
569
  // ---------------------------------------------------------------------------
148
570
  // getConnection — tries playwright-cli browser first, then our own
149
571
  // ---------------------------------------------------------------------------
150
- async function getConnection({ headless = false, profile = 'default', port: preferredPort = 9223 } = {}) {
572
+ async function getConnection({ headless = false, profile = 'default', port: preferredPort = 9223, extension = false } = {}) {
573
+ if (extension) {
574
+ return getExtensionConnection(extension);
575
+ }
576
+
151
577
  const playwright = loadPlaywright();
152
578
 
153
579
  // 1. Try to reuse playwright-cli's browser via its CDP port
@@ -158,7 +584,15 @@ async function getConnection({ headless = false, profile = 'default', port: pref
158
584
  try {
159
585
  const browser = await playwright.chromium.connectOverCDP(`http://127.0.0.1:${cliCdpPort}`);
160
586
  const { context, page } = await resolveContextAndPage(browser, cliCdpPort);
161
- return { browser, context, page, playwright };
587
+ return {
588
+ browser,
589
+ context,
590
+ page,
591
+ playwright,
592
+ close: async () => {
593
+ await browser.close();
594
+ },
595
+ };
162
596
  } catch {
163
597
  // fall through to own browser
164
598
  }
@@ -188,7 +622,15 @@ async function getConnection({ headless = false, profile = 'default', port: pref
188
622
  const browser = await playwright.chromium.connectOverCDP(cdpUrl);
189
623
  const { context, page } = await resolveContextAndPage(browser, state ? state.port : null);
190
624
 
191
- return { browser, context, page, playwright };
625
+ return {
626
+ browser,
627
+ context,
628
+ page,
629
+ playwright,
630
+ close: async () => {
631
+ await browser.close();
632
+ },
633
+ };
192
634
  }
193
635
 
194
636
  async function killBrowser() {
@@ -208,4 +650,11 @@ async function killBrowser() {
208
650
  return true;
209
651
  }
210
652
 
211
- module.exports = { getConnection, killBrowser, getPlaywrightCliCdpPort, pickPage };
653
+ module.exports = {
654
+ getConnection,
655
+ killBrowser,
656
+ getPlaywrightCliCdpPort,
657
+ pickPage,
658
+ buildExtensionConnectHeaders,
659
+ normalizeExtensionBrowser,
660
+ };
package/src/cli.js CHANGED
@@ -6,7 +6,7 @@ const { readState } = require('./state');
6
6
  const { readStdin, die, probeCDP } = require('./utils');
7
7
 
8
8
  function parseArgs(argv) {
9
- const global = { headless: false, profile: 'default', port: 9222 };
9
+ const global = { headless: false, profile: 'default', port: 9222, extension: false };
10
10
  const rest = [];
11
11
  let i = 0;
12
12
 
@@ -14,6 +14,10 @@ function parseArgs(argv) {
14
14
  const arg = argv[i];
15
15
  if (arg === '--headless') {
16
16
  global.headless = true;
17
+ } else if (arg === '--extension') {
18
+ global.extension = true;
19
+ } else if (arg.startsWith('--extension=')) {
20
+ global.extension = arg.split('=')[1] || true;
17
21
  } else if (arg === '--profile' && argv[i + 1]) {
18
22
  global.profile = argv[++i];
19
23
  } else if (arg === '--port' && argv[i + 1]) {
@@ -47,7 +51,7 @@ async function cmdRunCode(rest, opts) {
47
51
  console.log(result);
48
52
  }
49
53
  } finally {
50
- await conn.browser.close(); // disconnect only, browser keeps running
54
+ await (conn.close ? conn.close() : conn.browser.close());
51
55
  }
52
56
  }
53
57
 
@@ -65,7 +69,8 @@ Script format (standard module):
65
69
  Legacy bare-code scripts (without module.exports) are still supported.
66
70
 
67
71
  Example:
68
- pw-cli run-script ./scripts/extract-links.js --url https://example.com --output links.json`;
72
+ pw-cli run-script ./scripts/extract-links.js --url https://example.com --output links.json
73
+ pw-cli run-script --extension ./scripts/extract-links.js --url https://example.com`;
69
74
  }
70
75
 
71
76
  async function cmdRunScript(rest, opts) {
@@ -81,7 +86,7 @@ async function cmdRunScript(rest, opts) {
81
86
  console.log(result);
82
87
  }
83
88
  } finally {
84
- await conn.browser.close();
89
+ await (conn.close ? conn.close() : conn.browser.close());
85
90
  }
86
91
  }
87
92
 
@@ -117,6 +122,7 @@ USAGE
117
122
 
118
123
  GLOBAL OPTIONS
119
124
  --headless Run browser headlessly (default: headed)
125
+ --extension[=name] Run code/scripts through Playwright MCP Bridge (default: chrome)
120
126
  --profile <name> Named profile to use (default: "default")
121
127
  --port <number> CDP port (default: 9222)
122
128
 
@@ -134,6 +140,7 @@ EXAMPLES
134
140
  pw-cli run-code "await page.goto('https://example.com'); console.log(await page.title())"
135
141
  echo "await page.screenshot({ path: 'out.png' })" | pw-cli run-code
136
142
  pw-cli run-script ./scrape.js --url https://example.com
143
+ pw-cli run-script --extension ./scrape.js --url https://example.com
137
144
  pw-cli run-script ./scripts/extract-links.js --url https://example.com --output links.json
138
145
  pw-cli --headless run-code "await page.goto('https://example.com')"
139
146
  pw-cli --profile work status