Skip to content

fix(npm-globals): install transitive native binaries dropped by bun - #2027

Merged
shunkakinoki merged 1 commit into
mainfrom
fix/npm-globals-transitive-native-binary
Jul 8, 2026
Merged

fix(npm-globals): install transitive native binaries dropped by bun#2027
shunkakinoki merged 1 commit into
mainfrom
fix/npm-globals-transitive-native-binary

Conversation

@shunkakinoki

@shunkakinoki shunkakinoki commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Follow-up to #2023. tokscale was broken: the wrapper ran but errored with tokscale binary not found.

Root cause

tokscale is a JS wrapper that execs a Rust binary shipped as a transitive platform optionalDependency (tokscale -> @tokscale/cli -> @tokscale/cli-darwin-arm64). Bun silently drops transitive platform optional deps during global installs, so the binary was never on disk.

The existing missing_native_optional_dep guard only inspected a dep's own optionalDependencies, so it never fired for wrappers that hide the binary one level down.

Changes

  • missing_native_from_pkg / native_candidate_pkgs: follow one level of wrapper indirection (dep + its direct dependencies) to locate the dropped native binary.
  • repair_native_optional_dep: install the specific missing binary directly at its declaring version, instead of reinstalling the wrapper (which just re-triggers the bun drop).
  • Wired into the version-match pre-check and immediately after fresh bun add --global, so it heals in the same activation run.

Testing

  • Added wrapper-indirection integration test (tokscale pattern) + grep coverage; updated stale assertion.
  • shellspec spec/npm_globals_spec.sh -> 61 examples, 0 failures.
  • shellcheck clean.
  • Verified live: tokscale --version -> tokscale 4.0.7.

Summary by cubic

Fixes broken global CLIs by detecting and installing transitive platform-native binaries that bun drops during global installs, so wrappers like tokscale work again. Handles cases where the real binary is an optionalDependency one level down (e.g., tokscale -> @tokscale/cli -> platform package).

  • Bug Fixes
    • Scan a package and its direct dependencies to find missing platform-specific optional deps (missing_native_from_pkg, native_candidate_pkgs).
    • Install the specific native package at the declaring version (repair_native_optional_dep) during version-match checks and immediately after bun add --global, avoiding wrapper reinstalls.

Written for commit 570fd96. Summary will update on new commits.

Review in cubic

@indent-zero

indent-zero Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor
PR Summary

Fixes a case where bun add --global silently drops a wrapper package's transitive platform-native optionalDependency, leaving the wrapper "installed" but non-functional (e.g. tokscale -> @tokscale/cli -> @tokscale/cli-<platform>). The activation script now looks one hop into the wrapper's direct deps, installs the missing native binary directly, and self-heals in the same activation instead of destructively reinstalling the wrapper (which just re-triggers the same bun drop).

  • Added missing_native_from_pkg returning "<native-name> <declaring-version>" for the first uninstalled platform-native optionalDep in a given package.json.
  • Added native_candidate_pkgs yielding the wrapper's own package.json plus each of its direct dependencies' package.json (one level of indirection).
  • Rewrote missing_native_optional_dep on top of those helpers so it detects natives declared one hop down.
  • Added repair_native_optional_dep that installs the dropped native binary directly with bun add --global <name>@<ver> --minimum-release-age 0, purges the bun npm shim, and reports success only when the target dir exists.
  • Version-match branch now tries the direct repair first and only falls back to the destructive wrapper reinstall when repair fails.
  • MISSING install loop now runs the same detect/repair opportunistically after every bun add --global, healing bun's transitive drop in the same activation.
  • Renamed the existing version-match spec assertion and added greps + a mock-bun integration test covering the tokscale-style wrapper-indirection pattern.

Issues

