rpc-agent 0.0.0-stage → 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.
Files changed (43) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +16 -0
  4. package/README.md +471 -2
  5. package/assets/bezels/imac.svg +63 -0
  6. package/assets/bezels/ipad.svg +27 -0
  7. package/assets/bezels/iphone-11-pro.svg +35 -0
  8. package/assets/bezels/iphone-12-mini.svg +26 -0
  9. package/assets/bezels/iphone-12-pro-max.svg +26 -0
  10. package/assets/bezels/iphone-17-pro.svg +172 -0
  11. package/assets/bezels/iphone-se.svg +37 -0
  12. package/assets/bezels/laptop.svg +25 -0
  13. package/assets/bezels/macbook-air.svg +235 -0
  14. package/assets/bezels/macbook-pro-13.svg +63 -0
  15. package/assets/bezels/macbook-pro-14.svg +115 -0
  16. package/assets/bezels/macbook-pro.svg +32 -0
  17. package/assets/bezels/macbook.svg +25 -0
  18. package/assets/bezels/monitor.svg +57 -0
  19. package/assets/bezels/phone-classic.svg +17 -0
  20. package/assets/bezels/phone-home.svg +12 -0
  21. package/assets/bezels/phone-island.svg +130 -0
  22. package/assets/bezels/phone-notch.svg +26 -0
  23. package/assets/bezels/surface-laptop.svg +84 -0
  24. package/assets/bezels/tablet.svg +25 -0
  25. package/assets/bezels/windows-laptop.svg +84 -0
  26. package/dist/args.js +308 -0
  27. package/dist/auth.js +416 -0
  28. package/dist/bezel.js +74 -0
  29. package/dist/browser.js +82 -0
  30. package/dist/capture.js +274 -0
  31. package/dist/cli.js +255 -0
  32. package/dist/composite.js +115 -0
  33. package/dist/devices.js +407 -0
  34. package/dist/errors.js +115 -0
  35. package/dist/help.js +160 -0
  36. package/dist/login.js +150 -0
  37. package/dist/mcp.js +312 -0
  38. package/dist/options.js +51 -0
  39. package/dist/png.js +183 -0
  40. package/dist/slices.js +59 -0
  41. package/dist/term.js +66 -0
  42. package/dist/ui.js +599 -0
  43. package/package.json +68 -4
