pw-repl 0.2.2 → 0.3.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/AGENTS.md +10 -6
- package/README.md +31 -21
- package/bin/pw-repl.js +20 -6
- package/lib/background.js +3 -1
- package/lib/client.js +16 -6
- package/lib/commands.js +424 -73
- package/lib/help.js +36 -16
- package/lib/launch.js +115 -0
- package/lib/runner.js +2 -2
- package/lib/send.js +7 -4
- package/lib/server.js +2 -2
- package/lib/start.js +19 -6
- package/lib/state.js +5 -1
- package/lib/syntax.js +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +8 -4
package/AGENTS.md
CHANGED
|
@@ -11,13 +11,17 @@ If you hit a limitation or write a workaround, consider adding the capability to
|
|
|
11
11
|
- A command is a function in the `commands` object in `lib/commands.js`, plus an entry in `lib/help.js`
|
|
12
12
|
under one topic.
|
|
13
13
|
- `bin/pw-repl.js` is the command line; `lib/start.js` connects and runs the prompt; `lib/runner.js`
|
|
14
|
-
runs commands one at a time
|
|
15
|
-
are `send` and `where`; `lib/
|
|
14
|
+
runs commands one at a time (`quit` and `dialog` skip its queue); `lib/server.js` is the command
|
|
15
|
+
server; `lib/send.js` and `lib/client.js` are `send` and `where`; `lib/background.js` is
|
|
16
|
+
`serve --background`, `attach` and `stop`; `lib/launch.js` is `--launch`; `lib/syntax.js` is how a
|
|
17
|
+
command line's words are read, shared with `send`; `lib/state.js` holds the session state;
|
|
18
|
+
`lib/output.js` routes all output so the server can return it.
|
|
16
19
|
- Comment the *why* when it isn't obvious from the code.
|
|
17
|
-
- `npm test` runs the suite (about
|
|
18
|
-
REPL with its server. Nothing is mocked, and the shared browser is
|
|
19
|
-
|
|
20
|
-
none. Add a test with each new
|
|
20
|
+
- `npm test` runs the suite (about a minute): a private headless Chromium, a local test site
|
|
21
|
+
(`test/harness.js`), and the real REPL with its server. Nothing is mocked, and the shared browser is
|
|
22
|
+
never touched. It finds Chromium through `PW_TEST_CHROME`, or where `--launch` looks (Playwright's
|
|
23
|
+
browsers, then the `PATH`), and skips the browser tests if there is none. Add a test with each new
|
|
24
|
+
command or behaviour.
|
|
21
25
|
- For anything the tests can't reach, exercise the change in a running REPL. Use your own tmux session
|
|
22
26
|
and a new tab, not the user's.
|
|
23
27
|
- `skill/SKILL.md` is how agents learn to use the REPL: keep it in step with a change to how it is
|
package/README.md
CHANGED
|
@@ -21,6 +21,11 @@ chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.config/chrome-debug"
|
|
|
21
21
|
Use a separate `--user-data-dir`: recent Chrome versions do not open the debugging port on the default
|
|
22
22
|
profile. Check it is up with `curl http://localhost:9222/json/version`.
|
|
23
23
|
|
|
24
|
+
**No browser to hand, or one you would rather not share?** Skip this step: `pw-repl run --launch` (or
|
|
25
|
+
`serve --launch`) starts a private headless Chromium for the REPL and stops it with the REPL. It prints the
|
|
26
|
+
command it ran, so you can start one your own way instead. If there is no Chromium at all,
|
|
27
|
+
`npx playwright-core install chromium` downloads Playwright's.
|
|
28
|
+
|
|
24
29
|
**Headless** works the same way:
|
|
25
30
|
|
|
26
31
|
```bash
|
|
@@ -61,30 +66,35 @@ Commands act on the selected tab (`tab` lists the tabs, with `*` on the selected
|
|
|
61
66
|
|
|
62
67
|
## Common tasks
|
|
63
68
|
|
|
64
|
-
| I want to… | Commands
|
|
65
|
-
| ------------------------------------ |
|
|
66
|
-
| see where I am | `tab`, `info`
|
|
67
|
-
| see what is on the page | `snapshot`, `screenshot`
|
|
68
|
-
| do something on it | `click`, `fill`, `press`
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
| see
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
69
|
+
| I want to… | Commands |
|
|
70
|
+
| ------------------------------------ | ------------------------------------------------------------------------------------- |
|
|
71
|
+
| see where I am | `tab`, `info` |
|
|
72
|
+
| see what is on the page | `snapshot`, `screenshot` |
|
|
73
|
+
| do something on it | `click`, `fill`, `press` |
|
|
74
|
+
| wait for a page or element | `wait load`, `wait <selector>`, `wait <selector> --gone` |
|
|
75
|
+
| choose a file in a file input | `upload <selector> <file>` |
|
|
76
|
+
| see it as a phone, or in dark mode | `emulate mobile`, `emulate dark` (also `emulate locale`, `emulate timezone`) |
|
|
77
|
+
| see what the page requested | `requests`, then `body <#>` for what one got back |
|
|
78
|
+
| see console messages and errors | `console` |
|
|
79
|
+
| show an agent what I do | `watch on`, click around in the browser, then `watch` |
|
|
80
|
+
| see each step as I click | `watch on --live` |
|
|
81
|
+
| record requests and console together | `capture on`, then `capture off` |
|
|
82
|
+
| break the backend on purpose | `route <glob> <status> <json>` (fake a response), `route <glob> abort`, `network off` |
|
|
83
|
+
| change or slow an API response | `route <glob> patch <json>`, `route <glob> delay <secs>` |
|
|
84
|
+
| slow the whole network | `network slow` |
|
|
85
|
+
| clean up | `modes off` |
|
|
76
86
|
|
|
77
87
|
Everything else is in `help <topic>`; `help <command>` has usage and caveats.
|
|
78
88
|
|
|
79
89
|
### Modes
|
|
80
90
|
|
|
81
|
-
`watch`, `capture`, `route
|
|
82
|
-
`capture on|off`, `route ...|route off`, `network off|on
|
|
83
|
-
shows them: `(watch network:off routes:2) pw>`. `modes`
|
|
84
|
-
all off.
|
|
91
|
+
`watch`, `capture`, `route`, `network off` or `slow`, and `emulate` stay on until you turn them off:
|
|
92
|
+
`watch on|off`, `capture on|off`, `route ...|route off`, `network off|slow|on`, `emulate ...|emulate off`.
|
|
93
|
+
While any are on in the selected tab, the prompt shows them: `(watch network:off routes:2) pw>`. `modes`
|
|
94
|
+
lists them for every tab, and `modes off` turns them all off.
|
|
85
95
|
|
|
86
|
-
`tab`, `watch`, `capture`, `route`, `network` and `modes` on their own show their state and what
|
|
87
|
-
run next.
|
|
96
|
+
`tab`, `watch`, `capture`, `route`, `network`, `emulate` and `modes` on their own show their state and what
|
|
97
|
+
you can run next.
|
|
88
98
|
|
|
89
99
|
## Options
|
|
90
100
|
|
|
@@ -150,6 +160,6 @@ The same text is in `skill/SKILL.md`. Working on the REPL itself: see `AGENTS.md
|
|
|
150
160
|
npm test
|
|
151
161
|
```
|
|
152
162
|
|
|
153
|
-
Runs against a private headless Chromium it starts itself (
|
|
154
|
-
|
|
155
|
-
Chromium is found.
|
|
163
|
+
Runs against a private headless Chromium it starts itself (`PW_TEST_CHROME`, or the one `--launch` would
|
|
164
|
+
use: Playwright's, e.g. from `npx playwright-core install chromium`, then one on the `PATH`) and a local
|
|
165
|
+
test site; the browser tests are skipped when no Chromium is found.
|
package/bin/pw-repl.js
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
const { looksLikeEndpoint } = require('../lib/client');
|
|
5
5
|
|
|
6
6
|
const USAGE = `Usage:
|
|
7
|
-
pw-repl run [start-url]
|
|
7
|
+
pw-repl run [--launch [--headed]] [start-url] [-- <chromium flags>]
|
|
8
8
|
connect to the browser and open the prompt
|
|
9
9
|
|
|
10
|
-
pw-repl serve [--background] [endpoint] [start-url]
|
|
10
|
+
pw-repl serve [--background] [--launch [--headed]] [endpoint] [start-url] [-- <chromium flags>]
|
|
11
11
|
the same, plus a command server (socket, port, or 127.0.0.1:port; default /tmp/playwright-repl.sock);
|
|
12
12
|
--background runs it detached, with its output in a log next to the socket
|
|
13
13
|
|
|
@@ -37,6 +37,11 @@ send exit status: 0 ok, 1 command error, 2 completion not confirmed, 64 usage or
|
|
|
37
37
|
|
|
38
38
|
The browser must be running with --remote-debugging-port (default http://localhost:9222; set $PW_CDP_URL).
|
|
39
39
|
|
|
40
|
+
--launch is the quick start instead: it starts a Chromium of the REPL's own (headless unless --headed, in
|
|
41
|
+
a temporary profile), prints the command it ran, and stops it with the REPL. Flags after -- go to that
|
|
42
|
+
Chromium; PW_CHROME picks which one. To set a browser up your own way, start it yourself and use
|
|
43
|
+
PW_CDP_URL.
|
|
44
|
+
|
|
40
45
|
In a tmux session named playwright-repl, pw-repl send reaches pw-repl run without the server.`;
|
|
41
46
|
|
|
42
47
|
const REPL_HELP = `The REPL's own commands: help at the pw> prompt, or pw-repl send help here (no REPL needed).
|
|
@@ -74,15 +79,24 @@ function parseSendArgs(args, allowCommand) {
|
|
|
74
79
|
const { SELECTOR_FIRST } = require('../lib/syntax');
|
|
75
80
|
const requote = words.length > 1 && SELECTOR_FIRST.has(words[0]);
|
|
76
81
|
// An empty word (fill #name "") is the empty value.
|
|
77
|
-
|
|
82
|
+
// upload's files are read by the REPL, whose folder may not be this one.
|
|
83
|
+
const resolved = words[0] === 'upload' ? words.map((w, n) => (n > 1 ? require('path').resolve(w) : w)) : words;
|
|
84
|
+
const quoted = requote ? resolved.map(w => (w === '' || /[\s"']/.test(w) ? JSON.stringify(w) : w)) : resolved;
|
|
78
85
|
options.command = quoted.join(' ').trim();
|
|
79
86
|
return options;
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
function startOptions(args, serve) {
|
|
83
|
-
const options = { serve, endpoint: null, startUrl: null, background: false, pidFile: process.env.PW_REPL_PID_FILE || null };
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
const options = { serve, endpoint: null, startUrl: null, background: false, launch: false, headed: false, chromeArgs: [], pidFile: process.env.PW_REPL_PID_FILE || null };
|
|
91
|
+
// Whatever follows -- is for the Chromium that --launch starts.
|
|
92
|
+
const split = args.indexOf('--');
|
|
93
|
+
const rest = split === -1 ? [...args] : args.slice(0, split);
|
|
94
|
+
if (split !== -1) options.chromeArgs = args.slice(split + 1);
|
|
95
|
+
const flag = name => { const i = rest.indexOf(name); if (i === -1) return false; rest.splice(i, 1); return true; };
|
|
96
|
+
if (serve) options.background = flag('--background');
|
|
97
|
+
options.launch = flag('--launch');
|
|
98
|
+
options.headed = flag('--headed');
|
|
99
|
+
if ((options.headed || options.chromeArgs.length) && !options.launch) usage();
|
|
86
100
|
if (serve && rest[0] && looksLikeEndpoint(rest[0])) options.endpoint = rest.shift();
|
|
87
101
|
if (rest.length > 1 || (rest[0] && rest[0].startsWith('-'))) usage();
|
|
88
102
|
options.startUrl = rest[0] || null;
|
package/lib/background.js
CHANGED
|
@@ -64,7 +64,9 @@ async function start(options) {
|
|
|
64
64
|
if (await client.health(endpoint, 2000)) return fail(`a REPL is already serving on ${name}, in a terminal; pw-repl where says more`);
|
|
65
65
|
const log = fs.openSync(logFile, 'w', 0o600);
|
|
66
66
|
fs.fchmodSync(log, 0o600);
|
|
67
|
-
const args = [BIN, 'serve', ...(options.
|
|
67
|
+
const args = [BIN, 'serve', ...(options.launch ? ['--launch'] : []), ...(options.headed ? ['--headed'] : []),
|
|
68
|
+
...(options.endpoint ? [options.endpoint] : []), ...(options.startUrl ? [options.startUrl] : []),
|
|
69
|
+
...(options.chromeArgs.length ? ['--', ...options.chromeArgs] : [])];
|
|
68
70
|
const child = spawn(process.execPath, args, {
|
|
69
71
|
detached: true,
|
|
70
72
|
stdio: ['ignore', log, log],
|
package/lib/client.js
CHANGED
|
@@ -32,17 +32,27 @@ function target(endpoint) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// GET /health: does not go through the command queue or show in the pane.
|
|
35
|
-
|
|
35
|
+
// Resolves to what the REPL says about itself (e.g. its browser), or null.
|
|
36
|
+
function healthInfo(endpoint, timeoutMs) {
|
|
36
37
|
return new Promise(resolve => {
|
|
37
38
|
const req = http.get({ ...target(endpoint), path: '/health', timeout: timeoutMs }, res => {
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
let text = '';
|
|
40
|
+
res.setEncoding('utf8');
|
|
41
|
+
res.on('data', chunk => { text += chunk; });
|
|
42
|
+
res.on('end', () => {
|
|
43
|
+
if (res.statusCode !== 200) return resolve(null);
|
|
44
|
+
try { resolve(JSON.parse(text)); } catch { resolve({}); }
|
|
45
|
+
});
|
|
40
46
|
});
|
|
41
|
-
req.on('timeout', () => { req.destroy(); resolve(
|
|
42
|
-
req.on('error', () => resolve(
|
|
47
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
48
|
+
req.on('error', () => resolve(null));
|
|
43
49
|
});
|
|
44
50
|
}
|
|
45
51
|
|
|
52
|
+
async function health(endpoint, timeoutMs) {
|
|
53
|
+
return (await healthInfo(endpoint, timeoutMs)) !== null;
|
|
54
|
+
}
|
|
55
|
+
|
|
46
56
|
// One command. Resolves to { result }, { timeout: true }, { dropped: reason }
|
|
47
57
|
// (the connection closed after the command was sent, so it may have run) or
|
|
48
58
|
// { unreachable: reason } (it was never sent).
|
|
@@ -75,4 +85,4 @@ function request(endpoint, command, timeoutMs) {
|
|
|
75
85
|
});
|
|
76
86
|
}
|
|
77
87
|
|
|
78
|
-
module.exports = { DEFAULT_SOCKET, parseEndpoint, looksLikeEndpoint, describe, health, request };
|
|
88
|
+
module.exports = { DEFAULT_SOCKET, parseEndpoint, looksLikeEndpoint, describe, health, healthInfo, request };
|