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:

terminal
bun pm pack

bun 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:

terminal
bun pm pack
# Creates my-package-1.0.0.tgz in current directory

Quiet mode for scripting:

terminal
TARBALL=$(bun pm pack --quiet)
echo "Created: $TARBALL"
Created: my-package-1.0.0.tgz

Custom destination:

terminal
bun pm pack --destination ./dist
# Saves tarball in ./dist/ directory

Options#

  • --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: --filename and --destination cannot be used at the same time.

Output Modes#

Default output:

terminal
bun pm pack
bun 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: 249B

Quiet output:

terminal
bun pm pack --quiet
my-package-1.0.0.tgz

bin#

To print the path to the bin directory for the local project:

terminal
bun pm bin
/path/to/current/project/node_modules/.bin

To print the path to the global bin directory:

terminal
bun pm bin -g
<$HOME>/.bun/bin

ls#

To print a list of installed dependencies in the current project and their resolved versions, excluding their dependencies:

terminal
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.4

To print all installed dependencies, including nth-order dependencies:

terminal
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.

terminal
bun pm ls --trusted
# or
bun list --trusted
/path/to/project node_modules (135 installed)
└── esbuild@0.21.5

To leave out devDependencies and the packages only they need, pass --production (or --omit=dev; --omit also takes optional and peer):

terminal
bun pm ls --production
bun pm ls --all --omit=dev

As 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.

terminal
bun pm ls ansi-regex
/path/to/project node_modules
├── ansi-regex@5.0.1
└── ansi-regex@6.0.1

To print the packages installed globally with bun add -g:

terminal
bun pm ls -g
# or
bun list --global
<$HOME>/.bun/install/global node_modules (1 installed)
└── typescript@5.0.4

licenses#

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).

terminal
bun pm licenses
# or
bun pm licenses ls
bun 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]
FlagDescription
--jsonPrint a JSON object keyed by license
--longAlso print each package's author, description, and homepage
--prod, -pSkip devDependencies (same as --omit=dev)
--dev, -DOnly list what devDependencies pull in
--omit=<dev|optional|peer>Skip a dependency type, as with bun install
--filter <pattern>, -FOnly list dependencies of the matching workspaces (filter syntax)
terminal
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.

terminal
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 folder
is-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 + 1 vs 2, 'a' vs "a", !0 vs true, a re-wrapped call, a whole-file reformat, or a local renamed consistently everywhere (a bundler's utils → 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 reads formatting 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:col in 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.js diffs 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. .json and .css files are compared and shown as their canonical print (normalized), so a reformat around one edit is one edit; identical prints read formatting only.
  • The summary at the end calls out what a reviewer looks for first: new install/postinstall scripts, dependency and entry-point changes, a file that newly imports child_process/fs/net/http/vm/…, new package imports, growth in eval() / 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 install takes dependencies, bin, os and cpu from the registry's entry for it: where that entry and the package.json in the tarball disagree, the summary shows both and goes by the registry. *.map files 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 COLORFGBG or by asking the terminal for its background colour (OSC 11), and exact colours are used when COLORTERM=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›, with NO_COLOR too. Permission changes show as mode 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 directory
bun pm diff axios@1.6.0 axios@1.6.1 dist/node/axios.cjs README.md
bun pm diff lodash :trimEnd.js                       # installed → latest, one file
bun 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.

FlagDescription
--raw, --unformattedCompare bytes as-is; skip the JS/CSS/JSON re-print
--minifyAlso fold equivalent syntax (!0/true, quote style, redundant parens) so only meaning is compared
--unminifyRename short locals in lockstep in every JS file, not just ones that look minified
-w, --ignore-spaceOn a terminal, show files that differ only in whitespace as whitespace only instead of hunks
--statOne line per file with a change count and bar instead of the hunks
--name-onlyOnly list the files that differ, marked Added / Modified / Deleted
-U <n>, --unifiedLines of context around each change (default 3)
--diff <spec>npm-compatible way to name a side; may be given twice
--jsonOne 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:

terminal
bun pm whoami

hash#

To generate and print the hash of the current lockfile:

terminal
bun pm hash

To print the string used to hash the lockfile:

terminal
bun pm hash-string

To print the hash stored in the current lockfile:

terminal
bun pm hash-print

cache#

To print the path to Bun's global module cache:

terminal
bun pm cache

To clear Bun's global module cache:

terminal
bun pm cache rm
bun pm cache rm --dry-run   # print the directories that would be deleted

This 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:

terminal
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 and noatime mounts normally do not record that.
  • The next bun install that needs a removed package downloads it again. Keep this in mind before bun 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_modules tree 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:

terminal
# 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-lockfile
  • pack writes every package of bun.lock that is in the local cache. It warns about packages it had to leave out, except optional packages for another platform.
  • unpack adds 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, pack reads the cache and writes no file, and unpack checks 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:

terminal
bun pm fetch
bun pm fetch v1.5.0 (ca7428e9)

Fetched 42 packages into cache [1.20s]
Cache: /home/user/.bun/install/cache
  • Dependencies resolve the way bun install resolves them, but node_modules is not created, lifecycle scripts do not run, and package.json and 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.json and 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 with COPY package.json bun.lock ./ and RUN bun pm fetch is only rebuilt when one of those two files changes. Also copy bunfig.toml and .npmrc if they set a registry. Cache entries are keyed by the registry's host, so without them the later bun install downloads everything again. The layer also needs any patch files and local tarballs in the lockfile, and any workspace that the root package.json depends on with workspace:.
  • A later bun install (or bun install --offline) installs from the cache. Without a lockfile, --offline can't tell which commit a github: 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 / --cpu to fetch them.
  • -g fetches the dependencies of globally installed packages.
  • --dry-run prints 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 install that follows.

migrate#

To migrate another package manager's lockfile without installing anything:

terminal
bun pm migrate

Pass --dry-run to run the migration without writing bun.lock or package.json.

untrusted#

To print current untrusted dependencies with scripts:

terminal
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:

terminal
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 writing package.json and bun.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:

terminal
bun pm default-trusted

See the current list on GitHub.

version#

To display current package version and help:

terminal
bun pm version
bun 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:

terminal
bun pm version patch
v1.0.1

Supports 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:

terminal
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 element

Examples:

terminal
# 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 issues

With --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.