bun pm
Package manager utilities
The bun pm command group is a set of utilities for working with Bun's package manager.
pack#
To create a tarball of the current workspace:
bun pm packbun pm pack creates a .tgz file containing all files that would be published to npm, following the same rules as npm pack.
Examples#
Basic usage:
bun pm pack
# Creates my-package-1.0.0.tgz in current directoryQuiet mode for scripting:
TARBALL=$(bun pm pack --quiet)
echo "Created: $TARBALL"Created: my-package-1.0.0.tgzCustom destination:
bun pm pack --destination ./dist
# Saves tarball in ./dist/ directoryOptions#
--dry-run: Perform all tasks except writing the tarball to disk. Shows what would be included.--destination <dir>: The directory to save the tarball in.--filename <name>: An exact file name for the tarball.--ignore-scripts: Skip running pre/postpack and prepare scripts.--gzip-level <0-9>: Set the gzip compression level, from 0 to 9 (default 9).--quiet: Print only the tarball filename, suppressing the rest of the output. Useful in scripts.
Note:
--filenameand--destinationcannot be used at the same time.
Output Modes#
Default output:
bun pm packbun pack v1.2.19
packed 131B package.json
packed 40B index.js
my-package-1.0.0.tgz
Total files: 2
Shasum: f2451d6eb1e818f500a791d9aace80b394258a90
Unpacked size: 171B
Packed size: 249BQuiet output:
bun pm pack --quietmy-package-1.0.0.tgzbin#
To print the path to the bin directory for the local project:
bun pm bin/path/to/current/project/node_modules/.binTo print the path to the global bin directory:
bun pm bin -g<$HOME>/.bun/binls#
To print a list of installed dependencies in the current project and their resolved versions, excluding their dependencies:
bun pm ls
# or
bun list/path/to/project node_modules (135 installed)
├── eslint@8.38.0
├── react@18.2.0
├── react-dom@18.2.0
├── typescript@5.0.4
└── zod@3.21.4To print all installed dependencies, including nth-order dependencies:
bun pm ls --all
# or
bun list --all/path/to/project node_modules (135 installed)
├── @eslint-community/eslint-utils@4.4.0
├── @eslint-community/regexpp@4.5.0
├── @eslint/eslintrc@2.0.2
├── @eslint/js@8.38.0
├── @nodelib/fs.scandir@2.1.5
├── @nodelib/fs.stat@2.0.5
├── @nodelib/fs.walk@1.2.8
├── acorn@8.8.2
├── acorn-jsx@5.3.2
├── ajv@6.12.6
├── ansi-regex@5.0.1
├── ...To print only trusted dependencies (those allowed to run lifecycle scripts). When trustedDependencies is set in package.json, Bun shows the packages from that list; otherwise it shows packages from its default trusted dependencies list.
bun pm ls --trusted
# or
bun list --trusted/path/to/project node_modules (135 installed)
└── esbuild@0.21.5To leave out devDependencies and the packages only they need, pass --production (or --omit=dev; --omit also takes optional and peer):
bun pm ls --production
bun pm ls --all --omit=devAs with npm ls, --depth=0 lists the direct dependencies, which is the default. Bun accepts no other depth.
To print every installed version of a package, direct dependency or not, pass its name. The exit code is 1 when no package has that name. bun why shows what depends on it.
bun pm ls ansi-regex/path/to/project node_modules
├── ansi-regex@5.0.1
└── ansi-regex@6.0.1To print the packages installed globally with bun add -g:
bun pm ls -g
# or
bun list --global<$HOME>/.bun/install/global node_modules (1 installed)
└── typescript@5.0.4licenses#
List every installed package grouped by license, as read from each package's package.json in node_modules. Bun lists packages without a license field under Unknown. Bun marks packages only reachable through devDependencies with (dev).
bun pm licenses
# or
bun pm licenses lsbun pm licenses v1.3.0 (a4b2f86f)
MIT (2)
├── path-parse@1.0.6
└── resolve@1.9.0
Unknown (4)
├── a-dep@1.0.1 (dev)
├── no-deps@1.0.0
├── no-deps@1.0.1
└── one-dep@1.0.0
6 packages across 2 licenses (checked 6 packages in bun.lock) [4.00ms]| Flag | Description |
|---|---|
--json | Print a JSON object keyed by license |
--long | Also print each package's author, description, and homepage |
--prod, -p | Skip devDependencies (same as --omit=dev) |
--dev, -D | Only list what devDependencies pull in |
--omit=<dev|optional|peer> | Skip a dependency type, as with bun install |
--filter <pattern>, -F | Only list dependencies of the matching workspaces (filter syntax) |
bun pm licenses --json --prod{
"MIT": [
{
"name": "path-parse",
"versions": ["1.0.6"],
"paths": ["/home/me/app/node_modules/path-parse"],
"license": "MIT",
"homepage": "https://github.com/jbgutierrez/path-parse#readme",
"author": "Javier Blanco <http://jbgutierrez.info>",
"description": "Node.js path.parse() ponyfill"
}
]
}From a workspace root, Bun lists every workspace's dependencies. From inside a workspace package, Bun lists only that package's dependencies. Use bun why to find out what pulls in an unexpected package.
Requires both bun.lock and node_modules. Bun skips packages that are in the lockfile but missing from node_modules (e.g. after bun install --production) and prints a warning.
diff#
Show what changed between two versions of a package: a summary of the files and lines touched, the package.json changes worth a second look (new install scripts, dependencies, entry points, binaries), then a unified diff of every file. Each side can be a registry spec, a local folder, or a .tgz.
bun pm diff react # the version in bun.lock → latest
bun pm diff react@18.2.0 19.0.0 # two published versions
bun pm diff react@18.2.0..19.0.0 # same thing
bun pm diff ./vendored-pkg pkg@2.1.0 # a folder against a published version
bun pm diff # in a package folder: what is published → this folderis-number@6.0.0 → is-number@7.0.0
4 files changed, 0 added, 0 removed (+73 -44 lines)
! engines changed: { "node": ">=0.10.0" } → { "node": ">=0.12.0" }
diff --git a/index.js b/index.js
--- a/index.js
+++ b/index.js
@@ -9,10 +9,10 @@
module.exports = function(num) {
if (typeof num === 'number') {
- return num - num === 0;
+ return num - num === 0;
}
...On a terminal, code is compared by meaning and shown as written. Both sides of every .js/.mjs/.cjs/.jsx/.ts/.tsx file are parsed and re-printed in one canonical form (equivalent syntax folded, unreachable code dropped); that canonical form decides what changed, and the hunks show the original lines — comments, types, JSX and names intact:
1 + 1vs2,'a'vs"a",!0vstrue, a re-wrapped call, a whole-file reformat, or a local renamed consistently everywhere (a bundler'sutils→utils$1) are not changes — locals on both sides are matched by how they are used, never by what they are called. Lines folded away this way are counted in the file header (3 folded), and a file with nothing left readsformatting only. Nothing is folded on the strength of one line: the old file with the folded lines as the new file has them must still print in the same canonical form as the old file, otherwise the file is shown as a plain diff. Exported names and the#!line are compared as written.- A comment edit, a changed type annotation, or any change in control flow is a change, shown in the author's words.
- Unchanged text whose meaning moved — code that became unreachable, or dead code that came alive because a condition elsewhere flipped — is marked
~. - Minified bundles have no readable original, so an un-minified re-print stands in for it (one declaration, call and property per line, each numbered by its
line:colin the original) and the same folded comparison decides what changed — with the short names two builds' minifiers handed out differently renamed in lockstep;dist/*.min.jsdiffs down to the lines that actually changed rather than one enormous-/+pair. The re-print is used only when it parses back to the same program as the file; otherwise the file's own lines are shown..jsonand.cssfiles are compared and shown as their canonical print (normalized), so a reformat around one edit is one edit; identical prints readformatting only. - The summary at the end calls out what a reviewer looks for first: new
install/postinstallscripts, dependency and entry-point changes, a file that newly importschild_process/fs/net/http/vm/…, new package imports, growth ineval()/new Function()/process.env/fetch()use, newly executable files, a text file that became binary, and terminal escape sequences or bidirectional-text controls appearing in a release. For a published version,bun installtakes dependencies,bin,osandcpufrom the registry's entry for it: where that entry and thepackage.jsonin the tarball disagree, the summary shows both and goes by the registry.*.mapfiles are summarised as regenerated rather than shown. Files over 64 MB are diffed as text (too large to normalize). - Line tints follow the terminal: light or dark is taken from
COLORFGBGor by asking the terminal for its background colour (OSC 11), and exact colours are used whenCOLORTERM=truecolor. - Nothing in a package can drive your terminal: control characters, escape sequences and bidirectional-text overrides in file contents and names are drawn as
␛,^M,‹U+202E›, withNO_COLORtoo. Permission changes show asmode 644 → 755/new file mode 100755.
To look at part of a package, name the files: as a :path suffix on a spec, as a bare :path argument, or as extra arguments after the two sides. A path matches a file exactly, a directory, a file name anywhere in the package, or a glob:
bun pm diff axios@1.6.0:lib/adapters 1.6.1 # one directorybun pm diff axios@1.6.0 axios@1.6.1 dist/node/axios.cjs README.mdbun pm diff lodash :trimEnd.js # installed → latest, one filebun pm diff react-dom@18.2.0 18.3.1 '*.min.js'bun pm diff react-dom@18.2.0 18.3.1 'cjs/**/*.production.min.js'A local folder that has a package.json is read the way bun pm pack would publish it — the files field, .npmignore / .gitignore, bin — so diffing a checkout against the registry compares what would ship, not node_modules/, vendor/ or build output. Against a tarball or a published version, the workspace: and catalog: versions of a package of the current project are compared as the versions bun pm pack writes in their place. Two folders are compared as written.
Piped or with NO_COLOR, the output is a plain unified patch of the real file contents (so bun pm diff a b > changes.patch applies with git apply or patch -p1, empty files and permission changes included), and --raw turns the re-print off on a terminal too. Binary files and source maps are named under a diff --bun header that both tools skip: their contents are not in the patch, only their sizes and, for binary files, the start of each side's SHA-256 (--json has all of it). A file counts as binary when it has a NUL byte near its start and nothing would read it as text: a .js, .ts, .json, .css, .html, .md, .yaml, .sh, … file, a #! script, and any file that parses as JavaScript are always shown.
| Flag | Description |
|---|---|
--raw, --unformatted | Compare bytes as-is; skip the JS/CSS/JSON re-print |
--minify | Also fold equivalent syntax (!0/true, quote style, redundant parens) so only meaning is compared |
--unminify | Rename short locals in lockstep in every JS file, not just ones that look minified |
-w, --ignore-space | On a terminal, show files that differ only in whitespace as whitespace only instead of hunks |
--stat | One line per file with a change count and bar instead of the hunks |
--name-only | Only list the files that differ, marked Added / Modified / Deleted |
-U <n>, --unified | Lines of context around each change (default 3) |
--diff <spec> | npm-compatible way to name a side; may be given twice |
--json | One JSON document: from/to, per-file status/counts/patch, plain-text notes, and totals |
With one name and no version, the left side is the version this project's bun.lock resolved and the right side is latest, so bun pm diff <pkg> answers "what would updating this pull in?". That is the version <pkg> means in the folder you run it from: a workspace package's own dependency comes before the copy at the project's root. An alias ("legacy": "npm:pkg@1") names the package behind it. bun pm diff <pkg> 2.0.0 compares the same version with 2.0.0, and latest can stand where a second version can. Registry, scope and auth settings come from bunfig.toml / .npmrc as for bun install, and a download is checked against its integrity hash in the same way; outside a project only registry specs and absolute or ./ paths are accepted.
whoami#
Print your npm username. Requires you to be logged in (bunx npm login) with credentials in either bunfig.toml or .npmrc:
bun pm whoamihash#
To generate and print the hash of the current lockfile:
bun pm hashTo print the string used to hash the lockfile:
bun pm hash-stringTo print the hash stored in the current lockfile:
bun pm hash-printcache#
To print the path to Bun's global module cache:
bun pm cacheTo clear Bun's global module cache:
bun pm cache rm
bun pm cache rm --dry-run # print the directories that would be deletedThis deletes everything in the cache directory, including the global store at <cache>/links/. The directory itself stays, so it can be a symlink or a mount. There is no way to remove one package: bun pm cache rm <package> is an error. Every project with globalStore = true symlinks into that directory, so run bun install again in each of them afterwards. Bun refuses to clear a directory that is or contains your project, your home directory or your Bun install.
To refresh stale registry metadata without deleting the cache, use bun add <pkg> --force or bun update <pkg>. See Cache.
To remove only the packages that are older than 30 days:
bun pm cache prune
bun pm cache prune --max-age 7 # older than 7 days
bun pm cache prune --dry-run # list what would be removed- The age of a package is the time since Bun downloaded it. A package that you still install from the cache every day counts as old too.
- Where the file system records access times (Linux with the default
relatime), the age is the time since an install last linked the package from the cache. macOS, Windows andnoatimemounts normally do not record that. - The next
bun installthat needs a removed package downloads it again. Keep this in mind beforebun install --offline. - The global store in
<cache>/links/, cached git clones and package manifests stay. - The size in the summary is the size of the removed files. A file that a
node_modulestree hardlinks stays on disk until that tree is removed too.
rm and prune take the cache directory from $BUN_INSTALL_CACHE_DIR, --cache-dir or $BUN_INSTALL. They ignore a directory named in bunfig.toml, .npmrc or .env, so the files of a project cannot choose what gets deleted. To clear or prune such a directory, pass it with --cache-dir.
To move the cache entries a lockfile needs between machines as one file, for example through a CI cache:
# after an install
bun pm cache pack .ci/bun-cache.pack
# on another runner, before the install
bun pm cache unpack .ci/bun-cache.pack
bun install --frozen-lockfilepackwrites every package ofbun.lockthat is in the local cache. It warns about packages it had to leave out, except optional packages for another platform.unpackadds the entries the cache does not have and never changes an entry that exists. A damaged or incomplete pack is refused as a whole.- With
--dry-run,packreads the cache and writes no file, andunpackchecks the whole pack and leaves the cache as it is. Both print what they would have done. - The file is not compressed; CI caches compress on their own.
- Make one pack per operating system, and a new one after upgrading Bun.
bun install trusts its cache, so a pack is as trusted as the cache directory itself. Its checksums detect damage,
not tampering. Only unpack a file that you packed.
fetch#
To download every dependency into Bun's global module cache without installing anything:
bun pm fetchbun pm fetch v1.5.0 (ca7428e9)
Fetched 42 packages into cache [1.20s]
Cache: /home/user/.bun/install/cache- Dependencies resolve the way
bun installresolves them, butnode_modulesis not created, lifecycle scripts do not run, andpackage.jsonand the lockfile are left untouched. - With a lockfile, only what is missing from the cache is downloaded: the tarball URLs and git commits the lockfile records.
- In a monorepo, you only need the root
package.jsonand the lockfile. Bun downloads the dependencies of every workspace in the lockfile, even if the workspace isn't on disk yet. That means a Docker layer withCOPY package.json bun.lock ./andRUN bun pm fetchis only rebuilt when one of those two files changes. Also copybunfig.tomland.npmrcif they set a registry. Cache entries are keyed by the registry's host, so without them the laterbun installdownloads everything again. The layer also needs any patch files and local tarballs in the lockfile, and any workspace that the rootpackage.jsondepends on withworkspace:. - A later
bun install(orbun install --offline) installs from the cache. Without a lockfile,--offlinecan't tell which commit agithub:ref points to or which file a tarball URL serves, so commit the lockfile if you depend on those. - Packages for another operating system or CPU are skipped; pass
--os/--cputo fetch them. -gfetches the dependencies of globally installed packages.--dry-runprints how many packages are missing from the cache and downloads none of them.- A security scanner does not run: nothing is linked or executed. It runs on the
bun installthat follows.
migrate#
To migrate another package manager's lockfile without installing anything:
bun pm migratePass --dry-run to run the migration without writing bun.lock or package.json.
untrusted#
To print current untrusted dependencies with scripts:
bun pm untrusted./node_modules/@biomejs/biome @1.8.3
» [postinstall]: node scripts/postinstall.js
These dependencies had their lifecycle scripts blocked during install.trust#
To run scripts for untrusted dependencies and add to trustedDependencies:
bun pm trust <names>Options for the trust command:
--all: Trust all untrusted dependencies.--dry-run: Print the scripts that would run and the packages that would be trusted, without running scripts or writingpackage.jsonandbun.lock.
A package that does not come from the npm registry (a tarball, git, file: or link: dependency) also has to be declared in the package.json of the project or of a workspace. bun install, bun pm trust and bun add --trust do not run the scripts of the others, and bun pm untrusted marks them with cannot be trusted by name.
default-trusted#
To print the default trusted dependencies list:
bun pm default-trustedSee the current list on GitHub.
version#
To display current package version and help:
bun pm versionbun pm version v1.3.3 (ca7428e9)
Current package version: v1.0.0
Increment:
patch 1.0.0 → 1.0.1
minor 1.0.0 → 1.1.0
major 1.0.0 → 2.0.0
prerelease 1.0.0 → 1.0.1-0
prepatch 1.0.0 → 1.0.1-0
preminor 1.0.0 → 1.1.0-0
premajor 1.0.0 → 2.0.0-0
from-git Use version from latest git tag
1.2.3 Set specific version
Options:
--no-git-tag-version Skip git operations
--allow-same-version Prevents throwing error if version is the same
--message=<val>, -m Custom commit message, use %s for version substitution
--preid=<val> Prerelease identifier (i.e beta → 1.0.1-beta.0)
--force, -f Bypass dirty git history check
--dry-run Print the new version without writing, running scripts, or tagging
Examples:
bun pm version patch
bun pm version 1.2.3 --no-git-tag-version
bun pm version prerelease --preid beta --message "Release beta: %s"To bump the version in package.json:
bun pm version patchv1.0.1Supports patch, minor, major, premajor, preminor, prepatch, prerelease, from-git, or specific versions like 1.2.3. By default it creates a git commit and tag, also inside a git worktree or submodule; pass --no-git-tag-version to skip them. Pass --dry-run to print the new version without writing package.json, running lifecycle scripts, or touching git.
Increments give the same version as npm version: on 1.0.0-rc.1, major gives 1.0.0, the release that the prerelease was made for.
The preversion, version and postversion scripts see $npm_old_version and $npm_new_version. $npm_package_version is the old version in preversion and the new one after it.
pkg#
Manage package.json data with get, set, delete, and fix operations.
All commands support dot and bracket notation:
scripts.build # dot notation
contributors[0] # array access
workspaces.0 # dot with numeric index
scripts[test:watch] # bracket for special chars
exports[./package.json] # a bracketed name is literal, dots included
contributors.name # get: the name of every elementExamples:
# get
bun pm pkg get name # single property
bun pm pkg get name version # multiple properties
bun pm pkg get # entire package.json
bun pm pkg get scripts.build # nested property
# set
bun pm pkg set name="my-package" # simple property
bun pm pkg set scripts.test="jest" version=2.0.0 # multiple properties
bun pm pkg set private=true --json # JSON values with --json flag
bun pm pkg set 'keywords[]=cli' # append to an array (created if missing)
bun pm pkg set 'contributors[0].name=Jane' # update an array element
# delete
bun pm pkg delete description # single property
bun pm pkg delete scripts.test contributors[0] # multiple/nested
# fix
bun pm pkg fix # auto-fix common issuesWith --json, a value that is not valid JSON is an error. set, delete, and fix accept --dry-run, which prints the resulting package.json instead of writing it.