npx skills add ...
npx skills add brianlovin/agent-config --skill electron-wrapper
Wrap a Bun web app into an Electron desktop app with native window management, auto-updates, code signing, and CI/CD distribution. Use when the user wants to create a native desktop application from an existing Bun-based web server, package it for macOS/Windows, set up auto-updating, or handle Electron UX concerns like drag regions and traffic lights. Also use when cutting releases or tagging versions for Electron apps.
npx skills add brianlovin/agent-config --skill electron-wrapper
This skill guides wrapping an existing Bun web server into a native desktop app using Electron. It's based on a proven implementation that solved every major integration challenge.
"Electron as chrome, Bun as server" — Two runtimes working together:
The Electron main process spawns a bundled Bun binary that runs your server, then loads http://localhost:{port} in a BrowserWindow. Your web app doesn't know or care that it's inside Electron — it's just a web page with an optional window.electronAPI bridge for native features.
This architecture means:
node_modules — Electron uses npm, your web app uses BunCreate the Electron subproject alongside your existing Bun web app.
What to create:
electron/ directory with its own package.json (npm, not Bun), two tsconfigs (ESM for main, CJS for preload), electron-builder.yml, and macOS entitlementsscripts/build-server.ts for bundling the serverscripts/download-bun.ts for downloading platform-specific Bun binariesReference: project-setup.md
Build the Electron main process — the entry point, server spawning, window management, auto-updater, and preload bridge.
Files to create:
| File | Purpose |
|---|---|
electron/src/main/index.ts | App lifecycle, dev/prod mode, single-instance lock, IPC handlers |
electron/src/main/bun-server.ts | Spawn bundled Bun, port selection, health polling, env var injection |
electron/src/main/window.ts | BrowserWindow config, bounds persistence, security settings, navigation guards |
electron/src/main/updater.ts | electron-updater setup, event forwarding to renderer |
electron/src/preload/index.ts | contextBridge API with invoke/on patterns and unsubscribe support |
Key decisions:
ELECTRON_DEV_URL env var to connect to the external dev server (no internal Bun spawn)autoDownload: false — let users choose when to download updatesipcRendererelectron-storeReference: main-process.md
Adapt the existing web app to detect and respond to the Electron environment while remaining fully functional as a standalone web app.
Changes to the web app:
| Change | Details |
|---|---|
| Electron detection utility | isElectron(), getElectronPlatform(), isMacElectron(), applyElectronDocumentAttributes() |
| Type declarations | window.electronAPI with all properties optional |
| CSS drag regions | .app-window-drag/.app-window-no-drag classes, auto-exclude interactive elements |
| Traffic light spacing | --electron-traffic-left CSS variable (72px on macOS, 0px elsewhere) |
| Storage path | Env var for data directory, falling back to CWD |
| Static asset serving | Env var for static dir in production mode |
| Auto-update hook | useElectronUpdater() React hook with download/install controls |
| Update notification | Pill component showing available → downloading → ready states |
| Feature gating | Disable demo mode, hosted features when in Electron |
Reference: web-adaptation.md
Bundle everything, set up CI/CD, and handle code signing.
Build pipeline:
bun run build)Bun.build() → resources/server/index.js)resources/bun/{platform}-{arch}/extraResourcesCI/CD:
v*, clippy-v*)macos-14, Windows x64 on windows-latest--publish never in build step, separate publish job creates draft GitHub releaseCode signing:
APPLE_CERTIFICATE, APPLE_CERTIFICATE_PASSWORD, APPLE_ID, APPLE_PASSWORD, APPLE_TEAM_IDIcons:
sips + iconutil from source PNG → .icnspng-to-ico npm package → .icoReference: build-and-distribute.md
Never build release artifacts locally. CI has the signing certificates and notarization credentials. Local builds produce unsigned apps that macOS Gatekeeper will block.
electron/package.json, commit, and merge to mainelectron-builder --publish always locally — no notarizationgh release create with local artifacts — unsignedorigin/mainSee pitfalls.md §13 for full details.
Quick-reference list — see pitfalls.md for full details with symptoms and code examples.
| # | Pitfall | One-line fix |
|---|---|---|
| 1 | ESM/CJS conflicts | "type": "module" + default import pattern for CJS packages |
| 2 | Preload must be CJS | Separate tsconfig with "module": "CommonJS" |
| 3 | __dirname unavailable | fileURLToPath(import.meta.url) polyfill |
| 4 | Dev mode MIME errors | Connect to external dev server via ELECTRON_DEV_URL |
| 5 | Bun version mismatch | Pin version in download script, match dev version |
| 6 | nvm PATH issues | bash -lc for spawned processes |
| 7 | Wrong storage path | Env var + app.getPath("userData") |
| 8 | White flash on open | show: false + ready-to-show + dark backgroundColor |
| 11 | ${platform} != process.platform | Put Bun extraResources in mac:/win: sections with darwin-/win32- prefixes |
| 12 | Bun workspace hoists deps | Bundle main with esbuild + createRequire banner, or use npm for electron dir |
| 13 | Local builds aren't notarized | Always release via CI tags, never electron-builder --publish locally |
The electron:dev command runs the full development environment:
electron:devTo test production-like behavior locally:
When adapting this for a new project, update these project-specific values:
electron-builder.yml (productName, appId)window.tsdev:electron script and dev scriptAPP_DATA_DIR, APP_STATIC_DIR)backgroundColor in window config to match your app's themecategory in electron-builder.yml mac sectionelectron/package.jsondownload-bun.tselectron/assets/