Defining a CLI
defineCLI takes a definition object and returns the same object with methods attached to it. Parsing, validation, help output, and generated docs all read from that one definition.
import { coerce, defineCLI } from "@staticbolt/args-parser";import * as z from "zod";
export const listyCLI = defineCLI({ cliName: "listy",
meta: { descriptionMarkdown: "**Listy** is a small CLI for managing lists.", example: "listy --help\nlisty add-items --list groceries --items egg,milk", },
options: { help: { aliases: ["h"], exclusive: true, schema: z.boolean().optional(), coerce: coerce.boolean, meta: { description: "Show the help message." }, }, },
subcommands: [addItemsCommand, viewListCommand],});Every field is listed in the Cli reference.
| Field | Purpose |
|---|---|
cliName | The program name. Shown in help output and used by the generated completion scripts. |
options | Flags such as --name or -n. See Options. |
arguments | Typed values read by position. See Typed arguments. |
allowPositionals | Collect leftover input as an untyped string[]. |
subcommands | Commands built with defineSubcommand. |
meta | Descriptions and examples used by help output and generated Markdown. |
Handlers
onExecute adds a function that runs after the input parses and validates. You can add as many as you need. It returns an unsubscribe function that removes the handler that you just added.
const unsubscribe = listyCLI.onExecute(result => { result.options; // typed from the option schemas result.arguments; // typed from the argument schemas result.positionals; // string[], only when allowPositionals is true result.subcommand; // undefined for the main CLI result.context; // where each value came from and its raw input});Handlers only run for the command that was called. If the input names a subcommand, the subcommand's handlers run and the CLI's handlers do not.
result.context is useful when you need the raw input or the source of a value. See Context.
Running it
run takes the argv array, or a single string that it splits for you (handy in tests).
const result = listyCLI.run(process.argv.slice(2));const result = listyCLI.run("add-items --list groceries --items egg,milk");It returns one of two shapes:
{ value: { subcommand, options, arguments, positionals, context }, error: undefined }{ value: undefined, error: CliError }error covers everything the parser can detect: an invalid definition, unknown flags, failed schema validation, broken dependency rules. It never covers what your handler throws. That propagates to the caller.
Use runAsync when any handler is async, so their rejections are awaited:
const result = await listyCLI.runAsync(process.argv.slice(2));Attached methods
defineCLI returns the definition with these methods added:
| Method | Purpose |
|---|---|
run / runAsync | Parse terminal input and call the matching handlers. |
execute / executeAsync | Call the command from code with an already typed input object. |
onExecute | Add a handler. Returns an unsubscribe function. |
generateCliHelpMessage | Build the CLI help message as a string. |
generateSubcommandHelpMessage | Build a subcommand help message as a string. |
The two help methods are attached on the first run or runAsync call, so they are optional in the type. Inside a handler they are always available.
See Programmatic execution for execute, and Help messages for the help output.