pw-repl 0.2.2 → 0.3.1

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
@@ -11,13 +11,17 @@ If you hit a limitation or write a workaround, consider adding the capability to
11
11
  - A command is a function in the `commands` object in `lib/commands.js`, plus an entry in `lib/help.js`
12
12
  under one topic.
13
13
  - `bin/pw-repl.js` is the command line; `lib/start.js` connects and runs the prompt; `lib/runner.js`
14
- runs commands one at a time; `lib/server.js` is the opt-in server; `lib/send.js` and `lib/client.js`
15
- are `send` and `where`; `lib/output.js` routes all output so the server can return it.
14
+ runs commands one at a time (`quit` and `dialog` skip its queue); `lib/server.js` is the command
15
+ server; `lib/send.js` and `lib/client.js` are `send` and `where`; `lib/background.js` is
16
+ `serve --background`, `attach` and `stop`; `lib/launch.js` is `--launch`; `lib/syntax.js` is how a
17
+ command line's words are read, shared with `send`; `lib/state.js` holds the session state;
18
+ `lib/output.js` routes all output so the server can return it.
16
19
  - Comment the *why* when it isn't obvious from the code.
17
- - `npm test` runs the suite (about 40s): a private headless Chromium, a local test site, and the real
18
- REPL with its server. Nothing is mocked, and the shared browser is never touched. It finds Chromium
19
- through `PW_TEST_CHROME` or Playwright's installed browsers, and skips the browser tests if there is
20
- none. Add a test with each new command or behaviour.
20
+ - `npm test` runs the suite (about a minute): a private headless Chromium, a local test site
21
+ (`test/harness.js`), and the real REPL with its server. Nothing is mocked, and the shared browser is
22
+ never touched. It finds Chromium through `PW_TEST_CHROME`, or where `--launch` looks (Playwright's
23
+ browsers, then the `PATH`), and skips the browser tests if there is none. Add a test with each new
24
+ command or behaviour.
21
25
  - For anything the tests can't reach, exercise the change in a running REPL. Use your own tmux session
22
26
  and a new tab, not the user's.
23
27
  - `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
@@ -21,6 +21,11 @@ chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.config/chrome-debug"
21
21
  Use a separate `--user-data-dir`: recent Chrome versions do not open the debugging port on the default
22
22
  profile. Check it is up with `curl http://localhost:9222/json/version`.
23
23
 
24
+ **No browser to hand, or one you would rather not share?** Skip this step: `pw-repl run --launch` (or
25
+ `serve --launch`) starts a private headless Chromium for the REPL and stops it with the REPL. It prints the
26
+ command it ran, so you can start one your own way instead. If there is no Chromium at all,
27
+ `npx playwright-core install chromium` downloads Playwright's.
28
+
24
29
  **Headless** works the same way:
25
30
 
26
31
  ```bash
@@ -61,30 +66,35 @@ Commands act on the selected tab (`tab` lists the tabs, with `*` on the selected
61
66
 
62
67
  ## Common tasks
63
68
 
64
- | I want to… | Commands |
65
- | ------------------------------------ | --------------------------------------------------------------- |
66
- | see where I am | `tab`, `info` |
67
- | see what is on the page | `snapshot`, `screenshot` |
68
- | do something on it | `click`, `fill`, `press` |
69
- | see what the page requested | `requests`, then `body <#>` for what one got back |
70
- | see console messages and errors | `console` |
71
- | show an agent what I do | `watch on`, click around in the browser, then `watch` |
72
- | see each step as I click | `watch on --live` |
73
- | record requests and console together | `capture on`, then `capture off` |
74
- | break the backend on purpose | `route <glob> <status> <json>` (fake a response), `network off` |
75
- | clean up | `modes off` |
69
+ | I want to… | Commands |
70
+ | ------------------------------------ | ------------------------------------------------------------------------------------- |
71
+ | see where I am | `tab`, `info` |
72
+ | see what is on the page | `snapshot`, `screenshot` |
73
+ | do something on it | `click`, `fill`, `press` |
74
+ | wait for a page or element | `wait load`, `wait <selector>`, `wait <selector> --gone` |
75
+ | choose a file in a file input | `upload <selector> <file>` |
76
+ | see it as a phone, or in dark mode | `emulate mobile`, `emulate dark` (also `emulate locale`, `emulate timezone`) |
77
+ | see what the page requested | `requests`, then `body <#>` for what one got back |
78
+ | see console messages and errors | `console` |
79
+ | show an agent what I do | `watch on`, click around in the browser, then `watch` |
80
+ | see each step as I click | `watch on --live` |
81
+ | record requests and console together | `capture on`, then `capture off` |
82
+ | break the backend on purpose | `route <glob> <status> <json>` (fake a response), `route <glob> abort`, `network off` |
83
+ | change or slow an API response | `route <glob> patch <json>`, `route <glob> delay <secs>` |
84
+ | slow the whole network | `network slow` |
85
+ | clean up | `modes off` |
76
86
 
