Problem
pnpm dev broke locally with two stacked failures, both traced back to how Node
is provisioned on dev/agent machines:
env: node: No such file or directory — nvm only puts node on PATH via
shell functions sourced from .zshrc, which zsh only loads for interactive
shells. Any non-interactive process (scripts, some agent/CI shells) gets a PATH
with no node at all, even though node -v works fine in a normal terminal.
better-sqlite3 ABI mismatch (NODE_MODULE_VERSION 141 vs 137) — the native
binding had been compiled against a different Node build than the one currently
active via nvm. Because Node version activation is per-shell and implicit, it's
easy for one process to install/rebuild native deps against version A while
another later runs against version B.
Neither failure is repo-code related — both come from nvm's shell-hook model,
which becomes a real liability once many agents/processes run in parallel
against the same checkout(s): whether a given subshell sourced .zshrc is
unpredictable, so "which Node am I actually running" silently varies per process.
Proposal
Replace nvm with Volta across our repos (framework + objectui
- cloud + objectos — front and back end).
Why Volta specifically:
- Volta installs real shim binaries into a static
PATH directory (~/.volta/bin),
not shell functions — so version resolution works identically in interactive,
non-interactive, and sandboxed/agent shells. This is the exact failure mode (1)
above; Volta doesn't have it.
- Per-repo pinning lives in
package.json:
"volta": { "node": "24.18.0", "pnpm": "10.31.0" }
Volta auto-switches on cd, so repos with different toolchain needs (front-end
vs back-end) don't fight each other, and there's no more "whoever ran
pnpm install last with whatever Node happened to be active" drift that caused
failure mode (2).
Scope / open questions
Non-goals
- Not proposing a Node version upgrade — this is purely a version-manager swap to
fix determinism, not a bump to a newer major.
Problem
pnpm devbroke locally with two stacked failures, both traced back to how Nodeis provisioned on dev/agent machines:
env: node: No such file or directory—nvmonly putsnodeonPATHviashell functions sourced from
.zshrc, which zsh only loads for interactiveshells. Any non-interactive process (scripts, some agent/CI shells) gets a
PATHwith no
nodeat all, even thoughnode -vworks fine in a normal terminal.better-sqlite3ABI mismatch (NODE_MODULE_VERSION 141vs137) — the nativebinding had been compiled against a different Node build than the one currently
active via
nvm. Because Node version activation is per-shell and implicit, it'seasy for one process to install/rebuild native deps against version A while
another later runs against version B.
Neither failure is repo-code related — both come from
nvm's shell-hook model,which becomes a real liability once many agents/processes run in parallel
against the same checkout(s): whether a given subshell sourced
.zshrcisunpredictable, so "which Node am I actually running" silently varies per process.
Proposal
Replace
nvmwith Volta across our repos (framework + objectuiWhy Volta specifically:
PATHdirectory (~/.volta/bin),not shell functions — so version resolution works identically in interactive,
non-interactive, and sandboxed/agent shells. This is the exact failure mode (1)
above; Volta doesn't have it.
package.json:cd, so repos with different toolchain needs (front-endvs back-end) don't fight each other, and there's no more "whoever ran
pnpm installlast with whatever Node happened to be active" drift that causedfailure mode (2).
Scope / open questions
24.18.0(already the versioneveryone is effectively running; no forced upgrade as part of this migration).
pnpmversion via the samevoltafield (already pinned viapackageManager; Volta field should match)."volta"field inframework/package.jsonfirst, verify in a freshnon-interactive shell (
zsh -c "which node"), then propagate toobjectui / cloud / objectos.
nvm.nvmfrom shared dev machines or just stopsourcing it (leave installed, unhooked, in case something else depends on it).
nvmtoday, or already pin Node another way?Non-goals
fix determinism, not a bump to a newer major.