A TypeScript wrapper for Commander that lets you easily declare commands using classes & decorators and provides you with strongly typed arguments.
- 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
npm install classy-commander --saveFirst 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*asclifrom'classy-commander';import'./commands/add.ts';cli.execute();Our add command looks like this.
./commands/add.ts
import{Command,command,value}from'classy-commander';exportclassAddCommandParams{
@value()value1: number=0;
@value()value2: number=0;}
@command('add',AddCommandParams,'Adds two numbers')exportclassAddCommandimplementsCommand<AddCommandParams>{execute(params: AddCommandParams){const{ value1, value2 }=params;constresult=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
But what if we want to add 3 numbers?
Lets allow adding an optional third number.
import{Command,command,value}from'classy-commander';exportclassAddCommandParams{
@value()value1: number=0;
@value()value2: number=0;
@value({optional: true})value3: number=0;}
@command('add',AddCommandParams,'Adds two or three numbers')exportclassAddCommandimplementsCommand<AddCommandParams>{execute(params: AddCommandParams){const{ value1, value2, value3 }=params;constresult=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
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';exportclassAddCommandParams{
@value({variadic: {type: Number}})values: number[]=[];}
@command('add',AddCommandParams,'Adds two or more numbers')exportclassAddCommandimplementsCommand<AddCommandParams>{execute(params: AddCommandParams){const{ values }=params;constresult=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
Let's add an option to show thousand separators.
import{Command,command,option,value}from'classy-commander';exportclassAddCommandParams{
@value({variadic: {type: Number}})values: number[]=[];
@option({shortName: 't'})thousandSeparators: boolean=false;}
@command('add',AddCommandParams,'Adds two or more numbers')exportclassAddCommandimplementsCommand<AddCommandParams>{execute(params: AddCommandParams){const{ values, thousandSeparators }=params;constresult=values.reduce((total,val)=>total+val,0);constformat=(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
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';exportclassAddCommandParams{
@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')exportclassAddCommandimplementsCommand<AddCommandParams>{execute(params: AddCommandParams){const{ values, thousandSeparators, decimalPlaces }=params;constresult=values.reduce((total,val)=>total+val,0);constformat=(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
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
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()exportclassCalculator{add(...amounts: number[]){returnamounts.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';exportclassAddCommandParams{
@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()exportclassAddCommandimplementsCommand<AddCommandParams>{constructor(privatecalculator: Calculator){}execute(params: AddCommandParams){const{ values, thousandSeparators, decimalPlaces }=params;constresult=this.calculator.add(...values);constformat=(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*asclifrom'classy-commander';import'./commands/add.ts';import'./services/calculator';constcontainer=newContainer({autoBindInjectable: true});cli.ioc(container).execute();There are two ways to specify the version of your CLI:
Using the version in your package.json.
import*asclifrom'classy-commander';
...
cli.versionFromPackage(__dirname).execute();Or manually.
import*asclifrom'classy-commander';
...
cli.version('1.2.3').execute();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*asclifrom'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*asclifrom'classy-commander';import*aspathfrom'path';asyncfunctionrun(){awaitcli.commandsFromDirectory(path.join(__dirname,'/commands'));cli.execute();}run().catch(console.error);Got an issue or a feature request? Log it.
Pull-requests are also welcome. 😸