77
87
  Everything else is in `help <topic>`; `help <command>` has usage and caveats.
78
88
 
79
89
  ### Modes
80
90
 
81
- `watch`, `capture`, `route` and `network off` stay on until you turn them off: `watch on|off`,
82
- `capture on|off`, `route ...|route off`, `network off|on`. While any are on in the selected tab, the prompt
83
- shows them: `(watch network:off routes:2) pw>`. `modes` lists them for every tab, and `modes off` turns them
84
- all off.
91
+ `watch`, `capture`, `route`, `network off` or `slow`, and `emulate` stay on until you turn them off:
92
+ `watch on|off`, `capture on|off`, `route ...|route off`, `network off|slow|on`, `emulate ...|emulate off`.
93
+ While any are on in the selected tab, the prompt shows them: `(watch network:off routes:2) pw>`. `modes`
94
+ lists them for every tab, and `modes off` turns them all off.
85
95
 
86
- `tab`, `watch`, `capture`, `route`, `network` and `modes` on their own show their state and what you can
87
- run next.
96
+ `tab`, `watch`, `capture`, `route`, `network`, `emulate` and `modes` on their own show their state and what
97
+ you can run next.
88
98
 
89
99
  ## Options
90
100
 
@@ -150,6 +160,6 @@ The same text is in `skill/SKILL.md`. Working on the REPL itself: see `AGENTS.md
150
160
  npm test
151
161
  ```
152
162
 
