CLI

Command-line Interface (CLI) programs are programs that interact via the terminal in the form of text input and text output. The advantage of interacting with the application in this variant is that only a terminal must exist either locally or via an SSH connection.

A CLI application in Deepkit has full access to the DI container and can thus access all providers and configuration options.

The arguments and options of the CLI application are controlled by method parameters via TypeScript types and are automatically serialized and validated.

CLI is one of three entry points to a Deepkit Framework application. In the Deepkit framework, the application is always launched via a CLI program, which is itself written in TypeScript by the user. Therefore, there is no Deepkit specific global CLI tool to launch a Deepkit application. This is how you launch the HTTP/RPC server, perform migrations, or run your own commands. This is all done through the same entry point, the same file. Once the Deepkit framework is used by importing FrameworkModule from @deepkit/framework, the application gets additional commands for the application server, migrations, and more.

The CLI framework allows you to easily register your own commands and is based on simple classes. In fact, it is based on @deepkit/app, a small package intended only for this purpose, which can also be used standalone without the deepkit framework. In this package you can find decorators that are needed to decorate the CLI controller class.

Controllers are managed or instantiated by the Dependency Injection container and can therefore use other providers. See the Dependency Injection chapter for more details.

Installation

Since CLI programs in Deepkit are based on Runtime Types, it is necessary to have @deepkit/type already installed correctly. See Runtime Type Installation.

If this is done successfully, @deepkit/app can be installed or the Deepkit framework which already uses the library under the hood.

npm install @deekpit/app

Note that @deepkit/app is based on TypeScript decorators and this feature must be enabled accordingly with experimentalDecorators.

file: tsconfig.json

{
  "compilerOptions": {
    "module": "CommonJS",
    "target": "es6",
    "moduleResolution": "node",
    "experimentalDecorators": true
  },
  "reflection": true
}

Once the library is installed, the API of it can be used directly.

Use

To create a command for your application, you need to create a CLI controller. This is a simple class that has an exeecute method and is equipped with information about the command.

File: app.ts

#!/usr/bin/env ts-node-script
import { App, cli } from '@deepkit/app';

@cli.controller('test', {
    description: 'My first command'
})
class TestCommand {
    async execute() {
        console.log('Hello World')
    }
}

new App({
    controllers: [TestCommand]
}).run();

In the decorator @cli.controller the unique name of the CLI application is defined as the first argument. Further options like a description can be optionally added in the object at the second position.

This code is already a complete CLI application and can be started this way:

$ ts-node ./app.ts
VERSION
  Node

USAGE
  $ ts-node app.ts [COMMAND]

COMMANDS
  test

You can see that a "test" command is available. To execute this, the name must be passed as an argument:

$ ts-node ./app.ts test
Hello World

It is also possible to make the file executable using chmod +x app.ts, so that the command ./app.ts is already sufficient to start it. Note that then a so-called Shebang is necessary. Shebang denotes the character combination #! at the beginning of a script program. In the example above this is already present: #!/usr/bin/env ts-node-script and uses the script mode of ts-node.

$ ./app.ts test
Hello World

In this way, any number of commands can be created and registered. The unique name specified in @cli.controller should be well chosen and allows grouping of commands with the : character (e.g. user:create, user:remove, etc).

Arguments

To add arguments, new parameters are added to the execute method and decorated with the @arg decorator.

import { cli, arg } from '@deepkit/app';

@cli.controller('test')
class TestCommand {
    async execute(
        @arg name: string
    ) {
        console.log('Hello', name);
    }
}

If you execute this command now without specifying a name, an error will be issued:

$ ./app.ts test
RequiredArgsError: Missing 1 required arg:
name

By using --help you will get more information about the required arguments:

$ ./app.ts test --help
USAGE
  $ ts-node-script app.ts test NAME

Once the name is passed as an argument, the execute method in TestCommand is executed and the name is passed correctly.

$ ./app.ts test "beautiful world"
Hello beautiful world

Flags

