Commander

For nest-commander, a command is something that the CLI runner should do. This could be like @nestjs/cli’s start or build commands, or it could be something like git’s add or commit. To specify a command, all you need to do is decorate a class that extends the CommandRunner abstract class with the @Command() decorator. No need to use @Injectable() or anything else, the @Command() decorator will take care of allowing dependencies to be injected into the class.

Setting the Command Name and Arguments

The @Command() decorator takes in a set of options that allows the underlying commander package to handle the command properly. The options are things like the command’s name, its arguments, the description, the argsDescription and general options, all of which will get passed to commander and handled as described by their docs.

To set a command’s name, and make it the default actions, you would need to use the decorator as follows. Let’s make a command with the name my-exec. It will also take in a shell command to execute. We’ll call this, task.

@Command({
  name: 'my-exec',
  arguments: '<task>',
  options: { isDefault: true }
})
export class TaskRunner extends CommandRunner {
  async run(inputs: string[], options: Record<string, any>): Promise<void> {}
}

You’ll notice for the arguments we use angle brackets around the argument name. This specifies that the argument is required for the command, and commander will throw an error if it is not supplied. If, however, we wanted to make the argument optional, we could wrap it in square brackets instead like [task].

Now, to run this command, we’ll need to set up the CommandFactory and make use of one of the execution methods as described later in the docs. For now, we’ll just assume this application is installed globally under the crun name. Running the above command would then look like

crun my-exec 'echo Hello World!'
# OR
crun 'echo Hello World!'

We can use either of these because we set up the options: { isDefault: true } options.

This command doesn’t do anything yet, but we’ll get to the implementation of it later. For now, let’s explore how we can set options for the command.

Setting Options for the Command

Options allow users to change certain behaviors of the command. Going back to Nest’s CLI the start command can take a --watch or --debug option to start up in different modes. For nest-commander each of these options goes on its own method of the class it modifies. Going back to our run command, say we wanted to allow the use of a different shell for running the command. The easiest way to add this in would be to allow the user to use a flag, like -s and then the name of the shell to use. Adding this option would then make our class look like the following

@Command({
  name: 'my-exec',
  arguments: '<task>',
  options: { isDefault: true }
})
export class TaskRunner extends CommandRunner {
  async run(inputs: string[], options: Record<string, any>): Promise<void> {}
  @Option({
    flags: '-s, --shell <shell>',
    description: 'A different shell to spawn than the default'
  })
  parseShell(val: string) {
    return val;
  }
}

The reason for each option to be tied to a method handler is because options from the command line come in as strings. Most of the time, this is fine and works well, but there are times where it ends up having problems, like passing booleans. These methods allow developers to set up their own parsing methods for each option, so that when my-exec is called, all of the inputs are validated and ready to be used immediately.

You’ll also notice that the flags property has several parts to it, a -s short flag, a --shell long flag, and <shell>. This means that the user can either use -s or --shell to add this option, but if either is used they must provide a shell option. The shell name ends up being a key of the options parameter for the my-exec method, and can be retrieved later using options.shell.

The description is used when the --help flag is passed, and will help provide more information to the CLI consumer.

You can add as many of these @Option() methods as necessary for your command, so long as they do not duplicate option names. Each method may only have one @Option() decorator as well.

For more details on everything that is possible with options, take a look at commander’s Options documentation.

You can also make an option completely required, like an argument, by setting required: true in the metadata for the option.

Variadic Options

Options also allow for variadic inputs but you will need to provide an option parser that accumulates each option.

  @Option({
    flags: '-c, --options <options...>',
    description: 'Specify options',
  })
  parseOptions(option: string, optionsAccumulator: string[] = []): string[] {
    optionsAccumulator.push(option);
    return optionsAccumulator;
  }

Setting Choices for your Options

Commander also allows us to set up pre-defined choices for options. To do so we have two options: setting the choices array directly as a part of the @Option() decorator, or using the @OptionChoiceFor() decorator and a class method, similar to the InquirerService. With using the @OptionChoiceFor() decorator, we are also able to make use of class providers that are injected into the command via Nest’s DI which allows devs to read for the choices from a file or database if that happens to be necessary.

import { Option, OptionChoiceFor } from 'nest-commander';

@Command({ name: 'my-exec' })
export class RunCommand extends CommandRunner {
  constructor(
    private readonly choiceProvider: {
      getChoicesForRun: () => string[];
    }
  ) {
    super();
  }

  async run(args: any, options: { runWithColor: 'yes' | 'no' }) {
    console.log(options);
  }

  @Option({
    flags: '-c, --color [runWithColor]',
    name: 'withColor',
    description: 'Should the command use color in the output'
  })
  parseColorOption(option: string) {
    return option;
  }

  @OptionChoiceFor({ name: 'withColor' }) // make sure this matches the `name` of an `@Options()` decorator
  getColorChoices() {
    return this.choiceProvider.getChoicesForRun();
  }
}

Adding Custom Help

By default, commander sets help to the --help or -h flag. If you need extra help added to the command, you can use the @Help() decorator on a class method that returns a string. The valid values for the @Help() decorator are before, beforeAll, after and afterAll, just like for commander’s addHelpText method.

