pw-repl 0.12.7 → 0.13.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 CHANGED
@@ -23,7 +23,7 @@ If you hit a limitation or write a workaround, consider adding the capability to
23
23
  `lib/routes.js`, `lib/network.js`, `lib/emulation.js` (and viewport), `lib/capture.js`, `lib/profile.js` (performance traces;
24
24
  `lib/trace.js` reads and sums them up), `lib/record.js`
25
25
  (video, through ffmpeg, and the steps file saved with it), `lib/dialogs.js` and `lib/modes.js`. Beneath them, `lib/tabstate.js` is the one record kept per tab, `lib/cdp.js` the CDP
26
- session kept per tab, and `lib/util.js` small shared helpers. No facility requires `lib/modes.js` or
26
+ session kept per tab, `lib/util.js` small shared helpers, and `lib/written.js` the files written and for whom. No facility requires `lib/modes.js` or
27
27
  `lib/commands.js`, and a unit test fails on a require cycle, which CommonJS would not report.
28
28
  - `bin/pw-repl.js` is the command line; `lib/start.js` connects and runs the prompt; `lib/runner.js`
29
29
  runs commands one at a time (`quit` and `dialog` skip its queue); `lib/server.js` is the command
@@ -39,7 +39,7 @@ If you hit a limitation or write a workaround, consider adding the capability to
39
39
  accepts theirs too (`lib/cli-names.js`, `help playwright-cli`). Deviate only where it is necessary, as
40
40
  where sharing a browser with a user needs it, and say why in the help.
41
41
  - Comment the *why* when it isn't obvious from the code.
42
- - `npm test` runs the suite (under two minutes): a private headless Chromium, a local test site
42
+ - `npm test` runs the suite (about two and a half minutes): a private headless Chromium, a local test site
43
43
  (`test/harness.js`), and the real REPL with its server. Nothing is mocked, and the shared browser is
44
44
  never touched. It finds Chromium through `PW_TEST_CHROME`, or where `--launch` looks (Playwright's
45
45
  browsers, then the `PATH`), and skips the browser tests if there is none. Add a test with each new
@@ -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).
@@ -103,10 +105,14 @@ Commits follow [Conventional Commits](https://www.conventionalcommits.org/): `fe
103
105
  1.0, a change that breaks how the REPL is used is a minor bump (0.2.0 to 0.3.0), anything else a patch.
104
106
 
105
107
  1. `npm test` passes and the working tree is clean.
106
- 2. `npm outdated` and `npm audit` show nothing that needs doing first. The lockfile holds playwright-core
107
- 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
- "waits for the page to load, counting a navigation that just began" fails). Update it once a version
109
- passes `npm test`.
108
+ 2. `npm outdated` and `npm audit` show nothing that needs doing first. Dependencies are always the newest
109
+ stable release, never pinned back (user ruling). When the newest breaks a test, fix it in our code;
110
+ only a regression of the dependency's own, with the fix already in its next release, keeps the version
111
+ before it, until that release ships. playwright-core stays at 1.63 for that reason: on 1.64.1 a click
112
+ on a link to a 204 hangs at "waiting for scheduled navigations to finish" until its timeout, in plain
113
+ Playwright too (chromium.launch, a page with a link to a 204, page.click on it); 1.65's alphas fix it.
114
+ The test "waits for the page to load, counting a navigation that just began" shows it: move up once it
115
+ passes on the newest release.
110
116
  3. `npm version <patch|minor|major> -m "chore: release %s"` sets the version in `package.json` and
111
117
  `package-lock.json`, commits it as `chore: release X.Y.Z`, and tags that commit `vX.Y.Z`.
112
118
  4. `git push --follow-tags` pushes the commits and the tag.
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`; read what a Copy button put on the clipboard (`clipboard`).
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 (flags only; a start URL goes before `--`).
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 <time>` |
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
@@ -2,6 +2,7 @@
2
2
  // The one command: pw-repl run | serve | attach | stop | send | where | help | skill.
