Brother command line tool
Create type-safe CLIs to align local development and pipeline workflows.
Powered by vite.
Ever wanted a type safe, easy to set up CLI that is automatically self-updating locally and can be used in CI / CD workflows? Brocolito aims to support you with that.
pnpm create brocolito-cli@latest
# or
npx create-brocolito-cli@latest
# or
yarn create brocolito-cli@latest
# or
deno run -A npm:create-brocolito-cli@latestFeel free to check out the herein provided example.
In your src/main.ts you create this code:
import { CLI } from 'brocolito';
CLI.command('hello').action(() => console.log('hello world'));
CLI.parse(); // this needs to be executed after all "commands" were set upNow you can run cli -h to see the help message or cli hello to print hello world.
For more advanced features see below.
Please check the used custom actions of this repository yourself. It uses pnpm to install the CLI dependencies and a simple setup to set up the CLI.
In this workflow you can see it in action.
The CLI contains already many features right away. It was inspired by commander and cac, but it has a slightly different API and some features that I was found to be missing like native subcommand and tab completion support.
Using the -h or --help options on any command, subcommand or top-level will display an
automatically generated help text from your CLI configuration.
As soon as you have locally set up the completion, you will get automatically suggestions
based on your CLI configuration. Run cli completion to set up the tab completion.
If you want to use an alias for e.g. one of your subcommands like cli get data, you have
to register it to make the completion work like so: CLI.alias('cgd', 'cli get data').
You can run (async) functions to return the completion results for options and args of type string or file by passing it as third argument when specifiying the option/arg. E.g.,
CLI.command("do", "...")
.option("--service <string>", "name of the service", {
// "filter" is the string the user has already typed when running the completion
// ATTENTION: Filtering will work even if you would display all options, but
// it can improve performance to reduce the amount of processed data.
completion: async (filter) => (await getAllServices({ filter })).map(({ name }) => name),
});Options are basically named parameters for your command. The specified option names are
accessible under their camelCase name. E.g. foo-bar becomes fooBar. Values for options can
be specified using a space or = as separator, e.g. --my-option=foo and --my-option foo
are identically treated, whereas --my-option without parameters is only valid for option
flags and will be treated as boolean true.
-
Specification:
--option-name -
Parameter type:
boolean -
Completion: none
-
Code example:
CLI.command('hello', 'prints hello world') .option('--exclamation-mark', 'append exclamation mark') .action(({ exclamationMark }) => console.log(`hello world${exclamationMark ? '!' : ''}`));
-
Shell examples:
cli hello --exclamation-mark cli hello --exclamation-mark=false cli hello --no-exclamation-mark
-
Specification:
--option-name <string> -
Parameter type:
string | undefined -
Completion: none
-
Code example:
CLI.command('hello', 'prints greeting') .option('--name <string>', 'who to greet?') .action(({ name }) => console.log(name ? `hello ${name}` : "Too shy to give me a name?"));
-
Shell example:
cli hello --name mark
-
Specification:
--option-name <file> -
Parameter type:
string | undefined -
Completion: local file system
-
Code example:
CLI.command('char-count', { description: 'count characters', alias: 'cc' }) .option('--content <file>', 'what file to use?') .action(async ({ content }) => console.log((await fs.readFile(content, 'utf-8')).length));
-
Shell example:
cli char-count --content ./foo.txt
-
Specification:
--option-name <one|two> -
Parameter type:
"one" | "two" | undefined -
Completion:
oneortwo -
Code example:
CLI.command('fancy-stuff', "do fancy stuff") .option('--log-level <debug|info|warn|error>', 'what debug level to use (default: error)') .action(({ logLevel = "error" }) => doFancyStuff({ logLevel }));
-
Shell example:
cli fancy-stuff --log-level debug
-
Specification:
--option-name <string...>or--option-name <one|two...> -
Parameter type:
string[] | undefinedor("one" | "two")[] | undefined -
Completion: like for single options
-
Code example:
CLI.command('char-count', { description: 'count characters', alias: 'cc' }) .option('--files <file...>', 'what files to use?') .action(async ({ files }) => files.forEach((f) => console.log((await fs.readFile(f, 'utf-8')).length)));
-
Shell example:
cli char-count --files ./foo.txt --files ./bar.json
-
Specification:
--option-name! <string> -
Parameter type:
string -
Completion: like the non mandatory options
-
Code example:
CLI.command('hello', 'prints greeting') .option('--name! <string>', 'who to greet?') .action(({ name }) => console.log(`hello ${name}`));
-
Shell example:
cli hello --name mark
-
Specification:
--option-name|-s -
Parameter type: any of the above
-
Completion: like for the full option name
-
Code example:
CLI.command('hello', 'prints hello world') .option('--exclamation-mark|-m', 'append exclamation mark') .action(({ exclamationMark }) => console.log(`hello world${exclamationMark ? '!' : ''}`));
-
Shell examples:
cli hello -m
Args are basically unnamed parameters or parameter lists. The specified arg names are
accessible under their camelCase name. E.g. foo-bar becomes fooBar. You can have as
many different args for the same command as you like. You cannot have args and subcommands
on a command. If you cannot have another arg after an arg list.
-
Specification:
<arg-name> -
Parameter type:
string -
Completion: none
-
Code example:
CLI.command('hello', 'prints greeting') .arg('<arg-name>', 'greeting name') .action(({ argName }) => console.log(`hello ${name}`));
-
Shell example:
cli hello mark
-
Specification:
<arg-name:file> -
Parameter type:
string -
Completion: local file system
-
Code example:
CLI.command('exists', 'checks existance') .arg('<file-name:file>', 'what file to check?') .action(({ fileName }) => console.log(fs.existsSync(fileName)));
-
Shell example:
cli exists /tmp/someFile.js
-
Specification:
<arg-name:one|two> -
Parameter type:
"one" | "two" -
Completion:
oneortwo -
Code example:
CLI.command('configure', 'configure settings') .arg('<env:dev|test>', 'chosen env') .action(configureEnv);
-
Shell example:
cli configure dev
-
Specifications:
<arg-name...>or<arg-name:file...> -
Parameter type:
string[] -
Completions: none or local file system
-
Code example:
CLI.command('exists', 'checks existance') .arg('<file-names:file...>', 'what files to check?') .action(({ fileNames }) => console.log(fileNames.map((f) => fs.existsSync(f))));
-
Shell example:
cli exists /tmp/someFile.js ./foo.txt
Subcommands allow you to create a grouped functionality of commands within other commands. Subcommands can be as deeply nested as you like. If a command has subcommands it cannot have args. Options are inherited from the command to all afterwards specified subcommands. Every subcommand can use further options, args or subcommands as any regular command.
-
Code example:
CLI.command('string', 'do something with strings') .option('--error', 'logs as error') .subcommand('trim', 'trims a string', (sub) => { sub .arg('<str>') .action(({ str, error }) => console[error ? 'error' : 'log'](str.trim())); }) .subcommand('length', { description: 'counts the chars', alias: 'l' }, (sub) => { sub .arg('<str>') .action(({ str, error }) => console[error ? 'error' : 'log'](str.length)); })
-
Shell examples:
cli string trim " foo" cli string length "lorem ipsum" cli string l "lorem ipsum"
If you are using aliases for some commands, code completion would stop working correctly, if
you don't register the aliases. E.g. if you have for that command my-cli foo an alias
configured in your shell via alias cf=my-cli foo, then you need to configure it like this in your
package.json or deno.json:
"brocolito": {
"aliases": {
"cf": "my-cli foo"
}
}Also, when setting up completion the shell alias defintion is also already included.
brocolito ships already the following package for your CLI:
pc: Default export of the picocolors package to add colors to your printed output.
prompts: Default export of the prompts package to make use of interactive shell prompts. You need to install along with it@types/prompts.
When you "prepare" with brocolito <runtime> where runtime can be node, deno or bun, then you are opting out
of the Vite powered build, which only happens when running brocolito without args and having installed the
optional vite dependency. This will bundle all your CLI code in a single JS file.
If you are using external dependencies, these have to be listed in your package.json under
"dependencies". In case of build-in NodeJS dependencies, make sure to use the prefixed package
names, e.g. node:fs instead of fs. Otherwise, the dependencies might end up included in your
resulting bundle or aren't correctly resolved at all.
