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 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, upload files, move the mouse, scroll the
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 `cdp`.
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 without `--launch`:
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` (it marks the tab you are looking at `(visible)`), `info` |
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>`, `wait <selector> --gone`, `wait text <text>` |
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` (also `emulate locale`, `emulate timezone`) |
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 in the browser, then `watch` |
148
- | see each step as I click | `watch on --live` |
149
- | replay what I clicked through | `watch on`, click around, `watch save flow.txt`, then `pw-repl send --file flow.txt` |
150
- | record requests and console together | `capture on`, then `capture off` |
151
- | find what makes the page slow | `profile on`, then `profile off`; `profile reload` for a page load |
152
- | fake a backend response | `route <glob> <status> <json>`, `route <glob> abort` |
153
- | change or slow an API response | `route <glob> patch <json>`, `route <glob> delay <secs>` |
154
- | cut or slow the whole network | `network off`, `network slow`, then `network on` |
155
- | record a video of the page | `record on`, then `record off` (`help video` for a take with a pointer) |
156
- | show a pointer where the agent acts | `cursor on`, then `cursor off` (in screenshots and videos) |
157
- | say what is happening on screen | `toast <text>`, then `toast off` (`--duration=3s` hides it by itself) |
158
- | point something out on the page | `highlight <ref>` draws a box; `highlight off` hides it |
159
- | clean up | `modes off` |
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`, `route`,
166
- `network off` or `slow`, `emulate`, `viewport`, `record`, `highlight`, `cursor` and `toast`. While any
167
- are on in the selected tab, the prompt shows them: `(watch network:off routes:2) pw>`. `modes` lists them
168
- for every tab, and `modes off` turns them all off. Each of these commands on its own shows its state and
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: `PW_PASSWORD=... pw-repl send fill '#password' "{{ PW_PASSWORD
208
- }}"`, or the same line in a file run with `send --file`, as `watch save` writes it. `send` passes the
209
- value along with the command, and the REPL puts it in only as it runs the command, so the file, the
210
- REPL's output, its log and its history never hold it. Output that reads it back can: `snapshot`, `html`
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] <command...> | --file <file>
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) otherwise. If the socket exists but
35
- nothing answers, send fails rather than fall back. serve without an endpoint serves on that same one:
36
- $PW_ENDPOINT, then $PW_SOCKET, then the default.
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 exit status: 0 ok, 1 command error, 2 completion not confirmed, 64 usage or unreachable. send -t is
42
- how long send waits for the answer (default 20s); it does not lengthen a command's own limits, such as
43
- the 5s an action waits for its element.
44
-
45
- send --file <file> runs each line of a file as a command, in order, as if typed at the REPL's prompt:
46
- quoted as the prompt reads it (no shell), with a file it records, uploads or saves relative to this
47
- folder, as on send's command line, and # comments and blank lines skipped. It stops at the first
48
- command that fails, saying its line, with its exit status. -t is each command's own limit. Other
49
- clients' commands can run between its lines, on their own tabs.
50
-
51
- A {{ PW_NAME }} in a command, or in a line of the file, is a variable: send takes its value from its own
52
- environment (PW_NAME) and passes it along with the command, not in it, e.g. a password kept out of the
53
- file and out of the command as the REPL logs it. In a file: fill '#password' "{{ PW_PASSWORD }}",
54
- run with PW_PASSWORD=... pw-repl send --file login.txt; or one command: PW_PASSWORD=... pw-repl send
55
- fill '#password' "{{ PW_PASSWORD }}". Only names starting PW_ are variables: any other {{ ... }}, as in a
56
- page's template text, is sent as written. In a file, put it in double quotes: there its value is
57
- escaped as they read it (in single quotes, which escape nothing, a value with a ' is refused). The REPL
58
- puts it in only as it runs the command, and its pane and log show the command as written. A variable that
59
- is not set stops the command, or the file at that line, before it is sent. --no-vars sends
60
- {{ PW_NAME }} as it is. Variables need the command server (pw-repl serve), not the tmux pane.
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
- function usage() {
78
- console.error(USAGE);
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
- // -e, -s, -c and -t, then the command words (unquoted words are one command).
83
- function parseSendArgs(args, allowCommand) {
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') { options.noVars = true; continue; }
105
- if (arg === '-e' || arg === '-s' || arg === '-c' || arg === '-t') {
106
- const value = args[++i];
107
- if (value === undefined) usage();
108
- if (arg === '-e') options.endpoint = value;
109
- if (arg === '-s') options.session = value;
110
- if (arg === '-c') options.client = value;
111
- if (arg === '-t') {
112
- if (!/^\d+$/.test(value) || Number(value) < 1) usage();
113
- options.timeout = Number(value);
114
- }
115
- } else if (arg === '--') { i++; break; } else break;
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
- const words = args.slice(i);
128
- if (!allowCommand && (words.length || options.file)) usage();
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
- if ((options.headed || options.chromeArgs.length) && !options.launch) usage();
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
- if (rest.length > 1 || (rest[0] && rest[0].startsWith('-'))) usage();
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, false);
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, true);
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, false)));
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
- return usage();
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 or --device=<name> is emulate mobile [name]'],
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
- drag: 'not here yet', drop: 'not here yet', 'webmcp-list': 'not here yet', 'webmcp-call': 'not here yet',
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}`] : [])],