@jib/cli
TypeScript icon, indicating that this package has built-in type declarations

0.1.7 • Public • Published

The jib Command Line Framework

This project is meant to serve as a lightweight, reusable, TypeScript first CLI framework for projects with any level of CLI command functionality.

npm version wercker status codecov GitHub license install size

Why 'jib'

OK, so there's actually some method to the madness... The @jib/cli was inspired by the extremely popular Commander.js CLI development framework. In sailing, the jib is an essential component to commanding a ship - thus the name.

| jib |: A triangular staysail set forward of the forwardmost mast.

While it's not a dependency, this package was also somewhat influenced by Heroku's @oclif/command - namely that it's designed to be highly performant for large CLI applications. Other similiarities are:

  • TypeScript first development experience.
  • Only require(s) a file when its command is called.
  • Minimal dependencies & VERY lightweight.

Some additional benefits of @jib/cli include:

  • Run any combination of single/multi command CLIs in one project.
  • Supports custom command delimiter syntax (such as <command>:<subcommand> or <command> <subcommand>).
  • Built-in logger and ui stream classes.
  • Simple plugin framework.

Quickstart

The best way get started with @jib/cli is to use the @jib/jib project generator.

npx @jib/jib init --help

Structure

The basis for command processing is the project structure where they're defined. The example below shows one possible structure and its resulting commands.

sample $> tree
.
├── bin
├── package.json
├── src
│   └── commands
│       ├── help.ts
│       └── project
│           ├── build.ts
│           └── init
│               ├── desktop.ts
│               ├── mobile.ts
│               └── web.ts
└── tsconfig.json
  1. sample help
  2. sample project build
  3. sample project init <desktop|mobile|web>

Get Started

  1. Install & configure the @jib/cli in package.json:
{
  "jib": {
    "commandDir": "./build/commands", // default "commands"
    "commandDelim": ":", // default " "
  },
  "dependencies": {
    "@jib/cli": "latest"
  }
}

NOTE: that the "jib" configuration should reference the final compiled project outputs. The @jib/cli parser will automatically detect development envs where ts-node is used to transpile on the fly.

  1. Add a "bin" configuration to package.json where the node executable is located within the project, such as ./bin/jib:
#! /usr/bin/env node

/* Example `bin/jib` implementation */
const { CLI } = require('@jib/cli');

const parser = new CLI({ /* options */ });
parser.parse(process.argv);
/* "bin" entry in package.json */
{
  "bin": {
    "myjib": "bin/jib"
  }
}

NOTE: because of certain nuances in local development enviromments, it is best to use a static file as the bin, rather than a file emitted by TypeScript. Generally it's a good idea to ensure this file has executable permissions (ie chmod +x bin/jib in the example above).

  1. Configure the new CLI({ /* options */ }) for your implementation:
Option Description Default Required
baseDir Used to specify an alternate project directory. ⚠️ Unlikely to be necessary - approach with caution '.'
commandDelim Use a custom delimiter for subcommands. Must have length === 1 ' '
commandDir Directory where command implementations are stored. This should be the transpiled output directory, such as "build/commands" null
rootCommand Run a single command implementation when arguments don't resolve as subcommands. For example, create a main.ts implementation and specify rootCommand: "main" null
  1. Add files and folders to the commandDir path, then invoke by name:

Example command implementation hello.ts

import { Command, BaseCommand } from '@jib/cli';

export interface IOptsHello {
  /* optionally define option types for `run` method signature */
}

@Command({
  description: 'my hello world command',
  args: [ /* configure command args here */ ],
  options: [ /* configure option flags here */ ],
})
export class Hello extends BaseCommand {
  public async run(options: IOptsHello, ...args: any[]) {
    this.ui.output('hello world');
  }
}

Command Anatomy

This is meant to cover all facets of @jib/cli command implementations. If you'd rather move along and read details of CLI processing, then skip ahead to the run method section.

Command and BaseCommand

The first line of most commands will import two objects from @jib/cli. These objects represent the foundation of any command implementation, however only the use of Command is required.

