pursor 0.2.0 → 0.3.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/bin/pursor.mjs CHANGED
@@ -1,191 +1,227 @@
1
- #!/usr/bin/env node
2
- // pursor CLI. Thin wrapper around src/* that mirrors the npm bin.
3
-
4
- import { VERSION } from "../src/index.js";
5
- import { runClick, runType, runWait, runSeq } from "../src/interact.js";
6
- import { runEval } from "../src/eval.js";
7
- import { runProbe } from "../src/probe.js";
8
- import { runShot } from "../src/shot.js";
9
- import { runShootWithSidecar } from "../src/shoot.js";
10
- import { runHover } from "../src/hover.js";
11
- import { runFrames } from "../src/frames.js";
12
- import { runDiff } from "../src/diff.js";
13
- import { runSweep } from "../src/sweep.js";
14
- import { runEveryViewport } from "../src/every-viewport.js";
15
- import { runAudit } from "../src/plugin-audit.js";
16
- import { captureDomSnapshot } from "../src/dom-snapshot.js";
17
- import { listViewports } from "../src/viewport.js";
18
- import { parseFlags, asNum, readArg, makeOut, pickOutPath } from "../src/util.js";
19
- import { readFileSync as _readFileSync } from "node:fs";
20
- const readFile = _readFileSync;
21
- import { loadPlugins, listPlugins, getFlagHelp } from "../src/plugin.js";
22
-
23
- const USAGE = `usage:
24
- v1: pursor {probe|shot|full|eval|click|type|wait|diff|seq} <url> [...]
25
- v2: pursor {viewports|shoot|layer|frames|hover|sweep} <...>
26
- flags: --preset <name> --width N --height N --dpr N
27
- --zoom 1.5 --panX 200 --panY -100
28
- --cursor pointer|grab|grabbing|crosshair|none
29
- --layer entity|terrain|hud|ui
30
- --grid --grid-tile 64 --grid-color rgba(255,0,255,0.35)
31
- --no-animation --wait-frame 600 --full
32
- @file prefix reads argv contents from file (UTF-8, newline trimmed).
33
- plugins: pursor automatically loads built-in plugins from plugins/.
34
- You can also pass --plugin <path> to load custom plugins (repeatable).`;
35
-
36
- function die(msg, code = 2) {
37
- console.error(JSON.stringify({ error: msg, usage: USAGE }, null, 2));
38
- process.exit(code);
39
- }
40
-
41
- const argv = process.argv;
42
- const [, , cmd, a, b, c, d] = argv;
43
- const url = process.env.PURSOR_URL || a;
44
-
45
- // Plugin loading: scan for --plugin <path> and built-in plugins/
46
- const pluginPaths = [];
47
- for (let i = 0; i < argv.length; i++) if (argv[i] === "--plugin" && i + 1 < argv.length) pluginPaths.push(argv[++i]);
48
- await loadPlugins(pluginPaths);
49
-
50
- (async () => {
51
- try {
52
- switch (cmd) {
53
- case undefined: case "help": case "--help": case "-h": { console.log(JSON.stringify({ usage: USAGE }, null, 2)); break; }
54
- case "version": case "--version": case "-v": {
55
- console.log(JSON.stringify({ name: "pursor", version: VERSION, plugins: listPlugins() }, null, 2));
56
- break;
57
- }
58
- case "probe": { if (!url) die("missing url"); const r = await runProbe(url); console.log(JSON.stringify(r, null, 2)); break; }
59
- case "shot": { if (!url) die("missing url"); const out = b || makeOut("shot.png"); const r = await runShot(url, out, { fullPage: false }); console.log(JSON.stringify(r, null, 2)); break; }
60
- case "full": { if (!url) die("missing url"); const out = b || makeOut("full.png"); const r = await runShot(url, out, { fullPage: true }); console.log(JSON.stringify(r, null, 2)); break; }
61
- case "eval": { if (!url) die("missing url"); const js = readArg(b); if (!js) die("eval: missing <js> (or @file)"); const out = c || makeOut("eval.png"); const r = await runEval(url, js, out); console.log(JSON.stringify(r, null, 2)); break; }
62
- case "click": { if (!url) die("missing url"); const sel = b; if (!sel) die("click: missing <selector>"); const out = c || makeOut(`click-${(sel||"").replace(/[^a-z0-9]+/gi, "_").slice(0, 32)}.png`); const r = await runClick(url, sel, out); console.log(JSON.stringify(r, null, 2)); break; }
63
- case "type": { if (!url) die("missing url"); const sel = b; const text = readArg(c); if (!sel || text === undefined) die("type: missing <selector> or <text> (text can be @file)"); const out = d || makeOut(`type-${(sel||"").replace(/[^a-z0-9]+/gi, "_").slice(0, 32)}.png`); const r = await runType(url, sel, text, out); console.log(JSON.stringify(r, null, 2)); break; }
64
- case "wait": { if (!url) die("missing url"); const sel = b; if (!sel) die("wait: missing <selector>"); const t = c !== undefined ? asNum(c, 30000) : 30000; const r = await runWait(url, sel, t); console.log(JSON.stringify(r, null, 2)); break; }
65
- case "diff": { if (!url) die("missing url"); const ref = b; if (!ref) die("diff: missing <ref.png>"); const out = c || makeOut("diff.png"); const threshold = d !== undefined ? Number(d) : 0.1; const r = await runDiff(url, ref, out, threshold); console.log(JSON.stringify(r, null, 2)); break; }
66
- 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; }
67
- case "viewports": { console.log(JSON.stringify(listViewports(), null, 2)); break; }
68
- case "shoot": {
69
- if (!url) die("missing url");
70
- const out = (b && !b.startsWith("--")) ? b : pickOutPath(argv.slice(5)) || makeOut("shoot.png");
71
- const r = await runShootWithSidecar({ url, out, flags: parseFlags(argv.slice(5)) });
72
- console.log(JSON.stringify(r, null, 2));
73
- break;
74
- }
75
- case "layer": {
76
- if (!url) die("missing url");
77
- const layerName = b; if (!layerName) die("layer: missing <name>");
78
- const out = (c && !c.startsWith("--")) ? c : pickOutPath(argv.slice(6)) || makeOut(`layer-${layerName}.png`);
79
- const flags = parseFlags(argv.slice(7)); flags.layer = layerName;
80
- const r = await runShootWithSidecar({ url, out, flags });
81
- console.log(JSON.stringify(r, null, 2));
82
- break;
83
- }
84
- case "frames": {
85
- if (!url) die("missing url");
86
- const count = asNum(b, 8);
87
- const stepMs = asNum(c, 250);
88
- const outDir = (d && !d.startsWith("--")) ? d : makeOut(`frames-${count}x${stepMs}ms`);
89
- const r = await runFrames({ url, count, intervalMs: stepMs, outDir, flags: parseFlags(argv.slice(7)) });
90
- console.log(JSON.stringify(r, null, 2));
91
- break;
92
- }
93
- case "hover": {
94
- if (!url) die("missing url");
95
- const sel = b; if (!sel) die("hover: missing <selector>");
96
- const out = (c && !c.startsWith("--")) ? c : pickOutPath(argv.slice(6)) || makeOut(`hover-${(sel||"").replace(/[^a-z0-9]+/gi, "_").slice(0, 32)}.png`);
97
- const r = await runHover({ url, selector: sel, out, flags: parseFlags(argv.slice(6)) });
98
- console.log(JSON.stringify(r, null, 2));
99
- break;
100
- }
101
- case "sweep": {
102
- const planPath = readArg(a);
103
- if (!planPath) die("sweep: missing <plan.json> (or @file)");
104
- const outDirArg = (b && !b.startsWith("--")) ? b : undefined;
105
- const r = await runSweep(planPath, outDirArg);
106
- console.log(JSON.stringify(r, null, 2));
107
- break;
108
- }
109
- case "every-viewport": {
110
- if (!url) die("missing url");
111
- const outDir = (b && !b.startsWith("--")) ? b : undefined;
112
- const viewports = c?.startsWith("--") ? undefined : c?.split(",");
113
- const r = await runEveryViewport({ url, outDir, viewports });
114
- console.log(JSON.stringify(r, null, 2));
115
- break;
116
- }
117
- case "audit": {
118
- if (!url) die("missing url");
119
- const tags = (b && !b.startsWith("--")) ? b : undefined;
120
- const outDir = (c && !c.startsWith("--")) ? c : undefined;
121
- const r = await runAudit({ url, tags: tags?.split(",").map(t => t.trim()), outDir });
122
- console.log(JSON.stringify(r, null, 2));
123
- break;
124
- }
125
- case "dom-snapshot": case "dom": {
126
- if (!url) die("missing url");
127
- const out = (b && !b.startsWith("--")) ? b : undefined;
128
- const r = await captureDomSnapshot({ url, out });
129
- console.log(JSON.stringify({ url: r.url, title: r.title, elements: r.selectorMap?.length, domSize: r.dom?.length, out: r.url?.replace(/[^/]+$/, "") + "dom.json" }, null, 2));
130
- break;
131
- }
132
- case "validate": {
133
- const planPath = readArg(a);
134
- if (!planPath) die("validate: missing <plan.json> (or @file)");
135
- let plan;
136
- try { plan = JSON.parse(readFile(planPath, "utf8")); }
137
- catch (e) { die("validate: " + e.message); }
138
- const { validateSweepPlan } = await import("../src/sweep-schema.js");
139
- const v = validateSweepPlan(plan);
140
- console.log(JSON.stringify({ valid: v.valid, errors: v.errors, plan: planPath }, null, 2));
141
- if (!v.valid) process.exit(1);
142
- break;
143
- }
144
- case "baseline": {
145
- // pursor baseline <sub> [...args]
146
- // sub=list -> list baselines
147
- // sub=save <project> <png> <step> [--id <id>] [--url <u>] [--meta-json <file>]
148
- // sub=approve <project> <png> <step> [--id <id>] [--url <u>]
149
- // sub=show <project> <step> [--id <id>] [--url <u>]
150
- const sub = a;
151
- const { saveBaseline, listBaselines, loadBaseline, approveBaseline, diffKey } = await import("../src/baseline.js");
152
- if (sub === "list") {
153
- // baseline list [project]
154
- const project = b;
155
- console.log(JSON.stringify(listBaselines(project), null, 2));
156
- } else if (sub === "save") {
157
- if (!b || !c || !d) die("baseline save: <project> <png> <step> [--id <id>] [--url <u>] [--meta-json <file>]");
158
- const project = b, png = c, step = d;
159
- const flags = parseFlags(argv.slice(7));
160
- let meta = null;
161
- if (flags["meta-json"]) meta = JSON.parse(readFile(flags["meta-json"], "utf8"));
162
- else if (flags.url) meta = { url: flags.url };
163
- const id = flags.id || diffKey({ url: meta?.url || "", viewport: meta?.viewport, flags: meta?.flags || {} });
164
- const result = saveBaseline({ project, id, step, png, meta });
165
- console.log(JSON.stringify({ saved: true, ...result }, null, 2));
166
- } else if (sub === "approve") {
167
- if (!b || !c || !d) die("baseline approve: <project> <png> <step> [--id <id>] [--url <u>]");
168
- const project = b, png = c, step = d;
169
- const flags = parseFlags(argv.slice(7));
170
- const id = flags.id || diffKey({ url: flags.url || "", flags: {} });
171
- const result = approveBaseline({ project, id, step, fromPng: png });
172
- console.log(JSON.stringify({ approved: true, ...result }, null, 2));
173
- } else if (sub === "show") {
174
- if (!b || !c) die("baseline show: <project> <step> [--id <id>] [--url <u>]");
175
- const project = b, step = c;
176
- const flags = parseFlags(argv.slice(5));
177
- const id = flags.id || diffKey({ url: flags.url || "", flags: {} });
178
- const r = loadBaseline({ project, id, step });
179
- console.log(JSON.stringify(r, null, 2));
180
- } else {
181
- die("baseline subcommand: list | save | approve | show");
182
- }
183
- break;
184
- }
185
- default: { die(`unknown subcommand: ${cmd}`); }
186
- }
187
- } catch (e) {
188
- console.error(JSON.stringify({ error: e.message, stack: e.stack?.split("\n").slice(0, 3).join("\n") }, null, 2));
189
- process.exit(1);
190
- }
1
+ #!/usr/bin/env node
2
+ // pursor CLI. Thin wrapper around src/* that mirrors the npm bin.
3
+
4
+ import { VERSION } from "../src/index.js";
5
+ import { runClick, runType, runWait, runSeq } from "../src/interact.js";
6
+ import { runEval } from "../src/eval.js";
7
+ import { runProbe } from "../src/probe.js";
8
+ import { runShot } from "../src/shot.js";
9
+ import { runShootWithSidecar } from "../src/shoot.js";
10
+ import { runHover } from "../src/hover.js";
11
+ import { runFrames } from "../src/frames.js";
12
+ import { runDiff } from "../src/diff.js";
13
+ import { runSweep } from "../src/sweep.js";
14
+ import { runEveryViewport } from "../src/every-viewport.js";
15
+ import { runAudit } from "../src/plugin-audit.js";
16
+ import { captureDomSnapshot } from "../src/dom-snapshot.js";
17
+ import { listViewports } from "../src/viewport.js";
18
+ import { parseFlags, asNum, readArg, makeOut, pickOutPath } from "../src/util.js";
19
+ import { writeFileSync } from "node:fs";
20
+ import { readFileSync as _readFileSync } from "node:fs";
21
+ const readFile = _readFileSync;
22
+ import { loadPlugins, listPlugins, getFlagHelp } from "../src/plugin.js";
23
+
24
+ const USAGE = `usage:
25
+ v1: pursor {probe|shot|full|eval|click|type|wait|diff|seq} <url> [...]
26
+ v2: pursor {viewports|shoot|layer|frames|hover|sweep} <...>
27
+ flags: --preset <name> --width N --height N --dpr N
28
+ --zoom 1.5 --panX 200 --panY -100
29
+ --cursor pointer|grab|grabbing|crosshair|none
30
+ --layer entity|terrain|hud|ui
31
+ --grid --grid-tile 64 --grid-color rgba(255,0,255,0.35)
32
+ --no-animation --wait-frame 600 --full
33
+ @file prefix reads argv contents from file (UTF-8, newline trimmed).
34
+ plugins: pursor automatically loads built-in plugins from plugins/.
35
+ You can also pass --plugin <path> to load custom plugins (repeatable).`;
36
+
37
+ function die(msg, code = 2) {
38
+ console.error(JSON.stringify({ error: msg, usage: USAGE }, null, 2));
39
+ process.exit(code);
40
+ }
41
+
42
+ const argv = process.argv;
43
+ const [, , cmd, a, b, c, d] = argv;
44
+ const url = process.env.PURSOR_URL || a;
45
+
46
+ // Plugin loading: scan for --plugin <path> and built-in plugins/
47
+ const pluginPaths = [];
48
+ for (let i = 0; i < argv.length; i++) if (argv[i] === "--plugin" && i + 1 < argv.length) pluginPaths.push(argv[++i]);
49
+ await loadPlugins(pluginPaths);
50
+
51
+ (async () => {
52
+ try {
53
+ switch (cmd) {
54
+ case undefined: case "help": case "--help": case "-h": { console.log(JSON.stringify({ usage: USAGE }, null, 2)); break; }
55
+ case "version": case "--version": case "-v": {
56
+ console.log(JSON.stringify({ name: "pursor", version: VERSION, plugins: listPlugins() }, null, 2));
57
+ break;
58
+ }
59
+ case "probe": { if (!url) die("missing url"); const r = await runProbe(url); console.log(JSON.stringify(r, null, 2)); break; }
60
+ case "shot": { if (!url) die("missing url"); const out = b || makeOut("shot.png"); const r = await runShot(url, out, { fullPage: false }); console.log(JSON.stringify(r, null, 2)); break; }
61
+ case "full": { if (!url) die("missing url"); const out = b || makeOut("full.png"); const r = await runShot(url, out, { fullPage: true }); console.log(JSON.stringify(r, null, 2)); break; }
62
+ case "eval": { if (!url) die("missing url"); const js = readArg(b); if (!js) die("eval: missing <js> (or @file)"); const out = c || makeOut("eval.png"); const r = await runEval(url, js, out); console.log(JSON.stringify(r, null, 2)); break; }
63
+ case "click": { if (!url) die("missing url"); const sel = b; if (!sel) die("click: missing <selector>"); const out = c || makeOut(`click-${(sel||"").replace(/[^a-z0-9]+/gi, "_").slice(0, 32)}.png`); const r = await runClick(url, sel, out); console.log(JSON.stringify(r, null, 2)); break; }
64
+ case "type": { if (!url) die("missing url"); const sel = b; const text = readArg(c); if (!sel || text === undefined) die("type: missing <selector> or <text> (text can be @file)"); const out = d || makeOut(`type-${(sel||"").replace(/[^a-z0-9]+/gi, "_").slice(0, 32)}.png`); const r = await runType(url, sel, text, out); console.log(JSON.stringify(r, null, 2)); break; }
65
+ case "wait": { if (!url) die("missing url"); const sel = b; if (!sel) die("wait: missing <selector>"); const t = c !== undefined ? asNum(c, 30000) : 30000; const r = await runWait(url, sel, t); console.log(JSON.stringify(r, null, 2)); break; }
66
+ case "diff": { if (!url) die("missing url"); const ref = b; if (!ref) die("diff: missing <ref.png>"); const out = c || makeOut("diff.png"); const threshold = d !== undefined ? Number(d) : 0.1; const r = await runDiff(url, ref, out, threshold); console.log(JSON.stringify(r, null, 2)); break; }
67
+ 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; }
68
+ case "viewports": { console.log(JSON.stringify(listViewports(), null, 2)); break; }
69
+ case "shoot": {
70
+ if (!url) die("missing url");
71
+ const out = (b && !b.startsWith("--")) ? b : pickOutPath(argv.slice(5)) || makeOut("shoot.png");
72
+ const r = await runShootWithSidecar({ url, out, flags: parseFlags(argv.slice(5)) });
73
+ console.log(JSON.stringify(r, null, 2));
74
+ break;
75
+ }
76
+ case "layer": {
77
+ if (!url) die("missing url");
78
+ const layerName = b; if (!layerName) die("layer: missing <name>");
79
+ const out = (c && !c.startsWith("--")) ? c : pickOutPath(argv.slice(6)) || makeOut(`layer-${layerName}.png`);
80
+ const flags = parseFlags(argv.slice(7)); flags.layer = layerName;
81
+ const r = await runShootWithSidecar({ url, out, flags });
82
+ console.log(JSON.stringify(r, null, 2));
83
+ break;
84
+ }
85
+ case "frames": {
86
+ if (!url) die("missing url");
87
+ const count = asNum(b, 8);
88
+ const stepMs = asNum(c, 250);
89
+ const outDir = (d && !d.startsWith("--")) ? d : makeOut(`frames-${count}x${stepMs}ms`);
90
+ const r = await runFrames({ url, count, intervalMs: stepMs, outDir, flags: parseFlags(argv.slice(7)) });
91
+ console.log(JSON.stringify(r, null, 2));
92
+ break;
93
+ }
94
+ case "hover": {
95
+ if (!url) die("missing url");
96
+ const sel = b; if (!sel) die("hover: missing <selector>");
97
+ const out = (c && !c.startsWith("--")) ? c : pickOutPath(argv.slice(6)) || makeOut(`hover-${(sel||"").replace(/[^a-z0-9]+/gi, "_").slice(0, 32)}.png`);
98
+ const r = await runHover({ url, selector: sel, out, flags: parseFlags(argv.slice(6)) });
99
+ console.log(JSON.stringify(r, null, 2));
100
+ break;
101
+ }
102
+ case "sweep": {
103
+ const planPath = readArg(a);
104
+ if (!planPath) die("sweep: missing <plan.json> (or @file)");
105
+ const outDirArg = (b && !b.startsWith("--")) ? b : undefined;
106
+ const r = await runSweep(planPath, outDirArg);
107
+ console.log(JSON.stringify(r, null, 2));
108
+ break;
109
+ }
110
+ case "every-viewport": {
111
+ if (!url) die("missing url");
112
+ const outDir = (b && !b.startsWith("--")) ? b : undefined;
113
+ const viewports = c?.startsWith("--") ? undefined : c?.split(",");
114
+ const r = await runEveryViewport({ url, outDir, viewports });
115
+ console.log(JSON.stringify(r, null, 2));
116
+ break;
117
+ }
118
+ case "audit": {
119
+ if (!url) die("missing url");
120
+ const tags = (b && !b.startsWith("--")) ? b : undefined;
121
+ const outDir = (c && !c.startsWith("--")) ? c : undefined;
122
+ const r = await runAudit({ url, tags: tags?.split(",").map(t => t.trim()), outDir });
123
+ console.log(JSON.stringify(r, null, 2));
124
+ break;
125
+ }
126
+ case "dom-snapshot": case "dom": {
127
+ if (!url) die("missing url");
128
+ const out = (b && !b.startsWith("--")) ? b : undefined;
129
+ const r = await captureDomSnapshot({ url, out });
130
+ console.log(JSON.stringify({ url: r.url, title: r.title, elements: r.selectorMap?.length, domSize: r.dom?.length, out: r.url?.replace(/[^/]+$/, "") + "dom.json" }, null, 2));
131
+ break;
132
+ }
133
+ case "validate": {
134
+ const planPath = readArg(a);
135
+ if (!planPath) die("validate: missing <plan.json> (or @file)");
136
+ let plan;
137
+ try { plan = JSON.parse(readFile(planPath, "utf8")); }
138
+ catch (e) { die("validate: " + e.message); }
139
+ const { validateSweepPlan } = await import("../src/sweep-schema.js");
140
+ const v = validateSweepPlan(plan);
141
+ console.log(JSON.stringify({ valid: v.valid, errors: v.errors, plan: planPath }, null, 2));
142
+ if (!v.valid) process.exit(1);
143
+ break;
144
+ }
145
+ case "baseline": {
146
+ // pursor baseline <sub> [...args]
147
+ // sub=list -> list baselines
148
+ // sub=save <project> <png> <step> [--id <id>] [--url <u>] [--meta-json <file>]
149
+ // sub=approve <project> <png> <step> [--id <id>] [--url <u>]
150
+ // sub=show <project> <step> [--id <id>] [--url <u>]
151
+ const sub = a;
152
+ const { saveBaseline, listBaselines, loadBaseline, approveBaseline, diffKey } = await import("../src/baseline.js");
153
+ if (sub === "list") {
154
+ // baseline list [project]
155
+ const project = b;
156
+ console.log(JSON.stringify(listBaselines(project), null, 2));
157
+ } else if (sub === "save") {
158
+ if (!b || !c || !d) die("baseline save: <project> <png> <step> [--id <id>] [--url <u>] [--meta-json <file>]");
159
+ const project = b, png = c, step = d;
160
+ const flags = parseFlags(argv.slice(7));
161
+ let meta = null;
162
+ if (flags["meta-json"]) meta = JSON.parse(readFile(flags["meta-json"], "utf8"));
163
+ else if (flags.url) meta = { url: flags.url };
164
+ const id = flags.id || diffKey({ url: meta?.url || "", viewport: meta?.viewport, flags: meta?.flags || {} });
165
+ const result = saveBaseline({ project, id, step, png, meta });
166
+ console.log(JSON.stringify({ saved: true, ...result }, null, 2));
167
+ } else if (sub === "approve") {
168
+ if (!b || !c || !d) die("baseline approve: <project> <png> <step> [--id <id>] [--url <u>]");
169
+ const project = b, png = c, step = d;
170
+ const flags = parseFlags(argv.slice(7));
171
+ const id = flags.id || diffKey({ url: flags.url || "", flags: {} });
172
+ const result = approveBaseline({ project, id, step, fromPng: png });
173
+ console.log(JSON.stringify({ approved: true, ...result }, null, 2));
174
+ } else if (sub === "show") {
175
+ if (!b || !c) die("baseline show: <project> <step> [--id <id>] [--url <u>]");
176
+ const project = b, step = c;
177
+ const flags = parseFlags(argv.slice(5));
178
+ const id = flags.id || diffKey({ url: flags.url || "", flags: {} });
179
+ const r = loadBaseline({ project, id, step });
180
+ console.log(JSON.stringify(r, null, 2));
181
+ } else {
182
+ die("baseline subcommand: list | save | approve | show");
183
+ }
184
+ break;
185
+ }
186
+ case "auth": {
187
+ // pursor auth <sub> [...args]
188
+ // save <project> <name> --from <state.json>
189
+ // load <project> <name> --out <state.json>
190
+ // list [project]
191
+ // delete <project> <name>
192
+ const sub = a;
193
+ const { saveAuthState, loadAuthState, listAuthStates, deleteAuthState } = await import("../src/auth.js");
194
+ if (sub === "list") {
195
+ const project = b;
196
+ console.log(JSON.stringify(listAuthStates(project), null, 2));
197
+ } else if (sub === "save") {
198
+ if (!b || !c) die("auth save: <project> <name> --from <state.json>");
199
+ const fromFile = argv[argv.indexOf("--from") + 1];
200
+ if (!fromFile) die("auth save: missing --from <state.json>");
201
+ const state = JSON.parse(readFile(fromFile, "utf8"));
202
+ const r = saveAuthState({ project: b, name: c, state });
203
+ console.log(JSON.stringify({ saved: true, ...r }, null, 2));
204
+ } else if (sub === "load") {
205
+ if (!b || !c) die("auth load: <project> <name> --out <state.json>");
206
+ const outFile = argv[argv.indexOf("--out") + 1];
207
+ if (!outFile) die("auth load: missing --out <state.json>");
208
+ const state = loadAuthState({ project: b, name: c });
209
+ if (!state) { console.error("not found"); process.exit(2); }
210
+ writeFileSync(outFile, JSON.stringify(state, null, 2), "utf8");
211
+ console.log(JSON.stringify({ loaded: true, file: outFile, cookies: state.cookies.length, origins: state.origins.length }, null, 2));
212
+ } else if (sub === "delete") {
213
+ if (!b || !c) die("auth delete: <project> <name>");
214
+ const ok = deleteAuthState({ project: b, name: c });
215
+ console.log(JSON.stringify({ deleted: ok }, null, 2));
216
+ } else {
217
+ die("auth subcommand: list | save | load | delete");
218
+ }
219
+ break;
220
+ }
221
+ default: { die(`unknown subcommand: ${cmd}`); }
222
+ }
223
+ } catch (e) {
224
+ console.error(JSON.stringify({ error: e.message, stack: e.stack?.split("\n").slice(0, 3).join("\n") }, null, 2));
225
+ process.exit(1);
226
+ }
191
227
  })();
