bun-manage-workspace
v0.4.2
Published
Bun Multi-Workspace CLI: catalog updater, graph-aware script runner with cache, graph diagnostics
Maintainers
Readme
bun-manage-workspace
A friendly CLI for managing bun workspaces — it fills the gaps in bun's native workspace support. Zero runtime dependencies, bun-only.
Install it as bun-manage-workspace, call it as bmw.
What it does
bmw run— run a script across your workspace, in dependency order, with caching. Need to build every workspace dependency of the app you're working on before building the app itself? That's the one-liner bun doesn't have.bmw update— interactively update your catalogs (catalog/catalogsin the rootpackage.json), whichbun update --interactivedoesn't touch.- Plus small helpers:
bmw graph,bmw why, andbmw cache.
Getting started
bun install -g bun-manage-workspaceOr skip the install and run it directly:
bunx bmw <command>You need bun, and a repository whose root package.json declares workspaces.
Then, from anywhere in the workspace:
bmw run build # build the current package's deps, then the package
bmw run test --all # run "test" everywhere
bmw update # pick which catalog entries to bumpThat's the 80% use case. The rest of this README is the fine print.
bmw run <script> [flags]
Runs a package.json script across the workspace dependency graph.
By default, everything starts at once, but each package waits for its workspace dependencies to finish first — a package's script spawns the moment all of its in-plan dependencies have completed, with no global wave barriers. If a package fails, everything that (transitively) depends on it is skipped and the run aborts with that exit code.
What gets run?
| Mode | What runs |
| --- | --- |
| default (inside a package) | all transitive workspace dependencies of the cwd package, then the package itself |
| default (at the workspace root) | all non-private packages |
| --to, -t <pkg> | the package and its workspace dependencies (from any cwd, repeatable) |
| --from, -f <pkg> | the package and its dependents (from any cwd, repeatable) |
| --only, -o <pkg> | just the package, no dependencies (repeatable) |
| --deps, -d [pkg] | only the package's dependencies, without the package itself (default: cwd package) |
| --dependents, -D | default plan plus the packages that (transitively) depend on cwd, after it |
| --all, -a | all non-private packages, regardless of cwd |
| --no-deps | only the cwd package |
| --parallel, -p | run everything at once, without waiting for dependencies |
| --no-cache, -C | skip cache reads (entries are still refreshed) |
| --dry-run, -n | print the plan without executing (shows cache hits) |
| --watch, -w | re-run the plan whenever a package in it changes |
Selectors accept a package name or a path (relative to cwd or absolute):
bmw run build --to packages/api works the same as --to @acme/api.
Values also work attached (-t@acme/api, -t=@acme/api), and boolean short
flags cluster (-an = --all --dry-run).
Repeated selectors union: bmw run build --to @acme/api --to @acme/web builds
both apps and everything they need.
Packages without the script are skipped with a note. In the default cwd-relative mode, the script must exist in the cwd package.
bmw run build # build deps, then the current package
bmw run build --dependents # ...and everything that depends on it
bmw run test --all # run "test" across the whole workspace
bmw run build --dry-run # show the plan without executingWatch mode
bmw run build --watch runs the plan once, then watches every package in it.
On any change (respecting the same ignore rules as the cache: node_modules,
dist, .bmwignore, …) it re-runs the plan — and thanks to the cache, only
the changed packages and their dependents actually rebuild. Changes arriving
mid-run are coalesced into a single follow-up run. Uses native file watching,
with a polling fallback on platforms without recursive watches.
Caching
Results are cached in <root>/node_modules/.cache/bmw/run/. The cache key
combines the script name, a hash of the package's files, the workspace-root
manifest and lockfile (package.json, bun.lock/bun.lockb, bunfig.toml —
so a dependency bump followed by bun install invalidates everything), and
the content keys of its workspace dependencies — so changing a dependency
automatically invalidates its dependents. Cache hits are skipped:
▸ @acme/core — unchanged (cache), skippedHashed files exclude node_modules, .git, .turbo anywhere in the tree,
plus the output directories dist, build, out, .next and coverage at
the package root. Want more ignores? Add globs (one per line, #
comments allowed) to a .bmwignore file in the workspace root and/or in a
package directory. Override the cache location with the BMW_CACHE_DIR
environment variable.
bmw update [flags]
An interactive updater for catalog dependencies — like
bun update --interactive, but for catalog / catalogs in the root
package.json.
Use the arrow keys to move, space to select (nothing is selected by
default), a to toggle all, enter to confirm, esc to cancel. Entries are
grouped with aligned columns and colored by bump kind (major red, minor cyan,
patch green, prerelease yellow).
The bump level is switched live in the picker: p shows only patch bumps
(same major.minor), m stays within the current major (the default), l
offers the latest version including major jumps. Selections are preserved
when switching levels.
| Flag | Effect |
| --- | --- |
| default | bump within the current major, interactive picker (p/m/l switch the level) |
| --level <lvl> | bump level for non-interactive runs: patch, minor or latest (default minor) |
| --all | non-interactive, update everything (automatic outside a TTY) |
| --registry <url> | override the default npm registry |
Custom registries (e.g. Verdaccio) are handled by bun itself — metadata is
fetched via bun info, so the same bunfig.toml / .npmrc configuration
that bun install uses (scoped registries, auth tokens) applies here too.
Only the root package.json is modified, and its formatting (indentation,
trailing newline, key order) is preserved. Run bun install afterwards to
update the lockfile.
bmw cache [clean]
Shows the cache location, entry count, and size. bmw cache clean removes all
cached run results — the next bmw run rebuilds everything from scratch.
Respects BMW_CACHE_DIR.
bmw graph [--all]
Prints the workspace dependency tree from the cwd package — or every tree in
the workspace with --all. Cycles and already-shown packages are marked.
bmw why <package>
Shows the reverse dependency chains leading to <package> — who depends on
it, and through what.
bmw why @acme/core
# Packages depending on "@acme/core":
# @acme/app → @acme/utils → @acme/core
# @acme/web → @acme/coreWorkspace linking rules
A dependency on another workspace package links (joins the graph) when its
specifier is workspace:* / workspace:^ / workspace:~ /
workspace:<range>, or a plain semver range satisfied by the local version.
catalog: references and unsatisfied ranges are treated as external
(registry) dependencies. A workspace: specifier that doesn't match the local
version is a configuration error and aborts with a clear message.
Development
git clone <repo> && cd bun-manage-workspace
bun install
bun test # unit + integration tests (fixture monorepo)
bun x tsc --noEmit # typecheck
bun run src/cli.ts --helpPublishing
Publishing is handled by GitHub Actions via npm.
License
MIT
