rpc-agent 0.3.1 → 0.4.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/CHANGELOG.md +54 -0
- package/README.md +174 -5
- package/dist/args.js +126 -7
- package/dist/auth.js +416 -0
- package/dist/browser.js +6 -4
- package/dist/capture.js +42 -9
- package/dist/cli.js +83 -3
- package/dist/help.js +54 -4
- package/dist/login.js +150 -0
- package/dist/mcp.js +71 -4
- package/dist/term.js +20 -0
- package/dist/ui.js +114 -3
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
Authenticated captures: screenshot logged-in pages without a hand-written
|
|
6
|
+
"sign in, then screenshot" script.
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- `rpc login <url>` opens a browser window; sign in, press Enter (or close
|
|
11
|
+
the window) and the storage state is saved for that origin to
|
|
12
|
+
`~/.config/rpc-agent/sessions/<origin>.json` (file 0600, directory 0700).
|
|
13
|
+
`--save <file>` writes it elsewhere.
|
|
14
|
+
- `rpc login <url> --form`: headless email + password sign-in for CI and
|
|
15
|
+
agents, from `RPC_LOGIN_EMAIL` / `RPC_LOGIN_PASSWORD` (or `--email` /
|
|
16
|
+
`--password`), with `--email-selector`, `--password-selector` and
|
|
17
|
+
`--submit-selector`.
|
|
18
|
+
- `rpc shot` uses the saved session for the target origin automatically.
|
|
19
|
+
`--no-session` opts out. `rpc sessions [list]` and
|
|
20
|
+
`rpc sessions clear [origin]` manage them.
|
|
21
|
+
- `rpc shot --cookie "name=value[; Domain=…; Path=…]"`, `--header
|
|
22
|
+
"Name: value"` (both repeatable), `--storage-state <file>` and
|
|
23
|
+
`--allow-origin <origin>`.
|
|
24
|
+
- A capture redirected to a login page (`/login`, `/sign-in`, `/auth`, …)
|
|
25
|
+
fails with code `AUTH_REQUIRED` and the hint ``Session missing or
|
|
26
|
+
expired: run `rpc login <origin>` `` instead of screenshotting the sign-in
|
|
27
|
+
form. Sessions whose cookies have all expired aren't loaded.
|
|
28
|
+
- MCP `screenshot` takes `cookies`, `headers`, `storageState`, `useSession`
|
|
29
|
+
and `allowOrigins`. New `list_sessions` tool.
|
|
30
|
+
- `rpc install --headed` also downloads full Chromium, for the
|
|
31
|
+
`rpc login` window. Without it, `rpc login` uses an installed Google
|
|
32
|
+
Chrome if there is one.
|
|
33
|
+
|
|
34
|
+
### Security
|
|
35
|
+
|
|
36
|
+
- Headers and cookies go to the target origin only (plus any
|
|
37
|
+
`--allow-origin`). Headers are injected per request through the CDP Fetch
|
|
38
|
+
domain, so a redirect to another host doesn't carry them. Saved sessions
|
|
39
|
+
and storage-state files load only the target origin's cookies and
|
|
40
|
+
localStorage.
|
|
41
|
+
- Cookie, header and localStorage values are never printed: output shows
|
|
42
|
+
names with `•••`, and console errors and failure messages are scrubbed.
|
|
43
|
+
- `--password` prints a warning; use the environment variable.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- A page that redirects to a login URL now fails that device with
|
|
48
|
+
`AUTH_REQUIRED` (exit code 1) even when no auth was given. In 0.3 it
|
|
49
|
+
screenshotted the sign-in page and reported success.
|
|
50
|
+
|
|
51
|
+
### Unchanged
|
|
52
|
+
|
|
53
|
+
- With no auth in play, plain output, `--json` output and MCP results are
|
|
54
|
+
the 0.3 ones. With auth, they gain an `auth:` line / an `auth` field.
|
|
55
|
+
Existing MCP params and result fields are unchanged.
|
|
56
|
+
|
|
3
57
|
## 0.3.1
|
|
4
58
|
|
|
5
59
|
Published to npm as `rpc-agent`.
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# rpc-agent
|
|
2
2
|
|
|
3
|
+
<img src="https://raw.githubusercontent.com/akintola4/rpc-agent/main/docs/media/banner.png" width="100%" alt="rpc-agent: CLI + MCP server · device screenshots for AI agents · Claude Code, Codex, Cursor. A line drawing of a laptop, tablet and phone with a terminal in front running rpc shot; the Claude Code and Cursor marks and a terminal-prompt glyph for Codex sit beside it, with lines into the prompt.">
|
|
4
|
+
|
|
3
5
|
<picture>
|
|
4
6
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/akintola4/rpc-agent/main/docs/media/hero-dark.png">
|
|
5
7
|
<img alt="A site captured by rpc on an iPhone 15, an iPad Air 11 and a 1440 px desktop, each framed in its device bezel" src="https://raw.githubusercontent.com/akintola4/rpc-agent/main/docs/media/hero-light.png">
|
|
@@ -35,7 +37,8 @@ Chromium is not downloaded when the package installs. `rpc install` (or
|
|
|
35
37
|
`npx rpc-agent install`) fetches it with rpc's own Playwright version, so
|
|
36
38
|
the browser always matches the one rpc launches. It installs only
|
|
37
39
|
Chromium's headless shell (plus Playwright's small ffmpeg), not the full
|
|
38
|
-
Chrome for Testing build, since
|
|
40
|
+
Chrome for Testing build, since captures never open a visible window
|
|
41
|
+
(`rpc install --headed` adds it for `rpc login`). Until you run it, every
|
|
39
42
|
command says Chromium is missing and prints that command, instead of
|
|
40
43
|
failing with Playwright's own error. Browsers go to Playwright's usual
|
|
41
44
|
cache, so other Playwright projects on the same version share them.
|
|
@@ -60,9 +63,11 @@ these docs say `rpc`, or `pnpm rpc` from inside the repo.
|
|
|
60
63
|
```sh
|
|
61
64
|
rpc # interactive: pick a URL, devices and options
|
|
62
65
|
rpc shot <url> [options]
|
|
66
|
+
rpc login <url> [--form] # sign in once; later shots of that origin use the session
|
|
67
|
+
rpc sessions [list|clear [origin]]
|
|
63
68
|
rpc devices [--json]
|
|
64
69
|
rpc doctor [--json] # is Chromium installed, and does it launch?
|
|
65
|
-
rpc install
|
|
70
|
+
rpc install [--headed] # download Chromium with rpc's own Playwright version
|
|
66
71
|
rpc mcp
|
|
67
72
|
rpc help <command> # options and examples for one command
|
|
68
73
|
```
|
|
@@ -96,6 +101,11 @@ the files it wrote:
|
|
|
96
101
|
| `--no-emulate` | Viewport and DPR only: no mobile user agent or touch |
|
|
97
102
|
| `--json` | Print the result as JSON |
|
|
98
103
|
| `--open` | Open the output folder when done (`open`, `xdg-open` or `explorer`) |
|
|
104
|
+
| `--cookie <c>` | `name=value[; Domain=…; Path=…]`, repeatable. See [Authenticated pages](#authenticated-pages) |
|
|
105
|
+
| `--header <h>` | `Name: value`, repeatable, sent to the target origin only |
|
|
106
|
+
| `--storage-state <file>` | Load a Playwright storageState JSON |
|
|
107
|
+
| `--no-session` | Don't use the session saved by `rpc login` |
|
|
108
|
+
| `--allow-origin <o>` | Also send headers and loaded cookies to this origin, repeatable |
|
|
99
109
|
|
|
100
110
|
The URL can be a full URL, a bare local host (`localhost:3000` becomes
|
|
101
111
|
`http://`), a bare domain (`example.com` becomes `https://`) or a path to a
|
|
@@ -168,9 +178,130 @@ listening on port N"), proxy, tunnel and offline errors, TLS errors (https
|
|
|
168
178
|
against a plain-http dev server), missing files, redirect loops, navigation
|
|
169
179
|
and `--wait-for` timeouts.
|
|
170
180
|
|
|
181
|
+
## Authenticated pages
|
|
182
|
+
|
|
183
|
+
Dashboards, billing and settings pages sit behind a login. rpc can carry a
|
|
184
|
+
session so you don't need a hand-written "sign in, then screenshot" script.
|
|
185
|
+
|
|
186
|
+
### Sign in once with `rpc login`
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
rpc login localhost:3000/sign-in
|
|
190
|
+
rpc shot localhost:3000/app/settings/billing -d iphone-15,desktop-1440 --bezels
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`rpc login` opens a browser window at the URL. Sign in as you normally
|
|
194
|
+
would (SSO and 2FA work, it's a real browser), then press Enter in the
|
|
195
|
+
terminal or close the window. rpc saves the browser's storage state
|
|
196
|
+
(cookies and localStorage) to
|
|
197
|
+
`~/.config/rpc-agent/sessions/<origin>.json`, for example
|
|
198
|
+
`http-localhost-3000.json`. From then on, `rpc shot` on that origin uses it
|
|
199
|
+
automatically, and the output says so:
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
auth: session http://localhost:3000 (saved 2026-10-29 22:55)
|
|
203
|
+
ok iphone-15 (393x852 @3x)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The window needs full Chromium, which `rpc install` doesn't download (it
|
|
207
|
+
fetches only the headless shell). Run `rpc install --headed` once, or have
|
|
208
|
+
Google Chrome installed; rpc falls back to it.
|
|
209
|
+
|
|
210
|
+
`--save <file>` writes the storage state to a file of your choice instead;
|
|
211
|
+
use it with `rpc shot --storage-state <file>`.
|
|
212
|
+
|
|
213
|
+
```sh
|
|
214
|
+
rpc sessions # origins, cookie counts, saved and expiry times
|
|
215
|
+
rpc sessions clear localhost:3000 # forget one origin
|
|
216
|
+
rpc sessions clear # forget all
|
|
217
|
+
rpc shot localhost:3000/app --no-session # capture as a signed-out visitor
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### When the session is missing or expired
|
|
221
|
+
|
|
222
|
+
If the page redirects to a login URL (a path segment like `/login`,
|
|
223
|
+
`/sign-in`, `/signin` or `/auth`, different from the one you asked for),
|
|
224
|
+
rpc doesn't screenshot the sign-in form. The device fails with code
|
|
225
|
+
`AUTH_REQUIRED`:
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
x iphone-15 (393x852 @3x): Redirected to a login page (http://localhost:3000/sign-in) instead of http://localhost:3000/app/settings/billing
|
|
229
|
+
hint: Session missing or expired: run `rpc login http://localhost:3000`
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
A saved session whose cookies have all passed their expiry isn't loaded,
|
|
233
|
+
and gives the same hint.
|
|
234
|
+
|
|
235
|
+
### CI and agents: `rpc login --form`
|
|
236
|
+
|
|
237
|
+
Without a person at the keyboard, `--form` signs in headlessly through a
|
|
238
|
+
standard email + password form:
|
|
239
|
+
|
|
240
|
+
```sh
|
|
241
|
+
export RPC_LOGIN_EMAIL=ci@example.test
|
|
242
|
+
export RPC_LOGIN_PASSWORD=... # from your CI secret store
|
|
243
|
+
rpc login https://staging.example.com/sign-in --form
|
|
244
|
+
rpc shot https://staging.example.com/app/billing --json
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
It loads the page, waits for it to settle (so a client-side form has
|
|
248
|
+
hydrated), fills the first match of `--email-selector` (default
|
|
249
|
+
`input[type=email]`, then `autocomplete=username`, `name=email`,
|
|
250
|
+
`name=username`, `#email`) and `--password-selector` (default
|
|
251
|
+
`input[type=password]`), clicks `--submit-selector` (default
|
|
252
|
+
`button[type=submit], input[type=submit]`, or presses Enter if there is no
|
|
253
|
+
button), and waits for the URL to leave the login page. If it doesn't
|
|
254
|
+
within `--timeout`, it fails and quotes any alert the page shows ("Invalid
|
|
255
|
+
email or password"). `--email` works as a flag too; see the security notes
|
|
256
|
+
before using `--password`.
|
|
257
|
+
|
|
258
|
+
### Cookies and headers
|
|
259
|
+
|
|
260
|
+
```sh
|
|
261
|
+
rpc shot localhost:3000/app --cookie "session=$SESSION"
|
|
262
|
+
rpc shot app.example.com/admin --cookie "sid=$SID; Domain=example.com; Path=/"
|
|
263
|
+
rpc shot api.example.com/docs --header "Authorization: Bearer $TOKEN"
|
|
264
|
+
rpc shot localhost:3000/app --storage-state auth.json
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
- `--cookie` is host-only on the target unless you give a `Domain`, which
|
|
268
|
+
must match the target host (or an `--allow-origin` host). `Secure`,
|
|
269
|
+
`HttpOnly` and `SameSite` are accepted too.
|
|
270
|
+
- `--header` is sent only on requests to the target origin. Images,
|
|
271
|
+
scripts and API calls to other origins don't get it, and neither does a
|
|
272
|
+
redirect to another host (every redirect hop is checked).
|
|
273
|
+
- `--storage-state` and saved sessions load only the target origin's
|
|
274
|
+
cookies and localStorage, even if the file holds others (an SSO domain,
|
|
275
|
+
analytics).
|
|
276
|
+
- `--allow-origin https://api.example.com` widens all of the above to
|
|
277
|
+
another origin, for an app that calls a separate API host.
|
|
278
|
+
- A saved session is used unless `--no-session` or `--storage-state` is
|
|
279
|
+
given. Cookies and headers are added on top of it.
|
|
280
|
+
|
|
281
|
+
### Security notes
|
|
282
|
+
|
|
283
|
+
- **Values are never printed.** Plain, rich and `--json` output and MCP
|
|
284
|
+
results show cookie and header names with `•••`. Console errors and
|
|
285
|
+
failure messages are scrubbed of every value rpc injected. The `--json`
|
|
286
|
+
result gains an `auth` field, for example
|
|
287
|
+
`{"cookies":[{"name":"sid","value":"•••","domain":"localhost","path":"/"}]}`.
|
|
288
|
+
- **Session files are credentials.** They are written with mode 0600 in a
|
|
289
|
+
0700 directory (`$XDG_CONFIG_HOME/rpc-agent/sessions` if set;
|
|
290
|
+
`RPC_SESSIONS_DIR` overrides the location). Anyone who can read one can
|
|
291
|
+
act as you on that site until it expires. `rpc sessions clear` deletes
|
|
292
|
+
them. Don't commit a `--save` file.
|
|
293
|
+
- **Keep passwords out of shell history.** Prefer `RPC_LOGIN_PASSWORD` from
|
|
294
|
+
a secret store over `--password`, which lands in your shell history and
|
|
295
|
+
is visible to other users in the process list; rpc warns when you use it.
|
|
296
|
+
The same goes for tokens in `--cookie` and `--header`: pass them from an
|
|
297
|
+
environment variable (`--header "Authorization: Bearer $TOKEN"`), not
|
|
298
|
+
literally.
|
|
299
|
+
- **Use a test account.** Sign in as a user whose data you're happy for
|
|
300
|
+
screenshots (and the agent reading them) to see.
|
|
301
|
+
|
|
171
302
|
## MCP server
|
|
172
303
|
|
|
173
|
-
`rpc mcp` runs an MCP server on stdio with
|
|
304
|
+
`rpc mcp` runs an MCP server on stdio with three tools.
|
|
174
305
|
|
|
175
306
|
### `screenshot`
|
|
176
307
|
|
|
@@ -187,6 +318,11 @@ and `--wait-for` timeouts.
|
|
|
187
318
|
| `reducedMotion` | false | emulate `prefers-reduced-motion: reduce` |
|
|
188
319
|
| `waitFor`, `waitMs`, `colorScheme`, `outDir`, `timeoutMs` | | as in the CLI |
|
|
189
320
|
| `inline` | true | return images, not just paths |
|
|
321
|
+
| `cookies` | | `[{ name, value, domain?, path? }]`, set on the target origin |
|
|
322
|
+
| `headers` | | `{ "Authorization": "Bearer …" }`, sent to the target origin only |
|
|
323
|
+
| `storageState` | | path to a Playwright storageState JSON |
|
|
324
|
+
| `useSession` | true when one exists | use the session saved by `rpc login` for the origin |
|
|
325
|
+
| `allowOrigins` | | extra origins that also get the headers and cookies |
|
|
190
326
|
|
|
191
327
|
The result starts with a plain-text block: one line per device (or the error
|
|
192
328
|
and hint), followed by the JSON summary (file paths, pixel sizes,
|
|
@@ -213,13 +349,25 @@ skipped (`slices 11-26 (y=8520-21656 css px) skipped inline`), all slices are
|
|
|
213
349
|
written to disk and the summary gives their paths and the full PNG, so the
|
|
214
350
|
agent can Read the rest without slicing anything itself.
|
|
215
351
|
|
|
352
|
+
When auth is in play the text starts with an `auth:` line naming the
|
|
353
|
+
session, cookies and headers (values masked), and the JSON summary gains an
|
|
354
|
+
`auth` field. A capture that lands on a login page fails with
|
|
355
|
+
`AUTH_REQUIRED` and the `rpc login` hint. Signing in is something the user
|
|
356
|
+
does in a terminal (`rpc login`); the agent then just calls `screenshot`.
|
|
357
|
+
|
|
216
358
|
### `list_devices`
|
|
217
359
|
|
|
218
360
|
Takes an optional `category` (`apple-phone`, `android-phone`, `tablet`,
|
|
219
361
|
`laptop`) and returns ids, sizes and aliases.
|
|
220
362
|
|
|
363
|
+
### `list_sessions`
|
|
364
|
+
|
|
365
|
+
Lists the origins with a saved session: cookie count, when it was saved and
|
|
366
|
+
whether it has expired. Never returns values.
|
|
367
|
+
|
|
221
368
|
Files go to `outDir`, which is resolved against the server's working
|
|
222
|
-
directory. Claude Code and Cursor start the server in the project root
|
|
369
|
+
directory. Claude Code and Cursor start the server in the project root; in
|
|
370
|
+
Codex, set `cwd` on the server to choose the directory.
|
|
223
371
|
|
|
224
372
|
### Claude Code
|
|
225
373
|
|
|
@@ -261,6 +409,25 @@ so. Add the domain to the allowlist.
|
|
|
261
409
|
From a clone, use `"command": "node"` and
|
|
262
410
|
`"args": ["/absolute/path/to/rpc-agent/dist/cli.js", "mcp"]`.
|
|
263
411
|
|
|
412
|
+
### Codex
|
|
413
|
+
|
|
414
|
+
```sh
|
|
415
|
+
codex mcp add rpc -- npx -y rpc-agent mcp
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Or add it to `~/.codex/config.toml` (or `.codex/config.toml` in a trusted
|
|
419
|
+
project). The Codex CLI and IDE extension read the same file:
|
|
420
|
+
|
|
421
|
+
```toml
|
|
422
|
+
[mcp_servers.rpc]
|
|
423
|
+
command = "npx"
|
|
424
|
+
args = ["-y", "rpc-agent", "mcp"]
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
From a clone, use `command = "node"` and
|
|
428
|
+
`args = ["/absolute/path/to/rpc-agent/dist/cli.js", "mcp"]`. `/mcp` in the
|
|
429
|
+
Codex TUI lists the servers it started.
|
|
430
|
+
|
|
264
431
|
## Development
|
|
265
432
|
|
|
266
433
|
```sh
|
|
@@ -295,7 +462,9 @@ from the `skills` CLI, are in `docs/cli-design.md`.
|
|
|
295
462
|
- The bezel geometry is RPC's, unchanged. On kind-default phones the page
|
|
296
463
|
covers the dynamic island or notch, and generic monitors are drawn edge to
|
|
297
464
|
edge with only the stand showing.
|
|
298
|
-
- No
|
|
465
|
+
- No scripted interaction before capture (beyond `rpc login --form`).
|
|
466
|
+
- Login-page detection goes by the URL path. An app that shows its sign-in
|
|
467
|
+
form at the requested URL without redirecting is captured as is.
|
|
299
468
|
- Every MCP call launches a fresh Chromium, which adds about a second.
|
|
300
469
|
|
|
301
470
|
## License
|
package/dist/args.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
|
+
import { parseCookie, parseHeader, resolveAuth, toOrigin } from './auth.js';
|
|
2
3
|
import { resolveDevices } from './devices.js';
|
|
3
|
-
import { COLOR_SCHEMES, defaultShotOptions, } from './options.js';
|
|
4
|
-
export const HELP_TOPICS = ['shot', 'devices', 'doctor', 'install', 'mcp'];
|
|
4
|
+
import { COLOR_SCHEMES, DEFAULT_TIMEOUT_MS, defaultShotOptions, normalizeUrl, } from './options.js';
|
|
5
|
+
export const HELP_TOPICS = ['shot', 'login', 'sessions', 'devices', 'doctor', 'install', 'mcp'];
|
|
5
6
|
function isTopic(s) {
|
|
6
7
|
return HELP_TOPICS.includes(s ?? '');
|
|
7
8
|
}
|
|
@@ -15,10 +16,13 @@ export const USAGE = `rpc - screenshot a URL at device viewports, optionally in
|
|
|
15
16
|
|
|
16
17
|
Usage:
|
|
17
18
|
rpc shot <url> [options] capture screenshots
|
|
19
|
+
rpc login <url> [--form] sign in once and save the session for that origin
|
|
20
|
+
rpc sessions [list|clear [origin]]
|
|
21
|
+
list or delete saved sessions
|
|
18
22
|
rpc devices [--json] list device ids and aliases
|
|
19
23
|
rpc mcp run the MCP server on stdio
|
|
20
24
|
rpc doctor [--json] check that Playwright's Chromium is installed and launches
|
|
21
|
-
rpc install
|
|
25
|
+
rpc install [--headed] download Playwright's Chromium (same version rpc uses)
|
|
22
26
|
|
|
23
27
|
Shot options:
|
|
24
28
|
-d, --devices <ids> comma-separated device ids or aliases
|
|
@@ -39,14 +43,23 @@ Shot options:
|
|
|
39
43
|
--json print the result as JSON
|
|
40
44
|
--open open the output folder when done
|
|
41
45
|
|
|
46
|
+
Auth options (values are never printed):
|
|
47
|
+
--cookie <c> "name=value[; Domain=…; Path=…]", repeatable;
|
|
48
|
+
sent to the target origin only
|
|
49
|
+
--header <h> "Name: value", repeatable; target origin only
|
|
50
|
+
--storage-state <file> load a Playwright storageState JSON
|
|
51
|
+
--no-session don't use the session saved by \`rpc login\`
|
|
52
|
+
--allow-origin <o> also send headers/cookies to this origin, repeatable
|
|
53
|
+
|
|
42
54
|
Run \`rpc help <command>\` for one command's options and examples.
|
|
43
55
|
Run \`rpc\` with no arguments in a terminal for an interactive capture.
|
|
44
56
|
|
|
45
57
|
Examples:
|
|
46
58
|
rpc shot localhost:3000 -d iphone-15,ipad,desktop-1440 --bezels
|
|
47
59
|
rpc shot https://example.com --full-page --color-scheme dark
|
|
60
|
+
rpc login localhost:3000/sign-in && rpc shot localhost:3000/dashboard
|
|
48
61
|
`;
|
|
49
|
-
export function parseCli(argv) {
|
|
62
|
+
export function parseCli(argv, env = process.env) {
|
|
50
63
|
const [cmd, ...rest] = argv;
|
|
51
64
|
switch (cmd) {
|
|
52
65
|
case undefined:
|
|
@@ -67,10 +80,16 @@ export function parseCli(argv) {
|
|
|
67
80
|
if (wantsHelp(rest))
|
|
68
81
|
return { command: 'help', topic: 'mcp' };
|
|
69
82
|
return { command: 'mcp' };
|
|
70
|
-
case 'install':
|
|
83
|
+
case 'install': {
|
|
71
84
|
if (wantsHelp(rest))
|
|
72
85
|
return { command: 'help', topic: 'install' };
|
|
73
|
-
|
|
86
|
+
const { values } = parse(rest, { headed: { type: 'boolean' } }, false);
|
|
87
|
+
return values.headed ? { command: 'install', headed: true } : { command: 'install' };
|
|
88
|
+
}
|
|
89
|
+
case 'login':
|
|
90
|
+
return parseLogin(rest, env);
|
|
91
|
+
case 'sessions':
|
|
92
|
+
return parseSessions(rest);
|
|
74
93
|
case 'doctor': {
|
|
75
94
|
if (wantsHelp(rest))
|
|
76
95
|
return { command: 'help', topic: 'doctor' };
|
|
@@ -108,6 +127,11 @@ function parseShot(args) {
|
|
|
108
127
|
'reduced-motion': { type: 'boolean' },
|
|
109
128
|
json: { type: 'boolean' },
|
|
110
129
|
open: { type: 'boolean' },
|
|
130
|
+
cookie: { type: 'string', multiple: true },
|
|
131
|
+
header: { type: 'string', multiple: true },
|
|
132
|
+
'storage-state': { type: 'string' },
|
|
133
|
+
'no-session': { type: 'boolean' },
|
|
134
|
+
'allow-origin': { type: 'string', multiple: true },
|
|
111
135
|
}, true);
|
|
112
136
|
if (positionals.length === 0)
|
|
113
137
|
throw new ArgError('Missing <url>. Usage: rpc shot <url> [options]');
|
|
@@ -162,11 +186,106 @@ function parseShot(args) {
|
|
|
162
186
|
if (typeof values['max-height'] === 'string') {
|
|
163
187
|
options.maxHeight = parseInt_('--max-height', values['max-height'], 500, 200_000);
|
|
164
188
|
}
|
|
189
|
+
const auth = {};
|
|
190
|
+
try {
|
|
191
|
+
if (values.cookie?.length)
|
|
192
|
+
auth.cookies = values.cookie.map(parseCookie);
|
|
193
|
+
if (values.header?.length)
|
|
194
|
+
auth.headers = Object.fromEntries(values.header.map(parseHeader));
|
|
195
|
+
if (typeof values['storage-state'] === 'string')
|
|
196
|
+
auth.storageState = values['storage-state'];
|
|
197
|
+
if (values['no-session'])
|
|
198
|
+
auth.useSession = false;
|
|
199
|
+
if (values['allow-origin']?.length)
|
|
200
|
+
auth.allowOrigins = values['allow-origin'].flatMap(splitList).map(toOrigin);
|
|
201
|
+
if (Object.keys(auth).length) {
|
|
202
|
+
resolveAuth(options.url, auth); // validate now: bad input is a usage error
|
|
203
|
+
options.auth = auth;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
throw new ArgError(err.message);
|
|
208
|
+
}
|
|
165
209
|
return { command: 'shot', options, json: Boolean(values.json), open: Boolean(values.open) };
|
|
166
210
|
}
|
|
211
|
+
function parseLogin(args, env) {
|
|
212
|
+
if (wantsHelp(args))
|
|
213
|
+
return { command: 'help', topic: 'login' };
|
|
214
|
+
const { values, positionals } = parse(args, {
|
|
215
|
+
save: { type: 'string' },
|
|
216
|
+
form: { type: 'boolean' },
|
|
217
|
+
email: { type: 'string' },
|
|
218
|
+
password: { type: 'string' },
|
|
219
|
+
'email-selector': { type: 'string' },
|
|
220
|
+
'password-selector': { type: 'string' },
|
|
221
|
+
'submit-selector': { type: 'string' },
|
|
222
|
+
timeout: { type: 'string' },
|
|
223
|
+
json: { type: 'boolean' },
|
|
224
|
+
}, true);
|
|
225
|
+
if (positionals.length !== 1)
|
|
226
|
+
throw new ArgError('Usage: rpc login <url> [--form] [--save <file>]');
|
|
227
|
+
let url;
|
|
228
|
+
try {
|
|
229
|
+
url = normalizeUrl(positionals[0]);
|
|
230
|
+
toOrigin(url);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
throw new ArgError(err.message);
|
|
234
|
+
}
|
|
235
|
+
const cmd = {
|
|
236
|
+
command: 'login',
|
|
237
|
+
url,
|
|
238
|
+
timeoutMs: typeof values.timeout === 'string' ? parseMs('--timeout', values.timeout) : DEFAULT_TIMEOUT_MS,
|
|
239
|
+
json: Boolean(values.json),
|
|
240
|
+
};
|
|
241
|
+
if (typeof values.save === 'string')
|
|
242
|
+
cmd.save = values.save;
|
|
243
|
+
const formOnly = ['email', 'password', 'email-selector', 'password-selector', 'submit-selector'];
|
|
244
|
+
if (!values.form) {
|
|
245
|
+
const used = formOnly.filter(k => values[k] !== undefined);
|
|
246
|
+
if (used.length)
|
|
247
|
+
throw new ArgError(`--${used[0]} needs --form`);
|
|
248
|
+
return cmd;
|
|
249
|
+
}
|
|
250
|
+
const email = values.email ?? env.RPC_LOGIN_EMAIL;
|
|
251
|
+
const password = values.password ?? env.RPC_LOGIN_PASSWORD;
|
|
252
|
+
if (!email)
|
|
253
|
+
throw new ArgError('--form needs an email: set RPC_LOGIN_EMAIL (or pass --email)');
|
|
254
|
+
if (!password)
|
|
255
|
+
throw new ArgError('--form needs a password: set RPC_LOGIN_PASSWORD');
|
|
256
|
+
cmd.form = {
|
|
257
|
+
email,
|
|
258
|
+
password,
|
|
259
|
+
...(values['email-selector'] ? { emailSelector: values['email-selector'] } : {}),
|
|
260
|
+
...(values['password-selector'] ? { passwordSelector: values['password-selector'] } : {}),
|
|
261
|
+
...(values['submit-selector'] ? { submitSelector: values['submit-selector'] } : {}),
|
|
262
|
+
};
|
|
263
|
+
if (values.password !== undefined)
|
|
264
|
+
cmd.passwordFromFlag = true;
|
|
265
|
+
return cmd;
|
|
266
|
+
}
|
|
267
|
+
function parseSessions(args) {
|
|
268
|
+
if (wantsHelp(args))
|
|
269
|
+
return { command: 'help', topic: 'sessions' };
|
|
270
|
+
const { values, positionals } = parse(args, { json: { type: 'boolean' } }, true);
|
|
271
|
+
const [action = 'list', origin, ...extra] = positionals;
|
|
272
|
+
if (action !== 'list' && action !== 'clear')
|
|
273
|
+
throw new ArgError(`Unknown sessions action "${action}". Use list or clear.`);
|
|
274
|
+
if (extra.length || (action === 'list' && origin))
|
|
275
|
+
throw new ArgError('Usage: rpc sessions list | rpc sessions clear [origin]');
|
|
276
|
+
if (origin) {
|
|
277
|
+
try {
|
|
278
|
+
toOrigin(origin);
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
throw new ArgError(err.message);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return { command: 'sessions', action, ...(origin ? { origin } : {}), json: Boolean(values.json) };
|
|
285
|
+
}
|
|
167
286
|
function parse(args, options, allowPositionals) {
|
|
168
287
|
try {
|
|
169
|
-
return parseArgs({ args, options
|
|
288
|
+
return parseArgs({ args, options, allowPositionals, strict: true });
|
|
170
289
|
}
|
|
171
290
|
catch (err) {
|
|
172
291
|
throw new ArgError(err.message);
|