Your First CLI
This walkthrough builds a small CLI called greet from nothing to something you can run.
1. The smallest CLI
Section titled “1. The smallest CLI”Every CLI starts with cli(). name and version are required — they feed
the help message and --version.
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.
2. Take an argument
Section titled “2. Take an argument”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)$ greet worldhello, world3. Take an option
Section titled “3. Take an option”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) }})$ greet world --loudHELLO, WORLD$ greet world -lHELLO, WORLDSee Arguments & Options for typed and optional values.
4. Add a named command
Section titled “4. Add a named command”.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)$ greet hellohello world$ greet repo create my-projectcreating my-projectA command with commands but no run is a group: invoking it prints its help.
5. What you get for free
Section titled “5. What you get for free”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.
6. Make it executable
Section titled “6. Make it executable”Add a shebang to your entry file and point bin at it:
#!/usr/bin/env nodeimport { cli } from 'clibuilder'// ...{ "bin": { "greet": "./esm/index.js" }}See Publishing for the rest.
Where next
Section titled “Where next”- Commands — aliases, nesting, and command groups.
- Configuration — let users put settings in a file.
- Testing — assert on a command without spawning a process.