clirio
TypeScript icon, indicating that this package has built-in type declarations

0.1.0 • Public • Published

Clirio

A mini framework for node.js command-line interfaces based on TypeScript, decorators, DTOs

NOTE This lib is alpha quality. There is no guarantee it will be reliably. The documentation also needs to be corrected

Installation

npm install clirio
yarn add clirio

Peer dependencies

reflect-metadata@^0.1 joi@^17

Quick Start

For example to emulate git status cli command with options - 3 easy steps to build an app

  1. Create Dto
import { Option } from 'clirio';

class GitStatusDto {
  @Option('--branch, -b')
  readonly branch?: string;

  @Option('--ignore-submodules')
  readonly ignoreSubmodules?: 'none' | 'untracked' | 'dirty' | 'all';

  @Option('--short, -s')
  readonly short?: boolean;
}
  1. Create module
import { Module, Command, Options } from 'clirio';

@Module()
export class GitModule {
  @Command('git status')
  public status(@Options() options: GitStatusDto) {
    console.log(options);
  }
}
  1. Configure a base class
import { Clirio } from 'clirio';

const clirio = new Clirio();
clirio.addModule(GitModule);
clirio.build();
Result
$ cli git status -b master --ignore-submodules  all --short
{ branch: 'master', ignoreSubmodules: 'all', short: true }

Docs

The application structure should consist of the following parts:

  1. the base class - Clirio
  2. modules (custom classes)
  3. actions (methods in class-modules with decorators )

Examples here

A starter kit is in progress

The base class

Clirio - is the base class which configures the application and links modules

const cli = new Clirio();
cli.setModules([HelloModule, CommonModule, GitModule, MigrationModule]);
cli.build();

Methods

setConfig

Setting global configuration

cli.setConfig({
  nullableOptionValue: true,
  validateOptionsWithoutDto: true,
});
Param Description Default
nullableOptionValue value conversion of command line options that have no value e.g. --verbose - initially it is null after conversion will be true true
validateOptionsWithoutDto if dto options are not specified but options will be passed in the command, then there may be a validation error true
addModule

Adding one module

cli.addModule(PingModule);
setModules

Setting several modules

cli.setModules([HelloModule, CommonModule]);
setArgs

Arguments will be determined automatically but it is possible to set them manually

cli.setArgs(['git', 'add', 'test.txt', 'logo.png']);
onError

Callback for handling error

import chalk from 'chalk';

cli.onError((err: ClirioError) => {
  console.log(chalk.red(err.message));
  process.exit(9);
});
onSuccess

Callback for handling an success result

cli.onSuccess((data: ClirioSuccess) => {
  const successMessage = 'The command has been executed successfully!';
  console.log(chalk.green(data.message ?? successMessage));
  process.exit(0);
});
onWarning

Callback for handling an warning result

cli.onWarning((data: ClirioWarning) => {
  console.log(chalk.yellow(data.message));
  process.exit(0);
});
onComplete

Callback for handling an complete result

cli.onComplete((data: ClirioComplete) => {
  const message = data.message ?? 'Thanks!';

  console.log(chalk.blue(message));
  process.exit(0);
});
onDebug

Callback for handling an debugging error

cli.onDebug((err: ClirioDebug) => {
  const output = err.format();

  console.log(chalk.red(output));
  process.exit(5);
});

Modules

Modules are custom classes with the @Module() decorator (they can be considered as controllers). An application can have either one or many modules. Each module contains actions (patterns for commands)

Example of common module
@Module()
export class CommonModule {
  @Command('hello there')
  public helloThere() {
    // ...
  }

  @Command('migration run')
  public migrationRun() {
    // ...
  }
}

As a result 2 commands will be available:

$ cli hello there
$ cli migration run

Using different modules to separate unrelated commands and structure the code

@Module('hello')
export class HelloModule {
  @Command('there')
  public helloThere() {
    // ...
  }
}

@Module('migration')
export class MigrationModule {
  @Command('run')
  public run() {
    // ...
  }
}

Actions

The actions are methods in class-modules with decorators

@Module()
export class HelloModule {
  @Command('hello')
  public helloThere() {
    console.log('Hello! It works!');
  }

  @Command('bye')
  public help() {
    console.log('Bye! See you later');
  }

  @Empty()
  public empty() {
    console.log(chalk.yellow('You have not entered anything'));
  }

  @Failure()
  public failure() {
    console.log(chalk.red('You have entered a non-existent command'));
  }
}

Decorator for command pattern

The @Command() decorator takes command pattern

@Module()
export class MigrationModule {
  @Command('migration init')
  public initMigration() {}

  @Command('migration run|up')
  public runMigration() {}

  @Command('migration to <name>')
  public migrateTo() {}

  @Command('migration merge <name1> <name2>')
  public mergeMigrations() {}

