feat: resolve a program's pinned node version for build + runtime
Frontend builds triggered from the castle web app run inside the castle-api systemd service, whose PATH omits nvm's versioned node dir — so `pnpm build` died with `node: not found` even though it worked from an interactive shell. Introduce a per-program node convention: a program declares its version the ecosystem-standard way (.node-version / .nvmrc / package.json engines.node), and castle_core.toolchains.resolve_node_bin() maps it to a concrete nvm bin dir (CASTLE_NODE_VERSIONS_DIR, default ~/.nvm/versions/node; newest match wins). The same resolver feeds both sites that run a program's node: - build time: stacks._build_env() prepends the pinned node for the dev-verb subprocess (keyed on the source dir), so `castle program build` uses the program's node regardless of caller. - run time: deploy._build_deployed() stores it in Deployment.path_prepend, which the systemd generator puts ahead of the default unit PATH — so a `launcher: node` service runs its program's node. A pinned-but-uninstalled version fails loud with an `nvm install` hint instead of a cryptic `node: not found`. Unpinned → no injection (no guessing). Also: the systemd generator now honors an explicit PATH in defaults.env as a full override instead of clobbering it with a trailing Environment=PATH line (systemd's last-assignment-wins rule had silently defeated the documented escape hatch). Pins app/.node-version and documents the convention in the react-vite stack. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -121,6 +121,29 @@ pnpm run check # lint + type-check + test
|
||||
|
||||
The `build` output is a static SPA in `dist/` — just HTML, JS, and CSS files.
|
||||
|
||||
## Pinning the node version
|
||||
|
||||
Commit a **`.node-version`** (or `.nvmrc`, or `package.json` `engines.node`) at the
|
||||
project root:
|
||||
|
||||
```
|
||||
24.14.1
|
||||
```
|
||||
|
||||
Castle reads this pin and puts the matching node on PATH whenever it runs the
|
||||
program's node — at build time (`castle program build`, incl. builds triggered from
|
||||
the castle web app, which run inside the `castle-api` service) and at run time (a
|
||||
`launcher: node` service's systemd unit). This is why a build works from the web app
|
||||
even though the service's default PATH deliberately omits nvm's versioned dirs.
|
||||
|
||||
The pin is standard, tool-agnostic config — the program stays castle-independent; nvm
|
||||
(and editors/CI) honor the same file. Castle resolves it against
|
||||
`~/.nvm/versions/node` (override with `CASTLE_NODE_VERSIONS_DIR`), newest match wins.
|
||||
An exact pin (`24.14.1`) matches exactly; a partial (`24`) or a range (`>=24`) matches
|
||||
the newest installed major. If the pinned version isn't installed, the verb fails loud
|
||||
with a `nvm install <version>` hint instead of a cryptic `node: not found`. Unpinned →
|
||||
Castle injects no node (it uses whatever ambient node is on PATH, and does not guess).
|
||||
|
||||
## Registering as a program
|
||||
|
||||
A frontend program has a `build` spec (produces static output). Register it in
|
||||
|
||||
Reference in New Issue
Block a user