pw-repl 0.1.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/AGENTS.md +24 -0
- package/LICENSE.md +21 -0
- package/README.md +132 -0
- package/bin/pw-repl.js +128 -0
- package/lib/client.js +78 -0
- package/lib/commands.js +1370 -0
- package/lib/help.js +210 -0
- package/lib/output.js +70 -0
- package/lib/runner.js +163 -0
- package/lib/send.js +127 -0
- package/lib/server.js +91 -0
- package/lib/start.js +117 -0
- package/lib/state.js +61 -0
- package/package.json +41 -0
- package/skill/SKILL.md +102 -0
package/lib/server.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Opt-in command server (pw-repl serve): POST /run {"command": "..."} runs one
|
|
2
|
+
// command through the same queue as the prompt and returns its output.
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const net = require('net');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const out = require('./output');
|
|
7
|
+
const runner = require('./runner');
|
|
8
|
+
const { onShutdown, onBeforeExit } = require('./state');
|
|
9
|
+
const { parseEndpoint, describe } = require('./client');
|
|
10
|
+
|
|
11
|
+
const MAX_BODY = 1024 * 1024;
|
|
12
|
+
const FLUSH_TIMEOUT = 1000;
|
|
13
|
+
|
|
14
|
+
// Answers being written, so the REPL can let them reach their senders before it exits.
|
|
15
|
+
const sending = new Set();
|
|
16
|
+
|
|
17
|
+
function send(res, code, result) {
|
|
18
|
+
res.writeHead(code, { 'Content-Type': 'application/json' });
|
|
19
|
+
const done = new Promise(resolve => { res.once('finish', resolve); res.once('close', resolve); });
|
|
20
|
+
sending.add(done);
|
|
21
|
+
done.then(() => sending.delete(done));
|
|
22
|
+
res.end(`${JSON.stringify(result)}\n`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
onBeforeExit(() => Promise.race([Promise.all(sending), new Promise(resolve => setTimeout(resolve, FLUSH_TIMEOUT))]));
|
|
26
|
+
|
|
27
|
+
function handle(req, res) {
|
|
28
|
+
// A web page can reach a loopback port; it always sends Origin on a
|
|
29
|
+
// cross-origin POST, and cannot send a JSON content type without a
|
|
30
|
+
// preflight this server never answers.
|
|
31
|
+
if (req.headers.origin) return send(res, 403, { status: 'error', output: 'Requests from web pages are refused' });
|
|
32
|
+
if (req.method === 'GET' && req.url === '/health') return send(res, 200, { status: 'ok' });
|
|
33
|
+
if (req.method !== 'POST' || req.url !== '/run') return send(res, 404, { status: 'error', output: 'Use POST /run or GET /health' });
|
|
34
|
+
if (!/^application\/json\b/.test(req.headers['content-type'] || '')) return send(res, 415, { status: 'error', output: 'Content-Type must be application/json' });
|
|
35
|
+
let body = '';
|
|
36
|
+
req.setEncoding('utf8');
|
|
37
|
+
req.on('data', chunk => {
|
|
38
|
+
body += chunk;
|
|
39
|
+
if (body.length > MAX_BODY) { send(res, 413, { status: 'error', output: 'Request too large' }); req.destroy(); }
|
|
40
|
+
});
|
|
41
|
+
req.on('end', async () => {
|
|
42
|
+
let command;
|
|
43
|
+
try { ({ command } = JSON.parse(body)); } catch {}
|
|
44
|
+
if (typeof command !== 'string' || !command.trim() || /[\r\n]/.test(command)) {
|
|
45
|
+
return send(res, 400, { status: 'error', output: 'Body must be {"command": "<one line>"}' });
|
|
46
|
+
}
|
|
47
|
+
const result = await runner.submit(command.trim());
|
|
48
|
+
if (!result.uncertain) return send(res, 200, { status: result.status, output: result.output });
|
|
49
|
+
// unconfirmed: the command may or may not have done what it was sent to do.
|
|
50
|
+
if (result.interrupted) return send(res, 200, { status: result.status, output: result.output, unconfirmed: true });
|
|
51
|
+
send(res, 200, { status: result.status, output: `${result.output}\nBrowser command outcome is unknown; the REPL is disconnecting.`, unconfirmed: true });
|
|
52
|
+
runner.giveUp();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// A socket file left by a REPL that died is removed; one still answering is not.
|
|
57
|
+
async function clearStaleSocket(socketPath) {
|
|
58
|
+
let stat;
|
|
59
|
+
try { stat = fs.statSync(socketPath); } catch { return; }
|
|
60
|
+
if (!stat.isSocket()) throw new Error(`${socketPath} exists and is not a socket`);
|
|
61
|
+
const live = await new Promise(resolve => {
|
|
62
|
+
const probe = net.connect(socketPath);
|
|
63
|
+
probe.on('connect', () => { probe.destroy(); resolve(true); });
|
|
64
|
+
probe.on('error', () => resolve(false));
|
|
65
|
+
});
|
|
66
|
+
if (live) throw new Error(`Another REPL is already serving on ${socketPath}`);
|
|
67
|
+
fs.unlinkSync(socketPath);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function serve(endpointArg) {
|
|
71
|
+
const endpoint = parseEndpoint(endpointArg);
|
|
72
|
+
if (endpoint.socket) await clearStaleSocket(endpoint.socket);
|
|
73
|
+
const server = http.createServer(handle);
|
|
74
|
+
// Owner-only from the moment the socket file exists.
|
|
75
|
+
const previousUmask = endpoint.socket ? process.umask(0o177) : null;
|
|
76
|
+
try {
|
|
77
|
+
await new Promise((resolve, reject) => {
|
|
78
|
+
server.once('error', reject);
|
|
79
|
+
if (endpoint.socket) server.listen(endpoint.socket, resolve);
|
|
80
|
+
else server.listen(endpoint.port, endpoint.host, resolve);
|
|
81
|
+
});
|
|
82
|
+
} finally {
|
|
83
|
+
if (previousUmask !== null) process.umask(previousUmask);
|
|
84
|
+
}
|
|
85
|
+
const removeSocket = () => { if (endpoint.socket) try { fs.unlinkSync(endpoint.socket); } catch {} };
|
|
86
|
+
onShutdown(() => { server.close(); removeSocket(); });
|
|
87
|
+
process.on('exit', removeSocket);
|
|
88
|
+
out.log(`Serving commands on ${describe(endpoint)}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { serve };
|
package/lib/start.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Connects to the browser and runs the prompt (and the server with serve).
|
|
2
|
+
const { chromium } = require('playwright-core');
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const { state, withTimeout, shutdown, beforeExit } = require('./state');
|
|
5
|
+
const out = require('./output');
|
|
6
|
+
const { listTabs, watchPage, complete } = require('./commands');
|
|
7
|
+
const runner = require('./runner');
|
|
8
|
+
|
|
9
|
+
const CDP_URL = process.env.PW_CDP_URL || 'http://localhost:9222';
|
|
10
|
+
|
|
11
|
+
async function start(options) {
|
|
12
|
+
const START_URL = options.startUrl || process.env.PW_START_URL || null;
|
|
13
|
+
out.log(`Connecting to ${CDP_URL}...`);
|
|
14
|
+
state.browser = await chromium.connectOverCDP(CDP_URL);
|
|
15
|
+
if (state.stopping) {
|
|
16
|
+
try { await withTimeout(state.browser.close(), 'Chromium shutdown'); }
|
|
17
|
+
catch (error) { process.exitCode = 1; out.error(`Could not confirm Chromium shutdown: ${error.message}`); }
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
state.browser.on('disconnected', () => {
|
|
21
|
+
if (state.stopping) return;
|
|
22
|
+
state.connectionLost = true;
|
|
23
|
+
state.stopping = true;
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
out.error('Chromium connection lost; queued commands will not run.');
|
|
26
|
+
void shutdown().then(() => process.exit(1));
|
|
27
|
+
});
|
|
28
|
+
out.log('Connected to Chromium via CDP');
|
|
29
|
+
|
|
30
|
+
const contexts = state.browser.contexts();
|
|
31
|
+
const pages = contexts.flatMap(c => c.pages());
|
|
32
|
+
|
|
33
|
+
state.page = pages[0];
|
|
34
|
+
state.tabListing = pages.slice();
|
|
35
|
+
|
|
36
|
+
// Hooks first, so the start URL's own requests, logs and dialogs are recorded.
|
|
37
|
+
for (const ctx of contexts) {
|
|
38
|
+
ctx.on('page', watchPage);
|
|
39
|
+
ctx.pages().forEach(watchPage);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (START_URL) {
|
|
43
|
+
if (!state.page) {
|
|
44
|
+
state.page = await contexts[0].newPage();
|
|
45
|
+
}
|
|
46
|
+
await state.page.goto(START_URL, { waitUntil: 'networkidle', timeout: 15000 });
|
|
47
|
+
out.log(`Navigated to: ${state.page.url()}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!state.page) {
|
|
51
|
+
state.page = await (contexts[0] || await state.browser.newContext()).newPage();
|
|
52
|
+
out.log('Created new page');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
out.log('');
|
|
56
|
+
await listTabs();
|
|
57
|
+
out.log('');
|
|
58
|
+
if (options.serve) await require('./server').serve(options.endpoint);
|
|
59
|
+
out.log("Run 'help' to see the commands. They act on the selected tab (*).");
|
|
60
|
+
out.log('');
|
|
61
|
+
|
|
62
|
+
// The prompt shows whether the command server is on.
|
|
63
|
+
state.promptBase = options.serve ? 'pw[serve]> ' : 'pw> ';
|
|
64
|
+
const rl = readline.createInterface({
|
|
65
|
+
input: process.stdin,
|
|
66
|
+
output: process.stdout,
|
|
67
|
+
prompt: state.promptBase,
|
|
68
|
+
completer: complete,
|
|
69
|
+
historySize: 1000,
|
|
70
|
+
removeHistoryDuplicates: true,
|
|
71
|
+
});
|
|
72
|
+
state.rl = rl;
|
|
73
|
+
out.onIdlePrint({
|
|
74
|
+
idle: () => !runner.busy() && !state.stopping,
|
|
75
|
+
print: text => {
|
|
76
|
+
if (process.stdout.isTTY) { readline.clearLine(process.stdout, 0); readline.cursorTo(process.stdout, 0); }
|
|
77
|
+
console.log(text);
|
|
78
|
+
rl.prompt(true);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
rl.prompt();
|
|
83
|
+
rl.on('line', line => {
|
|
84
|
+
// Up-arrow should bring back the command, not the caller's completion id.
|
|
85
|
+
const bare = line.trim().replace(/^@[A-Za-z0-9][A-Za-z0-9._-]*\s+/, '');
|
|
86
|
+
if (bare !== line.trim() && rl.history[0] === line.trim()) {
|
|
87
|
+
rl.history.shift();
|
|
88
|
+
if (bare && rl.history[0] !== bare) rl.history.unshift(bare);
|
|
89
|
+
}
|
|
90
|
+
runner.enqueue(line);
|
|
91
|
+
});
|
|
92
|
+
rl.on('close', () => {
|
|
93
|
+
if (state.stopping) return;
|
|
94
|
+
endPromptLine();
|
|
95
|
+
runner.drained().then(() => shutdown(), () => shutdown())
|
|
96
|
+
.then(() => process.exit(process.exitCode || 0));
|
|
97
|
+
});
|
|
98
|
+
rl.on('SIGINT', stop);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Ctrl-C and Ctrl-D leave the cursor after the prompt; the shell's prompt
|
|
102
|
+
// belongs on a line of its own.
|
|
103
|
+
function endPromptLine() {
|
|
104
|
+
if (process.stdout.isTTY) process.stdout.write('\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Like quit: a server command still running is answered before the REPL exits.
|
|
108
|
+
function stop() {
|
|
109
|
+
endPromptLine();
|
|
110
|
+
runner.answerInterrupted();
|
|
111
|
+
shutdown().then(beforeExit).then(() => process.exit(process.exitCode || 0));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// SIGHUP too: closing the terminal must still remove the server's socket.
|
|
115
|
+
for (const signal of ['SIGTERM', 'SIGHUP']) process.on(signal, stop);
|
|
116
|
+
|
|
117
|
+
module.exports = { start: options => start(options).catch(e => { out.error(e.message); shutdown().finally(() => process.exit(1)); }) };
|
package/lib/state.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Session state shared by the entry point, the runner, and the commands, plus
|
|
2
|
+
// the shutdown that ends the session.
|
|
3
|
+
const out = require('./output');
|
|
4
|
+
|
|
5
|
+
const SHUTDOWN_TIMEOUT = 5000;
|
|
6
|
+
|
|
7
|
+
const state = {
|
|
8
|
+
browser: null,
|
|
9
|
+
page: null,
|
|
10
|
+
previousPage: null,
|
|
11
|
+
tabListing: [],
|
|
12
|
+
rl: null,
|
|
13
|
+
promptBase: 'pw> ',
|
|
14
|
+
stopping: false,
|
|
15
|
+
connectionLost: false,
|
|
16
|
+
shutdownFailed: false,
|
|
17
|
+
previousCommandAt: 0,
|
|
18
|
+
currentCommandAt: 0,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const cleanups = [];
|
|
22
|
+
const exitWaits = [];
|
|
23
|
+
let shutdownPromise = null;
|
|
24
|
+
|
|
25
|
+
function withTimeout(promise, label, duration = SHUTDOWN_TIMEOUT) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const timer = setTimeout(() => reject(new Error(`${label} timed out`)), duration);
|
|
28
|
+
promise.then(
|
|
29
|
+
value => { clearTimeout(timer); resolve(value); },
|
|
30
|
+
error => { clearTimeout(timer); reject(error); },
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function onShutdown(fn) {
|
|
36
|
+
cleanups.push(fn);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Things to finish before the process exits, e.g. answers still being sent.
|
|
40
|
+
function onBeforeExit(fn) {
|
|
41
|
+
exitWaits.push(fn);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function beforeExit() {
|
|
45
|
+
await Promise.all(exitWaits.map(fn => fn()));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function shutdown() {
|
|
49
|
+
if (shutdownPromise) return shutdownPromise;
|
|
50
|
+
state.stopping = true;
|
|
51
|
+
shutdownPromise = (async () => {
|
|
52
|
+
cleanups.forEach(fn => fn());
|
|
53
|
+
if (state.browser) {
|
|
54
|
+
try { await withTimeout(state.browser.close(), 'Chromium shutdown'); }
|
|
55
|
+
catch (error) { state.shutdownFailed = true; process.exitCode = 1; out.error(`Could not confirm Chromium shutdown: ${error.message}`); }
|
|
56
|
+
}
|
|
57
|
+
})();
|
|
58
|
+
return shutdownPromise;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { state, withTimeout, onShutdown, onBeforeExit, beforeExit, shutdown };
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pw-repl",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Text REPL for driving an existing Chromium through Playwright over CDP",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"playwright",
|
|
7
|
+
"repl",
|
|
8
|
+
"cdp",
|
|
9
|
+
"chromium",
|
|
10
|
+
"browser",
|
|
11
|
+
"devtools",
|
|
12
|
+
"agents"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/arcanemachine/playwright-repl.git"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"pw-repl": "bin/pw-repl.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"bin/",
|
|
24
|
+
"lib/",
|
|
25
|
+
"AGENTS.md",
|
|
26
|
+
"skill/",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE.md"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"start": "node bin/pw-repl.js run",
|
|
32
|
+
"test": "node --test 'test/*.test.js'",
|
|
33
|
+
"prepublishOnly": "npm test"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"playwright-core": "^1.63.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pw-repl
|
|
3
|
+
description: Inspect and drive a Chromium browser, possibly one a person is using, through the pw-repl (playwright-repl) REPL - tabs, page snapshots, clicks and typing, requests and their bodies, console messages, fake responses, cutting the network, and recording what the person does. Use when asked to look at, debug or test something in a browser that runs with remote debugging.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# pw-repl
|
|
7
|
+
|
|
8
|
+
pw-repl (playwright-repl) is a REPL that drives a running Chromium over CDP. The browser may be a
|
|
9
|
+
shared session: a person can have their own tabs open in it and be using it while you work.
|
|
10
|
+
|
|
11
|
+
`pw-repl` below is the command the `pw-repl` npm package installs. Without a global install,
|
|
12
|
+
`npx pw-repl` works the same; from a clone, `<clone>/bin/pw-repl.js`.
|
|
13
|
+
|
|
14
|
+
## Start it
|
|
15
|
+
|
|
16
|
+
The REPL runs in the tmux session `playwright-repl`. Check it first:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
tmux capture-pane -t playwright-repl -p -S -5
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
If the pane is at a shell prompt, start the REPL there. If there is no session, ask the user before
|
|
23
|
+
creating one.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
tmux send-keys -t playwright-repl -l 'pw-repl run'
|
|
27
|
+
tmux send-keys -t playwright-repl Enter
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`pw-repl serve` is the same plus a command server on `/tmp/playwright-repl.sock` (owner-only); its
|
|
31
|
+
prompt is `pw[serve]>` instead of `pw>`. It is opt-in: use it when the user asks for it or agrees.
|
|
32
|
+
Prefer the socket: `pw-repl serve <port>` (TCP on 127.0.0.1) has no authentication, so any local
|
|
33
|
+
process could drive the browser; use it only for a client that cannot reach the socket. Both take an
|
|
34
|
+
optional start URL, which navigates tab [0]. Stop either at the pane (`quit`, or Ctrl-C); `send quit` is
|
|
35
|
+
refused so an agent cannot close a REPL the user is using.
|
|
36
|
+
|
|
37
|
+
## Send commands
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pw-repl send tab
|
|
41
|
+
pw-repl send -t 90 'screenshot -d 60' # wait longer than the 20s default
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Always send commands with `pw-repl send`; don't type into the pane yourself. Unquoted words are one
|
|
45
|
+
command. It works however the REPL was started:
|
|
46
|
+
|
|
47
|
+
- `run`: `send` types the command into the tmux pane and reads the result back off the screen. It
|
|
48
|
+
types only when the pane's last line is a bare prompt, so nothing lands in a shell or in the middle
|
|
49
|
+
of what the user is typing; while a command is running, the user is typing, or the REPL is exiting,
|
|
50
|
+
it refuses (exit 64). Wait and retry.
|
|
51
|
+
- `serve`: `send` sends it over the socket and gets the output back as JSON. Same commands, more
|
|
52
|
+
reliable results: nothing is scraped, long output isn't cut off by scrollback, and a command can
|
|
53
|
+
never land in a shell. If you'll send many commands or read long output, ask the user whether to
|
|
54
|
+
restart the REPL with `pw-repl serve`.
|
|
55
|
+
|
|
56
|
+
`pw-repl where` says which one a command would reach (the server, or the tmux pane running the
|
|
57
|
+
REPL), or why neither is reachable, without running anything.
|
|
58
|
+
|
|
59
|
+
Either way, every command you run and its output show in the pane (server commands as `[server]` lines),
|
|
60
|
+
so the user sees what you do. The pane shows REPL commands only, not what the user clicked in the
|
|
61
|
+
browser (unless `watch on --live` is on); for that, look at the browser itself: `tab` and `info` for
|
|
62
|
+
where they are, `requests` for the requests their clicks made (`body <#>` for what one returned),
|
|
63
|
+
`console` for console messages and page errors. When the user wants to show you what they do, `watch on`
|
|
64
|
+
on their tab records each step with the requests it caused (`watch on --changes` adds what each step
|
|
65
|
+
changed on the page); `watch` reads it back, and `watch new` only what it has not shown yet. Watching
|
|
66
|
+
and reading are fine on the user's tabs; the rule below is about acting on them.
|
|
67
|
+
|
|
68
|
+
Exit status: `0` ok, `1` the command failed, `2` completion not confirmed (outcome unknown: do not
|
|
69
|
+
blindly retry a change), `64` usage or the REPL is not reachable. `pw-repl --help` has the options.
|
|
70
|
+
|
|
71
|
+
## Learn the commands
|
|
72
|
+
|
|
73
|
+
Run `pw-repl send help`. It lists six topics; `help <topic>` lists their commands, `help <command>`
|
|
74
|
+
gives usage and caveats, and `help --all` prints everything at once. It needs no running REPL. The help is
|
|
75
|
+
the command reference; this file does not repeat it.
|
|
76
|
+
|
|
77
|
+
## Shared-browser rules
|
|
78
|
+
|
|
79
|
+
- Act only on tabs you opened (`tab new`), unless the user asks you to act on theirs (e.g. a `route` in
|
|
80
|
+
their tab while they test); then say what you are doing and undo it the moment you are done. The REPL
|
|
81
|
+
selects tab [0] at startup, and that tab may be the user's: run `tab` before acting. Closing your
|
|
82
|
+
tab goes back only to a tab you opened; otherwise no tab is selected. Tab numbers change when tabs
|
|
83
|
+
open or close; `tab <url-part>` and `tab close <url-part>` pick a tab by its URL and refuse if it is
|
|
84
|
+
ambiguous.
|
|
85
|
+
- Dialogs are never answered automatically. The person at the browser handles them.
|
|
86
|
+
- Before leaving: turn off the modes you turned on (`modes` lists what is on in every tab; the prompt
|
|
87
|
+
shows the selected tab's, e.g. `(watch routes:1) pw>`), and close the tabs you opened. `modes off`
|
|
88
|
+
turns off everything, including modes the user turned on, so use it only when they are all yours.
|
|
89
|
+
- The tmux session may be attached by the user. Never kill it.
|
|
90
|
+
|
|
91
|
+
## Environment
|
|
92
|
+
|
|
93
|
+
- Chromium must be running with `--remote-debugging-port=9222`.
|
|
94
|
+
- `PW_CDP_URL` — CDP endpoint (default `http://localhost:9222`).
|
|
95
|
+
- `PW_SCREENSHOT_DIR` — where screenshots go (default `/tmp`). They are all named `screenshot-*.png`,
|
|
96
|
+
so `rm /tmp/screenshot-*.png` cleans up.
|
|
97
|
+
- `PW_ENDPOINT` / `PW_TMUX_SESSION` — defaults for `send -e` / `-s`. `PW_SOCKET` — the socket `send`
|
|
98
|
+
looks for when neither is given (default `/tmp/playwright-repl.sock`).
|
|
99
|
+
|
|
100
|
+
## Custom rules
|
|
101
|
+
|
|
102
|
+
No custom rules have been added yet.
|