0.1.7 • Published 4 years ago

@jib/cli v0.1.7

Weekly downloads
14
License
MIT
Repository
github
Last release
4 years ago

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:
OptionDescriptionDefaultRequired
baseDirUsed to specify an alternate project directory. ⚠️ Unlikely to be necessary - approach with caution'.'
commandDelimUse a custom delimiter for subcommands. Must have length === 1' '
commandDirDirectory where command implementations are stored. This should be the transpiled output directory, such as "build/commands"null
rootCommandRun 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¯\(ツ)
CommandClass decorator providing static command configuration as "annotations"
BaseCommandExtensible 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[];
}
ICommandArgumentTypeDescriptionDefaultRequired
namestringThe argument name to be rendered in usage text''
optionalbooleanIndication of whether or not the argument is optionalfalse
multibooleanIndicate variadic argument (multiple args as one array). If true, then must also be the lastfalse

Command Options

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

interface {
  options?: ICommandOption[];
}
ICommandOptionTypeDescriptionDefaultRequired
flagstringThe option flag syntaxnull
descriptionstringOption description for help rendering''
defaultanyDefault option valuenull
fn((val: any, values: any) => any) | RegExpValue processing function, especially useful for accepting multiple values with a single flagnull

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

  • 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
  • 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
0.1.7

4 years ago

0.1.6

4 years ago

0.1.5

5 years ago

0.1.4

5 years ago

0.1.3

5 years ago

0.1.2

5 years ago

0.1.1

5 years ago

0.1.0

5 years ago

0.0.7

6 years ago

0.0.6

6 years ago

0.0.5

6 years ago

0.0.4

6 years ago

0.0.3

6 years ago

0.0.2

6 years ago

0.0.1

6 years ago