@guanzhu.me/pw-cli 0.0.14 → 0.0.16

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
@@ -20,8 +20,8 @@ Raw Playwright is excellent for test suites and scripted automation, but ad hoc
20
20
  - Headed mode by default
21
21
  - Named profile support
22
22
  - `run-code` for inline JavaScript or piped stdin
23
- - `run-script` for executing local JavaScript files
24
- - `run-script` supports CommonJS-style scripts that use `require`, `module`, `exports`, `__filename`, and `__dirname`
23
+ - `run-script` for executing local JavaScript files with `main` function convention
24
+ - `run-script` supports standard CommonJS modules (`require`, `__filename`, `__dirname`) and also bare-code scripts
25
25
  - Queue management for multi-step flows
26
26
  - Automatic browser launch when needed
27
27
  - XPath command conversion for common actions
@@ -74,46 +74,42 @@ Run a local script:
74
74
  pw-cli run-script ./scrape.js --url https://example.com
75
75
  ```
76
76
 
77
- `run-script` is intended for multi-step automation. Your script can use:
78
-
79
- - Playwright globals: `page`, `context`, `browser`, `playwright`
80
- - Script args: `args`
81
- - CommonJS globals: `require`, `module`, `exports`, `__filename`, `__dirname`
82
-
83
- More complete example:
77
+ `run-script` is intended for multi-step automation. Define an `async function main` that receives Playwright globals as a single object:
84
78
 
85
79
  ```javascript
86
80
  // scripts/extract-links.js
87
81
  const fs = require('fs');
88
82
 
89
- const url = args[args.indexOf('--url') + 1] || 'https://example.com';
90
- const output = args[args.indexOf('--output') + 1] || 'links.json';
91
-
92
- await page.goto(url, { waitUntil: 'networkidle' });
93
-
94
- const links = await page.locator('a').evaluateAll(nodes =>
95
- nodes
96
- .map(a => ({
97
- text: a.textContent.trim(),
98
- href: a.href,
99
- }))
100
- .filter(item => item.href)
101
- );
102
-
103
- fs.writeFileSync(
104
- output,
105
- JSON.stringify(
106
- {
107
- url,
108
- count: links.length,
109
- links,
110
- },
111
- null,
112
- 2
113
- )
114
- );
115
-
116
- return `saved ${links.length} links to ${output}`;
83
+ async function main({ page, args }) {
84
+ const url = args[args.indexOf('--url') + 1] || 'https://example.com';
85
+ const output = args[args.indexOf('--output') + 1] || 'links.json';
86
+
87
+ await page.goto(url, { waitUntil: 'domcontentloaded' });
88
+
89
+ const links = await page.locator('a').evaluateAll(nodes =>
90
+ nodes
91
+ .map(a => ({
92
+ text: a.textContent.trim(),
93
+ href: a.href,
94
+ }))
95
+ .filter(item => item.href)
96
+ );
97
+
98
+ fs.writeFileSync(
99
+ output,
100
+ JSON.stringify(
101
+ {
102
+ url,
103
+ count: links.length,
104
+ links,
105
+ },
106
+ null,
107
+ 2
108
+ )
109
+ );
110
+
111
+ return `saved ${links.length} links to ${output}`;
112
+ }
117
113
  ```
118
114
 
119
115
  ```bash
@@ -154,7 +150,7 @@ pw-cli list
154
150
  - `open` injects headed and persistent defaults
155
151
  - Browser-backed commands can auto-open a browser session if needed
156
152
  - `run-code` accepts stdin and plain inline statements
157
- - `run-script` executes a local `.js` file with Playwright globals, CommonJS globals, and `args`
153
+ - `run-script` executes a local `.js` file auto-detects `main` function, `module.exports`, or bare code
158
154
  - Common element commands accept XPath refs
159
155
  - `queue` lets you batch multiple commands and run them in order
160
156
 
@@ -281,7 +277,7 @@ console [min-level] list console messages
281
277
  run-code <code> run playwright code snippet
282
278
  pw-cli: reads code from stdin when <code> is omitted
283
279
  pw-cli: wraps plain statements in an async function
284
- run-script <file> [...] run a local JavaScript file with Playwright globals and script args
280
+ run-script <file> [...] run a local JavaScript file (main function or module.exports)
285
281
  network list all network requests since loading the page
286
282
  tracing-start start trace recording
287
283
  tracing-stop stop trace recording
package/bin/pw-cli.js CHANGED
@@ -771,6 +771,43 @@ async function main() {
771
771
  return;
772
772
  }
773
773
 