4 potential issues found:

  • missing_native_from_pkg pins the install spec at the declaring wrapper's own .version rather than the version pinned under .optionalDependencies[<name>]; when a wrapper's native pin drifts from its own version (or is a range) this installs the wrong version or a nonexistent one, silently mismatching wrapper and binary. → Autofix
  • The new wrapper-indirection spec's mock bun is a pure logger that never creates any directory, so repair_native_optional_dep's final [ -d ... ] presence check is never meaningfully exercised — the two latent issues above (silent bun add no-op, phantom-empty dir) would pass this test. → Autofix
  • repair_native_optional_dep doesn't strip the target from the global optionalDependencies before bun add --global, unlike the aliased-native block at line 428 which documents bun add -g <pkg> as a no-op when <pkg> is already listed there — if a transitive native ever lands in that map the heal silently no-ops and the fallback wrapper reinstall re-triggers the same drop on every activation. → Autofix
  • repair_native_optional_dep's success signal is only [ -d "${GLOBAL_MODULES}/${native}" ] — the same file already treats bare-directory presence as untrustworthy for natives (see the "phantom dir, reinstalling" branch and the "still missing after install" warning), so an empty phantom dir will produce a false "native binary repaired in place" log line and skip the wrapper-reinstall fallback. → Autofix

CI Checks

Waiting for CI checks...


⚡ Autofix All Issues

@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@shunkakinoki shunkakinoki added the bug Indicates an unexpected problem or unintended behavior. label Jul 8, 2026
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 42f617a7-6108-44db-b233-838737773c23

📥 Commits

Reviewing files that changed from the base of the PR and between 9913f36 and 570fd96.

📒 Files selected for processing (2)
  • home-manager/modules/npm-globals/install-npm-globals.sh
  • spec/npm_globals_spec.sh

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Improved recovery for missing native optional packages during global installs, so affected packages are now repaired immediately instead of waiting for a later reinstall.
    • Better handles packages installed through a wrapper, including cases where the native binary is nested one level deeper.
  • Tests

    • Expanded coverage for native-binary recovery and wrapper-based installation scenarios.

Walkthrough

This PR modifies install-npm-globals.sh to detect and repair missing platform-native optionalDependencies within the same activation run, using new helper functions to scan wrapper package.json files and directly reinstall the correct native binary rather than reinstalling the whole wrapper. Corresponding test cases are added/updated.

Changes

Native optionalDependency repair

Layer / File(s) Summary
Detection helpers
home-manager/modules/npm-globals/install-npm-globals.sh
Adds missing_native_from_pkg and native_candidate_pkgs, and reimplements missing_native_optional_dep to scan wrapper package.json candidates for platform-matching native optionalDependencies.
Repair function and call-site wiring
home-manager/modules/npm-globals/install-npm-globals.sh
Adds repair_native_optional_dep to install the exact native binary via bun add --global and purge the shim; wires repair attempts into the version-match check and post-install missing-package flow.
Tests for detection, repair, and wrapper indirection
spec/npm_globals_spec.sh
Splits a generic reinstall test into three granular assertions and adds a new test for wrapper-indirection self-heal (tokscale pattern) verifying nested native install target.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant InstallScript
  participant missing_native_optional_dep
  participant repair_native_optional_dep
  participant Bun

  InstallScript->>missing_native_optional_dep: check wrapper for missing native dep
  missing_native_optional_dep-->>InstallScript: native dep missing
  InstallScript->>repair_native_optional_dep: attempt repair(dep)
  repair_native_optional_dep->>Bun: bun add --global native-pkg@version
  repair_native_optional_dep->>repair_native_optional_dep: purge bun shim
  repair_native_optional_dep-->>InstallScript: repaired or failed
  alt repair succeeded
    InstallScript->>InstallScript: continue (skip reinstall)
  else repair failed
    InstallScript->>InstallScript: remove wrapper, queue reinstall
  end
Loading

Possibly related PRs

  • shunkakinoki/dotfiles#1890: Both PRs modify install-npm-globals.sh to install/heal platform-specific native optionalDependencies during global install.
  • shunkakinoki/dotfiles#1982: Both PRs update install-npm-globals.sh to self-heal missing/incorrect platform-native optionalDependencies for wrapper-like native packages.

