pursr 0.5.0 → 0.6.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/README.md +549 -471
- package/bin/pursr.mjs +34 -2
- package/package.json +95 -92
- package/src/ai-diff.js +124 -0
- package/src/diff.js +76 -48
- package/src/index.js +9 -1
- package/src/report.js +176 -0
package/README.md
CHANGED
|
@@ -1,471 +1,549 @@
|
|
|
1
|
-
<!-- PROJECT_LOGO_START -->
|
|
2
|
-
<p align="center">
|
|
3
|
-
<img src="assets/social-preview.svg" alt="pursr - visual QA, audit, and MCP for the browser" width="100%">
|
|
4
|
-
</p>
|
|
5
|
-
|
|
6
|
-
<p align="center">
|
|
7
|
-
<img src="assets/logo.svg" alt="pursr" width="320">
|
|
8
|
-
</p>
|
|
9
|
-
|
|
10
|
-
<h1 align="center">pursr</h1>
|
|
11
|
-
|
|
12
|
-
<p align="center">
|
|
13
|
-
<strong>Visual QA, audit, and MCP for the browser.</strong><br>
|
|
14
|
-
Capture - sweep - diff - audit - repeat - from the CLI, an MCP server, or as a library.
|
|
15
|
-
</p>
|
|
16
|
-
|
|
17
|
-
<p align="center">
|
|
18
|
-
<a href="https://www.npmjs.com/package/pursr"><img src="https://img.shields.io/npm/v/pursr.svg?style=for-the-badge&color=FF2EA6" alt="npm version"></a>
|
|
19
|
-
<a href="https://github.com/0xheycat/pursr/blob/main/LICENSE"><img src="https://img.shields.io/github/license/0xheycat/pursr.svg?style=for-the-badge" alt="license"></a>
|
|
20
|
-
<a href="https://www.npmjs.com/package/pursr"><img src="https://img.shields.io/npm/dm/pursr.svg?style=for-the-badge" alt="npm downloads"></a>
|
|
21
|
-
<a href="https://github.com/0xheycat/pursr/actions"><img src="https://img.shields.io/github/actions/workflow/status/0xheycat/pursr/ci.yml?style=for-the-badge" alt="CI"></a>
|
|
22
|
-
<a href="https://nodejs.org"><img src="https://img.shields.io/node/v/pursr.svg?style=for-the-badge" alt="node"></a>
|
|
23
|
-
</p>
|
|
24
|
-
|
|
25
|
-
<p align="center">
|
|
26
|
-
<a href="#install">Install</a> · <a href="#30-seconds">30 seconds</a> · <a href="#cli">CLI</a> · <a href="#mcp-server">MCP</a> · <a href="#library-api">Library</a> · <a href="#plugins">Plugins</a> · <a href="#roadmap">Roadmap</a>
|
|
27
|
-
</p>
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
## Why pursr?
|
|
32
|
-
|
|
33
|
-
Most teams need **
|
|
34
|
-
|
|
35
|
-
- **A unified CLI** (`pursr`) for every capture, diff, sweep, and audit.
|
|
36
|
-
- **An MCP stdio server** (`pursr-mcp`) so Claude Code, Cursor, and Continue can take screenshots, run sweeps, and inspect prior captures as MCP resources.
|
|
37
|
-
- **A library** with
|
|
38
|
-
- **A plugin system** for custom viewports, sweep ops, and capture hooks.
|
|
39
|
-
- **
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
npm install
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
pursr
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
|
|
|
82
|
-
|
|
|
83
|
-
|
|
|
84
|
-
|
|
|
85
|
-
|
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
|
|
|
95
|
-
|
|
|
96
|
-
|
|
|
97
|
-
|
|
|
98
|
-
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
#
|
|
107
|
-
pursr
|
|
108
|
-
|
|
109
|
-
#
|
|
110
|
-
pursr
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
#
|
|
119
|
-
pursr
|
|
120
|
-
|
|
121
|
-
#
|
|
122
|
-
pursr
|
|
123
|
-
|
|
124
|
-
#
|
|
125
|
-
pursr
|
|
126
|
-
|
|
127
|
-
#
|
|
128
|
-
pursr
|
|
129
|
-
|
|
130
|
-
#
|
|
131
|
-
pursr
|
|
132
|
-
|
|
133
|
-
#
|
|
134
|
-
pursr
|
|
135
|
-
|
|
136
|
-
#
|
|
137
|
-
pursr
|
|
138
|
-
|
|
139
|
-
#
|
|
140
|
-
pursr shoot https://
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
pursr
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
|
158
|
-
|
|
|
159
|
-
| `
|
|
160
|
-
| `
|
|
161
|
-
| `
|
|
162
|
-
| `
|
|
163
|
-
| `
|
|
164
|
-
| `
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
169
|
-
| `
|
|
170
|
-
| `
|
|
171
|
-
| `
|
|
172
|
-
| `
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
|
190
|
-
|
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
194
|
-
| `
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
pursr baseline
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
"
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
{ "name": "
|
|
241
|
-
{ "name": "
|
|
242
|
-
{ "name": "
|
|
243
|
-
{ "name": "
|
|
244
|
-
]
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
await
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
pursr auth
|
|
271
|
-
pursr auth
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
"
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
import {
|
|
346
|
-
import {
|
|
347
|
-
import {
|
|
348
|
-
import {
|
|
349
|
-
import {
|
|
350
|
-
import {
|
|
351
|
-
import {
|
|
352
|
-
import {
|
|
353
|
-
import {
|
|
354
|
-
import {
|
|
355
|
-
import {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
},
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
- [x]
|
|
430
|
-
- [x]
|
|
431
|
-
- [x]
|
|
432
|
-
- [x]
|
|
433
|
-
- [x]
|
|
434
|
-
- [
|
|
435
|
-
- [
|
|
436
|
-
- [
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
##
|
|
470
|
-
|
|
471
|
-
|
|
1
|
+
<!-- PROJECT_LOGO_START -->
|
|
2
|
+
<p align="center">
|
|
3
|
+
<img src="assets/social-preview.svg" alt="pursr - visual QA, audit, and MCP for the browser" width="100%">
|
|
4
|
+
</p>
|
|
5
|
+
|
|
6
|
+
<p align="center">
|
|
7
|
+
<img src="assets/logo.svg" alt="pursr" width="320">
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<h1 align="center">pursr</h1>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<strong>Visual QA, audit, and MCP for the browser.</strong><br>
|
|
14
|
+
Capture - sweep - diff - audit - repeat - from the CLI, an MCP server, or as a library.
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<a href="https://www.npmjs.com/package/pursr"><img src="https://img.shields.io/npm/v/pursr.svg?style=for-the-badge&color=FF2EA6" alt="npm version"></a>
|
|
19
|
+
<a href="https://github.com/0xheycat/pursr/blob/main/LICENSE"><img src="https://img.shields.io/github/license/0xheycat/pursr.svg?style=for-the-badge" alt="license"></a>
|
|
20
|
+
<a href="https://www.npmjs.com/package/pursr"><img src="https://img.shields.io/npm/dm/pursr.svg?style=for-the-badge" alt="npm downloads"></a>
|
|
21
|
+
<a href="https://github.com/0xheycat/pursr/actions"><img src="https://img.shields.io/github/actions/workflow/status/0xheycat/pursr/ci.yml?style=for-the-badge" alt="CI"></a>
|
|
22
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/node/v/pursr.svg?style=for-the-badge" alt="node"></a>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<a href="#install">Install</a> · <a href="#30-seconds">30 seconds</a> · <a href="#cli">CLI</a> · <a href="#mcp-server">MCP</a> · <a href="#library-api">Library</a> · <a href="#plugins">Plugins</a> · <a href="#roadmap">Roadmap</a>
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Why pursr?
|
|
32
|
+
|
|
33
|
+
Most teams need **five separate tools** to do visual QA: a screenshot CLI, a regression diff runner, an accessibility auditor, a way to share captures with an AI assistant, and a way to **turn all of that into a PDF report** for stakeholders. **pursr is all five** - built as a single Node.js package with:
|
|
34
|
+
|
|
35
|
+
- **A unified CLI** (`pursr`) for every capture, diff, sweep, and audit.
|
|
36
|
+
- **An MCP stdio server** (`pursr-mcp`) so Claude Code, Cursor, and Continue can take screenshots, run sweeps, and inspect prior captures as MCP resources.
|
|
37
|
+
- **A library** with 34 named exports and 18 subpath modules, so you can embed it in your own tooling.
|
|
38
|
+
- **A plugin system** for custom viewports, sweep ops, and capture hooks.
|
|
39
|
+
- **PDF reports + AI diff summaries** built in - render a sweep to a styled PDF or ask a vision LLM to describe the regression in plain language.
|
|
40
|
+
- **Zero browser bundled** - drives your system Chrome via Playwright. No 200 MB Chromium download.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install pursr
|
|
46
|
+
npm install --save-dev playwright-core # peer dep - bring your own Chrome
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then verify:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pursr viewports # list 10+ registered viewport presets
|
|
53
|
+
pursr probe https://example.com # health check
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 30 seconds
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# 1. Capture a screenshot with overlays
|
|
60
|
+
pursr shoot https://example.com shot.png \
|
|
61
|
+
--preset desktop-1280 --grid --grid-tile 64
|
|
62
|
+
|
|
63
|
+
# 2. Save it as a visual baseline
|
|
64
|
+
pursr baseline save myapp shot.png home --url https://example.com
|
|
65
|
+
|
|
66
|
+
# 3. Next time you run, compare against the baseline
|
|
67
|
+
pursr diff https://example.com \
|
|
68
|
+
~/.pursor/baselines/myapp/<id>/home.png \
|
|
69
|
+
diff.png
|
|
70
|
+
|
|
71
|
+
# 4. Or: run a batched sweep + a11y audit + parallel workers
|
|
72
|
+
pursr sweep ./plan.json # see plans/ for an example
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Features
|
|
76
|
+
|
|
77
|
+
| Feature | Description | CLI flag |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| Multi-viewport capture | 10+ presets (mobile, tablet, desktop, ultrawide) | `--preset mobile-375` |
|
|
80
|
+
| Layered states | entity / terrain / hud / ui isolation | `--layer entity` |
|
|
81
|
+
| Animation freeze | pause CSS/JS animations for stable frames | `--no-animation` |
|
|
82
|
+
| Cursor overlay | pointer / grab / grabbing / crosshair | `--cursor crosshair` |
|
|
83
|
+
| Grid overlay | spacing guides, custom color + tile size | `--grid --grid-tile 64` |
|
|
84
|
+
| Camera control | zoom + pan via mouse wheel/drag | `--zoom 1.5 --panX 200` |
|
|
85
|
+
| Frame timeline | N captures at intervalMs for animations | `pursr frames <url> 8 200` |
|
|
86
|
+
| Hover capture | text=/role=/aria=/placeholder= matchers | `pursr hover <url> "text=Login"` |
|
|
87
|
+
| Pixel diff | `pixelmatch` against any reference PNG | `pursr diff <url> <ref>` |
|
|
88
|
+
| Visual baselines | save / approve / diff with stable IDs | `pursr baseline save ...` |
|
|
89
|
+
| Parallel sweep | opt-in worker pool across independent steps | `{ "parallel": 4 }` |
|
|
90
|
+
| Accessibility audit | axe-core WCAG 2.1 AA + highlighted screenshot | `pursr audit <url>` |
|
|
91
|
+
| DOM snapshot | serialized HTML + computed styles + selector map | `pursr dom <url>` |
|
|
92
|
+
| Sweep plans | JSON-driven batch with per-step ops | `pursr sweep plan.json` |
|
|
93
|
+
| HTML report | dark-themed grid of every capture + meta | auto-generated `index.html` |
|
|
94
|
+
| CI output | JUnit XML, GitHub Actions annotations, Markdown | written on every sweep |
|
|
95
|
+
| Auto-heal selectors | fallback chain + named matchers | `["text=Login", "#login"]` |
|
|
96
|
+
| HAR capture | HAR 1.2 spec, written next to your shot | `--har ./req.har.json` |
|
|
97
|
+
| Auth state | Playwright storageState, reuse logged-in sessions | `--auth-state admin` |
|
|
98
|
+
| Plugins | custom viewports, sweep ops, before/after hooks | `pursr-plugin-*` |
|
|
99
|
+
| MCP server | 7 tools + resources/list & resources/read for Claude/Cursor | `npx pursr-mcp` |
|
|
100
|
+
| PDF report | render sweep.json to a styled, embedded-PNG A4 PDF | `pursr report --sweep ./sweep.json` |
|
|
101
|
+
| AI diff summary | vision LLM describes the diff in plain language | `pursr diff ... --ai` |
|
|
102
|
+
|
|
103
|
+
## CLI
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Health check
|
|
107
|
+
pursr probe https://example.com
|
|
108
|
+
|
|
109
|
+
# Screenshot (simple)
|
|
110
|
+
pursr shot https://example.com ./out/shot.png
|
|
111
|
+
|
|
112
|
+
# Rich capture: viewport preset + cursor + grid
|
|
113
|
+
pursr shoot https://example.com \
|
|
114
|
+
--preset desktop-1280 \
|
|
115
|
+
--cursor crosshair \
|
|
116
|
+
--grid --grid-tile 64
|
|
117
|
+
|
|
118
|
+
# Isolate a layer
|
|
119
|
+
pursr layer https://example.com entity
|
|
120
|
+
|
|
121
|
+
# Animation timeline
|
|
122
|
+
pursr frames https://example.com 8 200 ./frames/
|
|
123
|
+
|
|
124
|
+
# Hover an element
|
|
125
|
+
pursr hover https://example.com "text=Login"
|
|
126
|
+
|
|
127
|
+
# Pixel diff vs reference
|
|
128
|
+
pursr diff https://example.com ./ref.png ./out/diff.png
|
|
129
|
+
|
|
130
|
+
# Batched plan
|
|
131
|
+
pursr sweep ./plan.json
|
|
132
|
+
|
|
133
|
+
# Accessibility audit
|
|
134
|
+
pursr audit https://example.com --tags wcag2a,wcag2aa
|
|
135
|
+
|
|
136
|
+
# DOM + selector map snapshot
|
|
137
|
+
pursr dom https://example.com
|
|
138
|
+
|
|
139
|
+
# HAR capture during a shoot
|
|
140
|
+
pursr shoot https://example.com shot.png --har ./req.har.json
|
|
141
|
+
|
|
142
|
+
# Auth state reuse
|
|
143
|
+
pursr shoot https://my.app/dashboard shot.png \
|
|
144
|
+
--auth-state admin --auth-project myapp
|
|
145
|
+
|
|
146
|
+
# Visual baselines
|
|
147
|
+
pursr baseline save myapp shot.png home --url https://example.com
|
|
148
|
+
pursr baseline list myapp
|
|
149
|
+
pursr baseline approve myapp ./new.png home --url https://example.com
|
|
150
|
+
|
|
151
|
+
# Plan validation
|
|
152
|
+
pursr validate ./plan.json
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Subcommands
|
|
156
|
+
|
|
157
|
+
| Subcommand | Purpose |
|
|
158
|
+
| --- | --- |
|
|
159
|
+
| `probe` | Health check (HTTP status, page title) |
|
|
160
|
+
| `shot` / `full` | Viewport / full-page screenshot |
|
|
161
|
+
| `eval` | Execute JS in the page, return result |
|
|
162
|
+
| `click` / `type` / `wait` / `seq` | Interaction primitives |
|
|
163
|
+
| `diff` | Pixel-level diff vs a reference PNG |
|
|
164
|
+
| `viewports` | List all registered viewport presets |
|
|
165
|
+
| `shoot` | Rich capture (overlays, freeze, camera, plugins) |
|
|
166
|
+
| `layer` | Capture one isolated layer (entity/hud/ui/terrain) |
|
|
167
|
+
| `frames` | N-frame animation timeline at interval |
|
|
168
|
+
| `hover` | Hover state capture |
|
|
169
|
+
| `sweep` | Batched capture plan -> HTML report + CI output |
|
|
170
|
+
| `audit` | axe-core WCAG accessibility audit + highlighted screenshot |
|
|
171
|
+
| `dom` / `dom-snapshot` | Serialized DOM + CSS selectors + XPath + bounding rects |
|
|
172
|
+
| `every-viewport` | Capture once per preset in parallel (3-wide pool) |
|
|
173
|
+
| `baseline` | save / list / approve / show visual baselines |
|
|
174
|
+
| `auth` | save / load / list / delete Playwright storageState |
|
|
175
|
+
| `validate` | Validate a sweep plan JSON without running it |
|
|
176
|
+
|
|
177
|
+
## MCP Server
|
|
178
|
+
|
|
179
|
+
`pursr-mcp` exposes every capability as MCP tools over stdio - works with Claude Code, Cursor, Continue, and any MCP host.
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
npx pursr-mcp
|
|
183
|
+
# or with verbose logging:
|
|
184
|
+
npx pursr-mcp --verbose
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Exposed Tools
|
|
188
|
+
|
|
189
|
+
| Tool | Description |
|
|
190
|
+
| --- | --- |
|
|
191
|
+
| `pursr_shoot` | Rich screenshot capture (viewport, grid, layer, cursor, camera, animation freeze, HAR) |
|
|
192
|
+
| `pursr_diff` | Pixel-diff a URL against a reference PNG |
|
|
193
|
+
| `pursr_sweep` | Execute a batch sweep plan |
|
|
194
|
+
| `pursr_frames` | Capture an N-frame animation timeline |
|
|
195
|
+
| `pursr_probe` | Health-check a URL |
|
|
196
|
+
| `pursr_audit` | axe-core WCAG audit + highlighted screenshot |
|
|
197
|
+
| `pursr_dom_snapshot` | Full DOM + selector map snapshot |
|
|
198
|
+
|
|
199
|
+
### Exposed Resources
|
|
200
|
+
|
|
201
|
+
| URI | Description |
|
|
202
|
+
| --- | --- |
|
|
203
|
+
| `pursr://shoot/<url|preset>` | Last screenshot PNG (image/png) |
|
|
204
|
+
| `pursr://sweep/<plan-name>` | Last sweep summary JSON (application/json) |
|
|
205
|
+
|
|
206
|
+
Resources are persisted to `~/.pursor/mcp/mcp-index.json` (override with `PURSOR_MCP_STATE`).
|
|
207
|
+
|
|
208
|
+
## Visual Regression Baselines
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
pursr baseline save myapp ./out/shoot.png home --url https://my.app
|
|
212
|
+
pursr baseline approve myapp ./out/shoot.png home --url https://my.app
|
|
213
|
+
pursr baseline list myapp
|
|
214
|
+
pursr baseline show myapp home --url https://my.app
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Baselines live under `~/.pursor/baselines/<project>/<id>/<step>.png` + `manifest.json`. Override with `PURSOR_BASELINES_DIR`. The `id` is a 16-char SHA1 prefix of `url|viewport|flags` so re-running a sweep maps to the same slot deterministically.
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
import { diffKey, saveBaseline, loadBaseline } from "pursr/baseline";
|
|
221
|
+
const id = diffKey({ url: "https://my.app", viewport: { width: 1280, height: 800, dpr: 1 }, flags: { preset: "desktop-1280" } });
|
|
222
|
+
saveBaseline({ project: "myapp", id, step: "home", png: "./shot.png", meta: { url: "https://my.app" } });
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Sweep Plan Validation
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
pursr validate ./plan.json
|
|
229
|
+
# { "valid": false, "errors": ["steps[2].frames.count: must be a number between 1 and 120"] }
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Catches: empty steps, unknown ops, out-of-range numbers, duplicate names, missing required fields. `pursr sweep` runs the same validator before executing - fail-fast.
|
|
233
|
+
|
|
234
|
+
```json
|
|
235
|
+
{
|
|
236
|
+
"name": "homepage-matrix",
|
|
237
|
+
"base": "https://example.com",
|
|
238
|
+
"parallel": 4,
|
|
239
|
+
"steps": [
|
|
240
|
+
{ "name": "baseline", "shoot": { "preset": "desktop-1280" } },
|
|
241
|
+
{ "name": "grid-64", "shoot": { "preset": "desktop-1280", "grid": true, "grid-tile": 64 } },
|
|
242
|
+
{ "name": "tablet", "shoot": { "preset": "tablet-768" } },
|
|
243
|
+
{ "name": "mobile", "shoot": { "preset": "mobile-375" } },
|
|
244
|
+
{ "name": "hover-cta", "hover": { "selector": ["text=Get started", "a.btn-primary"] } },
|
|
245
|
+
{ "name": "audit", "audit": { "tags": "wcag2a,wcag2aa" } },
|
|
246
|
+
{ "name": "diff", "diff": { "ref": "baseline" } }
|
|
247
|
+
]
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## HAR Capture
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
pursr shoot https://example.com shot.png --har ./out/req.har.json
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
import { startHarCapture, stopHarCapture, writeHar } from "pursr/har";
|
|
259
|
+
const state = await startHarCapture(page);
|
|
260
|
+
await page.goto(url);
|
|
261
|
+
const har = stopHarCapture(page);
|
|
262
|
+
await writeHar(har, "./out/req.har.json");
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Output is HAR 1.2 spec - pipe to `har-cli`, perf-tools, or any visualizer.
|
|
266
|
+
|
|
267
|
+
## Auth State
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
pursr auth save myapp admin --from ./playwright-state.json
|
|
271
|
+
pursr shoot https://my.app/dashboard shot.png --auth-state admin --auth-project myapp
|
|
272
|
+
pursr auth list myapp
|
|
273
|
+
pursr auth load myapp admin --out ./round-trip.json
|
|
274
|
+
pursr auth delete myapp admin
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
States live in `~/.pursor/auth/<project>/<name>.json` (override with `PURSOR_AUTH_DIR`). The on-disk format is the standard Playwright `storageState` shape: `{ cookies, origins }`.
|
|
278
|
+
|
|
279
|
+
## Parallel Sweep
|
|
280
|
+
|
|
281
|
+
Add `parallel: N` to your plan to run steps concurrently in a worker pool:
|
|
282
|
+
|
|
283
|
+
```json
|
|
284
|
+
{
|
|
285
|
+
"name": "matrix",
|
|
286
|
+
"base": "https://my.app",
|
|
287
|
+
"parallel": 4,
|
|
288
|
+
"steps": [
|
|
289
|
+
{ "name": "home", "shoot": { "preset": "desktop-1280" } },
|
|
290
|
+
{ "name": "pricing", "shoot": { "preset": "desktop-1280" } },
|
|
291
|
+
{ "name": "docs", "shoot": { "preset": "desktop-1280" } }
|
|
292
|
+
]
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Steps run in a shared browser context; results are still ordered by index in the summary. Defaults to serial (`parallel: 1`) - opt in only when steps are independent.
|
|
297
|
+
|
|
298
|
+
## Accessibility Audit
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
pursr audit https://example.com --tags wcag2a,wcag2aa
|
|
302
|
+
# Writes: audit.json, audit-summary.md, audit-highlighted.png
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Injects axe-core, runs a configurable tag set (`wcag2a`, `wcag2aa`, `wcag21a`, `wcag21aa`, `best-practice`), and overlays a red outline on every violating node with the rule id as a label. The summary Markdown includes per-rule failure snippets.
|
|
306
|
+
|
|
307
|
+
## DOM Snapshot
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
pursr dom https://example.com
|
|
311
|
+
# Writes: dom-snapshot-<ts>.dom.json
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Captures serialized HTML, computed CSS for every visible element, and a selector map (`id`, `role`, `accessible name`, `text`, `xpath`, `css selector`, viewport-relative `rect`). Great for regression diffing without re-running a browser.
|
|
315
|
+
|
|
316
|
+
## CI Output
|
|
317
|
+
|
|
318
|
+
Every sweep writes three sidecar artifacts alongside `sweep.json`:
|
|
319
|
+
|
|
320
|
+
- `sweep.junit.xml` - JUnit XML for Jenkins / GitLab / CircleCI
|
|
321
|
+
- `sweep.github.json` - GitHub Actions annotation file
|
|
322
|
+
- `sweep.md` - Human-readable Markdown summary with diffs + failures
|
|
323
|
+
|
|
324
|
+
## Library API
|
|
325
|
+
|
|
326
|
+
```js
|
|
327
|
+
import {
|
|
328
|
+
runProbe, runShot, runShoot, runSweep, runDiff, runAudit,
|
|
329
|
+
captureDomSnapshot, resolveHealedSelector,
|
|
330
|
+
saveBaseline, diffKey,
|
|
331
|
+
startHarCapture, stopHarCapture, writeHar,
|
|
332
|
+
loadAuthState,
|
|
333
|
+
PursorMCPServer, loadMcpConfig,
|
|
334
|
+
validateSweepPlan,
|
|
335
|
+
listResources, readResource,
|
|
336
|
+
listViewports, resolveViewport, VIEWPORTS,
|
|
337
|
+
loadPlugins, registerPlugin, getSweepOp,
|
|
338
|
+
VERSION,
|
|
339
|
+
} from "pursr";
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Subpath exports
|
|
343
|
+
|
|
344
|
+
```js
|
|
345
|
+
import { resolveLocator } from "pursr/selector";
|
|
346
|
+
import { launch } from "pursr/runway";
|
|
347
|
+
import { parseFlags, asNum } from "pursr/util";
|
|
348
|
+
import { overlayGrid } from "pursr/overlays";
|
|
349
|
+
import { captureDomSnapshot } from "pursr/dom-snapshot";
|
|
350
|
+
import { runAudit } from "pursr/plugin-audit";
|
|
351
|
+
import { resolveHealedSelector } from "pursr/selector-heal";
|
|
352
|
+
import { writeCiOutput } from "pursr/ci-output";
|
|
353
|
+
import { diffKey, saveBaseline, loadBaseline } from "pursr/baseline";
|
|
354
|
+
import { validateSweepPlan } from "pursr/sweep-schema";
|
|
355
|
+
import { startHarCapture, stopHarCapture } from "pursr/har";
|
|
356
|
+
import { saveAuthState, loadAuthState } from "pursr/auth";
|
|
357
|
+
import { listResources, readResource } from "pursr/mcp-resources";
|
|
358
|
+
import { PursorMCPServer } from "pursr/mcp";
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## Plugins
|
|
362
|
+
|
|
363
|
+
A plugin is a plain ES module that exports a default object:
|
|
364
|
+
|
|
365
|
+
```js
|
|
366
|
+
// plugins/my-plugin.js
|
|
367
|
+
export default {
|
|
368
|
+
name: "my-plugin",
|
|
369
|
+
viewport: { "my-laptop": { width: 1440, height: 900, dpr: 2, label: "MBP 14" } },
|
|
370
|
+
sweepOp: {
|
|
371
|
+
lighthouse: async (ctx, opts) => { /* ... */ },
|
|
372
|
+
},
|
|
373
|
+
beforeShoot: async (ctx) => { /* mutate ctx.flags / ctx.viewport */ },
|
|
374
|
+
afterShoot: async (ctx, meta) => { /* augment sidecar */ },
|
|
375
|
+
flagHelp: { "my-flag": "what it does" },
|
|
376
|
+
};
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Plugins are auto-loaded from `plugins/` (built-in) or via `--plugin <path>`.
|
|
380
|
+
|
|
381
|
+
## Architecture
|
|
382
|
+
|
|
383
|
+
```
|
|
384
|
+
src/
|
|
385
|
+
index.js - public library entry
|
|
386
|
+
mcp.js - MCP stdio server (JSON-RPC 2.0)
|
|
387
|
+
shoot.js - runShoot (overlays + camera + frame-stable)
|
|
388
|
+
sweep.js - runSweep (validated, parallel pool)
|
|
389
|
+
diff.js - pixelmatch wrapper
|
|
390
|
+
plugin-audit.js - axe-core injection + highlighted screenshot
|
|
391
|
+
dom-snapshot.js - full DOM + CSSOM + selector map
|
|
392
|
+
selector-heal.js - auto-heal chain resolver
|
|
393
|
+
ci-output.js - JUnit / GitHub / Markdown
|
|
394
|
+
baseline.js - visual regression storage
|
|
395
|
+
har.js - HAR 1.2 network capture
|
|
396
|
+
auth.js - Playwright storageState
|
|
397
|
+
sweep-schema.js - plan validator
|
|
398
|
+
mcp-resources.js - MCP resources adapter
|
|
399
|
+
overlays.js - page-side CSS overlays + camera
|
|
400
|
+
runway.js - Playwright launcher + system-Chrome detector
|
|
401
|
+
viewport.js - built-in viewport presets
|
|
402
|
+
selector.js - text=/role=/aria=/placeholder= parser
|
|
403
|
+
plugin.js - plugin registry + hook runner
|
|
404
|
+
util.js - flags, args, hashing, HTML escape, renderSweepHtml
|
|
405
|
+
every-viewport.js - one shot per preset in parallel
|
|
406
|
+
frames.js, hover.js, shot.js, eval.js, probe.js, interact.js
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Development
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
git clone https://github.com/0xheycat/pursr
|
|
413
|
+
cd pursr
|
|
414
|
+
npm install
|
|
415
|
+
npm install --save-dev playwright-core
|
|
416
|
+
npm test
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
`npm test` runs 60 unit + integration tests (Node's built-in test runner, zero test deps). Coverage includes: viewport resolution, flag parsing, selector parsing, HTML escaping, hashing, baseline storage, sweep-plan validation, MCP resources, HAR 1.2 shape, auth state, and end-to-end CLI smoke tests.
|
|
420
|
+
|
|
421
|
+
```
|
|
422
|
+
src/ - 27 modules
|
|
423
|
+
test/ - 60 tests, 0 failures
|
|
424
|
+
plugins/ - 2 built-in plugins, auto-loaded
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## Roadmap
|
|
428
|
+
|
|
429
|
+
- [x] Visual baselines (save / approve / diff)
|
|
430
|
+
- [x] Sweep plan schema validation
|
|
431
|
+
- [x] MCP resources (browse past captures from your AI host)
|
|
432
|
+
- [x] HAR 1.2 capture
|
|
433
|
+
- [x] Auth state (Playwright storageState)
|
|
434
|
+
- [x] Parallel sweep workers
|
|
435
|
+
- [x] Watch mode (`pursr watch <url>`)
|
|
436
|
+
- [x] Component-level snapshot (`pursr snap <selector>`)
|
|
437
|
+
- [x] PDF report export (`pursr report --sweep`)
|
|
438
|
+
- [ ] Cloud output adapters (S3 / GCS)
|
|
439
|
+
- [x] AI diff summary (vision model, `--ai`)
|
|
440
|
+
|
|
441
|
+
## PDF Report (v0.6.0)
|
|
442
|
+
|
|
443
|
+
Turn any sweep summary into a styled, self-contained A4 PDF you can email, attach to a PR, or hand to a designer.
|
|
444
|
+
|
|
445
|
+
```bash
|
|
446
|
+
# 1. Run a sweep (writes sweep.json + index.html + per-step PNGs)
|
|
447
|
+
pursr sweep ./plans/marketing.json
|
|
448
|
+
|
|
449
|
+
# 2. Generate a PDF from the most recent sweep
|
|
450
|
+
pursr report --sweep ./out/sweep-marketing/sweep.json --out ./out/report.pdf
|
|
451
|
+
|
|
452
|
+
# Or: skip image embedding for a tiny text-only report
|
|
453
|
+
pursr report --sweep ./out/sweep-marketing/sweep.json --no-embed
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
The PDF includes a colored header (pursr brand magenta), a summary stat grid (steps / passed / failed / total time), and a per-step card with: status badge, op + duration + URL, the embedded capture PNG, diff stats, audit violation count, and any error message. Page numbers in the footer.
|
|
457
|
+
|
|
458
|
+
Library:
|
|
459
|
+
|
|
460
|
+
```js
|
|
461
|
+
import { renderSweepPdf } from "pursr/report";
|
|
462
|
+
import { readFileSync } from "node:fs";
|
|
463
|
+
|
|
464
|
+
const summary = JSON.parse(readFileSync("./sweep.json", "utf8"));
|
|
465
|
+
const bytes = await renderSweepPdf(summary, { out: "./report.pdf" });
|
|
466
|
+
console.log("wrote", bytes.length, "bytes");
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
## AI Diff Summary (v0.6.0)
|
|
470
|
+
|
|
471
|
+
Add `--ai` to `pursr diff` and a vision LLM describes the differences in plain language alongside the pixel-diff percentage. Perfect for triaging a regression without opening the PNG.
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
# Basic
|
|
475
|
+
pursr diff https://my.app ./ref.png ./out/diff.png --ai
|
|
476
|
+
|
|
477
|
+
# Custom model + endpoint + key (e.g. local llama.cpp, Codex proxy, OpenAI)
|
|
478
|
+
pursr diff https://my.app ./ref.png ./out/diff.png \
|
|
479
|
+
--ai --ai-model gh/gpt-5.4 \
|
|
480
|
+
--ai-base-url http://127.0.0.1:20128/v1 \
|
|
481
|
+
--ai-api-key sk-...
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
The AI summary is written to `<out>.ai.json` (or alongside the current PNG) and is also attached to the diff result object as `r.ai = { aiSummary, aiModel, aiElapsedMs, aiAt }`.
|
|
485
|
+
|
|
486
|
+
Auth is picked up from these env vars (in order):
|
|
487
|
+
|
|
488
|
+
```
|
|
489
|
+
PURSR_AI_API_KEY (preferred)
|
|
490
|
+
PURSOR_AI_API_KEY (legacy alias)
|
|
491
|
+
ANTHROPIC_AUTH_TOKEN
|
|
492
|
+
OPENAI_API_KEY
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Base URL: `PURSR_AI_BASE_URL` (falls back to `ANTHROPIC_BASE_URL` then `https://api.openai.com/v1`).
|
|
496
|
+
Model: `PURSR_AI_MODEL` (falls back to `ANTHROPIC_DEFAULT_SONNET_MODEL` then `gpt-4o`).
|
|
497
|
+
|
|
498
|
+
Library:
|
|
499
|
+
|
|
500
|
+
```js
|
|
501
|
+
import { aiDiffSummary, aiDiffSidecar } from "pursr/ai-diff";
|
|
502
|
+
|
|
503
|
+
const r = await aiDiffSummary({
|
|
504
|
+
refPath: "./ref.png",
|
|
505
|
+
curPath: "./out/diff-current.png",
|
|
506
|
+
url: "https://my.app",
|
|
507
|
+
model: "gpt-4o",
|
|
508
|
+
});
|
|
509
|
+
console.log(r.summary); // markdown bullet report
|
|
510
|
+
console.log(r.elapsedMs); // how long the LLM took
|
|
511
|
+
|
|
512
|
+
// Or attach to a sweep step:
|
|
513
|
+
const sidecar = await aiDiffSidecar({ refPath, curPath, url });
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
## Watch Mode (v0.5.0)
|
|
517
|
+
|
|
518
|
+
```bash
|
|
519
|
+
# Re-shoot every time a CSS or HTML file changes
|
|
520
|
+
pursr watch https://my.app --on src/**/*.css --on src/**/*.html
|
|
521
|
+
|
|
522
|
+
# Re-run a sweep plan on file change
|
|
523
|
+
pursr watch --plan ./plan.json --on src/**/*.{css,html}
|
|
524
|
+
|
|
525
|
+
# Default (no --on) = watch everything in cwd
|
|
526
|
+
pursr watch https://my.app
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Glob patterns: * (one path segment), ** (any depth), ? (one char), backslash-X (literal X). Debounce is 300ms by default.
|
|
530
|
+
|
|
531
|
+
## Component Snapshots (v0.5.0)
|
|
532
|
+
|
|
533
|
+
```bash
|
|
534
|
+
# Capture one screenshot per matched element
|
|
535
|
+
pursr snap https://my.app a.btn --out ./snaps --max 20
|
|
536
|
+
|
|
537
|
+
# Use auto-heal selector chain
|
|
538
|
+
pursr snap https://my.app "text=Sign up" --out ./snaps
|
|
539
|
+
|
|
540
|
+
# Promote to baselines in one command
|
|
541
|
+
pursr snap https://my.app article.product --baseline myapp
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
Each capture is clipped precisely to the elements bounding box (even when scrolled offscreen), labelled with aria-label / text / tag, and written to ./snaps/<index>-<label>.png + snap.json summary.
|
|
545
|
+
|
|
546
|
+
---
|
|
547
|
+
## License
|
|
548
|
+
|
|
549
|
+
MIT (c) 2026 - [0xheycat](https://github.com/0xheycat)
|