Skip to content

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
});
Note

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.