Poem

A wrapper's dep was missing its heart,
so I hopped right in to fix that part,
bun add --global, quick and neat,
no full reinstall, just a targeted feat,
🐰 thump-thump — native binary complete!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the npm-globals fix for installing transitive native binaries dropped by Bun.
Description check ✅ Passed The description directly explains the bug, root cause, fix, and testing for the same changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/npm-globals-transitive-native-binary

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@shunkakinoki
shunkakinoki merged commit 7d9f90f into main Jul 8, 2026
29 of 35 checks passed
@shunkakinoki
shunkakinoki deleted the fix/npm-globals-transitive-native-binary branch July 8, 2026 15:57

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request improves the installation of npm global packages by adding support for resolving and repairing missing platform-native optional dependencies that are nested under a wrapper package (such as the tokscale pattern). It introduces helper functions to traverse candidate package definitions and directly install the missing native binaries. The feedback suggests adding a defensive guard in missing_native_from_pkg to handle empty platform variables and recommends moving the definition of the global variable GLOBAL_MODULES to the top of the script to prevent potential runtime issues.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +95 to +97
local pj="$1"
[ -f "$pj" ] || return 0
local opt_deps name decl_ver

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

To prevent unexpected behavior or incorrect pattern matching when PLATFORM_OS or PLATFORM_CPU are empty (e.g., on unsupported platforms), we should add a defensive guard at the beginning of missing_native_from_pkg to return early.

Suggested change
local pj="$1"
[ -f "$pj" ] || return 0
local opt_deps name decl_ver
local pj="$1"
[ -f "$pj" ] || return 0
[ -n "$PLATFORM_OS" ] && [ -n "$PLATFORM_CPU" ] || return 0
local opt_deps name decl_ver

Comment on lines +118 to +120
local dep="$1"
local pj="${GLOBAL_MODULES}/${dep}/package.json"
printf '%s\n' "$pj"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The variable GLOBAL_MODULES is used in several helper functions (such as native_candidate_pkgs, missing_native_optional_dep, and repair_native_optional_dep), but it is defined globally much later in the script (on line 255) and redundantly redefined on line 468.

This is fragile and can lead to bugs if any of these functions are called earlier in the script. It is highly recommended to define GLOBAL_MODULES at the top of the script (e.g., right after PACKAGE_JSON on line 20) and remove the redundant definitions.

@mesa-dot-dev mesa-dot-dev Bot 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.

Performed full review of 9913f36...570fd96

Analysis

• The fixed one-level indirection depth is a pragmatic constraint but lacks explicit documentation as a design boundary (e.g., MAX_NATIVE_SCAN_DEPTH=1). Deeper nesting scenarios will fail silently without clear diagnostics, potentially leaving installations in a broken state without alerting maintainers.

• Logging and observability are adequate but production debugging requires more actionable diagnostics around repair failures—specifically the exact native package name, declaring version, and attempted repair command—to triage why the fallback reinstall was triggered.

• Version extraction and matching logic couples wrapper/binary compatibility tightly to package.json declarations. Any mismatch in how Bun or npm resolves versions compared to your extraction logic could cause silent version skew between the wrapper and binary.

Tip

Help

Slash Commands:

  • /review - Request a full code review
  • /review latest - Review only changes since the last review
  • /describe - Generate PR description. This will update the PR body or issue comment depending on your configuration
  • /help - Get help with Mesa commands and configuration options

0 files reviewed | 3 comments | Edit Agent SettingsRead Docs

done < <(native_candidate_pkgs "$dep")
[ -n "$found" ] || return 1

native="${found%% *}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium

The word-splitting logic here assumes missing_native_from_pkg output format is stable. If decl_ver contains spaces or the native package name contains spaces, this parsing could break. Consider using a more robust delimiter (e.g., tab \t or null byte) or explicitly validate that neither field is empty before splitting to avoid silent failures.

