@ulpi/browse 2.3.3 → 2.3.4
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/dist/browse.cjs +1044 -859
- package/package.json +1 -1
- package/skill/{SKILL.md → browse/SKILL.md} +87 -8
- package/skill/browse-qa/SKILL.md +299 -0
- /package/skill/{references → browse/references}/commands.md +0 -0
- /package/skill/{references → browse/references}/guides.md +0 -0
- /package/skill/{references → browse/references}/permissions.md +0 -0
package/dist/browse.cjs
CHANGED
|
@@ -281,7 +281,7 @@ async function getKeychainPassword(service) {
|
|
|
281
281
|
stderr += chunk;
|
|
282
282
|
});
|
|
283
283
|
const exitPromise = new Promise(
|
|
284
|
-
(
|
|
284
|
+
(resolve10) => proc.on("close", (code) => resolve10(code))
|
|
285
285
|
);
|
|
286
286
|
const timeout = new Promise(
|
|
287
287
|
(_, reject) => setTimeout(() => {
|
|
@@ -511,11 +511,11 @@ function getChromeUserDataDir() {
|
|
|
511
511
|
return null;
|
|
512
512
|
}
|
|
513
513
|
function isPortFree(port) {
|
|
514
|
-
return new Promise((
|
|
514
|
+
return new Promise((resolve10) => {
|
|
515
515
|
const srv = net.createServer();
|
|
516
|
-
srv.once("error", () =>
|
|
516
|
+
srv.once("error", () => resolve10(false));
|
|
517
517
|
srv.once("listening", () => {
|
|
518
|
-
srv.close(() =>
|
|
518
|
+
srv.close(() => resolve10(true));
|
|
519
519
|
});
|
|
520
520
|
srv.listen(port, "127.0.0.1");
|
|
521
521
|
});
|
|
@@ -864,7 +864,7 @@ var require_package = __commonJS({
|
|
|
864
864
|
"package.json"(exports2, module2) {
|
|
865
865
|
module2.exports = {
|
|
866
866
|
name: "@ulpi/browse",
|
|
867
|
-
version: "2.3.
|
|
867
|
+
version: "2.3.4",
|
|
868
868
|
homepage: "https://browse.ulpi.io",
|
|
869
869
|
repository: {
|
|
870
870
|
type: "git",
|
|
@@ -949,6 +949,19 @@ var install_skill_exports = {};
|
|
|
949
949
|
__export(install_skill_exports, {
|
|
950
950
|
installSkill: () => installSkill
|
|
951
951
|
});
|
|
952
|
+
function copySkillDir(srcDir, destDir, projectRoot) {
|
|
953
|
+
fs6.mkdirSync(destDir, { recursive: true });
|
|
954
|
+
for (const entry of fs6.readdirSync(srcDir, { withFileTypes: true })) {
|
|
955
|
+
const srcPath = path6.join(srcDir, entry.name);
|
|
956
|
+
const destPath = path6.join(destDir, entry.name);
|
|
957
|
+
if (entry.isDirectory()) {
|
|
958
|
+
copySkillDir(srcPath, destPath, projectRoot);
|
|
959
|
+
} else if (entry.name.endsWith(".md")) {
|
|
960
|
+
fs6.copyFileSync(srcPath, destPath);
|
|
961
|
+
console.log(` ${path6.relative(projectRoot, destPath)}`);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
952
965
|
function installSkill(targetDir) {
|
|
953
966
|
const dir = targetDir || process.cwd();
|
|
954
967
|
const hasGit = fs6.existsSync(path6.join(dir, ".git"));
|
|
@@ -958,32 +971,23 @@ function installSkill(targetDir) {
|
|
|
958
971
|
console.error("Run from a directory with .git or .claude, or pass the path as an argument.");
|
|
959
972
|
process.exit(1);
|
|
960
973
|
}
|
|
961
|
-
const
|
|
962
|
-
fs6.
|
|
963
|
-
|
|
964
|
-
if (!fs6.existsSync(skillSourceDir)) {
|
|
965
|
-
console.error(`Skill directory not found at ${skillSourceDir}`);
|
|
974
|
+
const skillSourceRoot = path6.resolve(path6.dirname((0, import_url.fileURLToPath)(__import_meta_url)), "..", "skill");
|
|
975
|
+
if (!fs6.existsSync(skillSourceRoot)) {
|
|
976
|
+
console.error(`Skill directory not found at ${skillSourceRoot}`);
|
|
966
977
|
console.error("Is @ulpi/browse installed correctly?");
|
|
967
978
|
process.exit(1);
|
|
968
979
|
}
|
|
969
|
-
const
|
|
970
|
-
|
|
971
|
-
|
|
980
|
+
const skillFolders = fs6.readdirSync(skillSourceRoot).filter(
|
|
981
|
+
(f) => fs6.statSync(path6.join(skillSourceRoot, f)).isDirectory()
|
|
982
|
+
);
|
|
983
|
+
if (skillFolders.length === 0) {
|
|
984
|
+
console.error(`No skill folders found in ${skillSourceRoot}`);
|
|
972
985
|
process.exit(1);
|
|
973
986
|
}
|
|
974
|
-
for (const
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
const refsSourceDir = path6.join(skillSourceDir, "references");
|
|
979
|
-
if (fs6.existsSync(refsSourceDir)) {
|
|
980
|
-
const refsDestDir = path6.join(skillDir, "references");
|
|
981
|
-
fs6.mkdirSync(refsDestDir, { recursive: true });
|
|
982
|
-
const refFiles = fs6.readdirSync(refsSourceDir).filter((f) => f.endsWith(".md"));
|
|
983
|
-
for (const file of refFiles) {
|
|
984
|
-
fs6.copyFileSync(path6.join(refsSourceDir, file), path6.join(refsDestDir, file));
|
|
985
|
-
console.log(`Skill installed: ${path6.relative(dir, path6.join(refsDestDir, file))}`);
|
|
986
|
-
}
|
|
987
|
+
for (const folder of skillFolders) {
|
|
988
|
+
const srcDir = path6.join(skillSourceRoot, folder);
|
|
989
|
+
const destDir = path6.join(dir, ".claude", "skills", folder);
|
|
990
|
+
copySkillDir(srcDir, destDir, dir);
|
|
987
991
|
}
|
|
988
992
|
const settingsPath = path6.join(dir, ".claude", "settings.json");
|
|
989
993
|
let settings = {};
|
|
@@ -1080,6 +1084,356 @@ var init_install_skill = __esm({
|
|
|
1080
1084
|
}
|
|
1081
1085
|
});
|
|
1082
1086
|
|
|
1087
|
+
// src/app/ios/controller.ts
|
|
1088
|
+
var controller_exports = {};
|
|
1089
|
+
__export(controller_exports, {
|
|
1090
|
+
addMedia: () => addMedia,
|
|
1091
|
+
bootSimulator: () => bootSimulator,
|
|
1092
|
+
checkXcodeTools: () => checkXcodeTools,
|
|
1093
|
+
clearStatusBar: () => clearStatusBar,
|
|
1094
|
+
getAppContainer: () => getAppContainer,
|
|
1095
|
+
grantPermission: () => grantPermission,
|
|
1096
|
+
installApp: () => installApp,
|
|
1097
|
+
isAppInstalled: () => isAppInstalled,
|
|
1098
|
+
launchApp: () => launchApp,
|
|
1099
|
+
listSimulators: () => listSimulators,
|
|
1100
|
+
openURL: () => openURL,
|
|
1101
|
+
resetPermissions: () => resetPermissions,
|
|
1102
|
+
resolveSimulator: () => resolveSimulator,
|
|
1103
|
+
revokePermission: () => revokePermission,
|
|
1104
|
+
screenshotSimulator: () => screenshotSimulator,
|
|
1105
|
+
setStatusBar: () => setStatusBar,
|
|
1106
|
+
shutdownSimulator: () => shutdownSimulator,
|
|
1107
|
+
terminateApp: () => terminateApp,
|
|
1108
|
+
uninstallApp: () => uninstallApp
|
|
1109
|
+
});
|
|
1110
|
+
async function simctl(...args) {
|
|
1111
|
+
try {
|
|
1112
|
+
const { stdout } = await execFileAsync("xcrun", ["simctl", ...args], {
|
|
1113
|
+
timeout: 6e4,
|
|
1114
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1115
|
+
// 10 MB for device list JSON
|
|
1116
|
+
});
|
|
1117
|
+
return stdout.trim();
|
|
1118
|
+
} catch (err) {
|
|
1119
|
+
const stderr = err.stderr?.trim() || "";
|
|
1120
|
+
const message = stderr || err.message || "Unknown simctl error";
|
|
1121
|
+
throw new Error(`simctl ${args[0]} failed: ${message}`);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
async function listSimulators(filter) {
|
|
1125
|
+
const raw = await simctl("list", "devices", "--json");
|
|
1126
|
+
const parsed = JSON.parse(raw);
|
|
1127
|
+
const results = [];
|
|
1128
|
+
for (const [runtime, devices] of Object.entries(parsed.devices)) {
|
|
1129
|
+
for (const device of devices) {
|
|
1130
|
+
if (!device.isAvailable) continue;
|
|
1131
|
+
const info = {
|
|
1132
|
+
udid: device.udid,
|
|
1133
|
+
name: device.name,
|
|
1134
|
+
state: device.state,
|
|
1135
|
+
runtime,
|
|
1136
|
+
isAvailable: device.isAvailable
|
|
1137
|
+
};
|
|
1138
|
+
if (!filter || info.name.toLowerCase().includes(filter.toLowerCase()) || info.runtime.toLowerCase().includes(filter.toLowerCase())) {
|
|
1139
|
+
results.push(info);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return results;
|
|
1144
|
+
}
|
|
1145
|
+
async function resolveSimulator(identifier) {
|
|
1146
|
+
const all = await listSimulators();
|
|
1147
|
+
if (identifier) {
|
|
1148
|
+
const byUdid = all.find((s) => s.udid === identifier);
|
|
1149
|
+
if (byUdid) return byUdid;
|
|
1150
|
+
const byName = all.filter((s) => s.name === identifier && s.isAvailable);
|
|
1151
|
+
if (byName.length === 1) return byName[0];
|
|
1152
|
+
if (byName.length > 1) {
|
|
1153
|
+
const booted2 = byName.find((s) => s.state === "Booted");
|
|
1154
|
+
if (booted2) return booted2;
|
|
1155
|
+
return byName[0];
|
|
1156
|
+
}
|
|
1157
|
+
const fuzzy = all.filter((s) => s.name.includes(identifier) && s.isAvailable);
|
|
1158
|
+
if (fuzzy.length === 1) return fuzzy[0];
|
|
1159
|
+
if (fuzzy.length > 1) return fuzzy.find((s) => s.state === "Booted") || fuzzy[0];
|
|
1160
|
+
throw new Error(`Simulator '${identifier}' not found. Run: xcrun simctl list devices available`);
|
|
1161
|
+
}
|
|
1162
|
+
const booted = all.find((s) => s.state === "Booted");
|
|
1163
|
+
if (booted) return booted;
|
|
1164
|
+
if (all.length === 0) {
|
|
1165
|
+
throw new Error(
|
|
1166
|
+
'No iOS Simulators available. Create one with:\n xcrun simctl create "iPhone 16 Pro" "com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro"'
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
return all[0];
|
|
1170
|
+
}
|
|
1171
|
+
async function bootSimulator(udid) {
|
|
1172
|
+
const sims = await listSimulators();
|
|
1173
|
+
const sim = sims.find((s) => s.udid === udid);
|
|
1174
|
+
if (sim?.state === "Booted") return;
|
|
1175
|
+
await simctl("boot", udid);
|
|
1176
|
+
const deadline = Date.now() + 3e4;
|
|
1177
|
+
while (Date.now() < deadline) {
|
|
1178
|
+
const current = await listSimulators();
|
|
1179
|
+
const updated = current.find((s) => s.udid === udid);
|
|
1180
|
+
if (updated?.state === "Booted") return;
|
|
1181
|
+
await new Promise((r2) => setTimeout(r2, 1e3));
|
|
1182
|
+
}
|
|
1183
|
+
throw new Error(`Simulator ${udid} did not boot within 30 seconds`);
|
|
1184
|
+
}
|
|
1185
|
+
async function shutdownSimulator(udid) {
|
|
1186
|
+
await simctl("shutdown", udid);
|
|
1187
|
+
}
|
|
1188
|
+
async function installApp(udid, appPath) {
|
|
1189
|
+
await simctl("install", udid, appPath);
|
|
1190
|
+
}
|
|
1191
|
+
async function uninstallApp(udid, bundleId) {
|
|
1192
|
+
await simctl("uninstall", udid, bundleId);
|
|
1193
|
+
}
|
|
1194
|
+
async function launchApp(udid, bundleId, env) {
|
|
1195
|
+
const args = ["launch", udid, bundleId];
|
|
1196
|
+
if (env && Object.keys(env).length > 0) {
|
|
1197
|
+
for (const [key, value] of Object.entries(env)) {
|
|
1198
|
+
args.push(`SIMCTL_CHILD_${key}=${value}`);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
await simctl(...args);
|
|
1202
|
+
}
|
|
1203
|
+
async function terminateApp(udid, bundleId) {
|
|
1204
|
+
try {
|
|
1205
|
+
await simctl("terminate", udid, bundleId);
|
|
1206
|
+
} catch {
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
async function isAppInstalled(udid, bundleId) {
|
|
1210
|
+
try {
|
|
1211
|
+
await simctl("get_app_container", udid, bundleId);
|
|
1212
|
+
return true;
|
|
1213
|
+
} catch {
|
|
1214
|
+
return false;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
async function grantPermission(udid, bundleId, permission) {
|
|
1218
|
+
await simctl("privacy", udid, "grant", permission, bundleId);
|
|
1219
|
+
}
|
|
1220
|
+
async function revokePermission(udid, bundleId, permission) {
|
|
1221
|
+
await simctl("privacy", udid, "revoke", permission, bundleId);
|
|
1222
|
+
}
|
|
1223
|
+
async function resetPermissions(udid, bundleId) {
|
|
1224
|
+
await simctl("privacy", udid, "reset", "all", bundleId);
|
|
1225
|
+
}
|
|
1226
|
+
async function screenshotSimulator(udid, outputPath) {
|
|
1227
|
+
await simctl("io", udid, "screenshot", outputPath);
|
|
1228
|
+
}
|
|
1229
|
+
async function getAppContainer(udid, bundleId, container = "data") {
|
|
1230
|
+
return simctl("get_app_container", udid, bundleId, container);
|
|
1231
|
+
}
|
|
1232
|
+
async function openURL(udid, url) {
|
|
1233
|
+
await simctl("openurl", udid, url);
|
|
1234
|
+
}
|
|
1235
|
+
async function addMedia(udid, ...paths) {
|
|
1236
|
+
await simctl("addmedia", udid, ...paths);
|
|
1237
|
+
}
|
|
1238
|
+
async function setStatusBar(udid, overrides) {
|
|
1239
|
+
const args = ["status_bar", udid, "override"];
|
|
1240
|
+
if (overrides.time) args.push("--time", overrides.time);
|
|
1241
|
+
if (overrides.batteryLevel !== void 0) args.push("--batteryLevel", String(overrides.batteryLevel));
|
|
1242
|
+
if (overrides.batteryState) args.push("--batteryState", overrides.batteryState);
|
|
1243
|
+
if (overrides.cellularBars !== void 0) args.push("--cellularBars", String(overrides.cellularBars));
|
|
1244
|
+
if (overrides.wifiBars !== void 0) args.push("--wifiBars", String(overrides.wifiBars));
|
|
1245
|
+
if (overrides.operatorName) args.push("--operatorName", overrides.operatorName);
|
|
1246
|
+
await simctl(...args);
|
|
1247
|
+
}
|
|
1248
|
+
async function clearStatusBar(udid) {
|
|
1249
|
+
await simctl("status_bar", udid, "clear");
|
|
1250
|
+
}
|
|
1251
|
+
async function checkXcodeTools() {
|
|
1252
|
+
try {
|
|
1253
|
+
await execFileAsync("xcrun", ["--version"], { timeout: 5e3 });
|
|
1254
|
+
} catch {
|
|
1255
|
+
throw new Error(
|
|
1256
|
+
"Xcode Command Line Tools not found.\nInstall with: xcode-select --install\nOr install Xcode from the Mac App Store."
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
try {
|
|
1260
|
+
await execFileAsync("xcrun", ["simctl", "help"], { timeout: 5e3 });
|
|
1261
|
+
} catch {
|
|
1262
|
+
throw new Error(
|
|
1263
|
+
"simctl not available. Ensure Xcode is installed and selected:\n sudo xcode-select -s /Applications/Xcode.app"
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
var import_child_process3, import_util, execFileAsync;
|
|
1268
|
+
var init_controller = __esm({
|
|
1269
|
+
"src/app/ios/controller.ts"() {
|
|
1270
|
+
"use strict";
|
|
1271
|
+
import_child_process3 = require("child_process");
|
|
1272
|
+
import_util = require("util");
|
|
1273
|
+
execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
// src/app/resolve-app.ts
|
|
1278
|
+
var resolve_app_exports = {};
|
|
1279
|
+
__export(resolve_app_exports, {
|
|
1280
|
+
isAppFilePath: () => isAppFilePath,
|
|
1281
|
+
resolveAndroidApp: () => resolveAndroidApp,
|
|
1282
|
+
resolveIOSApp: () => resolveIOSApp
|
|
1283
|
+
});
|
|
1284
|
+
function isAppFilePath(value) {
|
|
1285
|
+
const lower = value.toLowerCase();
|
|
1286
|
+
if (lower.endsWith(".ipa") || lower.endsWith(".apk")) return true;
|
|
1287
|
+
if (lower.endsWith(".app") && fs7.existsSync(value)) return true;
|
|
1288
|
+
return false;
|
|
1289
|
+
}
|
|
1290
|
+
async function resolveIOSApp(appArg, udid, log2) {
|
|
1291
|
+
if (!isAppFilePath(appArg)) return appArg;
|
|
1292
|
+
const absPath = path7.resolve(appArg);
|
|
1293
|
+
if (appArg.toLowerCase().endsWith(".ipa")) {
|
|
1294
|
+
return resolveIPA(absPath, udid, log2);
|
|
1295
|
+
}
|
|
1296
|
+
if (!fs7.existsSync(absPath)) {
|
|
1297
|
+
throw new Error(`App bundle not found: ${absPath}`);
|
|
1298
|
+
}
|
|
1299
|
+
const bundleId = extractBundleId(absPath);
|
|
1300
|
+
log2(`Installing ${path7.basename(absPath)} (${bundleId})...`);
|
|
1301
|
+
const { installApp: installApp2 } = await Promise.resolve().then(() => (init_controller(), controller_exports));
|
|
1302
|
+
await installApp2(udid, absPath);
|
|
1303
|
+
return bundleId;
|
|
1304
|
+
}
|
|
1305
|
+
function extractBundleId(appPath) {
|
|
1306
|
+
const plistPath = path7.join(appPath, "Info.plist");
|
|
1307
|
+
if (!fs7.existsSync(plistPath)) {
|
|
1308
|
+
throw new Error(`Info.plist not found in ${appPath}. Is this a valid iOS app bundle?`);
|
|
1309
|
+
}
|
|
1310
|
+
try {
|
|
1311
|
+
const json = (0, import_child_process4.execSync)(`plutil -convert json -o - "${plistPath}"`, {
|
|
1312
|
+
encoding: "utf-8",
|
|
1313
|
+
timeout: 5e3
|
|
1314
|
+
});
|
|
1315
|
+
const parsed = JSON.parse(json);
|
|
1316
|
+
const bundleId = parsed.CFBundleIdentifier;
|
|
1317
|
+
if (!bundleId) {
|
|
1318
|
+
throw new Error(`CFBundleIdentifier not found in ${plistPath}`);
|
|
1319
|
+
}
|
|
1320
|
+
return bundleId;
|
|
1321
|
+
} catch (err) {
|
|
1322
|
+
if (err.message?.includes("CFBundleIdentifier")) throw err;
|
|
1323
|
+
throw new Error(`Failed to read bundle ID from ${plistPath}: ${err.message}`);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
async function resolveIPA(ipaPath, udid, log2) {
|
|
1327
|
+
if (!fs7.existsSync(ipaPath)) {
|
|
1328
|
+
throw new Error(`IPA file not found: ${ipaPath}`);
|
|
1329
|
+
}
|
|
1330
|
+
const tmpDir = fs7.mkdtempSync(path7.join(os3.tmpdir(), "browse-ipa-"));
|
|
1331
|
+
try {
|
|
1332
|
+
(0, import_child_process4.execSync)(`unzip -o -q "${ipaPath}" -d "${tmpDir}"`, { timeout: 3e4 });
|
|
1333
|
+
const payloadDir = path7.join(tmpDir, "Payload");
|
|
1334
|
+
if (!fs7.existsSync(payloadDir)) {
|
|
1335
|
+
throw new Error(`No Payload directory in ${ipaPath}. Is this a valid .ipa file?`);
|
|
1336
|
+
}
|
|
1337
|
+
const appDirs = fs7.readdirSync(payloadDir).filter((f) => f.endsWith(".app"));
|
|
1338
|
+
if (appDirs.length === 0) {
|
|
1339
|
+
throw new Error(`No .app bundle found in ${ipaPath}/Payload/`);
|
|
1340
|
+
}
|
|
1341
|
+
const appPath = path7.join(payloadDir, appDirs[0]);
|
|
1342
|
+
const bundleId = extractBundleId(appPath);
|
|
1343
|
+
log2(`Installing ${appDirs[0]} (${bundleId}) from IPA...`);
|
|
1344
|
+
const { installApp: installApp2 } = await Promise.resolve().then(() => (init_controller(), controller_exports));
|
|
1345
|
+
await installApp2(udid, appPath);
|
|
1346
|
+
return bundleId;
|
|
1347
|
+
} finally {
|
|
1348
|
+
try {
|
|
1349
|
+
fs7.rmSync(tmpDir, { recursive: true, force: true });
|
|
1350
|
+
} catch {
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
async function resolveAndroidApp(appArg, serial, log2) {
|
|
1355
|
+
if (!isAppFilePath(appArg)) return appArg;
|
|
1356
|
+
const absPath = path7.resolve(appArg);
|
|
1357
|
+
if (!fs7.existsSync(absPath)) {
|
|
1358
|
+
throw new Error(`APK file not found: ${absPath}`);
|
|
1359
|
+
}
|
|
1360
|
+
const packageName = extractPackageName(absPath);
|
|
1361
|
+
log2(`Installing ${path7.basename(absPath)} (${packageName})...`);
|
|
1362
|
+
try {
|
|
1363
|
+
(0, import_child_process4.execSync)(`adb -s ${serial} install -r -t "${absPath}"`, {
|
|
1364
|
+
stdio: "pipe",
|
|
1365
|
+
timeout: 6e4
|
|
1366
|
+
});
|
|
1367
|
+
} catch (err) {
|
|
1368
|
+
throw new Error(`Failed to install APK: ${err.message?.split("\n")[0]}`);
|
|
1369
|
+
}
|
|
1370
|
+
return packageName;
|
|
1371
|
+
}
|
|
1372
|
+
function extractPackageName(apkPath) {
|
|
1373
|
+
const aaptBins = findAaptBinaries();
|
|
1374
|
+
for (const aapt of aaptBins) {
|
|
1375
|
+
try {
|
|
1376
|
+
const output = (0, import_child_process4.execSync)(`"${aapt}" dump badging "${apkPath}"`, {
|
|
1377
|
+
encoding: "utf-8",
|
|
1378
|
+
timeout: 1e4,
|
|
1379
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1380
|
+
});
|
|
1381
|
+
const match = output.match(/package:\s*name='([^']+)'/);
|
|
1382
|
+
if (match) return match[1];
|
|
1383
|
+
} catch {
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
throw new Error(
|
|
1388
|
+
`Cannot determine package name from ${path7.basename(apkPath)}.
|
|
1389
|
+
Install Android build-tools (browse enable android) or provide the package name directly:
|
|
1390
|
+
browse sim start --platform android --app com.example.myapp`
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
function findAaptBinaries() {
|
|
1394
|
+
const bins = [];
|
|
1395
|
+
try {
|
|
1396
|
+
(0, import_child_process4.execSync)("which aapt2", { stdio: "pipe", timeout: 3e3 });
|
|
1397
|
+
bins.push("aapt2");
|
|
1398
|
+
} catch {
|
|
1399
|
+
}
|
|
1400
|
+
try {
|
|
1401
|
+
(0, import_child_process4.execSync)("which aapt", { stdio: "pipe", timeout: 3e3 });
|
|
1402
|
+
bins.push("aapt");
|
|
1403
|
+
} catch {
|
|
1404
|
+
}
|
|
1405
|
+
const sdkRoots = [
|
|
1406
|
+
process.env.ANDROID_HOME,
|
|
1407
|
+
process.env.ANDROID_SDK_ROOT,
|
|
1408
|
+
path7.join(os3.homedir(), "Library/Android/sdk"),
|
|
1409
|
+
"/opt/homebrew/share/android-commandlinetools",
|
|
1410
|
+
path7.join(os3.homedir(), "Android/Sdk")
|
|
1411
|
+
].filter(Boolean);
|
|
1412
|
+
for (const sdk of sdkRoots) {
|
|
1413
|
+
const btDir = path7.join(sdk, "build-tools");
|
|
1414
|
+
if (!fs7.existsSync(btDir)) continue;
|
|
1415
|
+
const versions = fs7.readdirSync(btDir).sort().reverse();
|
|
1416
|
+
for (const ver of versions) {
|
|
1417
|
+
const aapt2 = path7.join(btDir, ver, "aapt2");
|
|
1418
|
+
const aapt = path7.join(btDir, ver, "aapt");
|
|
1419
|
+
if (fs7.existsSync(aapt2)) bins.push(aapt2);
|
|
1420
|
+
if (fs7.existsSync(aapt)) bins.push(aapt);
|
|
1421
|
+
break;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
return bins;
|
|
1425
|
+
}
|
|
1426
|
+
var import_child_process4, fs7, path7, os3;
|
|
1427
|
+
var init_resolve_app = __esm({
|
|
1428
|
+
"src/app/resolve-app.ts"() {
|
|
1429
|
+
"use strict";
|
|
1430
|
+
import_child_process4 = require("child_process");
|
|
1431
|
+
fs7 = __toESM(require("fs"), 1);
|
|
1432
|
+
path7 = __toESM(require("path"), 1);
|
|
1433
|
+
os3 = __toESM(require("os"), 1);
|
|
1434
|
+
}
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1083
1437
|
// src/app/android/bridge.ts
|
|
1084
1438
|
var bridge_exports = {};
|
|
1085
1439
|
__export(bridge_exports, {
|
|
@@ -1091,41 +1445,41 @@ __export(bridge_exports, {
|
|
|
1091
1445
|
function resolveDriverApkPath() {
|
|
1092
1446
|
const candidates = [
|
|
1093
1447
|
// 1. Local dev build
|
|
1094
|
-
|
|
1448
|
+
path8.resolve(__dirname2, "../../../browse-android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk"),
|
|
1095
1449
|
// 2. Installed alongside source (bin/ at project root)
|
|
1096
|
-
|
|
1450
|
+
path8.resolve(__dirname2, "../../bin/browse-android.apk"),
|
|
1097
1451
|
// 3. Bundled build (dist/browse.cjs → ../bin/)
|
|
1098
|
-
|
|
1452
|
+
path8.resolve(__dirname2, "../bin/browse-android.apk"),
|
|
1099
1453
|
// 4. Same directory as binary
|
|
1100
|
-
|
|
1454
|
+
path8.resolve(__dirname2, "browse-android.apk")
|
|
1101
1455
|
];
|
|
1102
1456
|
for (const p of candidates) {
|
|
1103
|
-
if (
|
|
1457
|
+
if (fs8.existsSync(p)) return p;
|
|
1104
1458
|
}
|
|
1105
|
-
const lazyPath =
|
|
1106
|
-
process.env.BROWSE_LOCAL_DIR ||
|
|
1459
|
+
const lazyPath = path8.join(
|
|
1460
|
+
process.env.BROWSE_LOCAL_DIR || path8.join(process.cwd(), ".browse"),
|
|
1107
1461
|
"bin",
|
|
1108
1462
|
"browse-android.apk"
|
|
1109
1463
|
);
|
|
1110
|
-
if (
|
|
1464
|
+
if (fs8.existsSync(lazyPath)) return lazyPath;
|
|
1111
1465
|
throw new Error(
|
|
1112
1466
|
"browse-android APK not found. Run: browse enable android\nOr build manually: cd browse-android && ./gradlew :app:assembleDebugAndroidTest"
|
|
1113
1467
|
);
|
|
1114
1468
|
}
|
|
1115
1469
|
function resolveAppApkPath() {
|
|
1116
1470
|
const candidates = [
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1471
|
+
path8.resolve(__dirname2, "../../../browse-android/app/build/outputs/apk/debug/app-debug.apk"),
|
|
1472
|
+
path8.resolve(__dirname2, "../../bin/browse-android-app.apk"),
|
|
1473
|
+
path8.resolve(__dirname2, "../bin/browse-android-app.apk"),
|
|
1474
|
+
path8.resolve(__dirname2, "browse-android-app.apk")
|
|
1121
1475
|
];
|
|
1122
1476
|
for (const p of candidates) {
|
|
1123
|
-
if (
|
|
1477
|
+
if (fs8.existsSync(p)) return p;
|
|
1124
1478
|
}
|
|
1125
1479
|
return null;
|
|
1126
1480
|
}
|
|
1127
1481
|
function adbExec(serial, ...args) {
|
|
1128
|
-
return (0,
|
|
1482
|
+
return (0, import_child_process5.execSync)(["adb", "-s", serial, ...args].join(" "), {
|
|
1129
1483
|
encoding: "utf-8",
|
|
1130
1484
|
timeout: 3e4,
|
|
1131
1485
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1140,7 +1494,7 @@ function adbExecSafe(serial, ...args) {
|
|
|
1140
1494
|
}
|
|
1141
1495
|
async function ensureAndroidBridge(serial) {
|
|
1142
1496
|
try {
|
|
1143
|
-
(0,
|
|
1497
|
+
(0, import_child_process5.execSync)("adb version", { stdio: "ignore", timeout: 5e3 });
|
|
1144
1498
|
} catch {
|
|
1145
1499
|
throw new AdbNotFoundError();
|
|
1146
1500
|
}
|
|
@@ -1151,7 +1505,7 @@ async function installAdb(log2) {
|
|
|
1151
1505
|
`));
|
|
1152
1506
|
if (process.platform === "darwin") {
|
|
1153
1507
|
try {
|
|
1154
|
-
(0,
|
|
1508
|
+
(0, import_child_process5.execSync)("brew --version", { stdio: "ignore", timeout: 5e3 });
|
|
1155
1509
|
} catch {
|
|
1156
1510
|
print("Homebrew not found. Install it first: https://brew.sh");
|
|
1157
1511
|
print("Then run: brew install android-platform-tools");
|
|
@@ -1159,11 +1513,11 @@ async function installAdb(log2) {
|
|
|
1159
1513
|
}
|
|
1160
1514
|
print("Installing Android platform-tools via Homebrew...");
|
|
1161
1515
|
try {
|
|
1162
|
-
(0,
|
|
1516
|
+
(0, import_child_process5.execSync)("brew install android-platform-tools", {
|
|
1163
1517
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1164
1518
|
timeout: 12e4
|
|
1165
1519
|
});
|
|
1166
|
-
(0,
|
|
1520
|
+
(0, import_child_process5.execSync)("adb version", { stdio: "ignore", timeout: 5e3 });
|
|
1167
1521
|
print("adb installed successfully.");
|
|
1168
1522
|
return true;
|
|
1169
1523
|
} catch (err) {
|
|
@@ -1178,7 +1532,7 @@ async function installAdb(log2) {
|
|
|
1178
1532
|
return false;
|
|
1179
1533
|
}
|
|
1180
1534
|
function resolveDevice(serial) {
|
|
1181
|
-
const output = (0,
|
|
1535
|
+
const output = (0, import_child_process5.execSync)("adb devices", { encoding: "utf-8", timeout: 5e3 });
|
|
1182
1536
|
const lines = output.split("\n").slice(1).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("*"));
|
|
1183
1537
|
const booted = lines.filter((l) => l.endsWith(" device")).map((l) => l.split(" ")[0].trim());
|
|
1184
1538
|
if (booted.length === 0) {
|
|
@@ -1245,7 +1599,7 @@ Install it first, e.g.: adb -s ${serial} install path/to/app.apk`
|
|
|
1245
1599
|
}
|
|
1246
1600
|
function installDriverApk(serial, apkPath) {
|
|
1247
1601
|
try {
|
|
1248
|
-
const stat =
|
|
1602
|
+
const stat = fs8.statSync(apkPath);
|
|
1249
1603
|
if (stat.size < 1024) {
|
|
1250
1604
|
throw new Error(`APK file appears corrupt (${stat.size} bytes): ${apkPath}`);
|
|
1251
1605
|
}
|
|
@@ -1261,7 +1615,7 @@ function installDriverApk(serial, apkPath) {
|
|
|
1261
1615
|
return;
|
|
1262
1616
|
}
|
|
1263
1617
|
const appApkPath = resolveAppApkPath() || apkPath.replace("-androidTest", "");
|
|
1264
|
-
if (
|
|
1618
|
+
if (fs8.existsSync(appApkPath)) {
|
|
1265
1619
|
try {
|
|
1266
1620
|
adbExec(serial, "install", "-t", "-r", `"${appApkPath}"`);
|
|
1267
1621
|
} catch {
|
|
@@ -1284,7 +1638,7 @@ function killStaleInstrumentation(serial) {
|
|
|
1284
1638
|
adbExecSafe(serial, "shell", "am", "force-stop", DRIVER_PACKAGE);
|
|
1285
1639
|
}
|
|
1286
1640
|
function startInstrumentation(serial, targetPackage) {
|
|
1287
|
-
const proc = (0,
|
|
1641
|
+
const proc = (0, import_child_process5.spawn)(
|
|
1288
1642
|
"adb",
|
|
1289
1643
|
[
|
|
1290
1644
|
"-s",
|
|
@@ -1396,16 +1750,16 @@ function buildProtocol(serial, port) {
|
|
|
1396
1750
|
}
|
|
1397
1751
|
};
|
|
1398
1752
|
}
|
|
1399
|
-
var
|
|
1753
|
+
var import_child_process5, fs8, path8, import_url2, __filename_bridge, __dirname2, DRIVER_PACKAGE, DRIVER_TEST_PACKAGE, DRIVER_RUNNER, DRIVER_PORT, DRIVER_HEALTH_TIMEOUT_MS, DRIVER_HEALTH_POLL_MS, INSTRUMENTATION_MAX_RETRIES, AdbNotFoundError;
|
|
1400
1754
|
var init_bridge = __esm({
|
|
1401
1755
|
"src/app/android/bridge.ts"() {
|
|
1402
1756
|
"use strict";
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1757
|
+
import_child_process5 = require("child_process");
|
|
1758
|
+
fs8 = __toESM(require("fs"), 1);
|
|
1759
|
+
path8 = __toESM(require("path"), 1);
|
|
1406
1760
|
import_url2 = require("url");
|
|
1407
1761
|
__filename_bridge = (0, import_url2.fileURLToPath)(__import_meta_url);
|
|
1408
|
-
__dirname2 =
|
|
1762
|
+
__dirname2 = path8.dirname(__filename_bridge);
|
|
1409
1763
|
DRIVER_PACKAGE = "io.ulpi.browse.driver";
|
|
1410
1764
|
DRIVER_TEST_PACKAGE = `${DRIVER_PACKAGE}.test`;
|
|
1411
1765
|
DRIVER_RUNNER = "androidx.test.runner.AndroidJUnitRunner";
|
|
@@ -1445,7 +1799,7 @@ function ensureJavaHome() {
|
|
|
1445
1799
|
"/usr/local/opt/openjdk/libexec/openjdk.jdk/Contents/Home"
|
|
1446
1800
|
];
|
|
1447
1801
|
for (const jdk of candidates) {
|
|
1448
|
-
if (
|
|
1802
|
+
if (fs9.existsSync(jdk)) {
|
|
1449
1803
|
process.env.JAVA_HOME = jdk;
|
|
1450
1804
|
process.env.PATH = `${jdk}/bin:${process.env.PATH}`;
|
|
1451
1805
|
return;
|
|
@@ -1458,20 +1812,20 @@ function sdkCandidates() {
|
|
|
1458
1812
|
return [
|
|
1459
1813
|
process.env.ANDROID_HOME || "",
|
|
1460
1814
|
process.env.ANDROID_SDK_ROOT || "",
|
|
1461
|
-
|
|
1815
|
+
path9.join(home, "Library/Android/sdk"),
|
|
1462
1816
|
"/opt/homebrew/share/android-commandlinetools"
|
|
1463
1817
|
].filter(Boolean);
|
|
1464
1818
|
}
|
|
1465
1819
|
return [
|
|
1466
1820
|
process.env.ANDROID_HOME || "",
|
|
1467
1821
|
process.env.ANDROID_SDK_ROOT || "",
|
|
1468
|
-
|
|
1822
|
+
path9.join(home, "Android/Sdk"),
|
|
1469
1823
|
"/usr/lib/android-sdk"
|
|
1470
1824
|
].filter(Boolean);
|
|
1471
1825
|
}
|
|
1472
1826
|
function findSdkRoot() {
|
|
1473
1827
|
for (const dir of sdkCandidates()) {
|
|
1474
|
-
if (
|
|
1828
|
+
if (fs9.existsSync(path9.join(dir, "cmdline-tools")) || fs9.existsSync(path9.join(dir, "platform-tools")) || fs9.existsSync(path9.join(dir, "emulator"))) {
|
|
1475
1829
|
return dir;
|
|
1476
1830
|
}
|
|
1477
1831
|
}
|
|
@@ -1479,37 +1833,37 @@ function findSdkRoot() {
|
|
|
1479
1833
|
}
|
|
1480
1834
|
function findSdkManager(sdkRoot) {
|
|
1481
1835
|
const paths = [
|
|
1482
|
-
|
|
1483
|
-
|
|
1836
|
+
path9.join(sdkRoot, "cmdline-tools/latest/bin/sdkmanager"),
|
|
1837
|
+
path9.join(sdkRoot, "cmdline-tools/bin/sdkmanager"),
|
|
1484
1838
|
// Homebrew installs cmdline-tools directly
|
|
1485
|
-
|
|
1839
|
+
path9.join(sdkRoot, "bin/sdkmanager")
|
|
1486
1840
|
];
|
|
1487
1841
|
for (const p of paths) {
|
|
1488
|
-
if (
|
|
1842
|
+
if (fs9.existsSync(p)) return p;
|
|
1489
1843
|
}
|
|
1490
1844
|
return null;
|
|
1491
1845
|
}
|
|
1492
1846
|
function findAvdManager(sdkRoot) {
|
|
1493
1847
|
const paths = [
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1848
|
+
path9.join(sdkRoot, "cmdline-tools/latest/bin/avdmanager"),
|
|
1849
|
+
path9.join(sdkRoot, "cmdline-tools/bin/avdmanager"),
|
|
1850
|
+
path9.join(sdkRoot, "bin/avdmanager")
|
|
1497
1851
|
];
|
|
1498
1852
|
for (const p of paths) {
|
|
1499
|
-
if (
|
|
1853
|
+
if (fs9.existsSync(p)) return p;
|
|
1500
1854
|
}
|
|
1501
1855
|
return null;
|
|
1502
1856
|
}
|
|
1503
1857
|
function findEmulator(sdkRoot) {
|
|
1504
1858
|
const paths = [
|
|
1505
|
-
|
|
1506
|
-
|
|
1859
|
+
path9.join(sdkRoot, "emulator/emulator"),
|
|
1860
|
+
path9.join(sdkRoot, "tools/emulator")
|
|
1507
1861
|
];
|
|
1508
1862
|
for (const p of paths) {
|
|
1509
|
-
if (
|
|
1863
|
+
if (fs9.existsSync(p)) return p;
|
|
1510
1864
|
}
|
|
1511
1865
|
try {
|
|
1512
|
-
(0,
|
|
1866
|
+
(0, import_child_process6.execSync)("which emulator", { stdio: "pipe", timeout: 3e3 });
|
|
1513
1867
|
return "emulator";
|
|
1514
1868
|
} catch {
|
|
1515
1869
|
return null;
|
|
@@ -1518,7 +1872,7 @@ function findEmulator(sdkRoot) {
|
|
|
1518
1872
|
async function installSdk(log2) {
|
|
1519
1873
|
if (process.platform === "darwin") {
|
|
1520
1874
|
try {
|
|
1521
|
-
(0,
|
|
1875
|
+
(0, import_child_process6.execSync)("brew --version", { stdio: "ignore", timeout: 5e3 });
|
|
1522
1876
|
} catch {
|
|
1523
1877
|
log2("Homebrew not found. Install Android Studio manually:");
|
|
1524
1878
|
log2(" https://developer.android.com/studio");
|
|
@@ -1526,7 +1880,7 @@ async function installSdk(log2) {
|
|
|
1526
1880
|
}
|
|
1527
1881
|
log2("Installing Android SDK command-line tools via Homebrew...");
|
|
1528
1882
|
try {
|
|
1529
|
-
(0,
|
|
1883
|
+
(0, import_child_process6.execSync)("brew install --cask android-commandlinetools", {
|
|
1530
1884
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1531
1885
|
timeout: 3e5
|
|
1532
1886
|
});
|
|
@@ -1549,13 +1903,13 @@ async function installSdk(log2) {
|
|
|
1549
1903
|
return null;
|
|
1550
1904
|
}
|
|
1551
1905
|
function hasSystemImage(sdkRoot) {
|
|
1552
|
-
const imgDir =
|
|
1553
|
-
return
|
|
1906
|
+
const imgDir = path9.join(sdkRoot, "system-images", `android-${DEFAULT_API_LEVEL}`, "google_apis", "arm64-v8a");
|
|
1907
|
+
return fs9.existsSync(imgDir);
|
|
1554
1908
|
}
|
|
1555
1909
|
function installSystemImage(sdkManager, log2) {
|
|
1556
1910
|
log2(`Installing system image (API ${DEFAULT_API_LEVEL})... this may take a few minutes`);
|
|
1557
1911
|
try {
|
|
1558
|
-
(0,
|
|
1912
|
+
(0, import_child_process6.execSync)(`yes | "${sdkManager}" --install "${DEFAULT_SYSTEM_IMAGE}"`, {
|
|
1559
1913
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1560
1914
|
timeout: 6e5,
|
|
1561
1915
|
shell: "/bin/bash"
|
|
@@ -1569,7 +1923,7 @@ function installSystemImage(sdkManager, log2) {
|
|
|
1569
1923
|
}
|
|
1570
1924
|
function listAvds(avdManager) {
|
|
1571
1925
|
try {
|
|
1572
|
-
const output = (0,
|
|
1926
|
+
const output = (0, import_child_process6.execSync)(`"${avdManager}" list avd -c`, {
|
|
1573
1927
|
encoding: "utf-8",
|
|
1574
1928
|
timeout: 1e4,
|
|
1575
1929
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1582,7 +1936,7 @@ function listAvds(avdManager) {
|
|
|
1582
1936
|
function createAvd(sdkRoot, avdManager, log2) {
|
|
1583
1937
|
log2(`Creating AVD '${DEFAULT_AVD_NAME}'...`);
|
|
1584
1938
|
try {
|
|
1585
|
-
(0,
|
|
1939
|
+
(0, import_child_process6.execSync)(
|
|
1586
1940
|
`echo no | "${avdManager}" create avd -n ${DEFAULT_AVD_NAME} -k "${DEFAULT_SYSTEM_IMAGE}" --force`,
|
|
1587
1941
|
{ stdio: ["pipe", "pipe", "pipe"], timeout: 3e4, shell: "/bin/bash" }
|
|
1588
1942
|
);
|
|
@@ -1596,7 +1950,7 @@ function startEmulator(emulatorBin, avdName, log2, visible = false) {
|
|
|
1596
1950
|
log2(`Starting emulator '${avdName}'${visible ? "" : " (headless)"}...`);
|
|
1597
1951
|
const emulatorArgs = ["-avd", avdName, "-no-audio", "-gpu", "swiftshader_indirect"];
|
|
1598
1952
|
if (!visible) emulatorArgs.push("-no-window");
|
|
1599
|
-
const proc = (0,
|
|
1953
|
+
const proc = (0, import_child_process6.spawn)(emulatorBin, emulatorArgs, {
|
|
1600
1954
|
stdio: "ignore",
|
|
1601
1955
|
detached: true
|
|
1602
1956
|
});
|
|
@@ -1607,13 +1961,13 @@ async function waitForBoot(log2, timeoutMs = 12e4) {
|
|
|
1607
1961
|
log2("Waiting for emulator to boot...");
|
|
1608
1962
|
while (Date.now() < deadline) {
|
|
1609
1963
|
try {
|
|
1610
|
-
const output = (0,
|
|
1964
|
+
const output = (0, import_child_process6.execSync)("adb devices", { encoding: "utf-8", timeout: 5e3 });
|
|
1611
1965
|
const lines = output.split("\n").slice(1).filter((l) => l.includes(" device"));
|
|
1612
1966
|
const emulator = lines.find((l) => l.startsWith("emulator-"));
|
|
1613
1967
|
if (emulator) {
|
|
1614
1968
|
const serial = emulator.split(" ")[0].trim();
|
|
1615
1969
|
try {
|
|
1616
|
-
const bootAnim = (0,
|
|
1970
|
+
const bootAnim = (0, import_child_process6.execSync)(`adb -s ${serial} shell getprop init.svc.bootanim`, {
|
|
1617
1971
|
encoding: "utf-8",
|
|
1618
1972
|
timeout: 5e3
|
|
1619
1973
|
}).trim();
|
|
@@ -1633,7 +1987,7 @@ async function ensureEmulator(log2, visible = false) {
|
|
|
1633
1987
|
ensureJavaHome();
|
|
1634
1988
|
const hasJava = () => {
|
|
1635
1989
|
try {
|
|
1636
|
-
(0,
|
|
1990
|
+
(0, import_child_process6.execSync)("java -version", { stdio: "ignore", timeout: 5e3 });
|
|
1637
1991
|
return true;
|
|
1638
1992
|
} catch {
|
|
1639
1993
|
return false;
|
|
@@ -1642,7 +1996,7 @@ async function ensureEmulator(log2, visible = false) {
|
|
|
1642
1996
|
if (!hasJava()) {
|
|
1643
1997
|
if (process.platform === "darwin") {
|
|
1644
1998
|
try {
|
|
1645
|
-
(0,
|
|
1999
|
+
(0, import_child_process6.execSync)("brew --version", { stdio: "ignore", timeout: 5e3 });
|
|
1646
2000
|
} catch {
|
|
1647
2001
|
throw new Error("Java is required for Android SDK.\nInstall Homebrew (https://brew.sh) then: brew install --cask temurin");
|
|
1648
2002
|
}
|
|
@@ -1651,7 +2005,7 @@ async function ensureEmulator(log2, visible = false) {
|
|
|
1651
2005
|
for (const formula of javaFormulas) {
|
|
1652
2006
|
try {
|
|
1653
2007
|
const cmd = formula === "openjdk" ? `brew install ${formula}` : `brew install --cask ${formula}`;
|
|
1654
|
-
(0,
|
|
2008
|
+
(0, import_child_process6.execSync)(cmd, { stdio: ["ignore", "pipe", "pipe"], timeout: 3e5 });
|
|
1655
2009
|
} catch {
|
|
1656
2010
|
}
|
|
1657
2011
|
if (formula.startsWith("openjdk")) {
|
|
@@ -1682,7 +2036,7 @@ async function ensureEmulator(log2, visible = false) {
|
|
|
1682
2036
|
const avdManager = findAvdManager(sdkRoot);
|
|
1683
2037
|
if (sdkManager) {
|
|
1684
2038
|
try {
|
|
1685
|
-
(0,
|
|
2039
|
+
(0, import_child_process6.execSync)(`yes | "${sdkManager}" --licenses`, {
|
|
1686
2040
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1687
2041
|
timeout: 6e4,
|
|
1688
2042
|
shell: "/bin/bash"
|
|
@@ -1693,7 +2047,7 @@ async function ensureEmulator(log2, visible = false) {
|
|
|
1693
2047
|
if (!emulatorBin && sdkManager) {
|
|
1694
2048
|
log2("Installing Android emulator and build tools...");
|
|
1695
2049
|
try {
|
|
1696
|
-
(0,
|
|
2050
|
+
(0, import_child_process6.execSync)(
|
|
1697
2051
|
`"${sdkManager}" --install "emulator" "platform-tools" "platforms;android-${DEFAULT_API_LEVEL}" "build-tools;${DEFAULT_API_LEVEL}.0.0"`,
|
|
1698
2052
|
{ stdio: ["pipe", "pipe", "pipe"], timeout: 3e5 }
|
|
1699
2053
|
);
|
|
@@ -1725,13 +2079,13 @@ async function ensureEmulator(log2, visible = false) {
|
|
|
1725
2079
|
startEmulator(emulatorBin, DEFAULT_AVD_NAME, log2, visible);
|
|
1726
2080
|
return waitForBoot(log2);
|
|
1727
2081
|
}
|
|
1728
|
-
var
|
|
2082
|
+
var import_child_process6, fs9, path9, DEFAULT_AVD_NAME, DEFAULT_API_LEVEL, DEFAULT_SYSTEM_IMAGE;
|
|
1729
2083
|
var init_emulator = __esm({
|
|
1730
2084
|
"src/app/android/emulator.ts"() {
|
|
1731
2085
|
"use strict";
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
2086
|
+
import_child_process6 = require("child_process");
|
|
2087
|
+
fs9 = __toESM(require("fs"), 1);
|
|
2088
|
+
path9 = __toESM(require("path"), 1);
|
|
1735
2089
|
DEFAULT_AVD_NAME = "browse_default";
|
|
1736
2090
|
DEFAULT_API_LEVEL = "35";
|
|
1737
2091
|
DEFAULT_SYSTEM_IMAGE = `system-images;android-${DEFAULT_API_LEVEL};google_apis;arm64-v8a`;
|
|
@@ -1753,35 +2107,35 @@ function resolveStateDir() {
|
|
|
1753
2107
|
if (localDir) return localDir;
|
|
1754
2108
|
let dir = process.cwd();
|
|
1755
2109
|
for (let i = 0; i < 20; i++) {
|
|
1756
|
-
if (
|
|
1757
|
-
const browseDir =
|
|
1758
|
-
|
|
2110
|
+
if (fs10.existsSync(path10.join(dir, ".git")) || fs10.existsSync(path10.join(dir, ".claude"))) {
|
|
2111
|
+
const browseDir = path10.join(dir, ".browse");
|
|
2112
|
+
fs10.mkdirSync(browseDir, { recursive: true });
|
|
1759
2113
|
return browseDir;
|
|
1760
2114
|
}
|
|
1761
|
-
const parent =
|
|
2115
|
+
const parent = path10.dirname(dir);
|
|
1762
2116
|
if (parent === dir) break;
|
|
1763
2117
|
dir = parent;
|
|
1764
2118
|
}
|
|
1765
2119
|
return "/tmp";
|
|
1766
2120
|
}
|
|
1767
2121
|
function stateFilePath() {
|
|
1768
|
-
return
|
|
2122
|
+
return path10.join(resolveStateDir(), "android-state.json");
|
|
1769
2123
|
}
|
|
1770
2124
|
function readState() {
|
|
1771
2125
|
try {
|
|
1772
|
-
return JSON.parse(
|
|
2126
|
+
return JSON.parse(fs10.readFileSync(stateFilePath(), "utf-8"));
|
|
1773
2127
|
} catch {
|
|
1774
2128
|
return null;
|
|
1775
2129
|
}
|
|
1776
2130
|
}
|
|
1777
2131
|
function writeState(state) {
|
|
1778
|
-
const dir =
|
|
1779
|
-
|
|
1780
|
-
|
|
2132
|
+
const dir = path10.dirname(stateFilePath());
|
|
2133
|
+
fs10.mkdirSync(dir, { recursive: true });
|
|
2134
|
+
fs10.writeFileSync(stateFilePath(), JSON.stringify(state, null, 2));
|
|
1781
2135
|
}
|
|
1782
2136
|
function clearState() {
|
|
1783
2137
|
try {
|
|
1784
|
-
|
|
2138
|
+
fs10.unlinkSync(stateFilePath());
|
|
1785
2139
|
} catch {
|
|
1786
2140
|
}
|
|
1787
2141
|
}
|
|
@@ -1799,350 +2153,171 @@ async function status() {
|
|
|
1799
2153
|
const healthy = await checkHealth(state.port);
|
|
1800
2154
|
if (!healthy) {
|
|
1801
2155
|
clearState();
|
|
1802
|
-
return { running: false, state, healthy: false };
|
|
1803
|
-
}
|
|
1804
|
-
return { running: true, state, healthy: true };
|
|
1805
|
-
}
|
|
1806
|
-
async function stopDriver() {
|
|
1807
|
-
const state = readState();
|
|
1808
|
-
if (!state) return;
|
|
1809
|
-
try {
|
|
1810
|
-
(0, import_child_process5.execSync)(`adb -s ${state.serial} shell am force-stop io.ulpi.browse.driver.test`, { stdio: "pipe", timeout: 5e3 });
|
|
1811
|
-
(0, import_child_process5.execSync)(`adb -s ${state.serial} shell am force-stop io.ulpi.browse.driver`, { stdio: "pipe", timeout: 5e3 });
|
|
1812
|
-
(0, import_child_process5.execSync)(`adb -s ${state.serial} forward --remove tcp:${state.port}`, { stdio: "pipe", timeout: 5e3 });
|
|
1813
|
-
} catch {
|
|
1814
|
-
}
|
|
1815
|
-
clearState();
|
|
1816
|
-
}
|
|
1817
|
-
async function stop() {
|
|
1818
|
-
const state = readState();
|
|
1819
|
-
if (!state) return "No Android device/emulator running.";
|
|
1820
|
-
try {
|
|
1821
|
-
(0, import_child_process5.execSync)(`adb -s ${state.serial} shell am force-stop io.ulpi.browse.driver.test`, { stdio: "pipe", timeout: 5e3 });
|
|
1822
|
-
(0, import_child_process5.execSync)(`adb -s ${state.serial} shell am force-stop io.ulpi.browse.driver`, { stdio: "pipe", timeout: 5e3 });
|
|
1823
|
-
(0, import_child_process5.execSync)(`adb -s ${state.serial} forward --remove tcp:${state.port}`, { stdio: "pipe", timeout: 5e3 });
|
|
1824
|
-
if (state.serial.startsWith("emulator-")) {
|
|
1825
|
-
(0, import_child_process5.execSync)(`adb -s ${state.serial} emu kill`, { stdio: "pipe", timeout: 1e4 });
|
|
1826
|
-
}
|
|
1827
|
-
} catch {
|
|
1828
|
-
}
|
|
1829
|
-
clearState();
|
|
1830
|
-
return `Android stopped (${state.device}).`;
|
|
1831
|
-
}
|
|
1832
|
-
async function startAndroid(opts = {}) {
|
|
1833
|
-
const log2 = opts.log || ((msg) => process.stderr.write(`[browse] ${msg}
|
|
1834
|
-
`));
|
|
1835
|
-
const port = DRIVER_PORT2;
|
|
1836
|
-
const existing = readState();
|
|
1837
|
-
if (existing) {
|
|
1838
|
-
const healthy = await checkHealth(existing.port);
|
|
1839
|
-
if (healthy) {
|
|
1840
|
-
if (opts.app && existing.app !== opts.app) {
|
|
1841
|
-
log2(`Switching to ${opts.app}...`);
|
|
1842
|
-
await stopDriver();
|
|
1843
|
-
} else {
|
|
1844
|
-
return existing;
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
log2("Cleaning up stale driver...");
|
|
1848
|
-
await stopDriver();
|
|
1849
|
-
}
|
|
1850
|
-
log2("Finding Android device...");
|
|
1851
|
-
const { ensureAndroidBridge: ensureAndroidBridge2, createAndroidBridge: createAndroidBridge2, AdbNotFoundError: AdbNotFoundError2, installAdb: installAdb2 } = await Promise.resolve().then(() => (init_bridge(), bridge_exports));
|
|
1852
|
-
let serial;
|
|
1853
|
-
try {
|
|
1854
|
-
serial = await ensureAndroidBridge2(opts.device);
|
|
1855
|
-
} catch (err) {
|
|
1856
|
-
if (err instanceof AdbNotFoundError2) {
|
|
1857
|
-
log2("adb not found. Attempting to install...");
|
|
1858
|
-
const installed = await installAdb2(log2);
|
|
1859
|
-
if (!installed) throw err;
|
|
1860
|
-
try {
|
|
1861
|
-
serial = await ensureAndroidBridge2(opts.device);
|
|
1862
|
-
} catch (retryErr) {
|
|
1863
|
-
if (retryErr.message?.includes("No booted Android device")) {
|
|
1864
|
-
const { ensureEmulator: ensureEmulator2 } = await Promise.resolve().then(() => (init_emulator(), emulator_exports));
|
|
1865
|
-
serial = await ensureEmulator2(log2, opts.visible);
|
|
1866
|
-
} else {
|
|
1867
|
-
throw retryErr;
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
} else if (err.message?.includes("No booted Android device")) {
|
|
1871
|
-
const { ensureEmulator: ensureEmulator2 } = await Promise.resolve().then(() => (init_emulator(), emulator_exports));
|
|
1872
|
-
serial = await ensureEmulator2(log2, opts.visible);
|
|
1873
|
-
} else {
|
|
1874
|
-
throw err;
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
|
-
let deviceName = serial;
|
|
1878
|
-
try {
|
|
1879
|
-
deviceName = (0, import_child_process5.execSync)(`adb -s ${serial} shell getprop ro.product.model`, {
|
|
1880
|
-
encoding: "utf-8",
|
|
1881
|
-
timeout: 5e3,
|
|
1882
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1883
|
-
}).trim() || serial;
|
|
1884
|
-
} catch {
|
|
1885
|
-
}
|
|
1886
|
-
const driverApkDir = path9.resolve(__dirname3, "../../../browse-android");
|
|
1887
|
-
const apkPath = path9.join(driverApkDir, "app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk");
|
|
1888
|
-
if (!fs9.existsSync(apkPath) && fs9.existsSync(path9.join(driverApkDir, "gradlew"))) {
|
|
1889
|
-
log2("Building Android driver APK...");
|
|
1890
|
-
try {
|
|
1891
|
-
const { findSdkRoot: findSdkRoot2, ensureJavaHome: ensureJavaHome2 } = await Promise.resolve().then(() => (init_emulator(), emulator_exports));
|
|
1892
|
-
ensureJavaHome2();
|
|
1893
|
-
if (!process.env.ANDROID_HOME) {
|
|
1894
|
-
const sdkRoot = findSdkRoot2();
|
|
1895
|
-
if (sdkRoot) process.env.ANDROID_HOME = sdkRoot;
|
|
1896
|
-
}
|
|
1897
|
-
(0, import_child_process5.execSync)("./gradlew :app:assembleDebug :app:assembleDebugAndroidTest --stacktrace", {
|
|
1898
|
-
cwd: driverApkDir,
|
|
1899
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1900
|
-
timeout: 3e5
|
|
1901
|
-
});
|
|
1902
|
-
log2("APK built.");
|
|
1903
|
-
} catch (err) {
|
|
1904
|
-
const stdout = err.stdout?.toString() || "";
|
|
1905
|
-
const stderr = err.stderr?.toString() || "";
|
|
1906
|
-
const combined = stdout + "\n" + stderr;
|
|
1907
|
-
const lines = combined.split("\n");
|
|
1908
|
-
const errorLines = lines.filter(
|
|
1909
|
-
(l) => !l.startsWith(" at ") && !l.startsWith(" at ") && (l.includes("error") || l.includes("Error") || l.includes("FAILURE") || l.includes("Could not") || l.includes("wrong") || l.includes("Cannot") || l.includes("SDK") || l.includes("missing"))
|
|
1910
|
-
);
|
|
1911
|
-
log2(`APK build failed:
|
|
1912
|
-
${errorLines.slice(0, 10).join("\n") || lines.slice(0, 15).join("\n")}`);
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
const targetApp = opts.app || "com.android.settings";
|
|
1916
|
-
log2(`Launching ${targetApp}...`);
|
|
1917
|
-
try {
|
|
1918
|
-
(0, import_child_process5.execSync)(`adb -s ${serial} shell am start -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -p ${targetApp}`, {
|
|
1919
|
-
stdio: "pipe",
|
|
1920
|
-
timeout: 1e4
|
|
1921
|
-
});
|
|
1922
|
-
} catch {
|
|
1923
|
-
try {
|
|
1924
|
-
(0, import_child_process5.execSync)(`adb -s ${serial} shell monkey -p ${targetApp} -c android.intent.category.LAUNCHER 1`, {
|
|
1925
|
-
stdio: "pipe",
|
|
1926
|
-
timeout: 1e4
|
|
1927
|
-
});
|
|
1928
|
-
} catch {
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
|
-
await sleep(2e3);
|
|
1932
|
-
log2("Starting driver...");
|
|
1933
|
-
await createAndroidBridge2(serial, targetApp);
|
|
1934
|
-
const state = {
|
|
1935
|
-
platform: "android",
|
|
1936
|
-
device: deviceName,
|
|
1937
|
-
serial,
|
|
1938
|
-
app: targetApp,
|
|
1939
|
-
port,
|
|
1940
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1941
|
-
};
|
|
1942
|
-
writeState(state);
|
|
1943
|
-
return state;
|
|
1944
|
-
}
|
|
1945
|
-
var fs9, path9, import_child_process5, import_url3, __dirname3, DRIVER_PORT2, sleep;
|
|
1946
|
-
var init_sim_service = __esm({
|
|
1947
|
-
"src/app/android/sim-service.ts"() {
|
|
1948
|
-
"use strict";
|
|
1949
|
-
fs9 = __toESM(require("fs"), 1);
|
|
1950
|
-
path9 = __toESM(require("path"), 1);
|
|
1951
|
-
import_child_process5 = require("child_process");
|
|
1952
|
-
import_url3 = require("url");
|
|
1953
|
-
__dirname3 = path9.dirname((0, import_url3.fileURLToPath)(__import_meta_url));
|
|
1954
|
-
DRIVER_PORT2 = 7779;
|
|
1955
|
-
sleep = (ms) => new Promise((r2) => setTimeout(r2, ms));
|
|
1956
|
-
}
|
|
1957
|
-
});
|
|
1958
|
-
|
|
1959
|
-
// src/app/ios/controller.ts
|
|
1960
|
-
var controller_exports = {};
|
|
1961
|
-
__export(controller_exports, {
|
|
1962
|
-
addMedia: () => addMedia,
|
|
1963
|
-
bootSimulator: () => bootSimulator,
|
|
1964
|
-
checkXcodeTools: () => checkXcodeTools,
|
|
1965
|
-
clearStatusBar: () => clearStatusBar,
|
|
1966
|
-
getAppContainer: () => getAppContainer,
|
|
1967
|
-
grantPermission: () => grantPermission,
|
|
1968
|
-
installApp: () => installApp,
|
|
1969
|
-
isAppInstalled: () => isAppInstalled,
|
|
1970
|
-
launchApp: () => launchApp,
|
|
1971
|
-
listSimulators: () => listSimulators,
|
|
1972
|
-
openURL: () => openURL,
|
|
1973
|
-
resetPermissions: () => resetPermissions,
|
|
1974
|
-
resolveSimulator: () => resolveSimulator,
|
|
1975
|
-
revokePermission: () => revokePermission,
|
|
1976
|
-
screenshotSimulator: () => screenshotSimulator,
|
|
1977
|
-
setStatusBar: () => setStatusBar,
|
|
1978
|
-
shutdownSimulator: () => shutdownSimulator,
|
|
1979
|
-
terminateApp: () => terminateApp,
|
|
1980
|
-
uninstallApp: () => uninstallApp
|
|
1981
|
-
});
|
|
1982
|
-
async function simctl(...args) {
|
|
1983
|
-
try {
|
|
1984
|
-
const { stdout } = await execFileAsync("xcrun", ["simctl", ...args], {
|
|
1985
|
-
timeout: 6e4,
|
|
1986
|
-
maxBuffer: 10 * 1024 * 1024
|
|
1987
|
-
// 10 MB for device list JSON
|
|
1988
|
-
});
|
|
1989
|
-
return stdout.trim();
|
|
1990
|
-
} catch (err) {
|
|
1991
|
-
const stderr = err.stderr?.trim() || "";
|
|
1992
|
-
const message = stderr || err.message || "Unknown simctl error";
|
|
1993
|
-
throw new Error(`simctl ${args[0]} failed: ${message}`);
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
async function listSimulators(filter) {
|
|
1997
|
-
const raw = await simctl("list", "devices", "--json");
|
|
1998
|
-
const parsed = JSON.parse(raw);
|
|
1999
|
-
const results = [];
|
|
2000
|
-
for (const [runtime, devices] of Object.entries(parsed.devices)) {
|
|
2001
|
-
for (const device of devices) {
|
|
2002
|
-
if (!device.isAvailable) continue;
|
|
2003
|
-
const info = {
|
|
2004
|
-
udid: device.udid,
|
|
2005
|
-
name: device.name,
|
|
2006
|
-
state: device.state,
|
|
2007
|
-
runtime,
|
|
2008
|
-
isAvailable: device.isAvailable
|
|
2009
|
-
};
|
|
2010
|
-
if (!filter || info.name.toLowerCase().includes(filter.toLowerCase()) || info.runtime.toLowerCase().includes(filter.toLowerCase())) {
|
|
2011
|
-
results.push(info);
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
return results;
|
|
2016
|
-
}
|
|
2017
|
-
async function resolveSimulator(identifier) {
|
|
2018
|
-
const all = await listSimulators();
|
|
2019
|
-
if (identifier) {
|
|
2020
|
-
const byUdid = all.find((s) => s.udid === identifier);
|
|
2021
|
-
if (byUdid) return byUdid;
|
|
2022
|
-
const byName = all.filter((s) => s.name === identifier && s.isAvailable);
|
|
2023
|
-
if (byName.length === 1) return byName[0];
|
|
2024
|
-
if (byName.length > 1) {
|
|
2025
|
-
const booted2 = byName.find((s) => s.state === "Booted");
|
|
2026
|
-
if (booted2) return booted2;
|
|
2027
|
-
return byName[0];
|
|
2028
|
-
}
|
|
2029
|
-
const fuzzy = all.filter((s) => s.name.includes(identifier) && s.isAvailable);
|
|
2030
|
-
if (fuzzy.length === 1) return fuzzy[0];
|
|
2031
|
-
if (fuzzy.length > 1) return fuzzy.find((s) => s.state === "Booted") || fuzzy[0];
|
|
2032
|
-
throw new Error(`Simulator '${identifier}' not found. Run: xcrun simctl list devices available`);
|
|
2033
|
-
}
|
|
2034
|
-
const booted = all.find((s) => s.state === "Booted");
|
|
2035
|
-
if (booted) return booted;
|
|
2036
|
-
if (all.length === 0) {
|
|
2037
|
-
throw new Error(
|
|
2038
|
-
'No iOS Simulators available. Create one with:\n xcrun simctl create "iPhone 16 Pro" "com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro"'
|
|
2039
|
-
);
|
|
2040
|
-
}
|
|
2041
|
-
return all[0];
|
|
2042
|
-
}
|
|
2043
|
-
async function bootSimulator(udid) {
|
|
2044
|
-
const sims = await listSimulators();
|
|
2045
|
-
const sim = sims.find((s) => s.udid === udid);
|
|
2046
|
-
if (sim?.state === "Booted") return;
|
|
2047
|
-
await simctl("boot", udid);
|
|
2048
|
-
const deadline = Date.now() + 3e4;
|
|
2049
|
-
while (Date.now() < deadline) {
|
|
2050
|
-
const current = await listSimulators();
|
|
2051
|
-
const updated = current.find((s) => s.udid === udid);
|
|
2052
|
-
if (updated?.state === "Booted") return;
|
|
2053
|
-
await new Promise((r2) => setTimeout(r2, 1e3));
|
|
2054
|
-
}
|
|
2055
|
-
throw new Error(`Simulator ${udid} did not boot within 30 seconds`);
|
|
2056
|
-
}
|
|
2057
|
-
async function shutdownSimulator(udid) {
|
|
2058
|
-
await simctl("shutdown", udid);
|
|
2059
|
-
}
|
|
2060
|
-
async function installApp(udid, appPath) {
|
|
2061
|
-
await simctl("install", udid, appPath);
|
|
2062
|
-
}
|
|
2063
|
-
async function uninstallApp(udid, bundleId) {
|
|
2064
|
-
await simctl("uninstall", udid, bundleId);
|
|
2065
|
-
}
|
|
2066
|
-
async function launchApp(udid, bundleId, env) {
|
|
2067
|
-
const args = ["launch", udid, bundleId];
|
|
2068
|
-
if (env && Object.keys(env).length > 0) {
|
|
2069
|
-
for (const [key, value] of Object.entries(env)) {
|
|
2070
|
-
args.push(`SIMCTL_CHILD_${key}=${value}`);
|
|
2071
|
-
}
|
|
2156
|
+
return { running: false, state, healthy: false };
|
|
2072
2157
|
}
|
|
2073
|
-
|
|
2158
|
+
return { running: true, state, healthy: true };
|
|
2074
2159
|
}
|
|
2075
|
-
async function
|
|
2160
|
+
async function stopDriver() {
|
|
2161
|
+
const state = readState();
|
|
2162
|
+
if (!state) return;
|
|
2076
2163
|
try {
|
|
2077
|
-
|
|
2164
|
+
(0, import_child_process7.execSync)(`adb -s ${state.serial} shell am force-stop io.ulpi.browse.driver.test`, { stdio: "pipe", timeout: 5e3 });
|
|
2165
|
+
(0, import_child_process7.execSync)(`adb -s ${state.serial} shell am force-stop io.ulpi.browse.driver`, { stdio: "pipe", timeout: 5e3 });
|
|
2166
|
+
(0, import_child_process7.execSync)(`adb -s ${state.serial} forward --remove tcp:${state.port}`, { stdio: "pipe", timeout: 5e3 });
|
|
2078
2167
|
} catch {
|
|
2079
2168
|
}
|
|
2169
|
+
clearState();
|
|
2080
2170
|
}
|
|
2081
|
-
async function
|
|
2171
|
+
async function stop() {
|
|
2172
|
+
const state = readState();
|
|
2173
|
+
if (!state) return "No Android device/emulator running.";
|
|
2082
2174
|
try {
|
|
2083
|
-
|
|
2084
|
-
|
|
2175
|
+
(0, import_child_process7.execSync)(`adb -s ${state.serial} shell am force-stop io.ulpi.browse.driver.test`, { stdio: "pipe", timeout: 5e3 });
|
|
2176
|
+
(0, import_child_process7.execSync)(`adb -s ${state.serial} shell am force-stop io.ulpi.browse.driver`, { stdio: "pipe", timeout: 5e3 });
|
|
2177
|
+
(0, import_child_process7.execSync)(`adb -s ${state.serial} forward --remove tcp:${state.port}`, { stdio: "pipe", timeout: 5e3 });
|
|
2178
|
+
if (state.serial.startsWith("emulator-")) {
|
|
2179
|
+
(0, import_child_process7.execSync)(`adb -s ${state.serial} emu kill`, { stdio: "pipe", timeout: 1e4 });
|
|
2180
|
+
}
|
|
2085
2181
|
} catch {
|
|
2086
|
-
return false;
|
|
2087
2182
|
}
|
|
2183
|
+
clearState();
|
|
2184
|
+
return `Android stopped (${state.device}).`;
|
|
2088
2185
|
}
|
|
2089
|
-
async function
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
}
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
if (overrides.operatorName) args.push("--operatorName", overrides.operatorName);
|
|
2118
|
-
await simctl(...args);
|
|
2119
|
-
}
|
|
2120
|
-
async function clearStatusBar(udid) {
|
|
2121
|
-
await simctl("status_bar", udid, "clear");
|
|
2122
|
-
}
|
|
2123
|
-
async function checkXcodeTools() {
|
|
2186
|
+
async function startAndroid(opts = {}) {
|
|
2187
|
+
const log2 = opts.log || ((msg) => process.stderr.write(`[browse] ${msg}
|
|
2188
|
+
`));
|
|
2189
|
+
const port = DRIVER_PORT2;
|
|
2190
|
+
let pendingApkInstall = null;
|
|
2191
|
+
if (opts.app) {
|
|
2192
|
+
const { isAppFilePath: isAppFilePath2 } = await Promise.resolve().then(() => (init_resolve_app(), resolve_app_exports));
|
|
2193
|
+
if (isAppFilePath2(opts.app)) {
|
|
2194
|
+
pendingApkInstall = path10.resolve(opts.app);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
const existing = readState();
|
|
2198
|
+
if (existing) {
|
|
2199
|
+
const healthy = await checkHealth(existing.port);
|
|
2200
|
+
if (healthy) {
|
|
2201
|
+
if (opts.app && existing.app !== opts.app) {
|
|
2202
|
+
log2(`Switching to ${opts.app}...`);
|
|
2203
|
+
await stopDriver();
|
|
2204
|
+
} else {
|
|
2205
|
+
return existing;
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
log2("Cleaning up stale driver...");
|
|
2209
|
+
await stopDriver();
|
|
2210
|
+
}
|
|
2211
|
+
log2("Finding Android device...");
|
|
2212
|
+
const { ensureAndroidBridge: ensureAndroidBridge2, createAndroidBridge: createAndroidBridge2, AdbNotFoundError: AdbNotFoundError2, installAdb: installAdb2 } = await Promise.resolve().then(() => (init_bridge(), bridge_exports));
|
|
2213
|
+
let serial;
|
|
2124
2214
|
try {
|
|
2125
|
-
await
|
|
2215
|
+
serial = await ensureAndroidBridge2(opts.device);
|
|
2216
|
+
} catch (err) {
|
|
2217
|
+
if (err instanceof AdbNotFoundError2) {
|
|
2218
|
+
log2("adb not found. Attempting to install...");
|
|
2219
|
+
const installed = await installAdb2(log2);
|
|
2220
|
+
if (!installed) throw err;
|
|
2221
|
+
try {
|
|
2222
|
+
serial = await ensureAndroidBridge2(opts.device);
|
|
2223
|
+
} catch (retryErr) {
|
|
2224
|
+
if (retryErr.message?.includes("No booted Android device")) {
|
|
2225
|
+
const { ensureEmulator: ensureEmulator2 } = await Promise.resolve().then(() => (init_emulator(), emulator_exports));
|
|
2226
|
+
serial = await ensureEmulator2(log2, opts.visible);
|
|
2227
|
+
} else {
|
|
2228
|
+
throw retryErr;
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
} else if (err.message?.includes("No booted Android device")) {
|
|
2232
|
+
const { ensureEmulator: ensureEmulator2 } = await Promise.resolve().then(() => (init_emulator(), emulator_exports));
|
|
2233
|
+
serial = await ensureEmulator2(log2, opts.visible);
|
|
2234
|
+
} else {
|
|
2235
|
+
throw err;
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
let deviceName = serial;
|
|
2239
|
+
try {
|
|
2240
|
+
deviceName = (0, import_child_process7.execSync)(`adb -s ${serial} shell getprop ro.product.model`, {
|
|
2241
|
+
encoding: "utf-8",
|
|
2242
|
+
timeout: 5e3,
|
|
2243
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2244
|
+
}).trim() || serial;
|
|
2126
2245
|
} catch {
|
|
2127
|
-
throw new Error(
|
|
2128
|
-
"Xcode Command Line Tools not found.\nInstall with: xcode-select --install\nOr install Xcode from the Mac App Store."
|
|
2129
|
-
);
|
|
2130
2246
|
}
|
|
2247
|
+
const driverApkDir = path10.resolve(__dirname3, "../../../browse-android");
|
|
2248
|
+
const apkPath = path10.join(driverApkDir, "app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk");
|
|
2249
|
+
if (!fs10.existsSync(apkPath) && fs10.existsSync(path10.join(driverApkDir, "gradlew"))) {
|
|
2250
|
+
log2("Building Android driver APK...");
|
|
2251
|
+
try {
|
|
2252
|
+
const { findSdkRoot: findSdkRoot2, ensureJavaHome: ensureJavaHome2 } = await Promise.resolve().then(() => (init_emulator(), emulator_exports));
|
|
2253
|
+
ensureJavaHome2();
|
|
2254
|
+
if (!process.env.ANDROID_HOME) {
|
|
2255
|
+
const sdkRoot = findSdkRoot2();
|
|
2256
|
+
if (sdkRoot) process.env.ANDROID_HOME = sdkRoot;
|
|
2257
|
+
}
|
|
2258
|
+
(0, import_child_process7.execSync)("./gradlew :app:assembleDebug :app:assembleDebugAndroidTest --stacktrace", {
|
|
2259
|
+
cwd: driverApkDir,
|
|
2260
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2261
|
+
timeout: 3e5
|
|
2262
|
+
});
|
|
2263
|
+
log2("APK built.");
|
|
2264
|
+
} catch (err) {
|
|
2265
|
+
const stdout = err.stdout?.toString() || "";
|
|
2266
|
+
const stderr = err.stderr?.toString() || "";
|
|
2267
|
+
const combined = stdout + "\n" + stderr;
|
|
2268
|
+
const lines = combined.split("\n");
|
|
2269
|
+
const errorLines = lines.filter(
|
|
2270
|
+
(l) => !l.startsWith(" at ") && !l.startsWith(" at ") && (l.includes("error") || l.includes("Error") || l.includes("FAILURE") || l.includes("Could not") || l.includes("wrong") || l.includes("Cannot") || l.includes("SDK") || l.includes("missing"))
|
|
2271
|
+
);
|
|
2272
|
+
log2(`APK build failed:
|
|
2273
|
+
${errorLines.slice(0, 10).join("\n") || lines.slice(0, 15).join("\n")}`);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
if (pendingApkInstall) {
|
|
2277
|
+
const { resolveAndroidApp: resolveAndroidApp2 } = await Promise.resolve().then(() => (init_resolve_app(), resolve_app_exports));
|
|
2278
|
+
opts.app = await resolveAndroidApp2(pendingApkInstall, serial, log2);
|
|
2279
|
+
}
|
|
2280
|
+
const targetApp = opts.app || "com.android.settings";
|
|
2281
|
+
log2(`Launching ${targetApp}...`);
|
|
2131
2282
|
try {
|
|
2132
|
-
|
|
2283
|
+
(0, import_child_process7.execSync)(`adb -s ${serial} shell am start -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -p ${targetApp}`, {
|
|
2284
|
+
stdio: "pipe",
|
|
2285
|
+
timeout: 1e4
|
|
2286
|
+
});
|
|
2133
2287
|
} catch {
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2288
|
+
try {
|
|
2289
|
+
(0, import_child_process7.execSync)(`adb -s ${serial} shell monkey -p ${targetApp} -c android.intent.category.LAUNCHER 1`, {
|
|
2290
|
+
stdio: "pipe",
|
|
2291
|
+
timeout: 1e4
|
|
2292
|
+
});
|
|
2293
|
+
} catch {
|
|
2294
|
+
}
|
|
2137
2295
|
}
|
|
2296
|
+
await sleep(2e3);
|
|
2297
|
+
log2("Starting driver...");
|
|
2298
|
+
await createAndroidBridge2(serial, targetApp);
|
|
2299
|
+
const state = {
|
|
2300
|
+
platform: "android",
|
|
2301
|
+
device: deviceName,
|
|
2302
|
+
serial,
|
|
2303
|
+
app: targetApp,
|
|
2304
|
+
port,
|
|
2305
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2306
|
+
};
|
|
2307
|
+
writeState(state);
|
|
2308
|
+
return state;
|
|
2138
2309
|
}
|
|
2139
|
-
var
|
|
2140
|
-
var
|
|
2141
|
-
"src/app/
|
|
2310
|
+
var fs10, path10, import_child_process7, import_url3, __dirname3, DRIVER_PORT2, sleep;
|
|
2311
|
+
var init_sim_service = __esm({
|
|
2312
|
+
"src/app/android/sim-service.ts"() {
|
|
2142
2313
|
"use strict";
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2314
|
+
fs10 = __toESM(require("fs"), 1);
|
|
2315
|
+
path10 = __toESM(require("path"), 1);
|
|
2316
|
+
import_child_process7 = require("child_process");
|
|
2317
|
+
import_url3 = require("url");
|
|
2318
|
+
__dirname3 = path10.dirname((0, import_url3.fileURLToPath)(__import_meta_url));
|
|
2319
|
+
DRIVER_PORT2 = 7779;
|
|
2320
|
+
sleep = (ms) => new Promise((r2) => setTimeout(r2, ms));
|
|
2146
2321
|
}
|
|
2147
2322
|
});
|
|
2148
2323
|
|
|
@@ -2299,11 +2474,11 @@ function createIOSBridge(udid, bundleId, port = DEFAULT_RUNNER_PORT) {
|
|
|
2299
2474
|
const raw = await runnerRequest(port, "/tree");
|
|
2300
2475
|
return convertTree(raw);
|
|
2301
2476
|
},
|
|
2302
|
-
async action(
|
|
2303
|
-
return runnerRequest(port, "/action", { path:
|
|
2477
|
+
async action(path25, actionName) {
|
|
2478
|
+
return runnerRequest(port, "/action", { path: path25, actionName });
|
|
2304
2479
|
},
|
|
2305
|
-
async setValue(
|
|
2306
|
-
return runnerRequest(port, "/set-value", { path:
|
|
2480
|
+
async setValue(path25, value) {
|
|
2481
|
+
return runnerRequest(port, "/set-value", { path: path25, value });
|
|
2307
2482
|
},
|
|
2308
2483
|
async type(text) {
|
|
2309
2484
|
return runnerRequest(port, "/type", { text });
|
|
@@ -2359,35 +2534,35 @@ function resolveStateDir2() {
|
|
|
2359
2534
|
if (localDir) return localDir;
|
|
2360
2535
|
let dir = process.cwd();
|
|
2361
2536
|
for (let i = 0; i < 20; i++) {
|
|
2362
|
-
if (
|
|
2363
|
-
const browseDir =
|
|
2364
|
-
|
|
2537
|
+
if (fs11.existsSync(path11.join(dir, ".git")) || fs11.existsSync(path11.join(dir, ".claude"))) {
|
|
2538
|
+
const browseDir = path11.join(dir, ".browse");
|
|
2539
|
+
fs11.mkdirSync(browseDir, { recursive: true });
|
|
2365
2540
|
return browseDir;
|
|
2366
2541
|
}
|
|
2367
|
-
const parent =
|
|
2542
|
+
const parent = path11.dirname(dir);
|
|
2368
2543
|
if (parent === dir) break;
|
|
2369
2544
|
dir = parent;
|
|
2370
2545
|
}
|
|
2371
2546
|
return "/tmp";
|
|
2372
2547
|
}
|
|
2373
2548
|
function stateFilePath2() {
|
|
2374
|
-
return
|
|
2549
|
+
return path11.join(resolveStateDir2(), "sim-state.json");
|
|
2375
2550
|
}
|
|
2376
2551
|
function readState2() {
|
|
2377
2552
|
try {
|
|
2378
|
-
return JSON.parse(
|
|
2553
|
+
return JSON.parse(fs11.readFileSync(stateFilePath2(), "utf-8"));
|
|
2379
2554
|
} catch {
|
|
2380
2555
|
return null;
|
|
2381
2556
|
}
|
|
2382
2557
|
}
|
|
2383
2558
|
function writeState2(state) {
|
|
2384
|
-
const dir =
|
|
2385
|
-
|
|
2386
|
-
|
|
2559
|
+
const dir = path11.dirname(stateFilePath2());
|
|
2560
|
+
fs11.mkdirSync(dir, { recursive: true });
|
|
2561
|
+
fs11.writeFileSync(stateFilePath2(), JSON.stringify(state, null, 2));
|
|
2387
2562
|
}
|
|
2388
2563
|
function clearState2() {
|
|
2389
2564
|
try {
|
|
2390
|
-
|
|
2565
|
+
fs11.unlinkSync(stateFilePath2());
|
|
2391
2566
|
} catch {
|
|
2392
2567
|
}
|
|
2393
2568
|
}
|
|
@@ -2401,11 +2576,11 @@ async function checkHealth2(port = DEFAULT_PORT) {
|
|
|
2401
2576
|
}
|
|
2402
2577
|
async function configureTarget(port, bundleId, udid) {
|
|
2403
2578
|
if (bundleId !== "io.ulpi.browse-ios-runner") {
|
|
2404
|
-
const { execSync:
|
|
2579
|
+
const { execSync: execSync8 } = await import("child_process");
|
|
2405
2580
|
const deviceId = udid || readState2()?.udid;
|
|
2406
2581
|
if (deviceId) {
|
|
2407
2582
|
try {
|
|
2408
|
-
|
|
2583
|
+
execSync8(`xcrun simctl launch ${deviceId} ${bundleId}`, { stdio: "pipe", timeout: 3e4 });
|
|
2409
2584
|
} catch {
|
|
2410
2585
|
}
|
|
2411
2586
|
}
|
|
@@ -2424,7 +2599,7 @@ async function status2() {
|
|
|
2424
2599
|
return { running: true, state, healthy: true };
|
|
2425
2600
|
}
|
|
2426
2601
|
async function killStaleRunners(port, statePid) {
|
|
2427
|
-
const { execSync:
|
|
2602
|
+
const { execSync: execSync8 } = await import("child_process");
|
|
2428
2603
|
if (statePid) {
|
|
2429
2604
|
try {
|
|
2430
2605
|
process.kill(statePid, "SIGKILL");
|
|
@@ -2432,11 +2607,11 @@ async function killStaleRunners(port, statePid) {
|
|
|
2432
2607
|
}
|
|
2433
2608
|
}
|
|
2434
2609
|
try {
|
|
2435
|
-
|
|
2610
|
+
execSync8('pkill -9 -f "xcodebuild.*BrowseRunnerApp"', { stdio: "pipe", timeout: 5e3 });
|
|
2436
2611
|
} catch {
|
|
2437
2612
|
}
|
|
2438
2613
|
try {
|
|
2439
|
-
const lsof =
|
|
2614
|
+
const lsof = execSync8(`lsof -ti :${port}`, { stdio: "pipe", timeout: 5e3 }).toString().trim();
|
|
2440
2615
|
for (const pid of lsof.split("\n").filter(Boolean)) {
|
|
2441
2616
|
try {
|
|
2442
2617
|
process.kill(parseInt(pid, 10), "SIGKILL");
|
|
@@ -2448,7 +2623,7 @@ async function killStaleRunners(port, statePid) {
|
|
|
2448
2623
|
for (let i = 0; i < 5; i++) {
|
|
2449
2624
|
await sleep2(1e3);
|
|
2450
2625
|
try {
|
|
2451
|
-
|
|
2626
|
+
execSync8(`lsof -ti :${port}`, { stdio: "pipe", timeout: 2e3 });
|
|
2452
2627
|
} catch {
|
|
2453
2628
|
break;
|
|
2454
2629
|
}
|
|
@@ -2465,6 +2640,14 @@ async function startIOS(opts = {}) {
|
|
|
2465
2640
|
const log2 = opts.log || ((msg) => process.stderr.write(`[browse] ${msg}
|
|
2466
2641
|
`));
|
|
2467
2642
|
const port = parseInt(process.env.BROWSE_RUNNER_PORT || "", 10) || DEFAULT_PORT;
|
|
2643
|
+
if (opts.app) {
|
|
2644
|
+
const { isAppFilePath: isAppFilePath2, resolveIOSApp: resolveIOSApp2 } = await Promise.resolve().then(() => (init_resolve_app(), resolve_app_exports));
|
|
2645
|
+
if (isAppFilePath2(opts.app)) {
|
|
2646
|
+
const sim2 = await resolveSimulator(opts.device);
|
|
2647
|
+
if (sim2.state !== "Booted") await bootSimulator(sim2.udid);
|
|
2648
|
+
opts.app = await resolveIOSApp2(opts.app, sim2.udid, log2);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2468
2651
|
const existing = readState2();
|
|
2469
2652
|
if (existing) {
|
|
2470
2653
|
const healthy = await checkHealth2(existing.port);
|
|
@@ -2505,23 +2688,23 @@ async function startIOS(opts = {}) {
|
|
|
2505
2688
|
}
|
|
2506
2689
|
}
|
|
2507
2690
|
const runnerCandidates = [
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2691
|
+
path11.resolve(__dirname_svc, "../../../browse-ios-runner"),
|
|
2692
|
+
path11.resolve(__dirname_svc, "../../browse-ios-runner"),
|
|
2693
|
+
path11.resolve(__dirname_svc, "../bin/browse-ios-runner")
|
|
2511
2694
|
];
|
|
2512
|
-
const runnerDir = runnerCandidates.find((d) =>
|
|
2513
|
-
const { execSync:
|
|
2514
|
-
if (!
|
|
2695
|
+
const runnerDir = runnerCandidates.find((d) => fs11.existsSync(path11.join(d, "project.yml"))) || runnerCandidates[0];
|
|
2696
|
+
const { execSync: execSync8 } = await import("child_process");
|
|
2697
|
+
if (!fs11.existsSync(path11.join(runnerDir, "BrowseRunner.xcodeproj", "project.pbxproj"))) {
|
|
2515
2698
|
log2("Generating Xcode project...");
|
|
2516
|
-
|
|
2699
|
+
execSync8("xcodegen generate --spec project.yml", { cwd: runnerDir, stdio: "pipe" });
|
|
2517
2700
|
}
|
|
2518
2701
|
log2("Building iOS runner...");
|
|
2519
|
-
|
|
2702
|
+
execSync8(
|
|
2520
2703
|
`xcodebuild build-for-testing -project BrowseRunner.xcodeproj -scheme BrowseRunnerApp -sdk iphonesimulator -destination "id=${sim.udid}" -derivedDataPath .build CODE_SIGN_IDENTITY="" CODE_SIGNING_ALLOWED=NO -quiet`,
|
|
2521
2704
|
{ cwd: runnerDir, stdio: "pipe", timeout: 12e4 }
|
|
2522
2705
|
);
|
|
2523
2706
|
log2("Starting runner...");
|
|
2524
|
-
const proc = (0,
|
|
2707
|
+
const proc = (0, import_child_process8.spawn)("xcodebuild", [
|
|
2525
2708
|
"test",
|
|
2526
2709
|
"-project",
|
|
2527
2710
|
"BrowseRunner.xcodeproj",
|
|
@@ -2568,17 +2751,17 @@ async function startIOS(opts = {}) {
|
|
|
2568
2751
|
writeState2(state);
|
|
2569
2752
|
return state;
|
|
2570
2753
|
}
|
|
2571
|
-
var
|
|
2754
|
+
var fs11, path11, import_child_process8, import_url4, __filename_svc, __dirname_svc, DEFAULT_PORT, sleep2;
|
|
2572
2755
|
var init_sim_service2 = __esm({
|
|
2573
2756
|
"src/app/ios/sim-service.ts"() {
|
|
2574
2757
|
"use strict";
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2758
|
+
fs11 = __toESM(require("fs"), 1);
|
|
2759
|
+
path11 = __toESM(require("path"), 1);
|
|
2760
|
+
import_child_process8 = require("child_process");
|
|
2578
2761
|
import_url4 = require("url");
|
|
2579
2762
|
init_controller();
|
|
2580
2763
|
__filename_svc = (0, import_url4.fileURLToPath)(__import_meta_url);
|
|
2581
|
-
__dirname_svc =
|
|
2764
|
+
__dirname_svc = path11.dirname(__filename_svc);
|
|
2582
2765
|
DEFAULT_PORT = 9820;
|
|
2583
2766
|
sleep2 = (ms) => new Promise((r2) => setTimeout(r2, ms));
|
|
2584
2767
|
}
|
|
@@ -2592,11 +2775,12 @@ __export(sim_cli_exports, {
|
|
|
2592
2775
|
async function handleSimCLI(args) {
|
|
2593
2776
|
const sub = args[0];
|
|
2594
2777
|
if (!sub || sub === "--help") {
|
|
2595
|
-
console.log("Usage: browse sim start --platform ios|android [--device <name>] [--app <id>] [--visible]");
|
|
2778
|
+
console.log("Usage: browse sim start --platform ios|android [--device <name>] [--app <id-or-path>] [--visible]");
|
|
2596
2779
|
console.log(" browse sim stop [--platform ios|android]");
|
|
2597
2780
|
console.log(" browse sim status [--platform ios|android]");
|
|
2598
2781
|
console.log("");
|
|
2599
2782
|
console.log("Flags:");
|
|
2783
|
+
console.log(" --app Bundle ID, package name, or path to .app/.ipa/.apk");
|
|
2600
2784
|
console.log(" --visible Open the Simulator window (default: headless/background)");
|
|
2601
2785
|
return;
|
|
2602
2786
|
}
|
|
@@ -2694,22 +2878,22 @@ __export(bridge_exports3, {
|
|
|
2694
2878
|
function resolveBridgePath() {
|
|
2695
2879
|
const candidates = [
|
|
2696
2880
|
// 1. Local dev build
|
|
2697
|
-
|
|
2698
|
-
|
|
2881
|
+
path12.resolve(__dirname_bridge, "../../../browse-ax/.build/release/browse-ax"),
|
|
2882
|
+
path12.resolve(__dirname_bridge, "../../../browse-ax/.build/debug/browse-ax"),
|
|
2699
2883
|
// 2. Installed alongside source (bin/ at project root)
|
|
2700
|
-
|
|
2884
|
+
path12.resolve(__dirname_bridge, "../../bin/browse-ax"),
|
|
2701
2885
|
// 3. Bundled build (dist/browse.cjs → ../bin/)
|
|
2702
|
-
|
|
2886
|
+
path12.resolve(__dirname_bridge, "../bin/browse-ax")
|
|
2703
2887
|
];
|
|
2704
2888
|
for (const p of candidates) {
|
|
2705
|
-
if (
|
|
2889
|
+
if (fs12.existsSync(p)) return p;
|
|
2706
2890
|
}
|
|
2707
|
-
const lazyPath =
|
|
2708
|
-
process.env.BROWSE_LOCAL_DIR ||
|
|
2891
|
+
const lazyPath = path12.join(
|
|
2892
|
+
process.env.BROWSE_LOCAL_DIR || path12.join(process.cwd(), ".browse"),
|
|
2709
2893
|
"bin",
|
|
2710
2894
|
"browse-ax"
|
|
2711
2895
|
);
|
|
2712
|
-
if (
|
|
2896
|
+
if (fs12.existsSync(lazyPath)) return lazyPath;
|
|
2713
2897
|
throw new Error(
|
|
2714
2898
|
"browse-ax binary not found. Run: browse enable macos\nOr build manually: cd browse-ax && swift build -c release"
|
|
2715
2899
|
);
|
|
@@ -2721,8 +2905,8 @@ async function ensureMacOSBridge() {
|
|
|
2721
2905
|
return resolveBridgePath();
|
|
2722
2906
|
}
|
|
2723
2907
|
async function execBridge(bridgePath, args) {
|
|
2724
|
-
return new Promise((
|
|
2725
|
-
const proc = (0,
|
|
2908
|
+
return new Promise((resolve10, reject) => {
|
|
2909
|
+
const proc = (0, import_child_process9.spawn)(bridgePath, args, {
|
|
2726
2910
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2727
2911
|
});
|
|
2728
2912
|
const chunks = [];
|
|
@@ -2742,7 +2926,7 @@ async function execBridge(bridgePath, args) {
|
|
|
2742
2926
|
return;
|
|
2743
2927
|
}
|
|
2744
2928
|
try {
|
|
2745
|
-
|
|
2929
|
+
resolve10(JSON.parse(stdout));
|
|
2746
2930
|
} catch {
|
|
2747
2931
|
reject(new Error(`Invalid bridge output: ${stdout.slice(0, 200)}`));
|
|
2748
2932
|
}
|
|
@@ -2775,16 +2959,16 @@ function createMacOSBridge(bridgePath, pid) {
|
|
|
2775
2959
|
}
|
|
2776
2960
|
};
|
|
2777
2961
|
}
|
|
2778
|
-
var
|
|
2962
|
+
var import_child_process9, path12, fs12, import_url5, __filename_bridge2, __dirname_bridge;
|
|
2779
2963
|
var init_bridge3 = __esm({
|
|
2780
2964
|
"src/app/macos/bridge.ts"() {
|
|
2781
2965
|
"use strict";
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2966
|
+
import_child_process9 = require("child_process");
|
|
2967
|
+
path12 = __toESM(require("path"), 1);
|
|
2968
|
+
fs12 = __toESM(require("fs"), 1);
|
|
2785
2969
|
import_url5 = require("url");
|
|
2786
2970
|
__filename_bridge2 = (0, import_url5.fileURLToPath)(__import_meta_url);
|
|
2787
|
-
__dirname_bridge =
|
|
2971
|
+
__dirname_bridge = path12.dirname(__filename_bridge2);
|
|
2788
2972
|
}
|
|
2789
2973
|
});
|
|
2790
2974
|
|
|
@@ -2794,14 +2978,14 @@ __export(enable_exports, {
|
|
|
2794
2978
|
handleEnable: () => handleEnable
|
|
2795
2979
|
});
|
|
2796
2980
|
function findPath(candidates) {
|
|
2797
|
-
return candidates.find((p) =>
|
|
2981
|
+
return candidates.find((p) => fs13.existsSync(p)) || null;
|
|
2798
2982
|
}
|
|
2799
2983
|
async function enableAndroid() {
|
|
2800
2984
|
log("Enabling Android...");
|
|
2801
2985
|
const prebuiltApk = findPath([
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2986
|
+
path13.resolve(__dirname_enable, "../bin/browse-android.apk"),
|
|
2987
|
+
path13.resolve(__dirname_enable, "../../bin/browse-android.apk"),
|
|
2988
|
+
path13.resolve(__dirname_enable, "browse-android.apk")
|
|
2805
2989
|
]);
|
|
2806
2990
|
if (prebuiltApk) {
|
|
2807
2991
|
log(`Driver APK: ${prebuiltApk}`);
|
|
@@ -2812,7 +2996,7 @@ async function enableAndroid() {
|
|
|
2812
2996
|
const { ensureJavaHome: ensureJavaHome2, findSdkRoot: findSdkRoot2, installSdk: installSdk2, createAvd: createAvd2 } = await Promise.resolve().then(() => (init_emulator(), emulator_exports));
|
|
2813
2997
|
const { installAdb: installAdb2 } = await Promise.resolve().then(() => (init_bridge(), bridge_exports));
|
|
2814
2998
|
try {
|
|
2815
|
-
(0,
|
|
2999
|
+
(0, import_child_process10.execSync)("adb version", { stdio: "ignore", timeout: 5e3 });
|
|
2816
3000
|
log("adb: available");
|
|
2817
3001
|
} catch {
|
|
2818
3002
|
log("Installing adb...");
|
|
@@ -2821,7 +3005,7 @@ async function enableAndroid() {
|
|
|
2821
3005
|
ensureJavaHome2();
|
|
2822
3006
|
const hasJava = () => {
|
|
2823
3007
|
try {
|
|
2824
|
-
(0,
|
|
3008
|
+
(0, import_child_process10.execSync)("java -version", { stdio: "ignore", timeout: 5e3 });
|
|
2825
3009
|
return true;
|
|
2826
3010
|
} catch {
|
|
2827
3011
|
return false;
|
|
@@ -2830,7 +3014,7 @@ async function enableAndroid() {
|
|
|
2830
3014
|
if (!hasJava() && process.platform === "darwin") {
|
|
2831
3015
|
log("Installing Java (JDK 21)...");
|
|
2832
3016
|
try {
|
|
2833
|
-
(0,
|
|
3017
|
+
(0, import_child_process10.execSync)("brew install openjdk@21", { stdio: ["ignore", "pipe", "pipe"], timeout: 3e5 });
|
|
2834
3018
|
} catch {
|
|
2835
3019
|
}
|
|
2836
3020
|
ensureJavaHome2();
|
|
@@ -2842,29 +3026,29 @@ async function enableAndroid() {
|
|
|
2842
3026
|
if (!sdkRoot) throw new Error("Android SDK not found.");
|
|
2843
3027
|
log(`SDK: ${sdkRoot}`);
|
|
2844
3028
|
const sdkMgr = findPath([
|
|
2845
|
-
|
|
2846
|
-
|
|
3029
|
+
path13.join(sdkRoot, "cmdline-tools/latest/bin/sdkmanager"),
|
|
3030
|
+
path13.join(sdkRoot, "bin/sdkmanager")
|
|
2847
3031
|
]);
|
|
2848
3032
|
if (sdkMgr) {
|
|
2849
3033
|
try {
|
|
2850
|
-
(0,
|
|
3034
|
+
(0, import_child_process10.execSync)(`yes | "${sdkMgr}" --licenses`, { stdio: "ignore", timeout: 6e4, shell: "/bin/bash" });
|
|
2851
3035
|
} catch {
|
|
2852
3036
|
}
|
|
2853
3037
|
for (const comp of ["emulator", "platform-tools", "platforms;android-35", "build-tools;35.0.0", "system-images;android-35;google_apis;arm64-v8a"]) {
|
|
2854
3038
|
try {
|
|
2855
|
-
(0,
|
|
3039
|
+
(0, import_child_process10.execSync)(`"${sdkMgr}" --install "${comp}"`, { stdio: "ignore", timeout: 3e5 });
|
|
2856
3040
|
} catch {
|
|
2857
3041
|
}
|
|
2858
3042
|
}
|
|
2859
3043
|
log("SDK components: installed");
|
|
2860
3044
|
}
|
|
2861
3045
|
const avdMgr = findPath([
|
|
2862
|
-
|
|
2863
|
-
|
|
3046
|
+
path13.join(sdkRoot, "cmdline-tools/latest/bin/avdmanager"),
|
|
3047
|
+
path13.join(sdkRoot, "bin/avdmanager")
|
|
2864
3048
|
]);
|
|
2865
3049
|
if (avdMgr) {
|
|
2866
3050
|
try {
|
|
2867
|
-
const avds = (0,
|
|
3051
|
+
const avds = (0, import_child_process10.execSync)(`"${avdMgr}" list avd -c`, { encoding: "utf-8", timeout: 1e4, stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
2868
3052
|
if (!avds.includes("browse_default")) {
|
|
2869
3053
|
createAvd2(sdkRoot, avdMgr, log);
|
|
2870
3054
|
} else {
|
|
@@ -2874,14 +3058,14 @@ async function enableAndroid() {
|
|
|
2874
3058
|
}
|
|
2875
3059
|
}
|
|
2876
3060
|
const driverDir = findPath([
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
].filter((d) =>
|
|
3061
|
+
path13.resolve(__dirname_enable, "../browse-android"),
|
|
3062
|
+
path13.resolve(__dirname_enable, "../../browse-android")
|
|
3063
|
+
].filter((d) => fs13.existsSync(path13.join(d, "gradlew"))));
|
|
2880
3064
|
if (driverDir) {
|
|
2881
3065
|
if (!process.env.ANDROID_HOME) process.env.ANDROID_HOME = sdkRoot;
|
|
2882
3066
|
log("Building Android driver APK...");
|
|
2883
3067
|
try {
|
|
2884
|
-
(0,
|
|
3068
|
+
(0, import_child_process10.execSync)("./gradlew :app:assembleDebug :app:assembleDebugAndroidTest --no-daemon", {
|
|
2885
3069
|
cwd: driverDir,
|
|
2886
3070
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2887
3071
|
timeout: 3e5
|
|
@@ -2897,39 +3081,39 @@ async function enableIOS() {
|
|
|
2897
3081
|
log("Enabling iOS...");
|
|
2898
3082
|
if (process.platform !== "darwin") throw new Error("iOS requires macOS with Xcode.");
|
|
2899
3083
|
try {
|
|
2900
|
-
(0,
|
|
3084
|
+
(0, import_child_process10.execSync)("xcodebuild -version", { stdio: "pipe", timeout: 1e4 });
|
|
2901
3085
|
log("Xcode: available");
|
|
2902
3086
|
} catch {
|
|
2903
3087
|
throw new Error("Xcode not found. Install from the App Store.");
|
|
2904
3088
|
}
|
|
2905
3089
|
try {
|
|
2906
|
-
(0,
|
|
3090
|
+
(0, import_child_process10.execSync)("which xcodegen", { stdio: "pipe", timeout: 5e3 });
|
|
2907
3091
|
} catch {
|
|
2908
3092
|
log("Installing xcodegen...");
|
|
2909
3093
|
try {
|
|
2910
|
-
(0,
|
|
3094
|
+
(0, import_child_process10.execSync)("brew install xcodegen", { stdio: ["ignore", "pipe", "pipe"], timeout: 12e4 });
|
|
2911
3095
|
} catch {
|
|
2912
3096
|
throw new Error("xcodegen not found. Install: brew install xcodegen");
|
|
2913
3097
|
}
|
|
2914
3098
|
}
|
|
2915
3099
|
log("xcodegen: available");
|
|
2916
3100
|
const runnerDir = findPath([
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
].filter((d) =>
|
|
3101
|
+
path13.resolve(__dirname_enable, "../browse-ios-runner"),
|
|
3102
|
+
path13.resolve(__dirname_enable, "../../browse-ios-runner"),
|
|
3103
|
+
path13.resolve(__dirname_enable, "../bin/browse-ios-runner")
|
|
3104
|
+
].filter((d) => fs13.existsSync(path13.join(d, "project.yml"))));
|
|
2921
3105
|
if (!runnerDir) {
|
|
2922
3106
|
throw new Error("iOS runner source not found. Reinstall: npm install -g @ulpi/browse");
|
|
2923
3107
|
}
|
|
2924
|
-
if (!
|
|
3108
|
+
if (!fs13.existsSync(path13.join(runnerDir, "BrowseRunner.xcodeproj", "project.pbxproj"))) {
|
|
2925
3109
|
log("Generating Xcode project...");
|
|
2926
|
-
(0,
|
|
3110
|
+
(0, import_child_process10.execSync)("xcodegen generate --spec project.yml", { cwd: runnerDir, stdio: "pipe" });
|
|
2927
3111
|
}
|
|
2928
3112
|
const { resolveSimulator: resolveSimulator2 } = await Promise.resolve().then(() => (init_controller(), controller_exports));
|
|
2929
3113
|
try {
|
|
2930
3114
|
const sim = await resolveSimulator2();
|
|
2931
3115
|
log("Building iOS runner...");
|
|
2932
|
-
(0,
|
|
3116
|
+
(0, import_child_process10.execSync)(
|
|
2933
3117
|
`xcodebuild build-for-testing -project BrowseRunner.xcodeproj -scheme BrowseRunnerApp -sdk iphonesimulator -destination "id=${sim.udid}" -derivedDataPath .build CODE_SIGN_IDENTITY="" CODE_SIGNING_ALLOWED=NO -quiet`,
|
|
2934
3118
|
{ cwd: runnerDir, stdio: "pipe", timeout: 12e4 }
|
|
2935
3119
|
);
|
|
@@ -2951,12 +3135,12 @@ async function enableMacOS() {
|
|
|
2951
3135
|
} catch {
|
|
2952
3136
|
}
|
|
2953
3137
|
const axDir = findPath([
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
].filter((d) =>
|
|
3138
|
+
path13.resolve(__dirname_enable, "../browse-ax"),
|
|
3139
|
+
path13.resolve(__dirname_enable, "../../browse-ax")
|
|
3140
|
+
].filter((d) => fs13.existsSync(path13.join(d, "Package.swift"))));
|
|
2957
3141
|
if (axDir) {
|
|
2958
3142
|
log("Building browse-ax...");
|
|
2959
|
-
(0,
|
|
3143
|
+
(0, import_child_process10.execSync)("swift build -c release", { cwd: axDir, stdio: ["ignore", "pipe", "pipe"], timeout: 12e4 });
|
|
2960
3144
|
log("browse-ax built.");
|
|
2961
3145
|
} else {
|
|
2962
3146
|
throw new Error("browse-ax not found. Reinstall: npm install -g @ulpi/browse");
|
|
@@ -2996,15 +3180,15 @@ async function handleEnable(args) {
|
|
|
2996
3180
|
console.log("");
|
|
2997
3181
|
}
|
|
2998
3182
|
}
|
|
2999
|
-
var
|
|
3183
|
+
var import_child_process10, fs13, path13, import_url6, __dirname_enable, log;
|
|
3000
3184
|
var init_enable = __esm({
|
|
3001
3185
|
"src/enable.ts"() {
|
|
3002
3186
|
"use strict";
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3187
|
+
import_child_process10 = require("child_process");
|
|
3188
|
+
fs13 = __toESM(require("fs"), 1);
|
|
3189
|
+
path13 = __toESM(require("path"), 1);
|
|
3006
3190
|
import_url6 = require("url");
|
|
3007
|
-
__dirname_enable =
|
|
3191
|
+
__dirname_enable = path13.dirname((0, import_url6.fileURLToPath)(__import_meta_url));
|
|
3008
3192
|
log = (msg) => process.stderr.write(`[browse] ${msg}
|
|
3009
3193
|
`);
|
|
3010
3194
|
}
|
|
@@ -3298,8 +3482,8 @@ async function handleReadCommand(command, args, bm, buffers) {
|
|
|
3298
3482
|
case "eval": {
|
|
3299
3483
|
const filePath = args[0];
|
|
3300
3484
|
if (!filePath) throw new Error("Usage: browse eval <js-file>");
|
|
3301
|
-
if (!
|
|
3302
|
-
const code =
|
|
3485
|
+
if (!fs14.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
|
3486
|
+
const code = fs14.readFileSync(filePath, "utf-8");
|
|
3303
3487
|
const result = await evalCtx.evaluate(code);
|
|
3304
3488
|
return typeof result === "object" ? JSON.stringify(result, null, 2) : String(result ?? "");
|
|
3305
3489
|
}
|
|
@@ -3639,13 +3823,13 @@ function registerReadDefinitions(registry4) {
|
|
|
3639
3823
|
});
|
|
3640
3824
|
}
|
|
3641
3825
|
}
|
|
3642
|
-
var
|
|
3826
|
+
var fs14;
|
|
3643
3827
|
var init_read = __esm({
|
|
3644
3828
|
"src/commands/read.ts"() {
|
|
3645
3829
|
"use strict";
|
|
3646
3830
|
init_emulation();
|
|
3647
3831
|
init_constants();
|
|
3648
|
-
|
|
3832
|
+
fs14 = __toESM(require("fs"), 1);
|
|
3649
3833
|
}
|
|
3650
3834
|
});
|
|
3651
3835
|
|
|
@@ -4292,7 +4476,7 @@ async function handleWriteCommand(command, args, bm, domainFilter) {
|
|
|
4292
4476
|
const elapsed = Date.now() - start2;
|
|
4293
4477
|
return `Request matched: ${match.description} (${elapsed}ms)`;
|
|
4294
4478
|
}
|
|
4295
|
-
await new Promise((
|
|
4479
|
+
await new Promise((resolve10) => setTimeout(resolve10, 100));
|
|
4296
4480
|
}
|
|
4297
4481
|
const finalMatch = matchNetworkRequest2(cond, buffers);
|
|
4298
4482
|
const { method, pattern } = parseRequestValue2(requestPattern);
|
|
@@ -4337,14 +4521,14 @@ async function handleWriteCommand(command, args, bm, domainFilter) {
|
|
|
4337
4521
|
const file = args[1];
|
|
4338
4522
|
if (!file) throw new Error("Usage: browse cookie export <file>");
|
|
4339
4523
|
const cookies = await page.context().cookies();
|
|
4340
|
-
|
|
4524
|
+
fs15.writeFileSync(file, JSON.stringify(cookies, null, 2));
|
|
4341
4525
|
return `Exported ${cookies.length} cookie(s) to ${file}`;
|
|
4342
4526
|
}
|
|
4343
4527
|
if (cookieStr === "import") {
|
|
4344
4528
|
const file = args[1];
|
|
4345
4529
|
if (!file) throw new Error("Usage: browse cookie import <file>");
|
|
4346
|
-
if (!
|
|
4347
|
-
const cookies = JSON.parse(
|
|
4530
|
+
if (!fs15.existsSync(file)) throw new Error(`File not found: ${file}`);
|
|
4531
|
+
const cookies = JSON.parse(fs15.readFileSync(file, "utf-8"));
|
|
4348
4532
|
if (!Array.isArray(cookies)) throw new Error("Cookie file must contain a JSON array of cookie objects");
|
|
4349
4533
|
await page.context().addCookies(cookies);
|
|
4350
4534
|
return `Imported ${cookies.length} cookie(s) from ${file}`;
|
|
@@ -4411,7 +4595,7 @@ Note: Cookies and tab URLs preserved. localStorage/sessionStorage were reset (Pl
|
|
|
4411
4595
|
const [selector, ...filePaths] = args;
|
|
4412
4596
|
if (!selector || filePaths.length === 0) throw new Error("Usage: browse upload <selector> <file1> [file2] ...");
|
|
4413
4597
|
for (const fp of filePaths) {
|
|
4414
|
-
if (!
|
|
4598
|
+
if (!fs15.existsSync(fp)) throw new Error(`File not found: ${fp}`);
|
|
4415
4599
|
}
|
|
4416
4600
|
const resolved = bm.resolveRef(selector);
|
|
4417
4601
|
if ("locator" in resolved) {
|
|
@@ -4871,13 +5055,13 @@ function registerWriteDefinitions(registry4) {
|
|
|
4871
5055
|
});
|
|
4872
5056
|
}
|
|
4873
5057
|
}
|
|
4874
|
-
var
|
|
5058
|
+
var fs15;
|
|
4875
5059
|
var init_write = __esm({
|
|
4876
5060
|
"src/commands/write.ts"() {
|
|
4877
5061
|
"use strict";
|
|
4878
5062
|
init_emulation();
|
|
4879
5063
|
init_constants();
|
|
4880
|
-
|
|
5064
|
+
fs15 = __toESM(require("fs"), 1);
|
|
4881
5065
|
}
|
|
4882
5066
|
});
|
|
4883
5067
|
|
|
@@ -5234,9 +5418,9 @@ ${legend.join("\n")}`;
|
|
|
5234
5418
|
try {
|
|
5235
5419
|
for (const vp of viewports) {
|
|
5236
5420
|
await page.setViewportSize({ width: vp.width, height: vp.height });
|
|
5237
|
-
const
|
|
5238
|
-
await page.screenshot({ path:
|
|
5239
|
-
results.push(`${vp.name} (${vp.width}x${vp.height}): ${
|
|
5421
|
+
const path25 = `${prefix}-${vp.name}.png`;
|
|
5422
|
+
await page.screenshot({ path: path25, fullPage: true });
|
|
5423
|
+
results.push(`${vp.name} (${vp.width}x${vp.height}): ${path25}`);
|
|
5240
5424
|
}
|
|
5241
5425
|
} finally {
|
|
5242
5426
|
if (originalViewport) {
|
|
@@ -5251,13 +5435,13 @@ ${legend.join("\n")}`;
|
|
|
5251
5435
|
const diffArgs = args.filter((a) => a !== "--full");
|
|
5252
5436
|
const baseline = diffArgs[0];
|
|
5253
5437
|
if (!baseline) throw new Error("Usage: browse screenshot-diff <baseline> [current] [--threshold 0.1] [--full]");
|
|
5254
|
-
if (!
|
|
5438
|
+
if (!fs16.existsSync(baseline)) throw new Error(`Baseline file not found: ${baseline}`);
|
|
5255
5439
|
let thresholdPct = 0.1;
|
|
5256
5440
|
const threshIdx = diffArgs.indexOf("--threshold");
|
|
5257
5441
|
if (threshIdx !== -1 && diffArgs[threshIdx + 1]) {
|
|
5258
5442
|
thresholdPct = parseFloat(diffArgs[threshIdx + 1]);
|
|
5259
5443
|
}
|
|
5260
|
-
const baselineBuffer =
|
|
5444
|
+
const baselineBuffer = fs16.readFileSync(baseline);
|
|
5261
5445
|
let currentBuffer;
|
|
5262
5446
|
let currentPath;
|
|
5263
5447
|
for (let i = 1; i < diffArgs.length; i++) {
|
|
@@ -5271,8 +5455,8 @@ ${legend.join("\n")}`;
|
|
|
5271
5455
|
}
|
|
5272
5456
|
}
|
|
5273
5457
|
if (currentPath) {
|
|
5274
|
-
if (!
|
|
5275
|
-
currentBuffer =
|
|
5458
|
+
if (!fs16.existsSync(currentPath)) throw new Error(`Current screenshot not found: ${currentPath}`);
|
|
5459
|
+
currentBuffer = fs16.readFileSync(currentPath);
|
|
5276
5460
|
} else {
|
|
5277
5461
|
const page = bm.getPage();
|
|
5278
5462
|
currentBuffer = await page.screenshot({ fullPage: isFullPageDiff });
|
|
@@ -5282,7 +5466,7 @@ ${legend.join("\n")}`;
|
|
|
5282
5466
|
const extIdx = baseline.lastIndexOf(".");
|
|
5283
5467
|
const diffPath = extIdx > 0 ? baseline.slice(0, extIdx) + "-diff" + baseline.slice(extIdx) : baseline + "-diff.png";
|
|
5284
5468
|
if (!result.passed && result.diffImage) {
|
|
5285
|
-
|
|
5469
|
+
fs16.writeFileSync(diffPath, result.diffImage);
|
|
5286
5470
|
}
|
|
5287
5471
|
return [
|
|
5288
5472
|
`Pixels: ${result.totalPixels}`,
|
|
@@ -5297,11 +5481,11 @@ ${legend.join("\n")}`;
|
|
|
5297
5481
|
throw new Error(`Unknown screenshots command: ${command}`);
|
|
5298
5482
|
}
|
|
5299
5483
|
}
|
|
5300
|
-
var
|
|
5484
|
+
var fs16, LOCAL_DIR;
|
|
5301
5485
|
var init_screenshots = __esm({
|
|
5302
5486
|
"src/commands/meta/screenshots.ts"() {
|
|
5303
5487
|
"use strict";
|
|
5304
|
-
|
|
5488
|
+
fs16 = __toESM(require("fs"), 1);
|
|
5305
5489
|
LOCAL_DIR = process.env.BROWSE_LOCAL_DIR || "/tmp";
|
|
5306
5490
|
}
|
|
5307
5491
|
});
|
|
@@ -5383,17 +5567,17 @@ var require_visit = __commonJS({
|
|
|
5383
5567
|
visit.BREAK = BREAK;
|
|
5384
5568
|
visit.SKIP = SKIP;
|
|
5385
5569
|
visit.REMOVE = REMOVE;
|
|
5386
|
-
function visit_(key, node, visitor,
|
|
5387
|
-
const ctrl = callVisitor(key, node, visitor,
|
|
5570
|
+
function visit_(key, node, visitor, path25) {
|
|
5571
|
+
const ctrl = callVisitor(key, node, visitor, path25);
|
|
5388
5572
|
if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
|
|
5389
|
-
replaceNode(key,
|
|
5390
|
-
return visit_(key, ctrl, visitor,
|
|
5573
|
+
replaceNode(key, path25, ctrl);
|
|
5574
|
+
return visit_(key, ctrl, visitor, path25);
|
|
5391
5575
|
}
|
|
5392
5576
|
if (typeof ctrl !== "symbol") {
|
|
5393
5577
|
if (identity.isCollection(node)) {
|
|
5394
|
-
|
|
5578
|
+
path25 = Object.freeze(path25.concat(node));
|
|
5395
5579
|
for (let i = 0; i < node.items.length; ++i) {
|
|
5396
|
-
const ci = visit_(i, node.items[i], visitor,
|
|
5580
|
+
const ci = visit_(i, node.items[i], visitor, path25);
|
|
5397
5581
|
if (typeof ci === "number")
|
|
5398
5582
|
i = ci - 1;
|
|
5399
5583
|
else if (ci === BREAK)
|
|
@@ -5404,13 +5588,13 @@ var require_visit = __commonJS({
|
|
|
5404
5588
|
}
|
|
5405
5589
|
}
|
|
5406
5590
|
} else if (identity.isPair(node)) {
|
|
5407
|
-
|
|
5408
|
-
const ck = visit_("key", node.key, visitor,
|
|
5591
|
+
path25 = Object.freeze(path25.concat(node));
|
|
5592
|
+
const ck = visit_("key", node.key, visitor, path25);
|
|
5409
5593
|
if (ck === BREAK)
|
|
5410
5594
|
return BREAK;
|
|
5411
5595
|
else if (ck === REMOVE)
|
|
5412
5596
|
node.key = null;
|
|
5413
|
-
const cv = visit_("value", node.value, visitor,
|
|
5597
|
+
const cv = visit_("value", node.value, visitor, path25);
|
|
5414
5598
|
if (cv === BREAK)
|
|
5415
5599
|
return BREAK;
|
|
5416
5600
|
else if (cv === REMOVE)
|
|
@@ -5431,17 +5615,17 @@ var require_visit = __commonJS({
|
|
|
5431
5615
|
visitAsync.BREAK = BREAK;
|
|
5432
5616
|
visitAsync.SKIP = SKIP;
|
|
5433
5617
|
visitAsync.REMOVE = REMOVE;
|
|
5434
|
-
async function visitAsync_(key, node, visitor,
|
|
5435
|
-
const ctrl = await callVisitor(key, node, visitor,
|
|
5618
|
+
async function visitAsync_(key, node, visitor, path25) {
|
|
5619
|
+
const ctrl = await callVisitor(key, node, visitor, path25);
|
|
5436
5620
|
if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
|
|
5437
|
-
replaceNode(key,
|
|
5438
|
-
return visitAsync_(key, ctrl, visitor,
|
|
5621
|
+
replaceNode(key, path25, ctrl);
|
|
5622
|
+
return visitAsync_(key, ctrl, visitor, path25);
|
|
5439
5623
|
}
|
|
5440
5624
|
if (typeof ctrl !== "symbol") {
|
|
5441
5625
|
if (identity.isCollection(node)) {
|
|
5442
|
-
|
|
5626
|
+
path25 = Object.freeze(path25.concat(node));
|
|
5443
5627
|
for (let i = 0; i < node.items.length; ++i) {
|
|
5444
|
-
const ci = await visitAsync_(i, node.items[i], visitor,
|
|
5628
|
+
const ci = await visitAsync_(i, node.items[i], visitor, path25);
|
|
5445
5629
|
if (typeof ci === "number")
|
|
5446
5630
|
i = ci - 1;
|
|
5447
5631
|
else if (ci === BREAK)
|
|
@@ -5452,13 +5636,13 @@ var require_visit = __commonJS({
|
|
|
5452
5636
|
}
|
|
5453
5637
|
}
|
|
5454
5638
|
} else if (identity.isPair(node)) {
|
|
5455
|
-
|
|
5456
|
-
const ck = await visitAsync_("key", node.key, visitor,
|
|
5639
|
+
path25 = Object.freeze(path25.concat(node));
|
|
5640
|
+
const ck = await visitAsync_("key", node.key, visitor, path25);
|
|
5457
5641
|
if (ck === BREAK)
|
|
5458
5642
|
return BREAK;
|
|
5459
5643
|
else if (ck === REMOVE)
|
|
5460
5644
|
node.key = null;
|
|
5461
|
-
const cv = await visitAsync_("value", node.value, visitor,
|
|
5645
|
+
const cv = await visitAsync_("value", node.value, visitor, path25);
|
|
5462
5646
|
if (cv === BREAK)
|
|
5463
5647
|
return BREAK;
|
|
5464
5648
|
else if (cv === REMOVE)
|
|
@@ -5485,23 +5669,23 @@ var require_visit = __commonJS({
|
|
|
5485
5669
|
}
|
|
5486
5670
|
return visitor;
|
|
5487
5671
|
}
|
|
5488
|
-
function callVisitor(key, node, visitor,
|
|
5672
|
+
function callVisitor(key, node, visitor, path25) {
|
|
5489
5673
|
if (typeof visitor === "function")
|
|
5490
|
-
return visitor(key, node,
|
|
5674
|
+
return visitor(key, node, path25);
|
|
5491
5675
|
if (identity.isMap(node))
|
|
5492
|
-
return visitor.Map?.(key, node,
|
|
5676
|
+
return visitor.Map?.(key, node, path25);
|
|
5493
5677
|
if (identity.isSeq(node))
|
|
5494
|
-
return visitor.Seq?.(key, node,
|
|
5678
|
+
return visitor.Seq?.(key, node, path25);
|
|
5495
5679
|
if (identity.isPair(node))
|
|
5496
|
-
return visitor.Pair?.(key, node,
|
|
5680
|
+
return visitor.Pair?.(key, node, path25);
|
|
5497
5681
|
if (identity.isScalar(node))
|
|
5498
|
-
return visitor.Scalar?.(key, node,
|
|
5682
|
+
return visitor.Scalar?.(key, node, path25);
|
|
5499
5683
|
if (identity.isAlias(node))
|
|
5500
|
-
return visitor.Alias?.(key, node,
|
|
5684
|
+
return visitor.Alias?.(key, node, path25);
|
|
5501
5685
|
return void 0;
|
|
5502
5686
|
}
|
|
5503
|
-
function replaceNode(key,
|
|
5504
|
-
const parent =
|
|
5687
|
+
function replaceNode(key, path25, node) {
|
|
5688
|
+
const parent = path25[path25.length - 1];
|
|
5505
5689
|
if (identity.isCollection(parent)) {
|
|
5506
5690
|
parent.items[key] = node;
|
|
5507
5691
|
} else if (identity.isPair(parent)) {
|
|
@@ -6109,10 +6293,10 @@ var require_Collection = __commonJS({
|
|
|
6109
6293
|
var createNode = require_createNode();
|
|
6110
6294
|
var identity = require_identity();
|
|
6111
6295
|
var Node = require_Node();
|
|
6112
|
-
function collectionFromPath(schema,
|
|
6296
|
+
function collectionFromPath(schema, path25, value) {
|
|
6113
6297
|
let v = value;
|
|
6114
|
-
for (let i =
|
|
6115
|
-
const k =
|
|
6298
|
+
for (let i = path25.length - 1; i >= 0; --i) {
|
|
6299
|
+
const k = path25[i];
|
|
6116
6300
|
if (typeof k === "number" && Number.isInteger(k) && k >= 0) {
|
|
6117
6301
|
const a = [];
|
|
6118
6302
|
a[k] = v;
|
|
@@ -6131,7 +6315,7 @@ var require_Collection = __commonJS({
|
|
|
6131
6315
|
sourceObjects: /* @__PURE__ */ new Map()
|
|
6132
6316
|
});
|
|
6133
6317
|
}
|
|
6134
|
-
var isEmptyPath = (
|
|
6318
|
+
var isEmptyPath = (path25) => path25 == null || typeof path25 === "object" && !!path25[Symbol.iterator]().next().done;
|
|
6135
6319
|
var Collection = class extends Node.NodeBase {
|
|
6136
6320
|
constructor(type, schema) {
|
|
6137
6321
|
super(type);
|
|
@@ -6161,11 +6345,11 @@ var require_Collection = __commonJS({
|
|
|
6161
6345
|
* be a Pair instance or a `{ key, value }` object, which may not have a key
|
|
6162
6346
|
* that already exists in the map.
|
|
6163
6347
|
*/
|
|
6164
|
-
addIn(
|
|
6165
|
-
if (isEmptyPath(
|
|
6348
|
+
addIn(path25, value) {
|
|
6349
|
+
if (isEmptyPath(path25))
|
|
6166
6350
|
this.add(value);
|
|
6167
6351
|
else {
|
|
6168
|
-
const [key, ...rest] =
|
|
6352
|
+
const [key, ...rest] = path25;
|
|
6169
6353
|
const node = this.get(key, true);
|
|
6170
6354
|
if (identity.isCollection(node))
|
|
6171
6355
|
node.addIn(rest, value);
|
|
@@ -6179,8 +6363,8 @@ var require_Collection = __commonJS({
|
|
|
6179
6363
|
* Removes a value from the collection.
|
|
6180
6364
|
* @returns `true` if the item was found and removed.
|
|
6181
6365
|
*/
|
|
6182
|
-
deleteIn(
|
|
6183
|
-
const [key, ...rest] =
|
|
6366
|
+
deleteIn(path25) {
|
|
6367
|
+
const [key, ...rest] = path25;
|
|
6184
6368
|
if (rest.length === 0)
|
|
6185
6369
|
return this.delete(key);
|
|
6186
6370
|
const node = this.get(key, true);
|
|
@@ -6194,8 +6378,8 @@ var require_Collection = __commonJS({
|
|
|
6194
6378
|
* scalar values from their surrounding node; to disable set `keepScalar` to
|
|
6195
6379
|
* `true` (collections are always returned intact).
|
|
6196
6380
|
*/
|
|
6197
|
-
getIn(
|
|
6198
|
-
const [key, ...rest] =
|
|
6381
|
+
getIn(path25, keepScalar) {
|
|
6382
|
+
const [key, ...rest] = path25;
|
|
6199
6383
|
const node = this.get(key, true);
|
|
6200
6384
|
if (rest.length === 0)
|
|
6201
6385
|
return !keepScalar && identity.isScalar(node) ? node.value : node;
|
|
@@ -6213,8 +6397,8 @@ var require_Collection = __commonJS({
|
|
|
6213
6397
|
/**
|
|
6214
6398
|
* Checks if the collection includes a value with the key `key`.
|
|
6215
6399
|
*/
|
|
6216
|
-
hasIn(
|
|
6217
|
-
const [key, ...rest] =
|
|
6400
|
+
hasIn(path25) {
|
|
6401
|
+
const [key, ...rest] = path25;
|
|
6218
6402
|
if (rest.length === 0)
|
|
6219
6403
|
return this.has(key);
|
|
6220
6404
|
const node = this.get(key, true);
|
|
@@ -6224,8 +6408,8 @@ var require_Collection = __commonJS({
|
|
|
6224
6408
|
* Sets a value in this collection. For `!!set`, `value` needs to be a
|
|
6225
6409
|
* boolean to add/remove the item from the set.
|
|
6226
6410
|
*/
|
|
6227
|
-
setIn(
|
|
6228
|
-
const [key, ...rest] =
|
|
6411
|
+
setIn(path25, value) {
|
|
6412
|
+
const [key, ...rest] = path25;
|
|
6229
6413
|
if (rest.length === 0) {
|
|
6230
6414
|
this.set(key, value);
|
|
6231
6415
|
} else {
|
|
@@ -8737,9 +8921,9 @@ var require_Document = __commonJS({
|
|
|
8737
8921
|
this.contents.add(value);
|
|
8738
8922
|
}
|
|
8739
8923
|
/** Adds a value to the document. */
|
|
8740
|
-
addIn(
|
|
8924
|
+
addIn(path25, value) {
|
|
8741
8925
|
if (assertCollection(this.contents))
|
|
8742
|
-
this.contents.addIn(
|
|
8926
|
+
this.contents.addIn(path25, value);
|
|
8743
8927
|
}
|
|
8744
8928
|
/**
|
|
8745
8929
|
* Create a new `Alias` node, ensuring that the target `node` has the required anchor.
|
|
@@ -8814,14 +8998,14 @@ var require_Document = __commonJS({
|
|
|
8814
8998
|
* Removes a value from the document.
|
|
8815
8999
|
* @returns `true` if the item was found and removed.
|
|
8816
9000
|
*/
|
|
8817
|
-
deleteIn(
|
|
8818
|
-
if (Collection.isEmptyPath(
|
|
9001
|
+
deleteIn(path25) {
|
|
9002
|
+
if (Collection.isEmptyPath(path25)) {
|
|
8819
9003
|
if (this.contents == null)
|
|
8820
9004
|
return false;
|
|
8821
9005
|
this.contents = null;
|
|
8822
9006
|
return true;
|
|
8823
9007
|
}
|
|
8824
|
-
return assertCollection(this.contents) ? this.contents.deleteIn(
|
|
9008
|
+
return assertCollection(this.contents) ? this.contents.deleteIn(path25) : false;
|
|
8825
9009
|
}
|
|
8826
9010
|
/**
|
|
8827
9011
|
* Returns item at `key`, or `undefined` if not found. By default unwraps
|
|
@@ -8836,10 +9020,10 @@ var require_Document = __commonJS({
|
|
|
8836
9020
|
* scalar values from their surrounding node; to disable set `keepScalar` to
|
|
8837
9021
|
* `true` (collections are always returned intact).
|
|
8838
9022
|
*/
|
|
8839
|
-
getIn(
|
|
8840
|
-
if (Collection.isEmptyPath(
|
|
9023
|
+
getIn(path25, keepScalar) {
|
|
9024
|
+
if (Collection.isEmptyPath(path25))
|
|
8841
9025
|
return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents;
|
|
8842
|
-
return identity.isCollection(this.contents) ? this.contents.getIn(
|
|
9026
|
+
return identity.isCollection(this.contents) ? this.contents.getIn(path25, keepScalar) : void 0;
|
|
8843
9027
|
}
|
|
8844
9028
|
/**
|
|
8845
9029
|
* Checks if the document includes a value with the key `key`.
|
|
@@ -8850,10 +9034,10 @@ var require_Document = __commonJS({
|
|
|
8850
9034
|
/**
|
|
8851
9035
|
* Checks if the document includes a value at `path`.
|
|
8852
9036
|
*/
|
|
8853
|
-
hasIn(
|
|
8854
|
-
if (Collection.isEmptyPath(
|
|
9037
|
+
hasIn(path25) {
|
|
9038
|
+
if (Collection.isEmptyPath(path25))
|
|
8855
9039
|
return this.contents !== void 0;
|
|
8856
|
-
return identity.isCollection(this.contents) ? this.contents.hasIn(
|
|
9040
|
+
return identity.isCollection(this.contents) ? this.contents.hasIn(path25) : false;
|
|
8857
9041
|
}
|
|
8858
9042
|
/**
|
|
8859
9043
|
* Sets a value in this document. For `!!set`, `value` needs to be a
|
|
@@ -8870,13 +9054,13 @@ var require_Document = __commonJS({
|
|
|
8870
9054
|
* Sets a value in this document. For `!!set`, `value` needs to be a
|
|
8871
9055
|
* boolean to add/remove the item from the set.
|
|
8872
9056
|
*/
|
|
8873
|
-
setIn(
|
|
8874
|
-
if (Collection.isEmptyPath(
|
|
9057
|
+
setIn(path25, value) {
|
|
9058
|
+
if (Collection.isEmptyPath(path25)) {
|
|
8875
9059
|
this.contents = value;
|
|
8876
9060
|
} else if (this.contents == null) {
|
|
8877
|
-
this.contents = Collection.collectionFromPath(this.schema, Array.from(
|
|
9061
|
+
this.contents = Collection.collectionFromPath(this.schema, Array.from(path25), value);
|
|
8878
9062
|
} else if (assertCollection(this.contents)) {
|
|
8879
|
-
this.contents.setIn(
|
|
9063
|
+
this.contents.setIn(path25, value);
|
|
8880
9064
|
}
|
|
8881
9065
|
}
|
|
8882
9066
|
/**
|
|
@@ -10833,9 +11017,9 @@ var require_cst_visit = __commonJS({
|
|
|
10833
11017
|
visit.BREAK = BREAK;
|
|
10834
11018
|
visit.SKIP = SKIP;
|
|
10835
11019
|
visit.REMOVE = REMOVE;
|
|
10836
|
-
visit.itemAtPath = (cst,
|
|
11020
|
+
visit.itemAtPath = (cst, path25) => {
|
|
10837
11021
|
let item = cst;
|
|
10838
|
-
for (const [field, index] of
|
|
11022
|
+
for (const [field, index] of path25) {
|
|
10839
11023
|
const tok = item?.[field];
|
|
10840
11024
|
if (tok && "items" in tok) {
|
|
10841
11025
|
item = tok.items[index];
|
|
@@ -10844,23 +11028,23 @@ var require_cst_visit = __commonJS({
|
|
|
10844
11028
|
}
|
|
10845
11029
|
return item;
|
|
10846
11030
|
};
|
|
10847
|
-
visit.parentCollection = (cst,
|
|
10848
|
-
const parent = visit.itemAtPath(cst,
|
|
10849
|
-
const field =
|
|
11031
|
+
visit.parentCollection = (cst, path25) => {
|
|
11032
|
+
const parent = visit.itemAtPath(cst, path25.slice(0, -1));
|
|
11033
|
+
const field = path25[path25.length - 1][0];
|
|
10850
11034
|
const coll = parent?.[field];
|
|
10851
11035
|
if (coll && "items" in coll)
|
|
10852
11036
|
return coll;
|
|
10853
11037
|
throw new Error("Parent collection not found");
|
|
10854
11038
|
};
|
|
10855
|
-
function _visit(
|
|
10856
|
-
let ctrl = visitor(item,
|
|
11039
|
+
function _visit(path25, item, visitor) {
|
|
11040
|
+
let ctrl = visitor(item, path25);
|
|
10857
11041
|
if (typeof ctrl === "symbol")
|
|
10858
11042
|
return ctrl;
|
|
10859
11043
|
for (const field of ["key", "value"]) {
|
|
10860
11044
|
const token = item[field];
|
|
10861
11045
|
if (token && "items" in token) {
|
|
10862
11046
|
for (let i = 0; i < token.items.length; ++i) {
|
|
10863
|
-
const ci = _visit(Object.freeze(
|
|
11047
|
+
const ci = _visit(Object.freeze(path25.concat([[field, i]])), token.items[i], visitor);
|
|
10864
11048
|
if (typeof ci === "number")
|
|
10865
11049
|
i = ci - 1;
|
|
10866
11050
|
else if (ci === BREAK)
|
|
@@ -10871,10 +11055,10 @@ var require_cst_visit = __commonJS({
|
|
|
10871
11055
|
}
|
|
10872
11056
|
}
|
|
10873
11057
|
if (typeof ctrl === "function" && field === "key")
|
|
10874
|
-
ctrl = ctrl(item,
|
|
11058
|
+
ctrl = ctrl(item, path25);
|
|
10875
11059
|
}
|
|
10876
11060
|
}
|
|
10877
|
-
return typeof ctrl === "function" ? ctrl(item,
|
|
11061
|
+
return typeof ctrl === "function" ? ctrl(item, path25) : ctrl;
|
|
10878
11062
|
}
|
|
10879
11063
|
exports2.visit = visit;
|
|
10880
11064
|
}
|
|
@@ -12159,14 +12343,14 @@ var require_parser = __commonJS({
|
|
|
12159
12343
|
case "scalar":
|
|
12160
12344
|
case "single-quoted-scalar":
|
|
12161
12345
|
case "double-quoted-scalar": {
|
|
12162
|
-
const
|
|
12346
|
+
const fs30 = this.flowScalar(this.type);
|
|
12163
12347
|
if (atNextItem || it.value) {
|
|
12164
|
-
map.items.push({ start: start2, key:
|
|
12348
|
+
map.items.push({ start: start2, key: fs30, sep: [] });
|
|
12165
12349
|
this.onKeyLine = true;
|
|
12166
12350
|
} else if (it.sep) {
|
|
12167
|
-
this.stack.push(
|
|
12351
|
+
this.stack.push(fs30);
|
|
12168
12352
|
} else {
|
|
12169
|
-
Object.assign(it, { key:
|
|
12353
|
+
Object.assign(it, { key: fs30, sep: [] });
|
|
12170
12354
|
this.onKeyLine = true;
|
|
12171
12355
|
}
|
|
12172
12356
|
return;
|
|
@@ -12294,13 +12478,13 @@ var require_parser = __commonJS({
|
|
|
12294
12478
|
case "scalar":
|
|
12295
12479
|
case "single-quoted-scalar":
|
|
12296
12480
|
case "double-quoted-scalar": {
|
|
12297
|
-
const
|
|
12481
|
+
const fs30 = this.flowScalar(this.type);
|
|
12298
12482
|
if (!it || it.value)
|
|
12299
|
-
fc.items.push({ start: [], key:
|
|
12483
|
+
fc.items.push({ start: [], key: fs30, sep: [] });
|
|
12300
12484
|
else if (it.sep)
|
|
12301
|
-
this.stack.push(
|
|
12485
|
+
this.stack.push(fs30);
|
|
12302
12486
|
else
|
|
12303
|
-
Object.assign(it, { key:
|
|
12487
|
+
Object.assign(it, { key: fs30, sep: [] });
|
|
12304
12488
|
return;
|
|
12305
12489
|
}
|
|
12306
12490
|
case "flow-map-end":
|
|
@@ -13067,7 +13251,7 @@ async function handleRecordingCommand(command, args, target, currentSession) {
|
|
|
13067
13251
|
throw new Error(`Unknown format: ${format}. Use "browse" (chain JSON), "replay" (Puppeteer), "playwright" (Playwright Test), or "flow" (YAML).`);
|
|
13068
13252
|
}
|
|
13069
13253
|
if (filePath) {
|
|
13070
|
-
|
|
13254
|
+
fs17.writeFileSync(filePath, output);
|
|
13071
13255
|
return `Exported ${steps.length} steps as ${format}: ${filePath}`;
|
|
13072
13256
|
}
|
|
13073
13257
|
return output;
|
|
@@ -13092,7 +13276,7 @@ async function handleRecordingCommand(command, args, target, currentSession) {
|
|
|
13092
13276
|
const { formatAsHar: formatAsHar2 } = await Promise.resolve().then(() => (init_har(), har_exports));
|
|
13093
13277
|
const har = formatAsHar2(sessionBuffers.networkBuffer, recording.startTime);
|
|
13094
13278
|
const harPath = args[1] || (currentSession ? `${currentSession.outputDir}/recording.har` : `${LOCAL_DIR2}/browse-recording.har`);
|
|
13095
|
-
|
|
13279
|
+
fs17.writeFileSync(harPath, JSON.stringify(har, null, 2));
|
|
13096
13280
|
const entryCount = har.log.entries.length;
|
|
13097
13281
|
return `HAR saved: ${harPath} (${entryCount} entries)`;
|
|
13098
13282
|
}
|
|
@@ -13128,11 +13312,11 @@ async function handleRecordingCommand(command, args, target, currentSession) {
|
|
|
13128
13312
|
throw new Error(`Unknown recording command: ${command}`);
|
|
13129
13313
|
}
|
|
13130
13314
|
}
|
|
13131
|
-
var
|
|
13315
|
+
var fs17, LOCAL_DIR2;
|
|
13132
13316
|
var init_recording = __esm({
|
|
13133
13317
|
"src/commands/meta/recording.ts"() {
|
|
13134
13318
|
"use strict";
|
|
13135
|
-
|
|
13319
|
+
fs17 = __toESM(require("fs"), 1);
|
|
13136
13320
|
LOCAL_DIR2 = process.env.BROWSE_LOCAL_DIR || "/tmp";
|
|
13137
13321
|
}
|
|
13138
13322
|
});
|
|
@@ -13156,20 +13340,20 @@ async function saveSessionState(sessionDir, context, encryptionKey) {
|
|
|
13156
13340
|
} else {
|
|
13157
13341
|
content = json;
|
|
13158
13342
|
}
|
|
13159
|
-
|
|
13160
|
-
|
|
13343
|
+
fs18.mkdirSync(sessionDir, { recursive: true });
|
|
13344
|
+
fs18.writeFileSync(path14.join(sessionDir, STATE_FILENAME), content, { mode: 384 });
|
|
13161
13345
|
} catch (err) {
|
|
13162
13346
|
console.log(`[session-persist] Warning: failed to save state: ${err.message}`);
|
|
13163
13347
|
}
|
|
13164
13348
|
}
|
|
13165
13349
|
async function loadSessionState(sessionDir, context, encryptionKey) {
|
|
13166
|
-
const statePath =
|
|
13167
|
-
if (!
|
|
13350
|
+
const statePath = path14.join(sessionDir, STATE_FILENAME);
|
|
13351
|
+
if (!fs18.existsSync(statePath)) {
|
|
13168
13352
|
return false;
|
|
13169
13353
|
}
|
|
13170
13354
|
let stateData;
|
|
13171
13355
|
try {
|
|
13172
|
-
const raw =
|
|
13356
|
+
const raw = fs18.readFileSync(statePath, "utf-8");
|
|
13173
13357
|
const parsed = JSON.parse(raw);
|
|
13174
13358
|
if (parsed.encrypted) {
|
|
13175
13359
|
if (!encryptionKey) {
|
|
@@ -13225,24 +13409,24 @@ async function loadSessionState(sessionDir, context, encryptionKey) {
|
|
|
13225
13409
|
}
|
|
13226
13410
|
}
|
|
13227
13411
|
function hasPersistedState(sessionDir) {
|
|
13228
|
-
return
|
|
13412
|
+
return fs18.existsSync(path14.join(sessionDir, STATE_FILENAME));
|
|
13229
13413
|
}
|
|
13230
13414
|
function cleanOldStates(localDir, maxAgeDays) {
|
|
13231
13415
|
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1e3;
|
|
13232
13416
|
const now = Date.now();
|
|
13233
13417
|
let deleted = 0;
|
|
13234
|
-
const statesDir =
|
|
13235
|
-
if (
|
|
13418
|
+
const statesDir = path14.join(localDir, "states");
|
|
13419
|
+
if (fs18.existsSync(statesDir)) {
|
|
13236
13420
|
try {
|
|
13237
|
-
const entries =
|
|
13421
|
+
const entries = fs18.readdirSync(statesDir);
|
|
13238
13422
|
for (const entry of entries) {
|
|
13239
13423
|
if (!entry.endsWith(".json")) continue;
|
|
13240
|
-
const filePath =
|
|
13424
|
+
const filePath = path14.join(statesDir, entry);
|
|
13241
13425
|
try {
|
|
13242
|
-
const stat =
|
|
13426
|
+
const stat = fs18.statSync(filePath);
|
|
13243
13427
|
if (!stat.isFile()) continue;
|
|
13244
13428
|
if (now - stat.mtimeMs > maxAgeMs) {
|
|
13245
|
-
|
|
13429
|
+
fs18.unlinkSync(filePath);
|
|
13246
13430
|
deleted++;
|
|
13247
13431
|
}
|
|
13248
13432
|
} catch (_) {
|
|
@@ -13251,23 +13435,23 @@ function cleanOldStates(localDir, maxAgeDays) {
|
|
|
13251
13435
|
} catch (_) {
|
|
13252
13436
|
}
|
|
13253
13437
|
}
|
|
13254
|
-
const sessionsDir =
|
|
13255
|
-
if (
|
|
13438
|
+
const sessionsDir = path14.join(localDir, "sessions");
|
|
13439
|
+
if (fs18.existsSync(sessionsDir)) {
|
|
13256
13440
|
try {
|
|
13257
|
-
const sessionDirs =
|
|
13441
|
+
const sessionDirs = fs18.readdirSync(sessionsDir);
|
|
13258
13442
|
for (const dir of sessionDirs) {
|
|
13259
|
-
const dirPath =
|
|
13443
|
+
const dirPath = path14.join(sessionsDir, dir);
|
|
13260
13444
|
try {
|
|
13261
|
-
const dirStat =
|
|
13445
|
+
const dirStat = fs18.statSync(dirPath);
|
|
13262
13446
|
if (!dirStat.isDirectory()) continue;
|
|
13263
13447
|
} catch (_) {
|
|
13264
13448
|
continue;
|
|
13265
13449
|
}
|
|
13266
|
-
const statePath =
|
|
13450
|
+
const statePath = path14.join(dirPath, STATE_FILENAME);
|
|
13267
13451
|
try {
|
|
13268
|
-
const stat =
|
|
13452
|
+
const stat = fs18.statSync(statePath);
|
|
13269
13453
|
if (now - stat.mtimeMs > maxAgeMs) {
|
|
13270
|
-
|
|
13454
|
+
fs18.unlinkSync(statePath);
|
|
13271
13455
|
deleted++;
|
|
13272
13456
|
}
|
|
13273
13457
|
} catch (_) {
|
|
@@ -13278,12 +13462,12 @@ function cleanOldStates(localDir, maxAgeDays) {
|
|
|
13278
13462
|
}
|
|
13279
13463
|
return { deleted };
|
|
13280
13464
|
}
|
|
13281
|
-
var
|
|
13465
|
+
var fs18, path14, STATE_FILENAME;
|
|
13282
13466
|
var init_persist = __esm({
|
|
13283
13467
|
"src/session/persist.ts"() {
|
|
13284
13468
|
"use strict";
|
|
13285
|
-
|
|
13286
|
-
|
|
13469
|
+
fs18 = __toESM(require("fs"), 1);
|
|
13470
|
+
path14 = __toESM(require("path"), 1);
|
|
13287
13471
|
init_encryption();
|
|
13288
13472
|
STATE_FILENAME = "state.json";
|
|
13289
13473
|
}
|
|
@@ -13718,7 +13902,7 @@ async function handleSessionsCommand(command, args, bm, sessionManager2, current
|
|
|
13718
13902
|
const lines = entries.map(
|
|
13719
13903
|
(e) => `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}`
|
|
13720
13904
|
).join("\n") + "\n";
|
|
13721
|
-
|
|
13905
|
+
fs19.appendFileSync(consolePath, lines);
|
|
13722
13906
|
buffers.lastConsoleFlushed = buffers.consoleTotalAdded;
|
|
13723
13907
|
}
|
|
13724
13908
|
const newNetworkCount = buffers.networkTotalAdded - buffers.lastNetworkFlushed;
|
|
@@ -13728,7 +13912,7 @@ async function handleSessionsCommand(command, args, bm, sessionManager2, current
|
|
|
13728
13912
|
const lines = entries.map(
|
|
13729
13913
|
(e) => `[${new Date(e.timestamp).toISOString()}] ${e.method} ${e.url} \u2192 ${e.status || "pending"} (${e.duration || "?"}ms, ${e.size || "?"}B)`
|
|
13730
13914
|
).join("\n") + "\n";
|
|
13731
|
-
|
|
13915
|
+
fs19.appendFileSync(networkPath, lines);
|
|
13732
13916
|
buffers.lastNetworkFlushed = buffers.networkTotalAdded;
|
|
13733
13917
|
}
|
|
13734
13918
|
}
|
|
@@ -13744,22 +13928,22 @@ async function handleSessionsCommand(command, args, bm, sessionManager2, current
|
|
|
13744
13928
|
const statesDir = `${LOCAL_DIR3}/states`;
|
|
13745
13929
|
const statePath = `${statesDir}/${name}.json`;
|
|
13746
13930
|
if (subcommand === "list") {
|
|
13747
|
-
if (!
|
|
13748
|
-
const files =
|
|
13931
|
+
if (!fs19.existsSync(statesDir)) return "(no saved states)";
|
|
13932
|
+
const files = fs19.readdirSync(statesDir).filter((f) => f.endsWith(".json"));
|
|
13749
13933
|
if (files.length === 0) return "(no saved states)";
|
|
13750
13934
|
const lines = [];
|
|
13751
13935
|
for (const file of files) {
|
|
13752
13936
|
const fp = `${statesDir}/${file}`;
|
|
13753
|
-
const stat =
|
|
13937
|
+
const stat = fs19.statSync(fp);
|
|
13754
13938
|
lines.push(` ${file.replace(".json", "")} ${stat.size}B ${new Date(stat.mtimeMs).toISOString()}`);
|
|
13755
13939
|
}
|
|
13756
13940
|
return lines.join("\n");
|
|
13757
13941
|
}
|
|
13758
13942
|
if (subcommand === "show") {
|
|
13759
|
-
if (!
|
|
13943
|
+
if (!fs19.existsSync(statePath)) {
|
|
13760
13944
|
throw new Error(`State file not found: ${statePath}`);
|
|
13761
13945
|
}
|
|
13762
|
-
const data = JSON.parse(
|
|
13946
|
+
const data = JSON.parse(fs19.readFileSync(statePath, "utf-8"));
|
|
13763
13947
|
const cookieCount = data.cookies?.length || 0;
|
|
13764
13948
|
const originCount = data.origins?.length || 0;
|
|
13765
13949
|
const storageItems = (data.origins || []).reduce((sum, o) => sum + (o.localStorage?.length || 0), 0);
|
|
@@ -13784,15 +13968,15 @@ async function handleSessionsCommand(command, args, bm, sessionManager2, current
|
|
|
13784
13968
|
const context = bm.getContext();
|
|
13785
13969
|
if (!context) throw new Error("No browser context");
|
|
13786
13970
|
const state = await context.storageState();
|
|
13787
|
-
|
|
13788
|
-
|
|
13971
|
+
fs19.mkdirSync(statesDir, { recursive: true });
|
|
13972
|
+
fs19.writeFileSync(statePath, JSON.stringify(state, null, 2), { mode: 384 });
|
|
13789
13973
|
return `State saved: ${statePath}`;
|
|
13790
13974
|
}
|
|
13791
13975
|
if (subcommand === "load") {
|
|
13792
|
-
if (!
|
|
13976
|
+
if (!fs19.existsSync(statePath)) {
|
|
13793
13977
|
throw new Error(`State file not found: ${statePath}. Run "browse state save ${name}" first.`);
|
|
13794
13978
|
}
|
|
13795
|
-
const stateData = JSON.parse(
|
|
13979
|
+
const stateData = JSON.parse(fs19.readFileSync(statePath, "utf-8"));
|
|
13796
13980
|
const context = bm.getContext();
|
|
13797
13981
|
if (!context) throw new Error("No browser context");
|
|
13798
13982
|
const warnings = [];
|
|
@@ -13845,12 +14029,12 @@ ${snapshot}`;
|
|
|
13845
14029
|
throw new Error(`Unknown sessions command: ${command}`);
|
|
13846
14030
|
}
|
|
13847
14031
|
}
|
|
13848
|
-
var
|
|
14032
|
+
var fs19, LOCAL_DIR3;
|
|
13849
14033
|
var init_sessions = __esm({
|
|
13850
14034
|
"src/commands/meta/sessions.ts"() {
|
|
13851
14035
|
"use strict";
|
|
13852
14036
|
init_sanitize();
|
|
13853
|
-
|
|
14037
|
+
fs19 = __toESM(require("fs"), 1);
|
|
13854
14038
|
LOCAL_DIR3 = process.env.BROWSE_LOCAL_DIR || "/tmp";
|
|
13855
14039
|
}
|
|
13856
14040
|
});
|
|
@@ -14181,11 +14365,11 @@ var init_lib = __esm({
|
|
|
14181
14365
|
}
|
|
14182
14366
|
}
|
|
14183
14367
|
},
|
|
14184
|
-
addToPath: function addToPath(
|
|
14185
|
-
var last =
|
|
14368
|
+
addToPath: function addToPath(path25, added, removed, oldPosInc, options) {
|
|
14369
|
+
var last = path25.lastComponent;
|
|
14186
14370
|
if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
|
|
14187
14371
|
return {
|
|
14188
|
-
oldPos:
|
|
14372
|
+
oldPos: path25.oldPos + oldPosInc,
|
|
14189
14373
|
lastComponent: {
|
|
14190
14374
|
count: last.count + 1,
|
|
14191
14375
|
added,
|
|
@@ -14195,7 +14379,7 @@ var init_lib = __esm({
|
|
|
14195
14379
|
};
|
|
14196
14380
|
} else {
|
|
14197
14381
|
return {
|
|
14198
|
-
oldPos:
|
|
14382
|
+
oldPos: path25.oldPos + oldPosInc,
|
|
14199
14383
|
lastComponent: {
|
|
14200
14384
|
count: 1,
|
|
14201
14385
|
added,
|
|
@@ -14253,7 +14437,7 @@ var init_lib = __esm({
|
|
|
14253
14437
|
tokenize: function tokenize(value) {
|
|
14254
14438
|
return Array.from(value);
|
|
14255
14439
|
},
|
|
14256
|
-
join: function
|
|
14440
|
+
join: function join15(chars) {
|
|
14257
14441
|
return chars.join("");
|
|
14258
14442
|
},
|
|
14259
14443
|
postProcess: function postProcess(changeObjects) {
|
|
@@ -19814,19 +19998,19 @@ __export(detection_exports, {
|
|
|
19814
19998
|
detectStack: () => detectStack
|
|
19815
19999
|
});
|
|
19816
20000
|
function loadCustomSignatures(localDir) {
|
|
19817
|
-
const detectionDir =
|
|
19818
|
-
if (!
|
|
20001
|
+
const detectionDir = path15.join(localDir, "detections");
|
|
20002
|
+
if (!fs20.existsSync(detectionDir)) return [];
|
|
19819
20003
|
const sigs = [];
|
|
19820
20004
|
let entries;
|
|
19821
20005
|
try {
|
|
19822
|
-
entries =
|
|
20006
|
+
entries = fs20.readdirSync(detectionDir).filter((f) => f.endsWith(".json"));
|
|
19823
20007
|
} catch {
|
|
19824
20008
|
return [];
|
|
19825
20009
|
}
|
|
19826
20010
|
for (const entry of entries) {
|
|
19827
|
-
const filePath =
|
|
20011
|
+
const filePath = path15.join(detectionDir, entry);
|
|
19828
20012
|
try {
|
|
19829
|
-
const raw =
|
|
20013
|
+
const raw = fs20.readFileSync(filePath, "utf-8");
|
|
19830
20014
|
const parsed = JSON.parse(raw);
|
|
19831
20015
|
if (parsed == null || typeof parsed !== "object" || parsed.version !== 1 || typeof parsed.name !== "string" || typeof parsed.detect !== "string") {
|
|
19832
20016
|
process.stderr.write(
|
|
@@ -19952,15 +20136,15 @@ async function detectStack(page, networkEntries, localDir) {
|
|
|
19952
20136
|
const thirdParty = buildThirdPartyInventory(page, networkEntries ?? []);
|
|
19953
20137
|
return { frameworks, saas, infrastructure, thirdParty, custom };
|
|
19954
20138
|
}
|
|
19955
|
-
var
|
|
20139
|
+
var fs20, path15, THIRD_PARTY_DOMAINS;
|
|
19956
20140
|
var init_detection = __esm({
|
|
19957
20141
|
"src/detection/index.ts"() {
|
|
19958
20142
|
"use strict";
|
|
19959
20143
|
init_frameworks();
|
|
19960
20144
|
init_saas();
|
|
19961
20145
|
init_infrastructure();
|
|
19962
|
-
|
|
19963
|
-
|
|
20146
|
+
fs20 = __toESM(require("fs"), 1);
|
|
20147
|
+
path15 = __toESM(require("path"), 1);
|
|
19964
20148
|
THIRD_PARTY_DOMAINS = {
|
|
19965
20149
|
// Analytics
|
|
19966
20150
|
"google-analytics.com": "analytics",
|
|
@@ -22042,35 +22226,35 @@ __export(persist_exports2, {
|
|
|
22042
22226
|
saveAudit: () => saveAudit
|
|
22043
22227
|
});
|
|
22044
22228
|
function auditsDir(localDir) {
|
|
22045
|
-
return
|
|
22229
|
+
return path16.join(localDir, "audits");
|
|
22046
22230
|
}
|
|
22047
22231
|
function auditPath(localDir, name) {
|
|
22048
|
-
return
|
|
22232
|
+
return path16.join(auditsDir(localDir), `${sanitizeName(name)}.json`);
|
|
22049
22233
|
}
|
|
22050
22234
|
function saveAudit(localDir, name, report) {
|
|
22051
22235
|
const dir = auditsDir(localDir);
|
|
22052
|
-
|
|
22236
|
+
fs21.mkdirSync(dir, { recursive: true });
|
|
22053
22237
|
const filePath = auditPath(localDir, name);
|
|
22054
|
-
|
|
22238
|
+
fs21.writeFileSync(filePath, JSON.stringify(report, null, 2), { mode: 384 });
|
|
22055
22239
|
return filePath;
|
|
22056
22240
|
}
|
|
22057
22241
|
function loadAudit(localDir, name) {
|
|
22058
22242
|
const filePath = auditPath(localDir, name);
|
|
22059
|
-
if (!
|
|
22243
|
+
if (!fs21.existsSync(filePath)) {
|
|
22060
22244
|
throw new Error(
|
|
22061
22245
|
`Audit not found: ${filePath}. Run "browse perf-audit save ${name}" first.`
|
|
22062
22246
|
);
|
|
22063
22247
|
}
|
|
22064
|
-
return JSON.parse(
|
|
22248
|
+
return JSON.parse(fs21.readFileSync(filePath, "utf-8"));
|
|
22065
22249
|
}
|
|
22066
22250
|
function listAudits(localDir) {
|
|
22067
22251
|
const dir = auditsDir(localDir);
|
|
22068
|
-
if (!
|
|
22069
|
-
const files =
|
|
22252
|
+
if (!fs21.existsSync(dir)) return [];
|
|
22253
|
+
const files = fs21.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
22070
22254
|
if (files.length === 0) return [];
|
|
22071
22255
|
const entries = files.map((file) => {
|
|
22072
|
-
const fp =
|
|
22073
|
-
const stat =
|
|
22256
|
+
const fp = path16.join(dir, file);
|
|
22257
|
+
const stat = fs21.statSync(fp);
|
|
22074
22258
|
return {
|
|
22075
22259
|
name: file.replace(".json", ""),
|
|
22076
22260
|
sizeBytes: stat.size,
|
|
@@ -22082,19 +22266,19 @@ function listAudits(localDir) {
|
|
|
22082
22266
|
}
|
|
22083
22267
|
function deleteAudit(localDir, name) {
|
|
22084
22268
|
const filePath = auditPath(localDir, name);
|
|
22085
|
-
if (!
|
|
22269
|
+
if (!fs21.existsSync(filePath)) {
|
|
22086
22270
|
throw new Error(
|
|
22087
22271
|
`Audit not found: ${filePath}. Nothing to delete.`
|
|
22088
22272
|
);
|
|
22089
22273
|
}
|
|
22090
|
-
|
|
22274
|
+
fs21.unlinkSync(filePath);
|
|
22091
22275
|
}
|
|
22092
|
-
var
|
|
22276
|
+
var fs21, path16;
|
|
22093
22277
|
var init_persist2 = __esm({
|
|
22094
22278
|
"src/perf-audit/persist.ts"() {
|
|
22095
22279
|
"use strict";
|
|
22096
|
-
|
|
22097
|
-
|
|
22280
|
+
fs21 = __toESM(require("fs"), 1);
|
|
22281
|
+
path16 = __toESM(require("path"), 1);
|
|
22098
22282
|
init_sanitize();
|
|
22099
22283
|
}
|
|
22100
22284
|
});
|
|
@@ -23452,7 +23636,7 @@ All budgets met.`;
|
|
|
23452
23636
|
}
|
|
23453
23637
|
return "OK";
|
|
23454
23638
|
}
|
|
23455
|
-
await new Promise((
|
|
23639
|
+
await new Promise((resolve10) => setTimeout(resolve10, 100));
|
|
23456
23640
|
}
|
|
23457
23641
|
const failures = lastResults.filter((r2) => !r2.passed);
|
|
23458
23642
|
throw new Error(
|
|
@@ -23541,12 +23725,12 @@ async function autoDetectSelector(page, field) {
|
|
|
23541
23725
|
}
|
|
23542
23726
|
throw new Error("Could not auto-detect submit button.");
|
|
23543
23727
|
}
|
|
23544
|
-
var
|
|
23728
|
+
var fs22, path17, AuthVault;
|
|
23545
23729
|
var init_auth_vault = __esm({
|
|
23546
23730
|
"src/security/auth-vault.ts"() {
|
|
23547
23731
|
"use strict";
|
|
23548
|
-
|
|
23549
|
-
|
|
23732
|
+
fs22 = __toESM(require("fs"), 1);
|
|
23733
|
+
path17 = __toESM(require("path"), 1);
|
|
23550
23734
|
init_constants();
|
|
23551
23735
|
init_encryption();
|
|
23552
23736
|
init_sanitize();
|
|
@@ -23554,11 +23738,11 @@ var init_auth_vault = __esm({
|
|
|
23554
23738
|
authDir;
|
|
23555
23739
|
encryptionKey;
|
|
23556
23740
|
constructor(localDir) {
|
|
23557
|
-
this.authDir =
|
|
23741
|
+
this.authDir = path17.join(localDir, "auth");
|
|
23558
23742
|
this.encryptionKey = resolveEncryptionKey(localDir);
|
|
23559
23743
|
}
|
|
23560
23744
|
save(name, url, username, password, selectors) {
|
|
23561
|
-
|
|
23745
|
+
fs22.mkdirSync(this.authDir, { recursive: true });
|
|
23562
23746
|
const { ciphertext, iv, authTag } = encrypt(password, this.encryptionKey);
|
|
23563
23747
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
23564
23748
|
const credential = {
|
|
@@ -23575,15 +23759,15 @@ var init_auth_vault = __esm({
|
|
|
23575
23759
|
createdAt: now,
|
|
23576
23760
|
updatedAt: now
|
|
23577
23761
|
};
|
|
23578
|
-
const filePath =
|
|
23579
|
-
|
|
23762
|
+
const filePath = path17.join(this.authDir, `${sanitizeName(name)}.json`);
|
|
23763
|
+
fs22.writeFileSync(filePath, JSON.stringify(credential, null, 2), { mode: 384 });
|
|
23580
23764
|
}
|
|
23581
23765
|
load(name) {
|
|
23582
|
-
const filePath =
|
|
23583
|
-
if (!
|
|
23766
|
+
const filePath = path17.join(this.authDir, `${sanitizeName(name)}.json`);
|
|
23767
|
+
if (!fs22.existsSync(filePath)) {
|
|
23584
23768
|
throw new Error(`Credential "${name}" not found. Run "browse auth list" to see saved credentials.`);
|
|
23585
23769
|
}
|
|
23586
|
-
return JSON.parse(
|
|
23770
|
+
return JSON.parse(fs22.readFileSync(filePath, "utf-8"));
|
|
23587
23771
|
}
|
|
23588
23772
|
async login(name, bm) {
|
|
23589
23773
|
const cred = this.load(name);
|
|
@@ -23604,11 +23788,11 @@ var init_auth_vault = __esm({
|
|
|
23604
23788
|
return `Logged in as ${cred.username} at ${page.url()}`;
|
|
23605
23789
|
}
|
|
23606
23790
|
list() {
|
|
23607
|
-
if (!
|
|
23608
|
-
const files =
|
|
23791
|
+
if (!fs22.existsSync(this.authDir)) return [];
|
|
23792
|
+
const files = fs22.readdirSync(this.authDir).filter((f) => f.endsWith(".json"));
|
|
23609
23793
|
return files.map((f) => {
|
|
23610
23794
|
try {
|
|
23611
|
-
const data = JSON.parse(
|
|
23795
|
+
const data = JSON.parse(fs22.readFileSync(path17.join(this.authDir, f), "utf-8"));
|
|
23612
23796
|
return {
|
|
23613
23797
|
name: data.name,
|
|
23614
23798
|
url: data.url,
|
|
@@ -23622,11 +23806,11 @@ var init_auth_vault = __esm({
|
|
|
23622
23806
|
}).filter(Boolean);
|
|
23623
23807
|
}
|
|
23624
23808
|
delete(name) {
|
|
23625
|
-
const filePath =
|
|
23626
|
-
if (!
|
|
23809
|
+
const filePath = path17.join(this.authDir, `${sanitizeName(name)}.json`);
|
|
23810
|
+
if (!fs22.existsSync(filePath)) {
|
|
23627
23811
|
throw new Error(`Credential "${name}" not found.`);
|
|
23628
23812
|
}
|
|
23629
|
-
|
|
23813
|
+
fs22.unlinkSync(filePath);
|
|
23630
23814
|
}
|
|
23631
23815
|
};
|
|
23632
23816
|
}
|
|
@@ -23763,20 +23947,20 @@ __export(policy_exports, {
|
|
|
23763
23947
|
function findFileUpward(filename) {
|
|
23764
23948
|
let dir = process.cwd();
|
|
23765
23949
|
for (let i = 0; i < 20; i++) {
|
|
23766
|
-
const candidate =
|
|
23767
|
-
if (
|
|
23768
|
-
const parent =
|
|
23950
|
+
const candidate = path18.join(dir, filename);
|
|
23951
|
+
if (fs23.existsSync(candidate)) return candidate;
|
|
23952
|
+
const parent = path18.dirname(dir);
|
|
23769
23953
|
if (parent === dir) break;
|
|
23770
23954
|
dir = parent;
|
|
23771
23955
|
}
|
|
23772
23956
|
return null;
|
|
23773
23957
|
}
|
|
23774
|
-
var
|
|
23958
|
+
var fs23, path18, PolicyChecker;
|
|
23775
23959
|
var init_policy = __esm({
|
|
23776
23960
|
"src/security/policy.ts"() {
|
|
23777
23961
|
"use strict";
|
|
23778
|
-
|
|
23779
|
-
|
|
23962
|
+
fs23 = __toESM(require("fs"), 1);
|
|
23963
|
+
path18 = __toESM(require("path"), 1);
|
|
23780
23964
|
PolicyChecker = class {
|
|
23781
23965
|
filePath = null;
|
|
23782
23966
|
lastMtime = 0;
|
|
@@ -23795,10 +23979,10 @@ var init_policy = __esm({
|
|
|
23795
23979
|
reload() {
|
|
23796
23980
|
if (!this.filePath) return;
|
|
23797
23981
|
try {
|
|
23798
|
-
const stat =
|
|
23982
|
+
const stat = fs23.statSync(this.filePath);
|
|
23799
23983
|
if (stat.mtimeMs === this.lastMtime) return;
|
|
23800
23984
|
this.lastMtime = stat.mtimeMs;
|
|
23801
|
-
const raw =
|
|
23985
|
+
const raw = fs23.readFileSync(this.filePath, "utf-8");
|
|
23802
23986
|
this.policy = JSON.parse(raw);
|
|
23803
23987
|
} catch {
|
|
23804
23988
|
}
|
|
@@ -24076,7 +24260,7 @@ async function handleSystemCommand(command, args, target, shutdown2, sessionMana
|
|
|
24076
24260
|
return results.join("\n\n");
|
|
24077
24261
|
}
|
|
24078
24262
|
case "doctor": {
|
|
24079
|
-
const { execSync:
|
|
24263
|
+
const { execSync: execSync8 } = await import("child_process");
|
|
24080
24264
|
const platformArg = args.find((a) => a.startsWith("--platform="))?.split("=")[1] || (args.indexOf("--platform") !== -1 ? args[args.indexOf("--platform") + 1] : "");
|
|
24081
24265
|
const lines = [];
|
|
24082
24266
|
lines.push(`Node: ${process.version}`);
|
|
@@ -24106,7 +24290,7 @@ async function handleSystemCommand(command, args, target, shutdown2, sessionMana
|
|
|
24106
24290
|
lines.push("");
|
|
24107
24291
|
lines.push("--- Android ---");
|
|
24108
24292
|
try {
|
|
24109
|
-
const adbVersion =
|
|
24293
|
+
const adbVersion = execSync8("adb version", { encoding: "utf-8", timeout: 5e3 }).split("\n")[0].trim();
|
|
24110
24294
|
lines.push(`adb: ${adbVersion}`);
|
|
24111
24295
|
} catch {
|
|
24112
24296
|
lines.push("adb: NOT FOUND \u2014 install Android SDK platform-tools and add to PATH");
|
|
@@ -24114,7 +24298,7 @@ async function handleSystemCommand(command, args, target, shutdown2, sessionMana
|
|
|
24114
24298
|
if (platformArg === "android") return lines.join("\n");
|
|
24115
24299
|
}
|
|
24116
24300
|
try {
|
|
24117
|
-
const devicesOut =
|
|
24301
|
+
const devicesOut = execSync8("adb devices", { encoding: "utf-8", timeout: 5e3 });
|
|
24118
24302
|
const deviceLines = devicesOut.split("\n").slice(1).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
24119
24303
|
const booted = deviceLines.filter((l) => l.endsWith(" device"));
|
|
24120
24304
|
const other = deviceLines.filter((l) => !l.endsWith(" device"));
|
|
@@ -24131,8 +24315,8 @@ async function handleSystemCommand(command, args, target, shutdown2, sessionMana
|
|
|
24131
24315
|
lines.push("Devices: could not query (adb failed)");
|
|
24132
24316
|
}
|
|
24133
24317
|
try {
|
|
24134
|
-
|
|
24135
|
-
const avds =
|
|
24318
|
+
execSync8("emulator -list-avds", { encoding: "utf-8", timeout: 5e3, stdio: ["ignore", "pipe", "pipe"] });
|
|
24319
|
+
const avds = execSync8("emulator -list-avds", { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
24136
24320
|
if (avds) {
|
|
24137
24321
|
lines.push(`AVDs: ${avds.split("\n").join(", ")}`);
|
|
24138
24322
|
} else {
|
|
@@ -24142,13 +24326,13 @@ async function handleSystemCommand(command, args, target, shutdown2, sessionMana
|
|
|
24142
24326
|
lines.push("Emulator: not found (optional \u2014 only needed to start AVDs from CLI)");
|
|
24143
24327
|
}
|
|
24144
24328
|
try {
|
|
24145
|
-
const
|
|
24146
|
-
const
|
|
24147
|
-
const localBuild =
|
|
24148
|
-
const installed =
|
|
24149
|
-
if (
|
|
24329
|
+
const fs30 = await import("fs");
|
|
24330
|
+
const path25 = await import("path");
|
|
24331
|
+
const localBuild = path25.resolve(__dirname, "../../../browse-android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk");
|
|
24332
|
+
const installed = path25.resolve(__dirname, "../../bin/browse-android.apk");
|
|
24333
|
+
if (fs30.existsSync(localBuild)) {
|
|
24150
24334
|
lines.push(`Driver APK: ${localBuild}`);
|
|
24151
|
-
} else if (
|
|
24335
|
+
} else if (fs30.existsSync(installed)) {
|
|
24152
24336
|
lines.push(`Driver APK: ${installed}`);
|
|
24153
24337
|
} else {
|
|
24154
24338
|
lines.push("Driver APK: NOT FOUND");
|
|
@@ -24161,9 +24345,9 @@ async function handleSystemCommand(command, args, target, shutdown2, sessionMana
|
|
|
24161
24345
|
return lines.join("\n");
|
|
24162
24346
|
}
|
|
24163
24347
|
case "upgrade": {
|
|
24164
|
-
const { execSync:
|
|
24348
|
+
const { execSync: execSync8 } = await import("child_process");
|
|
24165
24349
|
try {
|
|
24166
|
-
const output =
|
|
24350
|
+
const output = execSync8("npm update -g @ulpi/browse 2>&1", { encoding: "utf-8", timeout: 3e4 });
|
|
24167
24351
|
return `Upgrade complete.
|
|
24168
24352
|
${output.trim()}`;
|
|
24169
24353
|
} catch (err) {
|
|
@@ -24470,21 +24654,21 @@ var init_refs = __esm({
|
|
|
24470
24654
|
function getProfileDir(localDir, name) {
|
|
24471
24655
|
const sanitized = sanitizeName(name);
|
|
24472
24656
|
if (!sanitized) throw new Error("Invalid profile name");
|
|
24473
|
-
return
|
|
24657
|
+
return path19.join(localDir, "profiles", sanitized);
|
|
24474
24658
|
}
|
|
24475
24659
|
function listProfiles(localDir) {
|
|
24476
|
-
const profilesDir =
|
|
24477
|
-
if (!
|
|
24478
|
-
return
|
|
24479
|
-
const dir =
|
|
24480
|
-
const stat =
|
|
24660
|
+
const profilesDir = path19.join(localDir, "profiles");
|
|
24661
|
+
if (!fs24.existsSync(profilesDir)) return [];
|
|
24662
|
+
return fs24.readdirSync(profilesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
|
|
24663
|
+
const dir = path19.join(profilesDir, d.name);
|
|
24664
|
+
const stat = fs24.statSync(dir);
|
|
24481
24665
|
let totalSize = 0;
|
|
24482
24666
|
try {
|
|
24483
|
-
const files =
|
|
24667
|
+
const files = fs24.readdirSync(dir, { recursive: true, withFileTypes: true });
|
|
24484
24668
|
for (const f of files) {
|
|
24485
24669
|
if (f.isFile()) {
|
|
24486
24670
|
try {
|
|
24487
|
-
totalSize +=
|
|
24671
|
+
totalSize += fs24.statSync(path19.join(f.parentPath || f.path || dir, f.name)).size;
|
|
24488
24672
|
} catch {
|
|
24489
24673
|
}
|
|
24490
24674
|
}
|
|
@@ -24501,15 +24685,15 @@ function listProfiles(localDir) {
|
|
|
24501
24685
|
}
|
|
24502
24686
|
function deleteProfile(localDir, name) {
|
|
24503
24687
|
const dir = getProfileDir(localDir, name);
|
|
24504
|
-
if (!
|
|
24505
|
-
|
|
24688
|
+
if (!fs24.existsSync(dir)) throw new Error(`Profile "${name}" not found`);
|
|
24689
|
+
fs24.rmSync(dir, { recursive: true, force: true });
|
|
24506
24690
|
}
|
|
24507
|
-
var
|
|
24691
|
+
var path19, fs24;
|
|
24508
24692
|
var init_profiles = __esm({
|
|
24509
24693
|
"src/browser/profiles.ts"() {
|
|
24510
24694
|
"use strict";
|
|
24511
|
-
|
|
24512
|
-
|
|
24695
|
+
path19 = __toESM(require("path"), 1);
|
|
24696
|
+
fs24 = __toESM(require("fs"), 1);
|
|
24513
24697
|
init_sanitize();
|
|
24514
24698
|
}
|
|
24515
24699
|
});
|
|
@@ -24683,9 +24867,9 @@ var init_manager = __esm({
|
|
|
24683
24867
|
});
|
|
24684
24868
|
} catch (err) {
|
|
24685
24869
|
if (err.message?.includes("Failed to launch") || err.message?.includes("Target closed")) {
|
|
24686
|
-
const
|
|
24870
|
+
const fs30 = await import("fs");
|
|
24687
24871
|
console.error(`[browse] Profile directory corrupted, recreating: ${profileDir}`);
|
|
24688
|
-
|
|
24872
|
+
fs30.rmSync(profileDir, { recursive: true, force: true });
|
|
24689
24873
|
context = await import_playwright2.chromium.launchPersistentContext(profileDir, {
|
|
24690
24874
|
headless: process.env.BROWSE_HEADED !== "1",
|
|
24691
24875
|
viewport: { width: 1920, height: 1080 }
|
|
@@ -25413,8 +25597,8 @@ var init_manager = __esm({
|
|
|
25413
25597
|
// ─── Video Recording ──────────────────────────────────────
|
|
25414
25598
|
async startVideoRecording(dir) {
|
|
25415
25599
|
if (this.videoRecording) throw new Error("Video recording already active");
|
|
25416
|
-
const
|
|
25417
|
-
|
|
25600
|
+
const fs30 = await import("fs");
|
|
25601
|
+
fs30.mkdirSync(dir, { recursive: true });
|
|
25418
25602
|
this.videoRecording = { dir, startedAt: Date.now() };
|
|
25419
25603
|
const viewport = this.currentDevice?.viewport || { width: 1920, height: 1080 };
|
|
25420
25604
|
await this.recreateContext({
|
|
@@ -25711,10 +25895,10 @@ __export(react_devtools_exports, {
|
|
|
25711
25895
|
requireReact: () => requireReact
|
|
25712
25896
|
});
|
|
25713
25897
|
async function ensureHook() {
|
|
25714
|
-
if (
|
|
25715
|
-
return
|
|
25898
|
+
if (fs25.existsSync(HOOK_PATH)) {
|
|
25899
|
+
return fs25.readFileSync(HOOK_PATH, "utf8");
|
|
25716
25900
|
}
|
|
25717
|
-
|
|
25901
|
+
fs25.mkdirSync(CACHE_DIR, { recursive: true });
|
|
25718
25902
|
const res = await fetch(HOOK_URL);
|
|
25719
25903
|
if (!res.ok) {
|
|
25720
25904
|
throw new Error(
|
|
@@ -25723,7 +25907,7 @@ Manual fallback: npm install -g react-devtools-core, then copy installHook.js to
|
|
|
25723
25907
|
);
|
|
25724
25908
|
}
|
|
25725
25909
|
const script = await res.text();
|
|
25726
|
-
|
|
25910
|
+
fs25.writeFileSync(HOOK_PATH, script);
|
|
25727
25911
|
return script;
|
|
25728
25912
|
}
|
|
25729
25913
|
async function injectHook(bm) {
|
|
@@ -25870,15 +26054,15 @@ async function getSuspense(bm, page) {
|
|
|
25870
26054
|
if (!roots || roots.size === 0) return "No fiber roots found";
|
|
25871
26055
|
const root = roots.values().next().value;
|
|
25872
26056
|
const boundaries = [];
|
|
25873
|
-
const walk = (fiber,
|
|
26057
|
+
const walk = (fiber, path25) => {
|
|
25874
26058
|
if (!fiber) return;
|
|
25875
26059
|
if (fiber.tag === 13) {
|
|
25876
26060
|
const resolved = fiber.memoizedState === null;
|
|
25877
|
-
const parent =
|
|
26061
|
+
const parent = path25.length > 0 ? path25[path25.length - 1] : "root";
|
|
25878
26062
|
boundaries.push(`Suspense in ${parent} \u2014 ${resolved ? "resolved (children visible)" : "pending (showing fallback)"}`);
|
|
25879
26063
|
}
|
|
25880
26064
|
const name = fiber.type?.displayName || fiber.type?.name || null;
|
|
25881
|
-
const newPath = name ? [...
|
|
26065
|
+
const newPath = name ? [...path25, name] : path25;
|
|
25882
26066
|
let child = fiber.child;
|
|
25883
26067
|
while (child) {
|
|
25884
26068
|
walk(child, newPath);
|
|
@@ -26057,15 +26241,15 @@ async function getContext(bm, page, selector) {
|
|
|
26057
26241
|
await handle.dispose();
|
|
26058
26242
|
return result;
|
|
26059
26243
|
}
|
|
26060
|
-
var
|
|
26244
|
+
var fs25, path20, os4, CACHE_DIR, HOOK_PATH, HOOK_URL;
|
|
26061
26245
|
var init_react_devtools = __esm({
|
|
26062
26246
|
"src/browser/react-devtools.ts"() {
|
|
26063
26247
|
"use strict";
|
|
26064
|
-
|
|
26065
|
-
|
|
26066
|
-
|
|
26067
|
-
CACHE_DIR =
|
|
26068
|
-
HOOK_PATH =
|
|
26248
|
+
fs25 = __toESM(require("fs"), 1);
|
|
26249
|
+
path20 = __toESM(require("path"), 1);
|
|
26250
|
+
os4 = __toESM(require("os"), 1);
|
|
26251
|
+
CACHE_DIR = path20.join(os4.homedir(), ".cache", "browse", "react-devtools");
|
|
26252
|
+
HOOK_PATH = path20.join(CACHE_DIR, "installHook.js");
|
|
26069
26253
|
HOOK_URL = "https://unpkg.com/react-devtools-core@latest/dist/installHook.js";
|
|
26070
26254
|
}
|
|
26071
26255
|
});
|
|
@@ -26353,9 +26537,9 @@ function getFlowsDir() {
|
|
|
26353
26537
|
const root = findProjectRoot();
|
|
26354
26538
|
if (config.flowPaths?.length && root) {
|
|
26355
26539
|
for (const fp of config.flowPaths) {
|
|
26356
|
-
const abs =
|
|
26540
|
+
const abs = path21.isAbsolute(fp) ? fp : path21.join(root, fp);
|
|
26357
26541
|
try {
|
|
26358
|
-
|
|
26542
|
+
fs26.mkdirSync(abs, { recursive: true });
|
|
26359
26543
|
return abs;
|
|
26360
26544
|
} catch {
|
|
26361
26545
|
}
|
|
@@ -26363,17 +26547,17 @@ function getFlowsDir() {
|
|
|
26363
26547
|
}
|
|
26364
26548
|
const localDir = process.env.BROWSE_LOCAL_DIR;
|
|
26365
26549
|
if (localDir) {
|
|
26366
|
-
const dir2 =
|
|
26367
|
-
|
|
26550
|
+
const dir2 = path21.join(localDir, "flows");
|
|
26551
|
+
fs26.mkdirSync(dir2, { recursive: true });
|
|
26368
26552
|
return dir2;
|
|
26369
26553
|
}
|
|
26370
26554
|
if (root) {
|
|
26371
|
-
const dir2 =
|
|
26372
|
-
|
|
26555
|
+
const dir2 = path21.join(root, ".browse", "flows");
|
|
26556
|
+
fs26.mkdirSync(dir2, { recursive: true });
|
|
26373
26557
|
return dir2;
|
|
26374
26558
|
}
|
|
26375
|
-
const dir =
|
|
26376
|
-
|
|
26559
|
+
const dir = path21.join(process.cwd(), ".browse", "flows");
|
|
26560
|
+
fs26.mkdirSync(dir, { recursive: true });
|
|
26377
26561
|
return dir;
|
|
26378
26562
|
}
|
|
26379
26563
|
function validateFlowName(name) {
|
|
@@ -26493,18 +26677,18 @@ async function handleFlowsCommand(command, args, target, shutdown2, sessionManag
|
|
|
26493
26677
|
}
|
|
26494
26678
|
const { exportFlowYaml: exportFlowYaml2 } = await Promise.resolve().then(() => (init_record(), record_exports));
|
|
26495
26679
|
const yamlContent = exportFlowYaml2(steps2);
|
|
26496
|
-
const flowPath =
|
|
26497
|
-
|
|
26680
|
+
const flowPath = path21.join(getFlowsDir(), `${name}.yaml`);
|
|
26681
|
+
fs26.writeFileSync(flowPath, yamlContent, "utf-8");
|
|
26498
26682
|
return `Flow saved: ${flowPath} (${steps2.length} steps)`;
|
|
26499
26683
|
}
|
|
26500
26684
|
if (subOrFile === "run") {
|
|
26501
26685
|
const name = args[1];
|
|
26502
26686
|
if (!name) throw new Error("Usage: browse flow run <name>");
|
|
26503
26687
|
validateFlowName(name);
|
|
26504
|
-
const flowPath =
|
|
26688
|
+
const flowPath = path21.join(getFlowsDir(), `${name}.yaml`);
|
|
26505
26689
|
let content2;
|
|
26506
26690
|
try {
|
|
26507
|
-
content2 =
|
|
26691
|
+
content2 = fs26.readFileSync(flowPath, "utf-8");
|
|
26508
26692
|
} catch (err) {
|
|
26509
26693
|
if (err.code === "ENOENT") {
|
|
26510
26694
|
throw new Error(`Saved flow not found: "${name}" (looked at ${flowPath})`);
|
|
@@ -26517,18 +26701,18 @@ async function handleFlowsCommand(command, args, target, shutdown2, sessionManag
|
|
|
26517
26701
|
}
|
|
26518
26702
|
if (subOrFile === "list") {
|
|
26519
26703
|
const flowsDir = getFlowsDir();
|
|
26520
|
-
if (!
|
|
26704
|
+
if (!fs26.existsSync(flowsDir)) {
|
|
26521
26705
|
return "No saved flows (directory does not exist yet)";
|
|
26522
26706
|
}
|
|
26523
|
-
const entries =
|
|
26707
|
+
const entries = fs26.readdirSync(flowsDir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).sort();
|
|
26524
26708
|
if (entries.length === 0) {
|
|
26525
26709
|
return "No saved flows";
|
|
26526
26710
|
}
|
|
26527
26711
|
const lines = [`Saved flows (${flowsDir}):`];
|
|
26528
26712
|
for (const entry of entries) {
|
|
26529
26713
|
const name = entry.replace(/\.(yaml|yml)$/, "");
|
|
26530
|
-
const fullPath =
|
|
26531
|
-
const stat =
|
|
26714
|
+
const fullPath = path21.join(flowsDir, entry);
|
|
26715
|
+
const stat = fs26.statSync(fullPath);
|
|
26532
26716
|
const mtime = new Date(stat.mtimeMs).toISOString().replace("T", " ").slice(0, 19);
|
|
26533
26717
|
lines.push(` ${name} (${mtime})`);
|
|
26534
26718
|
}
|
|
@@ -26537,7 +26721,7 @@ async function handleFlowsCommand(command, args, target, shutdown2, sessionManag
|
|
|
26537
26721
|
const filePath = subOrFile;
|
|
26538
26722
|
let content;
|
|
26539
26723
|
try {
|
|
26540
|
-
content =
|
|
26724
|
+
content = fs26.readFileSync(filePath, "utf-8");
|
|
26541
26725
|
} catch (err) {
|
|
26542
26726
|
if (err.code === "ENOENT") {
|
|
26543
26727
|
throw new Error(`Flow file not found: ${filePath}`);
|
|
@@ -26579,7 +26763,7 @@ async function handleFlowsCommand(command, args, target, shutdown2, sessionManag
|
|
|
26579
26763
|
lastError = err.message;
|
|
26580
26764
|
if (attempt < maxAttempts) {
|
|
26581
26765
|
if (backoff) {
|
|
26582
|
-
await new Promise((
|
|
26766
|
+
await new Promise((resolve10) => setTimeout(resolve10, delay));
|
|
26583
26767
|
delay *= 2;
|
|
26584
26768
|
}
|
|
26585
26769
|
continue;
|
|
@@ -26599,7 +26783,7 @@ async function handleFlowsCommand(command, args, target, shutdown2, sessionManag
|
|
|
26599
26783
|
);
|
|
26600
26784
|
}
|
|
26601
26785
|
if (backoff) {
|
|
26602
|
-
await new Promise((
|
|
26786
|
+
await new Promise((resolve10) => setTimeout(resolve10, delay));
|
|
26603
26787
|
delay *= 2;
|
|
26604
26788
|
}
|
|
26605
26789
|
}
|
|
@@ -26652,7 +26836,7 @@ async function handleFlowsCommand(command, args, target, shutdown2, sessionManag
|
|
|
26652
26836
|
changeSummary = result.summary;
|
|
26653
26837
|
break;
|
|
26654
26838
|
}
|
|
26655
|
-
await new Promise((
|
|
26839
|
+
await new Promise((resolve10) => setTimeout(resolve10, POLL_INTERVAL));
|
|
26656
26840
|
}
|
|
26657
26841
|
await page.evaluate((wid) => {
|
|
26658
26842
|
const info = window[wid];
|
|
@@ -26803,13 +26987,13 @@ function parseCommandString(input) {
|
|
|
26803
26987
|
}
|
|
26804
26988
|
return parts;
|
|
26805
26989
|
}
|
|
26806
|
-
var
|
|
26990
|
+
var fs26, path21, DEFAULT_MAX_FLOW_DEPTH, flowDepthMap, sessionlessSentinel;
|
|
26807
26991
|
var init_flows = __esm({
|
|
26808
26992
|
"src/commands/meta/flows.ts"() {
|
|
26809
26993
|
"use strict";
|
|
26810
26994
|
init_config();
|
|
26811
|
-
|
|
26812
|
-
|
|
26995
|
+
fs26 = __toESM(require("fs"), 1);
|
|
26996
|
+
path21 = __toESM(require("path"), 1);
|
|
26813
26997
|
DEFAULT_MAX_FLOW_DEPTH = 10;
|
|
26814
26998
|
flowDepthMap = /* @__PURE__ */ new WeakMap();
|
|
26815
26999
|
sessionlessSentinel = {};
|
|
@@ -26820,7 +27004,7 @@ var init_flows = __esm({
|
|
|
26820
27004
|
async function handleSimCommand(command, args, bm, shutdown2, sessionManager2, currentSession) {
|
|
26821
27005
|
const sub = args[0];
|
|
26822
27006
|
if (!sub || !["start", "stop", "status"].includes(sub)) {
|
|
26823
|
-
throw new Error("Usage: browse sim start --platform ios|android [--device <name>] [--app <id>] [--visible] | stop | status");
|
|
27007
|
+
throw new Error("Usage: browse sim start --platform ios|android [--device <name>] [--app <id-or-path>] [--visible] | stop | status");
|
|
26824
27008
|
}
|
|
26825
27009
|
let platform = "ios";
|
|
26826
27010
|
for (let i = 1; i < args.length; i++) {
|
|
@@ -27932,13 +28116,14 @@ var init_registry = __esm({
|
|
|
27932
28116
|
inputSchema: { type: "object", properties: { json: { type: "boolean", description: "Return raw JSON instead of formatted text." } } },
|
|
27933
28117
|
argDecode: (p) => p.json ? ["--json"] : []
|
|
27934
28118
|
} }),
|
|
27935
|
-
m("sim", "Simulator/emulator lifecycle", { usage: "start|stop|status [--platform ios|android] [--device <name>]", mcp: {
|
|
27936
|
-
description: "Manage iOS simulator or Android emulator lifecycle. Start boots the simulator/emulator and launches the browse runner. Stop kills the runner and optionally shuts down the device. Status shows the current runner health.",
|
|
27937
|
-
inputSchema: { type: "object", properties: { action: { type: "string", description: "Lifecycle action.", enum: ["start", "stop", "status"] }, platform: { type: "string", description: "Target platform.", enum: ["ios", "android"] }, device: { type: "string", description: "Device name, UDID, or serial." } }, required: ["action"] },
|
|
28119
|
+
m("sim", "Simulator/emulator lifecycle", { usage: "start|stop|status [--platform ios|android] [--device <name>] [--app <id-or-path>]", mcp: {
|
|
28120
|
+
description: "Manage iOS simulator or Android emulator lifecycle. Start boots the simulator/emulator, optionally installs an app from a file path (.app/.ipa/.apk), and launches the browse runner. Stop kills the runner and optionally shuts down the device. Status shows the current runner health.",
|
|
28121
|
+
inputSchema: { type: "object", properties: { action: { type: "string", description: "Lifecycle action.", enum: ["start", "stop", "status"] }, platform: { type: "string", description: "Target platform.", enum: ["ios", "android"] }, device: { type: "string", description: "Device name, UDID, or serial." }, app: { type: "string", description: "Bundle ID, package name, or path to .app/.ipa/.apk file to install and test." } }, required: ["action"] },
|
|
27938
28122
|
argDecode: (p) => {
|
|
27939
28123
|
const args = [String(p.action)];
|
|
27940
28124
|
if (p.platform) args.push("--platform", String(p.platform));
|
|
27941
28125
|
if (p.device) args.push("--device", String(p.device));
|
|
28126
|
+
if (p.app) args.push("--app", String(p.app));
|
|
27942
28127
|
return args;
|
|
27943
28128
|
}
|
|
27944
28129
|
} })
|
|
@@ -28490,9 +28675,9 @@ var manager_exports2 = {};
|
|
|
28490
28675
|
__export(manager_exports2, {
|
|
28491
28676
|
AndroidAppManager: () => AndroidAppManager
|
|
28492
28677
|
});
|
|
28493
|
-
function normalizeNode(raw,
|
|
28678
|
+
function normalizeNode(raw, path25 = []) {
|
|
28494
28679
|
return {
|
|
28495
|
-
path:
|
|
28680
|
+
path: path25,
|
|
28496
28681
|
role: androidRole(raw.className),
|
|
28497
28682
|
label: raw.text ?? raw.hint ?? "",
|
|
28498
28683
|
value: raw.editable ? raw.text ?? "" : void 0,
|
|
@@ -28507,7 +28692,7 @@ function normalizeNode(raw, path24 = []) {
|
|
|
28507
28692
|
selected: raw.selected,
|
|
28508
28693
|
editable: raw.editable,
|
|
28509
28694
|
actions: deriveActions(raw),
|
|
28510
|
-
children: raw.children.map((child, i) => normalizeNode(child, [...
|
|
28695
|
+
children: raw.children.map((child, i) => normalizeNode(child, [...path25, i]))
|
|
28511
28696
|
};
|
|
28512
28697
|
}
|
|
28513
28698
|
function androidRole(className) {
|
|
@@ -28623,15 +28808,15 @@ var init_manager2 = __esm({
|
|
|
28623
28808
|
}
|
|
28624
28809
|
/** Tap (click) an element by ref */
|
|
28625
28810
|
async tap(ref) {
|
|
28626
|
-
const { path:
|
|
28627
|
-
const result = await this.bridge.action(
|
|
28811
|
+
const { path: path25, label } = this.resolveRef(ref);
|
|
28812
|
+
const result = await this.bridge.action(path25, "click");
|
|
28628
28813
|
if (!result.success) throw new Error(result.error ?? "Tap failed");
|
|
28629
28814
|
return `Tapped ${ref}${label ? ` "${label}"` : ""}`;
|
|
28630
28815
|
}
|
|
28631
28816
|
/** Fill a text field by ref */
|
|
28632
28817
|
async fill(ref, value) {
|
|
28633
|
-
const { path:
|
|
28634
|
-
const result = await this.bridge.setValue(
|
|
28818
|
+
const { path: path25 } = this.resolveRef(ref);
|
|
28819
|
+
const result = await this.bridge.setValue(path25, value);
|
|
28635
28820
|
if (!result.success) throw new Error(result.error ?? "Fill failed");
|
|
28636
28821
|
return `Filled ${ref} with "${value}"`;
|
|
28637
28822
|
}
|
|
@@ -28652,8 +28837,8 @@ var init_manager2 = __esm({
|
|
|
28652
28837
|
const actionName = actionMap[direction.toLowerCase()];
|
|
28653
28838
|
if (!actionName) throw new Error(`Invalid swipe direction: ${direction}. Use up/down/left/right.`);
|
|
28654
28839
|
if (ref) {
|
|
28655
|
-
const { path:
|
|
28656
|
-
const result2 = await this.bridge.action(
|
|
28840
|
+
const { path: path25, label } = this.resolveRef(ref);
|
|
28841
|
+
const result2 = await this.bridge.action(path25, actionName);
|
|
28657
28842
|
if (!result2.success) return this.swipeBoundaryMessage(direction, result2.error);
|
|
28658
28843
|
return `Swiped ${direction} on ${ref}${label ? ` "${label}"` : ""}`;
|
|
28659
28844
|
}
|
|
@@ -28819,16 +29004,16 @@ var init_manager3 = __esm({
|
|
|
28819
29004
|
/** Tap (press) an element by ref. */
|
|
28820
29005
|
async tap(ref) {
|
|
28821
29006
|
this.requireBridge();
|
|
28822
|
-
const { path:
|
|
28823
|
-
const result = await this.bridge.action(
|
|
29007
|
+
const { path: path25, label } = this.resolveRef(ref);
|
|
29008
|
+
const result = await this.bridge.action(path25, "AXPress");
|
|
28824
29009
|
if (!result.success) throw new Error(result.error || "Tap failed");
|
|
28825
29010
|
return `Tapped ${ref}${label ? ` "${label}"` : ""}`;
|
|
28826
29011
|
}
|
|
28827
29012
|
/** Fill a text field by ref. */
|
|
28828
29013
|
async fill(ref, value) {
|
|
28829
29014
|
this.requireBridge();
|
|
28830
|
-
const { path:
|
|
28831
|
-
const result = await this.bridge.setValue(
|
|
29015
|
+
const { path: path25 } = this.resolveRef(ref);
|
|
29016
|
+
const result = await this.bridge.setValue(path25, value);
|
|
28832
29017
|
if (!result.success) throw new Error(result.error || "Fill failed");
|
|
28833
29018
|
return `Filled ${ref} with "${value}"`;
|
|
28834
29019
|
}
|
|
@@ -28844,8 +29029,8 @@ var init_manager3 = __esm({
|
|
|
28844
29029
|
this.requireBridge();
|
|
28845
29030
|
const actionName = `swipe${direction.charAt(0).toUpperCase()}${direction.slice(1).toLowerCase()}`;
|
|
28846
29031
|
if (ref) {
|
|
28847
|
-
const { path:
|
|
28848
|
-
const result2 = await this.bridge.action(
|
|
29032
|
+
const { path: path25, label } = this.resolveRef(ref);
|
|
29033
|
+
const result2 = await this.bridge.action(path25, actionName);
|
|
28849
29034
|
if (!result2.success) throw new Error(result2.error || `Swipe ${direction} failed`);
|
|
28850
29035
|
return `Swiped ${direction} on ${ref}${label ? ` "${label}"` : ""}`;
|
|
28851
29036
|
}
|
|
@@ -28994,15 +29179,15 @@ var init_manager4 = __esm({
|
|
|
28994
29179
|
}
|
|
28995
29180
|
/** Tap (press) an element by ref */
|
|
28996
29181
|
async tap(ref) {
|
|
28997
|
-
const { path:
|
|
28998
|
-
const result = await this.bridge.action(
|
|
29182
|
+
const { path: path25, label } = this.resolveRef(ref);
|
|
29183
|
+
const result = await this.bridge.action(path25, "AXPress");
|
|
28999
29184
|
if (!result.success) throw new Error(result.error || "Tap failed");
|
|
29000
29185
|
return `Tapped ${ref}${label ? ` "${label}"` : ""}`;
|
|
29001
29186
|
}
|
|
29002
29187
|
/** Fill a text field by ref */
|
|
29003
29188
|
async fill(ref, value) {
|
|
29004
|
-
const { path:
|
|
29005
|
-
const result = await this.bridge.setValue(
|
|
29189
|
+
const { path: path25 } = this.resolveRef(ref);
|
|
29190
|
+
const result = await this.bridge.setValue(path25, value);
|
|
29006
29191
|
if (!result.success) throw new Error(result.error || "Fill failed");
|
|
29007
29192
|
return `Filled ${ref} with "${value}"`;
|
|
29008
29193
|
}
|
|
@@ -29023,8 +29208,8 @@ var init_manager4 = __esm({
|
|
|
29023
29208
|
const actionName = actionMap[direction.toLowerCase()];
|
|
29024
29209
|
if (!actionName) throw new Error(`Invalid swipe direction: ${direction}. Use up/down/left/right.`);
|
|
29025
29210
|
if (ref) {
|
|
29026
|
-
const { path:
|
|
29027
|
-
const result2 = await this.bridge.action(
|
|
29211
|
+
const { path: path25, label } = this.resolveRef(ref);
|
|
29212
|
+
const result2 = await this.bridge.action(path25, actionName);
|
|
29028
29213
|
if (!result2.success) throw new Error(result2.error || `Swipe ${direction} failed`);
|
|
29029
29214
|
return `Swiped ${direction} on ${ref}${label ? ` "${label}"` : ""}`;
|
|
29030
29215
|
}
|
|
@@ -29148,10 +29333,10 @@ function createAppTargetFactory(appName) {
|
|
|
29148
29333
|
const { ensureMacOSBridge: ensureMacOSBridge2, createMacOSBridge: createMacOSBridge2 } = await Promise.resolve().then(() => (init_bridge3(), bridge_exports3));
|
|
29149
29334
|
const { AppManager: AppManager2 } = await Promise.resolve().then(() => (init_manager4(), manager_exports4));
|
|
29150
29335
|
const bridgePath = await ensureMacOSBridge2();
|
|
29151
|
-
const { execSync:
|
|
29336
|
+
const { execSync: execSync8 } = await import("child_process");
|
|
29152
29337
|
let pid;
|
|
29153
29338
|
try {
|
|
29154
|
-
const output =
|
|
29339
|
+
const output = execSync8(`pgrep -xo "${appName}"`, { encoding: "utf-8" }).trim();
|
|
29155
29340
|
pid = parseInt(output, 10);
|
|
29156
29341
|
if (isNaN(pid)) throw new Error(`App '${appName}' is not running`);
|
|
29157
29342
|
} catch (err) {
|
|
@@ -29203,7 +29388,7 @@ __export(resolver_exports, {
|
|
|
29203
29388
|
});
|
|
29204
29389
|
function findLightpanda() {
|
|
29205
29390
|
try {
|
|
29206
|
-
const result = (0,
|
|
29391
|
+
const result = (0, import_child_process11.execSync)("which lightpanda", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
29207
29392
|
if (result) return result;
|
|
29208
29393
|
} catch {
|
|
29209
29394
|
}
|
|
@@ -29227,13 +29412,13 @@ async function getRuntime(name) {
|
|
|
29227
29412
|
}
|
|
29228
29413
|
return loader();
|
|
29229
29414
|
}
|
|
29230
|
-
var import_os, import_fs,
|
|
29415
|
+
var import_os, import_fs, import_child_process11, import_path, net2, registry2, AVAILABLE_RUNTIMES;
|
|
29231
29416
|
var init_resolver = __esm({
|
|
29232
29417
|
"src/engine/resolver.ts"() {
|
|
29233
29418
|
"use strict";
|
|
29234
29419
|
import_os = require("os");
|
|
29235
29420
|
import_fs = require("fs");
|
|
29236
|
-
|
|
29421
|
+
import_child_process11 = require("child_process");
|
|
29237
29422
|
import_path = require("path");
|
|
29238
29423
|
net2 = __toESM(require("net"), 1);
|
|
29239
29424
|
registry2 = {
|
|
@@ -29258,16 +29443,16 @@ var init_resolver = __esm({
|
|
|
29258
29443
|
"Lightpanda not found. Install: https://lightpanda.io/docs/open-source/installation"
|
|
29259
29444
|
);
|
|
29260
29445
|
}
|
|
29261
|
-
const port = await new Promise((
|
|
29446
|
+
const port = await new Promise((resolve10, reject) => {
|
|
29262
29447
|
const srv = net2.createServer();
|
|
29263
29448
|
srv.listen(0, "127.0.0.1", () => {
|
|
29264
29449
|
const addr = srv.address();
|
|
29265
29450
|
const p = typeof addr === "object" && addr ? addr.port : 0;
|
|
29266
|
-
srv.close(() =>
|
|
29451
|
+
srv.close(() => resolve10(p));
|
|
29267
29452
|
});
|
|
29268
29453
|
srv.on("error", reject);
|
|
29269
29454
|
});
|
|
29270
|
-
const child = (0,
|
|
29455
|
+
const child = (0, import_child_process11.spawn)(
|
|
29271
29456
|
binaryPath,
|
|
29272
29457
|
["serve", "--host", "127.0.0.1", "--port", String(port), "--timeout", "604800"],
|
|
29273
29458
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
@@ -29623,7 +29808,7 @@ var init_domain_filter = __esm({
|
|
|
29623
29808
|
});
|
|
29624
29809
|
|
|
29625
29810
|
// src/session/manager.ts
|
|
29626
|
-
var
|
|
29811
|
+
var fs27, path22, SessionManager;
|
|
29627
29812
|
var init_manager5 = __esm({
|
|
29628
29813
|
"src/session/manager.ts"() {
|
|
29629
29814
|
"use strict";
|
|
@@ -29632,8 +29817,8 @@ var init_manager5 = __esm({
|
|
|
29632
29817
|
init_sanitize();
|
|
29633
29818
|
init_persist();
|
|
29634
29819
|
init_encryption();
|
|
29635
|
-
|
|
29636
|
-
|
|
29820
|
+
fs27 = __toESM(require("fs"), 1);
|
|
29821
|
+
path22 = __toESM(require("path"), 1);
|
|
29637
29822
|
SessionManager = class {
|
|
29638
29823
|
sessions = /* @__PURE__ */ new Map();
|
|
29639
29824
|
/** Factory-created target accessors for setup operations that need target-specific methods */
|
|
@@ -29714,8 +29899,8 @@ var init_manager5 = __esm({
|
|
|
29714
29899
|
}
|
|
29715
29900
|
return session;
|
|
29716
29901
|
}
|
|
29717
|
-
const outputDir =
|
|
29718
|
-
|
|
29902
|
+
const outputDir = path22.join(this.localDir, "sessions", sanitizeName(sessionId));
|
|
29903
|
+
fs27.mkdirSync(outputDir, { recursive: true });
|
|
29719
29904
|
const buffers = new SessionBuffers();
|
|
29720
29905
|
const effectiveFactory = this.appFactories.get(sessionId) ?? this.factory;
|
|
29721
29906
|
const ct = await effectiveFactory.create(buffers, this.reuseContext && this.sessions.size === 0);
|
|
@@ -29912,7 +30097,7 @@ function flushSessionBuffers(session, final) {
|
|
|
29912
30097
|
const lines = newEntries.map(
|
|
29913
30098
|
(e) => `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}`
|
|
29914
30099
|
).join("\n") + "\n";
|
|
29915
|
-
|
|
30100
|
+
fs28.appendFileSync(consolePath, lines);
|
|
29916
30101
|
buffers.lastConsoleFlushed = buffers.consoleTotalAdded;
|
|
29917
30102
|
}
|
|
29918
30103
|
let newNetworkCount = buffers.networkTotalAdded - buffers.lastNetworkFlushed;
|
|
@@ -29937,7 +30122,7 @@ function flushSessionBuffers(session, final) {
|
|
|
29937
30122
|
const lines = prefix.map(
|
|
29938
30123
|
(e) => `[${new Date(e.timestamp).toISOString()}] ${e.method} ${e.url} \u2192 ${e.status || "pending"} (${e.duration || "?"}ms, ${e.size || "?"}B)`
|
|
29939
30124
|
).join("\n") + "\n";
|
|
29940
|
-
|
|
30125
|
+
fs28.appendFileSync(networkPath, lines);
|
|
29941
30126
|
buffers.lastNetworkFlushed += prefixLen;
|
|
29942
30127
|
}
|
|
29943
30128
|
}
|
|
@@ -29949,11 +30134,11 @@ function trySessionBt(session) {
|
|
|
29949
30134
|
return null;
|
|
29950
30135
|
}
|
|
29951
30136
|
function isPortFree2(port) {
|
|
29952
|
-
return new Promise((
|
|
30137
|
+
return new Promise((resolve10) => {
|
|
29953
30138
|
const srv = net3.createServer();
|
|
29954
|
-
srv.once("error", () =>
|
|
30139
|
+
srv.once("error", () => resolve10(false));
|
|
29955
30140
|
srv.once("listening", () => {
|
|
29956
|
-
srv.close(() =>
|
|
30141
|
+
srv.close(() => resolve10(true));
|
|
29957
30142
|
});
|
|
29958
30143
|
srv.listen(port, "127.0.0.1");
|
|
29959
30144
|
});
|
|
@@ -30156,9 +30341,9 @@ async function shutdown() {
|
|
|
30156
30341
|
await activeRuntime?.close?.().catch(() => {
|
|
30157
30342
|
});
|
|
30158
30343
|
try {
|
|
30159
|
-
const currentState = JSON.parse(
|
|
30344
|
+
const currentState = JSON.parse(fs28.readFileSync(STATE_FILE, "utf-8"));
|
|
30160
30345
|
if (currentState.pid === process.pid || currentState.token === AUTH_TOKEN) {
|
|
30161
|
-
|
|
30346
|
+
fs28.unlinkSync(STATE_FILE);
|
|
30162
30347
|
}
|
|
30163
30348
|
} catch {
|
|
30164
30349
|
}
|
|
@@ -30176,14 +30361,14 @@ async function start() {
|
|
|
30176
30361
|
const { SessionBuffers: SessionBuffers2 } = await Promise.resolve().then(() => (init_buffers(), buffers_exports));
|
|
30177
30362
|
const { createPersistentBrowserTarget: createPersistentBrowserTarget2 } = await Promise.resolve().then(() => (init_target_factory(), target_factory_exports));
|
|
30178
30363
|
const profileDir = getProfileDir2(LOCAL_DIR7, profileName);
|
|
30179
|
-
|
|
30364
|
+
fs28.mkdirSync(profileDir, { recursive: true });
|
|
30180
30365
|
const profileTarget = await createPersistentBrowserTarget2(profileDir, () => {
|
|
30181
30366
|
if (isShuttingDown) return;
|
|
30182
30367
|
console.error("[browse] Chromium disconnected (profile mode). Shutting down.");
|
|
30183
30368
|
shutdown();
|
|
30184
30369
|
});
|
|
30185
|
-
const outputDir =
|
|
30186
|
-
|
|
30370
|
+
const outputDir = path23.join(LOCAL_DIR7, "sessions", profileName);
|
|
30371
|
+
fs28.mkdirSync(outputDir, { recursive: true });
|
|
30187
30372
|
profileSession = {
|
|
30188
30373
|
id: profileName,
|
|
30189
30374
|
manager: profileTarget.target,
|
|
@@ -30325,7 +30510,7 @@ async function start() {
|
|
|
30325
30510
|
const context = sessionBt_?.getContext();
|
|
30326
30511
|
if (context) {
|
|
30327
30512
|
try {
|
|
30328
|
-
const stateData = JSON.parse(
|
|
30513
|
+
const stateData = JSON.parse(fs28.readFileSync(stateFilePath3, "utf-8"));
|
|
30329
30514
|
if (stateData.cookies?.length) {
|
|
30330
30515
|
await context.addCookies(stateData.cookies);
|
|
30331
30516
|
}
|
|
@@ -30361,7 +30546,7 @@ async function start() {
|
|
|
30361
30546
|
port,
|
|
30362
30547
|
token: AUTH_TOKEN,
|
|
30363
30548
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
30364
|
-
serverPath:
|
|
30549
|
+
serverPath: path23.resolve(path23.dirname((0, import_url7.fileURLToPath)(__import_meta_url)), "server.ts")
|
|
30365
30550
|
};
|
|
30366
30551
|
if (profileName) {
|
|
30367
30552
|
state.profile = profileName;
|
|
@@ -30369,12 +30554,12 @@ async function start() {
|
|
|
30369
30554
|
if (DEBUG_PORT > 0) {
|
|
30370
30555
|
state.debugPort = DEBUG_PORT;
|
|
30371
30556
|
}
|
|
30372
|
-
|
|
30557
|
+
fs28.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), { mode: 384 });
|
|
30373
30558
|
console.log(`[browse] Server running on http://127.0.0.1:${port} (PID: ${process.pid})`);
|
|
30374
30559
|
console.log(`[browse] State file: ${STATE_FILE}`);
|
|
30375
30560
|
console.log(`[browse] Idle timeout: ${IDLE_TIMEOUT_MS / 1e3}s`);
|
|
30376
30561
|
}
|
|
30377
|
-
var
|
|
30562
|
+
var fs28, path23, crypto3, http, import_url7, net3, AUTH_TOKEN, DEBUG_PORT, BROWSE_PORT, BROWSE_INSTANCE, INSTANCE_SUFFIX, LOCAL_DIR7, STATE_FILE, IDLE_TIMEOUT_MS, sessionManager, browser, profileSession, activeRuntime, isShuttingDown, isRemoteBrowser, policyChecker, BOUNDARY_NONCE, rewriteError2, flushInterval, sessionCleanupInterval;
|
|
30378
30563
|
var init_server2 = __esm({
|
|
30379
30564
|
"src/server.ts"() {
|
|
30380
30565
|
"use strict";
|
|
@@ -30384,8 +30569,8 @@ var init_server2 = __esm({
|
|
|
30384
30569
|
init_constants();
|
|
30385
30570
|
init_action_context();
|
|
30386
30571
|
init_executor();
|
|
30387
|
-
|
|
30388
|
-
|
|
30572
|
+
fs28 = __toESM(require("fs"), 1);
|
|
30573
|
+
path23 = __toESM(require("path"), 1);
|
|
30389
30574
|
crypto3 = __toESM(require("crypto"), 1);
|
|
30390
30575
|
http = __toESM(require("http"), 1);
|
|
30391
30576
|
import_url7 = require("url");
|
|
@@ -30447,9 +30632,9 @@ __export(cli_exports, {
|
|
|
30447
30632
|
resolveServerScript: () => resolveServerScript
|
|
30448
30633
|
});
|
|
30449
30634
|
module.exports = __toCommonJS(cli_exports);
|
|
30450
|
-
var
|
|
30451
|
-
var
|
|
30452
|
-
var
|
|
30635
|
+
var fs29 = __toESM(require("fs"), 1);
|
|
30636
|
+
var path24 = __toESM(require("path"), 1);
|
|
30637
|
+
var import_child_process12 = require("child_process");
|
|
30453
30638
|
var import_url8 = require("url");
|
|
30454
30639
|
init_constants();
|
|
30455
30640
|
init_config();
|
|
@@ -30483,50 +30668,50 @@ var INSTANCE_SUFFIX2 = BROWSE_PORT2 ? `-${BROWSE_PORT2}` : BROWSE_INSTANCE2 ? `-
|
|
|
30483
30668
|
function resolveLocalDir() {
|
|
30484
30669
|
if (process.env.BROWSE_LOCAL_DIR) {
|
|
30485
30670
|
try {
|
|
30486
|
-
|
|
30671
|
+
fs29.mkdirSync(process.env.BROWSE_LOCAL_DIR, { recursive: true });
|
|
30487
30672
|
} catch {
|
|
30488
30673
|
}
|
|
30489
30674
|
return process.env.BROWSE_LOCAL_DIR;
|
|
30490
30675
|
}
|
|
30491
30676
|
let dir = process.cwd();
|
|
30492
30677
|
for (let i = 0; i < 20; i++) {
|
|
30493
|
-
if (
|
|
30494
|
-
const browseDir =
|
|
30678
|
+
if (fs29.existsSync(path24.join(dir, ".git")) || fs29.existsSync(path24.join(dir, ".claude"))) {
|
|
30679
|
+
const browseDir = path24.join(dir, ".browse");
|
|
30495
30680
|
try {
|
|
30496
|
-
|
|
30497
|
-
const gi =
|
|
30498
|
-
if (!
|
|
30499
|
-
|
|
30681
|
+
fs29.mkdirSync(browseDir, { recursive: true });
|
|
30682
|
+
const gi = path24.join(browseDir, ".gitignore");
|
|
30683
|
+
if (!fs29.existsSync(gi)) {
|
|
30684
|
+
fs29.writeFileSync(gi, "*\n");
|
|
30500
30685
|
}
|
|
30501
30686
|
} catch {
|
|
30502
30687
|
}
|
|
30503
30688
|
return browseDir;
|
|
30504
30689
|
}
|
|
30505
|
-
const parent =
|
|
30690
|
+
const parent = path24.dirname(dir);
|
|
30506
30691
|
if (parent === dir) break;
|
|
30507
30692
|
dir = parent;
|
|
30508
30693
|
}
|
|
30509
30694
|
return "/tmp";
|
|
30510
30695
|
}
|
|
30511
30696
|
var LOCAL_DIR8 = resolveLocalDir();
|
|
30512
|
-
var STATE_FILE2 = process.env.BROWSE_STATE_FILE ||
|
|
30697
|
+
var STATE_FILE2 = process.env.BROWSE_STATE_FILE || path24.join(LOCAL_DIR8, `browse-server${INSTANCE_SUFFIX2}.json`);
|
|
30513
30698
|
var MAX_START_WAIT = parseInt(process.env.BROWSE_START_TIMEOUT || "0", 10) || 8e3;
|
|
30514
30699
|
var LOCK_FILE = STATE_FILE2 + ".lock";
|
|
30515
30700
|
var LOCK_STALE_MS = DEFAULTS.LOCK_STALE_THRESHOLD_MS;
|
|
30516
30701
|
var __filename_cli = (0, import_url8.fileURLToPath)(__import_meta_url);
|
|
30517
|
-
var __dirname_cli =
|
|
30702
|
+
var __dirname_cli = path24.dirname(__filename_cli);
|
|
30518
30703
|
function resolveServerScript(env = process.env, metaDir = __dirname_cli) {
|
|
30519
30704
|
if (env.BROWSE_SERVER_SCRIPT) {
|
|
30520
30705
|
return env.BROWSE_SERVER_SCRIPT;
|
|
30521
30706
|
}
|
|
30522
30707
|
if (metaDir.startsWith("/")) {
|
|
30523
|
-
const direct =
|
|
30524
|
-
if (
|
|
30708
|
+
const direct = path24.resolve(metaDir, "server.ts");
|
|
30709
|
+
if (fs29.existsSync(direct)) {
|
|
30525
30710
|
return direct;
|
|
30526
30711
|
}
|
|
30527
30712
|
}
|
|
30528
30713
|
const selfPath = (0, import_url8.fileURLToPath)(__import_meta_url);
|
|
30529
|
-
if (
|
|
30714
|
+
if (fs29.existsSync(selfPath)) {
|
|
30530
30715
|
return "__self__";
|
|
30531
30716
|
}
|
|
30532
30717
|
throw new Error(
|
|
@@ -30536,7 +30721,7 @@ function resolveServerScript(env = process.env, metaDir = __dirname_cli) {
|
|
|
30536
30721
|
var SERVER_SCRIPT = resolveServerScript();
|
|
30537
30722
|
function readState3() {
|
|
30538
30723
|
try {
|
|
30539
|
-
const data =
|
|
30724
|
+
const data = fs29.readFileSync(STATE_FILE2, "utf-8");
|
|
30540
30725
|
return JSON.parse(data);
|
|
30541
30726
|
} catch {
|
|
30542
30727
|
return null;
|
|
@@ -30552,7 +30737,7 @@ function isProcessAlive(pid) {
|
|
|
30552
30737
|
}
|
|
30553
30738
|
async function listInstances() {
|
|
30554
30739
|
try {
|
|
30555
|
-
const files =
|
|
30740
|
+
const files = fs29.readdirSync(LOCAL_DIR8).filter(
|
|
30556
30741
|
(f) => f.startsWith("browse-server") && f.endsWith(".json") && !f.endsWith(".lock")
|
|
30557
30742
|
);
|
|
30558
30743
|
if (files.length === 0) {
|
|
@@ -30562,7 +30747,7 @@ async function listInstances() {
|
|
|
30562
30747
|
let found = false;
|
|
30563
30748
|
for (const file of files) {
|
|
30564
30749
|
try {
|
|
30565
|
-
const data = JSON.parse(
|
|
30750
|
+
const data = JSON.parse(fs29.readFileSync(path24.join(LOCAL_DIR8, file), "utf-8"));
|
|
30566
30751
|
if (!data.pid || !data.port) continue;
|
|
30567
30752
|
const alive = isProcessAlive(data.pid);
|
|
30568
30753
|
let status3 = "dead";
|
|
@@ -30585,7 +30770,7 @@ async function listInstances() {
|
|
|
30585
30770
|
found = true;
|
|
30586
30771
|
if (!alive) {
|
|
30587
30772
|
try {
|
|
30588
|
-
|
|
30773
|
+
fs29.unlinkSync(path24.join(LOCAL_DIR8, file));
|
|
30589
30774
|
} catch {
|
|
30590
30775
|
}
|
|
30591
30776
|
}
|
|
@@ -30599,8 +30784,8 @@ async function listInstances() {
|
|
|
30599
30784
|
}
|
|
30600
30785
|
function isBrowseProcess(pid) {
|
|
30601
30786
|
try {
|
|
30602
|
-
const { execSync:
|
|
30603
|
-
const cmd =
|
|
30787
|
+
const { execSync: execSync8 } = require("child_process");
|
|
30788
|
+
const cmd = execSync8(`ps -p ${pid} -o command=`, { encoding: "utf-8" }).trim();
|
|
30604
30789
|
return cmd.includes("browse") || cmd.includes("__BROWSE_SERVER_MODE");
|
|
30605
30790
|
} catch {
|
|
30606
30791
|
return false;
|
|
@@ -30608,15 +30793,15 @@ function isBrowseProcess(pid) {
|
|
|
30608
30793
|
}
|
|
30609
30794
|
function acquireLock() {
|
|
30610
30795
|
try {
|
|
30611
|
-
|
|
30796
|
+
fs29.writeFileSync(LOCK_FILE, String(process.pid), { flag: "wx" });
|
|
30612
30797
|
return true;
|
|
30613
30798
|
} catch (err) {
|
|
30614
30799
|
if (err.code === "EEXIST") {
|
|
30615
30800
|
try {
|
|
30616
|
-
const stat =
|
|
30801
|
+
const stat = fs29.statSync(LOCK_FILE);
|
|
30617
30802
|
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
30618
30803
|
try {
|
|
30619
|
-
|
|
30804
|
+
fs29.unlinkSync(LOCK_FILE);
|
|
30620
30805
|
} catch {
|
|
30621
30806
|
}
|
|
30622
30807
|
return acquireLock();
|
|
@@ -30630,7 +30815,7 @@ function acquireLock() {
|
|
|
30630
30815
|
}
|
|
30631
30816
|
function releaseLock() {
|
|
30632
30817
|
try {
|
|
30633
|
-
|
|
30818
|
+
fs29.unlinkSync(LOCK_FILE);
|
|
30634
30819
|
} catch {
|
|
30635
30820
|
}
|
|
30636
30821
|
}
|
|
@@ -30647,7 +30832,7 @@ async function startServer() {
|
|
|
30647
30832
|
}
|
|
30648
30833
|
await sleep3(100);
|
|
30649
30834
|
}
|
|
30650
|
-
if (!
|
|
30835
|
+
if (!fs29.existsSync(LOCK_FILE) || fs29.readFileSync(LOCK_FILE, "utf-8").trim() !== String(process.pid)) {
|
|
30651
30836
|
const state = readState3();
|
|
30652
30837
|
if (state && isProcessAlive(state.pid)) return state;
|
|
30653
30838
|
throw new Error("Server failed to start (another process is starting it)");
|
|
@@ -30657,7 +30842,7 @@ async function startServer() {
|
|
|
30657
30842
|
try {
|
|
30658
30843
|
const oldState = readState3();
|
|
30659
30844
|
if (oldState && !isProcessAlive(oldState.pid)) {
|
|
30660
|
-
|
|
30845
|
+
fs29.unlinkSync(STATE_FILE2);
|
|
30661
30846
|
}
|
|
30662
30847
|
} catch {
|
|
30663
30848
|
}
|
|
@@ -30666,7 +30851,7 @@ async function startServer() {
|
|
|
30666
30851
|
const spawnCmd = SERVER_SCRIPT === "__self__" ? [nodeExec, selfPath] : [nodeExec, "--import", "tsx", SERVER_SCRIPT];
|
|
30667
30852
|
const startTimeout = cliFlags.runtime === "chrome" ? String(DEFAULTS.CHROME_CDP_TIMEOUT_MS + 5e3) : "";
|
|
30668
30853
|
const spawnEnv = { ...process.env, __BROWSE_SERVER_MODE: "1", BROWSE_LOCAL_DIR: LOCAL_DIR8, BROWSE_INSTANCE: BROWSE_INSTANCE2, ...cliFlags.headed ? { BROWSE_HEADED: "1" } : {}, ...cliFlags.cdpUrl ? { BROWSE_CDP_URL: cliFlags.cdpUrl } : {}, ...cliFlags.profile ? { BROWSE_PROFILE: cliFlags.profile } : {}, ...cliFlags.runtime ? { BROWSE_RUNTIME: cliFlags.runtime } : {}, ...startTimeout ? { BROWSE_START_TIMEOUT: startTimeout } : {} };
|
|
30669
|
-
const proc = (0,
|
|
30854
|
+
const proc = (0, import_child_process12.spawn)(spawnCmd[0], spawnCmd.slice(1), {
|
|
30670
30855
|
stdio: ["ignore", "ignore", "pipe"],
|
|
30671
30856
|
env: spawnEnv,
|
|
30672
30857
|
detached: true
|
|
@@ -30743,7 +30928,7 @@ async function ensureServer() {
|
|
|
30743
30928
|
}
|
|
30744
30929
|
if (state) {
|
|
30745
30930
|
try {
|
|
30746
|
-
|
|
30931
|
+
fs29.unlinkSync(STATE_FILE2);
|
|
30747
30932
|
} catch {
|
|
30748
30933
|
}
|
|
30749
30934
|
}
|
|
@@ -30753,21 +30938,21 @@ async function ensureServer() {
|
|
|
30753
30938
|
}
|
|
30754
30939
|
function cleanOrphanedServers() {
|
|
30755
30940
|
try {
|
|
30756
|
-
const files =
|
|
30941
|
+
const files = fs29.readdirSync(LOCAL_DIR8);
|
|
30757
30942
|
for (const file of files) {
|
|
30758
30943
|
if (!file.startsWith("browse-server") || !file.endsWith(".json") || file.endsWith(".lock")) continue;
|
|
30759
|
-
const filePath =
|
|
30944
|
+
const filePath = path24.join(LOCAL_DIR8, file);
|
|
30760
30945
|
if (filePath === STATE_FILE2) continue;
|
|
30761
30946
|
try {
|
|
30762
|
-
const data = JSON.parse(
|
|
30947
|
+
const data = JSON.parse(fs29.readFileSync(filePath, "utf-8"));
|
|
30763
30948
|
if (!data.pid) {
|
|
30764
|
-
|
|
30949
|
+
fs29.unlinkSync(filePath);
|
|
30765
30950
|
continue;
|
|
30766
30951
|
}
|
|
30767
30952
|
const suffixMatch = file.match(/browse-server-(\d+)\.json$/);
|
|
30768
30953
|
if (suffixMatch && data.port === parseInt(suffixMatch[1], 10) && isProcessAlive(data.pid)) continue;
|
|
30769
30954
|
if (!isProcessAlive(data.pid)) {
|
|
30770
|
-
|
|
30955
|
+
fs29.unlinkSync(filePath);
|
|
30771
30956
|
continue;
|
|
30772
30957
|
}
|
|
30773
30958
|
if (isBrowseProcess(data.pid)) {
|
|
@@ -30778,7 +30963,7 @@ function cleanOrphanedServers() {
|
|
|
30778
30963
|
}
|
|
30779
30964
|
} catch {
|
|
30780
30965
|
try {
|
|
30781
|
-
|
|
30966
|
+
fs29.unlinkSync(filePath);
|
|
30782
30967
|
} catch {
|
|
30783
30968
|
}
|
|
30784
30969
|
}
|
|
@@ -30898,7 +31083,7 @@ async function sendCommand(state, command, args, retries = 0, sessionId) {
|
|
|
30898
31083
|
await sleep3(300);
|
|
30899
31084
|
}
|
|
30900
31085
|
try {
|
|
30901
|
-
|
|
31086
|
+
fs29.unlinkSync(STATE_FILE2);
|
|
30902
31087
|
} catch {
|
|
30903
31088
|
}
|
|
30904
31089
|
if (command === "restart") {
|
|
@@ -31259,7 +31444,7 @@ if (process.argv.includes("--mcp")) {
|
|
|
31259
31444
|
Promise.resolve().then(() => (init_mcp(), mcp_exports)).then((m2) => m2.startMcpServer(jsonMode));
|
|
31260
31445
|
} else if (process.env.__BROWSE_SERVER_MODE === "1") {
|
|
31261
31446
|
Promise.resolve().then(() => init_server2());
|
|
31262
|
-
} else if (process.argv[1] &&
|
|
31447
|
+
} else if (process.argv[1] && fs29.realpathSync(process.argv[1]) === fs29.realpathSync(__filename_cli)) {
|
|
31263
31448
|
main().catch((err) => {
|
|
31264
31449
|
console.error(`[browse] ${err.message}`);
|
|
31265
31450
|
process.exit(1);
|