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 @deepkit/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();