npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

classy-commander

v4.1.0

Published

A TypeScript wrapper for Commander that lets you easily declare commands using classes & decorators and provides strongly typed arguments.

Downloads

158

Readme

npm version Build Status Coverage Status

Features

  • Write commands as modular classes that can be easily tested
  • Specify command usage via a class with decorators
  • Command values
  • Optional values
  • Options
  • Options with values
  • Automatic coercion
  • Version from package.json
  • Support for Inversion of Control containers like Inversify

Install

npm install classy-commander --save

Usage

First enable support for decorators in your tsconfig.json compiler options.

{
  "compilerOptions": {
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
  }
}

Let's create a simple Calculator CLI app with a command that adds two numbers.

Our entry-point looks like this.

./calc.ts

import * as cli from 'classy-commander';

import './commands/add.ts';

cli.execute();

Our add command looks like this.

./commands/add.ts

import { Command, command, value } from 'classy-commander';

export class AddCommandParams {
  @value()
  value1: number = 0;

  @value()
  value2: number = 0;
}

@command('add', AddCommandParams, 'Adds two numbers')
export class AddCommand implements Command<AddCommandParams> {

  execute(params: AddCommandParams) {
    const { value1, value2 } = params;

    const result = value1 + value2;

    console.log(`${value1} + ${value2} = ${result}`);
  }

}

For simplicity, we'll use ts-node to run our app.

Running ts-node ./calc add 1 2 outputs:

1 + 2 = 3

Using optional values

But what if we want to add 3 numbers?

Lets allow adding an optional third number.

import { Command, command, value } from 'classy-commander';

export class AddCommandParams {
  @value()
  value1: number = 0;

  @value()
  value2: number = 0;

  @value({ optional: true })
  value3: number = 0;
}

@command('add', AddCommandParams, 'Adds two or three numbers')
export class AddCommand implements Command<AddCommandParams> {

  execute(params: AddCommandParams) {
    const { value1, value2, value3 } = params;

    const result = value1 + value2 + value3;

    if (value3) {
      console.log(`${value1} + ${value2} + ${value3} = ${result}`);
    } else {
      console.log(`${value1} + ${value2} = ${result}`);
    }
  }

}

Running ts-node ./calc add 1 2 3 now outputs:

1 + 2 + 3 = 6

Adding two numbers still works. ts-node ./calc add 1 2 outputs:

1 + 2 = 3

Variadic Arguments

Okay, but what if we want to add 4 numbers, or 5? This could get messy.

It's time to turn our values into a variadic value.

import { Command, command, value } from 'classy-commander';

export class AddCommandParams {
  @value({ variadic: { type: Number } })
  values: number[] = [];
}

@command('add', AddCommandParams, 'Adds two or more numbers')
export class AddCommand implements Command<AddCommandParams> {

  execute(params: AddCommandParams) {
    const { values } = params;

    const result = values.reduce((total, val) => total + val, 0);

    console.log(`${values.join(' + ')} = ${result}`);
  }

}

Running ts-node ./calc add 1 2 3 4 5 now outputs:

1 + 2 + 3 + 4 + 5 = 15

Using options

Let's add an option to show thousand separators.

import { Command, command, option, value } from 'classy-commander';

export class AddCommandParams {
  @value({ variadic: { type: Number } })
  values: number[] = [];

  @option({ shortName: 't' })
  thousandSeparators: boolean = false;
}

@command('add', AddCommandParams, 'Adds two or more numbers')
export class AddCommand implements Command<AddCommandParams> {

  execute(params: AddCommandParams) {
    const { values, thousandSeparators } = params;

    const result = values.reduce((total, val) => total + val, 0);

    const format = (val: number) => val.toLocaleString(undefined, {
      useGrouping: thousandSeparators
    });

    console.log(`${values.map((val) => format(val)).join(' + ')} = ${format(result)}`);
  }

}

Running ts-node ./calc add 500 1000 --thousandSeparators or ts-node ./calc add 500 1000 -t will output:

500 + 1,000 = 1,500

Using option values

Lets add an option with a value that lets us specify the number of decimal places to show.

import { Command, command, option, value } from 'classy-commander';

export class AddCommandParams {
  @value({ variadic: { type: Number } })
  values: number[] = [];

