A declarative wrapper around Yargs for building beautiful, fluent command line interfaces
$ black-pearl hoist the colors --black-flag
Black Flag is a fairly thin library that wraps yargs, extending its capabilities with several powerful declarative features. It can be used to create simple single-level CLIs or deeply nested sprawling interfaces alike.
Black Flag was built as a drop-in replacement for vanilla Yargs, specifically
for users of the yargs::commandDir()
(which has its
issues). Its features include:
- Declarative-first sync/async APIs β¨
- Zero configuration required β¨
- It's still yargs all the way down β¨ (nothing brand new to learn!)
- Built-in support for dynamic options β¨ (an infamous Yargs white whale)
- Consistent and safe CLI execution β¨
- Simple comprehensive error handling and reporting β¨
- A pleasant unit, integration, and e2e testing experience β¨
- Extensive intellisense support via TypeScript β¨
Black Flag is tested on Ubuntu and Windows 10, and like Yargs tracks Node.js LTS versions. Also comes with first-class support for both CJS and ESM source.
β β β ββ β β Quick start
β β β βββ β β Step-by-step getting started guide
β β β βββ β β Black Flag versus vanilla Yargs
β β β βββ β β Simple demo CLI project (or npx -p @black-flag/demo myctl --help
)
β β β βββ β β Black Flag recipes for solving common CLI design problems
β β β βββ β β Yargs's intro documentation
Tip
If you find yourself a fan of Black Flag's more declarative DX and want to go
all the way, check out Black Flag Extensions (BFE). BFE is a collection
of surprisingly simple set-theoretic APIs that build on
yargs::options()
for a fully declarative developer experience. BFE
also protects you from a couple Yargs footguns that Black Flag by itself
cannot.
You may also be interested in Black Flag Checks (BFC), which offers
several pluggable yargs::check
functionsβlike checkIsNotNegative
and
checkArrayNotEmpty
βbuilt to work with BFE.
To install:
npm install @black-flag/core
And if you're ready to go all in on Black Flag's declarative API, check out Black Flag Extensions:
npm install @black-flag/extensions
Install Black Flag:
npm install @black-flag/core
Create the file that will run your CLI, perhaps at ./cli.js
:
Tip
Both CJS and ESM source is acceptable!
#!/usr/bin/env node
import { runProgram } from '@black-flag/core';
export default runProgram(import.meta.resolve('./commands'));
Then create your root command, perhaps at ./commands/index.js
:
export const name = 'pirate-parser';
export const usage = 'Usage: $0 <cmd> [args]';
Finally, create your sub-command, perhaps at ./commands/hello.js
:
export const command = '$0 [name]';
export const description =
'Welcome ter black flag, a declarative wrapper around yargs!';
export function builder(blackFlag, helpOrVersionSet, argv) {
blackFlag.positional('name', {
type: 'string',
default: 'Cambi',
describe: 'The name to say hello to'
});
// A special --attention flag only available when greeting the captain!
if (helpOrVersionSet || argv?._.at(0) === 'CAPTAIN') {
return {
attention: {
boolean: true,
description: 'Alert the watch that the captain is around'
}
};
}
}
export async function handler(argv) {
if (argv.attention) {
console.log('-!- Captain is on the bridge -!-');
}
console.log(`Hello ${argv.name}, welcome to Black Flag!`);
}
Then run it:
node cli.js --help
Usage: pirate-parser <cmd> [args]
Commands:
pirate-parser hello Welcome ter black flag, a declarative wrapper around yargs!
Options:
--help Show help text [boolean]
--version Show version number [boolean]
node cli.js hello --help
Usage: pirate-parser hello [name]
Welcome ter black flag, a declarative wrapper around yargs!
Positionals:
name The name to say hello to [string] [default: "Cambi"]
Options:
--help Show help text [boolean]
--attention Alert the watch that the captain is around [boolean]
node cli.js hello Parrot
Hello Parrot, welcome to Black Flag!
node cli.js hello CAPTAIN
Hello CAPTAIN, welcome to Black Flag!
node cli.js hello Parrot --attention
Usage: pirate-parser hello [name]
Welcome ter black flag, a declarative wrapper around yargs!
Positionals:
name The name to say hello to [string] [default: "Cambi"]
Options:
--help Show help text [boolean]
Unknown argument: attention
node cli.js hello CAPTAIN --attention
-!- Captain is on the bridge -!-
Hello CAPTAIN, welcome to Black Flag!
Tip
Not sure what makes Black Flag "more declarative" than Yargs? Compare this quick start example to the vanilla Yargs version.
Next steps:
- Check out the step-by-step getting started guide
- Compare Black Flag versus vanilla Yargs
- Play with a simple demo CLI project (or
npx -p @black-flag/demo myctl --help
) - Review Black Flag recipes for solving common CLI design problems
- Deep dive into Black Flag's internals
- Pull up Yargs's intro documentation
- Pore over Yargs's parser tricks (which also apply to Black Flag)
For an example of a production CLI tool that puts Black Flag through its paces,
check out the source code for @-xun/symbiote
.
Further documentation can be found under docs/
and
docs/api/
. Common CLI design "recipes" can be found under
examples/
.
Term | Description |
---|---|
command | A "command" is a functional unit associated with a configuration file and represented internally as a trio of programs: effector, helper, and router. Further, each command is classified as one of: "pure parent" (root and parent), "parent-child" (parent and child), or "pure child" (child). |
program | A "program" is a Yargs instance wrapped in a Proxy granting the instance an expanded set of features. Programs are represented internally by the Program type. |
root | The tippy top command in your hierarchy of commands and the entry point for any Black Flag application. Also referred to as the "root command". |
default command | A "default command" is Yargs parlance for the CLI entry point. Technically there is no concept of a "default command" at the Black Flag level, though there is the root command. |
Expand details
I love Yargs π Yargs is the greatest! I've made dozens of CLI tools with Yargs, each with drastically different interfaces and requirements. Some help manage critical systems.
As I was copying-and-pasting some configs from past projects for yet another tool, I realized the (irritatingly disparate π) structures of my CLI projects up until this point were converging on a set of personal conventions around Yargs. And, as I'm always eager to "optimize" my workflows, I wondered how much common functionality could be abstracted away.
The goal: make my CLIs more stable upon release, much faster to build, and more pleasant to test. And also avoid Yargs's most egregious footguns. But perhaps most important: I wanted CLIs that would remain simple and consistent to maintain.
Throw in a re-watch of the PotC series and Black Flag was born! π΄ββ πΎ
This is a CJS2 package with statically-analyzable exports
built by Babel for use in Node.js versions that are not end-of-life. For
TypeScript users, this package supports both "Node10"
and "Node16"
module
resolution strategies.
Expand details
That means both CJS2 (via require(...)
) and ESM (via import { ... } from ...
or await import(...)
) source will load this package from the same entry points
when using Node. This has several benefits, the foremost being: less code
shipped/smaller package size, avoiding dual package
hazard entirely, distributables are not
packed/bundled/uglified, a drastically less complex build process, and CJS
consumers aren't shafted.
Each entry point (i.e. ENTRY
) in package.json
's
exports[ENTRY]
object includes one or more export
conditions. These entries may or may not include: an
exports[ENTRY].types
condition pointing to a type
declaration file for TypeScript and IDEs, a
exports[ENTRY].module
condition pointing to
(usually ESM) source for Webpack/Rollup, a exports[ENTRY].node
and/or
exports[ENTRY].default
condition pointing to (usually CJS2) source for Node.js
require
/import
and for browsers and other environments, and other
conditions not enumerated here. Check the
package.json file to see which export conditions are
supported.
Note that, regardless of the { "type": "..." }
specified in
package.json
, any JavaScript files written in ESM
syntax (including distributables) will always have the .mjs
extension. Note
also that package.json
may include the
sideEffects
key, which is almost always false
for
optimal tree shaking where appropriate.
See LICENSE.
New issues and pull requests are always welcome and greatly appreciated! π€© Just as well, you can star π this project to let me know you found it useful! βπΏ Or buy me a beer, I'd appreciate it. Thank you!
See CONTRIBUTING.md and SUPPORT.md for more information.
Thanks goes to these wonderful people (emoji key):
Bernard π π» π π§ |
||||||
|
This project follows the all-contributors specification. Contributions of any kind welcome!