@ulpi/browse 2.3.2 → 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 +1243 -1038
- 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,396 +2107,217 @@ 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
|
-
|
|
1781
|
-
}
|
|
1782
|
-
function clearState() {
|
|
1783
|
-
try {
|
|
1784
|
-
fs9.unlinkSync(stateFilePath());
|
|
1785
|
-
} catch {
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
async function checkHealth(port = DRIVER_PORT2) {
|
|
1789
|
-
try {
|
|
1790
|
-
const resp = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(2e3) });
|
|
1791
|
-
return resp.ok;
|
|
1792
|
-
} catch {
|
|
1793
|
-
return false;
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
|
-
async function status() {
|
|
1797
|
-
const state = readState();
|
|
1798
|
-
if (!state) return { running: false, state: null, healthy: false };
|
|
1799
|
-
const healthy = await checkHealth(state.port);
|
|
1800
|
-
if (!healthy) {
|
|
1801
|
-
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
|
-
}
|
|
2072
|
-
}
|
|
2073
|
-
await simctl(...args);
|
|
2132
|
+
const dir = path10.dirname(stateFilePath());
|
|
2133
|
+
fs10.mkdirSync(dir, { recursive: true });
|
|
2134
|
+
fs10.writeFileSync(stateFilePath(), JSON.stringify(state, null, 2));
|
|
2074
2135
|
}
|
|
2075
|
-
|
|
2136
|
+
function clearState() {
|
|
2076
2137
|
try {
|
|
2077
|
-
|
|
2138
|
+
fs10.unlinkSync(stateFilePath());
|
|
2078
2139
|
} catch {
|
|
2079
2140
|
}
|
|
2080
2141
|
}
|
|
2081
|
-
async function
|
|
2142
|
+
async function checkHealth(port = DRIVER_PORT2) {
|
|
2082
2143
|
try {
|
|
2083
|
-
await
|
|
2084
|
-
return
|
|
2144
|
+
const resp = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(2e3) });
|
|
2145
|
+
return resp.ok;
|
|
2085
2146
|
} catch {
|
|
2086
2147
|
return false;
|
|
2087
2148
|
}
|
|
2088
2149
|
}
|
|
2089
|
-
async function
|
|
2090
|
-
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
}
|
|
2098
|
-
async function screenshotSimulator(udid, outputPath) {
|
|
2099
|
-
await simctl("io", udid, "screenshot", outputPath);
|
|
2100
|
-
}
|
|
2101
|
-
async function getAppContainer(udid, bundleId, container = "data") {
|
|
2102
|
-
return simctl("get_app_container", udid, bundleId, container);
|
|
2103
|
-
}
|
|
2104
|
-
async function openURL(udid, url) {
|
|
2105
|
-
await simctl("openurl", udid, url);
|
|
2106
|
-
}
|
|
2107
|
-
async function addMedia(udid, ...paths) {
|
|
2108
|
-
await simctl("addmedia", udid, ...paths);
|
|
2150
|
+
async function status() {
|
|
2151
|
+
const state = readState();
|
|
2152
|
+
if (!state) return { running: false, state: null, healthy: false };
|
|
2153
|
+
const healthy = await checkHealth(state.port);
|
|
2154
|
+
if (!healthy) {
|
|
2155
|
+
clearState();
|
|
2156
|
+
return { running: false, state, healthy: false };
|
|
2157
|
+
}
|
|
2158
|
+
return { running: true, state, healthy: true };
|
|
2109
2159
|
}
|
|
2110
|
-
async function
|
|
2111
|
-
const
|
|
2112
|
-
if (
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2160
|
+
async function stopDriver() {
|
|
2161
|
+
const state = readState();
|
|
2162
|
+
if (!state) return;
|
|
2163
|
+
try {
|
|
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 });
|
|
2167
|
+
} catch {
|
|
2168
|
+
}
|
|
2169
|
+
clearState();
|
|
2119
2170
|
}
|
|
2120
|
-
async function
|
|
2121
|
-
|
|
2171
|
+
async function stop() {
|
|
2172
|
+
const state = readState();
|
|
2173
|
+
if (!state) return "No Android device/emulator running.";
|
|
2174
|
+
try {
|
|
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
|
+
}
|
|
2181
|
+
} catch {
|
|
2182
|
+
}
|
|
2183
|
+
clearState();
|
|
2184
|
+
return `Android stopped (${state.device}).`;
|
|
2122
2185
|
}
|
|
2123
|
-
async function
|
|
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
|
}
|
|
@@ -2673,14 +2857,118 @@ async function handleSimCLI(args) {
|
|
|
2673
2857
|
console.error(`Unknown platform: '${platform}'. Use ios or android.`);
|
|
2674
2858
|
process.exit(1);
|
|
2675
2859
|
}
|
|
2676
|
-
return;
|
|
2677
|
-
}
|
|
2678
|
-
console.error(`Unknown sim subcommand: ${sub}`);
|
|
2679
|
-
process.exit(1);
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2862
|
+
console.error(`Unknown sim subcommand: ${sub}`);
|
|
2863
|
+
process.exit(1);
|
|
2864
|
+
}
|
|
2865
|
+
var init_sim_cli = __esm({
|
|
2866
|
+
"src/sim-cli.ts"() {
|
|
2867
|
+
"use strict";
|
|
2868
|
+
}
|
|
2869
|
+
});
|
|
2870
|
+
|
|
2871
|
+
// src/app/macos/bridge.ts
|
|
2872
|
+
var bridge_exports3 = {};
|
|
2873
|
+
__export(bridge_exports3, {
|
|
2874
|
+
createMacOSBridge: () => createMacOSBridge,
|
|
2875
|
+
ensureMacOSBridge: () => ensureMacOSBridge,
|
|
2876
|
+
resolveBridgePath: () => resolveBridgePath
|
|
2877
|
+
});
|
|
2878
|
+
function resolveBridgePath() {
|
|
2879
|
+
const candidates = [
|
|
2880
|
+
// 1. Local dev build
|
|
2881
|
+
path12.resolve(__dirname_bridge, "../../../browse-ax/.build/release/browse-ax"),
|
|
2882
|
+
path12.resolve(__dirname_bridge, "../../../browse-ax/.build/debug/browse-ax"),
|
|
2883
|
+
// 2. Installed alongside source (bin/ at project root)
|
|
2884
|
+
path12.resolve(__dirname_bridge, "../../bin/browse-ax"),
|
|
2885
|
+
// 3. Bundled build (dist/browse.cjs → ../bin/)
|
|
2886
|
+
path12.resolve(__dirname_bridge, "../bin/browse-ax")
|
|
2887
|
+
];
|
|
2888
|
+
for (const p of candidates) {
|
|
2889
|
+
if (fs12.existsSync(p)) return p;
|
|
2890
|
+
}
|
|
2891
|
+
const lazyPath = path12.join(
|
|
2892
|
+
process.env.BROWSE_LOCAL_DIR || path12.join(process.cwd(), ".browse"),
|
|
2893
|
+
"bin",
|
|
2894
|
+
"browse-ax"
|
|
2895
|
+
);
|
|
2896
|
+
if (fs12.existsSync(lazyPath)) return lazyPath;
|
|
2897
|
+
throw new Error(
|
|
2898
|
+
"browse-ax binary not found. Run: browse enable macos\nOr build manually: cd browse-ax && swift build -c release"
|
|
2899
|
+
);
|
|
2900
|
+
}
|
|
2901
|
+
async function ensureMacOSBridge() {
|
|
2902
|
+
if (process.platform !== "darwin") {
|
|
2903
|
+
throw new Error("App automation requires macOS (uses Accessibility API)");
|
|
2904
|
+
}
|
|
2905
|
+
return resolveBridgePath();
|
|
2906
|
+
}
|
|
2907
|
+
async function execBridge(bridgePath, args) {
|
|
2908
|
+
return new Promise((resolve10, reject) => {
|
|
2909
|
+
const proc = (0, import_child_process9.spawn)(bridgePath, args, {
|
|
2910
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2911
|
+
});
|
|
2912
|
+
const chunks = [];
|
|
2913
|
+
const errChunks = [];
|
|
2914
|
+
proc.stdout.on("data", (c) => chunks.push(c));
|
|
2915
|
+
proc.stderr.on("data", (c) => errChunks.push(c));
|
|
2916
|
+
proc.on("close", (code) => {
|
|
2917
|
+
const stdout = Buffer.concat(chunks).toString("utf-8").trim();
|
|
2918
|
+
const stderr = Buffer.concat(errChunks).toString("utf-8").trim();
|
|
2919
|
+
if (code !== 0) {
|
|
2920
|
+
try {
|
|
2921
|
+
const err = JSON.parse(stderr || stdout);
|
|
2922
|
+
reject(new Error(err.error || `Bridge exited with code ${code}`));
|
|
2923
|
+
} catch {
|
|
2924
|
+
reject(new Error(stderr || `Bridge exited with code ${code}`));
|
|
2925
|
+
}
|
|
2926
|
+
return;
|
|
2927
|
+
}
|
|
2928
|
+
try {
|
|
2929
|
+
resolve10(JSON.parse(stdout));
|
|
2930
|
+
} catch {
|
|
2931
|
+
reject(new Error(`Invalid bridge output: ${stdout.slice(0, 200)}`));
|
|
2932
|
+
}
|
|
2933
|
+
});
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2936
|
+
function createMacOSBridge(bridgePath, pid) {
|
|
2937
|
+
const base = ["--pid", String(pid)];
|
|
2938
|
+
return {
|
|
2939
|
+
async tree() {
|
|
2940
|
+
return execBridge(bridgePath, [...base, "tree"]);
|
|
2941
|
+
},
|
|
2942
|
+
async action(nodePath, actionName) {
|
|
2943
|
+
return execBridge(bridgePath, [...base, "action", JSON.stringify(nodePath), actionName]);
|
|
2944
|
+
},
|
|
2945
|
+
async setValue(nodePath, value) {
|
|
2946
|
+
return execBridge(bridgePath, [...base, "set-value", JSON.stringify(nodePath), value]);
|
|
2947
|
+
},
|
|
2948
|
+
async type(text) {
|
|
2949
|
+
return execBridge(bridgePath, [...base, "type", text]);
|
|
2950
|
+
},
|
|
2951
|
+
async press(key) {
|
|
2952
|
+
return execBridge(bridgePath, [...base, "press", key]);
|
|
2953
|
+
},
|
|
2954
|
+
async screenshot(outputPath) {
|
|
2955
|
+
return execBridge(bridgePath, [...base, "screenshot", outputPath]);
|
|
2956
|
+
},
|
|
2957
|
+
async state() {
|
|
2958
|
+
return execBridge(bridgePath, [...base, "state"]);
|
|
2959
|
+
}
|
|
2960
|
+
};
|
|
2680
2961
|
}
|
|
2681
|
-
var
|
|
2682
|
-
|
|
2962
|
+
var import_child_process9, path12, fs12, import_url5, __filename_bridge2, __dirname_bridge;
|
|
2963
|
+
var init_bridge3 = __esm({
|
|
2964
|
+
"src/app/macos/bridge.ts"() {
|
|
2683
2965
|
"use strict";
|
|
2966
|
+
import_child_process9 = require("child_process");
|
|
2967
|
+
path12 = __toESM(require("path"), 1);
|
|
2968
|
+
fs12 = __toESM(require("fs"), 1);
|
|
2969
|
+
import_url5 = require("url");
|
|
2970
|
+
__filename_bridge2 = (0, import_url5.fileURLToPath)(__import_meta_url);
|
|
2971
|
+
__dirname_bridge = path12.dirname(__filename_bridge2);
|
|
2684
2972
|
}
|
|
2685
2973
|
});
|
|
2686
2974
|
|
|
@@ -2689,153 +2977,173 @@ var enable_exports = {};
|
|
|
2689
2977
|
__export(enable_exports, {
|
|
2690
2978
|
handleEnable: () => handleEnable
|
|
2691
2979
|
});
|
|
2980
|
+
function findPath(candidates) {
|
|
2981
|
+
return candidates.find((p) => fs13.existsSync(p)) || null;
|
|
2982
|
+
}
|
|
2692
2983
|
async function enableAndroid() {
|
|
2693
2984
|
log("Enabling Android...");
|
|
2694
|
-
const
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2985
|
+
const prebuiltApk = findPath([
|
|
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")
|
|
2989
|
+
]);
|
|
2990
|
+
if (prebuiltApk) {
|
|
2991
|
+
log(`Driver APK: ${prebuiltApk}`);
|
|
2992
|
+
log("Android ready (pre-built).");
|
|
2993
|
+
return;
|
|
2703
2994
|
}
|
|
2704
|
-
|
|
2705
|
-
ensureJavaHome2();
|
|
2995
|
+
log("Pre-built APK not found, building from source...");
|
|
2996
|
+
const { ensureJavaHome: ensureJavaHome2, findSdkRoot: findSdkRoot2, installSdk: installSdk2, createAvd: createAvd2 } = await Promise.resolve().then(() => (init_emulator(), emulator_exports));
|
|
2997
|
+
const { installAdb: installAdb2 } = await Promise.resolve().then(() => (init_bridge(), bridge_exports));
|
|
2706
2998
|
try {
|
|
2707
|
-
(0,
|
|
2708
|
-
log("
|
|
2999
|
+
(0, import_child_process10.execSync)("adb version", { stdio: "ignore", timeout: 5e3 });
|
|
3000
|
+
log("adb: available");
|
|
2709
3001
|
} catch {
|
|
2710
|
-
log("
|
|
3002
|
+
log("Installing adb...");
|
|
3003
|
+
await installAdb2(log);
|
|
3004
|
+
}
|
|
3005
|
+
ensureJavaHome2();
|
|
3006
|
+
const hasJava = () => {
|
|
3007
|
+
try {
|
|
3008
|
+
(0, import_child_process10.execSync)("java -version", { stdio: "ignore", timeout: 5e3 });
|
|
3009
|
+
return true;
|
|
3010
|
+
} catch {
|
|
3011
|
+
return false;
|
|
3012
|
+
}
|
|
3013
|
+
};
|
|
3014
|
+
if (!hasJava() && process.platform === "darwin") {
|
|
3015
|
+
log("Installing Java (JDK 21)...");
|
|
3016
|
+
try {
|
|
3017
|
+
(0, import_child_process10.execSync)("brew install openjdk@21", { stdio: ["ignore", "pipe", "pipe"], timeout: 3e5 });
|
|
3018
|
+
} catch {
|
|
3019
|
+
}
|
|
3020
|
+
ensureJavaHome2();
|
|
2711
3021
|
}
|
|
3022
|
+
if (!hasJava()) throw new Error("Java not found. Install: brew install openjdk@21");
|
|
3023
|
+
log("Java: available");
|
|
2712
3024
|
let sdkRoot = findSdkRoot2();
|
|
2713
|
-
if (!sdkRoot)
|
|
2714
|
-
|
|
2715
|
-
}
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
3025
|
+
if (!sdkRoot) sdkRoot = await installSdk2(log);
|
|
3026
|
+
if (!sdkRoot) throw new Error("Android SDK not found.");
|
|
3027
|
+
log(`SDK: ${sdkRoot}`);
|
|
3028
|
+
const sdkMgr = findPath([
|
|
3029
|
+
path13.join(sdkRoot, "cmdline-tools/latest/bin/sdkmanager"),
|
|
3030
|
+
path13.join(sdkRoot, "bin/sdkmanager")
|
|
3031
|
+
]);
|
|
3032
|
+
if (sdkMgr) {
|
|
3033
|
+
try {
|
|
3034
|
+
(0, import_child_process10.execSync)(`yes | "${sdkMgr}" --licenses`, { stdio: "ignore", timeout: 6e4, shell: "/bin/bash" });
|
|
3035
|
+
} catch {
|
|
3036
|
+
}
|
|
3037
|
+
for (const comp of ["emulator", "platform-tools", "platforms;android-35", "build-tools;35.0.0", "system-images;android-35;google_apis;arm64-v8a"]) {
|
|
2723
3038
|
try {
|
|
2724
|
-
(0,
|
|
3039
|
+
(0, import_child_process10.execSync)(`"${sdkMgr}" --install "${comp}"`, { stdio: "ignore", timeout: 3e5 });
|
|
2725
3040
|
} catch {
|
|
2726
3041
|
}
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
const avds = (0, import_child_process8.execSync)(`"${avdMgr}" list avd -c`, { encoding: "utf-8", timeout: 1e4, stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
2742
|
-
if (!avds.includes("browse_default")) {
|
|
2743
|
-
createAvd2(sdkRoot, avdMgr, log);
|
|
2744
|
-
} else {
|
|
2745
|
-
log("AVD browse_default: exists");
|
|
2746
|
-
}
|
|
2747
|
-
} catch {
|
|
2748
|
-
}
|
|
3042
|
+
}
|
|
3043
|
+
log("SDK components: installed");
|
|
3044
|
+
}
|
|
3045
|
+
const avdMgr = findPath([
|
|
3046
|
+
path13.join(sdkRoot, "cmdline-tools/latest/bin/avdmanager"),
|
|
3047
|
+
path13.join(sdkRoot, "bin/avdmanager")
|
|
3048
|
+
]);
|
|
3049
|
+
if (avdMgr) {
|
|
3050
|
+
try {
|
|
3051
|
+
const avds = (0, import_child_process10.execSync)(`"${avdMgr}" list avd -c`, { encoding: "utf-8", timeout: 1e4, stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
3052
|
+
if (!avds.includes("browse_default")) {
|
|
3053
|
+
createAvd2(sdkRoot, avdMgr, log);
|
|
3054
|
+
} else {
|
|
3055
|
+
log("AVD: browse_default exists");
|
|
2749
3056
|
}
|
|
3057
|
+
} catch {
|
|
2750
3058
|
}
|
|
2751
3059
|
}
|
|
2752
|
-
const driverDir =
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
if (sdkRoot2) process.env.ANDROID_HOME = sdkRoot2;
|
|
3060
|
+
const driverDir = findPath([
|
|
3061
|
+
path13.resolve(__dirname_enable, "../browse-android"),
|
|
3062
|
+
path13.resolve(__dirname_enable, "../../browse-android")
|
|
3063
|
+
].filter((d) => fs13.existsSync(path13.join(d, "gradlew"))));
|
|
3064
|
+
if (driverDir) {
|
|
3065
|
+
if (!process.env.ANDROID_HOME) process.env.ANDROID_HOME = sdkRoot;
|
|
2759
3066
|
log("Building Android driver APK...");
|
|
2760
3067
|
try {
|
|
2761
|
-
(0,
|
|
3068
|
+
(0, import_child_process10.execSync)("./gradlew :app:assembleDebug :app:assembleDebugAndroidTest --no-daemon", {
|
|
2762
3069
|
cwd: driverDir,
|
|
2763
3070
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2764
3071
|
timeout: 3e5
|
|
2765
3072
|
});
|
|
2766
|
-
log("
|
|
3073
|
+
log("APK built.");
|
|
2767
3074
|
} catch (err) {
|
|
2768
3075
|
log(`APK build failed: ${err.message?.split("\n")[0]}`);
|
|
2769
3076
|
}
|
|
2770
|
-
} else if (fs11.existsSync(apkPath)) {
|
|
2771
|
-
log("Android driver APK: already built");
|
|
2772
3077
|
}
|
|
2773
3078
|
log("Android enabled.");
|
|
2774
3079
|
}
|
|
2775
3080
|
async function enableIOS() {
|
|
2776
3081
|
log("Enabling iOS...");
|
|
2777
|
-
if (process.platform !== "darwin")
|
|
2778
|
-
throw new Error("iOS requires macOS with Xcode installed.");
|
|
2779
|
-
}
|
|
3082
|
+
if (process.platform !== "darwin") throw new Error("iOS requires macOS with Xcode.");
|
|
2780
3083
|
try {
|
|
2781
|
-
(0,
|
|
3084
|
+
(0, import_child_process10.execSync)("xcodebuild -version", { stdio: "pipe", timeout: 1e4 });
|
|
2782
3085
|
log("Xcode: available");
|
|
2783
3086
|
} catch {
|
|
2784
|
-
throw new Error("Xcode not found. Install from the App Store
|
|
3087
|
+
throw new Error("Xcode not found. Install from the App Store.");
|
|
2785
3088
|
}
|
|
2786
3089
|
try {
|
|
2787
|
-
(0,
|
|
2788
|
-
log("xcodegen: available");
|
|
3090
|
+
(0, import_child_process10.execSync)("which xcodegen", { stdio: "pipe", timeout: 5e3 });
|
|
2789
3091
|
} catch {
|
|
2790
3092
|
log("Installing xcodegen...");
|
|
2791
3093
|
try {
|
|
2792
|
-
(0,
|
|
3094
|
+
(0, import_child_process10.execSync)("brew install xcodegen", { stdio: ["ignore", "pipe", "pipe"], timeout: 12e4 });
|
|
2793
3095
|
} catch {
|
|
2794
3096
|
throw new Error("xcodegen not found. Install: brew install xcodegen");
|
|
2795
3097
|
}
|
|
2796
3098
|
}
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
3099
|
+
log("xcodegen: available");
|
|
3100
|
+
const runnerDir = findPath([
|
|
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"))));
|
|
3105
|
+
if (!runnerDir) {
|
|
3106
|
+
throw new Error("iOS runner source not found. Reinstall: npm install -g @ulpi/browse");
|
|
3107
|
+
}
|
|
3108
|
+
if (!fs13.existsSync(path13.join(runnerDir, "BrowseRunner.xcodeproj", "project.pbxproj"))) {
|
|
3109
|
+
log("Generating Xcode project...");
|
|
3110
|
+
(0, import_child_process10.execSync)("xcodegen generate --spec project.yml", { cwd: runnerDir, stdio: "pipe" });
|
|
3111
|
+
}
|
|
3112
|
+
const { resolveSimulator: resolveSimulator2 } = await Promise.resolve().then(() => (init_controller(), controller_exports));
|
|
3113
|
+
try {
|
|
3114
|
+
const sim = await resolveSimulator2();
|
|
2813
3115
|
log("Building iOS runner...");
|
|
2814
|
-
(0,
|
|
2815
|
-
`xcodebuild build-for-testing -project BrowseRunner.xcodeproj -scheme BrowseRunnerApp -sdk iphonesimulator -destination "id=${udid}" -derivedDataPath .build CODE_SIGN_IDENTITY="" CODE_SIGNING_ALLOWED=NO -quiet`,
|
|
3116
|
+
(0, import_child_process10.execSync)(
|
|
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`,
|
|
2816
3118
|
{ cwd: runnerDir, stdio: "pipe", timeout: 12e4 }
|
|
2817
3119
|
);
|
|
2818
3120
|
log("iOS runner built.");
|
|
3121
|
+
} catch {
|
|
3122
|
+
log("No simulator found. Runner will build on first sim start.");
|
|
2819
3123
|
}
|
|
2820
3124
|
log("iOS enabled.");
|
|
2821
3125
|
}
|
|
2822
3126
|
async function enableMacOS() {
|
|
2823
3127
|
log("Enabling macOS...");
|
|
2824
|
-
if (process.platform !== "darwin")
|
|
2825
|
-
|
|
3128
|
+
if (process.platform !== "darwin") throw new Error("macOS app automation requires macOS.");
|
|
3129
|
+
const { resolveBridgePath: resolveBridgePath2 } = await Promise.resolve().then(() => (init_bridge3(), bridge_exports3));
|
|
3130
|
+
try {
|
|
3131
|
+
const existing = resolveBridgePath2();
|
|
3132
|
+
log(`browse-ax: ${existing}`);
|
|
3133
|
+
log("macOS enabled.");
|
|
3134
|
+
return;
|
|
3135
|
+
} catch {
|
|
2826
3136
|
}
|
|
2827
|
-
const axDir =
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
log("browse-ax built.");
|
|
2836
|
-
}
|
|
3137
|
+
const axDir = findPath([
|
|
3138
|
+
path13.resolve(__dirname_enable, "../browse-ax"),
|
|
3139
|
+
path13.resolve(__dirname_enable, "../../browse-ax")
|
|
3140
|
+
].filter((d) => fs13.existsSync(path13.join(d, "Package.swift"))));
|
|
3141
|
+
if (axDir) {
|
|
3142
|
+
log("Building browse-ax...");
|
|
3143
|
+
(0, import_child_process10.execSync)("swift build -c release", { cwd: axDir, stdio: ["ignore", "pipe", "pipe"], timeout: 12e4 });
|
|
3144
|
+
log("browse-ax built.");
|
|
2837
3145
|
} else {
|
|
2838
|
-
throw new Error("browse-ax
|
|
3146
|
+
throw new Error("browse-ax not found. Reinstall: npm install -g @ulpi/browse");
|
|
2839
3147
|
}
|
|
2840
3148
|
log("macOS enabled.");
|
|
2841
3149
|
}
|
|
@@ -2844,8 +3152,8 @@ async function handleEnable(args) {
|
|
|
2844
3152
|
if (!platform || platform === "--help") {
|
|
2845
3153
|
console.log("Usage: browse enable android|ios|macos|all");
|
|
2846
3154
|
console.log("");
|
|
2847
|
-
console.log("
|
|
2848
|
-
console.log("
|
|
3155
|
+
console.log("Verifies native drivers are ready. Pre-built binaries ship with npm install.");
|
|
3156
|
+
console.log("If missing (building from source), installs dependencies and builds them.");
|
|
2849
3157
|
return;
|
|
2850
3158
|
}
|
|
2851
3159
|
const platforms = platform === "all" ? ["android", "ios", "macos"] : [platform];
|
|
@@ -2872,15 +3180,15 @@ async function handleEnable(args) {
|
|
|
2872
3180
|
console.log("");
|
|
2873
3181
|
}
|
|
2874
3182
|
}
|
|
2875
|
-
var
|
|
3183
|
+
var import_child_process10, fs13, path13, import_url6, __dirname_enable, log;
|
|
2876
3184
|
var init_enable = __esm({
|
|
2877
3185
|
"src/enable.ts"() {
|
|
2878
3186
|
"use strict";
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
__dirname_enable =
|
|
3187
|
+
import_child_process10 = require("child_process");
|
|
3188
|
+
fs13 = __toESM(require("fs"), 1);
|
|
3189
|
+
path13 = __toESM(require("path"), 1);
|
|
3190
|
+
import_url6 = require("url");
|
|
3191
|
+
__dirname_enable = path13.dirname((0, import_url6.fileURLToPath)(__import_meta_url));
|
|
2884
3192
|
log = (msg) => process.stderr.write(`[browse] ${msg}
|
|
2885
3193
|
`);
|
|
2886
3194
|
}
|
|
@@ -3174,8 +3482,8 @@ async function handleReadCommand(command, args, bm, buffers) {
|
|
|
3174
3482
|
case "eval": {
|
|
3175
3483
|
const filePath = args[0];
|
|
3176
3484
|
if (!filePath) throw new Error("Usage: browse eval <js-file>");
|
|
3177
|
-
if (!
|
|
3178
|
-
const code =
|
|
3485
|
+
if (!fs14.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
|
3486
|
+
const code = fs14.readFileSync(filePath, "utf-8");
|
|
3179
3487
|
const result = await evalCtx.evaluate(code);
|
|
3180
3488
|
return typeof result === "object" ? JSON.stringify(result, null, 2) : String(result ?? "");
|
|
3181
3489
|
}
|
|
@@ -3515,13 +3823,13 @@ function registerReadDefinitions(registry4) {
|
|
|
3515
3823
|
});
|
|
3516
3824
|
}
|
|
3517
3825
|
}
|
|
3518
|
-
var
|
|
3826
|
+
var fs14;
|
|
3519
3827
|
var init_read = __esm({
|
|
3520
3828
|
"src/commands/read.ts"() {
|
|
3521
3829
|
"use strict";
|
|
3522
3830
|
init_emulation();
|
|
3523
3831
|
init_constants();
|
|
3524
|
-
|
|
3832
|
+
fs14 = __toESM(require("fs"), 1);
|
|
3525
3833
|
}
|
|
3526
3834
|
});
|
|
3527
3835
|
|
|
@@ -4168,7 +4476,7 @@ async function handleWriteCommand(command, args, bm, domainFilter) {
|
|
|
4168
4476
|
const elapsed = Date.now() - start2;
|
|
4169
4477
|
return `Request matched: ${match.description} (${elapsed}ms)`;
|
|
4170
4478
|
}
|
|
4171
|
-
await new Promise((
|
|
4479
|
+
await new Promise((resolve10) => setTimeout(resolve10, 100));
|
|
4172
4480
|
}
|
|
4173
4481
|
const finalMatch = matchNetworkRequest2(cond, buffers);
|
|
4174
4482
|
const { method, pattern } = parseRequestValue2(requestPattern);
|
|
@@ -4213,14 +4521,14 @@ async function handleWriteCommand(command, args, bm, domainFilter) {
|
|
|
4213
4521
|
const file = args[1];
|
|
4214
4522
|
if (!file) throw new Error("Usage: browse cookie export <file>");
|
|
4215
4523
|
const cookies = await page.context().cookies();
|
|
4216
|
-
|
|
4524
|
+
fs15.writeFileSync(file, JSON.stringify(cookies, null, 2));
|
|
4217
4525
|
return `Exported ${cookies.length} cookie(s) to ${file}`;
|
|
4218
4526
|
}
|
|
4219
4527
|
if (cookieStr === "import") {
|
|
4220
4528
|
const file = args[1];
|
|
4221
4529
|
if (!file) throw new Error("Usage: browse cookie import <file>");
|
|
4222
|
-
if (!
|
|
4223
|
-
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"));
|
|
4224
4532
|
if (!Array.isArray(cookies)) throw new Error("Cookie file must contain a JSON array of cookie objects");
|
|
4225
4533
|
await page.context().addCookies(cookies);
|
|
4226
4534
|
return `Imported ${cookies.length} cookie(s) from ${file}`;
|
|
@@ -4287,7 +4595,7 @@ Note: Cookies and tab URLs preserved. localStorage/sessionStorage were reset (Pl
|
|
|
4287
4595
|
const [selector, ...filePaths] = args;
|
|
4288
4596
|
if (!selector || filePaths.length === 0) throw new Error("Usage: browse upload <selector> <file1> [file2] ...");
|
|
4289
4597
|
for (const fp of filePaths) {
|
|
4290
|
-
if (!
|
|
4598
|
+
if (!fs15.existsSync(fp)) throw new Error(`File not found: ${fp}`);
|
|
4291
4599
|
}
|
|
4292
4600
|
const resolved = bm.resolveRef(selector);
|
|
4293
4601
|
if ("locator" in resolved) {
|
|
@@ -4747,13 +5055,13 @@ function registerWriteDefinitions(registry4) {
|
|
|
4747
5055
|
});
|
|
4748
5056
|
}
|
|
4749
5057
|
}
|
|
4750
|
-
var
|
|
5058
|
+
var fs15;
|
|
4751
5059
|
var init_write = __esm({
|
|
4752
5060
|
"src/commands/write.ts"() {
|
|
4753
5061
|
"use strict";
|
|
4754
5062
|
init_emulation();
|
|
4755
5063
|
init_constants();
|
|
4756
|
-
|
|
5064
|
+
fs15 = __toESM(require("fs"), 1);
|
|
4757
5065
|
}
|
|
4758
5066
|
});
|
|
4759
5067
|
|
|
@@ -5110,9 +5418,9 @@ ${legend.join("\n")}`;
|
|
|
5110
5418
|
try {
|
|
5111
5419
|
for (const vp of viewports) {
|
|
5112
5420
|
await page.setViewportSize({ width: vp.width, height: vp.height });
|
|
5113
|
-
const
|
|
5114
|
-
await page.screenshot({ path:
|
|
5115
|
-
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}`);
|
|
5116
5424
|
}
|
|
5117
5425
|
} finally {
|
|
5118
5426
|
if (originalViewport) {
|
|
@@ -5127,13 +5435,13 @@ ${legend.join("\n")}`;
|
|
|
5127
5435
|
const diffArgs = args.filter((a) => a !== "--full");
|
|
5128
5436
|
const baseline = diffArgs[0];
|
|
5129
5437
|
if (!baseline) throw new Error("Usage: browse screenshot-diff <baseline> [current] [--threshold 0.1] [--full]");
|
|
5130
|
-
if (!
|
|
5438
|
+
if (!fs16.existsSync(baseline)) throw new Error(`Baseline file not found: ${baseline}`);
|
|
5131
5439
|
let thresholdPct = 0.1;
|
|
5132
5440
|
const threshIdx = diffArgs.indexOf("--threshold");
|
|
5133
5441
|
if (threshIdx !== -1 && diffArgs[threshIdx + 1]) {
|
|
5134
5442
|
thresholdPct = parseFloat(diffArgs[threshIdx + 1]);
|
|
5135
5443
|
}
|
|
5136
|
-
const baselineBuffer =
|
|
5444
|
+
const baselineBuffer = fs16.readFileSync(baseline);
|
|
5137
5445
|
let currentBuffer;
|
|
5138
5446
|
let currentPath;
|
|
5139
5447
|
for (let i = 1; i < diffArgs.length; i++) {
|
|
@@ -5147,8 +5455,8 @@ ${legend.join("\n")}`;
|
|
|
5147
5455
|
}
|
|
5148
5456
|
}
|
|
5149
5457
|
if (currentPath) {
|
|
5150
|
-
if (!
|
|
5151
|
-
currentBuffer =
|
|
5458
|
+
if (!fs16.existsSync(currentPath)) throw new Error(`Current screenshot not found: ${currentPath}`);
|
|
5459
|
+
currentBuffer = fs16.readFileSync(currentPath);
|
|
5152
5460
|
} else {
|
|
5153
5461
|
const page = bm.getPage();
|
|
5154
5462
|
currentBuffer = await page.screenshot({ fullPage: isFullPageDiff });
|
|
@@ -5158,7 +5466,7 @@ ${legend.join("\n")}`;
|
|
|
5158
5466
|
const extIdx = baseline.lastIndexOf(".");
|
|
5159
5467
|
const diffPath = extIdx > 0 ? baseline.slice(0, extIdx) + "-diff" + baseline.slice(extIdx) : baseline + "-diff.png";
|
|
5160
5468
|
if (!result.passed && result.diffImage) {
|
|
5161
|
-
|
|
5469
|
+
fs16.writeFileSync(diffPath, result.diffImage);
|
|
5162
5470
|
}
|
|
5163
5471
|
return [
|
|
5164
5472
|
`Pixels: ${result.totalPixels}`,
|
|
@@ -5173,11 +5481,11 @@ ${legend.join("\n")}`;
|
|
|
5173
5481
|
throw new Error(`Unknown screenshots command: ${command}`);
|
|
5174
5482
|
}
|
|
5175
5483
|
}
|
|
5176
|
-
var
|
|
5484
|
+
var fs16, LOCAL_DIR;
|
|
5177
5485
|
var init_screenshots = __esm({
|
|
5178
5486
|
"src/commands/meta/screenshots.ts"() {
|
|
5179
5487
|
"use strict";
|
|
5180
|
-
|
|
5488
|
+
fs16 = __toESM(require("fs"), 1);
|
|
5181
5489
|
LOCAL_DIR = process.env.BROWSE_LOCAL_DIR || "/tmp";
|
|
5182
5490
|
}
|
|
5183
5491
|
});
|
|
@@ -5259,17 +5567,17 @@ var require_visit = __commonJS({
|
|
|
5259
5567
|
visit.BREAK = BREAK;
|
|
5260
5568
|
visit.SKIP = SKIP;
|
|
5261
5569
|
visit.REMOVE = REMOVE;
|
|
5262
|
-
function visit_(key, node, visitor,
|
|
5263
|
-
const ctrl = callVisitor(key, node, visitor,
|
|
5570
|
+
function visit_(key, node, visitor, path25) {
|
|
5571
|
+
const ctrl = callVisitor(key, node, visitor, path25);
|
|
5264
5572
|
if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
|
|
5265
|
-
replaceNode(key,
|
|
5266
|
-
return visit_(key, ctrl, visitor,
|
|
5573
|
+
replaceNode(key, path25, ctrl);
|
|
5574
|
+
return visit_(key, ctrl, visitor, path25);
|
|
5267
5575
|
}
|
|
5268
5576
|
if (typeof ctrl !== "symbol") {
|
|
5269
5577
|
if (identity.isCollection(node)) {
|
|
5270
|
-
|
|
5578
|
+
path25 = Object.freeze(path25.concat(node));
|
|
5271
5579
|
for (let i = 0; i < node.items.length; ++i) {
|
|
5272
|
-
const ci = visit_(i, node.items[i], visitor,
|
|
5580
|
+
const ci = visit_(i, node.items[i], visitor, path25);
|
|
5273
5581
|
if (typeof ci === "number")
|
|
5274
5582
|
i = ci - 1;
|
|
5275
5583
|
else if (ci === BREAK)
|
|
@@ -5280,13 +5588,13 @@ var require_visit = __commonJS({
|
|
|
5280
5588
|
}
|
|
5281
5589
|
}
|
|
5282
5590
|
} else if (identity.isPair(node)) {
|
|
5283
|
-
|
|
5284
|
-
const ck = visit_("key", node.key, visitor,
|
|
5591
|
+
path25 = Object.freeze(path25.concat(node));
|
|
5592
|
+
const ck = visit_("key", node.key, visitor, path25);
|
|
5285
5593
|
if (ck === BREAK)
|
|
5286
5594
|
return BREAK;
|
|
5287
5595
|
else if (ck === REMOVE)
|
|
5288
5596
|
node.key = null;
|
|
5289
|
-
const cv = visit_("value", node.value, visitor,
|
|
5597
|
+
const cv = visit_("value", node.value, visitor, path25);
|
|
5290
5598
|
if (cv === BREAK)
|
|
5291
5599
|
return BREAK;
|
|
5292
5600
|
else if (cv === REMOVE)
|
|
@@ -5307,17 +5615,17 @@ var require_visit = __commonJS({
|
|
|
5307
5615
|
visitAsync.BREAK = BREAK;
|
|
5308
5616
|
visitAsync.SKIP = SKIP;
|
|
5309
5617
|
visitAsync.REMOVE = REMOVE;
|
|
5310
|
-
async function visitAsync_(key, node, visitor,
|
|
5311
|
-
const ctrl = await callVisitor(key, node, visitor,
|
|
5618
|
+
async function visitAsync_(key, node, visitor, path25) {
|
|
5619
|
+
const ctrl = await callVisitor(key, node, visitor, path25);
|
|
5312
5620
|
if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
|
|
5313
|
-
replaceNode(key,
|
|
5314
|
-
return visitAsync_(key, ctrl, visitor,
|
|
5621
|
+
replaceNode(key, path25, ctrl);
|
|
5622
|
+
return visitAsync_(key, ctrl, visitor, path25);
|
|
5315
5623
|
}
|
|
5316
5624
|
if (typeof ctrl !== "symbol") {
|
|
5317
5625
|
if (identity.isCollection(node)) {
|
|
5318
|
-
|
|
5626
|
+
path25 = Object.freeze(path25.concat(node));
|
|
5319
5627
|
for (let i = 0; i < node.items.length; ++i) {
|
|
5320
|
-
const ci = await visitAsync_(i, node.items[i], visitor,
|
|
5628
|
+
const ci = await visitAsync_(i, node.items[i], visitor, path25);
|
|
5321
5629
|
if (typeof ci === "number")
|
|
5322
5630
|
i = ci - 1;
|
|
5323
5631
|
else if (ci === BREAK)
|
|
@@ -5328,13 +5636,13 @@ var require_visit = __commonJS({
|
|
|
5328
5636
|
}
|
|
5329
5637
|
}
|
|
5330
5638
|
} else if (identity.isPair(node)) {
|
|
5331
|
-
|
|
5332
|
-
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);
|
|
5333
5641
|
if (ck === BREAK)
|
|
5334
5642
|
return BREAK;
|
|
5335
5643
|
else if (ck === REMOVE)
|
|
5336
5644
|
node.key = null;
|
|
5337
|
-
const cv = await visitAsync_("value", node.value, visitor,
|
|
5645
|
+
const cv = await visitAsync_("value", node.value, visitor, path25);
|
|
5338
5646
|
if (cv === BREAK)
|
|
5339
5647
|
return BREAK;
|
|
5340
5648
|
else if (cv === REMOVE)
|
|
@@ -5361,23 +5669,23 @@ var require_visit = __commonJS({
|
|
|
5361
5669
|
}
|
|
5362
5670
|
return visitor;
|
|
5363
5671
|
}
|
|
5364
|
-
function callVisitor(key, node, visitor,
|
|
5672
|
+
function callVisitor(key, node, visitor, path25) {
|
|
5365
5673
|
if (typeof visitor === "function")
|
|
5366
|
-
return visitor(key, node,
|
|
5674
|
+
return visitor(key, node, path25);
|
|
5367
5675
|
if (identity.isMap(node))
|
|
5368
|
-
return visitor.Map?.(key, node,
|
|
5676
|
+
return visitor.Map?.(key, node, path25);
|
|
5369
5677
|
if (identity.isSeq(node))
|
|
5370
|
-
return visitor.Seq?.(key, node,
|
|
5678
|
+
return visitor.Seq?.(key, node, path25);
|
|
5371
5679
|
if (identity.isPair(node))
|
|
5372
|
-
return visitor.Pair?.(key, node,
|
|
5680
|
+
return visitor.Pair?.(key, node, path25);
|
|
5373
5681
|
if (identity.isScalar(node))
|
|
5374
|
-
return visitor.Scalar?.(key, node,
|
|
5682
|
+
return visitor.Scalar?.(key, node, path25);
|
|
5375
5683
|
if (identity.isAlias(node))
|
|
5376
|
-
return visitor.Alias?.(key, node,
|
|
5684
|
+
return visitor.Alias?.(key, node, path25);
|
|
5377
5685
|
return void 0;
|
|
5378
5686
|
}
|
|
5379
|
-
function replaceNode(key,
|
|
5380
|
-
const parent =
|
|
5687
|
+
function replaceNode(key, path25, node) {
|
|
5688
|
+
const parent = path25[path25.length - 1];
|
|
5381
5689
|
if (identity.isCollection(parent)) {
|
|
5382
5690
|
parent.items[key] = node;
|
|
5383
5691
|
} else if (identity.isPair(parent)) {
|
|
@@ -5985,10 +6293,10 @@ var require_Collection = __commonJS({
|
|
|
5985
6293
|
var createNode = require_createNode();
|
|
5986
6294
|
var identity = require_identity();
|
|
5987
6295
|
var Node = require_Node();
|
|
5988
|
-
function collectionFromPath(schema,
|
|
6296
|
+
function collectionFromPath(schema, path25, value) {
|
|
5989
6297
|
let v = value;
|
|
5990
|
-
for (let i =
|
|
5991
|
-
const k =
|
|
6298
|
+
for (let i = path25.length - 1; i >= 0; --i) {
|
|
6299
|
+
const k = path25[i];
|
|
5992
6300
|
if (typeof k === "number" && Number.isInteger(k) && k >= 0) {
|
|
5993
6301
|
const a = [];
|
|
5994
6302
|
a[k] = v;
|
|
@@ -6007,7 +6315,7 @@ var require_Collection = __commonJS({
|
|
|
6007
6315
|
sourceObjects: /* @__PURE__ */ new Map()
|
|
6008
6316
|
});
|
|
6009
6317
|
}
|
|
6010
|
-
var isEmptyPath = (
|
|
6318
|
+
var isEmptyPath = (path25) => path25 == null || typeof path25 === "object" && !!path25[Symbol.iterator]().next().done;
|
|
6011
6319
|
var Collection = class extends Node.NodeBase {
|
|
6012
6320
|
constructor(type, schema) {
|
|
6013
6321
|
super(type);
|
|
@@ -6037,11 +6345,11 @@ var require_Collection = __commonJS({
|
|
|
6037
6345
|
* be a Pair instance or a `{ key, value }` object, which may not have a key
|
|
6038
6346
|
* that already exists in the map.
|
|
6039
6347
|
*/
|
|
6040
|
-
addIn(
|
|
6041
|
-
if (isEmptyPath(
|
|
6348
|
+
addIn(path25, value) {
|
|
6349
|
+
if (isEmptyPath(path25))
|
|
6042
6350
|
this.add(value);
|
|
6043
6351
|
else {
|
|
6044
|
-
const [key, ...rest] =
|
|
6352
|
+
const [key, ...rest] = path25;
|
|
6045
6353
|
const node = this.get(key, true);
|
|
6046
6354
|
if (identity.isCollection(node))
|
|
6047
6355
|
node.addIn(rest, value);
|
|
@@ -6055,8 +6363,8 @@ var require_Collection = __commonJS({
|
|
|
6055
6363
|
* Removes a value from the collection.
|
|
6056
6364
|
* @returns `true` if the item was found and removed.
|
|
6057
6365
|
*/
|
|
6058
|
-
deleteIn(
|
|
6059
|
-
const [key, ...rest] =
|
|
6366
|
+
deleteIn(path25) {
|
|
6367
|
+
const [key, ...rest] = path25;
|
|
6060
6368
|
if (rest.length === 0)
|
|
6061
6369
|
return this.delete(key);
|
|
6062
6370
|
const node = this.get(key, true);
|
|
@@ -6070,8 +6378,8 @@ var require_Collection = __commonJS({
|
|
|
6070
6378
|
* scalar values from their surrounding node; to disable set `keepScalar` to
|
|
6071
6379
|
* `true` (collections are always returned intact).
|
|
6072
6380
|
*/
|
|
6073
|
-
getIn(
|
|
6074
|
-
const [key, ...rest] =
|
|
6381
|
+
getIn(path25, keepScalar) {
|
|
6382
|
+
const [key, ...rest] = path25;
|
|
6075
6383
|
const node = this.get(key, true);
|
|
6076
6384
|
if (rest.length === 0)
|
|
6077
6385
|
return !keepScalar && identity.isScalar(node) ? node.value : node;
|
|
@@ -6089,8 +6397,8 @@ var require_Collection = __commonJS({
|
|
|
6089
6397
|
/**
|
|
6090
6398
|
* Checks if the collection includes a value with the key `key`.
|
|
6091
6399
|
*/
|
|
6092
|
-
hasIn(
|
|
6093
|
-
const [key, ...rest] =
|
|
6400
|
+
hasIn(path25) {
|
|
6401
|
+
const [key, ...rest] = path25;
|
|
6094
6402
|
if (rest.length === 0)
|
|
6095
6403
|
return this.has(key);
|
|
6096
6404
|
const node = this.get(key, true);
|
|
@@ -6100,8 +6408,8 @@ var require_Collection = __commonJS({
|
|
|
6100
6408
|
* Sets a value in this collection. For `!!set`, `value` needs to be a
|
|
6101
6409
|
* boolean to add/remove the item from the set.
|
|
6102
6410
|
*/
|
|
6103
|
-
setIn(
|
|
6104
|
-
const [key, ...rest] =
|
|
6411
|
+
setIn(path25, value) {
|
|
6412
|
+
const [key, ...rest] = path25;
|
|
6105
6413
|
if (rest.length === 0) {
|
|
6106
6414
|
this.set(key, value);
|
|
6107
6415
|
} else {
|
|
@@ -8613,9 +8921,9 @@ var require_Document = __commonJS({
|
|
|
8613
8921
|
this.contents.add(value);
|
|
8614
8922
|
}
|
|
8615
8923
|
/** Adds a value to the document. */
|
|
8616
|
-
addIn(
|
|
8924
|
+
addIn(path25, value) {
|
|
8617
8925
|
if (assertCollection(this.contents))
|
|
8618
|
-
this.contents.addIn(
|
|
8926
|
+
this.contents.addIn(path25, value);
|
|
8619
8927
|
}
|
|
8620
8928
|
/**
|
|
8621
8929
|
* Create a new `Alias` node, ensuring that the target `node` has the required anchor.
|
|
@@ -8690,14 +8998,14 @@ var require_Document = __commonJS({
|
|
|
8690
8998
|
* Removes a value from the document.
|
|
8691
8999
|
* @returns `true` if the item was found and removed.
|
|
8692
9000
|
*/
|
|
8693
|
-
deleteIn(
|
|
8694
|
-
if (Collection.isEmptyPath(
|
|
9001
|
+
deleteIn(path25) {
|
|
9002
|
+
if (Collection.isEmptyPath(path25)) {
|
|
8695
9003
|
if (this.contents == null)
|
|
8696
9004
|
return false;
|
|
8697
9005
|
this.contents = null;
|
|
8698
9006
|
return true;
|
|
8699
9007
|
}
|
|
8700
|
-
return assertCollection(this.contents) ? this.contents.deleteIn(
|
|
9008
|
+
return assertCollection(this.contents) ? this.contents.deleteIn(path25) : false;
|
|
8701
9009
|
}
|
|
8702
9010
|
/**
|
|
8703
9011
|
* Returns item at `key`, or `undefined` if not found. By default unwraps
|
|
@@ -8712,10 +9020,10 @@ var require_Document = __commonJS({
|
|
|
8712
9020
|
* scalar values from their surrounding node; to disable set `keepScalar` to
|
|
8713
9021
|
* `true` (collections are always returned intact).
|
|
8714
9022
|
*/
|
|
8715
|
-
getIn(
|
|
8716
|
-
if (Collection.isEmptyPath(
|
|
9023
|
+
getIn(path25, keepScalar) {
|
|
9024
|
+
if (Collection.isEmptyPath(path25))
|
|
8717
9025
|
return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents;
|
|
8718
|
-
return identity.isCollection(this.contents) ? this.contents.getIn(
|
|
9026
|
+
return identity.isCollection(this.contents) ? this.contents.getIn(path25, keepScalar) : void 0;
|
|
8719
9027
|
}
|
|
8720
9028
|
/**
|
|
8721
9029
|
* Checks if the document includes a value with the key `key`.
|
|
@@ -8726,10 +9034,10 @@ var require_Document = __commonJS({
|
|
|
8726
9034
|
/**
|
|
8727
9035
|
* Checks if the document includes a value at `path`.
|
|
8728
9036
|
*/
|
|
8729
|
-
hasIn(
|
|
8730
|
-
if (Collection.isEmptyPath(
|
|
9037
|
+
hasIn(path25) {
|
|
9038
|
+
if (Collection.isEmptyPath(path25))
|
|
8731
9039
|
return this.contents !== void 0;
|
|
8732
|
-
return identity.isCollection(this.contents) ? this.contents.hasIn(
|
|
9040
|
+
return identity.isCollection(this.contents) ? this.contents.hasIn(path25) : false;
|
|
8733
9041
|
}
|
|
8734
9042
|
/**
|
|
8735
9043
|
* Sets a value in this document. For `!!set`, `value` needs to be a
|
|
@@ -8746,13 +9054,13 @@ var require_Document = __commonJS({
|
|
|
8746
9054
|
* Sets a value in this document. For `!!set`, `value` needs to be a
|
|
8747
9055
|
* boolean to add/remove the item from the set.
|
|
8748
9056
|
*/
|
|
8749
|
-
setIn(
|
|
8750
|
-
if (Collection.isEmptyPath(
|
|
9057
|
+
setIn(path25, value) {
|
|
9058
|
+
if (Collection.isEmptyPath(path25)) {
|
|
8751
9059
|
this.contents = value;
|
|
8752
9060
|
} else if (this.contents == null) {
|
|
8753
|
-
this.contents = Collection.collectionFromPath(this.schema, Array.from(
|
|
9061
|
+
this.contents = Collection.collectionFromPath(this.schema, Array.from(path25), value);
|
|
8754
9062
|
} else if (assertCollection(this.contents)) {
|
|
8755
|
-
this.contents.setIn(
|
|
9063
|
+
this.contents.setIn(path25, value);
|
|
8756
9064
|
}
|
|
8757
9065
|
}
|
|
8758
9066
|
/**
|
|
@@ -10709,9 +11017,9 @@ var require_cst_visit = __commonJS({
|
|
|
10709
11017
|
visit.BREAK = BREAK;
|
|
10710
11018
|
visit.SKIP = SKIP;
|
|
10711
11019
|
visit.REMOVE = REMOVE;
|
|
10712
|
-
visit.itemAtPath = (cst,
|
|
11020
|
+
visit.itemAtPath = (cst, path25) => {
|
|
10713
11021
|
let item = cst;
|
|
10714
|
-
for (const [field, index] of
|
|
11022
|
+
for (const [field, index] of path25) {
|
|
10715
11023
|
const tok = item?.[field];
|
|
10716
11024
|
if (tok && "items" in tok) {
|
|
10717
11025
|
item = tok.items[index];
|
|
@@ -10720,23 +11028,23 @@ var require_cst_visit = __commonJS({
|
|
|
10720
11028
|
}
|
|
10721
11029
|
return item;
|
|
10722
11030
|
};
|
|
10723
|
-
visit.parentCollection = (cst,
|
|
10724
|
-
const parent = visit.itemAtPath(cst,
|
|
10725
|
-
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];
|
|
10726
11034
|
const coll = parent?.[field];
|
|
10727
11035
|
if (coll && "items" in coll)
|
|
10728
11036
|
return coll;
|
|
10729
11037
|
throw new Error("Parent collection not found");
|
|
10730
11038
|
};
|
|
10731
|
-
function _visit(
|
|
10732
|
-
let ctrl = visitor(item,
|
|
11039
|
+
function _visit(path25, item, visitor) {
|
|
11040
|
+
let ctrl = visitor(item, path25);
|
|
10733
11041
|
if (typeof ctrl === "symbol")
|
|
10734
11042
|
return ctrl;
|
|
10735
11043
|
for (const field of ["key", "value"]) {
|
|
10736
11044
|
const token = item[field];
|
|
10737
11045
|
if (token && "items" in token) {
|
|
10738
11046
|
for (let i = 0; i < token.items.length; ++i) {
|
|
10739
|
-
const ci = _visit(Object.freeze(
|
|
11047
|
+
const ci = _visit(Object.freeze(path25.concat([[field, i]])), token.items[i], visitor);
|
|
10740
11048
|
if (typeof ci === "number")
|
|
10741
11049
|
i = ci - 1;
|
|
10742
11050
|
else if (ci === BREAK)
|
|
@@ -10747,10 +11055,10 @@ var require_cst_visit = __commonJS({
|
|
|
10747
11055
|
}
|
|
10748
11056
|
}
|
|
10749
11057
|
if (typeof ctrl === "function" && field === "key")
|
|
10750
|
-
ctrl = ctrl(item,
|
|
11058
|
+
ctrl = ctrl(item, path25);
|
|
10751
11059
|
}
|
|
10752
11060
|
}
|
|
10753
|
-
return typeof ctrl === "function" ? ctrl(item,
|
|
11061
|
+
return typeof ctrl === "function" ? ctrl(item, path25) : ctrl;
|
|
10754
11062
|
}
|
|
10755
11063
|
exports2.visit = visit;
|
|
10756
11064
|
}
|
|
@@ -12035,14 +12343,14 @@ var require_parser = __commonJS({
|
|
|
12035
12343
|
case "scalar":
|
|
12036
12344
|
case "single-quoted-scalar":
|
|
12037
12345
|
case "double-quoted-scalar": {
|
|
12038
|
-
const
|
|
12346
|
+
const fs30 = this.flowScalar(this.type);
|
|
12039
12347
|
if (atNextItem || it.value) {
|
|
12040
|
-
map.items.push({ start: start2, key:
|
|
12348
|
+
map.items.push({ start: start2, key: fs30, sep: [] });
|
|
12041
12349
|
this.onKeyLine = true;
|
|
12042
12350
|
} else if (it.sep) {
|
|
12043
|
-
this.stack.push(
|
|
12351
|
+
this.stack.push(fs30);
|
|
12044
12352
|
} else {
|
|
12045
|
-
Object.assign(it, { key:
|
|
12353
|
+
Object.assign(it, { key: fs30, sep: [] });
|
|
12046
12354
|
this.onKeyLine = true;
|
|
12047
12355
|
}
|
|
12048
12356
|
return;
|
|
@@ -12170,13 +12478,13 @@ var require_parser = __commonJS({
|
|
|
12170
12478
|
case "scalar":
|
|
12171
12479
|
case "single-quoted-scalar":
|
|
12172
12480
|
case "double-quoted-scalar": {
|
|
12173
|
-
const
|
|
12481
|
+
const fs30 = this.flowScalar(this.type);
|
|
12174
12482
|
if (!it || it.value)
|
|
12175
|
-
fc.items.push({ start: [], key:
|
|
12483
|
+
fc.items.push({ start: [], key: fs30, sep: [] });
|
|
12176
12484
|
else if (it.sep)
|
|
12177
|
-
this.stack.push(
|
|
12485
|
+
this.stack.push(fs30);
|
|
12178
12486
|
else
|
|
12179
|
-
Object.assign(it, { key:
|
|
12487
|
+
Object.assign(it, { key: fs30, sep: [] });
|
|
12180
12488
|
return;
|
|
12181
12489
|
}
|
|
12182
12490
|
case "flow-map-end":
|
|
@@ -12943,7 +13251,7 @@ async function handleRecordingCommand(command, args, target, currentSession) {
|
|
|
12943
13251
|
throw new Error(`Unknown format: ${format}. Use "browse" (chain JSON), "replay" (Puppeteer), "playwright" (Playwright Test), or "flow" (YAML).`);
|
|
12944
13252
|
}
|
|
12945
13253
|
if (filePath) {
|
|
12946
|
-
|
|
13254
|
+
fs17.writeFileSync(filePath, output);
|
|
12947
13255
|
return `Exported ${steps.length} steps as ${format}: ${filePath}`;
|
|
12948
13256
|
}
|
|
12949
13257
|
return output;
|
|
@@ -12968,7 +13276,7 @@ async function handleRecordingCommand(command, args, target, currentSession) {
|
|
|
12968
13276
|
const { formatAsHar: formatAsHar2 } = await Promise.resolve().then(() => (init_har(), har_exports));
|
|
12969
13277
|
const har = formatAsHar2(sessionBuffers.networkBuffer, recording.startTime);
|
|
12970
13278
|
const harPath = args[1] || (currentSession ? `${currentSession.outputDir}/recording.har` : `${LOCAL_DIR2}/browse-recording.har`);
|
|
12971
|
-
|
|
13279
|
+
fs17.writeFileSync(harPath, JSON.stringify(har, null, 2));
|
|
12972
13280
|
const entryCount = har.log.entries.length;
|
|
12973
13281
|
return `HAR saved: ${harPath} (${entryCount} entries)`;
|
|
12974
13282
|
}
|
|
@@ -13004,11 +13312,11 @@ async function handleRecordingCommand(command, args, target, currentSession) {
|
|
|
13004
13312
|
throw new Error(`Unknown recording command: ${command}`);
|
|
13005
13313
|
}
|
|
13006
13314
|
}
|
|
13007
|
-
var
|
|
13315
|
+
var fs17, LOCAL_DIR2;
|
|
13008
13316
|
var init_recording = __esm({
|
|
13009
13317
|
"src/commands/meta/recording.ts"() {
|
|
13010
13318
|
"use strict";
|
|
13011
|
-
|
|
13319
|
+
fs17 = __toESM(require("fs"), 1);
|
|
13012
13320
|
LOCAL_DIR2 = process.env.BROWSE_LOCAL_DIR || "/tmp";
|
|
13013
13321
|
}
|
|
13014
13322
|
});
|
|
@@ -13032,20 +13340,20 @@ async function saveSessionState(sessionDir, context, encryptionKey) {
|
|
|
13032
13340
|
} else {
|
|
13033
13341
|
content = json;
|
|
13034
13342
|
}
|
|
13035
|
-
|
|
13036
|
-
|
|
13343
|
+
fs18.mkdirSync(sessionDir, { recursive: true });
|
|
13344
|
+
fs18.writeFileSync(path14.join(sessionDir, STATE_FILENAME), content, { mode: 384 });
|
|
13037
13345
|
} catch (err) {
|
|
13038
13346
|
console.log(`[session-persist] Warning: failed to save state: ${err.message}`);
|
|
13039
13347
|
}
|
|
13040
13348
|
}
|
|
13041
13349
|
async function loadSessionState(sessionDir, context, encryptionKey) {
|
|
13042
|
-
const statePath =
|
|
13043
|
-
if (!
|
|
13350
|
+
const statePath = path14.join(sessionDir, STATE_FILENAME);
|
|
13351
|
+
if (!fs18.existsSync(statePath)) {
|
|
13044
13352
|
return false;
|
|
13045
13353
|
}
|
|
13046
13354
|
let stateData;
|
|
13047
13355
|
try {
|
|
13048
|
-
const raw =
|
|
13356
|
+
const raw = fs18.readFileSync(statePath, "utf-8");
|
|
13049
13357
|
const parsed = JSON.parse(raw);
|
|
13050
13358
|
if (parsed.encrypted) {
|
|
13051
13359
|
if (!encryptionKey) {
|
|
@@ -13101,24 +13409,24 @@ async function loadSessionState(sessionDir, context, encryptionKey) {
|
|
|
13101
13409
|
}
|
|
13102
13410
|
}
|
|
13103
13411
|
function hasPersistedState(sessionDir) {
|
|
13104
|
-
return
|
|
13412
|
+
return fs18.existsSync(path14.join(sessionDir, STATE_FILENAME));
|
|
13105
13413
|
}
|
|
13106
13414
|
function cleanOldStates(localDir, maxAgeDays) {
|
|
13107
13415
|
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1e3;
|
|
13108
13416
|
const now = Date.now();
|
|
13109
13417
|
let deleted = 0;
|
|
13110
|
-
const statesDir =
|
|
13111
|
-
if (
|
|
13418
|
+
const statesDir = path14.join(localDir, "states");
|
|
13419
|
+
if (fs18.existsSync(statesDir)) {
|
|
13112
13420
|
try {
|
|
13113
|
-
const entries =
|
|
13421
|
+
const entries = fs18.readdirSync(statesDir);
|
|
13114
13422
|
for (const entry of entries) {
|
|
13115
13423
|
if (!entry.endsWith(".json")) continue;
|
|
13116
|
-
const filePath =
|
|
13424
|
+
const filePath = path14.join(statesDir, entry);
|
|
13117
13425
|
try {
|
|
13118
|
-
const stat =
|
|
13426
|
+
const stat = fs18.statSync(filePath);
|
|
13119
13427
|
if (!stat.isFile()) continue;
|
|
13120
13428
|
if (now - stat.mtimeMs > maxAgeMs) {
|
|
13121
|
-
|
|
13429
|
+
fs18.unlinkSync(filePath);
|
|
13122
13430
|
deleted++;
|
|
13123
13431
|
}
|
|
13124
13432
|
} catch (_) {
|
|
@@ -13127,23 +13435,23 @@ function cleanOldStates(localDir, maxAgeDays) {
|
|
|
13127
13435
|
} catch (_) {
|
|
13128
13436
|
}
|
|
13129
13437
|
}
|
|
13130
|
-
const sessionsDir =
|
|
13131
|
-
if (
|
|
13438
|
+
const sessionsDir = path14.join(localDir, "sessions");
|
|
13439
|
+
if (fs18.existsSync(sessionsDir)) {
|
|
13132
13440
|
try {
|
|
13133
|
-
const sessionDirs =
|
|
13441
|
+
const sessionDirs = fs18.readdirSync(sessionsDir);
|
|
13134
13442
|
for (const dir of sessionDirs) {
|
|
13135
|
-
const dirPath =
|
|
13443
|
+
const dirPath = path14.join(sessionsDir, dir);
|
|
13136
13444
|
try {
|
|
13137
|
-
const dirStat =
|
|
13445
|
+
const dirStat = fs18.statSync(dirPath);
|
|
13138
13446
|
if (!dirStat.isDirectory()) continue;
|
|
13139
13447
|
} catch (_) {
|
|
13140
13448
|
continue;
|
|
13141
13449
|
}
|
|
13142
|
-
const statePath =
|
|
13450
|
+
const statePath = path14.join(dirPath, STATE_FILENAME);
|
|
13143
13451
|
try {
|
|
13144
|
-
const stat =
|
|
13452
|
+
const stat = fs18.statSync(statePath);
|
|
13145
13453
|
if (now - stat.mtimeMs > maxAgeMs) {
|
|
13146
|
-
|
|
13454
|
+
fs18.unlinkSync(statePath);
|
|
13147
13455
|
deleted++;
|
|
13148
13456
|
}
|
|
13149
13457
|
} catch (_) {
|
|
@@ -13154,12 +13462,12 @@ function cleanOldStates(localDir, maxAgeDays) {
|
|
|
13154
13462
|
}
|
|
13155
13463
|
return { deleted };
|
|
13156
13464
|
}
|
|
13157
|
-
var
|
|
13465
|
+
var fs18, path14, STATE_FILENAME;
|
|
13158
13466
|
var init_persist = __esm({
|
|
13159
13467
|
"src/session/persist.ts"() {
|
|
13160
13468
|
"use strict";
|
|
13161
|
-
|
|
13162
|
-
|
|
13469
|
+
fs18 = __toESM(require("fs"), 1);
|
|
13470
|
+
path14 = __toESM(require("path"), 1);
|
|
13163
13471
|
init_encryption();
|
|
13164
13472
|
STATE_FILENAME = "state.json";
|
|
13165
13473
|
}
|
|
@@ -13594,7 +13902,7 @@ async function handleSessionsCommand(command, args, bm, sessionManager2, current
|
|
|
13594
13902
|
const lines = entries.map(
|
|
13595
13903
|
(e) => `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}`
|
|
13596
13904
|
).join("\n") + "\n";
|
|
13597
|
-
|
|
13905
|
+
fs19.appendFileSync(consolePath, lines);
|
|
13598
13906
|
buffers.lastConsoleFlushed = buffers.consoleTotalAdded;
|
|
13599
13907
|
}
|
|
13600
13908
|
const newNetworkCount = buffers.networkTotalAdded - buffers.lastNetworkFlushed;
|
|
@@ -13604,7 +13912,7 @@ async function handleSessionsCommand(command, args, bm, sessionManager2, current
|
|
|
13604
13912
|
const lines = entries.map(
|
|
13605
13913
|
(e) => `[${new Date(e.timestamp).toISOString()}] ${e.method} ${e.url} \u2192 ${e.status || "pending"} (${e.duration || "?"}ms, ${e.size || "?"}B)`
|
|
13606
13914
|
).join("\n") + "\n";
|
|
13607
|
-
|
|
13915
|
+
fs19.appendFileSync(networkPath, lines);
|
|
13608
13916
|
buffers.lastNetworkFlushed = buffers.networkTotalAdded;
|
|
13609
13917
|
}
|
|
13610
13918
|
}
|
|
@@ -13620,22 +13928,22 @@ async function handleSessionsCommand(command, args, bm, sessionManager2, current
|
|
|
13620
13928
|
const statesDir = `${LOCAL_DIR3}/states`;
|
|
13621
13929
|
const statePath = `${statesDir}/${name}.json`;
|
|
13622
13930
|
if (subcommand === "list") {
|
|
13623
|
-
if (!
|
|
13624
|
-
const files =
|
|
13931
|
+
if (!fs19.existsSync(statesDir)) return "(no saved states)";
|
|
13932
|
+
const files = fs19.readdirSync(statesDir).filter((f) => f.endsWith(".json"));
|
|
13625
13933
|
if (files.length === 0) return "(no saved states)";
|
|
13626
13934
|
const lines = [];
|
|
13627
13935
|
for (const file of files) {
|
|
13628
13936
|
const fp = `${statesDir}/${file}`;
|
|
13629
|
-
const stat =
|
|
13937
|
+
const stat = fs19.statSync(fp);
|
|
13630
13938
|
lines.push(` ${file.replace(".json", "")} ${stat.size}B ${new Date(stat.mtimeMs).toISOString()}`);
|
|
13631
13939
|
}
|
|
13632
13940
|
return lines.join("\n");
|
|
13633
13941
|
}
|
|
13634
13942
|
if (subcommand === "show") {
|
|
13635
|
-
if (!
|
|
13943
|
+
if (!fs19.existsSync(statePath)) {
|
|
13636
13944
|
throw new Error(`State file not found: ${statePath}`);
|
|
13637
13945
|
}
|
|
13638
|
-
const data = JSON.parse(
|
|
13946
|
+
const data = JSON.parse(fs19.readFileSync(statePath, "utf-8"));
|
|
13639
13947
|
const cookieCount = data.cookies?.length || 0;
|
|
13640
13948
|
const originCount = data.origins?.length || 0;
|
|
13641
13949
|
const storageItems = (data.origins || []).reduce((sum, o) => sum + (o.localStorage?.length || 0), 0);
|
|
@@ -13660,15 +13968,15 @@ async function handleSessionsCommand(command, args, bm, sessionManager2, current
|
|
|
13660
13968
|
const context = bm.getContext();
|
|
13661
13969
|
if (!context) throw new Error("No browser context");
|
|
13662
13970
|
const state = await context.storageState();
|
|
13663
|
-
|
|
13664
|
-
|
|
13971
|
+
fs19.mkdirSync(statesDir, { recursive: true });
|
|
13972
|
+
fs19.writeFileSync(statePath, JSON.stringify(state, null, 2), { mode: 384 });
|
|
13665
13973
|
return `State saved: ${statePath}`;
|
|
13666
13974
|
}
|
|
13667
13975
|
if (subcommand === "load") {
|
|
13668
|
-
if (!
|
|
13976
|
+
if (!fs19.existsSync(statePath)) {
|
|
13669
13977
|
throw new Error(`State file not found: ${statePath}. Run "browse state save ${name}" first.`);
|
|
13670
13978
|
}
|
|
13671
|
-
const stateData = JSON.parse(
|
|
13979
|
+
const stateData = JSON.parse(fs19.readFileSync(statePath, "utf-8"));
|
|
13672
13980
|
const context = bm.getContext();
|
|
13673
13981
|
if (!context) throw new Error("No browser context");
|
|
13674
13982
|
const warnings = [];
|
|
@@ -13721,12 +14029,12 @@ ${snapshot}`;
|
|
|
13721
14029
|
throw new Error(`Unknown sessions command: ${command}`);
|
|
13722
14030
|
}
|
|
13723
14031
|
}
|
|
13724
|
-
var
|
|
14032
|
+
var fs19, LOCAL_DIR3;
|
|
13725
14033
|
var init_sessions = __esm({
|
|
13726
14034
|
"src/commands/meta/sessions.ts"() {
|
|
13727
14035
|
"use strict";
|
|
13728
14036
|
init_sanitize();
|
|
13729
|
-
|
|
14037
|
+
fs19 = __toESM(require("fs"), 1);
|
|
13730
14038
|
LOCAL_DIR3 = process.env.BROWSE_LOCAL_DIR || "/tmp";
|
|
13731
14039
|
}
|
|
13732
14040
|
});
|
|
@@ -14057,11 +14365,11 @@ var init_lib = __esm({
|
|
|
14057
14365
|
}
|
|
14058
14366
|
}
|
|
14059
14367
|
},
|
|
14060
|
-
addToPath: function addToPath(
|
|
14061
|
-
var last =
|
|
14368
|
+
addToPath: function addToPath(path25, added, removed, oldPosInc, options) {
|
|
14369
|
+
var last = path25.lastComponent;
|
|
14062
14370
|
if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
|
|
14063
14371
|
return {
|
|
14064
|
-
oldPos:
|
|
14372
|
+
oldPos: path25.oldPos + oldPosInc,
|
|
14065
14373
|
lastComponent: {
|
|
14066
14374
|
count: last.count + 1,
|
|
14067
14375
|
added,
|
|
@@ -14071,7 +14379,7 @@ var init_lib = __esm({
|
|
|
14071
14379
|
};
|
|
14072
14380
|
} else {
|
|
14073
14381
|
return {
|
|
14074
|
-
oldPos:
|
|
14382
|
+
oldPos: path25.oldPos + oldPosInc,
|
|
14075
14383
|
lastComponent: {
|
|
14076
14384
|
count: 1,
|
|
14077
14385
|
added,
|
|
@@ -14129,7 +14437,7 @@ var init_lib = __esm({
|
|
|
14129
14437
|
tokenize: function tokenize(value) {
|
|
14130
14438
|
return Array.from(value);
|
|
14131
14439
|
},
|
|
14132
|
-
join: function
|
|
14440
|
+
join: function join15(chars) {
|
|
14133
14441
|
return chars.join("");
|
|
14134
14442
|
},
|
|
14135
14443
|
postProcess: function postProcess(changeObjects) {
|
|
@@ -19690,19 +19998,19 @@ __export(detection_exports, {
|
|
|
19690
19998
|
detectStack: () => detectStack
|
|
19691
19999
|
});
|
|
19692
20000
|
function loadCustomSignatures(localDir) {
|
|
19693
|
-
const detectionDir =
|
|
19694
|
-
if (!
|
|
20001
|
+
const detectionDir = path15.join(localDir, "detections");
|
|
20002
|
+
if (!fs20.existsSync(detectionDir)) return [];
|
|
19695
20003
|
const sigs = [];
|
|
19696
20004
|
let entries;
|
|
19697
20005
|
try {
|
|
19698
|
-
entries =
|
|
20006
|
+
entries = fs20.readdirSync(detectionDir).filter((f) => f.endsWith(".json"));
|
|
19699
20007
|
} catch {
|
|
19700
20008
|
return [];
|
|
19701
20009
|
}
|
|
19702
20010
|
for (const entry of entries) {
|
|
19703
|
-
const filePath =
|
|
20011
|
+
const filePath = path15.join(detectionDir, entry);
|
|
19704
20012
|
try {
|
|
19705
|
-
const raw =
|
|
20013
|
+
const raw = fs20.readFileSync(filePath, "utf-8");
|
|
19706
20014
|
const parsed = JSON.parse(raw);
|
|
19707
20015
|
if (parsed == null || typeof parsed !== "object" || parsed.version !== 1 || typeof parsed.name !== "string" || typeof parsed.detect !== "string") {
|
|
19708
20016
|
process.stderr.write(
|
|
@@ -19828,15 +20136,15 @@ async function detectStack(page, networkEntries, localDir) {
|
|
|
19828
20136
|
const thirdParty = buildThirdPartyInventory(page, networkEntries ?? []);
|
|
19829
20137
|
return { frameworks, saas, infrastructure, thirdParty, custom };
|
|
19830
20138
|
}
|
|
19831
|
-
var
|
|
20139
|
+
var fs20, path15, THIRD_PARTY_DOMAINS;
|
|
19832
20140
|
var init_detection = __esm({
|
|
19833
20141
|
"src/detection/index.ts"() {
|
|
19834
20142
|
"use strict";
|
|
19835
20143
|
init_frameworks();
|
|
19836
20144
|
init_saas();
|
|
19837
20145
|
init_infrastructure();
|
|
19838
|
-
|
|
19839
|
-
|
|
20146
|
+
fs20 = __toESM(require("fs"), 1);
|
|
20147
|
+
path15 = __toESM(require("path"), 1);
|
|
19840
20148
|
THIRD_PARTY_DOMAINS = {
|
|
19841
20149
|
// Analytics
|
|
19842
20150
|
"google-analytics.com": "analytics",
|
|
@@ -21918,35 +22226,35 @@ __export(persist_exports2, {
|
|
|
21918
22226
|
saveAudit: () => saveAudit
|
|
21919
22227
|
});
|
|
21920
22228
|
function auditsDir(localDir) {
|
|
21921
|
-
return
|
|
22229
|
+
return path16.join(localDir, "audits");
|
|
21922
22230
|
}
|
|
21923
22231
|
function auditPath(localDir, name) {
|
|
21924
|
-
return
|
|
22232
|
+
return path16.join(auditsDir(localDir), `${sanitizeName(name)}.json`);
|
|
21925
22233
|
}
|
|
21926
22234
|
function saveAudit(localDir, name, report) {
|
|
21927
22235
|
const dir = auditsDir(localDir);
|
|
21928
|
-
|
|
22236
|
+
fs21.mkdirSync(dir, { recursive: true });
|
|
21929
22237
|
const filePath = auditPath(localDir, name);
|
|
21930
|
-
|
|
22238
|
+
fs21.writeFileSync(filePath, JSON.stringify(report, null, 2), { mode: 384 });
|
|
21931
22239
|
return filePath;
|
|
21932
22240
|
}
|
|
21933
22241
|
function loadAudit(localDir, name) {
|
|
21934
22242
|
const filePath = auditPath(localDir, name);
|
|
21935
|
-
if (!
|
|
22243
|
+
if (!fs21.existsSync(filePath)) {
|
|
21936
22244
|
throw new Error(
|
|
21937
22245
|
`Audit not found: ${filePath}. Run "browse perf-audit save ${name}" first.`
|
|
21938
22246
|
);
|
|
21939
22247
|
}
|
|
21940
|
-
return JSON.parse(
|
|
22248
|
+
return JSON.parse(fs21.readFileSync(filePath, "utf-8"));
|
|
21941
22249
|
}
|
|
21942
22250
|
function listAudits(localDir) {
|
|
21943
22251
|
const dir = auditsDir(localDir);
|
|
21944
|
-
if (!
|
|
21945
|
-
const files =
|
|
22252
|
+
if (!fs21.existsSync(dir)) return [];
|
|
22253
|
+
const files = fs21.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
21946
22254
|
if (files.length === 0) return [];
|
|
21947
22255
|
const entries = files.map((file) => {
|
|
21948
|
-
const fp =
|
|
21949
|
-
const stat =
|
|
22256
|
+
const fp = path16.join(dir, file);
|
|
22257
|
+
const stat = fs21.statSync(fp);
|
|
21950
22258
|
return {
|
|
21951
22259
|
name: file.replace(".json", ""),
|
|
21952
22260
|
sizeBytes: stat.size,
|
|
@@ -21958,19 +22266,19 @@ function listAudits(localDir) {
|
|
|
21958
22266
|
}
|
|
21959
22267
|
function deleteAudit(localDir, name) {
|
|
21960
22268
|
const filePath = auditPath(localDir, name);
|
|
21961
|
-
if (!
|
|
22269
|
+
if (!fs21.existsSync(filePath)) {
|
|
21962
22270
|
throw new Error(
|
|
21963
22271
|
`Audit not found: ${filePath}. Nothing to delete.`
|
|
21964
22272
|
);
|
|
21965
22273
|
}
|
|
21966
|
-
|
|
22274
|
+
fs21.unlinkSync(filePath);
|
|
21967
22275
|
}
|
|
21968
|
-
var
|
|
22276
|
+
var fs21, path16;
|
|
21969
22277
|
var init_persist2 = __esm({
|
|
21970
22278
|
"src/perf-audit/persist.ts"() {
|
|
21971
22279
|
"use strict";
|
|
21972
|
-
|
|
21973
|
-
|
|
22280
|
+
fs21 = __toESM(require("fs"), 1);
|
|
22281
|
+
path16 = __toESM(require("path"), 1);
|
|
21974
22282
|
init_sanitize();
|
|
21975
22283
|
}
|
|
21976
22284
|
});
|
|
@@ -23328,7 +23636,7 @@ All budgets met.`;
|
|
|
23328
23636
|
}
|
|
23329
23637
|
return "OK";
|
|
23330
23638
|
}
|
|
23331
|
-
await new Promise((
|
|
23639
|
+
await new Promise((resolve10) => setTimeout(resolve10, 100));
|
|
23332
23640
|
}
|
|
23333
23641
|
const failures = lastResults.filter((r2) => !r2.passed);
|
|
23334
23642
|
throw new Error(
|
|
@@ -23417,12 +23725,12 @@ async function autoDetectSelector(page, field) {
|
|
|
23417
23725
|
}
|
|
23418
23726
|
throw new Error("Could not auto-detect submit button.");
|
|
23419
23727
|
}
|
|
23420
|
-
var
|
|
23728
|
+
var fs22, path17, AuthVault;
|
|
23421
23729
|
var init_auth_vault = __esm({
|
|
23422
23730
|
"src/security/auth-vault.ts"() {
|
|
23423
23731
|
"use strict";
|
|
23424
|
-
|
|
23425
|
-
|
|
23732
|
+
fs22 = __toESM(require("fs"), 1);
|
|
23733
|
+
path17 = __toESM(require("path"), 1);
|
|
23426
23734
|
init_constants();
|
|
23427
23735
|
init_encryption();
|
|
23428
23736
|
init_sanitize();
|
|
@@ -23430,11 +23738,11 @@ var init_auth_vault = __esm({
|
|
|
23430
23738
|
authDir;
|
|
23431
23739
|
encryptionKey;
|
|
23432
23740
|
constructor(localDir) {
|
|
23433
|
-
this.authDir =
|
|
23741
|
+
this.authDir = path17.join(localDir, "auth");
|
|
23434
23742
|
this.encryptionKey = resolveEncryptionKey(localDir);
|
|
23435
23743
|
}
|
|
23436
23744
|
save(name, url, username, password, selectors) {
|
|
23437
|
-
|
|
23745
|
+
fs22.mkdirSync(this.authDir, { recursive: true });
|
|
23438
23746
|
const { ciphertext, iv, authTag } = encrypt(password, this.encryptionKey);
|
|
23439
23747
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
23440
23748
|
const credential = {
|
|
@@ -23451,15 +23759,15 @@ var init_auth_vault = __esm({
|
|
|
23451
23759
|
createdAt: now,
|
|
23452
23760
|
updatedAt: now
|
|
23453
23761
|
};
|
|
23454
|
-
const filePath =
|
|
23455
|
-
|
|
23762
|
+
const filePath = path17.join(this.authDir, `${sanitizeName(name)}.json`);
|
|
23763
|
+
fs22.writeFileSync(filePath, JSON.stringify(credential, null, 2), { mode: 384 });
|
|
23456
23764
|
}
|
|
23457
23765
|
load(name) {
|
|
23458
|
-
const filePath =
|
|
23459
|
-
if (!
|
|
23766
|
+
const filePath = path17.join(this.authDir, `${sanitizeName(name)}.json`);
|
|
23767
|
+
if (!fs22.existsSync(filePath)) {
|
|
23460
23768
|
throw new Error(`Credential "${name}" not found. Run "browse auth list" to see saved credentials.`);
|
|
23461
23769
|
}
|
|
23462
|
-
return JSON.parse(
|
|
23770
|
+
return JSON.parse(fs22.readFileSync(filePath, "utf-8"));
|
|
23463
23771
|
}
|
|
23464
23772
|
async login(name, bm) {
|
|
23465
23773
|
const cred = this.load(name);
|
|
@@ -23480,11 +23788,11 @@ var init_auth_vault = __esm({
|
|
|
23480
23788
|
return `Logged in as ${cred.username} at ${page.url()}`;
|
|
23481
23789
|
}
|
|
23482
23790
|
list() {
|
|
23483
|
-
if (!
|
|
23484
|
-
const files =
|
|
23791
|
+
if (!fs22.existsSync(this.authDir)) return [];
|
|
23792
|
+
const files = fs22.readdirSync(this.authDir).filter((f) => f.endsWith(".json"));
|
|
23485
23793
|
return files.map((f) => {
|
|
23486
23794
|
try {
|
|
23487
|
-
const data = JSON.parse(
|
|
23795
|
+
const data = JSON.parse(fs22.readFileSync(path17.join(this.authDir, f), "utf-8"));
|
|
23488
23796
|
return {
|
|
23489
23797
|
name: data.name,
|
|
23490
23798
|
url: data.url,
|
|
@@ -23498,11 +23806,11 @@ var init_auth_vault = __esm({
|
|
|
23498
23806
|
}).filter(Boolean);
|
|
23499
23807
|
}
|
|
23500
23808
|
delete(name) {
|
|
23501
|
-
const filePath =
|
|
23502
|
-
if (!
|
|
23809
|
+
const filePath = path17.join(this.authDir, `${sanitizeName(name)}.json`);
|
|
23810
|
+
if (!fs22.existsSync(filePath)) {
|
|
23503
23811
|
throw new Error(`Credential "${name}" not found.`);
|
|
23504
23812
|
}
|
|
23505
|
-
|
|
23813
|
+
fs22.unlinkSync(filePath);
|
|
23506
23814
|
}
|
|
23507
23815
|
};
|
|
23508
23816
|
}
|
|
@@ -23639,20 +23947,20 @@ __export(policy_exports, {
|
|
|
23639
23947
|
function findFileUpward(filename) {
|
|
23640
23948
|
let dir = process.cwd();
|
|
23641
23949
|
for (let i = 0; i < 20; i++) {
|
|
23642
|
-
const candidate =
|
|
23643
|
-
if (
|
|
23644
|
-
const parent =
|
|
23950
|
+
const candidate = path18.join(dir, filename);
|
|
23951
|
+
if (fs23.existsSync(candidate)) return candidate;
|
|
23952
|
+
const parent = path18.dirname(dir);
|
|
23645
23953
|
if (parent === dir) break;
|
|
23646
23954
|
dir = parent;
|
|
23647
23955
|
}
|
|
23648
23956
|
return null;
|
|
23649
23957
|
}
|
|
23650
|
-
var
|
|
23958
|
+
var fs23, path18, PolicyChecker;
|
|
23651
23959
|
var init_policy = __esm({
|
|
23652
23960
|
"src/security/policy.ts"() {
|
|
23653
23961
|
"use strict";
|
|
23654
|
-
|
|
23655
|
-
|
|
23962
|
+
fs23 = __toESM(require("fs"), 1);
|
|
23963
|
+
path18 = __toESM(require("path"), 1);
|
|
23656
23964
|
PolicyChecker = class {
|
|
23657
23965
|
filePath = null;
|
|
23658
23966
|
lastMtime = 0;
|
|
@@ -23671,10 +23979,10 @@ var init_policy = __esm({
|
|
|
23671
23979
|
reload() {
|
|
23672
23980
|
if (!this.filePath) return;
|
|
23673
23981
|
try {
|
|
23674
|
-
const stat =
|
|
23982
|
+
const stat = fs23.statSync(this.filePath);
|
|
23675
23983
|
if (stat.mtimeMs === this.lastMtime) return;
|
|
23676
23984
|
this.lastMtime = stat.mtimeMs;
|
|
23677
|
-
const raw =
|
|
23985
|
+
const raw = fs23.readFileSync(this.filePath, "utf-8");
|
|
23678
23986
|
this.policy = JSON.parse(raw);
|
|
23679
23987
|
} catch {
|
|
23680
23988
|
}
|
|
@@ -23851,110 +24159,6 @@ var init_buffers = __esm({
|
|
|
23851
24159
|
}
|
|
23852
24160
|
});
|
|
23853
24161
|
|
|
23854
|
-
// src/app/macos/bridge.ts
|
|
23855
|
-
var bridge_exports3 = {};
|
|
23856
|
-
__export(bridge_exports3, {
|
|
23857
|
-
createMacOSBridge: () => createMacOSBridge,
|
|
23858
|
-
ensureMacOSBridge: () => ensureMacOSBridge,
|
|
23859
|
-
resolveBridgePath: () => resolveBridgePath
|
|
23860
|
-
});
|
|
23861
|
-
function resolveBridgePath() {
|
|
23862
|
-
const candidates = [
|
|
23863
|
-
// 1. Local dev build
|
|
23864
|
-
path17.resolve(__dirname_bridge, "../../../browse-ax/.build/release/browse-ax"),
|
|
23865
|
-
path17.resolve(__dirname_bridge, "../../../browse-ax/.build/debug/browse-ax"),
|
|
23866
|
-
// 2. Installed alongside source (bin/ at project root)
|
|
23867
|
-
path17.resolve(__dirname_bridge, "../../bin/browse-ax"),
|
|
23868
|
-
// 3. Bundled build (dist/browse.cjs → ../bin/)
|
|
23869
|
-
path17.resolve(__dirname_bridge, "../bin/browse-ax")
|
|
23870
|
-
];
|
|
23871
|
-
for (const p of candidates) {
|
|
23872
|
-
if (fs22.existsSync(p)) return p;
|
|
23873
|
-
}
|
|
23874
|
-
const lazyPath = path17.join(
|
|
23875
|
-
process.env.BROWSE_LOCAL_DIR || path17.join(process.cwd(), ".browse"),
|
|
23876
|
-
"bin",
|
|
23877
|
-
"browse-ax"
|
|
23878
|
-
);
|
|
23879
|
-
if (fs22.existsSync(lazyPath)) return lazyPath;
|
|
23880
|
-
throw new Error(
|
|
23881
|
-
"browse-ax binary not found. Run: browse enable macos\nOr build manually: cd browse-ax && swift build -c release"
|
|
23882
|
-
);
|
|
23883
|
-
}
|
|
23884
|
-
async function ensureMacOSBridge() {
|
|
23885
|
-
if (process.platform !== "darwin") {
|
|
23886
|
-
throw new Error("App automation requires macOS (uses Accessibility API)");
|
|
23887
|
-
}
|
|
23888
|
-
return resolveBridgePath();
|
|
23889
|
-
}
|
|
23890
|
-
async function execBridge(bridgePath, args) {
|
|
23891
|
-
return new Promise((resolve9, reject) => {
|
|
23892
|
-
const proc = (0, import_child_process9.spawn)(bridgePath, args, {
|
|
23893
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
23894
|
-
});
|
|
23895
|
-
const chunks = [];
|
|
23896
|
-
const errChunks = [];
|
|
23897
|
-
proc.stdout.on("data", (c) => chunks.push(c));
|
|
23898
|
-
proc.stderr.on("data", (c) => errChunks.push(c));
|
|
23899
|
-
proc.on("close", (code) => {
|
|
23900
|
-
const stdout = Buffer.concat(chunks).toString("utf-8").trim();
|
|
23901
|
-
const stderr = Buffer.concat(errChunks).toString("utf-8").trim();
|
|
23902
|
-
if (code !== 0) {
|
|
23903
|
-
try {
|
|
23904
|
-
const err = JSON.parse(stderr || stdout);
|
|
23905
|
-
reject(new Error(err.error || `Bridge exited with code ${code}`));
|
|
23906
|
-
} catch {
|
|
23907
|
-
reject(new Error(stderr || `Bridge exited with code ${code}`));
|
|
23908
|
-
}
|
|
23909
|
-
return;
|
|
23910
|
-
}
|
|
23911
|
-
try {
|
|
23912
|
-
resolve9(JSON.parse(stdout));
|
|
23913
|
-
} catch {
|
|
23914
|
-
reject(new Error(`Invalid bridge output: ${stdout.slice(0, 200)}`));
|
|
23915
|
-
}
|
|
23916
|
-
});
|
|
23917
|
-
});
|
|
23918
|
-
}
|
|
23919
|
-
function createMacOSBridge(bridgePath, pid) {
|
|
23920
|
-
const base = ["--pid", String(pid)];
|
|
23921
|
-
return {
|
|
23922
|
-
async tree() {
|
|
23923
|
-
return execBridge(bridgePath, [...base, "tree"]);
|
|
23924
|
-
},
|
|
23925
|
-
async action(nodePath, actionName) {
|
|
23926
|
-
return execBridge(bridgePath, [...base, "action", JSON.stringify(nodePath), actionName]);
|
|
23927
|
-
},
|
|
23928
|
-
async setValue(nodePath, value) {
|
|
23929
|
-
return execBridge(bridgePath, [...base, "set-value", JSON.stringify(nodePath), value]);
|
|
23930
|
-
},
|
|
23931
|
-
async type(text) {
|
|
23932
|
-
return execBridge(bridgePath, [...base, "type", text]);
|
|
23933
|
-
},
|
|
23934
|
-
async press(key) {
|
|
23935
|
-
return execBridge(bridgePath, [...base, "press", key]);
|
|
23936
|
-
},
|
|
23937
|
-
async screenshot(outputPath) {
|
|
23938
|
-
return execBridge(bridgePath, [...base, "screenshot", outputPath]);
|
|
23939
|
-
},
|
|
23940
|
-
async state() {
|
|
23941
|
-
return execBridge(bridgePath, [...base, "state"]);
|
|
23942
|
-
}
|
|
23943
|
-
};
|
|
23944
|
-
}
|
|
23945
|
-
var import_child_process9, path17, fs22, import_url6, __filename_bridge2, __dirname_bridge;
|
|
23946
|
-
var init_bridge3 = __esm({
|
|
23947
|
-
"src/app/macos/bridge.ts"() {
|
|
23948
|
-
"use strict";
|
|
23949
|
-
import_child_process9 = require("child_process");
|
|
23950
|
-
path17 = __toESM(require("path"), 1);
|
|
23951
|
-
fs22 = __toESM(require("fs"), 1);
|
|
23952
|
-
import_url6 = require("url");
|
|
23953
|
-
__filename_bridge2 = (0, import_url6.fileURLToPath)(__import_meta_url);
|
|
23954
|
-
__dirname_bridge = path17.dirname(__filename_bridge2);
|
|
23955
|
-
}
|
|
23956
|
-
});
|
|
23957
|
-
|
|
23958
24162
|
// src/commands/meta/system.ts
|
|
23959
24163
|
async function handleSystemCommand(command, args, target, shutdown2, sessionManager2, currentSession, lifecycle) {
|
|
23960
24164
|
switch (command) {
|
|
@@ -24056,7 +24260,7 @@ async function handleSystemCommand(command, args, target, shutdown2, sessionMana
|
|
|
24056
24260
|
return results.join("\n\n");
|
|
24057
24261
|
}
|
|
24058
24262
|
case "doctor": {
|
|
24059
|
-
const { execSync:
|
|
24263
|
+
const { execSync: execSync8 } = await import("child_process");
|
|
24060
24264
|
const platformArg = args.find((a) => a.startsWith("--platform="))?.split("=")[1] || (args.indexOf("--platform") !== -1 ? args[args.indexOf("--platform") + 1] : "");
|
|
24061
24265
|
const lines = [];
|
|
24062
24266
|
lines.push(`Node: ${process.version}`);
|
|
@@ -24086,7 +24290,7 @@ async function handleSystemCommand(command, args, target, shutdown2, sessionMana
|
|
|
24086
24290
|
lines.push("");
|
|
24087
24291
|
lines.push("--- Android ---");
|
|
24088
24292
|
try {
|
|
24089
|
-
const adbVersion =
|
|
24293
|
+
const adbVersion = execSync8("adb version", { encoding: "utf-8", timeout: 5e3 }).split("\n")[0].trim();
|
|
24090
24294
|
lines.push(`adb: ${adbVersion}`);
|
|
24091
24295
|
} catch {
|
|
24092
24296
|
lines.push("adb: NOT FOUND \u2014 install Android SDK platform-tools and add to PATH");
|
|
@@ -24094,7 +24298,7 @@ async function handleSystemCommand(command, args, target, shutdown2, sessionMana
|
|
|
24094
24298
|
if (platformArg === "android") return lines.join("\n");
|
|
24095
24299
|
}
|
|
24096
24300
|
try {
|
|
24097
|
-
const devicesOut =
|
|
24301
|
+
const devicesOut = execSync8("adb devices", { encoding: "utf-8", timeout: 5e3 });
|
|
24098
24302
|
const deviceLines = devicesOut.split("\n").slice(1).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
24099
24303
|
const booted = deviceLines.filter((l) => l.endsWith(" device"));
|
|
24100
24304
|
const other = deviceLines.filter((l) => !l.endsWith(" device"));
|
|
@@ -24111,8 +24315,8 @@ async function handleSystemCommand(command, args, target, shutdown2, sessionMana
|
|
|
24111
24315
|
lines.push("Devices: could not query (adb failed)");
|
|
24112
24316
|
}
|
|
24113
24317
|
try {
|
|
24114
|
-
|
|
24115
|
-
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();
|
|
24116
24320
|
if (avds) {
|
|
24117
24321
|
lines.push(`AVDs: ${avds.split("\n").join(", ")}`);
|
|
24118
24322
|
} else {
|
|
@@ -24122,13 +24326,13 @@ async function handleSystemCommand(command, args, target, shutdown2, sessionMana
|
|
|
24122
24326
|
lines.push("Emulator: not found (optional \u2014 only needed to start AVDs from CLI)");
|
|
24123
24327
|
}
|
|
24124
24328
|
try {
|
|
24125
|
-
const
|
|
24126
|
-
const
|
|
24127
|
-
const localBuild =
|
|
24128
|
-
const installed =
|
|
24129
|
-
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)) {
|
|
24130
24334
|
lines.push(`Driver APK: ${localBuild}`);
|
|
24131
|
-
} else if (
|
|
24335
|
+
} else if (fs30.existsSync(installed)) {
|
|
24132
24336
|
lines.push(`Driver APK: ${installed}`);
|
|
24133
24337
|
} else {
|
|
24134
24338
|
lines.push("Driver APK: NOT FOUND");
|
|
@@ -24141,9 +24345,9 @@ async function handleSystemCommand(command, args, target, shutdown2, sessionMana
|
|
|
24141
24345
|
return lines.join("\n");
|
|
24142
24346
|
}
|
|
24143
24347
|
case "upgrade": {
|
|
24144
|
-
const { execSync:
|
|
24348
|
+
const { execSync: execSync8 } = await import("child_process");
|
|
24145
24349
|
try {
|
|
24146
|
-
const output =
|
|
24350
|
+
const output = execSync8("npm update -g @ulpi/browse 2>&1", { encoding: "utf-8", timeout: 3e4 });
|
|
24147
24351
|
return `Upgrade complete.
|
|
24148
24352
|
${output.trim()}`;
|
|
24149
24353
|
} catch (err) {
|
|
@@ -24450,21 +24654,21 @@ var init_refs = __esm({
|
|
|
24450
24654
|
function getProfileDir(localDir, name) {
|
|
24451
24655
|
const sanitized = sanitizeName(name);
|
|
24452
24656
|
if (!sanitized) throw new Error("Invalid profile name");
|
|
24453
|
-
return
|
|
24657
|
+
return path19.join(localDir, "profiles", sanitized);
|
|
24454
24658
|
}
|
|
24455
24659
|
function listProfiles(localDir) {
|
|
24456
|
-
const profilesDir =
|
|
24457
|
-
if (!
|
|
24458
|
-
return
|
|
24459
|
-
const dir =
|
|
24460
|
-
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);
|
|
24461
24665
|
let totalSize = 0;
|
|
24462
24666
|
try {
|
|
24463
|
-
const files =
|
|
24667
|
+
const files = fs24.readdirSync(dir, { recursive: true, withFileTypes: true });
|
|
24464
24668
|
for (const f of files) {
|
|
24465
24669
|
if (f.isFile()) {
|
|
24466
24670
|
try {
|
|
24467
|
-
totalSize +=
|
|
24671
|
+
totalSize += fs24.statSync(path19.join(f.parentPath || f.path || dir, f.name)).size;
|
|
24468
24672
|
} catch {
|
|
24469
24673
|
}
|
|
24470
24674
|
}
|
|
@@ -24481,15 +24685,15 @@ function listProfiles(localDir) {
|
|
|
24481
24685
|
}
|
|
24482
24686
|
function deleteProfile(localDir, name) {
|
|
24483
24687
|
const dir = getProfileDir(localDir, name);
|
|
24484
|
-
if (!
|
|
24485
|
-
|
|
24688
|
+
if (!fs24.existsSync(dir)) throw new Error(`Profile "${name}" not found`);
|
|
24689
|
+
fs24.rmSync(dir, { recursive: true, force: true });
|
|
24486
24690
|
}
|
|
24487
|
-
var
|
|
24691
|
+
var path19, fs24;
|
|
24488
24692
|
var init_profiles = __esm({
|
|
24489
24693
|
"src/browser/profiles.ts"() {
|
|
24490
24694
|
"use strict";
|
|
24491
|
-
|
|
24492
|
-
|
|
24695
|
+
path19 = __toESM(require("path"), 1);
|
|
24696
|
+
fs24 = __toESM(require("fs"), 1);
|
|
24493
24697
|
init_sanitize();
|
|
24494
24698
|
}
|
|
24495
24699
|
});
|
|
@@ -24663,9 +24867,9 @@ var init_manager = __esm({
|
|
|
24663
24867
|
});
|
|
24664
24868
|
} catch (err) {
|
|
24665
24869
|
if (err.message?.includes("Failed to launch") || err.message?.includes("Target closed")) {
|
|
24666
|
-
const
|
|
24870
|
+
const fs30 = await import("fs");
|
|
24667
24871
|
console.error(`[browse] Profile directory corrupted, recreating: ${profileDir}`);
|
|
24668
|
-
|
|
24872
|
+
fs30.rmSync(profileDir, { recursive: true, force: true });
|
|
24669
24873
|
context = await import_playwright2.chromium.launchPersistentContext(profileDir, {
|
|
24670
24874
|
headless: process.env.BROWSE_HEADED !== "1",
|
|
24671
24875
|
viewport: { width: 1920, height: 1080 }
|
|
@@ -25393,8 +25597,8 @@ var init_manager = __esm({
|
|
|
25393
25597
|
// ─── Video Recording ──────────────────────────────────────
|
|
25394
25598
|
async startVideoRecording(dir) {
|
|
25395
25599
|
if (this.videoRecording) throw new Error("Video recording already active");
|
|
25396
|
-
const
|
|
25397
|
-
|
|
25600
|
+
const fs30 = await import("fs");
|
|
25601
|
+
fs30.mkdirSync(dir, { recursive: true });
|
|
25398
25602
|
this.videoRecording = { dir, startedAt: Date.now() };
|
|
25399
25603
|
const viewport = this.currentDevice?.viewport || { width: 1920, height: 1080 };
|
|
25400
25604
|
await this.recreateContext({
|
|
@@ -25691,10 +25895,10 @@ __export(react_devtools_exports, {
|
|
|
25691
25895
|
requireReact: () => requireReact
|
|
25692
25896
|
});
|
|
25693
25897
|
async function ensureHook() {
|
|
25694
|
-
if (
|
|
25695
|
-
return
|
|
25898
|
+
if (fs25.existsSync(HOOK_PATH)) {
|
|
25899
|
+
return fs25.readFileSync(HOOK_PATH, "utf8");
|
|
25696
25900
|
}
|
|
25697
|
-
|
|
25901
|
+
fs25.mkdirSync(CACHE_DIR, { recursive: true });
|
|
25698
25902
|
const res = await fetch(HOOK_URL);
|
|
25699
25903
|
if (!res.ok) {
|
|
25700
25904
|
throw new Error(
|
|
@@ -25703,7 +25907,7 @@ Manual fallback: npm install -g react-devtools-core, then copy installHook.js to
|
|
|
25703
25907
|
);
|
|
25704
25908
|
}
|
|
25705
25909
|
const script = await res.text();
|
|
25706
|
-
|
|
25910
|
+
fs25.writeFileSync(HOOK_PATH, script);
|
|
25707
25911
|
return script;
|
|
25708
25912
|
}
|
|
25709
25913
|
async function injectHook(bm) {
|
|
@@ -25850,15 +26054,15 @@ async function getSuspense(bm, page) {
|
|
|
25850
26054
|
if (!roots || roots.size === 0) return "No fiber roots found";
|
|
25851
26055
|
const root = roots.values().next().value;
|
|
25852
26056
|
const boundaries = [];
|
|
25853
|
-
const walk = (fiber,
|
|
26057
|
+
const walk = (fiber, path25) => {
|
|
25854
26058
|
if (!fiber) return;
|
|
25855
26059
|
if (fiber.tag === 13) {
|
|
25856
26060
|
const resolved = fiber.memoizedState === null;
|
|
25857
|
-
const parent =
|
|
26061
|
+
const parent = path25.length > 0 ? path25[path25.length - 1] : "root";
|
|
25858
26062
|
boundaries.push(`Suspense in ${parent} \u2014 ${resolved ? "resolved (children visible)" : "pending (showing fallback)"}`);
|
|
25859
26063
|
}
|
|
25860
26064
|
const name = fiber.type?.displayName || fiber.type?.name || null;
|
|
25861
|
-
const newPath = name ? [...
|
|
26065
|
+
const newPath = name ? [...path25, name] : path25;
|
|
25862
26066
|
let child = fiber.child;
|
|
25863
26067
|
while (child) {
|
|
25864
26068
|
walk(child, newPath);
|
|
@@ -26037,15 +26241,15 @@ async function getContext(bm, page, selector) {
|
|
|
26037
26241
|
await handle.dispose();
|
|
26038
26242
|
return result;
|
|
26039
26243
|
}
|
|
26040
|
-
var
|
|
26244
|
+
var fs25, path20, os4, CACHE_DIR, HOOK_PATH, HOOK_URL;
|
|
26041
26245
|
var init_react_devtools = __esm({
|
|
26042
26246
|
"src/browser/react-devtools.ts"() {
|
|
26043
26247
|
"use strict";
|
|
26044
|
-
|
|
26045
|
-
|
|
26046
|
-
|
|
26047
|
-
CACHE_DIR =
|
|
26048
|
-
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");
|
|
26049
26253
|
HOOK_URL = "https://unpkg.com/react-devtools-core@latest/dist/installHook.js";
|
|
26050
26254
|
}
|
|
26051
26255
|
});
|
|
@@ -26333,9 +26537,9 @@ function getFlowsDir() {
|
|
|
26333
26537
|
const root = findProjectRoot();
|
|
26334
26538
|
if (config.flowPaths?.length && root) {
|
|
26335
26539
|
for (const fp of config.flowPaths) {
|
|
26336
|
-
const abs =
|
|
26540
|
+
const abs = path21.isAbsolute(fp) ? fp : path21.join(root, fp);
|
|
26337
26541
|
try {
|
|
26338
|
-
|
|
26542
|
+
fs26.mkdirSync(abs, { recursive: true });
|
|
26339
26543
|
return abs;
|
|
26340
26544
|
} catch {
|
|
26341
26545
|
}
|
|
@@ -26343,17 +26547,17 @@ function getFlowsDir() {
|
|
|
26343
26547
|
}
|
|
26344
26548
|
const localDir = process.env.BROWSE_LOCAL_DIR;
|
|
26345
26549
|
if (localDir) {
|
|
26346
|
-
const dir2 =
|
|
26347
|
-
|
|
26550
|
+
const dir2 = path21.join(localDir, "flows");
|
|
26551
|
+
fs26.mkdirSync(dir2, { recursive: true });
|
|
26348
26552
|
return dir2;
|
|
26349
26553
|
}
|
|
26350
26554
|
if (root) {
|
|
26351
|
-
const dir2 =
|
|
26352
|
-
|
|
26555
|
+
const dir2 = path21.join(root, ".browse", "flows");
|
|
26556
|
+
fs26.mkdirSync(dir2, { recursive: true });
|
|
26353
26557
|
return dir2;
|
|
26354
26558
|
}
|
|
26355
|
-
const dir =
|
|
26356
|
-
|
|
26559
|
+
const dir = path21.join(process.cwd(), ".browse", "flows");
|
|
26560
|
+
fs26.mkdirSync(dir, { recursive: true });
|
|
26357
26561
|
return dir;
|
|
26358
26562
|
}
|
|
26359
26563
|
function validateFlowName(name) {
|
|
@@ -26473,18 +26677,18 @@ async function handleFlowsCommand(command, args, target, shutdown2, sessionManag
|
|
|
26473
26677
|
}
|
|
26474
26678
|
const { exportFlowYaml: exportFlowYaml2 } = await Promise.resolve().then(() => (init_record(), record_exports));
|
|
26475
26679
|
const yamlContent = exportFlowYaml2(steps2);
|
|
26476
|
-
const flowPath =
|
|
26477
|
-
|
|
26680
|
+
const flowPath = path21.join(getFlowsDir(), `${name}.yaml`);
|
|
26681
|
+
fs26.writeFileSync(flowPath, yamlContent, "utf-8");
|
|
26478
26682
|
return `Flow saved: ${flowPath} (${steps2.length} steps)`;
|
|
26479
26683
|
}
|
|
26480
26684
|
if (subOrFile === "run") {
|
|
26481
26685
|
const name = args[1];
|
|
26482
26686
|
if (!name) throw new Error("Usage: browse flow run <name>");
|
|
26483
26687
|
validateFlowName(name);
|
|
26484
|
-
const flowPath =
|
|
26688
|
+
const flowPath = path21.join(getFlowsDir(), `${name}.yaml`);
|
|
26485
26689
|
let content2;
|
|
26486
26690
|
try {
|
|
26487
|
-
content2 =
|
|
26691
|
+
content2 = fs26.readFileSync(flowPath, "utf-8");
|
|
26488
26692
|
} catch (err) {
|
|
26489
26693
|
if (err.code === "ENOENT") {
|
|
26490
26694
|
throw new Error(`Saved flow not found: "${name}" (looked at ${flowPath})`);
|
|
@@ -26497,18 +26701,18 @@ async function handleFlowsCommand(command, args, target, shutdown2, sessionManag
|
|
|
26497
26701
|
}
|
|
26498
26702
|
if (subOrFile === "list") {
|
|
26499
26703
|
const flowsDir = getFlowsDir();
|
|
26500
|
-
if (!
|
|
26704
|
+
if (!fs26.existsSync(flowsDir)) {
|
|
26501
26705
|
return "No saved flows (directory does not exist yet)";
|
|
26502
26706
|
}
|
|
26503
|
-
const entries =
|
|
26707
|
+
const entries = fs26.readdirSync(flowsDir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).sort();
|
|
26504
26708
|
if (entries.length === 0) {
|
|
26505
26709
|
return "No saved flows";
|
|
26506
26710
|
}
|
|
26507
26711
|
const lines = [`Saved flows (${flowsDir}):`];
|
|
26508
26712
|
for (const entry of entries) {
|
|
26509
26713
|
const name = entry.replace(/\.(yaml|yml)$/, "");
|
|
26510
|
-
const fullPath =
|
|
26511
|
-
const stat =
|
|
26714
|
+
const fullPath = path21.join(flowsDir, entry);
|
|
26715
|
+
const stat = fs26.statSync(fullPath);
|
|
26512
26716
|
const mtime = new Date(stat.mtimeMs).toISOString().replace("T", " ").slice(0, 19);
|
|
26513
26717
|
lines.push(` ${name} (${mtime})`);
|
|
26514
26718
|
}
|
|
@@ -26517,7 +26721,7 @@ async function handleFlowsCommand(command, args, target, shutdown2, sessionManag
|
|
|
26517
26721
|
const filePath = subOrFile;
|
|
26518
26722
|
let content;
|
|
26519
26723
|
try {
|
|
26520
|
-
content =
|
|
26724
|
+
content = fs26.readFileSync(filePath, "utf-8");
|
|
26521
26725
|
} catch (err) {
|
|
26522
26726
|
if (err.code === "ENOENT") {
|
|
26523
26727
|
throw new Error(`Flow file not found: ${filePath}`);
|
|
@@ -26559,7 +26763,7 @@ async function handleFlowsCommand(command, args, target, shutdown2, sessionManag
|
|
|
26559
26763
|
lastError = err.message;
|
|
26560
26764
|
if (attempt < maxAttempts) {
|
|
26561
26765
|
if (backoff) {
|
|
26562
|
-
await new Promise((
|
|
26766
|
+
await new Promise((resolve10) => setTimeout(resolve10, delay));
|
|
26563
26767
|
delay *= 2;
|
|
26564
26768
|
}
|
|
26565
26769
|
continue;
|
|
@@ -26579,7 +26783,7 @@ async function handleFlowsCommand(command, args, target, shutdown2, sessionManag
|
|
|
26579
26783
|
);
|
|
26580
26784
|
}
|
|
26581
26785
|
if (backoff) {
|
|
26582
|
-
await new Promise((
|
|
26786
|
+
await new Promise((resolve10) => setTimeout(resolve10, delay));
|
|
26583
26787
|
delay *= 2;
|
|
26584
26788
|
}
|
|
26585
26789
|
}
|
|
@@ -26632,7 +26836,7 @@ async function handleFlowsCommand(command, args, target, shutdown2, sessionManag
|
|
|
26632
26836
|
changeSummary = result.summary;
|
|
26633
26837
|
break;
|
|
26634
26838
|
}
|
|
26635
|
-
await new Promise((
|
|
26839
|
+
await new Promise((resolve10) => setTimeout(resolve10, POLL_INTERVAL));
|
|
26636
26840
|
}
|
|
26637
26841
|
await page.evaluate((wid) => {
|
|
26638
26842
|
const info = window[wid];
|
|
@@ -26783,13 +26987,13 @@ function parseCommandString(input) {
|
|
|
26783
26987
|
}
|
|
26784
26988
|
return parts;
|
|
26785
26989
|
}
|
|
26786
|
-
var
|
|
26990
|
+
var fs26, path21, DEFAULT_MAX_FLOW_DEPTH, flowDepthMap, sessionlessSentinel;
|
|
26787
26991
|
var init_flows = __esm({
|
|
26788
26992
|
"src/commands/meta/flows.ts"() {
|
|
26789
26993
|
"use strict";
|
|
26790
26994
|
init_config();
|
|
26791
|
-
|
|
26792
|
-
|
|
26995
|
+
fs26 = __toESM(require("fs"), 1);
|
|
26996
|
+
path21 = __toESM(require("path"), 1);
|
|
26793
26997
|
DEFAULT_MAX_FLOW_DEPTH = 10;
|
|
26794
26998
|
flowDepthMap = /* @__PURE__ */ new WeakMap();
|
|
26795
26999
|
sessionlessSentinel = {};
|
|
@@ -26800,7 +27004,7 @@ var init_flows = __esm({
|
|
|
26800
27004
|
async function handleSimCommand(command, args, bm, shutdown2, sessionManager2, currentSession) {
|
|
26801
27005
|
const sub = args[0];
|
|
26802
27006
|
if (!sub || !["start", "stop", "status"].includes(sub)) {
|
|
26803
|
-
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");
|
|
26804
27008
|
}
|
|
26805
27009
|
let platform = "ios";
|
|
26806
27010
|
for (let i = 1; i < args.length; i++) {
|
|
@@ -27912,13 +28116,14 @@ var init_registry = __esm({
|
|
|
27912
28116
|
inputSchema: { type: "object", properties: { json: { type: "boolean", description: "Return raw JSON instead of formatted text." } } },
|
|
27913
28117
|
argDecode: (p) => p.json ? ["--json"] : []
|
|
27914
28118
|
} }),
|
|
27915
|
-
m("sim", "Simulator/emulator lifecycle", { usage: "start|stop|status [--platform ios|android] [--device <name>]", mcp: {
|
|
27916
|
-
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.",
|
|
27917
|
-
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"] },
|
|
27918
28122
|
argDecode: (p) => {
|
|
27919
28123
|
const args = [String(p.action)];
|
|
27920
28124
|
if (p.platform) args.push("--platform", String(p.platform));
|
|
27921
28125
|
if (p.device) args.push("--device", String(p.device));
|
|
28126
|
+
if (p.app) args.push("--app", String(p.app));
|
|
27922
28127
|
return args;
|
|
27923
28128
|
}
|
|
27924
28129
|
} })
|
|
@@ -28470,9 +28675,9 @@ var manager_exports2 = {};
|
|
|
28470
28675
|
__export(manager_exports2, {
|
|
28471
28676
|
AndroidAppManager: () => AndroidAppManager
|
|
28472
28677
|
});
|
|
28473
|
-
function normalizeNode(raw,
|
|
28678
|
+
function normalizeNode(raw, path25 = []) {
|
|
28474
28679
|
return {
|
|
28475
|
-
path:
|
|
28680
|
+
path: path25,
|
|
28476
28681
|
role: androidRole(raw.className),
|
|
28477
28682
|
label: raw.text ?? raw.hint ?? "",
|
|
28478
28683
|
value: raw.editable ? raw.text ?? "" : void 0,
|
|
@@ -28487,7 +28692,7 @@ function normalizeNode(raw, path24 = []) {
|
|
|
28487
28692
|
selected: raw.selected,
|
|
28488
28693
|
editable: raw.editable,
|
|
28489
28694
|
actions: deriveActions(raw),
|
|
28490
|
-
children: raw.children.map((child, i) => normalizeNode(child, [...
|
|
28695
|
+
children: raw.children.map((child, i) => normalizeNode(child, [...path25, i]))
|
|
28491
28696
|
};
|
|
28492
28697
|
}
|
|
28493
28698
|
function androidRole(className) {
|
|
@@ -28603,15 +28808,15 @@ var init_manager2 = __esm({
|
|
|
28603
28808
|
}
|
|
28604
28809
|
/** Tap (click) an element by ref */
|
|
28605
28810
|
async tap(ref) {
|
|
28606
|
-
const { path:
|
|
28607
|
-
const result = await this.bridge.action(
|
|
28811
|
+
const { path: path25, label } = this.resolveRef(ref);
|
|
28812
|
+
const result = await this.bridge.action(path25, "click");
|
|
28608
28813
|
if (!result.success) throw new Error(result.error ?? "Tap failed");
|
|
28609
28814
|
return `Tapped ${ref}${label ? ` "${label}"` : ""}`;
|
|
28610
28815
|
}
|
|
28611
28816
|
/** Fill a text field by ref */
|
|
28612
28817
|
async fill(ref, value) {
|
|
28613
|
-
const { path:
|
|
28614
|
-
const result = await this.bridge.setValue(
|
|
28818
|
+
const { path: path25 } = this.resolveRef(ref);
|
|
28819
|
+
const result = await this.bridge.setValue(path25, value);
|
|
28615
28820
|
if (!result.success) throw new Error(result.error ?? "Fill failed");
|
|
28616
28821
|
return `Filled ${ref} with "${value}"`;
|
|
28617
28822
|
}
|
|
@@ -28632,8 +28837,8 @@ var init_manager2 = __esm({
|
|
|
28632
28837
|
const actionName = actionMap[direction.toLowerCase()];
|
|
28633
28838
|
if (!actionName) throw new Error(`Invalid swipe direction: ${direction}. Use up/down/left/right.`);
|
|
28634
28839
|
if (ref) {
|
|
28635
|
-
const { path:
|
|
28636
|
-
const result2 = await this.bridge.action(
|
|
28840
|
+
const { path: path25, label } = this.resolveRef(ref);
|
|
28841
|
+
const result2 = await this.bridge.action(path25, actionName);
|
|
28637
28842
|
if (!result2.success) return this.swipeBoundaryMessage(direction, result2.error);
|
|
28638
28843
|
return `Swiped ${direction} on ${ref}${label ? ` "${label}"` : ""}`;
|
|
28639
28844
|
}
|
|
@@ -28799,16 +29004,16 @@ var init_manager3 = __esm({
|
|
|
28799
29004
|
/** Tap (press) an element by ref. */
|
|
28800
29005
|
async tap(ref) {
|
|
28801
29006
|
this.requireBridge();
|
|
28802
|
-
const { path:
|
|
28803
|
-
const result = await this.bridge.action(
|
|
29007
|
+
const { path: path25, label } = this.resolveRef(ref);
|
|
29008
|
+
const result = await this.bridge.action(path25, "AXPress");
|
|
28804
29009
|
if (!result.success) throw new Error(result.error || "Tap failed");
|
|
28805
29010
|
return `Tapped ${ref}${label ? ` "${label}"` : ""}`;
|
|
28806
29011
|
}
|
|
28807
29012
|
/** Fill a text field by ref. */
|
|
28808
29013
|
async fill(ref, value) {
|
|
28809
29014
|
this.requireBridge();
|
|
28810
|
-
const { path:
|
|
28811
|
-
const result = await this.bridge.setValue(
|
|
29015
|
+
const { path: path25 } = this.resolveRef(ref);
|
|
29016
|
+
const result = await this.bridge.setValue(path25, value);
|
|
28812
29017
|
if (!result.success) throw new Error(result.error || "Fill failed");
|
|
28813
29018
|
return `Filled ${ref} with "${value}"`;
|
|
28814
29019
|
}
|
|
@@ -28824,8 +29029,8 @@ var init_manager3 = __esm({
|
|
|
28824
29029
|
this.requireBridge();
|
|
28825
29030
|
const actionName = `swipe${direction.charAt(0).toUpperCase()}${direction.slice(1).toLowerCase()}`;
|
|
28826
29031
|
if (ref) {
|
|
28827
|
-
const { path:
|
|
28828
|
-
const result2 = await this.bridge.action(
|
|
29032
|
+
const { path: path25, label } = this.resolveRef(ref);
|
|
29033
|
+
const result2 = await this.bridge.action(path25, actionName);
|
|
28829
29034
|
if (!result2.success) throw new Error(result2.error || `Swipe ${direction} failed`);
|
|
28830
29035
|
return `Swiped ${direction} on ${ref}${label ? ` "${label}"` : ""}`;
|
|
28831
29036
|
}
|
|
@@ -28974,15 +29179,15 @@ var init_manager4 = __esm({
|
|
|
28974
29179
|
}
|
|
28975
29180
|
/** Tap (press) an element by ref */
|
|
28976
29181
|
async tap(ref) {
|
|
28977
|
-
const { path:
|
|
28978
|
-
const result = await this.bridge.action(
|
|
29182
|
+
const { path: path25, label } = this.resolveRef(ref);
|
|
29183
|
+
const result = await this.bridge.action(path25, "AXPress");
|
|
28979
29184
|
if (!result.success) throw new Error(result.error || "Tap failed");
|
|
28980
29185
|
return `Tapped ${ref}${label ? ` "${label}"` : ""}`;
|
|
28981
29186
|
}
|
|
28982
29187
|
/** Fill a text field by ref */
|
|
28983
29188
|
async fill(ref, value) {
|
|
28984
|
-
const { path:
|
|
28985
|
-
const result = await this.bridge.setValue(
|
|
29189
|
+
const { path: path25 } = this.resolveRef(ref);
|
|
29190
|
+
const result = await this.bridge.setValue(path25, value);
|
|
28986
29191
|
if (!result.success) throw new Error(result.error || "Fill failed");
|
|
28987
29192
|
return `Filled ${ref} with "${value}"`;
|
|
28988
29193
|
}
|
|
@@ -29003,8 +29208,8 @@ var init_manager4 = __esm({
|
|
|
29003
29208
|
const actionName = actionMap[direction.toLowerCase()];
|
|
29004
29209
|
if (!actionName) throw new Error(`Invalid swipe direction: ${direction}. Use up/down/left/right.`);
|
|
29005
29210
|
if (ref) {
|
|
29006
|
-
const { path:
|
|
29007
|
-
const result2 = await this.bridge.action(
|
|
29211
|
+
const { path: path25, label } = this.resolveRef(ref);
|
|
29212
|
+
const result2 = await this.bridge.action(path25, actionName);
|
|
29008
29213
|
if (!result2.success) throw new Error(result2.error || `Swipe ${direction} failed`);
|
|
29009
29214
|
return `Swiped ${direction} on ${ref}${label ? ` "${label}"` : ""}`;
|
|
29010
29215
|
}
|
|
@@ -29128,10 +29333,10 @@ function createAppTargetFactory(appName) {
|
|
|
29128
29333
|
const { ensureMacOSBridge: ensureMacOSBridge2, createMacOSBridge: createMacOSBridge2 } = await Promise.resolve().then(() => (init_bridge3(), bridge_exports3));
|
|
29129
29334
|
const { AppManager: AppManager2 } = await Promise.resolve().then(() => (init_manager4(), manager_exports4));
|
|
29130
29335
|
const bridgePath = await ensureMacOSBridge2();
|
|
29131
|
-
const { execSync:
|
|
29336
|
+
const { execSync: execSync8 } = await import("child_process");
|
|
29132
29337
|
let pid;
|
|
29133
29338
|
try {
|
|
29134
|
-
const output =
|
|
29339
|
+
const output = execSync8(`pgrep -xo "${appName}"`, { encoding: "utf-8" }).trim();
|
|
29135
29340
|
pid = parseInt(output, 10);
|
|
29136
29341
|
if (isNaN(pid)) throw new Error(`App '${appName}' is not running`);
|
|
29137
29342
|
} catch (err) {
|
|
@@ -29183,7 +29388,7 @@ __export(resolver_exports, {
|
|
|
29183
29388
|
});
|
|
29184
29389
|
function findLightpanda() {
|
|
29185
29390
|
try {
|
|
29186
|
-
const result = (0,
|
|
29391
|
+
const result = (0, import_child_process11.execSync)("which lightpanda", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
29187
29392
|
if (result) return result;
|
|
29188
29393
|
} catch {
|
|
29189
29394
|
}
|
|
@@ -29207,13 +29412,13 @@ async function getRuntime(name) {
|
|
|
29207
29412
|
}
|
|
29208
29413
|
return loader();
|
|
29209
29414
|
}
|
|
29210
|
-
var import_os, import_fs,
|
|
29415
|
+
var import_os, import_fs, import_child_process11, import_path, net2, registry2, AVAILABLE_RUNTIMES;
|
|
29211
29416
|
var init_resolver = __esm({
|
|
29212
29417
|
"src/engine/resolver.ts"() {
|
|
29213
29418
|
"use strict";
|
|
29214
29419
|
import_os = require("os");
|
|
29215
29420
|
import_fs = require("fs");
|
|
29216
|
-
|
|
29421
|
+
import_child_process11 = require("child_process");
|
|
29217
29422
|
import_path = require("path");
|
|
29218
29423
|
net2 = __toESM(require("net"), 1);
|
|
29219
29424
|
registry2 = {
|
|
@@ -29238,16 +29443,16 @@ var init_resolver = __esm({
|
|
|
29238
29443
|
"Lightpanda not found. Install: https://lightpanda.io/docs/open-source/installation"
|
|
29239
29444
|
);
|
|
29240
29445
|
}
|
|
29241
|
-
const port = await new Promise((
|
|
29446
|
+
const port = await new Promise((resolve10, reject) => {
|
|
29242
29447
|
const srv = net2.createServer();
|
|
29243
29448
|
srv.listen(0, "127.0.0.1", () => {
|
|
29244
29449
|
const addr = srv.address();
|
|
29245
29450
|
const p = typeof addr === "object" && addr ? addr.port : 0;
|
|
29246
|
-
srv.close(() =>
|
|
29451
|
+
srv.close(() => resolve10(p));
|
|
29247
29452
|
});
|
|
29248
29453
|
srv.on("error", reject);
|
|
29249
29454
|
});
|
|
29250
|
-
const child = (0,
|
|
29455
|
+
const child = (0, import_child_process11.spawn)(
|
|
29251
29456
|
binaryPath,
|
|
29252
29457
|
["serve", "--host", "127.0.0.1", "--port", String(port), "--timeout", "604800"],
|
|
29253
29458
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
@@ -29603,7 +29808,7 @@ var init_domain_filter = __esm({
|
|
|
29603
29808
|
});
|
|
29604
29809
|
|
|
29605
29810
|
// src/session/manager.ts
|
|
29606
|
-
var
|
|
29811
|
+
var fs27, path22, SessionManager;
|
|
29607
29812
|
var init_manager5 = __esm({
|
|
29608
29813
|
"src/session/manager.ts"() {
|
|
29609
29814
|
"use strict";
|
|
@@ -29612,8 +29817,8 @@ var init_manager5 = __esm({
|
|
|
29612
29817
|
init_sanitize();
|
|
29613
29818
|
init_persist();
|
|
29614
29819
|
init_encryption();
|
|
29615
|
-
|
|
29616
|
-
|
|
29820
|
+
fs27 = __toESM(require("fs"), 1);
|
|
29821
|
+
path22 = __toESM(require("path"), 1);
|
|
29617
29822
|
SessionManager = class {
|
|
29618
29823
|
sessions = /* @__PURE__ */ new Map();
|
|
29619
29824
|
/** Factory-created target accessors for setup operations that need target-specific methods */
|
|
@@ -29694,8 +29899,8 @@ var init_manager5 = __esm({
|
|
|
29694
29899
|
}
|
|
29695
29900
|
return session;
|
|
29696
29901
|
}
|
|
29697
|
-
const outputDir =
|
|
29698
|
-
|
|
29902
|
+
const outputDir = path22.join(this.localDir, "sessions", sanitizeName(sessionId));
|
|
29903
|
+
fs27.mkdirSync(outputDir, { recursive: true });
|
|
29699
29904
|
const buffers = new SessionBuffers();
|
|
29700
29905
|
const effectiveFactory = this.appFactories.get(sessionId) ?? this.factory;
|
|
29701
29906
|
const ct = await effectiveFactory.create(buffers, this.reuseContext && this.sessions.size === 0);
|
|
@@ -29892,7 +30097,7 @@ function flushSessionBuffers(session, final) {
|
|
|
29892
30097
|
const lines = newEntries.map(
|
|
29893
30098
|
(e) => `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}`
|
|
29894
30099
|
).join("\n") + "\n";
|
|
29895
|
-
|
|
30100
|
+
fs28.appendFileSync(consolePath, lines);
|
|
29896
30101
|
buffers.lastConsoleFlushed = buffers.consoleTotalAdded;
|
|
29897
30102
|
}
|
|
29898
30103
|
let newNetworkCount = buffers.networkTotalAdded - buffers.lastNetworkFlushed;
|
|
@@ -29917,7 +30122,7 @@ function flushSessionBuffers(session, final) {
|
|
|
29917
30122
|
const lines = prefix.map(
|
|
29918
30123
|
(e) => `[${new Date(e.timestamp).toISOString()}] ${e.method} ${e.url} \u2192 ${e.status || "pending"} (${e.duration || "?"}ms, ${e.size || "?"}B)`
|
|
29919
30124
|
).join("\n") + "\n";
|
|
29920
|
-
|
|
30125
|
+
fs28.appendFileSync(networkPath, lines);
|
|
29921
30126
|
buffers.lastNetworkFlushed += prefixLen;
|
|
29922
30127
|
}
|
|
29923
30128
|
}
|
|
@@ -29929,11 +30134,11 @@ function trySessionBt(session) {
|
|
|
29929
30134
|
return null;
|
|
29930
30135
|
}
|
|
29931
30136
|
function isPortFree2(port) {
|
|
29932
|
-
return new Promise((
|
|
30137
|
+
return new Promise((resolve10) => {
|
|
29933
30138
|
const srv = net3.createServer();
|
|
29934
|
-
srv.once("error", () =>
|
|
30139
|
+
srv.once("error", () => resolve10(false));
|
|
29935
30140
|
srv.once("listening", () => {
|
|
29936
|
-
srv.close(() =>
|
|
30141
|
+
srv.close(() => resolve10(true));
|
|
29937
30142
|
});
|
|
29938
30143
|
srv.listen(port, "127.0.0.1");
|
|
29939
30144
|
});
|
|
@@ -30136,9 +30341,9 @@ async function shutdown() {
|
|
|
30136
30341
|
await activeRuntime?.close?.().catch(() => {
|
|
30137
30342
|
});
|
|
30138
30343
|
try {
|
|
30139
|
-
const currentState = JSON.parse(
|
|
30344
|
+
const currentState = JSON.parse(fs28.readFileSync(STATE_FILE, "utf-8"));
|
|
30140
30345
|
if (currentState.pid === process.pid || currentState.token === AUTH_TOKEN) {
|
|
30141
|
-
|
|
30346
|
+
fs28.unlinkSync(STATE_FILE);
|
|
30142
30347
|
}
|
|
30143
30348
|
} catch {
|
|
30144
30349
|
}
|
|
@@ -30156,14 +30361,14 @@ async function start() {
|
|
|
30156
30361
|
const { SessionBuffers: SessionBuffers2 } = await Promise.resolve().then(() => (init_buffers(), buffers_exports));
|
|
30157
30362
|
const { createPersistentBrowserTarget: createPersistentBrowserTarget2 } = await Promise.resolve().then(() => (init_target_factory(), target_factory_exports));
|
|
30158
30363
|
const profileDir = getProfileDir2(LOCAL_DIR7, profileName);
|
|
30159
|
-
|
|
30364
|
+
fs28.mkdirSync(profileDir, { recursive: true });
|
|
30160
30365
|
const profileTarget = await createPersistentBrowserTarget2(profileDir, () => {
|
|
30161
30366
|
if (isShuttingDown) return;
|
|
30162
30367
|
console.error("[browse] Chromium disconnected (profile mode). Shutting down.");
|
|
30163
30368
|
shutdown();
|
|
30164
30369
|
});
|
|
30165
|
-
const outputDir =
|
|
30166
|
-
|
|
30370
|
+
const outputDir = path23.join(LOCAL_DIR7, "sessions", profileName);
|
|
30371
|
+
fs28.mkdirSync(outputDir, { recursive: true });
|
|
30167
30372
|
profileSession = {
|
|
30168
30373
|
id: profileName,
|
|
30169
30374
|
manager: profileTarget.target,
|
|
@@ -30305,7 +30510,7 @@ async function start() {
|
|
|
30305
30510
|
const context = sessionBt_?.getContext();
|
|
30306
30511
|
if (context) {
|
|
30307
30512
|
try {
|
|
30308
|
-
const stateData = JSON.parse(
|
|
30513
|
+
const stateData = JSON.parse(fs28.readFileSync(stateFilePath3, "utf-8"));
|
|
30309
30514
|
if (stateData.cookies?.length) {
|
|
30310
30515
|
await context.addCookies(stateData.cookies);
|
|
30311
30516
|
}
|
|
@@ -30341,7 +30546,7 @@ async function start() {
|
|
|
30341
30546
|
port,
|
|
30342
30547
|
token: AUTH_TOKEN,
|
|
30343
30548
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
30344
|
-
serverPath:
|
|
30549
|
+
serverPath: path23.resolve(path23.dirname((0, import_url7.fileURLToPath)(__import_meta_url)), "server.ts")
|
|
30345
30550
|
};
|
|
30346
30551
|
if (profileName) {
|
|
30347
30552
|
state.profile = profileName;
|
|
@@ -30349,12 +30554,12 @@ async function start() {
|
|
|
30349
30554
|
if (DEBUG_PORT > 0) {
|
|
30350
30555
|
state.debugPort = DEBUG_PORT;
|
|
30351
30556
|
}
|
|
30352
|
-
|
|
30557
|
+
fs28.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), { mode: 384 });
|
|
30353
30558
|
console.log(`[browse] Server running on http://127.0.0.1:${port} (PID: ${process.pid})`);
|
|
30354
30559
|
console.log(`[browse] State file: ${STATE_FILE}`);
|
|
30355
30560
|
console.log(`[browse] Idle timeout: ${IDLE_TIMEOUT_MS / 1e3}s`);
|
|
30356
30561
|
}
|
|
30357
|
-
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;
|
|
30358
30563
|
var init_server2 = __esm({
|
|
30359
30564
|
"src/server.ts"() {
|
|
30360
30565
|
"use strict";
|
|
@@ -30364,8 +30569,8 @@ var init_server2 = __esm({
|
|
|
30364
30569
|
init_constants();
|
|
30365
30570
|
init_action_context();
|
|
30366
30571
|
init_executor();
|
|
30367
|
-
|
|
30368
|
-
|
|
30572
|
+
fs28 = __toESM(require("fs"), 1);
|
|
30573
|
+
path23 = __toESM(require("path"), 1);
|
|
30369
30574
|
crypto3 = __toESM(require("crypto"), 1);
|
|
30370
30575
|
http = __toESM(require("http"), 1);
|
|
30371
30576
|
import_url7 = require("url");
|
|
@@ -30427,9 +30632,9 @@ __export(cli_exports, {
|
|
|
30427
30632
|
resolveServerScript: () => resolveServerScript
|
|
30428
30633
|
});
|
|
30429
30634
|
module.exports = __toCommonJS(cli_exports);
|
|
30430
|
-
var
|
|
30431
|
-
var
|
|
30432
|
-
var
|
|
30635
|
+
var fs29 = __toESM(require("fs"), 1);
|
|
30636
|
+
var path24 = __toESM(require("path"), 1);
|
|
30637
|
+
var import_child_process12 = require("child_process");
|
|
30433
30638
|
var import_url8 = require("url");
|
|
30434
30639
|
init_constants();
|
|
30435
30640
|
init_config();
|
|
@@ -30463,50 +30668,50 @@ var INSTANCE_SUFFIX2 = BROWSE_PORT2 ? `-${BROWSE_PORT2}` : BROWSE_INSTANCE2 ? `-
|
|
|
30463
30668
|
function resolveLocalDir() {
|
|
30464
30669
|
if (process.env.BROWSE_LOCAL_DIR) {
|
|
30465
30670
|
try {
|
|
30466
|
-
|
|
30671
|
+
fs29.mkdirSync(process.env.BROWSE_LOCAL_DIR, { recursive: true });
|
|
30467
30672
|
} catch {
|
|
30468
30673
|
}
|
|
30469
30674
|
return process.env.BROWSE_LOCAL_DIR;
|
|
30470
30675
|
}
|
|
30471
30676
|
let dir = process.cwd();
|
|
30472
30677
|
for (let i = 0; i < 20; i++) {
|
|
30473
|
-
if (
|
|
30474
|
-
const browseDir =
|
|
30678
|
+
if (fs29.existsSync(path24.join(dir, ".git")) || fs29.existsSync(path24.join(dir, ".claude"))) {
|
|
30679
|
+
const browseDir = path24.join(dir, ".browse");
|
|
30475
30680
|
try {
|
|
30476
|
-
|
|
30477
|
-
const gi =
|
|
30478
|
-
if (!
|
|
30479
|
-
|
|
30681
|
+
fs29.mkdirSync(browseDir, { recursive: true });
|
|
30682
|
+
const gi = path24.join(browseDir, ".gitignore");
|
|
30683
|
+
if (!fs29.existsSync(gi)) {
|
|
30684
|
+
fs29.writeFileSync(gi, "*\n");
|
|
30480
30685
|
}
|
|
30481
30686
|
} catch {
|
|
30482
30687
|
}
|
|
30483
30688
|
return browseDir;
|
|
30484
30689
|
}
|
|
30485
|
-
const parent =
|
|
30690
|
+
const parent = path24.dirname(dir);
|
|
30486
30691
|
if (parent === dir) break;
|
|
30487
30692
|
dir = parent;
|
|
30488
30693
|
}
|
|
30489
30694
|
return "/tmp";
|
|
30490
30695
|
}
|
|
30491
30696
|
var LOCAL_DIR8 = resolveLocalDir();
|
|
30492
|
-
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`);
|
|
30493
30698
|
var MAX_START_WAIT = parseInt(process.env.BROWSE_START_TIMEOUT || "0", 10) || 8e3;
|
|
30494
30699
|
var LOCK_FILE = STATE_FILE2 + ".lock";
|
|
30495
30700
|
var LOCK_STALE_MS = DEFAULTS.LOCK_STALE_THRESHOLD_MS;
|
|
30496
30701
|
var __filename_cli = (0, import_url8.fileURLToPath)(__import_meta_url);
|
|
30497
|
-
var __dirname_cli =
|
|
30702
|
+
var __dirname_cli = path24.dirname(__filename_cli);
|
|
30498
30703
|
function resolveServerScript(env = process.env, metaDir = __dirname_cli) {
|
|
30499
30704
|
if (env.BROWSE_SERVER_SCRIPT) {
|
|
30500
30705
|
return env.BROWSE_SERVER_SCRIPT;
|
|
30501
30706
|
}
|
|
30502
30707
|
if (metaDir.startsWith("/")) {
|
|
30503
|
-
const direct =
|
|
30504
|
-
if (
|
|
30708
|
+
const direct = path24.resolve(metaDir, "server.ts");
|
|
30709
|
+
if (fs29.existsSync(direct)) {
|
|
30505
30710
|
return direct;
|
|
30506
30711
|
}
|
|
30507
30712
|
}
|
|
30508
30713
|
const selfPath = (0, import_url8.fileURLToPath)(__import_meta_url);
|
|
30509
|
-
if (
|
|
30714
|
+
if (fs29.existsSync(selfPath)) {
|
|
30510
30715
|
return "__self__";
|
|
30511
30716
|
}
|
|
30512
30717
|
throw new Error(
|
|
@@ -30516,7 +30721,7 @@ function resolveServerScript(env = process.env, metaDir = __dirname_cli) {
|
|
|
30516
30721
|
var SERVER_SCRIPT = resolveServerScript();
|
|
30517
30722
|
function readState3() {
|
|
30518
30723
|
try {
|
|
30519
|
-
const data =
|
|
30724
|
+
const data = fs29.readFileSync(STATE_FILE2, "utf-8");
|
|
30520
30725
|
return JSON.parse(data);
|
|
30521
30726
|
} catch {
|
|
30522
30727
|
return null;
|
|
@@ -30532,7 +30737,7 @@ function isProcessAlive(pid) {
|
|
|
30532
30737
|
}
|
|
30533
30738
|
async function listInstances() {
|
|
30534
30739
|
try {
|
|
30535
|
-
const files =
|
|
30740
|
+
const files = fs29.readdirSync(LOCAL_DIR8).filter(
|
|
30536
30741
|
(f) => f.startsWith("browse-server") && f.endsWith(".json") && !f.endsWith(".lock")
|
|
30537
30742
|
);
|
|
30538
30743
|
if (files.length === 0) {
|
|
@@ -30542,7 +30747,7 @@ async function listInstances() {
|
|
|
30542
30747
|
let found = false;
|
|
30543
30748
|
for (const file of files) {
|
|
30544
30749
|
try {
|
|
30545
|
-
const data = JSON.parse(
|
|
30750
|
+
const data = JSON.parse(fs29.readFileSync(path24.join(LOCAL_DIR8, file), "utf-8"));
|
|
30546
30751
|
if (!data.pid || !data.port) continue;
|
|
30547
30752
|
const alive = isProcessAlive(data.pid);
|
|
30548
30753
|
let status3 = "dead";
|
|
@@ -30565,7 +30770,7 @@ async function listInstances() {
|
|
|
30565
30770
|
found = true;
|
|
30566
30771
|
if (!alive) {
|
|
30567
30772
|
try {
|
|
30568
|
-
|
|
30773
|
+
fs29.unlinkSync(path24.join(LOCAL_DIR8, file));
|
|
30569
30774
|
} catch {
|
|
30570
30775
|
}
|
|
30571
30776
|
}
|
|
@@ -30579,8 +30784,8 @@ async function listInstances() {
|
|
|
30579
30784
|
}
|
|
30580
30785
|
function isBrowseProcess(pid) {
|
|
30581
30786
|
try {
|
|
30582
|
-
const { execSync:
|
|
30583
|
-
const cmd =
|
|
30787
|
+
const { execSync: execSync8 } = require("child_process");
|
|
30788
|
+
const cmd = execSync8(`ps -p ${pid} -o command=`, { encoding: "utf-8" }).trim();
|
|
30584
30789
|
return cmd.includes("browse") || cmd.includes("__BROWSE_SERVER_MODE");
|
|
30585
30790
|
} catch {
|
|
30586
30791
|
return false;
|
|
@@ -30588,15 +30793,15 @@ function isBrowseProcess(pid) {
|
|
|
30588
30793
|
}
|
|
30589
30794
|
function acquireLock() {
|
|
30590
30795
|
try {
|
|
30591
|
-
|
|
30796
|
+
fs29.writeFileSync(LOCK_FILE, String(process.pid), { flag: "wx" });
|
|
30592
30797
|
return true;
|
|
30593
30798
|
} catch (err) {
|
|
30594
30799
|
if (err.code === "EEXIST") {
|
|
30595
30800
|
try {
|
|
30596
|
-
const stat =
|
|
30801
|
+
const stat = fs29.statSync(LOCK_FILE);
|
|
30597
30802
|
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
30598
30803
|
try {
|
|
30599
|
-
|
|
30804
|
+
fs29.unlinkSync(LOCK_FILE);
|
|
30600
30805
|
} catch {
|
|
30601
30806
|
}
|
|
30602
30807
|
return acquireLock();
|
|
@@ -30610,7 +30815,7 @@ function acquireLock() {
|
|
|
30610
30815
|
}
|
|
30611
30816
|
function releaseLock() {
|
|
30612
30817
|
try {
|
|
30613
|
-
|
|
30818
|
+
fs29.unlinkSync(LOCK_FILE);
|
|
30614
30819
|
} catch {
|
|
30615
30820
|
}
|
|
30616
30821
|
}
|
|
@@ -30627,7 +30832,7 @@ async function startServer() {
|
|
|
30627
30832
|
}
|
|
30628
30833
|
await sleep3(100);
|
|
30629
30834
|
}
|
|
30630
|
-
if (!
|
|
30835
|
+
if (!fs29.existsSync(LOCK_FILE) || fs29.readFileSync(LOCK_FILE, "utf-8").trim() !== String(process.pid)) {
|
|
30631
30836
|
const state = readState3();
|
|
30632
30837
|
if (state && isProcessAlive(state.pid)) return state;
|
|
30633
30838
|
throw new Error("Server failed to start (another process is starting it)");
|
|
@@ -30637,7 +30842,7 @@ async function startServer() {
|
|
|
30637
30842
|
try {
|
|
30638
30843
|
const oldState = readState3();
|
|
30639
30844
|
if (oldState && !isProcessAlive(oldState.pid)) {
|
|
30640
|
-
|
|
30845
|
+
fs29.unlinkSync(STATE_FILE2);
|
|
30641
30846
|
}
|
|
30642
30847
|
} catch {
|
|
30643
30848
|
}
|
|
@@ -30646,7 +30851,7 @@ async function startServer() {
|
|
|
30646
30851
|
const spawnCmd = SERVER_SCRIPT === "__self__" ? [nodeExec, selfPath] : [nodeExec, "--import", "tsx", SERVER_SCRIPT];
|
|
30647
30852
|
const startTimeout = cliFlags.runtime === "chrome" ? String(DEFAULTS.CHROME_CDP_TIMEOUT_MS + 5e3) : "";
|
|
30648
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 } : {} };
|
|
30649
|
-
const proc = (0,
|
|
30854
|
+
const proc = (0, import_child_process12.spawn)(spawnCmd[0], spawnCmd.slice(1), {
|
|
30650
30855
|
stdio: ["ignore", "ignore", "pipe"],
|
|
30651
30856
|
env: spawnEnv,
|
|
30652
30857
|
detached: true
|
|
@@ -30723,7 +30928,7 @@ async function ensureServer() {
|
|
|
30723
30928
|
}
|
|
30724
30929
|
if (state) {
|
|
30725
30930
|
try {
|
|
30726
|
-
|
|
30931
|
+
fs29.unlinkSync(STATE_FILE2);
|
|
30727
30932
|
} catch {
|
|
30728
30933
|
}
|
|
30729
30934
|
}
|
|
@@ -30733,21 +30938,21 @@ async function ensureServer() {
|
|
|
30733
30938
|
}
|
|
30734
30939
|
function cleanOrphanedServers() {
|
|
30735
30940
|
try {
|
|
30736
|
-
const files =
|
|
30941
|
+
const files = fs29.readdirSync(LOCAL_DIR8);
|
|
30737
30942
|
for (const file of files) {
|
|
30738
30943
|
if (!file.startsWith("browse-server") || !file.endsWith(".json") || file.endsWith(".lock")) continue;
|
|
30739
|
-
const filePath =
|
|
30944
|
+
const filePath = path24.join(LOCAL_DIR8, file);
|
|
30740
30945
|
if (filePath === STATE_FILE2) continue;
|
|
30741
30946
|
try {
|
|
30742
|
-
const data = JSON.parse(
|
|
30947
|
+
const data = JSON.parse(fs29.readFileSync(filePath, "utf-8"));
|
|
30743
30948
|
if (!data.pid) {
|
|
30744
|
-
|
|
30949
|
+
fs29.unlinkSync(filePath);
|
|
30745
30950
|
continue;
|
|
30746
30951
|
}
|
|
30747
30952
|
const suffixMatch = file.match(/browse-server-(\d+)\.json$/);
|
|
30748
30953
|
if (suffixMatch && data.port === parseInt(suffixMatch[1], 10) && isProcessAlive(data.pid)) continue;
|
|
30749
30954
|
if (!isProcessAlive(data.pid)) {
|
|
30750
|
-
|
|
30955
|
+
fs29.unlinkSync(filePath);
|
|
30751
30956
|
continue;
|
|
30752
30957
|
}
|
|
30753
30958
|
if (isBrowseProcess(data.pid)) {
|
|
@@ -30758,7 +30963,7 @@ function cleanOrphanedServers() {
|
|
|
30758
30963
|
}
|
|
30759
30964
|
} catch {
|
|
30760
30965
|
try {
|
|
30761
|
-
|
|
30966
|
+
fs29.unlinkSync(filePath);
|
|
30762
30967
|
} catch {
|
|
30763
30968
|
}
|
|
30764
30969
|
}
|
|
@@ -30878,7 +31083,7 @@ async function sendCommand(state, command, args, retries = 0, sessionId) {
|
|
|
30878
31083
|
await sleep3(300);
|
|
30879
31084
|
}
|
|
30880
31085
|
try {
|
|
30881
|
-
|
|
31086
|
+
fs29.unlinkSync(STATE_FILE2);
|
|
30882
31087
|
} catch {
|
|
30883
31088
|
}
|
|
30884
31089
|
if (command === "restart") {
|
|
@@ -31239,7 +31444,7 @@ if (process.argv.includes("--mcp")) {
|
|
|
31239
31444
|
Promise.resolve().then(() => (init_mcp(), mcp_exports)).then((m2) => m2.startMcpServer(jsonMode));
|
|
31240
31445
|
} else if (process.env.__BROWSE_SERVER_MODE === "1") {
|
|
31241
31446
|
Promise.resolve().then(() => init_server2());
|
|
31242
|
-
} else if (process.argv[1] &&
|
|
31447
|
+
} else if (process.argv[1] && fs29.realpathSync(process.argv[1]) === fs29.realpathSync(__filename_cli)) {
|
|
31243
31448
|
main().catch((err) => {
|
|
31244
31449
|
console.error(`[browse] ${err.message}`);
|
|
31245
31450
|
process.exit(1);
|