Don't depend on Node anymore (#346)

## Summary
- package `packages/server` as a standalone desktop executable so
Electron and Tauri no longer depend on a system-installed Node runtime
in production
- align Electron and Tauri startup logic around launching the packaged
server, resolving binaries from the user shell, and bundling the same
server resources into both desktop apps
- replace the workspace instance proxy path that used
`@fastify/reply-from` with a direct streaming proxy so packaged
standalone builds can talk to spawned `opencode` instances correctly

## Why
Desktop production builds were still depending on a user-provided Node
runtime to launch `packages/server`, which made packaging less
self-contained and created different behavior across machines. While
moving to a standalone server executable, we also found that
Bun-compiled standalone builds could start `opencode` successfully but
failed when proxying requests to those instances through `reply-from`.

The goal of this change is to make desktop production startup
self-contained, keep Electron and Tauri behavior aligned, and restore
correct communication with local `opencode` instances in packaged
builds.

## What Changed
- added a standalone build path for `packages/server` and bundle
`codenomad-server` into desktop resources
- updated Electron production startup to resolve and launch the
standalone server executable
- updated Tauri production startup to resolve and launch the standalone
server executable with matching cwd and shell behavior
- added runtime path helpers so the packaged server can reliably find
its bundled UI, auth templates, config template, and package metadata
- improved bare binary resolution so commands like `opencode` can be
resolved from the user's login shell environment
- upgraded the server stack to newer Fastify-compatible packages needed
for the standalone/runtime work
- replaced the workspace instance proxy implementation with a direct
streaming proxy for requests to spawned `opencode` instances
- updated Electron and Tauri build/prebuild scripts to generate and
package the standalone server, while also repairing missing
platform-specific optional binaries during packaging

## Benefits
- desktop production builds no longer require Node to be installed on
the user's system
- Electron and Tauri now use the same packaged server model in
production, reducing platform drift
- packaged desktop apps can successfully create workspaces, launch
`opencode`, and proxy health/session traffic to those instances
- the server bundle is more self-contained and resilient to different
launch environments
- desktop packaging is more predictable because the required server
executable is built and bundled as part of the app build flow
This commit is contained in:
Shantur Rathore
2026-04-21 09:04:34 +01:00
committed by GitHub
parent 68551f6731
commit 67a10d12e0
24 changed files with 1750 additions and 490 deletions

View File

@@ -21,6 +21,7 @@ const serverDevInstallCommand =
const uiDevInstallCommand =
"npm install --workspace @codenomad/ui --include-workspace-root=false --install-strategy=nested --fund=false --audit=false"
const serverPrepareUiCommand = "npm run prepare-ui --workspace @neuralnomads/codenomad"
const serverStandaloneBuildCommand = "npm run build:standalone --workspace @neuralnomads/codenomad"
const envWithRootBin = {
...process.env,
@@ -77,6 +78,15 @@ function ensureServerBuild() {
}
}
function ensureStandaloneServerBuild() {
console.log("[prebuild] building standalone server executable...")
execSync(serverStandaloneBuildCommand, {
cwd: workspaceRoot,
stdio: "inherit",
env: envWithRootBin,
})
}
function ensureUiBuild() {
const loadingHtml = path.join(uiDist, "loading.html")
if (fs.existsSync(loadingHtml)) {
@@ -117,15 +127,19 @@ function ensureServerDevDependencies() {
}
function ensureServerDependencies() {
if (fs.existsSync(braceExpansionPath)) {
return
}
console.log("[prebuild] ensuring server production dependencies...")
execSync(serverInstallCommand, {
console.log("[prebuild] pruning server to production dependencies...")
execSync("npm prune --omit=dev --ignore-scripts --workspaces=false --fund=false --audit=false", {
cwd: serverRoot,
stdio: "inherit",
})
if (!fs.existsSync(braceExpansionPath)) {
console.log("[prebuild] restoring missing server production dependencies...")
execSync(serverInstallCommand, {
cwd: serverRoot,
stdio: "inherit",
})
}
}
function ensureUiDevDependencies() {
@@ -178,6 +192,47 @@ function ensureRollupPlatformBinary() {
})
}
function ensureEsbuildPlatformBinary() {
const platformKey = `${process.platform}-${process.arch}`
const platformPackages = {
"linux-x64": "@esbuild/linux-x64",
"linux-arm64": "@esbuild/linux-arm64",
"darwin-arm64": "@esbuild/darwin-arm64",
"darwin-x64": "@esbuild/darwin-x64",
"win32-arm64": "@esbuild/win32-arm64",
"win32-x64": "@esbuild/win32-x64",
}
const pkgName = platformPackages[platformKey]
if (!pkgName) {
return
}
const platformPackagePath = path.join(workspaceRoot, "node_modules", ...pkgName.split("/"))
if (fs.existsSync(platformPackagePath)) {
return
}
let esbuildVersion = ""
try {
esbuildVersion = require(path.join(workspaceRoot, "node_modules", "esbuild", "package.json")).version
} catch {
try {
esbuildVersion = require(path.join(workspaceRoot, "node_modules", "vite", "node_modules", "esbuild", "package.json")).version
} catch {
// leave version empty; fallback install will use latest compatible
}
}
const packageSpec = esbuildVersion ? `${pkgName}@${esbuildVersion}` : pkgName
console.log("[prebuild] installing esbuild platform binary (optional dep workaround)...")
execSync(`npm install ${packageSpec} --no-save --ignore-scripts --fund=false --audit=false`, {
cwd: workspaceRoot,
stdio: "inherit",
})
}
function copyServerArtifacts() {
fs.rmSync(serverDest, { recursive: true, force: true })
fs.mkdirSync(serverDest, { recursive: true })
@@ -256,8 +311,10 @@ function copyUiLoadingAssets() {
ensureUiDevDependencies()
await ensureMonacoAssets()
ensureRollupPlatformBinary()
ensureServerDependencies()
ensureEsbuildPlatformBinary()
ensureServerBuild()
ensureStandaloneServerBuild()
ensureServerDependencies()
ensureUiBuild()
syncServerUiBundle()
copyServerArtifacts()