Fix in Cursor • Fix in Claude

Prompt for Agent
Task: Address review feedback left on GitHub.
Repository: shunkakinoki/dotfiles#2027
File: home-manager/modules/npm-globals/install-npm-globals.sh#L158
Action: Open this file location in your editor, inspect the highlighted code, and resolve the issue described below.

Feedback:
The word-splitting logic here assumes `missing_native_from_pkg` output format is stable. If `decl_ver` contains spaces or the native package name contains spaces, this parsing could break. Consider using a more robust delimiter (e.g., tab `\t` or null byte) or explicitly validate that neither field is empty before splitting to avoid silent failures.

spec="$native"
[ -n "$decl_ver" ] && spec="${native}@${decl_ver}"
echo "Installing missing native binary: $spec"
timeout 600 bun add --global "$spec" --minimum-release-age 0 2>/dev/null ||

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium

The bun add --global command output is redirected to /dev/null, which may hide useful diagnostic information when the install fails. Consider capturing stderr to a variable or logging it for debugging purposes, especially since this is a repair path that may fail due to network issues, version conflicts, or package availability.

Fix in Cursor • Fix in Claude

Prompt for Agent
Task: Address review feedback left on GitHub.
Repository: shunkakinoki/dotfiles#2027
File: home-manager/modules/npm-globals/install-npm-globals.sh#L163
Action: Open this file location in your editor, inspect the highlighted code, and resolve the issue described below.

Feedback:
The `bun add --global` command output is redirected to `/dev/null`, which may hide useful diagnostic information when the install fails. Consider capturing stderr to a variable or logging it for debugging purposes, especially since this is a repair path that may fail due to network issues, version conflicts, or package availability.

# Heal in the same run: bun drops transitive platform binaries on fresh
# global installs, so repair immediately instead of waiting for next activation.
if missing_native_optional_dep "$dep"; then
repair_native_optional_dep "$dep" || echo "Native binary repair failed: $dep" >&2

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium

The repair happens after run_postinstall_if_needed completes. If the postinstall script depends on the native binary being present, it would fail before the repair runs. Consider moving the repair check before the postinstall step, or add error handling in postinstall to gracefully handle missing binaries during this window.

Fix in Cursor • Fix in Claude

Prompt for Agent
Task: Address review feedback left on GitHub.
Repository: shunkakinoki/dotfiles#2027
File: home-manager/modules/npm-globals/install-npm-globals.sh#L310
Action: Open this file location in your editor, inspect the highlighted code, and resolve the issue described below.

Feedback:
The repair happens after `run_postinstall_if_needed` completes. If the postinstall script depends on the native binary being present, it would fail before the repair runs. Consider moving the repair check before the postinstall step, or add error handling in postinstall to gracefully handle missing binaries during this window.

spec="$native"
[ -n "$decl_ver" ] && spec="${native}@${decl_ver}"
echo "Installing missing native binary: $spec"
timeout 600 bun add --global "$spec" --minimum-release-age 0 2>/dev/null ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Missing the documented bun add -g no-op workaround. The aliased-native block at lines 425-431 explicitly strips the target from $GLOBAL_PKG's optionalDependencies before bun add --global, with the comment:

Bun's bun add -g <pkg> is a no-op if is already in the global package.json's optionalDependencies (it never materializes the dir). Strip from optionalDependencies first so the add becomes a real install.

This repair path skips that mitigation. Transitive natives aren't normally promoted into the global optionalDependencies map, so the common case is safe — but if the entry ever ends up there (e.g. a prior aliased-native run for the same package, or a stale entry left from user-declared optionalDependencies), bun add --global <native>@<ver> will silently no-op. The presence check at line 166 will then fail, the caller logs "native binary repair failed, reinstalling wrapper", rm -rfs the wrapper and reinstalls it — which just re-triggers the same bun transitive drop. That's a permanent loop across activations for the exact scenario this function is meant to fix.

