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/src/har.js CHANGED
@@ -1,159 +1,159 @@
1
- // pursor — HAR (HTTP Archive) capture.
2
- //
3
- // Hooks page.on("request") / page.on("response") / page.on("requestfailed")
4
- // on an active page and produces a HAR 1.2 spec blob:
5
- // { log: { version: "1.2", creator: {...}, browser: {...}, pages: [...], entries: [...] } }
6
- //
7
- // Use in code:
8
- // const { startHarCapture, stopHarCapture } = await import("pursor/har");
9
- // const har = await startHarCapture(page);
10
- // await page.goto(url);
11
- // await stopHarCapture(page); // returns the HAR object
12
- //
13
- // Or via CLI/sweep:
14
- // pursor shoot <url> --har ./out/req.har.json
15
- //
16
- // HAR is useful for:
17
- // - killing flakiness from analytics/ads/CDN by mocking responses later
18
- // - inspecting what the page actually fetched during a capture
19
- // - regression diffing along with visual diffs
20
-
21
- import { writeFileSync, mkdirSync } from "node:fs";
22
- import { dirname } from "node:path";
23
- import { nowIso } from "./util.js";
24
-
25
- const SYM = Symbol.for("pursor.har.capture");
26
-
27
- function makeEntry(req, resp, startTs, endTs) {
28
- const url = req.url();
29
- const u = new URL(url);
30
- const headersList = (obj) => {
31
- if (!obj) return [];
32
- try {
33
- const out = [];
34
- for (const [name, value] of Object.entries(obj)) {
35
- if (Array.isArray(value)) {
36
- for (const v of value) out.push({ name, value: String(v) });
37
- } else {
38
- out.push({ name, value: String(value) });
39
- }
40
- }
41
- return out;
42
- } catch { return []; }
43
- };
44
- const queryString = u.search ? u.search.slice(1).split("&").filter(Boolean).map(kv => {
45
- const [k, v = ""] = kv.split("=");
46
- return { name: decodeURIComponent(k), value: decodeURIComponent(v) };
47
- }) : [];
48
- const postData = req.postData() ? { mimeType: req.postData() || "application/octet-stream", text: req.postData() } : undefined;
49
- const entry = {
50
- pageref: req.frame()?.url?.() || "_top",
51
- startedDateTime: new Date(startTs).toISOString(),
52
- time: Math.max(0, endTs - startTs),
53
- request: {
54
- method: req.method(),
55
- url,
56
- httpVersion: "HTTP/1.1",
57
- cookies: [],
58
- headers: headersList(req.headers()),
59
- queryString,
60
- headersSize: -1,
61
- bodySize: postData?.text?.length || 0,
62
- postData,
63
- },
64
- response: resp ? {
65
- status: resp.status(),
66
- statusText: resp.statusText() || "",
67
- httpVersion: "HTTP/1.1",
68
- cookies: [],
69
- headers: headersList(resp.headers()),
70
- content: { size: -1, mimeType: resp.headers()?.["content-type"] || "" },
71
- redirectURL: resp.headers()?.location || "",
72
- headersSize: -1,
73
- bodySize: -1,
74
- } : { status: 0, statusText: "Failed", httpVersion: "HTTP/1.1", cookies: [], headers: [], content: { size: 0, mimeType: "" }, redirectURL: "", headersSize: -1, bodySize: -1 },
75
- cache: {},
76
- timings: { send: 0, wait: Math.max(0, endTs - startTs), receive: 0 },
77
- serverIPAddress: "",
78
- connection: "",
79
- };
80
- return entry;
81
- }
82
-
83
- export async function startHarCapture(page, opts = {}) {
84
- if (!page) throw new Error("startHarCapture: page required");
85
- if (page[SYM]) return page[SYM]; // idempotent
86
- const started = Date.now();
87
- const entries = [];
88
- const pending = new Map(); // req -> startTs
89
- const state = {
90
- started,
91
- creator: { name: "pursor", version: opts.version || "0.3.0" },
92
- browser: { name: "chromium", version: "playwright-core" },
93
- pages: [],
94
- entries,
95
- pending,
96
- };
97
- const onReq = (req) => {
98
- pending.set(req, Date.now());
99
- };
100
- const onResp = async (resp) => {
101
- try {
102
- const req = resp.request();
103
- const startTs = pending.get(req) || Date.now();
104
- pending.delete(req);
105
- const endTs = Date.now();
106
- entries.push(makeEntry(req, resp, startTs, endTs));
107
- } catch {}
108
- };
109
- const onFailed = (req) => {
110
- try {
111
- const startTs = pending.get(req) || Date.now();
112
- pending.delete(req);
113
- entries.push(makeEntry(req, null, startTs, Date.now()));
114
- } catch {}
115
- };
116
- page.on("request", onReq);
117
- page.on("response", onResp);
118
- page.on("requestfailed", onFailed);
119
- state._teardown = () => {
120
- try { page.off("request", onReq); } catch {}
121
- try { page.off("response", onResp); } catch {}
122
- try { page.off("requestfailed", onFailed); } catch {}
123
- };
124
- page[SYM] = state;
125
- return state;
126
- }
127
-
128
- export function stopHarCapture(page) {
129
- if (!page || !page[SYM]) return null;
130
- const state = page[SYM];
131
- try { state._teardown?.(); } catch {}
132
- delete page[SYM];
133
- return finalizeHar(state);
134
- }
135
-
136
- export function finalizeHar(state) {
137
- if (!state) return null;
138
- return {
139
- log: {
140
- version: "1.2",
141
- creator: state.creator,
142
- browser: state.browser,
143
- pages: state.pages,
144
- entries: state.entries,
145
- },
146
- _meta: {
147
- started: state.started,
148
- finished: Date.now(),
149
- entryCount: state.entries.length,
150
- },
151
- };
152
- }
153
-
154
- export async function writeHar(har, file) {
155
- if (!har || !file) return null;
156
- mkdirSync(dirname(file), { recursive: true });
157
- writeFileSync(file, JSON.stringify(har, null, 2), "utf8");
158
- return file;
1
+ // pursr — HAR (HTTP Archive) capture.
2
+ //
3
+ // Hooks page.on("request") / page.on("response") / page.on("requestfailed")
4
+ // on an active page and produces a HAR 1.2 spec blob:
5
+ // { log: { version: "1.2", creator: {...}, browser: {...}, pages: [...], entries: [...] } }
6
+ //
7
+ // Use in code:
8
+ // const { startHarCapture, stopHarCapture } = await import("pursr/har");
9
+ // const har = await startHarCapture(page);
10
+ // await page.goto(url);
11
+ // await stopHarCapture(page); // returns the HAR object
12
+ //
13
+ // Or via CLI/sweep:
14
+ // pursr shoot <url> --har ./out/req.har.json
15
+ //
16
+ // HAR is useful for:
17
+ // pursr - killing flakiness from analytics/ads/CDN by mocking responses later
18
+ // pursr - inspecting what the page actually fetched during a capture
19
+ // pursr - regression diffing along with visual diffs
20
+
21
+ import { writeFileSync, mkdirSync } from "node:fs";
22
+ import { dirname } from "node:path";
23
+ import { nowIso } from "./util.js";
24
+
25
+ const SYM = Symbol.for("pursr.har.capture");
26
+
27
+ function makeEntry(req, resp, startTs, endTs) {
28
+ const url = req.url();
29
+ const u = new URL(url);
30
+ const headersList = (obj) => {
31
+ if (!obj) return [];
32
+ try {
33
+ const out = [];
34
+ for (const [name, value] of Object.entries(obj)) {
35
+ if (Array.isArray(value)) {
36
+ for (const v of value) out.push({ name, value: String(v) });
37
+ } else {
38
+ out.push({ name, value: String(value) });
39
+ }
40
+ }
41
+ return out;
42
+ } catch { return []; }
43
+ };
44
+ const queryString = u.search ? u.search.slice(1).split("&").filter(Boolean).map(kv => {
45
+ const [k, v = ""] = kv.split("=");
46
+ return { name: decodeURIComponent(k), value: decodeURIComponent(v) };
47
+ }) : [];
48
+ const postData = req.postData() ? { mimeType: req.postData() || "application/octet-stream", text: req.postData() } : undefined;
49
+ const entry = {
50
+ pageref: req.frame()?.url?.() || "_top",
51
+ startedDateTime: new Date(startTs).toISOString(),
52
+ time: Math.max(0, endTs - startTs),
53
+ request: {
54
+ method: req.method(),
55
+ url,
56
+ httpVersion: "HTTP/1.1",
57
+ cookies: [],
58
+ headers: headersList(req.headers()),
59
+ queryString,
60
+ headersSize: -1,
61
+ bodySize: postData?.text?.length || 0,
62
+ postData,
63
+ },
64
+ response: resp ? {
65
+ status: resp.status(),
66
+ statusText: resp.statusText() || "",
67
+ httpVersion: "HTTP/1.1",
68
+ cookies: [],
69
+ headers: headersList(resp.headers()),
70
+ content: { size: -1, mimeType: resp.headers()?.["content-type"] || "" },
71
+ redirectURL: resp.headers()?.location || "",
72
+ headersSize: -1,
73
+ bodySize: -1,
74
+ } : { status: 0, statusText: "Failed", httpVersion: "HTTP/1.1", cookies: [], headers: [], content: { size: 0, mimeType: "" }, redirectURL: "", headersSize: -1, bodySize: -1 },
75
+ cache: {},
76
+ timings: { send: 0, wait: Math.max(0, endTs - startTs), receive: 0 },
77
+ serverIPAddress: "",
78
+ connection: "",
79
+ };
80
+ return entry;
81
+ }
82
+
83
+ export async function startHarCapture(page, opts = {}) {
84
+ if (!page) throw new Error("startHarCapture: page required");
85
+ if (page[SYM]) return page[SYM]; // idempotent
86
+ const started = Date.now();
87
+ const entries = [];
88
+ const pending = new Map(); // req -> startTs
89
+ const state = {
90
+ started,
91
+ creator: { name: "pursr", version: opts.version || "0.3.0" },
92
+ browser: { name: "chromium", version: "playwright-core" },
93
+ pages: [],
94
+ entries,
95
+ pending,
96
+ };
97
+ const onReq = (req) => {
98
+ pending.set(req, Date.now());
99
+ };
100
+ const onResp = async (resp) => {
101
+ try {
102
+ const req = resp.request();
103
+ const startTs = pending.get(req) || Date.now();
104
+ pending.delete(req);
105
+ const endTs = Date.now();
106
+ entries.push(makeEntry(req, resp, startTs, endTs));
107
+ } catch {}
108
+ };
109
+ const onFailed = (req) => {
110
+ try {
111
+ const startTs = pending.get(req) || Date.now();
112
+ pending.delete(req);
113
+ entries.push(makeEntry(req, null, startTs, Date.now()));
114
+ } catch {}
115
+ };
116
+ page.on("request", onReq);
117
+ page.on("response", onResp);
118
+ page.on("requestfailed", onFailed);
119
+ state._teardown = () => {
120
+ try { page.off("request", onReq); } catch {}
121
+ try { page.off("response", onResp); } catch {}
122
+ try { page.off("requestfailed", onFailed); } catch {}
123
+ };
124
+ page[SYM] = state;
125
+ return state;
126
+ }
127
+
128
+ export function stopHarCapture(page) {
129
+ if (!page || !page[SYM]) return null;
130
+ const state = page[SYM];
131
+ try { state._teardown?.(); } catch {}
132
+ delete page[SYM];
133
+ return finalizeHar(state);
134
+ }
135
+
136
+ export function finalizeHar(state) {
137
+ if (!state) return null;
138
+ return {
139
+ log: {
140
+ version: "1.2",
141
+ creator: state.creator,
142
+ browser: state.browser,
143
+ pages: state.pages,
144
+ entries: state.entries,
145
+ },
146
+ _meta: {
147
+ started: state.started,
148
+ finished: Date.now(),
149
+ entryCount: state.entries.length,
150
+ },
151
+ };
152
+ }
153
+
154
+ export async function writeHar(har, file) {
155
+ if (!har || !file) return null;
156
+ mkdirSync(dirname(file), { recursive: true });
157
+ writeFileSync(file, JSON.stringify(har, null, 2), "utf8");
158
+ return file;
159
159
  }
package/src/hover.js CHANGED
@@ -1,26 +1,26 @@
1
- // Hover capture: navigate, hover a selector, screenshot.
2
-
3
- import { launch, newPage } from "./runway.js";
4
- import { resolveViewport } from "./viewport.js";
5
- import { gotoOrThrow, settle, CLICK_TIMEOUT_MS } from "./overlays.js";
6
- import { resolveLocator } from "./selector.js";
7
- import { asNum, asBool, nowIso, writeSidecar, requireArg } from "./util.js";
8
-
9
- export async function runHover({ url, selector, out, flags = {} }) {
10
- requireArg("url", url, "string");
11
- requireArg("selector", selector, "string");
12
- const viewport = resolveViewport(flags);
13
- const browser = await launch();
14
- try {
15
- const page = await newPage(browser, viewport);
16
- const r = await gotoOrThrow(page, url); await settle(page);
17
- const loc = await resolveLocator(page, selector);
18
- await loc.first().waitFor({ state: "visible", timeout: CLICK_TIMEOUT_MS });
19
- await loc.first().hover({ timeout: CLICK_TIMEOUT_MS });
20
- await page.waitForTimeout(asNum(flags["hover-ms"], 250));
21
- if (out) await page.screenshot({ path: out, fullPage: asBool(flags.full, false) });
22
- const meta = { ...r, url, out, selector, viewport, ts: nowIso() };
23
- if (out) await writeSidecar(meta);
24
- return meta;
25
- } finally { try { await browser.close(); } catch {} }
1
+ // Hover capture: navigate, hover a selector, screenshot.
2
+
3
+ import { launch, newPage } from "./runway.js";
4
+ import { resolveViewport } from "./viewport.js";
5
+ import { gotoOrThrow, settle, CLICK_TIMEOUT_MS } from "./overlays.js";
6
+ import { resolveLocator } from "./selector.js";
7
+ import { asNum, asBool, nowIso, writeSidecar, requireArg } from "./util.js";
8
+
9
+ export async function runHover({ url, selector, out, flags = {} }) {
10
+ requireArg("url", url, "string");
11
+ requireArg("selector", selector, "string");
12
+ const viewport = resolveViewport(flags);
13
+ const browser = await launch();
14
+ try {
15
+ const page = await newPage(browser, viewport);
16
+ const r = await gotoOrThrow(page, url); await settle(page);
17
+ const loc = await resolveLocator(page, selector);
18
+ await loc.first().waitFor({ state: "visible", timeout: CLICK_TIMEOUT_MS });
19
+ await loc.first().hover({ timeout: CLICK_TIMEOUT_MS });
20
+ await page.waitForTimeout(asNum(flags["hover-ms"], 250));
21
+ if (out) await page.screenshot({ path: out, fullPage: asBool(flags.full, false) });
22
+ const meta = { ...r, url, out, selector, viewport, ts: nowIso() };
23
+ if (out) await writeSidecar(meta);
24
+ return meta;
25
+ } finally { try { await browser.close(); } catch {} }
26
26
  }
package/src/index.js CHANGED
@@ -1,8 +1,8 @@
1
- // pursor — public library API.
1
+ // pursr — public library API.
2
2
  //
3
- // This is the entry point for consumers who want to embed pursor
3
+ // This is the entry point for consumers who want to embed pursr
4
4
  // inside their own scripts (instead of using the CLI). The CLI in
5
- // bin/pursor.mjs is a thin wrapper around the same exports.
5
+ // bin/pursr.mjs is a thin wrapper around the same exports.
6
6
  //
7
7
  // All capture / sweep helpers return a `Result` object: the path to the
8
8
  // PNG, a sidecar JSON metadata object, and timing info. They never throw
@@ -32,7 +32,7 @@ import { captureDomSnapshot, captureDomSnapshotSidecar } from "./dom-snapshot.js
32
32
  import { runAudit } from "./plugin-audit.js";
33
33
  import { resolveHealedSelector, healStepAction } from "./selector-heal.js";
34
34
  import { writeCiOutput } from "./ci-output.js";
35
- import { PursorMCPServer, loadConfig as loadMcpConfig, MCP_VERSION } from "./mcp.js";
35
+ import { PursrMCPServer, loadConfig as loadMcpConfig, MCP_VERSION } from "./mcp.js";
36
36
  import { createRequire } from "node:module";
37
37
  import { saveBaseline, loadBaseline, listBaselines, approveBaseline, diffKey, resolveBaselinePath } from "./baseline.js";
38
38
  import { validateSweepPlan, registerSweepOp } from "./sweep-schema.js";
@@ -68,7 +68,7 @@ export {
68
68
  // v3: selector healing, CI output, MCP server
69
69
  resolveHealedSelector, healStepAction,
70
70
  writeCiOutput,
71
- PursorMCPServer, loadMcpConfig, MCP_VERSION,
71
+ PursrMCPServer, loadMcpConfig, MCP_VERSION,
72
72
  // v4: baselines, sweep validation, MCP resources
73
73
  saveBaseline, loadBaseline, listBaselines, approveBaseline, diffKey, resolveBaselinePath,
74
74
  validateSweepPlan, registerSweepOp,
@@ -98,7 +98,7 @@ export default {
98
98
  resolveLocator, parseTextSelector,
99
99
  resolveHealedSelector, healStepAction,
100
100
  writeCiOutput,
101
- PursorMCPServer, loadMcpConfig, MCP_VERSION,
101
+ PursrMCPServer, loadMcpConfig, MCP_VERSION,
102
102
  saveBaseline, loadBaseline, listBaselines, approveBaseline, diffKey, resolveBaselinePath,
103
103
  validateSweepPlan, registerSweepOp,
104
104
  listResources, readResource, recordResource,