package/package.json CHANGED
@@ -1,73 +1,75 @@
1
- {
2
- "name": "pursor",
3
- "version": "0.2.0",
4
- "private": false,
5
- "description": "Visual QA & audit & MCP for the browser. Capture, sweep, and review any web target with multi-viewport, layered, animated, hover, grid, and cursor states.",
6
- "type": "module",
7
- "bin": {
8
- "pursor": "./bin/pursor.mjs",
9
- "pursor-mcp": "./bin/pursor-mcp.mjs"
10
- },
11
- "main": "./src/index.js",
12
- "exports": {
13
- ".": "./src/index.js",
14
- "./plugin": "./src/plugin.js",
15
- "./plugins/*": "./plugins/*.js",
16
- "./util": "./src/util.js",
17
- "./runway": "./src/runway.js",
18
- "./selector": "./src/selector.js",
19
- "./overlays": "./src/overlays.js",
20
- "./viewport": "./src/viewport.js",
21
- "./dom-snapshot": "./src/dom-snapshot.js",
22
- "./plugin-audit": "./src/plugin-audit.js",
23
- "./selector-heal": "./src/selector-heal.js",
24
- "./ci-output": "./src/ci-output.js",
25
- "./mcp": "./src/mcp.js",
26
- "./baseline": "./src/baseline.js",
27
- "./sweep-schema": "./src/sweep-schema.js",
28
- "./mcp-resources": "./src/mcp-resources.js"
29
- },
30
- "files": [
31
- "bin",
32
- "src",
33
- "plugins",
34
- "plans",
35
- "README.md",
36
- "LICENSE"
37
- ],
38
- "scripts": {
39
- "start": "node bin/pursor.mjs",
40
- "test": "node --test \"test/*.test.js\"",
41
- "smoke": "node bin/pursor.mjs viewports"
42
- },
43
- "engines": {
44
- "node": ">=18"
45
- },
46
- "keywords": [
47
- "visual-qa",
48
- "visual-regression",
49
- "screenshot",
50
- "audit",
51
- "isometric",
52
- "playwright",
53
- "testing",
54
- "devtools"
55
- ],
56
- "license": "MIT",
57
- "dependencies": {
58
- "axe-core": "^4.12.1",
59
- "pixelmatch": "^5.3.0",
60
- "pngjs": "^7.0.0"
61
- },
62
- "peerDependencies": {
63
- "playwright-core": "*"
64
- },
65
- "peerDependenciesMeta": {
66
- "playwright-core": {
67
- "optional": true
68
- }
69
- },
70
- "devDependencies": {
71
- "playwright-core": "^1.61.0"
72
- }
73
- }
1
+ {
2
+ "name": "pursor",
3
+ "version": "0.3.0",
4
+ "private": false,
5
+ "description": "Visual QA & audit & MCP for the browser. Capture, sweep, and review any web target with multi-viewport, layered, animated, hover, grid, and cursor states.",
6
+ "type": "module",
7
+ "bin": {
8
+ "pursor": "./bin/pursor.mjs",
9
+ "pursor-mcp": "./bin/pursor-mcp.mjs"
10
+ },
11
+ "main": "./src/index.js",
12
+ "exports": {
13
+ ".": "./src/index.js",
14
+ "./plugin": "./src/plugin.js",
15
+ "./plugins/*": "./plugins/*.js",
16
+ "./util": "./src/util.js",
17
+ "./runway": "./src/runway.js",
18
+ "./selector": "./src/selector.js",
19
+ "./overlays": "./src/overlays.js",
20
+ "./viewport": "./src/viewport.js",
21
+ "./dom-snapshot": "./src/dom-snapshot.js",
22
+ "./plugin-audit": "./src/plugin-audit.js",
23
+ "./selector-heal": "./src/selector-heal.js",
24
+ "./ci-output": "./src/ci-output.js",
25
+ "./mcp": "./src/mcp.js",
26
+ "./baseline": "./src/baseline.js",
27
+ "./sweep-schema": "./src/sweep-schema.js",
28
+ "./mcp-resources": "./src/mcp-resources.js",
29
+ "./har": "./src/har.js",
30
+ "./auth": "./src/auth.js"
31
+ },
32
+ "files": [
33
+ "bin",
34
+ "src",
35
+ "plugins",
36
+ "plans",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "scripts": {
41
+ "start": "node bin/pursor.mjs",
42
+ "test": "node --test \"test/*.test.js\"",
43
+ "smoke": "node bin/pursor.mjs viewports"
44
+ },
45
+ "engines": {
46
+ "node": ">=18"
47
+ },
48
+ "keywords": [
49
+ "visual-qa",
50
+ "visual-regression",
51
+ "screenshot",
52
+ "audit",
53
+ "isometric",
54
+ "playwright",
55
+ "testing",
56
+ "devtools"
57
+ ],
58
+ "license": "MIT",
59
+ "dependencies": {
60
+ "axe-core": "^4.12.1",
61
+ "pixelmatch": "^5.3.0",
62
+ "pngjs": "^7.0.0"
63
+ },
64
+ "peerDependencies": {
65
+ "playwright-core": "*"
66
+ },
67
+ "peerDependenciesMeta": {
68
+ "playwright-core": {
69
+ "optional": true
70
+ }
71
+ },
72
+ "devDependencies": {
73
+ "playwright-core": "^1.61.0"
74
+ }
75
+ }