@opencode/browse 0.0.3-dev.2 → 0.0.6-dev.18

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.
@@ -0,0 +1,731 @@
1
+ // src/protocol.ts
2
+ var extensionProtocolVersion = 3;
3
+ var minExtensionProtocolVersion = 3;
4
+ var unpackedExtensionId = "afeafocngkodbmaipcngoamamfmekgfo";
5
+ var unpackedReloadAdvice = "Reload Browse on your browser's extensions page (↻).";
6
+ var checkoutReloadPrefix = "Extension protocol incompatible: extension-older; reload ";
7
+ var extensionReconnectAlarmPeriodMs = 30000;
8
+ var legacyExtensionProtocolVersion = 1;
9
+ function extensionProtocolCompatibility(value) {
10
+ const legacy = value === undefined;
11
+ const valid = typeof value === "number" && Number.isSafeInteger(value) && value > 0;
12
+ const version = valid ? value : legacy ? legacyExtensionProtocolVersion : null;
13
+ return { version, compatible: version !== null && version >= minExtensionProtocolVersion && version <= extensionProtocolVersion, legacy };
14
+ }
15
+ function extensionProtocolSkew(version) {
16
+ if (typeof version !== "number")
17
+ return;
18
+ if (version > extensionProtocolVersion)
19
+ return "extension-newer";
20
+ if (version < minExtensionProtocolVersion)
21
+ return "extension-older";
22
+ return;
23
+ }
24
+ function extensionProtocolAdvice(version, commands, distribution) {
25
+ const skew = extensionProtocolSkew(version);
26
+ const versions = `extension protocol ${version ?? "unknown"}, this relay speaks ${minExtensionProtocolVersion === extensionProtocolVersion ? extensionProtocolVersion : `${minExtensionProtocolVersion} to ${extensionProtocolVersion}`}`;
27
+ if (skew === "extension-newer")
28
+ return `The Browse extension is newer than this browse CLI (${versions}). Update the CLI: run \`${commands.install}\` (an npm install with automatic updates also catches up by itself within a day), then restart opencode.`;
29
+ if (skew === "extension-older") {
30
+ const store = "The Chrome Web Store updates it within a few hours (to update now, open chrome://extensions, turn on Developer mode and click Update).";
31
+ const next = distribution === "unpacked" ? unpackedReloadAdvice : distribution === "store" ? store : `For an unpacked extension: ${unpackedReloadAdvice} For a store extension: ${store}`;
32
+ return `The Browse extension is older than this browse CLI (${versions}). ${next}`;
33
+ }
34
+ return `The Browse extension and this browse CLI don't speak the same protocol (${versions}). Run \`${commands.install}\` again and update the extension.`;
35
+ }
36
+ function makePageStatus(options) {
37
+ return {
38
+ state: options.state,
39
+ owner: options.targetOwner === "user" ? "user" : "session",
40
+ ...options.sessionId ? { sessionId: options.sessionId } : {},
41
+ ...options.sessionId && options.readOnly ? { readOnly: true } : {},
42
+ ...options.message ? { message: options.message } : {},
43
+ ...options.handoffId ? { handoffId: options.handoffId } : {},
44
+ ...options.recording ? { recording: true } : {}
45
+ };
46
+ }
47
+ var extensionCommandMethodValues = [
48
+ "ping",
49
+ "debugger.attach",
50
+ "debugger.detach",
51
+ "debugger.sendCommand",
52
+ "tabs.create",
53
+ "tabs.remove",
54
+ "tabs.group",
55
+ "tabs.ungroup",
56
+ "action.setAttached",
57
+ "action.setBadge",
58
+ "pageStatus.set",
59
+ "pageStatus.clear",
60
+ "runtime.reload",
61
+ "recording.start",
62
+ "recording.stop",
63
+ "recording.status",
64
+ "recording.cancel",
65
+ "tabs.cleanup",
66
+ "update.notice",
67
+ "relay.settings",
68
+ "settings.get",
69
+ "settings.set"
70
+ ];
71
+ var extensionEventMethodValues = [
72
+ "protocol",
73
+ "hello",
74
+ "auth.code",
75
+ "auth.response",
76
+ "ready",
77
+ "toolbar.clicked",
78
+ "handoff.completed",
79
+ "debugger.event",
80
+ "debugger.attached",
81
+ "debugger.detached",
82
+ "tabs.removed",
83
+ "tabs.opened",
84
+ "pong",
85
+ "log",
86
+ "recording.cancelled",
87
+ "pageStatus.requested",
88
+ "settings"
89
+ ];
90
+ function parseJsonObject(input) {
91
+ const parsed = JSON.parse(input);
92
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
93
+ throw new Error("Expected JSON object");
94
+ }
95
+ return parsed;
96
+ }
97
+ var extensionCommandMethods = new Set(extensionCommandMethodValues);
98
+ var extensionEventMethods = new Set(extensionEventMethodValues);
99
+ function isCdpRequest(input) {
100
+ return typeof input.id === "number" && typeof input.method === "string" && (input.params === undefined || isJsonObject(input.params)) && (input.sessionId === undefined || typeof input.sessionId === "string");
101
+ }
102
+ function isExtensionResponse(input) {
103
+ return typeof input.id === "number" && (input.result === undefined || isJsonObject(input.result)) && (input.error === undefined || typeof input.error === "string") && !(input.result !== undefined && input.error !== undefined);
104
+ }
105
+ function isExtensionEvent(input) {
106
+ return typeof input.method === "string" && extensionEventMethods.has(input.method) && (input.params === undefined || isJsonObject(input.params));
107
+ }
108
+ function isJsonObject(value) {
109
+ return value !== null && typeof value === "object" && !Array.isArray(value);
110
+ }
111
+
112
+ // src/paths.ts
113
+ import fs from "node:fs";
114
+ import os from "node:os";
115
+ import path from "node:path";
116
+ import { fileURLToPath } from "node:url";
117
+ function dataRoot(home = os.homedir()) {
118
+ const override = process.env.OPENCODE_BROWSER_HOME;
119
+ if (override)
120
+ return override;
121
+ if (process.platform === "win32")
122
+ return path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "opencode-browser");
123
+ const xdg = home === os.homedir() ? process.env.XDG_DATA_HOME : undefined;
124
+ return path.join(xdg ?? path.join(home, ".local", "share"), "opencode-browser");
125
+ }
126
+ var packageRoot = () => path.dirname(path.dirname(fileURLToPath(import.meta.url)));
127
+ var oldStoreMarker = () => path.join(dataRoot(), "old-store-extension");
128
+ function fromCheckout() {
129
+ return fs.existsSync(path.join(packageRoot(), "src", "main.ts"));
130
+ }
131
+ function checkoutRoot() {
132
+ return fromCheckout() ? path.dirname(packageRoot()) : undefined;
133
+ }
134
+ var quoted = (value) => /^[\w@%+=:,./-]+$/.test(value) ? value : JSON.stringify(value);
135
+ function cliCommand(args) {
136
+ if (fromCheckout())
137
+ return `node ${quoted(path.join(packageRoot(), "dist", "cli.mjs"))} ${args}`;
138
+ const shim = path.join(dataRoot(), "bin", "opencode-browser.mjs");
139
+ const versions = path.join(dataRoot(), "runtime", "versions");
140
+ if (!path.relative(versions, packageRoot()).startsWith("..") && fs.existsSync(shim))
141
+ return `node ${quoted(shim)} ${args}`;
142
+ return `npx @opencode/browse ${args}`;
143
+ }
144
+ var STORE_ITEM_ID = /^[a-p]{32}$/;
145
+ function storeConfig(env = process.env, baked = { id: "mfnicocicmmlkpjnaffgihfjhdgjkdjg" }) {
146
+ const id = [env.OPENCODE_BROWSER_STORE_ID, baked?.id].find((value) => value && STORE_ITEM_ID.test(value));
147
+ return id ? { id } : {};
148
+ }
149
+ var RELEASE_SERVICE = "https://anoma.ly/release";
150
+ function devExtension(baked = { file: "browse-extension-0.0.6-dev.18.zip", sha256: "2f8f69b9022f9835f564f96ccc5fd07c48ad78e95488eeb2a83f253d8fafbb49", size: 38413 }) {
151
+ return baked && /^[0-9a-f]{64}$/.test(baked.sha256) && /^[\w.-]+\.zip$/.test(baked.file) ? baked : undefined;
152
+ }
153
+ var devExtensionUrl = (extension, env = process.env) => `${(env.OPENCODE_BROWSER_RELEASE_SERVICE || RELEASE_SERVICE).replace(/\/+$/, "")}/browse/files/${extension.sha256}/${extension.file}`;
154
+ var devExtensionFolder = (root = dataRoot()) => path.join(root, "extension");
155
+ var storeUrl = (id) => `https://chromewebstore.google.com/detail/${id}`;
156
+ function installCommand() {
157
+ return fromCheckout() ? `cd ${quoted(path.dirname(packageRoot()))} && bun run setup` : "npx @opencode/browse install";
158
+ }
159
+
160
+ // src/auto-update.ts
161
+ import { spawn } from "node:child_process";
162
+ import { chmodSync, closeSync, cpSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
163
+ import { createRequire } from "node:module";
164
+ import path2 from "node:path";
165
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
166
+ var PACKAGE_NAME = "@opencode/browse";
167
+ var DAY_MS = 24 * 60 * 60 * 1000;
168
+ var DEFAULT_REGISTRY = "https://registry.npmjs.org";
169
+ var autoUpdateModes = ["on", "notify", "off"];
170
+ function runtimeLayout(root = dataRoot()) {
171
+ const runtime = path2.join(root, "runtime");
172
+ return {
173
+ root,
174
+ runtime,
175
+ versions: path2.join(runtime, "versions"),
176
+ current: path2.join(runtime, "current.json"),
177
+ shim: path2.join(root, "bin", "opencode-browser.mjs"),
178
+ state: path2.join(root, "auto-update.json")
179
+ };
180
+ }
181
+ var versionDirectory = (layout, version) => path2.join(layout.versions, version);
182
+ var versionEntry = (layout, version) => path2.join(versionDirectory(layout, version), "node_modules", "@opencode", "browse", "dist", "cli.mjs");
183
+ var channelOf = (version) => version.includes("-dev.") ? "dev" : "latest";
184
+ var UPDATE_VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-dev\.(\d+)(?:\.(\d+))?)?$/;
185
+ function compareVersions(left, right) {
186
+ const parse = (value) => {
187
+ const match = UPDATE_VERSION.exec(value);
188
+ if (!match)
189
+ return;
190
+ const release = match[4] === undefined ? 1 : 0;
191
+ return [Number(match[1]), Number(match[2]), Number(match[3]), release, Number(match[4] ?? 0), Number(match[5] ?? 0)];
192
+ };
193
+ const a = parse(left);
194
+ const b = parse(right);
195
+ if (!a || !b)
196
+ return 0;
197
+ for (let index = 0;index < a.length; index++)
198
+ if (a[index] !== b[index])
199
+ return a[index] < b[index] ? -1 : 1;
200
+ return 0;
201
+ }
202
+ function readJson(file) {
203
+ try {
204
+ return JSON.parse(readFileSync(file, "utf8"));
205
+ } catch {
206
+ return;
207
+ }
208
+ }
209
+ function writeJsonAtomic(file, value) {
210
+ mkdirSync(path2.dirname(file), { recursive: true });
211
+ const temporary = `${file}.${process.pid}.tmp`;
212
+ writeFileSync(temporary, JSON.stringify(value, null, 2) + `
213
+ `, { mode: 384 });
214
+ renameSync(temporary, file);
215
+ }
216
+ function readCurrent(layout = runtimeLayout()) {
217
+ const current = readJson(layout.current);
218
+ return current && typeof current.version === "string" ? current : undefined;
219
+ }
220
+ function writeCurrent(layout, current) {
221
+ writeJsonAtomic(layout.current, { ...current, switchedAt: new Date().toISOString() });
222
+ }
223
+ function readState(layout = runtimeLayout()) {
224
+ return readJson(layout.state) ?? {};
225
+ }
226
+ function updateState(layout, change) {
227
+ const next = change(readState(layout));
228
+ writeJsonAtomic(layout.state, next);
229
+ return next;
230
+ }
231
+ var realpath = (file) => {
232
+ try {
233
+ return realpathSync(file);
234
+ } catch {
235
+ return path2.resolve(file);
236
+ }
237
+ };
238
+ function npmRuntime(options = {}) {
239
+ if (options.checkout ?? fromCheckout())
240
+ return;
241
+ const layout = options.layout ?? runtimeLayout();
242
+ const defaultEntry = () => fileURLToPath2(import.meta.url.endsWith(".ts") ? import.meta.url : new URL("./cli.mjs", import.meta.url));
243
+ const entry = realpath(options.entry ?? defaultEntry());
244
+ const relative = path2.relative(realpath(layout.versions), entry).split(path2.sep);
245
+ if (relative.length !== 6 || relative[0] === ".." || relative.slice(1).join("/") !== "node_modules/@opencode/browse/dist/cli.mjs")
246
+ return;
247
+ const current = readCurrent(layout);
248
+ if (!current)
249
+ return;
250
+ return { layout, version: relative[0], current };
251
+ }
252
+ function autoUpdateOverride(env = process.env) {
253
+ const value = env.OPENCODE_BROWSER_AUTO_UPDATE?.trim().toLowerCase();
254
+ if (value === "on" || value === "1" || value === "true")
255
+ return "on";
256
+ if (value === "notify")
257
+ return "notify";
258
+ if (value === "off" || value === "0" || value === "false")
259
+ return "off";
260
+ if (env.OPENCODE_BROWSER_UPDATE_CHECK === "0")
261
+ return "off";
262
+ return;
263
+ }
264
+ function storedMode(layout = runtimeLayout()) {
265
+ const mode = readState(layout).mode;
266
+ return mode && autoUpdateModes.includes(mode) ? mode : "on";
267
+ }
268
+ function effectiveMode(layout = runtimeLayout(), env = process.env) {
269
+ return autoUpdateOverride(env) ?? storedMode(layout);
270
+ }
271
+ function setStoredMode(mode, layout = runtimeLayout()) {
272
+ updateState(layout, (state) => ({ ...state, mode }));
273
+ }
274
+ function autoUpdateStatus(options = {}) {
275
+ const layout = options.layout ?? runtimeLayout();
276
+ const env = options.env ?? process.env;
277
+ const runtime = options.runtime === undefined ? npmRuntime({ layout }) : options.runtime;
278
+ const state = readState(layout);
279
+ const override = autoUpdateOverride(env);
280
+ const current = runtime?.current ?? readCurrent(layout);
281
+ const install = runtime ? "npm" : fromCheckout() ? "checkout" : current ? "npm" : "other";
282
+ return {
283
+ install,
284
+ mode: storedMode(layout),
285
+ effective: install === "npm" ? override ?? storedMode(layout) : "off",
286
+ ...override ? { overriddenBy: env.OPENCODE_BROWSER_AUTO_UPDATE !== undefined ? "OPENCODE_BROWSER_AUTO_UPDATE" : "OPENCODE_BROWSER_UPDATE_CHECK" } : {},
287
+ ...current ? { version: current.version, channel: current.channel } : {},
288
+ ...current?.previous && existsSync(versionEntry(layout, current.previous)) ? { previous: current.previous } : {},
289
+ ...state.latest ? { latest: state.latest } : {},
290
+ ...state.pending ? { pending: state.pending } : {},
291
+ ...state.skipped ? { skipped: state.skipped } : {},
292
+ ...state.checkedAt ? { checkedAt: new Date(state.checkedAt).toISOString() } : {},
293
+ ...state.lastUpdate ? { lastUpdate: state.lastUpdate } : {},
294
+ ...state.lastError ? { lastError: state.lastError } : {}
295
+ };
296
+ }
297
+ function describeAutoUpdate(status) {
298
+ if (status.install === "checkout")
299
+ return "Updates: checkout install; `update` pulls new commits";
300
+ if (status.install !== "npm")
301
+ return "Updates: not an installed runtime";
302
+ const mode = { on: "automatic", notify: "notify only", off: "off" }[status.effective];
303
+ const parts = [`${mode}${status.overriddenBy ? ` (set by ${status.overriddenBy})` : ""}`, `channel ${status.channel ?? "latest"}`];
304
+ if (status.pending)
305
+ parts.push(`${status.pending} installed, switches in when no agent is using the browser`);
306
+ else if (status.latest && status.latest === status.skipped)
307
+ parts.push(`skipping ${status.latest} (rolled back from it; \`update\` installs it again)`);
308
+ else if (status.latest && status.version && compareVersions(status.latest, status.version) > 0)
309
+ parts.push(`${status.latest} available`);
310
+ if (status.previous && status.version && compareVersions(status.previous, status.version) < 0)
311
+ parts.push(`\`rollback\` returns to ${status.previous}`);
312
+ if (status.lastError)
313
+ parts.push(`last attempt failed: ${status.lastError.message}`);
314
+ return `Updates: ${parts.join("; ")}`;
315
+ }
316
+ var runCommand = (command, args, options = {}) => new Promise((resolve) => {
317
+ const npmCli = process.platform === "win32" && command === "npm" ? path2.join(path2.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js") : undefined;
318
+ const [file, argv] = npmCli && existsSync(npmCli) ? [process.execPath, [npmCli, ...args]] : [command, args];
319
+ const child = spawn(file, argv, { env: options.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
320
+ let stdout = "";
321
+ let stderr = "";
322
+ child.stdout.setEncoding("utf8").on("data", (chunk) => void (stdout += chunk));
323
+ child.stderr.setEncoding("utf8").on("data", (chunk) => void (stderr += chunk));
324
+ const timer = setTimeout(() => child.kill(), options.timeoutMs ?? 120000);
325
+ child.on("error", (error) => {
326
+ clearTimeout(timer);
327
+ resolve({ status: null, stdout, stderr: stderr + error.message });
328
+ });
329
+ child.on("close", (status) => {
330
+ clearTimeout(timer);
331
+ resolve({ status, stdout, stderr });
332
+ });
333
+ });
334
+ async function registryUrl(run = runCommand, env = process.env) {
335
+ const override = env.OPENCODE_BROWSER_NPM_REGISTRY?.trim();
336
+ const configured = override || (await run("npm", ["config", "get", "registry"], { timeoutMs: 15000 })).stdout.trim();
337
+ return /^https?:\/\//.test(configured) ? configured.replace(/\/+$/, "") : DEFAULT_REGISTRY;
338
+ }
339
+ async function fetchPublished(options) {
340
+ const request = options.fetch ?? fetch;
341
+ const response = await request(`${options.registry}/${PACKAGE_NAME.replace("/", "%2f")}`, {
342
+ headers: { accept: "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8" },
343
+ signal: AbortSignal.timeout(20000)
344
+ });
345
+ if (!response.ok)
346
+ throw new Error(`the registry answered ${response.status} for ${PACKAGE_NAME}`);
347
+ const body = await response.json();
348
+ const version = body["dist-tags"]?.[options.channel];
349
+ if (!version)
350
+ throw new Error(`${PACKAGE_NAME} has no ${options.channel} dist-tag`);
351
+ const integrity = body.versions?.[version]?.dist?.integrity;
352
+ if (!integrity || !/^sha512-/.test(integrity))
353
+ throw new Error(`${PACKAGE_NAME}@${version} has no sha512 integrity in the registry`);
354
+ return { version, integrity };
355
+ }
356
+ function copyRunningPackage(source, prefix) {
357
+ const modules = path2.dirname(path2.dirname(source));
358
+ const seen = new Set;
359
+ const copy = (dir) => {
360
+ if (seen.has(dir))
361
+ return;
362
+ seen.add(dir);
363
+ const target = path2.join(prefix, "node_modules", path2.relative(modules, dir));
364
+ cpSync(dir, target, { recursive: true, dereference: true, filter: (file) => path2.basename(file) !== "node_modules" });
365
+ const manifest = path2.join(dir, "package.json");
366
+ const pkg = JSON.parse(readFileSync(manifest, "utf8"));
367
+ const require2 = createRequire(manifest);
368
+ for (const name of Object.keys({ ...pkg.dependencies, ...pkg.peerDependencies, ...pkg.optionalDependencies })) {
369
+ const dependency = require2.resolve.paths(name)?.map((parent) => path2.join(parent, name)).find((candidate) => candidate.startsWith(modules + path2.sep) && existsSync(path2.join(candidate, "package.json")));
370
+ if (dependency)
371
+ copy(dependency);
372
+ else if (!pkg.optionalDependencies?.[name] && !pkg.peerDependenciesMeta?.[name]?.optional)
373
+ throw new Error(`Missing installed dependency ${name} of ${pkg.name}`);
374
+ }
375
+ };
376
+ copy(source);
377
+ }
378
+ async function installVersion(options) {
379
+ const run = options.run ?? runCommand;
380
+ const { layout } = options;
381
+ const spec = options.spec ?? `${PACKAGE_NAME}@${options.version}`;
382
+ if (spec.startsWith("-") || /[\s"'`&|<>^%!;]/.test(spec))
383
+ throw new Error(`Invalid package spec ${JSON.stringify(spec)}`);
384
+ mkdirSync(layout.versions, { recursive: true });
385
+ const temporary = path2.join(layout.versions, `.install-${process.pid}-${Date.now()}`);
386
+ rmSync(temporary, { recursive: true, force: true });
387
+ mkdirSync(temporary, { recursive: true });
388
+ try {
389
+ writeFileSync(path2.join(temporary, "package.json"), `{"private":true}
390
+ `);
391
+ if (options.runningPackage)
392
+ copyRunningPackage(options.runningPackage, temporary);
393
+ else {
394
+ const args = ["install", "--prefix", temporary, "--no-audit", "--no-fund", "--ignore-scripts", "--loglevel=error", ...options.registry ? [`--registry=${options.registry}`] : [], spec];
395
+ const npm = await run("npm", args, { timeoutMs: 10 * 60000 });
396
+ if (npm.status !== 0)
397
+ throw new Error(`npm install ${spec} failed: ${(npm.stderr || npm.stdout).trim().split(`
398
+ `).slice(-3).join(" ")}`);
399
+ }
400
+ const installed = readJson(path2.join(temporary, "node_modules", "@opencode", "browse", "package.json"))?.version;
401
+ if (!installed)
402
+ throw new Error(`npm install ${spec} didn't install ${PACKAGE_NAME}`);
403
+ if (options.spec === undefined && installed !== options.version)
404
+ throw new Error(`npm installed ${installed}, not ${options.version}`);
405
+ if (options.integrity) {
406
+ const locked = readJson(path2.join(temporary, "package-lock.json"))?.packages?.["node_modules/@opencode/browse"];
407
+ if (locked?.integrity !== options.integrity)
408
+ throw new Error(`${PACKAGE_NAME}@${installed} installed with integrity ${locked?.integrity ?? "unknown"}, not the registry's ${options.integrity}`);
409
+ }
410
+ const entry = path2.join(temporary, "node_modules", "@opencode", "browse", "dist", "cli.mjs");
411
+ const reported = await run(options.node ?? process.execPath, [entry, "--version"], { timeoutMs: 30000, env: { ...process.env, OPENCODE_BROWSER_AUTOSTART: "0" } });
412
+ if (reported.status !== 0 || reported.stdout.trim() !== installed)
413
+ throw new Error(`${PACKAGE_NAME}@${installed} didn't run: \`--version\` printed ${JSON.stringify(reported.stdout.trim() || reported.stderr.trim()).slice(0, 200)}`);
414
+ const target = versionDirectory(layout, installed);
415
+ rmSync(target, { recursive: true, force: true });
416
+ renameSync(temporary, target);
417
+ return versionEntry(layout, installed);
418
+ } finally {
419
+ rmSync(temporary, { recursive: true, force: true });
420
+ }
421
+ }
422
+ function processAlive(pid) {
423
+ if (!Number.isSafeInteger(pid) || pid <= 0)
424
+ return false;
425
+ try {
426
+ process.kill(pid, 0);
427
+ return true;
428
+ } catch (error) {
429
+ return error.code === "EPERM";
430
+ }
431
+ }
432
+ function pruneVersions(layout, keep = []) {
433
+ let names = [];
434
+ try {
435
+ names = readdirSync(layout.versions);
436
+ } catch {
437
+ return;
438
+ }
439
+ const current = readCurrent(layout);
440
+ const kept = new Set([...keep, current?.version, current?.previous, readState(layout).pending].filter((name) => !!name));
441
+ for (const name of names) {
442
+ if (kept.has(name))
443
+ continue;
444
+ const temporary = /^\.install-(\d+)-\d+$/.exec(name);
445
+ if (temporary && processAlive(Number(temporary[1])))
446
+ continue;
447
+ rmSync(path2.join(layout.versions, name), { recursive: true, force: true });
448
+ }
449
+ }
450
+ function validatePending(layout) {
451
+ const state = readState(layout);
452
+ if (!state.pending || existsSync(versionEntry(layout, state.pending)))
453
+ return state;
454
+ return updateState(layout, ({ pending: _pending, ...rest }) => rest);
455
+ }
456
+ function switchVersion(layout, version, previous) {
457
+ if (!existsSync(versionEntry(layout, version)))
458
+ throw new Error(`${version} isn't installed in ${layout.versions}`);
459
+ writeCurrent(layout, { version, ...previous && previous !== version ? { previous } : {}, channel: channelOf(version) });
460
+ writeShim(layout);
461
+ }
462
+ function shimSource() {
463
+ return `#!/usr/bin/env node
464
+ // Browse's stable entry, written by install (cli/src/auto-update.ts). opencode's browse MCP server runs this
465
+ // file, so its configuration never changes when the runtime updates. Don't edit; install rewrites it.
466
+ import { spawn } from "node:child_process"
467
+ import { readFileSync } from "node:fs"
468
+ import path from "node:path"
469
+ import { createInterface } from "node:readline"
470
+ import { fileURLToPath, pathToFileURL } from "node:url"
471
+
472
+ const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)))
473
+ const runtime = path.join(root, "runtime")
474
+ const current = () => JSON.parse(readFileSync(path.join(runtime, "current.json"), "utf8")).version
475
+ const entry = (version) => path.join(runtime, "versions", version, "node_modules", "@opencode", "browse", "dist", "cli.mjs")
476
+ process.env.OPENCODE_BROWSER_HOME ??= root
477
+ const relayPort = Number(process.env.OPENCODE_BROWSER_PORT) || 19988
478
+
479
+ if (process.argv[2] !== "mcp") {
480
+ process.argv[1] = entry(current())
481
+ await import(pathToFileURL(entry(current())).href)
482
+ } else {
483
+ const REPLAY = "opencode-browser-shim-initialize"
484
+ // Every server this supervisor starts keeps one implicit session, and the relay watches this process (not the
485
+ // server it replaces), so an update doesn't end the agent's session or close its tabs.
486
+ process.env.OPENCODE_BROWSER_MCP_SESSION ??= "mcp-" + Math.random().toString(16).slice(2, 10).padEnd(8, "0")
487
+ process.env.OPENCODE_BROWSER_CLIENT_PID = String(process.pid)
488
+ let version = current()
489
+ let child
490
+ let initialize
491
+ let initialized
492
+ let swapping = false
493
+ let listChanged = false
494
+ const inflight = new Set()
495
+ const queue = []
496
+ const toClient = (line) => process.stdout.write(line + "\\n")
497
+ const toChild = (line) => child.stdin.write(line + "\\n")
498
+ const parse = (line) => { try { return JSON.parse(line) } catch { return undefined } }
499
+
500
+ const start = (replay) => {
501
+ const next = spawn(process.execPath, [entry(version), "mcp"], { stdio: ["pipe", "pipe", "inherit"], env: process.env })
502
+ // A server that exits mid-write (an old one being replaced) must not take the supervisor down with EPIPE.
503
+ next.stdin.on("error", () => {})
504
+ child = next
505
+ next.on("exit", (code, signal) => {
506
+ if (next !== child) return
507
+ process.exit(code ?? (signal ? 1 : 0))
508
+ })
509
+ createInterface({ input: next.stdout }).on("line", (line) => {
510
+ if (next !== child) return
511
+ const message = parse(line)
512
+ if (message && message.id === REPLAY) {
513
+ if (initialized) toChild(initialized)
514
+ swapping = false
515
+ if (listChanged) toClient(JSON.stringify({ jsonrpc: "2.0", method: "notifications/tools/list_changed" }))
516
+ for (const queued of queue.splice(0)) fromClient(queued)
517
+ return
518
+ }
519
+ if (message && message.id !== undefined && message.method === undefined) {
520
+ inflight.delete(message.id)
521
+ if (message.result && message.result.capabilities && message.result.capabilities.tools) listChanged = message.result.capabilities.tools.listChanged === true
522
+ }
523
+ toClient(line)
524
+ void maybeSwap()
525
+ })
526
+ if (replay && initialize) {
527
+ swapping = true
528
+ toChild(JSON.stringify({ ...initialize, id: REPLAY }))
529
+ }
530
+ }
531
+
532
+ // Once current.json names another version, new requests wait for the swap instead of reaching a server whose relay is
533
+ // going away (or already runs the other version); a switch that doesn't happen releases them.
534
+ let heldSince
535
+ const moved = () => { try { return current() !== version } catch { return false } }
536
+ const fromClient = (line) => {
537
+ if (swapping) return void queue.push(line)
538
+ if (moved()) {
539
+ heldSince ??= Date.now()
540
+ queue.push(line)
541
+ return void maybeSwap()
542
+ }
543
+ const message = parse(line)
544
+ if (message && message.method === "initialize") initialize = message
545
+ if (message && message.method === "notifications/initialized") initialized = line
546
+ if (message && message.id !== undefined && message.method !== undefined) inflight.add(message.id)
547
+ toChild(line)
548
+ }
549
+
550
+ // A new version in current.json swaps the server only once the relay runs it too (GET /version), so a server never
551
+ // talks to a relay of another version. With no relay at all for a few checks, nothing can mix, and it swaps.
552
+ let checking = false
553
+ let unreachable = 0
554
+ const relayVersion = () => fetch("http://127.0.0.1:" + relayPort + "/version", { signal: AbortSignal.timeout(2000) })
555
+ .then((response) => (response.ok ? response.json() : undefined))
556
+ .then((body) => (body && typeof body.version === "string" ? body.version : undefined), () => undefined)
557
+ const maybeSwap = async () => {
558
+ if (swapping || checking || inflight.size > 0) return
559
+ let next
560
+ try { next = current() } catch { return }
561
+ if (next === version) {
562
+ unreachable = 0
563
+ heldSince = undefined
564
+ for (const queued of queue.splice(0)) fromClient(queued)
565
+ return
566
+ }
567
+ checking = true
568
+ const running = await relayVersion()
569
+ checking = false
570
+ unreachable = running === undefined ? unreachable + 1 : 0
571
+ // Held requests don't wait forever for a relay that never reports the new version: they go to the current server.
572
+ if (running !== next && unreachable < 3) {
573
+ if (heldSince !== undefined && Date.now() - heldSince > 15000) {
574
+ heldSince = undefined
575
+ const released = queue.splice(0)
576
+ for (const line of released) { const message = parse(line); if (message && message.id !== undefined && message.method !== undefined) inflight.add(message.id); toChild(line) }
577
+ }
578
+ return
579
+ }
580
+ if (swapping || inflight.size > 0 || current() !== next) return
581
+ const old = child
582
+ version = next
583
+ heldSince = undefined
584
+ start(true)
585
+ old.stdin.end()
586
+ setTimeout(() => old.kill(), 5000).unref()
587
+ }
588
+
589
+ start(false)
590
+ createInterface({ input: process.stdin }).on("line", fromClient).on("close", () => {
591
+ child.stdin.end()
592
+ setTimeout(() => process.exit(0), 5000).unref()
593
+ })
594
+ setInterval(() => void maybeSwap(), 1000).unref()
595
+ }
596
+ `;
597
+ }
598
+ function writeShim(layout) {
599
+ mkdirSync(path2.dirname(layout.shim), { recursive: true });
600
+ const source = shimSource();
601
+ try {
602
+ if (readFileSync(layout.shim, "utf8") === source)
603
+ return;
604
+ } catch {}
605
+ const temporary = `${layout.shim}.${process.pid}.tmp`;
606
+ writeFileSync(temporary, source);
607
+ chmodSync(temporary, 493);
608
+ renameSync(temporary, layout.shim);
609
+ }
610
+ var INSTALL_TIMEOUT_MS = 15 * 60000;
611
+ var SWITCH_TIMEOUT_MS = 5 * 60000;
612
+ function createAutoUpdater(deps) {
613
+ const env = deps.env ?? process.env;
614
+ const now = deps.now ?? Date.now;
615
+ const { layout, version: running } = deps.runtime;
616
+ const checkIntervalMs = Number(env.OPENCODE_BROWSER_AUTO_UPDATE_INTERVAL_MS) || DAY_MS;
617
+ const spawnChild = deps.spawnChild ?? ((args, onExit) => spawnRuntimeChild(layout, versionEntry(layout, running), args, onExit, env));
618
+ let busy = false;
619
+ let switching;
620
+ let timer;
621
+ const settled = readState(layout);
622
+ if (settled.pending === running) {
623
+ updateState(layout, ({ pending: _pending, lastError: _error, ...state }) => ({ ...state, lastUpdate: { from: deps.runtime.current.previous ?? "unknown", to: running, at: new Date(now()).toISOString() } }));
624
+ deps.log("autoupdate.switched", { version: running, previous: deps.runtime.current.previous });
625
+ pruneVersions(layout, [running]);
626
+ }
627
+ async function tick() {
628
+ if (switching && (now() - switching.startedAt > SWITCH_TIMEOUT_MS || switching.pid !== undefined && !processAlive(switching.pid))) {
629
+ deps.log("autoupdate.switch_abandoned", { pid: switching.pid });
630
+ switching = undefined;
631
+ }
632
+ if (busy || switching)
633
+ return;
634
+ busy = true;
635
+ try {
636
+ const mode = effectiveMode(layout, env);
637
+ if (mode === "off")
638
+ return;
639
+ let state = validatePending(layout);
640
+ if (!state.checkedAt || now() - state.checkedAt >= checkIntervalMs || now() < state.checkedAt) {
641
+ const channel = channelOf(running);
642
+ try {
643
+ const published = await fetchPublished({ registry: await registryUrl(deps.run, env), channel, ...deps.fetch ? { fetch: deps.fetch } : {} });
644
+ state = updateState(layout, ({ failed: _failed, ...rest }) => ({ ...rest, checkedAt: now(), latest: published.version, integrity: published.integrity }));
645
+ deps.log("autoupdate.checked", { channel, latest: published.version, running });
646
+ } catch (error) {
647
+ state = updateState(layout, (rest) => ({ ...rest, checkedAt: now(), failed: error.message }));
648
+ deps.log("autoupdate.check_failed", { error: error.message });
649
+ }
650
+ deps.onChange?.();
651
+ }
652
+ const latest = state.latest;
653
+ const wanted = latest && compareVersions(latest, running) > 0 && latest !== state.skipped ? latest : undefined;
654
+ if (!wanted || mode !== "on")
655
+ return;
656
+ const installing = state.installing;
657
+ if (installing) {
658
+ if (processAlive(installing.pid) && now() - installing.startedAt < INSTALL_TIMEOUT_MS)
659
+ return;
660
+ const done = existsSync(versionEntry(layout, installing.version));
661
+ state = updateState(layout, ({ installing: _installing, ...rest }) => ({
662
+ ...rest,
663
+ ...done ? { pending: installing.version } : rest.pending === installing.version ? {} : { lastError: { message: "the installer stopped without finishing", at: new Date(now()).toISOString(), version: installing.version } }
664
+ }));
665
+ deps.log("autoupdate.installer_gone", { version: installing.version });
666
+ }
667
+ if (state.pending !== wanted) {
668
+ if (state.lastError?.version === wanted && now() - Date.parse(state.lastError.at) < checkIntervalMs)
669
+ return;
670
+ deps.log("autoupdate.installing", { version: wanted });
671
+ const pid = spawnChild(["runtime-install", wanted, ...state.integrity ? ["--integrity", state.integrity] : []], (code) => {
672
+ deps.log(code === 0 ? "autoupdate.installed" : "autoupdate.install_failed", { version: wanted, code });
673
+ deps.onChange?.();
674
+ if (code === 0)
675
+ tick();
676
+ });
677
+ if (pid)
678
+ updateState(layout, (rest) => ({ ...rest, installing: { version: wanted, pid, startedAt: now() } }));
679
+ return;
680
+ }
681
+ if (!deps.isIdle())
682
+ return;
683
+ const current = readCurrent(layout);
684
+ if (!current || current.version !== running)
685
+ return;
686
+ const attempt = { pid: undefined, startedAt: now() };
687
+ switching = attempt;
688
+ deps.log("autoupdate.switching", { from: running, to: wanted });
689
+ attempt.pid = spawnChild(["runtime-switch", wanted], (code) => {
690
+ if (switching === attempt)
691
+ switching = undefined;
692
+ if (code !== 0)
693
+ deps.log("autoupdate.restart_refused", { version: wanted, code });
694
+ });
695
+ } catch (error) {
696
+ deps.log("autoupdate.error", { error: error.message });
697
+ } finally {
698
+ busy = false;
699
+ }
700
+ }
701
+ return {
702
+ tick,
703
+ start(intervalMs = Number(env.OPENCODE_BROWSER_AUTO_UPDATE_TICK_MS) || 10 * 60000) {
704
+ timer = setInterval(() => void tick(), intervalMs);
705
+ timer.unref();
706
+ setTimeout(() => void tick(), Math.min(intervalMs, 60000)).unref();
707
+ },
708
+ stop() {
709
+ clearInterval(timer);
710
+ }
711
+ };
712
+ }
713
+ function spawnRuntimeChild(layout, entry, args, onExit, env = process.env) {
714
+ const { OPENCODE_BROWSER_MANAGED_RELAY: _managed, OPENCODE_BROWSER_RESTART_REQUEST_ID: _request, ...rest } = env;
715
+ mkdirSync(path2.join(layout.root, "logs"), { recursive: true });
716
+ const log = openSync(path2.join(layout.root, "logs", "auto-update.log"), "a", 384);
717
+ try {
718
+ const child = spawn(process.execPath, [entry, ...args], { detached: true, stdio: ["ignore", log, log], env: { ...rest, OPENCODE_BROWSER_HOME: layout.root } });
719
+ child.on("error", () => onExit(null));
720
+ child.on("exit", (code) => onExit(code));
721
+ child.unref();
722
+ return child.pid;
723
+ } catch {
724
+ onExit(null);
725
+ return;
726
+ } finally {
727
+ closeSync(log);
728
+ }
729
+ }
730
+
731
+ export { extensionProtocolVersion, minExtensionProtocolVersion, unpackedExtensionId, unpackedReloadAdvice, checkoutReloadPrefix, extensionReconnectAlarmPeriodMs, extensionProtocolCompatibility, extensionProtocolSkew, extensionProtocolAdvice, makePageStatus, parseJsonObject, isCdpRequest, isExtensionResponse, isExtensionEvent, isJsonObject, dataRoot, oldStoreMarker, fromCheckout, checkoutRoot, cliCommand, storeConfig, devExtension, devExtensionUrl, devExtensionFolder, storeUrl, installCommand, autoUpdateModes, runtimeLayout, versionEntry, channelOf, UPDATE_VERSION, compareVersions, readCurrent, writeCurrent, updateState, npmRuntime, autoUpdateOverride, storedMode, effectiveMode, setStoredMode, autoUpdateStatus, describeAutoUpdate, registryUrl, fetchPublished, installVersion, pruneVersions, switchVersion, createAutoUpdater };