  @Command('migration delete <...names>')
  public deleteMigrations() {}
}
Exact match

The pattern of one or more space-separated arguments. Exact match will work

@Command('hello')

@Command('hello there')

@Command('hello my friends')
Match variants

Using the | operator to select match variants

@Command('hello|hey|hi')

@Command('migration run|up')
Pattern with value masks

Using the < > operator to specify a place for any value

@Command('hello <first-name> <last-name>')

@Command('set-time <time>')
Pattern with rest values mask

Using the <... > operator to specify a place for array of values This kind of mask can be only one per command pattern

@Command('hello <...all-names>')

@Command('message <...words>')

To get the entered values you should use the @Params() decorator and DTO, that is described in more detail below

Option match

This pattern is designed for special cases like "help" and "version". This is an exact match of the option key and value. Match variants can be separated by comma

@Command('--help, -h')

@Command('--version, -v')

@Command('--mode=check')

Decorator for empty input

The @Empty() action decorator is a way to catch the case when nothing is entered Each module can have its own @Empty() decorator in an action

@Module()
export class CommonModule {
  @Command('hello')
  public hello() {}

  @Empty()
  public empty() {
    console.log("You haven't entered anything");
  }
}
$ cli
You haven't entered anything

When a module has a command prefix, it will be matched and ranked

@Module('migration')
export class MigrationModule {
  @Command('init')
  public initMigration() {}

  @Empty()
  public empty() {
    console.log(
      'The migration module requires additional instruction. Type --help'
    );
  }
}
$ cli migration
The migration module requires additional instruction. Type --help

Decorator for failure input

The @Failure() action decorator is a way to catch the case when the specified command patterns don't match. Each module can have its own @Failure() decorator in an action if this decorator is not specified, then a default error will be displayed

@Module()
export class CommonModule {
  @Command('hello')
  public hello() {}

  @Failure()
  public failure() {
    console.log('There is no such a command!');
  }
}
$ cli goodbye
There is no such a command!

When a module has a command prefix, it will be matched and ranked

@Module('migration')
export class MigrationModule {
  @Command('init')
  public initMigration() {}

  @Failure()
  public failure() {
    console.log('The migration module got the wrong instruction');
  }
}
$ cli migration stop
The migration module got the wrong instruction

Injecting data

Using special decorators to pass input data

@Module()
export class LocatorModule {
  @Command('get-location <city>')
  public getLocation(@Params() params: unknown, @Options() options: unknown) {
    console.log(params);
    console.log(options);
  }
}
$ cli get-location Prague --format=DMS --verbose
{ city: "Prague" }
{ format: "DMS", verbose: true }

Instead of unknown types, you should use a DTOs in which the properties also have special decorators to have type checking and input validation. More detailed below

Passing command params

The "Params" term mean the values of the masks in the command pattern The @Params() decorator provided

For example
@Module()
export class HelloModule {
  @Command('hello <first-name> <last-name>')
  public hello(@Params() params: HelloParamsDto) {
    console.log(params);
  }
}

Here the second and third parts are masks for any values that the user enters The hello method will be called if the user enters a three-part command. The last 2 parts are passed to the params argument as keys and values

Params Dto

The @Param() decorator for dto properties provided. It can take a key in an param mask to map DTO properties

export class HelloParamsDto {
  @Param('first-name')
  readonly firstName?: string;

  @Param('last-name')
  readonly lastName?: string;
}
$ cli hello Alex Smith
{ firstName: "Alex", lastName: "Smith" }

The @Param() decorator may have no arguments. In this case there will be no key mapping

export class HelloParamsDto {
  @Param()
  readonly 'first-name'?: string;

  @Param()
  readonly 'last-name'?: string;
}
$ cli hello Alex Smith
{ "first-name": "Alex", "last-name": "Smith" }
Example with rest values mask
@Module()
export class GitModule {
  @Command('git add <...all-files>')
  public add(@Params() params: AddParamsDto) {
    // Type checking works for "params" variable
    console.log(params.allFiles);
  }
}
class AddParamsDto {
  @Param('all-files')
  readonly allFiles: string[];
}
$ cli git add test.txt logo.png
['test.txt', 'logo.png']

Passing command options

The "Options" term mean arguments starting with a dash. Each option is either a key-value or a key. If in the beginning 2 dashes is a long key if one dash is a short key which must be 1 character long: --name=Alex, --name Alex, -n Alex, --version, -v

The @Options() decorator provided

@Module()
export class GitModule {
  @Command('git status')
  public status(@Options() options: GitStatusOptionsDto) {
    console.log(options);
  }
}
Options Dto

The @Option() decorator for dto properties provided. It can accept key aliases (comma separated) to map DTO properties

class GitStatusOptionsDto {
  @Option('--branch, -b')
  readonly branch?: string;

