@hasna/browser 0.0.9 → 0.1.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.
@@ -291,6 +291,23 @@ function runMigrations(db) {
291
291
  );
292
292
  CREATE INDEX IF NOT EXISTS idx_session_tags_tag ON session_tags(tag);
293
293
  `
294
+ },
295
+ {
296
+ version: 6,
297
+ sql: `
298
+ CREATE TABLE IF NOT EXISTS auth_flows (
299
+ id TEXT PRIMARY KEY,
300
+ name TEXT NOT NULL UNIQUE,
301
+ domain TEXT NOT NULL,
302
+ recording_id TEXT REFERENCES recordings(id),
303
+ storage_state_path TEXT,
304
+ created_at TEXT DEFAULT (datetime('now')),
305
+ last_used TEXT
306
+ );
307
+
308
+ CREATE INDEX IF NOT EXISTS idx_auth_flows_domain ON auth_flows(domain);
309
+ CREATE INDEX IF NOT EXISTS idx_auth_flows_name ON auth_flows(name);
310
+ `
294
311
  }
295
312
  ];
296
313
  for (const m of migrations) {
@@ -339,6 +356,188 @@ var init_console_log = __esm(() => {
339
356
  init_schema();
340
357
  });
341
358
 
359
+ // src/engines/cdp.ts
360
+ var exports_cdp = {};
361
+ __export(exports_cdp, {
362
+ connectToExistingBrowser: () => connectToExistingBrowser,
363
+ CDPClient: () => CDPClient
364
+ });
365
+ async function connectToExistingBrowser(cdpUrl) {
366
+ const { chromium: chromium3 } = await import("playwright");
367
+ try {
368
+ return await chromium3.connectOverCDP(cdpUrl);
369
+ } catch (err) {
370
+ throw new BrowserError(`Failed to connect to browser at ${cdpUrl}: ${err instanceof Error ? err.message : String(err)}. Start Chrome with: google-chrome --remote-debugging-port=9222`, "CDP_CONNECT_FAILED", true);
371
+ }
372
+ }
373
+
374
+ class CDPClient {
375
+ session;
376
+ networkEnabled = false;
377
+ performanceEnabled = false;
378
+ constructor(session) {
379
+ this.session = session;
380
+ }
381
+ static async fromPage(page) {
382
+ try {
383
+ const session = await page.context().newCDPSession(page);
384
+ return new CDPClient(session);
385
+ } catch (err) {
386
+ throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
387
+ }
388
+ }
389
+ async send(method, params) {
390
+ try {
391
+ return await this.session.send(method, params);
392
+ } catch (err) {
393
+ throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
394
+ }
395
+ }
396
+ on(event, handler) {
397
+ this.session.on(event, handler);
398
+ }
399
+ off(event, handler) {
400
+ this.session.off(event, handler);
401
+ }
402
+ async enableNetwork() {
403
+ if (!this.networkEnabled) {
404
+ await this.send("Network.enable");
405
+ this.networkEnabled = true;
406
+ }
407
+ }
408
+ async enablePerformance() {
409
+ if (!this.performanceEnabled) {
410
+ await this.send("Performance.enable");
411
+ this.performanceEnabled = true;
412
+ }
413
+ }
414
+ async getPerformanceMetrics() {
415
+ await this.enablePerformance();
416
+ const result = await this.send("Performance.getMetrics");
417
+ const m = {};
418
+ for (const metric of result.metrics) {
419
+ m[metric.name] = metric.value;
420
+ }
421
+ return {
422
+ js_heap_size_used: m["JSHeapUsedSize"],
423
+ js_heap_size_total: m["JSHeapTotalSize"],
424
+ dom_interactive: m["DOMInteractive"],
425
+ dom_complete: m["DOMComplete"],
426
+ load_event: m["LoadEventEnd"]
427
+ };
428
+ }
429
+ async startJSCoverage() {
430
+ await this.send("Profiler.enable");
431
+ await this.send("Debugger.enable");
432
+ await this.send("Profiler.startPreciseCoverage", {
433
+ callCount: false,
434
+ detailed: true
435
+ });
436
+ }
437
+ async stopJSCoverage() {
438
+ const result = await this.send("Profiler.takePreciseCoverage");
439
+ await this.send("Profiler.stopPreciseCoverage");
440
+ return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
441
+ url: r.url,
442
+ text: "",
443
+ ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
444
+ }));
445
+ }
446
+ async getCoverage() {
447
+ await this.startJSCoverage();
448
+ const js = await this.stopJSCoverage();
449
+ const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
450
+ return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
451
+ }
452
+ async captureHAREntries(page, handler) {
453
+ await this.enableNetwork();
454
+ const requestTimings = new Map;
455
+ const onRequest = (params) => {
456
+ requestTimings.set(params.requestId, params.timestamp);
457
+ };
458
+ const onResponse = (params) => {
459
+ const start = requestTimings.get(params.requestId);
460
+ const duration = start != null ? (params.timestamp - start) * 1000 : 0;
461
+ handler({
462
+ method: "GET",
463
+ url: params.response.url,
464
+ status: params.response.status,
465
+ duration
466
+ });
467
+ };
468
+ this.on("Network.requestWillBeSent", onRequest);
469
+ this.on("Network.responseReceived", onResponse);
470
+ return () => {
471
+ this.off("Network.requestWillBeSent", onRequest);
472
+ this.off("Network.responseReceived", onResponse);
473
+ };
474
+ }
475
+ async detach() {
476
+ try {
477
+ await this.session.detach();
478
+ } catch {}
479
+ }
480
+ }
481
+ var init_cdp = __esm(() => {
482
+ init_types();
483
+ });
484
+
485
+ // src/lib/storage-state.ts
486
+ var exports_storage_state = {};
487
+ __export(exports_storage_state, {
488
+ saveStateFromPage: () => saveStateFromPage,
489
+ saveState: () => saveState,
490
+ loadStatePath: () => loadStatePath,
491
+ listStates: () => listStates,
492
+ deleteState: () => deleteState
493
+ });
494
+ import { mkdirSync as mkdirSync3, existsSync, readdirSync, unlinkSync } from "fs";
495
+ import { join as join3 } from "path";
496
+ import { homedir as homedir3 } from "os";
497
+ function ensureDir() {
498
+ mkdirSync3(STATES_DIR, { recursive: true });
499
+ }
500
+ function statePath(name) {
501
+ return join3(STATES_DIR, `${name}.json`);
502
+ }
503
+ async function saveState(context, name) {
504
+ ensureDir();
505
+ const path = statePath(name);
506
+ const state = await context.storageState({ path });
507
+ return path;
508
+ }
509
+ async function saveStateFromPage(page, name) {
510
+ return saveState(page.context(), name);
511
+ }
512
+ function loadStatePath(name) {
513
+ const path = statePath(name);
514
+ return existsSync(path) ? path : null;
515
+ }
516
+ function listStates() {
517
+ ensureDir();
518
+ return readdirSync(STATES_DIR).filter((f) => f.endsWith(".json")).map((f) => {
519
+ const path = join3(STATES_DIR, f);
520
+ const stat = Bun.file(path);
521
+ return {
522
+ name: f.replace(".json", ""),
523
+ path,
524
+ modified: new Date(stat.lastModified).toISOString()
525
+ };
526
+ }).sort((a, b) => b.modified.localeCompare(a.modified));
527
+ }
528
+ function deleteState(name) {
529
+ const path = statePath(name);
530
+ if (existsSync(path)) {
531
+ unlinkSync(path);
532
+ return true;
533
+ }
534
+ return false;
535
+ }
536
+ var STATES_DIR;
537
+ var init_storage_state = __esm(() => {
538
+ STATES_DIR = join3(process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser"), "states");
539
+ });
540
+
342
541
  // node_modules/sharp/lib/is.js
343
542
  var require_is = __commonJS((exports, module) => {
344
543
  /*!
@@ -6933,8 +7132,8 @@ var init_snapshots = __esm(() => {
6933
7132
  });
6934
7133
 
6935
7134
  // src/server/index.ts
6936
- import { join as join6 } from "path";
6937
- import { existsSync as existsSync3 } from "fs";
7135
+ import { join as join7 } from "path";
7136
+ import { existsSync as existsSync4 } from "fs";
6938
7137
 
6939
7138
  // src/lib/session.ts
6940
7139
  init_types();
@@ -7862,6 +8061,37 @@ function createBunProxy(view) {
7862
8061
  return view;
7863
8062
  }
7864
8063
  async function createSession2(opts = {}) {
8064
+ if (opts.cdpUrl) {
8065
+ const { connectToExistingBrowser: connectToExistingBrowser2 } = await Promise.resolve().then(() => (init_cdp(), exports_cdp));
8066
+ const cdpBrowser = await connectToExistingBrowser2(opts.cdpUrl);
8067
+ const contexts = cdpBrowser.contexts();
8068
+ const context = contexts.length > 0 ? contexts[0] : await cdpBrowser.newContext();
8069
+ const pages = context.pages();
8070
+ const page2 = pages.length > 0 ? pages[0] : await context.newPage();
8071
+ const session2 = createSession({
8072
+ engine: "cdp",
8073
+ projectId: opts.projectId,
8074
+ agentId: opts.agentId,
8075
+ startUrl: page2.url(),
8076
+ name: opts.name ?? "attached"
8077
+ });
8078
+ const cleanups2 = [];
8079
+ if (opts.captureNetwork !== false) {
8080
+ try {
8081
+ cleanups2.push(enableNetworkLogging(page2, session2.id));
8082
+ } catch {}
8083
+ }
8084
+ if (opts.captureConsole !== false) {
8085
+ try {
8086
+ cleanups2.push(enableConsoleCapture(page2, session2.id));
8087
+ } catch {}
8088
+ }
8089
+ try {
8090
+ cleanups2.push(setupDialogHandler(page2, session2.id));
8091
+ } catch {}
8092
+ handles.set(session2.id, { browser: cdpBrowser, bunView: null, page: page2, engine: "cdp", cleanups: cleanups2, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false });
8093
+ return { session: session2, page: page2 };
8094
+ }
7865
8095
  const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
7866
8096
  const resolvedEngine = engine === "auto" ? "playwright" : engine;
7867
8097
  let browser = null;
@@ -7887,7 +8117,22 @@ async function createSession2(opts = {}) {
7887
8117
  page = await context.newPage();
7888
8118
  } else {
7889
8119
  browser = await pool.acquire(opts.headless ?? true);
7890
- page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
8120
+ if (opts.storageState) {
8121
+ const { loadStatePath: loadStatePath2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
8122
+ const statePath2 = loadStatePath2(opts.storageState);
8123
+ if (statePath2) {
8124
+ const context = await browser.newContext({
8125
+ viewport: opts.viewport ?? { width: 1280, height: 720 },
8126
+ userAgent: opts.userAgent,
8127
+ storageState: statePath2
8128
+ });
8129
+ page = await context.newPage();
8130
+ } else {
8131
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
8132
+ }
8133
+ } else {
8134
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
8135
+ }
7891
8136
  }
7892
8137
  const sessionName = opts.name ?? (opts.startUrl ? (() => {
7893
8138
  try {
@@ -8003,6 +8248,66 @@ init_types();
8003
8248
  var lastSnapshots = new Map;
8004
8249
  var sessionRefMaps = new Map;
8005
8250
 
8251
+ // src/lib/self-heal.ts
8252
+ async function healSelector(page, selector, sessionId) {
8253
+ const attempts = [];
8254
+ attempts.push(`selector: ${selector}`);
8255
+ try {
8256
+ const loc = page.locator(selector).first();
8257
+ if (await loc.count() > 0) {
8258
+ return { found: true, locator: loc, method: "original", healed: false, attempts };
8259
+ }
8260
+ } catch {}
8261
+ if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[") && !selector.includes(">") && !selector.includes(" ")) {
8262
+ attempts.push(`text: "${selector}"`);
8263
+ try {
8264
+ const loc = page.getByText(selector, { exact: false }).first();
8265
+ if (await loc.count() > 0) {
8266
+ return { found: true, locator: loc, method: "text", healed: true, attempts };
8267
+ }
8268
+ } catch {}
8269
+ }
8270
+ const roleMap = {
8271
+ button: ["button", "submit", "reset"],
8272
+ link: ["a"],
8273
+ input: ["input", "textarea"],
8274
+ heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
8275
+ };
8276
+ const nameHint = selector.replace(/^[#.]/, "").replace(/[-_]/g, " ").toLowerCase();
8277
+ for (const [role, tags] of Object.entries(roleMap)) {
8278
+ attempts.push(`role: ${role} name~="${nameHint}"`);
8279
+ try {
8280
+ const loc = page.getByRole(role, { name: new RegExp(nameHint.split(" ")[0], "i") }).first();
8281
+ if (await loc.count() > 0) {
8282
+ return { found: true, locator: loc, method: "role", healed: true, attempts };
8283
+ }
8284
+ } catch {}
8285
+ }
8286
+ if (selector.startsWith("#")) {
8287
+ const idPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
8288
+ const partialSel = `[id*="${idPart}"]`;
8289
+ attempts.push(`partial_id: ${partialSel}`);
8290
+ try {
8291
+ const loc = page.locator(partialSel).first();
8292
+ if (await loc.count() > 0) {
8293
+ return { found: true, locator: loc, method: "partial_id", healed: true, attempts };
8294
+ }
8295
+ } catch {}
8296
+ }
8297
+ if (selector.startsWith(".")) {
8298
+ const classPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
8299
+ const partialSel = `[class*="${classPart}"]`;
8300
+ attempts.push(`partial_class: ${partialSel}`);
8301
+ try {
8302
+ const loc = page.locator(partialSel).first();
8303
+ if (await loc.count() > 0) {
8304
+ return { found: true, locator: loc, method: "partial_class", healed: true, attempts };
8305
+ }
8306
+ } catch {}
8307
+ }
8308
+ return { found: false, locator: null, method: "none", healed: false, attempts };
8309
+ }
8310
+
8006
8311
  // src/lib/actions.ts
8007
8312
  async function click(page, selector, opts) {
8008
8313
  try {
@@ -8012,11 +8317,22 @@ async function click(page, selector, opts) {
8012
8317
  delay: opts?.delay,
8013
8318
  timeout: opts?.timeout ?? 1e4
8014
8319
  });
8015
- } catch (err) {
8016
- if (err instanceof Error && err.message.includes("not found")) {
8320
+ return {};
8321
+ } catch (originalError) {
8322
+ if (opts?.selfHeal !== false) {
8323
+ const result = await healSelector(page, selector);
8324
+ if (result.found && result.locator) {
8325
+ await result.locator.click({
8326
+ button: opts?.button ?? "left",
8327
+ timeout: opts?.timeout ?? 1e4
8328
+ });
8329
+ return { healed: true, method: result.method, attempts: result.attempts };
8330
+ }
8331
+ }
8332
+ if (originalError instanceof Error && originalError.message.includes("not found")) {
8017
8333
  throw new ElementNotFoundError(selector);
8018
8334
  }
8019
- throw new BrowserError(`Click failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "CLICK_FAILED");
8335
+ throw new BrowserError(`Click failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "CLICK_FAILED");
8020
8336
  }
8021
8337
  }
8022
8338
  async function type(page, selector, text, opts) {
@@ -8025,11 +8341,21 @@ async function type(page, selector, text, opts) {
8025
8341
  await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
8026
8342
  }
8027
8343
  await page.type(selector, text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
8028
- } catch (err) {
8029
- if (err instanceof Error && err.message.includes("not found")) {
8344
+ return {};
8345
+ } catch (originalError) {
8346
+ if (opts?.selfHeal !== false) {
8347
+ const result = await healSelector(page, selector);
8348
+ if (result.found && result.locator) {
8349
+ if (opts?.clear)
8350
+ await result.locator.fill("", { timeout: opts?.timeout ?? 1e4 });
8351
+ await result.locator.pressSequentially(text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
8352
+ return { healed: true, method: result.method, attempts: result.attempts };
8353
+ }
8354
+ }
8355
+ if (originalError instanceof Error && originalError.message.includes("not found")) {
8030
8356
  throw new ElementNotFoundError(selector);
8031
8357
  }
8032
- throw new BrowserError(`Type failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "TYPE_FAILED");
8358
+ throw new BrowserError(`Type failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "TYPE_FAILED");
8033
8359
  }
8034
8360
  }
8035
8361
  async function scroll(page, direction = "down", amount = 300) {
@@ -8129,9 +8455,9 @@ async function extract(page, opts = {}) {
8129
8455
  // src/lib/screenshot.ts
8130
8456
  init_types();
8131
8457
  var import_sharp = __toESM(require_lib(), 1);
8132
- import { join as join3 } from "path";
8133
- import { mkdirSync as mkdirSync3 } from "fs";
8134
- import { homedir as homedir3 } from "os";
8458
+ import { join as join4 } from "path";
8459
+ import { mkdirSync as mkdirSync4 } from "fs";
8460
+ import { homedir as homedir4 } from "os";
8135
8461
 
8136
8462
  // src/db/gallery.ts
8137
8463
  init_schema();
@@ -8261,13 +8587,13 @@ function getGalleryStats(projectId) {
8261
8587
 
8262
8588
  // src/lib/screenshot.ts
8263
8589
  function getDataDir2() {
8264
- return process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser");
8590
+ return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
8265
8591
  }
8266
8592
  function getScreenshotDir(projectId) {
8267
- const base = join3(getDataDir2(), "screenshots");
8593
+ const base = join4(getDataDir2(), "screenshots");
8268
8594
  const date = new Date().toISOString().split("T")[0];
8269
- const dir = projectId ? join3(base, projectId, date) : join3(base, date);
8270
- mkdirSync3(dir, { recursive: true });
8595
+ const dir = projectId ? join4(base, projectId, date) : join4(base, date);
8596
+ mkdirSync4(dir, { recursive: true });
8271
8597
  return dir;
8272
8598
  }
8273
8599
  async function compressBuffer(raw, format, quality, maxWidth) {
@@ -8282,7 +8608,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
8282
8608
  }
8283
8609
  }
8284
8610
  async function generateThumbnail(raw, dir, stem) {
8285
- const thumbPath = join3(dir, `${stem}.thumb.webp`);
8611
+ const thumbPath = join4(dir, `${stem}.thumb.webp`);
8286
8612
  const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
8287
8613
  await Bun.write(thumbPath, thumbBuffer);
8288
8614
  return { path: thumbPath, base64: thumbBuffer.toString("base64") };
@@ -8339,7 +8665,7 @@ async function takeScreenshot(page, opts) {
8339
8665
  const compressedSizeBytes = finalBuffer.length;
8340
8666
  const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
8341
8667
  const ext = format;
8342
- const screenshotPath = opts?.path ?? join3(dir, `${stem}.${ext}`);
8668
+ const screenshotPath = opts?.path ?? join4(dir, `${stem}.${ext}`);
8343
8669
  await Bun.write(screenshotPath, finalBuffer);
8344
8670
  let thumbnailPath;
8345
8671
  let thumbnailBase64;
@@ -8398,118 +8724,8 @@ async function takeScreenshot(page, opts) {
8398
8724
  }
8399
8725
  }
8400
8726
 
8401
- // src/engines/cdp.ts
8402
- init_types();
8403
-
8404
- class CDPClient {
8405
- session;
8406
- networkEnabled = false;
8407
- performanceEnabled = false;
8408
- constructor(session) {
8409
- this.session = session;
8410
- }
8411
- static async fromPage(page) {
8412
- try {
8413
- const session = await page.context().newCDPSession(page);
8414
- return new CDPClient(session);
8415
- } catch (err) {
8416
- throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
8417
- }
8418
- }
8419
- async send(method, params) {
8420
- try {
8421
- return await this.session.send(method, params);
8422
- } catch (err) {
8423
- throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
8424
- }
8425
- }
8426
- on(event, handler) {
8427
- this.session.on(event, handler);
8428
- }
8429
- off(event, handler) {
8430
- this.session.off(event, handler);
8431
- }
8432
- async enableNetwork() {
8433
- if (!this.networkEnabled) {
8434
- await this.send("Network.enable");
8435
- this.networkEnabled = true;
8436
- }
8437
- }
8438
- async enablePerformance() {
8439
- if (!this.performanceEnabled) {
8440
- await this.send("Performance.enable");
8441
- this.performanceEnabled = true;
8442
- }
8443
- }
8444
- async getPerformanceMetrics() {
8445
- await this.enablePerformance();
8446
- const result = await this.send("Performance.getMetrics");
8447
- const m = {};
8448
- for (const metric of result.metrics) {
8449
- m[metric.name] = metric.value;
8450
- }
8451
- return {
8452
- js_heap_size_used: m["JSHeapUsedSize"],
8453
- js_heap_size_total: m["JSHeapTotalSize"],
8454
- dom_interactive: m["DOMInteractive"],
8455
- dom_complete: m["DOMComplete"],
8456
- load_event: m["LoadEventEnd"]
8457
- };
8458
- }
8459
- async startJSCoverage() {
8460
- await this.send("Profiler.enable");
8461
- await this.send("Debugger.enable");
8462
- await this.send("Profiler.startPreciseCoverage", {
8463
- callCount: false,
8464
- detailed: true
8465
- });
8466
- }
8467
- async stopJSCoverage() {
8468
- const result = await this.send("Profiler.takePreciseCoverage");
8469
- await this.send("Profiler.stopPreciseCoverage");
8470
- return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
8471
- url: r.url,
8472
- text: "",
8473
- ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
8474
- }));
8475
- }
8476
- async getCoverage() {
8477
- await this.startJSCoverage();
8478
- const js = await this.stopJSCoverage();
8479
- const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
8480
- return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
8481
- }
8482
- async captureHAREntries(page, handler) {
8483
- await this.enableNetwork();
8484
- const requestTimings = new Map;
8485
- const onRequest = (params) => {
8486
- requestTimings.set(params.requestId, params.timestamp);
8487
- };
8488
- const onResponse = (params) => {
8489
- const start = requestTimings.get(params.requestId);
8490
- const duration = start != null ? (params.timestamp - start) * 1000 : 0;
8491
- handler({
8492
- method: "GET",
8493
- url: params.response.url,
8494
- status: params.response.status,
8495
- duration
8496
- });
8497
- };
8498
- this.on("Network.requestWillBeSent", onRequest);
8499
- this.on("Network.responseReceived", onResponse);
8500
- return () => {
8501
- this.off("Network.requestWillBeSent", onRequest);
8502
- this.off("Network.responseReceived", onResponse);
8503
- };
8504
- }
8505
- async detach() {
8506
- try {
8507
- await this.session.detach();
8508
- } catch {}
8509
- }
8510
- }
8511
-
8512
8727
  // src/lib/performance.ts
8728
+ init_cdp();
8513
8729
  async function getPerformanceMetrics(page) {
8514
8730
  const navTiming = await page.evaluate(() => {
8515
8731
  const t = performance.timing;
@@ -8742,16 +8958,16 @@ init_console_log();
8742
8958
  init_recordings();
8743
8959
 
8744
8960
  // src/lib/downloads.ts
8745
- import { join as join4, basename, extname } from "path";
8746
- import { mkdirSync as mkdirSync4, existsSync, readdirSync, statSync, unlinkSync, copyFileSync, writeFileSync, readFileSync } from "fs";
8747
- import { homedir as homedir4 } from "os";
8961
+ import { join as join5, basename, extname } from "path";
8962
+ import { mkdirSync as mkdirSync5, existsSync as existsSync2, readdirSync as readdirSync2, statSync, unlinkSync as unlinkSync2, copyFileSync, writeFileSync, readFileSync } from "fs";
8963
+ import { homedir as homedir5 } from "os";
8748
8964
  function getDataDir3() {
8749
- return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
8965
+ return process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
8750
8966
  }
8751
8967
  function getDownloadsDir(sessionId) {
8752
- const base = join4(getDataDir3(), "downloads");
8753
- const dir = sessionId ? join4(base, sessionId) : base;
8754
- mkdirSync4(dir, { recursive: true });
8968
+ const base = join5(getDataDir3(), "downloads");
8969
+ const dir = sessionId ? join5(base, sessionId) : base;
8970
+ mkdirSync5(dir, { recursive: true });
8755
8971
  return dir;
8756
8972
  }
8757
8973
  function metaPath(filePath) {
@@ -8761,20 +8977,20 @@ function listDownloads(sessionId) {
8761
8977
  const dir = getDownloadsDir(sessionId);
8762
8978
  const results = [];
8763
8979
  function scanDir(d) {
8764
- if (!existsSync(d))
8980
+ if (!existsSync2(d))
8765
8981
  return;
8766
- const entries = readdirSync(d);
8982
+ const entries = readdirSync2(d);
8767
8983
  for (const entry of entries) {
8768
8984
  if (entry.endsWith(".meta.json"))
8769
8985
  continue;
8770
- const full = join4(d, entry);
8986
+ const full = join5(d, entry);
8771
8987
  const stat = statSync(full);
8772
8988
  if (stat.isDirectory()) {
8773
8989
  scanDir(full);
8774
8990
  continue;
8775
8991
  }
8776
8992
  const mpath = metaPath(full);
8777
- if (!existsSync(mpath))
8993
+ if (!existsSync2(mpath))
8778
8994
  continue;
8779
8995
  try {
8780
8996
  const meta = JSON.parse(readFileSync(mpath, "utf8"));
@@ -8804,9 +9020,9 @@ function deleteDownload(id, sessionId) {
8804
9020
  if (!file)
8805
9021
  return false;
8806
9022
  try {
8807
- unlinkSync(file.path);
8808
- if (existsSync(file.meta_path))
8809
- unlinkSync(file.meta_path);
9023
+ unlinkSync2(file.path);
9024
+ if (existsSync2(file.meta_path))
9025
+ unlinkSync2(file.meta_path);
8810
9026
  return true;
8811
9027
  } catch {
8812
9028
  return false;
@@ -8828,9 +9044,9 @@ function cleanStaleDownloads(olderThanDays = 7) {
8828
9044
 
8829
9045
  // src/lib/gallery-diff.ts
8830
9046
  var import_sharp2 = __toESM(require_lib(), 1);
8831
- import { join as join5 } from "path";
8832
- import { mkdirSync as mkdirSync5 } from "fs";
8833
- import { homedir as homedir5 } from "os";
9047
+ import { join as join6 } from "path";
9048
+ import { mkdirSync as mkdirSync6 } from "fs";
9049
+ import { homedir as homedir6 } from "os";
8834
9050
  async function diffImages(path1, path2) {
8835
9051
  const img1 = import_sharp2.default(path1);
8836
9052
  const img2 = import_sharp2.default(path2);
@@ -8861,10 +9077,10 @@ async function diffImages(path1, path2) {
8861
9077
  diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
8862
9078
  }
8863
9079
  }
8864
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
8865
- const diffDir = join5(dataDir, "diffs");
8866
- mkdirSync5(diffDir, { recursive: true });
8867
- const diffPath = join5(diffDir, `diff-${Date.now()}.webp`);
9080
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
9081
+ const diffDir = join6(dataDir, "diffs");
9082
+ mkdirSync6(diffDir, { recursive: true });
9083
+ const diffPath = join6(diffDir, `diff-${Date.now()}.webp`);
8868
9084
  const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
8869
9085
  await Bun.write(diffPath, diffImageBuffer);
8870
9086
  return {
@@ -9108,14 +9324,14 @@ var server = Bun.serve({
9108
9324
  if (path.match(/^\/api\/gallery\/([^/]+)\/thumbnail$/) && method === "GET") {
9109
9325
  const id = path.split("/")[3];
9110
9326
  const entry = getEntry(id);
9111
- if (!entry?.thumbnail_path || !existsSync3(entry.thumbnail_path))
9327
+ if (!entry?.thumbnail_path || !existsSync4(entry.thumbnail_path))
9112
9328
  return notFound("Thumbnail not found");
9113
9329
  return new Response(Bun.file(entry.thumbnail_path), { headers: { ...CORS_HEADERS } });
9114
9330
  }
9115
9331
  if (path.match(/^\/api\/gallery\/([^/]+)\/image$/) && method === "GET") {
9116
9332
  const id = path.split("/")[3];
9117
9333
  const entry = getEntry(id);
9118
- if (!entry?.path || !existsSync3(entry.path))
9334
+ if (!entry?.path || !existsSync4(entry.path))
9119
9335
  return notFound("Image not found");
9120
9336
  return new Response(Bun.file(entry.path), { headers: { ...CORS_HEADERS } });
9121
9337
  }
@@ -9143,7 +9359,7 @@ var server = Bun.serve({
9143
9359
  if (path.match(/^\/api\/downloads\/([^/]+)\/raw$/) && method === "GET") {
9144
9360
  const id = path.split("/")[3];
9145
9361
  const file = getDownload(id);
9146
- if (!file || !existsSync3(file.path))
9362
+ if (!file || !existsSync4(file.path))
9147
9363
  return notFound("Download not found");
9148
9364
  return new Response(Bun.file(file.path), { headers: { ...CORS_HEADERS } });
9149
9365
  }
@@ -9151,13 +9367,13 @@ var server = Bun.serve({
9151
9367
  const id = path.split("/")[3];
9152
9368
  return ok({ deleted: deleteDownload(id) });
9153
9369
  }
9154
- const dashboardDist = join6(import.meta.dir, "../../dashboard/dist");
9155
- if (existsSync3(dashboardDist)) {
9156
- const filePath = path === "/" ? join6(dashboardDist, "index.html") : join6(dashboardDist, path);
9157
- if (existsSync3(filePath)) {
9370
+ const dashboardDist = join7(import.meta.dir, "../../dashboard/dist");
9371
+ if (existsSync4(dashboardDist)) {
9372
+ const filePath = path === "/" ? join7(dashboardDist, "index.html") : join7(dashboardDist, path);
9373
+ if (existsSync4(filePath)) {
9158
9374
  return new Response(Bun.file(filePath), { headers: CORS_HEADERS });
9159
9375
  }
9160
- return new Response(Bun.file(join6(dashboardDist, "index.html")), { headers: CORS_HEADERS });
9376
+ return new Response(Bun.file(join7(dashboardDist, "index.html")), { headers: CORS_HEADERS });
9161
9377
  }
9162
9378
  if (path === "/" || path === "") {
9163
9379
  return new Response("@hasna/browser REST API running. Dashboard not built.", {