@ulpi/browse 1.0.6 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -1
- package/dist/browse.cjs +813 -94
- package/package.json +3 -4
- package/skill/SKILL.md +115 -319
- package/skill/references/commands.md +482 -0
- package/skill/references/guides.md +177 -0
- package/skill/references/permissions.md +51 -0
- package/BENCHMARKS.md +0 -222
package/dist/browse.cjs
CHANGED
|
@@ -124,7 +124,7 @@ var require_package = __commonJS({
|
|
|
124
124
|
"package.json"(exports2, module2) {
|
|
125
125
|
module2.exports = {
|
|
126
126
|
name: "@ulpi/browse",
|
|
127
|
-
version: "1.
|
|
127
|
+
version: "1.1.1",
|
|
128
128
|
repository: {
|
|
129
129
|
type: "git",
|
|
130
130
|
url: "https://github.com/ulpi-io/browse"
|
|
@@ -149,8 +149,7 @@ var require_package = __commonJS({
|
|
|
149
149
|
"dist/",
|
|
150
150
|
"skill/",
|
|
151
151
|
"LICENSE",
|
|
152
|
-
"README.md"
|
|
153
|
-
"BENCHMARKS.md"
|
|
152
|
+
"README.md"
|
|
154
153
|
],
|
|
155
154
|
keywords: [
|
|
156
155
|
"browser",
|
|
@@ -174,7 +173,7 @@ const __import_meta_url = require("url").pathToFileURL(__filename).href;' --defi
|
|
|
174
173
|
test: "vitest run",
|
|
175
174
|
start: "tsx src/server.ts",
|
|
176
175
|
postinstall: "npx playwright install chromium",
|
|
177
|
-
benchmark: "
|
|
176
|
+
benchmark: "echo 'Benchmarks moved to separate repo'"
|
|
178
177
|
},
|
|
179
178
|
type: "module",
|
|
180
179
|
devDependencies: {
|
|
@@ -209,15 +208,31 @@ function installSkill(targetDir) {
|
|
|
209
208
|
}
|
|
210
209
|
const skillDir = path3.join(dir, ".claude", "skills", "browse");
|
|
211
210
|
fs3.mkdirSync(skillDir, { recursive: true });
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
console.error(`SKILL.md not found at ${skillSource}`);
|
|
211
|
+
const skillSourceDir = path3.resolve(path3.dirname((0, import_url.fileURLToPath)(__import_meta_url)), "..", "skill");
|
|
212
|
+
if (!fs3.existsSync(skillSourceDir)) {
|
|
213
|
+
console.error(`Skill directory not found at ${skillSourceDir}`);
|
|
216
214
|
console.error("Is @ulpi/browse installed correctly?");
|
|
217
215
|
process.exit(1);
|
|
218
216
|
}
|
|
219
|
-
fs3.
|
|
220
|
-
|
|
217
|
+
const mdFiles = fs3.readdirSync(skillSourceDir).filter((f) => f.endsWith(".md"));
|
|
218
|
+
if (mdFiles.length === 0) {
|
|
219
|
+
console.error(`No .md files found in ${skillSourceDir}`);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
for (const file of mdFiles) {
|
|
223
|
+
fs3.copyFileSync(path3.join(skillSourceDir, file), path3.join(skillDir, file));
|
|
224
|
+
console.log(`Skill installed: ${path3.relative(dir, path3.join(skillDir, file))}`);
|
|
225
|
+
}
|
|
226
|
+
const refsSourceDir = path3.join(skillSourceDir, "references");
|
|
227
|
+
if (fs3.existsSync(refsSourceDir)) {
|
|
228
|
+
const refsDestDir = path3.join(skillDir, "references");
|
|
229
|
+
fs3.mkdirSync(refsDestDir, { recursive: true });
|
|
230
|
+
const refFiles = fs3.readdirSync(refsSourceDir).filter((f) => f.endsWith(".md"));
|
|
231
|
+
for (const file of refFiles) {
|
|
232
|
+
fs3.copyFileSync(path3.join(refsSourceDir, file), path3.join(refsDestDir, file));
|
|
233
|
+
console.log(`Skill installed: ${path3.relative(dir, path3.join(refsDestDir, file))}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
221
236
|
const settingsPath = path3.join(dir, ".claude", "settings.json");
|
|
222
237
|
let settings = {};
|
|
223
238
|
if (fs3.existsSync(settingsPath)) {
|
|
@@ -718,6 +733,10 @@ var init_browser_manager = __esm({
|
|
|
718
733
|
ownsBrowser = false;
|
|
719
734
|
// Whether this instance uses a persistent browser context (profile mode)
|
|
720
735
|
isPersistent = false;
|
|
736
|
+
// ─── Handoff state ──────────────────────────────────────
|
|
737
|
+
isHeaded = false;
|
|
738
|
+
consecutiveFailures = 0;
|
|
739
|
+
onCrashCallback;
|
|
721
740
|
constructor(buffers) {
|
|
722
741
|
this.buffers = buffers || new SessionBuffers();
|
|
723
742
|
}
|
|
@@ -737,6 +756,7 @@ var init_browser_manager = __esm({
|
|
|
737
756
|
async launch(onCrash) {
|
|
738
757
|
this.browser = await import_playwright.chromium.launch({ headless: true });
|
|
739
758
|
this.ownsBrowser = true;
|
|
759
|
+
this.onCrashCallback = onCrash;
|
|
740
760
|
this.browser.on("disconnected", () => {
|
|
741
761
|
if (onCrash) onCrash();
|
|
742
762
|
});
|
|
@@ -775,9 +795,9 @@ var init_browser_manager = __esm({
|
|
|
775
795
|
});
|
|
776
796
|
} catch (err) {
|
|
777
797
|
if (err.message?.includes("Failed to launch") || err.message?.includes("Target closed")) {
|
|
778
|
-
const
|
|
798
|
+
const fs17 = await import("fs");
|
|
779
799
|
console.error(`[browse] Profile directory corrupted, recreating: ${profileDir}`);
|
|
780
|
-
|
|
800
|
+
fs17.rmSync(profileDir, { recursive: true, force: true });
|
|
781
801
|
context = await import_playwright.chromium.launchPersistentContext(profileDir, {
|
|
782
802
|
headless: process.env.BROWSE_HEADED !== "1",
|
|
783
803
|
viewport: { width: 1920, height: 1080 }
|
|
@@ -844,6 +864,230 @@ var init_browser_manager = __esm({
|
|
|
844
864
|
getIsPersistent() {
|
|
845
865
|
return this.isPersistent;
|
|
846
866
|
}
|
|
867
|
+
// ─── React DevTools ──────────────────────────────────
|
|
868
|
+
reactDevToolsEnabled = false;
|
|
869
|
+
getReactDevToolsEnabled() {
|
|
870
|
+
return this.reactDevToolsEnabled;
|
|
871
|
+
}
|
|
872
|
+
setReactDevToolsEnabled(enabled) {
|
|
873
|
+
this.reactDevToolsEnabled = enabled;
|
|
874
|
+
}
|
|
875
|
+
getIsHeaded() {
|
|
876
|
+
return this.isHeaded;
|
|
877
|
+
}
|
|
878
|
+
// ─── Failure tracking (auto-suggest handoff) ────────────
|
|
879
|
+
incrementFailures() {
|
|
880
|
+
this.consecutiveFailures++;
|
|
881
|
+
}
|
|
882
|
+
resetFailures() {
|
|
883
|
+
this.consecutiveFailures = 0;
|
|
884
|
+
}
|
|
885
|
+
getFailureHint() {
|
|
886
|
+
if (this.consecutiveFailures >= 3 && !this.isHeaded) {
|
|
887
|
+
return `HINT: ${this.consecutiveFailures} consecutive failures. Consider using 'handoff' to let the user help.`;
|
|
888
|
+
}
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
// ─── State save/restore (shared by handoff/resume) ──────
|
|
892
|
+
async saveState() {
|
|
893
|
+
if (!this.context) throw new Error("Browser not launched");
|
|
894
|
+
const cookies = await this.context.cookies();
|
|
895
|
+
const pages = [];
|
|
896
|
+
for (const [id, page] of this.pages) {
|
|
897
|
+
const url = page.url();
|
|
898
|
+
let storage = null;
|
|
899
|
+
try {
|
|
900
|
+
storage = await page.evaluate(() => ({
|
|
901
|
+
localStorage: { ...localStorage },
|
|
902
|
+
sessionStorage: { ...sessionStorage }
|
|
903
|
+
}));
|
|
904
|
+
} catch {
|
|
905
|
+
}
|
|
906
|
+
pages.push({
|
|
907
|
+
url: url === "about:blank" ? "" : url,
|
|
908
|
+
isActive: id === this.activeTabId,
|
|
909
|
+
storage
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
return { cookies, pages };
|
|
913
|
+
}
|
|
914
|
+
async applyContextOptions(context) {
|
|
915
|
+
if (Object.keys(this.extraHeaders).length > 0) {
|
|
916
|
+
await context.setExtraHTTPHeaders(this.extraHeaders);
|
|
917
|
+
}
|
|
918
|
+
if (this.offline) {
|
|
919
|
+
await context.setOffline(true);
|
|
920
|
+
}
|
|
921
|
+
if (this.initScript) {
|
|
922
|
+
await context.addInitScript(this.initScript);
|
|
923
|
+
}
|
|
924
|
+
for (const r of this.userRoutes) {
|
|
925
|
+
if (r.action === "block") {
|
|
926
|
+
await context.route(r.pattern, (route) => route.abort("blockedbyclient"));
|
|
927
|
+
} else {
|
|
928
|
+
await context.route(r.pattern, (route) => route.fulfill({ status: r.status || 200, body: r.body || "", contentType: "text/plain" }));
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
if (this.domainFilter) {
|
|
932
|
+
const df = this.domainFilter;
|
|
933
|
+
await context.route("**/*", (route) => {
|
|
934
|
+
const url = route.request().url();
|
|
935
|
+
if (df.isAllowed(url)) {
|
|
936
|
+
route.fallback();
|
|
937
|
+
} else {
|
|
938
|
+
route.abort("blockedbyclient");
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
async restoreState(state) {
|
|
944
|
+
if (!this.context) throw new Error("Browser not launched");
|
|
945
|
+
if (state.cookies.length > 0) {
|
|
946
|
+
await this.context.addCookies(state.cookies);
|
|
947
|
+
}
|
|
948
|
+
await this.applyContextOptions(this.context);
|
|
949
|
+
let activeId = null;
|
|
950
|
+
for (const saved of state.pages) {
|
|
951
|
+
const page = await this.context.newPage();
|
|
952
|
+
const id = this.nextTabId++;
|
|
953
|
+
this.pages.set(id, page);
|
|
954
|
+
this.wirePageEvents(page);
|
|
955
|
+
if (saved.url) {
|
|
956
|
+
await page.goto(saved.url, { waitUntil: "domcontentloaded", timeout: 15e3 }).catch(() => {
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
if (saved.storage) {
|
|
960
|
+
try {
|
|
961
|
+
await page.evaluate((s) => {
|
|
962
|
+
if (s.localStorage) {
|
|
963
|
+
for (const [k, v] of Object.entries(s.localStorage)) localStorage.setItem(k, v);
|
|
964
|
+
}
|
|
965
|
+
if (s.sessionStorage) {
|
|
966
|
+
for (const [k, v] of Object.entries(s.sessionStorage)) sessionStorage.setItem(k, v);
|
|
967
|
+
}
|
|
968
|
+
}, saved.storage);
|
|
969
|
+
} catch {
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (saved.isActive) activeId = id;
|
|
973
|
+
}
|
|
974
|
+
if (this.pages.size === 0) {
|
|
975
|
+
await this.newTab();
|
|
976
|
+
} else if (activeId !== null) {
|
|
977
|
+
this.activeTabId = activeId;
|
|
978
|
+
}
|
|
979
|
+
this.clearRefs();
|
|
980
|
+
}
|
|
981
|
+
// ─── Handoff: headless ↔ headed swap ─────────────────────
|
|
982
|
+
async handoff(message) {
|
|
983
|
+
if (this.isHeaded) {
|
|
984
|
+
return `Already in headed mode at ${this.getCurrentUrl()}`;
|
|
985
|
+
}
|
|
986
|
+
if (this.isPersistent) {
|
|
987
|
+
throw new Error("Handoff not supported in profile mode \u2014 the browser is already visible. Use cookie-import or auth login instead.");
|
|
988
|
+
}
|
|
989
|
+
if (!this.browser || !this.context) {
|
|
990
|
+
throw new Error("Browser not launched");
|
|
991
|
+
}
|
|
992
|
+
const state = await this.saveState();
|
|
993
|
+
const currentUrl = this.getCurrentUrl();
|
|
994
|
+
let newBrowser;
|
|
995
|
+
try {
|
|
996
|
+
newBrowser = await import_playwright.chromium.launch({ headless: false, timeout: 15e3 });
|
|
997
|
+
} catch (err) {
|
|
998
|
+
return `Cannot open headed browser \u2014 ${err.message}. Headless browser still running.`;
|
|
999
|
+
}
|
|
1000
|
+
try {
|
|
1001
|
+
const newContext = await newBrowser.newContext({
|
|
1002
|
+
viewport: this.currentDevice?.viewport || { width: 1920, height: 1080 },
|
|
1003
|
+
...this.customUserAgent ? { userAgent: this.customUserAgent } : {},
|
|
1004
|
+
...this.currentDevice ? {
|
|
1005
|
+
deviceScaleFactor: this.currentDevice.deviceScaleFactor,
|
|
1006
|
+
isMobile: this.currentDevice.isMobile,
|
|
1007
|
+
hasTouch: this.currentDevice.hasTouch
|
|
1008
|
+
} : {}
|
|
1009
|
+
});
|
|
1010
|
+
const oldContext = this.context;
|
|
1011
|
+
this.browser = newBrowser;
|
|
1012
|
+
this.context = newContext;
|
|
1013
|
+
this.pages.clear();
|
|
1014
|
+
this.nextTabId = 1;
|
|
1015
|
+
this.tabSnapshots.clear();
|
|
1016
|
+
this.activeFramePerTab.clear();
|
|
1017
|
+
const onCrash = this.onCrashCallback;
|
|
1018
|
+
this.browser.on("disconnected", () => {
|
|
1019
|
+
if (onCrash) onCrash();
|
|
1020
|
+
});
|
|
1021
|
+
await this.restoreState(state);
|
|
1022
|
+
this.isHeaded = true;
|
|
1023
|
+
this.ownsBrowser = true;
|
|
1024
|
+
if (oldContext) {
|
|
1025
|
+
await oldContext.close().catch(() => {
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
return [
|
|
1029
|
+
`Handoff active \u2014 browser opened in visible mode.`,
|
|
1030
|
+
`Reason: ${message || "manual intervention needed"}`,
|
|
1031
|
+
`URL: ${currentUrl}`,
|
|
1032
|
+
``,
|
|
1033
|
+
`The user can now interact with the visible browser.`,
|
|
1034
|
+
`When done, run: browse resume`
|
|
1035
|
+
].join("\n");
|
|
1036
|
+
} catch (err) {
|
|
1037
|
+
await newBrowser.close().catch(() => {
|
|
1038
|
+
});
|
|
1039
|
+
return `Handoff failed during state restore \u2014 ${err.message}. Headless browser still running.`;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
async resume() {
|
|
1043
|
+
if (!this.isHeaded) {
|
|
1044
|
+
throw new Error('Not in handoff mode \u2014 run "handoff" first.');
|
|
1045
|
+
}
|
|
1046
|
+
if (!this.browser || !this.context) {
|
|
1047
|
+
throw new Error("Browser not launched");
|
|
1048
|
+
}
|
|
1049
|
+
const state = await this.saveState();
|
|
1050
|
+
const userUrl = this.getCurrentUrl();
|
|
1051
|
+
let newBrowser;
|
|
1052
|
+
try {
|
|
1053
|
+
newBrowser = await import_playwright.chromium.launch({ headless: true });
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
return `Cannot launch headless browser \u2014 ${err.message}. Headed browser still running.`;
|
|
1056
|
+
}
|
|
1057
|
+
try {
|
|
1058
|
+
const newContext = await newBrowser.newContext({
|
|
1059
|
+
viewport: this.currentDevice?.viewport || { width: 1920, height: 1080 },
|
|
1060
|
+
...this.customUserAgent ? { userAgent: this.customUserAgent } : {},
|
|
1061
|
+
...this.currentDevice ? {
|
|
1062
|
+
deviceScaleFactor: this.currentDevice.deviceScaleFactor,
|
|
1063
|
+
isMobile: this.currentDevice.isMobile,
|
|
1064
|
+
hasTouch: this.currentDevice.hasTouch
|
|
1065
|
+
} : {}
|
|
1066
|
+
});
|
|
1067
|
+
const oldBrowser = this.browser;
|
|
1068
|
+
this.browser = newBrowser;
|
|
1069
|
+
this.context = newContext;
|
|
1070
|
+
this.pages.clear();
|
|
1071
|
+
this.nextTabId = 1;
|
|
1072
|
+
this.tabSnapshots.clear();
|
|
1073
|
+
this.activeFramePerTab.clear();
|
|
1074
|
+
const onCrash = this.onCrashCallback;
|
|
1075
|
+
this.browser.on("disconnected", () => {
|
|
1076
|
+
if (onCrash) onCrash();
|
|
1077
|
+
});
|
|
1078
|
+
await this.restoreState(state);
|
|
1079
|
+
this.isHeaded = false;
|
|
1080
|
+
this.consecutiveFailures = 0;
|
|
1081
|
+
oldBrowser.removeAllListeners("disconnected");
|
|
1082
|
+
oldBrowser.close().catch(() => {
|
|
1083
|
+
});
|
|
1084
|
+
return userUrl;
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
await newBrowser.close().catch(() => {
|
|
1087
|
+
});
|
|
1088
|
+
return `Resume failed during state restore \u2014 ${err.message}. Headed browser still running.`;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
847
1091
|
// ─── Tab Management ────────────────────────────────────────
|
|
848
1092
|
async newTab(url) {
|
|
849
1093
|
if (!this.context) throw new Error("Browser not launched");
|
|
@@ -1308,8 +1552,8 @@ var init_browser_manager = __esm({
|
|
|
1308
1552
|
// ─── Video Recording ──────────────────────────────────────
|
|
1309
1553
|
async startVideoRecording(dir) {
|
|
1310
1554
|
if (this.videoRecording) throw new Error("Video recording already active");
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1555
|
+
const fs17 = await import("fs");
|
|
1556
|
+
fs17.mkdirSync(dir, { recursive: true });
|
|
1313
1557
|
this.videoRecording = { dir, startedAt: Date.now() };
|
|
1314
1558
|
const viewport = this.currentDevice?.viewport || { width: 1920, height: 1080 };
|
|
1315
1559
|
await this.recreateContext({
|
|
@@ -2505,6 +2749,21 @@ async function handleWriteCommand(command, args, bm, domainFilter) {
|
|
|
2505
2749
|
await page.waitForLoadState(state2, { timeout: timeout2 });
|
|
2506
2750
|
return `Load state reached: ${state2}`;
|
|
2507
2751
|
}
|
|
2752
|
+
if (selector === "--download") {
|
|
2753
|
+
const pathOrTimeout = args[1];
|
|
2754
|
+
const timeout2 = args[2] ? parseInt(args[2], 10) : pathOrTimeout && /^\d+$/.test(pathOrTimeout) ? parseInt(pathOrTimeout, 10) : 3e4;
|
|
2755
|
+
const savePath = pathOrTimeout && !/^\d+$/.test(pathOrTimeout) ? pathOrTimeout : null;
|
|
2756
|
+
const download = await page.waitForEvent("download", { timeout: timeout2 });
|
|
2757
|
+
const filename = download.suggestedFilename();
|
|
2758
|
+
const failure = await download.failure();
|
|
2759
|
+
if (failure) return `Download failed: ${filename} \u2014 ${failure}`;
|
|
2760
|
+
if (savePath) {
|
|
2761
|
+
await download.saveAs(savePath);
|
|
2762
|
+
return `Downloaded: ${savePath} (${filename})`;
|
|
2763
|
+
}
|
|
2764
|
+
const tmpPath = await download.path();
|
|
2765
|
+
return `Downloaded: ${filename} (saved to ${tmpPath})`;
|
|
2766
|
+
}
|
|
2508
2767
|
if (/^\d+$/.test(selector)) {
|
|
2509
2768
|
const ms = parseInt(selector, 10);
|
|
2510
2769
|
await page.waitForTimeout(ms);
|
|
@@ -2961,6 +3220,11 @@ var init_write = __esm({
|
|
|
2961
3220
|
});
|
|
2962
3221
|
|
|
2963
3222
|
// src/snapshot.ts
|
|
3223
|
+
var snapshot_exports = {};
|
|
3224
|
+
__export(snapshot_exports, {
|
|
3225
|
+
handleSnapshot: () => handleSnapshot,
|
|
3226
|
+
parseSnapshotArgs: () => parseSnapshotArgs
|
|
3227
|
+
});
|
|
2964
3228
|
function parseSnapshotArgs(args) {
|
|
2965
3229
|
const opts = {};
|
|
2966
3230
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -3683,11 +3947,11 @@ var init_lib = __esm({
|
|
|
3683
3947
|
}
|
|
3684
3948
|
}
|
|
3685
3949
|
},
|
|
3686
|
-
addToPath: function addToPath(
|
|
3687
|
-
var last =
|
|
3950
|
+
addToPath: function addToPath(path14, added, removed, oldPosInc, options) {
|
|
3951
|
+
var last = path14.lastComponent;
|
|
3688
3952
|
if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
|
|
3689
3953
|
return {
|
|
3690
|
-
oldPos:
|
|
3954
|
+
oldPos: path14.oldPos + oldPosInc,
|
|
3691
3955
|
lastComponent: {
|
|
3692
3956
|
count: last.count + 1,
|
|
3693
3957
|
added,
|
|
@@ -3697,7 +3961,7 @@ var init_lib = __esm({
|
|
|
3697
3961
|
};
|
|
3698
3962
|
} else {
|
|
3699
3963
|
return {
|
|
3700
|
-
oldPos:
|
|
3964
|
+
oldPos: path14.oldPos + oldPosInc,
|
|
3701
3965
|
lastComponent: {
|
|
3702
3966
|
count: 1,
|
|
3703
3967
|
added,
|
|
@@ -4767,6 +5031,385 @@ var init_record_export = __esm({
|
|
|
4767
5031
|
}
|
|
4768
5032
|
});
|
|
4769
5033
|
|
|
5034
|
+
// src/react-devtools.ts
|
|
5035
|
+
var react_devtools_exports = {};
|
|
5036
|
+
__export(react_devtools_exports, {
|
|
5037
|
+
ensureHook: () => ensureHook,
|
|
5038
|
+
getContext: () => getContext,
|
|
5039
|
+
getErrors: () => getErrors,
|
|
5040
|
+
getHydration: () => getHydration,
|
|
5041
|
+
getOwners: () => getOwners,
|
|
5042
|
+
getProfiler: () => getProfiler,
|
|
5043
|
+
getProps: () => getProps,
|
|
5044
|
+
getRenders: () => getRenders,
|
|
5045
|
+
getSuspense: () => getSuspense,
|
|
5046
|
+
getTree: () => getTree,
|
|
5047
|
+
injectHook: () => injectHook,
|
|
5048
|
+
isEnabled: () => isEnabled,
|
|
5049
|
+
removeHook: () => removeHook,
|
|
5050
|
+
requireEnabled: () => requireEnabled,
|
|
5051
|
+
requireReact: () => requireReact
|
|
5052
|
+
});
|
|
5053
|
+
async function ensureHook() {
|
|
5054
|
+
if (fs13.existsSync(HOOK_PATH)) {
|
|
5055
|
+
return fs13.readFileSync(HOOK_PATH, "utf8");
|
|
5056
|
+
}
|
|
5057
|
+
fs13.mkdirSync(CACHE_DIR, { recursive: true });
|
|
5058
|
+
const res = await fetch(HOOK_URL);
|
|
5059
|
+
if (!res.ok) {
|
|
5060
|
+
throw new Error(
|
|
5061
|
+
`Failed to download React DevTools hook (HTTP ${res.status}).
|
|
5062
|
+
Manual fallback: npm install -g react-devtools-core, then copy installHook.js to ${HOOK_PATH}`
|
|
5063
|
+
);
|
|
5064
|
+
}
|
|
5065
|
+
const script = await res.text();
|
|
5066
|
+
fs13.writeFileSync(HOOK_PATH, script);
|
|
5067
|
+
return script;
|
|
5068
|
+
}
|
|
5069
|
+
async function injectHook(bm) {
|
|
5070
|
+
const hookScript = await ensureHook();
|
|
5071
|
+
const context = bm.getContext();
|
|
5072
|
+
if (!context) throw new Error("No browser context available");
|
|
5073
|
+
await context.addInitScript(hookScript);
|
|
5074
|
+
bm.setReactDevToolsEnabled(true);
|
|
5075
|
+
}
|
|
5076
|
+
function removeHook(bm) {
|
|
5077
|
+
bm.setReactDevToolsEnabled(false);
|
|
5078
|
+
}
|
|
5079
|
+
function isEnabled(bm) {
|
|
5080
|
+
return bm.getReactDevToolsEnabled();
|
|
5081
|
+
}
|
|
5082
|
+
function requireEnabled(bm) {
|
|
5083
|
+
if (!isEnabled(bm)) {
|
|
5084
|
+
throw new Error(
|
|
5085
|
+
'React DevTools not enabled. Run "browse react-devtools enable" first.'
|
|
5086
|
+
);
|
|
5087
|
+
}
|
|
5088
|
+
}
|
|
5089
|
+
async function requireReact(page) {
|
|
5090
|
+
const hasReact = await page.evaluate(() => {
|
|
5091
|
+
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
5092
|
+
if (!hook) return false;
|
|
5093
|
+
if (hook.rendererInterfaces && hook.rendererInterfaces.size > 0) return true;
|
|
5094
|
+
if (hook.renderers && hook.renderers.size > 0) return true;
|
|
5095
|
+
return false;
|
|
5096
|
+
});
|
|
5097
|
+
if (!hasReact) {
|
|
5098
|
+
throw new Error("No React detected on this page.");
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
async function getTree(bm, page) {
|
|
5102
|
+
requireEnabled(bm);
|
|
5103
|
+
await requireReact(page);
|
|
5104
|
+
const tree = await page.evaluate(() => {
|
|
5105
|
+
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
5106
|
+
function walkFiber(fiber, depth) {
|
|
5107
|
+
const lines2 = [];
|
|
5108
|
+
if (!fiber) return lines2;
|
|
5109
|
+
const name = fiber.type?.displayName || fiber.type?.name || (typeof fiber.type === "string" ? fiber.type : null);
|
|
5110
|
+
if (name) {
|
|
5111
|
+
const indent = " ".repeat(depth);
|
|
5112
|
+
let extra = "";
|
|
5113
|
+
if (fiber.tag === 13) {
|
|
5114
|
+
const resolved = fiber.memoizedState === null;
|
|
5115
|
+
extra = resolved ? " (resolved)" : " (pending)";
|
|
5116
|
+
}
|
|
5117
|
+
lines2.push(`${indent}${name}${extra}`);
|
|
5118
|
+
}
|
|
5119
|
+
let child = fiber.child;
|
|
5120
|
+
while (child) {
|
|
5121
|
+
lines2.push(...walkFiber(child, name ? depth + 1 : depth));
|
|
5122
|
+
child = child.sibling;
|
|
5123
|
+
}
|
|
5124
|
+
return lines2;
|
|
5125
|
+
}
|
|
5126
|
+
const rendererID = hook.renderers?.keys().next().value ?? 1;
|
|
5127
|
+
const roots = hook.getFiberRoots?.(rendererID);
|
|
5128
|
+
if (!roots || roots.size === 0) return "(no fiber roots found)";
|
|
5129
|
+
const root = roots.values().next().value;
|
|
5130
|
+
const current = root.current;
|
|
5131
|
+
if (!current) return "(no current fiber)";
|
|
5132
|
+
const lines = walkFiber(current, 0);
|
|
5133
|
+
return lines.join("\n") || "(empty tree)";
|
|
5134
|
+
});
|
|
5135
|
+
return tree || "(no component tree available)";
|
|
5136
|
+
}
|
|
5137
|
+
async function getProps(bm, page, selector) {
|
|
5138
|
+
requireEnabled(bm);
|
|
5139
|
+
await requireReact(page);
|
|
5140
|
+
const resolved = bm.resolveRef(selector);
|
|
5141
|
+
const element = "locator" in resolved ? resolved.locator : page.locator(resolved.selector);
|
|
5142
|
+
const handle = await element.elementHandle();
|
|
5143
|
+
if (!handle) throw new Error(`Element not found: ${selector}`);
|
|
5144
|
+
const result = await page.evaluate((el) => {
|
|
5145
|
+
let fiber = null;
|
|
5146
|
+
for (const key of Object.keys(el)) {
|
|
5147
|
+
if (key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$")) {
|
|
5148
|
+
fiber = el[key];
|
|
5149
|
+
break;
|
|
5150
|
+
}
|
|
5151
|
+
}
|
|
5152
|
+
if (!fiber) return "No React fiber found for this element";
|
|
5153
|
+
while (fiber && typeof fiber.type === "string") {
|
|
5154
|
+
fiber = fiber.return;
|
|
5155
|
+
}
|
|
5156
|
+
if (!fiber) return "No React component found";
|
|
5157
|
+
const name = fiber.type?.displayName || fiber.type?.name || "Anonymous";
|
|
5158
|
+
const props = fiber.memoizedProps || {};
|
|
5159
|
+
const state = fiber.memoizedState;
|
|
5160
|
+
const propEntries = [];
|
|
5161
|
+
for (const [k, v] of Object.entries(props)) {
|
|
5162
|
+
if (k === "children") continue;
|
|
5163
|
+
if (typeof v === "function") {
|
|
5164
|
+
propEntries.push(`${k}: [Function]`);
|
|
5165
|
+
continue;
|
|
5166
|
+
}
|
|
5167
|
+
try {
|
|
5168
|
+
propEntries.push(`${k}: ${JSON.stringify(v)}`);
|
|
5169
|
+
} catch {
|
|
5170
|
+
propEntries.push(`${k}: [Complex]`);
|
|
5171
|
+
}
|
|
5172
|
+
}
|
|
5173
|
+
const stateEntries = [];
|
|
5174
|
+
let hookNode = state;
|
|
5175
|
+
let hookIdx = 0;
|
|
5176
|
+
while (hookNode && typeof hookNode === "object" && "memoizedState" in hookNode) {
|
|
5177
|
+
const val = hookNode.memoizedState;
|
|
5178
|
+
if (val !== void 0 && val !== null) {
|
|
5179
|
+
try {
|
|
5180
|
+
stateEntries.push(`hook[${hookIdx}]: ${JSON.stringify(val)}`);
|
|
5181
|
+
} catch {
|
|
5182
|
+
stateEntries.push(`hook[${hookIdx}]: [Complex]`);
|
|
5183
|
+
}
|
|
5184
|
+
}
|
|
5185
|
+
hookNode = hookNode.next;
|
|
5186
|
+
hookIdx++;
|
|
5187
|
+
}
|
|
5188
|
+
let output = `Component: ${name}
|
|
5189
|
+
`;
|
|
5190
|
+
if (propEntries.length) output += `
|
|
5191
|
+
Props:
|
|
5192
|
+
${propEntries.map((e) => ` ${e}`).join("\n")}
|
|
5193
|
+
`;
|
|
5194
|
+
if (stateEntries.length) output += `
|
|
5195
|
+
State:
|
|
5196
|
+
${stateEntries.map((e) => ` ${e}`).join("\n")}
|
|
5197
|
+
`;
|
|
5198
|
+
return output;
|
|
5199
|
+
}, handle);
|
|
5200
|
+
await handle.dispose();
|
|
5201
|
+
return result;
|
|
5202
|
+
}
|
|
5203
|
+
async function getSuspense(bm, page) {
|
|
5204
|
+
requireEnabled(bm);
|
|
5205
|
+
await requireReact(page);
|
|
5206
|
+
const result = await page.evaluate(() => {
|
|
5207
|
+
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
5208
|
+
const rendererID = hook.renderers?.keys().next().value ?? 1;
|
|
5209
|
+
const roots = hook.getFiberRoots?.(rendererID);
|
|
5210
|
+
if (!roots || roots.size === 0) return "No fiber roots found";
|
|
5211
|
+
const root = roots.values().next().value;
|
|
5212
|
+
const boundaries = [];
|
|
5213
|
+
function walk(fiber, path14) {
|
|
5214
|
+
if (!fiber) return;
|
|
5215
|
+
if (fiber.tag === 13) {
|
|
5216
|
+
const resolved = fiber.memoizedState === null;
|
|
5217
|
+
const parent = path14.length > 0 ? path14[path14.length - 1] : "root";
|
|
5218
|
+
boundaries.push(`Suspense in ${parent} \u2014 ${resolved ? "resolved (children visible)" : "pending (showing fallback)"}`);
|
|
5219
|
+
}
|
|
5220
|
+
const name = fiber.type?.displayName || fiber.type?.name || null;
|
|
5221
|
+
const newPath = name ? [...path14, name] : path14;
|
|
5222
|
+
let child = fiber.child;
|
|
5223
|
+
while (child) {
|
|
5224
|
+
walk(child, newPath);
|
|
5225
|
+
child = child.sibling;
|
|
5226
|
+
}
|
|
5227
|
+
}
|
|
5228
|
+
walk(root.current, []);
|
|
5229
|
+
return boundaries.length > 0 ? boundaries.join("\n") : "No Suspense boundaries found";
|
|
5230
|
+
});
|
|
5231
|
+
return result;
|
|
5232
|
+
}
|
|
5233
|
+
async function getErrors(bm, page) {
|
|
5234
|
+
requireEnabled(bm);
|
|
5235
|
+
await requireReact(page);
|
|
5236
|
+
const result = await page.evaluate(() => {
|
|
5237
|
+
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
5238
|
+
const rendererID = hook.renderers?.keys().next().value ?? 1;
|
|
5239
|
+
const roots = hook.getFiberRoots?.(rendererID);
|
|
5240
|
+
if (!roots || roots.size === 0) return "No fiber roots found";
|
|
5241
|
+
const root = roots.values().next().value;
|
|
5242
|
+
const errors = [];
|
|
5243
|
+
function walk(fiber) {
|
|
5244
|
+
if (!fiber) return;
|
|
5245
|
+
if (fiber.tag === 1 && fiber.type?.prototype?.componentDidCatch) {
|
|
5246
|
+
const name = fiber.type.displayName || fiber.type.name || "ErrorBoundary";
|
|
5247
|
+
const hasError = fiber.memoizedState?.error;
|
|
5248
|
+
errors.push(`${name} \u2014 ${hasError ? "caught: " + String(hasError) : "no error caught"}`);
|
|
5249
|
+
}
|
|
5250
|
+
let child = fiber.child;
|
|
5251
|
+
while (child) {
|
|
5252
|
+
walk(child);
|
|
5253
|
+
child = child.sibling;
|
|
5254
|
+
}
|
|
5255
|
+
}
|
|
5256
|
+
walk(root.current);
|
|
5257
|
+
return errors.length > 0 ? errors.join("\n") : "No error boundaries found";
|
|
5258
|
+
});
|
|
5259
|
+
return result;
|
|
5260
|
+
}
|
|
5261
|
+
async function getProfiler(bm, page) {
|
|
5262
|
+
requireEnabled(bm);
|
|
5263
|
+
await requireReact(page);
|
|
5264
|
+
const result = await page.evaluate(() => {
|
|
5265
|
+
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
5266
|
+
const rendererID = hook.renderers?.keys().next().value ?? 1;
|
|
5267
|
+
const roots = hook.getFiberRoots?.(rendererID);
|
|
5268
|
+
if (!roots || roots.size === 0) return "No fiber roots found";
|
|
5269
|
+
const root = roots.values().next().value;
|
|
5270
|
+
const timings = [];
|
|
5271
|
+
function walk(fiber) {
|
|
5272
|
+
if (!fiber) return;
|
|
5273
|
+
const name = fiber.type?.displayName || fiber.type?.name;
|
|
5274
|
+
if (name && fiber.actualDuration !== void 0) {
|
|
5275
|
+
timings.push(`${name}: ${fiber.actualDuration.toFixed(1)}ms (self: ${fiber.selfBaseDuration?.toFixed(1) || "?"}ms)`);
|
|
5276
|
+
}
|
|
5277
|
+
let child = fiber.child;
|
|
5278
|
+
while (child) {
|
|
5279
|
+
walk(child);
|
|
5280
|
+
child = child.sibling;
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5283
|
+
walk(root.current);
|
|
5284
|
+
return timings.length > 0 ? timings.join("\n") : "No profiling data (requires React profiling build)";
|
|
5285
|
+
});
|
|
5286
|
+
return result;
|
|
5287
|
+
}
|
|
5288
|
+
async function getHydration(bm, page) {
|
|
5289
|
+
requireEnabled(bm);
|
|
5290
|
+
await requireReact(page);
|
|
5291
|
+
const result = await page.evaluate(() => {
|
|
5292
|
+
const timing = window.__NEXT_BROWSER_REACT_TIMING__;
|
|
5293
|
+
if (timing && timing.length > 0) {
|
|
5294
|
+
const hydrationEntries = timing.filter((t) => t.label.includes("hydrat") || t.label.includes("Hydrat")).map((t) => `${t.label}: ${(t.endTime - t.startTime).toFixed(1)}ms`);
|
|
5295
|
+
if (hydrationEntries.length > 0) return hydrationEntries.join("\n");
|
|
5296
|
+
}
|
|
5297
|
+
const entries = performance.getEntriesByType("measure").filter((e) => e.name.toLowerCase().includes("hydrat"));
|
|
5298
|
+
if (entries.length > 0) {
|
|
5299
|
+
return entries.map((e) => `${e.name}: ${e.duration.toFixed(1)}ms`).join("\n");
|
|
5300
|
+
}
|
|
5301
|
+
return "No hydration timing data. Requires React profiling build or Next.js dev mode.";
|
|
5302
|
+
});
|
|
5303
|
+
return result;
|
|
5304
|
+
}
|
|
5305
|
+
async function getRenders(bm, page) {
|
|
5306
|
+
requireEnabled(bm);
|
|
5307
|
+
await requireReact(page);
|
|
5308
|
+
const result = await page.evaluate(() => {
|
|
5309
|
+
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
5310
|
+
const rendererID = hook.renderers?.keys().next().value ?? 1;
|
|
5311
|
+
const roots = hook.getFiberRoots?.(rendererID);
|
|
5312
|
+
if (!roots || roots.size === 0) return "No fiber roots found";
|
|
5313
|
+
const root = roots.values().next().value;
|
|
5314
|
+
const rendered = [];
|
|
5315
|
+
function walk(fiber) {
|
|
5316
|
+
if (!fiber) return;
|
|
5317
|
+
const name = fiber.type?.displayName || fiber.type?.name;
|
|
5318
|
+
if (name && fiber.alternate && fiber.actualDuration > 0) {
|
|
5319
|
+
rendered.push(`${name} (${fiber.actualDuration.toFixed(1)}ms)`);
|
|
5320
|
+
}
|
|
5321
|
+
let child = fiber.child;
|
|
5322
|
+
while (child) {
|
|
5323
|
+
walk(child);
|
|
5324
|
+
child = child.sibling;
|
|
5325
|
+
}
|
|
5326
|
+
}
|
|
5327
|
+
walk(root.current);
|
|
5328
|
+
return rendered.length > 0 ? `Re-rendered:
|
|
5329
|
+
${rendered.join("\n")}` : "No components re-rendered since last commit";
|
|
5330
|
+
});
|
|
5331
|
+
return result;
|
|
5332
|
+
}
|
|
5333
|
+
async function getOwners(bm, page, selector) {
|
|
5334
|
+
requireEnabled(bm);
|
|
5335
|
+
await requireReact(page);
|
|
5336
|
+
const resolved = bm.resolveRef(selector);
|
|
5337
|
+
const element = "locator" in resolved ? resolved.locator : page.locator(resolved.selector);
|
|
5338
|
+
const handle = await element.elementHandle();
|
|
5339
|
+
if (!handle) throw new Error(`Element not found: ${selector}`);
|
|
5340
|
+
const result = await page.evaluate((el) => {
|
|
5341
|
+
let fiber = null;
|
|
5342
|
+
for (const key of Object.keys(el)) {
|
|
5343
|
+
if (key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$")) {
|
|
5344
|
+
fiber = el[key];
|
|
5345
|
+
break;
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5348
|
+
if (!fiber) return "No React fiber found for this element";
|
|
5349
|
+
const chain = [];
|
|
5350
|
+
let current = fiber;
|
|
5351
|
+
while (current) {
|
|
5352
|
+
const name = current.type?.displayName || current.type?.name;
|
|
5353
|
+
if (name && typeof current.type !== "string") {
|
|
5354
|
+
chain.push(name);
|
|
5355
|
+
}
|
|
5356
|
+
current = current.return;
|
|
5357
|
+
}
|
|
5358
|
+
return chain.length > 0 ? chain.join(" \u2192 ") : "No component owners found";
|
|
5359
|
+
}, handle);
|
|
5360
|
+
await handle.dispose();
|
|
5361
|
+
return result;
|
|
5362
|
+
}
|
|
5363
|
+
async function getContext(bm, page, selector) {
|
|
5364
|
+
requireEnabled(bm);
|
|
5365
|
+
await requireReact(page);
|
|
5366
|
+
const resolved = bm.resolveRef(selector);
|
|
5367
|
+
const element = "locator" in resolved ? resolved.locator : page.locator(resolved.selector);
|
|
5368
|
+
const handle = await element.elementHandle();
|
|
5369
|
+
if (!handle) throw new Error(`Element not found: ${selector}`);
|
|
5370
|
+
const result = await page.evaluate((el) => {
|
|
5371
|
+
let fiber = null;
|
|
5372
|
+
for (const key of Object.keys(el)) {
|
|
5373
|
+
if (key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$")) {
|
|
5374
|
+
fiber = el[key];
|
|
5375
|
+
break;
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5378
|
+
if (!fiber) return "No React fiber found for this element";
|
|
5379
|
+
while (fiber && typeof fiber.type === "string") fiber = fiber.return;
|
|
5380
|
+
if (!fiber) return "No React component found";
|
|
5381
|
+
const deps = fiber.dependencies;
|
|
5382
|
+
if (!deps || !deps.firstContext) return "No context consumed by this component";
|
|
5383
|
+
const contexts = [];
|
|
5384
|
+
let ctx = deps.firstContext;
|
|
5385
|
+
while (ctx) {
|
|
5386
|
+
const name = ctx.context?.displayName || ctx.context?._debugLabel || "UnnamedContext";
|
|
5387
|
+
const value = ctx.context?._currentValue;
|
|
5388
|
+
try {
|
|
5389
|
+
contexts.push(`${name}: ${JSON.stringify(value, null, 2)}`);
|
|
5390
|
+
} catch {
|
|
5391
|
+
contexts.push(`${name}: [Complex value]`);
|
|
5392
|
+
}
|
|
5393
|
+
ctx = ctx.next;
|
|
5394
|
+
}
|
|
5395
|
+
return contexts.length > 0 ? contexts.join("\n\n") : "No context consumed";
|
|
5396
|
+
}, handle);
|
|
5397
|
+
await handle.dispose();
|
|
5398
|
+
return result;
|
|
5399
|
+
}
|
|
5400
|
+
var fs13, path11, os3, CACHE_DIR, HOOK_PATH, HOOK_URL;
|
|
5401
|
+
var init_react_devtools = __esm({
|
|
5402
|
+
"src/react-devtools.ts"() {
|
|
5403
|
+
"use strict";
|
|
5404
|
+
fs13 = __toESM(require("fs"), 1);
|
|
5405
|
+
path11 = __toESM(require("path"), 1);
|
|
5406
|
+
os3 = __toESM(require("os"), 1);
|
|
5407
|
+
CACHE_DIR = path11.join(os3.homedir(), ".cache", "browse", "react-devtools");
|
|
5408
|
+
HOOK_PATH = path11.join(CACHE_DIR, "installHook.js");
|
|
5409
|
+
HOOK_URL = "https://unpkg.com/react-devtools-core@latest/dist/installHook.js";
|
|
5410
|
+
}
|
|
5411
|
+
});
|
|
5412
|
+
|
|
4770
5413
|
// src/commands/meta.ts
|
|
4771
5414
|
async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2, currentSession) {
|
|
4772
5415
|
switch (command) {
|
|
@@ -4849,7 +5492,7 @@ async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2,
|
|
|
4849
5492
|
const lines = entries.map(
|
|
4850
5493
|
(e) => `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}`
|
|
4851
5494
|
).join("\n") + "\n";
|
|
4852
|
-
|
|
5495
|
+
fs14.appendFileSync(consolePath, lines);
|
|
4853
5496
|
buffers.lastConsoleFlushed = buffers.consoleTotalAdded;
|
|
4854
5497
|
}
|
|
4855
5498
|
const newNetworkCount = buffers.networkTotalAdded - buffers.lastNetworkFlushed;
|
|
@@ -4859,7 +5502,7 @@ async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2,
|
|
|
4859
5502
|
const lines = entries.map(
|
|
4860
5503
|
(e) => `[${new Date(e.timestamp).toISOString()}] ${e.method} ${e.url} \u2192 ${e.status || "pending"} (${e.duration || "?"}ms, ${e.size || "?"}B)`
|
|
4861
5504
|
).join("\n") + "\n";
|
|
4862
|
-
|
|
5505
|
+
fs14.appendFileSync(networkPath, lines);
|
|
4863
5506
|
buffers.lastNetworkFlushed = buffers.networkTotalAdded;
|
|
4864
5507
|
}
|
|
4865
5508
|
}
|
|
@@ -4876,22 +5519,22 @@ async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2,
|
|
|
4876
5519
|
const statesDir = `${LOCAL_DIR}/states`;
|
|
4877
5520
|
const statePath = `${statesDir}/${name}.json`;
|
|
4878
5521
|
if (subcommand === "list") {
|
|
4879
|
-
if (!
|
|
4880
|
-
const files =
|
|
5522
|
+
if (!fs14.existsSync(statesDir)) return "(no saved states)";
|
|
5523
|
+
const files = fs14.readdirSync(statesDir).filter((f) => f.endsWith(".json"));
|
|
4881
5524
|
if (files.length === 0) return "(no saved states)";
|
|
4882
5525
|
const lines = [];
|
|
4883
5526
|
for (const file of files) {
|
|
4884
5527
|
const fp = `${statesDir}/${file}`;
|
|
4885
|
-
const stat =
|
|
5528
|
+
const stat = fs14.statSync(fp);
|
|
4886
5529
|
lines.push(` ${file.replace(".json", "")} ${stat.size}B ${new Date(stat.mtimeMs).toISOString()}`);
|
|
4887
5530
|
}
|
|
4888
5531
|
return lines.join("\n");
|
|
4889
5532
|
}
|
|
4890
5533
|
if (subcommand === "show") {
|
|
4891
|
-
if (!
|
|
5534
|
+
if (!fs14.existsSync(statePath)) {
|
|
4892
5535
|
throw new Error(`State file not found: ${statePath}`);
|
|
4893
5536
|
}
|
|
4894
|
-
const data = JSON.parse(
|
|
5537
|
+
const data = JSON.parse(fs14.readFileSync(statePath, "utf-8"));
|
|
4895
5538
|
const cookieCount = data.cookies?.length || 0;
|
|
4896
5539
|
const originCount = data.origins?.length || 0;
|
|
4897
5540
|
const storageItems = (data.origins || []).reduce((sum, o) => sum + (o.localStorage?.length || 0), 0);
|
|
@@ -4916,15 +5559,15 @@ async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2,
|
|
|
4916
5559
|
const context = bm.getContext();
|
|
4917
5560
|
if (!context) throw new Error("No browser context");
|
|
4918
5561
|
const state = await context.storageState();
|
|
4919
|
-
|
|
4920
|
-
|
|
5562
|
+
fs14.mkdirSync(statesDir, { recursive: true });
|
|
5563
|
+
fs14.writeFileSync(statePath, JSON.stringify(state, null, 2), { mode: 384 });
|
|
4921
5564
|
return `State saved: ${statePath}`;
|
|
4922
5565
|
}
|
|
4923
5566
|
if (subcommand === "load") {
|
|
4924
|
-
if (!
|
|
5567
|
+
if (!fs14.existsSync(statePath)) {
|
|
4925
5568
|
throw new Error(`State file not found: ${statePath}. Run "browse state save ${name}" first.`);
|
|
4926
5569
|
}
|
|
4927
|
-
const stateData = JSON.parse(
|
|
5570
|
+
const stateData = JSON.parse(fs14.readFileSync(statePath, "utf-8"));
|
|
4928
5571
|
const context = bm.getContext();
|
|
4929
5572
|
if (!context) throw new Error("No browser context");
|
|
4930
5573
|
const warnings = [];
|
|
@@ -5083,9 +5726,9 @@ ${legend.join("\n")}`;
|
|
|
5083
5726
|
try {
|
|
5084
5727
|
for (const vp of viewports) {
|
|
5085
5728
|
await page.setViewportSize({ width: vp.width, height: vp.height });
|
|
5086
|
-
const
|
|
5087
|
-
await page.screenshot({ path:
|
|
5088
|
-
results.push(`${vp.name} (${vp.width}x${vp.height}): ${
|
|
5729
|
+
const path14 = `${prefix}-${vp.name}.png`;
|
|
5730
|
+
await page.screenshot({ path: path14, fullPage: true });
|
|
5731
|
+
results.push(`${vp.name} (${vp.width}x${vp.height}): ${path14}`);
|
|
5089
5732
|
}
|
|
5090
5733
|
} finally {
|
|
5091
5734
|
if (originalViewport) {
|
|
@@ -5229,13 +5872,13 @@ ${legend.join("\n")}`;
|
|
|
5229
5872
|
const diffArgs = args.filter((a) => a !== "--full");
|
|
5230
5873
|
const baseline = diffArgs[0];
|
|
5231
5874
|
if (!baseline) throw new Error("Usage: browse screenshot-diff <baseline> [current] [--threshold 0.1] [--full]");
|
|
5232
|
-
if (!
|
|
5875
|
+
if (!fs14.existsSync(baseline)) throw new Error(`Baseline file not found: ${baseline}`);
|
|
5233
5876
|
let thresholdPct = 0.1;
|
|
5234
5877
|
const threshIdx = diffArgs.indexOf("--threshold");
|
|
5235
5878
|
if (threshIdx !== -1 && diffArgs[threshIdx + 1]) {
|
|
5236
5879
|
thresholdPct = parseFloat(diffArgs[threshIdx + 1]);
|
|
5237
5880
|
}
|
|
5238
|
-
const baselineBuffer =
|
|
5881
|
+
const baselineBuffer = fs14.readFileSync(baseline);
|
|
5239
5882
|
let currentBuffer;
|
|
5240
5883
|
let currentPath;
|
|
5241
5884
|
for (let i = 1; i < diffArgs.length; i++) {
|
|
@@ -5249,8 +5892,8 @@ ${legend.join("\n")}`;
|
|
|
5249
5892
|
}
|
|
5250
5893
|
}
|
|
5251
5894
|
if (currentPath) {
|
|
5252
|
-
if (!
|
|
5253
|
-
currentBuffer =
|
|
5895
|
+
if (!fs14.existsSync(currentPath)) throw new Error(`Current screenshot not found: ${currentPath}`);
|
|
5896
|
+
currentBuffer = fs14.readFileSync(currentPath);
|
|
5254
5897
|
} else {
|
|
5255
5898
|
const page = bm.getPage();
|
|
5256
5899
|
currentBuffer = await page.screenshot({ fullPage: isFullPageDiff });
|
|
@@ -5260,7 +5903,7 @@ ${legend.join("\n")}`;
|
|
|
5260
5903
|
const extIdx = baseline.lastIndexOf(".");
|
|
5261
5904
|
const diffPath = extIdx > 0 ? baseline.slice(0, extIdx) + "-diff" + baseline.slice(extIdx) : baseline + "-diff.png";
|
|
5262
5905
|
if (!result.passed && result.diffImage) {
|
|
5263
|
-
|
|
5906
|
+
fs14.writeFileSync(diffPath, result.diffImage);
|
|
5264
5907
|
}
|
|
5265
5908
|
return [
|
|
5266
5909
|
`Pixels: ${result.totalPixels}`,
|
|
@@ -5396,7 +6039,7 @@ ${legend.join("\n")}`;
|
|
|
5396
6039
|
const { formatAsHar: formatAsHar2 } = await Promise.resolve().then(() => (init_har(), har_exports));
|
|
5397
6040
|
const har = formatAsHar2(sessionBuffers.networkBuffer, recording.startTime);
|
|
5398
6041
|
const harPath = args[1] || (currentSession ? `${currentSession.outputDir}/recording.har` : `${LOCAL_DIR}/browse-recording.har`);
|
|
5399
|
-
|
|
6042
|
+
fs14.writeFileSync(harPath, JSON.stringify(har, null, 2));
|
|
5400
6043
|
const entryCount = har.log.entries.length;
|
|
5401
6044
|
return `HAR saved: ${harPath} (${entryCount} entries)`;
|
|
5402
6045
|
}
|
|
@@ -5459,6 +6102,20 @@ ${output.trim()}`;
|
|
|
5459
6102
|
Manual: npm install -g @ulpi/browse`;
|
|
5460
6103
|
}
|
|
5461
6104
|
}
|
|
6105
|
+
// ─── Handoff ────────────────────────────────────────
|
|
6106
|
+
case "handoff": {
|
|
6107
|
+
const message = args.join(" ") || "User takeover requested";
|
|
6108
|
+
return await bm.handoff(message);
|
|
6109
|
+
}
|
|
6110
|
+
case "resume": {
|
|
6111
|
+
const url = await bm.resume();
|
|
6112
|
+
const { handleSnapshot: handleSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
|
|
6113
|
+
const snapshot = await handleSnapshot2(["-i"], bm);
|
|
6114
|
+
return `Resumed \u2014 back to headless.
|
|
6115
|
+
URL: ${url}
|
|
6116
|
+
|
|
6117
|
+
${snapshot}`;
|
|
6118
|
+
}
|
|
5462
6119
|
// ─── Semantic Locator ──────────────────────────────
|
|
5463
6120
|
case "find": {
|
|
5464
6121
|
const root = bm.getLocatorRoot();
|
|
@@ -5616,7 +6273,7 @@ Manual: npm install -g @ulpi/browse`;
|
|
|
5616
6273
|
}
|
|
5617
6274
|
const filePath = args[2];
|
|
5618
6275
|
if (filePath) {
|
|
5619
|
-
|
|
6276
|
+
fs14.writeFileSync(filePath, output);
|
|
5620
6277
|
return `Exported ${steps.length} steps as ${format}: ${filePath}`;
|
|
5621
6278
|
}
|
|
5622
6279
|
return output;
|
|
@@ -5662,11 +6319,59 @@ Manual: npm install -g @ulpi/browse`;
|
|
|
5662
6319
|
}
|
|
5663
6320
|
throw new Error("Usage: browse profile list | delete <name> | clean [--older-than <days>]");
|
|
5664
6321
|
}
|
|
6322
|
+
// ─── React DevTools ──────────────────────────────────
|
|
6323
|
+
case "react-devtools": {
|
|
6324
|
+
const sub = args[0];
|
|
6325
|
+
if (!sub) throw new Error(
|
|
6326
|
+
"Usage: browse react-devtools enable|disable|tree|props|suspense|errors|profiler|hydration|renders|owners|context"
|
|
6327
|
+
);
|
|
6328
|
+
const rd = await Promise.resolve().then(() => (init_react_devtools(), react_devtools_exports));
|
|
6329
|
+
switch (sub) {
|
|
6330
|
+
case "enable": {
|
|
6331
|
+
if (rd.isEnabled(bm)) return "React DevTools already enabled.";
|
|
6332
|
+
await rd.injectHook(bm);
|
|
6333
|
+
await bm.getPage().reload();
|
|
6334
|
+
return "React DevTools enabled. Page reloaded.";
|
|
6335
|
+
}
|
|
6336
|
+
case "disable": {
|
|
6337
|
+
rd.removeHook(bm);
|
|
6338
|
+
return "React DevTools disabled. Takes effect on next navigation.";
|
|
6339
|
+
}
|
|
6340
|
+
case "tree":
|
|
6341
|
+
return await rd.getTree(bm, bm.getPage());
|
|
6342
|
+
case "props": {
|
|
6343
|
+
if (!args[1]) throw new Error("Usage: browse react-devtools props <selector|@ref>");
|
|
6344
|
+
return await rd.getProps(bm, bm.getPage(), args[1]);
|
|
6345
|
+
}
|
|
6346
|
+
case "suspense":
|
|
6347
|
+
return await rd.getSuspense(bm, bm.getPage());
|
|
6348
|
+
case "errors":
|
|
6349
|
+
return await rd.getErrors(bm, bm.getPage());
|
|
6350
|
+
case "profiler":
|
|
6351
|
+
return await rd.getProfiler(bm, bm.getPage());
|
|
6352
|
+
case "hydration":
|
|
6353
|
+
return await rd.getHydration(bm, bm.getPage());
|
|
6354
|
+
case "renders":
|
|
6355
|
+
return await rd.getRenders(bm, bm.getPage());
|
|
6356
|
+
case "owners": {
|
|
6357
|
+
if (!args[1]) throw new Error("Usage: browse react-devtools owners <selector|@ref>");
|
|
6358
|
+
return await rd.getOwners(bm, bm.getPage(), args[1]);
|
|
6359
|
+
}
|
|
6360
|
+
case "context": {
|
|
6361
|
+
if (!args[1]) throw new Error("Usage: browse react-devtools context <selector|@ref>");
|
|
6362
|
+
return await rd.getContext(bm, bm.getPage(), args[1]);
|
|
6363
|
+
}
|
|
6364
|
+
default:
|
|
6365
|
+
throw new Error(
|
|
6366
|
+
`Unknown subcommand: ${sub}. Use: enable|disable|tree|props|suspense|errors|profiler|hydration|renders|owners|context`
|
|
6367
|
+
);
|
|
6368
|
+
}
|
|
6369
|
+
}
|
|
5665
6370
|
default:
|
|
5666
6371
|
throw new Error(`Unknown meta command: ${command}`);
|
|
5667
6372
|
}
|
|
5668
6373
|
}
|
|
5669
|
-
var
|
|
6374
|
+
var fs14, LOCAL_DIR;
|
|
5670
6375
|
var init_meta = __esm({
|
|
5671
6376
|
"src/commands/meta.ts"() {
|
|
5672
6377
|
"use strict";
|
|
@@ -5674,7 +6379,7 @@ var init_meta = __esm({
|
|
|
5674
6379
|
init_constants();
|
|
5675
6380
|
init_sanitize();
|
|
5676
6381
|
init_lib();
|
|
5677
|
-
|
|
6382
|
+
fs14 = __toESM(require("fs"), 1);
|
|
5678
6383
|
LOCAL_DIR = process.env.BROWSE_LOCAL_DIR || "/tmp";
|
|
5679
6384
|
}
|
|
5680
6385
|
});
|
|
@@ -5732,7 +6437,7 @@ function flushSessionBuffers(session, final) {
|
|
|
5732
6437
|
const lines = newEntries.map(
|
|
5733
6438
|
(e) => `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}`
|
|
5734
6439
|
).join("\n") + "\n";
|
|
5735
|
-
|
|
6440
|
+
fs15.appendFileSync(consolePath, lines);
|
|
5736
6441
|
buffers.lastConsoleFlushed = buffers.consoleTotalAdded;
|
|
5737
6442
|
}
|
|
5738
6443
|
let newNetworkCount = buffers.networkTotalAdded - buffers.lastNetworkFlushed;
|
|
@@ -5757,7 +6462,7 @@ function flushSessionBuffers(session, final) {
|
|
|
5757
6462
|
const lines = prefix.map(
|
|
5758
6463
|
(e) => `[${new Date(e.timestamp).toISOString()}] ${e.method} ${e.url} \u2192 ${e.status || "pending"} (${e.duration || "?"}ms, ${e.size || "?"}B)`
|
|
5759
6464
|
).join("\n") + "\n";
|
|
5760
|
-
|
|
6465
|
+
fs15.appendFileSync(networkPath, lines);
|
|
5761
6466
|
buffers.lastNetworkFlushed += prefixLen;
|
|
5762
6467
|
}
|
|
5763
6468
|
}
|
|
@@ -5886,6 +6591,7 @@ async function handleCommand(body, session, opts) {
|
|
|
5886
6591
|
headers: { "Content-Type": "application/json" }
|
|
5887
6592
|
});
|
|
5888
6593
|
}
|
|
6594
|
+
session.manager.resetFailures();
|
|
5889
6595
|
if (session.recording && !RECORDING_SKIP.has(command)) {
|
|
5890
6596
|
session.recording.push({ command, args, timestamp: Date.now() });
|
|
5891
6597
|
}
|
|
@@ -5910,7 +6616,10 @@ ${result}
|
|
|
5910
6616
|
headers: { "Content-Type": "text/plain" }
|
|
5911
6617
|
});
|
|
5912
6618
|
} catch (err) {
|
|
5913
|
-
|
|
6619
|
+
session.manager.incrementFailures();
|
|
6620
|
+
let friendlyError = rewriteError(err.message);
|
|
6621
|
+
const hint = session.manager.getFailureHint();
|
|
6622
|
+
if (hint) friendlyError += "\n" + hint;
|
|
5914
6623
|
if (opts.jsonMode) {
|
|
5915
6624
|
return new Response(JSON.stringify({ success: false, error: friendlyError, command }), {
|
|
5916
6625
|
status: 500,
|
|
@@ -5944,9 +6653,9 @@ async function shutdown() {
|
|
|
5944
6653
|
await activeRuntime?.close?.().catch(() => {
|
|
5945
6654
|
});
|
|
5946
6655
|
try {
|
|
5947
|
-
const currentState = JSON.parse(
|
|
6656
|
+
const currentState = JSON.parse(fs15.readFileSync(STATE_FILE, "utf-8"));
|
|
5948
6657
|
if (currentState.pid === process.pid || currentState.token === AUTH_TOKEN) {
|
|
5949
|
-
|
|
6658
|
+
fs15.unlinkSync(STATE_FILE);
|
|
5950
6659
|
}
|
|
5951
6660
|
} catch {
|
|
5952
6661
|
}
|
|
@@ -5963,15 +6672,15 @@ async function start() {
|
|
|
5963
6672
|
const { BrowserManager: BrowserManager2, getProfileDir: getProfileDir2 } = await Promise.resolve().then(() => (init_browser_manager(), browser_manager_exports));
|
|
5964
6673
|
const { SessionBuffers: SessionBuffers2 } = await Promise.resolve().then(() => (init_buffers(), buffers_exports));
|
|
5965
6674
|
const profileDir = getProfileDir2(LOCAL_DIR2, profileName);
|
|
5966
|
-
|
|
6675
|
+
fs15.mkdirSync(profileDir, { recursive: true });
|
|
5967
6676
|
const bm = new BrowserManager2();
|
|
5968
6677
|
await bm.launchPersistent(profileDir, () => {
|
|
5969
6678
|
if (isShuttingDown) return;
|
|
5970
6679
|
console.error("[browse] Chromium disconnected (profile mode). Shutting down.");
|
|
5971
6680
|
shutdown();
|
|
5972
6681
|
});
|
|
5973
|
-
const outputDir =
|
|
5974
|
-
|
|
6682
|
+
const outputDir = path12.join(LOCAL_DIR2, "sessions", profileName);
|
|
6683
|
+
fs15.mkdirSync(outputDir, { recursive: true });
|
|
5975
6684
|
profileSession = {
|
|
5976
6685
|
id: profileName,
|
|
5977
6686
|
manager: bm,
|
|
@@ -6065,7 +6774,7 @@ async function start() {
|
|
|
6065
6774
|
const context = session.manager.getContext();
|
|
6066
6775
|
if (context) {
|
|
6067
6776
|
try {
|
|
6068
|
-
const stateData = JSON.parse(
|
|
6777
|
+
const stateData = JSON.parse(fs15.readFileSync(stateFilePath, "utf-8"));
|
|
6069
6778
|
if (stateData.cookies?.length) {
|
|
6070
6779
|
await context.addCookies(stateData.cookies);
|
|
6071
6780
|
}
|
|
@@ -6092,7 +6801,7 @@ async function start() {
|
|
|
6092
6801
|
port,
|
|
6093
6802
|
token: AUTH_TOKEN,
|
|
6094
6803
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6095
|
-
serverPath:
|
|
6804
|
+
serverPath: path12.resolve(path12.dirname((0, import_url2.fileURLToPath)(__import_meta_url)), "server.ts")
|
|
6096
6805
|
};
|
|
6097
6806
|
if (profileName) {
|
|
6098
6807
|
state.profile = profileName;
|
|
@@ -6100,12 +6809,12 @@ async function start() {
|
|
|
6100
6809
|
if (DEBUG_PORT > 0) {
|
|
6101
6810
|
state.debugPort = DEBUG_PORT;
|
|
6102
6811
|
}
|
|
6103
|
-
|
|
6812
|
+
fs15.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), { mode: 384 });
|
|
6104
6813
|
console.log(`[browse] Server running on http://127.0.0.1:${port} (PID: ${process.pid})`);
|
|
6105
6814
|
console.log(`[browse] State file: ${STATE_FILE}`);
|
|
6106
6815
|
console.log(`[browse] Idle timeout: ${IDLE_TIMEOUT_MS / 1e3}s`);
|
|
6107
6816
|
}
|
|
6108
|
-
var
|
|
6817
|
+
var fs15, path12, 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;
|
|
6109
6818
|
var init_server = __esm({
|
|
6110
6819
|
"src/server.ts"() {
|
|
6111
6820
|
"use strict";
|
|
@@ -6116,8 +6825,8 @@ var init_server = __esm({
|
|
|
6116
6825
|
init_meta();
|
|
6117
6826
|
init_policy();
|
|
6118
6827
|
init_constants();
|
|
6119
|
-
|
|
6120
|
-
|
|
6828
|
+
fs15 = __toESM(require("fs"), 1);
|
|
6829
|
+
path12 = __toESM(require("path"), 1);
|
|
6121
6830
|
crypto3 = __toESM(require("crypto"), 1);
|
|
6122
6831
|
http = __toESM(require("http"), 1);
|
|
6123
6832
|
import_url2 = require("url");
|
|
@@ -6231,7 +6940,10 @@ var init_server = __esm({
|
|
|
6231
6940
|
"cookie-import",
|
|
6232
6941
|
"doctor",
|
|
6233
6942
|
"upgrade",
|
|
6234
|
-
"
|
|
6943
|
+
"handoff",
|
|
6944
|
+
"resume",
|
|
6945
|
+
"profile",
|
|
6946
|
+
"react-devtools"
|
|
6235
6947
|
]);
|
|
6236
6948
|
RECORDING_SKIP = /* @__PURE__ */ new Set([
|
|
6237
6949
|
"record",
|
|
@@ -6299,8 +7011,8 @@ __export(cli_exports, {
|
|
|
6299
7011
|
resolveServerScript: () => resolveServerScript
|
|
6300
7012
|
});
|
|
6301
7013
|
module.exports = __toCommonJS(cli_exports);
|
|
6302
|
-
var
|
|
6303
|
-
var
|
|
7014
|
+
var fs16 = __toESM(require("fs"), 1);
|
|
7015
|
+
var path13 = __toESM(require("path"), 1);
|
|
6304
7016
|
var import_child_process3 = require("child_process");
|
|
6305
7017
|
var import_url3 = require("url");
|
|
6306
7018
|
init_constants();
|
|
@@ -6354,50 +7066,50 @@ var INSTANCE_SUFFIX2 = BROWSE_PORT2 ? `-${BROWSE_PORT2}` : BROWSE_INSTANCE2 ? `-
|
|
|
6354
7066
|
function resolveLocalDir() {
|
|
6355
7067
|
if (process.env.BROWSE_LOCAL_DIR) {
|
|
6356
7068
|
try {
|
|
6357
|
-
|
|
7069
|
+
fs16.mkdirSync(process.env.BROWSE_LOCAL_DIR, { recursive: true });
|
|
6358
7070
|
} catch {
|
|
6359
7071
|
}
|
|
6360
7072
|
return process.env.BROWSE_LOCAL_DIR;
|
|
6361
7073
|
}
|
|
6362
7074
|
let dir = process.cwd();
|
|
6363
7075
|
for (let i = 0; i < 20; i++) {
|
|
6364
|
-
if (
|
|
6365
|
-
const browseDir =
|
|
7076
|
+
if (fs16.existsSync(path13.join(dir, ".git")) || fs16.existsSync(path13.join(dir, ".claude"))) {
|
|
7077
|
+
const browseDir = path13.join(dir, ".browse");
|
|
6366
7078
|
try {
|
|
6367
|
-
|
|
6368
|
-
const gi =
|
|
6369
|
-
if (!
|
|
6370
|
-
|
|
7079
|
+
fs16.mkdirSync(browseDir, { recursive: true });
|
|
7080
|
+
const gi = path13.join(browseDir, ".gitignore");
|
|
7081
|
+
if (!fs16.existsSync(gi)) {
|
|
7082
|
+
fs16.writeFileSync(gi, "*\n");
|
|
6371
7083
|
}
|
|
6372
7084
|
} catch {
|
|
6373
7085
|
}
|
|
6374
7086
|
return browseDir;
|
|
6375
7087
|
}
|
|
6376
|
-
const parent =
|
|
7088
|
+
const parent = path13.dirname(dir);
|
|
6377
7089
|
if (parent === dir) break;
|
|
6378
7090
|
dir = parent;
|
|
6379
7091
|
}
|
|
6380
7092
|
return "/tmp";
|
|
6381
7093
|
}
|
|
6382
7094
|
var LOCAL_DIR3 = resolveLocalDir();
|
|
6383
|
-
var STATE_FILE2 = process.env.BROWSE_STATE_FILE ||
|
|
7095
|
+
var STATE_FILE2 = process.env.BROWSE_STATE_FILE || path13.join(LOCAL_DIR3, `browse-server${INSTANCE_SUFFIX2}.json`);
|
|
6384
7096
|
var MAX_START_WAIT = 8e3;
|
|
6385
7097
|
var LOCK_FILE = STATE_FILE2 + ".lock";
|
|
6386
7098
|
var LOCK_STALE_MS = DEFAULTS.LOCK_STALE_THRESHOLD_MS;
|
|
6387
7099
|
var __filename_cli = (0, import_url3.fileURLToPath)(__import_meta_url);
|
|
6388
|
-
var __dirname_cli =
|
|
7100
|
+
var __dirname_cli = path13.dirname(__filename_cli);
|
|
6389
7101
|
function resolveServerScript(env = process.env, metaDir = __dirname_cli) {
|
|
6390
7102
|
if (env.BROWSE_SERVER_SCRIPT) {
|
|
6391
7103
|
return env.BROWSE_SERVER_SCRIPT;
|
|
6392
7104
|
}
|
|
6393
7105
|
if (metaDir.startsWith("/")) {
|
|
6394
|
-
const direct =
|
|
6395
|
-
if (
|
|
7106
|
+
const direct = path13.resolve(metaDir, "server.ts");
|
|
7107
|
+
if (fs16.existsSync(direct)) {
|
|
6396
7108
|
return direct;
|
|
6397
7109
|
}
|
|
6398
7110
|
}
|
|
6399
7111
|
const selfPath = (0, import_url3.fileURLToPath)(__import_meta_url);
|
|
6400
|
-
if (
|
|
7112
|
+
if (fs16.existsSync(selfPath)) {
|
|
6401
7113
|
return "__self__";
|
|
6402
7114
|
}
|
|
6403
7115
|
throw new Error(
|
|
@@ -6407,7 +7119,7 @@ function resolveServerScript(env = process.env, metaDir = __dirname_cli) {
|
|
|
6407
7119
|
var SERVER_SCRIPT = resolveServerScript();
|
|
6408
7120
|
function readState() {
|
|
6409
7121
|
try {
|
|
6410
|
-
const data =
|
|
7122
|
+
const data = fs16.readFileSync(STATE_FILE2, "utf-8");
|
|
6411
7123
|
return JSON.parse(data);
|
|
6412
7124
|
} catch {
|
|
6413
7125
|
return null;
|
|
@@ -6423,7 +7135,7 @@ function isProcessAlive(pid) {
|
|
|
6423
7135
|
}
|
|
6424
7136
|
async function listInstances() {
|
|
6425
7137
|
try {
|
|
6426
|
-
const files =
|
|
7138
|
+
const files = fs16.readdirSync(LOCAL_DIR3).filter(
|
|
6427
7139
|
(f) => f.startsWith("browse-server") && f.endsWith(".json") && !f.endsWith(".lock")
|
|
6428
7140
|
);
|
|
6429
7141
|
if (files.length === 0) {
|
|
@@ -6433,7 +7145,7 @@ async function listInstances() {
|
|
|
6433
7145
|
let found = false;
|
|
6434
7146
|
for (const file of files) {
|
|
6435
7147
|
try {
|
|
6436
|
-
const data = JSON.parse(
|
|
7148
|
+
const data = JSON.parse(fs16.readFileSync(path13.join(LOCAL_DIR3, file), "utf-8"));
|
|
6437
7149
|
if (!data.pid || !data.port) continue;
|
|
6438
7150
|
const alive = isProcessAlive(data.pid);
|
|
6439
7151
|
let status = "dead";
|
|
@@ -6456,7 +7168,7 @@ async function listInstances() {
|
|
|
6456
7168
|
found = true;
|
|
6457
7169
|
if (!alive) {
|
|
6458
7170
|
try {
|
|
6459
|
-
|
|
7171
|
+
fs16.unlinkSync(path13.join(LOCAL_DIR3, file));
|
|
6460
7172
|
} catch {
|
|
6461
7173
|
}
|
|
6462
7174
|
}
|
|
@@ -6479,15 +7191,15 @@ function isBrowseProcess(pid) {
|
|
|
6479
7191
|
}
|
|
6480
7192
|
function acquireLock() {
|
|
6481
7193
|
try {
|
|
6482
|
-
|
|
7194
|
+
fs16.writeFileSync(LOCK_FILE, String(process.pid), { flag: "wx" });
|
|
6483
7195
|
return true;
|
|
6484
7196
|
} catch (err) {
|
|
6485
7197
|
if (err.code === "EEXIST") {
|
|
6486
7198
|
try {
|
|
6487
|
-
const stat =
|
|
7199
|
+
const stat = fs16.statSync(LOCK_FILE);
|
|
6488
7200
|
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
6489
7201
|
try {
|
|
6490
|
-
|
|
7202
|
+
fs16.unlinkSync(LOCK_FILE);
|
|
6491
7203
|
} catch {
|
|
6492
7204
|
}
|
|
6493
7205
|
return acquireLock();
|
|
@@ -6501,7 +7213,7 @@ function acquireLock() {
|
|
|
6501
7213
|
}
|
|
6502
7214
|
function releaseLock() {
|
|
6503
7215
|
try {
|
|
6504
|
-
|
|
7216
|
+
fs16.unlinkSync(LOCK_FILE);
|
|
6505
7217
|
} catch {
|
|
6506
7218
|
}
|
|
6507
7219
|
}
|
|
@@ -6518,7 +7230,7 @@ async function startServer() {
|
|
|
6518
7230
|
}
|
|
6519
7231
|
await sleep(100);
|
|
6520
7232
|
}
|
|
6521
|
-
if (!
|
|
7233
|
+
if (!fs16.existsSync(LOCK_FILE) || fs16.readFileSync(LOCK_FILE, "utf-8").trim() !== String(process.pid)) {
|
|
6522
7234
|
const state = readState();
|
|
6523
7235
|
if (state && isProcessAlive(state.pid)) return state;
|
|
6524
7236
|
throw new Error("Server failed to start (another process is starting it)");
|
|
@@ -6528,7 +7240,7 @@ async function startServer() {
|
|
|
6528
7240
|
try {
|
|
6529
7241
|
const oldState = readState();
|
|
6530
7242
|
if (oldState && !isProcessAlive(oldState.pid)) {
|
|
6531
|
-
|
|
7243
|
+
fs16.unlinkSync(STATE_FILE2);
|
|
6532
7244
|
}
|
|
6533
7245
|
} catch {
|
|
6534
7246
|
}
|
|
@@ -6602,7 +7314,7 @@ async function ensureServer() {
|
|
|
6602
7314
|
}
|
|
6603
7315
|
if (state) {
|
|
6604
7316
|
try {
|
|
6605
|
-
|
|
7317
|
+
fs16.unlinkSync(STATE_FILE2);
|
|
6606
7318
|
} catch {
|
|
6607
7319
|
}
|
|
6608
7320
|
}
|
|
@@ -6612,21 +7324,21 @@ async function ensureServer() {
|
|
|
6612
7324
|
}
|
|
6613
7325
|
function cleanOrphanedServers() {
|
|
6614
7326
|
try {
|
|
6615
|
-
const files =
|
|
7327
|
+
const files = fs16.readdirSync(LOCAL_DIR3);
|
|
6616
7328
|
for (const file of files) {
|
|
6617
7329
|
if (!file.startsWith("browse-server") || !file.endsWith(".json") || file.endsWith(".lock")) continue;
|
|
6618
|
-
const filePath =
|
|
7330
|
+
const filePath = path13.join(LOCAL_DIR3, file);
|
|
6619
7331
|
if (filePath === STATE_FILE2) continue;
|
|
6620
7332
|
try {
|
|
6621
|
-
const data = JSON.parse(
|
|
7333
|
+
const data = JSON.parse(fs16.readFileSync(filePath, "utf-8"));
|
|
6622
7334
|
if (!data.pid) {
|
|
6623
|
-
|
|
7335
|
+
fs16.unlinkSync(filePath);
|
|
6624
7336
|
continue;
|
|
6625
7337
|
}
|
|
6626
7338
|
const suffixMatch = file.match(/browse-server-(\d+)\.json$/);
|
|
6627
7339
|
if (suffixMatch && data.port === parseInt(suffixMatch[1], 10) && isProcessAlive(data.pid)) continue;
|
|
6628
7340
|
if (!isProcessAlive(data.pid)) {
|
|
6629
|
-
|
|
7341
|
+
fs16.unlinkSync(filePath);
|
|
6630
7342
|
continue;
|
|
6631
7343
|
}
|
|
6632
7344
|
if (isBrowseProcess(data.pid)) {
|
|
@@ -6637,7 +7349,7 @@ function cleanOrphanedServers() {
|
|
|
6637
7349
|
}
|
|
6638
7350
|
} catch {
|
|
6639
7351
|
try {
|
|
6640
|
-
|
|
7352
|
+
fs16.unlinkSync(filePath);
|
|
6641
7353
|
} catch {
|
|
6642
7354
|
}
|
|
6643
7355
|
}
|
|
@@ -6677,7 +7389,8 @@ var SAFE_TO_RETRY = /* @__PURE__ */ new Set([
|
|
|
6677
7389
|
"box",
|
|
6678
7390
|
"errors",
|
|
6679
7391
|
"doctor",
|
|
6680
|
-
"upgrade"
|
|
7392
|
+
"upgrade",
|
|
7393
|
+
"react-devtools"
|
|
6681
7394
|
]);
|
|
6682
7395
|
var PAGE_INDEPENDENT = /* @__PURE__ */ new Set(["devices", "status"]);
|
|
6683
7396
|
async function sendCommand(state, command, args, retries = 0, sessionId) {
|
|
@@ -6738,7 +7451,7 @@ async function sendCommand(state, command, args, retries = 0, sessionId) {
|
|
|
6738
7451
|
await sleep(300);
|
|
6739
7452
|
}
|
|
6740
7453
|
try {
|
|
6741
|
-
|
|
7454
|
+
fs16.unlinkSync(STATE_FILE2);
|
|
6742
7455
|
} catch {
|
|
6743
7456
|
}
|
|
6744
7457
|
if (command === "restart") {
|
|
@@ -6942,22 +7655,25 @@ Interaction: click <sel> | rightclick <sel> | dblclick <sel>
|
|
|
6942
7655
|
check <sel> | uncheck <sel> | drag <src> <tgt>
|
|
6943
7656
|
type <text> | press <key> | keydown <key> | keyup <key>
|
|
6944
7657
|
keyboard inserttext <text>
|
|
6945
|
-
scroll [sel|up|down] | scrollinto <sel>
|
|
7658
|
+
scroll [sel|up|down] | scrollinto <sel> | scrollintoview <sel>
|
|
6946
7659
|
swipe <up|down|left|right> [px]
|
|
6947
7660
|
wait <sel|ms|--url|--text|--fn|--load|--network-idle>
|
|
6948
7661
|
viewport <WxH> | highlight <sel> | download <sel> [path]
|
|
7662
|
+
upload <sel> <files...>
|
|
7663
|
+
dialog-accept [text] | dialog-dismiss
|
|
6949
7664
|
Mouse: mouse move <x> <y> | mouse down [btn] | mouse up [btn]
|
|
6950
7665
|
mouse wheel <dy> [dx]
|
|
6951
7666
|
Settings: set geo <lat> <lng> | set media <dark|light|no-preference>
|
|
6952
7667
|
Device: emulate <device> | emulate reset | devices [filter]
|
|
6953
7668
|
Inspection: js <expr> | eval <file> | css <sel> <prop> | attrs <sel>
|
|
6954
|
-
element-state <sel> | box <sel>
|
|
7669
|
+
element-state <sel> | box <sel> | dialog
|
|
6955
7670
|
console [--clear] | errors [--clear] | network [--clear]
|
|
6956
7671
|
cookies | storage [set <k> <v>] | perf
|
|
6957
7672
|
value <sel> | count <sel> | clipboard [write <text>]
|
|
6958
7673
|
Visual: screenshot [sel|@ref] [path] [--full] [--clip x,y,w,h]
|
|
6959
7674
|
screenshot --annotate | pdf [path] | responsive [prefix]
|
|
6960
7675
|
Snapshot: snapshot [-i] [-f] [-V] [-c] [-C] [-d N] [-s sel]
|
|
7676
|
+
snapshot-diff
|
|
6961
7677
|
Find: find role|text|label|placeholder|testid|alt|title <query>
|
|
6962
7678
|
find first|last <sel> | find nth <n> <sel>
|
|
6963
7679
|
Compare: diff <url1> <url2> | screenshot-diff <baseline> [current]
|
|
@@ -6973,10 +7689,13 @@ Recording: har start | har stop [path]
|
|
|
6973
7689
|
Tabs: tabs | tab <id> | newtab [url] | closetab [id]
|
|
6974
7690
|
Frames: frame <sel> | frame main
|
|
6975
7691
|
Sessions: sessions | session-close <id>
|
|
7692
|
+
Profiles: --profile <name> | profile list|delete|clean
|
|
6976
7693
|
Auth: auth save <name> <url> <user> <pass|--password-stdin>
|
|
6977
7694
|
auth login <name> | auth list | auth delete <name>
|
|
6978
7695
|
cookie-import --list | cookie-import <browser> [--domain <d>] [--profile <p>]
|
|
6979
|
-
State: state save|load|list|show [name]
|
|
7696
|
+
State: state save|load|list|show|clean [name]
|
|
7697
|
+
Handoff: handoff [reason] | resume
|
|
7698
|
+
React: react-devtools enable|disable|tree|props|suspense|errors|profiler
|
|
6980
7699
|
Debug: inspect (requires BROWSE_DEBUG_PORT)
|
|
6981
7700
|
Server: status | instances | stop | restart | doctor | upgrade
|
|
6982
7701
|
Setup: install-skill [path]
|
|
@@ -7023,7 +7742,7 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
|
|
|
7023
7742
|
}
|
|
7024
7743
|
if (process.env.__BROWSE_SERVER_MODE === "1") {
|
|
7025
7744
|
Promise.resolve().then(() => init_server());
|
|
7026
|
-
} else if (process.argv[1] &&
|
|
7745
|
+
} else if (process.argv[1] && fs16.realpathSync(process.argv[1]) === fs16.realpathSync(__filename_cli)) {
|
|
7027
7746
|
main().catch((err) => {
|
|
7028
7747
|
console.error(`[browse] ${err.message}`);
|
|
7029
7748
|
process.exit(1);
|