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.
- package/CHANGELOG.md +157 -0
- package/LICENSE +21 -0
- package/NOTICE +16 -0
- package/README.md +471 -2
- package/assets/bezels/imac.svg +63 -0
- package/assets/bezels/ipad.svg +27 -0
- package/assets/bezels/iphone-11-pro.svg +35 -0
- package/assets/bezels/iphone-12-mini.svg +26 -0
- package/assets/bezels/iphone-12-pro-max.svg +26 -0
- package/assets/bezels/iphone-17-pro.svg +172 -0
- package/assets/bezels/iphone-se.svg +37 -0
- package/assets/bezels/laptop.svg +25 -0
- package/assets/bezels/macbook-air.svg +235 -0
- package/assets/bezels/macbook-pro-13.svg +63 -0
- package/assets/bezels/macbook-pro-14.svg +115 -0
- package/assets/bezels/macbook-pro.svg +32 -0
- package/assets/bezels/macbook.svg +25 -0
- package/assets/bezels/monitor.svg +57 -0
- package/assets/bezels/phone-classic.svg +17 -0
- package/assets/bezels/phone-home.svg +12 -0
- package/assets/bezels/phone-island.svg +130 -0
- package/assets/bezels/phone-notch.svg +26 -0
- package/assets/bezels/surface-laptop.svg +84 -0
- package/assets/bezels/tablet.svg +25 -0
- package/assets/bezels/windows-laptop.svg +84 -0
- package/dist/args.js +308 -0
- package/dist/auth.js +416 -0
- package/dist/bezel.js +74 -0
- package/dist/browser.js +82 -0
- package/dist/capture.js +274 -0
- package/dist/cli.js +255 -0
- package/dist/composite.js +115 -0
- package/dist/devices.js +407 -0
- package/dist/errors.js +115 -0
- package/dist/help.js +160 -0
- package/dist/login.js +150 -0
- package/dist/mcp.js +312 -0
- package/dist/options.js +51 -0
- package/dist/png.js +183 -0
- package/dist/slices.js +59 -0
- package/dist/term.js +66 -0
- package/dist/ui.js +599 -0
- package/package.json +68 -4
package/dist/png.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { createDeflate, inflateSync } from 'node:zlib';
|
|
2
|
+
const SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
3
|
+
/** Read width/height from a PNG's IHDR chunk without decoding it. */
|
|
4
|
+
export function pngSize(buf) {
|
|
5
|
+
if (buf.length < 24 || SIGNATURE.some((b, i) => buf[i] !== b)) {
|
|
6
|
+
throw new Error('Not a PNG');
|
|
7
|
+
}
|
|
8
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
9
|
+
return { width: view.getUint32(16), height: view.getUint32(20) };
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Decode an 8-bit RGB/RGBA non-interlaced PNG, which is what Chromium
|
|
13
|
+
* screenshots are. Anything else throws: this is not a general decoder.
|
|
14
|
+
*/
|
|
15
|
+
export function decodePng(buf) {
|
|
16
|
+
const { width, height } = pngSize(buf);
|
|
17
|
+
let pos = 8;
|
|
18
|
+
const idat = [];
|
|
19
|
+
let channels;
|
|
20
|
+
while (pos + 8 <= buf.length) {
|
|
21
|
+
const len = buf.readUInt32BE(pos);
|
|
22
|
+
const type = buf.toString('latin1', pos + 4, pos + 8);
|
|
23
|
+
const body = buf.subarray(pos + 8, pos + 8 + len);
|
|
24
|
+
if (type === 'IHDR') {
|
|
25
|
+
const [depth, color, , , interlace] = body.subarray(8, 13);
|
|
26
|
+
if (depth !== 8 || (color !== 2 && color !== 6) || interlace !== 0) {
|
|
27
|
+
throw new Error(`Unsupported PNG (bit depth ${depth}, colour type ${color}, interlace ${interlace})`);
|
|
28
|
+
}
|
|
29
|
+
channels = color === 6 ? 4 : 3;
|
|
30
|
+
}
|
|
31
|
+
else if (type === 'IDAT') {
|
|
32
|
+
idat.push(body);
|
|
33
|
+
}
|
|
34
|
+
else if (type === 'IEND') {
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
pos += 12 + len;
|
|
38
|
+
}
|
|
39
|
+
if (!channels)
|
|
40
|
+
throw new Error('PNG has no IHDR');
|
|
41
|
+
const filtered = inflateSync(Buffer.concat(idat));
|
|
42
|
+
const stride = width * channels;
|
|
43
|
+
if (filtered.length < height * (stride + 1))
|
|
44
|
+
throw new Error('Truncated PNG data');
|
|
45
|
+
const out = Buffer.alloc(height * stride);
|
|
46
|
+
const bpp = channels;
|
|
47
|
+
for (let y = 0; y < height; y++) {
|
|
48
|
+
const f = filtered[y * (stride + 1)];
|
|
49
|
+
const src = y * (stride + 1) + 1;
|
|
50
|
+
const dst = y * stride;
|
|
51
|
+
const up = dst - stride;
|
|
52
|
+
for (let i = 0; i < stride; i++) {
|
|
53
|
+
const x = filtered[src + i];
|
|
54
|
+
const a = i >= bpp ? out[dst + i - bpp] : 0;
|
|
55
|
+
const b = y > 0 ? out[up + i] : 0;
|
|
56
|
+
let v;
|
|
57
|
+
switch (f) {
|
|
58
|
+
case 0:
|
|
59
|
+
v = x;
|
|
60
|
+
break;
|
|
61
|
+
case 1:
|
|
62
|
+
v = x + a;
|
|
63
|
+
break;
|
|
64
|
+
case 2:
|
|
65
|
+
v = x + b;
|
|
66
|
+
break;
|
|
67
|
+
case 3:
|
|
68
|
+
v = x + ((a + b) >> 1);
|
|
69
|
+
break;
|
|
70
|
+
case 4: {
|
|
71
|
+
const c = y > 0 && i >= bpp ? out[up + i - bpp] : 0;
|
|
72
|
+
const p = a + b - c;
|
|
73
|
+
const pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);
|
|
74
|
+
v = x + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
default: throw new Error(`Bad PNG filter type ${f}`);
|
|
78
|
+
}
|
|
79
|
+
out[dst + i] = v & 0xff;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { width, height, channels, data: out };
|
|
83
|
+
}
|
|
84
|
+
function toChannels(img, channels) {
|
|
85
|
+
if (img.channels === channels)
|
|
86
|
+
return img.data;
|
|
87
|
+
const n = img.width * img.height;
|
|
88
|
+
const out = Buffer.alloc(n * channels, 0xff);
|
|
89
|
+
for (let p = 0; p < n; p++) {
|
|
90
|
+
for (let c = 0; c < 3; c++)
|
|
91
|
+
out[p * channels + c] = img.data[p * img.channels + c];
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
const CRC_TABLE = (() => {
|
|
96
|
+
const t = new Uint32Array(256);
|
|
97
|
+
for (let n = 0; n < 256; n++) {
|
|
98
|
+
let c = n;
|
|
99
|
+
for (let k = 0; k < 8; k++)
|
|
100
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
101
|
+
t[n] = c >>> 0;
|
|
102
|
+
}
|
|
103
|
+
return t;
|
|
104
|
+
})();
|
|
105
|
+
function crc32(parts) {
|
|
106
|
+
let c = 0xffffffff;
|
|
107
|
+
for (const buf of parts)
|
|
108
|
+
for (let i = 0; i < buf.length; i++)
|
|
109
|
+
c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
|
110
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
111
|
+
}
|
|
112
|
+
function chunk(type, body) {
|
|
113
|
+
const head = Buffer.alloc(8);
|
|
114
|
+
head.writeUInt32BE(body.length, 0);
|
|
115
|
+
head.write(type, 4, 'latin1');
|
|
116
|
+
const crc = Buffer.alloc(4);
|
|
117
|
+
crc.writeUInt32BE(crc32([head.subarray(4), body]));
|
|
118
|
+
return Buffer.concat([head, body, crc]);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Stack PNGs of equal width top to bottom into one PNG. Each input is decoded,
|
|
122
|
+
* filtered (Up) and streamed into a single deflate stream, so peak memory is
|
|
123
|
+
* about one input's raw pixels plus the compressed output: the result can be
|
|
124
|
+
* far taller than any canvas or Chromium capture allows.
|
|
125
|
+
*/
|
|
126
|
+
export async function stitchPngs(pngs) {
|
|
127
|
+
if (pngs.length === 0)
|
|
128
|
+
throw new Error('Nothing to stitch');
|
|
129
|
+
if (pngs.length === 1)
|
|
130
|
+
return pngs[0];
|
|
131
|
+
const sizes = pngs.map(pngSize);
|
|
132
|
+
const width = sizes[0].width;
|
|
133
|
+
if (sizes.some(s => s.width !== width))
|
|
134
|
+
throw new Error('stitchPngs: inputs differ in width');
|
|
135
|
+
const height = sizes.reduce((h, s) => h + s.height, 0);
|
|
136
|
+
// Peek each IHDR colour type so mixed RGB/RGBA inputs share one format.
|
|
137
|
+
const channels = pngs.some(p => p[25] === 6) ? 4 : 3;
|
|
138
|
+
const stride = width * channels;
|
|
139
|
+
const deflate = createDeflate({ level: 6, memLevel: 9 });
|
|
140
|
+
const compressed = [];
|
|
141
|
+
deflate.on('data', (d) => compressed.push(d));
|
|
142
|
+
const done = new Promise((res, rej) => {
|
|
143
|
+
deflate.on('end', res);
|
|
144
|
+
deflate.on('error', rej);
|
|
145
|
+
});
|
|
146
|
+
let prev;
|
|
147
|
+
for (const png of pngs) {
|
|
148
|
+
const img = decodePng(png);
|
|
149
|
+
const raw = toChannels(img, channels);
|
|
150
|
+
const out = Buffer.alloc(img.height * (stride + 1));
|
|
151
|
+
for (let y = 0; y < img.height; y++) {
|
|
152
|
+
const row = raw.subarray(y * stride, (y + 1) * stride);
|
|
153
|
+
const o = y * (stride + 1);
|
|
154
|
+
if (!prev) {
|
|
155
|
+
out[o] = 0;
|
|
156
|
+
row.copy(out, o + 1);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
out[o] = 2; // Up
|
|
160
|
+
for (let i = 0; i < stride; i++)
|
|
161
|
+
out[o + 1 + i] = (row[i] - prev[i]) & 0xff;
|
|
162
|
+
}
|
|
163
|
+
prev = row;
|
|
164
|
+
}
|
|
165
|
+
// Keep the last row alive past this iteration for the next input's Up filter.
|
|
166
|
+
prev = Buffer.from(prev);
|
|
167
|
+
if (!deflate.write(out))
|
|
168
|
+
await new Promise(r => deflate.once('drain', r));
|
|
169
|
+
}
|
|
170
|
+
deflate.end();
|
|
171
|
+
await done;
|
|
172
|
+
const ihdr = Buffer.alloc(13);
|
|
173
|
+
ihdr.writeUInt32BE(width, 0);
|
|
174
|
+
ihdr.writeUInt32BE(height, 4);
|
|
175
|
+
ihdr[8] = 8;
|
|
176
|
+
ihdr[9] = channels === 4 ? 6 : 2;
|
|
177
|
+
const data = Buffer.concat(compressed);
|
|
178
|
+
const idats = [];
|
|
179
|
+
const MAX_IDAT = 8 * 1024 * 1024;
|
|
180
|
+
for (let i = 0; i < data.length; i += MAX_IDAT)
|
|
181
|
+
idats.push(chunk('IDAT', data.subarray(i, i + MAX_IDAT)));
|
|
182
|
+
return Buffer.concat([SIGNATURE, chunk('IHDR', ihdr), ...idats, chunk('IEND', Buffer.alloc(0))]);
|
|
183
|
+
}
|
package/dist/slices.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Chromium can't produce a bitmap taller than this in one capture. */
|
|
2
|
+
export const MAX_CAPTURE_PX = 16_384;
|
|
3
|
+
/** Device px per stitched segment: well under the limit, a few MB raw each. */
|
|
4
|
+
export const SEGMENT_PX = 8_192;
|
|
5
|
+
/** Default absolute cap on full-page height, in CSS px. */
|
|
6
|
+
export const DEFAULT_MAX_HEIGHT = 60_000;
|
|
7
|
+
const isWhole = (n) => Math.abs(n - Math.round(n)) < 1e-6;
|
|
8
|
+
/**
|
|
9
|
+
* Split `totalCss` into capture segments of at most `maxDevicePx` device px.
|
|
10
|
+
* Every segment but the last is a CSS height whose device height is a whole
|
|
11
|
+
* number of pixels (e.g. multiples of 8 at DPR 2.625), so stitched seams
|
|
12
|
+
* land exactly on pixel rows.
|
|
13
|
+
*/
|
|
14
|
+
export function planSegments(totalCss, dpr, maxDevicePx = SEGMENT_PX) {
|
|
15
|
+
const total = Math.max(1, Math.ceil(totalCss));
|
|
16
|
+
let seg = Math.max(1, Math.floor(maxDevicePx / dpr));
|
|
17
|
+
while (seg > 1 && !isWhole(seg * dpr))
|
|
18
|
+
seg--;
|
|
19
|
+
const out = [];
|
|
20
|
+
for (let y = 0; y < total; y += seg)
|
|
21
|
+
out.push({ y, height: Math.min(seg, total - y) });
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
/** True when a page this tall fits a single Chromium capture. */
|
|
25
|
+
export function fitsOneCapture(totalCss, dpr) {
|
|
26
|
+
return Math.ceil(totalCss) * dpr <= MAX_CAPTURE_PX;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Cut a page into top-to-bottom review slices of `sliceHeight` CSS px (one
|
|
30
|
+
* viewport by default). A last slice shorter than a fifth of `sliceHeight` is
|
|
31
|
+
* folded into the one before it rather than shown as a sliver.
|
|
32
|
+
*/
|
|
33
|
+
export function planSlices(device, totalCss, sliceHeight) {
|
|
34
|
+
const total = Math.max(1, Math.round(totalCss));
|
|
35
|
+
const step = Math.max(1, Math.round(sliceHeight));
|
|
36
|
+
const bands = [];
|
|
37
|
+
for (let y = 0; y < total; y += step)
|
|
38
|
+
bands.push({ y, height: Math.min(step, total - y) });
|
|
39
|
+
if (bands.length > 1 && bands[bands.length - 1].height < step / 5) {
|
|
40
|
+
const last = bands.pop();
|
|
41
|
+
bands[bands.length - 1].height += last.height;
|
|
42
|
+
}
|
|
43
|
+
return bands.map((b, i) => ({
|
|
44
|
+
...b,
|
|
45
|
+
index: i + 1,
|
|
46
|
+
total: bands.length,
|
|
47
|
+
label: sliceLabel(device, i + 1, bands.length, b),
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
export function sliceLabel(device, index, total, b) {
|
|
51
|
+
return `${device} · slice ${index}/${total} · y=${b.y}–${b.y + b.height} css px`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Preview scale (output px per CSS px) for slices: as sharp as the capture
|
|
55
|
+
* allows (never above the DPR) but no wider than `maxWidth`.
|
|
56
|
+
*/
|
|
57
|
+
export function sliceScale(cssWidth, dpr, maxWidth) {
|
|
58
|
+
return Math.min(dpr, maxWidth / cssWidth);
|
|
59
|
+
}
|
package/dist/term.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
function set(v) {
|
|
3
|
+
return v !== undefined && v !== '';
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Rich output needs a real terminal: stdout is a TTY, not CI, TERM isn't
|
|
7
|
+
* `dumb`, and no --json. NO_COLOR turns colour off; FORCE_COLOR (except `0`)
|
|
8
|
+
* turns it on, even for the plain layout, and wins over NO_COLOR.
|
|
9
|
+
*/
|
|
10
|
+
export function outputMode({ isTTY, env, json = false }) {
|
|
11
|
+
if (json)
|
|
12
|
+
return { rich: false, color: false };
|
|
13
|
+
const rich = isTTY && !set(env.CI) && env.TERM !== 'dumb';
|
|
14
|
+
// Same precedence as Node's own util.styleText, which clack uses for its
|
|
15
|
+
// symbols: FORCE_COLOR beats NO_COLOR, NO_COLOR beats the TTY check.
|
|
16
|
+
let color = rich;
|
|
17
|
+
if (set(env.NO_COLOR))
|
|
18
|
+
color = false;
|
|
19
|
+
if (set(env.FORCE_COLOR))
|
|
20
|
+
color = env.FORCE_COLOR !== '0';
|
|
21
|
+
return { rich, color };
|
|
22
|
+
}
|
|
23
|
+
export function currentMode(json = false) {
|
|
24
|
+
return outputMode({ isTTY: Boolean(process.stdout.isTTY), env: process.env, json });
|
|
25
|
+
}
|
|
26
|
+
/** Command that opens a folder or file in the desktop shell. */
|
|
27
|
+
export function openCommand(platform = process.platform) {
|
|
28
|
+
if (platform === 'darwin')
|
|
29
|
+
return { cmd: 'open', args: [] };
|
|
30
|
+
if (platform === 'win32')
|
|
31
|
+
return { cmd: 'explorer', args: [] };
|
|
32
|
+
return { cmd: 'xdg-open', args: [] };
|
|
33
|
+
}
|
|
34
|
+
/** Open a folder or file in the desktop shell, detached and silent. */
|
|
35
|
+
export function openPath(path) {
|
|
36
|
+
const { cmd, args } = openCommand();
|
|
37
|
+
try {
|
|
38
|
+
const child = spawn(cmd, [...args, path], { detached: true, stdio: 'ignore' });
|
|
39
|
+
child.on('error', () => { });
|
|
40
|
+
child.unref();
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
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
|
+
}
|