@opencode/browse 0.0.6-dev.18 → 0.0.6-dev.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{cli-z00ttket.mjs → cli-anqhds1e.mjs} +771 -643
- package/dist/cli.mjs +8 -8
- package/dist/{relay-helpers-6a2vvzx9.mjs → helpers-n4k4pdag.mjs} +9 -9
- package/dist/{main-jjny7bmm.mjs → main-0nta0a2z.mjs} +200 -191
- package/dist/{main-9y22cffn.mjs → main-20ahaz2q.mjs} +3 -3
- package/dist/{main-w5sta7hq.mjs → main-3rsfghef.mjs} +4 -4
- package/dist/{main-d7rbre73.mjs → main-42qfr69q.mjs} +4 -4
- package/dist/main-arx3pfyn.mjs +4 -0
- package/dist/{main-0egy8naa.mjs → main-hywb9qbv.mjs} +5 -5
- package/dist/{main-ha5bnd77.mjs → main-ma25hhqh.mjs} +120 -118
- package/dist/{main-fkhwazsm.mjs → main-yd1d7qr1.mjs} +1708 -1532
- package/dist/{mcp-qh25n9s0.mjs → mcp-j9e2da4y.mjs} +584 -481
- package/dist/{relay-zs96tkfm.mjs → relay-cvhz2f0a.mjs} +16299 -15709
- package/dist/{runtime-update-8wte78jj.mjs → runtime-update-baranjc5.mjs} +5 -5
- package/dist/{setup-cqjdfcy0.mjs → setup-7shrgdza.mjs} +811 -705
- package/dist/{update-st5c8grg.mjs → update-vkspb2rz.mjs} +2 -2
- package/dist/{version-6g6gtnra.mjs → version-gfpqxbzm.mjs} +1 -1
- package/package.json +1 -2
- package/dist/main-ppz7sv9s.mjs +0 -4
|
@@ -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
|
-
|
|
19
|
+
switchVersion,
|
|
20
|
+
unpackedExtensionId,
|
|
21
|
+
unpackedReloadAdvice,
|
|
22
|
+
extensionProtocolSkew,
|
|
23
|
+
extensionProtocolAdvice
|
|
24
|
+
} from "./main-ma25hhqh.mjs";
|
|
25
25
|
import {
|
|
26
26
|
relayAuthorization
|
|
27
|
-
} from "./main-
|
|
27
|
+
} from "./main-20ahaz2q.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-
|
|
44
|
+
} from "./main-hywb9qbv.mjs";
|
|
45
45
|
import {
|
|
46
46
|
opencodeBrowserVersion2
|
|
47
|
-
} from "./main-
|
|
47
|
+
} from "./main-arx3pfyn.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 {
|
|
7086
|
-
import
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
import
|
|
7090
|
-
import
|
|
7091
|
-
import {
|
|
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
|
|
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) =>
|
|
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) =>
|
|
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", [
|
|
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 ??
|
|
10401
|
-
const roaming = process.env.APPDATA ??
|
|
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) =>
|
|
10464
|
+
const installs = (relative) => programs.map((dir) => path3.join(dir, relative));
|
|
10404
10465
|
return [
|
|
10405
|
-
{
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
10409
|
-
|
|
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,
|
|
10504
|
+
const browser = (name, rootDir, dir, executables) => ({
|
|
10414
10505
|
name,
|
|
10415
|
-
userData:
|
|
10416
|
-
manifests: [
|
|
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
|
|
10434
|
-
const
|
|
10435
|
-
|
|
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
|
|
10440
|
-
|
|
10441
|
-
|
|
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
|
|
10444
|
-
|
|
10445
|
-
return
|
|
10446
|
-
|
|
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
|
-
|
|
10451
|
-
|
|
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
|
|
10459
|
-
const
|
|
10460
|
-
if (
|
|
10461
|
-
return;
|
|
10462
|
-
|
|
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
|
|
10469
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
10499
|
-
|
|
10500
|
-
|
|
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
|
|
10503
|
-
|
|
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
|
|
10604
|
+
return JSON.parse(text);
|
|
10531
10605
|
} catch {
|
|
10532
|
-
return
|
|
10606
|
+
return;
|
|
10533
10607
|
}
|
|
10534
10608
|
}
|
|
10535
|
-
function
|
|
10536
|
-
const
|
|
10537
|
-
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
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
|
-
|
|
10546
|
-
if (
|
|
10547
|
-
const
|
|
10548
|
-
|
|
10549
|
-
|
|
10550
|
-
return
|
|
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
|
-
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
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
|
|
10563
|
-
|
|
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
|
|
10576
|
-
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
|
|
10580
|
-
|
|
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
|
|
10583
|
-
var
|
|
10584
|
-
|
|
10585
|
-
|
|
10586
|
-
|
|
10587
|
-
|
|
10588
|
-
const
|
|
10589
|
-
|
|
10590
|
-
|
|
10591
|
-
|
|
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
|
-
|
|
10595
|
-
|
|
10596
|
-
|
|
10597
|
-
|
|
10598
|
-
|
|
10599
|
-
|
|
10600
|
-
|
|
10601
|
-
|
|
10602
|
-
|
|
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
|
|
10605
|
-
|
|
10606
|
-
|
|
10607
|
-
|
|
10608
|
-
|
|
10609
|
-
|
|
10610
|
-
|
|
10611
|
-
|
|
10612
|
-
|
|
10613
|
-
|
|
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
|
-
|
|
10616
|
-
|
|
10617
|
-
|
|
10618
|
-
|
|
10619
|
-
|
|
10620
|
-
const
|
|
10621
|
-
|
|
10622
|
-
`);
|
|
10623
|
-
|
|
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
|
-
|
|
10663
|
-
|
|
10664
|
-
const
|
|
10665
|
-
if (
|
|
10666
|
-
|
|
10667
|
-
|
|
10668
|
-
|
|
10669
|
-
|
|
10670
|
-
|
|
10671
|
-
|
|
10672
|
-
|
|
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
|
|
10682
|
-
|
|
10683
|
-
|
|
10684
|
-
|
|
10685
|
-
|
|
10686
|
-
|
|
10687
|
-
|
|
10688
|
-
|
|
10689
|
-
|
|
10690
|
-
|
|
10691
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10724
|
-
|
|
10725
|
-
|
|
10726
|
-
|
|
10727
|
-
|
|
10728
|
-
|
|
10729
|
-
|
|
10730
|
-
|
|
10731
|
-
|
|
10732
|
-
|
|
10733
|
-
|
|
10734
|
-
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
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
|
-
|
|
10745
|
-
|
|
10746
|
-
const
|
|
10747
|
-
|
|
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
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
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
|
-
|
|
10778
|
-
|
|
10779
|
-
|
|
10780
|
-
|
|
10781
|
-
|
|
10782
|
-
const
|
|
10783
|
-
|
|
10784
|
-
|
|
10785
|
-
return
|
|
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 =
|
|
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
|
-
|
|
10793
|
-
|
|
10794
|
-
|
|
10795
|
-
|
|
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
|
-
|
|
10798
|
-
|
|
10799
|
-
|
|
10800
|
-
|
|
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
|
|
10803
|
-
const
|
|
10804
|
-
|
|
10805
|
-
|
|
10806
|
-
|
|
10807
|
-
|
|
10808
|
-
|
|
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 (!
|
|
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
|
-
|
|
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({
|
|
10857
|
-
|
|
10858
|
-
|
|
10859
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
10948
|
-
const profiles =
|
|
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
|
-
|
|
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
|
-
|
|
10981
|
-
|
|
10982
|
-
|
|
10983
|
-
|
|
10984
|
-
|
|
10985
|
-
|
|
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
|
|
11192
|
-
|
|
11193
|
-
|
|
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
|
-
|
|
11196
|
-
} catch {
|
|
11197
|
-
|
|
11206
|
+
flags = agentFlags(args);
|
|
11207
|
+
} catch (error) {
|
|
11208
|
+
fail(error.message);
|
|
11198
11209
|
}
|
|
11199
|
-
|
|
11200
|
-
|
|
11201
|
-
const
|
|
11202
|
-
|
|
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
|
-
|
|
11215
|
-
|
|
11216
|
-
|
|
11217
|
-
|
|
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
|
|
11220
|
-
|
|
11221
|
-
|
|
11222
|
-
|
|
11223
|
-
|
|
11224
|
-
|
|
11225
|
-
|
|
11226
|
-
|
|
11227
|
-
|
|
11228
|
-
|
|
11229
|
-
|
|
11230
|
-
|
|
11231
|
-
|
|
11232
|
-
|
|
11233
|
-
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
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
|
-
|
|
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 (
|
|
11263
|
-
return
|
|
11264
|
-
|
|
11265
|
-
|
|
11266
|
-
|
|
11267
|
-
|
|
11268
|
-
|
|
11269
|
-
|
|
11270
|
-
|
|
11271
|
-
|
|
11272
|
-
|
|
11273
|
-
|
|
11274
|
-
|
|
11275
|
-
|
|
11276
|
-
|
|
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
|
-
|
|
11280
|
-
|
|
11281
|
-
|
|
11282
|
-
|
|
11283
|
-
|
|
11284
|
-
|
|
11285
|
-
|
|
11286
|
-
|
|
11287
|
-
|
|
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
|
|
11297
|
-
|
|
11298
|
-
|
|
11299
|
-
|
|
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-
|
|
11419
|
+
const { runRollback } = await import("./runtime-update-baranjc5.mjs");
|
|
11314
11420
|
return runRollback(args);
|
|
11315
11421
|
}
|
|
11316
11422
|
}
|