Suggest applying the same strip-before-add pattern here, or hoisting it into a shared helper so both call sites stay consistent.

[ -n "$opt_deps" ] || return 1

[ -n "$opt_deps" ] || return 0
decl_ver=$(jq -r '.version // empty' "$pj" 2>/dev/null || true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

decl_ver reads the wrapper's own .version, not the pinned native version. The emitted "<name> <decl_ver>" is later concatenated into bun add --global <name>@<decl_ver>. That's only correct under the convention that a wrapper pins its native optionalDeps at its own version.

For wrappers that pin at a different version (e.g. republished natives, or a shared native versioned independently), or that pin at a semver range like ^1.0.0, we'll install a mismatched or nonexistent version. The new spec at spec/npm_globals_spec.sh:381-387 sets wrapper and native both to 1.0.0 so this case is not exercised.

Consider reading the actual pin with jq -r --arg n "$name" '.optionalDependencies[$n] // empty' "$pj" and falling back to .version only when that yields empty or a non-concrete range.

timeout 600 bun add --global "$spec" --minimum-release-age 0 2>/dev/null ||
echo "Install failed: $spec" >&2
purge_bun_npm_shim
[ -d "${GLOBAL_MODULES}/${native}" ]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Presence check accepts phantom-empty native dir. This function returns success purely on directory existence, but the rest of the script already recognizes that as insufficient for natives:

  • Line 421-423 explicitly treats a directory without a package.json as a phantom and reinstalls it.
  • Line 435-438 warns "$dep still missing after install ($spec)" when package.json is absent after a bun add.

Bun has been observed to create empty native dirs during transitive drops — exactly the scenario this function is invoked for. When that happens, this returns 0, the caller logs "$dep native binary repaired in place", and the destructive wrapper-reinstall fallback is skipped, leaving the user with a broken CLI and a misleading success message.

Suggest matching the aliased-native check at line 414: verify ${GLOBAL_MODULES}/${native}/package.json exists (and optionally that .version matches $decl_ver).

Comment thread spec/npm_globals_spec.sh
set -euo pipefail
printf 'bun %s\n' "$*" >>"$MOCK_LOG"
EOF
chmod +x "$MOCK_BIN/bun"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mock bun only logs argv — doesn't exercise the presence check. The bun mock at lines 400-405 just appends to MOCK_LOG and creates no files, and the assertion at line 417 only checks that the correct argv was invoked.

That means repair_native_optional_dep's trailing [ -d "${GLOBAL_MODULES}/${native}" ] always returns false, so the spec doesn't distinguish "repair succeeded" from "bun ran but no dir was materialized" (the phantom-empty case and the documented bun add -g no-op case). Extending the mock to mkdir -p "$GM/@wrap/cli-${os_tok}-${cpu_tok}" (and optionally writing a stub package.json inside) when the invoked argv matches would meaningfully validate the end-to-end heal.

@mesa-dot-dev

mesa-dot-dev Bot commented Jul 8, 2026

Copy link
Copy Markdown

Mesa Description

TL;DR

Fixes runtime "binary not found" errors by detecting and repairing transitive platform-native optional dependencies silently dropped by Bun during global installations.

What changed?

  • home-manager/modules/npm-globals/install-npm-globals.sh:
    • Refactored to traverse one level of wrapper indirection to locate and identify dropped native binaries.
    • Added repair_native_optional_dep to directly install and pin missing binaries at their declared version.
    • Wired repair mechanism into version-match pre-checks and post-install runs.
  • spec/npm_globals_spec.sh:
    • Added integration tests with a mocked nested package structure to validate wrapper indirection self-healing.
    • Updated assertions for missing binary warnings and verified in-place repair.

Description generated by Mesa. Update settings

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Indicates an unexpected problem or unintended behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant