# CLI and registry

The eoria CLI, what each command touches, and how the registry stays shadcn-compatible.

[Documentation](https://eoria.adamtrip.pt/start/cli/)

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

```sh
npx @eoria/cli init
```

Writes `eoria.json` next to your `package.json`, then sets up what Unistyles needs:

- `src/unistyles.ts`, which registers the light and dark themes, and an `import '@/unistyles'`
  line at the top of your root layout. It looks for `app/_layout.tsx`, `App.tsx` and `index.js`
  under the source root and the project root, and tells you if it finds none.
- `babel.config.js` with 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 existing `unistyles.ts` is
  reported and kept.
- A `@/*` path alias in `tsconfig.json`, if the alias is missing.
- The peer packages, through `expo install` on Expo projects and your package manager
  otherwise. Pass `--no-install` to 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

```sh
npx @eoria/cli add button select
```

Fetches 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.

```sh
npx @eoria/cli add button --dry-run
npx @eoria/cli add button --diff
```

Both 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

```sh
npx @eoria/cli add sign-in
```

A 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

```sh
npx @eoria/cli diff
npx @eoria/cli diff button
```

Compares 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

```sh
npx @eoria/cli extend button checkout-button
```

Writes `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](https://eoria.adamtrip.pt/start/recipes.md#extending) for the
merge rules.

## list and search

```sh
npx @eoria/cli list
npx @eoria/cli search menu
```

`list` 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.

```sh
npx @eoria/cli list --type block
npx @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

```sh
npx @eoria/cli view select
npx @eoria/cli view select --source
```

Prints 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

```sh
npx @eoria/cli docs select
npx @eoria/cli docs theming
npx @eoria/cli docs sign-in
npx @eoria/cli docs
```

Prints 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

```sh
npx @eoria/cli info
npx @eoria/cli info --json
```

Prints 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.

```json
{
  "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`](#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

```sh
npx @eoria/cli doctor
npx @eoria/cli doctor --fix
```

Checks 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.json` exists and parses. If it does not, `doctor` stops here.
- `registry`. The registry index loads.
- `components-folder`. The `components` folder exists.
- `alias`. `tsconfig.json` has a path for the alias prefix, such as `@/*`. A project without a
  `tsconfig.json` gets a warning.
- `unistyles-file`. `unistyles.ts` exists under the source root.
- `theme-import`. The root layout `init` looks for imports it. If `doctor` finds no layout, it
  warns.
- `babel`. `babel.config.js` lists `react-native-unistyles/plugin` and
  `react-native-worklets/plugin`. Reanimated wants the Worklets plugin last, which is how `init`
  writes it, so any other order is a warning. From Expo SDK 54, `babel-preset-expo` adds the
  Worklets plugin itself when `react-native-worklets` is 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 names `babel-preset-expo` or `expo`, it names no React Native
  preset (`@react-native/babel-preset` or `metro-react-native-babel-preset`), and no
  `worklets: false` or `reanimated: false` appears anywhere in it. `doctor` does not parse the
  file, so a comment that says `worklets: false` counts too. Otherwise it asks you to list the
  Worklets plugin, which is always valid. The SDK comes from the installed `expo` package, or
  else from the range in `package.json`, where only a single range starting at 54 or later
  counts, such as `~54.0.0`.
- `peers`. `package.json` lists `@eoria/core` and every peer `init` installs.
- `installed-files`. Every file recorded in `eoria.json` exists.
- `registry-dependencies`. Every item that an installed item depends on is installed too.
- `npm-dependencies`. `package.json` lists 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.

```json
{
  "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

```sh
npx @eoria/cli mcp
```

Starts a [Model Context Protocol](https://modelcontextprotocol.io/) 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:

```sh
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](https://eoria.adamtrip.pt/agents/mcp.md) has the file each client reads, the seven tools and what they can
change.

## eoria.json

```json title="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`](https://eoria.adamtrip.pt/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:

```sh
npx shadcn@latest add https://eoria.adamtrip.pt/r/button.json
```

You 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.