Getting the Commander Instance

If for some reason you need access to the commander instance, as of nest-commander@2.4.0 you can use @InjectCommander() to get the instance used.

Getting the Current Command

Similarly, if you need to get the current Commander commander instance, you can access it via this.command

This is only available in version 3.0.0 and on.

Sub Commands

It may also be that you want to add subcommands to your command, similar to docker compose up. This is possible with the @SubCommand() decorator. Using this decorator, you can have your original implementation for the @Command() decorator, with arguments as normal, and you can have sub commands, as specific arguments that take in even more options. With our my-exec example above, lets say we wanted to add a subcommand, foo. We’d make use of the parents option for the my-exec command and reference the subcommand class, like so:

@Command({
  name: 'my-exec',
  arguments: '<task>',
  subCommands: [FooCommand]
})
export class RunCommand extends CommandRunner {}

Now we just make a subcommand with the same metadata options as the @Command() decorator

@SubCommand({ name: 'foo', arguments: '[phooey]' })
export class FooCommand extends CommandRunner {
  // command runner implementation
}

After adding the subcommand to the appropriate module’s providers array, nest-commander will set up the command so you can call crun my-exec foo hello! and the FooCommand#run method will be ran instead of RunCommand#run. You can also chain commands as deep as you want, by adding subCommands to the subcommand’s metadata.

Subcommands can also take an aliases array for sub command aliases. We could add aliases: ['f'] to the above FooCommand and run it with my-exec f instead of my-exec foo and get the same result. aliases must be passed as an array.

You can also use the options: { isDefault: true } option of the@SubCommand() decorator to set a default subcommand for the command.

Request Scoped Commands

In a CLI environment requests don’t exist in the traditional HTTP sense. Because of this, REQUEST scoped providers are problematic inside of nest-commander commands. However, thanks to the @RequestModule() decorator it is possible to create a mock value for the command to make use of so that the command and all of its dependencies remain SINGLETON or DEFAULT scoped allowing the original providers of the application to be used without a major bit of re-architecting. The decorator works just like @Module() with one extra field, requestObject that gets merged into the providers array under the REQUEST provider token from @nestjs/core. This makes Nest resolve the “proper” value for REQUEST because it exists inside the current module.

@RequestModule({
  providers: [SimpleCommand, RequestScopedProvider],
  requestObject: { headers: { Authorization: 'Bearer token' } }
})
export class RequestScopedCommandModule {}

While we call it RequestScoped, it is very much set to be singleton, which is a win for re-using existing providers.

Using a Command as the Root Command

Sometimes using options: { isDefault: true } isn’t enough for the use case, like if you want to have the --help flag output the default command’s arguments and options. Fortunately, there is the @RootCommand() decorator to replace the base commander command with your own command. This command will be read, parsed, and set up before all of the other commands in your application to allow for everything to keep working as is. It works exactly like @Command(), but does not require a name argument to be passed.

The @RootCommand() decorator is also aliased as @DefaultCommand() if you prefer that nomenclature, is available from 3.6.0 and on.

The Full Command

Let’s say all we want to do is have our my-exec command run the task in another shell, and that’s it. If we take our above command we can see that it can be ran like so

crun my-exec 'echo Hello World!'
# OR
crun 'echo Hello World!'
# OR
crun 'echo Hello World!' --shell zsh
# OR
crun my-exec 'echo Hello World!' -s zsh

To create this kind of program, we can do the following:

import { spawn } from 'child_process';
import { Command, CommandRunner, Option } from 'nest-commander';
import { userInfo } from 'os';

@Command({
  name: 'my-exec',
  arguments: '<task>',
  options: { isDefault: true }
})
export class TaskRunner extends CommandRunner {
  async run(inputs: string[], options: Record<string, string>): Promise<void> {
    const echo = spawn(inputs[0], {
      shell: options.shell ?? userInfo().shell
    });
    echo.stdout.on('data', (data: Buffer) => {
      console.log(data.toString());
    });
  }
  @Option({
    flags: '-s, --shell <shell>',
    description: 'A different shell to spawn than the default'
  })
  parseShell(val: string) {
    return val;
  }
}

And now the TaskRunner is setup and ready to be used.

The above command is meant to be a basic example, and should not be taken as a fully fleshed out CLI example. There’s error handling, input validation, and security that should all be considered. Please do not use the above command in a production environment without adding the mentioned necessities at the very least.

Register Commands

Though you’ll find the implementation details in the factory page, you must register all of your commands including the sub commands as providers in a module class that the CommandFactory ends up registering. For convenience, given that we register examples in Sub Commands section and set them as providers in app.module.ts that set as root module to CommandFactory.

@Module({
  providers: [RunCommand, FooCommand]
})
export class AppModule {}

If you have many sub commands and nested directories for that, it may feel tough to import all of them. For this case, the static registerWithSubCommands method is available in all classes inheriting CommandRunner which returns a list of itself and all sub commands. This means you can write the setting like followed by example instead of the previous example.

@Module({
  providers: [...RunCommand.registerWithSubCommands()]
})
export class AppModule {}

This example works even if the RunCommand has more and nested subcommands and doesn’t interfere with registering other providers or commands if using spread operator or concat method of Array.