From 0b0cccaad07a05015ce6cc9c166452e9216a98cd Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Sat, 22 Nov 2025 13:06:47 +1100 Subject: [PATCH 1/7] nix: working bundle --- nix/bundle.ts | 39 ++++++++ nix/opencode.nix | 101 +++++++++++++------- packages/opencode/bundle.ts | 35 +++++++ packages/opencode/src/cli/cmd/tui/thread.ts | 10 +- 4 files changed, 148 insertions(+), 37 deletions(-) create mode 100644 nix/bundle.ts create mode 100644 packages/opencode/bundle.ts diff --git a/nix/bundle.ts b/nix/bundle.ts new file mode 100644 index 00000000000..9c3a9610f99 --- /dev/null +++ b/nix/bundle.ts @@ -0,0 +1,39 @@ +#!/usr/bin/env bun + +import solidPlugin from "./node_modules/@opentui/solid/scripts/solid-plugin" +import path from "path" +import fs from "fs" + +const dir = process.cwd() +const parser = fs.realpathSync(path.join(dir, "node_modules/@opentui/core/parser.worker.js")) +const worker = "./src/cli/cmd/tui/worker.ts" +const version = process.env.OPENCODE_VERSION ?? "local" +const channel = process.env.OPENCODE_CHANNEL ?? "local" + +fs.rmSync(path.join(dir, "dist"), { recursive: true, force: true }) + +const result = await Bun.build({ + entrypoints: ["./src/index.ts", worker, parser], + outdir: "./dist", + target: "bun", + sourcemap: "none", + tsconfig: "./tsconfig.json", + plugins: [solidPlugin], + external: ["@opentui/core"], + define: { + OPENCODE_VERSION: `'${version}'`, + OPENCODE_CHANNEL: `'${channel}'`, + OPENCODE_WORKER_PATH: "undefined", + OTUI_TREE_SITTER_WORKER_PATH: 'new URL("./cli/cmd/tui/parser.worker.js", import.meta.url).href', + }, +}) + +if (!result.success) { + console.error("bundle failed") + for (const log of result.logs) console.error(log) + process.exit(1) +} + +const parserOut = path.join(dir, "dist/src/cli/cmd/tui/parser.worker.js") +fs.mkdirSync(path.dirname(parserOut), { recursive: true }) +await Bun.write(parserOut, Bun.file(parser)) diff --git a/nix/opencode.nix b/nix/opencode.nix index bec2997608e..80bb833b0c5 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -1,4 +1,4 @@ -{ lib, stdenv, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }: +{ lib, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }: args: let scripts = args.scripts; @@ -28,64 +28,99 @@ stdenvNoCC.mkDerivation (finalAttrs: { makeBinaryWrapper ]; - configurePhase = '' - runHook preConfigure - cp -R ${finalAttrs.node_modules}/. . - runHook postConfigure - ''; - env.MODELS_DEV_API_JSON = args.modelsDev; env.OPENCODE_VERSION = args.version; env.OPENCODE_CHANNEL = "stable"; + dontConfigure = true; buildPhase = '' runHook preBuild - cp ${scripts + "/bun-build.ts"} bun-build.ts + cp -r ${finalAttrs.node_modules}/node_modules . + cp -r ${finalAttrs.node_modules}/packages . + + ( + cd packages/opencode - substituteInPlace bun-build.ts \ - --replace '@VERSION@' "${finalAttrs.version}" + chmod -R u+w ./node_modules + mkdir -p ./node_modules/@opencode-ai + rm -f ./node_modules/@opencode-ai/{script,sdk,plugin} + ln -s $(pwd)/../../packages/script ./node_modules/@opencode-ai/script + ln -s $(pwd)/../../packages/sdk/js ./node_modules/@opencode-ai/sdk + ln -s $(pwd)/../../packages/plugin ./node_modules/@opencode-ai/plugin - export BUN_COMPILE_TARGET=${args.target} - bun --bun bun-build.ts + cp ${./bundle.ts} ./bundle.ts + chmod +x ./bundle.ts + bun run ./bundle.ts + ) runHook postBuild ''; - dontStrip = true; - installPhase = '' runHook preInstall cd packages/opencode - if [ ! -f opencode ]; then - echo "ERROR: opencode binary not found in $(pwd)" - ls -la + if [ ! -d dist ]; then + echo "ERROR: dist directory missing after bundle step" exit 1 fi - if [ ! -f opencode-worker.js ]; then - echo "ERROR: opencode worker bundle not found in $(pwd)" - ls -la + + mkdir -p $out/lib/opencode + cp -r dist $out/lib/opencode/ + + worker_file=$(find "$out/lib/opencode/dist" -type f \( -path '*/tui/worker.*' -o -name 'worker.*' \) -print -quit) + parser_worker_file=$(find "$out/lib/opencode/dist" -type f -name 'parser.worker.*' -print -quit) + if [ -z "$worker_file" ]; then + echo "ERROR: bundled worker not found" exit 1 fi - install -Dm755 opencode $out/bin/opencode - install -Dm644 opencode-worker.js $out/bin/opencode-worker.js - if [ -f opencode-assets.manifest ]; then - while IFS= read -r asset; do - [ -z "$asset" ] && continue - if [ ! -f "$asset" ]; then - echo "ERROR: referenced asset \"$asset\" missing" - exit 1 + main_wasm=$(printf '%s\n' "$out"/lib/opencode/dist/tree-sitter-*.wasm | sort | head -n1) + wasm_list=$(find "$out/lib/opencode/dist" -maxdepth 1 -name 'tree-sitter-*.wasm' -print) + for patch_file in "$worker_file" "$parser_worker_file"; do + [ -z "$patch_file" ] && continue + [ ! -f "$patch_file" ] && continue + if [ -n "$wasm_list" ] && grep -q 'tree-sitter' "$patch_file"; then + for wasm in $wasm_list; do + name=$(basename "$wasm") + bun --bun -e 'import fs from "fs"; const [file, wasmPath, wasmName] = process.argv.slice(2); const src = fs.readFileSync(file, "utf8"); const next = src.replaceAll(wasmName, wasmPath); if (next !== src) fs.writeFileSync(file, next);' "$patch_file" "$wasm" "$name" + done + if [ -n "$main_wasm" ]; then + if grep -q 'web-tree-sitter/tree-sitter.wasm' "$patch_file"; then + substituteInPlace "$patch_file" \ + --replace-fail 'web-tree-sitter/tree-sitter.wasm' "$main_wasm" + fi + if grep -q 'tree-sitter.wasm' "$patch_file"; then + substituteInPlace "$patch_file" \ + --replace-fail 'tree-sitter.wasm' "$main_wasm" + fi fi - install -Dm644 "$asset" "$out/bin/$(basename "$asset")" - done < opencode-assets.manifest - fi + fi + done + + mkdir -p $out/lib/opencode/node_modules + cp -r ../../node_modules/.bun $out/lib/opencode/node_modules/ + mkdir -p $out/lib/opencode/node_modules/@opentui + + mkdir -p $out/bin + makeWrapper ${bun}/bin/bun $out/bin/opencode \ + --add-flags "run" \ + --add-flags "$out/lib/opencode/dist/src/index.js" \ + --prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]} \ + --argv0 opencode + runHook postInstall ''; - postFixup = '' - wrapProgram "$out/bin/opencode" --prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]} + postInstall = '' + for pkg in $out/lib/opencode/node_modules/.bun/@opentui+core-* $out/lib/opencode/node_modules/.bun/@opentui+solid-* $out/lib/opencode/node_modules/.bun/@opentui+core@* $out/lib/opencode/node_modules/.bun/@opentui+solid@*; do + if [ -d "$pkg" ]; then + pkgName=$(basename "$pkg" | sed 's/@opentui+\([^@]*\)@.*/\1/') + ln -sf ../.bun/$(basename "$pkg")/node_modules/@opentui/$pkgName \ + $out/lib/opencode/node_modules/@opentui/$pkgName + fi + done ''; meta = { diff --git a/packages/opencode/bundle.ts b/packages/opencode/bundle.ts new file mode 100644 index 00000000000..5789f1c11c6 --- /dev/null +++ b/packages/opencode/bundle.ts @@ -0,0 +1,35 @@ +#!/usr/bin/env bun + +import solidPlugin from "./node_modules/@opentui/solid/scripts/solid-plugin" +import path from "path" +import fs from "fs" + +const dir = process.cwd() +const parser = fs.realpathSync(path.join(dir, "node_modules/@opentui/core/parser.worker.js")) +const worker = "./src/cli/cmd/tui/worker.ts" +const version = process.env.OPENCODE_VERSION ?? "local" +const channel = process.env.OPENCODE_CHANNEL ?? "local" + +fs.rmSync(path.join(dir, "dist"), { recursive: true, force: true }) + +const result = await Bun.build({ + entrypoints: ["./src/index.ts", worker, parser], + outdir: "./dist", + target: "bun", + sourcemap: "none", + tsconfig: "./tsconfig.json", + plugins: [solidPlugin], + external: ["@opentui/core"], + define: { + OPENCODE_VERSION: `'${version}'`, + OPENCODE_CHANNEL: `'${channel}'`, + OPENCODE_WORKER_PATH: 'new URL("./worker.ts", import.meta.url).href', + OTUI_TREE_SITTER_WORKER_PATH: 'new URL("./parser.worker.js", import.meta.url).href', + }, +}) + +if (!result.success) { + console.error("bundle failed") + for (const log of result.logs) console.error(log) + process.exit(1) +} diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 43ce0853486..a53037ecc0a 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -58,16 +58,18 @@ export const TuiThreadCommand = cmd({ // Resolve relative paths against PWD to preserve behavior when using --cwd flag const baseCwd = process.env.PWD ?? process.cwd() const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd() - const defaultWorker = new URL("./worker.ts", import.meta.url) + const localWorker = new URL("./worker.ts", import.meta.url) + const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url) // Nix build creates a bundled worker next to the binary; prefer it when present. const execDir = path.dirname(process.execPath) const bundledWorker = path.join(execDir, "opencode-worker.js") const hasBundledWorker = await Bun.file(bundledWorker).exists() - const workerPath = (() => { + const workerPath = await iife(async () => { if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH if (hasBundledWorker) return bundledWorker - return defaultWorker - })() + if (await Bun.file(distWorker).exists()) return distWorker + return localWorker + }) try { process.chdir(cwd) } catch (e) { From 5a1af8917ee213b5c9015283c5158534e5f259d9 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Sat, 22 Nov 2025 13:28:19 +1100 Subject: [PATCH 2/7] nix: refactor wasm rewrite logic and sort bundled worker assets --- nix/bundle.ts | 1 + nix/opencode.nix | 21 +++++---------------- nix/scripts/patch-wasm.ts | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 16 deletions(-) create mode 100644 nix/scripts/patch-wasm.ts diff --git a/nix/bundle.ts b/nix/bundle.ts index 9c3a9610f99..effb1dff7cc 100644 --- a/nix/bundle.ts +++ b/nix/bundle.ts @@ -23,6 +23,7 @@ const result = await Bun.build({ define: { OPENCODE_VERSION: `'${version}'`, OPENCODE_CHANNEL: `'${channel}'`, + // Leave undefined so runtime picks bundled/dist worker or fallback in code. OPENCODE_WORKER_PATH: "undefined", OTUI_TREE_SITTER_WORKER_PATH: 'new URL("./cli/cmd/tui/parser.worker.js", import.meta.url).href', }, diff --git a/nix/opencode.nix b/nix/opencode.nix index 80bb833b0c5..6fd91a689f8 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -69,8 +69,9 @@ stdenvNoCC.mkDerivation (finalAttrs: { mkdir -p $out/lib/opencode cp -r dist $out/lib/opencode/ - worker_file=$(find "$out/lib/opencode/dist" -type f \( -path '*/tui/worker.*' -o -name 'worker.*' \) -print -quit) - parser_worker_file=$(find "$out/lib/opencode/dist" -type f -name 'parser.worker.*' -print -quit) + # Select bundled worker assets deterministically (sorted find output) + worker_file=$(find "$out/lib/opencode/dist" -type f \( -path '*/tui/worker.*' -o -name 'worker.*' \) | sort | head -n1) + parser_worker_file=$(find "$out/lib/opencode/dist" -type f -name 'parser.worker.*' | sort | head -n1) if [ -z "$worker_file" ]; then echo "ERROR: bundled worker not found" exit 1 @@ -82,20 +83,8 @@ stdenvNoCC.mkDerivation (finalAttrs: { [ -z "$patch_file" ] && continue [ ! -f "$patch_file" ] && continue if [ -n "$wasm_list" ] && grep -q 'tree-sitter' "$patch_file"; then - for wasm in $wasm_list; do - name=$(basename "$wasm") - bun --bun -e 'import fs from "fs"; const [file, wasmPath, wasmName] = process.argv.slice(2); const src = fs.readFileSync(file, "utf8"); const next = src.replaceAll(wasmName, wasmPath); if (next !== src) fs.writeFileSync(file, next);' "$patch_file" "$wasm" "$name" - done - if [ -n "$main_wasm" ]; then - if grep -q 'web-tree-sitter/tree-sitter.wasm' "$patch_file"; then - substituteInPlace "$patch_file" \ - --replace-fail 'web-tree-sitter/tree-sitter.wasm' "$main_wasm" - fi - if grep -q 'tree-sitter.wasm' "$patch_file"; then - substituteInPlace "$patch_file" \ - --replace-fail 'tree-sitter.wasm' "$main_wasm" - fi - fi + # Rewrite wasm references to absolute store paths to avoid runtime resolve failures. + bun --bun ${scripts + "/patch-wasm.ts"} "$patch_file" "$main_wasm" $wasm_list fi done diff --git a/nix/scripts/patch-wasm.ts b/nix/scripts/patch-wasm.ts new file mode 100644 index 00000000000..99f8a40e9f3 --- /dev/null +++ b/nix/scripts/patch-wasm.ts @@ -0,0 +1,39 @@ +#!/usr/bin/env bun + +import fs from "fs" +import path from "path" + +/** + * Rewrite tree-sitter wasm references inside a JS file to absolute paths. + * argv: [node, script, file, mainWasm, ...wasmPaths] + */ +const [, , file, mainWasm, ...wasmPaths] = process.argv + +if (!file || !mainWasm) { + console.error("usage: patch-wasm [wasmPaths...]") + process.exit(1) +} + +const content = fs.readFileSync(file, "utf8") +const byName = new Map() + +for (const wasm of wasmPaths) { + const name = path.basename(wasm) + byName.set(name, wasm) +} + +let next = content + +for (const [name, wasmPath] of byName) { + next = next.replaceAll(name, wasmPath) +} + +next = next.replaceAll("tree-sitter.wasm", mainWasm).replaceAll("web-tree-sitter/tree-sitter.wasm", mainWasm) + +// Collapse any relative prefixes before absolute store paths (e.g., "../../../..//nix/store/...") +next = next.replace(/(\.\/)+/g, "./") +next = next.replace(/(\.\.\/)+\/?(\/nix\/store[^"']+)/g, "/$2") +next = next.replace(/(["'])\/{2,}(\/nix\/store[^"']+)(["'])/g, "$1/$2$3") +next = next.replace(/(["'])\/\/(nix\/store[^"']+)(["'])/g, "$1/$2$3") + +if (next !== content) fs.writeFileSync(file, next) From c6b078f15a1dc598e1861954711a84ef01425028 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Sat, 22 Nov 2025 13:34:32 +1100 Subject: [PATCH 3/7] nix: cleanup redundant bundled worker --- packages/opencode/src/cli/cmd/tui/thread.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index a53037ecc0a..948f1b9cdc8 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -60,13 +60,9 @@ export const TuiThreadCommand = cmd({ const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd() const localWorker = new URL("./worker.ts", import.meta.url) const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url) - // Nix build creates a bundled worker next to the binary; prefer it when present. const execDir = path.dirname(process.execPath) - const bundledWorker = path.join(execDir, "opencode-worker.js") - const hasBundledWorker = await Bun.file(bundledWorker).exists() const workerPath = await iife(async () => { if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH - if (hasBundledWorker) return bundledWorker if (await Bun.file(distWorker).exists()) return distWorker return localWorker }) From d289c9cb77b0f4d17be029a8802faae9df246f8e Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Sun, 23 Nov 2025 08:29:25 +1100 Subject: [PATCH 4/7] nix: skip creating symlinks when target package is absent --- nix/hashes.json | 2 +- nix/scripts/canonicalize-node-modules.ts | 31 ++++++++++++++++++------ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 7f089c7e019..54cad6ba3a0 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,3 +1,3 @@ { - "nodeModules": "sha256-N33FQyKF6IgGIRZ8NFd9o1/sjHMwbQ6KQcnMFyN0WmI=" + "nodeModules": "sha256-0oH3xtEkjZIeYoswZF2w5wCeDsFkOOem4Hwj61ZkVmA=" } diff --git a/nix/scripts/canonicalize-node-modules.ts b/nix/scripts/canonicalize-node-modules.ts index bb004f3c54c..828a18fbc7e 100644 --- a/nix/scripts/canonicalize-node-modules.ts +++ b/nix/scripts/canonicalize-node-modules.ts @@ -24,15 +24,13 @@ for (const entry of directories) { if (!info.isDirectory()) { continue } - const marker = entry.lastIndexOf("@") - if (marker <= 0) { + const parsed = parseEntry(entry) + if (!parsed) { continue } - const slug = entry.slice(0, marker).replace(/\+/g, "/") - const version = entry.slice(marker + 1) - const list = versions.get(slug) ?? [] - list.push({ dir: full, version, label: entry }) - versions.set(slug, list) + const list = versions.get(parsed.name) ?? [] + list.push({ dir: full, version: parsed.version, label: entry }) + versions.set(parsed.name, list) } const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as @@ -79,6 +77,12 @@ for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0] await mkdir(parent, { recursive: true }) const linkPath = join(parent, leaf) const desired = join(entry.dir, "node_modules", slug) + const exists = await lstat(desired) + .then(info => info.isDirectory()) + .catch(() => false) + if (!exists) { + continue + } const relativeTarget = relative(parent, desired) const resolved = relativeTarget.length === 0 ? "." : relativeTarget await rm(linkPath, { recursive: true, force: true }) @@ -94,3 +98,16 @@ for (const line of rewrites.slice(0, 20)) { if (rewrites.length > 20) { console.log(" ...") } + +function parseEntry(label: string) { + const marker = label.startsWith("@") ? label.indexOf("@", 1) : label.indexOf("@") + if (marker <= 0) { + return null + } + const name = label.slice(0, marker).replace(/\+/g, "/") + const version = label.slice(marker + 1) + if (!name || !version) { + return null + } + return { name, version } +} From ddf5a8af0064f31d0aafb49c56128886851d34b6 Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Sun, 23 Nov 2025 08:46:57 +1100 Subject: [PATCH 5/7] nix: darwin EACCES fix --- nix/opencode.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nix/opencode.nix b/nix/opencode.nix index 6fd91a689f8..ff536cf8ff8 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -68,6 +68,7 @@ stdenvNoCC.mkDerivation (finalAttrs: { mkdir -p $out/lib/opencode cp -r dist $out/lib/opencode/ + chmod -R u+w $out/lib/opencode/dist # Select bundled worker assets deterministically (sorted find output) worker_file=$(find "$out/lib/opencode/dist" -type f \( -path '*/tui/worker.*' -o -name 'worker.*' \) | sort | head -n1) From 8a8687fa4dd7f23bb88a05a4638c92aa15362ddb Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Sun, 23 Nov 2025 09:52:11 +1100 Subject: [PATCH 6/7] nix: remove unecessary bundle.ts --- packages/opencode/bundle.ts | 35 ----------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 packages/opencode/bundle.ts diff --git a/packages/opencode/bundle.ts b/packages/opencode/bundle.ts deleted file mode 100644 index 5789f1c11c6..00000000000 --- a/packages/opencode/bundle.ts +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bun - -import solidPlugin from "./node_modules/@opentui/solid/scripts/solid-plugin" -import path from "path" -import fs from "fs" - -const dir = process.cwd() -const parser = fs.realpathSync(path.join(dir, "node_modules/@opentui/core/parser.worker.js")) -const worker = "./src/cli/cmd/tui/worker.ts" -const version = process.env.OPENCODE_VERSION ?? "local" -const channel = process.env.OPENCODE_CHANNEL ?? "local" - -fs.rmSync(path.join(dir, "dist"), { recursive: true, force: true }) - -const result = await Bun.build({ - entrypoints: ["./src/index.ts", worker, parser], - outdir: "./dist", - target: "bun", - sourcemap: "none", - tsconfig: "./tsconfig.json", - plugins: [solidPlugin], - external: ["@opentui/core"], - define: { - OPENCODE_VERSION: `'${version}'`, - OPENCODE_CHANNEL: `'${channel}'`, - OPENCODE_WORKER_PATH: 'new URL("./worker.ts", import.meta.url).href', - OTUI_TREE_SITTER_WORKER_PATH: 'new URL("./parser.worker.js", import.meta.url).href', - }, -}) - -if (!result.success) { - console.error("bundle failed") - for (const log of result.logs) console.error(log) - process.exit(1) -} From dd3347ecf48659d69ec81a1fd53d18e5cb7df0ac Mon Sep 17 00:00:00 2001 From: Github Action Date: Sun, 23 Nov 2025 07:34:11 +0000 Subject: [PATCH 7/7] Update Nix flake.lock and hashes --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index e2bf82efc29..8974f62b5e2 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,3 +1,3 @@ { - "nodeModules": "sha256-IKC5nABVIUWryihszkSL+0EKKGQxrFeRn5GZr8DOTpY=" + "nodeModules": "sha256-bqhptL/6WEVBhdkCFPh6bpz4fVbbSv7tZH2yK4FLux8=" }