joeycumines/bun-install

Install CLI commands from a local workspace or an NPM package into Bun's global package store. Supports command selection and Bun runtime override. Excellent for dev builds of MCPs and other tools. Automatically resolves dependencies within your project's workspace.

TypeScript

0

16 commits

updated Aug 1, 2026

See the code

See what people are saying

README

bun-install

Install CLI commands from a local workspace or an NPM package into Bun's global package store.

Supports command selection and Bun runtime override. Excellent for dev builds of MCPs and other tools. Automatically resolves dependencies within your project's workspace.

Install

From NPM

bun add -g bun-install

From source

Clone the repo and run the entrypoint directly:

git clone https://github.com/joeycumines/bun-install.git
cd bun-install
bun src/index.ts

This installs bun-install into Bun's global package store just as the published package would.

Usage

Local project

Run bun-install from your project root (or any subdirectory):

bun-install

Install only selected commands (by binary name):

bun-install my-command another-command

Select a specific package, optionally filtering to specific commands from it:

bun-install --package my-cli           # all commands from my-cli
bun-install -p my-cli tool-a tool-b   # only tool-a and tool-b

NPM package

Install from NPM by passing a package specifier to --package:

bun-install -p prettier                 # all commands from prettier
bun-install -p @scope/pkg@latest        # all commands from a scoped package
bun-install -p pkg@2.0.0 cmd1           # only cmd1 from pkg@2.0.0
bun-install --bun -p pkg@latest          # install under Bun runtime

Any specifier that bun add accepts works, including version ranges and dist-tags. The package is fetched via bun add into a temporary project, then packed and installed globally.

No-clobber guarantee

When a subset of a package's commands is selected, only those commands are symlinked into Bun's global bin directory. bun add -g overwrites existing symlinks without warning, so bun-install filters the bin field in the packed tarball before installation, ensuring unselected commands are never symlinked and cannot clobber existing commands from other packages. This applies to both local and NPM packages.

Command-level selection does not prune dependencies. Determining which deps a specific command uses is undecidable for dynamic imports, and the risk of runtime failures outweighs the marginal benefit.

Forcing the Bun runtime

bun-install --bun
bun-install --bun my-command
bun-install --package my-cli --bun
bun-install --bun -p pkg@latest

The --bun flag rewrites node shebangs in the installed commands to #!/usr/bin/env bun and injects a Bun shebang when a bin target has none, so they run under the Bun runtime instead of Node.js. This works cross-platform: on Unix the OS reads the shebang via the symlink; on Windows Bun's shim reads it from the target file. Files that cannot be safely rewritten (native binaries, non-node scripts) are skipped with a warning. The install proceeds.

Bun's own mechanism for forcing the Bun runtime is bunx --bun, which resolves packages from the current directory's node_modules first and does not consult globally installed packages. This makes it unsuitable for commands that should be available everywhere. bun-install --bun rewrites shebangs in the installed bin targets so they run under Bun regardless of the working directory.

Trusting dependencies

Bun blocks lifecycle scripts (postinstall, preinstall, etc.) for all dependencies by default — a security measure against arbitrary code execution during install. Some packages (e.g. esbuild, @swc/core, native addons) require these scripts to function correctly. Use --trust to allow them:

bun-install -p pkg --trust esbuild
bun-install -p pkg --trust esbuild --trust @swc/core
bun-install --trust esbuild my-cli
bun-install --trust esbuild                       # standalone: persist trust, no install
bun-install --trust esbuild --trust @swc/core     # trust multiple packages

The --trust flag persists package names to a global sidecar file at $BUN_INSTALL/trusted-dependencies.json (typically ~/.bun/trusted-dependencies.json). Before each bun add -g call, the tool collects trust entries from the sidecar and applies them to Bun's global package.json via bun pm trust — letting Bun modify its own state. The flag may be specified multiple times and persists across invocations.

When run without an install target (no project, no --package), --trust persists the trust entries and exits.

In local mode, the project's existing trustedDependencies in the root package.json are inherited and applied alongside the CLI-specified values. This happens automatically — even without --trust — and is cumulative: each distinct local project permanently adds its trust entries to the global store (~/.bun/install/global/package.json). When new entries are propagated, a warning is printed naming the count and the target file.