774
+ // ── Fast path: send command directly to playwright-cli daemon socket ────
775
+ // Skip heavy setup (npm root -g, require playwright, CDP probes) entirely.
776
+ // Only for commands that the daemon handles AND don't need local preprocessing.
777
+ if (command && !MGMT_COMMANDS.has(command)) {
778
+ // Build the args array the daemon expects: strip session flags, keep command + args
779
+ let fastArgs = [...rawArgv];
780
+ // Remove session flags (-s xxx / --session xxx / --session=xxx)
781
+ fastArgs = fastArgs.filter((a, i, arr) => {
782
+ if (a === '-s' || a === '--session') { arr[i + 1] = undefined; return false; }
783
+ if (a === undefined) return false;
784
+ if (a.startsWith('-s=') || a.startsWith('--session=')) return false;
785
+ return true;
786
+ }).filter(Boolean);
787
+
788
+ // Apply XPath conversion if needed
789
+ fastArgs = convertXPathCommand(fastArgs);
790
+
791
+ // Handle run-code wrapping for XPath-converted commands
792
+ if (fastArgs[0] === 'run-code' && fastArgs.length > 1) {
793
+ const code = fastArgs.slice(1).join(' ');
794
+ fastArgs = ['run-code', wrapCodeIfNeeded(code)];
795
+ }
796
+
797
+ const { sendCommand } = require('../src/fast-send');
798
+ const result = await sendCommand(fastArgs, session);
799
+ if (result !== null) {
800
+ // Daemon responded — use its result
801
+ if (result.isError) {
802
+ process.stderr.write(`${result.text}\n`);
803
+ process.exit(1);
804
+ }
805
+ if (result.text) process.stdout.write(result.text + '\n');
806
+ return;
807
+ }
808
+ // result === null means daemon not running — fall through to full path
809
+ }
810
+
774
811
  // ── From here on: delegate to playwright-cli (with enhancements) ─────────
775
812
  const cliPath = findPlaywrightCli();