153
- Runs against a private headless Chromium it starts itself (via `PW_TEST_CHROME`, or Playwright's installed
154
- browsers: `npx playwright-core install chromium`) and a local test site; the browser tests are skipped when no
155
- Chromium is found.
163
+ Runs against a private headless Chromium it starts itself (`PW_TEST_CHROME`, or the one `--launch` would
164
+ use: Playwright's, e.g. from `npx playwright-core install chromium`, then one on the `PATH`) and a local
165
+ test site; the browser tests are skipped when no Chromium is found.
package/bin/pw-repl.js CHANGED
@@ -4,10 +4,10 @@
4
4
  const { looksLikeEndpoint } = require('../lib/client');
5
5
 
6
6
  const USAGE = `Usage:
7
- pw-repl run [start-url]
7
+ pw-repl run [--launch [--headed]] [start-url] [-- <chromium flags>]
8
8
  connect to the browser and open the prompt
9
9
 
10
- pw-repl serve [--background] [endpoint] [start-url]
10
+ pw-repl serve [--background] [--launch [--headed]] [endpoint] [start-url] [-- <chromium flags>]
11
11
  the same, plus a command server (socket, port, or 127.0.0.1:port; default /tmp/playwright-repl.sock);
12
12
  --background runs it detached, with its output in a log next to the socket
13
13
 
@@ -37,6 +37,11 @@ send exit status: 0 ok, 1 command error, 2 completion not confirmed, 64 usage or
37
37
 
38
38
  The browser must be running with --remote-debugging-port (default http://localhost:9222; set $PW_CDP_URL).
39
39
 
40
+ --launch is the quick start instead: it starts a Chromium of the REPL's own (headless unless --headed, in
41
+ a temporary profile), prints the command it ran, and stops it with the REPL. Flags after -- go to that
42
+ Chromium; PW_CHROME picks which one. To set a browser up your own way, start it yourself and use
43
+ PW_CDP_URL.
44
+
40
45
  In a tmux session named playwright-repl, pw-repl send reaches pw-repl run without the server.`;
41
46
 
42
47
  const REPL_HELP = `The REPL's own commands: help at the pw> prompt, or pw-repl send help here (no REPL needed).
@@ -74,15 +79,24 @@ function parseSendArgs(args, allowCommand) {
74
79
  const { SELECTOR_FIRST } = require('../lib/syntax');
75
80
  const requote = words.length > 1 && SELECTOR_FIRST.has(words[0]);
76
81
  // An empty word (fill #name "") is the empty value.
77
- const quoted = requote ? words.map(w => (w === '' || /[\s"']/.test(w) ? JSON.stringify(w) : w)) : words;
82
+ // upload's files are read by the REPL, whose folder may not be this one.
83
+ const resolved = words[0] === 'upload' ? words.map((w, n) => (n > 1 ? require('path').resolve(w) : w)) : words;
84
+ const quoted = requote ? resolved.map(w => (w === '' || /[\s"']/.test(w) ? JSON.stringify(w) : w)) : resolved;
78
85
  options.command = quoted.join(' ').trim();
79
86
  return options;
80
87
  }
81
88
 
82
89
  function startOptions(args, serve) {
83
- const options = { serve, endpoint: null, startUrl: null, background: false, pidFile: process.env.PW_REPL_PID_FILE || null };
84
- const rest = [...args];
85
- if (serve && rest.includes('--background')) { options.background = true; rest.splice(rest.indexOf('--background'), 1); }
90
+ const options = { serve, endpoint: null, startUrl: null, background: false, launch: false, headed: false, chromeArgs: [], pidFile: process.env.PW_REPL_PID_FILE || null };
91
+ // Whatever follows -- is for the Chromium that --launch starts.
92
+ const split = args.indexOf('--');
93
+ const rest = split === -1 ? [...args] : args.slice(0, split);
94
+ if (split !== -1) options.chromeArgs = args.slice(split + 1);
95
+ const flag = name => { const i = rest.indexOf(name); if (i === -1) return false; rest.splice(i, 1); return true; };
96
+ if (serve) options.background = flag('--background');
97
+ options.launch = flag('--launch');
98
+ options.headed = flag('--headed');
99
+ if ((options.headed || options.chromeArgs.length) && !options.launch) usage();
86
100
  if (serve && rest[0] && looksLikeEndpoint(rest[0])) options.endpoint = rest.shift();
87
101
  if (rest.length > 1 || (rest[0] && rest[0].startsWith('-'))) usage();
88
102
  options.startUrl = rest[0] || null;
package/lib/background.js CHANGED
@@ -64,7 +64,9 @@ async function start(options) {
64
64
  if (await client.health(endpoint, 2000)) return fail(`a REPL is already serving on ${name}, in a terminal; pw-repl where says more`);
65
65
  const log = fs.openSync(logFile, 'w', 0o600);
66
66
  fs.fchmodSync(log, 0o600);
67
- const args = [BIN, 'serve', ...(options.endpoint ? [options.endpoint] : []), ...(options.startUrl ? [options.startUrl] : [])];
67
+ const args = [BIN, 'serve', ...(options.launch ? ['--launch'] : []), ...(options.headed ? ['--headed'] : []),
68
+ ...(options.endpoint ? [options.endpoint] : []), ...(options.startUrl ? [options.startUrl] : []),
69
+ ...(options.chromeArgs.length ? ['--', ...options.chromeArgs] : [])];
68
70
  const child = spawn(process.execPath, args, {
69
71
  detached: true,
70
72
  stdio: ['ignore', log, log],
package/lib/client.js CHANGED
@@ -32,17 +32,27 @@ function target(endpoint) {
32
32
  }
33
33
 
34
34
  // GET /health: does not go through the command queue or show in the pane.
35
- function health(endpoint, timeoutMs) {
35
+ // Resolves to what the REPL says about itself (e.g. its browser), or null.
36
+ function healthInfo(endpoint, timeoutMs) {
36
37
  return new Promise(resolve => {
37
38
  const req = http.get({ ...target(endpoint), path: '/health', timeout: timeoutMs }, res => {
38
- res.resume();
39
- resolve(res.statusCode === 200);
39
+ let text = '';
40
+ res.setEncoding('utf8');
41
+ res.on('data', chunk => { text += chunk; });
42
+ res.on('end', () => {
43
+ if (res.statusCode !== 200) return resolve(null);
44
+ try { resolve(JSON.parse(text)); } catch { resolve({}); }
45
+ });
40
46
  });
41
- req.on('timeout', () => { req.destroy(); resolve(false); });
42
- req.on('error', () => resolve(false));
47
+ req.on('timeout', () => { req.destroy(); resolve(null); });
48
+ req.on('error', () => resolve(null));
43
49
  });
44
50
  }
45
51
 
52
+ async function health(endpoint, timeoutMs) {
53
+ return (await healthInfo(endpoint, timeoutMs)) !== null;
54
+ }
55
+
46
56
  // One command. Resolves to { result }, { timeout: true }, { dropped: reason }
47
57
  // (the connection closed after the command was sent, so it may have run) or
48
58
  // { unreachable: reason } (it was never sent).
@@ -75,4 +85,4 @@ function request(endpoint, command, timeoutMs) {
75
85
  });
76
86
  }
77
87
 
78
- module.exports = { DEFAULT_SOCKET, parseEndpoint, looksLikeEndpoint, describe, health, request };
88
+ module.exports = { DEFAULT_SOCKET, parseEndpoint, looksLikeEndpoint, describe, health, healthInfo, request };