import { Command, BaseCommand } from '@jib/cli';
ref ¯\(ツ)
Command Class decorator providing static command configuration as "annotations"
BaseCommand Extensible command abstract that declares the public async run() contract, and provides ui and logger member instantiations - use is optional

@Command decorator

Aside from the project structure, this is the main instruction between a command implementation and the parser. The @Command() decorator is what describes the command and its arguments/options.

@Command({
  description: 'The purpose of your command',
  args: [ /* configure command args here */ ],
  options: [ /* configure option flags here */ ],
})
export class MyCommand extends BaseCommand { /* see below*/ }

Command Arguments

As part of the @Command annotations, args are specified as an array of argument definitions where ICommandArgument represents a single argument definition.

interface {
  args?: ICommandArgument[];
}
ICommandArgument Type Description Default Required
name string The argument name to be rendered in usage text ''
optional boolean Indication of whether or not the argument is optional false
multi boolean Indicate variadic argument (multiple args as one array). If true, then must also be the last false

Command Options

Specifing options for a command is also done with the @Command decorator.

interface {
  options?: ICommandOption[];
}
ICommandOption Type Description Default Required
flag string The option flag syntax null
description string Option description for help rendering ''
default any Default option value null
fn `((val: any, values: any) => any) RegExp` Value processing function, especially useful for accepting multiple values with a single flag null

Option Flag Syntax

Each flag may be written in the format [-{short}, ]--{name}[ [value]|<value>]. Some examples:

  • -c, --cheese <type> requires "type" in cheese option
  • -p, --project [path] "path" is optional for the project option
  • -d, --debug simple boolean option
  • --test a longhand only flag

Short boolean flags may be passed as a single argument, such as -abc. Multi-word arguments like --with-token become camelCase, as options.withToken. Also note that multi-word arguments with --no prefix will result in false as the option name value. So --no-output would parse as options.output === false.

If necessary, refer to commander for more information.

The run method

The last part of a command implementation is its public async run method. Once the parser is done parsing, and the program is done programming, it's finally time for a resolved command to run.

interface {
  /* Async method called on the instantiated command */
  run(options: {[name: string]: any}, ...args: any[]): Promise<any>;
}

Naturally it is the job of each command implementation to do whatever magic it must do according to its user's wishes. Hmm... sort of like a genie. All the program needs is for this method to return a Promise.

As is shown in the call signature above, the first argument will be the resolved options as defined in the decorator. Note that each property key will be defined as the long, camelCased option name (unless using the --no prefix as mentioned).

Then, all resolved args will be passed in the order which they are defined, again by using the decorator. Note that if an argument is declared as multi: true, then its value will be the final argument, and of type string[].

Plugins

Support for plugins is in it's early stages. Once stable, more information will be added here. Stay tuned...

TypeScript

This project is designed to embody the many benefits of using TypeScript, and recommends that users do the same. While vanilla JS is technically possible, it is not officially supported at this time.

TODOs

  • [x] Add support for single, named, command - this is configured as "rootCommand"
  • [ ] Enable command ordering with ICommand.priority annotation
  • [ ] Support command group with file implementation (descriptor) & directory of same name
  • [ ] Include test harness for implementations @jib/cli/testing
  • [ ] Add Dockerfile to cli implementations for docker-based execution
  • [ ] Add built-in support for stdin pipes
  • [ ] Add support for custom text additions on -v|--version (copyright, foo, etc.)
  • [ ] Add support for command mispellings
  • [ ] Add support for command aliases
  • [x] Add support for plugins
    • [ ] Yeoman generator framework @jib/cli-codegen
    • [ ] Config Storage framework @jib/cli-storage
    • [ ] Prompting @jib/cli-prompt
    • [ ] UI spinner @jib/cli-spinner
    • [ ] UI table @jib/cli-table
    • [ ] UI image rendering @jib/cli-image
    • [ ] UI video rendering @jib/cli-video

Readme

Keywords

Package Sidebar

Install

npm i @jib/cli

Weekly Downloads

4

Version

0.1.7

License

MIT

Unpacked Size

108 kB

Total Files

49

Last publish

Collaborators

  • mvandervliet