Note: Defining trustedDependencies in the global package.json replaces Bun's built-in default trusted list (~300+ packages). This is Bun's standard behavior. If you need default-trusted packages (like sharp or prisma) to also run their lifecycle scripts, add them via --trust as well.

Release-age gating and trust

bun-install honors Bun's minimum-release-age configuration (set in ~/.bunfig.toml as minimumReleaseAge in seconds, or ~/.npmrc as min-release-age in days). Versions published more recently than the window are not eligible for resolution at fetch time — a supply-chain safety feature that is on by default for untrusted packages.

Trusting a package with --trust also exempts it from the release-age gate at fetch time, so a trusted @latest resolves to the newest version (including those published inside the window) rather than falling back to an older unblocked version:

bun-install --trust @github/copilot          # persist trust (also exempts release-age)
bun-install -p @github/copilot@latest        # now resolves the true registry latest

This applies to registry specs (pkg, pkg@latest, @scope/pkg@^1.0.0). For npm: alias specs (e.g. myalias@npm:realpkg) the package name is discovered after fetch; if the resolved package is trusted, bun-install re-fetches it with the release-age exemption automatically. Git URLs, file: paths, and https: URLs are not re-fetched — they have no registry version to advance.

Blast radius: --minimum-release-age=0 disables the age gate for the ENTIRE temp dependency tree during the exempted bun add, not just the trusted package. Nested and bundled dependencies fetched during that bun add are also age-ungated, and because NPM-fetched packages preserve nested node_modules (bundled deps), an age-ungated nested dependency can be packed and shipped globally. This is a deliberate trade-off: the user explicitly trusted the package, and Bun does not offer a per-package release-age exclusion flag. Users who need strict age gating for ALL dependencies should NOT use --trust for packages with many transitive dependencies.

If an untrusted @latest (or range) resolves to an older version because of the gate, bun-install prints a warning naming both the resolved and registry latest versions, and suggests --trust. Non-registry specs (git URLs, file:, https:) are skipped — they have no registry version to compare against:

Warning: resolved @github/copilot@1.0.71 but registry latest is 1.0.75 (likely blocked by minimum-release-age (trust the package with --trust to exempt it)).

This warning is best-effort: it queries the registry via npm view, which requires npm to be installed and reachable. If npm is unavailable, the spawn times out (30s), or the lookup fails for any reason, the warning is silently skipped — it never aborts the install.

Note on ignore-scripts: if your global ~/.npmrc sets ignore-scripts=true, bun-install cannot override it per-package for a trusted install — Bun's --ignore-scripts flag is a boolean with no =false form, and --no-ignore-scripts does not override a .npmrc setting. If you need lifecycle scripts to run for a trusted package, remove ignore-scripts=true from your global ~/.npmrc or scope it per-project.

Show help:

bun-install --help

Local project types

bun-install supports two local project structures:

TypeDetectionWhat gets installed
Monorepo workspaceRoot package.json has a workspaces fieldPackages matched by workspace globs that expose bin entries
Single packageNo workspaces field; the root package.json is the packageThe root project's package itself (if it has a bin entry)

Discovery walks upwards from your current directory, so you can run bun-install from any nested directory inside the project.

When --package is specified and the package is not found in the local project, bun-install falls back to fetching it from NPM. If there is no local project at all (no package.json in any parent directory), it also falls back to NPM. However, if the local project is broken (e.g. malformed package.json, empty workspace), the error is surfaced rather than silently substituting a remote package.

Requirements

  • Bun 1.3.14 or newer

Local project mode

  • A package.json in the project root with:
    • A "name" field
    • At least one package that exposes a "bin" entry
  • Workspace mode only: a "workspaces" field pointing to package directories

NPM mode

  • No local project required
  • The package must expose a "bin" entry in its package.json

Development

bun install
bun run check

Release

# bump version (pick one)
bun run version:patch
bun run version:minor
bun run version:major
bun run version:prerelease

# publish (dry-run first, then latest or next)
bun run publish:dry
bun run publish:latest
bun run publish:next

# push the tag
bun run release
bun
bun-js
bun-package
cli
developer-tools
monorepo
package-manager
tools
workspace

joeycumines/bun-install

