@urbicon-ui/design
v8.7.0
Published
The urbicon CLI — version-pinned design validation and design-manifest tooling for projects built with Urbicon UI. Wraps @urbicon-ui/design-engine for editor hooks and CI.
Maintainers
Readme
@urbicon-ui/design
The urbicon CLI — version-pinned design validation and design-manifest
tooling for projects built with Urbicon UI.
It is the local, version-correct half of the Urbicon design loop: the knowledge
and rules are the ones shipped with the @urbicon-ui/* version you installed, and
the filesystem operations (reading/writing your project's design.manifest.md)
run on your machine — things a public, stateless remote MCP server structurally
cannot do. Under the hood it wraps the zero-dependency
@urbicon-ui/design-engine; the same engine backs the remote
validate_design MCP tool, so local and remote verdicts agree.
What the loop buys, measured. One A/B pair on v8.3.1 (2026-08-18): Haiku 4.5
built the same three-page auth app twice, with @urbicon-ui/* installed either
way. One run had this CLI and its edit-time gate wired in, the other had neither.
| | left to itself | CLI + gate wired |
| --------------------------------- | -------------- | ---------------- |
| urbicon validate src/ | 373 errors | clean |
| — raw Tailwind colours | 273 | 0 |
| — hand-written dark: patches | 96 | 0 |
| — focus not focus-visible | 4 | 0 |
| knowledge lookups / package reads | 0 / 8 | 23 / 0 |
| catalog components re-implemented | 72 | 29 |
| blind-judged design quality | 16/40 | 17/40 |
The mechanism is the lookups row: without the CLI the agent reads the installed package and guesses; with it, it asks and gets the answer for the version it actually has.
The last three rows are the honest limits, and they matter more than the
headline. A clean validate means the linter found nothing, which is not the
same as correct — the wired run still shipped text-on-surface-muted, a class
naming no token, which therefore renders as nothing, and the linter missed it.
Re-implementation drops but does not stop: a wired agent still hand-builds
components the catalog ships. And design quality does not move. Two further
findings from the same eval: the loop raises craft scores on no model tier
(three tiers, three arms), and it does not make runs cheaper (two independent
pairs, no advantage either time). Token discipline is what you get; taste and
budget stay with you.
Install
bun add -d @urbicon-ui/design # dev tooling — not a runtime dependencyThis exposes the urbicon command (a self-contained, Node-runnable bundle — no
Bun required at the consumer side).
Running it standalone (no local install). The bin is
urbiconbut the package is@urbicon-ui/design, so a barebunx urbicon …from a project that hasn't installed it fails withGET …/urbicon 404(it looks for a package literally namedurbicon). To run the CLI without a local install, name both the package and the bin:bunx --package @urbicon-ui/design urbicon validate src/ # or: npx --package @urbicon-ui/design urbicon …Inside a project that already has
@urbicon-ui/designinstalled, plainbunx urbicon …resolves fine.
Onboarding a consumer project
bun add -d @urbicon-ui/design # then:
bunx urbicon init # wire the project into the design loopStarting from scratch? The
@urbicon-ui/svadd-on (beta) does the mechanical setup in one line —bunx sv create my-app --add @urbicon-uiinstalls blocks + this CLI and wires the Tailwind stylesheet — and then hands over tourbicon init --hookbelow.
urbicon init is idempotent and non-destructive. It:
- Gives the agent context — inserts a managed
<!-- urbicon:start … -->block intoAGENTS.md(or--agents-file CLAUDE.md) describing the tools, the design loop, and the token rules. The single biggest lever on whether generated UI stays on-system. - Seeds the design memory — scaffolds
design.manifest.md(never overwriting an existing one). - With
--hook, merges the PostToolUse gate into.claude/settings.json; with--ci, writes the design-gate workflow.
After upgrading the library, re-run bunx urbicon init. The block is stamped with the CLI
version that wrote it, and urbicon context — step 1 of the design loop — warns when the block's
content no longer matches the installed CLI's template, so an agent sees the drift and can fix it
itself. (The check is content-based: a release that doesn't change the template stays quiet, and
a verbatim hand-paste of the current template counts as current.) A re-run refreshes the block in
place wherever it lives (AGENTS.md or CLAUDE.md, in your casing); a hook entry or CI workflow
you have customised is kept and reported, never overwritten.
Then run the guided intake — bunx urbicon verb adopt (brownfield) or onboard (greenfield) —
to fill the manifest with this project's design intent. From there an agent can urbicon primer
for the knowledge every task needs (component selection + the token core, one call),
urbicon context to read the intent, urbicon find / get-component to discover the catalog,
urbicon pattern / principles / css-reference for the task-specific rest, compose, and
urbicon validate what it produced.
The component knowledge is local and version-pinned:
@urbicon-ui/designpulls in the@urbicon-ui/design-contentbundle, sofind/get-componentmatch the library version you installed — no extra install, no skew against the latest-only hosted MCP.
Commands
| Command | What it does | Replaces (remote) |
| ------------------------------ | ----------------------------------------------------------------------------------------------------- | --------------------------- |
| urbicon init | Wire a project into the design loop (AGENTS.md block, manifest scaffold, --hook/--ci). | — (local only) |
| urbicon validate [paths...] | Lint .svelte markup against the design rules. The CI gate. | mirror of validate_design |
| urbicon i18n [check] | Audit @urbicon-ui/i18n: parity / unused keys / hardcoded strings / audit (all). | — (local only) |
| urbicon hook | PostToolUse adapter — validate the just-edited file, block on failure. | — (local only) |
| urbicon primer | The always-needed bundle in one call: component selection + the token core. Run it first. | — |
| urbicon find [query] | Fuzzy component discovery over the version-pinned catalog. | find_components |
| urbicon get-component <slug> | A component's API (its llm.txt) from the bundle. | get_component |
| urbicon icons [query] | Icon discovery (no query: the full grouped reference). | find_icons |
| urbicon recipe [id] | Complete Svelte 5 code recipes from the catalog. | get_recipe |
| urbicon guide [slug] | Canonical package guides (auth reference, blocks guide system, migration notes, table scroll models). | urbicon://guide/auth |
| urbicon pattern [name] | Composition patterns per page archetype. | get_pattern |
| urbicon principles | Design heuristics (--topic <t>); --rubric for the judge rubric. | get_design_principles |
| urbicon css-reference [sect] | The token truth: naming, dark mode, override patterns. | get_css_reference |
| urbicon context | Print the project's design.manifest.md summary. | get_design_context |
| urbicon record-decision … | Append an ADR to the manifest. | record_design_decision |
| urbicon sync-manifest | Re-index data-design-pattern markers into the manifest. | sync_design_manifest |
| urbicon verbs | List the design verbs (recipes over the design loop). | the MCP prompts |
| urbicon verb <name> | Print one verb recipe to stdout. | the MCP prompts |
The CLI covers the full knowledge surface locally, so the design loop runs
offline and version-pinned end to end. When an urbicon-ui MCP connection is
also present, prefer the CLI: the remote serves latest, the CLI serves the
version this project installed.
The three manifest commands move off the remote server deliberately: a public remote server has no access to your repo's filesystem, so manifest upkeep belongs on the consumer side (this CLI, or the agent's own write tools).
validate
urbicon validate src/ # lint a whole tree (CI)
urbicon validate App.svelte --strict # fail on warnings too, not just errors
urbicon validate src/ --craft-floor 40 # also fail files scoring < 40/100 on craft
cat Page.svelte | urbicon validate - # lint stdin
urbicon validate src/ --json # machine-readable: { ok, craftFloor, results }
urbicon validate src/ --record # also append a drift entry to the history (CI)validate reads ## Token Overrides from your design.manifest.md (if present)
and treats those token cores as valid, so a token your project defines on top of
Urbicon's is not flagged as hallucinated — the local, manifest-sourced counterpart
to the remote validate_design(extraTokens). Since v6.44 that is a genuine gate
release rather than a warning tweak: token-hallucination is an error
(a token that names nothing renders with no styling at all), so a manifest that
declares your token is what keeps validate at exit 0. Nothing else is relaxed —
the other error rules are unaffected. --record appends one
ValidationHistoryEntry per run to the sidecar design.manifest.history.ndjson
so drift is measurable over time (CI opts in; the editor hook stays silent).
Quoting is not violating. The class rules (raw colours, dark:/focus:,
z-index/motion, hallucinated tokens, deep imports) scan only class-bearing
content — class/*Class* attribute values, string/template literals in
<script> and {…} expressions (tv() configs, slotClasses), and @apply —
never element text content, style= attributes or other string attributes. A
docs page that shows bg-green-500 in prose or a before/after snippet is not
flagged for it. Plain .ts/.js input (a tv-config module) is scanned across
all its literals; for extension-less non-Svelte stdin pass the engine's
mode: 'code'.
Exemptions for deliberately off-system surfaces (a landing poster, a page rendering linter output): suppress specific rules — never everything — via
- an in-file pragma, visible next to what it exempts:
<!-- urbicon-ignore magic-dimension inline-style — reason -->(//and/* */forms work in TS/JS); the em-dash starts the free-text reason; - or a
## Exemptsection indesign.manifest.md, one bullet per path:- `src/routes/+page.svelte` — `magic-dimension`, `inline-style` — reason(a trailing/on the path exempts the subtree).
Suppressions are always visible: the report prints n suppressed with per-rule
counts (--json carries results[].suppressed), a suppression that matched
nothing is marked stale, and an unknown rule id raises an
invalid-suppression warning instead of silently suppressing nothing.
The linter scores two independent axes: correctness (raw
colours, dark:/focus:, hallucinated tokens — deterministic, always the
blocking gate) and craft (20 "looks generic" heuristics — advisory by default,
because they are FP-prone). --craft-floor <n> opts the craft axis into the gate:
any file scoring below n fails, checked per file so one generic page cannot hide
behind clean ones. Leave it off and craft stays informational.
Exit codes — designed for hooks and CI:
| Code | Meaning |
| ---- | ------------------------------------------------------------------------------------------------------------------------------- |
| 0 | Clean, or only warnings/notes |
| 1 | Failed — validate found errors (with --strict, warnings too), or a command could not complete (e.g. a manifest write error) |
| 2 | Usage error — bad flags / unreadable input |
--skip-heuristics runs only the deterministic rules (no distribution notes).
i18n
Audit @urbicon-ui/i18n usage — one check, or audit for all. Run under Bun (it
dynamic-imports .ts locale bundles).
urbicon i18n audit src/ --translations src/lib/translations # parity + unused + hardcoded
urbicon i18n parity --json # data-level locale audit only
urbicon i18n unused --dynamic-keys 'errors.*' # scan, allowlisting dynamic key families
urbicon i18n hardcoded src/ --strict # gate the advisory hardcoded-string lint too| Check | Finds | Gates? |
| ----------- | ----------------------------------------------------------------------------------- | ----------------------------------------- |
| parity | missing/extra keys, empty values, {{param}} drift, malformed/incomplete _plural | errors gate |
| unused | defined keys referenced nowhere (confirmed/suspect) + keys used-but-undefined | used-but-undefined gates; unused advisory |
| hardcoded | literal UI copy in .svelte markup that bypassed i18n | advisory (gate with --strict) |
Config via i18n.audit.json / --config + flags (--translations, --dynamic-keys,
--ignore-keys, --ignore-strings, --base-locale); --json for CI. Backed by the
@urbicon-ui/i18n/audit subpath; the pure data-level auditTranslations also runs as a
Vitest assertion without the CLI.
context / record-decision / sync-manifest
urbicon context # summarise ./design.manifest.md
urbicon context --json # the parsed manifest (+ history + contextBlock) as JSON
urbicon record-decision \
--title "Tabs for settings" \
--decision "Use Tab over Sidebar" \
--rationale "Three groups, shallow nesting"
# Changed your mind? Link both ends instead of leaving two contradictory ADRs:
urbicon record-decision \
--title "Sidebar for settings" \
--decision "Use Sidebar over Tab" \
--supersedes "Tabs for settings"
urbicon sync-manifest # scan ./src for data-design-pattern markers
urbicon sync-manifest --src app --manifest app/design.manifest.mdcontext summarises the whole manifest — the product intent (audience, voice,
references, anti-references), the token overrides, the pattern-usage index, the
recorded ADRs — and, when a *.history.ndjson sidecar exists, the recent
validation-drift trend. All commands default the manifest to
./design.manifest.md and the scan root to ./src; override with --manifest
/ --src.
The manifest is a plain Markdown file with these sections (urbicon creates a
scaffold on first write):
## Product Intent
**Audience:** who uses this — context, constraints, expertise
**Voice:** three adjectives, comma-separated
**References:** / **Anti-references:** bullet (or comma) lists
## Token Overrides
- `surface-brand` # project tokens `urbicon validate` should accept
## Pattern Usages # auto-generated by sync-manifest
## Design Decisions # append-only ADRs from record-decision, newest first by dateThe ADR log is append-only and ordered by --date, so a back-dated entry lands
where it belongs rather than on top. --supersedes "<title>" marks the named
entry superseded and links both ends; urbicon context then lists it as
history instead of as a current stand (it stays in the file — seeing that a stand
was tried and dropped is the point of an append-only log). An unknown or
ambiguous title fails loud rather than recording a dangling link.
Design verbs
Ten recipes that string the knowledge, the linter, and the manifest into one loop —
the same single source that is served remotely as MCP prompts. They ship
in this package under skill/, so they run offline and version-locked.
urbicon verbs # list them
urbicon verb compose # print one recipe — pipe it to an agent, or read it inline| Verb | Use-case |
| --------------------------------- | ---------------------------------------------------------- |
| onboard / adopt | seed the manifest for a greenfield / brownfield project |
| compose / redesign / polish | build / rework / tighten a page (gated on linter + rubric) |
| critique / fix | judge without changing / repair correctness defects |
| retheme / audit / migrate | rebrand / check consistency / roll out a change app-wide |
skill/SKILL.md is the router (intent → verb). Every recipe opens by reading the
manifest and closes by writing the decision back.
Enforcement — hook + CI
The gate runs in two places a stateless remote server structurally cannot reach:
at edit time (a Claude Code hook) and in CI. Ready-to-copy templates ship in
templates/.
Edit-time hook. Wire urbicon hook as a PostToolUse hook so every edited
.svelte file is validated the moment it is written — the loop becomes enforced,
not something the agent must remember. On a failure the hook exits 2 and the
findings are fed back to the agent to fix; a clean edit is silent. Merge
templates/claude-settings.json into your
.claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|MultiEdit|Write",
"hooks": [{ "type": "command", "command": "bunx urbicon hook" }]
}
]
}
}Add --craft-floor 40 to the command to gate the craft axis too. (urbicon hook
reads the edited path from the hook event on stdin — it does not take path
arguments.)
Keep the bunx prefix. A hook command runs in a plain shell whose PATH does not
include your node_modules/.bin, so a bare urbicon hook exits 127 and gates
nothing — and it fails silently, because the agent only ever sees exit 2. If your
project was scaffolded before this was fixed, re-run urbicon init --hook: it
repairs that exact entry in place.
CI. Run urbicon validate over your source tree; a non-zero exit fails the
build. Copy templates/ci-github.yml, or add one step
to an existing workflow:
bunx urbicon validate src/ --json # correctness gate (blocking)
# add --craft-floor 40 to also gate the craft axis — one run, correctness is always onNotes
- Bundled to
dist/cli.jsat publish time (bun build --target node, shebang preserved). In the monorepo, run the TypeScript source directly:bun run packages/design/src/cli/index.ts <command>. validate/hook/context/record-decision/sync-manifest/initare content-free (engine + your repo only);verbs/verbread the recipes shipped underskill/(package-relative, still no content-bundle dependency);css-referenceandprinciples --rubriccome straight from the engine.find/get-component/icons/recipe/guide/pattern/principlesread the version-pinned@urbicon-ui/design-contentbundle (a runtime dependency). The guided onboarding interview lives in theadopt/onboardverbs.
Related
@urbicon-ui/design-engine— the deterministic engine this CLI wraps@urbicon-ui/mcp-server— the remote MCP adapter over the same engine
