@ulpi/browse 1.0.1 → 1.0.2
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 +481 -210
- package/package.json +2 -1
- package/skill/SKILL.md +16 -0
package/dist/browse.mjs
CHANGED
|
@@ -127,7 +127,7 @@ var require_package = __commonJS({
|
|
|
127
127
|
"package.json"(exports, module) {
|
|
128
128
|
module.exports = {
|
|
129
129
|
name: "@ulpi/browse",
|
|
130
|
-
version: "1.0.
|
|
130
|
+
version: "1.0.2",
|
|
131
131
|
repository: {
|
|
132
132
|
type: "git",
|
|
133
133
|
url: "https://github.com/ulpi-io/browse"
|
|
@@ -181,6 +181,7 @@ var require_package = __commonJS({
|
|
|
181
181
|
devDependencies: {
|
|
182
182
|
"@types/better-sqlite3": "^7.0.0",
|
|
183
183
|
"@types/node": "^25.5.0",
|
|
184
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
184
185
|
esbuild: "^0.25.0",
|
|
185
186
|
tsx: "^4.0.0",
|
|
186
187
|
typescript: "^5.9.3",
|
|
@@ -452,7 +453,31 @@ stderr: ${stderrData.slice(0, 2e3)}` : "")
|
|
|
452
453
|
});
|
|
453
454
|
|
|
454
455
|
// src/buffers.ts
|
|
455
|
-
var
|
|
456
|
+
var buffers_exports = {};
|
|
457
|
+
__export(buffers_exports, {
|
|
458
|
+
SessionBuffers: () => SessionBuffers,
|
|
459
|
+
addConsoleEntry: () => addConsoleEntry,
|
|
460
|
+
addNetworkEntry: () => addNetworkEntry,
|
|
461
|
+
consoleBuffer: () => consoleBuffer,
|
|
462
|
+
consoleTotalAdded: () => consoleTotalAdded,
|
|
463
|
+
networkBuffer: () => networkBuffer,
|
|
464
|
+
networkTotalAdded: () => networkTotalAdded
|
|
465
|
+
});
|
|
466
|
+
function addConsoleEntry(entry) {
|
|
467
|
+
if (consoleBuffer.length >= DEFAULTS.BUFFER_HIGH_WATER_MARK) {
|
|
468
|
+
consoleBuffer.shift();
|
|
469
|
+
}
|
|
470
|
+
consoleBuffer.push(entry);
|
|
471
|
+
consoleTotalAdded++;
|
|
472
|
+
}
|
|
473
|
+
function addNetworkEntry(entry) {
|
|
474
|
+
if (networkBuffer.length >= DEFAULTS.BUFFER_HIGH_WATER_MARK) {
|
|
475
|
+
networkBuffer.shift();
|
|
476
|
+
}
|
|
477
|
+
networkBuffer.push(entry);
|
|
478
|
+
networkTotalAdded++;
|
|
479
|
+
}
|
|
480
|
+
var SessionBuffers, consoleBuffer, networkBuffer, consoleTotalAdded, networkTotalAdded;
|
|
456
481
|
var init_buffers = __esm({
|
|
457
482
|
"src/buffers.ts"() {
|
|
458
483
|
"use strict";
|
|
@@ -480,10 +505,39 @@ var init_buffers = __esm({
|
|
|
480
505
|
this.networkTotalAdded++;
|
|
481
506
|
}
|
|
482
507
|
};
|
|
508
|
+
consoleBuffer = [];
|
|
509
|
+
networkBuffer = [];
|
|
510
|
+
consoleTotalAdded = 0;
|
|
511
|
+
networkTotalAdded = 0;
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// src/sanitize.ts
|
|
516
|
+
function sanitizeName(name) {
|
|
517
|
+
const sanitized = name.replace(/[\/\\]/g, "_").replace(/\.\./g, "_");
|
|
518
|
+
if (!sanitized || /^[._]+$/.test(sanitized)) {
|
|
519
|
+
throw new Error(`Invalid name: "${name}"`);
|
|
520
|
+
}
|
|
521
|
+
return sanitized;
|
|
522
|
+
}
|
|
523
|
+
var init_sanitize = __esm({
|
|
524
|
+
"src/sanitize.ts"() {
|
|
525
|
+
"use strict";
|
|
483
526
|
}
|
|
484
527
|
});
|
|
485
528
|
|
|
486
529
|
// src/browser-manager.ts
|
|
530
|
+
var browser_manager_exports = {};
|
|
531
|
+
__export(browser_manager_exports, {
|
|
532
|
+
BrowserManager: () => BrowserManager,
|
|
533
|
+
deleteProfile: () => deleteProfile,
|
|
534
|
+
getProfileDir: () => getProfileDir,
|
|
535
|
+
listDevices: () => listDevices,
|
|
536
|
+
listProfiles: () => listProfiles,
|
|
537
|
+
resolveDevice: () => resolveDevice
|
|
538
|
+
});
|
|
539
|
+
import * as path4 from "path";
|
|
540
|
+
import * as fs4 from "fs";
|
|
487
541
|
import { chromium, devices as playwrightDevices } from "playwright";
|
|
488
542
|
function resolveDevice(name) {
|
|
489
543
|
const alias = DEVICE_ALIASES[name.toLowerCase()];
|
|
@@ -512,11 +566,49 @@ function listDevices() {
|
|
|
512
566
|
]);
|
|
513
567
|
return [...all].sort();
|
|
514
568
|
}
|
|
569
|
+
function getProfileDir(localDir, name) {
|
|
570
|
+
const sanitized = sanitizeName(name);
|
|
571
|
+
if (!sanitized) throw new Error("Invalid profile name");
|
|
572
|
+
return path4.join(localDir, "profiles", sanitized);
|
|
573
|
+
}
|
|
574
|
+
function listProfiles(localDir) {
|
|
575
|
+
const profilesDir = path4.join(localDir, "profiles");
|
|
576
|
+
if (!fs4.existsSync(profilesDir)) return [];
|
|
577
|
+
return fs4.readdirSync(profilesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
|
|
578
|
+
const dir = path4.join(profilesDir, d.name);
|
|
579
|
+
const stat = fs4.statSync(dir);
|
|
580
|
+
let totalSize = 0;
|
|
581
|
+
try {
|
|
582
|
+
const files = fs4.readdirSync(dir, { recursive: true, withFileTypes: true });
|
|
583
|
+
for (const f of files) {
|
|
584
|
+
if (f.isFile()) {
|
|
585
|
+
try {
|
|
586
|
+
totalSize += fs4.statSync(path4.join(f.parentPath || f.path || dir, f.name)).size;
|
|
587
|
+
} catch {
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
} catch {
|
|
592
|
+
}
|
|
593
|
+
const sizeMB = (totalSize / 1024 / 1024).toFixed(1);
|
|
594
|
+
return {
|
|
595
|
+
name: d.name,
|
|
596
|
+
size: `${sizeMB}MB`,
|
|
597
|
+
lastUsed: stat.mtime.toISOString().split("T")[0]
|
|
598
|
+
};
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
function deleteProfile(localDir, name) {
|
|
602
|
+
const dir = getProfileDir(localDir, name);
|
|
603
|
+
if (!fs4.existsSync(dir)) throw new Error(`Profile "${name}" not found`);
|
|
604
|
+
fs4.rmSync(dir, { recursive: true, force: true });
|
|
605
|
+
}
|
|
515
606
|
var DEVICE_ALIASES, CUSTOM_DEVICES, BrowserManager;
|
|
516
607
|
var init_browser_manager = __esm({
|
|
517
608
|
"src/browser-manager.ts"() {
|
|
518
609
|
"use strict";
|
|
519
610
|
init_buffers();
|
|
611
|
+
init_sanitize();
|
|
520
612
|
DEVICE_ALIASES = {
|
|
521
613
|
"iphone": "iPhone 15",
|
|
522
614
|
"iphone-12": "iPhone 12",
|
|
@@ -626,6 +718,8 @@ var init_browser_manager = __esm({
|
|
|
626
718
|
domainFilter = null;
|
|
627
719
|
// Whether this instance owns (and should close) the Browser process
|
|
628
720
|
ownsBrowser = false;
|
|
721
|
+
// Whether this instance uses a persistent browser context (profile mode)
|
|
722
|
+
isPersistent = false;
|
|
629
723
|
constructor(buffers) {
|
|
630
724
|
this.buffers = buffers || new SessionBuffers();
|
|
631
725
|
}
|
|
@@ -668,7 +762,66 @@ var init_browser_manager = __esm({
|
|
|
668
762
|
});
|
|
669
763
|
await this.newTab();
|
|
670
764
|
}
|
|
765
|
+
/**
|
|
766
|
+
* Launch with a persistent browser profile directory.
|
|
767
|
+
* Data (cookies, localStorage, cache) persists across restarts.
|
|
768
|
+
* The context IS the browser — closing it closes everything.
|
|
769
|
+
*/
|
|
770
|
+
async launchPersistent(profileDir, onCrash) {
|
|
771
|
+
let context;
|
|
772
|
+
try {
|
|
773
|
+
context = await chromium.launchPersistentContext(profileDir, {
|
|
774
|
+
headless: process.env.BROWSE_HEADED !== "1",
|
|
775
|
+
viewport: { width: 1920, height: 1080 },
|
|
776
|
+
...this.customUserAgent ? { userAgent: this.customUserAgent } : {}
|
|
777
|
+
});
|
|
778
|
+
} catch (err) {
|
|
779
|
+
if (err.message?.includes("Failed to launch") || err.message?.includes("Target closed")) {
|
|
780
|
+
const fs16 = await import("fs");
|
|
781
|
+
console.error(`[browse] Profile directory corrupted, recreating: ${profileDir}`);
|
|
782
|
+
fs16.rmSync(profileDir, { recursive: true, force: true });
|
|
783
|
+
context = await chromium.launchPersistentContext(profileDir, {
|
|
784
|
+
headless: process.env.BROWSE_HEADED !== "1",
|
|
785
|
+
viewport: { width: 1920, height: 1080 }
|
|
786
|
+
});
|
|
787
|
+
} else {
|
|
788
|
+
throw err;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
this.context = context;
|
|
792
|
+
this.browser = context.browser();
|
|
793
|
+
this.isPersistent = true;
|
|
794
|
+
this.ownsBrowser = true;
|
|
795
|
+
if (this.browser) {
|
|
796
|
+
this.browser.on("disconnected", () => {
|
|
797
|
+
if (onCrash) onCrash();
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
const pages = context.pages();
|
|
801
|
+
if (pages.length > 0) {
|
|
802
|
+
for (const page of pages) {
|
|
803
|
+
const tabId = this.nextTabId++;
|
|
804
|
+
this.wirePageEvents(page);
|
|
805
|
+
this.pages.set(tabId, page);
|
|
806
|
+
this.activeTabId = tabId;
|
|
807
|
+
}
|
|
808
|
+
} else {
|
|
809
|
+
await this.newTab();
|
|
810
|
+
}
|
|
811
|
+
}
|
|
671
812
|
async close() {
|
|
813
|
+
if (this.isPersistent) {
|
|
814
|
+
this.pages.clear();
|
|
815
|
+
this.tabSnapshots.clear();
|
|
816
|
+
this.refMap.clear();
|
|
817
|
+
if (this.context) {
|
|
818
|
+
await this.context.close().catch(() => {
|
|
819
|
+
});
|
|
820
|
+
this.context = null;
|
|
821
|
+
this.browser = null;
|
|
822
|
+
}
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
672
825
|
for (const [, page] of this.pages) {
|
|
673
826
|
await page.close().catch(() => {
|
|
674
827
|
});
|
|
@@ -690,6 +843,9 @@ var init_browser_manager = __esm({
|
|
|
690
843
|
isHealthy() {
|
|
691
844
|
return this.browser !== null && this.browser.isConnected();
|
|
692
845
|
}
|
|
846
|
+
getIsPersistent() {
|
|
847
|
+
return this.isPersistent;
|
|
848
|
+
}
|
|
693
849
|
// ─── Tab Management ────────────────────────────────────────
|
|
694
850
|
async newTab(url) {
|
|
695
851
|
if (!this.context) throw new Error("Browser not launched");
|
|
@@ -950,6 +1106,11 @@ var init_browser_manager = __esm({
|
|
|
950
1106
|
* Cannot preserve: localStorage/sessionStorage (bound to old context).
|
|
951
1107
|
*/
|
|
952
1108
|
async recreateContext(contextOptions) {
|
|
1109
|
+
if (this.isPersistent) {
|
|
1110
|
+
throw new Error(
|
|
1111
|
+
"Cannot change device/viewport/user-agent in profile mode \u2014 profiles use a fixed browser context. Use --session instead."
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
953
1114
|
if (!this.browser) return;
|
|
954
1115
|
if (this.videoRecording && !contextOptions.recordVideo) {
|
|
955
1116
|
contextOptions = {
|
|
@@ -1149,8 +1310,8 @@ var init_browser_manager = __esm({
|
|
|
1149
1310
|
// ─── Video Recording ──────────────────────────────────────
|
|
1150
1311
|
async startVideoRecording(dir) {
|
|
1151
1312
|
if (this.videoRecording) throw new Error("Video recording already active");
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1313
|
+
const fs16 = await import("fs");
|
|
1314
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
1154
1315
|
this.videoRecording = { dir, startedAt: Date.now() };
|
|
1155
1316
|
const viewport = this.currentDevice?.viewport || { width: 1920, height: 1080 };
|
|
1156
1317
|
await this.recreateContext({
|
|
@@ -1425,24 +1586,10 @@ var init_domain_filter = __esm({
|
|
|
1425
1586
|
}
|
|
1426
1587
|
});
|
|
1427
1588
|
|
|
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
1589
|
// src/encryption.ts
|
|
1443
1590
|
import * as crypto from "crypto";
|
|
1444
|
-
import * as
|
|
1445
|
-
import * as
|
|
1591
|
+
import * as fs5 from "fs";
|
|
1592
|
+
import * as path5 from "path";
|
|
1446
1593
|
function resolveEncryptionKey(localDir) {
|
|
1447
1594
|
const envKey = process.env.BROWSE_ENCRYPTION_KEY;
|
|
1448
1595
|
if (envKey) {
|
|
@@ -1451,13 +1598,13 @@ function resolveEncryptionKey(localDir) {
|
|
|
1451
1598
|
}
|
|
1452
1599
|
return Buffer.from(envKey, "hex");
|
|
1453
1600
|
}
|
|
1454
|
-
const keyPath =
|
|
1455
|
-
if (
|
|
1456
|
-
const hex =
|
|
1601
|
+
const keyPath = path5.join(localDir, ".encryption-key");
|
|
1602
|
+
if (fs5.existsSync(keyPath)) {
|
|
1603
|
+
const hex = fs5.readFileSync(keyPath, "utf-8").trim();
|
|
1457
1604
|
return Buffer.from(hex, "hex");
|
|
1458
1605
|
}
|
|
1459
1606
|
const key = crypto.randomBytes(32);
|
|
1460
|
-
|
|
1607
|
+
fs5.writeFileSync(keyPath, key.toString("hex") + "\n", { mode: 384 });
|
|
1461
1608
|
return key;
|
|
1462
1609
|
}
|
|
1463
1610
|
function encrypt(plaintext, key) {
|
|
@@ -1497,8 +1644,8 @@ __export(session_persist_exports, {
|
|
|
1497
1644
|
loadSessionState: () => loadSessionState,
|
|
1498
1645
|
saveSessionState: () => saveSessionState
|
|
1499
1646
|
});
|
|
1500
|
-
import * as
|
|
1501
|
-
import * as
|
|
1647
|
+
import * as fs6 from "fs";
|
|
1648
|
+
import * as path6 from "path";
|
|
1502
1649
|
async function saveSessionState(sessionDir, context, encryptionKey) {
|
|
1503
1650
|
try {
|
|
1504
1651
|
const state = await context.storageState();
|
|
@@ -1510,20 +1657,20 @@ async function saveSessionState(sessionDir, context, encryptionKey) {
|
|
|
1510
1657
|
} else {
|
|
1511
1658
|
content = json;
|
|
1512
1659
|
}
|
|
1513
|
-
|
|
1514
|
-
|
|
1660
|
+
fs6.mkdirSync(sessionDir, { recursive: true });
|
|
1661
|
+
fs6.writeFileSync(path6.join(sessionDir, STATE_FILENAME), content, { mode: 384 });
|
|
1515
1662
|
} catch (err) {
|
|
1516
1663
|
console.log(`[session-persist] Warning: failed to save state: ${err.message}`);
|
|
1517
1664
|
}
|
|
1518
1665
|
}
|
|
1519
1666
|
async function loadSessionState(sessionDir, context, encryptionKey) {
|
|
1520
|
-
const statePath =
|
|
1521
|
-
if (!
|
|
1667
|
+
const statePath = path6.join(sessionDir, STATE_FILENAME);
|
|
1668
|
+
if (!fs6.existsSync(statePath)) {
|
|
1522
1669
|
return false;
|
|
1523
1670
|
}
|
|
1524
1671
|
let stateData;
|
|
1525
1672
|
try {
|
|
1526
|
-
const raw =
|
|
1673
|
+
const raw = fs6.readFileSync(statePath, "utf-8");
|
|
1527
1674
|
const parsed = JSON.parse(raw);
|
|
1528
1675
|
if (parsed.encrypted) {
|
|
1529
1676
|
if (!encryptionKey) {
|
|
@@ -1579,24 +1726,24 @@ async function loadSessionState(sessionDir, context, encryptionKey) {
|
|
|
1579
1726
|
}
|
|
1580
1727
|
}
|
|
1581
1728
|
function hasPersistedState(sessionDir) {
|
|
1582
|
-
return
|
|
1729
|
+
return fs6.existsSync(path6.join(sessionDir, STATE_FILENAME));
|
|
1583
1730
|
}
|
|
1584
1731
|
function cleanOldStates(localDir, maxAgeDays) {
|
|
1585
1732
|
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1e3;
|
|
1586
1733
|
const now = Date.now();
|
|
1587
1734
|
let deleted = 0;
|
|
1588
|
-
const statesDir =
|
|
1589
|
-
if (
|
|
1735
|
+
const statesDir = path6.join(localDir, "states");
|
|
1736
|
+
if (fs6.existsSync(statesDir)) {
|
|
1590
1737
|
try {
|
|
1591
|
-
const entries =
|
|
1738
|
+
const entries = fs6.readdirSync(statesDir);
|
|
1592
1739
|
for (const entry of entries) {
|
|
1593
1740
|
if (!entry.endsWith(".json")) continue;
|
|
1594
|
-
const filePath =
|
|
1741
|
+
const filePath = path6.join(statesDir, entry);
|
|
1595
1742
|
try {
|
|
1596
|
-
const stat =
|
|
1743
|
+
const stat = fs6.statSync(filePath);
|
|
1597
1744
|
if (!stat.isFile()) continue;
|
|
1598
1745
|
if (now - stat.mtimeMs > maxAgeMs) {
|
|
1599
|
-
|
|
1746
|
+
fs6.unlinkSync(filePath);
|
|
1600
1747
|
deleted++;
|
|
1601
1748
|
}
|
|
1602
1749
|
} catch (_) {
|
|
@@ -1605,23 +1752,23 @@ function cleanOldStates(localDir, maxAgeDays) {
|
|
|
1605
1752
|
} catch (_) {
|
|
1606
1753
|
}
|
|
1607
1754
|
}
|
|
1608
|
-
const sessionsDir =
|
|
1609
|
-
if (
|
|
1755
|
+
const sessionsDir = path6.join(localDir, "sessions");
|
|
1756
|
+
if (fs6.existsSync(sessionsDir)) {
|
|
1610
1757
|
try {
|
|
1611
|
-
const sessionDirs =
|
|
1758
|
+
const sessionDirs = fs6.readdirSync(sessionsDir);
|
|
1612
1759
|
for (const dir of sessionDirs) {
|
|
1613
|
-
const dirPath =
|
|
1760
|
+
const dirPath = path6.join(sessionsDir, dir);
|
|
1614
1761
|
try {
|
|
1615
|
-
const dirStat =
|
|
1762
|
+
const dirStat = fs6.statSync(dirPath);
|
|
1616
1763
|
if (!dirStat.isDirectory()) continue;
|
|
1617
1764
|
} catch (_) {
|
|
1618
1765
|
continue;
|
|
1619
1766
|
}
|
|
1620
|
-
const statePath =
|
|
1767
|
+
const statePath = path6.join(dirPath, STATE_FILENAME);
|
|
1621
1768
|
try {
|
|
1622
|
-
const stat =
|
|
1769
|
+
const stat = fs6.statSync(statePath);
|
|
1623
1770
|
if (now - stat.mtimeMs > maxAgeMs) {
|
|
1624
|
-
|
|
1771
|
+
fs6.unlinkSync(statePath);
|
|
1625
1772
|
deleted++;
|
|
1626
1773
|
}
|
|
1627
1774
|
} catch (_) {
|
|
@@ -1642,8 +1789,8 @@ var init_session_persist = __esm({
|
|
|
1642
1789
|
});
|
|
1643
1790
|
|
|
1644
1791
|
// src/session-manager.ts
|
|
1645
|
-
import * as
|
|
1646
|
-
import * as
|
|
1792
|
+
import * as fs7 from "fs";
|
|
1793
|
+
import * as path7 from "path";
|
|
1647
1794
|
var SessionManager;
|
|
1648
1795
|
var init_session_manager = __esm({
|
|
1649
1796
|
"src/session-manager.ts"() {
|
|
@@ -1706,8 +1853,8 @@ var init_session_manager = __esm({
|
|
|
1706
1853
|
}
|
|
1707
1854
|
return session;
|
|
1708
1855
|
}
|
|
1709
|
-
const outputDir =
|
|
1710
|
-
|
|
1856
|
+
const outputDir = path7.join(this.localDir, "sessions", sanitizeName(sessionId));
|
|
1857
|
+
fs7.mkdirSync(outputDir, { recursive: true });
|
|
1711
1858
|
const buffers = new SessionBuffers();
|
|
1712
1859
|
const manager = new BrowserManager(buffers);
|
|
1713
1860
|
await manager.launchWithBrowser(this.browser);
|
|
@@ -1843,7 +1990,7 @@ var read_exports = {};
|
|
|
1843
1990
|
__export(read_exports, {
|
|
1844
1991
|
handleReadCommand: () => handleReadCommand
|
|
1845
1992
|
});
|
|
1846
|
-
import * as
|
|
1993
|
+
import * as fs8 from "fs";
|
|
1847
1994
|
async function handleReadCommand(command, args, bm, buffers) {
|
|
1848
1995
|
const page = bm.getPage();
|
|
1849
1996
|
const evalCtx = await bm.getFrameContext() || page;
|
|
@@ -1938,8 +2085,8 @@ async function handleReadCommand(command, args, bm, buffers) {
|
|
|
1938
2085
|
case "eval": {
|
|
1939
2086
|
const filePath = args[0];
|
|
1940
2087
|
if (!filePath) throw new Error("Usage: browse eval <js-file>");
|
|
1941
|
-
if (!
|
|
1942
|
-
const code =
|
|
2088
|
+
if (!fs8.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
|
2089
|
+
const code = fs8.readFileSync(filePath, "utf-8");
|
|
1943
2090
|
const result = await evalCtx.evaluate(code);
|
|
1944
2091
|
return typeof result === "object" ? JSON.stringify(result, null, 2) : String(result ?? "");
|
|
1945
2092
|
}
|
|
@@ -2190,7 +2337,7 @@ var write_exports = {};
|
|
|
2190
2337
|
__export(write_exports, {
|
|
2191
2338
|
handleWriteCommand: () => handleWriteCommand
|
|
2192
2339
|
});
|
|
2193
|
-
import * as
|
|
2340
|
+
import * as fs9 from "fs";
|
|
2194
2341
|
async function rebuildRoutes(context, bm, domainFilter) {
|
|
2195
2342
|
await context.unrouteAll();
|
|
2196
2343
|
for (const r of bm.getUserRoutes()) {
|
|
@@ -2394,14 +2541,14 @@ async function handleWriteCommand(command, args, bm, domainFilter) {
|
|
|
2394
2541
|
const file = args[1];
|
|
2395
2542
|
if (!file) throw new Error("Usage: browse cookie export <file>");
|
|
2396
2543
|
const cookies = await page.context().cookies();
|
|
2397
|
-
|
|
2544
|
+
fs9.writeFileSync(file, JSON.stringify(cookies, null, 2));
|
|
2398
2545
|
return `Exported ${cookies.length} cookie(s) to ${file}`;
|
|
2399
2546
|
}
|
|
2400
2547
|
if (cookieStr === "import") {
|
|
2401
2548
|
const file = args[1];
|
|
2402
2549
|
if (!file) throw new Error("Usage: browse cookie import <file>");
|
|
2403
|
-
if (!
|
|
2404
|
-
const cookies = JSON.parse(
|
|
2550
|
+
if (!fs9.existsSync(file)) throw new Error(`File not found: ${file}`);
|
|
2551
|
+
const cookies = JSON.parse(fs9.readFileSync(file, "utf-8"));
|
|
2405
2552
|
if (!Array.isArray(cookies)) throw new Error("Cookie file must contain a JSON array of cookie objects");
|
|
2406
2553
|
await page.context().addCookies(cookies);
|
|
2407
2554
|
return `Imported ${cookies.length} cookie(s) from ${file}`;
|
|
@@ -2468,7 +2615,7 @@ Note: Cookies and tab URLs preserved. localStorage/sessionStorage were reset (Pl
|
|
|
2468
2615
|
const [selector, ...filePaths] = args;
|
|
2469
2616
|
if (!selector || filePaths.length === 0) throw new Error("Usage: browse upload <selector> <file1> [file2] ...");
|
|
2470
2617
|
for (const fp of filePaths) {
|
|
2471
|
-
if (!
|
|
2618
|
+
if (!fs9.existsSync(fp)) throw new Error(`File not found: ${fp}`);
|
|
2472
2619
|
}
|
|
2473
2620
|
const resolved = bm.resolveRef(selector);
|
|
2474
2621
|
if ("locator" in resolved) {
|
|
@@ -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,14 +3919,14 @@ var policy_exports = {};
|
|
|
3772
3919
|
__export(policy_exports, {
|
|
3773
3920
|
PolicyChecker: () => PolicyChecker
|
|
3774
3921
|
});
|
|
3775
|
-
import * as
|
|
3776
|
-
import * as
|
|
3922
|
+
import * as fs10 from "fs";
|
|
3923
|
+
import * as path8 from "path";
|
|
3777
3924
|
function findFileUpward(filename) {
|
|
3778
3925
|
let dir = process.cwd();
|
|
3779
3926
|
for (let i = 0; i < 20; i++) {
|
|
3780
|
-
const candidate =
|
|
3781
|
-
if (
|
|
3782
|
-
const parent =
|
|
3927
|
+
const candidate = path8.join(dir, filename);
|
|
3928
|
+
if (fs10.existsSync(candidate)) return candidate;
|
|
3929
|
+
const parent = path8.dirname(dir);
|
|
3783
3930
|
if (parent === dir) break;
|
|
3784
3931
|
dir = parent;
|
|
3785
3932
|
}
|
|
@@ -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
|
}
|
|
@@ -4029,8 +4176,8 @@ var auth_vault_exports = {};
|
|
|
4029
4176
|
__export(auth_vault_exports, {
|
|
4030
4177
|
AuthVault: () => AuthVault
|
|
4031
4178
|
});
|
|
4032
|
-
import * as
|
|
4033
|
-
import * as
|
|
4179
|
+
import * as fs11 from "fs";
|
|
4180
|
+
import * as path9 from "path";
|
|
4034
4181
|
async function autoDetectSelector(page, field) {
|
|
4035
4182
|
if (field === "username") {
|
|
4036
4183
|
const candidates2 = [
|
|
@@ -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
|
}
|
|
@@ -4177,15 +4324,15 @@ __export(cookie_import_exports, {
|
|
|
4177
4324
|
import Database from "better-sqlite3";
|
|
4178
4325
|
import { spawn as spawn2 } from "child_process";
|
|
4179
4326
|
import * as crypto2 from "crypto";
|
|
4180
|
-
import * as
|
|
4181
|
-
import * as
|
|
4327
|
+
import * as fs12 from "fs";
|
|
4328
|
+
import * as path10 from "path";
|
|
4182
4329
|
import * as os2 from "os";
|
|
4183
4330
|
function findInstalledBrowsers() {
|
|
4184
|
-
const appSupport =
|
|
4331
|
+
const appSupport = path10.join(os2.homedir(), "Library", "Application Support");
|
|
4185
4332
|
return BROWSER_REGISTRY.filter((b) => {
|
|
4186
|
-
const dbPath =
|
|
4333
|
+
const dbPath = path10.join(appSupport, b.dataDir, "Default", "Cookies");
|
|
4187
4334
|
try {
|
|
4188
|
-
return
|
|
4335
|
+
return fs12.existsSync(dbPath);
|
|
4189
4336
|
} catch {
|
|
4190
4337
|
return false;
|
|
4191
4338
|
}
|
|
@@ -4268,9 +4415,9 @@ function validateProfile(profile) {
|
|
|
4268
4415
|
}
|
|
4269
4416
|
function getCookieDbPath(browser2, profile) {
|
|
4270
4417
|
validateProfile(profile);
|
|
4271
|
-
const appSupport =
|
|
4272
|
-
const dbPath =
|
|
4273
|
-
if (!
|
|
4418
|
+
const appSupport = path10.join(os2.homedir(), "Library", "Application Support");
|
|
4419
|
+
const dbPath = path10.join(appSupport, browser2.dataDir, profile, "Cookies");
|
|
4420
|
+
if (!fs12.existsSync(dbPath)) {
|
|
4274
4421
|
throw new CookieImportError(
|
|
4275
4422
|
`${browser2.name} is not installed (no cookie database at ${dbPath})`,
|
|
4276
4423
|
"not_installed"
|
|
@@ -4297,32 +4444,32 @@ function openDb(dbPath, browserName) {
|
|
|
4297
4444
|
function openDbFromCopy(dbPath, browserName) {
|
|
4298
4445
|
const tmpPath = `/tmp/browse-cookies-${browserName.toLowerCase()}-${crypto2.randomUUID()}.db`;
|
|
4299
4446
|
try {
|
|
4300
|
-
|
|
4447
|
+
fs12.copyFileSync(dbPath, tmpPath);
|
|
4301
4448
|
const walPath = dbPath + "-wal";
|
|
4302
4449
|
const shmPath = dbPath + "-shm";
|
|
4303
|
-
if (
|
|
4304
|
-
if (
|
|
4450
|
+
if (fs12.existsSync(walPath)) fs12.copyFileSync(walPath, tmpPath + "-wal");
|
|
4451
|
+
if (fs12.existsSync(shmPath)) fs12.copyFileSync(shmPath, tmpPath + "-shm");
|
|
4305
4452
|
const db = new Database(tmpPath, { readonly: true });
|
|
4306
4453
|
const origClose = db.close.bind(db);
|
|
4307
4454
|
db.close = (() => {
|
|
4308
4455
|
origClose();
|
|
4309
4456
|
try {
|
|
4310
|
-
|
|
4457
|
+
fs12.unlinkSync(tmpPath);
|
|
4311
4458
|
} catch {
|
|
4312
4459
|
}
|
|
4313
4460
|
try {
|
|
4314
|
-
|
|
4461
|
+
fs12.unlinkSync(tmpPath + "-wal");
|
|
4315
4462
|
} catch {
|
|
4316
4463
|
}
|
|
4317
4464
|
try {
|
|
4318
|
-
|
|
4465
|
+
fs12.unlinkSync(tmpPath + "-shm");
|
|
4319
4466
|
} catch {
|
|
4320
4467
|
}
|
|
4321
4468
|
});
|
|
4322
4469
|
return db;
|
|
4323
4470
|
} catch {
|
|
4324
4471
|
try {
|
|
4325
|
-
|
|
4472
|
+
fs12.unlinkSync(tmpPath);
|
|
4326
4473
|
} catch {
|
|
4327
4474
|
}
|
|
4328
4475
|
throw new CookieImportError(
|
|
@@ -4620,7 +4767,7 @@ var init_record_export = __esm({
|
|
|
4620
4767
|
});
|
|
4621
4768
|
|
|
4622
4769
|
// src/commands/meta.ts
|
|
4623
|
-
import * as
|
|
4770
|
+
import * as fs13 from "fs";
|
|
4624
4771
|
async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2, currentSession) {
|
|
4625
4772
|
switch (command) {
|
|
4626
4773
|
// ─── Tabs ──────────────────────────────────────────
|
|
@@ -4702,7 +4849,7 @@ async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2,
|
|
|
4702
4849
|
const lines = entries.map(
|
|
4703
4850
|
(e) => `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}`
|
|
4704
4851
|
).join("\n") + "\n";
|
|
4705
|
-
|
|
4852
|
+
fs13.appendFileSync(consolePath, lines);
|
|
4706
4853
|
buffers.lastConsoleFlushed = buffers.consoleTotalAdded;
|
|
4707
4854
|
}
|
|
4708
4855
|
const newNetworkCount = buffers.networkTotalAdded - buffers.lastNetworkFlushed;
|
|
@@ -4712,7 +4859,7 @@ async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2,
|
|
|
4712
4859
|
const lines = entries.map(
|
|
4713
4860
|
(e) => `[${new Date(e.timestamp).toISOString()}] ${e.method} ${e.url} \u2192 ${e.status || "pending"} (${e.duration || "?"}ms, ${e.size || "?"}B)`
|
|
4714
4861
|
).join("\n") + "\n";
|
|
4715
|
-
|
|
4862
|
+
fs13.appendFileSync(networkPath, lines);
|
|
4716
4863
|
buffers.lastNetworkFlushed = buffers.networkTotalAdded;
|
|
4717
4864
|
}
|
|
4718
4865
|
}
|
|
@@ -4729,22 +4876,22 @@ async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2,
|
|
|
4729
4876
|
const statesDir = `${LOCAL_DIR}/states`;
|
|
4730
4877
|
const statePath = `${statesDir}/${name}.json`;
|
|
4731
4878
|
if (subcommand === "list") {
|
|
4732
|
-
if (!
|
|
4733
|
-
const files =
|
|
4879
|
+
if (!fs13.existsSync(statesDir)) return "(no saved states)";
|
|
4880
|
+
const files = fs13.readdirSync(statesDir).filter((f) => f.endsWith(".json"));
|
|
4734
4881
|
if (files.length === 0) return "(no saved states)";
|
|
4735
4882
|
const lines = [];
|
|
4736
4883
|
for (const file of files) {
|
|
4737
4884
|
const fp = `${statesDir}/${file}`;
|
|
4738
|
-
const stat =
|
|
4885
|
+
const stat = fs13.statSync(fp);
|
|
4739
4886
|
lines.push(` ${file.replace(".json", "")} ${stat.size}B ${new Date(stat.mtimeMs).toISOString()}`);
|
|
4740
4887
|
}
|
|
4741
4888
|
return lines.join("\n");
|
|
4742
4889
|
}
|
|
4743
4890
|
if (subcommand === "show") {
|
|
4744
|
-
if (!
|
|
4891
|
+
if (!fs13.existsSync(statePath)) {
|
|
4745
4892
|
throw new Error(`State file not found: ${statePath}`);
|
|
4746
4893
|
}
|
|
4747
|
-
const data = JSON.parse(
|
|
4894
|
+
const data = JSON.parse(fs13.readFileSync(statePath, "utf-8"));
|
|
4748
4895
|
const cookieCount = data.cookies?.length || 0;
|
|
4749
4896
|
const originCount = data.origins?.length || 0;
|
|
4750
4897
|
const storageItems = (data.origins || []).reduce((sum, o) => sum + (o.localStorage?.length || 0), 0);
|
|
@@ -4769,15 +4916,15 @@ async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2,
|
|
|
4769
4916
|
const context = bm.getContext();
|
|
4770
4917
|
if (!context) throw new Error("No browser context");
|
|
4771
4918
|
const state = await context.storageState();
|
|
4772
|
-
|
|
4773
|
-
|
|
4919
|
+
fs13.mkdirSync(statesDir, { recursive: true });
|
|
4920
|
+
fs13.writeFileSync(statePath, JSON.stringify(state, null, 2), { mode: 384 });
|
|
4774
4921
|
return `State saved: ${statePath}`;
|
|
4775
4922
|
}
|
|
4776
4923
|
if (subcommand === "load") {
|
|
4777
|
-
if (!
|
|
4924
|
+
if (!fs13.existsSync(statePath)) {
|
|
4778
4925
|
throw new Error(`State file not found: ${statePath}. Run "browse state save ${name}" first.`);
|
|
4779
4926
|
}
|
|
4780
|
-
const stateData = JSON.parse(
|
|
4927
|
+
const stateData = JSON.parse(fs13.readFileSync(statePath, "utf-8"));
|
|
4781
4928
|
const context = bm.getContext();
|
|
4782
4929
|
if (!context) throw new Error("No browser context");
|
|
4783
4930
|
const warnings = [];
|
|
@@ -4936,9 +5083,9 @@ ${legend.join("\n")}`;
|
|
|
4936
5083
|
try {
|
|
4937
5084
|
for (const vp of viewports) {
|
|
4938
5085
|
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}): ${
|
|
5086
|
+
const path13 = `${prefix}-${vp.name}.png`;
|
|
5087
|
+
await page.screenshot({ path: path13, fullPage: true });
|
|
5088
|
+
results.push(`${vp.name} (${vp.width}x${vp.height}): ${path13}`);
|
|
4942
5089
|
}
|
|
4943
5090
|
} finally {
|
|
4944
5091
|
if (originalViewport) {
|
|
@@ -5082,13 +5229,13 @@ ${legend.join("\n")}`;
|
|
|
5082
5229
|
const diffArgs = args.filter((a) => a !== "--full");
|
|
5083
5230
|
const baseline = diffArgs[0];
|
|
5084
5231
|
if (!baseline) throw new Error("Usage: browse screenshot-diff <baseline> [current] [--threshold 0.1] [--full]");
|
|
5085
|
-
if (!
|
|
5232
|
+
if (!fs13.existsSync(baseline)) throw new Error(`Baseline file not found: ${baseline}`);
|
|
5086
5233
|
let thresholdPct = 0.1;
|
|
5087
5234
|
const threshIdx = diffArgs.indexOf("--threshold");
|
|
5088
5235
|
if (threshIdx !== -1 && diffArgs[threshIdx + 1]) {
|
|
5089
5236
|
thresholdPct = parseFloat(diffArgs[threshIdx + 1]);
|
|
5090
5237
|
}
|
|
5091
|
-
const baselineBuffer =
|
|
5238
|
+
const baselineBuffer = fs13.readFileSync(baseline);
|
|
5092
5239
|
let currentBuffer;
|
|
5093
5240
|
let currentPath;
|
|
5094
5241
|
for (let i = 1; i < diffArgs.length; i++) {
|
|
@@ -5102,8 +5249,8 @@ ${legend.join("\n")}`;
|
|
|
5102
5249
|
}
|
|
5103
5250
|
}
|
|
5104
5251
|
if (currentPath) {
|
|
5105
|
-
if (!
|
|
5106
|
-
currentBuffer =
|
|
5252
|
+
if (!fs13.existsSync(currentPath)) throw new Error(`Current screenshot not found: ${currentPath}`);
|
|
5253
|
+
currentBuffer = fs13.readFileSync(currentPath);
|
|
5107
5254
|
} else {
|
|
5108
5255
|
const page = bm.getPage();
|
|
5109
5256
|
currentBuffer = await page.screenshot({ fullPage: isFullPageDiff });
|
|
@@ -5113,7 +5260,7 @@ ${legend.join("\n")}`;
|
|
|
5113
5260
|
const extIdx = baseline.lastIndexOf(".");
|
|
5114
5261
|
const diffPath = extIdx > 0 ? baseline.slice(0, extIdx) + "-diff" + baseline.slice(extIdx) : baseline + "-diff.png";
|
|
5115
5262
|
if (!result.passed && result.diffImage) {
|
|
5116
|
-
|
|
5263
|
+
fs13.writeFileSync(diffPath, result.diffImage);
|
|
5117
5264
|
}
|
|
5118
5265
|
return [
|
|
5119
5266
|
`Pixels: ${result.totalPixels}`,
|
|
@@ -5249,7 +5396,7 @@ ${legend.join("\n")}`;
|
|
|
5249
5396
|
const { formatAsHar: formatAsHar2 } = await Promise.resolve().then(() => (init_har(), har_exports));
|
|
5250
5397
|
const har = formatAsHar2(sessionBuffers.networkBuffer, recording.startTime);
|
|
5251
5398
|
const harPath = args[1] || (currentSession ? `${currentSession.outputDir}/recording.har` : `${LOCAL_DIR}/browse-recording.har`);
|
|
5252
|
-
|
|
5399
|
+
fs13.writeFileSync(harPath, JSON.stringify(har, null, 2));
|
|
5253
5400
|
const entryCount = har.log.entries.length;
|
|
5254
5401
|
return `HAR saved: ${harPath} (${entryCount} entries)`;
|
|
5255
5402
|
}
|
|
@@ -5469,13 +5616,52 @@ Manual: npm install -g @ulpi/browse`;
|
|
|
5469
5616
|
}
|
|
5470
5617
|
const filePath = args[2];
|
|
5471
5618
|
if (filePath) {
|
|
5472
|
-
|
|
5619
|
+
fs13.writeFileSync(filePath, output);
|
|
5473
5620
|
return `Exported ${steps.length} steps as ${format}: ${filePath}`;
|
|
5474
5621
|
}
|
|
5475
5622
|
return output;
|
|
5476
5623
|
}
|
|
5477
5624
|
throw new Error("Usage: browse record start | stop | status | export browse|replay [path]");
|
|
5478
5625
|
}
|
|
5626
|
+
// ─── Profile Management ────────────────────────────────
|
|
5627
|
+
case "profile": {
|
|
5628
|
+
const subcommand = args[0];
|
|
5629
|
+
if (!subcommand) throw new Error("Usage: browse profile list | delete <name> | clean [--older-than <days>]");
|
|
5630
|
+
if (subcommand === "list") {
|
|
5631
|
+
const { listProfiles: listProfiles2 } = await Promise.resolve().then(() => (init_browser_manager(), browser_manager_exports));
|
|
5632
|
+
const profiles = listProfiles2(LOCAL_DIR);
|
|
5633
|
+
if (profiles.length === 0) return "No profiles found";
|
|
5634
|
+
return profiles.map((p) => `${p.name} ${p.size} last used: ${p.lastUsed}`).join("\n");
|
|
5635
|
+
}
|
|
5636
|
+
if (subcommand === "delete") {
|
|
5637
|
+
const name = args[1];
|
|
5638
|
+
if (!name) throw new Error("Usage: browse profile delete <name>");
|
|
5639
|
+
const { deleteProfile: deleteProfile2 } = await Promise.resolve().then(() => (init_browser_manager(), browser_manager_exports));
|
|
5640
|
+
deleteProfile2(LOCAL_DIR, name);
|
|
5641
|
+
return `Profile "${name}" deleted`;
|
|
5642
|
+
}
|
|
5643
|
+
if (subcommand === "clean") {
|
|
5644
|
+
const { listProfiles: listProfiles2, deleteProfile: deleteProfile2 } = await Promise.resolve().then(() => (init_browser_manager(), browser_manager_exports));
|
|
5645
|
+
let maxDays = 7;
|
|
5646
|
+
const olderIdx = args.indexOf("--older-than");
|
|
5647
|
+
if (olderIdx !== -1 && args[olderIdx + 1]) {
|
|
5648
|
+
maxDays = parseInt(args[olderIdx + 1], 10);
|
|
5649
|
+
if (isNaN(maxDays)) throw new Error("Usage: browse profile clean --older-than <days>");
|
|
5650
|
+
}
|
|
5651
|
+
const profiles = listProfiles2(LOCAL_DIR);
|
|
5652
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
5653
|
+
cutoff.setDate(cutoff.getDate() - maxDays);
|
|
5654
|
+
let cleaned = 0;
|
|
5655
|
+
for (const p of profiles) {
|
|
5656
|
+
if (new Date(p.lastUsed) < cutoff) {
|
|
5657
|
+
deleteProfile2(LOCAL_DIR, p.name);
|
|
5658
|
+
cleaned++;
|
|
5659
|
+
}
|
|
5660
|
+
}
|
|
5661
|
+
return cleaned > 0 ? `Cleaned ${cleaned} profile(s) older than ${maxDays} days` : "No profiles to clean";
|
|
5662
|
+
}
|
|
5663
|
+
throw new Error("Usage: browse profile list | delete <name> | clean [--older-than <days>]");
|
|
5664
|
+
}
|
|
5479
5665
|
default:
|
|
5480
5666
|
throw new Error(`Unknown meta command: ${command}`);
|
|
5481
5667
|
}
|
|
@@ -5494,8 +5680,8 @@ var init_meta = __esm({
|
|
|
5494
5680
|
|
|
5495
5681
|
// src/server.ts
|
|
5496
5682
|
var server_exports = {};
|
|
5497
|
-
import * as
|
|
5498
|
-
import * as
|
|
5683
|
+
import * as fs14 from "fs";
|
|
5684
|
+
import * as path11 from "path";
|
|
5499
5685
|
import * as crypto3 from "crypto";
|
|
5500
5686
|
import * as http from "http";
|
|
5501
5687
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -5531,9 +5717,13 @@ function validateAuth(req) {
|
|
|
5531
5717
|
const header = req.headers.get("authorization");
|
|
5532
5718
|
return header === `Bearer ${AUTH_TOKEN}`;
|
|
5533
5719
|
}
|
|
5534
|
-
function flushAllBuffers(
|
|
5535
|
-
|
|
5536
|
-
|
|
5720
|
+
function flushAllBuffers(sm, final = false) {
|
|
5721
|
+
if (sm) {
|
|
5722
|
+
for (const session of sm.getAllSessions()) {
|
|
5723
|
+
flushSessionBuffers(session, final);
|
|
5724
|
+
}
|
|
5725
|
+
} else if (profileSession) {
|
|
5726
|
+
flushSessionBuffers(profileSession, final);
|
|
5537
5727
|
}
|
|
5538
5728
|
}
|
|
5539
5729
|
function flushSessionBuffers(session, final) {
|
|
@@ -5547,7 +5737,7 @@ function flushSessionBuffers(session, final) {
|
|
|
5547
5737
|
const lines = newEntries.map(
|
|
5548
5738
|
(e) => `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}`
|
|
5549
5739
|
).join("\n") + "\n";
|
|
5550
|
-
|
|
5740
|
+
fs14.appendFileSync(consolePath, lines);
|
|
5551
5741
|
buffers.lastConsoleFlushed = buffers.consoleTotalAdded;
|
|
5552
5742
|
}
|
|
5553
5743
|
let newNetworkCount = buffers.networkTotalAdded - buffers.lastNetworkFlushed;
|
|
@@ -5572,7 +5762,7 @@ function flushSessionBuffers(session, final) {
|
|
|
5572
5762
|
const lines = prefix.map(
|
|
5573
5763
|
(e) => `[${new Date(e.timestamp).toISOString()}] ${e.method} ${e.url} \u2192 ${e.status || "pending"} (${e.duration || "?"}ms, ${e.size || "?"}B)`
|
|
5574
5764
|
).join("\n") + "\n";
|
|
5575
|
-
|
|
5765
|
+
fs14.appendFileSync(networkPath, lines);
|
|
5576
5766
|
buffers.lastNetworkFlushed += prefixLen;
|
|
5577
5767
|
}
|
|
5578
5768
|
}
|
|
@@ -5686,7 +5876,7 @@ async function handleCommand(body, session, opts) {
|
|
|
5686
5876
|
} else if (WRITE_COMMANDS.has(command)) {
|
|
5687
5877
|
result = await handleWriteCommand(command, args, session.manager, session.domainFilter);
|
|
5688
5878
|
} else if (META_COMMANDS.has(command)) {
|
|
5689
|
-
result = await handleMetaCommand(command, args, session.manager, shutdown, sessionManager, session);
|
|
5879
|
+
result = await handleMetaCommand(command, args, session.manager, shutdown, sessionManager ?? void 0, session);
|
|
5690
5880
|
} else {
|
|
5691
5881
|
const error = `Unknown command: ${command}`;
|
|
5692
5882
|
const hint = `Available commands: ${[...READ_COMMANDS, ...WRITE_COMMANDS, ...META_COMMANDS].sort().join(", ")}`;
|
|
@@ -5745,18 +5935,23 @@ async function shutdown() {
|
|
|
5745
5935
|
clearInterval(flushInterval);
|
|
5746
5936
|
clearInterval(sessionCleanupInterval);
|
|
5747
5937
|
flushAllBuffers(sessionManager, true);
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
browser.removeAllListeners("disconnected");
|
|
5751
|
-
await browser.close().catch(() => {
|
|
5938
|
+
if (profileSession) {
|
|
5939
|
+
await profileSession.manager.close().catch(() => {
|
|
5752
5940
|
});
|
|
5941
|
+
} else if (sessionManager) {
|
|
5942
|
+
await sessionManager.closeAll();
|
|
5943
|
+
if (browser && !isRemoteBrowser) {
|
|
5944
|
+
browser.removeAllListeners("disconnected");
|
|
5945
|
+
await browser.close().catch(() => {
|
|
5946
|
+
});
|
|
5947
|
+
}
|
|
5753
5948
|
}
|
|
5754
5949
|
await activeRuntime?.close?.().catch(() => {
|
|
5755
5950
|
});
|
|
5756
5951
|
try {
|
|
5757
|
-
const currentState = JSON.parse(
|
|
5952
|
+
const currentState = JSON.parse(fs14.readFileSync(STATE_FILE, "utf-8"));
|
|
5758
5953
|
if (currentState.pid === process.pid || currentState.token === AUTH_TOKEN) {
|
|
5759
|
-
|
|
5954
|
+
fs14.unlinkSync(STATE_FILE);
|
|
5760
5955
|
}
|
|
5761
5956
|
} catch {
|
|
5762
5957
|
}
|
|
@@ -5768,38 +5963,65 @@ async function start() {
|
|
|
5768
5963
|
const runtime = await getRuntime(runtimeName);
|
|
5769
5964
|
activeRuntime = runtime;
|
|
5770
5965
|
console.log(`[browse] Runtime: ${runtime.name}`);
|
|
5771
|
-
const
|
|
5772
|
-
if (
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5966
|
+
const profileName = process.env.BROWSE_PROFILE;
|
|
5967
|
+
if (profileName) {
|
|
5968
|
+
const { BrowserManager: BrowserManager2, getProfileDir: getProfileDir2 } = await Promise.resolve().then(() => (init_browser_manager(), browser_manager_exports));
|
|
5969
|
+
const { SessionBuffers: SessionBuffers2 } = await Promise.resolve().then(() => (init_buffers(), buffers_exports));
|
|
5970
|
+
const profileDir = getProfileDir2(LOCAL_DIR2, profileName);
|
|
5971
|
+
fs14.mkdirSync(profileDir, { recursive: true });
|
|
5972
|
+
const bm = new BrowserManager2();
|
|
5973
|
+
await bm.launchPersistent(profileDir, () => {
|
|
5779
5974
|
if (isShuttingDown) return;
|
|
5780
|
-
console.error("[browse]
|
|
5975
|
+
console.error("[browse] Chromium disconnected (profile mode). Shutting down.");
|
|
5781
5976
|
shutdown();
|
|
5782
5977
|
});
|
|
5978
|
+
const outputDir = path11.join(LOCAL_DIR2, "sessions", profileName);
|
|
5979
|
+
fs14.mkdirSync(outputDir, { recursive: true });
|
|
5980
|
+
profileSession = {
|
|
5981
|
+
id: profileName,
|
|
5982
|
+
manager: bm,
|
|
5983
|
+
buffers: new SessionBuffers2(),
|
|
5984
|
+
domainFilter: null,
|
|
5985
|
+
recording: null,
|
|
5986
|
+
outputDir,
|
|
5987
|
+
lastActivity: Date.now(),
|
|
5988
|
+
createdAt: Date.now()
|
|
5989
|
+
};
|
|
5990
|
+
console.log(`[browse] Profile mode: "${profileName}" (${profileDir})`);
|
|
5783
5991
|
} else {
|
|
5784
|
-
const
|
|
5785
|
-
if (
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
if (
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5992
|
+
const cdpUrl = process.env.BROWSE_CDP_URL;
|
|
5993
|
+
if (cdpUrl) {
|
|
5994
|
+
browser = await runtime.chromium.connectOverCDP(cdpUrl);
|
|
5995
|
+
isRemoteBrowser = true;
|
|
5996
|
+
console.log(`[browse] Connected to remote Chrome via CDP: ${cdpUrl}`);
|
|
5997
|
+
} else if (runtime.browser) {
|
|
5998
|
+
browser = runtime.browser;
|
|
5999
|
+
browser.on("disconnected", () => {
|
|
6000
|
+
if (isShuttingDown) return;
|
|
6001
|
+
console.error("[browse] Browser disconnected. Shutting down.");
|
|
6002
|
+
shutdown();
|
|
6003
|
+
});
|
|
6004
|
+
} else {
|
|
6005
|
+
const launchOptions = { headless: process.env.BROWSE_HEADED !== "1" };
|
|
6006
|
+
if (DEBUG_PORT > 0) {
|
|
6007
|
+
launchOptions.args = [`--remote-debugging-port=${DEBUG_PORT}`];
|
|
5793
6008
|
}
|
|
6009
|
+
const proxyServer = process.env.BROWSE_PROXY;
|
|
6010
|
+
if (proxyServer) {
|
|
6011
|
+
launchOptions.proxy = { server: proxyServer };
|
|
6012
|
+
if (process.env.BROWSE_PROXY_BYPASS) {
|
|
6013
|
+
launchOptions.proxy.bypass = process.env.BROWSE_PROXY_BYPASS;
|
|
6014
|
+
}
|
|
6015
|
+
}
|
|
6016
|
+
browser = await runtime.chromium.launch(launchOptions);
|
|
6017
|
+
browser.on("disconnected", () => {
|
|
6018
|
+
if (isShuttingDown) return;
|
|
6019
|
+
console.error("[browse] Chromium disconnected. Shutting down.");
|
|
6020
|
+
shutdown();
|
|
6021
|
+
});
|
|
5794
6022
|
}
|
|
5795
|
-
|
|
5796
|
-
browser.on("disconnected", () => {
|
|
5797
|
-
if (isShuttingDown) return;
|
|
5798
|
-
console.error("[browse] Chromium disconnected. Shutting down.");
|
|
5799
|
-
shutdown();
|
|
5800
|
-
});
|
|
6023
|
+
sessionManager = new SessionManager(browser, LOCAL_DIR2);
|
|
5801
6024
|
}
|
|
5802
|
-
sessionManager = new SessionManager(browser, LOCAL_DIR2);
|
|
5803
6025
|
const startTime = Date.now();
|
|
5804
6026
|
const server = nodeServe({
|
|
5805
6027
|
port,
|
|
@@ -5807,11 +6029,20 @@ async function start() {
|
|
|
5807
6029
|
fetch: async (req) => {
|
|
5808
6030
|
const url = new URL(req.url);
|
|
5809
6031
|
if (url.pathname === "/health") {
|
|
5810
|
-
|
|
6032
|
+
let healthy;
|
|
6033
|
+
let sessionCount;
|
|
6034
|
+
if (profileSession) {
|
|
6035
|
+
healthy = !isShuttingDown && !!profileSession.manager.getContext();
|
|
6036
|
+
sessionCount = 1;
|
|
6037
|
+
} else {
|
|
6038
|
+
healthy = !isShuttingDown && !!browser && browser.isConnected();
|
|
6039
|
+
sessionCount = sessionManager ? sessionManager.getSessionCount() : 0;
|
|
6040
|
+
}
|
|
5811
6041
|
return new Response(JSON.stringify({
|
|
5812
6042
|
status: healthy ? "healthy" : "unhealthy",
|
|
5813
6043
|
uptime: Math.floor((Date.now() - startTime) / 1e3),
|
|
5814
|
-
sessions:
|
|
6044
|
+
sessions: sessionCount,
|
|
6045
|
+
...profileName ? { profile: profileName } : {}
|
|
5815
6046
|
}), {
|
|
5816
6047
|
status: 200,
|
|
5817
6048
|
headers: { "Content-Type": "application/json" }
|
|
@@ -5825,15 +6056,21 @@ async function start() {
|
|
|
5825
6056
|
}
|
|
5826
6057
|
if (url.pathname === "/command" && req.method === "POST") {
|
|
5827
6058
|
const body = await req.json();
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
6059
|
+
let session;
|
|
6060
|
+
if (profileSession) {
|
|
6061
|
+
session = profileSession;
|
|
6062
|
+
session.lastActivity = Date.now();
|
|
6063
|
+
} else {
|
|
6064
|
+
const sessionId = req.headers.get("x-browse-session") || "default";
|
|
6065
|
+
const allowedDomains = req.headers.get("x-browse-allowed-domains") || void 0;
|
|
6066
|
+
session = await sessionManager.getOrCreate(sessionId, allowedDomains);
|
|
6067
|
+
}
|
|
5831
6068
|
const stateFilePath = req.headers.get("x-browse-state");
|
|
5832
6069
|
if (stateFilePath) {
|
|
5833
6070
|
const context = session.manager.getContext();
|
|
5834
6071
|
if (context) {
|
|
5835
6072
|
try {
|
|
5836
|
-
const stateData = JSON.parse(
|
|
6073
|
+
const stateData = JSON.parse(fs14.readFileSync(stateFilePath, "utf-8"));
|
|
5837
6074
|
if (stateData.cookies?.length) {
|
|
5838
6075
|
await context.addCookies(stateData.cookies);
|
|
5839
6076
|
}
|
|
@@ -5860,17 +6097,20 @@ async function start() {
|
|
|
5860
6097
|
port,
|
|
5861
6098
|
token: AUTH_TOKEN,
|
|
5862
6099
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5863
|
-
serverPath:
|
|
6100
|
+
serverPath: path11.resolve(path11.dirname(fileURLToPath2(import.meta.url)), "server.ts")
|
|
5864
6101
|
};
|
|
6102
|
+
if (profileName) {
|
|
6103
|
+
state.profile = profileName;
|
|
6104
|
+
}
|
|
5865
6105
|
if (DEBUG_PORT > 0) {
|
|
5866
6106
|
state.debugPort = DEBUG_PORT;
|
|
5867
6107
|
}
|
|
5868
|
-
|
|
6108
|
+
fs14.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), { mode: 384 });
|
|
5869
6109
|
console.log(`[browse] Server running on http://127.0.0.1:${port} (PID: ${process.pid})`);
|
|
5870
6110
|
console.log(`[browse] State file: ${STATE_FILE}`);
|
|
5871
6111
|
console.log(`[browse] Idle timeout: ${IDLE_TIMEOUT_MS / 1e3}s`);
|
|
5872
6112
|
}
|
|
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;
|
|
6113
|
+
var 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
6114
|
var init_server = __esm({
|
|
5875
6115
|
"src/server.ts"() {
|
|
5876
6116
|
"use strict";
|
|
@@ -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`);
|
|
@@ -6039,8 +6292,8 @@ var init_server = __esm({
|
|
|
6039
6292
|
|
|
6040
6293
|
// src/cli.ts
|
|
6041
6294
|
init_constants();
|
|
6042
|
-
import * as
|
|
6043
|
-
import * as
|
|
6295
|
+
import * as fs15 from "fs";
|
|
6296
|
+
import * as path12 from "path";
|
|
6044
6297
|
import { spawn as spawn3 } from "child_process";
|
|
6045
6298
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
6046
6299
|
|
|
@@ -6083,7 +6336,8 @@ var cliFlags = {
|
|
|
6083
6336
|
headed: false,
|
|
6084
6337
|
stateFile: "",
|
|
6085
6338
|
maxOutput: 0,
|
|
6086
|
-
cdpUrl: ""
|
|
6339
|
+
cdpUrl: "",
|
|
6340
|
+
profile: ""
|
|
6087
6341
|
};
|
|
6088
6342
|
var stateFileApplied = false;
|
|
6089
6343
|
var BROWSE_PORT2 = parseInt(process.env.BROWSE_PORT || "0", 10);
|
|
@@ -6092,50 +6346,50 @@ var INSTANCE_SUFFIX2 = BROWSE_PORT2 ? `-${BROWSE_PORT2}` : BROWSE_INSTANCE2 ? `-
|
|
|
6092
6346
|
function resolveLocalDir() {
|
|
6093
6347
|
if (process.env.BROWSE_LOCAL_DIR) {
|
|
6094
6348
|
try {
|
|
6095
|
-
|
|
6349
|
+
fs15.mkdirSync(process.env.BROWSE_LOCAL_DIR, { recursive: true });
|
|
6096
6350
|
} catch {
|
|
6097
6351
|
}
|
|
6098
6352
|
return process.env.BROWSE_LOCAL_DIR;
|
|
6099
6353
|
}
|
|
6100
6354
|
let dir = process.cwd();
|
|
6101
6355
|
for (let i = 0; i < 20; i++) {
|
|
6102
|
-
if (
|
|
6103
|
-
const browseDir =
|
|
6356
|
+
if (fs15.existsSync(path12.join(dir, ".git")) || fs15.existsSync(path12.join(dir, ".claude"))) {
|
|
6357
|
+
const browseDir = path12.join(dir, ".browse");
|
|
6104
6358
|
try {
|
|
6105
|
-
|
|
6106
|
-
const gi =
|
|
6107
|
-
if (!
|
|
6108
|
-
|
|
6359
|
+
fs15.mkdirSync(browseDir, { recursive: true });
|
|
6360
|
+
const gi = path12.join(browseDir, ".gitignore");
|
|
6361
|
+
if (!fs15.existsSync(gi)) {
|
|
6362
|
+
fs15.writeFileSync(gi, "*\n");
|
|
6109
6363
|
}
|
|
6110
6364
|
} catch {
|
|
6111
6365
|
}
|
|
6112
6366
|
return browseDir;
|
|
6113
6367
|
}
|
|
6114
|
-
const parent =
|
|
6368
|
+
const parent = path12.dirname(dir);
|
|
6115
6369
|
if (parent === dir) break;
|
|
6116
6370
|
dir = parent;
|
|
6117
6371
|
}
|
|
6118
6372
|
return "/tmp";
|
|
6119
6373
|
}
|
|
6120
6374
|
var LOCAL_DIR3 = resolveLocalDir();
|
|
6121
|
-
var STATE_FILE2 = process.env.BROWSE_STATE_FILE ||
|
|
6375
|
+
var STATE_FILE2 = process.env.BROWSE_STATE_FILE || path12.join(LOCAL_DIR3, `browse-server${INSTANCE_SUFFIX2}.json`);
|
|
6122
6376
|
var MAX_START_WAIT = 8e3;
|
|
6123
6377
|
var LOCK_FILE = STATE_FILE2 + ".lock";
|
|
6124
6378
|
var LOCK_STALE_MS = DEFAULTS.LOCK_STALE_THRESHOLD_MS;
|
|
6125
6379
|
var __filename_cli = fileURLToPath3(import.meta.url);
|
|
6126
|
-
var __dirname_cli =
|
|
6380
|
+
var __dirname_cli = path12.dirname(__filename_cli);
|
|
6127
6381
|
function resolveServerScript(env = process.env, metaDir = __dirname_cli) {
|
|
6128
6382
|
if (env.BROWSE_SERVER_SCRIPT) {
|
|
6129
6383
|
return env.BROWSE_SERVER_SCRIPT;
|
|
6130
6384
|
}
|
|
6131
6385
|
if (metaDir.startsWith("/")) {
|
|
6132
|
-
const direct =
|
|
6133
|
-
if (
|
|
6386
|
+
const direct = path12.resolve(metaDir, "server.ts");
|
|
6387
|
+
if (fs15.existsSync(direct)) {
|
|
6134
6388
|
return direct;
|
|
6135
6389
|
}
|
|
6136
6390
|
}
|
|
6137
6391
|
const selfPath = fileURLToPath3(import.meta.url);
|
|
6138
|
-
if (
|
|
6392
|
+
if (fs15.existsSync(selfPath)) {
|
|
6139
6393
|
return "__self__";
|
|
6140
6394
|
}
|
|
6141
6395
|
throw new Error(
|
|
@@ -6145,7 +6399,7 @@ function resolveServerScript(env = process.env, metaDir = __dirname_cli) {
|
|
|
6145
6399
|
var SERVER_SCRIPT = resolveServerScript();
|
|
6146
6400
|
function readState() {
|
|
6147
6401
|
try {
|
|
6148
|
-
const data =
|
|
6402
|
+
const data = fs15.readFileSync(STATE_FILE2, "utf-8");
|
|
6149
6403
|
return JSON.parse(data);
|
|
6150
6404
|
} catch {
|
|
6151
6405
|
return null;
|
|
@@ -6161,7 +6415,7 @@ function isProcessAlive(pid) {
|
|
|
6161
6415
|
}
|
|
6162
6416
|
async function listInstances() {
|
|
6163
6417
|
try {
|
|
6164
|
-
const files =
|
|
6418
|
+
const files = fs15.readdirSync(LOCAL_DIR3).filter(
|
|
6165
6419
|
(f) => f.startsWith("browse-server") && f.endsWith(".json") && !f.endsWith(".lock")
|
|
6166
6420
|
);
|
|
6167
6421
|
if (files.length === 0) {
|
|
@@ -6171,7 +6425,7 @@ async function listInstances() {
|
|
|
6171
6425
|
let found = false;
|
|
6172
6426
|
for (const file of files) {
|
|
6173
6427
|
try {
|
|
6174
|
-
const data = JSON.parse(
|
|
6428
|
+
const data = JSON.parse(fs15.readFileSync(path12.join(LOCAL_DIR3, file), "utf-8"));
|
|
6175
6429
|
if (!data.pid || !data.port) continue;
|
|
6176
6430
|
const alive = isProcessAlive(data.pid);
|
|
6177
6431
|
let status = "dead";
|
|
@@ -6194,7 +6448,7 @@ async function listInstances() {
|
|
|
6194
6448
|
found = true;
|
|
6195
6449
|
if (!alive) {
|
|
6196
6450
|
try {
|
|
6197
|
-
|
|
6451
|
+
fs15.unlinkSync(path12.join(LOCAL_DIR3, file));
|
|
6198
6452
|
} catch {
|
|
6199
6453
|
}
|
|
6200
6454
|
}
|
|
@@ -6217,15 +6471,15 @@ function isBrowseProcess(pid) {
|
|
|
6217
6471
|
}
|
|
6218
6472
|
function acquireLock() {
|
|
6219
6473
|
try {
|
|
6220
|
-
|
|
6474
|
+
fs15.writeFileSync(LOCK_FILE, String(process.pid), { flag: "wx" });
|
|
6221
6475
|
return true;
|
|
6222
6476
|
} catch (err) {
|
|
6223
6477
|
if (err.code === "EEXIST") {
|
|
6224
6478
|
try {
|
|
6225
|
-
const stat =
|
|
6479
|
+
const stat = fs15.statSync(LOCK_FILE);
|
|
6226
6480
|
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
6227
6481
|
try {
|
|
6228
|
-
|
|
6482
|
+
fs15.unlinkSync(LOCK_FILE);
|
|
6229
6483
|
} catch {
|
|
6230
6484
|
}
|
|
6231
6485
|
return acquireLock();
|
|
@@ -6239,7 +6493,7 @@ function acquireLock() {
|
|
|
6239
6493
|
}
|
|
6240
6494
|
function releaseLock() {
|
|
6241
6495
|
try {
|
|
6242
|
-
|
|
6496
|
+
fs15.unlinkSync(LOCK_FILE);
|
|
6243
6497
|
} catch {
|
|
6244
6498
|
}
|
|
6245
6499
|
}
|
|
@@ -6256,7 +6510,7 @@ async function startServer() {
|
|
|
6256
6510
|
}
|
|
6257
6511
|
await sleep(100);
|
|
6258
6512
|
}
|
|
6259
|
-
if (!
|
|
6513
|
+
if (!fs15.existsSync(LOCK_FILE) || fs15.readFileSync(LOCK_FILE, "utf-8").trim() !== String(process.pid)) {
|
|
6260
6514
|
const state = readState();
|
|
6261
6515
|
if (state && isProcessAlive(state.pid)) return state;
|
|
6262
6516
|
throw new Error("Server failed to start (another process is starting it)");
|
|
@@ -6266,13 +6520,13 @@ async function startServer() {
|
|
|
6266
6520
|
try {
|
|
6267
6521
|
const oldState = readState();
|
|
6268
6522
|
if (oldState && !isProcessAlive(oldState.pid)) {
|
|
6269
|
-
|
|
6523
|
+
fs15.unlinkSync(STATE_FILE2);
|
|
6270
6524
|
}
|
|
6271
6525
|
} catch {
|
|
6272
6526
|
}
|
|
6273
6527
|
const selfPath = fileURLToPath3(import.meta.url);
|
|
6274
6528
|
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 } : {} };
|
|
6529
|
+
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 } : {} };
|
|
6276
6530
|
const proc = spawn3(spawnCmd[0], spawnCmd.slice(1), {
|
|
6277
6531
|
stdio: ["ignore", "ignore", "pipe"],
|
|
6278
6532
|
env: spawnEnv,
|
|
@@ -6339,7 +6593,7 @@ async function ensureServer() {
|
|
|
6339
6593
|
}
|
|
6340
6594
|
if (state) {
|
|
6341
6595
|
try {
|
|
6342
|
-
|
|
6596
|
+
fs15.unlinkSync(STATE_FILE2);
|
|
6343
6597
|
} catch {
|
|
6344
6598
|
}
|
|
6345
6599
|
}
|
|
@@ -6349,21 +6603,21 @@ async function ensureServer() {
|
|
|
6349
6603
|
}
|
|
6350
6604
|
function cleanOrphanedServers() {
|
|
6351
6605
|
try {
|
|
6352
|
-
const files =
|
|
6606
|
+
const files = fs15.readdirSync(LOCAL_DIR3);
|
|
6353
6607
|
for (const file of files) {
|
|
6354
6608
|
if (!file.startsWith("browse-server") || !file.endsWith(".json") || file.endsWith(".lock")) continue;
|
|
6355
|
-
const filePath =
|
|
6609
|
+
const filePath = path12.join(LOCAL_DIR3, file);
|
|
6356
6610
|
if (filePath === STATE_FILE2) continue;
|
|
6357
6611
|
try {
|
|
6358
|
-
const data = JSON.parse(
|
|
6612
|
+
const data = JSON.parse(fs15.readFileSync(filePath, "utf-8"));
|
|
6359
6613
|
if (!data.pid) {
|
|
6360
|
-
|
|
6614
|
+
fs15.unlinkSync(filePath);
|
|
6361
6615
|
continue;
|
|
6362
6616
|
}
|
|
6363
6617
|
const suffixMatch = file.match(/browse-server-(\d+)\.json$/);
|
|
6364
6618
|
if (suffixMatch && data.port === parseInt(suffixMatch[1], 10) && isProcessAlive(data.pid)) continue;
|
|
6365
6619
|
if (!isProcessAlive(data.pid)) {
|
|
6366
|
-
|
|
6620
|
+
fs15.unlinkSync(filePath);
|
|
6367
6621
|
continue;
|
|
6368
6622
|
}
|
|
6369
6623
|
if (isBrowseProcess(data.pid)) {
|
|
@@ -6374,7 +6628,7 @@ function cleanOrphanedServers() {
|
|
|
6374
6628
|
}
|
|
6375
6629
|
} catch {
|
|
6376
6630
|
try {
|
|
6377
|
-
|
|
6631
|
+
fs15.unlinkSync(filePath);
|
|
6378
6632
|
} catch {
|
|
6379
6633
|
}
|
|
6380
6634
|
}
|
|
@@ -6475,7 +6729,7 @@ async function sendCommand(state, command, args, retries = 0, sessionId) {
|
|
|
6475
6729
|
await sleep(300);
|
|
6476
6730
|
}
|
|
6477
6731
|
try {
|
|
6478
|
-
|
|
6732
|
+
fs15.unlinkSync(STATE_FILE2);
|
|
6479
6733
|
} catch {
|
|
6480
6734
|
}
|
|
6481
6735
|
if (command === "restart") {
|
|
@@ -6523,7 +6777,7 @@ async function main() {
|
|
|
6523
6777
|
function findCommandIndex(a) {
|
|
6524
6778
|
for (let i = 0; i < a.length; i++) {
|
|
6525
6779
|
if (!a[i].startsWith("-")) return i;
|
|
6526
|
-
if (a[i] === "--session" || a[i] === "--allowed-domains" || a[i] === "--cdp" || a[i] === "--state") i++;
|
|
6780
|
+
if (a[i] === "--session" || a[i] === "--allowed-domains" || a[i] === "--cdp" || a[i] === "--state" || a[i] === "--profile") i++;
|
|
6527
6781
|
}
|
|
6528
6782
|
return a.length;
|
|
6529
6783
|
}
|
|
@@ -6538,6 +6792,21 @@ async function main() {
|
|
|
6538
6792
|
args.splice(sessionIdx, 2);
|
|
6539
6793
|
}
|
|
6540
6794
|
sessionId = sessionId || process.env.BROWSE_SESSION || config.session || void 0;
|
|
6795
|
+
let profileName;
|
|
6796
|
+
const profileIdx = args.indexOf("--profile");
|
|
6797
|
+
if (profileIdx !== -1 && profileIdx < findCommandIndex(args)) {
|
|
6798
|
+
profileName = args[profileIdx + 1];
|
|
6799
|
+
if (!profileName || profileName.startsWith("-")) {
|
|
6800
|
+
console.error("Usage: browse --profile <name> <command> [args...]");
|
|
6801
|
+
process.exit(1);
|
|
6802
|
+
}
|
|
6803
|
+
args.splice(profileIdx, 2);
|
|
6804
|
+
}
|
|
6805
|
+
profileName = profileName || process.env.BROWSE_PROFILE || void 0;
|
|
6806
|
+
if (sessionId && profileName) {
|
|
6807
|
+
console.error("Cannot use --profile and --session together. Profiles use their own Chromium; sessions share one.");
|
|
6808
|
+
process.exit(1);
|
|
6809
|
+
}
|
|
6541
6810
|
let jsonMode = false;
|
|
6542
6811
|
const jsonIdx = args.indexOf("--json");
|
|
6543
6812
|
if (jsonIdx !== -1 && jsonIdx < findCommandIndex(args)) {
|
|
@@ -6636,6 +6905,7 @@ async function main() {
|
|
|
6636
6905
|
cliFlags.stateFile = stateFile;
|
|
6637
6906
|
cliFlags.maxOutput = maxOutput;
|
|
6638
6907
|
cliFlags.cdpUrl = cdpUrl;
|
|
6908
|
+
cliFlags.profile = profileName || "";
|
|
6639
6909
|
if (args[0] === "version" || args[0] === "--version" || args[0] === "-V") {
|
|
6640
6910
|
const pkg = await Promise.resolve().then(() => __toESM(require_package(), 1));
|
|
6641
6911
|
console.log(pkg.version);
|
|
@@ -6704,6 +6974,7 @@ Setup: install-skill [path]
|
|
|
6704
6974
|
|
|
6705
6975
|
Options:
|
|
6706
6976
|
--session <id> Named session (isolates tabs, refs, cookies)
|
|
6977
|
+
--profile <name> Persistent browser profile (own Chromium, full state persistence)
|
|
6707
6978
|
--json Wrap output as {success, data, command}
|
|
6708
6979
|
--content-boundaries Wrap page content in nonce-delimited markers
|
|
6709
6980
|
--allowed-domains <d,d> Block navigation/resources outside allowlist
|
|
@@ -6743,7 +7014,7 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
|
|
|
6743
7014
|
}
|
|
6744
7015
|
if (process.env.__BROWSE_SERVER_MODE === "1") {
|
|
6745
7016
|
Promise.resolve().then(() => init_server());
|
|
6746
|
-
} else if (process.argv[1] &&
|
|
7017
|
+
} else if (process.argv[1] && fs15.realpathSync(process.argv[1]) === fs15.realpathSync(__filename_cli)) {
|
|
6747
7018
|
main().catch((err) => {
|
|
6748
7019
|
console.error(`[browse] ${err.message}`);
|
|
6749
7020
|
process.exit(1);
|