Embed a terminal-style command console directly inside your React app.
Citadel helps you turn repetitive UI workflows into fast keyboard commands for developers, support engineers, and power users, without sending them to a separate admin tool.
- Move faster in existing apps: expose internal actions as commands instead of building more buttons and forms
- Debug in context: call APIs, inspect JSON, clear storage, and run app actions without leaving the page
- Keep UI clean: hidden-by-default overlay (toggle key is configurable;
default is
., and can be shown on load if desired) - Scale safely: typed command DSL with argument help, async handlers, and
structured result rendering (
text,json,image,error,bool)
- Internal Tools: replace repetitive click paths with direct commands
- Support & Operations: add safe operational commands to admin dashboards
- API Testing & Debugging: execute REST calls and inspect responses inline
- Power User Workflows: give advanced users terminal speed in web UI
npm i citadel_cliCommands are the core concept in Citadel. Think user add 1234 or
qa deploy my_feature_branch.
To get running:
- Define commands with the typed DSL
- Build a
CommandRegistryfrom those definitions - Pass the registry to
Citadel
import {
Citadel,
command,
createCommandRegistry,
text,
} from "citadel_cli";
// 1. Define and register commands
const registry = createCommandRegistry([
command("greet")
.describe("Say hello to someone")
.arg("name", (arg) => arg.describe("Who are we greeting?"))
.handle(async ({ namedArgs }) => text(`Hello ${namedArgs.name} world!`)),
]);
// 2. Pass the registry to the component
function App() {
return <Citadel commandRegistry={registry} />;
}Citadel CLI uses auto-expansion to make entering commands as fast as
possible. When you type the first letter of a command it automatically expands
to the full word. For the above example, typing g would expand
in-place to greet (with a trailing space) whereupon you can enter in a value
for the name argument.
For hierarchical commands, expansion is prefix-based and unambiguous:
uscan resolve touser showudcan resolve touser deactivate- If two options share a prefix (
showandsearch), continue until unique:ush=>user show,use=>user search
Argument segment description values are shown as argument-level help text.
Example built-in help output:
user show <userId> - Show user details
<userId>: Enter user ID
Handlers must return one of the following:
TextCommandResultJsonCommandResultImageCommandResultErrorCommandResultBooleanCommandResult
For clearer command authoring, you can define commands with a DSL and compile
them into a CommandRegistry:
import {
Citadel,
command,
createCommandRegistry,
text,
} from "citadel_cli";
const registry = createCommandRegistry([
command("user.show")
.describe("Show user details")
.arg("userId", (arg) => arg.describe("Enter user ID"))
.handle(async ({ namedArgs }) => {
return text(`Showing user ${namedArgs.userId}`);
}),
]);
function App() {
return <Citadel commandRegistry={registry} />;
}DSL handlers receive:
rawArgs: positional values (string[])namedArgs: argument-name map (Record<string, string | undefined>)commandPath: dot-delimited path string
Helper constructors exported by the DSL:
text(value)json(value)image(url, altText?)error(message)bool(value, trueText?, falseText?)
import { command, createCommandRegistry, bool } from "citadel_cli";
const registry = createCommandRegistry([
command("bool.random")
.describe("Return a random boolean")
.handle(async () => bool(Math.random() >= 0.5, "π", "π")),
]);Demo registries include boolean commands:
- Basic example:
bool.true,bool.false,bool.random - DevOps example:
bool.deploy.window,bool.error.budget.healthy,bool.autoscale.recommended
CommandRegistry#addCommand still works and is fully supported. The DSL is now
the recommended authoring path for new command definitions.
- Each command can have zero or more arguments
- Argument values are passed to the handler as a
String[] - Arguments can be single- or double-quoted
Clearing localstorage:
async () => {
localStorage.clear();
return new TextCommandResult('localStorage cleared!');
}
Make an HTTP POST with a body containing a given name:
async (args: string[]) => {
const response = await fetch('https://api.example.com/endpoint', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: args[0] }),
});
return new JsonCommandResult(await response.json());
}
Certain configuration options can be passed to the Citadel component. These are given below, along with their default values.
const config = {
commandTimeoutMs: 10000,
includeHelpCommand: true,
fontFamily: '"JetBrains Mono", monospace',
fontSize: '0.875rem', // CSS font-size value (e.g. '14px', '0.875rem')
maxHeight: '80vh',
initialHeight: '50vh',
minHeight: '200',
outputFontSize: '0.75rem', // optional CSS font-size override for output text
showOutputPane: true, // set false to hide command output pane
resetStateOnHide: false,
closeOnEscape: true,
showCitadelKey: '.',
showOnLoad: false,
cursorType: 'blink', // 'blink', 'spin', 'solid', or 'bbs'
cursorSpeed: 530,
storage: {
type: 'localStorage',
maxCommands: 100
}
};
Then to make the component aware of them:
<Citadel commandRegistry={cmdRegistry} config={config} />
Citadel includes scripts to capture and compare before/after performance and size metrics.
- Build metrics:
- Bundle size (raw + gzip) for
dist/citadel.es.js,dist/citadel.umd.cjs, anddist/citadel.css - Total LOC and extension breakdown
- Dependency presence for
tailwindcss,postcss, andautoprefixer node_modulessize (du -sk)
- Bundle size (raw + gzip) for
- Runtime metrics (Chromium):
- JS heap usage before/after interaction
- Input latency (keydown to input update)
- FPS sample over a short window
- Long task count and duration
- DOM node count
All outputs are written to test-results/metrics/.
npm run metrics:build
npm run metrics:runtime
npm run metrics:compare -- --before <before.json> --after <after.json>
npm run metrics:all -- --label <label>
npm run metrics:report -- --label <label> --before-build <before-build.json> --before-runtime <before-runtime.json>- Capture a baseline snapshot:
npm run metrics:all -- --label before- After your changes, capture the new snapshot and generate comparisons:
npm run metrics:all -- --label after \
--before-build test-results/metrics/build-before-<timestamp>.json \
--before-runtime test-results/metrics/runtime-before-<timestamp>.json- Open generated reports:
test-results/metrics/run-after.mdtest-results/metrics/compare-build-after.md(if--before-buildprovided)test-results/metrics/compare-runtime-after.md(if--before-runtimeprovided)
Notes:
metrics:runtimestarts a local dev server and requires local port binding.- If you only want comparison output from existing snapshots, use
npm run metrics:report.
See CONTRIBUTING.md for guidelines on developing, testing, and releasing Citadel CLI.

