Skip to content

Your First CLI

This walkthrough builds a small CLI called greet from nothing to something you can run.

Every CLI starts with cli(). name and version are required — they feed the help message and --version.

src/index.ts
import { cli } from 'clibuilder'
cli({ name: 'greet', version: '1.0.0' })
.default({
run() {
this.ui.info('hello world')
}
})
.parse(process.argv)
.catch(e => process.exit(e?.code || 1))

.default() defines the command that runs when no sub-command is named — greet on its own. .parse() returns a promise; reject handling is yours, and the convention above exits with the error’s code when it has one.

Arguments are positional and declared in order. Without a type they are strings.

cli({ name: 'greet', version: '1.0.0' })
.default({
arguments: [{ name: 'name', description: 'who to greet' }],
run(args) {
// args.name is string
this.ui.info(`hello, ${args.name}`)
}
})
.parse(process.argv)
Terminal window
$ greet world
hello, world

Options are named and declared as a record. Without a type they are boolean | undefined.

.default({
arguments: [{ name: 'name', description: 'who to greet' }],
options: {
loud: { description: 'shout it', alias: ['l'] }
},
run(args) {
const message = `hello, ${args.name}`
this.ui.info(args.loud ? message.toUpperCase() : message)
}
})
Terminal window
$ greet world --loud
HELLO, WORLD
$ greet world -l
HELLO, WORLD

See Arguments & Options for typed and optional values.

.command() adds a sub-command. It can be chained as many times as you like, and commands can nest.

import { cli, command } from 'clibuilder'
cli({ name: 'greet', version: '1.0.0' })
.command({
name: 'hello',
description: 'say hello',
run() {
this.ui.info('hello world')
}
})
.command({
name: 'repo',
description: 'manage repositories',
commands: [
command({
name: 'create',
description: 'create a repository',
arguments: [{ name: 'name', description: 'repository name' }],
run(args) {
this.ui.info(`creating ${args.name}`)
}
})
]
})
.parse(process.argv)
Terminal window
$ greet hello
hello world
$ greet repo create my-project
creating my-project

A command with commands but no run is a group: invoking it prints its help.

Every CLI answers these without you writing them:

Flag Alias Effect
--help -h Print the help message for the resolved command
--version -v Print the CLI version
--verbose -V Raise the display level to debug
--silent Suppress all UI output
--debug-cli Raise the display level to trace, including clibuilder’s own messages

The help message is generated from the description fields on the CLI, its commands, arguments, and options — which is why they are worth writing carefully.

Add a shebang to your entry file and point bin at it:

#!/usr/bin/env node
import { cli } from 'clibuilder'
// ...
{
"bin": { "greet": "./esm/index.js" }
}

See Publishing for the rest.

  • Commands — aliases, nesting, and command groups.
  • Configuration — let users put settings in a file.
  • Testing — assert on a command without spawning a process.