Bun package manager and build commands for this OwlMeans monorepo — workspace filters, tsc -b builds, from-clean rebuilds, and the nested node_modules copies that shadow the root. Use when installing packages, building, watching, running scripts, or diagnosing a build/test failure that makes no sense against the source.
$npx -y skills add owlmeans/common --skill bun --agent claude-code
Installs into .claude/skills of the current project.
Are you the author of Bun?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/owlmeans-bun)
---
name: bun
description: Bun package manager and build commands for this OwlMeans monorepo — workspace filters, tsc -b builds, from-clean rebuilds, and the nested node_modules copies that shadow the root. Use when installing packages, building, watching, running scripts, or diagnosing a build/test failure that makes no sense against the source.
allowed-tools: Bash(bun *)
---
# Bun — OwlMeans Monorepo
This monorepo is managed with **Bun** — always `bun`, never `yarn` or `npm`. The root pins
`packageManager: bun@1.4.2`; every linked consumer repo pins the same version. `bunfig.toml` sets
`linker = "hoisted"` and a 15s default `bun test` timeout. A package whose specs need longer
raises it in its own `bunfig.toml`, and `redis-queue` is the only one that does (60s, because its
specs drive a real broker end to end); its file repeats `linker = "hoisted"` next to the timeout.
The one other `bunfig.toml` under `packages/` is the scaffold seed in `create-app/template`, which
sets the linker and no timeout.
## Package Management
- Install all workspace deps: `bun install`
- Add dep to specific workspace: `bun add <pkg> --cwd packages/<name>`
- Add dep to root: `bun add <pkg>`
- Lock file: `bun.lock` (untracked — listed in `.gitignore`)
- Workspace config: `workspaces: ["packages/*"]` in root `package.json`
- Pin a transitive dependency through root `overrides` (Bun's field — not yarn's `resolutions`);
the standing entry is `"react-router": "^8.*"`
## Building
- Build all packages: `bun run build` from root
- Build one package: `bun run build` from inside `packages/<name>`, or
`bun run --filter '@owlmeans/<name>' build` from root
- Each package compiles with: `tsc -b` (no bundler, pure TypeScript)
- Output: `packages/<name>/build/` — what every consumer loads (`main`, `types`, `exports`). Linked
consumers (internal, viable-agent, viable, static) see a change only after it is rebuilt here, and
a running environment only after it reloads (viable: `sh deploy/redeploy.sh`).
- Watch mode: `bun run watch` → `tsc -b -w --preserveWatchOutput --pretty` per package
- Dev mode: `bun run dev` → nodemon re-running `tsc -p ./tsconfig.json` on `src` changes. Each
package's `dev` script opens with a `sleep <n>` whose value staggers it against the others, so a
parallel dev run does not start ~100 compilers in the same second. Keep the stagger when editing
one.
## Root Scripts
```json
"dev": "bun run --filter '@owlmeans/*' --filter '!@owlmeans/_tpl' --parallel dev",
"build": "bun run --filter '@owlmeans/*' --filter '!@owlmeans/_tpl' build",
"watch": "bun run --filter '@owlmeans/*' --filter '!@owlmeans/_tpl' --parallel watch",
"test": "bun run --filter '@owlmeans/*' --filter '!@owlmeans/_tpl' --filter '!@owlmeans/dep-config' test"
```
## Rules & Gotchas
- Syntax: `bun run --filter <pattern> <script>` — NOT `bun --filter ... run <script>`
- Negation filter alone (`'!@owlmeans/_tpl'`) matches nothing — always pair with a positive filter
- `_tpl` package is named `@owlmeans/_tpl` in its `package.json` — always exclude it from builds
- There is no `.yarnrc.yml` and no yarn/npm lockfile here; do not add one
- `bun install` should finish with **no** peer-dependency warnings — treat one as a range to fix,
not noise (see the shadowing section below)
- `bun run --filter` does **not** order builds topologically here — the workspace graph has SCCs
(see `tree.md`), so every package's `tsc -b` starts in parallel. Incremental builds are fine
because `build/` already exists; a build from clean needs the command repeated until it exits 0
(typically 2–3 passes). Never read a single failing pass from clean as a source error.
- A clean rebuild must delete **two** kinds of artifact. `build/` output, and each package's
incremental state at `packages/<pkg>/tsconfig.tsbuildinfo` — **outside** `build/`. Leaving the
`tsbuildinfo` behind makes `tsc -b` skip work, so the build is not really from scratch. A `build/`
directory may hold a tracked `.gitkeep`; keep it:
```bash
for d in packages/*/build; do find "$d" -mindepth 1 -not -name .gitkeep -delete; done
find packages -name 'tsconfig.tsbuildinfo' -not -path '*/node_modules/*' -delete
```
A genuine from-clean run converges in about three passes (order of ~2700 → single digits → 0).
- `bun.lock` is **gitignored**, so it never appears in a merge conflict and any `bun install`
silently re-resolves every floating range. After one, re-check the versions that matter rather
than assuming the tree is unchanged.
- `dep-config` is config-only and declares no `build` script; every other package under `packages/`
builds. A directory there holding only `build/` and `node_modules/` with no `package.json` is an
untracked leftover from a removed package, not a workspace member — delete it rather than
debugging why it will not compile.
- Several packages declare a `test` script but ship no `tests/` directory, so `bun run test` reports
`Test filter "./tests" had no matches` and exits 1 for each: `auth-otp`, `mailer`,
`server-mailer-mailgun`, `web-auth`. Expected, not a regression — read the
per-package pass/fail counts, not the aggregate exit code.
- **The pinned Bun floor is load-bearing for MongoDB.** `bson@7.3.x` runs a static initializer that
calls `process.getBuiltinModule('v8').startupSnapshot.isBuildingSnapshot()`, and Bun implements
that call from **1.4.0** on. Every repo here pins `packageManager: bun@1.4.2`, so the driver's own
`bson: ^7.2.0` range resolves freely and **no repo carries a `bson` override** — do not add one.
Every runtime that loads `mongodb` must be at 1.4.0 or above: the local shell, CI, and the
`oven/bun` image in the pod. Below it `import 'mongodb'` throws `NotImplementedError: node:v8
isBuildingSnapshot is not yet implemented in Bun` before any OwlMeans code runs, so the
`mongo`/`mongo-resource` suites die at import before their env gate can skip them, while the build
and every other unit suite still pass — the break surfaces only at runtime. Check the runtime, not
the lockfile, before lowering one:
```bash
bun -e "import('./node_modules/mongodb/lib/index.js').then(()=>console.log('OK')).catch(e=>console.log(e.code))"
```
## No `@owlmeans/*` entry inside a package's own `node_modules`
A workspace package never holds `@owlmeans/*` in its own `node_modules` — neither a real directory
(a published tarball: see the shadowing section below) nor a symlink to a sibling package. The
symlink form is what an install with Bun's **isolated** linker leaves behind: any repo that lists
this monorepo's packages as workspace entries and runs `bun install` without `linker = "hoisted"`
in its `bunfig.toml` writes one link per workspace dependency into
`packages/<pkg>/node_modules/@owlmeans/` here. A later hoisted install neither records nor prunes
them. Under `bun --preserve-symlinks` — how the viable dev pods run — a dependency resolved from
`node_modules/@owlmeans/api/node_modules/@owlmeans/error` is a different module from
`node_modules/@owlmeans/error`, so `instanceof`, class-keyed registries and error types split in two.
Every consumer repo that links this one keeps `linker = "hoisted"` in its root `bunfig.toml`. The
check is part of the pin audit in the library-manager harness, so every release pre-flight runs it
and fails on a "nested @owlmeans copy" row:
```bash
# in the library-manager workspace — exit 12 lists every nested entry
bun run scripts/bump-deps.ts --pins-only
```
The same question, from this repo, must print nothing:
```bash
find packages/*/node_modules/@owlmeans -mindepth 1 -maxdepth 1 2>/dev/null
```
Fix: delete each listed entry (`rm` on the link itself, never a trailing slash), remove the
now-dangling `node_modules/.bin` links that pointed through it, then `bun install`.
## Troubleshooting: nested `node_modules` copies that shadow the root
**The single most common cause of inexplicable build and test failures in this repo.** Bun puts a
real directory in `packages/<pkg>/node_modules/<dep>` whenever that package's declared range cannot
be served by the hoisted root copy. It wins over the root for anything resolved from inside that
package, and a later `bun install` will not prune it. Five faces of the same bug, all observed:
| Symptom | Shadowed dep | Real cause |
|---|---|---|
| `TS2305 Module '@owlmeans/x' has no exported member 'Y'` — but `packages/x/build/*.d.ts` is fresh and correct | `@owlmeans/*` | published tarball of an older version, from a mid-version-bump install |
| `TS2769 No overload matches this call` on MUI `Stack`/`Box` props in one package only | `@mui/material` | a `devDependencies` range that outran the `peerDependencies` range — the package typechecks against a major it does not declare support for |
| Browser specs all fail with `Target page, context or browser has been closed`, browser log shows `Incompatible React versions` | `react-dom` | stale nested copy left one patch behind the root, and React demands exact-equal `react`/`react-dom` |
| One nested copy per package — `packages/*/node_modules/typescript`, at an older major than the root | `typescript` | a **build-time** dep whose range moved: `bun install` adds the nested copy while the ranges disagree and never prunes it afterwards. Nothing fails loudly — each package compiles with its own `tsc`, so it shows up only as `.d.ts` typechecked by the wrong compiler. Sweep the stale copies after raising the range |
| `TS2322 … Two different types with this name exist, but they are unrelated` on plain `<div>` props or `CSSProperties` | `@types/react` | the two repos resolved different patches of the same `^` range. Type identity is per-declaration-file, so one patch apart is enough. `bun update @types/react @types/react-dom` in **every** repo, then confirm they match |
**A linked repo may be on a different compiler major, and that is not a copy to sweep.** `common`,
`internal`, `viable` and `viable-agent` all declare `typescript: ^7.0.2`. `static` lists five of
this repo's packages as workspace entries (`dep-config`, `consent`, `web-consent`, `web-gtm`,
`astro`) while declaring `typescript: ^5.8.3` in its own manifests, so wherever its
`libraries/common` link is in place a TypeScript 5 compiler reads this repo's emitted `.d.ts`.
Read the consumer's own manifest before deciding a nested copy is stale, and keep emitted
declarations readable by the oldest compiler a linked repo declares.
Diagnose — list every duplicate, not just the one you suspect:
```bash
find packages/*/node_modules -maxdepth 2 -name package.json \
| while read f; do echo "$(bun -e "console.log(require('$f').name, require('$f').version)") <- $f"; done
grep -c 'owlmeans/[a-z-]*@0\.' bun.lock # should be 0 (workspace only)
```
Fix — delete the nested copy and reinstall; then fix the range that invited it, or it comes back:
```bash
rm -rf packages/*/node_modules/<dep>
bun install
```
**Keep `devDependencies` inside the `peerDependencies` range.** A package declaring
`peerDependencies: { "@mui/material": "^7.*" }` must dev-depend on `^7.*` too. Dependabot's
dev-dependency group bumps do not respect peer ranges, so check that pairing whenever one lands.