pw-repl 0.12.8 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/send.js CHANGED
@@ -93,6 +93,15 @@ function doubleQuote(word) {
93
93
  return `"${word.replace(/[\\"]/g, '\\$&').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t')}"`;
94
94
  }
95
95
 
96
+ // Where eval's --file is, after its --timeout if it has one; -1 when it has none.
97
+ function evalFileAt(words) {
98
+ if (words[0] !== 'eval') return -1;
99
+ let at = 1;
100
+ if (/^--timeout=/.test(words[at] || '')) at += 1;
101
+ else if (words[at] === '--timeout') at += 2;
102
+ return /^--file(?:=|$)/.test(words[at] || '') ? at : -1;
103
+ }
104
+
96
105
  // A command's words as send was given them, made into the line the REPL reads: quoted again where the
97
106
  // shell had kept a word whole, and with the files it reads or writes made absolute from the sender's
98
107
  // folder, since the REPL's may be another.
@@ -104,7 +113,7 @@ function commandLine(words) {
104
113
  const { SELECTOR_FIRST } = require('./syntax');
105
114
  // So does playwright-cli's storage key (localstorage-set "my key" v).
106
115
  // And record's file, which may have spaces in it, and wait's text, whose last word would otherwise be
107
- // taken as the seconds when it is a number (wait text 'Showing 1').
116
+ // taken as the time when it is a number (wait text 'Showing 1').
108
117
  const requote = words.length > 1 && (SELECTOR_FIRST.has(words[0]) || words[0] === 'drag' || /^(?:local|session)storage-/.test(words[0]) || words[0] === 'record' || words[0] === 'wait' || /^video-(?:start|stop)$/.test(words[0]));
109
118
  // An empty word (fill #name "") is the empty value.
110
119
  // upload's files are read by the REPL, whose folder may not be this one.
@@ -123,10 +132,11 @@ function commandLine(words) {
123
132
  if (words[0] === 'profile' && words[1] === 'save' && words.length > 2) resolved = ['profile', 'save', resolve(words.slice(2).join(' '))];
124
133
  if (words[0] === 'watch' && words[1] === 'save' && words.length > 2) resolved = ['watch', 'save', resolve(words.slice(2).join(' '))];
125
134
  // So does the file eval --file reads.
126
- if (words[0] === 'eval' && /^--file(?:=|$)/.test(words[1] || '')) {
135
+ const at = evalFileAt(words);
136
+ if (at > 0) {
127
137
  // As one word, --file=<path>, which is quoted below when the path has spaces.
128
- const [file, rest] = words[1] === '--file' ? [words[2], words.slice(3)] : [words[1].slice(7), words.slice(2)];
129
- if (file) resolved = ['eval', `--file=${resolve(file)}`, ...rest];
138
+ const [file, rest] = words[at] === '--file' ? [words[at + 1], words.slice(at + 2)] : [words[at].slice(7), words.slice(at + 1)];
139
+ if (file) resolved = [...words.slice(0, at), `--file=${resolve(file)}`, ...rest];
130
140
  }
131
141
  if (words[0] === 'screenshot') {
132
142
  resolved = words.map((w, n) => (w.startsWith('--filename=') && w.length > 11 ? `--filename=${resolve(w.slice(11))}` : words[n - 1] === '--filename' ? resolve(w) : w));
@@ -167,7 +177,7 @@ async function sendFile(options) {
167
177
  // Read as the prompt reads it; a file it reads or writes is the sender's, as on send's command line.
168
178
  const words = shellWords(line);
169
179
  // eval's own line keeps its quotes; only eval --file names a file.
170
- const command = PATHS.test(words[0]) || (words[0] === 'eval' && /^--file(?:=|$)/.test(words[1] || '')) ? commandLine(words) : line;
180
+ const command = PATHS.test(words[0]) || evalFileAt(words) > 0 ? commandLine(words) : line;
171
181
  const found = options.noVars ? null : variablesFor(command);
172
182
  if (found?.error) { console.error(`pw-repl: stopped at line ${i + 1} of ${options.file} (${line}): ${found.error}`); return 1; }
173
183
  const status = await send({ ...options, file: null, command, vars: found?.vars || null });
@@ -194,7 +204,7 @@ async function send(options) {
194
204
  if (found?.error) return fail(found.error);
195
205
  options = { ...options, vars: found?.vars || null };
196
206
  }
197
- const timeoutMs = options.timeout * 1000;
207
+ const timeoutMs = options.timeout;
198
208
  const way = route(options);
199
209
  const name = options.client || process.env.PW_CLIENT || null;
200
210
  if (name && !/^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$/.test(name)) return fail('a client name is 1-40 letters, digits, dots, dashes and underscores');
package/lib/server.js CHANGED
@@ -56,7 +56,10 @@ function handle(req, res) {
56
56
  }
57
57
  // Bound before the browser is reached, so where and send find it while it starts.
58
58
  if (!state.serving) return send(res, 503, { status: 'error', starting: true, output: `The REPL is starting (${state.startingStep}); retry shortly` });
59
- const result = await runner.submit(command.trim(), client || null, vars || null);
59
+ // The sender gave up (send -t) once its connection closes before the answer is written.
60
+ const gone = new AbortController();
61
+ res.once('close', () => { if (!res.writableEnded) gone.abort(); });
62
+ const result = await runner.submit(command.trim(), client || null, vars || null, gone.signal);
60
63
  if (!result.uncertain) return send(res, 200, { status: result.status, output: result.output });
61
64
  // unconfirmed: the command may or may not have done what it was sent to do.
62
65
  send(res, 200, { status: result.status, output: result.output, unconfirmed: true });
package/lib/state.js CHANGED
@@ -1,14 +1,12 @@
1
1
  // Session state shared by the entry point, the runner, and the commands, plus
2
2
  // the shutdown that ends the session.
3
+ const { AsyncLocalStorage } = require('async_hooks');
3
4
  const out = require('./output');
4
5
 
5
6
  const SHUTDOWN_TIMEOUT = 5000;
6
7
 
7
8
  const state = {
8
9
  browser: null,
9
- // The client whose command runs now: null for the prompt and senders without
10
- // a name, who share one selected tab, as before clients had names.
11
- client: null,
12
10
  rl: null,
13
11
  promptBase: 'pw> ',
14
12
  // The browser the REPL is connected to, and whether --launch started it.
@@ -36,18 +34,31 @@ function clientRecord(name = state.client) {
36
34
  return clients.get(name);
37
35
  }
38
36
 
37
+ // The command running in this async context: its client, and whether its sender gave up on it.
38
+ // Code outside any command (a browser event, quit and dialog, which skip the queue) goes by the
39
+ // queued command running now.
40
+ const running = new AsyncLocalStorage();
41
+ let queued = null;
42
+
43
+ // The client whose command runs now: null for the prompt and senders without a name, who share one
44
+ // selected tab, as before clients had names. Each command keeps its own for its whole life, so one
45
+ // its sender gave up on, still running, never acts as the command after it.
46
+ Object.defineProperty(state, 'client', { enumerable: true, get: () => (running.getStore() ?? queued)?.client ?? null });
47
+
48
+ out.onCancelled(() => running.getStore()?.cancelled === true);
49
+
39
50
  for (const key of Object.keys(PER_CLIENT)) {
40
51
  Object.defineProperty(state, key, { enumerable: true, get: () => clientRecord()[key], set: value => { clientRecord()[key] = value; } });
41
52
  }
42
53
 
43
- // Runs fn as the named client. Commands run one at a time, so this is safe.
44
- async function asClient(name, fn) {
45
- const previous = state.client;
46
- state.client = name || null;
47
- try { return await fn(); } finally { state.client = previous; }
54
+ // Runs fn as the named client. run is the command's record, whose cancelled the runner sets.
55
+ async function asClient(name, fn, run = {}) {
56
+ queued = Object.assign(run, { client: name || null });
57
+ try { return await running.run(run, fn); } finally { if (queued === run) queued = null; }
48
58
  }
49
59
 
50
60
  const cleanups = [];
61
+ const endings = [];
51
62
  const exitWaits = [];
52
63
  let shutdownPromise = null;
53
64
 
@@ -65,6 +76,11 @@ function onShutdown(fn) {
65
76
  cleanups.push(fn);
66
77
  }
67
78
 
79
+ // Run once every cleanup has finished, before the browser is closed.
80
+ function onShutdownEnd(fn) {
81
+ endings.push(fn);
82
+ }
83
+
68
84
  // Things to finish before the process exits, e.g. answers still being sent.
69
85
  function onBeforeExit(fn) {
70
86
  exitWaits.push(fn);
@@ -80,6 +96,7 @@ async function shutdown() {
80
96
  shutdownPromise = (async () => {
81
97
  // Some undo what the REPL changed in the browser, so they finish first.
82
98
  await withTimeout(Promise.all(cleanups.map(async fn => fn())), 'Cleanup').catch(() => {});
99
+ for (const fn of endings) try { await fn(); } catch {}
83
100
  if (state.browser) {
84
101
  try { await withTimeout(state.browser.close(), 'Chromium shutdown'); }
85
102
  catch (error) { state.shutdownFailed = true; process.exitCode = 1; out.error(`Could not confirm Chromium shutdown: ${error.message}`); }
@@ -88,4 +105,4 @@ async function shutdown() {
88
105
  return shutdownPromise;
89
106
  }
90
107
 
91
- module.exports = { state, clientRecord, asClient, withTimeout, onShutdown, onBeforeExit, beforeExit, shutdown, SHUTDOWN_TIMEOUT };
108
+ module.exports = { state, clientRecord, asClient, withTimeout, onShutdown, onShutdownEnd, onBeforeExit, beforeExit, shutdown, SHUTDOWN_TIMEOUT };
package/lib/toast.js CHANGED
@@ -7,6 +7,7 @@ const { unquote } = require('./syntax');
7
7
  const { takeOptions } = require('./cli-names');
8
8
  const { inOverlay, keepAcrossPages, forget } = require('./overlay');
9
9
  const { recordingOf, beforeRecordingEnds } = require('./record');
10
+ const { parseDuration } = require('./util');
10
11
 
11
12
  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
12
13
 
@@ -81,12 +82,6 @@ async function toastOff(p, fade = true) {
81
82
  beforeRecordingEnds(p => (p && !p.isClosed() ? waitToRead(p) : null));
82
83
 
83
84
  // 3s, 1.5s, 800ms, or a number of milliseconds.
84
- function parseDuration(value) {
85
- const match = /^(\d+(?:\.\d+)?)(ms|s)?$/.exec(value || '');
86
- if (!match) return null;
87
- return Math.round(Number(match[1]) * (match[2] === 's' ? 1000 : 1));
88
- }
89
-
90
85
  onShutdown(async () => {
91
86
  if (!state.browser || state.connectionLost) return;
92
87
  const pages = state.browser.contexts().flatMap(c => c.pages()).filter(p => !p.isClosed());
package/lib/util.js CHANGED
@@ -20,6 +20,28 @@ function hints(pairs) {
20
20
  return `\n${pairs.map(([command, what]) => ` ${command.padEnd(width)} ${what}`).join('\n')}`;
21
21
  }
22
22
 
23
+ // A time: milliseconds, as Playwright and JavaScript count it, or seconds with an s (1500, 1.5s, 1500ms).
24
+ const TIME = /^(\d+(?:\.\d+)?)(ms|s)?$/;
25
+
26
+ function parseDuration(value) {
27
+ const match = TIME.exec(value || '');
28
+ if (!match) return null;
29
+ return Math.round(Number(match[1]) * (match[2] === 's' ? 1000 : 1));
30
+ }
31
+
32
+ // How a time reads back: 1.5s, or 250ms under a second.
33
+ function durationText(ms) {
34
+ return ms >= 1000 ? `${Number((ms / 1000).toFixed(3))}s` : `${ms}ms`;
35
+ }
36
+
37
+ // A time that is a second or more, where a smaller one is most likely seconds written as before.
38
+ function longDuration(value, what) {
39
+ const ms = parseDuration(value);
40
+ if (ms === null) throw new Error(`${what} is a time, in milliseconds or with s for seconds: 30000 or 30s (got ${value})`);
41
+ if (ms < 1000) throw new Error(`${what} is in milliseconds, 1000 or more: ${value} is ${durationText(ms)}; ${value}s for seconds`);
42
+ return ms;
43
+ }
44
+
23
45
  function clipText(text) {
24
46
  return text.length > MAX_EVENT_TEXT ? `${text.slice(0, MAX_EVENT_TEXT)}…` : text;
25
47
  }
@@ -56,6 +78,8 @@ function clock(t) {
56
78
  // by itself unseen: stop prints those lines.
57
79
  const STOPPED_RECORDING = 'The REPL stopped, which ended a recording';
58
80
  const ENDED_UNSEEN = 'A recording ended before the REPL stopped, unseen since';
81
+ // Each line stop prints of the files a client left.
82
+ const FILES_LEFT = 'Files left';
59
83
 
60
84
  // Where Playwright installs its browsers and ffmpeg, worked out as playwright-core does it: a cache of
61
85
  // each platform's own (~/Library/Caches on macOS), unless PLAYWRIGHT_BROWSERS_PATH says otherwise.
@@ -69,4 +93,4 @@ function playwrightBrowsersDir() {
69
93
  return path.join(cache, 'ms-playwright');
70
94
  }
71
95
 
72
- module.exports = { BUSY_PAGE, STOPPED_RECORDING, ENDED_UNSEEN, COMMAND_TIMEOUT, MAX_EVENT_TEXT, NO_TAB, hints, clipText, numbers, regexOf, patternText, clock, playwrightBrowsersDir };
96
+ module.exports = { TIME, parseDuration, durationText, longDuration, BUSY_PAGE, STOPPED_RECORDING, ENDED_UNSEEN, FILES_LEFT, COMMAND_TIMEOUT, MAX_EVENT_TEXT, NO_TAB, hints, clipText, numbers, regexOf, patternText, clock, playwrightBrowsersDir };
package/lib/watch.js CHANGED
@@ -4,6 +4,7 @@ const out = require('./output');
4
4
  const { printOutput } = out;
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
+ const { noteWritten } = require('./written');
7
8
  const { clipText, clock, hints, NO_TAB } = require('./util');
8
9
  const { tabState } = require('./tabstate');
9
10
  const { keptSession, keepDrawing } = require('./cdp');
@@ -22,8 +23,8 @@ const PW_SETTINGS = ['PW_CDP_URL', 'PW_CHROME', 'PW_CLIENT', 'PW_ENDPOINT', 'PW_
22
23
  'PW_SOCKET', 'PW_START_URL', 'PW_TEST_CHROME', 'PW_TMUX_SESSION'];
23
24
  const PAGE_ACTIONS = new Set(['click', 'check', 'uncheck', 'select', 'fill', 'type', 'press', 'submit']);
24
25
  const WATCH_BINDING = '__pwReplWatch';
25
- // Seconds a saved step waits for its element after the app's own routing (watch save).
26
- const ROUTE_WAIT = 30;
26
+ // How long a saved step waits for its element after the app's own routing (watch save).
27
+ const ROUTE_WAIT = '30s';
27
28
 
28
29
  // Runs in the page. Describes elements the way snapshot does: role and name.
29
30
  const WATCH_SCRIPT = `(() => {
@@ -811,6 +812,7 @@ const commands = {
811
812
  if (variables.length) header.push(`# It needs ${variables.join(', ')} set in the environment: what was typed into ${where}, not saved here`);
812
813
  // It holds what was typed, so it is the user's own to read.
813
814
  fs.writeFileSync(file, `${[...header, ...lines].join('\n')}\n`, { mode: 0o600, flag: 'wx' });
815
+ noteWritten(file, 'watch file');
814
816
  out.log(`Saved ${watch.events.length} step${watch.events.length === 1 ? '' : 's'} to ${file}${skipped ? `; ${skipped} could not be written as commands, and are comments there saying why` : ''}.`);
815
817
  if (variables.length) out.log(`What was typed into ${where} is a variable there, filled in from the environment: ${variables.map(name => `{{ ${name} }}`).join(', ')}.`);
816
818
  out.log(`${variables.map(name => `${name}=... `).join('')}pw-repl send --file ${file} runs them.`);
package/lib/written.js ADDED
@@ -0,0 +1,67 @@
1
+ // The files the REPL wrote this session, and for whom, so close and stop can say which are still
2
+ // there to clean up. Kept in memory only, and nothing is removed.
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { state, onShutdownEnd } = require('./state');
6
+ const out = require('./output');
7
+ const { FILES_LEFT } = require('./util');
8
+
9
+ const written = new Map();
10
+
11
+ // kind: what it is, as the summary counts it (screenshot, video, steps file, profile, watch file).
12
+ function noteWritten(file, kind, client = state.client) {
13
+ written.set(path.resolve(file), { kind, client: client ?? null });
14
+ }
15
+
16
+ // A file renamed (record off <file>) is still the same one.
17
+ function noteMoved(from, to) {
18
+ const entry = written.get(path.resolve(from));
19
+ if (!entry) return;
20
+ written.delete(path.resolve(from));
21
+ written.set(path.resolve(to), entry);
22
+ }
23
+
24
+ function remaining(client) {
25
+ const files = [];
26
+ for (const [file, entry] of written) {
27
+ if (entry.client !== (client ?? null)) continue;
28
+ try { files.push({ file, kind: entry.kind, size: fs.statSync(file).size }); } catch {}
29
+ }
30
+ return files;
31
+ }
32
+
33
+ function sizeText(bytes) {
34
+ if (bytes < 1024 * 1024) return `${Math.max(1, Math.round(bytes / 1024))} KB`;
35
+ return `${Number((bytes / 1024 / 1024).toFixed(1))} MB`;
36
+ }
37
+
38
+ // For close: the client's own files, in full.
39
+ function ownFilesText(client) {
40
+ const files = remaining(client);
41
+ if (!files.length) return null;
42
+ return [`Files you wrote that are still there; remove what is no longer needed:`, ...files.map(f => ` ${f.file} (${sizeText(f.size)})`)].join('\n');
43
+ }
44
+
45
+ const plural = (n, kind) => `${n} ${kind}${n === 1 ? '' : 's'}`;
46
+
47
+ // For stop: one line for each client that left files, each with what and where.
48
+ function summaryLines() {
49
+ const clients = [...new Set([...written.values()].map(e => e.client))];
50
+ const lines = [];
51
+ for (const client of clients) {
52
+ const files = remaining(client);
53
+ if (!files.length) continue;
54
+ const kinds = new Map();
55
+ for (const f of files) kinds.set(f.kind, (kinds.get(f.kind) || 0) + 1);
56
+ const folders = [...new Set(files.map(f => `${path.dirname(f.file)}/`))];
57
+ const bytes = files.reduce((sum, f) => sum + f.size, 0);
58
+ const who = client ? `by ${client}` : 'without a client name';
59
+ lines.push(`${FILES_LEFT} ${who}: ${plural(files.length, 'file')}, ${sizeText(bytes)} (${[...kinds].map(([k, n]) => plural(n, k)).join(', ')}) in ${folders.join(' and ')}`);
60
+ }
61
+ return lines;
62
+ }
63
+
64
+ // After every recording has been saved, so its file is whole.
65
+ onShutdownEnd(() => { for (const line of summaryLines()) out.log(line); });
66
+
67
+ module.exports = { noteWritten, noteMoved, ownFilesText, summaryLines };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pw-repl",
3
- "version": "0.12.8",
3
+ "version": "0.13.0",
4
4
  "description": "Text REPL for driving an existing Chromium through Playwright over CDP",
5
5
  "keywords": [
6
6
  "playwright",
package/skill/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pw-repl
3
- description: Inspect and drive a Chromium browser, possibly one the user is using, through the pw-repl REPL - tabs, page snapshots, clicks and typing, the mouse at a point (moves, buttons, wheel), requests and their bodies, console messages, waiting for pages and elements, choosing files, faking, patching or delaying responses, cutting or slowing the network, emulating a phone, dark mode, a locale or a timezone, recording what the user does, profiling what makes a page slow (long tasks, forced layouts, input delay, dropped frames), and recording a video of a tab, with a pointer and on-screen captions. Use when asked to look at, debug or test something in a web page, in an existing Chromium with remote debugging or in one it starts itself.
3
+ description: Inspect and drive a Chromium browser, possibly one the user is using, through the pw-repl REPL - tabs, page snapshots, clicks and typing, the mouse at a point (moves, buttons, wheel), requests and their bodies, console messages, what a page copied to the clipboard, waiting for pages and elements, choosing files, faking, patching or delaying responses, cutting or slowing the network, emulating a phone, dark mode, a locale or a timezone, recording what the user does, profiling what makes a page slow (long tasks, forced layouts, input delay, dropped frames), and recording a video of a tab, with a pointer and on-screen captions. Use when asked to look at, debug or test something in a web page, in an existing Chromium with remote debugging or in one it starts itself.
4
4
  allowed-tools: Bash(pw-repl:*) Bash(npx pw-repl@latest:*)
5
5
  ---
6
6
 
@@ -93,7 +93,7 @@ client of your own: `PW_CLIENT=<name>` (or `send -c <name>`), with a name that s
93
93
  as `[server:<name>]` lines.
94
94
  - `modes` and `tab` say which client turned each mode on and opened each tab. `modes off --mine` turns
95
95
  off only what you turned on; `tab close <url-part>` closes a tab you opened.
96
- - Commands run one at a time, from every client: a long one (`capture on 60`, `sleep`) holds the rest.
96
+ - Commands run one at a time, from every client: a long one (`capture on 60s`, `sleep`) holds the rest.
97
97
 
98
98
  A REPL of your own, on a socket of its own, is for a browser of your own (`--launch`), or when you are
99
99
  asked for one. Several REPLs on one browser each see every tab but list only their own modes, and can
@@ -105,16 +105,19 @@ A REPL in a terminal stops at its prompt (`quit`, or Ctrl-C); `send quit` is ref
105
105
 
106
106
  ```bash
107
107
  pw-repl send tab
108
- pw-repl send -t 90 'screenshot -d 60' # wait longer than the 20s default
108
+ pw-repl send -t 90s 'screenshot -d 60s' # wait longer than the 20s default
109
109
  ```
110
110
 
111
+ Times, in send's `-t` and in every command, are in milliseconds, as in Playwright, or in seconds with an
112
+ `s`: `-t 90000` or `-t 90s`.
113
+
111
114
  Always send commands with `pw-repl send`; don't type into the pane yourself, except `quit` at the prompt
112
115
  of a REPL of your own, which `send` refuses. The words after `send` are the command. For `fill`, `type`,
113
116
  `select`, `press`, `upload` and `wait`, a word quoted in your shell stays one word
114
117
  (`pw-repl send fill "text=Your name" Ada`, `pw-repl send wait text "Showing 1"`); other commands get the words as they are, joined by
115
118
  spaces (`pw-repl send eval "document.title + ' x'"`). A command that takes only a selector takes the
116
119
  whole line, spaces and all: `pw-repl send click "text=Your name"`. `send`'s own options (`-t`, `-c`, `-e`,
117
- `-s`, `--no-vars`) go before the command or after it (`pw-repl send open <url> -t 30`); after `--`, every
120
+ `-s`, `--no-vars`) go before the command or after it (`pw-repl send open <url> -t 30s`); after `--`, every
118
121
  word is the command's. It works however the REPL was started:
119
122
 
120
123
  - `run` (in tmux): `send` types the command into the tmux pane and reads the result back off the screen.
@@ -227,6 +230,9 @@ pw-repl send -c <name> close # your modes off; tab close
227
230
  lists what is on in every tab (the prompt shows the selected tab's, e.g. `(watch routes:1) pw>`);
228
231
  `modes off` turns off every mode in every tab.
229
232
  - Someone may be attached to the REPL's tmux session; killing the session ends it for them too.
233
+ - What a page copied (a Copy button): click it, then `clipboard` prints it. In a headless browser
234
+ (`--launch`) that is the browser's own clipboard. In one with a window it is the user's, which may
235
+ hold a password, so `clipboard` refuses there without `--shared-clipboard`: ask the user first.
230
236
  - To point something out, to the user or in a screenshot, `highlight <ref|selector>` draws a box over it
231
237
  without changing the page (`--style=<css>` restyles it; `highlight off` hides them), without
232
238
  Playwright's locator label unless `highlight --labels on`.
@@ -249,6 +255,8 @@ pw-repl send -c <name> close # your modes off; tab close
249
255
  not by `send`. Without it, a REPL on a socket of its own saves them next to its socket, and any other
250
256
  in `/tmp`. `screenshot` prints each file's path; other REPLs may save theirs in the same place. Remove
251
257
  only your own, and keep those too if they are needed, e.g. as evidence or as something to hand over.
258
+ `close` (as a client, `-c <name>`) lists every file you wrote that is still there: screenshots,
259
+ videos, saved profiles and watches.
252
260
  - `PW_ENDPOINT` / `PW_TMUX_SESSION` — defaults for `send -e` / `-s`. `PW_SOCKET` — the socket `serve`
253
261
  serves on and `send` looks for when neither is given (default `/tmp/pw-repl.sock`).
254
262