@guanzhu.me/pw-cli 0.0.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.
- package/LICENSE +21 -0
- package/README.md +333 -0
- package/bin/pw-cli.js +870 -0
- package/package.json +38 -0
- package/src/browser-manager.js +178 -0
- package/src/cli.js +168 -0
- package/src/executor.js +77 -0
- package/src/launch-daemon.js +65 -0
- package/src/queue.js +48 -0
- package/src/state.js +47 -0
- package/src/utils.js +55 -0
package/bin/pw-cli.js
ADDED
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
process.on('uncaughtException', err => {
|
|
5
|
+
process.stderr.write(`pw-cli fatal: ${err.message}\n`);
|
|
6
|
+
process.exit(1);
|
|
7
|
+
});
|
|
8
|
+
process.on('unhandledRejection', err => {
|
|
9
|
+
process.stderr.write(`pw-cli fatal: ${err && err.message || err}\n`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const net = require('net');
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
const { execSync } = require('child_process');
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Global session setup — makes playwright-cli always find the same session
|
|
22
|
+
// regardless of which directory pw-cli is run from.
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
const HOME_DIR = os.homedir();
|
|
25
|
+
const PW_CLI_DIR = path.join(HOME_DIR, '.pw-cli');
|
|
26
|
+
const DEFAULT_PROFILE = path.join(PW_CLI_DIR, 'profiles', 'default');
|
|
27
|
+
const PLAYWRIGHT_MARKER = path.join(PW_CLI_DIR, '.playwright'); // makes cwd a "workspace"
|
|
28
|
+
|
|
29
|
+
fs.mkdirSync(DEFAULT_PROFILE, { recursive: true });
|
|
30
|
+
fs.mkdirSync(PLAYWRIGHT_MARKER, { recursive: true });
|
|
31
|
+
|
|
32
|
+
// Compute session hash the same way playwright-cli does:
|
|
33
|
+
// sha1(workspaceDir).hex[:16] where workspaceDir = PW_CLI_DIR
|
|
34
|
+
const WORKSPACE_HASH = crypto.createHash('sha1')
|
|
35
|
+
.update(PW_CLI_DIR)
|
|
36
|
+
.digest('hex')
|
|
37
|
+
.substring(0, 16);
|
|
38
|
+
|
|
39
|
+
// Locate @playwright/cli via npm global root
|
|
40
|
+
function findPlaywrightCli() {
|
|
41
|
+
try {
|
|
42
|
+
const globalRoot = execSync('npm root -g', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
43
|
+
const cliEntry = path.join(globalRoot, '@playwright', 'cli', 'node_modules', 'playwright', 'lib', 'cli', 'client', 'program.js');
|
|
44
|
+
if (fs.existsSync(cliEntry)) return cliEntry;
|
|
45
|
+
} catch { /* ignore */ }
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Daemon session file location (Windows / macOS / Linux)
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
function getDaemonDir() {
|
|
53
|
+
if (process.platform === 'win32') {
|
|
54
|
+
return path.join(process.env.LOCALAPPDATA || path.join(HOME_DIR, 'AppData', 'Local'), 'ms-playwright', 'daemon');
|
|
55
|
+
} else if (process.platform === 'darwin') {
|
|
56
|
+
return path.join(HOME_DIR, 'Library', 'Caches', 'ms-playwright', 'daemon');
|
|
57
|
+
}
|
|
58
|
+
return path.join(process.env.XDG_CACHE_HOME || path.join(HOME_DIR, '.cache'), 'ms-playwright', 'daemon');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isSessionFilePresent(sessionName = 'default') {
|
|
62
|
+
const sessionFile = path.join(getDaemonDir(), WORKSPACE_HASH, `${sessionName}.session`);
|
|
63
|
+
return fs.existsSync(sessionFile);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function isSessionAlive(sessionName = 'default') {
|
|
67
|
+
if (!isSessionFilePresent(sessionName)) return false;
|
|
68
|
+
// probe the named pipe / socket
|
|
69
|
+
const socketPath = process.platform === 'win32'
|
|
70
|
+
? `\\\\.\\pipe\\${WORKSPACE_HASH}-${sessionName}.sock`
|
|
71
|
+
: path.join(process.env.PLAYWRIGHT_DAEMON_SOCKETS_DIR || path.join(os.tmpdir(), 'playwright-cli'), WORKSPACE_HASH, `${sessionName}.sock`);
|
|
72
|
+
return new Promise(resolve => {
|
|
73
|
+
const s = net.connect(socketPath);
|
|
74
|
+
s.once('connect', () => { s.destroy(); resolve(true); });
|
|
75
|
+
s.once('error', () => resolve(false));
|
|
76
|
+
setTimeout(() => { s.destroy(); resolve(false); }, 1500);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Argv helpers
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
function hasFlag(argv, ...flags) {
|
|
84
|
+
return flags.some(f => argv.includes(f));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function injectOpenDefaults(argv) {
|
|
88
|
+
const result = [...argv];
|
|
89
|
+
// inject --headed unless explicitly suppressed
|
|
90
|
+
if (!hasFlag(result, '--headed', '--no-headed')) {
|
|
91
|
+
result.push('--headed');
|
|
92
|
+
}
|
|
93
|
+
// inject persistent + default profile unless user specified their own
|
|
94
|
+
if (!hasFlag(result, '--persistent', '--no-persistent', '--profile')) {
|
|
95
|
+
result.push('--persistent', '--profile', DEFAULT_PROFILE);
|
|
96
|
+
} else if (hasFlag(result, '--profile') && !hasFlag(result, '--persistent', '--no-persistent')) {
|
|
97
|
+
result.push('--persistent');
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getCommandAndSession(argv) {
|
|
103
|
+
// find session flag
|
|
104
|
+
let session = 'default';
|
|
105
|
+
const sIdx = argv.findIndex(a => a === '-s' || a === '--session');
|
|
106
|
+
if (sIdx !== -1 && argv[sIdx + 1]) session = argv[sIdx + 1];
|
|
107
|
+
const sEq = argv.find(a => a.startsWith('-s=') || a.startsWith('--session='));
|
|
108
|
+
if (sEq) session = sEq.split('=')[1];
|
|
109
|
+
|
|
110
|
+
// first non-flag positional is the command
|
|
111
|
+
const command = argv.find(a => !a.startsWith('-') && !/^\d/.test(a));
|
|
112
|
+
return { command, session };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parsePwCliGlobalOptions(argv) {
|
|
116
|
+
const options = { headless: false, profile: 'default', port: 9223 };
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < argv.length; i++) {
|
|
119
|
+
const arg = argv[i];
|
|
120
|
+
if (arg === '--headless') {
|
|
121
|
+
options.headless = true;
|
|
122
|
+
} else if (arg === '--profile' && argv[i + 1]) {
|
|
123
|
+
options.profile = argv[++i];
|
|
124
|
+
} else if (arg === '--port' && argv[i + 1]) {
|
|
125
|
+
options.port = parseInt(argv[++i], 10);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return options;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function printMainHelp() {
|
|
133
|
+
process.stdout.write(`pw-cli - run Playwright terminal commands with pw-cli enhancements
|
|
134
|
+
|
|
135
|
+
Usage: pw-cli <command> [args] [options]
|
|
136
|
+
Usage: pw-cli -s=<session> <command> [args] [options]
|
|
137
|
+
|
|
138
|
+
pw-cli wraps @playwright/cli and keeps its command model. It also adds:
|
|
139
|
+
- headed + persistent defaults for browser open flows
|
|
140
|
+
- auto-open when a browser-backed command needs a session
|
|
141
|
+
- run-code from stdin or inline JavaScript
|
|
142
|
+
- run-script for local .js files
|
|
143
|
+
- queue subcommands for batching commands
|
|
144
|
+
- XPath support for click/dblclick/hover/fill/check/uncheck/select/drag
|
|
145
|
+
|
|
146
|
+
Core:
|
|
147
|
+
open [url] open the browser
|
|
148
|
+
pw-cli: injects headed + persistent defaults
|
|
149
|
+
pw-cli: if url is provided, opens first and then navigates
|
|
150
|
+
close close the browser
|
|
151
|
+
goto <url> navigate to a url
|
|
152
|
+
type <text> type text into editable element
|
|
153
|
+
click <ref> [button] perform click on a web page
|
|
154
|
+
pw-cli: accepts XPath ref
|
|
155
|
+
dblclick <ref> [button] perform double click on a web page
|
|
156
|
+
pw-cli: accepts XPath ref
|
|
157
|
+
fill <ref> <text> fill text into editable element
|
|
158
|
+
pw-cli: accepts XPath ref
|
|
159
|
+
drag <startRef> <endRef> perform drag and drop between two elements
|
|
160
|
+
pw-cli: accepts XPath refs
|
|
161
|
+
hover <ref> hover over element on page
|
|
162
|
+
pw-cli: accepts XPath ref
|
|
163
|
+
select <ref> <val> select an option in a dropdown
|
|
164
|
+
pw-cli: accepts XPath ref
|
|
165
|
+
upload <file> upload one or multiple files
|
|
166
|
+
check <ref> check a checkbox or radio button
|
|
167
|
+
pw-cli: accepts XPath ref
|
|
168
|
+
uncheck <ref> uncheck a checkbox or radio button
|
|
169
|
+
pw-cli: accepts XPath ref
|
|
170
|
+
snapshot capture page snapshot to obtain element ref
|
|
171
|
+
eval <func> [ref] evaluate javascript expression on page or element
|
|
172
|
+
dialog-accept [prompt] accept a dialog
|
|
173
|
+
dialog-dismiss dismiss a dialog
|
|
174
|
+
resize <w> <h> resize the browser window
|
|
175
|
+
delete-data delete session data
|
|
176
|
+
|
|
177
|
+
Navigation:
|
|
178
|
+
go-back go back to the previous page
|
|
179
|
+
go-forward go forward to the next page
|
|
180
|
+
reload reload the current page
|
|
181
|
+
|
|
182
|
+
Keyboard:
|
|
183
|
+
press <key> press a key on the keyboard, \`a\`, \`arrowleft\`
|
|
184
|
+
keydown <key> press a key down on the keyboard
|
|
185
|
+
keyup <key> press a key up on the keyboard
|
|
186
|
+
|
|
187
|
+
Mouse:
|
|
188
|
+
mousemove <x> <y> move mouse to a given position
|
|
189
|
+
mousedown [button] press mouse down
|
|
190
|
+
mouseup [button] press mouse up
|
|
191
|
+
mousewheel <dx> <dy> scroll mouse wheel
|
|
192
|
+
|
|
193
|
+
Save as:
|
|
194
|
+
screenshot [ref] screenshot of the current page or element
|
|
195
|
+
pdf save page as pdf
|
|
196
|
+
|
|
197
|
+
Tabs:
|
|
198
|
+
tab-list list all tabs
|
|
199
|
+
tab-new [url] create a new tab
|
|
200
|
+
tab-close [index] close a browser tab
|
|
201
|
+
tab-select <index> select a browser tab
|
|
202
|
+
|
|
203
|
+
Storage:
|
|
204
|
+
state-load <filename> loads browser storage (authentication) state from a file
|
|
205
|
+
state-save [filename] saves the current storage (authentication) state to a file
|
|
206
|
+
cookie-list list all cookies (optionally filtered by domain/path)
|
|
207
|
+
cookie-get <name> get a specific cookie by name
|
|
208
|
+
cookie-set <name> <value> set a cookie with optional flags
|
|
209
|
+
cookie-delete <name> delete a specific cookie
|
|
210
|
+
cookie-clear clear all cookies
|
|
211
|
+
localstorage-list list all localstorage key-value pairs
|
|
212
|
+
localstorage-get <key> get a localstorage item by key
|
|
213
|
+
localstorage-set <key> <value> set a localstorage item
|
|
214
|
+
localstorage-delete <key> delete a localstorage item
|
|
215
|
+
localstorage-clear clear all localstorage
|
|
216
|
+
sessionstorage-list list all sessionstorage key-value pairs
|
|
217
|
+
sessionstorage-get <key> get a sessionstorage item by key
|
|
218
|
+
sessionstorage-set <key> <value> set a sessionstorage item
|
|
219
|
+
sessionstorage-delete <key> delete a sessionstorage item
|
|
220
|
+
sessionstorage-clear clear all sessionstorage
|
|
221
|
+
|
|
222
|
+
Network:
|
|
223
|
+
route <pattern> mock network requests matching a url pattern
|
|
224
|
+
route-list list all active network routes
|
|
225
|
+
unroute [pattern] remove routes matching a pattern (or all routes)
|
|
226
|
+
|
|
227
|
+
DevTools:
|
|
228
|
+
console [min-level] list console messages
|
|
229
|
+
run-code <code> run playwright code snippet
|
|
230
|
+
pw-cli: reads code from stdin when <code> is omitted
|
|
231
|
+
pw-cli: wraps plain statements in an async function
|
|
232
|
+
run-script <file> [...] run a local JavaScript file with page/context/browser globals
|
|
233
|
+
network list all network requests since loading the page
|
|
234
|
+
tracing-start start trace recording
|
|
235
|
+
tracing-stop stop trace recording
|
|
236
|
+
video-start start video recording
|
|
237
|
+
video-stop stop video recording
|
|
238
|
+
show show browser devtools
|
|
239
|
+
devtools-start show browser devtools
|
|
240
|
+
|
|
241
|
+
Install:
|
|
242
|
+
install initialize workspace
|
|
243
|
+
install-browser install browser
|
|
244
|
+
|
|
245
|
+
Browser sessions:
|
|
246
|
+
list list browser sessions
|
|
247
|
+
close-all close all browser sessions
|
|
248
|
+
kill-all forcefully kill all browser sessions (for stale/zombie processes)
|
|
249
|
+
|
|
250
|
+
pw-cli queue:
|
|
251
|
+
queue add <command> [args...] add a command to the queue
|
|
252
|
+
queue list show queued commands
|
|
253
|
+
queue run [--fail-fast] execute queued commands in order
|
|
254
|
+
queue remove <id> remove a queued command by id prefix
|
|
255
|
+
queue clear clear the queue
|
|
256
|
+
|
|
257
|
+
Global options:
|
|
258
|
+
--help [command] print help
|
|
259
|
+
-h print help
|
|
260
|
+
--version print version
|
|
261
|
+
-s, --session <name> choose browser session
|
|
262
|
+
--headless used by pw-cli-managed browser launches
|
|
263
|
+
|
|
264
|
+
Requirements:
|
|
265
|
+
Node.js 18+
|
|
266
|
+
playwright
|
|
267
|
+
@playwright/cli
|
|
268
|
+
|
|
269
|
+
Examples:
|
|
270
|
+
pw-cli open https://example.com
|
|
271
|
+
pw-cli run-code "await page.goto('https://example.com'); return await page.title()"
|
|
272
|
+
echo "return await page.url()" | pw-cli run-code
|
|
273
|
+
pw-cli run-script .\\scripts\\smoke.js --env prod
|
|
274
|
+
pw-cli click "//button[contains(., 'Submit')]"
|
|
275
|
+
pw-cli queue add goto https://example.com
|
|
276
|
+
pw-cli queue add snapshot
|
|
277
|
+
pw-cli queue run
|
|
278
|
+
`.trim() + '\n');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Management commands that don't need a running browser
|
|
282
|
+
const MGMT_COMMANDS = new Set([
|
|
283
|
+
'open', 'close', 'list', 'kill-all', 'close-all', 'delete-data',
|
|
284
|
+
'install', 'install-browser', 'show', 'config-print', 'tray',
|
|
285
|
+
'queue',
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// Auto-open: launches playwright-cli open with defaults if no session running
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
function autoOpen(session) {
|
|
292
|
+
const { spawnSync } = require('child_process');
|
|
293
|
+
const cliPath = findPlaywrightCli();
|
|
294
|
+
if (!cliPath) {
|
|
295
|
+
process.stderr.write('pw-cli: @playwright/cli not found. Install with: npm install -g @playwright/cli\n');
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
const openArgv = [
|
|
299
|
+
'node', 'pw-cli',
|
|
300
|
+
...(session !== 'default' ? ['-s', session] : []),
|
|
301
|
+
'open',
|
|
302
|
+
'--headed', '--persistent', '--profile', DEFAULT_PROFILE,
|
|
303
|
+
];
|
|
304
|
+
process.chdir(PW_CLI_DIR);
|
|
305
|
+
const prog = require(cliPath); // runs synchronously until browser ready
|
|
306
|
+
// playwright-cli's program() is async and calls process.exit when done,
|
|
307
|
+
// but "open" detaches the daemon – so we need to wait a beat.
|
|
308
|
+
// Instead, spawn a separate node process synchronously.
|
|
309
|
+
// Reset and re-approach: spawn as a child.
|
|
310
|
+
// (Require approach won't work cleanly for open — see delegation below)
|
|
311
|
+
void prog; // unused, actual open is done via spawnSync below
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
// stdin reader
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
function readStdin() {
|
|
318
|
+
return new Promise((resolve, reject) => {
|
|
319
|
+
let data = '';
|
|
320
|
+
process.stdin.setEncoding('utf8');
|
|
321
|
+
process.stdin.on('data', c => { data += c; });
|
|
322
|
+
process.stdin.on('end', () => resolve(data.trim()));
|
|
323
|
+
process.stdin.on('error', reject);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Wrap raw code in async function if not already a function expression
|
|
328
|
+
function wrapCodeIfNeeded(code) {
|
|
329
|
+
const t = code.trim();
|
|
330
|
+
// Match: async (page) => ..., async function, (async ..., function, async page => ...
|
|
331
|
+
if (/^async\s*[\w(]|^async\s+function|^\(async|^function/.test(t)) return t;
|
|
332
|
+
return `async (page) => {\n${code}\n}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
// XPath support
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
// 检测是否为 XPath 表达式
|
|
340
|
+
function isXPath(str) {
|
|
341
|
+
if (!str || typeof str !== 'string') return false;
|
|
342
|
+
const t = str.trim();
|
|
343
|
+
return t.startsWith('//') || t.startsWith('(//') || t.startsWith('xpath=');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 标准化为 xpath=... 格式
|
|
347
|
+
function toXPathLocator(str) {
|
|
348
|
+
const t = str.trim();
|
|
349
|
+
if (t.startsWith('xpath=')) return t;
|
|
350
|
+
return `xpath=${t}`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 用单引号包裹字符串(单引号本身用 \' 转义),避免双引号与 minimist 解析冲突
|
|
354
|
+
function jsStr(s) {
|
|
355
|
+
return `'${String(s).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// locator 表达式片段
|
|
359
|
+
function xpathLocator(ref) {
|
|
360
|
+
return `page.locator(${jsStr(toXPathLocator(ref))})`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// 各命令转 run-code 代码的映射
|
|
364
|
+
// 返回 JS 代码字符串,或 null 表示无需转换
|
|
365
|
+
function buildXPathCode(command, positionals, flags) {
|
|
366
|
+
const buttonFlag = flags['--button'] || flags['-b'];
|
|
367
|
+
const forceFlag = flags['--force'] || flags['-f'];
|
|
368
|
+
|
|
369
|
+
switch (command) {
|
|
370
|
+
case 'click':
|
|
371
|
+
case 'dblclick': {
|
|
372
|
+
const [ref] = positionals;
|
|
373
|
+
if (!isXPath(ref) && !forceFlag) return null;
|
|
374
|
+
const method = command === 'dblclick' ? 'dblclick' : 'click';
|
|
375
|
+
const locator = isXPath(ref)
|
|
376
|
+
? xpathLocator(ref)
|
|
377
|
+
: `page.locator(${jsStr('aria-ref=' + ref)})`;
|
|
378
|
+
const opts = [];
|
|
379
|
+
if (buttonFlag) opts.push(`button: ${jsStr(buttonFlag)}`);
|
|
380
|
+
if (forceFlag) opts.push('force: true');
|
|
381
|
+
const optsStr = opts.length ? `{ ${opts.join(', ')} }` : '';
|
|
382
|
+
return `await ${locator}.${method}(${optsStr})`;
|
|
383
|
+
}
|
|
384
|
+
case 'hover': {
|
|
385
|
+
const [ref] = positionals;
|
|
386
|
+
if (!isXPath(ref)) return null;
|
|
387
|
+
return `await ${xpathLocator(ref)}.hover()`;
|
|
388
|
+
}
|
|
389
|
+
case 'fill': {
|
|
390
|
+
const [ref, text] = positionals;
|
|
391
|
+
if (!isXPath(ref)) return null;
|
|
392
|
+
return `await ${xpathLocator(ref)}.fill(${jsStr(text || '')})`;
|
|
393
|
+
}
|
|
394
|
+
case 'check': {
|
|
395
|
+
const [ref] = positionals;
|
|
396
|
+
if (!isXPath(ref)) return null;
|
|
397
|
+
return `await ${xpathLocator(ref)}.check()`;
|
|
398
|
+
}
|
|
399
|
+
case 'uncheck': {
|
|
400
|
+
const [ref] = positionals;
|
|
401
|
+
if (!isXPath(ref)) return null;
|
|
402
|
+
return `await ${xpathLocator(ref)}.uncheck()`;
|
|
403
|
+
}
|
|
404
|
+
case 'select': {
|
|
405
|
+
const [ref, value] = positionals;
|
|
406
|
+
if (!isXPath(ref)) return null;
|
|
407
|
+
return `await ${xpathLocator(ref)}.selectOption(${jsStr(value || '')})`;
|
|
408
|
+
}
|
|
409
|
+
case 'drag': {
|
|
410
|
+
const [startRef, endRef] = positionals;
|
|
411
|
+
if (!isXPath(startRef) && !isXPath(endRef)) return null;
|
|
412
|
+
// 任意一端是 XPath 就整条转换;非 XPath 端保持 aria-ref 格式
|
|
413
|
+
const srcLocator = isXPath(startRef)
|
|
414
|
+
? xpathLocator(startRef)
|
|
415
|
+
: `page.locator(${jsStr(`aria-ref=${startRef}`)})`;
|
|
416
|
+
const dstLocator = isXPath(endRef)
|
|
417
|
+
? xpathLocator(endRef)
|
|
418
|
+
: `page.locator(${jsStr(`aria-ref=${endRef}`)})`;
|
|
419
|
+
return `await ${srcLocator}.dragTo(${dstLocator})`;
|
|
420
|
+
}
|
|
421
|
+
default:
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const XPATH_COMMANDS = new Set(['click', 'dblclick', 'hover', 'fill', 'check', 'uncheck', 'select', 'drag']);
|
|
427
|
+
|
|
428
|
+
// 主入口:检测 argv 中是否有 XPath ref,有则整条命令转为 run-code
|
|
429
|
+
function convertXPathCommand(argv) {
|
|
430
|
+
const cmdIdx = argv.findIndex(a => XPATH_COMMANDS.has(a));
|
|
431
|
+
if (cmdIdx === -1) return argv;
|
|
432
|
+
|
|
433
|
+
const command = argv[cmdIdx];
|
|
434
|
+
const afterCmd = argv.slice(cmdIdx + 1);
|
|
435
|
+
|
|
436
|
+
// 分离 positionals 和 flags
|
|
437
|
+
const positionals = [];
|
|
438
|
+
const flags = {};
|
|
439
|
+
for (let i = 0; i < afterCmd.length; i++) {
|
|
440
|
+
const arg = afterCmd[i];
|
|
441
|
+
const next = afterCmd[i + 1];
|
|
442
|
+
if (arg.startsWith('--') && next && !next.startsWith('-')) {
|
|
443
|
+
flags[arg] = next;
|
|
444
|
+
i++;
|
|
445
|
+
} else if (arg.startsWith('-') && arg.length === 2 && next && !next.startsWith('-')) {
|
|
446
|
+
flags[arg] = next;
|
|
447
|
+
i++;
|
|
448
|
+
} else if (arg.startsWith('-')) {
|
|
449
|
+
flags[arg] = true; // 布尔标志,如 --force
|
|
450
|
+
} else {
|
|
451
|
+
positionals.push(arg);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const code = buildXPathCode(command, positionals, flags);
|
|
456
|
+
if (!code) return argv; // 没有 XPath,原样返回
|
|
457
|
+
|
|
458
|
+
// 保留命令前的全局参数(session 等),替换为 run-code
|
|
459
|
+
const beforeCmd = argv.slice(0, cmdIdx);
|
|
460
|
+
const wrappedCode = `async (page) => { ${code} }`;
|
|
461
|
+
return [...beforeCmd, 'run-code', wrappedCode];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ---------------------------------------------------------------------------
|
|
465
|
+
// queue — batch multiple actions and run them together
|
|
466
|
+
// ---------------------------------------------------------------------------
|
|
467
|
+
async function handleQueue(rawArgv) {
|
|
468
|
+
const { readQueue, addItem, removeItem, clearQueue } = require('../src/queue');
|
|
469
|
+
const { spawnSync } = require('child_process');
|
|
470
|
+
|
|
471
|
+
const queueIdx = rawArgv.indexOf('queue');
|
|
472
|
+
const globalArgs = rawArgv.slice(0, queueIdx); // e.g. ['-s', 'work']
|
|
473
|
+
const subCmd = rawArgv[queueIdx + 1];
|
|
474
|
+
const rest = rawArgv.slice(queueIdx + 2);
|
|
475
|
+
|
|
476
|
+
switch (subCmd) {
|
|
477
|
+
case 'add': {
|
|
478
|
+
if (!rest.length) {
|
|
479
|
+
process.stderr.write('pw-cli: queue add requires a command\n\nUsage: pw-cli queue add <command> [args...]\n');
|
|
480
|
+
process.exit(1);
|
|
481
|
+
}
|
|
482
|
+
const [command, ...args] = rest;
|
|
483
|
+
const item = addItem(command, args);
|
|
484
|
+
console.log(`queued [${item.id}] ${command}${args.length ? ' ' + args.join(' ') : ''}`);
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
case 'list': {
|
|
489
|
+
const queue = readQueue();
|
|
490
|
+
if (!queue.length) {
|
|
491
|
+
console.log('Queue is empty.');
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
console.log(`Queue (${queue.length} item${queue.length === 1 ? '' : 's'}):`);
|
|
495
|
+
queue.forEach((item, i) => {
|
|
496
|
+
const argStr = item.args && item.args.length ? ' ' + item.args.join(' ') : '';
|
|
497
|
+
console.log(` #${i + 1} [${item.id.slice(0, 7)}] ${item.command}${argStr}`);
|
|
498
|
+
});
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
case 'remove': {
|
|
503
|
+
const [idPrefix] = rest;
|
|
504
|
+
if (!idPrefix) {
|
|
505
|
+
process.stderr.write('pw-cli: queue remove requires an id\n\nUsage: pw-cli queue remove <id>\n');
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
const removed = removeItem(idPrefix);
|
|
509
|
+
if (!removed) {
|
|
510
|
+
process.stderr.write(`pw-cli: no queue item matching "${idPrefix}"\n`);
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
console.log(`removed [${removed.id}] ${removed.command}`);
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
case 'clear': {
|
|
518
|
+
clearQueue();
|
|
519
|
+
console.log('Queue cleared.');
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
case 'run': {
|
|
524
|
+
const queue = readQueue();
|
|
525
|
+
if (!queue.length) {
|
|
526
|
+
console.log('Queue is empty, nothing to run.');
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const failFast = rest.includes('--fail-fast');
|
|
531
|
+
const scriptPath = process.argv[1]; // absolute path to pw-cli.js
|
|
532
|
+
|
|
533
|
+
console.log(`Running ${queue.length} queued item${queue.length === 1 ? '' : 's'}...`);
|
|
534
|
+
let failed = 0;
|
|
535
|
+
|
|
536
|
+
for (let i = 0; i < queue.length; i++) {
|
|
537
|
+
const item = queue[i];
|
|
538
|
+
const argStr = item.args && item.args.length ? ' ' + item.args.join(' ') : '';
|
|
539
|
+
process.stdout.write(` [${i + 1}/${queue.length}] ${item.command}${argStr} ... `);
|
|
540
|
+
|
|
541
|
+
const itemArgv = [item.command, ...(item.args || [])];
|
|
542
|
+
const res = spawnSync(process.execPath, [scriptPath, ...globalArgs, ...itemArgv], {
|
|
543
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
544
|
+
encoding: 'utf8',
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
if (res.status === 0) {
|
|
548
|
+
process.stdout.write('ok\n');
|
|
549
|
+
if (res.stdout && res.stdout.trim()) console.log(res.stdout.trimEnd());
|
|
550
|
+
} else {
|
|
551
|
+
process.stdout.write('FAILED\n');
|
|
552
|
+
if (res.stderr && res.stderr.trim()) process.stderr.write(res.stderr.trimEnd() + '\n');
|
|
553
|
+
failed++;
|
|
554
|
+
if (failFast) {
|
|
555
|
+
process.stderr.write(`pw-cli: queue run aborted at item ${i + 1} (--fail-fast)\n`);
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (failed === 0) {
|
|
562
|
+
console.log('All items completed successfully.');
|
|
563
|
+
} else {
|
|
564
|
+
console.log(`Done. ${failed} item${failed === 1 ? '' : 's'} failed.`);
|
|
565
|
+
process.exit(1);
|
|
566
|
+
}
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
default: {
|
|
571
|
+
process.stdout.write(`pw-cli queue — batch actions and run them together
|
|
572
|
+
|
|
573
|
+
USAGE
|
|
574
|
+
pw-cli queue <subcommand> [args...]
|
|
575
|
+
|
|
576
|
+
SUBCOMMANDS
|
|
577
|
+
add <command> [args...] Add an action to the queue
|
|
578
|
+
list Show all queued actions
|
|
579
|
+
run [--fail-fast] Execute all queued actions in order
|
|
580
|
+
remove <id> Remove a specific item by id prefix
|
|
581
|
+
clear Empty the queue
|
|
582
|
+
|
|
583
|
+
EXAMPLES
|
|
584
|
+
pw-cli queue add goto https://example.com
|
|
585
|
+
pw-cli queue add click e12
|
|
586
|
+
pw-cli queue add fill e5 "hello world"
|
|
587
|
+
pw-cli queue add run-code "return await page.title()"
|
|
588
|
+
pw-cli queue list
|
|
589
|
+
pw-cli queue run
|
|
590
|
+
pw-cli queue run --fail-fast
|
|
591
|
+
pw-cli queue clear
|
|
592
|
+
`);
|
|
593
|
+
if (subCmd && subCmd !== 'help' && subCmd !== '--help' && subCmd !== '-h') {
|
|
594
|
+
process.stderr.write(`pw-cli: unknown queue subcommand: ${subCmd}\n`);
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ---------------------------------------------------------------------------
|
|
603
|
+
// run-script — delegates to our CDP-based executor
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
async function handleRunScript(rawArgv) {
|
|
606
|
+
const { getConnection, killBrowser } = require('../src/browser-manager');
|
|
607
|
+
const { execScript } = require('../src/executor');
|
|
608
|
+
|
|
609
|
+
// parse: pw-cli [global-opts] run-script <file> [script-args...]
|
|
610
|
+
const rsIdx = rawArgv.indexOf('run-script');
|
|
611
|
+
const afterRs = rawArgv.slice(rsIdx + 1);
|
|
612
|
+
const globalBefore = rawArgv.slice(0, rsIdx);
|
|
613
|
+
|
|
614
|
+
// extract --headless from global opts
|
|
615
|
+
const headless = hasFlag(globalBefore, '--headless');
|
|
616
|
+
const [scriptPath, ...scriptArgs] = afterRs;
|
|
617
|
+
|
|
618
|
+
if (!scriptPath) {
|
|
619
|
+
process.stderr.write('pw-cli: run-script requires a script path\n\nUsage: pw-cli run-script <file.js> [args...]\n');
|
|
620
|
+
process.exit(1);
|
|
621
|
+
}
|
|
622
|
+
if (!fs.existsSync(path.resolve(scriptPath))) {
|
|
623
|
+
process.stderr.write(`pw-cli: script not found: ${path.resolve(scriptPath)}\n`);
|
|
624
|
+
process.exit(3);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const conn = await getConnection({ headless, profile: 'default', port: 9223 });
|
|
628
|
+
try {
|
|
629
|
+
const result = await execScript(scriptPath, scriptArgs, conn);
|
|
630
|
+
if (result !== undefined) console.log(result);
|
|
631
|
+
} catch (err) {
|
|
632
|
+
process.stderr.write(`pw-cli: ${err.message}\n`);
|
|
633
|
+
process.exit(1);
|
|
634
|
+
} finally {
|
|
635
|
+
await conn.browser.close();
|
|
636
|
+
}
|
|
637
|
+
process.exit(0);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async function handleRunCode(rawArgv) {
|
|
641
|
+
const { getConnection } = require('../src/browser-manager');
|
|
642
|
+
const { execCode } = require('../src/executor');
|
|
643
|
+
|
|
644
|
+
const rcIdx = rawArgv.indexOf('run-code');
|
|
645
|
+
const beforeRc = rawArgv.slice(0, rcIdx);
|
|
646
|
+
const afterRc = rawArgv.slice(rcIdx + 1);
|
|
647
|
+
const options = parsePwCliGlobalOptions(beforeRc);
|
|
648
|
+
|
|
649
|
+
let code = afterRc.join(' ').trim();
|
|
650
|
+
|
|
651
|
+
if (!code) {
|
|
652
|
+
if (process.stdin.isTTY) {
|
|
653
|
+
process.stderr.write(
|
|
654
|
+
'pw-cli: no code provided.\n\n' +
|
|
655
|
+
'Usage:\n' +
|
|
656
|
+
' pw-cli run-code "await page.goto(\'https://example.com\')"\n' +
|
|
657
|
+
' pw-cli run-code "async (page) => { await page.goto(\'https://example.com\') }"\n' +
|
|
658
|
+
' @\'\n' +
|
|
659
|
+
' await page.goto(\'https://example.com\')\n' +
|
|
660
|
+
' \'@ | pw-cli run-code\n'
|
|
661
|
+
);
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
code = (await readStdin()).trim();
|
|
666
|
+
if (!code) {
|
|
667
|
+
process.stderr.write('pw-cli: empty code from stdin\n');
|
|
668
|
+
process.exit(1);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const conn = await getConnection(options);
|
|
673
|
+
try {
|
|
674
|
+
const result = await execCode(code, conn);
|
|
675
|
+
if (result !== undefined) {
|
|
676
|
+
console.log(result);
|
|
677
|
+
}
|
|
678
|
+
} catch (err) {
|
|
679
|
+
process.stderr.write(`pw-cli: ${err.message || err}\n`);
|
|
680
|
+
process.exit(1);
|
|
681
|
+
} finally {
|
|
682
|
+
await conn.browser.close();
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
process.exit(0);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ---------------------------------------------------------------------------
|
|
689
|
+
// Main
|
|
690
|
+
// ---------------------------------------------------------------------------
|
|
691
|
+
async function main() {
|
|
692
|
+
const ORIGINAL_CWD = process.cwd();
|
|
693
|
+
const rawArgv = process.argv.slice(2);
|
|
694
|
+
const { command, session } = getCommandAndSession(rawArgv);
|
|
695
|
+
|
|
696
|
+
if (!command || command === 'help' || (rawArgv.length === 1 && hasFlag(rawArgv, '--help', '-h'))) {
|
|
697
|
+
printMainHelp();
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// ── queue: batch actions and run them together ────────────────────────────
|
|
702
|
+
if (command === 'queue') {
|
|
703
|
+
await handleQueue(rawArgv);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ── run-script: handled entirely by our CDP executor ─────────────────────
|
|
708
|
+
if (command === 'run-script') {
|
|
709
|
+
await handleRunScript(rawArgv);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// ── run-code: handled entirely by our CDP executor ───────────────────────
|
|
714
|
+
if (command === 'run-code') {
|
|
715
|
+
await handleRunCode(rawArgv);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// ── From here on: delegate to playwright-cli (with enhancements) ─────────
|
|
720
|
+
const cliPath = findPlaywrightCli();
|
|
721
|
+
if (!cliPath) {
|
|
722
|
+
process.stderr.write('pw-cli: @playwright/cli not found.\nInstall: npm install -g @playwright/cli\n');
|
|
723
|
+
process.exit(1);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
let argv = [...rawArgv];
|
|
727
|
+
|
|
728
|
+
// ── run-code: stdin support + auto-wrap plain code as function ───────────
|
|
729
|
+
if (command === 'run-code') {
|
|
730
|
+
const cmdIdx = argv.indexOf('run-code');
|
|
731
|
+
const afterCmd = argv.slice(cmdIdx + 1);
|
|
732
|
+
const positionals = afterCmd.filter(a => !a.startsWith('-'));
|
|
733
|
+
|
|
734
|
+
if (positionals.length === 0) {
|
|
735
|
+
// No inline code — try stdin
|
|
736
|
+
if (process.stdin.isTTY) {
|
|
737
|
+
process.stderr.write('pw-cli: no code provided.\n\nUsage:\n pw-cli run-code "<async (page) => { ... }>"\n echo "return await page.title()" | pw-cli run-code\n');
|
|
738
|
+
process.exit(1);
|
|
739
|
+
}
|
|
740
|
+
const code = await readStdin();
|
|
741
|
+
if (!code) {
|
|
742
|
+
process.stderr.write('pw-cli: empty code from stdin\n');
|
|
743
|
+
process.exit(1);
|
|
744
|
+
}
|
|
745
|
+
argv.splice(cmdIdx + 1, 0, wrapCodeIfNeeded(code));
|
|
746
|
+
} else {
|
|
747
|
+
// Inline code provided — wrap if it's plain statements, not a function
|
|
748
|
+
const codeArg = positionals[0];
|
|
749
|
+
const wrapped = wrapCodeIfNeeded(codeArg);
|
|
750
|
+
if (wrapped !== codeArg) {
|
|
751
|
+
// replace original positional with wrapped version
|
|
752
|
+
const origIdx = argv.indexOf(codeArg, cmdIdx + 1);
|
|
753
|
+
if (origIdx !== -1) argv[origIdx] = wrapped;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// ── Inject defaults for open ─────────────────────────────────────────────
|
|
759
|
+
if (command === 'open') {
|
|
760
|
+
const openIdx = argv.indexOf('open');
|
|
761
|
+
const afterOpen = argv.slice(openIdx + 1);
|
|
762
|
+
|
|
763
|
+
// If a URL is provided with open, split into two steps to avoid goto timeout:
|
|
764
|
+
// 1) spawn open (no URL) to start the browser daemon
|
|
765
|
+
// 2) navigate via run-code with waitUntil:'domcontentloaded' + timeout:0
|
|
766
|
+
// so redirects (login flows, SPA routing) never time out
|
|
767
|
+
const urlArg = afterOpen.find(a => !a.startsWith('-') && /^https?:\/\//.test(a));
|
|
768
|
+
if (urlArg) {
|
|
769
|
+
const { spawnSync } = require('child_process');
|
|
770
|
+
const openOnlyArgs = injectOpenDefaults(afterOpen.filter(a => a !== urlArg));
|
|
771
|
+
const res = spawnSync(process.execPath, [cliPath, 'open', ...openOnlyArgs], {
|
|
772
|
+
stdio: 'inherit',
|
|
773
|
+
cwd: PW_CLI_DIR,
|
|
774
|
+
});
|
|
775
|
+
if (res.status !== 0) {
|
|
776
|
+
process.stderr.write('pw-cli: failed to open browser\n');
|
|
777
|
+
process.exit(res.status || 1);
|
|
778
|
+
}
|
|
779
|
+
// Replace open+url with a run-code navigation (lenient wait strategy)
|
|
780
|
+
const navCode = `async page => { await page.goto(${JSON.stringify(urlArg)}, { waitUntil: 'domcontentloaded', timeout: 0 }); return page.url(); }`;
|
|
781
|
+
argv = ['run-code', navCode];
|
|
782
|
+
} else {
|
|
783
|
+
const enhanced = injectOpenDefaults(afterOpen);
|
|
784
|
+
argv = [...argv.slice(0, openIdx + 1), ...enhanced];
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// ── XPath support: convert XPath ref args to run-code ───────────────────
|
|
789
|
+
argv = convertXPathCommand(argv);
|
|
790
|
+
|
|
791
|
+
// ── Auto-open if session not running and command needs a browser ──────────
|
|
792
|
+
if (command && !MGMT_COMMANDS.has(command)) {
|
|
793
|
+
const alive = await isSessionAlive(session);
|
|
794
|
+
if (!alive) {
|
|
795
|
+
// Spawn playwright-cli open as a separate process (it detaches the daemon)
|
|
796
|
+
const { spawnSync } = require('child_process');
|
|
797
|
+
const openArgs = [
|
|
798
|
+
cliPath,
|
|
799
|
+
...(session !== 'default' ? ['-s', session] : []),
|
|
800
|
+
'open',
|
|
801
|
+
'--headed', '--persistent', '--profile', DEFAULT_PROFILE,
|
|
802
|
+
];
|
|
803
|
+
process.chdir(PW_CLI_DIR);
|
|
804
|
+
const res = spawnSync(process.execPath, openArgs, { stdio: 'inherit' });
|
|
805
|
+
if (res.status !== 0) {
|
|
806
|
+
process.stderr.write('pw-cli: failed to auto-open browser\n');
|
|
807
|
+
process.exit(1);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// ── Rewrite --filename relative paths to absolute (based on original cwd) ─
|
|
813
|
+
// playwright-cli runs from PW_CLI_DIR, so relative --filename paths would
|
|
814
|
+
// land there. Rewrite them before chdir so files end up where the user expects.
|
|
815
|
+
argv = argv.map((arg, i) => {
|
|
816
|
+
if (arg.startsWith('--filename=')) {
|
|
817
|
+
const val = arg.slice('--filename='.length);
|
|
818
|
+
if (val && !path.isAbsolute(val)) return `--filename=${path.resolve(ORIGINAL_CWD, val)}`;
|
|
819
|
+
}
|
|
820
|
+
if (arg === '--filename' && argv[i + 1] && !argv[i + 1].startsWith('-') && !path.isAbsolute(argv[i + 1])) {
|
|
821
|
+
// value is the next element; will be rewritten on that element's turn — skip
|
|
822
|
+
}
|
|
823
|
+
// handle the value token after a bare --filename
|
|
824
|
+
if (i > 0 && argv[i - 1] === '--filename' && !arg.startsWith('-') && !path.isAbsolute(arg)) {
|
|
825
|
+
return path.resolve(ORIGINAL_CWD, arg);
|
|
826
|
+
}
|
|
827
|
+
return arg;
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
// ── Redirect snapshot/console output to original cwd ────────────────────
|
|
831
|
+
// playwright-cli writes to .playwright-cli/ relative to its cwd (PW_CLI_DIR).
|
|
832
|
+
// On exit, move any newly created files back to the directory pw-cli was
|
|
833
|
+
// called from, so snapshots appear alongside the user's project.
|
|
834
|
+
{
|
|
835
|
+
const snapshotSrc = path.join(PW_CLI_DIR, '.playwright-cli');
|
|
836
|
+
const snapshotDst = path.join(ORIGINAL_CWD, '.playwright-cli');
|
|
837
|
+
let before = new Set();
|
|
838
|
+
try { before = new Set(fs.readdirSync(snapshotSrc)); } catch {}
|
|
839
|
+
process.on('exit', () => {
|
|
840
|
+
try {
|
|
841
|
+
const files = fs.readdirSync(snapshotSrc);
|
|
842
|
+
if (files.some(f => !before.has(f))) {
|
|
843
|
+
fs.mkdirSync(snapshotDst, { recursive: true });
|
|
844
|
+
for (const f of files) {
|
|
845
|
+
if (before.has(f)) continue;
|
|
846
|
+
const src = path.join(snapshotSrc, f);
|
|
847
|
+
const dst = path.join(snapshotDst, f);
|
|
848
|
+
try {
|
|
849
|
+
fs.renameSync(src, dst);
|
|
850
|
+
} catch (e) {
|
|
851
|
+
if (e.code === 'EXDEV') { fs.copyFileSync(src, dst); fs.unlinkSync(src); }
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
} catch {}
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// ── Delegate to playwright-cli ────────────────────────────────────────────
|
|
860
|
+
// Set cwd to PW_CLI_DIR so playwright-cli always finds our workspace
|
|
861
|
+
process.chdir(PW_CLI_DIR);
|
|
862
|
+
// Override argv so playwright-cli's minimist sees our modified args
|
|
863
|
+
process.argv = ['node', 'pw-cli.js', ...argv];
|
|
864
|
+
require(cliPath);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
main().catch(err => {
|
|
868
|
+
process.stderr.write(`pw-cli: ${err.message}\n`);
|
|
869
|
+
process.exit(1);
|
|
870
|
+
});
|