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

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,72 @@
1
+ import {
2
+ dataRoot
3
+ } from "./main-g5mke6rn.mjs";
4
+
5
+ // src/relay-secrets.ts
6
+ import crypto from "node:crypto";
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ var relayTokenQueryParam = "opencodeBrowserToken";
10
+ var tokenPattern = /^[A-Za-z0-9_-]{43}$/;
11
+ function relayTokenPath(root = dataRoot()) {
12
+ return path.join(root, "relay-token");
13
+ }
14
+ function relayToken(root = dataRoot()) {
15
+ return readOrCreateSecret(relayTokenPath(root), () => crypto.randomBytes(32).toString("base64url"), (value) => tokenPattern.test(value));
16
+ }
17
+ function relayAuthorization(root = dataRoot()) {
18
+ return `Bearer ${relayToken(root)}`;
19
+ }
20
+ function secretsEqual(expected, actual) {
21
+ if (actual === undefined)
22
+ return false;
23
+ const a = crypto.createHash("sha256").update(expected).digest();
24
+ const b = crypto.createHash("sha256").update(actual).digest();
25
+ return crypto.timingSafeEqual(a, b) && expected.length === actual.length;
26
+ }
27
+ function readOrCreateSecret(file, generate, valid) {
28
+ const existing = readSecret(file);
29
+ if (existing !== undefined && valid(existing))
30
+ return existing;
31
+ ensurePrivateDirectory(path.dirname(file));
32
+ const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
33
+ fs.writeFileSync(temporary, `${generate()}
34
+ `, { mode: 384 });
35
+ try {
36
+ if (existing === undefined) {
37
+ try {
38
+ fs.linkSync(temporary, file);
39
+ } catch (error) {
40
+ if (error.code !== "EEXIST")
41
+ throw error;
42
+ }
43
+ } else {
44
+ fs.renameSync(temporary, file);
45
+ }
46
+ } finally {
47
+ fs.rmSync(temporary, { force: true });
48
+ }
49
+ const created = readSecret(file);
50
+ if (created === undefined || !valid(created))
51
+ throw new Error(`Could not create ${file}`);
52
+ return created;
53
+ }
54
+ function readSecret(file) {
55
+ try {
56
+ const stat = fs.statSync(file);
57
+ if (process.platform !== "win32" && (stat.mode & 63) !== 0)
58
+ fs.chmodSync(file, 384);
59
+ return fs.readFileSync(file, "utf8").trim();
60
+ } catch (error) {
61
+ if (error.code === "ENOENT")
62
+ return;
63
+ throw error;
64
+ }
65
+ }
66
+ function ensurePrivateDirectory(directory = dataRoot()) {
67
+ fs.mkdirSync(directory, { recursive: true, mode: 448 });
68
+ if (process.platform !== "win32")
69
+ fs.chmodSync(directory, 448);
70
+ }
71
+
72
+ export { relayTokenQueryParam, relayToken, relayAuthorization, secretsEqual, ensurePrivateDirectory };
@@ -0,0 +1,334 @@
1
+ import {
2
+ unpackedReloadAdvice,
3
+ dataRoot,
4
+ checkoutRoot,
5
+ cliCommand,
6
+ compareVersions,
7
+ npmRuntime,
8
+ autoUpdateStatus
9
+ } from "./main-g5mke6rn.mjs";
10
+
11
+ // src/update.ts
12
+ import { execFile, spawnSync } from "node:child_process";
13
+ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
14
+ import path from "node:path";
15
+ var UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
16
+ var FAILED_CHECK_INTERVAL_MS = 60 * 60 * 1000;
17
+ var CACHE_FILE = "update-check.json";
18
+ var quietGitEnv = () => ({
19
+ ...process.env,
20
+ GIT_TERMINAL_PROMPT: "0",
21
+ GIT_SSH_COMMAND: process.env.GIT_SSH_COMMAND ?? "ssh -o BatchMode=yes"
22
+ });
23
+ var gitIn = (repo) => (args, timeoutMs = 5000) => new Promise((resolve) => {
24
+ execFile("git", ["-C", repo, ...args], { timeout: timeoutMs, encoding: "utf8", env: quietGitEnv() }, (error, stdout) => resolve(error ? undefined : stdout.trim()));
25
+ });
26
+ var short = (sha) => sha.slice(0, 7);
27
+ async function defaultBranch(remote, git) {
28
+ return parseDefaultBranch(await git(["ls-remote", "--symref", remote, "HEAD"], 1e4));
29
+ }
30
+ var parseDefaultBranch = (listed) => /^ref: (refs\/heads\/\S+)\s+HEAD$/m.exec(listed ?? "")?.[1];
31
+ async function checkForUpdate(repo, git = gitIn(repo), now = Date.now()) {
32
+ const [head, branch] = await Promise.all([git(["rev-parse", "HEAD"]), git(["symbolic-ref", "--short", "-q", "HEAD"])]);
33
+ if (!head || !branch)
34
+ return;
35
+ const [remote, merge] = await Promise.all([git(["config", `branch.${branch}.remote`]), git(["config", `branch.${branch}.merge`])]);
36
+ if (!remote || !merge || remote === ".")
37
+ return;
38
+ let upstream = `${remote}/${merge.replace(/^refs\/heads\//, "")}`;
39
+ let listed = await git(["ls-remote", remote, merge], 1e4);
40
+ if (listed === "") {
41
+ const fallback = await defaultBranch(remote, git);
42
+ if (fallback && fallback !== merge) {
43
+ upstream = `${remote}/${fallback.replace(/^refs\/heads\//, "")}`;
44
+ listed = await git(["ls-remote", remote, fallback], 1e4);
45
+ }
46
+ }
47
+ const base = { repo, checkedAt: now, head, upstream };
48
+ const latest = listed?.split(/\s/)[0];
49
+ if (!latest || !/^[0-9a-f]{40,64}$/.test(latest))
50
+ return { ...base, available: false, failed: true };
51
+ if (latest === head)
52
+ return { ...base, latest, available: false };
53
+ if (await git(["cat-file", "-e", `${latest}^{commit}`]) !== undefined) {
54
+ if (await git(["merge-base", "--is-ancestor", latest, "HEAD"]) !== undefined)
55
+ return { ...base, latest, available: false };
56
+ const behind = Number(await git(["rev-list", "--count", `HEAD..${latest}`]));
57
+ return { ...base, latest, available: true, ...Number.isInteger(behind) && behind > 0 ? { behind } : {} };
58
+ }
59
+ return { ...base, latest, available: true };
60
+ }
61
+ function readCache(file) {
62
+ try {
63
+ return JSON.parse(readFileSync(file, "utf8"));
64
+ } catch {
65
+ return;
66
+ }
67
+ }
68
+ function writeCache(file, check) {
69
+ try {
70
+ mkdirSync(path.dirname(file), { recursive: true });
71
+ const temporary = `${file}.${process.pid}.tmp`;
72
+ writeFileSync(temporary, JSON.stringify(check) + `
73
+ `);
74
+ renameSync(temporary, file);
75
+ } catch {}
76
+ }
77
+ async function cachedUpdateCheck(options) {
78
+ const git = options.git ?? gitIn(options.repo);
79
+ const now = options.now ?? Date.now();
80
+ const file = path.join(options.dataDir, CACHE_FILE);
81
+ const cached = readCache(file);
82
+ const head = await git(["rev-parse", "HEAD"]);
83
+ if (!head)
84
+ return;
85
+ const maxAge = cached?.failed ? FAILED_CHECK_INTERVAL_MS : UPDATE_CHECK_INTERVAL_MS;
86
+ if (cached && cached.repo === options.repo && cached.head === head && now - cached.checkedAt >= 0 && now - cached.checkedAt < maxAge)
87
+ return cached;
88
+ const fresh = await checkForUpdate(options.repo, git, now);
89
+ if (fresh)
90
+ writeCache(file, fresh);
91
+ return fresh;
92
+ }
93
+ var updateCheckEnabled = () => process.env.OPENCODE_BROWSER_UPDATE_CHECK !== "0";
94
+ function describeUpdate(check) {
95
+ if (check.behind)
96
+ return `${check.behind} new commit${check.behind === 1 ? "" : "s"} on ${check.upstream}`;
97
+ return `${check.upstream} is at ${short(check.latest ?? "")} (you have ${short(check.head)})`;
98
+ }
99
+ function updateJson2(update) {
100
+ if (!update)
101
+ return null;
102
+ const { check } = update;
103
+ return {
104
+ available: true,
105
+ summary: update.summary,
106
+ command: update.command,
107
+ notice: formatUpdateNotice2(update),
108
+ current: check.head,
109
+ latest: check.latest ?? null,
110
+ behind: check.behind ?? null,
111
+ upstream: check.upstream
112
+ };
113
+ }
114
+ function formatUpdateNotice2(notice) {
115
+ return `Update available: ${notice.summary}. Run \`${notice.command}\`, then restart opencode.`;
116
+ }
117
+ async function updateNotice2(options = {}) {
118
+ try {
119
+ if (!options.repo) {
120
+ const npm = npmUpdateNotice();
121
+ if (npm !== false)
122
+ return npm;
123
+ }
124
+ const repo = options.repo ?? checkoutRoot();
125
+ if (!repo || !updateCheckEnabled() || !existsSync(path.join(repo, ".git")))
126
+ return;
127
+ const check = await cachedUpdateCheck({ repo, dataDir: options.dataDir ?? dataRoot(), ...options.git ? { git: options.git } : {} });
128
+ return check?.available ? { summary: describeUpdate(check), command: cliCommand("update"), check } : undefined;
129
+ } catch {
130
+ return;
131
+ }
132
+ }
133
+ function npmUpdateNotice() {
134
+ const runtime = npmRuntime();
135
+ if (!runtime)
136
+ return false;
137
+ const status = autoUpdateStatus({ runtime });
138
+ const latest = status.latest;
139
+ if (!latest || compareVersions(latest, runtime.version) <= 0 || status.effective === "off" || latest === status.skipped)
140
+ return;
141
+ if (status.effective === "on" && !status.lastError)
142
+ return;
143
+ const check = { repo: "npm", checkedAt: Date.now(), head: runtime.version, upstream: `npm ${status.channel ?? "latest"}`, latest, available: true };
144
+ return { summary: `browse ${latest} is available (you have ${runtime.version})`, command: cliCommand("update"), check };
145
+ }
146
+ function createUpdateWatcher2(options = {}) {
147
+ const check = options.check ?? (() => updateNotice2());
148
+ const now = options.now ?? Date.now;
149
+ const interval = options.intervalMs ?? FAILED_CHECK_INTERVAL_MS;
150
+ let current;
151
+ let lastRun;
152
+ let running;
153
+ return {
154
+ get current() {
155
+ return current;
156
+ },
157
+ refresh() {
158
+ if (running)
159
+ return running;
160
+ if (lastRun !== undefined && now() - lastRun < interval)
161
+ return Promise.resolve();
162
+ lastRun = now();
163
+ running = check().catch(() => {
164
+ return;
165
+ }).then((next) => {
166
+ const changed = (next ? formatUpdateNotice2(next) : undefined) !== (current ? formatUpdateNotice2(current) : undefined);
167
+ current = next;
168
+ if (changed)
169
+ options.onChange?.(next);
170
+ }).finally(() => {
171
+ running = undefined;
172
+ });
173
+ return running;
174
+ }
175
+ };
176
+ }
177
+ var syncGit = (repo) => (args, timeoutMs = 30000) => {
178
+ const result = spawnSync("git", ["-C", repo, ...args], { encoding: "utf8", timeout: timeoutMs, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });
179
+ return { status: result.status, stdout: (result.stdout ?? "").trim(), stderr: (result.stderr ?? "").trim(), ...result.error ? { error: result.error } : {} };
180
+ };
181
+ var realpath = (file) => {
182
+ try {
183
+ return realpathSync(file);
184
+ } catch {
185
+ return path.resolve(file);
186
+ }
187
+ };
188
+ var firstLine = (text) => text.split(/\r?\n/).find((line) => line.trim())?.trim() ?? "";
189
+ var shellQuoted = (value) => /^[\w@%+=:,./-]+$/.test(value) ? value : JSON.stringify(value);
190
+ function defaultSetup(step) {
191
+ const cli = path.join(step.repo, "cli");
192
+ const run = (command, args, cwd) => {
193
+ const result = spawnSync(command, args, { cwd, stdio: "inherit" });
194
+ return result.status === 0 ? undefined : `\`${[command, ...args].join(" ")}\` failed in ${cwd}${result.error ? ` (${result.error.message})` : ""}`;
195
+ };
196
+ const installs = [[step.root, step.repo], [step.cli, cli]];
197
+ for (const [changed, directory] of installs) {
198
+ if (!changed && existsSync(path.join(directory, "node_modules")))
199
+ continue;
200
+ const failed = run("bun", ["install", "--frozen-lockfile"], directory);
201
+ if (failed)
202
+ return failed;
203
+ }
204
+ const built = run("bun", ["run", "build"], cli);
205
+ if (built)
206
+ return built;
207
+ const node = process.versions.bun ? "node" : process.execPath;
208
+ return run(node, [path.join(cli, "dist", "cli.mjs"), "install", ...step.args], cli);
209
+ }
210
+ function installArgs(args) {
211
+ const passed = [];
212
+ for (let index = 0;index < args.length; index++) {
213
+ const arg = args[index];
214
+ if (arg === "--opencode" && args[index + 1] !== undefined)
215
+ passed.push(arg, args[++index]);
216
+ else if (arg === "--no-open")
217
+ passed.push(arg);
218
+ }
219
+ return [...passed, "--no-wait"];
220
+ }
221
+ function runUpdate(args, deps) {
222
+ const { log } = deps;
223
+ const refuse = (problem, fix) => {
224
+ log(`✗ Not updated: ${problem}`);
225
+ log(` ${fix}`);
226
+ return 1;
227
+ };
228
+ const repo = deps.repo;
229
+ if (!repo) {
230
+ log("Browse was installed from npm. Update it with:");
231
+ log(" npx @opencode/browse@latest install");
232
+ log("then restart opencode.");
233
+ return 0;
234
+ }
235
+ const git = deps.git ?? syncGit(repo);
236
+ const at = shellQuoted(repo);
237
+ const version = git(["--version"]);
238
+ if (version.error || version.status !== 0)
239
+ return refuse("git isn't installed or isn't on PATH.", "Install git, then run this again.");
240
+ const top = git(["rev-parse", "--show-toplevel"]).stdout;
241
+ if (!top || realpath(top) !== realpath(repo))
242
+ return refuse(`${repo} isn't a git clone.`, `Clone it again: git clone https://github.com/anomalyco/browse.git && cd browse && bun run setup`);
243
+ if (!(deps.hasBun ?? (() => !spawnSync("bun", ["--version"], { stdio: "ignore" }).error))())
244
+ return refuse("bun isn't installed or isn't on PATH (setup builds with it).", "Install Bun from https://bun.sh, then run this again.");
245
+ const branch = git(["symbolic-ref", "--short", "-q", "HEAD"]).stdout;
246
+ const head = git(["rev-parse", "HEAD"]).stdout;
247
+ if (!branch)
248
+ return refuse(`${repo} isn't on a branch (HEAD is detached at ${short(head)}).`, `Check out the branch to follow, e.g. \`git -C ${at} checkout dev\`, then run this again.`);
249
+ let upstream = git(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]).stdout;
250
+ const remote = git(["config", `branch.${branch}.remote`]).stdout;
251
+ let merge = git(["config", `branch.${branch}.merge`]).stdout;
252
+ if (!upstream || !remote || !merge || remote === ".")
253
+ return refuse(`branch ${branch} doesn't track a remote branch.`, `Set one, e.g. \`git -C ${at} branch --set-upstream-to origin/dev\`, then run this again.`);
254
+ const changes = git(["diff", "--name-only", "HEAD"]).stdout;
255
+ if (changes) {
256
+ const files = changes.split(/\r?\n/);
257
+ return refuse(`${repo} has uncommitted changes (${files.slice(0, 5).join(", ")}${files.length > 5 ? `, and ${files.length - 5} more` : ""}).`, `Commit or stash them (\`git -C ${at} stash\`), then run this again.`);
258
+ }
259
+ if (git(["ls-remote", "--exit-code", remote, merge], 30000).status === 2) {
260
+ const fallback = parseDefaultBranch(git(["ls-remote", "--symref", remote, "HEAD"], 30000).stdout);
261
+ if (fallback && fallback !== merge) {
262
+ const gone = upstream;
263
+ merge = fallback;
264
+ upstream = `${remote}/${fallback.replace(/^refs\/heads\//, "")}`;
265
+ git(["config", `branch.${branch}.merge`, merge]);
266
+ log(`${gone} is gone from the remote; following ${upstream}, its default branch, from now on.`);
267
+ }
268
+ }
269
+ const fetched = git(["fetch", "--quiet", remote, merge], 120000);
270
+ if (fetched.status !== 0) {
271
+ const url = git(["remote", "get-url", remote]).stdout;
272
+ return refuse(`couldn't fetch ${upstream}${url ? ` from ${url}` : ""}: ${firstLine(fetched.stderr) || fetched.error?.message || "git fetch failed"}.`, "Check your network connection, then run this again.");
273
+ }
274
+ const latest = git(["rev-parse", "FETCH_HEAD"]).stdout;
275
+ if (!latest)
276
+ return refuse(`couldn't read ${upstream} after fetching.`, `Run \`git -C ${at} pull --ff-only\` to see why.`);
277
+ if (latest === head || git(["merge-base", "--is-ancestor", latest, "HEAD"]).status === 0) {
278
+ log(`✓ Already up to date: ${short(head)} on ${branch} (${upstream}).`);
279
+ log(` If your unpacked extension still reports an older protocol: ${unpackedReloadAdvice}`);
280
+ return 0;
281
+ }
282
+ if (git(["merge-base", "--is-ancestor", "HEAD", latest]).status !== 0) {
283
+ const counts = git(["rev-list", "--left-right", "--count", `HEAD...${latest}`]).stdout.split(/\s+/);
284
+ return refuse(`${branch} has diverged from ${upstream} (${counts[0] ?? "?"} local commit(s), ${counts[1] ?? "?"} new on the remote), so it can't fast-forward.`, `Reconcile them yourself (e.g. \`git -C ${at} pull --rebase\`), then run \`cd ${at} && bun run setup\`.`);
285
+ }
286
+ const count = Number(git(["rev-list", "--count", `HEAD..${latest}`]).stdout) || 0;
287
+ const commits = git(["log", "--oneline", "--no-decorate", "-n", "20", `HEAD..${latest}`]).stdout;
288
+ const changed = (...files) => !!git(["diff", "--name-only", "HEAD", latest, "--", ...files]).stdout;
289
+ const rootDeps = changed("package.json", "bun.lock");
290
+ const cliDeps = changed("cli/package.json", "cli/bun.lock");
291
+ log(`Updating browse ${short(head)} → ${short(latest)} (${count} new commit${count === 1 ? "" : "s"} on ${upstream}):`);
292
+ for (const line of commits.split(/\r?\n/).filter(Boolean))
293
+ log(` ${line}`);
294
+ if (count > 20)
295
+ log(` … and ${count - 20} more`);
296
+ log("");
297
+ const pulled = git(["merge", "--ff-only", "--quiet", latest], 60000);
298
+ if (pulled.status !== 0)
299
+ return refuse(`couldn't fast-forward to ${short(latest)}: ${firstLine(pulled.stderr) || "git merge failed"}.`, `Run \`git -C ${at} pull --ff-only\` to see why.`);
300
+ const failed = (deps.setup ?? defaultSetup)({ repo, root: rootDeps, cli: cliDeps, args: installArgs(args) });
301
+ if (failed) {
302
+ log("");
303
+ log(`✗ Pulled ${short(head)} → ${short(latest)}, but setup failed: ${failed}.`);
304
+ log(` Fix it, then run \`cd ${at} && bun run setup\`.`);
305
+ return 1;
306
+ }
307
+ log("");
308
+ log(`✓ Updated browse ${short(head)} → ${short(latest)}.`);
309
+ log(" Restart opencode to load the new browse MCP server.");
310
+ return 0;
311
+ }
312
+ async function runUpdateCommand2(args) {
313
+ const log = (line) => process.stdout.write(line + `
314
+ `);
315
+ const { runNpmUpdate } = await import("./runtime-update-0mddkfy5.mjs");
316
+ if (await runNpmUpdate(args))
317
+ return;
318
+ if (args.includes("--check")) {
319
+ const repo = checkoutRoot();
320
+ if (!repo)
321
+ return log("Installed from npm: run `npx @opencode/browse@latest install` to update.");
322
+ const check = await checkForUpdate(repo);
323
+ if (check)
324
+ writeCache(path.join(dataRoot(), CACHE_FILE), check);
325
+ if (!check)
326
+ return log("Can't check: this clone isn't on a branch that tracks a remote.");
327
+ if (check.failed)
328
+ return log(`Can't check: ${check.upstream} couldn't be reached.`);
329
+ return log(check.available ? formatUpdateNotice2({ summary: describeUpdate(check), command: cliCommand("update") }) : `✓ Up to date with ${check.upstream}.`);
330
+ }
331
+ process.exitCode = runUpdate(args, { repo: checkoutRoot(), log });
332
+ }
333
+
334
+ export { updateJson2, formatUpdateNotice2, updateNotice2, createUpdateWatcher2, runUpdateCommand2 };