@llui/cli
Current package version: 0.1.0
llui add <item> copies component source into your project — shadcn/ui's
distribution model, applied to LLui.
The model fits LLui better than it fits React. A copied file is compiled by your
@llui/vite-plugin, so it gets view lowering, the non-bypassable
compile-time lint rules, and the agent metadata ($ms / $ss /
__lluiVariants) that a precompiled library cannot give you. See
Styling & the component registry for the full guide.
pnpm add -D @llui/cli
pnpm llui init
pnpm llui list
pnpm llui add button card dialog
Commands
| Command | Does |
|---|---|
llui init |
Write components.json. --ui / --lib set target dirs; --alias sets an import prefix. |
llui add <item...> |
Copy items and their registryDependencies. --overwrite, --dry-run. |
llui list |
Show what the registry offers. |
All commands accept --registry <url\|path> and --cwd <dir>.
components.json
{
"registry": "https://llui.dev/r",
"paths": { "ui": "src/components/ui", "lib": "src/lib" }
}
aliases is optional and unset by default. Add
"aliases": { "ui": "@/components/ui", "lib": "@/lib" } only if your tsconfig
declares those paths; otherwise the CLI rewrites the registry's @/lib/utils import to a
relative specifier computed from where the file landed. That is the default rather
than the fallback: an alias the project does not declare produces a file that
type-checks nowhere, which is a worse failure than a longer path.
Safety
llui addnever overwrites an existing file. The copied file is your source and is expected to have been edited; a secondaddreports it as skipped.--overwriteis the explicit opt-in.- File targets are validated at load, not at write. A registry is remote third-party
content, so
..and absolute paths are rejected before anything touches the disk. - Unknown registry keys are ignored, so a registry ahead of your CLI still installs instead of failing closed.
Using another registry
The item format is a deliberate subset of shadcn's registry-item.json. Point at any
host, a local directory, or a checkout:
llui add button --registry ./registry
llui add button --registry https://example.com/r
A local source may leave file contents on disk (files[].path); a remote one must serve
them inlined (files[].content).
Functions
add()
Copy registry items into the project.
Not overwriting by default is the whole safety story of this command: the
point of the registry model is that the copied file becomes the consumer's
source, which they are expected to edit. A second llui add button after
those edits must not silently discard them, so an existing file is reported
as skipped and --overwrite is the explicit opt-in.
function add(options: AddOptions): Promise<AddResult>
aliasKeyOf()
The alias KEY a registry @/-import maps to (@/lib/utils -> 'lib').
function aliasKeyOf(specifier: string): 'ui' | 'lib' | null
assertSafeTarget()
A registry target is written INTO a directory the CLI chose, so it must not be
able to escape it. .. and absolute paths are rejected at LOAD time rather
than at write time: a registry is remote, third-party content, and the write
site is several calls away from the place a reviewer would think to look.
function assertSafeTarget(target: string, itemName: string): void
collectDependencies()
Collect the npm packages the given items need, deduped and sorted.
function collectDependencies(items: readonly RegistryItem[]): {
dependencies: string[]
devDependencies: string[]
}
isRemote()
function isRemote(source: string): boolean
loadRegistry()
Load a registry INDEX from an https URL or a local path.
registry.json is appended for BOTH — a registry setting names the registry,
not one file in it, and the documented default (https://llui.dev/r) is a
directory. Appending only for local paths made every remote install 404 on the
default value. A source that already ends in .json is taken as-is, so a host
that publishes its index under another name still works.
function loadRegistry(source: string): Promise<Registry>
loadRemoteItem()
Fetch ONE item's full record, with its file contents inlined.
The index deliberately strips file bodies — llui list only needs to name
things, and inlining every component into the index would make the common
case the largest download. So an item resolved from the index carries the file
LIST but no content, and installing it needs this second request. A local
registry needs no equivalent: its path still points at a real file on disk.
function loadRemoteItem(source: string, name: string): Promise<RegistryItem>
readConfig()
function readConfig(cwd: string): Promise<Config | null>
resolveItems()
Resolve the requested names plus everything they depend on, in dependency-first order.
Cycles are TOLERATED, not rejected: visiting short-circuits a name already on
the stack, so a registry whose items reference each other still installs every
file exactly once instead of recursing forever. A registry author's mistake
should not be a crash in someone else's install.
function resolveItems(registry: Registry, names: readonly string[]): RegistryItem[]
rewriteImports()
Rewrite the registry's @/… imports for one written file.
The registry ships @/lib/utils because that is the shadcn convention and
reads the same in every item. What it must become depends on the project:
aliasesconfigured -><alias>/utils, the shadcn behaviour.- no aliases -> a RELATIVE specifier computed from where this file actually landed. An alias the project's tsconfig does not declare produces a file that type-checks nowhere, which is a worse failure than a longer path, so relative is the default rather than the fallback of last resort.
Extensions: LLui packages are ESM with explicit .js specifiers, but a
consumer's bundler resolves extensionless TS imports fine and @/lib/utils
is what a shadcn user expects to see. Relative rewrites therefore keep the
project's own convention by emitting no extension either.
function rewriteImports(source: string, fileTargetDir: string, config: Config): string
targetDir()
Directory on disk for a registry file type (registry:ui -> paths.ui).
function targetDir(config: Config, type: string): string
writeConfig()
function writeConfig(cwd: string, config: Config): Promise<string>
Types
Config
export type Config = z.infer<typeof ConfigSchema>
Registry
export type Registry = z.infer<typeof RegistrySchema>
RegistryFile
export type RegistryFile = z.infer<typeof RegistryFileSchema>
RegistryItem
export type RegistryItem = z.infer<typeof RegistryItemSchema>
Interfaces
AddOptions
export interface AddOptions {
cwd: string
config: Config
names: readonly string[]
/** Replace files that already exist. Default false — see `AddResult.skipped`. */
overwrite?: boolean
/** Resolve and report without touching the filesystem. */
dryRun?: boolean
}
AddResult
export interface AddResult {
written: string[]
/** Files that already existed and were LEFT ALONE. */
skipped: string[]
items: RegistryItem[]
dependencies: string[]
devDependencies: string[]
}
Constants
CONFIG_FILE
const CONFIG_FILE
ConfigSchema
const ConfigSchema
DEFAULT_CONFIG
const DEFAULT_CONFIG: Config
RegistryFileSchema
Registry schema — a deliberate subset of shadcn/ui's registry-item.json, with
the same field names and semantics so an LLui item is readable by anyone who
has seen a shadcn one (and so a future shadcn-CLI-compatible host stays an
option). Fields shadcn defines that LLui has no use for — registry:hook,
tailwind, docs, meta — are simply not modelled; unknown keys are
IGNORED rather than rejected, because a registry is a remote document whose
author may be ahead of this CLI, and failing closed on an unread key would
make every additive registry change a breaking one.
const RegistryFileSchema
RegistryItemSchema
const RegistryItemSchema
RegistrySchema
const RegistrySchema
Public Entry Points
@llui/cli/registry
Functions
assertSafeTarget() from @llui/cli/registry
A registry target is written INTO a directory the CLI chose, so it must not be
able to escape it. .. and absolute paths are rejected at LOAD time rather
than at write time: a registry is remote, third-party content, and the write
site is several calls away from the place a reviewer would think to look.
function assertSafeTarget(target: string, itemName: string): void
collectDependencies() from @llui/cli/registry
Collect the npm packages the given items need, deduped and sorted.
function collectDependencies(items: readonly RegistryItem[]): {
dependencies: string[]
devDependencies: string[]
}
isRemote() from @llui/cli/registry
function isRemote(source: string): boolean
loadRegistry() from @llui/cli/registry
Load a registry INDEX from an https URL or a local path.
registry.json is appended for BOTH — a registry setting names the registry,
not one file in it, and the documented default (https://llui.dev/r) is a
directory. Appending only for local paths made every remote install 404 on the
default value. A source that already ends in .json is taken as-is, so a host
that publishes its index under another name still works.
function loadRegistry(source: string): Promise<Registry>
loadRemoteItem() from @llui/cli/registry
Fetch ONE item's full record, with its file contents inlined.
The index deliberately strips file bodies — llui list only needs to name
things, and inlining every component into the index would make the common
case the largest download. So an item resolved from the index carries the file
LIST but no content, and installing it needs this second request. A local
registry needs no equivalent: its path still points at a real file on disk.
function loadRemoteItem(source: string, name: string): Promise<RegistryItem>
resolveItems() from @llui/cli/registry
Resolve the requested names plus everything they depend on, in dependency-first order.
Cycles are TOLERATED, not rejected: visiting short-circuits a name already on
the stack, so a registry whose items reference each other still installs every
file exactly once instead of recursing forever. A registry author's mistake
should not be a crash in someone else's install.
function resolveItems(registry: Registry, names: readonly string[]): RegistryItem[]
Types
Registry from @llui/cli/registry
export type Registry = z.infer<typeof RegistrySchema>
RegistryFile from @llui/cli/registry
export type RegistryFile = z.infer<typeof RegistryFileSchema>
RegistryItem from @llui/cli/registry
export type RegistryItem = z.infer<typeof RegistryItemSchema>
Constants
RegistryFileSchema from @llui/cli/registry
Registry schema — a deliberate subset of shadcn/ui's registry-item.json, with
the same field names and semantics so an LLui item is readable by anyone who
has seen a shadcn one (and so a future shadcn-CLI-compatible host stays an
option). Fields shadcn defines that LLui has no use for — registry:hook,
tailwind, docs, meta — are simply not modelled; unknown keys are
IGNORED rather than rejected, because a registry is a remote document whose
author may be ahead of this CLI, and failing closed on an unread key would
make every additive registry change a breaking one.
const RegistryFileSchema
RegistryItemSchema from @llui/cli/registry
const RegistryItemSchema
RegistrySchema from @llui/cli/registry
const RegistrySchema