A source-distribution component registry for Beast and Octane, in the shape of a shadcn-compatible registry. Components are not installed as a runtime dependency — the CLI copies the source into your project, where you own and edit it.
bun install
bun run dev # serves the component showcase at http://127.0.0.1:3000
bun run check # typecheck + test + build
bun run check:ci # typecheck + build (used by CI and CLI publishing)bunx @beastjs/cli@0.3.2 table initCopies a standalone spreadsheet viewer with table controls, local parsers, typed icons, and styles into a Beast + Octane project. See docs/table.md for setup, options, supported toolchains, and limits.
| Path | Purpose |
|---|---|
registry.json |
The catalog: one entry per item, with its files, npm dependencies, and registry dependencies. |
packages/registry/ |
The component sources themselves (ui/, lib/, styles/) plus the shared Zod schema and path helpers. |
packages/cli/ |
The beast-ui CLI: init, list, add, icons init, table init. |
apps/web/ |
The component showcase: live previews of every registry component, built with Rsbuild. |
apps/docs/ |
The docs site, to become the markdown documentation. In development it also serves the built registry at /r/*.json. |
apps/registry/ |
The Cloudflare Worker that serves the registry in production. |
scripts/build-registry.ts |
Compiles registry.json into apps/registry/public/r/ payloads. |
scripts/registry-import/ |
bun run registry:import: wires staged components into the registry and showcase. |
scripts/preview-gen/ |
bun run showcase:preview: generates and typechecks showcase previews. |
docs/adding-components.md |
Usage guide for registry:import and showcase:preview. |
packages/icons/ |
@beast-ui/icons: the icons the web and docs apps draw, one .svg per icon, and the Icon component. |
scripts/build-icons.ts |
bun run icons:build: turns a folder of .svg files into an icons.ts module. Other apps copy it. |
scripts/migrate-icons.ts |
Moves an app's older icons.ts map to .svg files, once per app. |
docs/icons.md |
How icons work, and how to set them up or move them over in any app. |
tests/registry.test.ts |
End-to-end coverage of the build, the resolver, and the installer. |
The fastest path is the two interactive commands. The full walkthrough is in docs/adding-components.md: every prompt, the conventions they rely on, a worked example, and troubleshooting.
# 1. Put finished .btsx files in staging/ (git-ignored), then:
bun run registry:import # adds each one to the registry, then offers to write its preview
bun run showcase:preview # writes any missing or starter previews
bun run showcase:preview button # regenerates one component's preview
# 2. Check the pages, then everything:
bun run dev
bun run checkregistry:importgoes throughstaging/one file at a time, dependencies first. It:- maps imports to registry and npm dependencies;
- rewrites paths that would break once installed;
- asks for the name, title, description, base component, dependencies, and icon;
- writes the source, the
registry.jsonentry, new packages, and a registered preview, then removes the file fromstaging/.
showcase:previewreads the component's props with the TypeScript checker. It builds a preview from your answers: a controlled value or press counter, a row per union prop, and disabled and custom examples. It compiles and typechecks the preview before writing it.
Both accept --help. Enter accepts a default, - means none, and Ctrl+C stops
without touching the component in progress.
- Write the source under
packages/registry/ui/,lib/, orstyles/. UI files are.btsx, lib files are.ts, style files are.css— the installer rejects any other extension for a given type. - Import the class helper as
@/lib/utils. That exact specifier is what the installer rewrites to the consuming project's configuredaliases.utils, so a relative import would silently ship a broken path. - Add an entry to
registry.jsonwith itsdependencies(npm) andregistryDependencies(other items). The build fails on unknown names, duplicate names, or dependency cycles. - Run
bun run check. The build rejects relative imports that leave a file's install root, such as../lib/utilsfrom a UI file.
A variant is a separate UI item that builds on a base component, such as
button-ripple and button-bouncy on button. Visual-only differences belong
in the base component's cva variants; behavior and extra dependencies belong
in a variant item, so users of the base do not install them.
- Import the base by relative path (
import Button from "./button.btsx"). Both files install intopaths.ui, so the path holds in the consuming project. - In
registry.json, list the base inregistryDependenciesand mark the item with"meta": { "variantOf": "button" }. The build checks that the base exists, is aregistry:uiitem, is not itself a variant, and is listed as a dependency. listin the CLI and the showcase sidebar show variants under their base, and installing a variant installs its base first.
The installer strips packages/registry/ui/, packages/registry/lib/, or
packages/registry/styles/ from each file's path according to its type. The short
roots ui/, lib/, and styles/ are also supported. Everything beneath that
root is preserved: packages/registry/lib/internal/value.ts installs to
<paths.lib>/internal/value.ts. Paths without one of these prefixes are already
relative to the configured destination. Use relative imports within a type's
directory tree; use configured aliases when importing across destination roots.
The CLI is published as @beastjs/cli
and installs from the hosted registry at
https://beast-ui-registry.beastjs.workers.dev/r by default. Run it inside an
existing Beast + Octane app with a package.json; init does not scaffold an
app. Node 22.22.2 or newer is required.
npx @beastjs/cli init
npx @beastjs/cli add buttonTo try registry or CLI changes before they are released, run both from this repository instead. Start a local registry in one terminal:
bun install
bun run registry:devIn a second terminal, also at the beast-ui repository root, initialize and install
into your app (replace ../my-app with its relative or absolute path):
bun run cli init --cwd ../my-app --registry http://127.0.0.1:8787/r
bun run cli list --cwd ../my-app
bun run cli add button --cwd ../my-app --dry-run
bun run cli add button --cwd ../my-appinit writes beast-ui.json (registry URL, destination paths, utils alias,
package manager, detected from the lockfile). add resolves the full dependency
closure, plans every file before writing any of them, and refuses to clobber a
modified file without --overwrite.
The consuming project needs Octane 0.4 (the components' @octanejs/*
dependencies require it), Tailwind CSS v4, a @/* alias resolving to src/*
in both TypeScript and the bundler, and src/styles/theme.css imported after
@import "tailwindcss".
scrubfield and squishy are built on @octanejs/motion and render on the
client only; Octane's server renderer does not yet provide the context API that
motion uses.
For example, in src/style.css:
@import 'tailwindcss';
@import './styles/theme.css';Then use the installed component in a .btsx file:
import Button from '@/components/ui/button.btsx'
Button(variant='outline' onClick={() => console.log('Clicked')}) Click me
init creates beast-ui.json in the target project. Review it before adding
components if your project uses different directories or aliases:
{
"registry": "https://beast-ui-registry.beastjs.workers.dev/r",
"paths": {
"ui": "src/components/ui",
"lib": "src/lib",
"styles": "src/styles"
},
"aliases": { "utils": "@/lib/utils" },
"packageManager": "bun"
}Paths are relative to the target project. aliases.utils must resolve to the
installed utility file through your TypeScript and bundler configuration. The
CLI rewrites that import but does not configure either tool for you.
The package manager is detected from Bun, pnpm, or Yarn lockfiles, falling back
to npm. Override detection during initialization with
--package-manager bun|npm|pnpm|yarn. An existing config is never overwritten;
edit it directly to change settings.
| Command or option | Behavior |
|---|---|
init |
Create configuration using the hosted registry. |
init --registry <url> |
Use another registry; the URL must point to the directory containing the JSON items. |
list |
Show available items from the configured registry. |
list --registry <url> |
List items from another registry. Without a config, list uses the hosted registry. |
add <items...> |
Copy one or more items and their registry dependencies; install their npm dependencies. |
-c, --cwd <path> |
Select the target project; defaults to the current directory. |
-r, --registry <url> |
Sets the registry for init; overrides it for a single list or add invocation. |
add -y, --yes |
Create beast-ui.json without asking when it is missing. |
add --dry-run |
Fetch and validate items, then show changes without writing files or installing packages. |
add --overwrite |
Allow replacement of differing existing source files. |
add --skip-install |
Copy source only; print the npm dependencies to install yourself. |
--help |
Show help; also available for each command. |
Examples:
# Browse the hosted registry before initializing an app
npx @beastjs/cli list
# Add several items (shared dependencies are resolved once)
npx @beastjs/cli add utils theme
# Preview an update that would replace edited source files
npx @beastjs/cli add button --overwrite --dry-run
# Apply that update
npx @beastjs/cli add button --overwrite
# Copy sources and handle npm dependencies separately
npx @beastjs/cli add button --skip-install
npx @beastjs/cli add --helpAdding button also installs utils and theme. Files that already contain
the code being added are left in place, even with different quotes, semicolons,
or whitespace; files with different code stop the operation unless
--overwrite is set. Package installation still runs on repeated adds unless
--skip-install is set. Without beast-ui.json, add offers to create it, or
creates it without asking when you pass --yes.
To run an unreleased CLI under Node, build it with
bun run --cwd packages/cli build, then run
node /absolute/path/to/beast-ui/packages/cli/dist/index.js --help from any
directory.
apps/web renders every registry:ui item from registry.json at
/components/<name>, with navigation built from the same catalog. Each
component needs a preview at apps/web/src/previews/<name>-preview.btsx,
registered in apps/web/src/previews/index.ts; a test fails until both exist.
Preview files end in -preview because Beast names a file's component after
the file, and a preview named button.btsx would shadow the Button it imports.
To write or regenerate a preview, run bun run showcase:preview [name...].
See Writing previews. A test
compiles and typechecks every committed preview, because the web typecheck
does not read .btsx files.
The showcase is a single-page app served by its own Cloudflare Worker,
beast-ui-web, from apps/web/dist (see apps/web/wrangler.jsonc). Unknown
paths serve index.html, so routes such as /components/button survive a
reload, and hashed files under /static/ are cached as immutable. Wrangler runs
the web build before every upload. Create the Worker in Workers Builds with the
same settings as the registry, using these commands:
| Setting | Value |
|---|---|
| Deploy command | bun run web:deploy |
| Non-production branch deploy command | bun run web:preview |
The registry is static JSON served by a Cloudflare Worker with
static assets; no
Worker script runs. apps/registry/public/_headers allows cross-origin reads
and makes clients revalidate on every request, and / redirects to
/r/registry.json.
bun run registry:dev # build, then serve at http://127.0.0.1:8787/r
bun run registry:deploy # build, then deploy (needs a Cloudflare login)
bun run registry:preview # build, then upload a preview versionWrangler runs the registry build first (build.command in
apps/registry/wrangler.jsonc), so every upload ships the current items.
Cloudflare Workers Builds deploys the Worker from this repository: main
deploys to production, and other branches upload preview versions with their
own preview URLs. Configure the build under Workers & Pages →
beast-ui-registry → Settings → Build:
| Setting | Value |
|---|---|
| Root directory | / (the repository root, where bun.lock is) |
| Build command | bun install --frozen-lockfile |
| Deploy command | bun run registry:deploy |
| Non-production branch deploy command | bun run registry:preview |
| Build variable | BUN_VERSION = 1.4.0 (older Bun releases cannot read bun.lock) |
The deploy commands run Wrangler from apps/registry with the version pinned
in bun.lock. The Worker is served at
https://beast-ui-registry.beastjs.workers.dev, the CLI's default registry. To
use your own domain, add a routes entry to apps/registry/wrangler.jsonc and
update DEFAULT_REGISTRY in packages/cli/src/utils/config.ts.
From the repo root, bump the CLI version and update bun.lock:
bun run cli:version # patch (default)
bun run cli:version minor
bun run cli:version major
bun run cli:version 0.4.0 # exact versionThe command updates packages/cli/package.json and the lockfile without
creating a commit or Git tag. To release, commit the changes in a PR and merge it.
.github/workflows/publish-cli.yml runs on merges to main that change that
file. When the version is not yet on npm, it runs bun run check:ci, publishes
@beastjs/cli with provenance, and creates the cli-v<version> tag and GitHub
release with generated notes. A version already on npm is skipped, so the
workflow is safe to re-run from the Actions tab. It needs an Actions secret
NPM_TOKEN with publish access to the @beastjs scope.
These are enforced by the schema and the path helpers, and covered by tests:
- Registry item names are lowercase and hyphenated;
registryis reserved. - File paths are relative, with no traversal, drive letters, or backslashes.
- Destinations are resolved segment by segment and refuse to follow symlinks.
- Registry URLs must be HTTP(S) with no credentials, query, or fragment.
- npm dependency strings must look like package names with optional semver ranges — never flags or URLs, which are passed to the package manager.
- A failed catalog build leaves the previously published output untouched.
Record changes in CHANGELOG.md.
beast-ui is released under the MIT License.
