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 +13 -7
- package/README.md +76 -74
- package/bin/pw-repl.js +65 -40
- package/lib/background.js +2 -2
- package/lib/capture.js +11 -12
- package/lib/cli-names.js +4 -4
- package/lib/commands.js +3 -0
- package/lib/elements.js +115 -26
- package/lib/emulation.js +2 -1
- package/lib/help.js +166 -73
- package/lib/inspect.js +103 -41
- package/lib/launch.js +3 -1
- package/lib/output.js +10 -3
- package/lib/profile.js +16 -14
- package/lib/record.js +18 -14
- package/lib/routes.js +8 -9
- package/lib/runner.js +39 -9
- package/lib/send.js +28 -6
- package/lib/server.js +4 -1
- package/lib/start.js +7 -2
- package/lib/state.js +26 -9
- package/lib/syntax.js +11 -5
- package/lib/toast.js +1 -6
- package/lib/util.js +25 -1
- package/lib/watch.js +4 -2
- package/lib/written.js +67 -0
- package/package.json +1 -1
- package/skill/SKILL.md +39 -44
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,
|
|
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 (
|
|
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.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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,
|
|
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`; 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
|
|
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
|
|
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 <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`,
|
|
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
|
@@ -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
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
42
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
the
|
|
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
|
|
49
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
--
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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] [
|
|
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
|
|
27
|
-
if (kinds.length > 1 ||
|
|
28
|
-
const
|
|
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 (
|
|
65
|
-
out.log(`Capturing ${label} for ${
|
|
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,
|
|
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] [
|
|
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] [
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|