Eric Guo's blog.cloud-mes.com

Hoping writing JS, Ruby & Rails and Go article, but fallback to DevOps note

Fixing Bun Run Dev Failure in Opencode Desktop

Permalink

Today I encountered a classic "works on my machine... wait, no it doesn't" moment while setting up the Opencode Desktop project. Running bun run dev failed with a cryptic Error: Electron uninstall message. Here's the full story of how I diagnosed and fixed it.

The Symptom

Running bun run dev in the packages/desktop directory would build successfully through Vite and electron-vite, then crash at the final step:

error during start dev server and electron app:
Error: Electron uninstall
at getElectronPath (file:///.../electron-vite/dist/chunks/lib-q6ns0vZr.js:155:19)
at startElectron (file:///.../electron-vite/dist/chunks/lib-q6ns0vZr.js:222:26)
...
error: script "dev" exited with code 1

The build completed, but Electron itself couldn't launch.

Root Cause Analysis

Step 1: Check Electron Installation

First, I checked if the electron npm package was installed:

$ ls node_modules/electron
abi_version checksums.json cli.js dist index.js install.js ...

Looks present. But node_modules/electron was actually a symlink to Bun's global package store:

$ readlink node_modules/electron
../../../node_modules/.bun/electron@42.3.3+759ce506b1ed1a42/node_modules/electron

Step 2: Check the Actual Binary

The electron npm package is just a downloader wrapper. The actual Chromium binary lives in dist/. Let's check:

$ ls -la node_modules/electron/dist/
total 39040
-rw-r--r-- 1 staff 19325554 Jun 10 19:23 LICENSES.chromium.html

Only a LICENSE file! The actual Electron.app bundle was missing. The binary had never been extracted.

Step 3: Why Didn't Postinstall Work?

Electron's package.json includes a postinstall script that runs node install.js. This script:

  1. Checks if dist/version and path.txt exist
  2. If not, downloads the platform-specific zip via @electron/get
  3. Extracts it via extract-zip

The zip was already cached locally:

$ ls ~/Library/Caches/electron/
electron-v42.3.3-darwin-arm64.zip # ~120MB, present

So download worked. But extraction didn't. I wrote a test script to verify extract-zip behavior:

const extract = require('extract-zip');
extract('/path/to/electron-v42.3.3-darwin-arm64.zip', {
dir: '/path/to/dist'
}).then(() => console.log('done'));
console.log('after call');

Output:

after call
# Process exits immediately with code 0, never prints "done"

extract-zip v2.0.1 was returning a promise but resolving instantly without doing the work, causing the install script to exit cleanly before extraction completed. This appears to be a compatibility quirk with Node.js v24 and the specific extract-zip version used by Electron 42.3.3.

Step 4: The Smoking Gun — path.txt

Even if extraction had worked, I later discovered another issue. The install.js script creates path.txt containing the relative path to the Electron executable. When I manually created this file, I initially used echo:

echo "Electron.app/Contents/MacOS/Electron" > path.txt

This added a trailing newline (0x0a), which caused a hilarious secondary error:

Error: spawn /path/to/Electron.app/Contents/MacOS/Electron\n ENOENT

Note the \n in the path! Always use printf without a newline for machine-readable path files:

printf 'Electron.app/Contents/MacOS/Electron' > path.txt

The Fix

Since the automated install script couldn't extract the archive, I performed a manual surgical fix:

# 1. Clean the incomplete dist directory
rm -rf node_modules/.bun/electron@42.3.3+759ce506b1ed1a42/node_modules/electron/dist/*
# 2. Extract the cached zip using system unzip (reliable)
unzip -q ~/Library/Caches/electron/0398eab6ff3b.../electron-v42.3.3-darwin-arm64.zip \
-d node_modules/.bun/electron@42.3.3+759ce506b1ed1a42/node_modules/electron/dist
# 3. Create path.txt WITHOUT trailing newline
printf 'Electron.app/Contents/MacOS/Electron' \
> node_modules/.bun/electron@42.3.3+759ce506b1ed1a42/node_modules/electron/path.txt

Verification:

$ node -e "console.log(require('electron'))"
/Users/guochunzhong/git/oss/opencode/node_modules/.bun/electron@42.3.3+.../dist/Electron.app/Contents/MacOS/Electron

Then bun run dev worked perfectly — the Electron app launched, connected to the Vite dev server, and the sidecar initialized.

Lessons Learned

  1. Symlinks obscure the real problem: When using Bun or pnpm, packages live in a content-addressable store. The node_modules/electron directory you see isn't where the actual files are.

  2. Don't trust postinstall in modern package managers: Bun's installation strategy sometimes doesn't play well with native binary postinstall scripts, especially ones using older extraction libraries.

  3. echo is dangerous for file contents: That trailing newline cost me an extra debugging cycle. Use printf or echo -n when writing exact strings.

  4. Cache is your friend: The 120MB zip was already in ~/Library/Caches/electron/. No need to re-download — just extract it properly.

Reference

  • Electron package store path: node_modules/.bun/electron@42.3.3+*/node_modules/electron
  • Electron cache: ~/Library/Caches/electron/ (macOS)
  • Expected dist contents after extraction: Electron.app/, LICENSE, LICENSES.chromium.html, version
  • The path.txt format: plain relative path, no newline, pointing to the executable inside dist/

Comments