3
3
 
4
4
  const { looksLikeEndpoint } = require('../lib/client');
5
+ const { longDuration } = require('../lib/util');
5
6
 
6
7
  const USAGE = `Usage:
7
8
  pw-repl run [--launch [--headed]] [start-url] [-- <chromium flags>]
@@ -9,7 +10,8 @@ const USAGE = `Usage:
9
10
 
10
11
  pw-repl serve [--background] [--launch [--headed]] [[-e] endpoint] [start-url] [-- <chromium flags>]
11
12
  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
13
+ --background runs it detached, with its output in a log next to the socket; a socket's folder is
14
+ made if it is missing
13
15
 
14
16
  pw-repl attach [-e endpoint]
15
17
  see everything a background REPL does, and type commands to it; Ctrl-C leaves it running
@@ -18,7 +20,8 @@ const USAGE = `Usage:
18
20
  stop a background REPL, and say what became of each recording it ended; its socket is removed, its
19
21
  log stays
20
22
 
21
- pw-repl send [-e endpoint | -s session] [-c client] [-t seconds] [--no-vars] <command...> | --file <file>
23
+ pw-repl send [-e endpoint | -s session] [-c client] [-t time] [--no-vars]
24
+ <command...> | --file <file>
22
25
  run one command in a running REPL and print its output; --file runs a file of them, one per line
23
26
 
24
27
  pw-repl where [-e endpoint | -s session]
@@ -30,47 +33,53 @@ const USAGE = `Usage:
30
33
  pw-repl skill
31
34
  print an agent skill (SKILL.md) that teaches an agent to use the REPL
32
35
 
33
- 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.
36
+ A browser: --launch starts a Chromium of the REPL's own, headless unless --headed, 1280x800, in a
37
+ temporary profile, on a free port of its own (pw-repl where names it). It prints the command it ran, and
38
+ stops with the REPL (Ctrl-C while it starts too); stop returns once it has exited. Flags after -- go to
39
+ it, and only flags (a start URL goes before --); a --user-data-dir=<dir> among them is used instead of
40
+ the temporary profile, and kept. PW_CHROME picks which Chromium.
37
41
 