Install CLI commands from a local workspace or an NPM package into Bun's global package store. Supports command selection and Bun runtime override. Excellent for dev builds of MCPs and other tools. Automatically resolves dependencies within your project's workspace.

TypeScript

0

16 commits

updated Aug 1, 2026

See the code

See what people are saying

README

bun-install

Install CLI commands from a local workspace or an NPM package into Bun's global package store.

Supports command selection and Bun runtime override. Excellent for dev builds of MCPs and other tools. Automatically resolves dependencies within your project's workspace.

Install

From NPM

bun add -g bun-install

From source

Clone the repo and run the entrypoint directly:

git clone https://github.com/joeycumines/bun-install.git
cd bun-install
bun src/index.ts

This installs bun-install into Bun's global package store just as the published package would.

Usage

Local project

Run bun-install from your project root (or any subdirectory):

bun-install

Install only selected commands (by binary name):

bun-install my-command another-command

Select a specific package, optionally filtering to specific commands from it:

bun-install --package my-cli           # all commands from my-cli
bun-install -p my-cli tool-a tool-b   # only tool-a and tool-b

NPM package

Install from NPM by passing a package specifier to --package:

bun-install -p prettier                 # all commands from prettier
bun-install -p @scope/pkg@latest        # all commands from a scoped package
bun-install -p pkg@2.0.0 cmd1           # only cmd1 from pkg@2.0.0
bun-install --bun -p pkg@latest          # install under Bun runtime

Any specifier that bun add accepts works, including version ranges and dist-tags. The package is fetched via bun add into a temporary project, then packed and installed globally.

No-clobber guarantee

When a subset of a package's commands is selected, only those commands are symlinked into Bun's global bin directory. bun add -g overwrites existing symlinks without warning, so bun-install filters the bin field in the packed tarball before installation, ensuring unselected commands are never symlinked and cannot clobber existing commands from other packages. This applies to both local and NPM packages.

Command-level selection does not prune dependencies. Determining which deps a specific command uses is undecidable for dynamic imports, and the risk of runtime failures outweighs the marginal benefit.

Forcing the Bun runtime

bun-install --bun
bun-install --bun my-command
bun-install --package my-cli --bun
bun-install --bun -p pkg@latest

The --bun flag rewrites node shebangs in the installed commands to #!/usr/bin/env bun and injects a Bun shebang when a bin target has none, so they run under the Bun runtime instead of Node.js. This works cross-platform: on Unix the OS reads the shebang via the symlink; on Windows Bun's shim reads it from the target file. Files that cannot be safely rewritten (native binaries, non-node scripts) are skipped with a warning. The install proceeds.

Bun's own mechanism for forcing the Bun runtime is bunx --bun, which resolves packages from the current directory's node_modules first and does not consult globally installed packages. This makes it unsuitable for commands that should be available everywhere. bun-install --bun rewrites shebangs in the installed bin targets so they run under Bun regardless of the working directory.

Trusting dependencies

Bun blocks lifecycle scripts (postinstall, preinstall, etc.) for all dependencies by default — a security measure against arbitrary code execution during install. Some packages (e.g. esbuild, @swc/core, native addons) require these scripts to function correctly. Use --trust to allow them:

bun-install -p pkg --trust esbuild
bun-install -p pkg --trust esbuild --trust @swc/core
bun-install --trust esbuild my-cli
bun-install --trust esbuild                       # standalone: persist trust, no install
bun-install --trust esbuild --trust @swc/core     # trust multiple packages

The --trust flag persists package names to a global sidecar file at $BUN_INSTALL/trusted-dependencies.json (typically ~/.bun/trusted-dependencies.json). Before each bun add -g call, the tool collects trust entries from the sidecar and applies them to Bun's global package.json via bun pm trust — letting Bun modify its own state. The flag may be specified multiple times and persists across invocations.

When run without an install target (no project, no --package), --trust persists the trust entries and exits.

In local mode, the project's existing trustedDependencies in the root package.json are inherited and applied alongside the CLI-specified values. This happens automatically — even without --trust — and is cumulative: each distinct local project permanently adds its trust entries to the global store (~/.bun/install/global/package.json). When new entries are propagated, a warning is printed naming the count and the target file.