776
813
  if (!cliPath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanzhu.me/pw-cli",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
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"
package/src/cli.js CHANGED
@@ -55,10 +55,14 @@ function getRunScriptHelp() {
55
55
  return `Usage:
56
56
  pw-cli run-script <file.js> [args...]
57
57
 
58
- What the script receives:
59
- - Playwright globals: page, context, browser, playwright
60
- - Script args array: args
61
- - CommonJS globals: require, module, exports, __filename, __dirname
58
+ Script format (standard module):
59
+ module.exports = async function ({ page, context, browser, playwright, args }) {
60
+ // your code here
61
+ };
62
+
63
+ The exported function receives { page, context, browser, playwright, args }.
64
+ CommonJS globals (require, __filename, __dirname) are available as usual.
65
+ Legacy bare-code scripts (without module.exports) are still supported.
62
66
 
63
67
  Example:
64
68
  pw-cli run-script ./scripts/extract-links.js --url https://example.com --output links.json`;
package/src/executor.js CHANGED
@@ -79,25 +79,7 @@ async function execCode(code, { browser, context, page, playwright }) {
79
79
  return withTemporaryGlobals(globals, () => runCode(code, globals));
80
80
  }
81
81
 
82
- async function execScript(scriptPath, scriptArgs, { browser, context, page, playwright }) {
83
- const absPath = path.resolve(scriptPath);
84
- if (!fs.existsSync(absPath)) {
85
- const err = new Error(`Script not found: ${absPath}`);
86
- err.code = 'ENOENT';
87
- throw err;
88
- }
89
-
90
- const code = fs.readFileSync(absPath, 'utf8');
91
- const moduleDir = path.dirname(absPath);
92
- const scriptModule = {
93
- id: absPath,
94
- filename: absPath,
95
- path: moduleDir,
96
- exports: {},
97
- loaded: false,
98
- children: [],
99
- parent: require.main || module,
100
- };
82
+ function buildScriptRequire(absPath) {
101
83
  const localRequire = Module.createRequire(absPath);
102
84
  const scriptRequire = function scriptRequire(id) {
103
85
  try {
@@ -119,16 +101,92 @@ async function execScript(scriptPath, scriptArgs, { browser, context, page, play
119
101
  scriptRequire.resolve = localRequire.resolve.bind(localRequire);
120
102
  scriptRequire.cache = require.cache;
121
103
  scriptRequire.extensions = require.extensions;
104
+ return scriptRequire;
105
+ }
106
+
107
+ function isModuleExport(code) {
108
+ return /\bmodule\.exports\b/.test(code) || /\bexports\./.test(code);
109
+ }
110
+
111
+ function hasMainFunction(code) {
112
+ return /\b(async\s+)?function\s+main\s*\(/.test(code);
113
+ }
114
+
115
+ async function execScript(scriptPath, scriptArgs, { browser, context, page, playwright }) {
116
+ const absPath = path.resolve(scriptPath);
117
+ if (!fs.existsSync(absPath)) {
118
+ const err = new Error(`Script not found: ${absPath}`);
119
+ err.code = 'ENOENT';
120
+ throw err;
121
+ }
122
+
123
+ const code = fs.readFileSync(absPath, 'utf8');
124
+ const pwGlobals = { page, context, browser, playwright, args: scriptArgs };
125
+
126
+ // Standard module pattern: script uses module.exports = function(...)
127
+ // The exported function receives Playwright globals as a single object argument.
128
+ if (isModuleExport(code)) {
129
+ return execModuleScript(absPath, code, pwGlobals);
130
+ }
131
+
132
+ // Legacy bare-code pattern: script body is executed directly with globals as local variables.
133
+ return execBareScript(absPath, code, pwGlobals);
134
+ }
135
+
136
+ async function execModuleScript(absPath, code, pwGlobals) {
137
+ const moduleDir = path.dirname(absPath);
138
+ const scriptRequire = buildScriptRequire(absPath);
139
+ const scriptModule = {
140
+ id: absPath,
141
+ filename: absPath,
142
+ path: moduleDir,
143
+ exports: {},
144
+ loaded: false,
145
+ children: [],
146
+ parent: require.main || module,
147
+ require: scriptRequire,
148
+ };
122
149
  scriptRequire.main = scriptModule;
123
150
 
124
- scriptModule.require = scriptRequire;
151
+ // Evaluate the module body to populate module.exports.
152
+ // Playwright globals are available during evaluation for backward compat.
153
+ const wrapGlobals = {
154
+ ...pwGlobals,
155
+ require: scriptRequire,
156
+ module: scriptModule,
157
+ exports: scriptModule.exports,
158
+ console,
159
+ process,
160
+ __filename: absPath,
161
+ __dirname: moduleDir,
162
+ };
163
+ await withTemporaryGlobals(wrapGlobals, () => runProgram(code, wrapGlobals));
164
+ scriptModule.loaded = true;
165
+
166
+ const exported = scriptModule.exports;
167
+ if (typeof exported === 'function') {
168
+ return exported(pwGlobals);
169
+ }
170
+ return exported;
171
+ }
172
+
173
+ async function execBareScript(absPath, code, pwGlobals) {
174
+ const moduleDir = path.dirname(absPath);
175
+ const scriptRequire = buildScriptRequire(absPath);
176
+ const scriptModule = {
177
+ id: absPath,
178
+ filename: absPath,
179
+ path: moduleDir,
180
+ exports: {},
181
+ loaded: false,
182
+ children: [],
183
+ parent: require.main || module,
184
+ require: scriptRequire,
185
+ };
186
+ scriptRequire.main = scriptModule;
125
187
 
126
188
  const globals = {
127
- browser,
128
- context,
129
- page,
130
- playwright,
131
- args: scriptArgs,
189
+ ...pwGlobals,
132
190
  require: scriptRequire,
133
191
  module: scriptModule,
134
192
  exports: scriptModule.exports,
@@ -137,7 +195,11 @@ async function execScript(scriptPath, scriptArgs, { browser, context, page, play
137
195
  __filename: absPath,
138
196
  __dirname: moduleDir,
139
197
  };
140
- const result = await withTemporaryGlobals(globals, () => runProgram(code, globals));
198
+ // If the script defines a main function, append a call to it.
199
+ const finalCode = hasMainFunction(code)
200
+ ? code + '\nreturn main({ page, context, browser, playwright, args });'
201
+ : code;
202
+ const result = await withTemporaryGlobals(globals, () => runProgram(finalCode, globals));
141
203
  scriptModule.loaded = true;
142
204
  return result === undefined ? scriptModule.exports : result;
143
205
  }
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+
3
+ const net = require('net');
4
+ const os = require('os');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const crypto = require('crypto');
8
+
9
+ const HOME_DIR = os.homedir();
10
+ const PW_CLI_DIR = path.join(HOME_DIR, '.pw-cli');
11
+ const WORKSPACE_HASH = crypto.createHash('sha1')
12
+ .update(PW_CLI_DIR)
13
+ .digest('hex')
14
+ .substring(0, 16);
15
+
16
+ function getSocketPath(sessionName = 'default') {
17
+ const socketName = `${sessionName}.sock`;
18
+ if (os.platform() === 'win32')
19
+ return `\\\\.\\pipe\\${WORKSPACE_HASH}-${socketName}`;
20
+ const socketsDir = process.env.PLAYWRIGHT_DAEMON_SOCKETS_DIR || path.join(os.tmpdir(), 'playwright-cli');
21
+ return path.join(socketsDir, WORKSPACE_HASH, socketName);
22
+ }
23
+
24
+ function getVersion() {
25
+ // Read version from session file (already written by playwright-cli daemon)
26
+ const sessionFile = path.join(
27
+ os.platform() === 'win32'
28
+ ? path.join(process.env.LOCALAPPDATA || path.join(HOME_DIR, 'AppData', 'Local'), 'ms-playwright', 'daemon')
29
+ : os.platform() === 'darwin'
30
+ ? path.join(HOME_DIR, 'Library', 'Caches', 'ms-playwright', 'daemon')
31
+ : path.join(process.env.XDG_CACHE_HOME || path.join(HOME_DIR, '.cache'), 'ms-playwright', 'daemon'),
32
+ WORKSPACE_HASH,
33
+ 'default.session'
34
+ );
35
+ try {
36
+ const session = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
37
+ return session.version;
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Convert a string array to minimist-style object.
45
+ * The daemon expects { _: [cmd, arg1, ...], flagName: value, ... }.
46
+ */
47
+ function toMinimistArgs(argv) {
48
+ const result = { _: [] };
49
+ for (let i = 0; i < argv.length; i++) {
50
+ const arg = argv[i];
51
+ if (arg.startsWith('--')) {
52
+ const key = arg.slice(2);
53
+ const next = argv[i + 1];
54
+ if (next && !next.startsWith('-')) {
55
+ result[key] = next;
56
+ i++;
57
+ } else {
58
+ result[key] = true;
59
+ }
60
+ } else if (arg.startsWith('-') && arg.length === 2) {
61
+ const key = arg.slice(1);
62
+ const next = argv[i + 1];
63
+ if (next && !next.startsWith('-')) {
64
+ result[key] = next;
65
+ i++;
66
+ } else {
67
+ result[key] = true;
68
+ }
69
+ } else {
70
+ result._.push(arg);
71
+ }
72
+ }
73
+ return result;
74
+ }
75
+
76
+ /**
77
+ * Send a command directly to the playwright-cli daemon socket.
78
+ * Returns { text, isError } or null (daemon not running).
79
+ */
80
+ function sendCommand(args, sessionName = 'default') {
81
+ const socketPath = getSocketPath(sessionName);
82
+ const version = getVersion();
83
+ if (!version) return Promise.resolve(null);
84
+
85
+ const minimistArgs = toMinimistArgs(args);
86
+
87
+ return new Promise((resolve, reject) => {
88
+ const socket = net.createConnection(socketPath, () => {
89
+ const message = JSON.stringify({
90
+ id: 1,
91
+ method: 'run',
92
+ params: { args: minimistArgs, cwd: process.cwd() },
93
+ version,
94
+ }) + '\n';
95
+ socket.write(message);
96
+ });
97
+
98
+ let buf = '';
99
+ socket.on('data', chunk => {
100
+ buf += chunk.toString();
101
+ const nlIdx = buf.indexOf('\n');
102
+ if (nlIdx === -1) return;
103
+ const line = buf.slice(0, nlIdx);
104
+ clearTimeout(timer);
105
+ socket.destroy();
106
+ try {
107
+ const resp = JSON.parse(line);
108
+ if (resp.error) {
109
+ reject(new Error(resp.error));
110
+ } else {
111
+ resolve(resp.result);
112
+ }
113
+ } catch (e) {
114
+ reject(new Error('Invalid daemon response'));
115
+ }
116
+ });
117
+
118
+ socket.on('error', () => { clearTimeout(timer); resolve(null); }); // connection failed = daemon not running
119
+ const timer = setTimeout(() => { socket.destroy(); resolve(null); }, 3000);
120
+ });
121
+ }
122
+
123
+ module.exports = { sendCommand, getSocketPath, getVersion };
@@ -62,7 +62,7 @@ if (!profileDir) {
62
62
  channel: 'chrome',
63
63
  headless,
64
64
  args: [`--remote-debugging-port=${port}`],
65
- ignoreDefaultArgs: ['--enable-automation'],
65
+ ignoreDefaultArgs: ['--enable-automation', '--no-sandbox'],
66
66
  });
67
67
 
68
68
  // Wait briefly for CDP to be ready, then signal