pursr 0.6.0 → 0.7.1
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/LICENSE +20 -20
- package/README.md +9 -9
- package/assets/icon.svg +20 -20
- package/assets/logo.svg +28 -28
- package/assets/social-preview.svg +76 -76
- package/bin/pursr-mcp.mjs +10 -9
- package/bin/pursr.mjs +15 -14
- package/package.json +4 -4
- package/plans/m5.4-polish.json +21 -21
- package/plugins/plugin-audit.js +57 -57
- package/plugins/plugin-demo.js +63 -63
- package/src/ai-diff.js +7 -6
- package/src/auth.js +92 -91
- package/src/baseline.js +126 -125
- package/src/ci-output.js +156 -156
- package/src/diff.js +18 -7
- package/src/dom-snapshot.js +192 -192
- package/src/eval.js +17 -17
- package/src/every-viewport.js +51 -51
- package/src/frames.js +33 -33
- package/src/har.js +158 -158
- package/src/hover.js +25 -25
- package/src/index.js +6 -6
- package/src/interact.js +137 -137
- package/src/mcp-resources.js +111 -110
- package/src/mcp.js +436 -435
- package/src/overlays.js +169 -169
- package/src/plugin-audit.js +278 -260
- package/src/plugin.js +120 -120
- package/src/probe.js +19 -19
- package/src/report.js +175 -175
- package/src/runway.js +65 -65
- package/src/selector-heal.js +85 -85
- package/src/selector.js +38 -38
- package/src/shoot.js +73 -73
- package/src/shot.js +17 -17
- package/src/snap.js +128 -128
- package/src/sweep-schema.js +69 -69
- package/src/sweep.js +1 -1
- package/src/util.js +204 -188
- package/src/viewport.js +38 -38
- package/src/watch.js +134 -134
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 pursr
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pursr
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ pursr baseline save myapp shot.png home --url https://example.com
|
|
|
65
65
|
|
|
66
66
|
# 3. Next time you run, compare against the baseline
|
|
67
67
|
pursr diff https://example.com \
|
|
68
|
-
~/.
|
|
68
|
+
~/.pursr/baselines/myapp/<id>/home.png \
|
|
69
69
|
diff.png
|
|
70
70
|
|
|
71
71
|
# 4. Or: run a batched sweep + a11y audit + parallel workers
|
|
@@ -203,7 +203,7 @@ npx pursr-mcp --verbose
|
|
|
203
203
|
| `pursr://shoot/<url|preset>` | Last screenshot PNG (image/png) |
|
|
204
204
|
| `pursr://sweep/<plan-name>` | Last sweep summary JSON (application/json) |
|
|
205
205
|
|
|
206
|
-
Resources are persisted to `~/.
|
|
206
|
+
Resources are persisted to `~/.pursr/mcp/mcp-index.json` (override with `PURSR_MCP_STATE`).
|
|
207
207
|
|
|
208
208
|
## Visual Regression Baselines
|
|
209
209
|
|
|
@@ -214,7 +214,7 @@ pursr baseline list myapp
|
|
|
214
214
|
pursr baseline show myapp home --url https://my.app
|
|
215
215
|
```
|
|
216
216
|
|
|
217
|
-
Baselines live under `~/.
|
|
217
|
+
Baselines live under `~/.pursr/baselines/<project>/<id>/<step>.png` + `manifest.json`. Override with `PURSR_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
218
|
|
|
219
219
|
```js
|
|
220
220
|
import { diffKey, saveBaseline, loadBaseline } from "pursr/baseline";
|
|
@@ -274,7 +274,7 @@ pursr auth load myapp admin --out ./round-trip.json
|
|
|
274
274
|
pursr auth delete myapp admin
|
|
275
275
|
```
|
|
276
276
|
|
|
277
|
-
States live in `~/.
|
|
277
|
+
States live in `~/.pursr/auth/<project>/<name>.json` (override with `PURSR_AUTH_DIR`). The on-disk format is the standard Playwright `storageState` shape: `{ cookies, origins }`.
|
|
278
278
|
|
|
279
279
|
## Parallel Sweep
|
|
280
280
|
|
|
@@ -330,7 +330,7 @@ import {
|
|
|
330
330
|
saveBaseline, diffKey,
|
|
331
331
|
startHarCapture, stopHarCapture, writeHar,
|
|
332
332
|
loadAuthState,
|
|
333
|
-
|
|
333
|
+
PursrMCPServer, loadMcpConfig,
|
|
334
334
|
validateSweepPlan,
|
|
335
335
|
listResources, readResource,
|
|
336
336
|
listViewports, resolveViewport, VIEWPORTS,
|
|
@@ -355,7 +355,7 @@ import { validateSweepPlan } from "pursr/sweep-schema";
|
|
|
355
355
|
import { startHarCapture, stopHarCapture } from "pursr/har";
|
|
356
356
|
import { saveAuthState, loadAuthState } from "pursr/auth";
|
|
357
357
|
import { listResources, readResource } from "pursr/mcp-resources";
|
|
358
|
-
import {
|
|
358
|
+
import { PursrMCPServer } from "pursr/mcp";
|
|
359
359
|
```
|
|
360
360
|
|
|
361
361
|
## Plugins
|
|
@@ -416,11 +416,11 @@ npm install --save-dev playwright-core
|
|
|
416
416
|
npm test
|
|
417
417
|
```
|
|
418
418
|
|
|
419
|
-
`npm test` runs
|
|
419
|
+
`npm test` runs 63 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
420
|
|
|
421
421
|
```
|
|
422
|
-
src/ -
|
|
423
|
-
test/ -
|
|
422
|
+
src/ - 29 modules
|
|
423
|
+
test/ - 63 tests, 0 failures
|
|
424
424
|
plugins/ - 2 built-in plugins, auto-loaded
|
|
425
425
|
```
|
|
426
426
|
|
package/assets/icon.svg
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="pursr icon">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
-
<stop offset="0" stop-color="#FF2EA6"/>
|
|
5
|
-
<stop offset="1" stop-color="#FF5CC1"/>
|
|
6
|
-
</linearGradient>
|
|
7
|
-
</defs>
|
|
8
|
-
<rect width="128" height="128" rx="24" fill="#0B0B0F"/>
|
|
9
|
-
<g transform="translate(8,8)">
|
|
10
|
-
<circle cx="56" cy="56" r="42" fill="none" stroke="url(#g)" stroke-width="7"/>
|
|
11
|
-
<g fill="none" stroke="url(#g)" stroke-width="7" stroke-linecap="round">
|
|
12
|
-
<path d="M56 22 L72 44"/>
|
|
13
|
-
<path d="M87 41 L74 59"/>
|
|
14
|
-
<path d="M87 75 L66 67"/>
|
|
15
|
-
<path d="M56 90 L46 69"/>
|
|
16
|
-
<path d="M25 75 L38 66"/>
|
|
17
|
-
<path d="M25 41 L39 51"/>
|
|
18
|
-
</g>
|
|
19
|
-
<path d="M56 28 L72 70 L60 66 L52 80 L48 70 L56 28 Z" fill="url(#g)"/>
|
|
20
|
-
</g>
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="pursr icon">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#FF2EA6"/>
|
|
5
|
+
<stop offset="1" stop-color="#FF5CC1"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="128" height="128" rx="24" fill="#0B0B0F"/>
|
|
9
|
+
<g transform="translate(8,8)">
|
|
10
|
+
<circle cx="56" cy="56" r="42" fill="none" stroke="url(#g)" stroke-width="7"/>
|
|
11
|
+
<g fill="none" stroke="url(#g)" stroke-width="7" stroke-linecap="round">
|
|
12
|
+
<path d="M56 22 L72 44"/>
|
|
13
|
+
<path d="M87 41 L74 59"/>
|
|
14
|
+
<path d="M87 75 L66 67"/>
|
|
15
|
+
<path d="M56 90 L46 69"/>
|
|
16
|
+
<path d="M25 75 L38 66"/>
|
|
17
|
+
<path d="M25 41 L39 51"/>
|
|
18
|
+
</g>
|
|
19
|
+
<path d="M56 28 L72 70 L60 66 L52 80 L48 70 L56 28 Z" fill="url(#g)"/>
|
|
20
|
+
</g>
|
|
21
21
|
</svg>
|
package/assets/logo.svg
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 96" role="img" aria-label="pursr">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="pursr-mark-grad" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
-
<stop offset="0" stop-color="#FF2EA6"/>
|
|
5
|
-
<stop offset="1" stop-color="#FF5CC1"/>
|
|
6
|
-
</linearGradient>
|
|
7
|
-
</defs>
|
|
8
|
-
<!-- Mark: cursor arrowhead fused with camera aperture / lens iris -->
|
|
9
|
-
<g transform="translate(8,8)">
|
|
10
|
-
<!-- Outer iris ring -->
|
|
11
|
-
<circle cx="40" cy="40" r="34" fill="none" stroke="url(#pursr-mark-grad)" stroke-width="6"/>
|
|
12
|
-
<!-- Aperture blades (6) -->
|
|
13
|
-
<g fill="none" stroke="url(#pursr-mark-grad)" stroke-width="6" stroke-linecap="round">
|
|
14
|
-
<path d="M40 12 L52 30"/>
|
|
15
|
-
<path d="M64.4 25.6 L54.5 40"/>
|
|
16
|
-
<path d="M64.4 54.4 L48 48"/>
|
|
17
|
-
<path d="M40 68 L32 50"/>
|
|
18
|
-
<path d="M15.6 54.4 L26 47"/>
|
|
19
|
-
<path d="M15.6 25.6 L27 34"/>
|
|
20
|
-
</g>
|
|
21
|
-
<!-- Cursor chevron pointer on top of iris -->
|
|
22
|
-
<path d="M40 18 L52 50 L42 47 L36 58 L33 50 L40 18 Z" fill="url(#pursr-mark-grad)"/>
|
|
23
|
-
</g>
|
|
24
|
-
<!-- Wordmark: pursr -->
|
|
25
|
-
<g transform="translate(108,62)" font-family="-apple-system,BlinkMacSystemFont,Inter,Segoe UI,Roboto,sans-serif" font-weight="700" font-size="56" letter-spacing="-2">
|
|
26
|
-
<text fill="#0B0B0F" x="0" y="0">pursr</text>
|
|
27
|
-
<rect x="170" y="-12" width="10" height="10" fill="#FF2EA6"/>
|
|
28
|
-
</g>
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 96" role="img" aria-label="pursr">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="pursr-mark-grad" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#FF2EA6"/>
|
|
5
|
+
<stop offset="1" stop-color="#FF5CC1"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<!-- Mark: cursor arrowhead fused with camera aperture / lens iris -->
|
|
9
|
+
<g transform="translate(8,8)">
|
|
10
|
+
<!-- Outer iris ring -->
|
|
11
|
+
<circle cx="40" cy="40" r="34" fill="none" stroke="url(#pursr-mark-grad)" stroke-width="6"/>
|
|
12
|
+
<!-- Aperture blades (6) -->
|
|
13
|
+
<g fill="none" stroke="url(#pursr-mark-grad)" stroke-width="6" stroke-linecap="round">
|
|
14
|
+
<path d="M40 12 L52 30"/>
|
|
15
|
+
<path d="M64.4 25.6 L54.5 40"/>
|
|
16
|
+
<path d="M64.4 54.4 L48 48"/>
|
|
17
|
+
<path d="M40 68 L32 50"/>
|
|
18
|
+
<path d="M15.6 54.4 L26 47"/>
|
|
19
|
+
<path d="M15.6 25.6 L27 34"/>
|
|
20
|
+
</g>
|
|
21
|
+
<!-- Cursor chevron pointer on top of iris -->
|
|
22
|
+
<path d="M40 18 L52 50 L42 47 L36 58 L33 50 L40 18 Z" fill="url(#pursr-mark-grad)"/>
|
|
23
|
+
</g>
|
|
24
|
+
<!-- Wordmark: pursr -->
|
|
25
|
+
<g transform="translate(108,62)" font-family="-apple-system,BlinkMacSystemFont,Inter,Segoe UI,Roboto,sans-serif" font-weight="700" font-size="56" letter-spacing="-2">
|
|
26
|
+
<text fill="#0B0B0F" x="0" y="0">pursr</text>
|
|
27
|
+
<rect x="170" y="-12" width="10" height="10" fill="#FF2EA6"/>
|
|
28
|
+
</g>
|
|
29
29
|
</svg>
|
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 640" role="img" aria-label="pursr — visual QA, audit, and MCP for the browser">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
-
<stop offset="0" stop-color="#0B0B0F"/>
|
|
5
|
-
<stop offset="0.6" stop-color="#15101A"/>
|
|
6
|
-
<stop offset="1" stop-color="#1F0A1A"/>
|
|
7
|
-
</linearGradient>
|
|
8
|
-
<linearGradient id="mark" x1="0" y1="0" x2="1" y2="1">
|
|
9
|
-
<stop offset="0" stop-color="#FF2EA6"/>
|
|
10
|
-
<stop offset="1" stop-color="#FF5CC1"/>
|
|
11
|
-
</linearGradient>
|
|
12
|
-
<pattern id="grid" width="48" height="48" patternUnits="userSpaceOnUse">
|
|
13
|
-
<path d="M48 0 L0 0 0 48" fill="none" stroke="#FF2EA6" stroke-opacity="0.08" stroke-width="1"/>
|
|
14
|
-
</pattern>
|
|
15
|
-
<radialGradient id="glow" cx="0.85" cy="0.15" r="0.6">
|
|
16
|
-
<stop offset="0" stop-color="#FF2EA6" stop-opacity="0.35"/>
|
|
17
|
-
<stop offset="1" stop-color="#FF2EA6" stop-opacity="0"/>
|
|
18
|
-
</radialGradient>
|
|
19
|
-
</defs>
|
|
20
|
-
<rect width="1280" height="640" fill="url(#bg)"/>
|
|
21
|
-
<rect width="1280" height="640" fill="url(#grid)"/>
|
|
22
|
-
<rect width="1280" height="640" fill="url(#glow)"/>
|
|
23
|
-
|
|
24
|
-
<!-- Decorative screenshot card on the right -->
|
|
25
|
-
<g transform="translate(720,160)">
|
|
26
|
-
<rect width="440" height="320" rx="16" fill="#101015" stroke="#2A2A30"/>
|
|
27
|
-
<rect x="0" y="0" width="440" height="40" rx="16" fill="#1B1B22"/>
|
|
28
|
-
<circle cx="20" cy="20" r="6" fill="#FF5F56"/>
|
|
29
|
-
<circle cx="40" cy="20" r="6" fill="#FFBD2E"/>
|
|
30
|
-
<circle cx="60" cy="20" r="6" fill="#27C93F"/>
|
|
31
|
-
<text x="220" y="25" fill="#666" font-family="monospace" font-size="12" text-anchor="middle">
|
|
32
|
-
<line x1="20" y1="80" x2="200" y2="80" stroke="#FF2EA6" stroke-width="14" stroke-linecap="round"/>
|
|
33
|
-
<line x1="20" y1="120" x2="380" y2="120" stroke="#3A3A45" stroke-width="8" stroke-linecap="round"/>
|
|
34
|
-
<line x1="20" y1="160" x2="320" y2="160" stroke="#3A3A45" stroke-width="8" stroke-linecap="round"/>
|
|
35
|
-
<line x1="20" y1="200" x2="260" y2="200" stroke="#3A3A45" stroke-width="8" stroke-linecap="round"/>
|
|
36
|
-
<rect x="20" y="240" width="120" height="48" rx="6" fill="#FF2EA6"/>
|
|
37
|
-
<line x1="160" y1="264" x2="220" y2="264" stroke="#3A3A45" stroke-width="6" stroke-linecap="round"/>
|
|
38
|
-
<line x1="240" y1="252" x2="380" y2="252" stroke="#3A3A45" stroke-width="6" stroke-linecap="round"/>
|
|
39
|
-
<line x1="240" y1="276" x2="340" y2="276" stroke="#3A3A45" stroke-width="6" stroke-linecap="round"/>
|
|
40
|
-
</g>
|
|
41
|
-
|
|
42
|
-
<!-- Brand mark + wordmark on the left -->
|
|
43
|
-
<g transform="translate(80,200)">
|
|
44
|
-
<g transform="scale(1.5)">
|
|
45
|
-
<circle cx="40" cy="40" r="34" fill="none" stroke="url(#mark)" stroke-width="6"/>
|
|
46
|
-
<g fill="none" stroke="url(#mark)" stroke-width="6" stroke-linecap="round">
|
|
47
|
-
<path d="M40 12 L52 30"/>
|
|
48
|
-
<path d="M64.4 25.6 L54.5 40"/>
|
|
49
|
-
<path d="M64.4 54.4 L48 48"/>
|
|
50
|
-
<path d="M40 68 L32 50"/>
|
|
51
|
-
<path d="M15.6 54.4 L26 47"/>
|
|
52
|
-
<path d="M15.6 25.6 L27 34"/>
|
|
53
|
-
</g>
|
|
54
|
-
<path d="M40 18 L52 50 L42 47 L36 58 L33 50 L40 18 Z" fill="url(#mark)"/>
|
|
55
|
-
</g>
|
|
56
|
-
<text x="140" y="56" fill="#FFFFFF" font-family="-apple-system,BlinkMacSystemFont,Inter,sans-serif" font-weight="800" font-size="76" letter-spacing="-3">pursr</text>
|
|
57
|
-
<rect x="438" y="0" width="14" height="14" fill="#FF2EA6"/>
|
|
58
|
-
</g>
|
|
59
|
-
|
|
60
|
-
<!-- Tagline -->
|
|
61
|
-
<text x="80" y="380" fill="#FFFFFF" font-family="-apple-system,BlinkMacSystemFont,Inter,sans-serif" font-weight="600" font-size="34" letter-spacing="-1">Visual QA, audit & MCP for the browser.</text>
|
|
62
|
-
<text x="80" y="430" fill="#A0A0AA" font-family="-apple-system,BlinkMacSystemFont,Inter,sans-serif" font-weight="400" font-size="22" letter-spacing="-0.5">Capture · sweep · diff · audit · repeat.</text>
|
|
63
|
-
|
|
64
|
-
<!-- Footer chip line -->
|
|
65
|
-
<g transform="translate(80,520)">
|
|
66
|
-
<rect width="180" height="36" rx="18" fill="#1A1A22" stroke="#2A2A30"/>
|
|
67
|
-
<text x="90" y="23" fill="#FF5CC1" font-family="monospace" font-size="14" text-anchor="middle">npm i pursr</text>
|
|
68
|
-
</g>
|
|
69
|
-
<g transform="translate(280,520)">
|
|
70
|
-
<rect width="200" height="36" rx="18" fill="#1A1A22" stroke="#2A2A30"/>
|
|
71
|
-
<text x="100" y="23" fill="#A0A0AA" font-family="monospace" font-size="14" text-anchor="middle">7 MCP tools · 32 tests</text>
|
|
72
|
-
</g>
|
|
73
|
-
<g transform="translate(500,520)">
|
|
74
|
-
<rect width="220" height="36" rx="18" fill="#1A1A22" stroke="#2A2A30"/>
|
|
75
|
-
<text x="110" y="23" fill="#A0A0AA" font-family="monospace" font-size="14" text-anchor="middle">HAR · baselines · parallel</text>
|
|
76
|
-
</g>
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 640" role="img" aria-label="pursr — visual QA, audit, and MCP for the browser">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#0B0B0F"/>
|
|
5
|
+
<stop offset="0.6" stop-color="#15101A"/>
|
|
6
|
+
<stop offset="1" stop-color="#1F0A1A"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
<linearGradient id="mark" x1="0" y1="0" x2="1" y2="1">
|
|
9
|
+
<stop offset="0" stop-color="#FF2EA6"/>
|
|
10
|
+
<stop offset="1" stop-color="#FF5CC1"/>
|
|
11
|
+
</linearGradient>
|
|
12
|
+
<pattern id="grid" width="48" height="48" patternUnits="userSpaceOnUse">
|
|
13
|
+
<path d="M48 0 L0 0 0 48" fill="none" stroke="#FF2EA6" stroke-opacity="0.08" stroke-width="1"/>
|
|
14
|
+
</pattern>
|
|
15
|
+
<radialGradient id="glow" cx="0.85" cy="0.15" r="0.6">
|
|
16
|
+
<stop offset="0" stop-color="#FF2EA6" stop-opacity="0.35"/>
|
|
17
|
+
<stop offset="1" stop-color="#FF2EA6" stop-opacity="0"/>
|
|
18
|
+
</radialGradient>
|
|
19
|
+
</defs>
|
|
20
|
+
<rect width="1280" height="640" fill="url(#bg)"/>
|
|
21
|
+
<rect width="1280" height="640" fill="url(#grid)"/>
|
|
22
|
+
<rect width="1280" height="640" fill="url(#glow)"/>
|
|
23
|
+
|
|
24
|
+
<!-- Decorative screenshot card on the right -->
|
|
25
|
+
<g transform="translate(720,160)">
|
|
26
|
+
<rect width="440" height="320" rx="16" fill="#101015" stroke="#2A2A30"/>
|
|
27
|
+
<rect x="0" y="0" width="440" height="40" rx="16" fill="#1B1B22"/>
|
|
28
|
+
<circle cx="20" cy="20" r="6" fill="#FF5F56"/>
|
|
29
|
+
<circle cx="40" cy="20" r="6" fill="#FFBD2E"/>
|
|
30
|
+
<circle cx="60" cy="20" r="6" fill="#27C93F"/>
|
|
31
|
+
<text x="220" y="25" fill="#666" font-family="monospace" font-size="12" text-anchor="middle">pursr.mjs shoot</text>
|
|
32
|
+
<line x1="20" y1="80" x2="200" y2="80" stroke="#FF2EA6" stroke-width="14" stroke-linecap="round"/>
|
|
33
|
+
<line x1="20" y1="120" x2="380" y2="120" stroke="#3A3A45" stroke-width="8" stroke-linecap="round"/>
|
|
34
|
+
<line x1="20" y1="160" x2="320" y2="160" stroke="#3A3A45" stroke-width="8" stroke-linecap="round"/>
|
|
35
|
+
<line x1="20" y1="200" x2="260" y2="200" stroke="#3A3A45" stroke-width="8" stroke-linecap="round"/>
|
|
36
|
+
<rect x="20" y="240" width="120" height="48" rx="6" fill="#FF2EA6"/>
|
|
37
|
+
<line x1="160" y1="264" x2="220" y2="264" stroke="#3A3A45" stroke-width="6" stroke-linecap="round"/>
|
|
38
|
+
<line x1="240" y1="252" x2="380" y2="252" stroke="#3A3A45" stroke-width="6" stroke-linecap="round"/>
|
|
39
|
+
<line x1="240" y1="276" x2="340" y2="276" stroke="#3A3A45" stroke-width="6" stroke-linecap="round"/>
|
|
40
|
+
</g>
|
|
41
|
+
|
|
42
|
+
<!-- Brand mark + wordmark on the left -->
|
|
43
|
+
<g transform="translate(80,200)">
|
|
44
|
+
<g transform="scale(1.5)">
|
|
45
|
+
<circle cx="40" cy="40" r="34" fill="none" stroke="url(#mark)" stroke-width="6"/>
|
|
46
|
+
<g fill="none" stroke="url(#mark)" stroke-width="6" stroke-linecap="round">
|
|
47
|
+
<path d="M40 12 L52 30"/>
|
|
48
|
+
<path d="M64.4 25.6 L54.5 40"/>
|
|
49
|
+
<path d="M64.4 54.4 L48 48"/>
|
|
50
|
+
<path d="M40 68 L32 50"/>
|
|
51
|
+
<path d="M15.6 54.4 L26 47"/>
|
|
52
|
+
<path d="M15.6 25.6 L27 34"/>
|
|
53
|
+
</g>
|
|
54
|
+
<path d="M40 18 L52 50 L42 47 L36 58 L33 50 L40 18 Z" fill="url(#mark)"/>
|
|
55
|
+
</g>
|
|
56
|
+
<text x="140" y="56" fill="#FFFFFF" font-family="-apple-system,BlinkMacSystemFont,Inter,sans-serif" font-weight="800" font-size="76" letter-spacing="-3">pursr</text>
|
|
57
|
+
<rect x="438" y="0" width="14" height="14" fill="#FF2EA6"/>
|
|
58
|
+
</g>
|
|
59
|
+
|
|
60
|
+
<!-- Tagline -->
|
|
61
|
+
<text x="80" y="380" fill="#FFFFFF" font-family="-apple-system,BlinkMacSystemFont,Inter,sans-serif" font-weight="600" font-size="34" letter-spacing="-1">Visual QA, audit & MCP for the browser.</text>
|
|
62
|
+
<text x="80" y="430" fill="#A0A0AA" font-family="-apple-system,BlinkMacSystemFont,Inter,sans-serif" font-weight="400" font-size="22" letter-spacing="-0.5">Capture · sweep · diff · audit · repeat.</text>
|
|
63
|
+
|
|
64
|
+
<!-- Footer chip line -->
|
|
65
|
+
<g transform="translate(80,520)">
|
|
66
|
+
<rect width="180" height="36" rx="18" fill="#1A1A22" stroke="#2A2A30"/>
|
|
67
|
+
<text x="90" y="23" fill="#FF5CC1" font-family="monospace" font-size="14" text-anchor="middle">npm i pursr</text>
|
|
68
|
+
</g>
|
|
69
|
+
<g transform="translate(280,520)">
|
|
70
|
+
<rect width="200" height="36" rx="18" fill="#1A1A22" stroke="#2A2A30"/>
|
|
71
|
+
<text x="100" y="23" fill="#A0A0AA" font-family="monospace" font-size="14" text-anchor="middle">7 MCP tools · 32 tests</text>
|
|
72
|
+
</g>
|
|
73
|
+
<g transform="translate(500,520)">
|
|
74
|
+
<rect width="220" height="36" rx="18" fill="#1A1A22" stroke="#2A2A30"/>
|
|
75
|
+
<text x="110" y="23" fill="#A0A0AA" font-family="monospace" font-size="14" text-anchor="middle">HAR · baselines · parallel</text>
|
|
76
|
+
</g>
|
|
77
77
|
</svg>
|
package/bin/pursr-mcp.mjs
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
2
|
+
// pursr — MCP server binary.
|
|
3
3
|
//
|
|
4
|
-
// Runs the
|
|
4
|
+
// Runs the pursr MCP stdio server, exposing all capture/audit/sweep
|
|
5
5
|
// capabilities as MCP tools for Claude Code, Cursor, Continue, etc.
|
|
6
6
|
//
|
|
7
|
-
// Usage:
|
|
8
|
-
// Config via
|
|
7
|
+
// Usage: pursr-mcp
|
|
8
|
+
// Config via PURSR_MCP_CONFIG env or ~/.pursr/mcp-config.json
|
|
9
9
|
//
|
|
10
|
-
// echo '{"url":"https://example.com"}' |
|
|
10
|
+
// echo '{"url":"https://example.com"}' | pursr-mcp
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { PursrMCPServer, loadConfig } from "../src/mcp.js";
|
|
13
|
+
import { __PURSR_GET } from "../src/util.js";
|
|
13
14
|
|
|
14
15
|
const config = loadConfig();
|
|
15
16
|
|
|
16
17
|
// Verbose mode: --verbose or debug env
|
|
17
|
-
const verbose = process.argv.includes("--verbose") || !!
|
|
18
|
+
const verbose = process.argv.includes("--verbose") || !!__PURSR_GET("PURSR_DEBUG");
|
|
18
19
|
config.verbose = verbose;
|
|
19
20
|
|
|
20
|
-
const server = new
|
|
21
|
-
await server.start();
|
|
21
|
+
const server = new PursrMCPServer(config);
|
|
22
|
+
await server.start();
|
package/bin/pursr.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
//
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// pursr CLI. Thin wrapper around src/* that mirrors the npm bin.
|
|
3
3
|
|
|
4
4
|
import { VERSION } from "../src/index.js";
|
|
5
5
|
import { runClick, runType, runWait, runSeq } from "../src/interact.js";
|
|
@@ -15,7 +15,7 @@ import { runEveryViewport } from "../src/every-viewport.js";
|
|
|
15
15
|
import { runAudit } from "../src/plugin-audit.js";
|
|
16
16
|
import { captureDomSnapshot } from "../src/dom-snapshot.js";
|
|
17
17
|
import { listViewports } from "../src/viewport.js";
|
|
18
|
-
import { parseFlags, asNum, readArg, makeOut, pickOutPath } from "../src/util.js";
|
|
18
|
+
import { parseFlags, asNum, readArg, makeOut, pickOutPath, __PURSR_GET } from "../src/util.js";
|
|
19
19
|
import { writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
20
20
|
import { dirname } from "node:path";
|
|
21
21
|
import { readFileSync as _readFileSync } from "node:fs";
|
|
@@ -23,8 +23,8 @@ const readFile = _readFileSync;
|
|
|
23
23
|
import { loadPlugins, listPlugins, getFlagHelp } from "../src/plugin.js";
|
|
24
24
|
|
|
25
25
|
const USAGE = `usage:
|
|
26
|
-
v1:
|
|
27
|
-
v2:
|
|
26
|
+
v1: pursr {probe|shot|full|eval|click|type|wait|diff|seq} <url> [...]
|
|
27
|
+
v2: pursr {viewports|shoot|layer|frames|hover|sweep} <...>
|
|
28
28
|
flags: --preset <name> --width N --height N --dpr N
|
|
29
29
|
--zoom 1.5 --panX 200 --panY -100
|
|
30
30
|
--cursor pointer|grab|grabbing|crosshair|none
|
|
@@ -34,7 +34,7 @@ const USAGE = `usage:
|
|
|
34
34
|
@file prefix reads argv contents from file (UTF-8, newline trimmed).
|
|
35
35
|
report: pursr report --sweep <sweep.json> [--out <report.pdf>] [--title "..."] [--no-embed]
|
|
36
36
|
diff extras: --ai [--ai-model M] [--ai-base-url U] [--ai-api-key K]
|
|
37
|
-
plugins:
|
|
37
|
+
plugins: pursr automatically loads built-in plugins from plugins/.
|
|
38
38
|
You can also pass --plugin <path> to load custom plugins (repeatable).`;
|
|
39
39
|
|
|
40
40
|
function die(msg, code = 2) {
|
|
@@ -44,7 +44,7 @@ function die(msg, code = 2) {
|
|
|
44
44
|
|
|
45
45
|
const argv = process.argv;
|
|
46
46
|
const [, , cmd, a, b, c, d] = argv;
|
|
47
|
-
const url =
|
|
47
|
+
const url = __PURSR_GET("PURSR_URL") || a;
|
|
48
48
|
// Top-level --plan / --out parsing for subcommands that need it before dispatch
|
|
49
49
|
function _topOpts() {
|
|
50
50
|
const o = {};
|
|
@@ -66,7 +66,7 @@ await loadPlugins(pluginPaths);
|
|
|
66
66
|
switch (cmd) {
|
|
67
67
|
case undefined: case "help": case "--help": case "-h": { console.log(JSON.stringify({ usage: USAGE }, null, 2)); break; }
|
|
68
68
|
case "version": case "--version": case "-v": {
|
|
69
|
-
console.log(JSON.stringify({ name: "
|
|
69
|
+
console.log(JSON.stringify({ name: "pursr", version: VERSION, plugins: listPlugins() }, null, 2));
|
|
70
70
|
break;
|
|
71
71
|
}
|
|
72
72
|
case "probe": { if (!url) die("missing url"); const r = await runProbe(url); console.log(JSON.stringify(r, null, 2)); break; }
|
|
@@ -79,14 +79,15 @@ await loadPlugins(pluginPaths);
|
|
|
79
79
|
case "diff": {
|
|
80
80
|
if (!url) die("missing url"); const ref = b; if (!ref) die("diff: missing <ref.png>");
|
|
81
81
|
const out = c || makeOut("diff.png"); const threshold = d !== undefined ? Number(d) : 0.1;
|
|
82
|
+
const flags = parseFlags(argv.slice(5));
|
|
82
83
|
// --ai / --ai-model / --ai-base-url / --ai-api-key
|
|
83
84
|
const useAi = argv.includes("--ai");
|
|
84
85
|
const aiModel = (() => { const i = argv.indexOf("--ai-model"); return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined; })();
|
|
85
86
|
const aiBaseUrl = (() => { const i = argv.indexOf("--ai-base-url"); return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined; })();
|
|
86
87
|
const aiApiKey = (() => { const i = argv.indexOf("--ai-api-key"); return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined; })();
|
|
87
88
|
const r = useAi
|
|
88
|
-
? await runDiffWithAi(url, ref, out, threshold, { model: aiModel, baseUrl: aiBaseUrl, apiKey: aiApiKey })
|
|
89
|
-
: await runDiff(url, ref, out, threshold);
|
|
89
|
+
? await runDiffWithAi(url, ref, out, threshold, flags, { model: aiModel, baseUrl: aiBaseUrl, apiKey: aiApiKey })
|
|
90
|
+
: await runDiff(url, ref, out, threshold, flags);
|
|
90
91
|
console.log(JSON.stringify(r, null, 2)); break;
|
|
91
92
|
}
|
|
92
93
|
case "seq": { if (!url) die("missing url"); const actions = readArg(b); if (!actions) die("seq: missing <actions.json> (or @file)"); const out = c || makeOut("seq.png"); const r = await runSeq(url, actions, out); console.log(JSON.stringify(r, null, 2)); break; }
|
|
@@ -139,7 +140,7 @@ await loadPlugins(pluginPaths);
|
|
|
139
140
|
if (!sweepPath) die("report: missing --sweep <sweep.json>");
|
|
140
141
|
if (!existsSync(sweepPath)) die("report: sweep not found: " + sweepPath);
|
|
141
142
|
const outIdx = argv.indexOf("--out");
|
|
142
|
-
const outPath = outIdx >= 0 && outIdx + 1 < argv.length ? argv[outIdx + 1] : (opts.out || makeOut("report.pdf").replace(/
|
|
143
|
+
const outPath = outIdx >= 0 && outIdx + 1 < argv.length ? argv[outIdx + 1] : (opts.out || makeOut("report.pdf").replace(/pursr-[^-]+-shot.png$/, "report.pdf"));
|
|
143
144
|
if (outPath && outPath !== "-") mkdirSync(dirname(outPath), { recursive: true });
|
|
144
145
|
const titleIdx = argv.indexOf("--title");
|
|
145
146
|
const title = titleIdx >= 0 && titleIdx + 1 < argv.length ? argv[titleIdx + 1] : undefined;
|
|
@@ -186,7 +187,7 @@ await loadPlugins(pluginPaths);
|
|
|
186
187
|
break;
|
|
187
188
|
}
|
|
188
189
|
case "baseline": {
|
|
189
|
-
//
|
|
190
|
+
// baseline <sub> [...args]
|
|
190
191
|
// sub=list -> list baselines
|
|
191
192
|
// sub=save <project> <png> <step> [--id <id>] [--url <u>] [--meta-json <file>]
|
|
192
193
|
// sub=approve <project> <png> <step> [--id <id>] [--url <u>]
|
|
@@ -227,7 +228,7 @@ await loadPlugins(pluginPaths);
|
|
|
227
228
|
break;
|
|
228
229
|
}
|
|
229
230
|
case "auth": {
|
|
230
|
-
//
|
|
231
|
+
// auth <sub> [...args]
|
|
231
232
|
// save <project> <name> --from <state.json>
|
|
232
233
|
// load <project> <name> --out <state.json>
|
|
233
234
|
// list [project]
|
|
@@ -300,7 +301,7 @@ await loadPlugins(pluginPaths);
|
|
|
300
301
|
const sel = b; if (!sel) die("snap: missing <selector>");
|
|
301
302
|
const flags = parseFlags(argv.slice(4));
|
|
302
303
|
const { runSnap, approveSnapsAsBaselines } = await import("../src/snap.js");
|
|
303
|
-
const outDir = flags.out || makeOut("snaps").replace(/
|
|
304
|
+
const outDir = flags.out || makeOut("snaps").replace(/pursr-[^-]+-snap\.png$/, "snaps");
|
|
304
305
|
const snap = await runSnap({ url, selector: sel, outDir, name: flags.name, max: flags.max, flags });
|
|
305
306
|
console.log(JSON.stringify({
|
|
306
307
|
url: snap.url,
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pursr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "Visual QA, audit, and MCP for the browser. One CLI + one MCP server for screenshots, sweeps, baselines, diffs, axe-core a11y audits, HAR capture, and auth state — with parallel sweep workers, auto-healing selectors, and a plugin system. Zero browser bundled: drives your system Chrome via Playwright.",
|
|
5
|
+
"description": "pursr — Visual QA, audit, and MCP for the browser. One CLI + one MCP server for screenshots, sweeps, baselines, diffs, axe-core a11y audits, HAR capture, and auth state — with parallel sweep workers, auto-healing selectors, and a plugin system. Zero browser bundled: drives your system Chrome via Playwright.",
|
|
6
6
|
"homepage": "https://github.com/0xheycat/pursr",
|
|
7
7
|
"bugs": "https://github.com/0xheycat/pursr/issues",
|
|
8
8
|
"repository": {
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"LICENSE"
|
|
51
51
|
],
|
|
52
52
|
"scripts": {
|
|
53
|
-
"start": "node bin/
|
|
53
|
+
"start": "node bin/pursr.mjs",
|
|
54
54
|
"test": "node --test \"test/*.test.js\"",
|
|
55
|
-
"smoke": "node bin/
|
|
55
|
+
"smoke": "node bin/pursr.mjs viewports"
|
|
56
56
|
},
|
|
57
57
|
"engines": {
|
|
58
58
|
"node": ">=18"
|
package/plans/m5.4-polish.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "m5.4-polish",
|
|
3
|
-
"base": "http://localhost:3010",
|
|
4
|
-
"outDir": "./out/m54-sweep",
|
|
5
|
-
"steps": [
|
|
6
|
-
{ "name": "baseline", "shoot": { "preset": "desktop-1280" } },
|
|
7
|
-
{ "name": "cursor-pointer", "shoot": { "preset": "desktop-1280", "cursor": "pointer" } },
|
|
8
|
-
{ "name": "grid-64", "shoot": { "preset": "desktop-1280", "grid": true, "grid-tile": 64 } },
|
|
9
|
-
{ "name": "grid-128", "shoot": { "preset": "desktop-1280", "grid": true, "grid-tile": 128 } },
|
|
10
|
-
{ "name": "layer-entity", "shoot": { "preset": "desktop-1280", "layer": "entity" } },
|
|
11
|
-
{ "name": "layer-terrain", "shoot": { "preset": "desktop-1280", "layer": "terrain" } },
|
|
12
|
-
{ "name": "no-hud", "shoot": { "preset": "desktop-1280", "no-hud": true } },
|
|
13
|
-
{ "name": "frozen", "shoot": { "preset": "desktop-1280", "no-animation": true } },
|
|
14
|
-
{ "name": "tablet-768", "shoot": { "preset": "tablet-768" } },
|
|
15
|
-
{ "name": "mobile-375", "shoot": { "preset": "mobile-375" } },
|
|
16
|
-
{ "name": "ultrawide-3440", "shoot": { "preset": "ultrawide-3440" } },
|
|
17
|
-
{ "name": "hover-build", "hover": { "selector": "text=Build" } },
|
|
18
|
-
{ "name": "hover-decor", "hover": { "selector": "text=Decor" } },
|
|
19
|
-
{ "name": "frames-8", "frames": { "count": 8, "intervalMs": 200 } },
|
|
20
|
-
{ "name": "diff-vs-baseline", "diff": { "ref": "baseline.png" } }
|
|
21
|
-
]
|
|
1
|
+
{
|
|
2
|
+
"name": "m5.4-polish",
|
|
3
|
+
"base": "http://localhost:3010",
|
|
4
|
+
"outDir": "./out/m54-sweep",
|
|
5
|
+
"steps": [
|
|
6
|
+
{ "name": "baseline", "shoot": { "preset": "desktop-1280" } },
|
|
7
|
+
{ "name": "cursor-pointer", "shoot": { "preset": "desktop-1280", "cursor": "pointer" } },
|
|
8
|
+
{ "name": "grid-64", "shoot": { "preset": "desktop-1280", "grid": true, "grid-tile": 64 } },
|
|
9
|
+
{ "name": "grid-128", "shoot": { "preset": "desktop-1280", "grid": true, "grid-tile": 128 } },
|
|
10
|
+
{ "name": "layer-entity", "shoot": { "preset": "desktop-1280", "layer": "entity" } },
|
|
11
|
+
{ "name": "layer-terrain", "shoot": { "preset": "desktop-1280", "layer": "terrain" } },
|
|
12
|
+
{ "name": "no-hud", "shoot": { "preset": "desktop-1280", "no-hud": true } },
|
|
13
|
+
{ "name": "frozen", "shoot": { "preset": "desktop-1280", "no-animation": true } },
|
|
14
|
+
{ "name": "tablet-768", "shoot": { "preset": "tablet-768" } },
|
|
15
|
+
{ "name": "mobile-375", "shoot": { "preset": "mobile-375" } },
|
|
16
|
+
{ "name": "ultrawide-3440", "shoot": { "preset": "ultrawide-3440" } },
|
|
17
|
+
{ "name": "hover-build", "hover": { "selector": "text=Build" } },
|
|
18
|
+
{ "name": "hover-decor", "hover": { "selector": "text=Decor" } },
|
|
19
|
+
{ "name": "frames-8", "frames": { "count": 8, "intervalMs": 200 } },
|
|
20
|
+
{ "name": "diff-vs-baseline", "diff": { "ref": "baseline.png" } }
|
|
21
|
+
]
|
|
22
22
|
}
|