Skip to content

fix(setup_nodejs): verify the binaries a package declares, not its name - #21

Closed
Lunchb0ne wants to merge 1 commit into
community-scripts:mainfrom
Lunchb0ne:fix/nodejs-verify-declared-bins
Closed

fix(setup_nodejs): verify the binaries a package declares, not its name#21
Lunchb0ne wants to merge 1 commit into
community-scripts:mainfrom
Lunchb0ne:fix/nodejs-verify-declared-bins

Conversation

@Lunchb0ne

Copy link
Copy Markdown

What's wrong

setup_nodejs's post-install check derives the command name from the package name:

mod_bin="${mod%@*}"
command -v "$mod_bin" >/dev/null 2>&1 && continue

A package is not required to name its command after itself. opencode-ai installs opencode; single-file-cli installs single-file. For those, command -v <package-name> finds nothing, so a healthy install is reported broken, reinstalled with --force, and reported broken again:

opencode-ai is installed but not on PATH - reinstalling
opencode-ai is not callable and could not be restored
1 Node.js module(s) failed: ...

Nothing actually breaks — msg_error here doesn't abort — but the install ends in red and the failure summary is wrong. A user watching that reasonably concludes the build failed.

Scoped packages had the same problem from the other side. They were skipped outright:

# Scoped packages do not name their binary (@vue/cli installs `vue`), so
# there is nothing reliable to probe for.
[[ "$mod" == @*/* ]] && continue

So the check silently didn't apply to them at all.

How it affects other installs

NODE_MODULE appears in 153 files across ProxmoxVE. The overwhelming majority pass yarn, pnpm, next or serve, where the package name and the command match — those are unaffected, before and after.

The change matters for two groups:

Packages whose command differs from their name — currently a false failure. install/archivebox-install.sh hits this today:

NODE_VERSION="22" NODE_MODULE="@postlight/parser@latest,single-file-cli@latest" setup_nodejs

single-file-cli declares bin: { "single-file": ... }, so every archivebox install prints the "not callable" error and an inflated failed-module count for a package that installed correctly.

Scoped packages — currently unverified. In the same line, @postlight/parser declares mercury-parser and postlight-parser. It was skipped, so if those commands really were missing, the check that exists to catch exactly that would say nothing.

After the change, both are checked correctly:

@postlight/parser          ->  mercury-parser, postlight-parser
single-file-cli            ->  single-file
@anthropic-ai/claude-code  ->  claude
opencode-ai                ->  opencode
yarn / pnpm / next / serve ->  unchanged

The fix

Read the installed package.json instead of guessing. bin is either a string — in which case the command is the package's own unscoped name — or an object keyed by command name:

mod_bins="$(jq -r 'if (.bin | type) == "string" then (.name | split("/") | last)
                   elif (.bin | type) == "object" then (.bin | keys[])
                   else empty end' "$mod_manifest")"

That resolves both failure modes and makes scoped packages checkable for the first time. jq is already in the container base packages, so this adds no dependency.

Three behaviours are deliberately preserved:

  • A library with no bin is skipped rather than probed for a command it never had.
  • When the manifest can't be read, the old name-guess is kept for unscoped packages and scoped ones are still skipped — a degraded npm root behaves exactly as before.
  • The repair path is unchanged: warn, reinstall with --force, re-check, and count a failure if it still isn't there. Only the name being probed changed.

Testing

Found on Proxmox VE 9.2.11 (kernel 7.0.6-2-pve) while installing t3@latest,@anthropic-ai/claude-code@latest,@openai/codex@latest,opencode-ai@latestopencode-ai produced the false failure on every run while /usr/bin/opencode was present and working (opencode --version → 1.18.26).

The bin resolution was checked against the real published manifests for opencode-ai, t3, @anthropic-ai/claude-code, @openai/codex, single-file-cli and @postlight/parser — all resolve to the correct commands.

The patched block was then exercised against fixtures mirroring a real global npm root:

Case Result
All declared commands present no warnings, failed_modules=0
A declared command genuinely missing warns naming the command (opencode), not the package
Manifest unreadable falls back to the old guess, scoped still skipped

bash -n passes across every .func in the repo.

What I have not verified

  • Not exercised end-to-end in a container build. The container fetches lib/runtime.func from COMMUNITY_SCRIPTS_CORE_URL, so a local checkout only covers the host side. Happy to run it against a branch URL if that's useful.
  • Incus untested — I only have a Proxmox VE host. The change is in lib/, so it should be backend-independent, but I can't claim I ran it.
  • shellcheck / shfmt not run — neither is installed on my host. Worth a look before merge.

No function was added, renamed or removed, so lib/API.txt is unchanged, and no new file needs listing in _CS_ENGINE_FILES.

The post-install check assumed a package installs a command named after
itself. It does not have to: opencode-ai installs `opencode`, and
single-file-cli installs `single-file`. For those, `command -v <package>`
finds nothing, so a healthy install is reported broken, reinstalled with
--force, and reported broken again:

    opencode-ai is installed but not on PATH - reinstalling
    opencode-ai is not callable and could not be restored
    1 Node.js module(s) failed: ...

install/archivebox-install.sh in ProxmoxVE hits this today with
single-file-cli. Nothing actually breaks - msg_error does not abort - but
the build ends in red and the failure summary is wrong.

Scoped packages had the same problem from the other direction. They were
skipped outright for want of anything reliable to probe, so
@postlight/parser (mercury-parser, postlight-parser) and the rest were
never verified at all.

Read the installed package.json instead. `bin` is either a string, in
which case the command is the package's own unscoped name, or an object
keyed by command name. That resolves both cases and makes scoped
packages checkable for the first time.

When the manifest cannot be read the old guess is kept for unscoped
packages and scoped ones are still skipped, so a degraded npm root
behaves exactly as before.
Copilot AI lite review requested due to automatic review settings September 2, 2026 19:38
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Try this branch

The engine and the scripts resolve independently, so a production script can
be run against the engine from this PR by setting one variable:

COMMUNITY_SCRIPTS_CORE_URL=https://raw.githubusercontent.com/Lunchb0ne/core/fix/nodejs-verify-declared-bins \
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/ct/debian.sh)"

Swap ct/debian.sh for whatever exercises the change.

Run a script from a fork as well
curl -fsSL https://raw.githubusercontent.com/Lunchb0ne/core/fix/nodejs-verify-declared-bins/tools/run.sh |
  bash -s -- https://raw.githubusercontent.com/YOU/ProxmoxVED/your-branch ct/debian.sh \
             https://raw.githubusercontent.com/Lunchb0ne/core/fix/nodejs-verify-declared-bins

Note that run.sh is reached through a pipe, so the script it starts inherits
an exhausted stdin. Whiptail is fine — it opens /dev/tty — but a plain read
would see EOF. The single-variable form above does not have that problem.

Useful flags while testing

dev_mode=net logs every engine fetch with status and duration, which is the
quickest way to confirm the branch is really being used. dev_mode=keep stops a
failed build from deleting the container along with the evidence.

@Lunchb0ne

Copy link
Copy Markdown
Author

Closing — opened prematurely, not ready for review yet.

@Lunchb0ne Lunchb0ne closed this Sep 2, 2026
@Lunchb0ne
Lunchb0ne deleted the fix/nodejs-verify-declared-bins branch September 2, 2026 19:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new verification can incorrectly skip checks when a manifest is unreadable/jq fails and can misreport success for packages declaring multiple binaries.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR improves setup_nodejs’s post-install verification by probing the actual executable(s) a global npm package declares via its installed package.json bin field, rather than assuming the command name matches the package name (and previously skipping scoped packages).

Changes:

  • Derive the probed command(s) from the installed module manifest’s bin field (handling scoped packages).
  • Preserve prior fallback behavior when the manifest cannot be used, and keep the existing reinstall-with---force repair path.
File summaries
File Description
lib/runtime.func Updates the Node.js global module “callability” check to use declared bin entries from installed manifests, including scoped packages.
Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lib/runtime.func
Comment on lines +1533 to +1541
mod_manifest="${npm_global_root}/${mod_name}/package.json"
if [[ -n "$npm_global_root" && -f "$mod_manifest" ]]; then
mod_has_manifest=1
# `bin` is either a string (one command, named after the package) or an
# object keyed by command name.
mod_bins="$(jq -r 'if (.bin | type) == "string" then (.name | split("/") | last)
elif (.bin | type) == "object" then (.bin | keys[])
else empty end' "$mod_manifest" 2>/dev/null)"
fi
Comment thread lib/runtime.func
Comment on lines +1553 to +1557
mod_missing=""
while IFS= read -r mod_bin; do
[[ -z "$mod_bin" ]] && continue
command -v "$mod_bin" >/dev/null 2>&1 || mod_missing="$mod_bin"
done <<<"$mod_bins"
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.

2 participants