Commands
A clibuilder CLI is a tree of commands. The root of the tree is the CLI itself; .default() gives
that root something to run, and .command() hangs named commands off it.
The default command
Section titled “The default command”.default() defines what runs when no sub-command is named. It has no name — the CLI’s name is its
name.
cli({ name: 'app', version: '1.0.0' }) .default({ description: 'do the thing', run() { this.ui.info('running') } }) .parse(process.argv).default() can be called only once — it is removed from the builder type after the first call.
Named commands
Section titled “Named commands”cli({ name: 'app', version: '1.0.0' }) .command({ name: 'hello', description: 'say hello', run() { this.ui.info('hello world') } }) .command({ name: 'goodbye', description: 'say goodbye', run() { this.ui.info('goodbye') } }) .parse(process.argv)Each .command() returns the builder, so they chain.
Sub-commands
Section titled “Sub-commands”A command can carry its own commands, to any depth. Use the
command() helper for the nested ones — it does nothing at runtime, but
it gives you type inference and editor completion on a standalone command object.
import { cli, command } from 'clibuilder'
const create = command({ name: 'create', description: 'create a repository', arguments: [{ name: 'name', description: 'repository name' }], run(args) { this.ui.info(`creating ${args.name}`) }})
const remove = command({ name: 'remove', alias: ['rm'], description: 'remove a repository', arguments: [{ name: 'name', description: 'repository name' }], run(args) { this.ui.info(`removing ${args.name}`) }})
cli({ name: 'app', version: '1.0.0' }) .command({ name: 'repo', description: 'manage repositories', commands: [create, remove] }) .parse(process.argv)$ app repo create my-project$ app repo rm my-projectCommand groups
Section titled “Command groups”A command may declare commands without a run. That makes it a pure group: invoking it on its
own prints the group’s help message listing its sub-commands. This is enforced in the type — a
command must have either a run or a commands.
Aliases
Section titled “Aliases”Both commands and options take aliases.
command({ name: 'search-packages', alias: ['sp'], description: 'search for packages', run() { /* ... */ }})$ app sp # same as `app search-packages`Inside run()
Section titled “Inside run()”run is called with this bound to a command instance. It is a regular method, not an arrow
function — that binding is the point.
this |
What it is |
|---|---|
this.ui |
The UI — info, warn, error, debug, showHelp, showVersion |
this.config |
The loaded, validated config, typed from the command’s config schema |
this.cwd |
The directory the CLI was invoked from |
this.keywords |
The keywords declared on the CLI, used for plugin lookup |
this.context |
Whatever you put in the command’s context — see below |
The first parameter, args, holds the parsed
arguments and options.
context — the seam for testing
Section titled “context — the seam for testing”A command can declare a context object, and it is handed back on this.context. Put your I/O
dependencies there and a test can substitute them without mocking modules.
import { readFile } from 'node:fs/promises'
command({ name: 'show', description: 'print a file', context: { readFile }, arguments: [{ name: 'file', description: 'file to print' }], async run(args) { this.ui.info(await this.context.readFile(args.file, 'utf8')) }})Return values
Section titled “Return values”Whatever run() returns (or resolves to) becomes the resolved value of .parse(). That is mostly
useful in tests — testCommand() hands it back as result — but it
also lets one program drive another’s commands directly.
const total = await cli({ name: 'app', version: '1.0.0' }) .default({ run: () => 42 }) .parse(process.argv)// total === 42