@ulpi/browse 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/{browse.mjs → browse.cjs} +564 -285
- package/package.json +4 -3
- package/skill/SKILL.md +16 -0
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
const __import_meta_url = require("url").pathToFileURL(__filename).href;
|
|
3
|
+
"use strict";
|
|
2
4
|
var __create = Object.create;
|
|
3
5
|
var __defProp = Object.defineProperty;
|
|
4
6
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
7
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
8
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
9
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
9
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
10
|
-
}) : x)(function(x) {
|
|
11
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
12
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
13
|
-
});
|
|
14
10
|
var __esm = (fn, res) => function __init() {
|
|
15
11
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
16
12
|
};
|
|
17
|
-
var __commonJS = (cb, mod) => function
|
|
13
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
18
14
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
19
15
|
};
|
|
20
16
|
var __export = (target, all) => {
|
|
@@ -38,6 +34,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
38
34
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
39
35
|
mod
|
|
40
36
|
));
|
|
37
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
41
38
|
|
|
42
39
|
// src/constants.ts
|
|
43
40
|
var BROWSE_TIMEOUT, DEFAULTS;
|
|
@@ -68,9 +65,6 @@ var chrome_discover_exports = {};
|
|
|
68
65
|
__export(chrome_discover_exports, {
|
|
69
66
|
discoverChrome: () => discoverChrome
|
|
70
67
|
});
|
|
71
|
-
import * as os from "os";
|
|
72
|
-
import * as fs2 from "fs";
|
|
73
|
-
import * as path2 from "path";
|
|
74
68
|
async function fetchWsUrl(port) {
|
|
75
69
|
try {
|
|
76
70
|
const res = await fetch(`http://127.0.0.1:${port}/json/version`, {
|
|
@@ -108,10 +102,13 @@ async function discoverChrome() {
|
|
|
108
102
|
}
|
|
109
103
|
return null;
|
|
110
104
|
}
|
|
111
|
-
var PROFILE_PATHS, PROBE_PORTS;
|
|
105
|
+
var os, fs2, path2, PROFILE_PATHS, PROBE_PORTS;
|
|
112
106
|
var init_chrome_discover = __esm({
|
|
113
107
|
"src/chrome-discover.ts"() {
|
|
114
108
|
"use strict";
|
|
109
|
+
os = __toESM(require("os"), 1);
|
|
110
|
+
fs2 = __toESM(require("fs"), 1);
|
|
111
|
+
path2 = __toESM(require("path"), 1);
|
|
115
112
|
PROFILE_PATHS = [
|
|
116
113
|
"Google/Chrome",
|
|
117
114
|
"Arc/User Data",
|
|
@@ -124,10 +121,10 @@ var init_chrome_discover = __esm({
|
|
|
124
121
|
|
|
125
122
|
// package.json
|
|
126
123
|
var require_package = __commonJS({
|
|
127
|
-
"package.json"(
|
|
128
|
-
|
|
124
|
+
"package.json"(exports2, module2) {
|
|
125
|
+
module2.exports = {
|
|
129
126
|
name: "@ulpi/browse",
|
|
130
|
-
version: "1.0.
|
|
127
|
+
version: "1.0.3",
|
|
131
128
|
repository: {
|
|
132
129
|
type: "git",
|
|
133
130
|
url: "https://github.com/ulpi-io/browse"
|
|
@@ -141,7 +138,7 @@ var require_package = __commonJS({
|
|
|
141
138
|
"playwright-core": "^1.58.2"
|
|
142
139
|
},
|
|
143
140
|
bin: {
|
|
144
|
-
browse: "dist/browse.
|
|
141
|
+
browse: "dist/browse.cjs"
|
|
145
142
|
},
|
|
146
143
|
description: "Fast headless browser CLI \u2014 persistent Chromium daemon via Playwright.",
|
|
147
144
|
engines: {
|
|
@@ -168,7 +165,8 @@ var require_package = __commonJS({
|
|
|
168
165
|
access: "public"
|
|
169
166
|
},
|
|
170
167
|
scripts: {
|
|
171
|
-
build:
|
|
168
|
+
build: `esbuild src/cli.ts --bundle --format=cjs --platform=node --target=node18 --outfile=dist/browse.cjs --external:playwright --external:playwright-core --external:better-sqlite3 --external:electron --external:chromium-bidi --banner:js='#!/usr/bin/env node
|
|
169
|
+
const __import_meta_url = require("url").pathToFileURL(__filename).href;' --define:import.meta.url=__import_meta_url`,
|
|
172
170
|
"build:all": "bash scripts/build-all.sh",
|
|
173
171
|
dev: "tsx src/cli.ts",
|
|
174
172
|
server: "tsx src/server.ts",
|
|
@@ -181,6 +179,7 @@ var require_package = __commonJS({
|
|
|
181
179
|
devDependencies: {
|
|
182
180
|
"@types/better-sqlite3": "^7.0.0",
|
|
183
181
|
"@types/node": "^25.5.0",
|
|
182
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
184
183
|
esbuild: "^0.25.0",
|
|
185
184
|
tsx: "^4.0.0",
|
|
186
185
|
typescript: "^5.9.3",
|
|
@@ -198,9 +197,6 @@ var install_skill_exports = {};
|
|
|
198
197
|
__export(install_skill_exports, {
|
|
199
198
|
installSkill: () => installSkill
|
|
200
199
|
});
|
|
201
|
-
import * as fs3 from "fs";
|
|
202
|
-
import * as path3 from "path";
|
|
203
|
-
import { fileURLToPath } from "url";
|
|
204
200
|
function installSkill(targetDir) {
|
|
205
201
|
const dir = targetDir || process.cwd();
|
|
206
202
|
const hasGit = fs3.existsSync(path3.join(dir, ".git"));
|
|
@@ -212,7 +208,7 @@ function installSkill(targetDir) {
|
|
|
212
208
|
}
|
|
213
209
|
const skillDir = path3.join(dir, ".claude", "skills", "browse");
|
|
214
210
|
fs3.mkdirSync(skillDir, { recursive: true });
|
|
215
|
-
const skillSource = path3.resolve(path3.dirname(
|
|
211
|
+
const skillSource = path3.resolve(path3.dirname((0, import_url.fileURLToPath)(__import_meta_url)), "..", "skill", "SKILL.md");
|
|
216
212
|
const skillDest = path3.join(skillDir, "SKILL.md");
|
|
217
213
|
if (!fs3.existsSync(skillSource)) {
|
|
218
214
|
console.error(`SKILL.md not found at ${skillSource}`);
|
|
@@ -249,10 +245,13 @@ function installSkill(targetDir) {
|
|
|
249
245
|
}
|
|
250
246
|
console.log("\nDone. Claude Code will now use browse for web tasks automatically.");
|
|
251
247
|
}
|
|
252
|
-
var PERMISSIONS;
|
|
248
|
+
var fs3, path3, import_url, PERMISSIONS;
|
|
253
249
|
var init_install_skill = __esm({
|
|
254
250
|
"src/install-skill.ts"() {
|
|
255
251
|
"use strict";
|
|
252
|
+
fs3 = __toESM(require("fs"), 1);
|
|
253
|
+
path3 = __toESM(require("path"), 1);
|
|
254
|
+
import_url = require("url");
|
|
256
255
|
PERMISSIONS = [
|
|
257
256
|
"Bash(browse:*)",
|
|
258
257
|
"Bash(browse goto:*)",
|
|
@@ -318,35 +317,29 @@ var rebrowser_playwright_exports = {};
|
|
|
318
317
|
__export(rebrowser_playwright_exports, {
|
|
319
318
|
default: () => rebrowser_playwright_default
|
|
320
319
|
});
|
|
321
|
-
|
|
322
|
-
import playwright from "playwright-core";
|
|
323
|
-
var rebrowser_playwright_default;
|
|
320
|
+
var import_playwright_core, rebrowser_playwright_default;
|
|
324
321
|
var init_rebrowser_playwright = __esm({
|
|
325
322
|
"node_modules/rebrowser-playwright/index.mjs"() {
|
|
326
|
-
__reExport(rebrowser_playwright_exports,
|
|
327
|
-
|
|
323
|
+
__reExport(rebrowser_playwright_exports, require("playwright-core"));
|
|
324
|
+
import_playwright_core = __toESM(require("playwright-core"), 1);
|
|
325
|
+
rebrowser_playwright_default = import_playwright_core.default;
|
|
328
326
|
}
|
|
329
327
|
});
|
|
330
328
|
|
|
331
329
|
// src/runtime.ts
|
|
332
|
-
import { homedir as homedir2 } from "os";
|
|
333
|
-
import { existsSync as existsSync3 } from "fs";
|
|
334
|
-
import { execSync, spawn } from "child_process";
|
|
335
|
-
import { join as join4 } from "path";
|
|
336
|
-
import * as net from "net";
|
|
337
330
|
function findLightpanda() {
|
|
338
331
|
try {
|
|
339
|
-
const result = execSync("which lightpanda", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
332
|
+
const result = (0, import_child_process.execSync)("which lightpanda", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
340
333
|
if (result) return result;
|
|
341
334
|
} catch {
|
|
342
335
|
}
|
|
343
|
-
const home =
|
|
336
|
+
const home = (0, import_os.homedir)();
|
|
344
337
|
const candidates = [
|
|
345
|
-
|
|
346
|
-
|
|
338
|
+
(0, import_path.join)(home, ".lightpanda", "lightpanda"),
|
|
339
|
+
(0, import_path.join)(home, ".local", "bin", "lightpanda")
|
|
347
340
|
];
|
|
348
341
|
for (const candidate of candidates) {
|
|
349
|
-
if (
|
|
342
|
+
if ((0, import_fs.existsSync)(candidate)) return candidate;
|
|
350
343
|
}
|
|
351
344
|
return null;
|
|
352
345
|
}
|
|
@@ -360,10 +353,15 @@ async function getRuntime(name) {
|
|
|
360
353
|
}
|
|
361
354
|
return loader();
|
|
362
355
|
}
|
|
363
|
-
var registry, AVAILABLE_RUNTIMES;
|
|
356
|
+
var import_os, import_fs, import_child_process, import_path, net, registry, AVAILABLE_RUNTIMES;
|
|
364
357
|
var init_runtime = __esm({
|
|
365
358
|
"src/runtime.ts"() {
|
|
366
359
|
"use strict";
|
|
360
|
+
import_os = require("os");
|
|
361
|
+
import_fs = require("fs");
|
|
362
|
+
import_child_process = require("child_process");
|
|
363
|
+
import_path = require("path");
|
|
364
|
+
net = __toESM(require("net"), 1);
|
|
367
365
|
registry = {
|
|
368
366
|
playwright: async () => {
|
|
369
367
|
const pw = await import("playwright");
|
|
@@ -395,7 +393,7 @@ var init_runtime = __esm({
|
|
|
395
393
|
});
|
|
396
394
|
srv.on("error", reject);
|
|
397
395
|
});
|
|
398
|
-
const child = spawn(
|
|
396
|
+
const child = (0, import_child_process.spawn)(
|
|
399
397
|
binaryPath,
|
|
400
398
|
["serve", "--host", "127.0.0.1", "--port", String(port), "--timeout", "604800"],
|
|
401
399
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
@@ -452,7 +450,31 @@ stderr: ${stderrData.slice(0, 2e3)}` : "")
|
|
|
452
450
|
});
|
|
453
451
|
|
|
454
452
|
// src/buffers.ts
|
|
455
|
-
var
|
|
453
|
+
var buffers_exports = {};
|
|
454
|
+
__export(buffers_exports, {
|
|
455
|
+
SessionBuffers: () => SessionBuffers,
|
|
456
|
+
addConsoleEntry: () => addConsoleEntry,
|
|
457
|
+
addNetworkEntry: () => addNetworkEntry,
|
|
458
|
+
consoleBuffer: () => consoleBuffer,
|
|
459
|
+
consoleTotalAdded: () => consoleTotalAdded,
|
|
460
|
+
networkBuffer: () => networkBuffer,
|
|
461
|
+
networkTotalAdded: () => networkTotalAdded
|
|
462
|
+
});
|
|
463
|
+
function addConsoleEntry(entry) {
|
|
464
|
+
if (consoleBuffer.length >= DEFAULTS.BUFFER_HIGH_WATER_MARK) {
|
|
465
|
+
consoleBuffer.shift();
|
|
466
|
+
}
|
|
467
|
+
consoleBuffer.push(entry);
|
|
468
|
+
consoleTotalAdded++;
|
|
469
|
+
}
|
|
470
|
+
function addNetworkEntry(entry) {
|
|
471
|
+
if (networkBuffer.length >= DEFAULTS.BUFFER_HIGH_WATER_MARK) {
|
|
472
|
+
networkBuffer.shift();
|
|
473
|
+
}
|
|
474
|
+
networkBuffer.push(entry);
|
|
475
|
+
networkTotalAdded++;
|
|
476
|
+
}
|
|
477
|
+
var SessionBuffers, consoleBuffer, networkBuffer, consoleTotalAdded, networkTotalAdded;
|
|
456
478
|
var init_buffers = __esm({
|
|
457
479
|
"src/buffers.ts"() {
|
|
458
480
|
"use strict";
|
|
@@ -480,25 +502,51 @@ var init_buffers = __esm({
|
|
|
480
502
|
this.networkTotalAdded++;
|
|
481
503
|
}
|
|
482
504
|
};
|
|
505
|
+
consoleBuffer = [];
|
|
506
|
+
networkBuffer = [];
|
|
507
|
+
consoleTotalAdded = 0;
|
|
508
|
+
networkTotalAdded = 0;
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// src/sanitize.ts
|
|
513
|
+
function sanitizeName(name) {
|
|
514
|
+
const sanitized = name.replace(/[\/\\]/g, "_").replace(/\.\./g, "_");
|
|
515
|
+
if (!sanitized || /^[._]+$/.test(sanitized)) {
|
|
516
|
+
throw new Error(`Invalid name: "${name}"`);
|
|
517
|
+
}
|
|
518
|
+
return sanitized;
|
|
519
|
+
}
|
|
520
|
+
var init_sanitize = __esm({
|
|
521
|
+
"src/sanitize.ts"() {
|
|
522
|
+
"use strict";
|
|
483
523
|
}
|
|
484
524
|
});
|
|
485
525
|
|
|
486
526
|
// src/browser-manager.ts
|
|
487
|
-
|
|
527
|
+
var browser_manager_exports = {};
|
|
528
|
+
__export(browser_manager_exports, {
|
|
529
|
+
BrowserManager: () => BrowserManager,
|
|
530
|
+
deleteProfile: () => deleteProfile,
|
|
531
|
+
getProfileDir: () => getProfileDir,
|
|
532
|
+
listDevices: () => listDevices,
|
|
533
|
+
listProfiles: () => listProfiles,
|
|
534
|
+
resolveDevice: () => resolveDevice
|
|
535
|
+
});
|
|
488
536
|
function resolveDevice(name) {
|
|
489
537
|
const alias = DEVICE_ALIASES[name.toLowerCase()];
|
|
490
538
|
const aliasTarget = alias || name;
|
|
491
539
|
if (CUSTOM_DEVICES[aliasTarget]) {
|
|
492
540
|
return CUSTOM_DEVICES[aliasTarget];
|
|
493
541
|
}
|
|
494
|
-
if (
|
|
495
|
-
return
|
|
542
|
+
if (import_playwright.devices[aliasTarget]) {
|
|
543
|
+
return import_playwright.devices[aliasTarget];
|
|
496
544
|
}
|
|
497
545
|
const lower = name.toLowerCase();
|
|
498
546
|
for (const [key, desc] of Object.entries(CUSTOM_DEVICES)) {
|
|
499
547
|
if (key.toLowerCase() === lower) return desc;
|
|
500
548
|
}
|
|
501
|
-
for (const [key, desc] of Object.entries(
|
|
549
|
+
for (const [key, desc] of Object.entries(import_playwright.devices)) {
|
|
502
550
|
if (key.toLowerCase() === lower) {
|
|
503
551
|
return desc;
|
|
504
552
|
}
|
|
@@ -508,15 +556,56 @@ function resolveDevice(name) {
|
|
|
508
556
|
function listDevices() {
|
|
509
557
|
const all = /* @__PURE__ */ new Set([
|
|
510
558
|
...Object.keys(CUSTOM_DEVICES),
|
|
511
|
-
...Object.keys(
|
|
559
|
+
...Object.keys(import_playwright.devices)
|
|
512
560
|
]);
|
|
513
561
|
return [...all].sort();
|
|
514
562
|
}
|
|
515
|
-
|
|
563
|
+
function getProfileDir(localDir, name) {
|
|
564
|
+
const sanitized = sanitizeName(name);
|
|
565
|
+
if (!sanitized) throw new Error("Invalid profile name");
|
|
566
|
+
return path4.join(localDir, "profiles", sanitized);
|
|
567
|
+
}
|
|
568
|
+
function listProfiles(localDir) {
|
|
569
|
+
const profilesDir = path4.join(localDir, "profiles");
|
|
570
|
+
if (!fs4.existsSync(profilesDir)) return [];
|
|
571
|
+
return fs4.readdirSync(profilesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
|
|
572
|
+
const dir = path4.join(profilesDir, d.name);
|
|
573
|
+
const stat = fs4.statSync(dir);
|
|
574
|
+
let totalSize = 0;
|
|
575
|
+
try {
|
|
576
|
+
const files = fs4.readdirSync(dir, { recursive: true, withFileTypes: true });
|
|
577
|
+
for (const f of files) {
|
|
578
|
+
if (f.isFile()) {
|
|
579
|
+
try {
|
|
580
|
+
totalSize += fs4.statSync(path4.join(f.parentPath || f.path || dir, f.name)).size;
|
|
581
|
+
} catch {
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
} catch {
|
|
586
|
+
}
|
|
587
|
+
const sizeMB = (totalSize / 1024 / 1024).toFixed(1);
|
|
588
|
+
return {
|
|
589
|
+
name: d.name,
|
|
590
|
+
size: `${sizeMB}MB`,
|
|
591
|
+
lastUsed: stat.mtime.toISOString().split("T")[0]
|
|
592
|
+
};
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
function deleteProfile(localDir, name) {
|
|
596
|
+
const dir = getProfileDir(localDir, name);
|
|
597
|
+
if (!fs4.existsSync(dir)) throw new Error(`Profile "${name}" not found`);
|
|
598
|
+
fs4.rmSync(dir, { recursive: true, force: true });
|
|
599
|
+
}
|
|
600
|
+
var path4, fs4, import_playwright, DEVICE_ALIASES, CUSTOM_DEVICES, BrowserManager;
|
|
516
601
|
var init_browser_manager = __esm({
|
|
517
602
|
"src/browser-manager.ts"() {
|
|
518
603
|
"use strict";
|
|
604
|
+
path4 = __toESM(require("path"), 1);
|
|
605
|
+
fs4 = __toESM(require("fs"), 1);
|
|
606
|
+
import_playwright = require("playwright");
|
|
519
607
|
init_buffers();
|
|
608
|
+
init_sanitize();
|
|
520
609
|
DEVICE_ALIASES = {
|
|
521
610
|
"iphone": "iPhone 15",
|
|
522
611
|
"iphone-12": "iPhone 12",
|
|
@@ -626,6 +715,8 @@ var init_browser_manager = __esm({
|
|
|
626
715
|
domainFilter = null;
|
|
627
716
|
// Whether this instance owns (and should close) the Browser process
|
|
628
717
|
ownsBrowser = false;
|
|
718
|
+
// Whether this instance uses a persistent browser context (profile mode)
|
|
719
|
+
isPersistent = false;
|
|
629
720
|
constructor(buffers) {
|
|
630
721
|
this.buffers = buffers || new SessionBuffers();
|
|
631
722
|
}
|
|
@@ -643,7 +734,7 @@ var init_browser_manager = __esm({
|
|
|
643
734
|
* This instance owns the browser and will close it on close().
|
|
644
735
|
*/
|
|
645
736
|
async launch(onCrash) {
|
|
646
|
-
this.browser = await chromium.launch({ headless: true });
|
|
737
|
+
this.browser = await import_playwright.chromium.launch({ headless: true });
|
|
647
738
|
this.ownsBrowser = true;
|
|
648
739
|
this.browser.on("disconnected", () => {
|
|
649
740
|
if (onCrash) onCrash();
|
|
@@ -668,7 +759,66 @@ var init_browser_manager = __esm({
|
|
|
668
759
|
});
|
|
669
760
|
await this.newTab();
|
|
670
761
|
}
|
|
762
|
+
/**
|
|
763
|
+
* Launch with a persistent browser profile directory.
|
|
764
|
+
* Data (cookies, localStorage, cache) persists across restarts.
|
|
765
|
+
* The context IS the browser — closing it closes everything.
|
|
766
|
+
*/
|
|
767
|
+
async launchPersistent(profileDir, onCrash) {
|
|
768
|
+
let context;
|
|
769
|
+
try {
|
|
770
|
+
context = await import_playwright.chromium.launchPersistentContext(profileDir, {
|
|
771
|
+
headless: process.env.BROWSE_HEADED !== "1",
|
|
772
|
+
viewport: { width: 1920, height: 1080 },
|
|
773
|
+
...this.customUserAgent ? { userAgent: this.customUserAgent } : {}
|
|
774
|
+
});
|
|
775
|
+
} catch (err) {
|
|
776
|
+
if (err.message?.includes("Failed to launch") || err.message?.includes("Target closed")) {
|
|
777
|
+
const fs16 = await import("fs");
|
|
778
|
+
console.error(`[browse] Profile directory corrupted, recreating: ${profileDir}`);
|
|
779
|
+
fs16.rmSync(profileDir, { recursive: true, force: true });
|
|
780
|
+
context = await import_playwright.chromium.launchPersistentContext(profileDir, {
|
|
781
|
+
headless: process.env.BROWSE_HEADED !== "1",
|
|
782
|
+
viewport: { width: 1920, height: 1080 }
|
|
783
|
+
});
|
|
784
|
+
} else {
|
|
785
|
+
throw err;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
this.context = context;
|
|
789
|
+
this.browser = context.browser();
|
|
790
|
+
this.isPersistent = true;
|
|
791
|
+
this.ownsBrowser = true;
|
|
792
|
+
if (this.browser) {
|
|
793
|
+
this.browser.on("disconnected", () => {
|
|
794
|
+
if (onCrash) onCrash();
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
const pages = context.pages();
|
|
798
|
+
if (pages.length > 0) {
|
|
799
|
+
for (const page of pages) {
|
|
800
|
+
const tabId = this.nextTabId++;
|
|
801
|
+
this.wirePageEvents(page);
|
|
802
|
+
this.pages.set(tabId, page);
|
|
803
|
+
this.activeTabId = tabId;
|
|
804
|
+
}
|
|
805
|
+
} else {
|
|
806
|
+
await this.newTab();
|
|
807
|
+
}
|
|
808
|
+
}
|
|
671
809
|
async close() {
|
|
810
|
+
if (this.isPersistent) {
|
|
811
|
+
this.pages.clear();
|
|
812
|
+
this.tabSnapshots.clear();
|
|
813
|
+
this.refMap.clear();
|
|
814
|
+
if (this.context) {
|
|
815
|
+
await this.context.close().catch(() => {
|
|
816
|
+
});
|
|
817
|
+
this.context = null;
|
|
818
|
+
this.browser = null;
|
|
819
|
+
}
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
672
822
|
for (const [, page] of this.pages) {
|
|
673
823
|
await page.close().catch(() => {
|
|
674
824
|
});
|
|
@@ -690,6 +840,9 @@ var init_browser_manager = __esm({
|
|
|
690
840
|
isHealthy() {
|
|
691
841
|
return this.browser !== null && this.browser.isConnected();
|
|
692
842
|
}
|
|
843
|
+
getIsPersistent() {
|
|
844
|
+
return this.isPersistent;
|
|
845
|
+
}
|
|
693
846
|
// ─── Tab Management ────────────────────────────────────────
|
|
694
847
|
async newTab(url) {
|
|
695
848
|
if (!this.context) throw new Error("Browser not launched");
|
|
@@ -950,6 +1103,11 @@ var init_browser_manager = __esm({
|
|
|
950
1103
|
* Cannot preserve: localStorage/sessionStorage (bound to old context).
|
|
951
1104
|
*/
|
|
952
1105
|
async recreateContext(contextOptions) {
|
|
1106
|
+
if (this.isPersistent) {
|
|
1107
|
+
throw new Error(
|
|
1108
|
+
"Cannot change device/viewport/user-agent in profile mode \u2014 profiles use a fixed browser context. Use --session instead."
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
953
1111
|
if (!this.browser) return;
|
|
954
1112
|
if (this.videoRecording && !contextOptions.recordVideo) {
|
|
955
1113
|
contextOptions = {
|
|
@@ -1149,8 +1307,8 @@ var init_browser_manager = __esm({
|
|
|
1149
1307
|
// ─── Video Recording ──────────────────────────────────────
|
|
1150
1308
|
async startVideoRecording(dir) {
|
|
1151
1309
|
if (this.videoRecording) throw new Error("Video recording already active");
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1310
|
+
const fs16 = await import("fs");
|
|
1311
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
1154
1312
|
this.videoRecording = { dir, startedAt: Date.now() };
|
|
1155
1313
|
const viewport = this.currentDevice?.viewport || { width: 1920, height: 1080 };
|
|
1156
1314
|
await this.recreateContext({
|
|
@@ -1425,24 +1583,7 @@ var init_domain_filter = __esm({
|
|
|
1425
1583
|
}
|
|
1426
1584
|
});
|
|
1427
1585
|
|
|
1428
|
-
// src/sanitize.ts
|
|
1429
|
-
function sanitizeName(name) {
|
|
1430
|
-
const sanitized = name.replace(/[\/\\]/g, "_").replace(/\.\./g, "_");
|
|
1431
|
-
if (!sanitized || /^[._]+$/.test(sanitized)) {
|
|
1432
|
-
throw new Error(`Invalid name: "${name}"`);
|
|
1433
|
-
}
|
|
1434
|
-
return sanitized;
|
|
1435
|
-
}
|
|
1436
|
-
var init_sanitize = __esm({
|
|
1437
|
-
"src/sanitize.ts"() {
|
|
1438
|
-
"use strict";
|
|
1439
|
-
}
|
|
1440
|
-
});
|
|
1441
|
-
|
|
1442
1586
|
// src/encryption.ts
|
|
1443
|
-
import * as crypto from "crypto";
|
|
1444
|
-
import * as fs4 from "fs";
|
|
1445
|
-
import * as path4 from "path";
|
|
1446
1587
|
function resolveEncryptionKey(localDir) {
|
|
1447
1588
|
const envKey = process.env.BROWSE_ENCRYPTION_KEY;
|
|
1448
1589
|
if (envKey) {
|
|
@@ -1451,13 +1592,13 @@ function resolveEncryptionKey(localDir) {
|
|
|
1451
1592
|
}
|
|
1452
1593
|
return Buffer.from(envKey, "hex");
|
|
1453
1594
|
}
|
|
1454
|
-
const keyPath =
|
|
1455
|
-
if (
|
|
1456
|
-
const hex =
|
|
1595
|
+
const keyPath = path5.join(localDir, ".encryption-key");
|
|
1596
|
+
if (fs5.existsSync(keyPath)) {
|
|
1597
|
+
const hex = fs5.readFileSync(keyPath, "utf-8").trim();
|
|
1457
1598
|
return Buffer.from(hex, "hex");
|
|
1458
1599
|
}
|
|
1459
1600
|
const key = crypto.randomBytes(32);
|
|
1460
|
-
|
|
1601
|
+
fs5.writeFileSync(keyPath, key.toString("hex") + "\n", { mode: 384 });
|
|
1461
1602
|
return key;
|
|
1462
1603
|
}
|
|
1463
1604
|
function encrypt(plaintext, key) {
|
|
@@ -1483,9 +1624,13 @@ function decrypt(ciphertext, iv, authTag, key) {
|
|
|
1483
1624
|
]);
|
|
1484
1625
|
return decrypted.toString("utf-8");
|
|
1485
1626
|
}
|
|
1627
|
+
var crypto, fs5, path5;
|
|
1486
1628
|
var init_encryption = __esm({
|
|
1487
1629
|
"src/encryption.ts"() {
|
|
1488
1630
|
"use strict";
|
|
1631
|
+
crypto = __toESM(require("crypto"), 1);
|
|
1632
|
+
fs5 = __toESM(require("fs"), 1);
|
|
1633
|
+
path5 = __toESM(require("path"), 1);
|
|
1489
1634
|
}
|
|
1490
1635
|
});
|
|
1491
1636
|
|
|
@@ -1497,8 +1642,6 @@ __export(session_persist_exports, {
|
|
|
1497
1642
|
loadSessionState: () => loadSessionState,
|
|
1498
1643
|
saveSessionState: () => saveSessionState
|
|
1499
1644
|
});
|
|
1500
|
-
import * as fs5 from "fs";
|
|
1501
|
-
import * as path5 from "path";
|
|
1502
1645
|
async function saveSessionState(sessionDir, context, encryptionKey) {
|
|
1503
1646
|
try {
|
|
1504
1647
|
const state = await context.storageState();
|
|
@@ -1510,20 +1653,20 @@ async function saveSessionState(sessionDir, context, encryptionKey) {
|
|
|
1510
1653
|
} else {
|
|
1511
1654
|
content = json;
|
|
1512
1655
|
}
|
|
1513
|
-
|
|
1514
|
-
|
|
1656
|
+
fs6.mkdirSync(sessionDir, { recursive: true });
|
|
1657
|
+
fs6.writeFileSync(path6.join(sessionDir, STATE_FILENAME), content, { mode: 384 });
|
|
1515
1658
|
} catch (err) {
|
|
1516
1659
|
console.log(`[session-persist] Warning: failed to save state: ${err.message}`);
|
|
1517
1660
|
}
|
|
1518
1661
|
}
|
|
1519
1662
|
async function loadSessionState(sessionDir, context, encryptionKey) {
|
|
1520
|
-
const statePath =
|
|
1521
|
-
if (!
|
|
1663
|
+
const statePath = path6.join(sessionDir, STATE_FILENAME);
|
|
1664
|
+
if (!fs6.existsSync(statePath)) {
|
|
1522
1665
|
return false;
|
|
1523
1666
|
}
|
|
1524
1667
|
let stateData;
|
|
1525
1668
|
try {
|
|
1526
|
-
const raw =
|
|
1669
|
+
const raw = fs6.readFileSync(statePath, "utf-8");
|
|
1527
1670
|
const parsed = JSON.parse(raw);
|
|
1528
1671
|
if (parsed.encrypted) {
|
|
1529
1672
|
if (!encryptionKey) {
|
|
@@ -1579,24 +1722,24 @@ async function loadSessionState(sessionDir, context, encryptionKey) {
|
|
|
1579
1722
|
}
|
|
1580
1723
|
}
|
|
1581
1724
|
function hasPersistedState(sessionDir) {
|
|
1582
|
-
return
|
|
1725
|
+
return fs6.existsSync(path6.join(sessionDir, STATE_FILENAME));
|
|
1583
1726
|
}
|
|
1584
1727
|
function cleanOldStates(localDir, maxAgeDays) {
|
|
1585
1728
|
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1e3;
|
|
1586
1729
|
const now = Date.now();
|
|
1587
1730
|
let deleted = 0;
|
|
1588
|
-
const statesDir =
|
|
1589
|
-
if (
|
|
1731
|
+
const statesDir = path6.join(localDir, "states");
|
|
1732
|
+
if (fs6.existsSync(statesDir)) {
|
|
1590
1733
|
try {
|
|
1591
|
-
const entries =
|
|
1734
|
+
const entries = fs6.readdirSync(statesDir);
|
|
1592
1735
|
for (const entry of entries) {
|
|
1593
1736
|
if (!entry.endsWith(".json")) continue;
|
|
1594
|
-
const filePath =
|
|
1737
|
+
const filePath = path6.join(statesDir, entry);
|
|
1595
1738
|
try {
|
|
1596
|
-
const stat =
|
|
1739
|
+
const stat = fs6.statSync(filePath);
|
|
1597
1740
|
if (!stat.isFile()) continue;
|
|
1598
1741
|
if (now - stat.mtimeMs > maxAgeMs) {
|
|
1599
|
-
|
|
1742
|
+
fs6.unlinkSync(filePath);
|
|
1600
1743
|
deleted++;
|
|
1601
1744
|
}
|
|
1602
1745
|
} catch (_) {
|
|
@@ -1605,23 +1748,23 @@ function cleanOldStates(localDir, maxAgeDays) {
|
|
|
1605
1748
|
} catch (_) {
|
|
1606
1749
|
}
|
|
1607
1750
|
}
|
|
1608
|
-
const sessionsDir =
|
|
1609
|
-
if (
|
|
1751
|
+
const sessionsDir = path6.join(localDir, "sessions");
|
|
1752
|
+
if (fs6.existsSync(sessionsDir)) {
|
|
1610
1753
|
try {
|
|
1611
|
-
const sessionDirs =
|
|
1754
|
+
const sessionDirs = fs6.readdirSync(sessionsDir);
|
|
1612
1755
|
for (const dir of sessionDirs) {
|
|
1613
|
-
const dirPath =
|
|
1756
|
+
const dirPath = path6.join(sessionsDir, dir);
|
|
1614
1757
|
try {
|
|
1615
|
-
const dirStat =
|
|
1758
|
+
const dirStat = fs6.statSync(dirPath);
|
|
1616
1759
|
if (!dirStat.isDirectory()) continue;
|
|
1617
1760
|
} catch (_) {
|
|
1618
1761
|
continue;
|
|
1619
1762
|
}
|
|
1620
|
-
const statePath =
|
|
1763
|
+
const statePath = path6.join(dirPath, STATE_FILENAME);
|
|
1621
1764
|
try {
|
|
1622
|
-
const stat =
|
|
1765
|
+
const stat = fs6.statSync(statePath);
|
|
1623
1766
|
if (now - stat.mtimeMs > maxAgeMs) {
|
|
1624
|
-
|
|
1767
|
+
fs6.unlinkSync(statePath);
|
|
1625
1768
|
deleted++;
|
|
1626
1769
|
}
|
|
1627
1770
|
} catch (_) {
|
|
@@ -1632,19 +1775,19 @@ function cleanOldStates(localDir, maxAgeDays) {
|
|
|
1632
1775
|
}
|
|
1633
1776
|
return { deleted };
|
|
1634
1777
|
}
|
|
1635
|
-
var STATE_FILENAME;
|
|
1778
|
+
var fs6, path6, STATE_FILENAME;
|
|
1636
1779
|
var init_session_persist = __esm({
|
|
1637
1780
|
"src/session-persist.ts"() {
|
|
1638
1781
|
"use strict";
|
|
1782
|
+
fs6 = __toESM(require("fs"), 1);
|
|
1783
|
+
path6 = __toESM(require("path"), 1);
|
|
1639
1784
|
init_encryption();
|
|
1640
1785
|
STATE_FILENAME = "state.json";
|
|
1641
1786
|
}
|
|
1642
1787
|
});
|
|
1643
1788
|
|
|
1644
1789
|
// src/session-manager.ts
|
|
1645
|
-
|
|
1646
|
-
import * as path6 from "path";
|
|
1647
|
-
var SessionManager;
|
|
1790
|
+
var fs7, path7, SessionManager;
|
|
1648
1791
|
var init_session_manager = __esm({
|
|
1649
1792
|
"src/session-manager.ts"() {
|
|
1650
1793
|
"use strict";
|
|
@@ -1654,6 +1797,8 @@ var init_session_manager = __esm({
|
|
|
1654
1797
|
init_sanitize();
|
|
1655
1798
|
init_session_persist();
|
|
1656
1799
|
init_encryption();
|
|
1800
|
+
fs7 = __toESM(require("fs"), 1);
|
|
1801
|
+
path7 = __toESM(require("path"), 1);
|
|
1657
1802
|
SessionManager = class {
|
|
1658
1803
|
sessions = /* @__PURE__ */ new Map();
|
|
1659
1804
|
browser;
|
|
@@ -1706,8 +1851,8 @@ var init_session_manager = __esm({
|
|
|
1706
1851
|
}
|
|
1707
1852
|
return session;
|
|
1708
1853
|
}
|
|
1709
|
-
const outputDir =
|
|
1710
|
-
|
|
1854
|
+
const outputDir = path7.join(this.localDir, "sessions", sanitizeName(sessionId));
|
|
1855
|
+
fs7.mkdirSync(outputDir, { recursive: true });
|
|
1711
1856
|
const buffers = new SessionBuffers();
|
|
1712
1857
|
const manager = new BrowserManager(buffers);
|
|
1713
1858
|
await manager.launchWithBrowser(this.browser);
|
|
@@ -1843,7 +1988,6 @@ var read_exports = {};
|
|
|
1843
1988
|
__export(read_exports, {
|
|
1844
1989
|
handleReadCommand: () => handleReadCommand
|
|
1845
1990
|
});
|
|
1846
|
-
import * as fs7 from "fs";
|
|
1847
1991
|
async function handleReadCommand(command, args, bm, buffers) {
|
|
1848
1992
|
const page = bm.getPage();
|
|
1849
1993
|
const evalCtx = await bm.getFrameContext() || page;
|
|
@@ -1938,8 +2082,8 @@ async function handleReadCommand(command, args, bm, buffers) {
|
|
|
1938
2082
|
case "eval": {
|
|
1939
2083
|
const filePath = args[0];
|
|
1940
2084
|
if (!filePath) throw new Error("Usage: browse eval <js-file>");
|
|
1941
|
-
if (!
|
|
1942
|
-
const code =
|
|
2085
|
+
if (!fs8.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
|
2086
|
+
const code = fs8.readFileSync(filePath, "utf-8");
|
|
1943
2087
|
const result = await evalCtx.evaluate(code);
|
|
1944
2088
|
return typeof result === "object" ? JSON.stringify(result, null, 2) : String(result ?? "");
|
|
1945
2089
|
}
|
|
@@ -2177,11 +2321,13 @@ async function handleReadCommand(command, args, bm, buffers) {
|
|
|
2177
2321
|
throw new Error(`Unknown read command: ${command}`);
|
|
2178
2322
|
}
|
|
2179
2323
|
}
|
|
2324
|
+
var fs8;
|
|
2180
2325
|
var init_read = __esm({
|
|
2181
2326
|
"src/commands/read.ts"() {
|
|
2182
2327
|
"use strict";
|
|
2183
2328
|
init_browser_manager();
|
|
2184
2329
|
init_constants();
|
|
2330
|
+
fs8 = __toESM(require("fs"), 1);
|
|
2185
2331
|
}
|
|
2186
2332
|
});
|
|
2187
2333
|
|
|
@@ -2190,7 +2336,6 @@ var write_exports = {};
|
|
|
2190
2336
|
__export(write_exports, {
|
|
2191
2337
|
handleWriteCommand: () => handleWriteCommand
|
|
2192
2338
|
});
|
|
2193
|
-
import * as fs8 from "fs";
|
|
2194
2339
|
async function rebuildRoutes(context, bm, domainFilter) {
|
|
2195
2340
|
await context.unrouteAll();
|
|
2196
2341
|
for (const r of bm.getUserRoutes()) {
|
|
@@ -2394,14 +2539,14 @@ async function handleWriteCommand(command, args, bm, domainFilter) {
|
|
|
2394
2539
|
const file = args[1];
|
|
2395
2540
|
if (!file) throw new Error("Usage: browse cookie export <file>");
|
|
2396
2541
|
const cookies = await page.context().cookies();
|
|
2397
|
-
|
|
2542
|
+
fs9.writeFileSync(file, JSON.stringify(cookies, null, 2));
|
|
2398
2543
|
return `Exported ${cookies.length} cookie(s) to ${file}`;
|
|
2399
2544
|
}
|
|
2400
2545
|
if (cookieStr === "import") {
|
|
2401
2546
|
const file = args[1];
|
|
2402
2547
|
if (!file) throw new Error("Usage: browse cookie import <file>");
|
|
2403
|
-
if (!
|
|
2404
|
-
const cookies = JSON.parse(
|
|
2548
|
+
if (!fs9.existsSync(file)) throw new Error(`File not found: ${file}`);
|
|
2549
|
+
const cookies = JSON.parse(fs9.readFileSync(file, "utf-8"));
|
|
2405
2550
|
if (!Array.isArray(cookies)) throw new Error("Cookie file must contain a JSON array of cookie objects");
|
|
2406
2551
|
await page.context().addCookies(cookies);
|
|
2407
2552
|
return `Imported ${cookies.length} cookie(s) from ${file}`;
|
|
@@ -2468,7 +2613,7 @@ Note: Cookies and tab URLs preserved. localStorage/sessionStorage were reset (Pl
|
|
|
2468
2613
|
const [selector, ...filePaths] = args;
|
|
2469
2614
|
if (!selector || filePaths.length === 0) throw new Error("Usage: browse upload <selector> <file1> [file2] ...");
|
|
2470
2615
|
for (const fp of filePaths) {
|
|
2471
|
-
if (!
|
|
2616
|
+
if (!fs9.existsSync(fp)) throw new Error(`File not found: ${fp}`);
|
|
2472
2617
|
}
|
|
2473
2618
|
const resolved = bm.resolveRef(selector);
|
|
2474
2619
|
if ("locator" in resolved) {
|
|
@@ -2804,11 +2949,13 @@ Note: Cookies and tab URLs preserved. localStorage/sessionStorage were reset (Pl
|
|
|
2804
2949
|
throw new Error(`Unknown write command: ${command}`);
|
|
2805
2950
|
}
|
|
2806
2951
|
}
|
|
2952
|
+
var fs9;
|
|
2807
2953
|
var init_write = __esm({
|
|
2808
2954
|
"src/commands/write.ts"() {
|
|
2809
2955
|
"use strict";
|
|
2810
2956
|
init_browser_manager();
|
|
2811
2957
|
init_constants();
|
|
2958
|
+
fs9 = __toESM(require("fs"), 1);
|
|
2812
2959
|
}
|
|
2813
2960
|
});
|
|
2814
2961
|
|
|
@@ -3535,11 +3682,11 @@ var init_lib = __esm({
|
|
|
3535
3682
|
}
|
|
3536
3683
|
}
|
|
3537
3684
|
},
|
|
3538
|
-
addToPath: function addToPath(
|
|
3539
|
-
var last =
|
|
3685
|
+
addToPath: function addToPath(path13, added, removed, oldPosInc, options) {
|
|
3686
|
+
var last = path13.lastComponent;
|
|
3540
3687
|
if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
|
|
3541
3688
|
return {
|
|
3542
|
-
oldPos:
|
|
3689
|
+
oldPos: path13.oldPos + oldPosInc,
|
|
3543
3690
|
lastComponent: {
|
|
3544
3691
|
count: last.count + 1,
|
|
3545
3692
|
added,
|
|
@@ -3549,7 +3696,7 @@ var init_lib = __esm({
|
|
|
3549
3696
|
};
|
|
3550
3697
|
} else {
|
|
3551
3698
|
return {
|
|
3552
|
-
oldPos:
|
|
3699
|
+
oldPos: path13.oldPos + oldPosInc,
|
|
3553
3700
|
lastComponent: {
|
|
3554
3701
|
count: 1,
|
|
3555
3702
|
added,
|
|
@@ -3607,7 +3754,7 @@ var init_lib = __esm({
|
|
|
3607
3754
|
tokenize: function tokenize(value) {
|
|
3608
3755
|
return Array.from(value);
|
|
3609
3756
|
},
|
|
3610
|
-
join: function
|
|
3757
|
+
join: function join9(chars) {
|
|
3611
3758
|
return chars.join("");
|
|
3612
3759
|
},
|
|
3613
3760
|
postProcess: function postProcess(changeObjects) {
|
|
@@ -3772,23 +3919,23 @@ var policy_exports = {};
|
|
|
3772
3919
|
__export(policy_exports, {
|
|
3773
3920
|
PolicyChecker: () => PolicyChecker
|
|
3774
3921
|
});
|
|
3775
|
-
import * as fs9 from "fs";
|
|
3776
|
-
import * as path7 from "path";
|
|
3777
3922
|
function findFileUpward(filename) {
|
|
3778
3923
|
let dir = process.cwd();
|
|
3779
3924
|
for (let i = 0; i < 20; i++) {
|
|
3780
|
-
const candidate =
|
|
3781
|
-
if (
|
|
3782
|
-
const parent =
|
|
3925
|
+
const candidate = path8.join(dir, filename);
|
|
3926
|
+
if (fs10.existsSync(candidate)) return candidate;
|
|
3927
|
+
const parent = path8.dirname(dir);
|
|
3783
3928
|
if (parent === dir) break;
|
|
3784
3929
|
dir = parent;
|
|
3785
3930
|
}
|
|
3786
3931
|
return null;
|
|
3787
3932
|
}
|
|
3788
|
-
var PolicyChecker;
|
|
3933
|
+
var fs10, path8, PolicyChecker;
|
|
3789
3934
|
var init_policy = __esm({
|
|
3790
3935
|
"src/policy.ts"() {
|
|
3791
3936
|
"use strict";
|
|
3937
|
+
fs10 = __toESM(require("fs"), 1);
|
|
3938
|
+
path8 = __toESM(require("path"), 1);
|
|
3792
3939
|
PolicyChecker = class {
|
|
3793
3940
|
filePath = null;
|
|
3794
3941
|
lastMtime = 0;
|
|
@@ -3807,10 +3954,10 @@ var init_policy = __esm({
|
|
|
3807
3954
|
reload() {
|
|
3808
3955
|
if (!this.filePath) return;
|
|
3809
3956
|
try {
|
|
3810
|
-
const stat =
|
|
3957
|
+
const stat = fs10.statSync(this.filePath);
|
|
3811
3958
|
if (stat.mtimeMs === this.lastMtime) return;
|
|
3812
3959
|
this.lastMtime = stat.mtimeMs;
|
|
3813
|
-
const raw =
|
|
3960
|
+
const raw = fs10.readFileSync(this.filePath, "utf-8");
|
|
3814
3961
|
this.policy = JSON.parse(raw);
|
|
3815
3962
|
} catch {
|
|
3816
3963
|
}
|
|
@@ -3841,7 +3988,6 @@ __export(png_compare_exports, {
|
|
|
3841
3988
|
encodePNG: () => encodePNG,
|
|
3842
3989
|
generateDiffImage: () => generateDiffImage
|
|
3843
3990
|
});
|
|
3844
|
-
import * as zlib from "zlib";
|
|
3845
3991
|
function decodePNG(buf) {
|
|
3846
3992
|
for (let i = 0; i < 8; i++) {
|
|
3847
3993
|
if (buf[i] !== PNG_MAGIC[i]) throw new Error("Not a valid PNG file");
|
|
@@ -4016,10 +4162,11 @@ function compareScreenshots(baselineBuf, currentBuf, thresholdPct = 0.1, colorTh
|
|
|
4016
4162
|
}
|
|
4017
4163
|
return result;
|
|
4018
4164
|
}
|
|
4019
|
-
var PNG_MAGIC;
|
|
4165
|
+
var zlib, PNG_MAGIC;
|
|
4020
4166
|
var init_png_compare = __esm({
|
|
4021
4167
|
"src/png-compare.ts"() {
|
|
4022
4168
|
"use strict";
|
|
4169
|
+
zlib = __toESM(require("zlib"), 1);
|
|
4023
4170
|
PNG_MAGIC = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
4024
4171
|
}
|
|
4025
4172
|
});
|
|
@@ -4029,8 +4176,6 @@ var auth_vault_exports = {};
|
|
|
4029
4176
|
__export(auth_vault_exports, {
|
|
4030
4177
|
AuthVault: () => AuthVault
|
|
4031
4178
|
});
|
|
4032
|
-
import * as fs10 from "fs";
|
|
4033
|
-
import * as path8 from "path";
|
|
4034
4179
|
async function autoDetectSelector(page, field) {
|
|
4035
4180
|
if (field === "username") {
|
|
4036
4181
|
const candidates2 = [
|
|
@@ -4077,10 +4222,12 @@ async function autoDetectSelector(page, field) {
|
|
|
4077
4222
|
}
|
|
4078
4223
|
throw new Error("Could not auto-detect submit button.");
|
|
4079
4224
|
}
|
|
4080
|
-
var AuthVault;
|
|
4225
|
+
var fs11, path9, AuthVault;
|
|
4081
4226
|
var init_auth_vault = __esm({
|
|
4082
4227
|
"src/auth-vault.ts"() {
|
|
4083
4228
|
"use strict";
|
|
4229
|
+
fs11 = __toESM(require("fs"), 1);
|
|
4230
|
+
path9 = __toESM(require("path"), 1);
|
|
4084
4231
|
init_constants();
|
|
4085
4232
|
init_encryption();
|
|
4086
4233
|
init_sanitize();
|
|
@@ -4088,11 +4235,11 @@ var init_auth_vault = __esm({
|
|
|
4088
4235
|
authDir;
|
|
4089
4236
|
encryptionKey;
|
|
4090
4237
|
constructor(localDir) {
|
|
4091
|
-
this.authDir =
|
|
4238
|
+
this.authDir = path9.join(localDir, "auth");
|
|
4092
4239
|
this.encryptionKey = resolveEncryptionKey(localDir);
|
|
4093
4240
|
}
|
|
4094
4241
|
save(name, url, username, password, selectors) {
|
|
4095
|
-
|
|
4242
|
+
fs11.mkdirSync(this.authDir, { recursive: true });
|
|
4096
4243
|
const { ciphertext, iv, authTag } = encrypt(password, this.encryptionKey);
|
|
4097
4244
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4098
4245
|
const credential = {
|
|
@@ -4109,15 +4256,15 @@ var init_auth_vault = __esm({
|
|
|
4109
4256
|
createdAt: now,
|
|
4110
4257
|
updatedAt: now
|
|
4111
4258
|
};
|
|
4112
|
-
const filePath =
|
|
4113
|
-
|
|
4259
|
+
const filePath = path9.join(this.authDir, `${sanitizeName(name)}.json`);
|
|
4260
|
+
fs11.writeFileSync(filePath, JSON.stringify(credential, null, 2), { mode: 384 });
|
|
4114
4261
|
}
|
|
4115
4262
|
load(name) {
|
|
4116
|
-
const filePath =
|
|
4117
|
-
if (!
|
|
4263
|
+
const filePath = path9.join(this.authDir, `${sanitizeName(name)}.json`);
|
|
4264
|
+
if (!fs11.existsSync(filePath)) {
|
|
4118
4265
|
throw new Error(`Credential "${name}" not found. Run "browse auth list" to see saved credentials.`);
|
|
4119
4266
|
}
|
|
4120
|
-
return JSON.parse(
|
|
4267
|
+
return JSON.parse(fs11.readFileSync(filePath, "utf-8"));
|
|
4121
4268
|
}
|
|
4122
4269
|
async login(name, bm) {
|
|
4123
4270
|
const cred = this.load(name);
|
|
@@ -4138,11 +4285,11 @@ var init_auth_vault = __esm({
|
|
|
4138
4285
|
return `Logged in as ${cred.username} at ${page.url()}`;
|
|
4139
4286
|
}
|
|
4140
4287
|
list() {
|
|
4141
|
-
if (!
|
|
4142
|
-
const files =
|
|
4288
|
+
if (!fs11.existsSync(this.authDir)) return [];
|
|
4289
|
+
const files = fs11.readdirSync(this.authDir).filter((f) => f.endsWith(".json"));
|
|
4143
4290
|
return files.map((f) => {
|
|
4144
4291
|
try {
|
|
4145
|
-
const data = JSON.parse(
|
|
4292
|
+
const data = JSON.parse(fs11.readFileSync(path9.join(this.authDir, f), "utf-8"));
|
|
4146
4293
|
return {
|
|
4147
4294
|
name: data.name,
|
|
4148
4295
|
url: data.url,
|
|
@@ -4156,11 +4303,11 @@ var init_auth_vault = __esm({
|
|
|
4156
4303
|
}).filter(Boolean);
|
|
4157
4304
|
}
|
|
4158
4305
|
delete(name) {
|
|
4159
|
-
const filePath =
|
|
4160
|
-
if (!
|
|
4306
|
+
const filePath = path9.join(this.authDir, `${sanitizeName(name)}.json`);
|
|
4307
|
+
if (!fs11.existsSync(filePath)) {
|
|
4161
4308
|
throw new Error(`Credential "${name}" not found.`);
|
|
4162
4309
|
}
|
|
4163
|
-
|
|
4310
|
+
fs11.unlinkSync(filePath);
|
|
4164
4311
|
}
|
|
4165
4312
|
};
|
|
4166
4313
|
}
|
|
@@ -4174,18 +4321,12 @@ __export(cookie_import_exports, {
|
|
|
4174
4321
|
importCookies: () => importCookies,
|
|
4175
4322
|
listDomains: () => listDomains
|
|
4176
4323
|
});
|
|
4177
|
-
import Database from "better-sqlite3";
|
|
4178
|
-
import { spawn as spawn2 } from "child_process";
|
|
4179
|
-
import * as crypto2 from "crypto";
|
|
4180
|
-
import * as fs11 from "fs";
|
|
4181
|
-
import * as path9 from "path";
|
|
4182
|
-
import * as os2 from "os";
|
|
4183
4324
|
function findInstalledBrowsers() {
|
|
4184
|
-
const appSupport =
|
|
4325
|
+
const appSupport = path10.join(os2.homedir(), "Library", "Application Support");
|
|
4185
4326
|
return BROWSER_REGISTRY.filter((b) => {
|
|
4186
|
-
const dbPath =
|
|
4327
|
+
const dbPath = path10.join(appSupport, b.dataDir, "Default", "Cookies");
|
|
4187
4328
|
try {
|
|
4188
|
-
return
|
|
4329
|
+
return fs12.existsSync(dbPath);
|
|
4189
4330
|
} catch {
|
|
4190
4331
|
return false;
|
|
4191
4332
|
}
|
|
@@ -4268,9 +4409,9 @@ function validateProfile(profile) {
|
|
|
4268
4409
|
}
|
|
4269
4410
|
function getCookieDbPath(browser2, profile) {
|
|
4270
4411
|
validateProfile(profile);
|
|
4271
|
-
const appSupport =
|
|
4272
|
-
const dbPath =
|
|
4273
|
-
if (!
|
|
4412
|
+
const appSupport = path10.join(os2.homedir(), "Library", "Application Support");
|
|
4413
|
+
const dbPath = path10.join(appSupport, browser2.dataDir, profile, "Cookies");
|
|
4414
|
+
if (!fs12.existsSync(dbPath)) {
|
|
4274
4415
|
throw new CookieImportError(
|
|
4275
4416
|
`${browser2.name} is not installed (no cookie database at ${dbPath})`,
|
|
4276
4417
|
"not_installed"
|
|
@@ -4280,7 +4421,7 @@ function getCookieDbPath(browser2, profile) {
|
|
|
4280
4421
|
}
|
|
4281
4422
|
function openDb(dbPath, browserName) {
|
|
4282
4423
|
try {
|
|
4283
|
-
return new
|
|
4424
|
+
return new import_better_sqlite3.default(dbPath, { readonly: true });
|
|
4284
4425
|
} catch (err) {
|
|
4285
4426
|
if (err.message?.includes("SQLITE_BUSY") || err.message?.includes("database is locked")) {
|
|
4286
4427
|
return openDbFromCopy(dbPath, browserName);
|
|
@@ -4297,32 +4438,32 @@ function openDb(dbPath, browserName) {
|
|
|
4297
4438
|
function openDbFromCopy(dbPath, browserName) {
|
|
4298
4439
|
const tmpPath = `/tmp/browse-cookies-${browserName.toLowerCase()}-${crypto2.randomUUID()}.db`;
|
|
4299
4440
|
try {
|
|
4300
|
-
|
|
4441
|
+
fs12.copyFileSync(dbPath, tmpPath);
|
|
4301
4442
|
const walPath = dbPath + "-wal";
|
|
4302
4443
|
const shmPath = dbPath + "-shm";
|
|
4303
|
-
if (
|
|
4304
|
-
if (
|
|
4305
|
-
const db = new
|
|
4444
|
+
if (fs12.existsSync(walPath)) fs12.copyFileSync(walPath, tmpPath + "-wal");
|
|
4445
|
+
if (fs12.existsSync(shmPath)) fs12.copyFileSync(shmPath, tmpPath + "-shm");
|
|
4446
|
+
const db = new import_better_sqlite3.default(tmpPath, { readonly: true });
|
|
4306
4447
|
const origClose = db.close.bind(db);
|
|
4307
4448
|
db.close = (() => {
|
|
4308
4449
|
origClose();
|
|
4309
4450
|
try {
|
|
4310
|
-
|
|
4451
|
+
fs12.unlinkSync(tmpPath);
|
|
4311
4452
|
} catch {
|
|
4312
4453
|
}
|
|
4313
4454
|
try {
|
|
4314
|
-
|
|
4455
|
+
fs12.unlinkSync(tmpPath + "-wal");
|
|
4315
4456
|
} catch {
|
|
4316
4457
|
}
|
|
4317
4458
|
try {
|
|
4318
|
-
|
|
4459
|
+
fs12.unlinkSync(tmpPath + "-shm");
|
|
4319
4460
|
} catch {
|
|
4320
4461
|
}
|
|
4321
4462
|
});
|
|
4322
4463
|
return db;
|
|
4323
4464
|
} catch {
|
|
4324
4465
|
try {
|
|
4325
|
-
|
|
4466
|
+
fs12.unlinkSync(tmpPath);
|
|
4326
4467
|
} catch {
|
|
4327
4468
|
}
|
|
4328
4469
|
throw new CookieImportError(
|
|
@@ -4341,7 +4482,7 @@ async function getDerivedKey(browser2) {
|
|
|
4341
4482
|
return derived;
|
|
4342
4483
|
}
|
|
4343
4484
|
async function getKeychainPassword(service) {
|
|
4344
|
-
const proc =
|
|
4485
|
+
const proc = (0, import_child_process2.spawn)("security", ["find-generic-password", "-s", service, "-w"], {
|
|
4345
4486
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4346
4487
|
});
|
|
4347
4488
|
let stdout = "";
|
|
@@ -4448,10 +4589,16 @@ function mapSameSite(value) {
|
|
|
4448
4589
|
return "Lax";
|
|
4449
4590
|
}
|
|
4450
4591
|
}
|
|
4451
|
-
var CookieImportError, BROWSER_REGISTRY, keyCache, CHROMIUM_EPOCH_OFFSET;
|
|
4592
|
+
var import_better_sqlite3, import_child_process2, crypto2, fs12, path10, os2, CookieImportError, BROWSER_REGISTRY, keyCache, CHROMIUM_EPOCH_OFFSET;
|
|
4452
4593
|
var init_cookie_import = __esm({
|
|
4453
4594
|
"src/cookie-import.ts"() {
|
|
4454
4595
|
"use strict";
|
|
4596
|
+
import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
4597
|
+
import_child_process2 = require("child_process");
|
|
4598
|
+
crypto2 = __toESM(require("crypto"), 1);
|
|
4599
|
+
fs12 = __toESM(require("fs"), 1);
|
|
4600
|
+
path10 = __toESM(require("path"), 1);
|
|
4601
|
+
os2 = __toESM(require("os"), 1);
|
|
4455
4602
|
CookieImportError = class extends Error {
|
|
4456
4603
|
constructor(message, code, action) {
|
|
4457
4604
|
super(message);
|
|
@@ -4620,7 +4767,6 @@ var init_record_export = __esm({
|
|
|
4620
4767
|
});
|
|
4621
4768
|
|
|
4622
4769
|
// src/commands/meta.ts
|
|
4623
|
-
import * as fs12 from "fs";
|
|
4624
4770
|
async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2, currentSession) {
|
|
4625
4771
|
switch (command) {
|
|
4626
4772
|
// ─── Tabs ──────────────────────────────────────────
|
|
@@ -4702,7 +4848,7 @@ async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2,
|
|
|
4702
4848
|
const lines = entries.map(
|
|
4703
4849
|
(e) => `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}`
|
|
4704
4850
|
).join("\n") + "\n";
|
|
4705
|
-
|
|
4851
|
+
fs13.appendFileSync(consolePath, lines);
|
|
4706
4852
|
buffers.lastConsoleFlushed = buffers.consoleTotalAdded;
|
|
4707
4853
|
}
|
|
4708
4854
|
const newNetworkCount = buffers.networkTotalAdded - buffers.lastNetworkFlushed;
|
|
@@ -4712,7 +4858,7 @@ async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2,
|
|
|
4712
4858
|
const lines = entries.map(
|
|
4713
4859
|
(e) => `[${new Date(e.timestamp).toISOString()}] ${e.method} ${e.url} \u2192 ${e.status || "pending"} (${e.duration || "?"}ms, ${e.size || "?"}B)`
|
|
4714
4860
|
).join("\n") + "\n";
|
|
4715
|
-
|
|
4861
|
+
fs13.appendFileSync(networkPath, lines);
|
|
4716
4862
|
buffers.lastNetworkFlushed = buffers.networkTotalAdded;
|
|
4717
4863
|
}
|
|
4718
4864
|
}
|
|
@@ -4729,22 +4875,22 @@ async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2,
|
|
|
4729
4875
|
const statesDir = `${LOCAL_DIR}/states`;
|
|
4730
4876
|
const statePath = `${statesDir}/${name}.json`;
|
|
4731
4877
|
if (subcommand === "list") {
|
|
4732
|
-
if (!
|
|
4733
|
-
const files =
|
|
4878
|
+
if (!fs13.existsSync(statesDir)) return "(no saved states)";
|
|
4879
|
+
const files = fs13.readdirSync(statesDir).filter((f) => f.endsWith(".json"));
|
|
4734
4880
|
if (files.length === 0) return "(no saved states)";
|
|
4735
4881
|
const lines = [];
|
|
4736
4882
|
for (const file of files) {
|
|
4737
4883
|
const fp = `${statesDir}/${file}`;
|
|
4738
|
-
const stat =
|
|
4884
|
+
const stat = fs13.statSync(fp);
|
|
4739
4885
|
lines.push(` ${file.replace(".json", "")} ${stat.size}B ${new Date(stat.mtimeMs).toISOString()}`);
|
|
4740
4886
|
}
|
|
4741
4887
|
return lines.join("\n");
|
|
4742
4888
|
}
|
|
4743
4889
|
if (subcommand === "show") {
|
|
4744
|
-
if (!
|
|
4890
|
+
if (!fs13.existsSync(statePath)) {
|
|
4745
4891
|
throw new Error(`State file not found: ${statePath}`);
|
|
4746
4892
|
}
|
|
4747
|
-
const data = JSON.parse(
|
|
4893
|
+
const data = JSON.parse(fs13.readFileSync(statePath, "utf-8"));
|
|
4748
4894
|
const cookieCount = data.cookies?.length || 0;
|
|
4749
4895
|
const originCount = data.origins?.length || 0;
|
|
4750
4896
|
const storageItems = (data.origins || []).reduce((sum, o) => sum + (o.localStorage?.length || 0), 0);
|
|
@@ -4769,15 +4915,15 @@ async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2,
|
|
|
4769
4915
|
const context = bm.getContext();
|
|
4770
4916
|
if (!context) throw new Error("No browser context");
|
|
4771
4917
|
const state = await context.storageState();
|
|
4772
|
-
|
|
4773
|
-
|
|
4918
|
+
fs13.mkdirSync(statesDir, { recursive: true });
|
|
4919
|
+
fs13.writeFileSync(statePath, JSON.stringify(state, null, 2), { mode: 384 });
|
|
4774
4920
|
return `State saved: ${statePath}`;
|
|
4775
4921
|
}
|
|
4776
4922
|
if (subcommand === "load") {
|
|
4777
|
-
if (!
|
|
4923
|
+
if (!fs13.existsSync(statePath)) {
|
|
4778
4924
|
throw new Error(`State file not found: ${statePath}. Run "browse state save ${name}" first.`);
|
|
4779
4925
|
}
|
|
4780
|
-
const stateData = JSON.parse(
|
|
4926
|
+
const stateData = JSON.parse(fs13.readFileSync(statePath, "utf-8"));
|
|
4781
4927
|
const context = bm.getContext();
|
|
4782
4928
|
if (!context) throw new Error("No browser context");
|
|
4783
4929
|
const warnings = [];
|
|
@@ -4936,9 +5082,9 @@ ${legend.join("\n")}`;
|
|
|
4936
5082
|
try {
|
|
4937
5083
|
for (const vp of viewports) {
|
|
4938
5084
|
await page.setViewportSize({ width: vp.width, height: vp.height });
|
|
4939
|
-
const
|
|
4940
|
-
await page.screenshot({ path:
|
|
4941
|
-
results.push(`${vp.name} (${vp.width}x${vp.height}): ${
|
|
5085
|
+
const path13 = `${prefix}-${vp.name}.png`;
|
|
5086
|
+
await page.screenshot({ path: path13, fullPage: true });
|
|
5087
|
+
results.push(`${vp.name} (${vp.width}x${vp.height}): ${path13}`);
|
|
4942
5088
|
}
|
|
4943
5089
|
} finally {
|
|
4944
5090
|
if (originalViewport) {
|
|
@@ -5082,13 +5228,13 @@ ${legend.join("\n")}`;
|
|
|
5082
5228
|
const diffArgs = args.filter((a) => a !== "--full");
|
|
5083
5229
|
const baseline = diffArgs[0];
|
|
5084
5230
|
if (!baseline) throw new Error("Usage: browse screenshot-diff <baseline> [current] [--threshold 0.1] [--full]");
|
|
5085
|
-
if (!
|
|
5231
|
+
if (!fs13.existsSync(baseline)) throw new Error(`Baseline file not found: ${baseline}`);
|
|
5086
5232
|
let thresholdPct = 0.1;
|
|
5087
5233
|
const threshIdx = diffArgs.indexOf("--threshold");
|
|
5088
5234
|
if (threshIdx !== -1 && diffArgs[threshIdx + 1]) {
|
|
5089
5235
|
thresholdPct = parseFloat(diffArgs[threshIdx + 1]);
|
|
5090
5236
|
}
|
|
5091
|
-
const baselineBuffer =
|
|
5237
|
+
const baselineBuffer = fs13.readFileSync(baseline);
|
|
5092
5238
|
let currentBuffer;
|
|
5093
5239
|
let currentPath;
|
|
5094
5240
|
for (let i = 1; i < diffArgs.length; i++) {
|
|
@@ -5102,8 +5248,8 @@ ${legend.join("\n")}`;
|
|
|
5102
5248
|
}
|
|
5103
5249
|
}
|
|
5104
5250
|
if (currentPath) {
|
|
5105
|
-
if (!
|
|
5106
|
-
currentBuffer =
|
|
5251
|
+
if (!fs13.existsSync(currentPath)) throw new Error(`Current screenshot not found: ${currentPath}`);
|
|
5252
|
+
currentBuffer = fs13.readFileSync(currentPath);
|
|
5107
5253
|
} else {
|
|
5108
5254
|
const page = bm.getPage();
|
|
5109
5255
|
currentBuffer = await page.screenshot({ fullPage: isFullPageDiff });
|
|
@@ -5113,7 +5259,7 @@ ${legend.join("\n")}`;
|
|
|
5113
5259
|
const extIdx = baseline.lastIndexOf(".");
|
|
5114
5260
|
const diffPath = extIdx > 0 ? baseline.slice(0, extIdx) + "-diff" + baseline.slice(extIdx) : baseline + "-diff.png";
|
|
5115
5261
|
if (!result.passed && result.diffImage) {
|
|
5116
|
-
|
|
5262
|
+
fs13.writeFileSync(diffPath, result.diffImage);
|
|
5117
5263
|
}
|
|
5118
5264
|
return [
|
|
5119
5265
|
`Pixels: ${result.totalPixels}`,
|
|
@@ -5249,7 +5395,7 @@ ${legend.join("\n")}`;
|
|
|
5249
5395
|
const { formatAsHar: formatAsHar2 } = await Promise.resolve().then(() => (init_har(), har_exports));
|
|
5250
5396
|
const har = formatAsHar2(sessionBuffers.networkBuffer, recording.startTime);
|
|
5251
5397
|
const harPath = args[1] || (currentSession ? `${currentSession.outputDir}/recording.har` : `${LOCAL_DIR}/browse-recording.har`);
|
|
5252
|
-
|
|
5398
|
+
fs13.writeFileSync(harPath, JSON.stringify(har, null, 2));
|
|
5253
5399
|
const entryCount = har.log.entries.length;
|
|
5254
5400
|
return `HAR saved: ${harPath} (${entryCount} entries)`;
|
|
5255
5401
|
}
|
|
@@ -5469,18 +5615,57 @@ Manual: npm install -g @ulpi/browse`;
|
|
|
5469
5615
|
}
|
|
5470
5616
|
const filePath = args[2];
|
|
5471
5617
|
if (filePath) {
|
|
5472
|
-
|
|
5618
|
+
fs13.writeFileSync(filePath, output);
|
|
5473
5619
|
return `Exported ${steps.length} steps as ${format}: ${filePath}`;
|
|
5474
5620
|
}
|
|
5475
5621
|
return output;
|
|
5476
5622
|
}
|
|
5477
5623
|
throw new Error("Usage: browse record start | stop | status | export browse|replay [path]");
|
|
5478
5624
|
}
|
|
5625
|
+
// ─── Profile Management ────────────────────────────────
|
|
5626
|
+
case "profile": {
|
|
5627
|
+
const subcommand = args[0];
|
|
5628
|
+
if (!subcommand) throw new Error("Usage: browse profile list | delete <name> | clean [--older-than <days>]");
|
|
5629
|
+
if (subcommand === "list") {
|
|
5630
|
+
const { listProfiles: listProfiles2 } = await Promise.resolve().then(() => (init_browser_manager(), browser_manager_exports));
|
|
5631
|
+
const profiles = listProfiles2(LOCAL_DIR);
|
|
5632
|
+
if (profiles.length === 0) return "No profiles found";
|
|
5633
|
+
return profiles.map((p) => `${p.name} ${p.size} last used: ${p.lastUsed}`).join("\n");
|
|
5634
|
+
}
|
|
5635
|
+
if (subcommand === "delete") {
|
|
5636
|
+
const name = args[1];
|
|
5637
|
+
if (!name) throw new Error("Usage: browse profile delete <name>");
|
|
5638
|
+
const { deleteProfile: deleteProfile2 } = await Promise.resolve().then(() => (init_browser_manager(), browser_manager_exports));
|
|
5639
|
+
deleteProfile2(LOCAL_DIR, name);
|
|
5640
|
+
return `Profile "${name}" deleted`;
|
|
5641
|
+
}
|
|
5642
|
+
if (subcommand === "clean") {
|
|
5643
|
+
const { listProfiles: listProfiles2, deleteProfile: deleteProfile2 } = await Promise.resolve().then(() => (init_browser_manager(), browser_manager_exports));
|
|
5644
|
+
let maxDays = 7;
|
|
5645
|
+
const olderIdx = args.indexOf("--older-than");
|
|
5646
|
+
if (olderIdx !== -1 && args[olderIdx + 1]) {
|
|
5647
|
+
maxDays = parseInt(args[olderIdx + 1], 10);
|
|
5648
|
+
if (isNaN(maxDays)) throw new Error("Usage: browse profile clean --older-than <days>");
|
|
5649
|
+
}
|
|
5650
|
+
const profiles = listProfiles2(LOCAL_DIR);
|
|
5651
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
5652
|
+
cutoff.setDate(cutoff.getDate() - maxDays);
|
|
5653
|
+
let cleaned = 0;
|
|
5654
|
+
for (const p of profiles) {
|
|
5655
|
+
if (new Date(p.lastUsed) < cutoff) {
|
|
5656
|
+
deleteProfile2(LOCAL_DIR, p.name);
|
|
5657
|
+
cleaned++;
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
return cleaned > 0 ? `Cleaned ${cleaned} profile(s) older than ${maxDays} days` : "No profiles to clean";
|
|
5661
|
+
}
|
|
5662
|
+
throw new Error("Usage: browse profile list | delete <name> | clean [--older-than <days>]");
|
|
5663
|
+
}
|
|
5479
5664
|
default:
|
|
5480
5665
|
throw new Error(`Unknown meta command: ${command}`);
|
|
5481
5666
|
}
|
|
5482
5667
|
}
|
|
5483
|
-
var LOCAL_DIR;
|
|
5668
|
+
var fs13, LOCAL_DIR;
|
|
5484
5669
|
var init_meta = __esm({
|
|
5485
5670
|
"src/commands/meta.ts"() {
|
|
5486
5671
|
"use strict";
|
|
@@ -5488,18 +5673,13 @@ var init_meta = __esm({
|
|
|
5488
5673
|
init_constants();
|
|
5489
5674
|
init_sanitize();
|
|
5490
5675
|
init_lib();
|
|
5676
|
+
fs13 = __toESM(require("fs"), 1);
|
|
5491
5677
|
LOCAL_DIR = process.env.BROWSE_LOCAL_DIR || "/tmp";
|
|
5492
5678
|
}
|
|
5493
5679
|
});
|
|
5494
5680
|
|
|
5495
5681
|
// src/server.ts
|
|
5496
5682
|
var server_exports = {};
|
|
5497
|
-
import * as fs13 from "fs";
|
|
5498
|
-
import * as path10 from "path";
|
|
5499
|
-
import * as crypto3 from "crypto";
|
|
5500
|
-
import * as http from "http";
|
|
5501
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5502
|
-
import * as net2 from "net";
|
|
5503
5683
|
function nodeServe(opts) {
|
|
5504
5684
|
const server = http.createServer(async (nodeReq, nodeRes) => {
|
|
5505
5685
|
try {
|
|
@@ -5531,9 +5711,13 @@ function validateAuth(req) {
|
|
|
5531
5711
|
const header = req.headers.get("authorization");
|
|
5532
5712
|
return header === `Bearer ${AUTH_TOKEN}`;
|
|
5533
5713
|
}
|
|
5534
|
-
function flushAllBuffers(
|
|
5535
|
-
|
|
5536
|
-
|
|
5714
|
+
function flushAllBuffers(sm, final = false) {
|
|
5715
|
+
if (sm) {
|
|
5716
|
+
for (const session of sm.getAllSessions()) {
|
|
5717
|
+
flushSessionBuffers(session, final);
|
|
5718
|
+
}
|
|
5719
|
+
} else if (profileSession) {
|
|
5720
|
+
flushSessionBuffers(profileSession, final);
|
|
5537
5721
|
}
|
|
5538
5722
|
}
|
|
5539
5723
|
function flushSessionBuffers(session, final) {
|
|
@@ -5547,7 +5731,7 @@ function flushSessionBuffers(session, final) {
|
|
|
5547
5731
|
const lines = newEntries.map(
|
|
5548
5732
|
(e) => `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}`
|
|
5549
5733
|
).join("\n") + "\n";
|
|
5550
|
-
|
|
5734
|
+
fs14.appendFileSync(consolePath, lines);
|
|
5551
5735
|
buffers.lastConsoleFlushed = buffers.consoleTotalAdded;
|
|
5552
5736
|
}
|
|
5553
5737
|
let newNetworkCount = buffers.networkTotalAdded - buffers.lastNetworkFlushed;
|
|
@@ -5572,7 +5756,7 @@ function flushSessionBuffers(session, final) {
|
|
|
5572
5756
|
const lines = prefix.map(
|
|
5573
5757
|
(e) => `[${new Date(e.timestamp).toISOString()}] ${e.method} ${e.url} \u2192 ${e.status || "pending"} (${e.duration || "?"}ms, ${e.size || "?"}B)`
|
|
5574
5758
|
).join("\n") + "\n";
|
|
5575
|
-
|
|
5759
|
+
fs14.appendFileSync(networkPath, lines);
|
|
5576
5760
|
buffers.lastNetworkFlushed += prefixLen;
|
|
5577
5761
|
}
|
|
5578
5762
|
}
|
|
@@ -5686,7 +5870,7 @@ async function handleCommand(body, session, opts) {
|
|
|
5686
5870
|
} else if (WRITE_COMMANDS.has(command)) {
|
|
5687
5871
|
result = await handleWriteCommand(command, args, session.manager, session.domainFilter);
|
|
5688
5872
|
} else if (META_COMMANDS.has(command)) {
|
|
5689
|
-
result = await handleMetaCommand(command, args, session.manager, shutdown, sessionManager, session);
|
|
5873
|
+
result = await handleMetaCommand(command, args, session.manager, shutdown, sessionManager ?? void 0, session);
|
|
5690
5874
|
} else {
|
|
5691
5875
|
const error = `Unknown command: ${command}`;
|
|
5692
5876
|
const hint = `Available commands: ${[...READ_COMMANDS, ...WRITE_COMMANDS, ...META_COMMANDS].sort().join(", ")}`;
|
|
@@ -5745,18 +5929,23 @@ async function shutdown() {
|
|
|
5745
5929
|
clearInterval(flushInterval);
|
|
5746
5930
|
clearInterval(sessionCleanupInterval);
|
|
5747
5931
|
flushAllBuffers(sessionManager, true);
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
browser.removeAllListeners("disconnected");
|
|
5751
|
-
await browser.close().catch(() => {
|
|
5932
|
+
if (profileSession) {
|
|
5933
|
+
await profileSession.manager.close().catch(() => {
|
|
5752
5934
|
});
|
|
5935
|
+
} else if (sessionManager) {
|
|
5936
|
+
await sessionManager.closeAll();
|
|
5937
|
+
if (browser && !isRemoteBrowser) {
|
|
5938
|
+
browser.removeAllListeners("disconnected");
|
|
5939
|
+
await browser.close().catch(() => {
|
|
5940
|
+
});
|
|
5941
|
+
}
|
|
5753
5942
|
}
|
|
5754
5943
|
await activeRuntime?.close?.().catch(() => {
|
|
5755
5944
|
});
|
|
5756
5945
|
try {
|
|
5757
|
-
const currentState = JSON.parse(
|
|
5946
|
+
const currentState = JSON.parse(fs14.readFileSync(STATE_FILE, "utf-8"));
|
|
5758
5947
|
if (currentState.pid === process.pid || currentState.token === AUTH_TOKEN) {
|
|
5759
|
-
|
|
5948
|
+
fs14.unlinkSync(STATE_FILE);
|
|
5760
5949
|
}
|
|
5761
5950
|
} catch {
|
|
5762
5951
|
}
|
|
@@ -5768,38 +5957,65 @@ async function start() {
|
|
|
5768
5957
|
const runtime = await getRuntime(runtimeName);
|
|
5769
5958
|
activeRuntime = runtime;
|
|
5770
5959
|
console.log(`[browse] Runtime: ${runtime.name}`);
|
|
5771
|
-
const
|
|
5772
|
-
if (
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5960
|
+
const profileName = process.env.BROWSE_PROFILE;
|
|
5961
|
+
if (profileName) {
|
|
5962
|
+
const { BrowserManager: BrowserManager2, getProfileDir: getProfileDir2 } = await Promise.resolve().then(() => (init_browser_manager(), browser_manager_exports));
|
|
5963
|
+
const { SessionBuffers: SessionBuffers2 } = await Promise.resolve().then(() => (init_buffers(), buffers_exports));
|
|
5964
|
+
const profileDir = getProfileDir2(LOCAL_DIR2, profileName);
|
|
5965
|
+
fs14.mkdirSync(profileDir, { recursive: true });
|
|
5966
|
+
const bm = new BrowserManager2();
|
|
5967
|
+
await bm.launchPersistent(profileDir, () => {
|
|
5779
5968
|
if (isShuttingDown) return;
|
|
5780
|
-
console.error("[browse]
|
|
5969
|
+
console.error("[browse] Chromium disconnected (profile mode). Shutting down.");
|
|
5781
5970
|
shutdown();
|
|
5782
5971
|
});
|
|
5972
|
+
const outputDir = path11.join(LOCAL_DIR2, "sessions", profileName);
|
|
5973
|
+
fs14.mkdirSync(outputDir, { recursive: true });
|
|
5974
|
+
profileSession = {
|
|
5975
|
+
id: profileName,
|
|
5976
|
+
manager: bm,
|
|
5977
|
+
buffers: new SessionBuffers2(),
|
|
5978
|
+
domainFilter: null,
|
|
5979
|
+
recording: null,
|
|
5980
|
+
outputDir,
|
|
5981
|
+
lastActivity: Date.now(),
|
|
5982
|
+
createdAt: Date.now()
|
|
5983
|
+
};
|
|
5984
|
+
console.log(`[browse] Profile mode: "${profileName}" (${profileDir})`);
|
|
5783
5985
|
} else {
|
|
5784
|
-
const
|
|
5785
|
-
if (
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
if (
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5986
|
+
const cdpUrl = process.env.BROWSE_CDP_URL;
|
|
5987
|
+
if (cdpUrl) {
|
|
5988
|
+
browser = await runtime.chromium.connectOverCDP(cdpUrl);
|
|
5989
|
+
isRemoteBrowser = true;
|
|
5990
|
+
console.log(`[browse] Connected to remote Chrome via CDP: ${cdpUrl}`);
|
|
5991
|
+
} else if (runtime.browser) {
|
|
5992
|
+
browser = runtime.browser;
|
|
5993
|
+
browser.on("disconnected", () => {
|
|
5994
|
+
if (isShuttingDown) return;
|
|
5995
|
+
console.error("[browse] Browser disconnected. Shutting down.");
|
|
5996
|
+
shutdown();
|
|
5997
|
+
});
|
|
5998
|
+
} else {
|
|
5999
|
+
const launchOptions = { headless: process.env.BROWSE_HEADED !== "1" };
|
|
6000
|
+
if (DEBUG_PORT > 0) {
|
|
6001
|
+
launchOptions.args = [`--remote-debugging-port=${DEBUG_PORT}`];
|
|
5793
6002
|
}
|
|
6003
|
+
const proxyServer = process.env.BROWSE_PROXY;
|
|
6004
|
+
if (proxyServer) {
|
|
6005
|
+
launchOptions.proxy = { server: proxyServer };
|
|
6006
|
+
if (process.env.BROWSE_PROXY_BYPASS) {
|
|
6007
|
+
launchOptions.proxy.bypass = process.env.BROWSE_PROXY_BYPASS;
|
|
6008
|
+
}
|
|
6009
|
+
}
|
|
6010
|
+
browser = await runtime.chromium.launch(launchOptions);
|
|
6011
|
+
browser.on("disconnected", () => {
|
|
6012
|
+
if (isShuttingDown) return;
|
|
6013
|
+
console.error("[browse] Chromium disconnected. Shutting down.");
|
|
6014
|
+
shutdown();
|
|
6015
|
+
});
|
|
5794
6016
|
}
|
|
5795
|
-
|
|
5796
|
-
browser.on("disconnected", () => {
|
|
5797
|
-
if (isShuttingDown) return;
|
|
5798
|
-
console.error("[browse] Chromium disconnected. Shutting down.");
|
|
5799
|
-
shutdown();
|
|
5800
|
-
});
|
|
6017
|
+
sessionManager = new SessionManager(browser, LOCAL_DIR2);
|
|
5801
6018
|
}
|
|
5802
|
-
sessionManager = new SessionManager(browser, LOCAL_DIR2);
|
|
5803
6019
|
const startTime = Date.now();
|
|
5804
6020
|
const server = nodeServe({
|
|
5805
6021
|
port,
|
|
@@ -5807,11 +6023,20 @@ async function start() {
|
|
|
5807
6023
|
fetch: async (req) => {
|
|
5808
6024
|
const url = new URL(req.url);
|
|
5809
6025
|
if (url.pathname === "/health") {
|
|
5810
|
-
|
|
6026
|
+
let healthy;
|
|
6027
|
+
let sessionCount;
|
|
6028
|
+
if (profileSession) {
|
|
6029
|
+
healthy = !isShuttingDown && !!profileSession.manager.getContext();
|
|
6030
|
+
sessionCount = 1;
|
|
6031
|
+
} else {
|
|
6032
|
+
healthy = !isShuttingDown && !!browser && browser.isConnected();
|
|
6033
|
+
sessionCount = sessionManager ? sessionManager.getSessionCount() : 0;
|
|
6034
|
+
}
|
|
5811
6035
|
return new Response(JSON.stringify({
|
|
5812
6036
|
status: healthy ? "healthy" : "unhealthy",
|
|
5813
6037
|
uptime: Math.floor((Date.now() - startTime) / 1e3),
|
|
5814
|
-
sessions:
|
|
6038
|
+
sessions: sessionCount,
|
|
6039
|
+
...profileName ? { profile: profileName } : {}
|
|
5815
6040
|
}), {
|
|
5816
6041
|
status: 200,
|
|
5817
6042
|
headers: { "Content-Type": "application/json" }
|
|
@@ -5825,15 +6050,21 @@ async function start() {
|
|
|
5825
6050
|
}
|
|
5826
6051
|
if (url.pathname === "/command" && req.method === "POST") {
|
|
5827
6052
|
const body = await req.json();
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
6053
|
+
let session;
|
|
6054
|
+
if (profileSession) {
|
|
6055
|
+
session = profileSession;
|
|
6056
|
+
session.lastActivity = Date.now();
|
|
6057
|
+
} else {
|
|
6058
|
+
const sessionId = req.headers.get("x-browse-session") || "default";
|
|
6059
|
+
const allowedDomains = req.headers.get("x-browse-allowed-domains") || void 0;
|
|
6060
|
+
session = await sessionManager.getOrCreate(sessionId, allowedDomains);
|
|
6061
|
+
}
|
|
5831
6062
|
const stateFilePath = req.headers.get("x-browse-state");
|
|
5832
6063
|
if (stateFilePath) {
|
|
5833
6064
|
const context = session.manager.getContext();
|
|
5834
6065
|
if (context) {
|
|
5835
6066
|
try {
|
|
5836
|
-
const stateData = JSON.parse(
|
|
6067
|
+
const stateData = JSON.parse(fs14.readFileSync(stateFilePath, "utf-8"));
|
|
5837
6068
|
if (stateData.cookies?.length) {
|
|
5838
6069
|
await context.addCookies(stateData.cookies);
|
|
5839
6070
|
}
|
|
@@ -5860,17 +6091,20 @@ async function start() {
|
|
|
5860
6091
|
port,
|
|
5861
6092
|
token: AUTH_TOKEN,
|
|
5862
6093
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5863
|
-
serverPath:
|
|
6094
|
+
serverPath: path11.resolve(path11.dirname((0, import_url2.fileURLToPath)(__import_meta_url)), "server.ts")
|
|
5864
6095
|
};
|
|
6096
|
+
if (profileName) {
|
|
6097
|
+
state.profile = profileName;
|
|
6098
|
+
}
|
|
5865
6099
|
if (DEBUG_PORT > 0) {
|
|
5866
6100
|
state.debugPort = DEBUG_PORT;
|
|
5867
6101
|
}
|
|
5868
|
-
|
|
6102
|
+
fs14.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), { mode: 384 });
|
|
5869
6103
|
console.log(`[browse] Server running on http://127.0.0.1:${port} (PID: ${process.pid})`);
|
|
5870
6104
|
console.log(`[browse] State file: ${STATE_FILE}`);
|
|
5871
6105
|
console.log(`[browse] Idle timeout: ${IDLE_TIMEOUT_MS / 1e3}s`);
|
|
5872
6106
|
}
|
|
5873
|
-
var AUTH_TOKEN, DEBUG_PORT, BROWSE_PORT, BROWSE_INSTANCE, INSTANCE_SUFFIX, LOCAL_DIR2, STATE_FILE, IDLE_TIMEOUT_MS, sessionManager, browser, activeRuntime, isShuttingDown, isRemoteBrowser, policyChecker, READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS, RECORDING_SKIP, PAGE_CONTENT_COMMANDS, BOUNDARY_NONCE, flushInterval, sessionCleanupInterval;
|
|
6107
|
+
var fs14, path11, crypto3, http, import_url2, net2, AUTH_TOKEN, DEBUG_PORT, BROWSE_PORT, BROWSE_INSTANCE, INSTANCE_SUFFIX, LOCAL_DIR2, STATE_FILE, IDLE_TIMEOUT_MS, sessionManager, browser, profileSession, activeRuntime, isShuttingDown, isRemoteBrowser, policyChecker, READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS, RECORDING_SKIP, PAGE_CONTENT_COMMANDS, BOUNDARY_NONCE, flushInterval, sessionCleanupInterval;
|
|
5874
6108
|
var init_server = __esm({
|
|
5875
6109
|
"src/server.ts"() {
|
|
5876
6110
|
"use strict";
|
|
@@ -5881,6 +6115,12 @@ var init_server = __esm({
|
|
|
5881
6115
|
init_meta();
|
|
5882
6116
|
init_policy();
|
|
5883
6117
|
init_constants();
|
|
6118
|
+
fs14 = __toESM(require("fs"), 1);
|
|
6119
|
+
path11 = __toESM(require("path"), 1);
|
|
6120
|
+
crypto3 = __toESM(require("crypto"), 1);
|
|
6121
|
+
http = __toESM(require("http"), 1);
|
|
6122
|
+
import_url2 = require("url");
|
|
6123
|
+
net2 = __toESM(require("net"), 1);
|
|
5884
6124
|
AUTH_TOKEN = crypto3.randomUUID();
|
|
5885
6125
|
DEBUG_PORT = parseInt(process.env.BROWSE_DEBUG_PORT || "0", 10);
|
|
5886
6126
|
BROWSE_PORT = parseInt(process.env.BROWSE_PORT || "0", 10);
|
|
@@ -5889,6 +6129,9 @@ var init_server = __esm({
|
|
|
5889
6129
|
LOCAL_DIR2 = process.env.BROWSE_LOCAL_DIR || "/tmp";
|
|
5890
6130
|
STATE_FILE = process.env.BROWSE_STATE_FILE || `${LOCAL_DIR2}/browse-server${INSTANCE_SUFFIX}.json`;
|
|
5891
6131
|
IDLE_TIMEOUT_MS = parseInt(process.env.BROWSE_IDLE_TIMEOUT || String(DEFAULTS.IDLE_TIMEOUT_MS), 10);
|
|
6132
|
+
sessionManager = null;
|
|
6133
|
+
browser = null;
|
|
6134
|
+
profileSession = null;
|
|
5892
6135
|
isShuttingDown = false;
|
|
5893
6136
|
isRemoteBrowser = false;
|
|
5894
6137
|
policyChecker = new PolicyChecker();
|
|
@@ -5986,7 +6229,8 @@ var init_server = __esm({
|
|
|
5986
6229
|
"record",
|
|
5987
6230
|
"cookie-import",
|
|
5988
6231
|
"doctor",
|
|
5989
|
-
"upgrade"
|
|
6232
|
+
"upgrade",
|
|
6233
|
+
"profile"
|
|
5990
6234
|
]);
|
|
5991
6235
|
RECORDING_SKIP = /* @__PURE__ */ new Set([
|
|
5992
6236
|
"record",
|
|
@@ -6017,10 +6261,19 @@ var init_server = __esm({
|
|
|
6017
6261
|
process.on("SIGTERM", shutdown);
|
|
6018
6262
|
process.on("SIGINT", shutdown);
|
|
6019
6263
|
flushInterval = setInterval(() => {
|
|
6020
|
-
if (sessionManager) flushAllBuffers(sessionManager);
|
|
6264
|
+
if (sessionManager || profileSession) flushAllBuffers(sessionManager);
|
|
6021
6265
|
}, DEFAULTS.BUFFER_FLUSH_INTERVAL_MS);
|
|
6022
6266
|
sessionCleanupInterval = setInterval(async () => {
|
|
6023
|
-
if (
|
|
6267
|
+
if (isShuttingDown) return;
|
|
6268
|
+
if (profileSession) {
|
|
6269
|
+
const idleMs = Date.now() - profileSession.lastActivity;
|
|
6270
|
+
if (idleMs > IDLE_TIMEOUT_MS) {
|
|
6271
|
+
console.log(`[browse] Profile session idle for ${IDLE_TIMEOUT_MS / 1e3}s \u2014 shutting down`);
|
|
6272
|
+
shutdown();
|
|
6273
|
+
}
|
|
6274
|
+
return;
|
|
6275
|
+
}
|
|
6276
|
+
if (!sessionManager) return;
|
|
6024
6277
|
const closed = await sessionManager.closeIdleSessions(IDLE_TIMEOUT_MS, (session) => flushSessionBuffers(session, true));
|
|
6025
6278
|
for (const id of closed) {
|
|
6026
6279
|
console.log(`[browse] Session "${id}" idle for ${IDLE_TIMEOUT_MS / 1e3}s \u2014 closed`);
|
|
@@ -6038,15 +6291,22 @@ var init_server = __esm({
|
|
|
6038
6291
|
});
|
|
6039
6292
|
|
|
6040
6293
|
// src/cli.ts
|
|
6294
|
+
var cli_exports = {};
|
|
6295
|
+
__export(cli_exports, {
|
|
6296
|
+
SAFE_TO_RETRY: () => SAFE_TO_RETRY,
|
|
6297
|
+
main: () => main,
|
|
6298
|
+
resolveServerScript: () => resolveServerScript
|
|
6299
|
+
});
|
|
6300
|
+
module.exports = __toCommonJS(cli_exports);
|
|
6301
|
+
var fs15 = __toESM(require("fs"), 1);
|
|
6302
|
+
var path12 = __toESM(require("path"), 1);
|
|
6303
|
+
var import_child_process3 = require("child_process");
|
|
6304
|
+
var import_url3 = require("url");
|
|
6041
6305
|
init_constants();
|
|
6042
|
-
import * as fs14 from "fs";
|
|
6043
|
-
import * as path11 from "path";
|
|
6044
|
-
import { spawn as spawn3 } from "child_process";
|
|
6045
|
-
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
6046
6306
|
|
|
6047
6307
|
// src/config.ts
|
|
6048
|
-
|
|
6049
|
-
|
|
6308
|
+
var fs = __toESM(require("fs"), 1);
|
|
6309
|
+
var path = __toESM(require("path"), 1);
|
|
6050
6310
|
function loadConfig() {
|
|
6051
6311
|
let dir = process.cwd();
|
|
6052
6312
|
for (let i = 0; i < 20; i++) {
|
|
@@ -6083,7 +6343,8 @@ var cliFlags = {
|
|
|
6083
6343
|
headed: false,
|
|
6084
6344
|
stateFile: "",
|
|
6085
6345
|
maxOutput: 0,
|
|
6086
|
-
cdpUrl: ""
|
|
6346
|
+
cdpUrl: "",
|
|
6347
|
+
profile: ""
|
|
6087
6348
|
};
|
|
6088
6349
|
var stateFileApplied = false;
|
|
6089
6350
|
var BROWSE_PORT2 = parseInt(process.env.BROWSE_PORT || "0", 10);
|
|
@@ -6092,50 +6353,50 @@ var INSTANCE_SUFFIX2 = BROWSE_PORT2 ? `-${BROWSE_PORT2}` : BROWSE_INSTANCE2 ? `-
|
|
|
6092
6353
|
function resolveLocalDir() {
|
|
6093
6354
|
if (process.env.BROWSE_LOCAL_DIR) {
|
|
6094
6355
|
try {
|
|
6095
|
-
|
|
6356
|
+
fs15.mkdirSync(process.env.BROWSE_LOCAL_DIR, { recursive: true });
|
|
6096
6357
|
} catch {
|
|
6097
6358
|
}
|
|
6098
6359
|
return process.env.BROWSE_LOCAL_DIR;
|
|
6099
6360
|
}
|
|
6100
6361
|
let dir = process.cwd();
|
|
6101
6362
|
for (let i = 0; i < 20; i++) {
|
|
6102
|
-
if (
|
|
6103
|
-
const browseDir =
|
|
6363
|
+
if (fs15.existsSync(path12.join(dir, ".git")) || fs15.existsSync(path12.join(dir, ".claude"))) {
|
|
6364
|
+
const browseDir = path12.join(dir, ".browse");
|
|
6104
6365
|
try {
|
|
6105
|
-
|
|
6106
|
-
const gi =
|
|
6107
|
-
if (!
|
|
6108
|
-
|
|
6366
|
+
fs15.mkdirSync(browseDir, { recursive: true });
|
|
6367
|
+
const gi = path12.join(browseDir, ".gitignore");
|
|
6368
|
+
if (!fs15.existsSync(gi)) {
|
|
6369
|
+
fs15.writeFileSync(gi, "*\n");
|
|
6109
6370
|
}
|
|
6110
6371
|
} catch {
|
|
6111
6372
|
}
|
|
6112
6373
|
return browseDir;
|
|
6113
6374
|
}
|
|
6114
|
-
const parent =
|
|
6375
|
+
const parent = path12.dirname(dir);
|
|
6115
6376
|
if (parent === dir) break;
|
|
6116
6377
|
dir = parent;
|
|
6117
6378
|
}
|
|
6118
6379
|
return "/tmp";
|
|
6119
6380
|
}
|
|
6120
6381
|
var LOCAL_DIR3 = resolveLocalDir();
|
|
6121
|
-
var STATE_FILE2 = process.env.BROWSE_STATE_FILE ||
|
|
6382
|
+
var STATE_FILE2 = process.env.BROWSE_STATE_FILE || path12.join(LOCAL_DIR3, `browse-server${INSTANCE_SUFFIX2}.json`);
|
|
6122
6383
|
var MAX_START_WAIT = 8e3;
|
|
6123
6384
|
var LOCK_FILE = STATE_FILE2 + ".lock";
|
|
6124
6385
|
var LOCK_STALE_MS = DEFAULTS.LOCK_STALE_THRESHOLD_MS;
|
|
6125
|
-
var __filename_cli =
|
|
6126
|
-
var __dirname_cli =
|
|
6386
|
+
var __filename_cli = (0, import_url3.fileURLToPath)(__import_meta_url);
|
|
6387
|
+
var __dirname_cli = path12.dirname(__filename_cli);
|
|
6127
6388
|
function resolveServerScript(env = process.env, metaDir = __dirname_cli) {
|
|
6128
6389
|
if (env.BROWSE_SERVER_SCRIPT) {
|
|
6129
6390
|
return env.BROWSE_SERVER_SCRIPT;
|
|
6130
6391
|
}
|
|
6131
6392
|
if (metaDir.startsWith("/")) {
|
|
6132
|
-
const direct =
|
|
6133
|
-
if (
|
|
6393
|
+
const direct = path12.resolve(metaDir, "server.ts");
|
|
6394
|
+
if (fs15.existsSync(direct)) {
|
|
6134
6395
|
return direct;
|
|
6135
6396
|
}
|
|
6136
6397
|
}
|
|
6137
|
-
const selfPath =
|
|
6138
|
-
if (
|
|
6398
|
+
const selfPath = (0, import_url3.fileURLToPath)(__import_meta_url);
|
|
6399
|
+
if (fs15.existsSync(selfPath)) {
|
|
6139
6400
|
return "__self__";
|
|
6140
6401
|
}
|
|
6141
6402
|
throw new Error(
|
|
@@ -6145,7 +6406,7 @@ function resolveServerScript(env = process.env, metaDir = __dirname_cli) {
|
|
|
6145
6406
|
var SERVER_SCRIPT = resolveServerScript();
|
|
6146
6407
|
function readState() {
|
|
6147
6408
|
try {
|
|
6148
|
-
const data =
|
|
6409
|
+
const data = fs15.readFileSync(STATE_FILE2, "utf-8");
|
|
6149
6410
|
return JSON.parse(data);
|
|
6150
6411
|
} catch {
|
|
6151
6412
|
return null;
|
|
@@ -6161,7 +6422,7 @@ function isProcessAlive(pid) {
|
|
|
6161
6422
|
}
|
|
6162
6423
|
async function listInstances() {
|
|
6163
6424
|
try {
|
|
6164
|
-
const files =
|
|
6425
|
+
const files = fs15.readdirSync(LOCAL_DIR3).filter(
|
|
6165
6426
|
(f) => f.startsWith("browse-server") && f.endsWith(".json") && !f.endsWith(".lock")
|
|
6166
6427
|
);
|
|
6167
6428
|
if (files.length === 0) {
|
|
@@ -6171,7 +6432,7 @@ async function listInstances() {
|
|
|
6171
6432
|
let found = false;
|
|
6172
6433
|
for (const file of files) {
|
|
6173
6434
|
try {
|
|
6174
|
-
const data = JSON.parse(
|
|
6435
|
+
const data = JSON.parse(fs15.readFileSync(path12.join(LOCAL_DIR3, file), "utf-8"));
|
|
6175
6436
|
if (!data.pid || !data.port) continue;
|
|
6176
6437
|
const alive = isProcessAlive(data.pid);
|
|
6177
6438
|
let status = "dead";
|
|
@@ -6194,7 +6455,7 @@ async function listInstances() {
|
|
|
6194
6455
|
found = true;
|
|
6195
6456
|
if (!alive) {
|
|
6196
6457
|
try {
|
|
6197
|
-
|
|
6458
|
+
fs15.unlinkSync(path12.join(LOCAL_DIR3, file));
|
|
6198
6459
|
} catch {
|
|
6199
6460
|
}
|
|
6200
6461
|
}
|
|
@@ -6208,7 +6469,7 @@ async function listInstances() {
|
|
|
6208
6469
|
}
|
|
6209
6470
|
function isBrowseProcess(pid) {
|
|
6210
6471
|
try {
|
|
6211
|
-
const { execSync: execSync2 } =
|
|
6472
|
+
const { execSync: execSync2 } = require("child_process");
|
|
6212
6473
|
const cmd = execSync2(`ps -p ${pid} -o command=`, { encoding: "utf-8" }).trim();
|
|
6213
6474
|
return cmd.includes("browse") || cmd.includes("__BROWSE_SERVER_MODE");
|
|
6214
6475
|
} catch {
|
|
@@ -6217,15 +6478,15 @@ function isBrowseProcess(pid) {
|
|
|
6217
6478
|
}
|
|
6218
6479
|
function acquireLock() {
|
|
6219
6480
|
try {
|
|
6220
|
-
|
|
6481
|
+
fs15.writeFileSync(LOCK_FILE, String(process.pid), { flag: "wx" });
|
|
6221
6482
|
return true;
|
|
6222
6483
|
} catch (err) {
|
|
6223
6484
|
if (err.code === "EEXIST") {
|
|
6224
6485
|
try {
|
|
6225
|
-
const stat =
|
|
6486
|
+
const stat = fs15.statSync(LOCK_FILE);
|
|
6226
6487
|
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
6227
6488
|
try {
|
|
6228
|
-
|
|
6489
|
+
fs15.unlinkSync(LOCK_FILE);
|
|
6229
6490
|
} catch {
|
|
6230
6491
|
}
|
|
6231
6492
|
return acquireLock();
|
|
@@ -6239,7 +6500,7 @@ function acquireLock() {
|
|
|
6239
6500
|
}
|
|
6240
6501
|
function releaseLock() {
|
|
6241
6502
|
try {
|
|
6242
|
-
|
|
6503
|
+
fs15.unlinkSync(LOCK_FILE);
|
|
6243
6504
|
} catch {
|
|
6244
6505
|
}
|
|
6245
6506
|
}
|
|
@@ -6256,7 +6517,7 @@ async function startServer() {
|
|
|
6256
6517
|
}
|
|
6257
6518
|
await sleep(100);
|
|
6258
6519
|
}
|
|
6259
|
-
if (!
|
|
6520
|
+
if (!fs15.existsSync(LOCK_FILE) || fs15.readFileSync(LOCK_FILE, "utf-8").trim() !== String(process.pid)) {
|
|
6260
6521
|
const state = readState();
|
|
6261
6522
|
if (state && isProcessAlive(state.pid)) return state;
|
|
6262
6523
|
throw new Error("Server failed to start (another process is starting it)");
|
|
@@ -6266,14 +6527,14 @@ async function startServer() {
|
|
|
6266
6527
|
try {
|
|
6267
6528
|
const oldState = readState();
|
|
6268
6529
|
if (oldState && !isProcessAlive(oldState.pid)) {
|
|
6269
|
-
|
|
6530
|
+
fs15.unlinkSync(STATE_FILE2);
|
|
6270
6531
|
}
|
|
6271
6532
|
} catch {
|
|
6272
6533
|
}
|
|
6273
|
-
const selfPath =
|
|
6534
|
+
const selfPath = (0, import_url3.fileURLToPath)(__import_meta_url);
|
|
6274
6535
|
const spawnCmd = SERVER_SCRIPT === "__self__" ? [process.execPath, selfPath] : [process.execPath, "--import", "tsx", SERVER_SCRIPT];
|
|
6275
|
-
const spawnEnv = { ...process.env, __BROWSE_SERVER_MODE: "1", BROWSE_LOCAL_DIR: LOCAL_DIR3, BROWSE_INSTANCE: BROWSE_INSTANCE2, ...cliFlags.headed ? { BROWSE_HEADED: "1" } : {}, ...cliFlags.cdpUrl ? { BROWSE_CDP_URL: cliFlags.cdpUrl } : {} };
|
|
6276
|
-
const proc =
|
|
6536
|
+
const spawnEnv = { ...process.env, __BROWSE_SERVER_MODE: "1", BROWSE_LOCAL_DIR: LOCAL_DIR3, BROWSE_INSTANCE: BROWSE_INSTANCE2, ...cliFlags.headed ? { BROWSE_HEADED: "1" } : {}, ...cliFlags.cdpUrl ? { BROWSE_CDP_URL: cliFlags.cdpUrl } : {}, ...cliFlags.profile ? { BROWSE_PROFILE: cliFlags.profile } : {} };
|
|
6537
|
+
const proc = (0, import_child_process3.spawn)(spawnCmd[0], spawnCmd.slice(1), {
|
|
6277
6538
|
stdio: ["ignore", "ignore", "pipe"],
|
|
6278
6539
|
env: spawnEnv,
|
|
6279
6540
|
detached: true
|
|
@@ -6339,7 +6600,7 @@ async function ensureServer() {
|
|
|
6339
6600
|
}
|
|
6340
6601
|
if (state) {
|
|
6341
6602
|
try {
|
|
6342
|
-
|
|
6603
|
+
fs15.unlinkSync(STATE_FILE2);
|
|
6343
6604
|
} catch {
|
|
6344
6605
|
}
|
|
6345
6606
|
}
|
|
@@ -6349,21 +6610,21 @@ async function ensureServer() {
|
|
|
6349
6610
|
}
|
|
6350
6611
|
function cleanOrphanedServers() {
|
|
6351
6612
|
try {
|
|
6352
|
-
const files =
|
|
6613
|
+
const files = fs15.readdirSync(LOCAL_DIR3);
|
|
6353
6614
|
for (const file of files) {
|
|
6354
6615
|
if (!file.startsWith("browse-server") || !file.endsWith(".json") || file.endsWith(".lock")) continue;
|
|
6355
|
-
const filePath =
|
|
6616
|
+
const filePath = path12.join(LOCAL_DIR3, file);
|
|
6356
6617
|
if (filePath === STATE_FILE2) continue;
|
|
6357
6618
|
try {
|
|
6358
|
-
const data = JSON.parse(
|
|
6619
|
+
const data = JSON.parse(fs15.readFileSync(filePath, "utf-8"));
|
|
6359
6620
|
if (!data.pid) {
|
|
6360
|
-
|
|
6621
|
+
fs15.unlinkSync(filePath);
|
|
6361
6622
|
continue;
|
|
6362
6623
|
}
|
|
6363
6624
|
const suffixMatch = file.match(/browse-server-(\d+)\.json$/);
|
|
6364
6625
|
if (suffixMatch && data.port === parseInt(suffixMatch[1], 10) && isProcessAlive(data.pid)) continue;
|
|
6365
6626
|
if (!isProcessAlive(data.pid)) {
|
|
6366
|
-
|
|
6627
|
+
fs15.unlinkSync(filePath);
|
|
6367
6628
|
continue;
|
|
6368
6629
|
}
|
|
6369
6630
|
if (isBrowseProcess(data.pid)) {
|
|
@@ -6374,7 +6635,7 @@ function cleanOrphanedServers() {
|
|
|
6374
6635
|
}
|
|
6375
6636
|
} catch {
|
|
6376
6637
|
try {
|
|
6377
|
-
|
|
6638
|
+
fs15.unlinkSync(filePath);
|
|
6378
6639
|
} catch {
|
|
6379
6640
|
}
|
|
6380
6641
|
}
|
|
@@ -6475,7 +6736,7 @@ async function sendCommand(state, command, args, retries = 0, sessionId) {
|
|
|
6475
6736
|
await sleep(300);
|
|
6476
6737
|
}
|
|
6477
6738
|
try {
|
|
6478
|
-
|
|
6739
|
+
fs15.unlinkSync(STATE_FILE2);
|
|
6479
6740
|
} catch {
|
|
6480
6741
|
}
|
|
6481
6742
|
if (command === "restart") {
|
|
@@ -6523,7 +6784,7 @@ async function main() {
|
|
|
6523
6784
|
function findCommandIndex(a) {
|
|
6524
6785
|
for (let i = 0; i < a.length; i++) {
|
|
6525
6786
|
if (!a[i].startsWith("-")) return i;
|
|
6526
|
-
if (a[i] === "--session" || a[i] === "--allowed-domains" || a[i] === "--cdp" || a[i] === "--state") i++;
|
|
6787
|
+
if (a[i] === "--session" || a[i] === "--allowed-domains" || a[i] === "--cdp" || a[i] === "--state" || a[i] === "--profile") i++;
|
|
6527
6788
|
}
|
|
6528
6789
|
return a.length;
|
|
6529
6790
|
}
|
|
@@ -6538,6 +6799,21 @@ async function main() {
|
|
|
6538
6799
|
args.splice(sessionIdx, 2);
|
|
6539
6800
|
}
|
|
6540
6801
|
sessionId = sessionId || process.env.BROWSE_SESSION || config.session || void 0;
|
|
6802
|
+
let profileName;
|
|
6803
|
+
const profileIdx = args.indexOf("--profile");
|
|
6804
|
+
if (profileIdx !== -1 && profileIdx < findCommandIndex(args)) {
|
|
6805
|
+
profileName = args[profileIdx + 1];
|
|
6806
|
+
if (!profileName || profileName.startsWith("-")) {
|
|
6807
|
+
console.error("Usage: browse --profile <name> <command> [args...]");
|
|
6808
|
+
process.exit(1);
|
|
6809
|
+
}
|
|
6810
|
+
args.splice(profileIdx, 2);
|
|
6811
|
+
}
|
|
6812
|
+
profileName = profileName || process.env.BROWSE_PROFILE || void 0;
|
|
6813
|
+
if (sessionId && profileName) {
|
|
6814
|
+
console.error("Cannot use --profile and --session together. Profiles use their own Chromium; sessions share one.");
|
|
6815
|
+
process.exit(1);
|
|
6816
|
+
}
|
|
6541
6817
|
let jsonMode = false;
|
|
6542
6818
|
const jsonIdx = args.indexOf("--json");
|
|
6543
6819
|
if (jsonIdx !== -1 && jsonIdx < findCommandIndex(args)) {
|
|
@@ -6636,6 +6912,7 @@ async function main() {
|
|
|
6636
6912
|
cliFlags.stateFile = stateFile;
|
|
6637
6913
|
cliFlags.maxOutput = maxOutput;
|
|
6638
6914
|
cliFlags.cdpUrl = cdpUrl;
|
|
6915
|
+
cliFlags.profile = profileName || "";
|
|
6639
6916
|
if (args[0] === "version" || args[0] === "--version" || args[0] === "-V") {
|
|
6640
6917
|
const pkg = await Promise.resolve().then(() => __toESM(require_package(), 1));
|
|
6641
6918
|
console.log(pkg.version);
|
|
@@ -6704,6 +6981,7 @@ Setup: install-skill [path]
|
|
|
6704
6981
|
|
|
6705
6982
|
Options:
|
|
6706
6983
|
--session <id> Named session (isolates tabs, refs, cookies)
|
|
6984
|
+
--profile <name> Persistent browser profile (own Chromium, full state persistence)
|
|
6707
6985
|
--json Wrap output as {success, data, command}
|
|
6708
6986
|
--content-boundaries Wrap page content in nonce-delimited markers
|
|
6709
6987
|
--allowed-domains <d,d> Block navigation/resources outside allowlist
|
|
@@ -6743,14 +7021,15 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
|
|
|
6743
7021
|
}
|
|
6744
7022
|
if (process.env.__BROWSE_SERVER_MODE === "1") {
|
|
6745
7023
|
Promise.resolve().then(() => init_server());
|
|
6746
|
-
} else if (process.argv[1] &&
|
|
7024
|
+
} else if (process.argv[1] && fs15.realpathSync(process.argv[1]) === fs15.realpathSync(__filename_cli)) {
|
|
6747
7025
|
main().catch((err) => {
|
|
6748
7026
|
console.error(`[browse] ${err.message}`);
|
|
6749
7027
|
process.exit(1);
|
|
6750
7028
|
});
|
|
6751
7029
|
}
|
|
6752
|
-
export
|
|
7030
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
7031
|
+
0 && (module.exports = {
|
|
6753
7032
|
SAFE_TO_RETRY,
|
|
6754
7033
|
main,
|
|
6755
7034
|
resolveServerScript
|
|
6756
|
-
};
|
|
7035
|
+
});
|