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/options.js
CHANGED
|
@@ -24,6 +24,20 @@ export function defaultShotOptions(url) {
|
|
|
24
24
|
sliceWidth: DEFAULT_SLICE_WIDTH,
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
|
+
export const FAIL_ON = ['error', 'warn'];
|
|
28
|
+
export function defaultCheckOptions(url) {
|
|
29
|
+
return {
|
|
30
|
+
url: normalizeUrl(url),
|
|
31
|
+
devices: [...DEFAULT_DEVICES],
|
|
32
|
+
outDir: DEFAULT_OUT_DIR,
|
|
33
|
+
waitMs: 0,
|
|
34
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
35
|
+
emulate: true,
|
|
36
|
+
reducedMotion: false,
|
|
37
|
+
shots: false,
|
|
38
|
+
failOn: 'error',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
27
41
|
/**
|
|
28
42
|
* Accept what an agent is likely to type:
|
|
29
43
|
* http(s)://…, file://… -> unchanged
|
package/dist/png.js
CHANGED
|
@@ -8,6 +8,53 @@ export function pngSize(buf) {
|
|
|
8
8
|
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
9
9
|
return { width: view.getUint32(16), height: view.getUint32(20) };
|
|
10
10
|
}
|
|
11
|
+
/** Width/height of a PNG, JPEG, GIF or WebP from its header; undefined for anything else. */
|
|
12
|
+
export function imageSize(buf) {
|
|
13
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
14
|
+
const ascii = (at, len) => String.fromCharCode(...buf.subarray(at, at + len));
|
|
15
|
+
try {
|
|
16
|
+
if (buf.length >= 24 && SIGNATURE.every((b, i) => buf[i] === b))
|
|
17
|
+
return pngSize(buf);
|
|
18
|
+
if (ascii(0, 4) === 'GIF8' && buf.length >= 10) {
|
|
19
|
+
return { width: view.getUint16(6, true), height: view.getUint16(8, true) };
|
|
20
|
+
}
|
|
21
|
+
if (ascii(0, 4) === 'RIFF' && ascii(8, 4) === 'WEBP') {
|
|
22
|
+
const kind = ascii(12, 4);
|
|
23
|
+
if (kind === 'VP8 ')
|
|
24
|
+
return { width: view.getUint16(26, true) & 0x3fff, height: view.getUint16(28, true) & 0x3fff };
|
|
25
|
+
if (kind === 'VP8L') {
|
|
26
|
+
const bits = view.getUint32(21, true);
|
|
27
|
+
return { width: (bits & 0x3fff) + 1, height: ((bits >> 14) & 0x3fff) + 1 };
|
|
28
|
+
}
|
|
29
|
+
if (kind === 'VP8X') {
|
|
30
|
+
const u24 = (at) => buf[at] | (buf[at + 1] << 8) | (buf[at + 2] << 16);
|
|
31
|
+
return { width: u24(24) + 1, height: u24(27) + 1 };
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
if (buf[0] === 0xff && buf[1] === 0xd8) {
|
|
36
|
+
// Walk the JPEG segments to the first start-of-frame marker.
|
|
37
|
+
for (let at = 2; at + 9 < buf.length;) {
|
|
38
|
+
if (buf[at] !== 0xff)
|
|
39
|
+
return undefined;
|
|
40
|
+
const marker = buf[at + 1];
|
|
41
|
+
if (marker === 0xff) {
|
|
42
|
+
at++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const len = view.getUint16(at + 2);
|
|
46
|
+
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
47
|
+
return { width: view.getUint16(at + 7), height: view.getUint16(at + 5) };
|
|
48
|
+
}
|
|
49
|
+
at += 2 + len;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Truncated header.
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
11
58
|
/**
|
|
12
59
|
* Decode an 8-bit RGB/RGBA non-interlaced PNG, which is what Chromium
|
|
13
60
|
* screenshots are. Anything else throws: this is not a general decoder.
|
package/dist/term.js
CHANGED
|
@@ -44,3 +44,23 @@ export function openPath(path) {
|
|
|
44
44
|
return false;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve when a line (Enter) arrives on stdin. EOF never resolves it, so a
|
|
49
|
+
* caller can race it against something else (the login window closing).
|
|
50
|
+
*/
|
|
51
|
+
export function waitForEnter(input = process.stdin) {
|
|
52
|
+
let onData = () => { };
|
|
53
|
+
const promise = new Promise(res => {
|
|
54
|
+
onData = chunk => {
|
|
55
|
+
if (/[\r\n]/.test(String(chunk)))
|
|
56
|
+
res();
|
|
57
|
+
};
|
|
58
|
+
input.on('data', onData);
|
|
59
|
+
input.resume();
|
|
60
|
+
});
|
|
61
|
+
const stop = () => {
|
|
62
|
+
input.off('data', onData);
|
|
63
|
+
input.pause();
|
|
64
|
+
};
|
|
65
|
+
return { promise, stop };
|
|
66
|
+
}
|
package/dist/ui.js
CHANGED
|
@@ -6,11 +6,14 @@ import { connect } from 'node:net';
|
|
|
6
6
|
import { relative } from 'node:path';
|
|
7
7
|
import * as p from '@clack/prompts';
|
|
8
8
|
import pc from 'picocolors';
|
|
9
|
+
import { clearSessions, listSessions, localTime, MASK, resolveAuth, sessionsDir, } from './auth.js';
|
|
9
10
|
import { BrowserMissingError, installHint, launchBrowser } from './browser.js';
|
|
10
11
|
import { capture } from './capture.js';
|
|
12
|
+
import { checkExitCode, runCheck as audit, summaryLine } from './check.js';
|
|
13
|
+
import { countsPhrase, groupFindings, groupLines } from './check-format.js';
|
|
11
14
|
import { CATEGORY_ORDER, DEVICE_ALIASES, DEVICES, } from './devices.js';
|
|
12
15
|
import { defaultShotOptions, normalizeUrl } from './options.js';
|
|
13
|
-
import { openCommand, openPath } from './term.js';
|
|
16
|
+
import { openCommand, openPath, waitForEnter } from './term.js';
|
|
14
17
|
export function colors(enabled) {
|
|
15
18
|
return pc.createColors(enabled);
|
|
16
19
|
}
|
|
@@ -174,7 +177,7 @@ export function printDevices(c) {
|
|
|
174
177
|
}
|
|
175
178
|
// ------------------------------------------------------------------ shot
|
|
176
179
|
/** Lines for the "Capture Summary" box. */
|
|
177
|
-
export function summaryLines(o, c) {
|
|
180
|
+
export function summaryLines(o, c, auth) {
|
|
178
181
|
const yes = (b) => (b ? c.green('yes') : c.dim('no'));
|
|
179
182
|
const rows = [
|
|
180
183
|
['devices', formatList(o.devices.map(d => c.cyan(d)))],
|
|
@@ -188,10 +191,34 @@ export function summaryLines(o, c) {
|
|
|
188
191
|
rows.push(['wait for', o.waitFor]);
|
|
189
192
|
if (!o.emulate)
|
|
190
193
|
rows.push(['emulation', c.yellow('viewport + DPR only')]);
|
|
194
|
+
if (auth)
|
|
195
|
+
rows.push(...authRows(auth, c));
|
|
191
196
|
rows.push(['output', `${rel(o.outDir)}/`]);
|
|
192
197
|
const w = Math.max(...rows.map(([k]) => k.length)) + 1;
|
|
193
198
|
return [c.cyan(o.url), ...rows.map(([k, v]) => ` ${c.dim(`${k}:`.padEnd(w))} ${v}`)];
|
|
194
199
|
}
|
|
200
|
+
/** Auth rows for the summary box: names only, values masked. */
|
|
201
|
+
export function authRows(a, c) {
|
|
202
|
+
const rows = [];
|
|
203
|
+
if (a.session) {
|
|
204
|
+
const saved = a.session.savedAt ? c.dim(` saved ${localTime(a.session.savedAt)}`) : '';
|
|
205
|
+
if (a.session.loaded)
|
|
206
|
+
rows.push(['session', `${c.green(a.session.origin)}${saved}`]);
|
|
207
|
+
else if (a.session.expired)
|
|
208
|
+
rows.push(['session', c.yellow(`${a.session.origin} expired, not loaded`)]);
|
|
209
|
+
else
|
|
210
|
+
rows.push(['session', c.dim(`${a.session.origin} skipped (--no-session)`)]);
|
|
211
|
+
}
|
|
212
|
+
if (a.storageState)
|
|
213
|
+
rows.push(['storage state', rel(a.storageState)]);
|
|
214
|
+
if (a.cookies?.length)
|
|
215
|
+
rows.push(['cookies', formatList(a.cookies.map(x => `${x.name}=${c.dim(MASK)}`))]);
|
|
216
|
+
if (a.headers)
|
|
217
|
+
rows.push(['headers', formatList(Object.keys(a.headers).map(h => `${h}: ${c.dim(MASK)}`))]);
|
|
218
|
+
if (a.allowOrigins?.length)
|
|
219
|
+
rows.push(['also allowed', formatList(a.allowOrigins)]);
|
|
220
|
+
return rows;
|
|
221
|
+
}
|
|
195
222
|
function statusCell(s, c) {
|
|
196
223
|
if (s.error)
|
|
197
224
|
return c.red(s.code ?? 'error');
|
|
@@ -279,10 +306,11 @@ export function capturedLines(result, c) {
|
|
|
279
306
|
return out;
|
|
280
307
|
}
|
|
281
308
|
export async function runShot(options, run, c) {
|
|
309
|
+
const auth = resolveAuth(options.url, options.auth ?? {});
|
|
282
310
|
if (!run.continued) {
|
|
283
311
|
printBanner(c);
|
|
284
312
|
intro(c);
|
|
285
|
-
p.note(summaryLines(options, c).join('\n'), 'Capture Summary');
|
|
313
|
+
p.note(summaryLines(options, c, auth.summary).join('\n'), 'Capture Summary');
|
|
286
314
|
}
|
|
287
315
|
const s = p.spinner();
|
|
288
316
|
s.start('Starting Chromium');
|
|
@@ -313,6 +341,7 @@ export async function runShot(options, run, c) {
|
|
|
313
341
|
try {
|
|
314
342
|
result = await capture(options, {
|
|
315
343
|
browser,
|
|
344
|
+
auth,
|
|
316
345
|
onDeviceStart: (d) => {
|
|
317
346
|
started = performance.now();
|
|
318
347
|
current = p.spinner();
|
|
@@ -362,6 +391,110 @@ export async function runShot(options, run, c) {
|
|
|
362
391
|
p.outro(`${c.green('Done!')} ${c.dim(advice)}`);
|
|
363
392
|
return failed > 0 ? 1 : 0;
|
|
364
393
|
}
|
|
394
|
+
// ------------------------------------------------------------------ check
|
|
395
|
+
/** Lines for the "Check Summary" box. */
|
|
396
|
+
export function checkSummaryLines(o, c, auth) {
|
|
397
|
+
const rows = [
|
|
398
|
+
['devices', formatList(o.devices.map(d => c.cyan(d)))],
|
|
399
|
+
['fail on', o.failOn === 'warn' ? c.yellow('errors and warnings') : 'errors'],
|
|
400
|
+
];
|
|
401
|
+
if (o.colorScheme)
|
|
402
|
+
rows.push(['colour scheme', o.colorScheme]);
|
|
403
|
+
if (o.waitFor)
|
|
404
|
+
rows.push(['wait for', o.waitFor]);
|
|
405
|
+
if (auth)
|
|
406
|
+
rows.push(...authRows(auth, c));
|
|
407
|
+
if (o.shots)
|
|
408
|
+
rows.push(['shots', `${rel(o.outDir)}/`]);
|
|
409
|
+
const w = Math.max(...rows.map(([k]) => k.length)) + 1;
|
|
410
|
+
return [c.cyan(o.url), ...rows.map(([k, v]) => ` ${c.dim(`${k}:`.padEnd(w))} ${v}`)];
|
|
411
|
+
}
|
|
412
|
+
/** The spinner line once a device is done: status, CLS, time. */
|
|
413
|
+
function checkDeviceLine(d, ms, label, c) {
|
|
414
|
+
if (d.error)
|
|
415
|
+
return `${label} ${c.red(d.code ?? 'error')} ${c.dim(formatMs(ms))}`;
|
|
416
|
+
const status = d.status == null ? c.dim('--') : d.status >= 400 ? c.red(String(d.status)) : c.green(String(d.status));
|
|
417
|
+
const cls = d.cls === undefined ? '' : d.cls > 0.25 ? c.red(d.cls.toFixed(3)) : d.cls > 0.1 ? c.yellow(d.cls.toFixed(3)) : c.green(d.cls.toFixed(3));
|
|
418
|
+
return `${label} ${status} ${c.dim('CLS')} ${cls} ${c.dim(formatMs(ms))}`;
|
|
419
|
+
}
|
|
420
|
+
/** One box per check that found something, titled with its counts. */
|
|
421
|
+
export function checkBoxes(report, c) {
|
|
422
|
+
const all = report.devices.map(d => d.device);
|
|
423
|
+
return groupFindings(report).map(g => {
|
|
424
|
+
const counts = countsPhrase(g.findings);
|
|
425
|
+
const tint = g.findings.some(f => f.severity === 'error') ? c.red : g.findings.some(f => f.severity === 'warn') ? c.yellow : c.cyan;
|
|
426
|
+
return { title: `${g.title} ${c.dim('·')} ${tint(counts)}`, lines: groupLines(g, all, c, { icons: true, width: textWidth() }) };
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
export async function runCheck(options, c) {
|
|
430
|
+
const auth = resolveAuth(options.url, options.auth ?? {});
|
|
431
|
+
intro(c);
|
|
432
|
+
p.note(checkSummaryLines(options, c, auth.summary).join('\n'), 'Check Summary');
|
|
433
|
+
const s = p.spinner();
|
|
434
|
+
s.start('Starting Chromium');
|
|
435
|
+
let browser;
|
|
436
|
+
try {
|
|
437
|
+
browser = await launchBrowser();
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
if (err instanceof BrowserMissingError) {
|
|
441
|
+
s.error(c.red("Playwright's Chromium is not installed"));
|
|
442
|
+
p.note(wrap(installHint().replace(/^Fix: /, ''), textWidth()).join('\n'), c.yellow('Fix'));
|
|
443
|
+
p.outro(`${c.red('Nothing checked.')} ${c.dim('Run: rpc install')}`);
|
|
444
|
+
return 1;
|
|
445
|
+
}
|
|
446
|
+
s.error(c.red(`Chromium did not start: ${err.message.split('\n')[0]}`));
|
|
447
|
+
p.outro(c.red('Nothing checked.'));
|
|
448
|
+
return 1;
|
|
449
|
+
}
|
|
450
|
+
s.stop(`Chromium ${c.dim(browser.version())}`);
|
|
451
|
+
let current;
|
|
452
|
+
let started = 0;
|
|
453
|
+
let report;
|
|
454
|
+
const picked = options.devices.map(id => DEVICES.find(d => d.id === id)).filter((d) => Boolean(d));
|
|
455
|
+
const idW = Math.max(0, ...picked.map(d => d.id.length));
|
|
456
|
+
const vpW = Math.max(0, ...picked.map(d => viewport(d).length));
|
|
457
|
+
const label = (id, vp) => `${id.padEnd(idW)} ${c.dim(vp.padEnd(vpW))}`;
|
|
458
|
+
try {
|
|
459
|
+
report = await audit(options, {
|
|
460
|
+
browser,
|
|
461
|
+
auth,
|
|
462
|
+
onDeviceStart: (d) => {
|
|
463
|
+
started = performance.now();
|
|
464
|
+
current = p.spinner();
|
|
465
|
+
current.start(label(d.id, viewport(d)));
|
|
466
|
+
},
|
|
467
|
+
onDevice: d => {
|
|
468
|
+
const line = checkDeviceLine(d, performance.now() - started, label(d.device, viewport({ ...d.viewport, dpr: d.dpr })), c);
|
|
469
|
+
if (d.error)
|
|
470
|
+
current?.error(line);
|
|
471
|
+
else
|
|
472
|
+
current?.stop(line);
|
|
473
|
+
current = undefined;
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
catch (err) {
|
|
478
|
+
current?.error(c.red(err.message));
|
|
479
|
+
p.outro(c.red('Check failed.'));
|
|
480
|
+
return 1;
|
|
481
|
+
}
|
|
482
|
+
finally {
|
|
483
|
+
await browser.close().catch(() => { });
|
|
484
|
+
}
|
|
485
|
+
for (const box of checkBoxes(report, c))
|
|
486
|
+
p.note(box.lines.join('\n'), box.title);
|
|
487
|
+
const code = checkExitCode(report, options.failOn);
|
|
488
|
+
const line = summaryLine(report.summary);
|
|
489
|
+
const sum = report.summary;
|
|
490
|
+
if (sum.errors)
|
|
491
|
+
p.outro(`${c.red(line)} ${c.dim('Exit code 1.')}`);
|
|
492
|
+
else if (sum.warnings)
|
|
493
|
+
p.outro(`${c.yellow(line)} ${c.dim(code ? 'Exit code 1 (--fail-on warn).' : 'No errors.')}`);
|
|
494
|
+
else
|
|
495
|
+
p.outro(`${c.green(line)} ${c.dim('Ready to ship.')}`);
|
|
496
|
+
return code;
|
|
497
|
+
}
|
|
365
498
|
// ------------------------------------------------------------------ interactive
|
|
366
499
|
const PRESELECT = ['iphone-15-pro', 'ipad-air-11', 'desktop-1440'];
|
|
367
500
|
const DEV_PORTS = [3000, 5173, 4321, 8080];
|
|
@@ -466,6 +599,90 @@ export async function runInteractive(c) {
|
|
|
466
599
|
}
|
|
467
600
|
return runShot(options, { open: false, continued: true, showCommand: true }, c);
|
|
468
601
|
}
|
|
602
|
+
// ------------------------------------------------------------------ login
|
|
603
|
+
export async function runLogin(cmd, c) {
|
|
604
|
+
intro(c);
|
|
605
|
+
const { formLogin, interactiveLogin, HeadedBrowserMissingError } = await import('./login.js');
|
|
606
|
+
const s = p.spinner();
|
|
607
|
+
let spinning = false;
|
|
608
|
+
try {
|
|
609
|
+
let result;
|
|
610
|
+
if (cmd.form) {
|
|
611
|
+
s.start(`Signing in at ${c.cyan(cmd.url)} ${c.dim('(headless, form)')}`);
|
|
612
|
+
spinning = true;
|
|
613
|
+
result = await formLogin(cmd, cmd.form);
|
|
614
|
+
spinning = false;
|
|
615
|
+
s.stop(`Signed in ${c.dim(`now at ${result.finalUrl}`)}`);
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
s.start('Opening Chromium');
|
|
619
|
+
spinning = true;
|
|
620
|
+
const enter = waitForEnter();
|
|
621
|
+
try {
|
|
622
|
+
result = await interactiveLogin(cmd, enter.promise, () => {
|
|
623
|
+
spinning = false;
|
|
624
|
+
s.stop(`Opened ${c.cyan(cmd.url)}`);
|
|
625
|
+
p.log.info(`Sign in in the browser window, then press ${c.bold('Enter')} here ${c.dim('(or close the window)')}`);
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
finally {
|
|
629
|
+
enter.stop();
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (result.stillOnLogin) {
|
|
633
|
+
p.log.warn(c.yellow(`The page was still on a login URL (${result.finalUrl}). The sign-in may not have finished.`));
|
|
634
|
+
}
|
|
635
|
+
p.note([
|
|
636
|
+
`${c.dim('origin:')} ${c.cyan(result.origin)}`,
|
|
637
|
+
`${c.dim('file:')} ${rel(result.path)} ${c.dim('(mode 0600)')}`,
|
|
638
|
+
`${c.dim('cookies:')} ${result.cookies}`,
|
|
639
|
+
`${c.dim('localStorage:')} ${result.localStorage} origin(s)`,
|
|
640
|
+
].join('\n'), 'Saved session');
|
|
641
|
+
const next = cmd.save ? `rpc shot <url> --storage-state ${quote(rel(result.path))}` : `rpc shot ${result.origin}/…`;
|
|
642
|
+
p.outro(`${c.green('Done!')} ${c.dim(`Next: ${next}`)}`);
|
|
643
|
+
return 0;
|
|
644
|
+
}
|
|
645
|
+
catch (err) {
|
|
646
|
+
const msg = err.message;
|
|
647
|
+
if (spinning)
|
|
648
|
+
s.error(c.red(err instanceof HeadedBrowserMissingError || err instanceof BrowserMissingError ? 'No browser to open' : 'Sign-in failed'));
|
|
649
|
+
if (err instanceof HeadedBrowserMissingError || err instanceof BrowserMissingError) {
|
|
650
|
+
p.note(wrap(msg.replace(/^Fix: /, ''), textWidth()).join('\n'), c.yellow('Fix'));
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
p.log.error(wrap(msg, textWidth()).join('\n'));
|
|
654
|
+
}
|
|
655
|
+
p.outro(c.red('No session saved.'));
|
|
656
|
+
return 1;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
export async function runSessions(action, origin, c) {
|
|
660
|
+
intro(c);
|
|
661
|
+
if (action === 'clear') {
|
|
662
|
+
const gone = await clearSessions(origin);
|
|
663
|
+
if (gone.length === 0)
|
|
664
|
+
p.log.info(origin ? `No saved session for ${c.cyan(origin)}` : 'No saved sessions');
|
|
665
|
+
for (const o of gone)
|
|
666
|
+
p.log.success(`Removed ${c.cyan(o)}`);
|
|
667
|
+
p.outro(c.dim(`${gone.length} removed`));
|
|
668
|
+
return 0;
|
|
669
|
+
}
|
|
670
|
+
const list = await listSessions();
|
|
671
|
+
if (list.length === 0) {
|
|
672
|
+
p.log.info(`No saved sessions in ${c.dim(sessionsDir())}`);
|
|
673
|
+
p.outro(c.dim('Create one with: rpc login <url>'));
|
|
674
|
+
return 0;
|
|
675
|
+
}
|
|
676
|
+
const when = localTime;
|
|
677
|
+
const originW = Math.max(...list.map(s => s.origin.length));
|
|
678
|
+
const lines = list.map(s => {
|
|
679
|
+
const exp = s.expired ? c.red(`expired ${when(s.expires)}`) : s.expires ? c.dim(`expires ${when(s.expires)}`) : c.dim('no expiry');
|
|
680
|
+
return `${c.cyan(s.origin.padEnd(originW))} ${String(s.cookies).padStart(2)} cookie${s.cookies === 1 ? ' ' : 's'} ${c.dim(`saved ${when(s.savedAt)}`)} ${exp}`;
|
|
681
|
+
});
|
|
682
|
+
p.note(lines.join('\n'), `${list.length} saved session${list.length === 1 ? '' : 's'}`);
|
|
683
|
+
p.outro(c.dim(sessionsDir()));
|
|
684
|
+
return 0;
|
|
685
|
+
}
|
|
469
686
|
// ------------------------------------------------------------------ doctor
|
|
470
687
|
export async function runDoctor(check, c) {
|
|
471
688
|
intro(c);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rpc-agent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Screenshot a URL at device viewports, optionally framed in device bezels. CLI and MCP server for AI coding agents.",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Screenshot a URL at device viewports, optionally framed in device bezels, signed in or not, and audit it before launch. CLI and MCP server for AI coding agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
7
7
|
"mcp-server",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"zod": "^4.6.7"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
+
"@fontsource/ibm-plex-mono": "^5.3.0",
|
|
58
59
|
"@types/node": "^22.20.5",
|
|
59
60
|
"@xterm/headless": "^5.5.0",
|
|
60
61
|
"tsx": "^4.23.15",
|