Skip to content

Latest commit

 

History

History
125 lines (88 loc) · 4.01 KB

README.md

File metadata and controls

125 lines (88 loc) · 4.01 KB

@jitl/cli

Straightforward and type-safe command-line argument parsing with subcommands.

Example

import { createCLICommand, validate, logger, runThenExitIfMain, runCommand } from '@jitl/cli'

export const command = createCLICommand({
  description: `Say hello`,
  flags: {
    name: {
      description: `What name should we greet?`,
      validate: validate.string(),
    },
    exuberant: {
      description: `Are we excited?`,
      validate: validate.boolean(),
      default: () => false,
    },
  },
  async run({ name, exuberant }) {
    logger.log(`Hello, ${name}. Nice to see you today${exuberant ? '!' : '.'}`)
  },
})

runThenExitIfMain(module, (name, argv) => runCommand(name, command, argv))

Motivation

  1. Type-safe without stuttering. While existing libraries offer typings via @types/... packages, few infer the arguments of the run() function of a command from the parameters.

    The parameters of the run(flags, args, rest) function are completely inferred from the runtime type specification, so your CLI command declarations are succinct and readable.

  2. Library, not a framework. Existing Typescript frameworks like oclif force a specific setup, and contain many layers of abstraction between invocation and your run() function.

    This package offers flexibility through composition, rather than a complex "plug-ins" or "hooks" interface.

  3. Usable conventions. Many command-line frameworks follow typical Unix conventions, which are suitable for experts, but provide a poor user experience for casual CLI users.

    • Prefer long (GNU-style) named flags like --bool | --value given-value. Removes mental burden of argument order, and makes reading your shell history more clear.

      Positional arguments are still available for glob-related use-cases.

    • Prompt users for missing required arguments. Don't kick the user and force them to read the help.

  4. Bike-shedding. There are many command-line frameworks out there. It's easy to be opinionated about the "mistakes" (choices) a CLI framework makes.

    Because this package is short, has a flat dependency graph, and favors composition over hooks/plugins/inheritance, it's easy to replace, wrap, extend, smoosh, and make it your own.

Dependencies

This package only imports its dependencies when they are needed for a specific purpose.

  • chalk: imported when displaying help output. Used to bold text, which has a nice appearance.

  • table: imported when displaying help output. Used to format the table of flags, etc.

  • fs-extra: imported when loading a directory of subcommands. Used to scan a directory for subcommand files. For better performance and type-safety, but also more boilerplate, consider manually constructing a CommandLoaders object.

  • inquirer: imported when a user fails to provide a required argument. Used to interactively prompt the user for the needed value.

In short, this package never imports a dependency on the happy path to run a fully-specified command.

Documentation

For now, you'll need to read the code.

Known Deficiencies

  • Design modifications for the Command interface to allow nesting subcommands easily without custom code. Currently, because we throw if an unknown flag is given, a standard CLI command cannot dispatch to subcommands unless they both share the same flags.

    An elegant design would allow for arbitrary nesting of commands and subcommand loaders.

  • The current validate: Validator<T> interface is not introspectable at runtime, which limits the usefulness and accuracy of help messages for an argument. All we can show to guide the user is the default value.

    A usable system should be able to infer specific syntax based on the type of the flag.

  • Documentation.

    It should be auto-generated from TSDoc comments in the source.

  • Examples for subcommands.

    This should go in the ./examples directory.

  • Tests.

    Ah, tests.