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/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 download Playwright's Chromium (same version rpc uses)
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
- return { command: 'install' };
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
- const list = splitList(values.devices);
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
- const s = values['color-scheme'];
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: options, allowPositionals, strict: true });
358
+ return parseArgs({ args, options, allowPositionals, strict: true });
170
359
  }
171
360
  catch (err) {
172
361
  throw new ArgError(err.message);