package/dist/args.js ADDED
@@ -0,0 +1,308 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { parseCookie, parseHeader, resolveAuth, toOrigin } from './auth.js';
3
+ import { resolveDevices } from './devices.js';
4
+ import { COLOR_SCHEMES, DEFAULT_TIMEOUT_MS, defaultShotOptions, normalizeUrl, } from './options.js';
5
+ export const HELP_TOPICS = ['shot', 'login', 'sessions', 'devices', 'doctor', 'install', 'mcp'];
6
+ function isTopic(s) {
7
+ return HELP_TOPICS.includes(s ?? '');
8
+ }
9
+ function wantsHelp(args) {
10
+ return args.includes('--help') || args.includes('-h');
11
+ }
12
+ export class ArgError extends Error {
13
+ name = 'ArgError';
14
+ }
15
+ export const USAGE = `rpc - screenshot a URL at device viewports, optionally in device bezels
16
+
17
+ Usage:
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
22
+ rpc devices [--json] list device ids and aliases
23
+ rpc mcp run the MCP server on stdio
24
+ rpc doctor [--json] check that Playwright's Chromium is installed and launches
25
+ rpc install [--headed] download Playwright's Chromium (same version rpc uses)
26
+
27
+ Shot options:
28
+ -d, --devices <ids> comma-separated device ids or aliases
29
+ (default: iphone-15-pro,desktop-1440)
30
+ -b, --bezels frame each shot in its device bezel
31
+ -f, --full-page capture the full scroll height (stitched, any height)
32
+ --slices with --full-page: also write viewport-tall JPEG
33
+ review slices (<stem>-slice-NN.jpg)
34
+ --slice-width <px> max slice width (default: 768)
35
+ --max-height <px> full-page cap in CSS px (default: 60000)
36
+ --reduced-motion emulate prefers-reduced-motion: reduce
37
+ -o, --out <dir> output directory (default: ./.rpc-shots)
38
+ --wait-for <selector> wait for a CSS selector before capturing
39
+ --wait <ms> extra settle delay after load (default: 0)
40
+ --color-scheme <s> light | dark | no-preference
41
+ --timeout <ms> navigation timeout (default: 30000)
42
+ --no-emulate viewport + DPR only (no mobile UA / touch)
43
+ --json print the result as JSON
44
+ --open open the output folder when done
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
+
54
+ Run \`rpc help <command>\` for one command's options and examples.
55
+ Run \`rpc\` with no arguments in a terminal for an interactive capture.
56
+
57
+ Examples:
58
+ rpc shot localhost:3000 -d iphone-15,ipad,desktop-1440 --bezels
59
+ rpc shot https://example.com --full-page --color-scheme dark
60
+ rpc login localhost:3000/sign-in && rpc shot localhost:3000/dashboard
61
+ `;
62
+ export function parseCli(argv, env = process.env) {
63
+ const [cmd, ...rest] = argv;
64
+ switch (cmd) {
65
+ case undefined:
66
+ case 'help':
67
+ case '-h':
68
+ case '--help': {
69
+ const topic = rest[0];
70
+ if (topic === undefined)
71
+ return { command: 'help' };
72
+ if (!isTopic(topic))
73
+ throw new ArgError(`No help for "${topic}". Topics: ${HELP_TOPICS.join(', ')}`);
74
+ return { command: 'help', topic };
75
+ }
76
+ case '-v':
77
+ case '--version':
78
+ return { command: 'version' };
79
+ case 'mcp':
80
+ if (wantsHelp(rest))
81
+ return { command: 'help', topic: 'mcp' };
82
+ return { command: 'mcp' };
83
+ case 'install': {
84
+ if (wantsHelp(rest))
85
+ return { command: 'help', topic: 'install' };
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);
93
+ case 'doctor': {
94
+ if (wantsHelp(rest))
95
+ return { command: 'help', topic: 'doctor' };
96
+ const { values } = parse(rest, { json: { type: 'boolean' } }, false);
97
+ return { command: 'doctor', json: Boolean(values.json) };
98
+ }
99
+ case 'devices': {
100
+ if (wantsHelp(rest))
101
+ return { command: 'help', topic: 'devices' };
102
+ const { values } = parse(rest, { json: { type: 'boolean' } }, false);
103
+ return { command: 'devices', json: Boolean(values.json) };
104
+ }
105
+ case 'shot':
106
+ return parseShot(rest);
107
+ default:
108
+ throw new ArgError(`Unknown command "${cmd}". Run \`rpc --help\`.`);
109
+ }
110
+ }
111
+ function parseShot(args) {
112
+ if (wantsHelp(args))
113
+ return { command: 'help', topic: 'shot' };
114
+ const { values, positionals } = parse(args, {
115
+ devices: { type: 'string', short: 'd' },
116
+ bezels: { type: 'boolean', short: 'b' },
117
+ 'full-page': { type: 'boolean', short: 'f' },
118
+ out: { type: 'string', short: 'o' },
119
+ 'wait-for': { type: 'string' },
120
+ wait: { type: 'string' },
121
+ 'color-scheme': { type: 'string' },
122
+ timeout: { type: 'string' },
123
+ 'no-emulate': { type: 'boolean' },
124
+ slices: { type: 'boolean' },
125
+ 'slice-width': { type: 'string' },
126
+ 'max-height': { type: 'string' },
127
+ 'reduced-motion': { type: 'boolean' },
128
+ json: { type: 'boolean' },
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 },
135
+ }, true);
136
+ if (positionals.length === 0)
137
+ throw new ArgError('Missing <url>. Usage: rpc shot <url> [options]');
138
+ if (positionals.length > 1)
139
+ throw new ArgError(`Expected one URL, got: ${positionals.join(' ')}`);
140
+ let options;
141
+ try {
142
+ options = defaultShotOptions(positionals[0]);
143
+ }
144
+ catch (err) {
145
+ throw new ArgError(err.message);
146
+ }
147
+ if (typeof values.devices === 'string') {
148
+ const list = splitList(values.devices);
149
+ if (list.length === 0)
150
+ throw new ArgError('--devices is empty');
151
+ try {
152
+ options.devices = resolveDevices(list).map(d => d.id);
153
+ }
154
+ catch (err) {
155
+ throw new ArgError(err.message);
156
+ }
157
+ }
158
+ if (values.bezels)
159
+ options.bezels = true;
160
+ if (values['full-page'])
161
+ options.fullPage = true;
162
+ if (typeof values.out === 'string')
163
+ options.outDir = values.out;
164
+ if (typeof values['wait-for'] === 'string')
165
+ options.waitFor = values['wait-for'];
166
+ if (typeof values.wait === 'string')
167
+ options.waitMs = parseMs('--wait', values.wait);
168
+ if (typeof values.timeout === 'string')
169
+ options.timeoutMs = parseMs('--timeout', values.timeout);
170
+ if (typeof values['color-scheme'] === 'string') {
171
+ const s = values['color-scheme'];
172
+ if (!COLOR_SCHEMES.includes(s)) {
173
+ throw new ArgError(`--color-scheme must be one of ${COLOR_SCHEMES.join(', ')}`);
174
+ }
175
+ options.colorScheme = s;
176
+ }
177
+ if (values['no-emulate'])
178
+ options.emulate = false;
179
+ if (values.slices)
180
+ options.saveSlices = true;
181
+ if (values['reduced-motion'])
182
+ options.reducedMotion = true;
183
+ if (typeof values['slice-width'] === 'string') {
184
+ options.sliceWidth = parseInt_('--slice-width', values['slice-width'], 256, 4096);
185
+ }
186
+ if (typeof values['max-height'] === 'string') {
187
+ options.maxHeight = parseInt_('--max-height', values['max-height'], 500, 200_000);
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
+ }
209
+ return { command: 'shot', options, json: Boolean(values.json), open: Boolean(values.open) };
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
+ }
286
+ function parse(args, options, allowPositionals) {
287
+ try {
288
+ return parseArgs({ args, options, allowPositionals, strict: true });
289
+ }
290
+ catch (err) {
291
+ throw new ArgError(err.message);
292
+ }
293
+ }
294
+ function splitList(s) {
295
+ return s.split(',').map(x => x.trim()).filter(Boolean);
296
+ }
297
+ function parseInt_(flag, raw, min, max) {
298
+ const n = Number(raw);
299
+ if (!Number.isInteger(n) || n < min || n > max)
300
+ throw new ArgError(`${flag} must be an integer from ${min} to ${max}`);
301
+ return n;
302
+ }
303
+ function parseMs(flag, raw) {
304
+ const n = Number(raw);
305
+ if (!Number.isFinite(n) || n < 0)
306
+ throw new ArgError(`${flag} must be a non-negative number of ms`);
307
+ return Math.round(n);
308
+ }