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/devices.js
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
// Ported from responsive-point-check lib/devices.ts (MIT). Device data is
|
|
2
|
+
// unchanged; user agents, aliases and lookup helpers are added at the end.
|
|
3
|
+
export const DEVICES = [
|
|
4
|
+
// ----------------------------------------------------------- Apple Phones
|
|
5
|
+
{
|
|
6
|
+
id: 'iphone-se',
|
|
7
|
+
name: 'iPhone SE',
|
|
8
|
+
category: 'apple-phone',
|
|
9
|
+
width: 375,
|
|
10
|
+
height: 667,
|
|
11
|
+
dpr: 2,
|
|
12
|
+
bezel: 'phone-home',
|
|
13
|
+
bezelSrc: 'bezels/iphone-se.svg',
|
|
14
|
+
bezelLayout: {
|
|
15
|
+
outerWidth: 427,
|
|
16
|
+
outerHeight: 858,
|
|
17
|
+
screen: { x: 26, y: 95, width: 375, height: 667, radius: 4 },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'iphone-11-pro',
|
|
22
|
+
name: 'iPhone 11 Pro',
|
|
23
|
+
category: 'apple-phone',
|
|
24
|
+
width: 375,
|
|
25
|
+
height: 812,
|
|
26
|
+
dpr: 3,
|
|
27
|
+
bezel: 'phone-notch',
|
|
28
|
+
// Same outer artwork dimensions as iPhone SE in this pack; thinner top
|
|
29
|
+
// and bottom bezels because it's a notched edge-to-edge display.
|
|
30
|
+
bezelSrc: 'bezels/iphone-11-pro.svg',
|
|
31
|
+
bezelLayout: {
|
|
32
|
+
outerWidth: 427,
|
|
33
|
+
outerHeight: 858,
|
|
34
|
+
screen: { x: 26, y: 23, width: 375, height: 812, radius: 40 },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
// iPhone 12 mini — uses iphone-12-mini.svg (artwork 423×858, screen
|
|
38
|
+
// at (24, 23, 375, 812)). Scale viewport 360/375 = 0.96.
|
|
39
|
+
{
|
|
40
|
+
id: 'iphone-12-mini',
|
|
41
|
+
name: 'iPhone 12 mini',
|
|
42
|
+
category: 'apple-phone',
|
|
43
|
+
width: 360,
|
|
44
|
+
height: 780,
|
|
45
|
+
dpr: 3,
|
|
46
|
+
bezel: 'phone-notch',
|
|
47
|
+
bezelSrc: 'bezels/iphone-12-mini.svg',
|
|
48
|
+
bezelLayout: {
|
|
49
|
+
outerWidth: 410,
|
|
50
|
+
outerHeight: 824,
|
|
51
|
+
screen: { x: 25, y: 22, width: 360, height: 780, radius: 40 },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{ id: 'iphone-13', name: 'iPhone 13', category: 'apple-phone', width: 390, height: 844, dpr: 3, bezel: 'phone-notch' },
|
|
55
|
+
{ id: 'iphone-14', name: 'iPhone 14', category: 'apple-phone', width: 390, height: 844, dpr: 3, bezel: 'phone-notch' },
|
|
56
|
+
// iPhone 14 Plus — large notch design. Uses iphone-12-pro-max.svg
|
|
57
|
+
// (artwork 423×858, screen ~(24, 23, 375, 812)). Scale 428/375 = 1.14.
|
|
58
|
+
{
|
|
59
|
+
id: 'iphone-14-plus',
|
|
60
|
+
name: 'iPhone 14 Plus',
|
|
61
|
+
category: 'apple-phone',
|
|
62
|
+
width: 428,
|
|
63
|
+
height: 926,
|
|
64
|
+
dpr: 3,
|
|
65
|
+
bezel: 'phone-notch',
|
|
66
|
+
bezelSrc: 'bezels/iphone-12-pro-max.svg',
|
|
67
|
+
bezelLayout: {
|
|
68
|
+
outerWidth: 482,
|
|
69
|
+
outerHeight: 978,
|
|
70
|
+
screen: { x: 30, y: 26, width: 428, height: 926, radius: 46 },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{ id: 'iphone-14-pro', name: 'iPhone 14 Pro', category: 'apple-phone', width: 393, height: 852, dpr: 3, bezel: 'phone-island' },
|
|
74
|
+
{ id: 'iphone-14-pro-max', name: 'iPhone 14 Pro Max', category: 'apple-phone', width: 430, height: 932, dpr: 3, bezel: 'phone-island' },
|
|
75
|
+
{ id: 'iphone-15', name: 'iPhone 15', category: 'apple-phone', width: 393, height: 852, dpr: 3, bezel: 'phone-island' },
|
|
76
|
+
{ id: 'iphone-15-plus', name: 'iPhone 15 Plus', category: 'apple-phone', width: 430, height: 932, dpr: 3, bezel: 'phone-island' },
|
|
77
|
+
{ id: 'iphone-15-pro', name: 'iPhone 15 Pro', category: 'apple-phone', width: 393, height: 852, dpr: 3, bezel: 'phone-island' },
|
|
78
|
+
{ id: 'iphone-15-pro-max', name: 'iPhone 15 Pro Max', category: 'apple-phone', width: 430, height: 932, dpr: 3, bezel: 'phone-island' },
|
|
79
|
+
{ id: 'iphone-16', name: 'iPhone 16', category: 'apple-phone', width: 393, height: 852, dpr: 3, bezel: 'phone-island' },
|
|
80
|
+
{ id: 'iphone-16-pro', name: 'iPhone 16 Pro', category: 'apple-phone', width: 402, height: 874, dpr: 3, bezel: 'phone-island' },
|
|
81
|
+
{ id: 'iphone-16-pro-max', name: 'iPhone 16 Pro Max', category: 'apple-phone', width: 440, height: 956, dpr: 3, bezel: 'phone-island' },
|
|
82
|
+
// iPhone 17 Pro — uses dedicated iphone-17-pro.svg artwork (1280×2642
|
|
83
|
+
// body with rx=210, twin-lens orange/coral accent on the back). Iframe
|
|
84
|
+
// sits within the inner screen rect at (55, 55, 1170, 2532) per the
|
|
85
|
+
// artwork's white "screen" rectangle. Scale: 402 / 1170 = 0.3436.
|
|
86
|
+
{
|
|
87
|
+
id: 'iphone-17-pro',
|
|
88
|
+
name: 'iPhone 17 Pro',
|
|
89
|
+
category: 'apple-phone',
|
|
90
|
+
width: 402,
|
|
91
|
+
height: 874,
|
|
92
|
+
dpr: 3,
|
|
93
|
+
bezel: 'phone-island',
|
|
94
|
+
bezelSrc: 'bezels/iphone-17-pro.svg',
|
|
95
|
+
bezelLayout: {
|
|
96
|
+
outerWidth: 440,
|
|
97
|
+
outerHeight: 908,
|
|
98
|
+
screen: { x: 19, y: 19, width: 402, height: 874, radius: 56 },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'iphone-17-pro-max',
|
|
103
|
+
name: 'iPhone 17 Pro Max',
|
|
104
|
+
category: 'apple-phone',
|
|
105
|
+
width: 440,
|
|
106
|
+
height: 956,
|
|
107
|
+
dpr: 3,
|
|
108
|
+
bezel: 'phone-island',
|
|
109
|
+
bezelSrc: 'bezels/iphone-17-pro.svg',
|
|
110
|
+
bezelLayout: {
|
|
111
|
+
outerWidth: 482,
|
|
112
|
+
outerHeight: 994,
|
|
113
|
+
screen: { x: 21, y: 21, width: 440, height: 956, radius: 60 },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
// --------------------------------------------------------- Android Phones
|
|
117
|
+
{ id: 'pixel-6', name: 'Pixel 6', category: 'android-phone', width: 412, height: 915, dpr: 2.625, bezel: 'phone-classic' },
|
|
118
|
+
{ id: 'pixel-7', name: 'Pixel 7', category: 'android-phone', width: 412, height: 915, dpr: 2.625, bezel: 'phone-classic' },
|
|
119
|
+
{ id: 'pixel-7-pro', name: 'Pixel 7 Pro', category: 'android-phone', width: 412, height: 892, dpr: 3.5, bezel: 'phone-classic' },
|
|
120
|
+
{ id: 'pixel-8', name: 'Pixel 8', category: 'android-phone', width: 412, height: 915, dpr: 2.625, bezel: 'phone-classic' },
|
|
121
|
+
{ id: 'pixel-8-pro', name: 'Pixel 8 Pro', category: 'android-phone', width: 448, height: 998, dpr: 3, bezel: 'phone-classic' },
|
|
122
|
+
{ id: 'pixel-9', name: 'Pixel 9', category: 'android-phone', width: 412, height: 915, dpr: 2.625, bezel: 'phone-classic' },
|
|
123
|
+
{ id: 'pixel-9-pro', name: 'Pixel 9 Pro', category: 'android-phone', width: 412, height: 915, dpr: 3, bezel: 'phone-classic' },
|
|
124
|
+
{ id: 'galaxy-s22', name: 'Galaxy S22', category: 'android-phone', width: 360, height: 780, dpr: 3, bezel: 'phone-classic' },
|
|
125
|
+
{ id: 'galaxy-s23', name: 'Galaxy S23', category: 'android-phone', width: 360, height: 780, dpr: 3, bezel: 'phone-classic' },
|
|
126
|
+
{ id: 'galaxy-s24', name: 'Galaxy S24', category: 'android-phone', width: 384, height: 854, dpr: 3, bezel: 'phone-classic' },
|
|
127
|
+
{ id: 'galaxy-s24-ultra', name: 'Galaxy S24 Ultra', category: 'android-phone', width: 412, height: 915, dpr: 3.5, bezel: 'phone-classic' },
|
|
128
|
+
{ id: 'oneplus-11', name: 'OnePlus 11', category: 'android-phone', width: 412, height: 919, dpr: 3, bezel: 'phone-classic' },
|
|
129
|
+
// ------------------------------------------------------------------ Tablets
|
|
130
|
+
// iPads use the dedicated ipad.svg (633×908). The kind-default tablet
|
|
131
|
+
// padding (50/56px absolute) handles the visible bezel.
|
|
132
|
+
{ id: 'ipad-mini-6', name: 'iPad mini', category: 'tablet', width: 744, height: 1133, dpr: 2, bezel: 'tablet', bezelSrc: 'bezels/ipad.svg' },
|
|
133
|
+
{ id: 'ipad-air-11', name: 'iPad Air 11"', category: 'tablet', width: 820, height: 1180, dpr: 2, bezel: 'tablet', bezelSrc: 'bezels/ipad.svg' },
|
|
134
|
+
{ id: 'ipad-pro-11', name: 'iPad Pro 11"', category: 'tablet', width: 834, height: 1194, dpr: 2, bezel: 'tablet', bezelSrc: 'bezels/ipad.svg' },
|
|
135
|
+
{ id: 'ipad-pro-13', name: 'iPad Pro 13"', category: 'tablet', width: 1024, height: 1366, dpr: 2, bezel: 'tablet', bezelSrc: 'bezels/ipad.svg' },
|
|
136
|
+
{ id: 'galaxy-tab-s9', name: 'Galaxy Tab S9', category: 'tablet', width: 800, height: 1280, dpr: 2, bezel: 'tablet' },
|
|
137
|
+
// ------------------------------------------------------------------ Laptops
|
|
138
|
+
{
|
|
139
|
+
id: 'macbook',
|
|
140
|
+
name: 'MacBook',
|
|
141
|
+
category: 'laptop',
|
|
142
|
+
width: 1440,
|
|
143
|
+
height: 940,
|
|
144
|
+
dpr: 2,
|
|
145
|
+
bezel: 'laptop', // bezels/laptop.svg is the macbook artwork (kind default)
|
|
146
|
+
},
|
|
147
|
+
// Surface Laptop 13.5" — viewport 1504×1003 matches the artwork's screen
|
|
148
|
+
// aspect (1.499) almost exactly. Lid: (180, 21)→1128×754, scale 1.333.
|
|
149
|
+
{
|
|
150
|
+
id: 'surface-laptop',
|
|
151
|
+
name: 'Surface Laptop 13.5"',
|
|
152
|
+
category: 'laptop',
|
|
153
|
+
width: 1504,
|
|
154
|
+
height: 1003,
|
|
155
|
+
dpr: 1.5,
|
|
156
|
+
bezel: 'laptop',
|
|
157
|
+
bezelSrc: 'bezels/surface-laptop.svg',
|
|
158
|
+
bezelLayout: {
|
|
159
|
+
outerWidth: 1984,
|
|
160
|
+
outerHeight: 1108,
|
|
161
|
+
screen: { x: 240, y: 28, width: 1504, height: 1003, radius: 5 },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
// MacBook Air — uses the new high-fidelity Sky Blue M4 artwork
|
|
165
|
+
// (2048×1241 viewBox; lid path with rx=61 from x=167.97 to 1881.82,
|
|
166
|
+
// y=0 to ~1145.26; inner screen rect (178.75, 10.78)→(1871.04, 1147.06),
|
|
167
|
+
// i.e. 1692×1136 inside the lid; keyboard hump and feet below).
|
|
168
|
+
// Scale 13": 1280 / 1692.3 = 0.7563. Scale 15": 1456 / 1692.3 = 0.8604.
|
|
169
|
+
{
|
|
170
|
+
id: 'macbook-air-13',
|
|
171
|
+
name: 'MacBook Air 13"',
|
|
172
|
+
category: 'laptop',
|
|
173
|
+
width: 1280,
|
|
174
|
+
height: 832,
|
|
175
|
+
dpr: 2,
|
|
176
|
+
bezel: 'laptop',
|
|
177
|
+
bezelSrc: 'bezels/macbook-air.svg',
|
|
178
|
+
bezelLayout: {
|
|
179
|
+
outerWidth: 1549,
|
|
180
|
+
outerHeight: 938,
|
|
181
|
+
screen: { x: 135, y: 8, width: 1280, height: 832, radius: 6 },
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: 'macbook-air-15',
|
|
186
|
+
name: 'MacBook Air 15"',
|
|
187
|
+
category: 'laptop',
|
|
188
|
+
width: 1456,
|
|
189
|
+
height: 945,
|
|
190
|
+
dpr: 2,
|
|
191
|
+
bezel: 'laptop',
|
|
192
|
+
bezelSrc: 'bezels/macbook-air.svg',
|
|
193
|
+
bezelLayout: {
|
|
194
|
+
outerWidth: 1762,
|
|
195
|
+
outerHeight: 1068,
|
|
196
|
+
screen: { x: 154, y: 9, width: 1456, height: 945, radius: 7 },
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
// MBP 14/16 use the high-fidelity macbook-pro-14.svg artwork.
|
|
200
|
+
// Lid: x=110, y=2, w=997, h=681. Screen above chin: ~y=2 to y=657 (h=655),
|
|
201
|
+
// aspect 1.521 — matches MBP 14/16's 1.539 closely.
|
|
202
|
+
{
|
|
203
|
+
id: 'macbook-pro-14',
|
|
204
|
+
name: 'MacBook Pro 14"',
|
|
205
|
+
category: 'laptop',
|
|
206
|
+
width: 1512,
|
|
207
|
+
height: 982,
|
|
208
|
+
dpr: 2,
|
|
209
|
+
bezel: 'laptop',
|
|
210
|
+
bezelSrc: 'bezels/macbook-pro-14.svg',
|
|
211
|
+
// Scale: 1512 / 997 = 1.516.
|
|
212
|
+
bezelLayout: {
|
|
213
|
+
outerWidth: 1843,
|
|
214
|
+
outerHeight: 1114,
|
|
215
|
+
screen: { x: 167, y: 3, width: 1512, height: 982, radius: 8 },
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: 'macbook-pro-16',
|
|
220
|
+
name: 'MacBook Pro 16"',
|
|
221
|
+
category: 'laptop',
|
|
222
|
+
width: 1728,
|
|
223
|
+
height: 1117,
|
|
224
|
+
dpr: 2,
|
|
225
|
+
bezel: 'laptop',
|
|
226
|
+
bezelSrc: 'bezels/macbook-pro-14.svg',
|
|
227
|
+
// Same artwork, scaled by 1728 / 997 = 1.733.
|
|
228
|
+
bezelLayout: {
|
|
229
|
+
outerWidth: 2108,
|
|
230
|
+
outerHeight: 1273,
|
|
231
|
+
screen: { x: 191, y: 3, width: 1728, height: 1117, radius: 8 },
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
// MacBook Pro 13" (M1 / M2 era, 1280×800). Uses the dedicated
|
|
235
|
+
// macbook-pro-13.svg with chin/keyboard detail. Lid above chin:
|
|
236
|
+
// (105, 2)→871×565, scale 1280/871 = 1.470.
|
|
237
|
+
{
|
|
238
|
+
id: 'macbook-pro-13',
|
|
239
|
+
name: 'MacBook Pro 13"',
|
|
240
|
+
category: 'laptop',
|
|
241
|
+
width: 1280,
|
|
242
|
+
height: 800,
|
|
243
|
+
dpr: 2,
|
|
244
|
+
bezel: 'laptop',
|
|
245
|
+
bezelSrc: 'bezels/macbook-pro-13.svg',
|
|
246
|
+
bezelLayout: {
|
|
247
|
+
outerWidth: 1588,
|
|
248
|
+
outerHeight: 917,
|
|
249
|
+
screen: { x: 154, y: 3, width: 1280, height: 800, radius: 8 },
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
{ id: 'laptop-1280', name: 'Laptop 1280', category: 'laptop', width: 1280, height: 800, dpr: 1, bezel: 'laptop' },
|
|
253
|
+
{ id: 'desktop-1366', name: 'Desktop 1366', category: 'laptop', width: 1366, height: 768, dpr: 1, bezel: 'monitor' },
|
|
254
|
+
{ id: 'desktop-1440', name: 'Desktop 1440', category: 'laptop', width: 1440, height: 900, dpr: 1, bezel: 'monitor' },
|
|
255
|
+
{ id: 'desktop-1536', name: 'Desktop 1536', category: 'laptop', width: 1536, height: 864, dpr: 1, bezel: 'monitor' },
|
|
256
|
+
{ id: 'desktop-1920', name: 'Desktop 1920', category: 'laptop', width: 1920, height: 1080, dpr: 1, bezel: 'monitor' },
|
|
257
|
+
{
|
|
258
|
+
id: 'imac-24',
|
|
259
|
+
name: 'iMac 24"',
|
|
260
|
+
category: 'laptop',
|
|
261
|
+
width: 1440,
|
|
262
|
+
height: 997,
|
|
263
|
+
dpr: 2,
|
|
264
|
+
bezel: 'monitor',
|
|
265
|
+
bezelSrc: 'bezels/imac.svg',
|
|
266
|
+
bezelLayout: {
|
|
267
|
+
outerWidth: 1442,
|
|
268
|
+
outerHeight: 1230,
|
|
269
|
+
screen: { x: 1, y: 1, width: 1440, height: 997, radius: 24 },
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
id: 'studio-display',
|
|
274
|
+
name: 'Studio Display 27"',
|
|
275
|
+
category: 'laptop',
|
|
276
|
+
width: 2560,
|
|
277
|
+
height: 1440,
|
|
278
|
+
dpr: 2,
|
|
279
|
+
bezel: 'monitor',
|
|
280
|
+
// Pro Display XDR artwork at scale 0.831 (= 2560 / 3082).
|
|
281
|
+
bezelLayout: {
|
|
282
|
+
outerWidth: 2560,
|
|
283
|
+
outerHeight: 1899,
|
|
284
|
+
screen: { x: 0, y: 0, width: 2560, height: 1440, radius: 17 },
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
id: 'pro-display-xdr',
|
|
289
|
+
name: 'Pro Display XDR',
|
|
290
|
+
category: 'laptop',
|
|
291
|
+
width: 3008,
|
|
292
|
+
height: 1692,
|
|
293
|
+
dpr: 2,
|
|
294
|
+
bezel: 'monitor',
|
|
295
|
+
// 1:1 with the artwork (scale 0.976 ≈ identity for our purposes).
|
|
296
|
+
bezelLayout: {
|
|
297
|
+
outerWidth: 3008,
|
|
298
|
+
outerHeight: 2232,
|
|
299
|
+
screen: { x: 0, y: 0, width: 3008, height: 1692, radius: 20 },
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
];
|
|
303
|
+
export const DEVICES_BY_ID = Object.fromEntries(DEVICES.map(d => [d.id, d]));
|
|
304
|
+
export const CATEGORY_LABELS = {
|
|
305
|
+
'apple-phone': 'Apple Phones',
|
|
306
|
+
'android-phone': 'Android Phones',
|
|
307
|
+
tablet: 'Tablets',
|
|
308
|
+
laptop: 'Laptops',
|
|
309
|
+
};
|
|
310
|
+
export const CATEGORY_ORDER = [
|
|
311
|
+
'apple-phone',
|
|
312
|
+
'android-phone',
|
|
313
|
+
'tablet',
|
|
314
|
+
'laptop',
|
|
315
|
+
];
|
|
316
|
+
export const DEFAULT_DEVICE_ID = 'iphone-15-pro';
|
|
317
|
+
// ------------------------------------------------------------------ Additions
|
|
318
|
+
const UA_IOS = 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1';
|
|
319
|
+
const UA_IPAD = 'Mozilla/5.0 (iPad; CPU OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1';
|
|
320
|
+
const UA_ANDROID_PHONE = 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.36';
|
|
321
|
+
const UA_ANDROID_TABLET = 'Mozilla/5.0 (Linux; Android 14; SM-X710) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36';
|
|
322
|
+
/** Category-level emulation. RPC itself sets no UA (it renders in an iframe
|
|
323
|
+
* inside desktop Chrome); agents usually want the mobile UA + touch. */
|
|
324
|
+
export function emulationFor(device) {
|
|
325
|
+
switch (device.category) {
|
|
326
|
+
case 'apple-phone':
|
|
327
|
+
return { userAgent: UA_IOS, isMobile: true, hasTouch: true };
|
|
328
|
+
case 'android-phone':
|
|
329
|
+
return { userAgent: UA_ANDROID_PHONE, isMobile: true, hasTouch: true };
|
|
330
|
+
case 'tablet':
|
|
331
|
+
return device.id.startsWith('ipad')
|
|
332
|
+
? { userAgent: UA_IPAD, isMobile: true, hasTouch: true }
|
|
333
|
+
: { userAgent: UA_ANDROID_TABLET, isMobile: true, hasTouch: true };
|
|
334
|
+
case 'laptop':
|
|
335
|
+
return { userAgent: undefined, isMobile: false, hasTouch: false };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/** Short names agents are likely to guess. */
|
|
339
|
+
export const DEVICE_ALIASES = {
|
|
340
|
+
iphone: 'iphone-15-pro',
|
|
341
|
+
mobile: 'iphone-15-pro',
|
|
342
|
+
phone: 'iphone-15-pro',
|
|
343
|
+
android: 'pixel-8',
|
|
344
|
+
pixel: 'pixel-8',
|
|
345
|
+
galaxy: 'galaxy-s24',
|
|
346
|
+
ipad: 'ipad-air-11',
|
|
347
|
+
tablet: 'ipad-air-11',
|
|
348
|
+
macbook: 'macbook-air-13',
|
|
349
|
+
laptop: 'laptop-1280',
|
|
350
|
+
desktop: 'desktop-1440',
|
|
351
|
+
imac: 'imac-24',
|
|
352
|
+
};
|
|
353
|
+
export const DEFAULT_DEVICES = ['iphone-15-pro', 'desktop-1440'];
|
|
354
|
+
export class UnknownDeviceError extends Error {
|
|
355
|
+
input;
|
|
356
|
+
suggestions;
|
|
357
|
+
constructor(input, suggestions) {
|
|
358
|
+
super(`Unknown device "${input}".` +
|
|
359
|
+
(suggestions.length ? ` Did you mean: ${suggestions.join(', ')}?` : '') +
|
|
360
|
+
' Run `rpc devices` to list all ids.');
|
|
361
|
+
this.input = input;
|
|
362
|
+
this.suggestions = suggestions;
|
|
363
|
+
this.name = 'UnknownDeviceError';
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
export function resolveDevice(input) {
|
|
367
|
+
const key = input.trim().toLowerCase();
|
|
368
|
+
const id = DEVICE_ALIASES[key] ?? key;
|
|
369
|
+
const device = DEVICES_BY_ID[id];
|
|
370
|
+
if (device)
|
|
371
|
+
return device;
|
|
372
|
+
throw new UnknownDeviceError(input, suggestDevices(key));
|
|
373
|
+
}
|
|
374
|
+
/** Resolve a list of ids/aliases, de-duplicating while keeping order. */
|
|
375
|
+
export function resolveDevices(inputs) {
|
|
376
|
+
const seen = new Set();
|
|
377
|
+
const out = [];
|
|
378
|
+
for (const input of inputs) {
|
|
379
|
+
const d = resolveDevice(input);
|
|
380
|
+
if (seen.has(d.id))
|
|
381
|
+
continue;
|
|
382
|
+
seen.add(d.id);
|
|
383
|
+
out.push(d);
|
|
384
|
+
}
|
|
385
|
+
return out;
|
|
386
|
+
}
|
|
387
|
+
function suggestDevices(key) {
|
|
388
|
+
const ids = [...DEVICES.map(d => d.id), ...Object.keys(DEVICE_ALIASES)];
|
|
389
|
+
const scored = ids
|
|
390
|
+
.map(id => ({ id, score: id.includes(key) || key.includes(id) ? 0 : levenshtein(key, id) }))
|
|
391
|
+
.filter(s => s.score <= Math.max(2, Math.floor(key.length / 3)))
|
|
392
|
+
.sort((a, b) => a.score - b.score || a.id.localeCompare(b.id));
|
|
393
|
+
return scored.slice(0, 5).map(s => s.id);
|
|
394
|
+
}
|
|
395
|
+
function levenshtein(a, b) {
|
|
396
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
397
|
+
for (let i = 1; i <= a.length; i++) {
|
|
398
|
+
let prev = row[0];
|
|
399
|
+
row[0] = i;
|
|
400
|
+
for (let j = 1; j <= b.length; j++) {
|
|
401
|
+
const tmp = row[j];
|
|
402
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
403
|
+
prev = tmp;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return row[b.length];
|
|
407
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const SANDBOX_HINT = 'If the host works in a normal browser, this process may be blocked by a sandbox or network allowlist ' +
|
|
2
|
+
'(e.g. the Claude Code sandbox: add the domain to the allowed hosts, or run outside the sandbox). ' +
|
|
3
|
+
'Note that nslookup can succeed while the sandboxed process still cannot connect.';
|
|
4
|
+
function isLocalHost(host) {
|
|
5
|
+
return /^(localhost|127\.\d+\.\d+\.\d+|0\.0\.0\.0|\[::1\]|::1)$/i.test(host) || host.endsWith('.localhost');
|
|
6
|
+
}
|
|
7
|
+
function describeTarget(url) {
|
|
8
|
+
try {
|
|
9
|
+
const u = new URL(url);
|
|
10
|
+
const port = u.port || (u.protocol === 'https:' ? '443' : u.protocol === 'http:' ? '80' : '');
|
|
11
|
+
return { host: u.host || u.pathname, local: isLocalHost(u.hostname), file: u.protocol === 'file:', port };
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return { host: url, local: false, file: false, port: '' };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Map a Playwright/Chromium error to `{ error, code, hint }`. */
|
|
18
|
+
export function explainError(err, ctx) {
|
|
19
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
20
|
+
const first = (raw.split('\n')[0] ?? raw).trim();
|
|
21
|
+
const t = describeTarget(ctx.url);
|
|
22
|
+
const net = /net::(ERR_[A-Z0-9_]+)/.exec(raw)?.[1];
|
|
23
|
+
if (net) {
|
|
24
|
+
const tag = `(net::${net})`;
|
|
25
|
+
switch (net) {
|
|
26
|
+
case 'ERR_NAME_NOT_RESOLVED':
|
|
27
|
+
case 'ERR_NAME_RESOLUTION_FAILED':
|
|
28
|
+
return {
|
|
29
|
+
code: net,
|
|
30
|
+
error: `Could not resolve host ${t.host} ${tag}`,
|
|
31
|
+
hint: t.local
|
|
32
|
+
? `Check the host name in the URL.`
|
|
33
|
+
: `Check the URL for typos. ${SANDBOX_HINT}`,
|
|
34
|
+
};
|
|
35
|
+
case 'ERR_CONNECTION_REFUSED':
|
|
36
|
+
return {
|
|
37
|
+
code: net,
|
|
38
|
+
error: `Connection refused by ${t.host} ${tag}`,
|
|
39
|
+
hint: t.local
|
|
40
|
+
? `Nothing is listening on port ${t.port}. Start the dev server (or check which port it printed) and retry.`
|
|
41
|
+
: `The server refused the connection. ${SANDBOX_HINT}`,
|
|
42
|
+
};
|
|
43
|
+
case 'ERR_INTERNET_DISCONNECTED':
|
|
44
|
+
return {
|
|
45
|
+
code: net,
|
|
46
|
+
error: `No network connection ${tag}`,
|
|
47
|
+
hint: `This process has no network access. ${SANDBOX_HINT}`,
|
|
48
|
+
};
|
|
49
|
+
case 'ERR_PROXY_CONNECTION_FAILED':
|
|
50
|
+
case 'ERR_TUNNEL_CONNECTION_FAILED':
|
|
51
|
+
case 'ERR_PROXY_AUTH_REQUESTED':
|
|
52
|
+
case 'ERR_PROXY_CERTIFICATE_INVALID':
|
|
53
|
+
case 'ERR_NO_SUPPORTED_PROXIES':
|
|
54
|
+
return {
|
|
55
|
+
code: net,
|
|
56
|
+
error: `Proxy refused the connection to ${t.host} ${tag}`,
|
|
57
|
+
hint: `Traffic goes through a proxy that did not allow this host. ${SANDBOX_HINT}`,
|
|
58
|
+
};
|
|
59
|
+
case 'ERR_CONNECTION_TIMED_OUT':
|
|
60
|
+
case 'ERR_TIMED_OUT':
|
|
61
|
+
case 'ERR_ADDRESS_UNREACHABLE':
|
|
62
|
+
case 'ERR_NETWORK_ACCESS_DENIED':
|
|
63
|
+
case 'ERR_CONNECTION_RESET':
|
|
64
|
+
case 'ERR_CONNECTION_CLOSED':
|
|
65
|
+
case 'ERR_CONNECTION_FAILED':
|
|
66
|
+
case 'ERR_EMPTY_RESPONSE':
|
|
67
|
+
case 'ERR_BLOCKED_BY_ADMINISTRATOR':
|
|
68
|
+
return {
|
|
69
|
+
code: net,
|
|
70
|
+
error: `Could not connect to ${t.host} ${tag}`,
|
|
71
|
+
hint: t.local
|
|
72
|
+
? `Check that the dev server on port ${t.port} is running and healthy.`
|
|
73
|
+
: `The host is unreachable from here. ${SANDBOX_HINT}`,
|
|
74
|
+
};
|
|
75
|
+
case 'ERR_SSL_PROTOCOL_ERROR':
|
|
76
|
+
case 'ERR_SSL_VERSION_OR_CIPHER_MISMATCH':
|
|
77
|
+
return {
|
|
78
|
+
code: net,
|
|
79
|
+
error: `TLS handshake with ${t.host} failed ${tag}`,
|
|
80
|
+
hint: t.local
|
|
81
|
+
? `The server probably speaks plain HTTP. Use http:// instead of https://.`
|
|
82
|
+
: `The server's TLS setup is broken or something intercepts the connection.`,
|
|
83
|
+
};
|
|
84
|
+
case 'ERR_FILE_NOT_FOUND':
|
|
85
|
+
return { code: net, error: `File not found: ${t.host} ${tag}`, hint: `Check the path; relative paths resolve against the working directory.` };
|
|
86
|
+
case 'ERR_TOO_MANY_REDIRECTS':
|
|
87
|
+
return { code: net, error: `Redirect loop at ${t.host} ${tag}`, hint: `The page redirects to itself; check auth/locale redirects on the server.` };
|
|
88
|
+
case 'ERR_ABORTED':
|
|
89
|
+
return { code: net, error: `Navigation to ${ctx.url} was aborted ${tag}`, hint: `The URL may trigger a download or an immediate client-side redirect.` };
|
|
90
|
+
default:
|
|
91
|
+
return { code: net, error: first };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (/TimeoutError|Timeout \d+ms exceeded/i.test(raw)) {
|
|
95
|
+
const secs = ctx.timeoutMs ? `${Math.round(ctx.timeoutMs / 1000)} s` : 'the timeout';
|
|
96
|
+
if (/waitForSelector|waiting for locator/i.test(raw) && ctx.waitFor) {
|
|
97
|
+
return {
|
|
98
|
+
code: 'WAIT_FOR_TIMEOUT',
|
|
99
|
+
error: `Selector "${ctx.waitFor}" did not become visible within ${secs}`,
|
|
100
|
+
hint: `Check the selector, or that the page reaches that state without interaction.`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (/goto|navigat/i.test(raw)) {
|
|
104
|
+
return {
|
|
105
|
+
code: 'TIMEOUT',
|
|
106
|
+
error: `Loading ${t.host} did not finish within ${secs}`,
|
|
107
|
+
hint: t.local
|
|
108
|
+
? `The dev server may still be compiling; retry or raise timeoutMs.`
|
|
109
|
+
: `Raise timeoutMs if the site is slow. A silently dropped connection can also mean the host is blocked: ${SANDBOX_HINT}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return { code: 'TIMEOUT', error: first, hint: `Raise timeoutMs or waitMs and retry.` };
|
|
113
|
+
}
|
|
114
|
+
return { code: 'UNKNOWN', error: first };
|
|
115
|
+
}
|