The whole front door for a tool built on the kit: parse, help, run (or, on --dry-run, plan), print the result as text or --json, and turn every failure into the one error line and its exit code. An outcome or a wait sets the exit code from its state.
npx shadcn@latest add ourostack/ouro-ui/ax-cli#v1.3.10It installs lib/ouro/ax/cli.ts and lib/ouro/ax/help.ts, with ax-checks, ax-command, ax-effects, ax-outcome, ax-output, ax-problem, ax-terminal, ax-voice, ax-wait. Its tests are ax-cli-tests. Every item runs on Node 22.18 or later as it is copied: the files import each other as ./<file>.ts. The Ouro AX guide shows how they fit together.
Exports of cli.ts
CliIo
The whole front door for a tool built on the kit: parse, help, run (or, on --dry-run, plan), print the result as text or --json, and turn every failure into the one error line and its exit code. An outcome or a wait sets the exit code from its state.
interface CliIo {
stdout: { write(s: string): unknown; isTTY?: boolean };
stderr: { write(s: string): unknown };
stdin?: AsyncIterable<string | Uint8Array> | Iterable<string | Uint8Array>;
}dispatch
Runs a parsed command as runCli does, for a tool that reads stdin and prints errors its own way: on --dry-run the command's plan(), refused ("no-preview") when it has none, else its run(). A self-scoped command is refused first when its item is not the calling agent's (checkOwner). It prints nothing and reads no stdin.
function dispatch<C extends RunContext>(cli: Cli<C>, parsed: Parsed, ctx: C) => Promise<unknown>isEmptyResult
Whether a result is an empty list: an empty array, or a page ({ items }) whose items are empty.
function isEmptyResult(result: unknown): booleanprintResult
Prints a command's result as runCli does and returns the exit code: an outcome by its state (a refusal or failure in text is the one error line on stderr), a wait by its state, a checks report (ax-checks) by its verdict, with its text styled for stdout and every check under --verbose, anything else as its text or --json. A report whose checks all ran and passed may get the voice's healthy line. Given the process's voice, a voice line may follow the text on stdout, never before it: after a done or queued outcome, an empty list, or the summary command.
function printResult(cli: Cli<RunContext>, cmd: CommandDef<unknown, RunContext>, input: Readonly<Record<string, InputValue>>, result: unknown, json: boolean, io: Pick<CliIo, "stdout" | "stderr">, voice?: Voice | undefined, env?: Readonly<Record<string, string | undefined>>): numberquietPipes
Ends the tool quietly when whatever reads its output goes away, as in tool help | head -1: on EPIPE it exits with the exit code so far (0 when none was set) and prints nothing, instead of a stack trace. Call it once in the tool's bin, before runCli: quietPipes(process). Any other stream error is thrown as before.
function quietPipes(proc: { stdout: ErrorEvents; stderr: ErrorEvents; exitCode?: string | number | null | undefined; exit(code?: number | undefined): unknown; }): voidrunCli
function runCli(cli: Cli<RunContext>, argv: readonly string[], io: CliIo, options?: RunOptions): Promise<number>RunOptions
interface RunOptions {
overview?: OverviewOptions;
topics?: Readonly<Record<string, string>>;
env?: Readonly<Record<string, string | undefined>>;
voiceMemory?: VoiceMemory;
}Exports of help.ts
commandManifest
One command as data.
function commandManifest(cli: Cli<RunContext>, cmd: CommandDef<unknown, RunContext>): CommandManifestCommandManifest
interface CommandManifest {
name: string;
aliases: readonly string[];
effect: "private" | "others";
whoSees: string;
readOnly: boolean;
needsWhy: boolean;
dryRun: boolean;
runs: boolean;
summary: string;
usage: string;
inputSchema: JsonSchema;
output: string | null;
examples: readonly string[];
hidden: boolean;
scope?: "self";
}DEFAULT_WIDTH
Help from the definitions: a one-screen overview under the two effect headings, each command's details, and the same as JSON (help --json), which an agent parses instead of scraping text. Every line is wrapped to width columns.
const DEFAULT_WIDTH: 100definitionLines
The generated part of a command's help: its usage from the definition, its arguments and its flags.
function definitionLines(cli: Cli<RunContext>, cmd: CommandDef<unknown, RunContext>, width?: number): string[]flagLines
One line per flag: usage on the left, what it does on the right; the built-ins and the tool's own are left out.
function flagLines(cli: Cli<RunContext>, cmd: CommandDef<unknown, RunContext>, width?: number): string[]helpJson
The help as data: the tool, its headings, the commands, the words only the person acts on (personOnly: name, reason and how), the exit codes, the tool's fixed voice lines (for a checker that looks for them in an agent's output) and the one error shape.
function helpJson(cli: Cli<RunContext>): { tool: string; about: readonly string[]; effects: { private: string; others: string; }; commands: CommandManifest[]; personOnly: { name: string; reason: string; how: string; }[]; exitCodes: readonly ExitCode[]; voice: string[]; errorShape: { error: { code: string; exit: string; thing: string; problem: string; fix: string; message: string; }; }; }OverviewOptions
interface OverviewOptions {
width?: number;
othersNote?: string;
footer?: readonly string[];
}renderCommand
One command's whole help, from its definition alone.
function renderCommand(cli: Cli<RunContext>, cmd: CommandDef<unknown, RunContext>, width?: number): stringrenderOverview
The overview: what the tool is, the commands under "Only you see these" and "Other people see these", the words only the person acts on, the footer.
function renderOverview(cli: Cli<RunContext>, options?: OverviewOptions): stringwrap
Wraps text to width columns, every line indented; a word longer than a line stays whole.
function wrap(text: string, indent: number, first?: number, width?: number): [...string[], string]