CAC
Use CAC to build small, expressive CLIs with a compact command grammar:
import { cac } from 'cac'
const cli = cac('my-cli')
cli
.command('build <entry> [...files]', 'Build project files')
.option('--minify', 'Minify output')
.action((entry, files, options) => {
console.info({ entry, files, options })
})
cli.help()
cli.version('1.0.0')
cli.parse()
Install with pnpm add cac.
Build CLIs This Way
- Choose the command shape first.
- Use global parsing only for tiny CLIs.
- Use
cli.command(...)for verb-based CLIs. - Use a default command when the main behavior should run without an explicit verb.
- Model positional arguments before options.
<arg>is required.[arg]is optional....is variadic and may only appear on the final positional argument.
- Attach options at the narrowest useful scope.
- Use
cli.option(...)for flags shared by multiple commands. - Use
command.option(...)for command-specific flags.
- Use
- Add
help()andversion()for user-facing tools. - Use plain
cli.parse()for simple flows; usecli.parse(argv, { run: false })plusawait cli.runMatchedCommand()when you need centralized or async error handling.
Syntax and Parsing
| Need | CAC form | Result |
|---|---|---|
| Required arg | build <entry> |
action receives entry |
| Optional arg | build [entry] |
action receives undefined when omitted |
| Variadic arg | build <entry> [...rest] |
final action arg is an array |
| Boolean flag | --open |
accepts both --open and --no-open automatically |
| Optional option value | --scale [level] |
value or true |
| Required option value | --out <dir> |
missing value throws |
| Global option | cli.option('--cwd <dir>', ...) |
available to all commands |
| Command option | command.option('--watch', ...) |
scoped to one command |
| Repeated option | --include a --include b |
becomes ['a', 'b'] |
| Dot-nested option | --env.API_SECRET xxx |
becomes options.env.API_SECRET |
| Passthrough args | -- pnpm test |
stored in options['--'] |
Kebab-case option names are read in camelCase:
cli.option('--clear-screen', 'Clear screen')
// read options.clearScreen
Only the first segment is camel-cased, so --env.API_SECRET becomes options.env.API_SECRET.
Bare booleans already accept negation:
cli.option('--open', 'Open browser')
// --open -> { open: true }
// --no-open -> { open: false }
Declare an explicit negated option only when the negated form should appear in help, should default to true, or must pair with a required-value option:
cli
.option('--no-config', 'Disable config file')
.option('--config <path>', 'Use a custom config file')
For array-valued options, prefer repeated flags:
--include a --include b --include c
CAC parses that as:
{ include: ['a', 'b', 'c'] }
Do not model array input as --include a,b,c and split the string manually unless the CLI intentionally documents comma-separated syntax. If consumers should always receive an array, use type: [] so even one --include a becomes ['a'].
Scenario References
Use the reference that matches the user's task:
| Scenario | Reference |
|---|---|
| Parse args plus global options | basic-usage |
| Parse explicit argv outside normal Node flow | browser |
| Add examples to command help | command-examples |
| Add command-scoped options | command-options |
| Define an empty-name default command | default-command |
| Make a named command also act as default | default-command-inverted |
| Use CAC from Deno | deno |
| Parse nested option keys | dot-nested-options |
| Combine help, version, global options, and a default command | help |
| Show defaults in help without filling parsed output | ignore-default-value |
| Declare an explicit negated option | negated-option |
| Combine multiple subcommands in one CLI | sub-command |
| Accept trailing positional lists | variadic-arguments |
Centralized Error Handling
try {
cli.parse(process.argv, { run: false })
await cli.runMatchedCommand()
} catch (error) {
console.error(error)
process.exit(1)
}
Important Behaviors
- When a matched command has an action, CAC validates unknown options, missing option values, missing required args, and unused extra args.
- Use
allowUnknownOptions()only when forwarding another tool's flags. - Use
ignoreOptionDefaultValue()when help should show defaults but parsed output should stay sparse. - Variadic positional args arrive as arrays.
options['--']is reserved for everything after a double dash.command.alias(name)accepts plain aliases; usealias('!')to mark a named command as the default command.- Listen to
command:<name>,command:!, andcommand:*when you need matched/default/unknown-command hooks.
API Surface
CLI
cac(name?)
cli.command(name, description?, config?)
cli.option(name, description, config?)
cli.help(callback?)
cli.version(version, customFlags?)
cli.usage(text)
cli.example(example)
cli.parse(argv?, { run }?)
cli.runMatchedCommand()
cli.outputHelp()
cli.outputVersion()
cli.parse() returns { args, options } and also sets:
cli.rawArgs
cli.args
cli.options
cli.matchedCommand
cli.matchedCommandName
Command
command.option(name, description, config?)
command.action(callback)
command.alias(name)
command.allowUnknownOptions()
command.ignoreOptionDefaultValue()
command.example(example)
command.usage(text)
Option config supports:
{
default?: any
type?: any[]
}
Use type: [] to force array output and type: [String] to transform each value.