  @Option('--ignore-submodules, -i')
  readonly ignoreSubmodules?: 'none' | 'untracked' | 'dirty' | 'all';

  @Option('--short, -s')
  readonly short?: boolean;
}
$ cli git status --branch=master --ignore-submodules=all --short

$ cli git status --branch master --ignore-submodules all --short

$ cli git status -b master -i all -s

Each input data will lead to one result:

{ branch: 'master', ignoreSubmodules: 'all', short: true }

The @Option() decorator may have no arguments. In this case there will be no key mapping and no aliases

class GitStatusOptionsDto {
  @Option()
  readonly branch?: string;

  @Option()
  readonly 'ignore-submodules'?: 'none' | 'untracked' | 'dirty' | 'all';

  @Option()
  readonly short?: boolean;
}
$ cli git status --branch=master --ignore-submodules=all --short
{ branch: 'master', "ignore-submodule": 'all', short: true }
Array of values in options

By default, the command parser cannot determine whether an option is an array. You can specify this, in which case the same option names will be collected in an array, even if there is only one option

@Module()
export class Module {
  @Command('model')
  public model(@Options() options: ModelOptionsDto) {
    console.log(options);
  }
}
class ModelOptionsDto {
  @Option('--name, -n', {
    isArray: true,
  })
  readonly names: string[];
}
$ cli model --name Ford
{ names: ['Ford'] }
$ cli model --name Ford -n Tesla
{ names: ['Ford', 'Tesla'] }
Variable values in options

There is a special case for using variables. All variables will be collected in an object

@Module()
export class Module {
  @Command('connect')
  public connect(@Options() options: DbConnectOptionsDto) {
    console.log(options);
  }
}
class DbConnectOptionsDto {
  @Option('--env, -e', {
    variable: true,
  })
  readonly envs: Record<string, string>;
}
$ cli connect -e DB_NAME=db-name -e DB_USER=db-user
{ envs: { DB_USER: 'db-name', DB_USER: 'db-user' } }

Using Joi

All values that come out as a results of parsing the command are either strings or booleans To validate and convert to the desired type - use Joi and DTO type annotations

Clirio uses and re-exports the joi-class-decorators package https://www.npmjs.com/package/joi-class-decorators

Joi options validation
import { Module, Command, Options } from 'clirio';

@Module()
export class GitModule {
  @Command('git status')
  public status(@Options() options: GitStatusDto) {
    console.log(options);
  }
}
import Joi from 'joi';
import { Option, JoiSchema } from 'clirio';

class GitStatusDto {
  @Option('--branch, -b')
  @JoiSchema(Joi.string().required())
  readonly branch: string;

  @Option('--ignore-submodules')
  @JoiSchema(
    Joi.string()
      .valid('none' | 'untracked' | 'dirty' | 'all')
      .optional()
  )
  readonly ignoreSubmodules?: 'none' | 'untracked' | 'dirty' | 'all';

  @Option('--short, -s')
  @JoiSchema(Joi.boolean().optional())
  readonly short?: boolean;
}
$ cli git status --ignore-submodules
"branch" is required
$ cli git status --log=true
"log" is an unknown key
$ cli git status -b master --ignore-submodules  all --short
{ branch: 'master', ignoreSubmodules: 'all', short: true }
Joi params validation
@Module()
export class GitModule {
  @Command('git checkout <branch>')
  public checkout(@Params() params: CheckoutParamsDto) {
    console.log(params);
  }
}
class CheckoutParamsDto {
  @Param('branch')
  @JoiSchema(Joi.string().required())
  readonly branch: string;
}
$ cli git checkout develop
{  branch: 'develop' }
Joi validating and converting

Joi validates and converts input values that are originally string. That is a very useful feature.

Summation and concatenation examples
@Module()
export class SumModule {
  @Command('sum <first> <second>')
  public sum(@Params() params: SumParamsDto) {
    console.log(params.first + params.second);
  }
}
Without Joi
class SumParamsDto {
  @Param()
  readonly first: unknown;

  @Param()
  readonly second: unknown;
}
$ cli sum 5 15
'515'
$ cli sum 5 rabbits
'5rabbits'
With Joi
class SumParamsDto {
  @Param()
  @JoiSchema(Joi.number().required())
  readonly first: number;

  @Param()
  @JoiSchema(Joi.number().required())
  readonly second: number;
}
$ cli sum 5 15
20
$ cli sum 5 rabbits
"second" is not a number

Special cases

Help mode

Special case for the command as an option designed

@Module()
export class CommonModule {
  @Command('-h, --help')
  public help() {
    console.log('Description of commands is here');
  }
}
$ cli --help
Description of commands is here

Of course you can use other options and params

@Command('-m, --man')

@Command('help|h')

@Command('man <command>')

Helper decorator

The @Helper() decorator provided to handle help mode

import { Module, Command, Description, Helper, ClirioHelper } from 'clirio';

