rpc-agent 0.3.1 → 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/CHANGELOG.md +94 -0
- package/README.md +428 -5
- package/dist/args.js +214 -25
- package/dist/auth.js +416 -0
- package/dist/browser.js +6 -4
- package/dist/capture.js +47 -11
- package/dist/check-format.js +120 -0
- package/dist/check-page.js +336 -0
- package/dist/check.js +677 -0
- package/dist/cli.js +115 -4
- package/dist/help.js +78 -4
- package/dist/login.js +150 -0
- package/dist/mcp.js +152 -7
- package/dist/options.js +14 -0
- package/dist/png.js +47 -0
- package/dist/term.js +20 -0
- package/dist/ui.js +220 -3
- package/package.json +3 -2
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, defaultCheckOptions, defaultShotOptions, FAIL_ON, normalizeUrl, } from './options.js';
|
|
5
|
+
export const HELP_TOPICS = ['shot', 'check', 'login', 'sessions', 'devices', 'doctor', 'install', 'mcp'];
|
|
5
6
|
function isTopic(s) {
|
|
6
7
|
return HELP_TOPICS.includes(s ?? '');
|
|
7
8
|
}
|
|
@@ -15,10 +16,14 @@ 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 check <url> [options] pre-launch audit: meta, layout shift, labels, fonts, mobile
|
|
20
|
+
rpc login <url> [--form] sign in once and save the session for that origin
|
|
21
|
+
rpc sessions [list|clear [origin]]
|
|
22
|
+
list or delete saved sessions
|
|
18
23
|
rpc devices [--json] list device ids and aliases
|
|
19
24
|
rpc mcp run the MCP server on stdio
|
|
20
25
|
rpc doctor [--json] check that Playwright's Chromium is installed and launches
|
|
21
|
-
rpc install
|
|
26
|
+
rpc install [--headed] download Playwright's Chromium (same version rpc uses)
|
|
22
27
|
|
|
23
28
|
Shot options:
|
|
24
29
|
-d, --devices <ids> comma-separated device ids or aliases
|
|
@@ -39,14 +44,29 @@ Shot options:
|
|
|
39
44
|
--json print the result as JSON
|
|
40
45
|
--open open the output folder when done
|
|
41
46
|
|
|
47
|
+
Check options (plus -d, --wait-for, --wait, --timeout, --color-scheme, -o, --json):
|
|
48
|
+
--fail-on <level> error (default) | warn: exit 1 on warnings too
|
|
49
|
+
--shots screenshot the elements behind layout-shift and
|
|
50
|
+
overflow findings (into --out)
|
|
51
|
+
|
|
52
|
+
Auth options (values are never printed, shot and check):
|
|
53
|
+
--cookie <c> "name=value[; Domain=…; Path=…]", repeatable;
|
|
54
|
+
sent to the target origin only
|
|
55
|
+
--header <h> "Name: value", repeatable; target origin only
|
|
56
|
+
--storage-state <file> load a Playwright storageState JSON
|
|
57
|
+
--no-session don't use the session saved by \`rpc login\`
|
|
58
|
+
--allow-origin <o> also send headers/cookies to this origin, repeatable
|
|
59
|
+
|
|
42
60
|
Run \`rpc help <command>\` for one command's options and examples.
|
|
43
61
|
Run \`rpc\` with no arguments in a terminal for an interactive capture.
|
|
44
62
|
|
|
45
63
|
Examples:
|
|
46
64
|
rpc shot localhost:3000 -d iphone-15,ipad,desktop-1440 --bezels
|
|
47
65
|
rpc shot https://example.com --full-page --color-scheme dark
|
|
66
|
+
rpc login localhost:3000/sign-in && rpc shot localhost:3000/dashboard
|
|
67
|
+
rpc check localhost:3000 --fail-on warn
|
|
48
68
|
`;
|
|
49
|
-
export function parseCli(argv) {
|
|
69
|
+
export function parseCli(argv, env = process.env) {
|
|
50
70
|
const [cmd, ...rest] = argv;
|
|
51
71
|
switch (cmd) {
|
|
52
72
|
case undefined:
|
|
@@ -67,10 +87,16 @@ export function parseCli(argv) {
|
|
|
67
87
|
if (wantsHelp(rest))
|
|
68
88
|
return { command: 'help', topic: 'mcp' };
|
|
69
89
|
return { command: 'mcp' };
|
|
70
|
-
case 'install':
|
|
90
|
+
case 'install': {
|
|
71
91
|
if (wantsHelp(rest))
|
|
72
92
|
return { command: 'help', topic: 'install' };
|
|
73
|
-
|
|
93
|
+
const { values } = parse(rest, { headed: { type: 'boolean' } }, false);
|
|
94
|
+
return values.headed ? { command: 'install', headed: true } : { command: 'install' };
|
|
95
|
+
}
|
|
96
|
+
case 'login':
|
|
97
|
+
return parseLogin(rest, env);
|
|
98
|
+
case 'sessions':
|
|
99
|
+
return parseSessions(rest);
|
|
74
100
|
case 'doctor': {
|
|
75
101
|
if (wantsHelp(rest))
|
|
76
102
|
return { command: 'help', topic: 'doctor' };
|
|
@@ -85,6 +111,8 @@ export function parseCli(argv) {
|
|
|
85
111
|
}
|
|
86
112
|
case 'shot':
|
|
87
113
|
return parseShot(rest);
|
|
114
|
+
case 'check':
|
|
115
|
+
return parseCheck(rest);
|
|
88
116
|
default:
|
|
89
117
|
throw new ArgError(`Unknown command "${cmd}". Run \`rpc --help\`.`);
|
|
90
118
|
}
|
|
@@ -108,6 +136,7 @@ function parseShot(args) {
|
|
|
108
136
|
'reduced-motion': { type: 'boolean' },
|
|
109
137
|
json: { type: 'boolean' },
|
|
110
138
|
open: { type: 'boolean' },
|
|
139
|
+
...AUTH_FLAGS,
|
|
111
140
|
}, true);
|
|
112
141
|
if (positionals.length === 0)
|
|
113
142
|
throw new ArgError('Missing <url>. Usage: rpc shot <url> [options]');
|
|
@@ -120,17 +149,8 @@ function parseShot(args) {
|
|
|
120
149
|
catch (err) {
|
|
121
150
|
throw new ArgError(err.message);
|
|
122
151
|
}
|
|
123
|
-
if (typeof values.devices === 'string')
|
|
124
|
-
|
|
125
|
-
if (list.length === 0)
|
|
126
|
-
throw new ArgError('--devices is empty');
|
|
127
|
-
try {
|
|
128
|
-
options.devices = resolveDevices(list).map(d => d.id);
|
|
129
|
-
}
|
|
130
|
-
catch (err) {
|
|
131
|
-
throw new ArgError(err.message);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
152
|
+
if (typeof values.devices === 'string')
|
|
153
|
+
options.devices = parseDevices(values.devices);
|
|
134
154
|
if (values.bezels)
|
|
135
155
|
options.bezels = true;
|
|
136
156
|
if (values['full-page'])
|
|
@@ -143,13 +163,8 @@ function parseShot(args) {
|
|
|
143
163
|
options.waitMs = parseMs('--wait', values.wait);
|
|
144
164
|
if (typeof values.timeout === 'string')
|
|
145
165
|
options.timeoutMs = parseMs('--timeout', values.timeout);
|
|
146
|
-
if (typeof values['color-scheme'] === 'string')
|
|
147
|
-
|
|
148
|
-
if (!COLOR_SCHEMES.includes(s)) {
|
|
149
|
-
throw new ArgError(`--color-scheme must be one of ${COLOR_SCHEMES.join(', ')}`);
|
|
150
|
-
}
|
|
151
|
-
options.colorScheme = s;
|
|
152
|
-
}
|
|
166
|
+
if (typeof values['color-scheme'] === 'string')
|
|
167
|
+
options.colorScheme = parseColorScheme(values['color-scheme']);
|
|
153
168
|
if (values['no-emulate'])
|
|
154
169
|
options.emulate = false;
|
|
155
170
|
if (values.slices)
|
|
@@ -162,11 +177,185 @@ function parseShot(args) {
|
|
|
162
177
|
if (typeof values['max-height'] === 'string') {
|
|
163
178
|
options.maxHeight = parseInt_('--max-height', values['max-height'], 500, 200_000);
|
|
164
179
|
}
|
|
180
|
+
const auth = parseAuth(values, options.url);
|
|
181
|
+
if (auth)
|
|
182
|
+
options.auth = auth;
|
|
165
183
|
return { command: 'shot', options, json: Boolean(values.json), open: Boolean(values.open) };
|
|
166
184
|
}
|
|
185
|
+
const AUTH_FLAGS = {
|
|
186
|
+
cookie: { type: 'string', multiple: true },
|
|
187
|
+
header: { type: 'string', multiple: true },
|
|
188
|
+
'storage-state': { type: 'string' },
|
|
189
|
+
'no-session': { type: 'boolean' },
|
|
190
|
+
'allow-origin': { type: 'string', multiple: true },
|
|
191
|
+
};
|
|
192
|
+
/** Cookie/header/session flags, validated against the URL now: bad input is a usage error. */
|
|
193
|
+
function parseAuth(values, url) {
|
|
194
|
+
const auth = {};
|
|
195
|
+
try {
|
|
196
|
+
if (values.cookie?.length)
|
|
197
|
+
auth.cookies = values.cookie.map(parseCookie);
|
|
198
|
+
if (values.header?.length)
|
|
199
|
+
auth.headers = Object.fromEntries(values.header.map(parseHeader));
|
|
200
|
+
if (typeof values['storage-state'] === 'string')
|
|
201
|
+
auth.storageState = values['storage-state'];
|
|
202
|
+
if (values['no-session'])
|
|
203
|
+
auth.useSession = false;
|
|
204
|
+
if (values['allow-origin']?.length)
|
|
205
|
+
auth.allowOrigins = values['allow-origin'].flatMap(splitList).map(toOrigin);
|
|
206
|
+
if (!Object.keys(auth).length)
|
|
207
|
+
return undefined;
|
|
208
|
+
resolveAuth(url, auth);
|
|
209
|
+
return auth;
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
throw new ArgError(err.message);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function parseDevices(raw) {
|
|
216
|
+
const list = splitList(raw);
|
|
217
|
+
if (list.length === 0)
|
|
218
|
+
throw new ArgError('--devices is empty');
|
|
219
|
+
try {
|
|
220
|
+
return resolveDevices(list).map(d => d.id);
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
throw new ArgError(err.message);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function parseCheck(args) {
|
|
227
|
+
if (wantsHelp(args))
|
|
228
|
+
return { command: 'help', topic: 'check' };
|
|
229
|
+
const { values, positionals } = parse(args, {
|
|
230
|
+
devices: { type: 'string', short: 'd' },
|
|
231
|
+
out: { type: 'string', short: 'o' },
|
|
232
|
+
'wait-for': { type: 'string' },
|
|
233
|
+
wait: { type: 'string' },
|
|
234
|
+
'color-scheme': { type: 'string' },
|
|
235
|
+
timeout: { type: 'string' },
|
|
236
|
+
'fail-on': { type: 'string' },
|
|
237
|
+
shots: { type: 'boolean' },
|
|
238
|
+
json: { type: 'boolean' },
|
|
239
|
+
...AUTH_FLAGS,
|
|
240
|
+
}, true);
|
|
241
|
+
if (positionals.length === 0)
|
|
242
|
+
throw new ArgError('Missing <url>. Usage: rpc check <url> [options]');
|
|
243
|
+
if (positionals.length > 1)
|
|
244
|
+
throw new ArgError(`Expected one URL, got: ${positionals.join(' ')}`);
|
|
245
|
+
let options;
|
|
246
|
+
try {
|
|
247
|
+
options = defaultCheckOptions(positionals[0]);
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
throw new ArgError(err.message);
|
|
251
|
+
}
|
|
252
|
+
if (typeof values.devices === 'string')
|
|
253
|
+
options.devices = parseDevices(values.devices);
|
|
254
|
+
if (typeof values.out === 'string')
|
|
255
|
+
options.outDir = values.out;
|
|
256
|
+
if (typeof values['wait-for'] === 'string')
|
|
257
|
+
options.waitFor = values['wait-for'];
|
|
258
|
+
if (typeof values.wait === 'string')
|
|
259
|
+
options.waitMs = parseMs('--wait', values.wait);
|
|
260
|
+
if (typeof values.timeout === 'string')
|
|
261
|
+
options.timeoutMs = parseMs('--timeout', values.timeout);
|
|
262
|
+
if (typeof values['color-scheme'] === 'string')
|
|
263
|
+
options.colorScheme = parseColorScheme(values['color-scheme']);
|
|
264
|
+
if (typeof values['fail-on'] === 'string') {
|
|
265
|
+
if (!FAIL_ON.includes(values['fail-on']))
|
|
266
|
+
throw new ArgError(`--fail-on must be one of ${FAIL_ON.join(', ')}`);
|
|
267
|
+
options.failOn = values['fail-on'];
|
|
268
|
+
}
|
|
269
|
+
if (values.shots)
|
|
270
|
+
options.shots = true;
|
|
271
|
+
const auth = parseAuth(values, options.url);
|
|
272
|
+
if (auth)
|
|
273
|
+
options.auth = auth;
|
|
274
|
+
return { command: 'check', options, json: Boolean(values.json) };
|
|
275
|
+
}
|
|
276
|
+
function parseColorScheme(s) {
|
|
277
|
+
if (!COLOR_SCHEMES.includes(s))
|
|
278
|
+
throw new ArgError(`--color-scheme must be one of ${COLOR_SCHEMES.join(', ')}`);
|
|
279
|
+
return s;
|
|
280
|
+
}
|
|
281
|
+
function parseLogin(args, env) {
|
|
282
|
+
if (wantsHelp(args))
|
|
283
|
+
return { command: 'help', topic: 'login' };
|
|
284
|
+
const { values, positionals } = parse(args, {
|
|
285
|
+
save: { type: 'string' },
|
|
286
|
+
form: { type: 'boolean' },
|
|
287
|
+
email: { type: 'string' },
|
|
288
|
+
password: { type: 'string' },
|
|
289
|
+
'email-selector': { type: 'string' },
|
|
290
|
+
'password-selector': { type: 'string' },
|
|
291
|
+
'submit-selector': { type: 'string' },
|
|
292
|
+
timeout: { type: 'string' },
|
|
293
|
+
json: { type: 'boolean' },
|
|
294
|
+
}, true);
|
|
295
|
+
if (positionals.length !== 1)
|
|
296
|
+
throw new ArgError('Usage: rpc login <url> [--form] [--save <file>]');
|
|
297
|
+
let url;
|
|
298
|
+
try {
|
|
299
|
+
url = normalizeUrl(positionals[0]);
|
|
300
|
+
toOrigin(url);
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
throw new ArgError(err.message);
|
|
304
|
+
}
|
|
305
|
+
const cmd = {
|
|
306
|
+
command: 'login',
|
|
307
|
+
url,
|
|
308
|
+
timeoutMs: typeof values.timeout === 'string' ? parseMs('--timeout', values.timeout) : DEFAULT_TIMEOUT_MS,
|
|
309
|
+
json: Boolean(values.json),
|
|
310
|
+
};
|
|
311
|
+
if (typeof values.save === 'string')
|
|
312
|
+
cmd.save = values.save;
|
|
313
|
+
const formOnly = ['email', 'password', 'email-selector', 'password-selector', 'submit-selector'];
|
|
314
|
+
if (!values.form) {
|
|
315
|
+
const used = formOnly.filter(k => values[k] !== undefined);
|
|
316
|
+
if (used.length)
|
|
317
|
+
throw new ArgError(`--${used[0]} needs --form`);
|
|
318
|
+
return cmd;
|
|
319
|
+
}
|
|
320
|
+
const email = values.email ?? env.RPC_LOGIN_EMAIL;
|
|
321
|
+
const password = values.password ?? env.RPC_LOGIN_PASSWORD;
|
|
322
|
+
if (!email)
|
|
323
|
+
throw new ArgError('--form needs an email: set RPC_LOGIN_EMAIL (or pass --email)');
|
|
324
|
+
if (!password)
|
|
325
|
+
throw new ArgError('--form needs a password: set RPC_LOGIN_PASSWORD');
|
|
326
|
+
cmd.form = {
|
|
327
|
+
email,
|
|
328
|
+
password,
|
|
329
|
+
...(values['email-selector'] ? { emailSelector: values['email-selector'] } : {}),
|
|
330
|
+
...(values['password-selector'] ? { passwordSelector: values['password-selector'] } : {}),
|
|
331
|
+
...(values['submit-selector'] ? { submitSelector: values['submit-selector'] } : {}),
|
|
332
|
+
};
|
|
333
|
+
if (values.password !== undefined)
|
|
334
|
+
cmd.passwordFromFlag = true;
|
|
335
|
+
return cmd;
|
|
336
|
+
}
|
|
337
|
+
function parseSessions(args) {
|
|
338
|
+
if (wantsHelp(args))
|
|
339
|
+
return { command: 'help', topic: 'sessions' };
|
|
340
|
+
const { values, positionals } = parse(args, { json: { type: 'boolean' } }, true);
|
|
341
|
+
const [action = 'list', origin, ...extra] = positionals;
|
|
342
|
+
if (action !== 'list' && action !== 'clear')
|
|
343
|
+
throw new ArgError(`Unknown sessions action "${action}". Use list or clear.`);
|
|
344
|
+
if (extra.length || (action === 'list' && origin))
|
|
345
|
+
throw new ArgError('Usage: rpc sessions list | rpc sessions clear [origin]');
|
|
346
|
+
if (origin) {
|
|
347
|
+
try {
|
|
348
|
+
toOrigin(origin);
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
throw new ArgError(err.message);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return { command: 'sessions', action, ...(origin ? { origin } : {}), json: Boolean(values.json) };
|
|
355
|
+
}
|
|
167
356
|
function parse(args, options, allowPositionals) {
|
|
168
357
|
try {
|
|
169
|
-
return parseArgs({ args, options
|
|
358
|
+
return parseArgs({ args, options, allowPositionals, strict: true });
|
|
170
359
|
}
|
|
171
360
|
catch (err) {
|
|
172
361
|
throw new ArgError(err.message);
|