Skip to content

fix: run node CLIs on a private runtime immune to workspace node changes - #17

Merged
ashwin153 merged 3 commits into
mainfrom
claude/playwright-install-node-error-a01yve
Aug 18, 2026
Merged

fix: run node CLIs on a private runtime immune to workspace node changes#17
ashwin153 merged 3 commits into
mainfrom
claude/playwright-install-node-error-a01yve

Conversation

@ashwin153

@ashwin153 ashwin153 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Softlight Overview

UX Score: 3/5

Suggested fixes

  • TypeScript language server can no longer be installed on Alpine images

To run Softlight again, click here or comment @softlight.

Why

The playwright and typescript-language-server features installed npm shims whose #!/usr/bin/env node shebang resolves the interpreter from the caller's PATH at spawn time. The only node in the sandbox image lives under nvm's mutable current symlink, and workspace provisioning runs are allowed to rewrite that state — switch versions, uninstall the build-time version, or wipe nvm outright. When that happened in production, every plugins=[playwright] request in the pod failed with:

failed to run ['playwright-cli', 'install', '--skills', 'claude'] (exit 127):
env: 'node': No such file or directory

devcontainer-features#14 made the CLI nvm-proof (own prefix + /usr/local/bin symlink), but its interpreter still came from PATH. This closes the other half.

What

  • Each feature provisions a private node runtime under /usr/local/share/node-runtime/v<nodeVersion>: fetched from nodejs.org, checksum-verified against SHASUMS256.txt, root-owned and read-only, keyed by version so features pinning the same nodeVersion (default 24.18.0) share one copy.
  • /usr/local/bin/playwright-cli and /usr/local/bin/typescript-language-server are now /bin/sh wrappers that exec that runtime by absolute path instead of symlinks to the npm shims. Nothing these CLIs run resolves through PATH or nvm anymore: playwright respawns its children via process.execPath, and tsserver is spawned via fork(), so the whole tree stays on the private runtime.
  • The features no longer require the node feature: installs run the runtime's own npm (installsAfter on node dropped, works on images with no node at all). A new nodeVersion option pins the runtime.
  • buildbuddy, cloud-sql-proxy, and grok are native binaries and were already immune — no changes.
  • Versions: playwright 1.2.0 → 1.3.0, typescript-language-server 1.0.0 → 1.1.0, so :1 consumers pick this up on their next image build.

Verification

  • pre-commit run --all-files passes (shellcheck, shfmt, json/yaml hooks).
  • Ran the new typescript-language-server install.sh verbatim on a clean container: runtime download + checksum verify + npm install + wrapper all succeed; typescript-language-server --version and a full LSP diagnostics round-trip (forked tsserver) work with PATH=/usr/bin:/bin — no node resolvable anywhere.
  • Ran the exact production command that failed — playwright-cli install --skills claude — through the new wrapper with PATH=/usr/bin:/bin: exit 0, skill installed.
  • Tests: the node-switch scenarios now also delete nvm entirely (the production failure mode) and assert the CLIs keep working; new ubuntu_no_node scenarios install each feature without the node feature and assert node/npm are absent while the CLIs work.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Fjd3JvBzVEfk9iiJ2JeoJh


Generated by Claude Code


Note

Cursor Bugbot is generating a summary for commit debf558. Configure here.

ashwin153 and others added 2 commits August 18, 2026 15:20
The playwright and typescript-language-server features installed npm shims
whose #!/usr/bin/env node shebang resolved the interpreter from PATH at
spawn time. The only node in the image lives under nvm's mutable current
symlink, and workspace provisioning is allowed to rewrite that state —
switch versions, uninstall the build-time version, or wipe nvm outright —
which took playwright-cli down in production with
"env: 'node': No such file or directory" (exit 127).

Each feature now provisions a private node runtime under
/usr/local/share/node-runtime/v<version> (fetched from nodejs.org,
checksum-verified, root-owned read-only, shared between features pinning
the same version) and fronts its CLI with a /bin/sh wrapper that execs
that runtime by absolute path. Nothing the CLIs run resolves through PATH
or nvm anymore: playwright respawns children via process.execPath and
tsserver is forked, so both stay on the private runtime. The features no
longer require the node feature at all; installs use the runtime's own
npm. The buildbuddy, cloud-sql-proxy, and grok features are native
binaries and were already immune.

Tests: the node-switch scenarios now also delete nvm entirely — the
production failure mode — and new ubuntu_no_node scenarios prove both
features work on an image with no node of its own.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fjd3JvBzVEfk9iiJ2JeoJh
fi

local arch
case "$(dpkg --print-architecture)" in

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeScript language server can no longer be installed on Alpine images

Users adding the language server to an Alpine-based container now get a failed image build before the server is installed. Add platform-aware runtime selection or explicitly retain the prior supported path for non-Debian systems (install_node_runtime).

Prompt to fix with AI
This is a comment left during a bug review (does this PR's changed code do the wrong thing when it runs?).

Comment:
**TypeScript language server can no longer be installed on Alpine images**
Users adding the language server to an Alpine-based container now get a failed image build before the server is installed. Add platform-aware runtime selection or explicitly retain the prior supported path for non-Debian systems (`install_node_runtime`).

Issue:
The new runtime installer unconditionally executes `dpkg --print-architecture` and only downloads Node's glibc Linux archive. Alpine does not provide `dpkg` by default and uses musl, so the script exits under `set -e` before installing the language server, whereas the previous implementation used the selected node feature's working `npm`.

How it is reached:
Build a dev container from an Alpine-based image with the `typescript-language-server` feature; its feature installer invokes `install_node_runtime` before npm installation.

Suggested fix:
Detect the distribution and architecture before selecting a runtime, then install a compatible musl Node build on Alpine or preserve a supported npm/node-feature path there. If support is intentionally Debian-only, add an explicit platform guard and update the feature requirements instead of advertising no base-image requirement.

Where:
src/typescript-language-server/install.sh (install_node_runtime)

How can I resolve this? Keep the fix scoped to the PR-touched paths and make the smallest change that makes the behavior correct, following existing patterns in this codebase.

@ashwin153
ashwin153 enabled auto-merge (squash) August 18, 2026 15:24
The runtime fetch detects the architecture with dpkg and downloads node's
glibc build, neither of which exists on alpine. Fail with an explicit
message like the playwright feature does instead of dying at dpkg.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fjd3JvBzVEfk9iiJ2JeoJh
@ashwin153
ashwin153 merged commit 9df8b49 into main Aug 18, 2026
2 checks passed
@ashwin153
ashwin153 deleted the claude/playwright-install-node-error-a01yve branch August 18, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant