@opencode/browse 0.0.6-dev.18 → 0.0.6-dev.19

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.
@@ -1,8 +1,4 @@
1
1
  import {
2
- unpackedExtensionId,
3
- unpackedReloadAdvice,
4
- extensionProtocolSkew,
5
- extensionProtocolAdvice,
6
2
  dataRoot,
7
3
  oldStoreMarker,
8
4
  cliCommand,
@@ -20,11 +16,15 @@ import {
20
16
  describeAutoUpdate,
21
17
  installVersion,
22
18
  pruneVersions,
23
- switchVersion
24
- } from "./main-ha5bnd77.mjs";
19
+ switchVersion,
20
+ unpackedExtensionId,
21
+ unpackedReloadAdvice,
22
+ extensionProtocolSkew,
23
+ extensionProtocolAdvice
24
+ } from "./main-yd50d95g.mjs";
25
25
  import {
26
26
  relayAuthorization
27
- } from "./main-9y22cffn.mjs";
27
+ } from "./main-75rdz6bc.mjs";
28
28
  import {
29
29
  pairingCodeTtlMs,
30
30
  bundledPairingCodeTtlMs,
@@ -41,10 +41,10 @@ import {
41
41
  parseHostname,
42
42
  readRelayConfig,
43
43
  writeRelayConfig
44
- } from "./main-0egy8naa.mjs";
44
+ } from "./main-xatp12cr.mjs";
45
45
  import {
46
46
  opencodeBrowserVersion2
47
- } from "./main-ppz7sv9s.mjs";
47
+ } from "./main-ts9jjqs8.mjs";
48
48
  import { createRequire } from "node:module";
49
49
  var __create = Object.create;
50
50
  var __getProtoOf = Object.getPrototypeOf;
@@ -7082,13 +7082,13 @@ var require_src = __commonJS(function(exports, module) {
7082
7082
  });
7083
7083
 
7084
7084
  // src/setup.ts
