@ulpi/browse 2.3.3 → 2.3.4

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