Modern type-safe way of building CLIs with TypeScript or JavaScript
byDrizzle Team
import{command,string,boolean,run}from"@drizzle-team/brocli";
constpush=command({
name:"push",
options:{
dialect:string().enum("postgresql","mysql","sqlite"),
databaseSchema:string().required(),
databaseUrl:string().required(),
strict:boolean().default(false),
},
handler:(opts)=>{
...
},
});
run([push]);// parse shell arguments and run command
Brocli is meant to solve a list of challenges we've faced while building Drizzle ORMCLI companion for generating and running SQL schema migrations:
- Explicit, straightforward and discoverable API
- Typed options(arguments) with built in validation
- Ability to reuse options(or option sets) across commands
- Transformer hook to decouple runtime config consumption from command business logic
--version
,-v
as either string or callback- Command hooks to run common stuff before/after command
- Explicit global params passthrough
- Testability, the most important part for us to iterate without breaking
- Themes, simple API to style global/command helps
- Docs generation API to eliminate docs drifting
If you need API referece -see here,this list of practical example is meant to a be a zero to hero walk through for you to learn Brocli 🚀
Simple echo command with positional argument:
import{run,command,positional}from"@drizzle-team/brocli";
constecho=command({
name:"echo",
options:{
text:positional().desc("Text to echo").default("echo"),
},
handler:(opts)=>{
console.log(opts.text);
},
});
run([echo])
~bun run index.tsecho
echo
~bun run index.tsechotext
text
Print version with--version -v
:
...
run([echo],{
version:"1.0.0",
);
~bun run index.ts --version
1.0.0
Version accepts async callback for you to do any kind of io if necessary before printing cli version:
import{run,command,positional}from"@drizzle-team/brocli";
constversion=async()=>{
// you can run async here, for example fetch version of runtime-dependend library
constenvVersion=process.env.CLI_VERSION;
console.log(chalk.gray(envVersion),"\n");
};
constecho=command({...});
run([echo],{
version:version,
);
command → name
command → desc
command → shortDesc
command → aliases
command → options
command → transform
command → handler
command → help
command → hidden
command → metadata
Broclicommand
declaration has:
name
- command name, will be listed inhelp
desc
- optional description, will be listed in the commandhelp
shortDesc
- optional short description, will be listed in the all commands/all subcommandshelp
aliases
- command name aliases
hidden
- flag to hide command fromhelp
help
- command help text or a callback to print help text with dynamically provided config
options
- typed list of shell arguments to be parsed and provided totransform
orhandler
transform
- optional hook, will be called before handler to modify CLI params
handler
- called with either typedoptions
ortransform
params, place to run your command business logic
metadata
- optional meta information for docs generation flow
name
,desc
,shortDesc
andmetadata
are provided to docs generation step
import{command,string,boolean}from"@drizzle-team/brocli";
constpush=command({
name:"push",
options:{
dialect:string().enum("postgresql","mysql","sqlite"),
databaseSchema:string().required(),
databaseUrl:string().required(),
strict:boolean().default(false),
},
transform:(opts)=>{
},
handler:(opts)=>{
...
},
});
import{command}from"@drizzle-team/brocli";
constcmd=command({
name:"cmd",
options:{
dialect:string().enum("postgresql","mysql","sqlite"),
schema:string().required(),
url:string().required(),
},
handler:(opts)=>{
...
},
});
Initial builder functions:
-
string(name?: string)
- defines option as a string-type option which requires data to be passed as--option=value
or--option value
name
- name by which option is passed in cli args
If not specified, defaults to key of this option
⚠️ - must not contain=
character, not be in--help
,-h
,--version
,-v
and be unique per each command
💬 - will be automatically prefixed with-
if one character long,--
if longer
If you wish to have only single hyphen as a prefix on multi character name - simply specify name with it:string('-longname')
-
number(name?: string)
- defines option as a number-type option which requires data to be passed as--option=value
or--option value
name
- name by which option is passed in cli args
If not specified, defaults to key of this option
⚠️ - must not contain=
character, not be in--help
,-h
,--version
,-v
and be unique per each command
💬 - will be automatically prefixed with-
if one character long,--
if longer
If you wish to have only single hyphen as a prefix on multi character name - simply specify name with it:number('-longname')
-
boolean(name?: string)
- defines option as a boolean-type option which requires data to be passed as--option
name
- name by which option is passed in cli args
If not specified, defaults to key of this option
⚠️ - must not contain=
character, not be in--help
,-h
,--version
,-v
and be unique per each command
💬 - will be automatically prefixed with-
if one character long,--
if longer
If you wish to have only single hyphen as a prefix on multi character name - simply specify name with it:boolean('-longname')
-
positional(displayName?: string)
- defines option as a positional-type option which requires data to be passed after a command ascommand value
displayName
- name by which option is passed in cli args
If not specified, defaults to key of this option
⚠️ - does not consume options and data that starts with
Extensions:
-
.alias(...aliases: string[])
- defines aliases for optionaliases
- aliases by which option is passed in cli args
⚠️ - must not contain=
character, not be in--help
,-h
,--version
,-v
and be unique per each command
💬 - will be automatically prefixed with-
if one character long,--
if longer
If you wish to have only single hyphen as a prefix on multi character alias - simply specify alias with it:.alias('-longname')
-
.desc(description: string)
- defines description for option to be displayed inhelp
command -
.required()
- sets option as required, which means that application will print an error if it is not present in cli args -
.default(value: string | boolean)
- sets default value for option which will be assigned to it in case it is not present in cli args -
.hidden()
- sets option as hidden - option will be omitted from being displayed inhelp
command -
.enum(values: [string,...string[]])
- limits values of string to one of specified herevalues
- allowed enum values
-
.int()
- ensures that number is an integer -
.min(value: number)
- specified minimal allowed value for numbersvalue
- minimal allowed value
⚠️ - does not limit defaults
-
.max(value: number)
- specified maximal allowed value for numbersvalue
- maximal allowed value
⚠️ - does not limit defaults
Normally, you can write handlers right in thecommand()
function, however there might be cases where you'd want to define your handlers separately.
For such cases, you'd want to infer type ofoptions
that will be passes inside your handler.
You can do it usingTypeOf
type:
import{string,boolean,typeTypeOf}from'@drizzle-team/brocli'
constcommandOptions={
opt1:string(),
opt2:boolean('flag').alias('f'),
// And so on...
}
exportconstcommandHandler=(options:TypeOf<typeofcommandOptions>)=>{
// Your logic goes here...
}
Or by usinghandler(options, myHandler () => {...})
import{string,boolean,handler}from'@drizzle-team/brocli'
constcommandOptions={
opt1:string(),
opt2:boolean('flag').alias('f'),
// And so on...
}
exportconstcommandHandler=handler(commandOptions,(options)=>{
// Your logic goes here...
});
To define commands, usecommand()
function:
import{command,typeCommand,string,boolean,typeTypeOf}from'@drizzle-team/brocli'
constcommandOptions={
opt1:string(),
opt2:boolean('flag').alias('f'),
// And so on...
}
constcommands:Command[]=[]
commands.push(command({
name:'command',
aliases:['c','cmd'],
desc:'Description goes here',
shortDesc:'Short description'
hidden:false,
options:commandOptions,
transform:(options)=>{
// Preprocess options here...
returnprocessedOptions
},
handler:(processedOptions)=>{
// Your logic goes here...
},
help:()=>'This command works like this:...',
subcommands:[
command(
// You can define subcommands like this
)
]
}));
Parameters:
-
name
- name by which command is searched in cli args
⚠️ - must not start with-
character, be equal to [true
,false
,0
,1
] (case-insensitive) and be unique per command collection -
aliases
- aliases by which command is searched in cli args
⚠️ - must not start with-
character, be equal to [true
,false
,0
,1
] (case-insensitive) and be unique per command collection -
desc
- description for command to be displayed inhelp
command -
shortDesc
- short description for command to be displayed inhelp
command -
hidden
- sets command as hidden - iftrue
,command will be omitted from being displayed inhelp
command -
options
- object containing command options created usingstring
andboolean
functions -
transform
- optional function to preprocess options before they are passed to handler
⚠️ - type of return mutates type of handler's input -
handler
- function, which will be executed in case of successful option parse
⚠️ - must be present if your command doesn't have subcommands
If command has subcommands but no handler, help for this command is going to be called instead of handler -
help
- function or string, which will be executed or printed when help is called for this command
⚠️ - if passed, takes prevalence over theme'scommandHelp
event -
subcommands
- subcommands for command
⚠️ - command can't have subcommands andpositional
options at the same time -
metadata
- any data that you want to attach to command to later use in docs generation step
After defining commands, you're going to need to executerun
function to start command execution
import{command,typeCommand,run,string,boolean,typeTypeOf}from'@drizzle-team/brocli'
constcommandOptions={
opt1:string(),
opt2:boolean('flag').alias('f'),
// And so on...
}
constcommandHandler=(options:TypeOf<typeofcommandOptions>)=>{
// Your logic goes here...
}
constcommands:Command[]=[]
commands.push(command({
name:'command',
aliases:['c','cmd'],
desc:'Description goes here',
hidden:false,
options:commandOptions,
handler:commandHandler,
}));
// And so on...
run(commands,{
name:'mysoft',
description:'MySoft CLI',
omitKeysOfUndefinedOptions:true,
argSource:customEnvironmentArgvStorage,
version:'1.0.0',
help:()=>{
console.log('Command list:');
commands.forEach(c=>console.log('This command does... and has options...'));
},
theme:async(event)=>{
if(event.type==='commandHelp'){
awaitmyCustomUniversalCommandHelp(event.command);
returntrue;
}
if(event.type==='unknownError'){
console.log('Something went wrong...');
returntrue;
}
returnfalse;
},
hook:(event,command)=>{
if(event==='before')console.log(`Command '${command.name}' started`)
if(event==='after')console.log(`Command '${command.name}' succesfully finished it's work`)
}
})
Parameters:
-
name
- name that's used to invoke your application from cli.
Used for themes that print usage examples, example:
app do-task --help
results inUsage: app do-task <positional> [flags]...
Default:undefined
-
description
- description of your app
Used for themes, example:
myapp --help
results in
MyApp CLI
Usage: myapp [command]...
Default:undefined
-
omitKeysOfUndefinedOptions
- flag that determines whether undefined options will be passed to transform\handler or not
Default:false
-
argSource
- location of array of args in your environment
⚠️ - first two items of this storage will be ignored as they typically contain executable and executed file paths
Default:process.argv
-
version
- string or handler used to print your app version
⚠️ - if passed, takes prevalence over theme's version event -
help
- string or handler used to print your app's global help
⚠️ - if passed, takes prevalence over theme'sglobalHelp
event -
theme(event: BroCliEvent)
- function that's used to customize messages that are printed on various events
Return:
true
|Promise<true>
if you consider event processed
false
|Promise<false>
to redirect event to default theme -
hook(event: EventType, command: Command)
- function that's used to execute code before and after every command'stransform
andhandler
execution
-
commandsInfo(commands: Command[])
- get simplified representation of your command collection
Can be used to generate docs -
test(command: Command, args: string)
- test behaviour for command with specified arguments
⚠️ - if command hastransform
,it will get called, howeverhandler
won't -
getCommandNameWithParents(command: Command)
- get subcommand's name with parent command names
InBroCLI
,command doesn't have to be the first argument, instead it may be passed in any order.
To make this possible, hovewer, option that's passed right before command should have an explicit value, even if it is a flag:--verbose true <command-name>
(does not apply to reserved flags: [--help
|-h
|--version
|-v
])
Options are parsed in strict mode, meaning that having any unrecognized options will result in an error.