pw-repl 0.4.2 → 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 +2 -2
- package/lib/background.js +4 -3
- package/lib/cli-names.js +1 -3
- package/lib/client.js +6 -1
- package/lib/commands.js +342 -36
- package/lib/help.js +28 -16
- package/lib/runner.js +1 -0
- package/lib/send.js +9 -0
- package/lib/server.js +9 -2
- package/lib/skill.js +29 -0
- package/lib/start.js +5 -1
- package/lib/state.js +3 -0
- package/package.json +1 -1
- package/skill/SKILL.md +35 -12
package/lib/help.js
CHANGED
|
@@ -44,8 +44,8 @@ const TOPICS = {
|
|
|
44
44
|
commands: ['tab', 'goto', 'back', 'forward', 'reload', 'info'],
|
|
45
45
|
},
|
|
46
46
|
interact: {
|
|
47
|
-
intro: 'Selectors are Playwright selectors (CSS, text=..., role=...) or snapshot refs such as e5.\nCommands use the first match; see help
|
|
48
|
-
commands: ['click', 'dblclick', 'hover', 'fill', 'type', 'press', 'select', 'check', 'uncheck', 'upload'],
|
|
47
|
+
intro: 'Selectors are Playwright selectors (CSS, text=..., role=...) or snapshot refs such as e5.\nCommands use the first match (click, dblclick and hover the first they can use: help click); see help\nfill for selectors with spaces. mousemove and the other mouse commands act at a point instead.',
|
|
48
|
+
commands: ['click', 'dblclick', 'hover', 'fill', 'type', 'press', 'select', 'check', 'uncheck', 'upload', 'mousemove', 'mousedown', 'mouseup', 'mousewheel'],
|
|
49
49
|
},
|
|
50
50
|
inspect: {
|
|
51
51
|
intro: 'Output is capped; --all right after the command, or at the end, shows everything (text --all body).',
|
|
@@ -91,7 +91,7 @@ const COMMANDS = {
|
|
|
91
91
|
goto: {
|
|
92
92
|
usage: 'goto <url>',
|
|
93
93
|
summary: 'navigate the selected tab',
|
|
94
|
-
detail: 'Without a scheme, http:// is assumed for localhost, 127.x and [::1], and https:// otherwise, as in\nthe address bar. tab new <url> reads its URL the same way.',
|
|
94
|
+
detail: 'Without a scheme, http:// is assumed for localhost, 127.x and [::1], and https:// otherwise, as in\nthe address bar. tab new <url> reads its URL the same way.\n\nA page that does not load says why, as wait load does: a failed request, after which the tab shows\nChrome\'s error page (info names the URL that failed), or an answer with no page (a 204), which\nleaves the tab where it was. reload, back and forward say it the same way.',
|
|
95
95
|
},
|
|
96
96
|
back: {
|
|
97
97
|
usage: 'back',
|
|
@@ -103,11 +103,23 @@ const COMMANDS = {
|
|
|
103
103
|
info: { usage: 'info', summary: 'selected tab URL, title and viewport' },
|
|
104
104
|
click: {
|
|
105
105
|
usage: 'click <selector> [button]',
|
|
106
|
-
summary: 'click the first match',
|
|
107
|
-
detail: 'After a ref or a quoted selector, a button (left, right, middle) may follow: click e5 right.\n--modifiers=<key>[,<key>] holds keys down meanwhile (Alt, Control, ControlOrMeta, Meta, Shift).',
|
|
106
|
+
summary: 'click the first match that can be clicked',
|
|
107
|
+
detail: 'After a ref or a quoted selector, a button (left, right, middle) may follow: click e5 right.\n--modifiers=<key>[,<key>] holds keys down meanwhile (Alt, Control, ControlOrMeta, Meta, Shift).\n\nWhen a selector matches several elements, hidden ones and ones covered by something else (a dialog\'s\noverlay) are passed over, and it says which match it used: Clicked: text=Buy (match 3 of 4; ...). If\nnone can be clicked within 5s, it says why for each and does nothing. A ref, or a single match, is\nclicked as it is, waiting up to 5s for it to be ready.\n\nTimed out after it began acting, it says what stopped it (e.g. <div class="overlay"> intercepts\npointer events) and that the outcome is unknown; Playwright\'s full call log goes to the REPL\'s pane\nor log.',
|
|
108
|
+
},
|
|
109
|
+
dblclick: { usage: 'dblclick <selector> [button]', summary: 'double-click, choosing the match as click does' },
|
|
110
|
+
hover: { usage: 'hover <selector>', summary: 'hover, choosing the match as click does' },
|
|
111
|
+
mousemove: {
|
|
112
|
+
usage: 'mousemove <x> <y>',
|
|
113
|
+
summary: 'move the mouse to a point in the viewport',
|
|
114
|
+
detail: 'x and y are CSS pixels from the top left of the tab\'s viewport (not the page), as mouse events\nhave them. A snapshot has no coordinates; eval of getBoundingClientRect gives an element\'s centre:\neval (r => [r.x + r.width / 2, r.y + r.height / 2])(document.querySelector("#map")\n .getBoundingClientRect())\n\nA click at a point: mousemove <x> <y>, mousedown, mouseup. A drag: mousedown, then mousemove to\nanother point, then mouseup.',
|
|
115
|
+
},
|
|
116
|
+
mousedown: { usage: 'mousedown [button]', summary: 'press a mouse button where the mouse is (left, right or middle)' },
|
|
117
|
+
mouseup: { usage: 'mouseup [button]', summary: 'release a mouse button where the mouse is' },
|
|
118
|
+
mousewheel: {
|
|
119
|
+
usage: 'mousewheel <dx> <dy>',
|
|
120
|
+
summary: 'turn the wheel where the mouse is',
|
|
121
|
+
detail: 'Wheel events at the mouse\'s point (mousemove puts it there), in CSS pixels: positive dy scrolls\ndown. A map zooms around that point: mousemove 400 300, then mousewheel 0 -300 zooms in. It returns\nonce the events are sent, not once the page has scrolled or redrawn.',
|
|
108
122
|
},
|
|
109
|
-
dblclick: { usage: 'dblclick <selector> [button]', summary: 'double-click the first match; button and --modifiers as for click' },
|
|
110
|
-
hover: { usage: 'hover <selector>', summary: 'hover the first match' },
|
|
111
123
|
fill: {
|
|
112
124
|
usage: 'fill <selector> <value> [--submit]',
|
|
113
125
|
summary: 'clear an input and fill it',
|
|
@@ -130,12 +142,12 @@ const COMMANDS = {
|
|
|
130
142
|
snapshot: {
|
|
131
143
|
usage: 'snapshot [--full] [--grep <text> | <eN> | selector]',
|
|
132
144
|
summary: 'outline by role and name, with [ref=eN] labels',
|
|
133
|
-
detail: 'Playwright\'s accessibility snapshot, with unnamed layout wrappers (generic) and cursor hints left\nout; --full shows it unchanged.\n\n--grep <text> prints only the lines containing text (role, name or flag such as [disabled], any\ncase), each with the named elements around it. A hit with no named element around it prints without\na path. A hit on text (a label) keeps the ref of the element holding it, so snapshot <that ref>\nshows what sits beside it, such as the value next to the label. --regex <pattern> is --grep by a\nregular expression: snapshot --regex "Total: \\d+".\n\nsnapshot e3 outlines one element. A ref (e3, or f1e3
|
|
145
|
+
detail: 'Playwright\'s accessibility snapshot, with unnamed layout wrappers (generic) and cursor hints left\nout; --full shows it unchanged.\n\n--grep <text> prints only the lines containing text (role, name or flag such as [disabled], any\ncase), each with the named elements around it. A hit with no named element around it prints without\na path. A hit on text (a label) keeps the ref of the element holding it, so snapshot <that ref>\nshows what sits beside it, such as the value next to the label. --regex <pattern> is --grep by a\nregular expression: snapshot --regex "Total: \\d+".\n\nAn element with no role or name of its own (a card that is only a div) is left out, with its ref;\nsnapshot --full keeps it.\n\nsnapshot e3 outlines one element, and the refs of the last snapshot keep working. A ref (e3, or f1e3\nin newer Playwright) works as a selector in any command: click e3. Refs can change when the page\nchanges (e102 may become f4e98), so take a new snapshot after it does. snapshot <selector> (not a\nref) gives new refs, and those from before stop working.\n\nOutput over 60 lines ends with a line count.',
|
|
134
146
|
},
|
|
135
147
|
watch: {
|
|
136
148
|
usage: 'watch on [--changes] [--live] | off | [n|new]',
|
|
137
149
|
summary: 'record what happens in the tab; show the last n steps',
|
|
138
|
-
detail: 'Off until watch on; a watch on after watch off starts a new recording. Records clicks, typing (once\nit pauses), Enter and Escape, form changes, submits and navigations, each described by role and name\nlike snapshot, with up to 5 of the requests it caused underneath, each with its status as in requests\n(body <#> for one), leaving out scripts and what requests hides. The REPL\'s own fill is recorded\nas type.\n\nFrom its first watch on, a tab keeps response bodies of up to 100 KB, so body <#> still shows one\nafter the tab navigates, even when the page navigates as soon as the request answers.\n\nwatch on --next-tab [url-part] returns at once, then waits for the next tab someone opens (tab new,\na client\'s own, does not count), or the next whose URL contains url-part, and watches it from its\nfirst page and selects it for the client that asked: for a tab someone is about to open. It takes\n--changes and --live too; watch off stops waiting, and modes lists it.\n\nwatch on its own says whether it is on and shows the last 20 steps; watch <n> shows the last n.\nwatch new shows only the steps it has not shown yet, and requests that have since arrived for the\nlast one.\n\n--live also prints each step in the REPL window (never in the server\'s answer to a command) once it\nsettles, with its requests and changes, marked [watch], or [watch <url>] for a tab that is not the\nselected one. A step cut short by the next one, or by watch off, prints at once; changes it made\nthen show under the next step.\n\n--changes also shows what each step changed on the page once it settles, in up to 5 lines: + added,\n- removed, ~ changed, each element once with the first names inside it. Not for navigations: what a\npage changes on its own after a step settles (data arriving later) shows under the next step.\n\nA watch belongs to the tab, not to a client: every client sees the same steps, watch new\'s place is\nshared, and anyone\'s watch off stops it.\n\nTyped values are not recorded, and password fields not at all; --changes leaves out field values,\nbut what the page itself shows (e.g. "Hello <name>") is shown. Nothing is replayed.',
|
|
150
|
+
detail: 'Off until watch on; a watch on after watch off starts a new recording. Records clicks, typing (once\nit pauses), Enter and Escape, form changes, submits and navigations, each described by role and name\nlike snapshot, with up to 5 of the requests it caused underneath, each with its status as in requests\n(body <#> for one), leaving out scripts and what requests hides. The REPL\'s own fill is recorded\nas type. A double-click shows as two clicks; hovering and moving the mouse are not recorded.\n\nFrom its first watch on, a tab keeps response bodies of up to 100 KB, so body <#> still shows one\nafter the tab navigates, even when the page navigates as soon as the request answers.\n\nwatch on --next-tab [url-part] returns at once, then waits for the next tab someone opens (tab new,\na client\'s own, does not count), or the next whose URL contains url-part, and watches it from its\nfirst page and selects it for the client that asked: for a tab someone is about to open. It takes\n--changes and --live too; watch off stops waiting, and modes lists it.\n\nwatch on its own says whether it is on and shows the last 20 steps; watch <n> shows the last n.\nwatch new shows only the steps it has not shown yet, and requests that have since arrived for the\nlast one.\n\n--live also prints each step in the REPL window (never in the server\'s answer to a command) once it\nsettles, with its requests and changes, marked [watch], or [watch <url>] for a tab that is not the\nselected one. A step cut short by the next one, or by watch off, prints at once; changes it made\nthen show under the next step.\n\n--changes also shows what each step changed on the page once it settles, in up to 5 lines: + added,\n- removed, ~ changed, each element once with the first names inside it. Not for navigations: what a\npage changes on its own after a step settles (data arriving later) shows under the next step.\n\nA watch belongs to the tab, not to a client: every client sees the same steps, watch new\'s place is\nshared, and anyone\'s watch off stops it.\n\nTyped values are not recorded, and password fields not at all; --changes leaves out field values,\nbut what the page itself shows (e.g. "Hello <name>") is shown. Nothing is replayed.',
|
|
139
151
|
},
|
|
140
152
|
text: { usage: 'text [--all] <selector>', summary: 'visible text of the first match' },
|
|
141
153
|
html: { usage: 'html [--all] <selector>', summary: 'outer HTML of the first match' },
|
|
@@ -143,7 +155,7 @@ const COMMANDS = {
|
|
|
143
155
|
listeners: {
|
|
144
156
|
usage: 'listeners <selector>|document|window',
|
|
145
157
|
summary: 'the page\'s event listeners on an element',
|
|
146
|
-
detail: '
|
|
158
|
+
detail: 'For the first match, one line each: the event, how it was added (capture, once, passive), the\nstart of the handler, where ↵ marks a line break, and its line in its script. --all shows each handler\nwhole, as written. Listeners added on a parent (e.g. document, for delegation) are not the element\'s\nown: check listeners document too. Those Playwright adds to window as it acts are left out, and it\nsays how many.',
|
|
147
159
|
},
|
|
148
160
|
count: { usage: 'count <selector>', summary: 'number of matches' },
|
|
149
161
|
visible: { usage: 'visible <selector>', summary: 'whether the first match is visible' },
|
|
@@ -152,7 +164,7 @@ const COMMANDS = {
|
|
|
152
164
|
screenshot: {
|
|
153
165
|
usage: 'screenshot [<ref>] [--full] [-d <secs>] [name]',
|
|
154
166
|
summary: 'save a PNG of the viewport, --full page or an element',
|
|
155
|
-
detail: 'Saved as screenshot-<name or timestamp>.png in $PW_SCREENSHOT_DIR; without it, next to the REPL\'s\nsocket when it serves on one of its own, or else in /tmp. --filename=<file> saves it there instead (a\nJPEG if it ends in .jpg), relative to the folder pw-repl send runs in; it never replaces a file.\n\nscreenshot e5 saves only that element, from a snapshot ref. --full-page is --full.\n\nIt brings the tab to the front of its window first: Chrome draws only the tab in front. The image\nis at CSS pixel size, one image pixel per CSS pixel
|
|
167
|
+
detail: 'Saved as screenshot-<name or timestamp>.png in $PW_SCREENSHOT_DIR; without it, next to the REPL\'s\nsocket when it serves on one of its own, or else in /tmp. --filename=<file> saves it there instead (a\nJPEG if it ends in .jpg), relative to the folder pw-repl send runs in; it never replaces a file.\n\nscreenshot e5 saves only that element, from a snapshot ref. --full-page is --full.\n\nIt brings the tab to the front of its window first: Chrome draws only the tab in front. The image\nis at CSS pixel size, one image pixel per CSS pixel. While emulate mobile is on, it is what the\nphone shows: a page laid out wider than the phone (no viewport meta tag) comes out shrunk to fit.\n\n--delay counts down out loud first (maximum 60s), so someone can hold a hover or open a menu.',
|
|
156
168
|
},
|
|
157
169
|
viewport: { usage: 'viewport [WxH]', summary: 'show or set the viewport size' },
|
|
158
170
|
emulate: {
|
|
@@ -163,13 +175,13 @@ const COMMANDS = {
|
|
|
163
175
|
wait: {
|
|
164
176
|
usage: 'wait [text|request] <what> [--gone] [secs]',
|
|
165
177
|
summary: 'wait for an element, text, a response or a load (10s)',
|
|
166
|
-
detail: 'wait <selector> waits for a matching element to be visible, and wait text <text> for text to be;\nwait request <url-part|glob> waits for a matching response, counting one that finished since the\nprevious command began (so click, then wait request, does not miss it).\n\nwait load waits for the page to finish loading (its load event). A navigation that began since the\nprevious command began counts, so click a link, then wait load, waits for the new page. A page\nthat changes its own URL without loading (an app\'s own routing) is not a load: wait for something on\nthe new view instead.\n\nwait text matches any part of an element\'s text, in any case.\n\n--gone waits instead until nothing matching is visible (removed or hidden): wait .spinner --gone.\nIt is done at once if nothing matches yet, so wait for it to appear first if it may not have.\n\nUp to 120s. A wait that times out is an error; the REPL carries on.',
|
|
178
|
+
detail: 'wait <selector> waits for a matching element to be visible, and wait text <text> for text to be;\nwait request <url-part|glob> waits for a matching response, counting one that finished since the\nprevious command began, or is still under way (so click, then wait request, does not miss it).\n\nwait load waits for the page to finish loading (its load event). A navigation that began since the\nprevious command began counts, so click a link, then wait load, waits for the new page. A page\nthat changes its own URL without loading (an app\'s own routing) is not a load: wait for something on\nthe new view instead.\n\nwait text matches any part of an element\'s text, in any case.\n\n--gone waits instead until nothing matching is visible (removed or hidden): wait .spinner --gone.\nIt is done at once if nothing matches yet, so wait for it to appear first if it may not have.\n\nUp to 120s. A wait that times out is an error; the REPL carries on.',
|
|
167
179
|
},
|
|
168
180
|
sleep: { usage: 'sleep <ms>', summary: 'wait a fixed time, in milliseconds (maximum 3600000)' },
|
|
169
181
|
requests: {
|
|
170
182
|
usage: 'requests [--all] [n] [url-filter]',
|
|
171
183
|
summary: 'the selected tab\'s last n requests (default 20)',
|
|
172
|
-
detail: 'Recording starts when the REPL connects; the last 200 per tab are kept. Each line: #number, time
|
|
184
|
+
detail: 'Recording starts when the REPL connects; the last 200 per tab are kept, of every kind the page\nrequests (not the browser\'s own, such as the favicon). Each line: #number, time, method, status (HTTP\ncode, pending, no response, failed: <reason>, <code>, then failed: <reason> when its body was cut\noff, or <code> faked), duration, kind (document, fetch, xhr, script, image, ...), URL. A request that\nfinished a moment ago can still show pending.\n\nn is how many of the latest to show (default 20). When more match, a first line says so: (last 20\nof 112 kept; requests 112 shows them all). A page often makes more than 20 in one load. A line\n"--- the page loads <url>" marks where each page load starts, even when a filter hides that request.\n\nbody <#> shows what a request got back.\n\nImages, fonts, stylesheets, media and extension requests are hidden unless --all is given.\n\nurl-filter is a substring: requests 20 /api/ shows API calls only, e.g. when dev-server scripts\ncrowd the list. --regex <pattern>, at the end, matches URLs by a regular expression instead.',
|
|
173
185
|
},
|
|
174
186
|
body: {
|
|
175
187
|
usage: 'body [--all] <#>|<url-part>',
|
|
@@ -179,7 +191,7 @@ const COMMANDS = {
|
|
|
179
191
|
console: {
|
|
180
192
|
usage: 'console [--all] [n] [level|filter]',
|
|
181
193
|
summary: 'the last n console messages and page errors',
|
|
182
|
-
detail: 'Recording starts when the REPL connects; the last 200 per tab are kept. Each line: time, [type],\ntext. Types are console levels (log, warning, error, ...) and pageerror for uncaught exceptions. A\n"--- the page loaded <url>" line marks each load after the first message shown.\n\nA level (error, warning, info, debug) shows that level and those above it: console warning shows\nwarnings and errors, page errors among them. info, which leaves out debug, is what playwright-cli\nshows by default. Anything else is a filter, matching part of the type or the text.\n\nA page brought back from the back/forward cache (back, forward) reports its earlier messages again,\nat the time it comes back, though nothing loaded again.',
|
|
194
|
+
detail: 'Recording starts when the REPL connects; the last 200 per tab are kept. Each line: time, [type],\ntext. Types are console levels (log, warning, error, ...) and pageerror for uncaught exceptions. A\n"--- the page loaded <url>" line marks each load after the first message shown.\n\nA level (error, warning, info, debug) shows that level and those above it: console warning shows\nwarnings and errors, page errors among them. info, which leaves out debug, is what playwright-cli\nshows by default. Anything else is a filter, matching part of the type or the text.\n\nn is how many of the latest to show (default 20); when more match, a first line says so.\n\nA page brought back from the back/forward cache (back, forward) reports its earlier messages again,\nat the time it comes back, though nothing loaded again.',
|
|
183
195
|
},
|
|
184
196
|
capture: {
|
|
185
197
|
usage: 'capture on [requests|console] [secs] | off',
|
|
@@ -189,7 +201,7 @@ const COMMANDS = {
|
|
|
189
201
|
route: {
|
|
190
202
|
usage: 'route <glob> <how> | off <glob>|--all',
|
|
191
203
|
summary: 'fake, patch, delay or fail the selected tab\'s matching requests',
|
|
192
|
-
detail: 'route <glob> <status> <json> answer with this status (200-599) and JSON, or no body; it never\n reaches the network, so it still answers while the network is off.\n The body is the rest of the line. Another kind needs its type:\n route <glob> 200 --content-type=text/plain hello\nroute <glob> patch <json> let it through, then change its JSON response: a JSON Merge Patch, where\n objects merge, null removes a key and anything else replaces\nroute <glob> delay <secs> hold it for up to 120 seconds, then let it through\nroute <glob> abort fail it as if the connection broke\n\nEach matching request prints a line (Faked:, Patched:, Delayed:, Aborted:) in the REPL window, and in\nthe answer to a command running then, with its number as in requests; requests shows a fake
|
|
204
|
+
detail: 'route <glob> <status> <json> answer with this status (200-599) and JSON, or no body; it never\n reaches the network, so it still answers while the network is off.\n The body is the rest of the line. Another kind needs its type:\n route <glob> 200 --content-type=text/plain hello\nroute <glob> patch <json> let it through, then change its JSON response: a JSON Merge Patch, where\n objects merge, null removes a key and anything else replaces; a\n patch that is not an object (an array) replaces the whole body\nroute <glob> delay <secs> hold it for up to 120 seconds, then let it through\nroute <glob> abort fail it as if the connection broke\n\nEach matching request prints a line (Faked:, Patched:, Delayed:, Aborted:) in the REPL window, and in\nthe answer to a command running then, with its number as in requests (one the page makes after reload or\na click has answered shows only in the window: wait request <glob> waits for it); requests shows a fake\nas <status> faked and a patch as <status> patched. If a route fails it prints "Route failed" and aborts\nthe request.\n\nRoutes belong to the tab and last until route off <glob> (or route off --all) or the REPL exits.\nRouting the same glob again replaces it. route on its own lists the selected tab\'s routes. A glob may\nbe quoted.\n\nExample: route **/api/cart patch {"total": 0}',
|
|
193
205
|
},
|
|
194
206
|
network: {
|
|
195
207
|
usage: 'network [on|off|slow [<ms> [<kbps>]]]',
|
|
@@ -221,7 +233,7 @@ const COMMANDS = {
|
|
|
221
233
|
help: { usage: 'help [topic | command | --all]', summary: 'this help; --all prints every topic and command in full' },
|
|
222
234
|
quit: {
|
|
223
235
|
usage: 'quit',
|
|
224
|
-
summary: 'disconnect, leaving Chromium running',
|
|
236
|
+
summary: 'disconnect, leaving Chromium running (one --launch started stops too)',
|
|
225
237
|
detail: 'Runs immediately, even while another command is waiting. Only available at the prompt.',
|
|
226
238
|
},
|
|
227
239
|
};
|
package/lib/runner.js
CHANGED
|
@@ -44,6 +44,7 @@ function parseInput(line) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function isUncertain(error) {
|
|
47
|
+
if (typeof error.uncertain === 'boolean') return error.uncertain;
|
|
47
48
|
const message = error.message || '';
|
|
48
49
|
if (!/timed out|timeout|connection (?:lost|closed)/i.test(message)) return false;
|
|
49
50
|
// Playwright's call log shows whether the element was ever found; if not, nothing was done.
|
package/lib/send.js
CHANGED
|
@@ -100,6 +100,7 @@ async function send(options) {
|
|
|
100
100
|
if (way.kind === 'tmux' && options.client) return fail('-c needs the command server (pw-repl serve); a REPL in tmux has no clients');
|
|
101
101
|
if (way.kind === 'tmux') return viaTmux(way.session, command, timeoutMs);
|
|
102
102
|
const answer = await client.request(way.endpoint, command, timeoutMs, name);
|
|
103
|
+
if (answer.result?.starting) return fail(`${answer.result.output} (pw-repl where says when it serves)`);
|
|
103
104
|
if (answer.result) {
|
|
104
105
|
if (answer.result.output) console.log(answer.result.output);
|
|
105
106
|
if (answer.result.unconfirmed) { console.error('completion not confirmed'); return 2; }
|
|
@@ -115,6 +116,9 @@ async function send(options) {
|
|
|
115
116
|
|
|
116
117
|
// Reports the route a command would take, checking it the way a command would.
|
|
117
118
|
async function where(options) {
|
|
119
|
+
// First, whatever else is reachable: a saved skill is compared with it (pw-repl skill: Start it).
|
|
120
|
+
const skill = require('./skill').current();
|
|
121
|
+
console.log(`skill: ${skill.hash} (pw-repl ${skill.version})`);
|
|
118
122
|
const way = route(options);
|
|
119
123
|
const info = way.kind === 'server' ? await client.healthInfo(way.endpoint, 5000) : null;
|
|
120
124
|
// The browser a running REPL uses (one it launched has its own address); otherwise the one it would
|
|
@@ -126,6 +130,11 @@ async function where(options) {
|
|
|
126
130
|
console.log(version ? `browser: ${cdp} answers (${version.Browser}${whose})` : `browser: nothing answers at ${cdp}; pw-repl run says how to start one`);
|
|
127
131
|
if (way.kind === 'server') {
|
|
128
132
|
const name = client.describe(way.endpoint);
|
|
133
|
+
if (info?.status === 'starting') {
|
|
134
|
+
// Not an error, but not serving yet: exit 64 as for no REPL, so a loop on where waits for it.
|
|
135
|
+
console.log(`server: ${name} (the REPL was started with pw-repl serve and is starting: ${info.step || 'starting'}; retry shortly)`);
|
|
136
|
+
return 64;
|
|
137
|
+
}
|
|
129
138
|
if (info) {
|
|
130
139
|
const background = require('./background').describeBackground(way.endpoint);
|
|
131
140
|
console.log(`server: ${name} (the REPL was started with pw-repl serve${background ? ' --background' : ''})`);
|
package/lib/server.js
CHANGED
|
@@ -31,7 +31,7 @@ function handle(req, res) {
|
|
|
31
31
|
// cross-origin POST, and cannot send a JSON content type without a
|
|
32
32
|
// preflight this server never answers.
|
|
33
33
|
if (req.headers.origin) return send(res, 403, { status: 'error', output: 'Requests from web pages are refused' });
|
|
34
|
-
if (req.method === 'GET' && req.url === '/health') return send(res, 200, { status: 'ok', browser: state.cdpUrl, launched: state.launched });
|
|
34
|
+
if (req.method === 'GET' && req.url === '/health') return send(res, 200, { status: state.serving ? 'ok' : 'starting', step: state.serving ? undefined : state.startingStep, browser: state.cdpUrl, launched: state.launched });
|
|
35
35
|
if (req.method !== 'POST' || req.url !== '/run') return send(res, 404, { status: 'error', output: 'Use POST /run or GET /health' });
|
|
36
36
|
if (!/^application\/json\b/.test(req.headers['content-type'] || '')) return send(res, 415, { status: 'error', output: 'Content-Type must be application/json' });
|
|
37
37
|
let body = '';
|
|
@@ -50,6 +50,8 @@ function handle(req, res) {
|
|
|
50
50
|
if (client !== undefined && client !== null && !CLIENT_NAME.test(client)) {
|
|
51
51
|
return send(res, 400, { status: 'error', output: 'A client name is 1-40 letters, digits, dots, dashes and underscores' });
|
|
52
52
|
}
|
|
53
|
+
// Bound before the browser is reached, so where and send find it while it starts.
|
|
54
|
+
if (!state.serving) return send(res, 503, { status: 'error', starting: true, output: `The REPL is starting (${state.startingStep}); retry shortly` });
|
|
53
55
|
const result = await runner.submit(command.trim(), client || null);
|
|
54
56
|
if (!result.uncertain) return send(res, 200, { status: result.status, output: result.output });
|
|
55
57
|
// unconfirmed: the command may or may not have done what it was sent to do.
|
|
@@ -71,6 +73,8 @@ async function clearStaleSocket(socketPath) {
|
|
|
71
73
|
fs.unlinkSync(socketPath);
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
// Listens before the REPL connects, answering "starting" until the function it returns is called,
|
|
77
|
+
// so a REPL that is still connecting is not taken for one without a server.
|
|
74
78
|
async function serve(endpointArg) {
|
|
75
79
|
const endpoint = parseEndpoint(endpointArg);
|
|
76
80
|
if (endpoint.socket) await clearStaleSocket(endpoint.socket);
|
|
@@ -90,7 +94,10 @@ async function serve(endpointArg) {
|
|
|
90
94
|
const removeSocket = () => { if (endpoint.socket) try { fs.unlinkSync(endpoint.socket); } catch {} };
|
|
91
95
|
onShutdown(() => { server.close(); removeSocket(); });
|
|
92
96
|
process.on('exit', removeSocket);
|
|
93
|
-
|
|
97
|
+
return () => {
|
|
98
|
+
state.serving = true;
|
|
99
|
+
out.log(`Serving commands on ${describe(endpoint)}`);
|
|
100
|
+
};
|
|
94
101
|
}
|
|
95
102
|
|
|
96
103
|
module.exports = { serve };
|
package/lib/skill.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// The agent skill, stamped as it is printed with the version it came from and a hash of its text,
|
|
2
|
+
// so a saved copy can be told apart from the skill of the pw-repl being run.
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const FILE = path.join(__dirname, '..', 'skill', 'SKILL.md');
|
|
8
|
+
const STAMP = '<!-- pw-repl skill stamp -->';
|
|
9
|
+
|
|
10
|
+
// Only the skill's own text counts: not the stamp line, nor the Custom rules a person adds to a
|
|
11
|
+
// saved copy. Line endings are made the same first, so a Windows checkout gets the same hash.
|
|
12
|
+
function hashOf(text) {
|
|
13
|
+
const own = text.replace(/\r\n?/g, '\n').split('\n## Custom rules\n')[0]
|
|
14
|
+
.split('\n').filter(line => line !== STAMP && !/^This skill is from pw-repl /.test(line)).join('\n').trimEnd();
|
|
15
|
+
return crypto.createHash('sha256').update(own).digest('hex').slice(0, 8);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function current() {
|
|
19
|
+
const text = fs.readFileSync(FILE, 'utf8');
|
|
20
|
+
return { text, hash: hashOf(text), version: require('../package.json').version };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// What `pw-repl skill` prints.
|
|
24
|
+
function stamped() {
|
|
25
|
+
const { text, hash, version } = current();
|
|
26
|
+
return text.replace(STAMP, `This skill is from pw-repl ${version} (skill ${hash}).`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { STAMP, hashOf, current, stamped };
|
package/lib/start.js
CHANGED
|
@@ -29,7 +29,9 @@ Or let pw-repl start a private one for this REPL: pw-repl run --launch (or serve
|
|
|
29
29
|
async function start(options) {
|
|
30
30
|
const START_URL = options.startUrl || process.env.PW_START_URL || null;
|
|
31
31
|
if (options.pidFile) require('./background').markStopInLog();
|
|
32
|
+
const serving = options.serve ? await require('./server').serve(options.endpoint) : null;
|
|
32
33
|
if (options.launch) {
|
|
34
|
+
state.startingStep = 'launching a Chromium of its own';
|
|
33
35
|
const launched = await require('./launch').launch({ headed: options.headed, extraArgs: options.chromeArgs });
|
|
34
36
|
onBeforeExit(launched.stop);
|
|
35
37
|
CDP_URL = launched.url;
|
|
@@ -41,6 +43,7 @@ async function start(options) {
|
|
|
41
43
|
state.cdpUrl = CDP_URL;
|
|
42
44
|
state.launched = !!options.launch;
|
|
43
45
|
out.log(`Connecting to ${CDP_URL}...`);
|
|
46
|
+
state.startingStep = 'connecting to the browser';
|
|
44
47
|
try { state.browser = await chromium.connectOverCDP(CDP_URL, { timeout: CONNECT_TIMEOUT }); }
|
|
45
48
|
catch (error) { throw new Error(connectHelp(error)); }
|
|
46
49
|
if (state.stopping) {
|
|
@@ -72,6 +75,7 @@ async function start(options) {
|
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
if (START_URL) {
|
|
78
|
+
state.startingStep = 'opening the start URL';
|
|
75
79
|
state.page = await openTab();
|
|
76
80
|
startTabs.add(state.page);
|
|
77
81
|
await state.page.goto(START_URL, { waitUntil: 'networkidle', timeout: 15000 });
|
|
@@ -82,7 +86,7 @@ async function start(options) {
|
|
|
82
86
|
await listTabs();
|
|
83
87
|
out.log('');
|
|
84
88
|
if (!state.page) out.log('No tab is selected: tab new [url] opens one of your own, tab <index|url-part> selects one.');
|
|
85
|
-
if (
|
|
89
|
+
if (serving) serving();
|
|
86
90
|
// In the background there is no prompt: commands come only through the server.
|
|
87
91
|
if (options.pidFile) {
|
|
88
92
|
require('./background').claimPidFile(options.pidFile);
|
package/lib/state.js
CHANGED
|
@@ -14,6 +14,9 @@ const state = {
|
|
|
14
14
|
// The browser the REPL is connected to, and whether --launch started it.
|
|
15
15
|
cdpUrl: null,
|
|
16
16
|
launched: false,
|
|
17
|
+
// The command server takes commands; before, it answers that the REPL is starting, and what it is doing.
|
|
18
|
+
serving: false,
|
|
19
|
+
startingStep: 'starting',
|
|
17
20
|
stopping: false,
|
|
18
21
|
connectionLost: false,
|
|
19
22
|
shutdownFailed: false,
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pw-repl
|
|
3
|
-
description: Inspect and drive a Chromium browser, possibly one a person is using, through the pw-repl (playwright-repl) REPL - tabs, page snapshots, clicks and typing, 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, and recording what the person does. 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
|
-
allowed-tools: Bash(pw-repl:*) Bash(npx pw-repl:*)
|
|
3
|
+
description: Inspect and drive a Chromium browser, possibly one a person is using, through the pw-repl (playwright-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, and recording what the person does. 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
|
+
allowed-tools: Bash(pw-repl:*) Bash(npx pw-repl@latest:*)
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# pw-repl
|
|
8
8
|
|
|
9
|
+
<!-- pw-repl skill stamp -->
|
|
10
|
+
|
|
9
11
|
pw-repl (playwright-repl) is a REPL that drives a running Chromium over CDP. The browser may be a
|
|
10
12
|
shared session: a person can have their own tabs open in it and be using it while you work.
|
|
11
13
|
|
|
12
|
-
`pw-repl` below is the command the `pw-repl` npm package installs. Without a global install,
|
|
13
|
-
`npx pw-repl`
|
|
14
|
+
`pw-repl` below is the command the `pw-repl` npm package installs. Without a global install, use
|
|
15
|
+
`npx pw-repl@latest` in its place, in that same form for every command, so each one runs the same
|
|
16
|
+
version; from a clone, `<clone>/bin/pw-repl.js`.
|
|
17
|
+
|
|
18
|
+
`pw-repl where` also names the skill of the pw-repl you run: `skill: e5f6a7b8 (pw-repl 0.4.3)`. When that
|
|
19
|
+
is not the skill named at the top of this file, tell the user before going on:
|
|
20
|
+
|
|
21
|
+
- if this file's pw-repl version is newer than that one, pw-repl is behind: say "⚠️ pw-repl is older
|
|
22
|
+
than this skill (pw-repl <that version>, skill from <this file's>); `npx pw-repl@latest` or
|
|
23
|
+
`npm install -g pw-repl` updates it."
|
|
24
|
+
- otherwise, the skill is: ask "⚠️ This pw-repl skill is out of date (from pw-repl <this file's
|
|
25
|
+
version>; <that version> has a newer one). Update it? Everything but its Custom rules section will be
|
|
26
|
+
replaced." On a yes, save what `pw-repl skill` prints over this file, with this file's Custom rules
|
|
27
|
+
section in place of the new one's.
|
|
14
28
|
|
|
15
29
|
## Arguments
|
|
16
30
|
|
|
@@ -20,15 +34,18 @@ the sections below are how to carry it out. With no args, get a REPL running and
|
|
|
20
34
|
|
|
21
35
|
## Start it
|
|
22
36
|
|
|
23
|
-
`pw-repl where` says whether a REPL is running and how `send` reaches it
|
|
24
|
-
|
|
37
|
+
`pw-repl where` says whether a REPL is running and how `send` reaches it, or that one is still starting
|
|
38
|
+
(retry shortly; it exits 64 until the REPL serves, as when none is running). Right after you start one,
|
|
39
|
+
for a moment before its process is up, it can still say none is there: retry for a few seconds before
|
|
40
|
+
taking that as a failed start. There are three ways to run one; each takes an optional start URL, which
|
|
41
|
+
opens in a new tab.
|
|
25
42
|
|
|
26
43
|
- `pw-repl serve --background` runs it detached, with a command server on `/tmp/playwright-repl.sock`
|
|
27
44
|
(owner-only) and its output in `/tmp/playwright-repl.log`. `pw-repl attach` shows everything it does
|
|
28
45
|
and takes commands; `pw-repl stop` stops it. It returns once the REPL serves; Ctrl-C before then gives
|
|
29
46
|
up on the start, stops the REPL, exits 130, and says so in the log.
|
|
30
47
|
- `pw-repl serve` runs the same in a terminal, where the pane shows every command. Its prompt is
|
|
31
|
-
`pw[serve]>`.
|
|
48
|
+
`pw[serve]>`. It stops at that prompt (`quit`, or Ctrl-C); `pw-repl stop` is for a background one.
|
|
32
49
|
- Add `--launch` to `run` or `serve` (with or without `--background`) to have it start a Chromium of its
|
|
33
50
|
own instead of connecting to one: headless unless `--headed`, in a temporary profile, on a free port of
|
|
34
51
|
its own (never 9222). It stops with the REPL, Ctrl-C while it starts included, and `stop` returns once
|
|
@@ -79,11 +96,12 @@ pw-repl send tab
|
|
|
79
96
|
pw-repl send -t 90 'screenshot -d 60' # wait longer than the 20s default
|
|
80
97
|
```
|
|
81
98
|
|
|
82
|
-
Always send commands with `pw-repl send`; don't type into the pane yourself
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
99
|
+
Always send commands with `pw-repl send`; don't type into the pane yourself, except `quit` at the prompt
|
|
100
|
+
of a REPL of your own, which `send` refuses. The words after `send` are the command. For `fill`, `type`,
|
|
101
|
+
`select`, `press` and `upload`, a word quoted in your shell stays one word
|
|
102
|
+
(`pw-repl send fill "text=Your name" Ada`); other commands get the words as they are, joined by spaces
|
|
103
|
+
(`pw-repl send eval "document.title + ' x'"`). A command that takes only a selector takes the whole line,
|
|
104
|
+
spaces and all: `pw-repl send click "text=Your name"`. It works however the REPL was started:
|
|
87
105
|
|
|
88
106
|
- `run` (in tmux): `send` types the command into the tmux pane and reads the result back off the screen.
|
|
89
107
|
It types only when the pane's last line is a bare prompt, so nothing lands in a shell or in the middle
|
|
@@ -166,6 +184,11 @@ pw-repl send -c <name> close # your modes off; tab close
|
|
|
166
184
|
lists what is on in every tab (the prompt shows the selected tab's, e.g. `(watch routes:1) pw>`);
|
|
167
185
|
`modes off` turns off every mode in every tab.
|
|
168
186
|
- Someone may be attached to the REPL's tmux session; killing the session ends it for them too.
|
|
187
|
+
- pw-repl does not record video. For one of a flow in a browser of its own, Microsoft's playwright-cli (a
|
|
188
|
+
separate tool, the npm package `@playwright/cli`: `video-start`, `video-stop`); for this browser as the
|
|
189
|
+
person sees it, a screen recorder (`ffmpeg -f x11grab`, macOS `screencapture -v`, OBS). A screen
|
|
190
|
+
recorder records their whole screen, not the tab: ask before starting one, or have them start it. A
|
|
191
|
+
headless browser is not on the screen, so it does not show.
|
|
169
192
|
|
|
170
193
|
## Environment
|
|
171
194
|
|