Skip to content

🏴 A declarative yargs-based framework for building fluent command line interfaces that are easy to maintain and extend

License

Notifications You must be signed in to change notification settings

Xunnamius/black-flag

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

A declarative wrapper around Yargs for building beautiful, fluent command line interfaces
$ black-pearl hoist the colors --black-flag


Black Lives Matter! Last commit timestamp Codecov Source license Uses Semantic Release!

NPM version Monthly Downloads


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:


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.



Install

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

Quick Start

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:

For an example of a production CLI tool that puts Black Flag through its paces, check out the source code for @-xun/symbiote.

Appendix 🏴

Further documentation can be found under docs/ and docs/api/. Common CLI design "recipes" can be found under examples/.

Terminology

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.

Inspiration

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! πŸ΄β€β˜ πŸΎ

Published Package Details

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.

License

See LICENSE.

Contributing and Support

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.

Contributors

All Contributors

Thanks goes to these wonderful people (emoji key):

Bernard
Bernard

πŸš‡ πŸ’» πŸ“– 🚧 ⚠️ πŸ‘€
Add your contributions

This project follows the all-contributors specification. Contributions of any kind welcome!