Flags are another way to pass values to your command. Mostly these are optional, but they don`t have to be. Parameters decorated with @flag name can be passed via --name value or --name=value.

import { flag } from '@deepkit/app';

class TestCommand {
    async execute(
        @flag id: number
    ) {
        console.log('id', id);
    }
}
$ ./app.ts test --help
USAGE
  $ ts-node app.ts test

OPTIONS
  --id=id  (required)

In the help view you can see in the "OPTIONS" that a --id flag is necessary. If you enter this flag correctly, the command will receive this value.

$ ./app.ts test --id 23
id 23

$ ./app.ts test --id=23
id 23

Boolean Flags

Flags have the advantage that they can also be used as a worthless flag, for example to activate a certain behavior. As soon as a parameter is marked as an optional Boolean, this behavior is activated.

import { flag } from '@deepkit/app';

class TestCommand {
    async execute(
        @flag remove: boolean = false
    ) {
        console.log('delete?', remove);
    }
}
$ ./app.ts test
delete? false

$ ./app.ts test --remove
delete? true

Multiple Flags

To pass multiple values to the same flag, a flag can be marked as an array.

import { flag } from '@deepkit/app';

class TestCommand {
    async execute(
        @flag id: number[] = []
    ) {
        console.log('ids', id);
    }
}
$ ./app.ts test
ids: []

$ ./app.ts test --id 12
ids: [12]

$ ./app.ts test --id 12 --id 23
ids: [12, 23]

Single Character Flags

To allow a flag to be passed as a single character as well, @flag.char('x') can be used.

import { flag } from '@deepkit/app';

class TestCommand {
    async execute(
        @flag.char('o') output: string
    ) {
        console.log('output: ', output);
    }
}
$ ./app.ts test --help
USAGE
  $ ts-node app.ts test

OPTIONS
  -o, --output=output  (required)


$ ./app.ts test --output test.txt
output: test.txt

$ ./app.ts test -o test.txt
output: test.txt

Optional / Default

The signature of the method execute defines which arguments or flags are optional. If the parameter is optional in the type system, the user does not have to provide it.

class TestCommand {
    async execute(
        @arg name?: string
    ) {
        console.log('Hello', name || 'nobody');
    }
}
$ ./app.ts test
Hello nobody

The same for parameters with a default value:

class TestCommand {
    async execute(
        @arg name: string = 'body'
    ) {
        console.log('Hello', name);
    }
}
$ ./app.ts test
Hello nobody

This also applies to flags in the same way.

Serialization / Validation

All arguments and flags are automatically deserialized based on its types, validated and can be provided with additional constraints.

Thus, arguments defined as numbers are always guaranteed to be real numbers in the controller, even though the command-line interface is based on text and thus strings. The conversion happens automatically with the feature xref:serialization.adoc#serialization-loosely-conversion.

class TestCommand {
    async execute(
        @arg id: number
    ) {
        console.log('id', id, typeof id);
    }
}
$ ./app.ts test 123
id 123 number

Additional constraints can be defined with the type decorators from @deepkit/type.

import { Positive } from '@deepkit/type';

class TestCommand {
    async execute(
        @arg id: number & Positive
    ) {
        console.log('id', id, typeof id);
    }
}

The type Postive in id indicates that only positive numbers are wanted. If the user now passes a negative number, the code in execute will not be executed at all and an error message will be presented.

$ ./app.ts test -123
Validation error in id: Number needs to be positive [positive]

If the number is positive, this works again as before. This additional validation, which is very easy to do, makes the command much more robust against wrong entries. See the chapter Validation for more information.

Description

To describe a flag or argument, @flag.description or @arg.description can be used respectively.

import { Positive } from '@deepkit/type';

class TestCommand {
    async execute(
        @arg.description('The users identifier') id: number & Positive,
        @flag.description('Delete the user?') remove: boolean = false,
    ) {
        console.log('id', id, typeof id);
    }
}

In the help view, this description appears after the flag or argument:

$ ./app.ts test --help
USAGE
  $ ts-node app.ts test ID

ARGUMENTS
  ID  The users identifier

OPTIONS
  --remove  Delete the user?

Exit Code

The exit code is 0 by default, which means that the command was executed successfully. To change the exit code, a number other than 0 should be returned in the exucute method.

@cli.controller('test')
export class TestCommand {
    async execute() {
        console.error('Error :(');
        return 12;
    }
}
$ ./app.ts
Error :(
$ echo $?
12

Dependency Injection

The class of the command is managed by the DI Container, so dependencies can be defined that are resolved via the DI Container.

#!/usr/bin/env ts-node-script
import { App, cli } from '@deepkit/app';
import { Logger, ConsoleTransport } from '@deepkit/logger';

@cli.controller('test', {
    description: 'My super first command'
})
class TestCommand {
    constructor(protected logger: Logger) {
    }

    async execute() {
        this.logger.log('Hello World!');
    }
}

new App({
    providers: [{provide: Logger, useValue: new Logger([new ConsoleTransport]}],
    controllers: [TestCommand]
}).run();