  @option({ shortName: 't' })
  thousandSeparators: boolean = false;

  @option({ shortName: 'd', valueName: 'count' })
  decimalPlaces: number = 0;
}

@command('add', AddCommandParams, 'Adds two or more numbers')
export class AddCommand implements Command<AddCommandParams> {

  execute(params: AddCommandParams) {
    const { values, thousandSeparators, decimalPlaces } = params;

    const result = values.reduce((total, val) => total + val, 0);

    const format = (val: number) => val.toLocaleString(undefined, {
      useGrouping: thousandSeparators,
      maximumFractionDigits: decimalPlaces
    });

    console.log(`${values.map((val) => format(val)).join(' + ')} = ${format(result)}`);
  }

}

Running ts-node ./calc add 1 2.2345 --decimalPlaces 2 will output:

1 + 2.23 = 3.23

Getting usage

Running ts-node ./calc.ts --help outputs:

  Usage: calc [options] [command]

Options:

  -h, --help                 output usage information

Commands:

  add [options] <values...>

Running ts-node ./calc.ts add --help shows the usage for our add command:

Usage: add [options] <values...>

Options:

  -t, --thousandSeparators
  -d, --decimalPlaces <count>   (default: 0)
  -h, --help                   output usage information

Dependency Injection

To keep our add command easy to test, lets move that heavy math into a calculator service, and have that service automatically injected into the command when it gets created. Let's use the awesome Inversify library which has excellent support for TypeScript (though in principal we could use any JavaScript Dependency Injection library).

Let's start by adding the calculator service.

./services/calculator.ts

import { injectable } from 'inversify';

@injectable()
export class Calculator {
  add(...amounts: number[]) {
    return amounts.reduce((total, amount) => total + amount, 0);
  }
}

Now lets update our add command to use the service.

./commands/add.ts

import { injectable } from 'inversify';
import { Command, command, option, value } from 'classy-commander';
import { Calculator } from '../services/calculator';

export class AddCommandParams {
  @value({ variadic: { type: Number } })
  values: number[] = [];

  @option({ shortName: 't' })
  thousandSeparators: boolean = false;

  @option({ shortName: 'd', valueName: 'count' })
  decimalPlaces: number = 0;
}

@command('add', AddCommandParams, 'Adds two or more numbers')
@injectable()
export class AddCommand implements Command<AddCommandParams> {
  constructor(private calculator: Calculator) {
  }

  execute(params: AddCommandParams) {
    const { values, thousandSeparators, decimalPlaces } = params;

    const result = this.calculator.add(...values);

    const format = (val: number) => val.toLocaleString(undefined, {
      useGrouping: thousandSeparators,
      maximumFractionDigits: decimalPlaces
    });

    console.log(`${values.map((val) => format(val)).join(' + ')} = ${format(result)}`);
  }

}

Finally, in our entrypoint, lets create our inversify container and pass it to classy-commander.

./calc.ts

import { Container } from 'inversify';
import * as cli from 'classy-commander';

import './commands/add.ts';
import './services/calculator';

const container = new Container({ autoBindInjectable: true });

cli
  .ioc(container)
  .execute();

Specifying the version

There are two ways to specify the version of your CLI:

Using the version in your package.json.

import * as cli from 'classy-commander';

...

cli
  .versionFromPackage(__dirname)
  .execute();

Or manually.

import * as cli from 'classy-commander';

...

cli
  .version('1.2.3')
  .execute();

Loading commands from a directory

Maybe we end up adding a bunch of commands to our CLI app and we don't want to manually import each command in our entry point like below:

import * as cli from 'classy-commander';

import './commands/add.ts';
import './commands/subtract.ts';
import './commands/multiply.ts';
import './commands/divide.ts';
import './commands/square.ts';
import './commands/squareRoot.ts';
import './commands/cube.ts';
import './commands/cubeRoot.ts';

cli.execute();

We can tell classy-commander to dynamically load all commands from a directory thus reducing our imports.

import * as cli from 'classy-commander';
import * as path from 'path';

async function run() {
  await cli.commandsFromDirectory(path.join(__dirname, '/commands'));
  cli.execute();
}

run().catch(console.error);

Contributing

Got an issue or a feature request? Log it.

Pull-requests are also welcome. 😸