README
CLI Builder
A highly customizable command line library.
Version 7
clibuilder v7 is released!
It is once again re-written from v6 to improve the usage in a fundamental way. Here are some of the highlights:
- single
cli()
for both basic cli and plugin cli - config, argument, and option type inference now work with both basic type and array
- using
zod@next
to define type definition and validation - each command can have their own config specification
- support combining short options
-abc 100 => -a -b -c 100
Features
- plugin support: write commands in separate packages and reuse by multiple cli
- configuration file support
- type inference for config, arguments, and options
- nested commands
my-cli cmd1 cmd2 cmd3
- type validation for config, arguments, and options
using zod@next (exported asz
)
Install
yarn add clibuilder
Usage
You can use clibuilder
to create your command line application in many different ways.
The most basic way looks like this:
cli()
.default({ run() { /* ...snip... */ }})
.parse(process.argv)
.catch(e => process.exit(e?.code || 1))
The above code will:
- get your application name, version, and description from your
package.json
- call your default
run()
method when invoked - handle errors so you won't get a ugly NodeJS stacktrace.
You can specify name, version, and description directly:
cli({ name: 'foo', version: '1.2.3', description: 'some fool' })
You can add additional commands and sub-commands:
cli()
.command({ name: 'hello', run() { this.ui.info('hello world') }})
.command({
name: 'repo',
commands:[
command({ name: 'create', run() { /* ..snip.. */ }})
]
})
You can add alias to the command:
cli()
.command({
name: 'search-packages',
alias: ['sp'],
/* ..snip.. */
})
You can specify arguments:
cli().default({
arguments: [
// type defaults to string
{ name: 'name', description: 'your name' }
],
run(args) { this.ui.info(`hello, ${args.name}`) }
})
import { cli, z } from 'clibuilder'
cli().command({
name: 'sum',
arguments: [
// using `zod` to specify number[]
{ name: 'values', description: 'values to add', type: z.array(z.number()) }
],
run(args) {
// inferred as number[]
return args.values.reduce((p, v) => p + v, 0)
}
})
Of course, you can also specify options:
cli().default({
options: {
// type defaults to boolean
'no-progress': { description: 'disable progress bar' },
run(args) {
if (args['no-progress']) this.ui.info('disable progress bar')
}
}
})
and you can add option alias too:
cli().command({
options: {
project: {
alias: ['p']
}
}
})
You can use z
to mark argument and/or options as optional
import { cli, z } from 'clibuilder'
cli().default({
arguments: [{ name: 'a', description: '', type: z.optional(z.string()) }],
options: {
y: { type: z.optional(z.number()) }
}
})
If you invoke a command expecting a config
,
the config will be loaded.
Each command defines their own config.
import { cli, z } from 'clibuilder'
cli()
.default({
config: z.object({ presets: z.string() }),
run() {
this.ui.info(`presets: ${this.config.presets}`)
}
})
By default, the config file can be ${cli}.json
, .${cli}rc.json
, or .${cli}rc
.
You can override the config name too:
cli({ configName: 'another-config' })
One important feature of clibuilder
is supporting plugins.
You can load plugins by calling loadPlugins()
:
cli().loadPlugins()
By default, it uses ${cli}-plugin
as the keyword to identify plugins.
You can change that by:
cli().loadPlugins('another-keyword')
When you create a command from a different files or for plugin,
you can use the command()
function which provides type validation and inference support.
import { command, z } from 'clibuilder'
export const echo = command({
name: 'echo',
config: z.object({ a: z.string() }),
run() { this.ui.info(`echo ${this.config.a}`)}
})
Defining Plugins
clibuilder
allows you to build plugins to add commands to your application.
i.e. You can build your application in a distributed fashion.
cli().loadPlugins()
will load plugins when available.
To create a plugins:
- export an
activate(ctx: PluginActivationContext)
function - add the plugin keyword in your
package.json
import { command, PluginActivationContext } from 'clibuilder'
// in plugin package
const sing = command({ ... })
const dance = command({ ... })
export function activate({ addCommand }: PluginCli.ActivationContext) {
addCommand({
name: 'miku',
commands: [sing, dance]
})
}
// in plugin's package.json
{
"keywords": ['your-app-plugin']
}
The cli will determine that a package is a plugin by looking at the keywords
in its package.json
.
testing
testCommand()
can be used to test your command:
import { command, testCommand } from 'clibuilder'
test('some test', async () => {
const { result, messages } = await testCommand(command({
name: 'cmd-a',
run() {
this.ui.info('miku')
return 'x'
}
}), 'cmd-a')
expect(result).toBe('x')
expect(messages).toBe('miku')
})
shebang
To make your cli easily executable, you can add shebang to your script:
#!/usr/bin/env node
// your code