Note: Defining trustedDependencies in the global package.json replaces Bun's built-in default trusted list (~300+ packages). This is Bun's standard behavior. If you need default-trusted packages (like sharp or prisma) to also run their lifecycle scripts, add them via --trust as well.

Release-age gating and trust

bun-install honors Bun's minimum-release-age configuration (set in ~/.bunfig.toml as minimumReleaseAge in seconds, or ~/.npmrc as min-release-age in days). Versions published more recently than the window are not eligible for resolution at fetch time — a supply-chain safety feature that is on by default for untrusted packages.

Trusting a package with --trust also exempts it from the release-age gate at fetch time, so a trusted @latest resolves to the newest version (including those published inside the window) rather than falling back to an older unblocked version:

bun-install --trust @github/copilot          # persist trust (also exempts release-age)
bun-install -p @github/copilot@latest        # now resolves the true registry latest

This applies to registry specs (pkg, pkg@latest, @scope/pkg@^1.0.0). For npm: alias specs (e.g. myalias@npm:realpkg) the package name is discovered after fetch; if the resolved package is trusted, bun-install re-fetches it with the release-age exemption automatically. Git URLs, file: paths, and https: URLs are not re-fetched — they have no registry version to advance.

Blast radius: --minimum-release-age=0 disables the age gate for the ENTIRE temp dependency tree during the exempted bun add, not just the trusted package. Nested and bundled dependencies fetched during that bun add are also age-ungated, and because NPM-fetched packages preserve nested node_modules (bundled deps), an age-ungated nested dependency can be packed and shipped globally. This is a deliberate trade-off: the user explicitly trusted the package, and Bun does not offer a per-package release-age exclusion flag. Users who need strict age gating for ALL dependencies should NOT use --trust for packages with many transitive dependencies.

If an untrusted @latest (or range) resolves to an older version because of the gate, bun-install prints a warning naming both the resolved and registry latest versions, and suggests --trust. Non-registry specs (git URLs, file:, https:) are skipped — they have no registry version to compare against:

Warning: resolved @github/copilot@1.0.71 but registry latest is 1.0.75 (likely blocked by minimum-release-age (trust the package with --trust to exempt it)).

This warning is best-effort: it queries the registry via npm view, which requires npm to be installed and reachable. If npm is unavailable, the spawn times out (30s), or the lookup fails for any reason, the warning is silently skipped — it never aborts the install.

Note on ignore-scripts: if your global ~/.npmrc sets ignore-scripts=true, bun-install cannot override it per-package for a trusted install — Bun's --ignore-scripts flag is a boolean with no =false form, and --no-ignore-scripts does not override a .npmrc setting. If you need lifecycle scripts to run for a trusted package, remove ignore-scripts=true from your global ~/.npmrc or scope it per-project.

Show help:

bun-install --help

Local project types

bun-install supports two local project structures:

TypeDetectionWhat gets installed
Monorepo workspaceRoot package.json has a workspaces fieldPackages matched by workspace globs that expose bin entries
Single packageNo workspaces field; the root package.json is the packageThe root project's package itself (if it has a bin entry)

Discovery walks upwards from your current directory, so you can run bun-install from any nested directory inside the project.

When --package is specified and the package is not found in the local project, bun-install falls back to fetching it from NPM. If there is no local project at all (no package.json in any parent directory), it also falls back to NPM. However, if the local project is broken (e.g. malformed package.json, empty workspace), the error is surfaced rather than silently substituting a remote package.

Requirements

  • Bun 1.3.14 or newer

Local project mode

  • A package.json in the project root with:
    • A "name" field
    • At least one package that exposes a "bin" entry
  • Workspace mode only: a "workspaces" field pointing to package directories

NPM mode

  • No local project required
  • The package must expose a "bin" entry in its package.json

Development

bun install
bun run check

Release

# bump version (pick one)
bun run version:patch
bun run version:minor
bun run version:major
bun run version:prerelease

# publish (dry-run first, then latest or next)
bun run publish:dry
bun run publish:latest
bun run publish:next

# push the tag
bun run release
bun
bun-js
bun-package
cli
developer-tools
monorepo
package-manager
tools
workspace

Languages

TypeScript

99.9%