---
url: /guide/commands.md
---
# Commands

A command is a description and a list of parameters, registered under a name. A handler runs it and
returns an outcome. The registry holds the registrations and dispatches to them by name.

```ts
import { Registry, failed, ok } from '@ufo-soft/caelus'

const todos = new Set<string>()
const registry = new Registry()

registry.register({
  name: 'add',
  command: {
    description: 'Add a todo',
    parameters: [{ name: 'text', type: 'string', description: 'The todo text' }],
  },
  handler: ({ text }) => {
    if (typeof text !== 'string' || text === '') return failed('text must be a non-empty string')
    todos.add(text)
    return ok(todos.size)
  },
})
```

## Parameters

Each parameter has a `name`, a `type`, and a `description`. The type is `'string'`, `'integer'`, or
`'boolean'`.

The registry does not check args against the parameters. A handler receives whatever the caller
passed, typed as `Readonly<Record<string, unknown>>`, and checks what it uses. The [CLI](./cli.md)
and [MCP](./mcp.md) surfaces do validate: they build their options and input schemas from the
parameters. A [key chord](./keybindings.md) passes `{}`.

## Outcomes

A handler returns an outcome, or a promise of one. Build it with `ok(data)` or `failed(reason)`.

| Outcome | `kind` | Carries |
|---|---|---|
| `ok(data)` | `'ok'` | `data`. `ok()` with no argument carries `null`. |
| `failed(reason)` | `'failed'` | `reason`, a string. |

`OK` and `FAILED` are exported constants for the two kinds.

Return `failed` for anything the caller should see as a failure: bad args, a missing record, a
refused operation. An exception thrown by a handler is not caught. It rejects the promise `invoke`
returns.

## Invoking

```ts
const added = await registry.invoke('add', { text: 'write the docs' })
const missing = await registry.invoke('add', {})
const unknown = await registry.invoke('remove', { text: 'write the docs' })

console.log(added)
console.log(missing)
console.log(unknown)
```

```txt
{ kind: 'ok', data: 1 }
{ kind: 'failed', reason: 'text must be a non-empty string' }
{ kind: 'failed', reason: 'no command named remove' }
```

`invoke(name, args)` always returns a promise, whether the handler is synchronous or not. An unknown
name is a failed outcome, not an exception.

## Rendering data

```ts
import { render } from '@ufo-soft/caelus'

console.log(render('saved'))
console.log(render({ count: 3 }))
console.log(JSON.stringify(render(null)))

```

```txt
saved
{"count":3}
""
```

Surfaces that show an outcome as text use `render`. A string is shown as it is, anything else as JSON, and `null` or `undefined` as nothing. The CLI
prints this text. The MCP server returns it as the tool result, or `ok` when it is empty.

## The registry

| Method | Does |
|---|---|
| `register(registrations)` | Adds one registration or an array of them. A name registered twice keeps the last one. |
| `retrieve(name)` | Returns the registration for a name, or `undefined`. |
| `all()` | Returns every registration, in the order they were first registered. |
| `invoke(name, args)` | Runs the handler for a name and returns a promise of its outcome. |

## Shared descriptions

When more than one process registers the same commands, describe them once and bind handlers to the
descriptions. Declare the descriptions with `satisfies Commands`:

```ts
import type { Commands } from '@ufo-soft/caelus'

export const commands = {
  increment: { description: 'Add one', parameters: [] },
  decrement: { description: 'Subtract one', parameters: [] },
  reset: { description: 'Back to zero', parameters: [] },
} satisfies Commands
```

Then pair them with handlers using `bind`:

```ts
import { Registry, bind, ok } from '@ufo-soft/caelus'

let count = 0
const registry = new Registry()

registry.register(
  bind(commands, {
    increment: () => ok(++count),
    decrement: () => ok(--count),
    reset: () => ok((count = 0)),
  }),
)
```

`bind` returns one registration per description. A description without a handler fails to compile,
and so does a handler written inline for a name that has no description.

The check needs the exact keys. An object annotated as `Commands` has plain `string` keys, and `bind`
rejects it. `satisfies Commands` checks the shape and keeps the keys.

A description file holds data only, so a process that needs the commands but not their handlers can
import it without pulling in the code behind them. That process registers
[forwarders](./forwarding.md) to the same descriptions.

See the [API reference](/api/) for every type and signature.