@Module()
export class CommonModule {
  @Command('hello there')
  @Description('Say hello there')
  public helloThere() {
    // ...
  }

  @Command('-h, --help')
  public help(@Helper() helper: ClirioHelper) {
    const moduleDescription = helper.describeAllModules();
    console.log(ClirioHelper.formatModuleDescription(moduleDescription));
  }
}

The @Description() decorator for module action provided to describe the command

The ClirioHelper class provides methods for getting descriptions of commands and formatting

The helper.describeAllModules() method provides description for all commands

helper.describeAllModules

The method returns array of objects

Key Type Description
command string full formed command
moduleCommand string | null module-bound command
actionCommand string action-bound command
description string text from the @Description() decorator
optionDescription array of objects description of command options in action
optionDescription
Key Type Description
options array of string option aliases
description string text from the @Description() decorator
type string type of variable
itemType string | null type of item variable if the base type is array

You can format the received data custom or use the ClirioHelper.formatModuleDescription static method

Hidden command in Helper

The @Hidden() decorator for module action provided to hide description of the command

import { Module, Command, Hidden, Description } from 'clirio';

@Module()
export class Module {
  @Command('debug')
  @Hidden()
  public debug() {
    // ...
  }

  @Command('hello there')
  @Description('Say hello there')
  public helloThere() {
    // ...
  }
}

The helper.describeAllModules() method will not provide description of the debug command in this case

Version

import { Module, Command } from 'clirio';

@Module()
export class CommonModule {
  @Command('-v, --version')
  public version() {
    console.log('1.3.1');
  }
}
$ cli --version
1.3.1

Exceptions

Special exceptions designed to complete the script with the desired result

Exception Description Handler
new ClirioError(message: string) Error cli.onError(callback)
new ClirioSuccess(message?: string) Success cli.onSuccess(callback)
new ClirioWarning(message: string) Warning cli.onWarning(callback)
new ClirioComplete(message?: string) Complete cli.onComplete(callback)
new ClirioDebug(message: string) Debugging cli.onDebug(callback)

By default, all handlers in Clirio are configured, but you can override to your own callback for each ones Use one of the available exceptions to throw the desired event, after that the related callback will be called and the script will end

Examples
import { Module, Command, ClirioError } from 'clirio';

@Module()
export class CommonModule {
  @Command('check')
  public check() {
    throw new ClirioError('Not working!');
  }
}
const cli = new Clirio();
cli.addModule(CommonModule);
cli.onError((err: ClirioError) => {
  console.log(chalk.red('An error occurred: ' + err.message));
});
cli.build();
$ cli check
An error occurred: Not working!
import { Module, Command, ClirioSuccess } from 'clirio';

@Module()
export class CommonModule {
  @Command('start')
  public start() {
    throw new ClirioSuccess();
  }
}
const cli = new Clirio();
cli.addModule(CommonModule);
cli.onSuccess((data: ClirioSuccess) => {
  if (data.message) {
    console.log(chalk.green(message));
  } else {
    console.log(chalk.green('Successfully!'));
  }
});
cli.build();
$ cli start
Successfully!

Receipts

Array of values in options

import { Module, Command, Options } from 'clirio';

@Module()
export class Module {
  @Command('model')
  public model(@Options() options: ModelOptionsDto) {
    console.log(options);
  }
}
import Joi from 'joi';
import { Option, JoiSchema } from 'clirio';

class ModelOptionsDto {
  @Option('--name, -n', {
    isArray: true,
  })
  @JoiSchema(Joi.array().items(Joi.string()).required())
  readonly names: string[];
}

Variable values in options

import { Module, Command, Options } from 'clirio';

@Module()
export class Module {
  @Command('connect')
  public connect(@Options() options: DbConnectOptionsDto) {
    console.log(options);
  }
}
import Joi from 'joi';
import { Option, JoiSchema } from 'clirio';

class DbConnectOptionsDto {
  @Option('--env, -e', {
    variable: true,
  })
  @JoiSchema(Joi.object().pattern(Joi.string(), Joi.string()))
  readonly envs: Record<string, string>;
}

Example with rest values mask

import { Module, Command, Params } from 'clirio';

@Module()
export class GitModule {
  @Command('git add <...all-files>')
  public add(@Params() params: AddParamsDto) {
    console.log(params.allFiles);
  }
}
import Joi from 'joi';
import { Param, JoiSchema } from 'clirio';

class AddParamsDto {
  @Param('all-files')
  @JoiSchema(Joi.array().items(Joi.string()).required())
  readonly allFiles: string[];
}

Contributing

Contributing flow is in progress

Install

npm i clirio

DownloadsWeekly Downloads

0

Version

0.1.0

License

MIT

Unpacked Size

446 kB

Total Files

415

Last publish

Collaborators

  • stepanzabelin