pw-repl 0.4.1 → 0.5.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 +7 -3
- package/README.md +90 -101
- package/bin/pw-repl.js +6 -5
- package/lib/background.js +41 -8
- package/lib/cli-names.js +1 -3
- package/lib/client.js +6 -1
- package/lib/commands.js +372 -41
- package/lib/help.js +30 -18
- package/lib/launch.js +97 -27
- package/lib/output.js +1 -1
- package/lib/runner.js +6 -3
- package/lib/send.js +9 -0
- package/lib/server.js +9 -2
- package/lib/skill.js +29 -0
- package/lib/start.js +15 -5
- package/lib/state.js +4 -1
- package/package.json +1 -1
- package/skill/SKILL.md +54 -20
package/AGENTS.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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
|
|
|
@@ -27,6 +27,10 @@ If you hit a limitation or write a workaround, consider adding the capability to
|
|
|
27
27
|
never touched. It finds Chromium through `PW_TEST_CHROME`, or where `--launch` looks (Playwright's
|
|
28
28
|
browsers, then the `PATH`), and skips the browser tests if there is none. Add a test with each new
|
|
29
29
|
command or behaviour.
|
|
30
|
+
- Time each full run, and put the result in the body of the commit it tests (`npm test: 151 tests, 79s`).
|
|
31
|
+
Compare with the last commit that has one (`git log --grep='npm test:'`): a run that grew by more than
|
|
32
|
+
a few seconds means a slow test crept in, usually one waiting out a real timeout (5s for a click).
|
|
33
|
+
Find it (each test prints its time) and make it faster, or say why it can't be, before committing.
|
|
30
34
|
- For anything the tests can't reach, exercise the change in a running REPL of your own: on a socket of
|
|
31
35
|
your own, with `--launch` unless the change needs the user's browser, and then in a tab of your own.
|
|
32
36
|
- `skill/SKILL.md` is how agents learn to use the REPL: keep it in step with a change to how it is
|
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
|
@@ -41,9 +41,10 @@ send exit status: 0 ok, 1 command error, 2 completion not confirmed, 64 usage or
|
|
|
41
41
|
The browser must be running with --remote-debugging-port (default http://localhost:9222; set $PW_CDP_URL).
|
|
42
42
|
|
|
43
43
|
--launch is the quick start instead: it starts a Chromium of the REPL's own (headless unless --headed, in
|
|
44
|
-
a temporary profile), prints the command it ran, and stops it with the REPL
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
a temporary profile, on a free port of its own), prints the command it ran, and stops it with the REPL
|
|
45
|
+
(Ctrl-C while it starts too); stop returns once that Chromium has exited. Flags after -- go to it; a
|
|
46
|
+
--user-data-dir=<dir> among them is used instead of the temporary profile, and kept. PW_CHROME picks
|
|
47
|
+
which one. To set a browser up your own way, start it yourself and use PW_CDP_URL.
|
|
47
48
|
|
|
48
49
|
In a tmux session named playwright-repl, pw-repl send reaches pw-repl run without the server.`;
|
|
49
50
|
|
|
@@ -138,9 +139,9 @@ function help(topic) {
|
|
|
138
139
|
console.log(text);
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
// Printed
|
|
142
|
+
// Printed to be saved as a skill, stamped with where it came from; the hint goes to the terminal only.
|
|
142
143
|
function skill() {
|
|
143
|
-
process.stdout.write(require('
|
|
144
|
+
process.stdout.write(require('../lib/skill').stamped());
|
|
144
145
|
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');
|
|
145
146
|
}
|
|
146
147
|
|
package/lib/background.js
CHANGED
|
@@ -8,7 +8,13 @@ const client = require('./client');
|
|
|
8
8
|
|
|
9
9
|
const BIN = path.join(__dirname, '..', 'bin', 'pw-repl.js');
|
|
10
10
|
const START_TIMEOUT = 20000;
|
|
11
|
-
|
|
11
|
+
// Longer than --launch gives Chromium, twice when it retries without the sandbox, then the connect: its own
|
|
12
|
+
// error, and its cleanup, come first.
|
|
13
|
+
const LAUNCH_START_TIMEOUT = 2 * require('./launch').START_TIMEOUT + 20000;
|
|
14
|
+
// Longer than a REPL can take to stop: its cleanups and letting go of the browser (up to
|
|
15
|
+
// SHUTDOWN_TIMEOUT each), then stopping a Chromium it launched.
|
|
16
|
+
const STOP_TIMEOUT = 2 * require('./state').SHUTDOWN_TIMEOUT + require('./launch').STOP_WORST + 4000;
|
|
17
|
+
const LOG_LIMIT = 5 * 1024 * 1024;
|
|
12
18
|
const FOLLOW_INTERVAL = 150;
|
|
13
19
|
const ATTACH_BACKLOG = 20;
|
|
14
20
|
const ATTACH_COMMAND_TIMEOUT = 3600000;
|
|
@@ -47,6 +53,13 @@ function tail(file, lines) {
|
|
|
47
53
|
try { return fs.readFileSync(file, 'utf8').trimEnd().split('\n').slice(-lines).join('\n'); } catch { return ''; }
|
|
48
54
|
}
|
|
49
55
|
|
|
56
|
+
// Called in the background REPL as it starts: however it ends, served or not, its log says so.
|
|
57
|
+
function markStopInLog() {
|
|
58
|
+
process.on('exit', code => {
|
|
59
|
+
try { fs.writeSync(process.stdout.fd, `--- ${new Date().toISOString()} stopped${code ? ` (exit code ${code})` : ''}\n`); } catch {}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
50
63
|
// Called in the background REPL once it serves: its pid file says it runs, and goes when it exits.
|
|
51
64
|
function claimPidFile(pidFile) {
|
|
52
65
|
fs.writeFileSync(pidFile, String(process.pid), { mode: 0o600 });
|
|
@@ -62,8 +75,12 @@ async function start(options) {
|
|
|
62
75
|
const running = runningPid(pidFile);
|
|
63
76
|
if (running) return fail(`a background REPL is already serving on ${name} (pid ${running}); pw-repl attach uses it, pw-repl stop stops it`);
|
|
64
77
|
if (await client.health(endpoint, 2000)) return fail(`a REPL is already serving on ${name}, in a terminal; pw-repl where says more`);
|
|
65
|
-
|
|
78
|
+
// Appended to, so an earlier run's log stays; each run is marked where it starts and stops. One grown
|
|
79
|
+
// past LOG_LIMIT is moved to <log>.1 first, replacing the one there.
|
|
80
|
+
try { if (fs.statSync(logFile).size > LOG_LIMIT) fs.renameSync(logFile, `${logFile}.1`); } catch {}
|
|
81
|
+
const log = fs.openSync(logFile, 'a', 0o600);
|
|
66
82
|
fs.fchmodSync(log, 0o600);
|
|
83
|
+
fs.writeSync(log, `--- ${new Date().toISOString()} started\n`);
|
|
67
84
|
const args = [BIN, 'serve', ...(options.launch ? ['--launch'] : []), ...(options.headed ? ['--headed'] : []),
|
|
68
85
|
...(options.endpoint ? [options.endpoint] : []), ...(options.startUrl ? [options.startUrl] : []),
|
|
69
86
|
...(options.chromeArgs.length ? ['--', ...options.chromeArgs] : [])];
|
|
@@ -74,11 +91,26 @@ async function start(options) {
|
|
|
74
91
|
});
|
|
75
92
|
fs.closeSync(log);
|
|
76
93
|
let exited = false;
|
|
77
|
-
child.on('exit', () => { exited = true; });
|
|
78
|
-
|
|
79
|
-
|
|
94
|
+
const gone = new Promise(resolve => child.on('exit', () => { exited = true; resolve(); }));
|
|
95
|
+
// Ctrl-C while it waits gives up on the start: the REPL is stopped (and a Chromium it launched with it),
|
|
96
|
+
// rather than left starting where stop cannot reach it yet.
|
|
97
|
+
let interrupted = false;
|
|
98
|
+
const interrupt = () => { interrupted = true; };
|
|
99
|
+
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
100
|
+
for (const signal of signals) process.on(signal, interrupt);
|
|
101
|
+
const deadline = Date.now() + (options.launch ? LAUNCH_START_TIMEOUT : START_TIMEOUT);
|
|
102
|
+
while (!exited && !interrupted && Date.now() < deadline && !(await client.ready(endpoint, 1000) && runningPid(pidFile))) {
|
|
80
103
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
81
104
|
}
|
|
105
|
+
for (const signal of signals) process.removeListener(signal, interrupt);
|
|
106
|
+
if (interrupted) {
|
|
107
|
+
if (!exited) child.kill();
|
|
108
|
+
await Promise.race([gone, new Promise(resolve => setTimeout(resolve, STOP_TIMEOUT))]);
|
|
109
|
+
try { fs.appendFileSync(logFile, `--- ${new Date().toISOString()} the start was interrupted\n`); } catch {}
|
|
110
|
+
console.error(`pw-repl: ${exited ? 'interrupted; the background REPL was stopped before it served' : `interrupted; the background REPL (pid ${child.pid}) has not stopped after ${STOP_TIMEOUT / 1000}s`}`);
|
|
111
|
+
// As a shell reports a command stopped by Ctrl-C.
|
|
112
|
+
return 130;
|
|
113
|
+
}
|
|
82
114
|
if (exited || !runningPid(pidFile)) {
|
|
83
115
|
if (!exited) child.kill();
|
|
84
116
|
const last = tail(logFile, 12);
|
|
@@ -117,8 +149,9 @@ async function attach(options) {
|
|
|
117
149
|
const { pidFile, logFile } = filesFor(endpoint);
|
|
118
150
|
const pid = runningPid(pidFile);
|
|
119
151
|
if (!pid) {
|
|
120
|
-
const
|
|
121
|
-
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`);
|
|
122
155
|
}
|
|
123
156
|
const fd = fs.openSync(logFile, 'r');
|
|
124
157
|
const backlog = tail(logFile, ATTACH_BACKLOG);
|
|
@@ -186,4 +219,4 @@ function describeBackground(endpoint) {
|
|
|
186
219
|
return `background: pid ${pid}${since}; log ${logFile} (pw-repl attach${e}, pw-repl stop${e})`;
|
|
187
220
|
}
|
|
188
221
|
|
|
189
|
-
module.exports = { filesFor, endpointFlag, claimPidFile, start, stop, attach, describeBackground };
|
|
222
|
+
module.exports = { filesFor, endpointFlag, markStopInLog, claimPidFile, start, stop, attach, describeBackground };
|
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 };
|