pw-repl 0.12.6 → 0.12.8
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 +4 -2
- package/README.md +76 -74
- package/bin/pw-repl.js +152 -65
- package/lib/cli-names.js +3 -3
- package/lib/elements.js +115 -26
- package/lib/emulation.js +5 -3
- package/lib/help.js +118 -47
- package/lib/inspect.js +28 -6
- package/lib/launch.js +3 -1
- package/lib/record.js +1 -1
- package/lib/runner.js +11 -0
- package/lib/send.js +16 -4
- package/lib/start.js +9 -3
- package/lib/syntax.js +11 -5
- package/package.json +1 -1
- package/skill/SKILL.md +30 -42
package/AGENTS.md
CHANGED
|
@@ -53,7 +53,9 @@ If you hit a limitation or write a workaround, consider adding the capability to
|
|
|
53
53
|
shown by `help <command> --all` and `help --all`. An error about something in `more` ends by naming it
|
|
54
54
|
(`help record --all`). Paragraphs are plain: `wrap()` lays each out at 104 columns, and keeps as written
|
|
55
55
|
a paragraph with a bullet, a table or an indented example. `test/fixtures` holds the rendered help;
|
|
56
|
-
after an intended change to it, save it again (the command is in `test/units.test.js`).
|
|
56
|
+
after an intended change to it, save it again (the command is in `test/units.test.js`). The common
|
|
57
|
+
tasks and modes on help's front page (`TASKS` and `MODES`) are the README's too: change both, as a
|
|
58
|
+
unit test compares them.
|
|
57
59
|
- Time each full run, and put the result in the body of the commit it tests (`npm test: 151 tests, 79s`).
|
|
58
60
|
Compare with the last commit that has one (`git log --grep='npm test:'`): a run that grew by more than
|
|
59
61
|
a few seconds means a slow test crept in, usually one waiting out a real timeout (5s for a click).
|
|
@@ -104,7 +106,7 @@ Commits follow [Conventional Commits](https://www.conventionalcommits.org/): `fe
|
|
|
104
106
|
|
|
105
107
|
1. `npm test` passes and the working tree is clean.
|
|
106
108
|
2. `npm outdated` and `npm audit` show nothing that needs doing first. The lockfile holds playwright-core
|
|
107
|
-
at 1.63: on 1.64, a click on a link to a 204 waits out its timeout instead of returning (the test
|
|
109
|
+
at 1.63: on 1.64 (and 1.64.1), a click on a link to a 204 waits out its timeout instead of returning (the test
|
|
108
110
|
"waits for the page to load, counting a navigation that just began" fails). Update it once a version
|
|
109
111
|
passes `npm test`.
|
|
110
112
|
3. `npm version <patch|minor|major> -m "chore: release %s"` sets the version in `package.json` and
|
package/README.md
CHANGED
|
@@ -24,6 +24,39 @@ pw> reload
|
|
|
24
24
|
pw> console error # and see how the page copes
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
You need Node.js 20 or newer. pw-repl can use a Chromium-based browser you already have (Chrome,
|
|
30
|
+
Chromium, ...), or Playwright's own: `npx playwright-core install chromium` downloads it.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx pw-repl@latest run --launch https://example.com
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`--launch` starts a private headless Chromium for the REPL, in a 1280x800 window, and stops it when
|
|
37
|
+
the REPL quits. Add `--headed` to see that browser and click in it yourself. The page opens, and a
|
|
38
|
+
`pw>` prompt waits:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
pw> help # common tasks and topics; help <topic>, help <command>, help --all
|
|
42
|
+
pw> snapshot
|
|
43
|
+
pw> click e3
|
|
44
|
+
pw> screenshot
|
|
45
|
+
pw> quit
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Commands act on the selected tab. `tab` lists the tabs, with `*` on the selected one.
|
|
49
|
+
|
|
50
|
+
### Install
|
|
51
|
+
|
|
52
|
+
| How | Setup | Then run |
|
|
53
|
+
| ------------ | -------------------------------------------- | ------------------------------ |
|
|
54
|
+
| Without | nothing | `npx pw-repl@latest <command>` |
|
|
55
|
+
| Global | `npm install -g pw-repl` | `pw-repl <command>` |
|
|
56
|
+
| From a clone | `npm install` (and `npm link` for `pw-repl`) | `bin/pw-repl.js <command>` |
|
|
57
|
+
|
|
58
|
+
With `npx`, use `npx pw-repl@latest` the same way every time, so every command runs the same version.
|
|
59
|
+
|
|
27
60
|
## Why use it
|
|
28
61
|
|
|
29
62
|
- **Your browser, as it is.** It connects to a running Chromium, with your tabs, logins and state, and
|
|
@@ -42,14 +75,15 @@ pw> console error # and see how the page copes
|
|
|
42
75
|
|
|
43
76
|
- **Read the page:** accessibility snapshots with clickable refs, text, HTML, attributes, links, form
|
|
44
77
|
controls, event listeners, screenshots (viewport, full page or one element).
|
|
45
|
-
- **Act on it:** click, fill, type, press keys, select, check,
|
|
46
|
-
wheel, answer dialogs.
|
|
78
|
+
- **Act on it:** click, double-click, fill, type, press keys, select, check, drag and drop, upload files,
|
|
79
|
+
click or drag at a point, scroll the wheel, answer dialogs.
|
|
47
80
|
- **Watch the network:** every request and response body, console messages and page errors, all recorded
|
|
48
81
|
as they happen.
|
|
49
82
|
- **Change the network:** fake a response (`route`), patch its JSON, delay it, fail it, or cut or slow the
|
|
50
83
|
whole tab's network.
|
|
51
84
|
- **Emulate:** a phone, dark mode, a locale, a timezone, a viewport size.
|
|
52
|
-
- **Run code:** `eval` any JavaScript in the page, or send a raw CDP command with
|
|
85
|
+
- **Run code:** `eval` any JavaScript in the page, a line or a file of it, or send a raw CDP command with
|
|
86
|
+
`cdp`.
|
|
53
87
|
- **Wait:** for an element, some text, a response or a page load.
|
|
54
88
|
- **Record:** what someone does in a tab (`watch`), a timeline of requests and console (`capture`), or a
|
|
55
89
|
video of the tab (`record`), with a virtual pointer (`cursor`), captions (`toast`) and highlights
|
|
@@ -59,42 +93,10 @@ pw> console error # and see how the page copes
|
|
|
59
93
|
- **Share:** many clients on one REPL, each with its own tab; a command server on a Unix socket or a local
|
|
60
94
|
port, with an HTTP+JSON protocol.
|
|
61
95
|
|
|
62
|
-
## Quick start
|
|
63
|
-
|
|
64
|
-
You need Node.js 20 or newer. pw-repl can use a Chromium-based browser you already have (Chrome,
|
|
65
|
-
Chromium, ...), or Playwright's own: `npx playwright-core install chromium` downloads it.
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
npx pw-repl@latest run --launch https://example.com
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
`--launch` starts a private headless Chromium for the REPL and stops it when the REPL quits. Add
|
|
72
|
-
`--headed` to see that browser and click in it yourself. The page opens in a new tab, and a `pw>`
|
|
73
|
-
prompt waits:
|
|
74
|
-
|
|
75
|
-
```text
|
|
76
|
-
pw> help # common tasks and topics; help <topic>, help <command>, help --all
|
|
77
|
-
pw> snapshot
|
|
78
|
-
pw> click e3
|
|
79
|
-
pw> screenshot
|
|
80
|
-
pw> quit
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Commands act on the selected tab. `tab` lists the tabs, with `*` on the selected one.
|
|
84
|
-
|
|
85
|
-
### Install
|
|
86
|
-
|
|
87
|
-
| How | Setup | Then run |
|
|
88
|
-
| ------------ | -------------------------------------------- | ------------------------------ |
|
|
89
|
-
| Without | nothing | `npx pw-repl@latest <command>` |
|
|
90
|
-
| Global | `npm install -g pw-repl` | `pw-repl <command>` |
|
|
91
|
-
| From a clone | `npm install` (and `npm link` for `pw-repl`) | `bin/pw-repl.js <command>` |
|
|
92
|
-
|
|
93
|
-
With `npx`, use `npx pw-repl@latest` the same way every time, so every command runs the same version.
|
|
94
|
-
|
|
95
96
|
## Use your own browser
|
|
96
97
|
|
|
97
|
-
Start Chrome with remote debugging, then run the REPL
|
|
98
|
+
Start Chrome with remote debugging (on Linux it may be `google-chrome` or `chromium`), then run the REPL
|
|
99
|
+
without `--launch`:
|
|
98
100
|
|
|
99
101
|
```bash
|
|
100
102
|
chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.config/chrome-debug"
|
|
@@ -126,47 +128,49 @@ example the Docker bridge gateway, `ip route | awk '/default/ {print $3}'`).
|
|
|
126
128
|
it. `pw-repl where` says what is running and how `send` will reach it.
|
|
127
129
|
- For `send` to reach `run`, start it in tmux: `tmux new -s pw-repl`, then `pw-repl run` there.
|
|
128
130
|
- A URL after the command opens in a new tab: `pw-repl serve http://localhost:3000`.
|
|
129
|
-
- `--launch` works with each of them. Flags after `--` go to that Chromium
|
|
131
|
+
- `--launch` works with each of them. Flags after `--` go to that Chromium (flags only; a start URL goes
|
|
132
|
+
before `--`).
|
|
130
133
|
- A REPL of your own goes on a socket of its own: `-e /tmp/mine.sock` on every command, `serve`
|
|
131
134
|
included, or set `PW_SOCKET`. `serve <port>` listens on TCP, on 127.0.0.1 only.
|
|
132
135
|
|
|
133
136
|
## Common tasks
|
|
134
137
|
|
|
135
|
-
| I want to… | Commands
|
|
136
|
-
| ------------------------------------- |
|
|
137
|
-
| see where I am | `tab
|
|
138
|
-
| see what is on the page | `snapshot`, `screenshot`
|
|
139
|
-
| do something on it | `click`, `fill`, `type`, `press`
|
|
140
|
-
| click what has no selector (a canvas) | `mouseclick <x> <y>`
|
|
141
|
-
| run some JavaScript in the page | `eval document.title`
|
|
142
|
-
| wait for a page or element | `wait load`, `wait <selector
|
|
143
|
-
| choose a file in a file input | `upload <selector> <file>`
|
|
144
|
-
| see it as a phone, or in dark mode | `emulate mobile`, `emulate dark`
|
|
145
|
-
| see what the page requested | `requests`, then `body <#>` for what one got back
|
|
146
|
-
| see console messages and errors | `console`
|
|
147
|
-
| show an agent what I do | `watch on`, click around
|
|
148
|
-
| see each step as I click | `watch on --live`
|
|
149
|
-
|
|
|
150
|
-
|
|
|
151
|
-
|
|
|
152
|
-
|
|
|
153
|
-
|
|
|
154
|
-
|
|
|
155
|
-
|
|
|
156
|
-
|
|
|
157
|
-
|
|
|
158
|
-
|
|
|
159
|
-
|
|
|
138
|
+
| I want to… | Commands |
|
|
139
|
+
| ------------------------------------- | ------------------------------------------------------------- |
|
|
140
|
+
| see where I am | `tab`, `info` |
|
|
141
|
+
| see what is on the page | `snapshot`, `screenshot` |
|
|
142
|
+
| do something on it | `click`, `fill`, `type`, `press`, `drag` |
|
|
143
|
+
| click what has no selector (a canvas) | `mouseclick <x> <y>`, `mousedblclick <x> <y>` |
|
|
144
|
+
| run some JavaScript in the page | `eval document.title`; `eval --file=check.js` for a long one |
|
|
145
|
+
| wait for a page or element | `wait load`, `wait <selector> [--gone]`, `wait text <text>` |
|
|
146
|
+
| choose a file in a file input | `upload <selector> <file>` |
|
|
147
|
+
| see it as a phone, or in dark mode | `emulate mobile`, `emulate dark` |
|
|
148
|
+
| see what the page requested | `requests`, then `body <#>` for what one got back |
|
|
149
|
+
| see console messages and errors | `console` |
|
|
150
|
+
| show an agent what I do | `watch on`, click around, then `watch` |
|
|
151
|
+
| see each step as I click | `watch on --live` |
|
|
152
|
+
| watch a tab someone will open | `watch on --next-tab [url-part]` |
|
|
153
|
+
| replay what I clicked through | `watch save flow.txt`, then `pw-repl send --file flow.txt` |
|
|
154
|
+
| record requests and console together | `capture on`, then `capture off` |
|
|
155
|
+
| find what makes the page slow | `profile on`, then `profile off`; `profile reload` for a load |
|
|
156
|
+
| fake a backend response | `route <glob> <status> <json>`, `route <glob> abort` |
|
|
157
|
+
| change or slow an API response | `route <glob> patch <json>`, `route <glob> delay <secs>` |
|
|
158
|
+
| cut or slow the whole network | `network off`, `network slow`, then `network on` |
|
|
159
|
+
| record a video of the page | `record on`, then `record off` (`help video` for a take) |
|
|
160
|
+
| show a pointer where the agent acts | `cursor on`, then `cursor off` |
|
|
161
|
+
| say what is happening on screen | `toast <text>`, then `toast off` |
|
|
162
|
+
| point something out on the page | `highlight <ref>`, then `highlight off` |
|
|
163
|
+
| clean up | `modes off` |
|
|
160
164
|
|
|
161
165
|
`help <topic>` lists the rest, and `help <command>` has usage and caveats.
|
|
162
166
|
|
|
163
167
|
### Modes
|
|
164
168
|
|
|
165
|
-
Some commands turn on a mode that stays on until you turn it off: `watch`, `capture`, `profile`,
|
|
166
|
-
`network off` or `slow`, `emulate`, `viewport`, `
|
|
167
|
-
are on in the selected tab, the prompt shows them: `(watch network:off routes:2) pw>`. `modes`
|
|
168
|
-
for every tab, and `modes off` turns them all off. Each of these commands on its own shows
|
|
169
|
-
what you can run next.
|
|
169
|
+
Some commands turn on a mode that stays on until you turn it off: `watch`, `capture`, `profile`,
|
|
170
|
+
`record`, `route`, `network off` or `slow`, `emulate`, `viewport`, `highlight`, `cursor` and `toast`.
|
|
171
|
+
While any are on in the selected tab, the prompt shows them: `(watch network:off routes:2) pw>`. `modes`
|
|
172
|
+
lists them for every tab, and `modes off` turns them all off. Each of these commands on its own shows
|
|
173
|
+
its state and what you can run next.
|
|
170
174
|
|
|
171
175
|
## For agents and scripts
|
|
172
176
|
|
|
@@ -204,12 +208,10 @@ each mode on, and `modes off --mine` turns off only the sender's own.
|
|
|
204
208
|
`send --file take.txt` runs a file of commands, one per line, and stops at the first that fails: a
|
|
205
209
|
repro to keep, or a video take.
|
|
206
210
|
|
|
207
|
-
**Passwords:** type one as a variable
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
and `attrs` hide a password field's value, but `eval` on the field, a field that a Show password button
|
|
212
|
-
made plain text, or page text that repeats it do not. Only `PW_` names are variables.
|
|
211
|
+
**Passwords:** type one as a variable, so it is never in the command, a file of commands or the REPL's
|
|
212
|
+
log: `PW_PASSWORD=... pw-repl send fill '#password' "{{ PW_PASSWORD }}"` (`watch save` writes them that
|
|
213
|
+
way). What reads the page back can still show it: `snapshot`, `html` and `attrs` hide a password field's
|
|
214
|
+
value; `eval` on the field does not.
|
|
213
215
|
|
|
214
216
|
### The HTTP protocol
|
|
215
217
|
|
package/bin/pw-repl.js
CHANGED
|
@@ -9,7 +9,8 @@ const USAGE = `Usage:
|
|
|
9
9
|
|
|
10
10
|
pw-repl serve [--background] [--launch [--headed]] [[-e] endpoint] [start-url] [-- <chromium flags>]
|
|
11
11
|
the same, plus a command server (socket, port, or 127.0.0.1:port; default /tmp/pw-repl.sock);
|
|
12
|
-
--background runs it detached, with its output in a log next to the socket
|
|
12
|
+
--background runs it detached, with its output in a log next to the socket; a socket's folder is
|
|
13
|
+
made if it is missing
|
|
13
14
|
|
|
14
15
|
pw-repl attach [-e endpoint]
|
|
15
16
|
see everything a background REPL does, and type commands to it; Ctrl-C leaves it running
|
|
@@ -18,7 +19,8 @@ const USAGE = `Usage:
|
|
|
18
19
|
stop a background REPL, and say what became of each recording it ended; its socket is removed, its
|
|
19
20
|
log stays
|
|
20
21
|
|
|
21
|
-
pw-repl send [-e endpoint | -s session] [-c client] [-t seconds] [--no-vars]
|
|
22
|
+
pw-repl send [-e endpoint | -s session] [-c client] [-t seconds] [--no-vars]
|
|
23
|
+
<command...> | --file <file>
|
|
22
24
|
run one command in a running REPL and print its output; --file runs a file of them, one per line
|
|
23
25
|
|
|
24
26
|
pw-repl where [-e endpoint | -s session]
|
|
@@ -30,59 +32,100 @@ const USAGE = `Usage:
|
|
|
30
32
|
pw-repl skill
|
|
31
33
|
print an agent skill (SKILL.md) that teaches an agent to use the REPL
|
|
32
34
|
|
|
35
|
+
A browser: --launch starts a Chromium of the REPL's own, headless unless --headed, 1280x800, in a
|
|
36
|
+
temporary profile, on a free port of its own (pw-repl where names it). It prints the command it ran, and
|
|
37
|
+
stops with the REPL (Ctrl-C while it starts too); stop returns once it has exited. Flags after -- go to
|
|
38
|
+
it, and only flags (a start URL goes before --); a --user-data-dir=<dir> among them is used instead of
|
|
39
|
+
the temporary profile, and kept. PW_CHROME picks which Chromium.
|
|
40
|
+
|
|
41
|
+
Without --launch, the REPL connects to a browser running with --remote-debugging-port, at $PW_CDP_URL
|
|
42
|
+
(default http://localhost:9222).
|
|
43
|
+
|
|
33
44
|
send uses the server when -e, $PW_ENDPOINT, or the socket ($PW_SOCKET, default /tmp/pw-repl.sock) is
|
|
34
|
-
there, and the tmux session (-s, $PW_TMUX_SESSION, default pw-repl)
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
there, and the tmux session where pw-repl run runs otherwise (-s, $PW_TMUX_SESSION, default pw-repl).
|
|
46
|
+
serve without an endpoint serves on that same socket. send exit status: 0 ok, 1 command error, 2
|
|
47
|
+
completion not confirmed, 64 usage or unreachable. pw-repl send --help has the rest: clients, -t,
|
|
48
|
+
--file and variables.`;
|
|
49
|
+
|
|
50
|
+
// pw-repl send --help: what send does beyond its usage line.
|
|
51
|
+
const SEND_HELP = `send uses the server when -e, $PW_ENDPOINT, or the socket ($PW_SOCKET, default /tmp/pw-repl.sock) is
|
|
52
|
+
there, and the tmux session (-s, $PW_TMUX_SESSION, default pw-repl) otherwise, where pw-repl run runs.
|
|
53
|
+
If the socket exists but nothing answers, send fails rather than fall back. serve without an endpoint
|
|
54
|
+
serves on that same one: $PW_ENDPOINT, then $PW_SOCKET, then the default.
|
|
37
55
|
|
|
38
56
|
send -c <client> (or $PW_CLIENT) sends as a client of the REPL's with a selected tab of its own, so
|
|
39
57
|
several agents can share one REPL; pw-repl help session has the rest.
|
|
40
58
|
|
|
41
|
-
send
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
send
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
is not set stops the command, or the file at that line, before it is sent
|
|
60
|
-
{{ PW_NAME }} as it is. Variables need the command server (pw-repl serve), not the tmux
|
|
61
|
-
|
|
62
|
-
The browser must be running with --remote-debugging-port (default http://localhost:9222; set $PW_CDP_URL).
|
|
63
|
-
|
|
64
|
-
--launch is the quick start instead: it starts a Chromium of the REPL's own (headless unless --headed, in
|
|
65
|
-
a temporary profile, on a free port of its own, which pw-repl where names), prints the command it ran,
|
|
66
|
-
and stops it with the REPL (Ctrl-C while it starts too); stop returns once that Chromium has exited.
|
|
67
|
-
Flags after -- go to it; a --user-data-dir=<dir> among them is used instead of the temporary profile, and
|
|
68
|
-
kept. PW_CHROME picks which one. To set a browser up your own way, start it yourself and use PW_CDP_URL.
|
|
69
|
-
|
|
70
|
-
In a tmux session named pw-repl, pw-repl send reaches pw-repl run without the server.`;
|
|
59
|
+
send's options can also follow the command, as in pw-repl send open <url> -t 30; after a -- before the
|
|
60
|
+
command, every word is the command's, -t 30 included.
|
|
61
|
+
|
|
62
|
+
Exit status: 0 ok, 1 command error, 2 completion not confirmed, 64 usage or unreachable. -t is how long
|
|
63
|
+
send waits for the answer (default 20s); it does not lengthen a command's own limits, such as the 5s an
|
|
64
|
+
action waits for its element.
|
|
65
|
+
|
|
66
|
+
--file <file> runs each line of a file as a command, in order, as if typed at the REPL's prompt: quoted
|
|
67
|
+
as the prompt reads it (no shell), with a file it records, uploads or saves relative to this folder, as
|
|
68
|
+
on send's command line, and # comments and blank lines skipped. It stops at the first command that
|
|
69
|
+
fails, saying its line, with its exit status. -t is each command's own limit. Other clients' commands
|
|
70
|
+
can run between its lines, on their own tabs.
|
|
71
|
+
|
|
72
|
+
Variables: a {{ PW_NAME }} in a command, or in a line of the file, takes its value from send's own
|
|
73
|
+
environment (PW_NAME), passed along with the command, not in it, so a password stays out of the file and
|
|
74
|
+
out of the REPL's log: PW_PASSWORD=... pw-repl send fill '#password' "{{ PW_PASSWORD }}", or
|
|
75
|
+
PW_PASSWORD="$(cat <file>)" to give it from a file without typing it. Only PW_ names are variables; any
|
|
76
|
+
other {{ ... }} is sent as written. In a file, put one in double quotes (in single quotes, a value with
|
|
77
|
+
a ' is refused). One that is not set stops the command, or the file at that line, before it is sent;
|
|
78
|
+
--no-vars sends {{ PW_NAME }} as it is. Variables need the command server (pw-repl serve), not the tmux
|
|
79
|
+
pane.`;
|
|
71
80
|
|
|
72
81
|
const REPL_HELP = `The REPL's own commands: help at the pw> prompt, or pw-repl send help here (no REPL needed).
|
|
73
82
|
|
|
74
83
|
pw-repl help <topic | command [--all] | --all> shows one part of it; pw-repl help video and pw-repl
|
|
75
84
|
help playwright-cli are guides.`;
|
|
76
85
|
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
const SUBCOMMANDS = ['run', 'serve', 'attach', 'stop', 'send', 'where', 'help', 'skill'];
|
|
87
|
+
|
|
88
|
+
// What is wrong, then only the usage of the subcommand it is about: the whole of it would bury the line
|
|
89
|
+
// that matters. With no message, the whole usage (-h after a subcommand).
|
|
90
|
+
function usage(message, subcommand) {
|
|
91
|
+
if (!message) {
|
|
92
|
+
console.error(USAGE);
|
|
93
|
+
process.exit(64);
|
|
94
|
+
}
|
|
95
|
+
console.error(`pw-repl: ${message}`);
|
|
96
|
+
const lines = subcommand && USAGE.split('\n\n').find(block => new RegExp(`^(?:Usage:\\n)? pw-repl ${subcommand}(?: |\\n|$)`).test(block));
|
|
97
|
+
if (lines) console.error(`Usage:\n${lines.replace(/^Usage:\n/, '')}`);
|
|
98
|
+
console.error(`pw-repl --help has the ${lines ? 'rest' : 'usage'}.`);
|
|
79
99
|
process.exit(64);
|
|
80
100
|
}
|
|
81
101
|
|
|
82
|
-
//
|
|
83
|
-
function
|
|
102
|
+
// A command line to copy, quoted as a shell reads it.
|
|
103
|
+
function shellLine(words) {
|
|
104
|
+
return words.map(w => (/^[\w@%+=:,./-]+$/.test(w) ? w : `'${w.replace(/'/g, "'\\''")}'`)).join(' ');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// -e, -s, -c and -t, then the command words (unquoted words are one command), and send's options again
|
|
108
|
+
// at the end, as an agent types them (send open <url> -t 30); after --, every word is the command.
|
|
109
|
+
function parseSendArgs(args, subcommand) {
|
|
110
|
+
const allowCommand = subcommand === 'send';
|
|
84
111
|
const options = { endpoint: null, session: null, client: null, timeout: 20, command: '', file: null };
|
|
112
|
+
const given = new Set();
|
|
113
|
+
const take = (arg, value, after) => {
|
|
114
|
+
if (value === undefined) usage(`${arg} needs a value`, subcommand);
|
|
115
|
+
// Given before the command and after it, neither silently wins.
|
|
116
|
+
if (after && given.has(arg)) usage(`${arg} is given twice, ${value === true ? arg : `${arg} ${value}`} after the command; give it once`, subcommand);
|
|
117
|
+
given.add(arg);
|
|
118
|
+
if (arg === '--no-vars') options.noVars = true;
|
|
119
|
+
if (arg === '-e') options.endpoint = value;
|
|
120
|
+
if (arg === '-s') options.session = value;
|
|
121
|
+
if (arg === '-c') options.client = value;
|
|
122
|
+
if (arg === '-t') {
|
|
123
|
+
if (!/^\d+$/.test(value) || Number(value) < 1) usage(`-t takes whole seconds, 1 or more (got ${value})`, subcommand);
|
|
124
|
+
options.timeout = Number(value);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
85
127
|
let i = 0;
|
|
128
|
+
let dashes = false;
|
|
86
129
|
for (; i < args.length; i++) {
|
|
87
130
|
const arg = args[i];
|
|
88
131
|
// playwright-cli's --raw: the output here is only the command's already.
|
|
@@ -97,22 +140,22 @@ function parseSendArgs(args, allowCommand) {
|
|
|
97
140
|
// A file of commands, one per line, as typed at the prompt.
|
|
98
141
|
if (arg.startsWith('--file=') && arg.length > 7) { options.file = require('path').resolve(arg.slice(7)); continue; }
|
|
99
142
|
if (arg === '--file') {
|
|
100
|
-
if (args[i + 1] === undefined) usage();
|
|
143
|
+
if (args[i + 1] === undefined) usage('--file needs a file of commands', subcommand);
|
|
101
144
|
options.file = require('path').resolve(args[++i]);
|
|
102
145
|
continue;
|
|
103
146
|
}
|
|
104
|
-
if (arg === '--no-vars')
|
|
105
|
-
if (arg === '-e' || arg === '-s' || arg === '-c' || arg === '-t')
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
147
|
+
if (arg === '--no-vars') take(arg, true);
|
|
148
|
+
else if (arg === '-e' || arg === '-s' || arg === '-c' || arg === '-t') take(arg, args[++i]);
|
|
149
|
+
else if (arg === '--') { i++; dashes = true; break; } else break;
|
|
150
|
+
}
|
|
151
|
+
const words = args.slice(i);
|
|
152
|
+
// A word the shell kept whole ("a -t 3") is never one of these; an option with a bad value is still
|
|
153
|
+
// send's, refused, not the end of a URL or a selector.
|
|
154
|
+
while (allowCommand && !dashes) {
|
|
155
|
+
const last = words.length - 1;
|
|
156
|
+
if (last > 0 && words[last] === '--raw') words.pop();
|
|
157
|
+
else if (last > 0 && words[last] === '--no-vars') take(words.pop(), true, true);
|
|
158
|
+
else if (last > 1 && /^-[ecst]$/.test(words[last - 1])) { const value = words.pop(); take(words.pop(), value, true); } else break;
|
|
116
159
|
}
|
|
117
160
|
// An address that is refused (not loopback) is a usage error, said once, not a stack trace.
|
|
118
161
|
const endpoint = options.endpoint || process.env.PW_ENDPOINT;
|
|
@@ -124,9 +167,9 @@ function parseSendArgs(args, allowCommand) {
|
|
|
124
167
|
process.exit(64);
|
|
125
168
|
}
|
|
126
169
|
}
|
|
127
|
-
|
|
128
|
-
if (!allowCommand &&
|
|
129
|
-
if (options.file && words.length) usage();
|
|
170
|
+
if (!allowCommand && options.file) usage(`--file is send's; ${subcommand} runs no commands`, subcommand);
|
|
171
|
+
if (!allowCommand && words.length) usage(`${subcommand} takes no command (got ${shellLine(words)})`, subcommand);
|
|
172
|
+
if (options.file && words.length) usage(`a command or --file, not both (got --file and ${shellLine(words)})`, subcommand);
|
|
130
173
|
options.command = require('../lib/send').commandLine(words);
|
|
131
174
|
return options;
|
|
132
175
|
}
|
|
@@ -141,17 +184,35 @@ function startOptions(args, serve) {
|
|
|
141
184
|
if (serve) options.background = flag('--background');
|
|
142
185
|
options.launch = flag('--launch');
|
|
143
186
|
options.headed = flag('--headed');
|
|
144
|
-
|
|
187
|
+
const subcommand = serve ? 'serve' : 'run';
|
|
188
|
+
if (options.headed && !options.launch) usage('--headed is for the Chromium --launch starts; add --launch', subcommand);
|
|
189
|
+
if (options.chromeArgs.length && !options.launch) usage('flags after -- go to the Chromium --launch starts; add --launch', subcommand);
|
|
190
|
+
// Chromium takes any word that is not a --switch as a page to open, and a headless one refuses a second.
|
|
191
|
+
const n = options.chromeArgs.findIndex(word => !word.startsWith('-'));
|
|
192
|
+
if (n !== -1) {
|
|
193
|
+
const [page, before] = [options.chromeArgs[n], options.chromeArgs[n - 1]];
|
|
194
|
+
// Most likely a switch's value given as a word of its own (--user-data-dir /tmp/p); a URL after a
|
|
195
|
+
// switch is a URL (--disable-gpu http://...).
|
|
196
|
+
if (before && /^--[^=]+$/.test(before) && !page.includes('://')) usage(`${page} after -- would go to Chromium as a page to open: it takes a switch's value as ${before}=${page}`, subcommand);
|
|
197
|
+
usage(`${page} after -- would go to Chromium as a page to open; only flags go after --. A start URL goes before --; for a browser already running, leave out --launch and set PW_CDP_URL`, subcommand);
|
|
198
|
+
}
|
|
145
199
|
// -e <endpoint>, as send, attach, stop and where take it, or the endpoint as the first word.
|
|
146
200
|
const e = rest.indexOf('-e');
|
|
147
201
|
if (serve && e !== -1 && rest[e + 1] && looksLikeEndpoint(rest[e + 1])) {
|
|
148
202
|
options.endpoint = rest.splice(e, 2)[1];
|
|
149
203
|
// Two endpoints, one with -e: neither silently wins.
|
|
150
|
-
if (rest[0] && looksLikeEndpoint(rest[0])) usage();
|
|
204
|
+
if (rest[0] && looksLikeEndpoint(rest[0])) usage(`two endpoints, -e ${options.endpoint} and ${rest[0]}: give one`, subcommand);
|
|
151
205
|
} else if (serve && rest[0] && looksLikeEndpoint(rest[0])) options.endpoint = rest.shift();
|
|
152
206
|
// Otherwise the socket send would look for: PW_ENDPOINT, then PW_SOCKET, then the default.
|
|
153
207
|
if (serve && !options.endpoint) options.endpoint = process.env.PW_ENDPOINT || process.env.PW_SOCKET || null;
|
|
154
|
-
|
|
208
|
+
const option = rest.find(word => word.startsWith('-'));
|
|
209
|
+
if (option === '-h' || option === '--help') usage();
|
|
210
|
+
if (option === '-e') {
|
|
211
|
+
usage(serve ? `-e takes an endpoint: a socket (/tmp/<name>.sock), a port, or 127.0.0.1:port (got ${rest[rest.indexOf('-e') + 1] ?? 'nothing'})` : 'run has no command server, so no endpoint; pw-repl serve has one', subcommand);
|
|
212
|
+
}
|
|
213
|
+
if (option === '--background') usage('--background is serve\'s: pw-repl serve --background', subcommand);
|
|
214
|
+
if (option) usage(`unknown option ${option}`, subcommand);
|
|
215
|
+
if (rest.length > 1) usage(`one start URL at most (got ${shellLine(rest)})`, subcommand);
|
|
155
216
|
options.startUrl = rest[0] || null;
|
|
156
217
|
return options;
|
|
157
218
|
}
|
|
@@ -174,26 +235,43 @@ function skill() {
|
|
|
174
235
|
|
|
175
236
|
async function main() {
|
|
176
237
|
let [subcommand, ...args] = process.argv.slice(2);
|
|
238
|
+
// -h after send, where, attach or stop is a request for help, never a command to send.
|
|
239
|
+
if (['send', 'where', 'attach', 'stop'].includes(subcommand) && /^(?:-h|--help)$/.test(args[0] || '')) {
|
|
240
|
+
if (subcommand !== 'send') return console.log(`${USAGE}\n\n${REPL_HELP}`);
|
|
241
|
+
const block = USAGE.split('\n\n').find(b => b.startsWith(' pw-repl send '));
|
|
242
|
+
return console.log(`Usage:\n${block}\n\n${SEND_HELP}`);
|
|
243
|
+
}
|
|
177
244
|
// Bare pw-repl explains itself rather than connect to someone's browser.
|
|
178
245
|
if (subcommand === undefined) return console.log(`${USAGE}\n\n${REPL_HELP}`);
|
|
179
246
|
switch (subcommand) {
|
|
180
247
|
case 'run':
|
|
181
248
|
case 'serve': {
|
|
182
249
|
const options = startOptions(args, subcommand === 'serve');
|
|
250
|
+
// A socket of one's own is often in a folder of its own (/tmp/<name>/repl.sock): made here, before
|
|
251
|
+
// the log, the pid file or the socket go in it, whichever way serve runs. Owner-only, as the socket is.
|
|
252
|
+
const socket = subcommand === 'serve' ? require('../lib/client').parseEndpoint(options.endpoint).socket : null;
|
|
253
|
+
if (socket) {
|
|
254
|
+
const folder = require('path').dirname(socket);
|
|
255
|
+
try { require('fs').mkdirSync(folder, { recursive: true, mode: 0o700 }); } catch (error) {
|
|
256
|
+
console.error(`pw-repl: cannot create ${folder} for the socket (${error.code === 'EEXIST' ? 'a file is there' : error.code || error.message})`);
|
|
257
|
+
process.exit(64);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
183
260
|
if (options.background) process.exit(await require('../lib/background').start(options));
|
|
184
261
|
return require('../lib/start').start(options);
|
|
185
262
|
}
|
|
186
263
|
case 'attach':
|
|
187
264
|
case 'stop': {
|
|
188
|
-
const options = parseSendArgs(args,
|
|
189
|
-
if (options.session) usage();
|
|
265
|
+
const options = parseSendArgs(args, subcommand);
|
|
266
|
+
if (options.session) usage(`-s is send's tmux session; ${subcommand} takes -e <endpoint>`, subcommand);
|
|
190
267
|
// The same socket send would use when there is no -e: PW_ENDPOINT, then PW_SOCKET.
|
|
191
268
|
const endpoint = options.endpoint || process.env.PW_ENDPOINT || process.env.PW_SOCKET || null;
|
|
192
269
|
process.exit(await require('../lib/background')[subcommand]({ endpoint }));
|
|
193
270
|
}
|
|
194
271
|
// falls through never: process.exit above
|
|
195
272
|
case 'send': {
|
|
196
|
-
const options = parseSendArgs(args,
|
|
273
|
+
const options = parseSendArgs(args, 'send');
|
|
274
|
+
if (!options.command && !options.file) usage('no command given', 'send');
|
|
197
275
|
// help is fixed text, so it needs no browser: answer it here.
|
|
198
276
|
const asksHelp = /^help(?:\s+(.*))?$/.exec(options.command);
|
|
199
277
|
if (asksHelp) return help((asksHelp[1] || '').trim());
|
|
@@ -201,7 +279,7 @@ async function main() {
|
|
|
201
279
|
}
|
|
202
280
|
// falls through never: process.exit above
|
|
203
281
|
case 'where':
|
|
204
|
-
process.exit(await require('../lib/send').where(parseSendArgs(args,
|
|
282
|
+
process.exit(await require('../lib/send').where(parseSendArgs(args, 'where')));
|
|
205
283
|
// falls through never
|
|
206
284
|
case 'help': {
|
|
207
285
|
const topic = args.join(' ').trim();
|
|
@@ -209,7 +287,7 @@ async function main() {
|
|
|
209
287
|
return console.log(`${USAGE}\n\n${REPL_HELP}`);
|
|
210
288
|
}
|
|
211
289
|
case 'skill':
|
|
212
|
-
if (args.length) usage();
|
|
290
|
+
if (args.length) usage(`skill takes no arguments (got ${shellLine(args)})`, 'skill');
|
|
213
291
|
return skill();
|
|
214
292
|
case '-v':
|
|
215
293
|
case '--version':
|
|
@@ -217,8 +295,17 @@ async function main() {
|
|
|
217
295
|
case '-h':
|
|
218
296
|
case '--help':
|
|
219
297
|
return console.log(`${USAGE}\n\n${REPL_HELP}`);
|
|
220
|
-
default:
|
|
221
|
-
|
|
298
|
+
default: {
|
|
299
|
+
// An option before the subcommand (pw-repl -t 30 send ...): say where it goes, with the line fixed.
|
|
300
|
+
if (subcommand.startsWith('-')) {
|
|
301
|
+
const all = [subcommand, ...args];
|
|
302
|
+
const named = all.find(word => SUBCOMMANDS.includes(word));
|
|
303
|
+
const fixed = named ? shellLine(['pw-repl', named, ...all.filter((word, n) => n !== all.indexOf(named))]) : `pw-repl send ${shellLine(all)} <command>`;
|
|
304
|
+
return usage(`${subcommand} is an option of a subcommand, and goes after it: ${fixed}`, named || 'send');
|
|
305
|
+
}
|
|
306
|
+
const url = /^[a-z][\w+.-]*:/i.test(subcommand) && !looksLikeEndpoint(subcommand) ? `; a start URL goes after run or serve: pw-repl run ${subcommand}` : '';
|
|
307
|
+
return usage(`${subcommand} is not a command (they are ${SUBCOMMANDS.join(', ')})${url}`);
|
|
308
|
+
}
|
|
222
309
|
}
|
|
223
310
|
}
|
|
224
311
|
|
package/lib/cli-names.js
CHANGED
|
@@ -13,7 +13,7 @@ const NAMES = {
|
|
|
13
13
|
'tab-select': [a => `tab-select ${a}`, 'tab <index>, by index only'],
|
|
14
14
|
// By index there, by URL here: a number could match a port in another tab's URL.
|
|
15
15
|
'tab-close': [a => (a ? null : 'tab close'), 'tab close [url-part], or tab <index> then tab close'],
|
|
16
|
-
open: [a => `tab new ${a}`.trim(), 'tab new [url]; --mobile
|
|
16
|
+
open: [a => `tab new ${a}`.trim(), 'tab new [url] (goto <url> navigates); --mobile, --device=<name>: emulate mobile'],
|
|
17
17
|
// It ends playwright-cli's own browser; this one is shared, so tabs stay open.
|
|
18
18
|
close: [() => 'close', 'modes off --mine, and tabs stay open'],
|
|
19
19
|
'go-back': [() => 'back', 'back'],
|
|
@@ -69,7 +69,7 @@ const NEAREST = {
|
|
|
69
69
|
list: 'pw-repl where says which REPL is running; one REPL serves everyone using its browser',
|
|
70
70
|
attach: 'pw-repl serve connects to the browser on PW_CDP_URL; pw-repl skill has the rest',
|
|
71
71
|
detach: 'pw-repl stop, or quit at the prompt, disconnects and leaves the browser running',
|
|
72
|
-
|
|
72
|
+
drop: 'not here yet', 'webmcp-list': 'not here yet', 'webmcp-call': 'not here yet',
|
|
73
73
|
keydown: 'not here yet; press <key> presses and releases one', keyup: 'not here yet; press <key> presses and releases one',
|
|
74
74
|
pdf: 'not here yet; screenshot --full saves the whole page as a PNG',
|
|
75
75
|
'generate-locator': 'not here yet; a snapshot ref (e5) works as a selector in any command',
|
|
@@ -244,7 +244,7 @@ function translate(text) {
|
|
|
244
244
|
if (name === 'open') {
|
|
245
245
|
const { found, rest: url } = takeOptions(args, OPTIONS.open);
|
|
246
246
|
const device = found.find(o => o.name === 'device');
|
|
247
|
-
if (!device && !found.length) return { text: `tab new ${url}`.trim(), as: 'tab new [url]' };
|
|
247
|
+
if (!device && !found.length) return { text: `tab new ${url}`.trim(), as: 'tab new [url], a new tab each time; goto <url> navigates the selected tab' };
|
|
248
248
|
if (device && !device.value) return { refuse: 'Usage: open [url] [--mobile | --device=<name>]' };
|
|
249
249
|
return {
|
|
250
250
|
steps: ['tab new', `emulate mobile${device ? ` ${device.value}` : ''}`, ...(url ? [`goto ${url}`] : [])],
|