Get set up
You need Bun 1.3 or newer. That is the only hard prerequisite for most of the repo — Bun is the runtime, the test runner, and the script runner.bun run test:workers runs under Vitest, whose bin has a Node
shebang, so you need Node installed for that one. Everything else is Bun.
Layout
core imports no sibling package, cli and mcp
import core, and toolbar may reach core for types only. That last rule is what
keeps the toolbar’s runtime dependency-free.
Commands
Run these from the repo root.
Run
bun run ci:validate before opening a pull request. It is exactly what the validate
workflow runs, so a green local run means a green CI run.
Tests
Unit tests are colocated:foo.test.ts next to foo.ts. Anything that crosses a package
boundary goes in e2e/, never in a per-package test.
The Workers suite is the one exception to bun test. Durable Object SQLite has to be
exercised on the runtime it ships to, so e2e/workers/ uses
@cloudflare/vitest-pool-workers, which is real workerd. Those files are named
*.worker-test.ts so root bun test never picks them up. One of its two projects runs
against the actual examples/worker/wrangler.jsonc, so the deployable template is what
gets tested.
The review script
bun run agent:review runs fallow over the repo.
It reports things a line count cannot see: unused files, exports and dependencies, import
cycles, duplication, oversized functions, and cyclomatic complexity.
It also enforces the architecture boundaries described above. They are configured in
.fallowrc.json, not left to convention — adding a runtime core import to a toolbar file
is reported as a boundary violation.
On a branch, prefer bun run agent:review:changed. It reports only findings that are new
relative to main, so the existing baseline does not drown out your diff.
Findings are guidance, not gates. The review is deliberately not wired into CI. If you
ignore one on purpose, say so in the pull request and why.
Conventions that will surprise you
Files have a hard size cap
Files have a hard size cap
No source file exceeds 1,000 lines. Ever.Before adding code to a file, check its length. At 500 lines, stop — do the split
first. Design the decomposition, break the file into focused modules with clear
interfaces, then land your new code in the right one. Make the change easy, then make the
easy change.Split along responsibility boundaries, never arbitrary halves. Each resulting file should
have one purpose and read on its own.There is a check at
tools/hooks/file-size-guard.sh you can wire into your editor or a
local pre-commit hook. It warns at 500 lines and blocks at 1,000.Bun APIs are the default — with exactly two exceptions
Bun APIs are the default — with exactly two exceptions
Reach for Bun directly:
Bun.serve, bun:sqlite, Bun.spawn, Bun.$, Bun.which,
Bun.S3Client. Not the node: equivalents.Two places invert this rule, and both are deliberate:packages/toolbar/src/plugins/*— the Vite and Next dev plugins.- The generated npm launcher shim in
tools/release/.
node:fs, node:path, node:child_process), all of which Bun implements fully.This is not “some users are on Node”. Measured on a Bun-only machine, bunx vite build
and bun run dev both execute plugin code under Node, because Vite’s bin carries a
#!/usr/bin/env node shebang and Bun honors shebangs. Only bun --bun or a bunfig.toml
[run] bun = true gets you Bun. A Bun API in a dev plugin therefore throws for nearly
everyone, including Bun users.So: do not “fix” the node: imports in those two locations, and do not widen the
exception anywhere else. Both files say so in a header comment.Several files are generated — never hand-edit them
Several files are generated — never hand-edit them
The agent skill and the plugin packaging are generated from the CLI’s command tree.
CI re-runs it in
skills/pinbox/SKILL.md, the copies under plugins/ and integrations/, the plugin
manifests, and packages/cli/src/init/plugin-assets.ts all come out of one generator.If you change a command, its description, or a flag, regenerate:--check mode and opens a pull request with the regeneration if the
committed output has drifted.The worker template is mirrored the same way: examples/worker/ must stay byte-identical
to packages/cli/templates/worker/ (the example README carries a two-line provenance
header, and nothing else may differ). tools/validate/template-drift.ts fails CI on any
divergence. Edit the template, then copy it across.Machine output is a versioned contract
Machine output is a versioned contract
The
--json envelope, the exit-code table, the published JSON schema, and the WebSocket
protocol are contracts other people’s tooling depends on. Change them deliberately, and
say so in the changeset. Adding a field is usually fine; renaming or removing one is not.Style and packaging
Style and packaging
- ESM only. Strict TypeScript from the root
tsconfig.base.json; every package extends it. tsc --noEmittypechecks; tsdown owns emit (ESM plus.d.ts).bun build --compileproduces the standalone binaries.- Biome handles lint and format from the root
biome.json. No nested configs. Two-space indent, width 100, double quotes. - Internal dependencies use
workspace:*. Shared tool versions live in the root catalog, referenced ascatalog:. - Build order comes from topological
bun run --filter, never a hardcoded&&chain.
Opening a pull request
1
Branch from main
2
Write the test first
Tests live next to the code, or in
e2e/ if the change crosses packages.3
Add a changeset
Releases are driven by changesets. If your
change affects a published package, describe it:Pick the packages and the bump, and write the line the way a user reading a changelog
would want it.
4
Run the full gate
5
Open the PR
Say what changed and why. If you left a review finding unaddressed, say which and why.
Reporting bugs
Open an issue with the output of:PATH — which is most of what anyone needs to reproduce a problem.
For anything security-related, do not open a public issue. See the
security model page.