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 CHANGED
@@ -1,22 +1,32 @@
1
1
  # Agent Instructions — playwright-repl
2
2
 
3
- How to use the REPL is in `skill/SKILL.md` (the same text `pw-repl skill` prints). Read it first. In
4
- this clone, `pw-repl` there means `bin/pw-repl.js` (from the folder this file is in) when it is not
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 in `lib/commands.js`, plus an entry in `lib/help.js`
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 text REPL for driving an existing Chromium through Playwright over CDP: one session that people and agents
4
- share. People click in the browser and type commands at the prompt; agents send the same commands, and every
5
- one shows in the REPL, so each sees what the other does. The REPL inspects the page, records what happened
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
- ## How it differs from playwright-cli
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
- - `open` and `close` open a tab and turn off your modes; they never start, close or wipe a browser.
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
- For a browser of its own per agent, isolated sessions, traces, video, PDFs or generated test code,
37
- playwright-cli is the better fit.
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
- chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.config/chrome-debug"
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
- Use a separate `--user-data-dir`: recent Chrome versions do not open the debugging port on the default
55
- profile. Check it is up with `curl http://localhost:9222/json/version`.
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
- **No browser to hand, or one you would rather not share?** Skip this step: `pw-repl run --launch` (or
58
- `serve --launch`) starts a private headless Chromium for the REPL and stops it with the REPL. It prints the
59
- command it ran, so you can start one your own way instead. If there is no Chromium at all,
60
- `npx playwright-core install chromium` downloads Playwright's.
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
- **Headless** works the same way:
43
+ Commands act on the selected tab (`tab` lists the tabs, with `*` on the selected one).
63
44
 
64
- ```bash
65
- chrome --headless=new --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-headless
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
- With nobody at the browser, `dialog accept` or `dialog dismiss` answers a dialog (`alert`, `confirm`);
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
- ### 2. Install and start the REPL
51
+ Start it with remote debugging, then run the REPL without `--launch`:
73
52
 
74
53
  ```bash
75
- npm install -g pw-repl
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
- Without installing: `npx pw-repl` (subcommands work the same way).
80
- From a clone: `npm install`, then `bin/pw-repl.js run` (or `npm link` to get `pw-repl`).
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
- It lists the open tabs and shows a `pw>` prompt. To send it commands from scripts or agents later
83
- (`pw-repl send`), run it in a tmux session named `playwright-repl` (`tmux new -s playwright-repl`), or
84
- start it with `pw-repl serve` instead, which needs no tmux (`pw-repl serve --background` runs it without
85
- a terminal; see [In the background](#in-the-background)).
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
- ### 3. Try it
70
+ ## Ways to run it
88
71
 
89
- ```text
90
- pw> help # common tasks and topics; help <topic>, help <command>, help --all
91
- pw> tab new https://example.com # open your own tab to work in
92
- pw> snapshot # the page by role and name, with [ref=eN] labels
93
- pw> click e3 # click by snapshot ref (or any Playwright selector)
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
- Commands act on the selected tab (`tab` lists the tabs, with `*` on the selected one).
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
- ## Options
118
+ ## How it differs from playwright-cli
133
119
 
134
- ```bash
135
- pw-repl run http://localhost:3000 # connect and open a URL in a new tab
136
- pw-repl serve # also accept commands on /tmp/playwright-repl.sock (prompt: pw[serve]>)
137
- PW_CDP_URL=http://host:9222 pw-repl run # a browser elsewhere
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
- ### In the background
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
- pw-repl serve --background # detached, serving /tmp/playwright-repl.sock; output in /tmp/playwright-repl.log
144
- pw-repl attach # see everything it does and type commands to it; Ctrl-C leaves it running
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
- A background process cannot be brought back to the foreground like a Ctrl-Z job; `attach` is how you
150
- get back to it, from any terminal.
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
- **Browser on the host, REPL in a container:** with host networking, `localhost:9222` reaches the host's
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. The server speaks HTTP with JSON:
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
- ### A skill for agents
168
+ ### The HTTP protocol
182
169
 
183
- `pw-repl skill` prints an agent skill (`SKILL.md`) that teaches an agent to use the REPL: how to start
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
- mkdir -p <skills folder>/pw-repl
189
- pw-repl skill > <skills folder>/pw-repl/SKILL.md
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
- The same text is in `skill/SKILL.md`. Working on the REPL itself: see `AGENTS.md`.
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 the one `--launch` would
201
- use: Playwright's, e.g. from `npx playwright-core install chromium`, then one on the `PATH`) and a local
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 as is, to be saved as a skill; the hint goes to the terminal only.
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('fs').readFileSync(require('path').join(__dirname, '..', 'skill', 'SKILL.md'), 'utf8'));
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.health(endpoint, 1000) && runningPid(pidFile))) {
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 served = await client.health(endpoint, 2000);
153
- return fail(served ? `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`);
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 };