Citadel commands are hierarchical. In the DSL, you write a dot-delimited path such as user.show. In the UI, Citadel expands short, unambiguous prefixes into the full command path. For user.show, the user can type us and Citadel expands it to user show.
The main authoring API is the command DSL:
import {
Citadel,
bool,
command,
createCommandRegistry,
image,
json,
text,
} from 'citadel_cli';
const commandRegistry = createCommandRegistry([
command('user.show')
.describe('Show one user record')
.arg('userId', (arg) => arg.describe('The user id to load'))
.handle(async ({ namedArgs }) =>
json({
id: namedArgs.userId,
name: 'Ada Lovelace',
}),
),
command('note.add')
.describe('Create a note')
.arg('title', (arg) => arg.describe('Short title'))
.arg('body', (arg) => arg.describe('Longer note body'))
.handle(async ({ namedArgs, rawArgs, commandPath }) =>
text(
`Saved ${commandPath} with title "${namedArgs.title}" and ${rawArgs.length} arguments.`,
),
),
command('system.status')
.describe('Check whether the system is healthy')
.handle(async () => bool(true, 'healthy', 'unhealthy')),
command('avatar.random')
.describe('Show a placeholder image')
.handle(async () => image('https://picsum.photos/160')),
]);
export function CommandExamples() {
return <Citadel commandRegistry={commandRegistry} />;
}When users run those commands, they usually enter prefixes rather than the full expanded command text. For example:
user.showcan be entered asusnote.addcan be entered asnasystem.statuscan be entered asss
A path is a sequence of literal words.
command('hello')becomeshellocommand('user.show')becomesuser showcommand('team.member.remove')becomesteam member remove
Use short, specific words. Auto-expansion works best when sibling commands diverge early.
Users usually do not type the full command text.
- For
hello, typinghexpands tohello - For
user.show, typingusexpands touser show - For
team.member.remove, the user types the shortest unambiguous prefix for each segment
Think of the DSL path as the canonical command definition. In the UI, the user typically enters the shortest prefix that uniquely identifies that path.
Add arguments with .arg(name).
command('user.show')
.arg('userId', (arg) => arg.describe('The user id to load'))
.handle(async ({ namedArgs }) => json({ id: namedArgs.userId }));The argument description is shown in help output.
Arguments can be quoted when they contain spaces. These examples show the fully expanded command text after Citadel has resolved the command prefix:
note add "Sprint retro" "Capture follow-up items"note add 'Sprint retro' 'Capture follow-up items'
Each handler receives one object:
rawArgs: positional arguments in ordernamedArgs: argument values keyed by the names you declaredcommandPath: the original dot-delimited path string
Example:
command('note.add')
.arg('title')
.arg('body')
.handle(async ({ rawArgs, namedArgs, commandPath }) =>
text(`${commandPath}: ${namedArgs.title} (${rawArgs.length} args)`),
);Handlers should return one of Citadel's result types. These are what is used to determine what is shown in the output console when the handler is done executing. Each handle must return one of these:
text(value)json(value)image(url, altText?)error(message)bool(value, trueText?, falseText?)
Example:
command('deploy.check')
.handle(async () => bool(true, 'ready', 'blocked'));By default, Citadel injects a built-in help command into the registry. It lists available commands and argument descriptions.
If you want to disable the default help command, set config.includeHelpCommand to false. See Configuring Citadel and command history.