pw-repl 0.4.2 → 0.6.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 +34 -8
- package/README.md +90 -101
- package/bin/pw-repl.js +12 -2
- package/lib/background.js +4 -3
- package/lib/capture.js +139 -0
- package/lib/cdp.js +31 -0
- package/lib/cli-names.js +1 -3
- package/lib/client.js +6 -1
- package/lib/commands.js +34 -2285
- package/lib/dialogs.js +58 -0
- package/lib/elements.js +360 -0
- package/lib/emulation.js +180 -0
- package/lib/help.js +298 -39
- package/lib/inspect.js +460 -0
- package/lib/modes.js +115 -0
- package/lib/network.js +59 -0
- package/lib/output.js +12 -2
- package/lib/requestlog.js +367 -0
- package/lib/routes.js +179 -0
- package/lib/runner.js +7 -3
- package/lib/send.js +9 -0
- package/lib/server.js +9 -2
- package/lib/skill.js +29 -0
- package/lib/start.js +10 -5
- package/lib/state.js +5 -0
- package/lib/tabs.js +237 -0
- package/lib/tabstate.js +35 -0
- package/lib/util.js +49 -0
- package/lib/watch.js +530 -0
- package/package.json +1 -1
- package/skill/SKILL.md +35 -12
package/AGENTS.md
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
# Agent Instructions — playwright-repl
|
|
2
2
|
|
|
3
|
-
How to use the REPL is in
|
|
4
|
-
this clone, `pw-repl` there means `bin/pw-repl.js` (from the
|
|
5
|
-
installed globally.
|
|
3
|
+
How to use the REPL is in the skill `bin/pw-repl.js skill` prints (from `skill/SKILL.md`, stamped with
|
|
4
|
+
its version and hash). Read it first. In this clone, `pw-repl` there means `bin/pw-repl.js` (from the
|
|
5
|
+
folder this file is in) when it is not installed globally.
|
|
6
6
|
|
|
7
7
|
## Contributing
|
|
8
8
|
|
|
9
9
|
If you hit a limitation or write a workaround, consider adding the capability to the REPL instead.
|
|
10
10
|
|
|
11
|
-
- A command is a function in the `commands` object
|
|
12
|
-
under one topic.
|
|
11
|
+
- A command is a function in the `commands` object of the module for what it works on, plus an entry
|
|
12
|
+
in `lib/help.js` under one topic; `lib/commands.js` gathers every module's commands. A command that
|
|
13
|
+
needs no tab, only reads, or takes `--all` is also listed in `lib/runner.js`'s `NO_TAB_NEEDED`,
|
|
14
|
+
`READ_ONLY` or `INSPECTION`, and one that takes fixed words in `complete` (`lib/commands.js`).
|
|
15
|
+
- The modules, one per facility, each with its commands: `lib/tabs.js` (tabs, navigating),
|
|
16
|
+
`lib/elements.js` (choosing a match; the interact and mouse commands), `lib/inspect.js` (reading the
|
|
17
|
+
page, screenshots, eval, CDP, waits), `lib/watch.js`, `lib/requestlog.js` (requests, bodies, console),
|
|
18
|
+
`lib/routes.js`, `lib/network.js`, `lib/emulation.js`, `lib/capture.js`, `lib/dialogs.js` and
|
|
19
|
+
`lib/modes.js`. Beneath them, `lib/tabstate.js` is the one record kept per tab, `lib/cdp.js` the CDP
|
|
20
|
+
session kept per tab, and `lib/util.js` small shared helpers. No facility requires `lib/modes.js` or
|
|
21
|
+
`lib/commands.js`, and a unit test fails on a require cycle, which CommonJS would not report.
|
|
13
22
|
- `bin/pw-repl.js` is the command line; `lib/start.js` connects and runs the prompt; `lib/runner.js`
|
|
14
23
|
runs commands one at a time (`quit` and `dialog` skip its queue); `lib/server.js` is the command
|
|
15
24
|
server; `lib/send.js` and `lib/client.js` are `send` and `where`; `lib/background.js` is
|
|
16
25
|
`serve --background`, `attach` and `stop`; `lib/launch.js` is `--launch`; `lib/syntax.js` is how a
|
|
17
26
|
command line's words are read, shared with `send`; `lib/cli-names.js` maps playwright-cli's command
|
|
18
27
|
names to these; `lib/state.js` holds the session state, with each client's selected tab;
|
|
19
|
-
`lib/output.js` routes all output so the server can return it.
|
|
28
|
+
`lib/output.js` routes all output so the server can return it; `lib/skill.js` stamps the skill
|
|
29
|
+
`pw-repl skill` prints, and gives `where` its hash.
|
|
20
30
|
- Match playwright-cli (Microsoft's CLI for agents) where it is reasonable: agents are likelier to be
|
|
21
31
|
trained on it. A new command, option or output that does what one of its does takes its name and form,
|
|
22
32
|
or accepts them too (`lib/cli-names.js`, `help playwright-cli`). Differ where sharing a browser with a
|
|
@@ -26,11 +36,27 @@ If you hit a limitation or write a workaround, consider adding the capability to
|
|
|
26
36
|
(`test/harness.js`), and the real REPL with its server. Nothing is mocked, and the shared browser is
|
|
27
37
|
never touched. It finds Chromium through `PW_TEST_CHROME`, or where `--launch` looks (Playwright's
|
|
28
38
|
browsers, then the `PATH`), and skips the browser tests if there is none. Add a test with each new
|
|
29
|
-
command or behaviour.
|
|
39
|
+
command or behaviour. In `test/repl.test.js` each test starts in a fresh tab of its own, and every mode
|
|
40
|
+
is turned off after it; a test whose REPL quits, times out or is killed goes in
|
|
41
|
+
`test/repl-alone.test.js`, with a Chromium of its own.
|
|
42
|
+
- Help entries (`lib/help.js`) are plain paragraphs: `wrap()` lays each out at 104 columns, and keeps as
|
|
43
|
+
written a paragraph with a bullet, a table or an indented example. `test/fixtures` holds the rendered
|
|
44
|
+
help; after an intended change to it, save it again (the command is in `test/units.test.js`).
|
|
45
|
+
- Time each full run, and put the result in the body of the commit it tests (`npm test: 151 tests, 79s`).
|
|
46
|
+
Compare with the last commit that has one (`git log --grep='npm test:'`): a run that grew by more than
|
|
47
|
+
a few seconds means a slow test crept in, usually one waiting out a real timeout (5s for a click).
|
|
48
|
+
Find it (each test prints its time) and make it faster, or say why it can't be, before committing.
|
|
49
|
+
- Don't race the clock in a test: a page that changes on a timer, or a command timed against a budget,
|
|
50
|
+
fails on a busy machine. Have the page change when the test asks (eval), or measure the thing itself
|
|
51
|
+
(frames drawn, not clicks timed). A click waits for its element to hold still for two frames.
|
|
52
|
+
- A test that times out in a run much slower than usual (the machine is busy) may be load, not a bug:
|
|
53
|
+
run it again, alone. If it fails alone too, find out why before going on.
|
|
30
54
|
- For anything the tests can't reach, exercise the change in a running REPL of your own: on a socket of
|
|
31
55
|
your own, with `--launch` unless the change needs the user's browser, and then in a tab of your own.
|
|
32
56
|
- `skill/SKILL.md` is how agents learn to use the REPL: keep it in step with a change to how it is
|
|
33
|
-
used, and leave its Custom rules section empty.
|
|
57
|
+
used, and leave its Custom rules section empty. Keep its `<!-- pw-repl skill stamp -->` line: `pw-repl
|
|
58
|
+
skill` replaces it with the version and a hash of the skill's text, which `where` reports, so agents
|
|
59
|
+
can tell a saved copy is out of date.
|
|
34
60
|
|
|
35
61
|
## Waves
|
|
36
62
|
|
package/README.md
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
# playwright-repl
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
(requests, console, and each step a person takes), fakes, patches or slows responses, and emulates phones,
|
|
7
|
-
dark mode, locales and timezones.
|
|
3
|
+
A command REPL for a Chromium you are already using, shared by you and your agents. It connects over CDP
|
|
4
|
+
through Playwright; people click in the browser and type at the prompt, agents send the same commands,
|
|
5
|
+
and every command shows in the REPL, so each sees what the other does.
|
|
8
6
|
|
|
9
7
|
## Why use it
|
|
10
8
|
|
|
@@ -19,83 +17,71 @@ For working on a web page together with an agent, in the browser you already use
|
|
|
19
17
|
- **Breaking things on purpose.** Fake, patch, delay or fail responses, cut or slow the network, emulate a
|
|
20
18
|
phone, a locale or a timezone, all per tab, so your other tabs are left alone.
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
[playwright-cli](https://github.com/microsoft/playwright-cli) is Microsoft's command line for agents. It
|
|
25
|
-
gives each agent a browser session of its own; pw-repl shares one browser between a person and agents.
|
|
26
|
-
Its command names and most of their options work here too (`help playwright-cli` lists them), and where
|
|
27
|
-
pw-repl differs, it is because someone else may be using the browser:
|
|
20
|
+
Coming from Microsoft's playwright-cli? See [How it differs](#how-it-differs-from-playwright-cli).
|
|
28
21
|
|
|
29
|
-
|
|
30
|
-
- Routes, network and emulation apply to one tab, not the whole browser.
|
|
31
|
-
- File pickers are not caught, since they may be the person's: `upload` names the input instead.
|
|
32
|
-
- There is no snapshot after every command, since a person reads the REPL too; `snapshot` shows one.
|
|
33
|
-
- Instead of `-s=<session>`, an agent is a client of its own (`send -c <name>`).
|
|
34
|
-
- `watch` records what a person does as steps with their requests, not as generated code.
|
|
22
|
+
## Quick start
|
|
35
23
|
|
|
36
|
-
|
|
37
|
-
playwright-
|
|
38
|
-
|
|
39
|
-
## Getting started
|
|
40
|
-
|
|
41
|
-
### Prerequisites
|
|
42
|
-
|
|
43
|
-
- Node.js 20 or newer (`npm test` needs 21 or newer).
|
|
44
|
-
- A Chromium-based browser (Chrome, Chromium, ungoogled-chromium, ...).
|
|
45
|
-
- Optional: tmux, to use `pw-repl send` without the command server.
|
|
46
|
-
|
|
47
|
-
### 1. Start the browser with remote debugging
|
|
24
|
+
Needs Node.js 20 or newer and a Chromium-based browser (Chrome, Chromium, ...). With none installed,
|
|
25
|
+
`npx playwright-core install chromium` downloads Playwright's.
|
|
48
26
|
|
|
49
27
|
```bash
|
|
50
|
-
|
|
51
|
-
# or: chromium --remote-debugging-port=9222 --user-data-dir="$HOME/.config/chromium-debug"
|
|
28
|
+
npx pw-repl@latest run --launch https://example.com
|
|
52
29
|
```
|
|
53
30
|
|
|
54
|
-
|
|
55
|
-
|
|
31
|
+
`--launch` starts a private headless Chromium for the REPL, and stops it with the REPL; it prints the
|
|
32
|
+
command it ran. `--headed` shows that browser, to click in it yourself. The page opens in a new tab,
|
|
33
|
+
and a `pw>` prompt waits:
|
|
56
34
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
35
|
+
```text
|
|
36
|
+
pw> help # common tasks and topics; help <topic>, help <command>, help --all
|
|
37
|
+
pw> snapshot # the page by role and name, with [ref=eN] labels
|
|
38
|
+
pw> click e3 # click by snapshot ref (or any Playwright selector)
|
|
39
|
+
pw> requests # requests the tab made
|
|
40
|
+
pw> quit
|
|
41
|
+
```
|
|
61
42
|
|
|
62
|
-
|
|
43
|
+
Commands act on the selected tab (`tab` lists the tabs, with `*` on the selected one).
|
|
63
44
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
45
|
+
Without installing, use `npx pw-repl@latest <command>` in the same form every time, so each command runs
|
|
46
|
+
the same version. Installed: `npm install -g pw-repl`, then `pw-repl <command>`. From a clone:
|
|
47
|
+
`npm install`, then `bin/pw-repl.js <command>` (or `npm link` to get `pw-repl`).
|
|
67
48
|
|
|
68
|
-
|
|
69
|
-
the REPL never answers one on its own. Pages start at about 800x600; `viewport 1280x800` sets another
|
|
70
|
-
size.
|
|
49
|
+
## Use your own browser
|
|
71
50
|
|
|
72
|
-
|
|
51
|
+
Start it with remote debugging, then run the REPL without `--launch`:
|
|
73
52
|
|
|
74
53
|
```bash
|
|
75
|
-
|
|
54
|
+
chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.config/chrome-debug"
|
|
76
55
|
pw-repl run # connects to localhost:9222; pw-repl on its own shows the usage
|
|
77
56
|
```
|
|
78
57
|
|
|
79
|
-
|
|
80
|
-
|
|
58
|
+
Use a separate `--user-data-dir`: recent Chrome versions do not open the debugging port on the default
|
|
59
|
+
profile. Check it is up with `curl http://localhost:9222/json/version`. `PW_CDP_URL=http://host:9222`
|
|
60
|
+
reaches a browser elsewhere.
|
|
81
61
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
62
|
+
Headless works the same way (`chrome --headless=new --remote-debugging-port=9222
|
|
63
|
+
--user-data-dir=/tmp/chrome-headless`). Nobody answers a headless browser's dialogs: `dialog
|
|
64
|
+
accept|dismiss` does. Pages start at about 800x600; `viewport 1280x800` changes it.
|
|
65
|
+
|
|
66
|
+
**Browser on the host, REPL in a container:** with host networking, `localhost:9222` reaches the host's
|
|
67
|
+
browser directly. Otherwise set `PW_CDP_URL` to the host's address as seen from the container (for example
|
|
68
|
+
the Docker bridge gateway, `ip route | awk '/default/ {print $3}'`).
|
|
86
69
|
|
|
87
|
-
|
|
70
|
+
## Ways to run it
|
|
88
71
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
pw
|
|
92
|
-
pw
|
|
93
|
-
pw
|
|
94
|
-
pw> requests # requests the tab made
|
|
95
|
-
pw> tab close
|
|
96
|
-
```
|
|
72
|
+
| Command | What you get |
|
|
73
|
+
| ---------------------------- | --------------------------------------------------------------------------------------------- |
|
|
74
|
+
| `pw-repl run` | a prompt in this terminal; `send` reaches it only through a tmux session named `playwright-repl` |
|
|
75
|
+
| `pw-repl serve` | a prompt (`pw[serve]>`), plus a command server on `/tmp/playwright-repl.sock`; `send` needs no tmux |
|
|
76
|
+
| `pw-repl serve --background` | no terminal: `attach` to use it, `stop` to stop it, `where` to find it; output in `/tmp/playwright-repl.log` |
|
|
97
77
|
|
|
98
|
-
|
|
78
|
+
- For `send` to reach `run`, start it in tmux: `tmux new -s playwright-repl`, then `pw-repl run` there.
|
|
79
|
+
- A URL after the command opens in a new tab: `pw-repl run http://localhost:3000`.
|
|
80
|
+
- `--launch` works with each of them; flags after `--` go to that Chromium.
|
|
81
|
+
- `serve` takes a socket of its own (`pw-repl serve --background /tmp/mine.sock`; then `-e /tmp/mine.sock`
|
|
82
|
+
for `send`, `attach`, `stop` and `where`), or `serve <port>` for TCP on 127.0.0.1.
|
|
83
|
+
- A background REPL cannot be brought back like a Ctrl-Z job; `attach` is how you get back to it, from
|
|
84
|
+
any terminal.
|
|
99
85
|
|
|
100
86
|
## Common tasks
|
|
101
87
|
|
|
@@ -129,31 +115,42 @@ lists them for every tab, and `modes off` turns them all off.
|
|
|
129
115
|
`tab`, `watch`, `capture`, `route`, `network`, `emulate` and `modes` on their own show their state and what
|
|
130
116
|
you can run next.
|
|
131
117
|
|
|
132
|
-
##
|
|
118
|
+
## How it differs from playwright-cli
|
|
133
119
|
|
|
134
|
-
|
|
135
|
-
pw-repl
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
120
|
+
[playwright-cli](https://github.com/microsoft/playwright-cli) is Microsoft's command line for agents. It
|
|
121
|
+
gives each agent a browser session of its own; pw-repl shares one browser between a person and agents.
|
|
122
|
+
Its command names and most of their options work here too (`help playwright-cli` lists them), and where
|
|
123
|
+
pw-repl differs, it is because someone else may be using the browser:
|
|
124
|
+
|
|
125
|
+
- `open` and `close` open a tab and turn off your modes; they never start, close or wipe a browser.
|
|
126
|
+
- Routes, network and emulation apply to one tab, not the whole browser.
|
|
127
|
+
- File pickers are not caught, since they may be the person's: `upload` names the input instead.
|
|
128
|
+
- There is no snapshot after every command, since a person reads the REPL too; `snapshot` shows one.
|
|
129
|
+
- Instead of `-s=<session>`, an agent is a client of its own (`send -c <name>`).
|
|
130
|
+
- `watch` records what a person does as steps with their requests, not as generated code.
|
|
131
|
+
|
|
132
|
+
For a browser of its own per agent, isolated sessions, traces, video, PDFs or generated test code,
|
|
133
|
+
playwright-cli is the better fit. pw-repl does not record video; for one of the shared browser as you
|
|
134
|
+
see it, use a screen recorder (`ffmpeg -f x11grab`, macOS `screencapture -v`, OBS).
|
|
139
135
|
|
|
140
|
-
|
|
136
|
+
## From scripts and agents
|
|
137
|
+
|
|
138
|
+
### A skill for agents
|
|
139
|
+
|
|
140
|
+
`pw-repl skill` prints an agent skill (`SKILL.md`) that teaches an agent to use the REPL: how to start
|
|
141
|
+
it, send commands, and share the browser with a person. Save it as `pw-repl/SKILL.md` in the folder
|
|
142
|
+
your agent reads skills from:
|
|
141
143
|
|
|
142
144
|
```bash
|
|
143
|
-
|
|
144
|
-
pw-repl
|
|
145
|
-
pw-repl stop # stop it
|
|
146
|
-
pw-repl where # is one running, and where
|
|
145
|
+
mkdir -p <skills folder>/pw-repl
|
|
146
|
+
npx pw-repl@latest skill > <skills folder>/pw-repl/SKILL.md
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
It is stamped with the version it came from and a hash of its text, and `pw-repl where` names the skill
|
|
150
|
+
of the pw-repl being run, so an agent can tell when its saved copy is out of date and offer to update it
|
|
151
|
+
(keeping its Custom rules section). Working on the REPL itself: see `AGENTS.md`.
|
|
151
152
|
|
|
152
|
-
|
|
153
|
-
browser directly. Otherwise set `PW_CDP_URL` to the host's address as seen from the container (for example
|
|
154
|
-
the Docker bridge gateway, `ip route | awk '/default/ {print $3}'`).
|
|
155
|
-
|
|
156
|
-
## From scripts and agents
|
|
153
|
+
### send, clients and where
|
|
157
154
|
|
|
158
155
|
```bash
|
|
159
156
|
pw-repl send info
|
|
@@ -162,34 +159,26 @@ pw-repl where # which REPL send would reach
|
|
|
162
159
|
```
|
|
163
160
|
|
|
164
161
|
`pw-repl send` sends one command and prints its result, through the server when it is running and through the
|
|
165
|
-
`playwright-repl` tmux session otherwise.
|
|
166
|
-
|
|
167
|
-
```bash
|
|
168
|
-
curl --unix-socket /tmp/playwright-repl.sock -H 'Content-Type: application/json' \
|
|
169
|
-
-d '{"command": "info"}' http://localhost/run
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
It returns `{"status": "ok" | "error", "output": "..."}`, plus `"unconfirmed": true` when the command may or
|
|
173
|
-
may not have done what it was sent to do (it timed out, or the REPL quit while it ran and it was not
|
|
174
|
-
read-only). It is off unless started with `pw-repl serve`.
|
|
175
|
-
`pw-repl serve <port>` serves TCP on 127.0.0.1 instead; other addresses are refused.
|
|
162
|
+
`playwright-repl` tmux session otherwise.
|
|
176
163
|
|
|
177
164
|
Several agents can share one REPL: `pw-repl send -c <name>` (or `PW_CLIENT=<name>`) sends as a client with a
|
|
178
165
|
selected tab of its own, shown in the pane as `[server:<name>]`. `modes` says which client turned each mode on,
|
|
179
166
|
and `modes off --mine` turns off only the sender's own.
|
|
180
167
|
|
|
181
|
-
###
|
|
168
|
+
### The HTTP protocol
|
|
182
169
|
|
|
183
|
-
`pw-repl
|
|
184
|
-
it, send commands, and share the browser with a person. Save it as `pw-repl/SKILL.md` in the folder
|
|
185
|
-
your agent reads skills from:
|
|
170
|
+
With `pw-repl serve`, the server speaks HTTP with JSON, for clients of your own:
|
|
186
171
|
|
|
187
172
|
```bash
|
|
188
|
-
|
|
189
|
-
|
|
173
|
+
curl --unix-socket /tmp/playwright-repl.sock -H 'Content-Type: application/json' \
|
|
174
|
+
-d '{"command": "info"}' http://localhost/run
|
|
190
175
|
```
|
|
191
176
|
|
|
192
|
-
|
|
177
|
+
It returns `{"status": "ok" | "error", "output": "..."}`, plus `"unconfirmed": true` when the command may or
|
|
178
|
+
may not have done what it was sent to do (it timed out, or the REPL quit while it ran and it was not
|
|
179
|
+
read-only). While the REPL starts, `/run` answers 503 with `"starting": true`: retry. `GET /health` says
|
|
180
|
+
`"status": "starting"` until it serves, then `"ok"`. `pw-repl serve <port>` serves TCP on 127.0.0.1
|
|
181
|
+
instead; other addresses are refused.
|
|
193
182
|
|
|
194
183
|
## Tests
|
|
195
184
|
|
|
@@ -197,6 +186,6 @@ The same text is in `skill/SKILL.md`. Working on the REPL itself: see `AGENTS.md
|
|
|
197
186
|
npm test
|
|
198
187
|
```
|
|
199
188
|
|
|
200
|
-
Runs against a private headless Chromium it starts itself (`PW_TEST_CHROME`, or
|
|
201
|
-
use: Playwright's, e.g. from `npx playwright-core install chromium`, then one on
|
|
202
|
-
test site; the browser tests are skipped when no Chromium is found.
|
|
189
|
+
Needs Node.js 21 or newer. Runs against a private headless Chromium it starts itself (`PW_TEST_CHROME`, or
|
|
190
|
+
the one `--launch` would use: Playwright's, e.g. from `npx playwright-core install chromium`, then one on
|
|
191
|
+
the `PATH`) and a local test site; the browser tests are skipped when no Chromium is found.
|
package/bin/pw-repl.js
CHANGED
|
@@ -84,6 +84,16 @@ function parseSendArgs(args, allowCommand) {
|
|
|
84
84
|
}
|
|
85
85
|
} else if (arg === '--') { i++; break; } else break;
|
|
86
86
|
}
|
|
87
|
+
// An address that is refused (not loopback) is a usage error, said once, not a stack trace.
|
|
88
|
+
const endpoint = options.endpoint || process.env.PW_ENDPOINT;
|
|
89
|
+
if (endpoint) {
|
|
90
|
+
try {
|
|
91
|
+
require('../lib/client').parseEndpoint(endpoint);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error(`pw-repl: ${error.message}`);
|
|
94
|
+
process.exit(64);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
87
97
|
const words = args.slice(i);
|
|
88
98
|
if (!allowCommand && words.length) usage();
|
|
89
99
|
// For a command that reads a quoted selector (fill "text=Your name" Ada), a
|
|
@@ -139,9 +149,9 @@ function help(topic) {
|
|
|
139
149
|
console.log(text);
|
|
140
150
|
}
|
|
141
151
|
|
|
142
|
-
// Printed
|
|
152
|
+
// Printed to be saved as a skill, stamped with where it came from; the hint goes to the terminal only.
|
|
143
153
|
function skill() {
|
|
144
|
-
process.stdout.write(require('
|
|
154
|
+
process.stdout.write(require('../lib/skill').stamped());
|
|
145
155
|
if (process.stdout.isTTY) console.error('\nSave it as pw-repl/SKILL.md in the folder your agent reads skills from:\npw-repl skill > <skills folder>/pw-repl/SKILL.md');
|
|
146
156
|
}
|
|
147
157
|
|
package/lib/background.js
CHANGED
|
@@ -99,7 +99,7 @@ async function start(options) {
|
|
|
99
99
|
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
100
100
|
for (const signal of signals) process.on(signal, interrupt);
|
|
101
101
|
const deadline = Date.now() + (options.launch ? LAUNCH_START_TIMEOUT : START_TIMEOUT);
|
|
102
|
-
while (!exited && !interrupted && Date.now() < deadline && !(await client.
|
|
102
|
+
while (!exited && !interrupted && Date.now() < deadline && !(await client.ready(endpoint, 1000) && runningPid(pidFile))) {
|
|
103
103
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
104
104
|
}
|
|
105
105
|
for (const signal of signals) process.removeListener(signal, interrupt);
|
|
@@ -149,8 +149,9 @@ async function attach(options) {
|
|
|
149
149
|
const { pidFile, logFile } = filesFor(endpoint);
|
|
150
150
|
const pid = runningPid(pidFile);
|
|
151
151
|
if (!pid) {
|
|
152
|
-
const
|
|
153
|
-
return fail(
|
|
152
|
+
const info = await client.healthInfo(endpoint, 2000);
|
|
153
|
+
if (info?.status === 'starting') return fail(`the REPL on ${name} is starting; retry shortly`);
|
|
154
|
+
return fail(info ? `the REPL on ${name} runs in a terminal, not in the background; use it there` : `no background REPL is serving on ${name}; pw-repl serve --background starts one`);
|
|
154
155
|
}
|
|
155
156
|
const fd = fs.openSync(logFile, 'r');
|
|
156
157
|
const backlog = tail(logFile, ATTACH_BACKLOG);
|
package/lib/capture.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Capturing a tab's requests and console messages together, from now until capture off.
|
|
2
|
+
const { state, onShutdown } = require('./state');
|
|
3
|
+
const out = require('./output');
|
|
4
|
+
const { printOutput } = out;
|
|
5
|
+
const { numbers, MAX_EVENT_TEXT, NO_TAB, clock, hints } = require('./util');
|
|
6
|
+
const { consoleText, requestEntries } = require('./requestlog');
|
|
7
|
+
|
|
8
|
+
const MAX_CAPTURE_EVENTS = 10000;
|
|
9
|
+
const MAX_CAPTURE_TEXT = 400000;
|
|
10
|
+
|
|
11
|
+
let cap = null;
|
|
12
|
+
let lastCapture = null;
|
|
13
|
+
|
|
14
|
+
// The capture running now, or null, for the modes it counts as.
|
|
15
|
+
function currentCapture() {
|
|
16
|
+
return cap;
|
|
17
|
+
}
|
|
18
|
+
|
|
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.
|
|
21
|
+
async function startCapture(tokens) {
|
|
22
|
+
if (cap) { out.log('Already capturing; capture off stops it.'); return; }
|
|
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)';
|
|
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);
|
|
30
|
+
const label = kinds[0] || 'requests and console';
|
|
31
|
+
const startedAt = Date.now();
|
|
32
|
+
const events = [];
|
|
33
|
+
const handlers = [];
|
|
34
|
+
let dropped = 0;
|
|
35
|
+
let shortened = 0;
|
|
36
|
+
let bytes = 0;
|
|
37
|
+
const record = event => {
|
|
38
|
+
let text = String(event.text);
|
|
39
|
+
if (text.length > MAX_EVENT_TEXT) { text = `${text.slice(0, MAX_EVENT_TEXT)}…`; shortened += 1; }
|
|
40
|
+
const size = text.length;
|
|
41
|
+
if (events.length >= MAX_CAPTURE_EVENTS || bytes + size > MAX_CAPTURE_TEXT) { dropped += 1; return; }
|
|
42
|
+
bytes += size;
|
|
43
|
+
events.push({ ...event, text });
|
|
44
|
+
};
|
|
45
|
+
const page = state.page;
|
|
46
|
+
const listen = (event, handler) => { page.on(event, handler); handlers.push([event, handler]); };
|
|
47
|
+
if (label !== 'console') listen('request', req => record({ t: Date.now(), tag: 'request', req, text: `${req.method()} ${req.url()}` }));
|
|
48
|
+
if (label !== 'requests') {
|
|
49
|
+
listen('console', msg => record({ t: Date.now(), tag: msg.type(), text: consoleText(msg) }));
|
|
50
|
+
listen('pageerror', error => record({ t: Date.now(), tag: 'pageerror', text: error.stack || error.message }));
|
|
51
|
+
}
|
|
52
|
+
// A capture must not outlive its tab: nothing could see or stop it.
|
|
53
|
+
listen('close', () => {
|
|
54
|
+
if (cap?.page !== page) return;
|
|
55
|
+
endCapture();
|
|
56
|
+
out.notice('The captured tab closed, which ended the capture; capture shows what it recorded.', page);
|
|
57
|
+
});
|
|
58
|
+
const capture = {
|
|
59
|
+
page, label, startedAt, events, handlers, wake: null, owner: state.client,
|
|
60
|
+
get dropped() { return dropped; },
|
|
61
|
+
get shortened() { return shortened; },
|
|
62
|
+
};
|
|
63
|
+
cap = capture;
|
|
64
|
+
if (seconds) {
|
|
65
|
+
out.log(`Capturing ${label} for ${seconds}s...`);
|
|
66
|
+
// Also ends early if the capture does, e.g. because its tab closed.
|
|
67
|
+
await new Promise(resolve => {
|
|
68
|
+
const timer = setTimeout(resolve, seconds * 1000);
|
|
69
|
+
capture.wake = () => { clearTimeout(timer); resolve(); };
|
|
70
|
+
});
|
|
71
|
+
if (cap === capture) stopCapture();
|
|
72
|
+
else printCapture(lastCapture);
|
|
73
|
+
} else {
|
|
74
|
+
out.log(`Capturing ${label}; capture off stops it and prints what it recorded.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function endCapture() {
|
|
79
|
+
cap.handlers.forEach(([event, h]) => cap.page.off(event, h));
|
|
80
|
+
const { label, startedAt, events, dropped, shortened, wake } = cap;
|
|
81
|
+
cap = null;
|
|
82
|
+
lastCapture = { label, startedAt, events, dropped, shortened };
|
|
83
|
+
if (wake) wake();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function stopCapture(all = false) {
|
|
87
|
+
if (!cap) { out.log('Not capturing.'); return; }
|
|
88
|
+
endCapture();
|
|
89
|
+
printCapture(lastCapture, all);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// One line per event, from the start of the capture: a request as requests
|
|
93
|
+
// shows it (its status is looked up now, so it is known if it has finished),
|
|
94
|
+
// a console message or page error as console shows it.
|
|
95
|
+
function printCapture(capture, all = false) {
|
|
96
|
+
const lines = capture.events.map(event => {
|
|
97
|
+
const at = `+${((event.t - capture.startedAt) / 1000).toFixed(3)}s`;
|
|
98
|
+
if (event.tag !== 'request') return `${at} [${event.tag}] ${event.text}`;
|
|
99
|
+
const entry = requestEntries.get(event.req);
|
|
100
|
+
return entry ? `${at} #${entry.id} ${entry.method} ${entry.status} ${entry.url}` : `${at} ${event.text}`;
|
|
101
|
+
});
|
|
102
|
+
if (lines.length) printOutput(lines.join('\n'), all);
|
|
103
|
+
else out.log('Nothing captured');
|
|
104
|
+
const notes = [];
|
|
105
|
+
if (capture.dropped) notes.push(`${capture.dropped} event(s) omitted`);
|
|
106
|
+
if (capture.shortened) notes.push(`${capture.shortened} message(s) shortened`);
|
|
107
|
+
if (notes.length) out.log(`[capture limits: ${notes.join('; ')}]`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function discardCapture() {
|
|
111
|
+
if (!cap) return;
|
|
112
|
+
cap.handlers.forEach(([event, handler]) => cap.page.off(event, handler));
|
|
113
|
+
cap = null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
onShutdown(discardCapture);
|
|
117
|
+
|
|
118
|
+
const commands = {
|
|
119
|
+
async capture(args, all) {
|
|
120
|
+
const [sub, ...rest] = (args || '').trim().split(/\s+/).filter(Boolean);
|
|
121
|
+
if (sub === 'on') return startCapture(rest);
|
|
122
|
+
if (sub === 'off' && !rest.length) return stopCapture(all);
|
|
123
|
+
if (sub) throw new Error('Usage: capture on [requests|console] [seconds] | capture off');
|
|
124
|
+
if (cap) {
|
|
125
|
+
out.log(`Capturing ${cap.label} on ${cap.page.url()} since ${clock(cap.startedAt)} (${cap.events.length} so far)`);
|
|
126
|
+
out.log(hints([['capture off', 'stop and print what it recorded']]));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (lastCapture) {
|
|
130
|
+
out.log(`Not capturing. The last capture, of ${lastCapture.label} from ${clock(lastCapture.startedAt)}:`);
|
|
131
|
+
printCapture(lastCapture, all);
|
|
132
|
+
} else {
|
|
133
|
+
out.log('Not capturing.');
|
|
134
|
+
}
|
|
135
|
+
out.log(hints([['capture on [requests|console] [seconds]', 'record the selected tab\'s requests and console messages in time order']]));
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
module.exports = { commands, currentCapture, endCapture };
|
package/lib/cdp.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The CDP session kept for each tab, and keeping a background tab drawing.
|
|
2
|
+
const { tabState } = require('./tabstate');
|
|
3
|
+
|
|
4
|
+
// Per page: a CDP session's emulation applies to the page it is attached to
|
|
5
|
+
// and resets, in part, when it detaches, so one is kept for each tab.
|
|
6
|
+
function keptSession(p) {
|
|
7
|
+
if (!('session' in tabState(p))) tabState(p).session = p.context().newCDPSession(p).catch(error => { delete tabState(p).session; throw error; });
|
|
8
|
+
return tabState(p).session;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Chrome all but stops drawing a tab that is not in front once it has had
|
|
12
|
+
// input (about a frame a second), and Playwright waits for frames to see an
|
|
13
|
+
// element hold still before acting on it, so each click in a tab opened in the
|
|
14
|
+
// background took 1-2s. A screencast keeps the tab drawing without bringing it
|
|
15
|
+
// to the front, for as long as the REPL runs (the throttle is back within
|
|
16
|
+
// seconds of stopping it). Its frames are tiny and rare; the cost is that the
|
|
17
|
+
// tab renders at full rate, as if in front, an animation or a video included.
|
|
18
|
+
async function keepDrawing(p) {
|
|
19
|
+
if ('drawing' in tabState(p)) return;
|
|
20
|
+
tabState(p).drawing = true;
|
|
21
|
+
try {
|
|
22
|
+
const session = await keptSession(p);
|
|
23
|
+
session.on('Page.screencastFrame', frame => session.send('Page.screencastFrameAck', { sessionId: frame.sessionId }).catch(() => {}));
|
|
24
|
+
await session.send('Page.startScreencast', { format: 'jpeg', quality: 1, maxWidth: 8, maxHeight: 8, everyNthFrame: 60 });
|
|
25
|
+
} catch {
|
|
26
|
+
// Only slower without it.
|
|
27
|
+
delete tabState(p).drawing;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { keptSession, keepDrawing };
|
package/lib/cli-names.js
CHANGED
|
@@ -68,7 +68,6 @@ const NEAREST = {
|
|
|
68
68
|
detach: 'pw-repl stop, or quit at the prompt, disconnects and leaves the browser running',
|
|
69
69
|
drag: 'not here yet', drop: 'not here yet', highlight: 'not here yet', 'webmcp-list': 'not here yet', 'webmcp-call': 'not here yet',
|
|
70
70
|
keydown: 'not here yet; press <key> presses and releases one', keyup: 'not here yet; press <key> presses and releases one',
|
|
71
|
-
mousewheel: 'not here yet; eval scrollBy(<dx>, <dy>) scrolls the page, without wheel events',
|
|
72
71
|
pdf: 'not here yet; screenshot --full saves the whole page as a PNG',
|
|
73
72
|
'generate-locator': 'not here yet; a snapshot ref (e5) works as a selector in any command',
|
|
74
73
|
'cookie-get': 'not here yet; cookies lists them without values, and cdp Network.getCookies {} shows the values',
|
|
@@ -84,14 +83,13 @@ const NEAREST = {
|
|
|
84
83
|
install: 'not needed here: pw-repl needs no workspace set up; pw-repl skill says how to use it',
|
|
85
84
|
'install-browser': 'not needed here: --launch uses Playwright\'s Chromium or one on the PATH (PW_CHROME)',
|
|
86
85
|
};
|
|
87
|
-
for (const name of ['mousemove', 'mousedown', 'mouseup']) NEAREST[name] = 'not here yet; click, dblclick and hover act on an element';
|
|
88
86
|
for (const what of ['reduced-motion', 'forced-colors', 'contrast', 'media']) {
|
|
89
87
|
NEAREST[`set-${what}`] = 'not here yet; emulate dark|light sets the color scheme';
|
|
90
88
|
NEAREST[`clear-${what}`] = 'not here yet; emulate dark|light sets the color scheme';
|
|
91
89
|
}
|
|
92
90
|
for (const name of ['request-headers', 'response-headers', 'request-body']) NEAREST[name] = 'not here yet; body <#> shows the response body';
|
|
93
91
|
for (const name of ['video-start', 'video-stop', 'video-chapter', 'video-show-actions', 'video-hide-actions']) {
|
|
94
|
-
NEAREST[name] = 'not here; screenshot saves an image, and watch on records each step';
|
|
92
|
+
NEAREST[name] = 'not here: pw-repl does not record video (playwright-cli itself does, in a browser of its own); screenshot saves an image, and watch on records each step';
|
|
95
93
|
}
|
|
96
94
|
for (const name of ['pause-at', 'resume', 'step-over']) NEAREST[name] = 'not here: they step through a Playwright test';
|
|
97
95
|
|
package/lib/client.js
CHANGED
|
@@ -53,6 +53,11 @@ async function health(endpoint, timeoutMs) {
|
|
|
53
53
|
return (await healthInfo(endpoint, timeoutMs)) !== null;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// Serving, not only starting.
|
|
57
|
+
async function ready(endpoint, timeoutMs) {
|
|
58
|
+
return (await healthInfo(endpoint, timeoutMs))?.status === 'ok';
|
|
59
|
+
}
|
|
60
|
+
|
|
56
61
|
// One command. Resolves to { result }, { timeout: true }, { dropped: reason }
|
|
57
62
|
// (the connection closed after the command was sent, so it may have run) or
|
|
58
63
|
// { unreachable: reason } (it was never sent).
|
|
@@ -85,4 +90,4 @@ function request(endpoint, command, timeoutMs, client = null) {
|
|
|
85
90
|
});
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
module.exports = { DEFAULT_SOCKET, parseEndpoint, looksLikeEndpoint, describe, health, healthInfo, request };
|
|
93
|
+
module.exports = { DEFAULT_SOCKET, parseEndpoint, looksLikeEndpoint, describe, health, ready, healthInfo, request };
|