7085
- import { spawn, spawnSync } from "node:child_process";
7086
- import { createHash, randomUUID } from "node:crypto";
7087
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
7088
- import { homedir, networkInterfaces } from "node:os";
7089
- import path2 from "node:path";
7090
- import { createInterface } from "node:readline/promises";
7091
- import { fileURLToPath } from "node:url";
7085
+ import { mkdirSync as mkdirSync3, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "node:fs";
7086
+ import path6 from "node:path";
7087
+
7088
+ // src/setup/agent-config.ts
7089
+ import fs from "node:fs";
7090
+ import path from "node:path";
7091
+ import { isDeepStrictEqual } from "node:util";
7092
7092
 
7093
7093
  // node_modules/jsonc-parser/lib/esm/impl/scanner.js
7094
7094
  function createScanner(text, ignoreTrivia = false) {
@@ -8413,11 +8413,6 @@ function applyEdits(text, edits) {
8413
8413
  return text;
8414
8414
  }
8415
8415
 
8416
- // src/agent-config.ts
8417
- import fs from "node:fs";
8418
- import path from "node:path";
8419
- import { isDeepStrictEqual } from "node:util";
8420
-
8421
8416
  // node_modules/smol-toml/dist/error.js
8422
8417
  /*!
8423
8418
  * Copyright (c) Squirrel Chat et al., All rights reserved.
@@ -9434,7 +9429,7 @@ var $stringify = publicApi.stringify;
9434
9429
  var $visit = visit2.visit;
9435
9430
  var $visitAsync = visit2.visitAsync;
9436
9431
 
9437
- // src/agent-config.ts
9432
+ // src/setup/agent-config.ts
9438
9433
  var agentLabels = {
9439
9434
  opencode: "OpenCode",
9440
9435
  claude: "Claude Code",
@@ -10244,7 +10239,7 @@ class y {
10244
10239
  }
10245
10240
  }
10246
10241
 
10247
- // src/agent-picker.ts
10242
+ // src/setup/agent-picker.ts
10248
10243
  function agentFlags(args) {
10249
10244
  const requested = new Set;
10250
10245
  let all = args.includes("--all-agents");
@@ -10333,7 +10328,17 @@ async function pickAgents(locations, initial) {
10333
10328
  return isCancel(result) ? undefined : result;
10334
10329
  }
10335
10330
 
10336
- // src/setup.ts
10331
+ // src/setup/browsers.ts
10332
+ import { spawn, spawnSync as spawnSync2 } from "node:child_process";
10333
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "node:fs";
10334
+ import path3 from "node:path";
10335
+
10336
+ // src/setup/environment.ts
10337
+ import { spawnSync } from "node:child_process";
10338
+ import { existsSync, readFileSync } from "node:fs";
10339
+ import { homedir } from "node:os";
10340
+ import path2 from "node:path";
10341
+ import { fileURLToPath } from "node:url";
10337
10342
  var store = storeConfig();
10338
10343
  var STORE_IDS = [store.id].filter((id) => !!id);
10339
10344
  var STORE_URL = store.id ? storeUrl(store.id) : undefined;
@@ -10361,7 +10366,8 @@ var legacyFiles = [
10361
10366
  path2.join(root, "cli.mjs"),
10362
10367
  path2.join(configRoot, "opencode", "plugins", "opencode-browser.ts")
10363
10368
  ];
10364
- var packageRoot = path2.dirname(path2.dirname(fileURLToPath(import.meta.url)));
10369
+ var here = path2.dirname(fileURLToPath(import.meta.url));
10370
+ var packageRoot = path2.basename(here) === "setup" ? path2.dirname(path2.dirname(here)) : path2.dirname(here);
10365
10371
  var fromSource = existsSync(path2.join(packageRoot, "src", "main.ts")) && !process.env.OPENCODE_BROWSER_PACKAGE;
10366
10372
  var checkoutExtensionDirs = (() => {
10367
10373
  const repo = path2.dirname(packageRoot);
@@ -10369,20 +10375,75 @@ var checkoutExtensionDirs = (() => {
10369
10375
  return [];
10370
10376
  return [path2.join(repo, "public"), path2.join(repo, "dist")];
10371
10377
  })();
10378
+ var dev = fromSource ? undefined : devExtension();
10379
+ var unpackedFolder = () => checkoutExtensionDirs[1] ?? (dev ? devExtensionFolder(root) : undefined);
10380
+ var connectedMessage = "✓ Connected. Restart your agent, then ask it to open a page.";
10381
+ var pairedOfflineMessage = "! Not connected yet. Open the browser profile already paired with Browse and make sure the extension is enabled. It reconnects automatically.";
10382
+ var manualPair = (code) => `Open the Extensions menu → Browse → Options, enter ${formatPairingCode(code)} as the pairing code, then click Pair.`;
10383
+ function flag(args, name) {
10384
+ const index = args.indexOf(name);
10385
+ return index === -1 ? undefined : args[index + 1];
10386
+ }
10387
+ function findOnPath(name) {
10388
+ const found = spawnSync(windows ? "where" : "which", [name], { encoding: "utf8" });
10389
+ return found.status === 0 ? found.stdout.split(/\r?\n/)[0]?.trim() || undefined : undefined;
10390
+ }
10391
+ function findOpencode(args) {
10392
+ const explicit = flag(args, "--opencode") ?? process.env.OPENCODE_BIN;
10393
+ if (explicit)
10394
+ return path2.resolve(explicit);
10395
+ return findOnPath("opencode");
10396
+ }
10397
+ function readJson(file) {
10398
+ try {
10399
+ return JSON.parse(readFileSync(file, "utf8"));
10400
+ } catch {
10401
+ return;
10402
+ }
10403
+ }
10404
+ function readFileOr(file, fallback) {
10405
+ try {
10406
+ return readFileSync(file, "utf8");
10407
+ } catch {
10408
+ return fallback;
10409
+ }
10410
+ }
10411
+ function log(line) {
10412
+ process.stdout.write(line + `
10413
+ `);
10414
+ }
10415
+ function fail(message) {
10416
+ process.stderr.write(message + `
10417
+ `);
10418
+ process.exit(1);
10419
+ }
10420
+ var installWaitTimeout = (args, tty) => waitTimeout(args, tty ? 180 : 90);
10421
+ function waitTimeout(args, defaultSeconds = 180) {
10422
+ const seconds = Number(args.includes("--wait-seconds") ? flag(args, "--wait-seconds") : defaultSeconds);
10423
+ if (!Number.isSafeInteger(seconds) || seconds < 1 || seconds > 3600)
10424
+ fail("--wait-seconds must be between 1 and 3600.");
10425
+ return seconds * 1000;
10426
+ }
10427
+
10428
+ // src/setup/browsers.ts
10429
+ var oldStoreBuild = (match) => /^0\.0\.[012]$/.test(match.version ?? "");
10372
10430
  function browsers() {
10373
10431
  if (process.platform === "darwin") {
10374
- const support = (dir) => path2.join(home, "Library/Application Support", dir);
10432
+ const support = (dir) => path3.join(home, "Library/Application Support", dir);
10375
10433
  const browser = (name, app, bundleID, dirs) => ({
10376
10434
  name,
10377
10435
  app,
10378
10436
  userData: support(dirs[0]),
10379
- manifests: dirs.map((dir) => path2.join(support(dir), "NativeMessagingHosts")),
10437
+ manifests: dirs.map((dir) => path3.join(support(dir), "NativeMessagingHosts")),
10380
10438
  defaultIDs: [bundleID],
10381
10439
  executables: []
10382
10440
  });
10383
10441
  return [
10384
10442
  browser("Google Chrome", "Google Chrome", "com.google.Chrome", ["Google/Chrome"]),
10385
- browser("Chrome for Testing", "Google Chrome for Testing", "com.google.chrome.for.testing", ["Google/Chrome for Testing", "Google/ChromeForTesting"]),
10443
+ browser("Chrome for Testing", "Google Chrome for Testing", "com.google.chrome.for.testing", [
10444
+ "Google/Chrome for Testing",
10445
+ "Google/ChromeForTesting"
10446
+ ]),
10386
10447
  browser("Google Chrome Beta", "Google Chrome Beta", "com.google.Chrome.beta", ["Google/Chrome Beta"]),
10387
10448
  browser("Google Chrome Canary", "Google Chrome Canary", "com.google.Chrome.canary", ["Google/Chrome Canary"]),
10388
10449
  browser("Chromium", "Chromium", "org.chromium.Chromium", ["Chromium"]),
@@ -10397,23 +10458,53 @@ function browsers() {
10397
10458
  ];
10398
10459
  }
10399
10460
  if (windows) {
10400
- const local = process.env.LOCALAPPDATA ?? path2.join(home, "AppData", "Local");
10401
- const roaming = process.env.APPDATA ?? path2.join(home, "AppData", "Roaming");
10461
+ const local = process.env.LOCALAPPDATA ?? path3.join(home, "AppData", "Local");
10462
+ const roaming = process.env.APPDATA ?? path3.join(home, "AppData", "Roaming");
10402
10463
  const programs = [process.env.ProgramFiles, process.env["ProgramFiles(x86)"], local].filter((dir) => !!dir);
10403
- const installs = (relative) => programs.map((dir) => path2.join(dir, relative));
10464
+ const installs = (relative) => programs.map((dir) => path3.join(dir, relative));
10404
10465
  return [
10405
- { name: "Google Chrome", userData: path2.join(local, "Google", "Chrome", "User Data"), manifests: [], defaultIDs: ["ChromeHTML"], executables: installs("Google\\Chrome\\Application\\chrome.exe") },
10406
- { name: "Microsoft Edge", userData: path2.join(local, "Microsoft", "Edge", "User Data"), manifests: [], defaultIDs: ["MSEdgeHTM"], executables: installs("Microsoft\\Edge\\Application\\msedge.exe") },
10407
- { name: "Brave", userData: path2.join(local, "BraveSoftware", "Brave-Browser", "User Data"), manifests: [], defaultIDs: ["BraveHTML"], executables: installs("BraveSoftware\\Brave-Browser\\Application\\brave.exe") },
10408
- { name: "Opera", userData: path2.join(roaming, "Opera Software", "Opera Stable"), manifests: [], defaultIDs: ["OperaStable"], executables: installs("Programs\\Opera\\opera.exe") },
10409
- { name: "Vivaldi", userData: path2.join(local, "Vivaldi", "User Data"), manifests: [], defaultIDs: ["VivaldiHTM"], executables: installs("Vivaldi\\Application\\vivaldi.exe") }
10466
+ {
10467
+ name: "Google Chrome",
10468
+ userData: path3.join(local, "Google", "Chrome", "User Data"),
10469
+ manifests: [],
10470
+ defaultIDs: ["ChromeHTML"],
10471
+ executables: installs("Google\\Chrome\\Application\\chrome.exe")
10472
+ },
10473
+ {
10474
+ name: "Microsoft Edge",
10475
+ userData: path3.join(local, "Microsoft", "Edge", "User Data"),
10476
+ manifests: [],
10477
+ defaultIDs: ["MSEdgeHTM"],
10478
+ executables: installs("Microsoft\\Edge\\Application\\msedge.exe")
10479
+ },
10480
+ {
10481
+ name: "Brave",
10482
+ userData: path3.join(local, "BraveSoftware", "Brave-Browser", "User Data"),
10483
+ manifests: [],
10484
+ defaultIDs: ["BraveHTML"],
10485
+ executables: installs("BraveSoftware\\Brave-Browser\\Application\\brave.exe")
10486
+ },
10487
+ {
10488
+ name: "Opera",
10489
+ userData: path3.join(roaming, "Opera Software", "Opera Stable"),
10490
+ manifests: [],
10491
+ defaultIDs: ["OperaStable"],
10492
+ executables: installs("Programs\\Opera\\opera.exe")
10493
+ },
10494
+ {
10495
+ name: "Vivaldi",
10496
+ userData: path3.join(local, "Vivaldi", "User Data"),
10497
+ manifests: [],
10498
+ defaultIDs: ["VivaldiHTM"],
10499
+ executables: installs("Vivaldi\\Application\\vivaldi.exe")
10500
+ }
10410
10501
  ];
10411
10502
  }
10412
10503
  const chrome = process.env.CHROME_CONFIG_HOME ?? configRoot;
10413
- const browser = (name, root, dir, executables) => ({
10504
+ const browser = (name, rootDir, dir, executables) => ({
10414
10505
  name,
10415
- userData: path2.join(root, dir),
10416
- manifests: [path2.join(root, dir, "NativeMessagingHosts")],
10506
+ userData: path3.join(rootDir, dir),
10507
+ manifests: [path3.join(rootDir, dir, "NativeMessagingHosts")],
10417
10508
  defaultIDs: executables,
10418
10509
  executables
10419
10510
  });
@@ -10430,385 +10521,509 @@ function browsers() {
10430
10521
  browser("Helium", configRoot, "net.imput.helium", ["helium", "helium-browser"])
10431
10522
  ];
10432
10523
  }
10433
- function findOpencode(args) {
10434
- const explicit = flag(args, "--opencode") ?? process.env.OPENCODE_BIN;
10435
- if (explicit)
10436
- return path2.resolve(explicit);
10437
- return findOnPath("opencode");
10524
+ function extensionsPage(browser) {
10525
+ const scheme = { "Microsoft Edge": "edge", Brave: "brave", Vivaldi: "vivaldi", Opera: "opera" }[browser.name];
10526
+ return `${scheme ?? "chrome"}://extensions`;
10438
10527
  }
10439
- function flag(args, name) {
10440
- const index = args.indexOf(name);
10441
- return index === -1 ? undefined : args[index + 1];
10528
+ function detectBrowsers() {
10529
+ return browsers().flatMap((browser) => {
10530
+ const state = readLocked(path3.join(browser.userData, "Local State"));
10531
+ if (state.missing)
10532
+ return [];
10533
+ if (state.blocked)
10534
+ return launcher(browser) ? [{ browser, blocked: true, enabled: [], disabled: [] }] : [];
10535
+ let blocked = false;
10536
+ const matches = listProfiles(browser, state.text).flatMap((profile) => {
10537
+ const settings = ["Secure Preferences", "Preferences"].map((file) => {
10538
+ const prefs = readLocked(path3.join(browser.userData, profile.dir, file));
10539
+ blocked ||= prefs.blocked;
10540
+ return parseJson(prefs.text)?.extensions?.settings ?? {};
10541
+ });
10542
+ const id = [...STORE_IDS, unpackedExtensionId].find((candidate) => settings.some((entries) => entries[candidate]));
10543
+ if (!id)
10544
+ return [];
10545
+ const setting = settings.find((entries) => entries[id])[id];
10546
+ const version = installedExtensionVersion(id, setting);
10547
+ return [{ browser, profile, id, ...version ? { version } : {}, enabled: !disabled(setting) }];
10548
+ });
10549
+ return [
10550
+ {
10551
+ browser,
10552
+ blocked,
10553
+ enabled: matches.filter((match) => match.enabled),
10554
+ disabled: matches.filter((match) => !match.enabled)
10555
+ }
10556
+ ];
10557
+ });
10442
10558
  }
10443
- function readJson(file) {
10444
- try {
10445
- return JSON.parse(readFileSync(file, "utf8"));
10446
- } catch {
10559
+ function installedExtensionVersion(id, setting) {
10560
+ if (setting.manifest?.version)
10561
+ return setting.manifest.version;
10562
+ if (!setting.path)
10447
10563
  return;
10448
- }
10449
- }
10450
- function configFile() {
10451
- const directory = path2.join(configRoot, "opencode");
10452
- const candidates = ["opencode.json", "opencode.jsonc", ".opencode/opencode.json", ".opencode/opencode.jsonc"].map((name) => path2.join(directory, name));
10453
- return candidates.find((file) => existsSync(file) && statSync(file).isFile()) ?? candidates[0];
10454
- }
10455
- function fingerprint(entry) {
10456
- return createHash("sha256").update(readFileOr(entry, entry)).digest("hex").slice(0, 16);
10564
+ if (path3.isAbsolute(setting.path))
10565
+ return readJson(path3.join(setting.path, "manifest.json"))?.version;
10566
+ const packed = /^([a-p]{32})[/\\](\d+(?:\.\d+){0,3})_\d+$/.exec(setting.path);
10567
+ return packed?.[1] === id ? packed[2] : undefined;
10457
10568
  }
10458
- function restartOutdatedRelay(entry, node) {
10459
- const status = spawnSync(node, [entry, "status", "--json"], { encoding: "utf8", timeout: 15000 });
10460
- if (!/"stale":\s*true/.test(status.stdout ?? ""))
10461
- return;
10462
- const restart = spawnSync(node, [entry, "relay", "restart"], { encoding: "utf8", timeout: 60000 });
10463
- if (restart.status === 0)
10464
- return ["✓ Relay restarted with the new version"];
10465
- return relayNotRestarted(status.stdout ?? "", `${restart.stderr ?? ""}
10466
- ${restart.stdout ?? ""}`);
10569
+ function disabled(setting) {
10570
+ const reasons = setting.disable_reasons;
10571
+ if (Array.isArray(reasons))
10572
+ return reasons.length > 0;
10573
+ return typeof reasons === "number" && reasons !== 0;
10467
10574
  }
10468
- function relayNotRestarted(statusJson, output, command = cliCommand("relay restart")) {
10469
- const sessions = (() => {
10575
+ function listProfiles(browser, localState) {
10576
+ const cache = parseJson(localState)?.profile?.info_cache;
10577
+ if (cache && Object.keys(cache).length)
10578
+ return Object.entries(cache).map(([dir, info]) => ({ dir, name: info.name || dir }));
10579
+ const dirs = (() => {
10470
10580
  try {
10471
- const parsed = JSON.parse(statusJson);
10472
- return (parsed.sessions ?? []).filter((item) => item.connected === true && typeof item.id === "string").map((item) => item.id);
10581
+ return readdirSync(browser.userData);
10473
10582
  } catch {
10474
10583
  return [];
10475
10584
  }
10476
10585
  })();
10477
- const reason = output.split(`
10478
- `).map((line) => line.trim()).filter((line) => line && !line.startsWith("Restarting the relay leaves")).join(" ");
10479
- const who = sessions.length === 1 ? `an agent session is active (${sessions[0]})` : sessions.length > 1 ? `${sessions.length} agent sessions are active (${sessions.slice(0, 5).join(", ")}${sessions.length > 5 ? ", …" : ""})` : undefined;
10480
- const forceable = /--force/.test(reason);
10481
- return [
10482
- `! Relay not restarted: ${who ?? (reason || "it declined")}${who && reason ? ` (${reason})` : ""}.`,
10483
- " It keeps running the previous version, so agents get this update only after it restarts.",
10484
- forceable ? ` When no agent is using the browser, run: ${command} (or ${command} --force to end what's blocking it now)` : ` When no agent is using the browser, run: ${command}`
10485
- ];
10486
- }
10487
- function mcpServers(file = configFile()) {
10488
- const errors = [];
10489
- const config = parse2(existsSync(file) ? readFileSync(file, "utf8") : "{}", errors, { allowTrailingComma: true });
10490
- if (errors.length || [config, config?.mcp, config?.mcp?.servers].some((v) => v !== undefined && (!v || typeof v !== "object" || Array.isArray(v))))
10491
- fail(`Invalid ${file}; left unchanged`);
10492
- return config.mcp?.servers ?? {};
10493
- }
10494
- function ownedMcp(server, entry) {
10495
- const command = server?.command;
10496
- return Array.isArray(command) && command.length === 3 && command[2] === "mcp" && [entry, layout.shim, path2.join(packageRoot, "dist/cli.mjs")].some((e) => e && command[1] === e);
10586
+ return dirs.filter((dir) => /^(Default|Profile \d+)$/.test(dir)).map((dir) => ({ dir, name: dir }));
10497
10587
  }
10498
- function mergedMcpServer(existing, command, runtime) {
10499
- const environment = typeof existing?.environment === "object" && existing.environment !== null && !Array.isArray(existing.environment) ? existing.environment : {};
10500
- return { ...existing, type: "local", command, environment: { ...environment, OPENCODE_BROWSER_RUNTIME: runtime } };
10588
+ function readLocked(file) {
10589
+ try {
10590
+ return { text: readFileSync2(file, "utf8"), missing: false, blocked: false };
10591
+ } catch (error) {
10592
+ const code = error.code;
10593
+ return {
10594
+ text: undefined,
10595
+ missing: code === "ENOENT" || code === "ENOTDIR",
10596
+ blocked: code === "EPERM" || code === "EACCES"
10597
+ };
10598
+ }
10501
10599
  }
10502
- function configureMcp(file, entry, previousEntry) {
10503
- const existing = mcpServers(file)[MCP_NAME];
10504
- if (existing && !ownedMcp(existing, previousEntry))
10600
+ function parseJson(text) {
10601
+ if (!text)
10505
10602
  return;
10506
- const renamed = removeMcp(LEGACY_MCP_NAME, previousEntry, file);
10507
- const server = mergedMcpServer(existing, [process.execPath, entry, "mcp"], entry === layout.shim ? "auto-update" : fingerprint(entry));
10508
- if (existing && JSON.stringify(existing) === JSON.stringify(server))
10509
- return `"${MCP_NAME}" is up to date`;
10510
- const text = readFileOr(file, "{}");
10511
- const edits = modify(text, ["mcp", "servers", MCP_NAME], server, { formattingOptions: { tabSize: 2, insertSpaces: true } });
10512
- mkdirSync(path2.dirname(file), { recursive: true });
10513
- writeFileSync(file, applyEdits(text, edits));
10514
- if (renamed)
10515
- return `"${LEGACY_MCP_NAME}" renamed to "${MCP_NAME}" in ${file}`;
10516
- return existing ? `"${MCP_NAME}" updated in ${file}` : `"${MCP_NAME}" added to ${file}`;
10517
- }
10518
- function removeMcp(name, entry, file = configFile()) {
10519
- const text = readFileOr(file, "");
10520
- if (!text || !ownedMcp(mcpServers(file)[name], entry))
10521
- return false;
10522
- writeFileSync(file, applyEdits(text, modify(text, ["mcp", "servers", name], undefined, {})));
10523
- return true;
10524
- }
10525
- function legacyBrowserControl() {
10526
- return Object.entries(mcpServers()).filter(([name, server]) => name === "browser-control" || JSON.stringify(server.command ?? "").includes("browser-control")).map(([name]) => name);
10527
- }
10528
- function readFileOr(file, fallback) {
10529
10603
  try {
10530
- return readFileSync(file, "utf8");
10604
+ return JSON.parse(text);
10531
10605
  } catch {
10532
- return fallback;
10606
+ return;
10533
10607
  }
10534
10608
  }
10535
- function removeLegacyHost() {
10536
- const leftovers = [
10537
- ...browsers().flatMap((browser) => browser.manifests.map((directory) => path2.join(directory, `${LEGACY_HOST_NAME}.json`))),
10538
- ...legacyFiles
10539
- ].filter((file) => existsSync(file));
10540
- leftovers.forEach((file) => rmSync(file, { force: true }));
10541
- const keys = windows ? LEGACY_REGISTRY_ROOTS.map((root) => `${root}\\NativeMessagingHosts\\${LEGACY_HOST_NAME}`).filter((key) => spawnSync("reg", ["query", key, "/ve"]).status === 0) : [];
10542
- keys.forEach((key) => spawnSync("reg", ["delete", key, "/f"]));
10543
- return leftovers.length + keys.length > 0;
10609
+ function defaultBrowser(candidates) {
10610
+ const id = defaultBrowserID();
10611
+ if (!id)
10612
+ return;
10613
+ const known = browsers().find((browser) => browser.defaultIDs.some((name) => name.toLowerCase() === id.toLowerCase() || id.startsWith(`${name}.`)));
10614
+ return known && (candidates.find((browser) => browser.name === known.name) ?? known);
10544
10615
  }
10545
- async function installRuntime() {
10546
- if (fromSource) {
10547
- const bundle = path2.join(packageRoot, "dist", "cli.mjs");
10548
- if (!existsSync(bundle))
10549
- fail("Build the CLI first: `bun run build` in cli/ (or `bun run setup` at the top).");
10550
- return { entry: bundle, version: opencodeBrowserVersion2 };
10616
+ function defaultBrowserID() {
10617
+ if (process.platform === "darwin") {
10618
+ const plist = path3.join(home, "Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist");
10619
+ const json = spawnSync2("plutil", ["-convert", "json", "-o", "-", plist], { encoding: "utf8" });
10620
+ const handlers = parseJson(json.stdout)?.LSHandlers ?? [];
10621
+ return ["https", "http"].map((scheme) => handlers.find((handler) => handler.LSHandlerURLScheme === scheme)?.LSHandlerRoleAll).find((bundle) => !!bundle);
10551
10622
  }
10552
- const spec = process.env.OPENCODE_BROWSER_PACKAGE;
10553
- const before = readCurrent(layout);
10554
- const existing = versionEntry(layout, opencodeBrowserVersion2);
10555
- const reuse = !spec && existsSync(existing) && readFileSync(existing).equals(readFileSync(path2.join(packageRoot, "dist", "cli.mjs"))) && spawnSync(process.execPath, [existing, "--version"], { encoding: "utf8", timeout: 30000 }).stdout?.trim() === opencodeBrowserVersion2;
10556
- let installed;
10557
- try {
10558
- installed = reuse ? existing : await installVersion({ layout, version: opencodeBrowserVersion2, ...spec ? { spec } : { runningPackage: packageRoot } });
10559
- } catch (error) {
10560
- fail(`Could not install ${spec ?? `${PACKAGE}@${opencodeBrowserVersion2}`}: ${error.message}`);
10623
+ if (windows) {
10624
+ const key = "HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\https\\UserChoice";
10625
+ const query = spawnSync2("reg", ["query", key, "/v", "ProgId"], { encoding: "utf8" });
10626
+ return /ProgId\s+REG_SZ\s+(\S+)/.exec(query.stdout ?? "")?.[1];
10561
10627
  }
10562
- const version = path2.basename(path2.dirname(path2.dirname(path2.dirname(path2.dirname(path2.dirname(installed))))));
10563
- const previous = before?.version === version ? before.previous : before?.version;
10564
- switchVersion(layout, version, previous);
10565
- updateState(layout, ({ pending, skipped, ...state }) => ({
10566
- ...state,
10567
- ...pending && compareVersions(pending, version) > 0 ? { pending } : {},
10568
- ...skipped && skipped !== version ? { skipped } : {}
10569
- }));
10570
- pruneVersions(layout, [version, previous]);
10571
- for (const legacy of ["node_modules", "package.json", "package-lock.json"])
10572
- rmSync(path2.join(files.runtime, legacy), { recursive: true, force: true });
10573
- return { entry: layout.shim, version };
10628
+ const xdg = spawnSync2("xdg-settings", ["get", "default-web-browser"], { encoding: "utf8" });
10629
+ return xdg.status === 0 ? xdg.stdout.trim().replace(/\.desktop$/, "") || undefined : undefined;
10574
10630
  }
10575
- function checkNode() {
10576
- if (process.versions.bun)
10577
- fail("Browse runs on Node.js 22.19 or newer, but this ran under Bun (`bun run` uses Bun when `node` isn't on PATH). Install Node.js from https://nodejs.org, then run setup again.");
10578
- const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
10579
- if (major < 22 || major === 22 && minor < 19)
10580
- fail(`Browse needs Node.js 22.19 or newer; this is ${process.version} (${process.execPath}). Update Node.js (https://nodejs.org), then run setup again.`);
10631
+ function reportDetections(detections, preferred, writeLog) {
10632
+ writeLog(`Default browser: ${preferred?.name ?? "unknown"}`);
10633
+ detections.forEach((detection) => {
10634
+ const name = detection.browser.name;
10635
+ if (detection.blocked)
10636
+ return writeLog(` ${name}: couldn't check (${blockedReason()})`);
10637
+ if (detection.enabled.length)
10638
+ return writeLog(` ${name}: Browse in ${detection.enabled.map(describe).join(", ")}`);
10639
+ if (detection.disabled.length)
10640
+ return writeLog(` ${name}: Browse turned off in ${detection.disabled.map(describe).join(", ")}`);
10641
+ writeLog(` ${name}: no Browse`);
10642
+ });
10581
10643
  }
10582
- var dev = fromSource ? undefined : devExtension();
10583
- var unpackedFolder = () => checkoutExtensionDirs[1] ?? (dev ? devExtensionFolder(root) : undefined);
10584
- async function syncDev() {
10585
- if (!dev)
10586
- return;
10587
- try {
10588
- const result = await syncDevExtension(dev, { root });
10589
- log(result === "updated" ? `✓ Downloaded the dev extension (${dev.file}) to ${devExtensionFolder(root)}` : `✓ Dev extension ${dev.file} is in ${devExtensionFolder(root)}`);
10590
- } catch (error) {
10591
- fail(`Could not get the dev extension: ${error.message}`);
10644
+ var describe = (match) => `${match.profile.name}${match.id === unpackedExtensionId ? " (unpacked)" : ""}`;
10645
+ var blockedReason = () => process.platform === "darwin" ? "macOS privacy protection" : "permission denied";
10646
+ function openInBrowser(browser, url, profile) {
10647
+ const executable = launcher(browser)?.executable;
10648
+ const web = /^https?:/.test(url);
10649
+ if (executable) {
10650
+ const child = spawn(executable, [...profile ? [`--profile-directory=${profile}`] : [], url], {
10651
+ detached: true,
10652
+ stdio: "ignore"
10653
+ });
10654
+ child.on("error", () => web && openWithSystem(browser, url));
10655
+ child.unref();
10656
+ return true;
10592
10657
  }
10658
+ if (!web)
10659
+ return false;
10660
+ return openWithSystem(browser, url);
10593
10661
  }
10594
- async function install(args) {
10595
- checkNode();
10596
- const tty = !!(process.stdin.isTTY && process.stdout.isTTY);
10597
- const timeoutMs = installWaitTimeout(args, tty);
10598
- let flags;
10599
- try {
10600
- flags = agentFlags(args);
10601
- } catch (error) {
10602
- fail(error.message);
10662
+ function openWithSystem(browser, url) {
10663
+ if (process.platform === "darwin")
10664
+ return spawnSync2("open", browser?.app ? ["-a", browser.app, url] : [url]).status === 0;
10665
+ if (windows)
10666
+ return spawnSync2("rundll32", ["url.dll,FileProtocolHandler", url]).status === 0;
10667
+ return spawnSync2("xdg-open", [url]).status === 0;
10668
+ }
10669
+ function revealFolder(folder) {
10670
+ const run = (command, args) => {
10671
+ const result = spawnSync2(command, args, { stdio: "ignore", timeout: 1e4 });
10672
+ return !result.error && (windows || result.status === 0);
10673
+ };
10674
+ if (process.platform === "darwin")
10675
+ return run("open", ["-R", folder]);
10676
+ if (windows)
10677
+ return run("explorer", [`/select,${folder}`]);
10678
+ return run("xdg-open", [path3.dirname(folder)]);
10679
+ }
10680
+ function copyToClipboard(text) {
10681
+ const tools = process.platform === "darwin" ? [["pbcopy", []]] : windows ? [["clip", []]] : [
10682
+ ...process.env.WAYLAND_DISPLAY ? [["wl-copy", []]] : [],
10683
+ ["xclip", ["-selection", "clipboard"]],
10684
+ ["xsel", ["--clipboard", "--input"]]
10685
+ ];
10686
+ return tools.some(([command, args]) => {
10687
+ const result = spawnSync2(command, args, { input: text, stdio: ["pipe", "ignore", "ignore"], timeout: 5000 });
10688
+ return !result.error && result.status === 0;
10689
+ });
10690
+ }
10691
+ var systemEffects = {
10692
+ open: openInBrowser,
10693
+ openDefault: (url) => openWithSystem(undefined, url),
10694
+ reveal: revealFolder,
10695
+ copy: copyToClipboard,
10696
+ log
10697
+ };
10698
+ function launcher(browser) {
10699
+ if (process.platform === "darwin") {
10700
+ const bundle = [
10701
+ path3.join("/Applications", `${browser.app}.app`),
10702
+ path3.join(home, "Applications", `${browser.app}.app`)
10703
+ ].find((dir) => existsSync2(dir));
10704
+ if (!bundle)
10705
+ return;
10706
+ const name = spawnSync2("plutil", ["-extract", "CFBundleExecutable", "raw", "-o", "-", path3.join(bundle, "Contents", "Info.plist")], { encoding: "utf8" }).stdout?.trim();
10707
+ const executable = name ? path3.join(bundle, "Contents", "MacOS", name) : undefined;
10708
+ return executable && existsSync2(executable) ? { executable } : {};
10603
10709
  }
10604
- const opencode = findOpencode(args);
10605
- const caller = callingAgent(process.env);
10606
- const locations = agentLocations(home, configRoot, findOnPath, configFile(), opencode);
10607
- const previous = readJson(files.settings);
10608
- const remembered = previous?.selectedAgents ?? (previous?.declinedAgents?.length ? locations.filter((a) => a.installed && !previous.declinedAgents.includes(a.name)).map((a) => a.name) : undefined);
10609
- const plan = agentPlan(locations, flags, tty, caller, remembered);
10610
- const selected = plan.picker ? await pickAgents(locations, plan.selected) : plan.selected;
10611
- if (!selected) {
10612
- process.exitCode = 130;
10613
- return;
10710
+ const executable = windows ? browser.executables.find((file) => existsSync2(file)) : browser.executables.map(findOnPath).find((file) => !!file);
10711
+ return executable ? { executable } : undefined;
10712
+ }
10713
+
10714
+ // src/setup/commands.ts
10715
+ import { spawnSync as spawnSync4 } from "node:child_process";
10716
+ import { randomUUID } from "node:crypto";
10717
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2, rmSync as rmSync2 } from "node:fs";
10718
+ import { networkInterfaces } from "node:os";
10719
+ import path5 from "node:path";
10720
+ import { createInterface } from "node:readline/promises";
10721
+
10722
+ // src/setup/guidance.ts
10723
+ function locateExtension(options, effects = systemEffects) {
10724
+ const { log } = effects;
10725
+ const listing = options.storeUrl ?? STORE_URL;
10726
+ const noListing = () => log(`! This package doesn't know its Chrome Web Store item, so it can't install the extension. Install a released version (\`npx @opencode/browse install\`), or set up Browse from a checkout.`);
10727
+ const pairLater = () => log(` Once it's added, run \`${cliCommand("install")}\` again to pair it with the relay.`);
10728
+ if (options.pairedOffline)
10729
+ return log(pairedOfflineMessage), "ready";
10730
+ if (options.env.ssh) {
10731
+ if (options.connected)
10732
+ return log(connectedMessage), "ready";
10733
+ if (options.folder)
10734
+ unpackedSteps({ folder: options.folder, platform: options.env.platform, log });
10735
+ else if (listing)
10736
+ log(`Install Browse in your browser from the Chrome Web Store: ${listing}`), pairLater();
10737
+ else
10738
+ noListing();
10739
+ return "guided";
10614
10740
  }
10615
- mkdirSync(files.dir, { recursive: true });
10616
- const { entry, version } = await installRuntime();
10617
- log(`✓ Browse ${version} installed${fromSource ? " (from source)" : ""}`);
10618
- const agents = previous?.agents ?? [];
10619
- const opencodeConfigs = previous?.opencodeConfigs ?? (previous?.entry ? [configFile()] : []);
10620
- const selectedAgents = plan.picker ? selected : remembered;
10621
- const save = () => writeFileSync(files.settings, JSON.stringify({ ...opencode ? { opencode } : {}, entry, version, agents, opencodeConfigs, ...selectedAgents && { selectedAgents } }, null, 2) + `
10622
- `);
10623
- save();
10624
- if (removeLegacyHost())
10625
- log("✓ Removed the native messaging host of an earlier version");
10626
- const node = process.execPath;
10627
- for (const agent of locations) {
10628
- if (!selected.includes(agent.name)) {
10629
- if (!plan.picker && agent.installed && !flags.requested.length)
10630
- log(`To add ${agentLabels[agent.name]}: ${cliCommand(`install --agent ${agent.name}`)}`);
10631
- continue;
10632
- }
10633
- if (agent.name === "opencode") {
10634
- const mcp = configureMcp(agent.file, entry, previous?.entry);
10635
- if (mcp && !opencodeConfigs.includes(agent.file)) {
10636
- opencodeConfigs.push(agent.file);
10637
- save();
10638
- }
10639
- if (!mcp)
10640
- process.exitCode = 1;
10641
- log(mcp ? `✓ MCP server ${mcp} (OpenCode)` : `! OpenCode already has a different server named browse in ${agent.file}; left unchanged.`);
10642
- continue;
10643
- }
10644
- const receipt = { name: agent.name, file: agent.file, entry, node };
10645
- try {
10646
- const result = editAgentConfig(receipt);
10647
- if (result === "added") {
10648
- const i = agents.findIndex((a) => a.file === agent.file);
10649
- if (i !== -1)
10650
- agents.splice(i, 1);
10651
- agents.push(receipt);
10652
- save();
10653
- }
10654
- if (result === "conflict")
10655
- process.exitCode = 1;
10656
- log(result === "conflict" ? `! ${agent.name} already has a different browse server; left ${agent.file} unchanged.` : `✓ ${agent.name}: browse ${result} in ${agent.file}`);
10657
- } catch {
10658
- process.exitCode = 1;
10659
- log(`! Couldn't edit ${agent.file} for ${agent.name}. Check its syntax and permissions, then run ${cliCommand(`install --agent ${agent.name}`)}.`);
10660
- }
10741
+ reportDetections(options.detections, options.preferred, log);
10742
+ if (options.connected)
10743
+ return log(connectedMessage), "ready";
10744
+ if (options.detections.some((detection) => detection.enabled.length))
10745
+ return "ready";
10746
+ const off = options.detections.flatMap((detection) => detection.disabled);
10747
+ if (off.length) {
10748
+ log(`! Browse is turned off in ${off.map((match) => `${match.browser.name} (${match.profile.name})`).join(", ")}. Turn it on in the browser's extensions page.`);
10749
+ return "off";
10661
10750
  }
10662
- for (const line of restartOutdatedRelay(entry, node) ?? [])
10663
- log(line);
10664
- const legacy = selected.includes("opencode") ? legacyBrowserControl() : [];
10665
- if (legacy.length)
10666
- log(`! Browser Control MCP server${legacy.length === 1 ? "" : "s"} ${legacy.map((name) => `"${name}"`).join(", ")} also configured. Browse replaces it; remove ${legacy.length === 1 ? "it" : "them"} from ${configFile()} to avoid two sets of browser tools.`);
10667
- await syncDev();
10668
- const initial = await settledExtensionStatus(entry);
10669
- const connected = initial?.connected ? initial : undefined;
10670
- const pairedOffline = initial?.pairingState === "paired-offline";
10671
- const folder = unpackedFolder();
10672
- if (folder && !pairedOffline) {
10673
- pairUnpacked();
10674
- log("✓ Put a one-time pairing code in the unpacked extension (good for an hour)");
10675
- const reload = connected ? await reloadOutdatedExtension(connected, folder) : "current";
10676
- if (reload === "reloaded")
10677
- log("✓ Reloaded Browse in your browser");
10678
- if (reload === "stale")
10679
- return log(`! ${unpackedReloadAdvice}`);
10751
+ const blocked = options.detections.filter((detection) => detection.blocked).map((detection) => detection.browser.name);
10752
+ const target = options.preferred ?? options.detections.find((detection) => !detection.blocked)?.browser;
10753
+ const interactive = options.allowOpen && options.env.display;
10754
+ if (!options.folder) {
10755
+ if (!listing)
10756
+ return noListing(), "guided";
10757
+ const opened = interactive && !blocked.length && (target ? effects.open(target, listing) : effects.openDefault?.(listing));
10758
+ log(`Install Browse from the Chrome Web Store${opened ? ` (opened in ${target?.name ?? "your default browser"})` : ""}:`);
10759
+ log(` ${listing}`);
10760
+ log(` Once it's added, install pairs it with the relay (if install isn't waiting for it, run \`${cliCommand("install")}\` again).`);
10761
+ return "guided";
10680
10762
  }
10681
- const reportMismatch = (status) => {
10682
- const advice = status.nextStep ?? extensionProtocolAdvice(status.protocolVersion, { install: installCommand() });
10683
- if (args.includes("--no-wait") && folder && status.origin === `chrome-extension://${unpackedExtensionId}` && extensionProtocolSkew(status.protocolVersion) === "extension-older")
10684
- return log(`! Not connected yet. ${advice}`);
10685
- fail(advice);
10686
- };
10687
- if (initial?.protocolCompatible === false && initial.origin)
10688
- return reportMismatch(initial);
10689
- log("");
10690
- const ssh = !!(process.env.SSH_CONNECTION || process.env.SSH_TTY);
10691
- const detections = ssh ? [] : detectBrowsers();
10692
- rmSync(oldStoreMarker(), { force: true });
10693
- const checkStore = (scanned) => {
10694
- const matches = scanned.flatMap((d) => d.enabled).filter((m) => STORE_IDS.includes(m.id));
10695
- if (!matches.length || !matches.every(oldStoreBuild))
10696
- return true;
10697
- writeFileSync(oldStoreMarker(), `2
10698
- `);
10699
- ensureRelay(entry);
10700
- log(`! Found Browse ${matches[0].version} in ${matches[0].browser.name}. ${extensionProtocolAdvice(2, { install: cliCommand("install") }, "store")}`);
10701
- process.exitCode = 1;
10702
- return false;
10703
- };
10704
- if (!connected && !checkStore(detections))
10705
- return;
10706
- const env = {
10707
- ssh,
10708
- display: process.platform !== "linux" || !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY),
10709
- tty,
10710
- platform: process.platform
10711
- };
10712
- const found = locateExtension({
10713
- allowOpen: !args.includes("--no-open"),
10714
- connected: !!connected,
10715
- pairedOffline,
10716
- ...folder ? { folder } : {},
10717
- detections,
10718
- preferred: ssh ? undefined : defaultBrowser(detections.map((detection) => detection.browser)),
10719
- env
10763
+ const act = interactive && !blocked.length;
10764
+ const page = target ? extensionsPage(target) : "chrome://extensions";
10765
+ unpackedSteps({
10766
+ folder: options.folder,
10767
+ platform: options.env.platform,
10768
+ log,
10769
+ browser: target?.name,
10770
+ page,
10771
+ opened: act && target !== undefined && effects.open(target, page),
10772
+ revealed: act && options.env.tty && effects.reveal(options.folder),
10773
+ copied: act && options.env.tty && effects.copy(options.folder)
10720
10774
  });
10721
- if (connected || found === "off" || pairedOffline || !folder && !STORE_URL)
10775
+ return "guided";
10776
+ }
10777
+ function pairStoreExtension(options, effects = systemEffects) {
10778
+ const ids = options.storeIds ?? STORE_IDS;
10779
+ const matches = options.detections.flatMap((detection) => detection.enabled).filter((match) => ids.includes(match.id) && !oldStoreBuild(match));
10780
+ const match = matches.find((candidate) => candidate.browser.name === options.preferred?.name) ?? matches[0];
10781
+ if (!match)
10782
+ return "none";
10783
+ const interactive = options.allowOpen && options.env.display && !options.env.ssh;
10784
+ if (interactive && effects.open(match.browser, `chrome-extension://${match.id}/welcome.html#code=${options.code()}`, match.profile.dir)) {
10785
+ effects.log(`✓ Opened Browse's status page in ${match.browser.name} (${match.profile.name}) to pair it with the relay`);
10786
+ return "opened";
10787
+ }
10788
+ effects.log(`To pair Browse in ${match.browser.name} (${match.profile.name}): ${manualPair(options.code())}`);
10789
+ return "manual";
10790
+ }
10791
+ function unpackedSteps(step) {
10792
+ const { log } = step;
10793
+ const page = step.page ?? "chrome://extensions";
10794
+ const fileManager = step.platform === "darwin" ? "Finder" : step.platform === "win32" ? "Explorer" : "your file manager";
10795
+ const paste = step.platform === "darwin" ? "press Cmd+Shift+G in the folder picker and paste" : "paste it into the folder picker";
10796
+ log("Load the extension in your browser (one time):");
10797
+ log(` 1. Open ${page}${step.browser ? ` in ${step.browser}` : " (or your browser's extensions page)"}${step.opened ? " (opened)" : ""}`);
10798
+ log(" 2. Turn on Developer mode");
10799
+ log(` 3. Click Load unpacked and choose this folder${step.copied ? ` (path copied: ${paste})` : ""}, or drag it onto the page:`);
10800
+ log(` ${step.folder}${step.revealed ? ` (shown in ${fileManager})` : ""}`);
10801
+ }
10802
+ async function waitForExtension(options) {
10803
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
10804
+ for (let waited = 0;; waited += options.intervalMs) {
10805
+ if (await options.connected())
10806
+ return true;
10807
+ if (waited >= options.timeoutMs)
10808
+ return false;
10809
+ await sleep(options.intervalMs);
10810
+ }
10811
+ }
10812
+
10813
+ // src/setup/mcp.ts
10814
+ import { spawnSync as spawnSync3 } from "node:child_process";
10815
+ import { createHash } from "node:crypto";
10816
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, rmSync, statSync, writeFileSync } from "node:fs";
10817
+ import path4 from "node:path";
10818
+ function configFile() {
10819
+ const directory = path4.join(configRoot, "opencode");
10820
+ const candidates = ["opencode.json", "opencode.jsonc", ".opencode/opencode.json", ".opencode/opencode.jsonc"].map((name) => path4.join(directory, name));
10821
+ return candidates.find((file) => existsSync3(file) && statSync(file).isFile()) ?? candidates[0];
10822
+ }
10823
+ function fingerprint(entry) {
10824
+ return createHash("sha256").update(readFileOr(entry, entry)).digest("hex").slice(0, 16);
10825
+ }
10826
+ function restartOutdatedRelay(entry, node) {
10827
+ const status = spawnSync3(node, [entry, "status", "--json"], { encoding: "utf8", timeout: 15000 });
10828
+ if (!/"stale":\s*true/.test(status.stdout ?? ""))
10722
10829
  return;
10723
- ensureRelay(entry);
10724
- let code;
10725
- const pairingCode = () => code && pendingPairingCodeKeys().includes(pairingCodeKey(code)) ? code : code = mintPairingCode();
10726
- const pairStore = (scanned) => pairStoreExtension({ detections: scanned, preferred: defaultBrowser(scanned.map((d) => d.browser)), allowOpen: !args.includes("--no-open"), env, code: pairingCode });
10727
- let offer = ssh ? "none" : pairStore(detections);
10728
- const blocked = detections.filter((d) => d.blocked).map((d) => d.browser.name);
10729
- if (offer === "none" && blocked.length)
10730
- log(`Couldn't check ${blocked.join(", ")}. If Browse is already installed there, pair it manually. ${manualPair(pairingCode())}`);
10731
- const next = folder ? `If Browse is already loaded unpacked: ${unpackedReloadAdvice}` : `If Browse isn't connected after you add it, run \`${cliCommand("install")}\` again to pair it.`;
10732
- const agentNext = () => {
10733
- if (folder && offer === "none" && !blocked.length)
10734
- return `Agent: if Browse is already loaded unpacked, ask the user: ${unpackedReloadAdvice} Otherwise ask them to load ${folder}. Check the connection with the browse status tool.`;
10735
- return `Agent: ${offer === "opened" ? "ask the user to open Browse's status page in their browser" : offer === "manual" || blocked.length ? "ask the user to finish pairing in Browse's Options" : "ask the user to add Browse in their browser"}. Then re-run \`${cliCommand("install")}\` to finish, or call the browse status tool to check the connection.`;
10736
- };
10737
- if (!env.tty)
10738
- log(agentNext());
10739
- if (args.includes("--no-wait") || ssh || !env.display) {
10740
- if (env.tty)
10741
- log(next);
10830
+ const restart = spawnSync3(node, [entry, "relay", "restart"], { encoding: "utf8", timeout: 60000 });
10831
+ if (restart.status === 0)
10832
+ return ["✓ Relay restarted with the new version"];
10833
+ return relayNotRestarted(status.stdout ?? "", `${restart.stderr ?? ""}
10834
+ ${restart.stdout ?? ""}`);
10835
+ }
10836
+ function relayNotRestarted(statusJson, output, command = cliCommand("relay restart")) {
10837
+ const sessions = (() => {
10838
+ try {
10839
+ const parsed = JSON.parse(statusJson);
10840
+ return (parsed.sessions ?? []).filter((item) => item.connected === true && typeof item.id === "string").map((item) => item.id);
10841
+ } catch {
10842
+ return [];
10843
+ }
10844
+ })();
10845
+ const reason = output.split(`
10846
+ `).map((line) => line.trim()).filter((line) => line && !line.startsWith("Restarting the relay leaves")).join(" ");
10847
+ const who = sessions.length === 1 ? `an agent session is active (${sessions[0]})` : sessions.length > 1 ? `${sessions.length} agent sessions are active (${sessions.slice(0, 5).join(", ")}${sessions.length > 5 ? ", …" : ""})` : undefined;
10848
+ const forceable = /--force/.test(reason);
10849
+ return [
10850
+ `! Relay not restarted: ${who ?? (reason || "it declined")}${who && reason ? ` (${reason})` : ""}.`,
10851
+ " It keeps running the previous version, so agents get this update only after it restarts.",
10852
+ forceable ? ` When no agent is using the browser, run: ${command} (or ${command} --force to end what's blocking it now)` : ` When no agent is using the browser, run: ${command}`
10853
+ ];
10854
+ }
10855
+ function mcpServers(file = configFile()) {
10856
+ const errors = [];
10857
+ const config = parse2(existsSync3(file) ? readFileSync3(file, "utf8") : "{}", errors, { allowTrailingComma: true });
10858
+ if (errors.length || [config, config?.mcp, config?.mcp?.servers].some((v) => v !== undefined && (!v || typeof v !== "object" || Array.isArray(v))))
10859
+ fail(`Invalid ${file}; left unchanged`);
10860
+ return config.mcp?.servers ?? {};
10861
+ }
10862
+ function ownedMcp(server, entry) {
10863
+ const command = server?.command;
10864
+ return Array.isArray(command) && command.length === 3 && command[2] === "mcp" && [entry, layout.shim, path4.join(packageRoot, "dist/cli.mjs")].some((e) => e && command[1] === e);
10865
+ }
10866
+ function mergedMcpServer(existing, command, runtime) {
10867
+ const environment = typeof existing?.environment === "object" && existing.environment !== null && !Array.isArray(existing.environment) ? existing.environment : {};
10868
+ return { ...existing, type: "local", command, environment: { ...environment, OPENCODE_BROWSER_RUNTIME: runtime } };
10869
+ }
10870
+ function configureMcp(file, entry, previousEntry) {
10871
+ const existing = mcpServers(file)[MCP_NAME];
10872
+ if (existing && !ownedMcp(existing, previousEntry))
10742
10873
  return;
10874
+ const renamed = removeMcp(LEGACY_MCP_NAME, previousEntry, file);
10875
+ const server = mergedMcpServer(existing, [process.execPath, entry, "mcp"], entry === layout.shim ? "auto-update" : fingerprint(entry));
10876
+ if (existing && JSON.stringify(existing) === JSON.stringify(server))
10877
+ return `"${MCP_NAME}" is up to date`;
10878
+ const text = readFileOr(file, "{}");
10879
+ const edits = modify(text, ["mcp", "servers", MCP_NAME], server, {
10880
+ formattingOptions: { tabSize: 2, insertSpaces: true }
10881
+ });
10882
+ mkdirSync(path4.dirname(file), { recursive: true });
10883
+ writeFileSync(file, applyEdits(text, edits));
10884
+ if (renamed)
10885
+ return `"${LEGACY_MCP_NAME}" renamed to "${MCP_NAME}" in ${file}`;
10886
+ return existing ? `"${MCP_NAME}" updated in ${file}` : `"${MCP_NAME}" added to ${file}`;
10887
+ }
10888
+ function removeMcp(name, entry, file = configFile()) {
10889
+ const text = readFileOr(file, "");
10890
+ if (!text || !ownedMcp(mcpServers(file)[name], entry))
10891
+ return false;
10892
+ writeFileSync(file, applyEdits(text, modify(text, ["mcp", "servers", name], undefined, {})));
10893
+ return true;
10894
+ }
10895
+ function legacyBrowserControl() {
10896
+ return Object.entries(mcpServers()).filter(([name, server]) => name === "browser-control" || JSON.stringify(server.command ?? "").includes("browser-control")).map(([name]) => name);
10897
+ }
10898
+ function removeLegacyHost() {
10899
+ const leftovers = [
10900
+ ...browsers().flatMap((browser) => browser.manifests.map((directory) => path4.join(directory, `${LEGACY_HOST_NAME}.json`))),
10901
+ ...legacyFiles
10902
+ ].filter((file) => existsSync3(file));
10903
+ leftovers.forEach((file) => rmSync(file, { force: true }));
10904
+ const keys = windows ? LEGACY_REGISTRY_ROOTS.map((regRoot) => `${regRoot}\\NativeMessagingHosts\\${LEGACY_HOST_NAME}`).filter((key) => spawnSync3("reg", ["query", key, "/ve"]).status === 0) : [];
10905
+ keys.forEach((key) => spawnSync3("reg", ["delete", key, "/f"]));
10906
+ return leftovers.length + keys.length > 0;
10907
+ }
10908
+ async function installRuntime() {
10909
+ if (fromSource) {
10910
+ const bundle = path4.join(packageRoot, "dist", "cli.mjs");
10911
+ if (!existsSync3(bundle))
10912
+ fail("Build the CLI first: `bun run build` in cli/ (or `bun run setup` at the top).");
10913
+ return { entry: bundle, version: opencodeBrowserVersion2 };
10743
10914
  }
10744
- log(`Waiting for Browse to connect (up to ${timeoutMs / 1000}s; Ctrl+C to skip)…`);
10745
- let stopped = false;
10746
- const stop = () => {
10747
- stopped = true;
10748
- };
10749
- process.once("SIGINT", stop);
10915
+ const spec = process.env.OPENCODE_BROWSER_PACKAGE;
10916
+ const before = readCurrent(layout);
10917
+ const existing = versionEntry(layout, opencodeBrowserVersion2);
10918
+ const reuse = !spec && existsSync3(existing) && readFileSync3(existing).equals(readFileSync3(path4.join(packageRoot, "dist", "cli.mjs"))) && spawnSync3(process.execPath, [existing, "--version"], { encoding: "utf8", timeout: 30000 }).stdout?.trim() === opencodeBrowserVersion2;
10919
+ let installed;
10750
10920
  try {
10751
- const deadline = Date.now() + timeoutMs;
10752
- do {
10753
- const status = await extensionStatus(!args.includes("--no-open") && (offer === "opened" || offer === "none" && !blocked.length));
10754
- if (status?.protocolCompatible === false)
10755
- return reportMismatch(status);
10756
- if (status?.connected)
10757
- return log(connectedMessage);
10758
- if (status?.pairingState === "paired-offline")
10759
- return log(pairedOfflineMessage);
10760
- if (!folder && offer === "none") {
10761
- const scan = detectBrowsers();
10762
- if (!checkStore(scan))
10763
- return;
10764
- if (scan.some((d) => d.enabled.length))
10765
- offer = pairStore(scan);
10766
- }
10767
- await new Promise((resolve) => setTimeout(resolve, Math.max(0, Math.min(1000, deadline - Date.now()))));
10768
- } while (!stopped && Date.now() < deadline);
10769
- if (!env.tty)
10770
- return log(`! Not connected yet. ${agentNext()}`);
10771
- log(`! Not connected yet. ${code ? manualPair(pairingCode()) : next}`);
10772
- process.exitCode = 1;
10773
- } finally {
10774
- process.off("SIGINT", stop);
10921
+ installed = reuse ? existing : await installVersion({
10922
+ layout,
10923
+ version: opencodeBrowserVersion2,
10924
+ ...spec ? { spec } : { runningPackage: packageRoot }
10925
+ });
10926
+ } catch (error) {
10927
+ fail(`Could not install ${spec ?? `${PACKAGE}@${opencodeBrowserVersion2}`}: ${error.message}`);
10775
10928
  }
10929
+ const version = path4.relative(layout.versions, installed).split(path4.sep)[0];
10930
+ const previous = before?.version === version ? before.previous : before?.version;
10931
+ switchVersion(layout, version, previous);
10932
+ updateState(layout, ({ pending, skipped, ...state }) => ({
10933
+ ...state,
10934
+ ...pending && compareVersions(pending, version) > 0 ? { pending } : {},
10935
+ ...skipped && skipped !== version ? { skipped } : {}
10936
+ }));
10937
+ pruneVersions(layout, [version, previous]);
10938
+ for (const legacy of ["node_modules", "package.json", "package-lock.json"])
10939
+ rmSync(path4.join(files.runtime, legacy), { recursive: true, force: true });
10940
+ return { entry: layout.shim, version };
10776
10941
  }
10777
- var connectedMessage = "✓ Connected. Restart your agent, then ask it to open a page.";
10778
- var pairedOfflineMessage = "! Not connected yet. Open the browser profile already paired with Browse and make sure the extension is enabled. It reconnects automatically.";
10779
- var manualPair = (code) => `Open the Extensions menu → Browse → Options, enter ${formatPairingCode(code)} as the pairing code, then click Pair.`;
10780
- var installWaitTimeout = (args, tty) => waitTimeout(args, tty ? 180 : 90);
10781
- function waitTimeout(args, defaultSeconds = 180) {
10782
- const seconds = Number(args.includes("--wait-seconds") ? flag(args, "--wait-seconds") : defaultSeconds);
10783
- if (!Number.isSafeInteger(seconds) || seconds < 1 || seconds > 3600)
10784
- fail("--wait-seconds must be between 1 and 3600.");
10785
- return seconds * 1000;
10942
+ function installedRuntime(settings, server) {
10943
+ const command = Array.isArray(server?.command) ? server.command.filter((part) => typeof part === "string") : [];
10944
+ const entry = settings?.entry ?? command.find((part) => /cli\.m?js$/.test(part));
10945
+ if (!entry || !settings?.version && !existsSync3(entry))
10946
+ return;
10947
+ const packageDir = path4.dirname(path4.dirname(entry));
10948
+ const source = existsSync3(path4.join(packageDir, "src", "main.ts"));
10949
+ const version = (entry === layout.shim ? readCurrent(layout)?.version : undefined) ?? settings?.version ?? (path4.resolve(entry) === path4.join(packageRoot, "dist", "cli.mjs") ? opencodeBrowserVersion2 : readJson(path4.join(packageDir, "package.json"))?.version);
10950
+ return { entry, version, source };
10786
10951
  }
10952
+
10953
+ // src/setup/commands.ts
10954
+ var relayBase = () => `http://127.0.0.1:${Number(process.env.OPENCODE_BROWSER_PORT) || 19988}`;
10955
+ var relayHeaders = () => {
10956
+ try {
10957
+ return { authorization: relayAuthorization() };
10958
+ } catch {
10959
+ return {};
10960
+ }
10961
+ };
10787
10962
  function ensureRelay(entry) {
10788
- const started = spawnSync(process.execPath, [entry, "session", "list", "--json"], { encoding: "utf8", timeout: 30000 });
10963
+ const started = spawnSync4(process.execPath, [entry, "session", "list", "--json"], {
10964
+ encoding: "utf8",
10965
+ timeout: 30000
10966
+ });
10789
10967
  if (started.status !== 0)
10790
10968
  fail(`Couldn't start the Browse relay: ${started.stderr?.trim() || started.error?.message || `run ${cliCommand("doctor")}`}`);
10791
10969
  }
10792
- function installedRuntime(settings, server) {
10793
- const command = Array.isArray(server?.command) ? server.command.filter((part) => typeof part === "string") : [];
10794
- const entry = settings?.entry ?? command.find((part) => /cli\.m?js$/.test(part));
10795
- if (!entry || !settings?.version && !existsSync(entry))
10970
+ var extensionStatus = (installWaiting = false) => fetch(`${relayBase()}/extension/status`, {
10971
+ headers: { ...relayHeaders(), ...installWaiting ? { "x-browse-install": "waiting" } : {} },
10972
+ signal: AbortSignal.timeout(3000)
10973
+ }).then((response) => response.ok ? response.json() : undefined).catch(() => {
10974
+ return;
10975
+ });
10976
+ async function settledExtensionStatus(entry) {
10977
+ let status = await extensionStatus();
10978
+ if (!status && Object.keys(readBrowserPairings()).length) {
10979
+ ensureRelay(entry);
10980
+ status = await extensionStatus();
10981
+ }
10982
+ for (let attempt = 0;status?.connected === false && attempt < 10; attempt++) {
10983
+ await new Promise((resolve) => setTimeout(resolve, 500));
10984
+ status = await extensionStatus();
10985
+ }
10986
+ return status;
10987
+ }
10988
+ async function reloadOutdatedExtension(before, folder) {
10989
+ if (before.origin !== `chrome-extension://${unpackedExtensionId}`)
10990
+ return "current";
10991
+ const bundled = readJson(path5.join(folder, "build.json"))?.build;
10992
+ if (bundled && before.build === bundled)
10993
+ return "current";
10994
+ const sent = await fetch(`${relayBase()}/extension/reload`, {
10995
+ method: "POST",
10996
+ body: "{}",
10997
+ headers: { "content-type": "application/json", ...relayHeaders() },
10998
+ signal: AbortSignal.timeout(5000)
10999
+ }).then((response) => response.ok, () => false);
11000
+ if (!sent)
11001
+ return "stale";
11002
+ for (let attempt = 0;attempt < 20; attempt++) {
11003
+ await new Promise((resolve) => setTimeout(resolve, 500));
11004
+ const after = await extensionStatus();
11005
+ if (after?.connected && after.build !== before.build)
11006
+ return "reloaded";
11007
+ }
11008
+ return "stale";
11009
+ }
11010
+ async function syncDev() {
11011
+ if (!dev)
10796
11012
  return;
10797
- const packageDir = path2.dirname(path2.dirname(entry));
10798
- const source = existsSync(path2.join(packageDir, "src", "main.ts"));
10799
- const version = (entry === layout.shim ? readCurrent(layout)?.version : undefined) ?? settings?.version ?? (path2.resolve(entry) === path2.join(packageRoot, "dist", "cli.mjs") ? opencodeBrowserVersion2 : readJson(path2.join(packageDir, "package.json"))?.version);
10800
- return { entry, version, source };
11013
+ try {
11014
+ const result = await syncDevExtension(dev, { root });
11015
+ log(result === "updated" ? `✓ Downloaded the dev extension (${dev.file}) to ${devExtensionFolder(root)}` : `✓ Dev extension ${dev.file} is in ${devExtensionFolder(root)}`);
11016
+ } catch (error) {
11017
+ fail(`Could not get the dev extension: ${error.message}`);
11018
+ }
10801
11019
  }
10802
- function printSetupStatus() {
10803
- const settings = readJson(files.settings);
10804
- const runtime = installedRuntime(settings, mcpServers()[MCP_NAME]);
10805
- log(runtime ? `Setup: installed${runtime.source ? " (from source)" : ""}` : `Setup: not installed. Run \`${installCommand()}\`.`);
10806
- log(`Version: ${runtime ? `${runtime.version ?? "unknown"} (${runtime.entry})` : "not installed"}`);
10807
- log(`opencode: ${settings?.opencode ?? findOpencode([]) ?? "not found"}`);
10808
- log(mcpServers()[MCP_NAME] ? `MCP: "${MCP_NAME}" configured` : "MCP: not configured");
10809
- log(describeAutoUpdate(autoUpdateStatus({ layout })));
10810
- log(`Logs: ${path2.join(files.dir, "logs")}`);
10811
- log("");
11020
+ function pairUnpacked() {
11021
+ const directories = [...checkoutExtensionDirs, ...dev ? [devExtensionFolder(root)] : []].filter((directory) => existsSync4(path5.join(directory, "manifest.json")));
11022
+ if (directories.length === 0)
11023
+ return;
11024
+ const code = mintPairingCode(undefined, { ttlMs: bundledPairingCodeTtlMs });
11025
+ for (const directory of directories)
11026
+ writeExtensionPairing(directory, code);
10812
11027
  }
10813
11028
  async function extension() {
10814
11029
  const folder = unpackedFolder();
@@ -10820,22 +11035,14 @@ async function extension() {
10820
11035
  return;
10821
11036
  }
10822
11037
  await syncDev();
10823
- if (!existsSync(path2.join(folder, "manifest.json")))
11038
+ if (!existsSync4(path5.join(folder, "manifest.json")))
10824
11039
  fail(`${folder} has no built extension. Run \`${installCommand()}\` first.`);
10825
11040
  pairUnpacked();
10826
11041
  revealFolder(folder);
10827
11042
  log(`Load unpacked from: ${folder}`);
10828
11043
  }
10829
- function pairUnpacked() {
10830
- const directories = [...checkoutExtensionDirs, ...dev ? [devExtensionFolder(root)] : []].filter((directory) => existsSync(path2.join(directory, "manifest.json")));
10831
- if (directories.length === 0)
10832
- return;
10833
- const code = mintPairingCode(undefined, { ttlMs: bundledPairingCodeTtlMs });
10834
- for (const directory of directories)
10835
- writeExtensionPairing(directory, code);
10836
- }
10837
11044
  async function pair(args) {
10838
- mkdirSync(files.dir, { recursive: true });
11045
+ mkdirSync2(files.dir, { recursive: true });
10839
11046
  if (args[0] === "--list")
10840
11047
  return listPairings();
10841
11048
  if (args[0] === "--revoke")
@@ -10853,18 +11060,22 @@ async function pair(args) {
10853
11060
  log(`${manualPair(code)} It works once, within ${pairingCodeTtlMs / 60000} minutes.`);
10854
11061
  log(`Waiting up to ${timeoutMs / 1000}s for that browser. Later, \`${cliCommand("pair --list")}\` shows paired browsers.`);
10855
11062
  let paired;
10856
- await waitForExtension({ timeoutMs, intervalMs: 500, connected: async () => {
10857
- const match = Object.values(readBrowserPairings()).find((browser) => browser.receipt === receipt && browser.lastSeen !== undefined);
10858
- paired = match?.name ?? match?.id;
10859
- return paired !== undefined;
10860
- } });
11063
+ await waitForExtension({
11064
+ timeoutMs,
11065
+ intervalMs: 500,
11066
+ connected: async () => {
11067
+ const match = Object.values(readBrowserPairings()).find((browser) => browser.receipt === receipt && browser.lastSeen !== undefined);
11068
+ paired = match?.name ?? match?.id;
11069
+ return paired !== undefined;
11070
+ }
11071
+ });
10861
11072
  if (paired)
10862
11073
  return log(`✓ Paired ${paired}.`);
10863
11074
  log(`! Pairing not confirmed. For a fresh code run \`${cliCommand("pair")}\`.`);
10864
11075
  process.exitCode = 1;
10865
11076
  }
10866
11077
  function pairedBrowsers() {
10867
- const known = readJson(path2.join(files.dir, "browsers.json")) ?? {};
11078
+ const known = readJson(path5.join(files.dir, "browsers.json")) ?? {};
10868
11079
  return Object.entries(readBrowserPairings()).map(([key, entry]) => ({
10869
11080
  key,
10870
11081
  id: known[key]?.id ?? entry.id,
@@ -10929,7 +11140,7 @@ async function set(args) {
10929
11140
  log(`✓ hostname is ${hostname}`);
10930
11141
  const running = await fetch(`${relayBase()}/version`, { signal: AbortSignal.timeout(2000) }).then((response) => response.ok, () => false);
10931
11142
  if (running) {
10932
- const restarted = spawnSync(process.execPath, [process.argv[1], "relay", "restart"], { stdio: "inherit" });
11143
+ const restarted = spawnSync4(process.execPath, [process.argv[1], "relay", "restart"], { stdio: "inherit" });
10933
11144
  if (restarted.status !== 0)
10934
11145
  log(`! The relay is still running with the old hostname. Run \`${cliCommand("relay restart --force")}\` to apply it.`);
10935
11146
  }
@@ -10944,8 +11155,8 @@ async function set(args) {
10944
11155
  log("The connection isn't encrypted by Browse: use it over Tailscale or a LAN you trust, not the open internet.");
10945
11156
  }
10946
11157
  async function confirmSecretRemoval(args) {
10947
- const secrets = path2.join(files.dir, "secrets");
10948
- const profiles = existsSync(secrets) ? readdirSync(secrets).filter((name) => name.endsWith(".json")) : [];
11158
+ const secrets = path5.join(files.dir, "secrets");
11159
+ const profiles = existsSync4(secrets) ? readdirSync2(secrets).filter((name) => name.endsWith(".json")) : [];
10949
11160
  if (profiles.length === 0 || args.includes("--yes") || args.includes("-y"))
10950
11161
  return;
10951
11162
  const notice = `Uninstalling deletes ${profiles.length} stored secret profile${profiles.length === 1 ? "" : "s"} in ${secrets}.`;
@@ -10969,7 +11180,7 @@ async function uninstall(args) {
10969
11180
  fail(`Couldn't safely remove Browse from ${receipt.file}. Check its syntax and permissions, then re-run uninstall.`);
10970
11181
  }
10971
11182
  }
10972
- rmSync(files.dir, { recursive: true, force: true });
11183
+ rmSync2(files.dir, { recursive: true, force: true });
10973
11184
  log(`✓ Removed ${files.dir}`);
10974
11185
  if (host)
10975
11186
  log("✓ Removed the native messaging host of an earlier version");
@@ -10977,326 +11188,221 @@ async function uninstall(args) {
10977
11188
  log(`✓ Removed the ${mcp.map((name) => `"${name}"`).join(" and ")} MCP server${mcp.length === 1 ? "" : "s"}`);
10978
11189
  log("Remove the extension from your browser to finish.");
10979
11190
  }
10980
- var relayBase = () => `http://127.0.0.1:${Number(process.env.OPENCODE_BROWSER_PORT) || 19988}`;
10981
- var relayHeaders = () => {
10982
- try {
10983
- return { authorization: relayAuthorization() };
10984
- } catch {
10985
- return {};
10986
- }
10987
- };
10988
- var extensionStatus = (installWaiting = false) => fetch(`${relayBase()}/extension/status`, { headers: { ...relayHeaders(), ...installWaiting ? { "x-browse-install": "waiting" } : {} }, signal: AbortSignal.timeout(3000) }).then((response) => response.ok ? response.json() : undefined).catch(() => {
10989
- return;
10990
- });
10991
- async function settledExtensionStatus(entry) {
10992
- let status = await extensionStatus();
10993
- if (!status && Object.keys(readBrowserPairings()).length) {
10994
- ensureRelay(entry);
10995
- status = await extensionStatus();
10996
- }
10997
- for (let attempt = 0;status?.connected === false && attempt < 10; attempt++) {
10998
- await new Promise((resolve) => setTimeout(resolve, 500));
10999
- status = await extensionStatus();
11000
- }
11001
- return status;
11002
- }
11003
- async function reloadOutdatedExtension(before, folder) {
11004
- if (before.origin !== `chrome-extension://${unpackedExtensionId}`)
11005
- return "current";
11006
- const bundled = readJson(path2.join(folder, "build.json"))?.build;
11007
- if (bundled && before.build === bundled)
11008
- return "current";
11009
- const sent = await fetch(`${relayBase()}/extension/reload`, {
11010
- method: "POST",
11011
- body: "{}",
11012
- headers: { "content-type": "application/json", ...relayHeaders() },
11013
- signal: AbortSignal.timeout(5000)
11014
- }).then((response) => response.ok, () => false);
11015
- if (!sent)
11016
- return "stale";
11017
- for (let attempt = 0;attempt < 20; attempt++) {
11018
- await new Promise((resolve) => setTimeout(resolve, 500));
11019
- const after = await extensionStatus();
11020
- if (after?.connected && after.build !== before.build)
11021
- return "reloaded";
11022
- }
11023
- return "stale";
11024
- }
11025
- function findOnPath(name) {
11026
- const found = spawnSync(windows ? "where" : "which", [name], { encoding: "utf8" });
11027
- return found.status === 0 ? found.stdout.split(/\r?\n/)[0]?.trim() || undefined : undefined;
11028
- }
11029
- var oldStoreBuild = (match) => /^0\.0\.[012]$/.test(match.version ?? "");
11030
- function locateExtension(options, effects = systemEffects) {
11031
- const { log } = effects;
11032
- const listing = options.storeUrl ?? STORE_URL;
11033
- const noListing = () => log(`! This package doesn't know its Chrome Web Store item, so it can't install the extension. Install a released version (\`npx @opencode/browse install\`), or set up Browse from a checkout.`);
11034
- const pairLater = () => log(` Once it's added, run \`${cliCommand("install")}\` again to pair it with the relay.`);
11035
- if (options.pairedOffline)
11036
- return log(pairedOfflineMessage), "ready";
11037
- if (options.env.ssh) {
11038
- if (options.connected)
11039
- return log(connectedMessage), "ready";
11040
- if (options.folder)
11041
- unpackedSteps({ folder: options.folder, platform: options.env.platform, log });
11042
- else if (listing)
11043
- log(`Install Browse in your browser from the Chrome Web Store: ${listing}`), pairLater();
11044
- else
11045
- noListing();
11046
- return "guided";
11047
- }
11048
- reportDetections(options.detections, options.preferred, log);
11049
- if (options.connected)
11050
- return log(connectedMessage), "ready";
11051
- if (options.detections.some((detection) => detection.enabled.length))
11052
- return "ready";
11053
- const off = options.detections.flatMap((detection) => detection.disabled);
11054
- if (off.length) {
11055
- log(`! Browse is turned off in ${off.map((match) => `${match.browser.name} (${match.profile.name})`).join(", ")}. Turn it on in the browser's extensions page.`);
11056
- return "off";
11057
- }
11058
- const blocked = options.detections.filter((detection) => detection.blocked).map((detection) => detection.browser.name);
11059
- const target = options.preferred ?? options.detections.find((detection) => !detection.blocked)?.browser;
11060
- const interactive = options.allowOpen && options.env.display;
11061
- if (!options.folder) {
11062
- if (!listing)
11063
- return noListing(), "guided";
11064
- const opened = interactive && !blocked.length && (target ? effects.open(target, listing) : effects.openDefault?.(listing));
11065
- log(`Install Browse from the Chrome Web Store${opened ? ` (opened in ${target?.name ?? "your default browser"})` : ""}:`);
11066
- log(` ${listing}`);
11067
- log(` Once it's added, install pairs it with the relay (if install isn't waiting for it, run \`${cliCommand("install")}\` again).`);
11068
- return "guided";
11069
- }
11070
- const act = interactive && !blocked.length;
11071
- const page = target ? extensionsPage(target) : "chrome://extensions";
11072
- unpackedSteps({
11073
- folder: options.folder,
11074
- platform: options.env.platform,
11075
- log,
11076
- browser: target?.name,
11077
- page,
11078
- opened: act && target !== undefined && effects.open(target, page),
11079
- revealed: act && options.env.tty && effects.reveal(options.folder),
11080
- copied: act && options.env.tty && effects.copy(options.folder)
11081
- });
11082
- return "guided";
11083
- }
11084
- function pairStoreExtension(options, effects = systemEffects) {
11085
- const ids = options.storeIds ?? STORE_IDS;
11086
- const matches = options.detections.flatMap((detection) => detection.enabled).filter((match) => ids.includes(match.id) && !oldStoreBuild(match));
11087
- const match = matches.find((candidate) => candidate.browser.name === options.preferred?.name) ?? matches[0];
11088
- if (!match)
11089
- return "none";
11090
- const interactive = options.allowOpen && options.env.display && !options.env.ssh;
11091
- if (interactive && effects.open(match.browser, `chrome-extension://${match.id}/welcome.html#code=${options.code()}`, match.profile.dir)) {
11092
- effects.log(`✓ Opened Browse's status page in ${match.browser.name} (${match.profile.name}) to pair it with the relay`);
11093
- return "opened";
11094
- }
11095
- effects.log(`To pair Browse in ${match.browser.name} (${match.profile.name}): ${manualPair(options.code())}`);
11096
- return "manual";
11097
- }
11098
- function unpackedSteps(step) {
11099
- const { log } = step;
11100
- const page = step.page ?? "chrome://extensions";
11101
- const fileManager = step.platform === "darwin" ? "Finder" : step.platform === "win32" ? "Explorer" : "your file manager";
11102
- const paste = step.platform === "darwin" ? "press Cmd+Shift+G in the folder picker and paste" : "paste it into the folder picker";
11103
- log("Load the extension in your browser (one time):");
11104
- log(` 1. Open ${page}${step.browser ? ` in ${step.browser}` : " (or your browser's extensions page)"}${step.opened ? " (opened)" : ""}`);
11105
- log(" 2. Turn on Developer mode");
11106
- log(` 3. Click Load unpacked and choose this folder${step.copied ? ` (path copied: ${paste})` : ""}, or drag it onto the page:`);
11107
- log(` ${step.folder}${step.revealed ? ` (shown in ${fileManager})` : ""}`);
11108
- }
11109
- function extensionsPage(browser) {
11110
- const scheme = { "Microsoft Edge": "edge", Brave: "brave", Vivaldi: "vivaldi", Opera: "opera" }[browser.name];
11111
- return `${scheme ?? "chrome"}://extensions`;
11112
- }
11113
- async function waitForExtension(options) {
11114
- const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
11115
- for (let waited = 0;; waited += options.intervalMs) {
11116
- if (await options.connected())
11117
- return true;
11118
- if (waited >= options.timeoutMs)
11119
- return false;
11120
- await sleep(options.intervalMs);
11121
- }
11122
- }
11123
- function detectBrowsers() {
11124
- return browsers().flatMap((browser) => {
11125
- const state = readLocked(path2.join(browser.userData, "Local State"));
11126
- if (state.missing)
11127
- return [];
11128
- if (state.blocked)
11129
- return launcher(browser) ? [{ browser, blocked: true, enabled: [], disabled: [] }] : [];
11130
- let blocked = false;
11131
- const matches = listProfiles(browser, state.text).flatMap((profile) => {
11132
- const settings = ["Secure Preferences", "Preferences"].map((file) => {
11133
- const prefs = readLocked(path2.join(browser.userData, profile.dir, file));
11134
- blocked ||= prefs.blocked;
11135
- return parseJson(prefs.text)?.extensions?.settings ?? {};
11136
- });
11137
- const id = [...STORE_IDS, unpackedExtensionId].find((candidate) => settings.some((entries) => entries[candidate]));
11138
- if (!id)
11139
- return [];
11140
- const setting = settings.find((entries) => entries[id])[id];
11141
- const version = installedExtensionVersion(id, setting);
11142
- return [{ browser, profile, id, ...version ? { version } : {}, enabled: !disabled(setting) }];
11143
- });
11144
- return [
11145
- {
11146
- browser,
11147
- blocked,
11148
- enabled: matches.filter((match) => match.enabled),
11149
- disabled: matches.filter((match) => !match.enabled)
11150
- }
11151
- ];
11152
- });
11153
- }
11154
- function installedExtensionVersion(id, setting) {
11155
- if (setting.manifest?.version)
11156
- return setting.manifest.version;
11157
- if (!setting.path)
11158
- return;
11159
- if (path2.isAbsolute(setting.path))
11160
- return readJson(path2.join(setting.path, "manifest.json"))?.version;
11161
- const packed = /^([a-p]{32})[/\\](\d+(?:\.\d+){0,3})_\d+$/.exec(setting.path);
11162
- return packed?.[1] === id ? packed[2] : undefined;
11163
- }
11164
- function disabled(setting) {
11165
- const reasons = setting.disable_reasons;
11166
- if (Array.isArray(reasons))
11167
- return reasons.length > 0;
11168
- return typeof reasons === "number" && reasons !== 0;
11169
- }
11170
- function listProfiles(browser, localState) {
11171
- const cache = parseJson(localState)?.profile?.info_cache;
11172
- if (cache && Object.keys(cache).length)
11173
- return Object.entries(cache).map(([dir, info]) => ({ dir, name: info.name || dir }));
11174
- const dirs = (() => {
11175
- try {
11176
- return readdirSync(browser.userData);
11177
- } catch {
11178
- return [];
11179
- }
11180
- })();
11181
- return dirs.filter((dir) => /^(Default|Profile \d+)$/.test(dir)).map((dir) => ({ dir, name: dir }));
11182
- }
11183
- function readLocked(file) {
11184
- try {
11185
- return { text: readFileSync(file, "utf8"), missing: false, blocked: false };
11186
- } catch (error) {
11187
- const code = error.code;
11188
- return { text: undefined, missing: code === "ENOENT" || code === "ENOTDIR", blocked: code === "EPERM" || code === "EACCES" };
11189
- }
11191
+
11192
+ // src/setup.ts
11193
+ function checkNode() {
11194
+ if (process.versions.bun)
11195
+ fail("Browse runs on Node.js 22.19 or newer, but this ran under Bun (`bun run` uses Bun when `node` isn't on PATH). Install Node.js from https://nodejs.org, then run setup again.");
11196
+ const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
11197
+ if (major < 22 || major === 22 && minor < 19)
11198
+ fail(`Browse needs Node.js 22.19 or newer; this is ${process.version} (${process.execPath}). Update Node.js (https://nodejs.org), then run setup again.`);
11190
11199
  }
11191
- function parseJson(text) {
11192
- if (!text)
11193
- return;
11200
+ async function install(args) {
11201
+ checkNode();
11202
+ const tty = !!(process.stdin.isTTY && process.stdout.isTTY);
11203
+ const timeoutMs = installWaitTimeout(args, tty);
11204
+ let flags;
11194
11205
  try {
11195
- return JSON.parse(text);
11196
- } catch {
11197
- return;
11206
+ flags = agentFlags(args);
11207
+ } catch (error) {
11208
+ fail(error.message);
11198
11209
  }
11199
- }
11200
- function defaultBrowser(candidates) {
11201
- const id = defaultBrowserID();
11202
- if (!id)
11210
+ const opencode = findOpencode(args);
11211
+ const caller = callingAgent(process.env);
11212
+ const locations = agentLocations(home, configRoot, findOnPath, configFile(), opencode);
11213
+ const previous = readJson(files.settings);
11214
+ const remembered = previous?.selectedAgents ?? (previous?.declinedAgents?.length ? locations.filter((a) => a.installed && !previous.declinedAgents.includes(a.name)).map((a) => a.name) : undefined);
11215
+ const plan = agentPlan(locations, flags, tty, caller, remembered);
11216
+ const selected = plan.picker ? await pickAgents(locations, plan.selected) : plan.selected;
11217
+ if (!selected) {
11218
+ process.exitCode = 130;
11203
11219
  return;
11204
- const known = browsers().find((browser) => browser.defaultIDs.some((name) => name.toLowerCase() === id.toLowerCase() || id.startsWith(`${name}.`)));
11205
- return known && (candidates.find((browser) => browser.name === known.name) ?? known);
11206
- }
11207
- function defaultBrowserID() {
11208
- if (process.platform === "darwin") {
11209
- const plist = path2.join(home, "Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist");
11210
- const json = spawnSync("plutil", ["-convert", "json", "-o", "-", plist], { encoding: "utf8" });
11211
- const handlers = parseJson(json.stdout)?.LSHandlers ?? [];
11212
- return ["https", "http"].map((scheme) => handlers.find((handler) => handler.LSHandlerURLScheme === scheme)?.LSHandlerRoleAll).find((bundle) => !!bundle);
11213
11220
  }
11214
- if (windows) {
11215
- const key = "HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\https\\UserChoice";
11216
- const query = spawnSync("reg", ["query", key, "/v", "ProgId"], { encoding: "utf8" });
11217
- return /ProgId\s+REG_SZ\s+(\S+)/.exec(query.stdout ?? "")?.[1];
11221
+ mkdirSync3(files.dir, { recursive: true });
11222
+ const { entry, version } = await installRuntime();
11223
+ log(`✓ Browse ${version} installed${fromSource ? " (from source)" : ""}`);
11224
+ const agents = previous?.agents ?? [];
11225
+ const opencodeConfigs = previous?.opencodeConfigs ?? (previous?.entry ? [configFile()] : []);
11226
+ const selectedAgents = plan.picker ? selected : remembered;
11227
+ const save = () => writeFileSync2(files.settings, JSON.stringify({
11228
+ ...opencode ? { opencode } : {},
11229
+ entry,
11230
+ version,
11231
+ agents,
11232
+ opencodeConfigs,
11233
+ ...selectedAgents && { selectedAgents }
11234
+ }, null, 2) + `
11235
+ `);
11236
+ save();
11237
+ if (removeLegacyHost())
11238
+ log("✓ Removed the native messaging host of an earlier version");
11239
+ const node = process.execPath;
11240
+ for (const agent of locations) {
11241
+ if (!selected.includes(agent.name)) {
11242
+ if (!plan.picker && agent.installed && !flags.requested.length)
11243
+ log(`To add ${agentLabels[agent.name]}: ${cliCommand(`install --agent ${agent.name}`)}`);
11244
+ continue;
11245
+ }
11246
+ if (agent.name === "opencode") {
11247
+ const mcp = configureMcp(agent.file, entry, previous?.entry);
11248
+ if (mcp && !opencodeConfigs.includes(agent.file)) {
11249
+ opencodeConfigs.push(agent.file);
11250
+ save();
11251
+ }
11252
+ if (!mcp)
11253
+ process.exitCode = 1;
11254
+ log(mcp ? `✓ MCP server ${mcp} (OpenCode)` : `! OpenCode already has a different server named browse in ${agent.file}; left unchanged.`);
11255
+ continue;
11256
+ }
11257
+ const receipt = { name: agent.name, file: agent.file, entry, node };
11258
+ try {
11259
+ const result = editAgentConfig(receipt);
11260
+ if (result === "added") {
11261
+ const i = agents.findIndex((a) => a.file === agent.file);
11262
+ if (i !== -1)
11263
+ agents.splice(i, 1);
11264
+ agents.push(receipt);
11265
+ save();
11266
+ }
11267
+ if (result === "conflict")
11268
+ process.exitCode = 1;
11269
+ log(result === "conflict" ? `! ${agent.name} already has a different browse server; left ${agent.file} unchanged.` : `✓ ${agent.name}: browse ${result} in ${agent.file}`);
11270
+ } catch {
11271
+ process.exitCode = 1;
11272
+ log(`! Couldn't edit ${agent.file} for ${agent.name}. Check its syntax and permissions, then run ${cliCommand(`install --agent ${agent.name}`)}.`);
11273
+ }
11218
11274
  }
11219
- const xdg = spawnSync("xdg-settings", ["get", "default-web-browser"], { encoding: "utf8" });
11220
- return xdg.status === 0 ? xdg.stdout.trim().replace(/\.desktop$/, "") || undefined : undefined;
11221
- }
11222
- function reportDetections(detections, preferred, log) {
11223
- log(`Default browser: ${preferred?.name ?? "unknown"}`);
11224
- detections.forEach((detection) => {
11225
- const name = detection.browser.name;
11226
- if (detection.blocked)
11227
- return log(` ${name}: couldn't check (${blockedReason()})`);
11228
- if (detection.enabled.length)
11229
- return log(` ${name}: Browse in ${detection.enabled.map(describe).join(", ")}`);
11230
- if (detection.disabled.length)
11231
- return log(` ${name}: Browse turned off in ${detection.disabled.map(describe).join(", ")}`);
11232
- log(` ${name}: no Browse`);
11233
- });
11234
- }
11235
- var describe = (match) => `${match.profile.name}${match.id === unpackedExtensionId ? " (unpacked)" : ""}`;
11236
- var blockedReason = () => process.platform === "darwin" ? "macOS privacy protection" : "permission denied";
11237
- function openInBrowser(browser, url, profile) {
11238
- const executable = launcher(browser)?.executable;
11239
- const web = /^https?:/.test(url);
11240
- if (executable) {
11241
- const child = spawn(executable, [...profile ? [`--profile-directory=${profile}`] : [], url], { detached: true, stdio: "ignore" });
11242
- child.on("error", () => web && openWithSystem(browser, url));
11243
- child.unref();
11244
- return true;
11275
+ for (const line of restartOutdatedRelay(entry, node) ?? [])
11276
+ log(line);
11277
+ const legacy = selected.includes("opencode") ? legacyBrowserControl() : [];
11278
+ if (legacy.length)
11279
+ log(`! Browser Control MCP server${legacy.length === 1 ? "" : "s"} ${legacy.map((name) => `"${name}"`).join(", ")} also configured. Browse replaces it; remove ${legacy.length === 1 ? "it" : "them"} from ${configFile()} to avoid two sets of browser tools.`);
11280
+ await syncDev();
11281
+ const initial = await settledExtensionStatus(entry);
11282
+ const connected = initial?.connected ? initial : undefined;
11283
+ const pairedOffline = initial?.pairingState === "paired-offline";
11284
+ const folder = unpackedFolder();
11285
+ if (folder && !pairedOffline) {
11286
+ pairUnpacked();
11287
+ log("✓ Put a one-time pairing code in the unpacked extension (good for an hour)");
11288
+ const reload = connected ? await reloadOutdatedExtension(connected, folder) : "current";
11289
+ if (reload === "reloaded")
11290
+ log("✓ Reloaded Browse in your browser");
11291
+ if (reload === "stale")
11292
+ return log(`! ${unpackedReloadAdvice}`);
11245
11293
  }
11246
- if (!web)
11294
+ const reportMismatch = (status) => {
11295
+ const advice = status.nextStep ?? extensionProtocolAdvice(status.protocolVersion, { install: installCommand() });
11296
+ if (args.includes("--no-wait") && folder && status.origin === `chrome-extension://${unpackedExtensionId}` && extensionProtocolSkew(status.protocolVersion) === "extension-older")
11297
+ return log(`! Not connected yet. ${advice}`);
11298
+ fail(advice);
11299
+ };
11300
+ if (initial?.protocolCompatible === false && initial.origin)
11301
+ return reportMismatch(initial);
11302
+ log("");
11303
+ const ssh = !!(process.env.SSH_CONNECTION || process.env.SSH_TTY);
11304
+ const detections = ssh ? [] : detectBrowsers();
11305
+ rmSync3(oldStoreMarker(), { force: true });
11306
+ const checkStore = (scanned) => {
11307
+ const matches = scanned.flatMap((d) => d.enabled).filter((m) => STORE_IDS.includes(m.id));
11308
+ if (!matches.length || !matches.every(oldStoreBuild))
11309
+ return true;
11310
+ writeFileSync2(oldStoreMarker(), `2
11311
+ `);
11312
+ ensureRelay(entry);
11313
+ log(`! Found Browse ${matches[0].version} in ${matches[0].browser.name}. ${extensionProtocolAdvice(2, { install: cliCommand("install") }, "store")}`);
11314
+ process.exitCode = 1;
11247
11315
  return false;
11248
- return openWithSystem(browser, url);
11249
- }
11250
- function openWithSystem(browser, url) {
11251
- if (process.platform === "darwin")
11252
- return spawnSync("open", browser?.app ? ["-a", browser.app, url] : [url]).status === 0;
11253
- if (windows)
11254
- return spawnSync("rundll32", ["url.dll,FileProtocolHandler", url]).status === 0;
11255
- return spawnSync("xdg-open", [url]).status === 0;
11256
- }
11257
- function revealFolder(folder) {
11258
- const run = (command, args) => {
11259
- const result = spawnSync(command, args, { stdio: "ignore", timeout: 1e4 });
11260
- return !result.error && (windows || result.status === 0);
11261
11316
  };
11262
- if (process.platform === "darwin")
11263
- return run("open", ["-R", folder]);
11264
- if (windows)
11265
- return run("explorer", [`/select,${folder}`]);
11266
- return run("xdg-open", [path2.dirname(folder)]);
11267
- }
11268
- function copyToClipboard(text) {
11269
- const tools = process.platform === "darwin" ? [["pbcopy", []]] : windows ? [["clip", []]] : [
11270
- ...process.env.WAYLAND_DISPLAY ? [["wl-copy", []]] : [],
11271
- ["xclip", ["-selection", "clipboard"]],
11272
- ["xsel", ["--clipboard", "--input"]]
11273
- ];
11274
- return tools.some(([command, args]) => {
11275
- const result = spawnSync(command, args, { input: text, stdio: ["pipe", "ignore", "ignore"], timeout: 5000 });
11276
- return !result.error && result.status === 0;
11317
+ if (!connected && !checkStore(detections))
11318
+ return;
11319
+ const env = {
11320
+ ssh,
11321
+ display: process.platform !== "linux" || !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY),
11322
+ tty,
11323
+ platform: process.platform
11324
+ };
11325
+ const found = locateExtension({
11326
+ allowOpen: !args.includes("--no-open"),
11327
+ connected: !!connected,
11328
+ pairedOffline,
11329
+ ...folder ? { folder } : {},
11330
+ detections,
11331
+ preferred: ssh ? undefined : defaultBrowser(detections.map((detection) => detection.browser)),
11332
+ env
11277
11333
  });
11278
- }
11279
- var systemEffects = { open: openInBrowser, openDefault: (url) => openWithSystem(undefined, url), reveal: revealFolder, copy: copyToClipboard, log };
11280
- function launcher(browser) {
11281
- if (process.platform === "darwin") {
11282
- const bundle = [path2.join("/Applications", `${browser.app}.app`), path2.join(home, "Applications", `${browser.app}.app`)].find((dir) => existsSync(dir));
11283
- if (!bundle)
11284
- return;
11285
- const name = spawnSync("plutil", ["-extract", "CFBundleExecutable", "raw", "-o", "-", path2.join(bundle, "Contents", "Info.plist")], { encoding: "utf8" }).stdout?.trim();
11286
- const executable = name ? path2.join(bundle, "Contents", "MacOS", name) : undefined;
11287
- return executable && existsSync(executable) ? { executable } : {};
11334
+ if (connected || found === "off" || pairedOffline || !folder && !STORE_URL)
11335
+ return;
11336
+ ensureRelay(entry);
11337
+ let code;
11338
+ const pairingCode = () => code && pendingPairingCodeKeys().includes(pairingCodeKey(code)) ? code : code = mintPairingCode();
11339
+ const pairStore = (scanned) => pairStoreExtension({
11340
+ detections: scanned,
11341
+ preferred: defaultBrowser(scanned.map((d) => d.browser)),
11342
+ allowOpen: !args.includes("--no-open"),
11343
+ env,
11344
+ code: pairingCode
11345
+ });
11346
+ let offer = ssh ? "none" : pairStore(detections);
11347
+ const blocked = detections.filter((d) => d.blocked).map((d) => d.browser.name);
11348
+ if (offer === "none" && blocked.length)
11349
+ log(`Couldn't check ${blocked.join(", ")}. If Browse is already installed there, pair it manually. ${manualPair(pairingCode())}`);
11350
+ const next = folder ? `If Browse is already loaded unpacked: ${unpackedReloadAdvice}` : `If Browse isn't connected after you add it, run \`${cliCommand("install")}\` again to pair it.`;
11351
+ const agentNext = () => {
11352
+ if (folder && offer === "none" && !blocked.length)
11353
+ return `Agent: if Browse is already loaded unpacked, ask the user: ${unpackedReloadAdvice} Otherwise ask them to load ${folder}. Check the connection with the browse status tool.`;
11354
+ return `Agent: ${offer === "opened" ? "ask the user to open Browse's status page in their browser" : offer === "manual" || blocked.length ? "ask the user to finish pairing in Browse's Options" : "ask the user to add Browse in their browser"}. Then re-run \`${cliCommand("install")}\` to finish, or call the browse status tool to check the connection.`;
11355
+ };
11356
+ if (!env.tty)
11357
+ log(agentNext());
11358
+ if (args.includes("--no-wait") || ssh || !env.display) {
11359
+ if (env.tty)
11360
+ log(next);
11361
+ return;
11362
+ }
11363
+ log(`Waiting for Browse to connect (up to ${timeoutMs / 1000}s; Ctrl+C to skip)…`);
11364
+ let stopped = false;
11365
+ const stop = () => {
11366
+ stopped = true;
11367
+ };
11368
+ process.once("SIGINT", stop);
11369
+ try {
11370
+ const deadline = Date.now() + timeoutMs;
11371
+ do {
11372
+ const status = await extensionStatus(!args.includes("--no-open") && (offer === "opened" || offer === "none" && !blocked.length));
11373
+ if (status?.protocolCompatible === false)
11374
+ return reportMismatch(status);
11375
+ if (status?.connected)
11376
+ return log(connectedMessage);
11377
+ if (status?.pairingState === "paired-offline")
11378
+ return log(pairedOfflineMessage);
11379
+ if (!folder && offer === "none") {
11380
+ const scan = detectBrowsers();
11381
+ if (!checkStore(scan))
11382
+ return;
11383
+ if (scan.some((d) => d.enabled.length))
11384
+ offer = pairStore(scan);
11385
+ }
11386
+ await new Promise((resolve) => setTimeout(resolve, Math.max(0, Math.min(1000, deadline - Date.now()))));
11387
+ } while (!stopped && Date.now() < deadline);
11388
+ if (!env.tty)
11389
+ return log(`! Not connected yet. ${agentNext()}`);
11390
+ log(`! Not connected yet. ${code ? manualPair(pairingCode()) : next}`);
11391
+ process.exitCode = 1;
11392
+ } finally {
11393
+ process.off("SIGINT", stop);
11288
11394
  }
11289
- const executable = windows ? browser.executables.find((file) => existsSync(file)) : browser.executables.map(findOnPath).find((file) => !!file);
11290
- return executable ? { executable } : undefined;
11291
- }
11292
- function log(line) {
11293
- process.stdout.write(line + `
11294
- `);
11295
11395
  }
11296
- function fail(message) {
11297
- process.stderr.write(message + `
11298
- `);
11299
- process.exit(1);
11396
+ function printSetupStatus() {
11397
+ const settings = readJson(files.settings);
11398
+ const runtime = installedRuntime(settings, mcpServers()[MCP_NAME]);
11399
+ log(runtime ? `Setup: installed${runtime.source ? " (from source)" : ""}` : `Setup: not installed. Run \`${installCommand()}\`.`);
11400
+ log(`Version: ${runtime ? `${runtime.version ?? "unknown"} (${runtime.entry})` : "not installed"}`);
11401
+ log(`opencode: ${settings?.opencode ?? findOpencode([]) ?? "not found"}`);
11402
+ log(mcpServers()[MCP_NAME] ? `MCP: "${MCP_NAME}" configured` : "MCP: not configured");
11403
+ log(describeAutoUpdate(autoUpdateStatus({ layout })));
11404
+ log(`Logs: ${path6.join(files.dir, "logs")}`);
11405
+ log("");
11300
11406
  }
11301
11407
  async function runSetup(command, args) {
11302
11408
  if (command === "install")
@@ -11310,7 +11416,7 @@ async function runSetup(command, args) {
11310
11416
  if (command === "set")
11311
11417
  return set(args);
11312
11418
  if (command === "rollback") {
11313
- const { runRollback } = await import("./runtime-update-8wte78jj.mjs");
11419
+ const { runRollback } = await import("./runtime-update-pxbj49tf.mjs");
11314
11420
  return runRollback(args);
11315
11421
  }
11316
11422
  }