38
- send -c <client> (or $PW_CLIENT) sends as a client of the REPL's with a selected tab of its own, so
39
- several agents can share one REPL; pw-repl help session has the rest.
42
+ Without --launch, the REPL connects to a browser running with --remote-debugging-port, at $PW_CDP_URL
43
+ (default http://localhost:9222).
40
44
 
41
- send's options can also follow the command, as in pw-repl send open <url> -t 30; after a -- before the
42
- command, every word is the command's, -t 30 included.
45
+ send uses the server when -e, $PW_ENDPOINT, or the socket ($PW_SOCKET, default /tmp/pw-repl.sock) is
46
+ there, and the tmux session where pw-repl run runs otherwise (-s, $PW_TMUX_SESSION, default pw-repl).
47
+ serve without an endpoint serves on that same socket. send exit status: 0 ok, 1 command error, 2
48
+ completion not confirmed, 64 usage or unreachable. pw-repl send --help has the rest: clients, -t,
49
+ --file and variables.`;
43
50
 
44
- send exit status: 0 ok, 1 command error, 2 completion not confirmed, 64 usage or unreachable. send -t is
45
- how long send waits for the answer (default 20s); it does not lengthen a command's own limits, such as
46
- the 5s an action waits for its element.
51
+ // pw-repl send --help: what send does beyond its usage line.
52
+ const SEND_HELP = `send uses the server when -e, $PW_ENDPOINT, or the socket ($PW_SOCKET, default /tmp/pw-repl.sock) is
53
+ there, and the tmux session (-s, $PW_TMUX_SESSION, default pw-repl) otherwise, where pw-repl run runs.
54
+ If the socket exists but nothing answers, send fails rather than fall back. serve without an endpoint
55
+ serves on that same one: $PW_ENDPOINT, then $PW_SOCKET, then the default.
47
56
 
48
- send --file <file> runs each line of a file as a command, in order, as if typed at the REPL's prompt:
49
- quoted as the prompt reads it (no shell), with a file it records, uploads or saves relative to this
50
- folder, as on send's command line, and # comments and blank lines skipped. It stops at the first
51
- command that fails, saying its line, with its exit status. -t is each command's own limit. Other
52
- clients' commands can run between its lines, on their own tabs.
57
+ send -c <client> (or $PW_CLIENT) sends as a client of the REPL's with a selected tab of its own, so
58
+ several agents can share one REPL; pw-repl help session has the rest.
53
59
 
54
- A {{ PW_NAME }} in a command, or in a line of the file, is a variable: send takes its value from its own
55
- environment (PW_NAME) and passes it along with the command, not in it, e.g. a password kept out of the
56
- file and out of the command as the REPL logs it. In a file: fill '#password' "{{ PW_PASSWORD }}",
57
- run with PW_PASSWORD=... pw-repl send --file login.txt; or one command: PW_PASSWORD=... pw-repl send
58
- fill '#password' "{{ PW_PASSWORD }}". Only names starting PW_ are variables: any other {{ ... }}, as in a
59
- page's template text, is sent as written. In a file, put it in double quotes: there its value is
60
- escaped as they read it (in single quotes, which escape nothing, a value with a ' is refused). The REPL
61
- puts it in only as it runs the command, and its pane and log show the command as written. A variable that
62
- is not set stops the command, or the file at that line, before it is sent. --no-vars sends
63
- {{ PW_NAME }} as it is. Variables need the command server (pw-repl serve), not the tmux pane.
60
+ send's options can also follow the command, as in pw-repl send open <url> -t 30s; after a -- before the
61
+ command, every word is the command's, -t 30s included.
64
62
 
65
- The browser must be running with --remote-debugging-port (default http://localhost:9222; set $PW_CDP_URL).
63
+ Exit status: 0 ok, 1 command error, 2 completion not confirmed, 64 usage or unreachable. -t is how long
64
+ send waits for the answer, in milliseconds or with s for seconds (default 20s: 20000); it does not
65
+ lengthen a command's own limits, such as the 5s an action waits for its element. Over the server, giving
66
+ up cancels the command: one still queued is never run, and one running is no longer waited for, though
67
+ what it began in the page may go on.
66
68
 
67
- --launch is the quick start instead: it starts a Chromium of the REPL's own (headless unless --headed, in
68
- a temporary profile, on a free port of its own, which pw-repl where names), prints the command it ran,
69
- and stops it with the REPL (Ctrl-C while it starts too); stop returns once that Chromium has exited.
70
- Flags after -- go to it, and only flags; a --user-data-dir=<dir> among them is used instead of the temporary profile, and
71
- kept. PW_CHROME picks which one. To set a browser up your own way, start it yourself and use PW_CDP_URL.
69
+ --file <file> runs each line of a file as a command, in order, as if typed at the REPL's prompt: quoted
70
+ as the prompt reads it (no shell), with a file it records, uploads or saves relative to this folder, as
71
+ on send's command line, and # comments and blank lines skipped. It stops at the first command that
72
+ fails, saying its line, with its exit status. -t is each command's own limit. Other clients' commands
73
+ can run between its lines, on their own tabs.
72
74
 
73
- In a tmux session named pw-repl, pw-repl send reaches pw-repl run without the server.`;
75
+ Variables: a {{ PW_NAME }} in a command, or in a line of the file, takes its value from send's own
76
+ environment (PW_NAME), passed along with the command, not in it, so a password stays out of the file and
77
+ out of the REPL's log: PW_PASSWORD=... pw-repl send fill '#password' "{{ PW_PASSWORD }}", or
78
+ PW_PASSWORD="$(cat <file>)" to give it from a file without typing it. Only PW_ names are variables; any
79
+ other {{ ... }} is sent as written. In a file, put one in double quotes (in single quotes, a value with
80
+ a ' is refused). One that is not set stops the command, or the file at that line, before it is sent;
81
+ --no-vars sends {{ PW_NAME }} as it is. Variables need the command server (pw-repl serve), not the tmux
82
+ pane.`;
74
83
 
75
84
  const REPL_HELP = `The REPL's own commands: help at the pw> prompt, or pw-repl send help here (no REPL needed).
76
85
 
@@ -99,10 +108,10 @@ function shellLine(words) {
99
108
  }
100
109
 
101
110
  // -e, -s, -c and -t, then the command words (unquoted words are one command), and send's options again
102
- // at the end, as an agent types them (send open <url> -t 30); after --, every word is the command.
111
+ // at the end, as an agent types them (send open <url> -t 30s); after --, every word is the command.
103
112
  function parseSendArgs(args, subcommand) {
104
113
  const allowCommand = subcommand === 'send';
105
- const options = { endpoint: null, session: null, client: null, timeout: 20, command: '', file: null };
114
+ const options = { endpoint: null, session: null, client: null, timeout: 20000, command: '', file: null };
106
115
  const given = new Set();
107
116
  const take = (arg, value, after) => {
108
117
  if (value === undefined) usage(`${arg} needs a value`, subcommand);
@@ -114,8 +123,7 @@ function parseSendArgs(args, subcommand) {
114
123
  if (arg === '-s') options.session = value;
115
124
  if (arg === '-c') options.client = value;
116
125
  if (arg === '-t') {
117
- if (!/^\d+$/.test(value) || Number(value) < 1) usage(`-t takes whole seconds, 1 or more (got ${value})`, subcommand);
118
- options.timeout = Number(value);
126
+ try { options.timeout = longDuration(value, '-t'); } catch (error) { usage(error.message, subcommand); }
119
127
  }
120
128
  };
121
129
  let i = 0;
@@ -229,12 +237,28 @@ function skill() {
229
237
 
230
238
  async function main() {
231
239
  let [subcommand, ...args] = process.argv.slice(2);
240
+ // -h after send, where, attach or stop is a request for help, never a command to send.
241
+ if (['send', 'where', 'attach', 'stop'].includes(subcommand) && /^(?:-h|--help)$/.test(args[0] || '')) {
242
+ if (subcommand !== 'send') return console.log(`${USAGE}\n\n${REPL_HELP}`);
243
+ const block = USAGE.split('\n\n').find(b => b.startsWith(' pw-repl send '));
244
+ return console.log(`Usage:\n${block}\n\n${SEND_HELP}`);
245
+ }
232
246
  // Bare pw-repl explains itself rather than connect to someone's browser.
233
247
  if (subcommand === undefined) return console.log(`${USAGE}\n\n${REPL_HELP}`);
234
248
  switch (subcommand) {
235
249
  case 'run':
236
250
  case 'serve': {
237
251
  const options = startOptions(args, subcommand === 'serve');
252
+ // A socket of one's own is often in a folder of its own (/tmp/<name>/repl.sock): made here, before
253
+ // the log, the pid file or the socket go in it, whichever way serve runs. Owner-only, as the socket is.
254
+ const socket = subcommand === 'serve' ? require('../lib/client').parseEndpoint(options.endpoint).socket : null;
255
+ if (socket) {
256
+ const folder = require('path').dirname(socket);
257
+ try { require('fs').mkdirSync(folder, { recursive: true, mode: 0o700 }); } catch (error) {
258
+ console.error(`pw-repl: cannot create ${folder} for the socket (${error.code === 'EEXIST' ? 'a file is there' : error.code || error.message})`);
259
+ process.exit(64);
260
+ }
261
+ }
238
262
  if (options.background) process.exit(await require('../lib/background').start(options));
239
263
  return require('../lib/start').start(options);
240
264
  }
@@ -249,6 +273,7 @@ async function main() {
249
273
  // falls through never: process.exit above
250
274
  case 'send': {
251
275
  const options = parseSendArgs(args, 'send');
276
+ if (!options.command && !options.file) usage('no command given', 'send');
252
277
  // help is fixed text, so it needs no browser: answer it here.
253
278
  const asksHelp = /^help(?:\s+(.*))?$/.exec(options.command);
254
279
  if (asksHelp) return help((asksHelp[1] || '').trim());
@@ -273,7 +298,7 @@ async function main() {
273
298
  case '--help':
274
299
  return console.log(`${USAGE}\n\n${REPL_HELP}`);
275
300
  default: {
276
- // An option before the subcommand (pw-repl -t 30 send ...): say where it goes, with the line fixed.
301
+ // An option before the subcommand (pw-repl -t 30s send ...): say where it goes, with the line fixed.
277
302
  if (subcommand.startsWith('-')) {
278
303
  const all = [subcommand, ...args];
279
304
  const named = all.find(word => SUBCOMMANDS.includes(word));
package/lib/background.js CHANGED
@@ -5,7 +5,7 @@ const path = require('path');
5
5
  const readline = require('readline');
6
6
  const { spawn } = require('child_process');
7
7
  const client = require('./client');
8
- const { STOPPED_RECORDING, ENDED_UNSEEN } = require('./util');
8
+ const { STOPPED_RECORDING, ENDED_UNSEEN, FILES_LEFT } = require('./util');
9
9
 
10
10
  const BIN = path.join(__dirname, '..', 'bin', 'pw-repl.js');
11
11
  const START_TIMEOUT = 20000;
@@ -162,7 +162,7 @@ async function stop(options) {
162
162
  fs.closeSync(fd);
163
163
  logged = buffer.toString('utf8');
164
164
  } catch {}
165
- for (const line of logged.split('\n')) if (line.startsWith(STOPPED_RECORDING) || line.startsWith(ENDED_UNSEEN)) console.log(line);
165
+ for (const line of logged.split('\n')) if (line.startsWith(STOPPED_RECORDING) || line.startsWith(ENDED_UNSEEN) || line.startsWith(FILES_LEFT)) console.log(line);
166
166
  return 0;
167
167
  }
168
168
 
package/lib/capture.js CHANGED
@@ -2,7 +2,7 @@
2
2
  const { state, onShutdown } = require('./state');
3
3
  const out = require('./output');
4
4
  const { printOutput } = out;
5
- const { numbers, MAX_EVENT_TEXT, NO_TAB, clock, hints } = require('./util');
5
+ const { numbers, MAX_EVENT_TEXT, NO_TAB, clock, hints, TIME, longDuration, durationText } = require('./util');
6
6
  const { consoleText, requestEntries } = require('./requestlog');
7
7
 
8
8
  const MAX_CAPTURE_EVENTS = 10000;
@@ -17,16 +17,15 @@ function currentCapture() {
17
17
  }
18
18
 
19
19
  // Records the selected tab's requests and console messages together, in time
20
- // order, until capture off, or for a set number of seconds.
20
+ // order, until capture off, or for a set time.
21
21
  async function startCapture(tokens) {
22
22
  if (cap) { out.log('Already capturing; capture off stops it.'); return; }
23
23
  if (!state.page || state.page.isClosed()) throw new Error(NO_TAB);
24
- const usage = 'Usage: capture on [requests|console] [seconds] (seconds from 1 to 3600)';
24
+ const usage = 'Usage: capture on [requests|console] [time] (ms, or 30s)';
25
25
  const kinds = tokens.filter(t => t === 'requests' || t === 'console');
26
- const numbers = tokens.filter(t => /^\d+$/.test(t));
27
- if (kinds.length > 1 || numbers.length > 1 || kinds.length + numbers.length !== tokens.length) throw new Error(usage);
28
- const seconds = numbers.length ? Number(numbers[0]) : null;
29
- if (seconds !== null && (seconds < 1 || seconds > 3600)) throw new Error(usage);
26
+ const times = tokens.filter(t => TIME.test(t));
27
+ if (kinds.length > 1 || times.length > 1 || kinds.length + times.length !== tokens.length) throw new Error(usage);
28
+ const ms = times.length ? longDuration(times[0], 'capture on\'s time') : null;
30
29
  const label = kinds[0] || 'requests and console';
31
30
  const startedAt = Date.now();
32
31
  const events = [];
@@ -61,11 +60,11 @@ async function startCapture(tokens) {
61
60
  get shortened() { return shortened; },
62
61
  };
63
62
  cap = capture;
64
- if (seconds) {
65
- out.log(`Capturing ${label} for ${seconds}s...`);
63
+ if (ms) {
64
+ out.log(`Capturing ${label} for ${durationText(ms)}...`);
66
65
  // Also ends early if the capture does, e.g. because its tab closed.
67
66
  await new Promise(resolve => {
68
- const timer = setTimeout(resolve, seconds * 1000);
67
+ const timer = setTimeout(resolve, ms);
69
68
  capture.wake = () => { clearTimeout(timer); resolve(); };
70
69
  });
71
70
  if (cap === capture) stopCapture();
@@ -120,7 +119,7 @@ const commands = {
120
119
  const [sub, ...rest] = (args || '').trim().split(/\s+/).filter(Boolean);
121
120
  if (sub === 'on') return startCapture(rest);
122
121
  if (sub === 'off' && !rest.length) return stopCapture(all);
123
- if (sub) throw new Error('Usage: capture on [requests|console] [seconds] | capture off');
122
+ if (sub) throw new Error('Usage: capture on [requests|console] [time] | capture off');
124
123
  if (cap) {
125
124
  out.log(`Capturing ${cap.label} on ${cap.page.url()} since ${clock(cap.startedAt)} (${cap.events.length} so far)`);
126
125
  out.log(hints([['capture off', 'stop and print what it recorded']]));
@@ -132,7 +131,7 @@ const commands = {
132
131
  } else {
133
132
  out.log('Not capturing.');
134
133
  }
135
- out.log(hints([['capture on [requests|console] [seconds]', 'record the selected tab\'s requests and console messages in time order']]));
134
+ out.log(hints([['capture on [requests|console] [time]', 'record the selected tab\'s requests and console messages in time order']]));
136
135
  },
137
136
  };
138
137
 
package/lib/cli-names.js CHANGED
@@ -13,9 +13,9 @@ 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
- close: [() => 'close', 'modes off --mine, and tabs stay open'],
18
+ close: [() => 'close', 'modes off --mine; tabs stay open; lists the files you wrote'],
19
19
  'go-back': [() => 'back', 'back'],
20
20
  'go-forward': [() => 'forward', 'forward'],
21
21
  find: [a => `snapshot --grep ${a}`, 'snapshot --grep <text>'],
@@ -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}`] : [])],
package/lib/commands.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // The REPL's commands, gathered from each facility's module, and what the prompt and the runner
2
2
  // need besides: playwright-cli's own commands, the hooks every page gets, and tab completion.
3
3
  const { state, shutdown } = require('./state');
4
+ const { ownFilesText } = require('./written');
4
5
  const out = require('./output');
5
6
  const HELP = require('./help');
6
7
  const { tabState } = require('./tabstate');
@@ -80,6 +81,8 @@ const cliCommands = {
80
81
  await allModesOff(true);
81
82
  const own = state.browser.contexts().flatMap(c => c.pages()).filter(p => tabState(p).owner === state.client);
82
83
  if (own.length) out.log(`Tabs you opened are still open: ${own.map(p => p.url()).join(', ')}; tab close <url-part> closes one, or select it and tab close.`);
84
+ const files = ownFilesText(state.client);
85
+ if (files) out.log(files);
83
86
  },
84
87
  };
85
88