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
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.
bun add -g bun-install
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.
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
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.
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.
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.
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
trustedDependenciesin 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 (likesharporprisma) to also run their lifecycle scripts, add them via--trustas well.
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=0disables the age gate for the ENTIRE temp dependency tree during the exemptedbun add, not just the trusted package. Nested and bundled dependencies fetched during thatbun addare also age-ungated, and because NPM-fetched packages preserve nestednode_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--trustfor 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~/.npmrcsetsignore-scripts=true, bun-install cannot override it per-package for a trusted install — Bun's--ignore-scriptsflag is a boolean with no=falseform, and--no-ignore-scriptsdoes not override a.npmrcsetting. If you need lifecycle scripts to run for a trusted package, removeignore-scripts=truefrom your global~/.npmrcor scope it per-project.
Show help:
bun-install --help
bun-install supports two local project structures:
| Type | Detection | What gets installed |
|---|---|---|
| Monorepo workspace | Root package.json has a workspaces field | Packages matched by workspace globs that expose bin entries |
| Single package | No workspaces field; the root package.json is the package | The 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.
package.json in the project root with:
"name" field"bin" entry"workspaces" field pointing to package directories"bin" entry in its package.jsonbun install
bun run check
# 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
TypeScript
99.9%
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
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.
bun add -g bun-install
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.
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
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.
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.
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.
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
trustedDependenciesin 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 (likesharporprisma) to also run their lifecycle scripts, add them via--trustas well.
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=0disables the age gate for the ENTIRE temp dependency tree during the exemptedbun add, not just the trusted package. Nested and bundled dependencies fetched during thatbun addare also age-ungated, and because NPM-fetched packages preserve nestednode_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--trustfor 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~/.npmrcsetsignore-scripts=true, bun-install cannot override it per-package for a trusted install — Bun's--ignore-scriptsflag is a boolean with no=falseform, and--no-ignore-scriptsdoes not override a.npmrcsetting. If you need lifecycle scripts to run for a trusted package, removeignore-scripts=truefrom your global~/.npmrcor scope it per-project.
Show help:
bun-install --help
bun-install supports two local project structures:
| Type | Detection | What gets installed |
|---|---|---|
| Monorepo workspace | Root package.json has a workspaces field | Packages matched by workspace globs that expose bin entries |
| Single package | No workspaces field; the root package.json is the package | The 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.
package.json in the project root with:
"name" field"bin" entry"workspaces" field pointing to package directories"bin" entry in its package.jsonbun install
bun run check
# 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
TypeScript
99.9%