@vervian/choiceui
v0.16.0
Published
Governance for design systems that live as code. Extract a manifest of your Tailwind tokens and components, enforce it for humans and AI, and generate browsable design-system pages.
Downloads
514
Maintainers
Readme
choiceui
The command-line tool for ChoiceUI. It makes your design system, real Tailwind components in your repo, the enforced source of truth for every consumer of the codebase, human or AI.
You point it at a repo. It extracts a manifest of your tokens and components, generates enforcement (a Claude Code skill, CLAUDE.md rules, an ESLint gate, and a CI check), builds browsable design-system pages, and serves your components as a private shadcn registry. After that, the codebase cannot drift from the design system without CI catching it.
Prerequisites
ChoiceUI governs Tailwind design systems that live as code. Before you start, your project should have:
- [ ] Tailwind CSS (v3 with a
tailwind.config.*, or v4 configured in CSS with@import "tailwindcss"/@theme). - [ ] A global stylesheet where your tokens live, at one of:
src/app/globals.css,app/globals.css,styles/globals.css,packages/ui/src/globals.css,src/globals.css, orglobals.css. Colors are read from@themeas either numbered scales (--color-primary-500) or single-value semantic tokens (--color-primary,--color-muted,oklch/hsl/rgb,var()indirection). Files your stylesheet@imports are followed too, so aglobals.cssthat imports atheme.cssworks. - [ ] A components directory (e.g.
src/components/uiorcomponents/ui). Components usingclass-variance-authority(cva) ortailwind-variants(tv) get their variants captured; others are still tracked. shadcn matches all of this out of the box, but it is not required. - [ ] A
@/path alias intsconfig.json(recommended), since the default component import path is@/components/ui.
init checks all of these and tells you what is missing.
Install
npm install -D @vervian/choiceui
# or run without installing
npx @vervian/choiceui <command>The package is scoped, the binary is not: once it is installed in the project,
every command below works as plain npx choiceui <command>.
Quick start
From your project root, run these in order. Each step is independently useful.
# 1. Inspect the project and generate a real manifest from your tokens/components
npx choiceui init
# 2. (init already generated it, but regenerate any time your design changes)
npx choiceui manifest
# 3. Generate the enforcement kit: Claude skill, CLAUDE.md, ESLint config, CI
npx choiceui guard
# 4. Generate browsable design-system pages into your app (visit /design-system)
npx choiceui showcase
# 5. Generate an installable shadcn registry under public/r/
npx choiceui registry
# Anytime: scan for off-system code
npx choiceui doctorAfter guard, wire the generated ESLint config into your root config and install
the parser it needs:
npm install --save-dev typescript-eslint// eslint.config.mjs
import choiceui from './choiceui.eslint.config.mjs'
export default [ ...yourConfig, ...choiceui ]Commands
choiceui init
Inspect your project and set up ChoiceUI against your real design system.
npx choiceui init
npx choiceui init --yes --skip-link # accept detected defaults
npx choiceui init --components src/ui # point at a components directory
npx choiceui init --css src/styles/theme.css # point at your token file directly
npx choiceui init --project-id abc123 # link a cloud projectMonorepos: run it in the package that holds your components (e.g.
packages/ui). If your tokens are in a non-standard file, pass --css; guard
writes its CI workflow to the repo root using your package manager (pnpm/yarn/
npm), and showcase renders into a real app (a sibling apps/* if the current
package has no app router).
Detects Tailwind, the global CSS, the @/ alias, and the components directory,
then writes choiceui.config.ts and generates choiceui.manifest.json. Exits
with a clear message on an unsupported setup (no Tailwind, or no global CSS).
choiceui manifest
Generate or validate choiceui.manifest.json, the source of truth.
npx choiceui manifest # (re)generate from the codebase
npx choiceui manifest --check # fail (exit 1) if code has drifted from it
npx choiceui manifest --push # upload to the ChoiceUI cloud (needs login)choiceui intake
Govern components that exist in the code but are not yet in the design system.
When ChoiceUI sees an ungoverned component, it makes you decide: adopt it into
the system, allow it as a deliberate one-off, or reject it (must be
replaced). Decisions persist in .choiceui/decisions.json and stick.
npx choiceui intake # interactive: decide each ungoverned component
npx choiceui intake --check # CI: exit 1 if anything is undecided or rejected-but-present
npx choiceui intake --json # emit the list for an agent to decide in bulkAdopted components enter the manifest on the next choiceui manifest. This is the
governance loop: nothing drifts off to the side unnoticed.
In a monorepo, run it from the repo root and point it at the design system's manifest, so components in the apps that consume the system get triaged too:
npx choiceui intake --check --manifest packages/ui/choiceui.manifest.jsonThe managed set still comes from the package's own manifest; only the scan
widens. Decisions are read from both packages/ui/.choiceui/decisions.json and
the repo root, and each new one is written beside the component it is about, so
a repo that triaged its system before never loses those decisions. choiceui
guard generates this form of the CI step automatically.
choiceui guard
Generate the enforcement kit from the manifest.
npx choiceui guard
npx choiceui guard --only eslint,ci # a subset: skill, claudemd, eslint, ci
npx choiceui guard --dry-run # preview without writingWrites .claude/skills/choiceui-design-system/SKILL.md, a marker-delimited block
in CLAUDE.md, choiceui.eslint.config.mjs,
.github/workflows/choiceui-guard.yml, and a .mcp.json entry pointing AI tools
at the MCP server below (merged into any existing .mcp.json).
choiceui mcp
Run the design-system MCP server (stdio) so AI tools like Claude Code and Cursor can query the manifest directly while they generate code, instead of being caught by the gate afterward.
npx choiceui mcpYou normally do not run this by hand: choiceui guard writes the .mcp.json
that starts it automatically. It exposes four tools: list_components,
get_component, list_tokens, and check_classes (validate a Tailwind class
string against the design system). All read the same choiceui.manifest.json.
choiceui showcase
Eject a complete, branded design-system page you own, then vibe-code it.
npx choiceui showcase # eject into <app>/design-system
npx choiceui showcase --route src/app/ds # custom route
npx choiceui showcase --force # re-eject from scratch (discards your edits)Eject and own. The page, shell, live demos, and styles land in your repo and
are yours from that moment. Re-running showcase never overwrites them (it prints
kept (yours)); it only refreshes manifest.data.ts, the inventory the page maps
over at runtime. So a newly captured token or component appears on its own, without
touching anything you vibe-coded. The guard lints the page like any UI code, so you
cannot drift off-token while restyling it. Want the original scaffold back? Re-run
with --force.
Make it yours. Drop a .choiceui/showcase.json to brand the page with your
logo, title, version, a light/dark toggle, and your own written descriptions.
ChoiceUI carries your words; you write them. (A logo is also captured
automatically from a brand/ Logo component or a public/logo.* asset; this
config overrides it.)
{
"title": "Acme Design System",
"version": "v1.0",
"logo": "/logo.svg", // asset path or URL, shown in the header + hero
"themeToggle": true, // adds a light/dark toggle
"lede": "Every component here is a live import from @acme/ui.",
"descriptions": { // per-section and per-component prose
"color": "Grouped by purpose: surfaces, brand, semantic.",
"typography": "Inter on the editorial scale.",
"Button": "All variants and sizes."
}
}Start your dev server and visit /design-system. Every captured component gets a
section with its variant table (read from the data module at runtime). Live demos
live in the owned previews.tsx: simple primitives are scaffolded automatically,
and common shadcn compounds (Dialog, Select, Tabs, DropdownMenu, Sheet, Popover,
Tooltip) come from built-in demo recipes. Add or restyle any demo by editing
previews.tsx directly, or seed one at eject time by dropping a TSX snippet at
.choiceui/demos/<ComponentName>.tsx (it must reference only that component's own
exports). A component with no demo still appears, with an import hint until you add
one.
choiceui registry
Generate an installable shadcn-compatible registry into public/r/.
npx choiceui registryConsumers then run npx shadcn@latest add https://your-app.com/r/<name>.json, or
any AI tool with the shadcn MCP can read the registry index.
choiceui doctor and choiceui reconcile
npx choiceui doctor # scan for off-system code
npx choiceui doctor --fix # apply safe, token-aware fixes
npx choiceui reconcile # guided 3-tier fix (deterministic, AI, ask)Fixes follow your manifest's real token names. A value that cannot be mapped confidently is left for review rather than guessed.
Cloud (optional)
npx choiceui login # authenticate
npx choiceui link --project-id id # link this repo to a cloud projectLogin gates only the cloud sync and the drift dashboard. Everything above works fully offline.
Configuration (choiceui.config.ts)
import { defineConfig } from '@choiceui/core/preset';
export default defineConfig({
// projectId: 'abc123', // optional cloud link
css: 'src/app/globals.css', // global stylesheet with your tokens
components: {
importPath: '@/components/ui', // how consumers import your components
},
// Custom or monorepo layouts: where your components live.
componentPaths: ['packages/ui/src/components', 'apps/web/components/ui'],
});Tokens and components are extracted into choiceui.manifest.json. Treat that
file as generated: regenerate it with choiceui manifest rather than editing it.
Framework support
The one hard requirement is Tailwind. shadcn is the smoothest case because it matches every convention, but any Tailwind project with a global stylesheet and a components directory works.
| Setup | Status |
| --- | --- |
| Next.js App Router + Tailwind + shadcn | Tested |
| Tailwind app with semantic (oklch) tokens, no numbered scales | Tested |
| Monorepo where globals.css imports a shared theme.css | Tested |
| Tailwind v4 (CSS @theme) and v3 (tailwind.config.*) | Tested |
| Next.js Pages Router | Should work (untested) |
| Vite / Remix + Tailwind | Should work for manifest/guard/doctor; showcase emits Next.js pages (untested) |
| Non-Tailwind projects | Not supported |
Troubleshooting
- Empty manifest / "No design tokens". No global CSS was found, or it has no
@themecolors. Confirm your stylesheet is at one of the prerequisite paths. Tokens can be numbered scales (--color-primary-500) or single-value semantic tokens (--color-primary); files you@importare followed. If your tokens live in a non-standard file, pass it:choiceui manifest --css <path>. - "No UI components discovered". Your components are not in a default
location. Set
componentPathsinchoiceui.config.ts(the message lists every path that was searched). - ESLint gate crashes on
.tsx/ "Parsing error: Unexpected token <". The generated config needs a TypeScript parser: runnpm install --save-dev typescript-eslint, and make sure the config is spread into your rooteslint.config.mjs. manifest --checkfails in CI. A tracked component changed. Re-runchoiceui manifestto re-baseline, review the diff, and commit it.
How it stays governed
choiceui.manifest.jsonis the contract: tokens, components, and a source hash per component.- At authoring time, the generated Claude skill and CLAUDE.md rules steer humans and AI agents to reuse components and semantic tokens, and the MCP server lets AI tools query the manifest directly (what exists, what is allowed) as they generate code.
- At PR time, the generated GitHub Actions workflow runs
choiceui manifest --check(structural drift) and the ESLint gate (off-system classes), failing the build on drift.
License
MIT
