CLI and registry
The eoria CLI, what each command touches, and how the registry stays shadcn-compatible.
Eleven commands, shipped as @eoria/cli with an eoria binary. Run them with npx @eoria/cli,
or add the package as a dev dependency and call pnpm eoria. All of them are file plumbing
over a static registry, so there is nothing to log in to and nothing that phones home.
init, add and extend write files. diff, list, search, view, info, docs and
doctor only read, unless you pass doctor --fix. mcp hands the read commands and add to an
agent as tools, and mcp init writes one config file. Every command that talks to the registry
takes --registry <url or folder> to use another one for a single run. list, search, view,
info and doctor take --json, which prints one JSON object to stdout and nothing else.
init
npx @eoria/cli initWrites eoria.json next to your package.json, then sets up what Unistyles needs:
src/unistyles.ts, which registers the light and dark themes, and animport '@/unistyles'line at the top of your root layout. It looks forapp/_layout.tsx,App.tsxandindex.jsunder the source root and the project root, and tells you if it finds none.babel.config.jswith the Unistyles and Worklets plugins, if you have no Babel config yet. If you do, it prints the two plugin lines for you to paste. An existingunistyles.tsis reported and kept.- A
@/*path alias intsconfig.json, if the alias is missing. - The peer packages, through
expo installon Expo projects and your package manager otherwise. Pass--no-installto only print the command.
The CLI picks the package manager from the packageManager field in package.json, then from
the lockfile (pnpm-workspace.yaml also counts as pnpm). It looks in the app folder first, then
in each folder above it, and stops at the first answer or at the folder that holds .git. So an
app inside a pnpm workspace installs with pnpm even though the lockfile sits at the workspace
root. With no answer it uses npm. On Expo projects the CLI passes its choice to expo install
as --pnpm, --yarn, --bun or --npm. Expo’s own lookup only reads lockfiles and falls
back to npm, so a new workspace without a lockfile would otherwise install with npm.
If the install fails, init exits with 1. The error lists the files it already wrote, which it
leaves in place, and the install command to run once you fix the cause.
If a shadcn components.json exists, init reads its aliases.ui so the components land in
the folder you already use.
add
npx @eoria/cli add button selectFetches each item from the registry, follows registryDependencies until nothing is missing,
and writes the files in dependency order. button brings text. select brings popper,
portal and text. Imports inside the copied files are rewritten from @/components/ui to
your alias.
A file that already exists is left alone and reported. add records a hash of every file it
writes, so it can tell “you changed this” from “it was already there”. Files it skipped get no
hash, so diff will not pretend to know what they looked like. Pass --overwrite to
replace either. npm dependencies the items declare are installed unless you pass
--no-install. A failed install makes add exit with 1, after the files are written. The error
names them and prints the install command to run again.
npx @eoria/cli add button --dry-runnpx @eoria/cli add button --diffBoth flags write nothing, eoria.json included. --dry-run resolves everything and lists each
file as would write, would skip or would overwrite, then prints the install command it would
run. --diff prints the hunks between each file you already have and the registry file, in the
format diff uses, and marks the rest as new files. Lines that start with - are yours.
Blocks
npx @eoria/cli add sign-inA block is a whole screen built from components, such as a sign-in form. You add one by name,
the same way you add a component, and add brings the components it imports. The registry
marks a block as registry:block and a component as registry:ui.
Block files go in their own folder. By default that is the blocks folder next to your
components directory, so src/components/ui gives src/components/blocks/sign-in.tsx, and
src/ui gives src/blocks/sign-in.tsx. Set blocks in eoria.json to put them somewhere
else, for example "blocks": "src/screens". init does not ask for it, and add creates the
folder with the first block. A block imports its components through your alias, so it
compiles wherever it lands.
diff, view, info and doctor treat a block like any other item. After you add one the
file is yours to edit, and diff shows what you changed.
diff
npx @eoria/cli diffnpx @eoria/cli diff buttonCompares each installed item with the registry and prints changed hunks. Every file gets one
of five labels: up to date, changed in the registry, edited locally, both, or missing. The hash from
add is what makes the distinction possible. --full prints whole files.
diff also scans your components folder for files that import an item, such as a
checkout-button.tsx that extends button. When the base changed upstream, it names those
files, because they inherit the change through extendSlotRecipe the moment you update.
extend
npx @eoria/cli extend button checkout-buttonWrites checkout-button.tsx next to button.tsx. The file imports the base recipe, calls
extendSlotRecipe on it with empty slots and variants for you to fill, and exports
CheckoutButton from the base’s createButton factory. Bases without a factory get a
comment explaining the two options instead. See Recipes for the
merge rules.
list and search
npx @eoria/cli listnpx @eoria/cli search menulist prints every registry item with its name, type, title and description. The type is ui
for a component and block for a block. Inside a project,
installed items get a ✓. Outside one it reads the default registry, so you can browse before
running init.
search matches the query against name, title and description, ignoring case. Every word of
the query has to appear somewhere in the item. Name matches come first, then title matches,
then description matches.
npx @eoria/cli list --type blocknpx @eoria/cli search sign --type block--type ui or --type block keeps one type of item, in both commands.
With --json, list prints { registry, items } and search prints
{ registry, query, items }. Each item has name, title, description, type and
installed, which is null outside a project. type is the registry value, registry:ui or
registry:block.
view
npx @eoria/cli view selectnpx @eoria/cli view select --sourcePrints one item’s title, description, npm dependencies and registry dependencies, then each
file with the path add would write it to. The add also brings line follows the registry
dependencies all the way down, which for select gives portal, popper and text.
--source appends the file contents, with imports rewritten to your alias.
In --json output the direct lists are dependencies and registryDependencies, and the
followed ones are allDependencies and allRegistryDependencies. Each entry of files has
path, type, target and localPath, plus content when you pass --source.
docs
npx @eoria/cli docs selectnpx @eoria/cli docs themingnpx @eoria/cli docs sign-innpx @eoria/cli docsPrints the Markdown version of a docs page to stdout. docs tries a bare topic as a component
first, so select fetches /components/select.md. If that page does not exist it tries the
other sections in turn: /start, /blocks, /guides and /agents. So theming fetches
/start/theming.md and sign-in fetches /blocks/sign-in.md. A topic with a slash, such as
start/theming, names the page outright. With no topic you get /llms.txt, the index of every
page.
The docs URL is the registry URL without its trailing /r. A local registry folder has no docs
site, so docs stops and asks for --docs-url, which takes a URL or the folder of a local docs
build. When no page answers, the error names every URL it tried.
info
npx @eoria/cli infonpx @eoria/cli info --jsonPrints what the CLI can see of the project. That is the CLI version, where eoria.json lives
and what it says, the registry in use, the package manager, whether the app uses Expo, and the
version ranges of @eoria/core, react-native-unistyles, react-native-reanimated and
react-native-worklets from your package.json. Every file of every installed item gets one
of the five diff labels. Paste the output into a bug report, or give --json to an agent
before it touches your components.
{ "cli": { "version": "0.1.1" }, "root": "/work/app", "config": { "path": "/work/app/eoria.json", "error": null, "values": { "registry": "https://eoria.adamtrip.pt/r", "components": "src/components/ui", "alias": "@/components/ui" } }, "registry": { "location": "https://eoria.adamtrip.pt/r", "source": "eoria.json", "remote": true, "reachable": true, "error": null }, "components": { "directory": "src/components/ui", "alias": "@/components/ui", "exists": true }, "blocks": { "directory": "src/components/blocks", "exists": false }, "installed": [ { "name": "button", "files": [ { "path": "src/components/ui/button.tsx", "status": "local", "label": "edited locally" } ] } ], "packageManager": "pnpm", "expo": true, "packages": { "@eoria/core": "^0.1.0", "react-native-unistyles": "^3.0.0", "react-native-reanimated": "~4.1.0", "react-native-worklets": null }}The keys keep their names and types between releases, and new ones may appear. status is
unchanged, upstream, local, both or missing, the five diff labels in the order the
diff section lists them, and label is the text diff prints. A sixth value, unknown, means the
registry could not supply the item, so info could only check the file against its hash.
registry.source is flag, eoria.json or default. packageManager is pnpm, yarn,
bun or npm, found the way init finds it. blocks.directory is where block files go, and
blocks.exists stays false until you add the first block. Outside a project config.path,
config.values, components and blocks are null and installed is empty. A package that package.json does not list
is null.
doctor
npx @eoria/cli doctornpx @eoria/cli doctor --fixChecks the setup init and add leave behind. Each check reports pass, warn or fail, with one
line on what to do about it. The exit code is 1 when any check fails and 0 otherwise, so it
works as a CI step. Warnings do not change the exit code. The checks run in this order, and
the names are the id values in the JSON:
config.eoria.jsonexists and parses. If it does not,doctorstops here.registry. The registry index loads.components-folder. Thecomponentsfolder exists.alias.tsconfig.jsonhas a path for the alias prefix, such as@/*. A project without atsconfig.jsongets a warning.unistyles-file.unistyles.tsexists under the source root.theme-import. The root layoutinitlooks for imports it. Ifdoctorfinds no layout, it warns.babel.babel.config.jslistsreact-native-unistyles/pluginandreact-native-worklets/plugin. Reanimated wants the Worklets plugin last, which is howinitwrites it, so any other order is a warning. From Expo SDK 54,babel-preset-expoadds the Worklets plugin itself whenreact-native-workletsis installed and runs it after your own plugins. So a config with only the Unistyles plugin passes when all of these hold: the project is on SDK 54 or later, the file namesbabel-preset-expoorexpo, it names no React Native preset (@react-native/babel-presetormetro-react-native-babel-preset), and noworklets: falseorreanimated: falseappears anywhere in it.doctordoes not parse the file, so a comment that saysworklets: falsecounts too. Otherwise it asks you to list the Worklets plugin, which is always valid. The SDK comes from the installedexpopackage, or else from the range inpackage.json, where only a single range starting at 54 or later counts, such as~54.0.0.peers.package.jsonlists@eoria/coreand every peerinitinstalls.installed-files. Every file recorded ineoria.jsonexists.registry-dependencies. Every item that an installed item depends on is installed too.npm-dependencies.package.jsonlists every package the installed items declare.
The last two need the registry. When it does not answer they turn into warnings.
--fix runs the same code as init for the three repairs that cannot clobber your work. It
adds the path alias, writes a missing unistyles.ts, and installs missing packages. Then it
checks again and reports the second pass. It leaves your root layout and your Babel config alone,
and the remedy line tells you what to paste.
{ "ok": false, "root": "/work/app", "checks": [ { "id": "alias", "status": "fail", "message": "tsconfig.json has no \"@/*\" path, so @/components/ui does not resolve.", "remedy": "Run `eoria doctor --fix`, or add \"@/*\": [\"./src/*\"] to compilerOptions.paths.", "fixable": true } ], "fixed": [], "summary": { "pass": 10, "warn": 0, "fail": 1 }}The example shows one of the eleven checks. ok is false when any check failed. remedy is null on a pass. fixable marks the checks
--fix can repair, and fixed lists the ids it did repair in this run. With --json, progress
lines and installer output go to stderr, so stdout stays parseable.
mcp
npx @eoria/cli mcpStarts a Model Context Protocol server on stdin and stdout, so an agent can call the CLI as tools without a shell. Your editor or agent starts it as a child process on your machine and stops it when the session ends. There is nothing to host and no account to make, and it reads the same static registry as every other command.
You rarely type that command yourself. The client runs it, and mcp init writes the entry the
client reads:
npx @eoria/cli mcp init --client claude--client takes claude, cursor, vscode or codex. Without it you get the four names.
--force replaces an eoria entry that has other settings. --cwd, --registry and
--docs-url go in the entry’s args and apply to every tool.
MCP server has the file each client reads, the seven tools and what they can change.
eoria.json
{ "registry": "https://eoria.adamtrip.pt/r", "components": "src/components/ui", "alias": "@/components/ui", "installed": { "text": { "src/components/ui/text.tsx": "3f0b…" }, "button": { "src/components/ui/button.tsx": "a91c…" } }}| Key | What it holds |
|---|---|
registry |
Registry URL, or a local folder |
components |
Folder that receives component files |
blocks |
Optional. Folder that receives block files. Defaults to blocks next to components |
alias |
Import alias that resolves to components |
installed |
Hash of every file add wrote, by item name and file path |
Commit it. The installed hashes are what add and diff use to spot local edits, keyed by
file path, so renaming components or blocks after installing loses that history. registry
can be a URL or a local folder, which is how the repository tests the CLI against its own
registry/dist.
shadcn compatibility
The registry is plain shadcn registry JSON. The index is at
/r/index.json and each item at /r/<name>.json with its source inlined.
If your team already runs the shadcn CLI, this works too:
npx shadcn@latest add https://eoria.adamtrip.pt/r/button.jsonYou lose the hash tracking, diff and extend, but the files are identical.
Hosting your own
The registry is a folder. pnpm build:registry writes registry/dist, and the docs build
copies it into public/r. Deploy the docs output to any static host and point registry in
eoria.json at it. There is no server.
registry/registry.json lists every item with its files, npm dependencies and registry
dependencies. The build fails if an item names a registry dependency that does not exist, so a
typo cannot ship.