Detalhes do pacote

nest-commander

jmcdo291.4mMIT3.17.0

A module for making CLI applications with NestJS. Decorators for running commands and separating out config parsers included. This package works on top of commander.

cli, nestjs, application, command

readme (leia-me)

NestJS Commander

Have you been building amazing REST and RPC applications with NestJS? Do you want that same structure for absolutely everything you're working with? Have you always wanted to build up some sweet CLI application but don't really know where to start? This is the solution. A package to bring building CLI applications to the Nest world with the same structure that you already know and love :heart: Built on top of the popular Commander package.

Installation

Before you get started, you'll need to install a few packages. First and foremost, this one: nest-commander (name pending). You'll also need to install @nestjs/common and @nestjs/core as this package makes use of them under the hood, but doesn't want to tie you down to a specific version, yay peerDependencies!

npm i nest-commander @nestjs/common @nestjs/core
# OR
yarn add nest-commander @nestjs/common @nestjs/core
# OR
pnpm i nest-commander @nestjs/common @nestjs/core

A Command File

nest-commander makes it easy to write new command line applications with decorators via the @Command() decorator for classes and the @Option() decorator for methods of that class. Every command file should implement the CommandRunner interface and should be decorated with a @Command() decorator.

CommandRunner

Every command is seen as an @Injectable() by Nest, so your normal Dependency Injection still works as you would expect it to (woohoo!). The only thing to take note of is the interface CommandRunner, which should be implemented by each command. The CommandRunner interface ensures that all commands have a run method that return a Promise<void> and takes in the parameters string[], Record<string, any>. The run command is where you can kick all of your logic off from, it will take in whatever parameters did not match option flags and pass them in as an array, just in case you are really meaning to work with multiple parameters. As for the options, the Record<string, any>, the names of these properties match the name property given to the @Option() decorators, while their value matches the return of the option handler. If you'd like better type safety, you are welcome to create an interface for your options as well. You can view how the Basic Command test manages that if interested.

@Command()

The @Command() decorator is to define what CLI command the class is going to manage and take care of. The decorator takes in an object to define properties of the command. The options passed here would be the same as the options passed to a new command for Commander

property type required description
name string true the name of the command
arguments string false Named arguments for the command to work with. These can be required <> or optional [], but do not map to an option like a flag does
description string false the description of the command. This will be used by the --help or -h flags to have a formalized way of what to print out
argsDescription Record<string, string> false An object containing the description of each argument. This will be used by -h or --help
Options CommandOptions false Extra options to pass on down to commander

For mor information on the @Command() and @Option() parameters, check out the Commander docs.

@Option()

Often times you're not just running a single command with a single input, but rather you're running a command with multiple options and flags. Think of something like git commit: you can pass a --amend flag, a -m flag, or even -a, all of these change how the command runs. These flags are able to be set for each command using the @Option() decorator on a method for how that flag should be parsed. Do note that every command sent in via the command line is a raw string, so if you need to transform that string to a number or a boolean, or any other type, this handler is where it can be done. See the putting it all together for an example. The @Option() decorator, like the @Command() one, takes in an object of options defined in the table below

property type required description
flags string true a string that represents the option's incoming flag and if the option is required (using <>) or optional (using [])
description string false the description of the option, used if adding a --help flag
defaultValue string or boolean false the default value for the flag

Under the hood, the method that the@Option() is decorating is the custom parser passed to commander for how the value should be parsed. This means if you want to parse a boolean value, the best way to do so would be to use JSON.parse(val) as Boolean('false') actually returns true instead of the expected false.

Inquirer Integration

nest-commander also can integrate with inquirer to allow for user input during your CLI run. I tried to keep this integration as smooth as possible, but there are some caveats to watch for:

  1. Whatever inputs you want to handle via inquirer, must be omitted from commander if you don't want them passed in at all, or they must be optional if you want them to be passable from the command line. If you use a required option and it is not passed from the command line, commander will fail the CLI call.
  2. Inquirer plugins are not yet supported. I do have an idea for this, but they are out of scope for the initial integration.
  3. You, as the developer, have options on how to set up the inquirer integration. The details will come later, but know that you are given power here. Use it wisely.

QuestionSet

A class decorated with @QuestionSet() is a class that represents a related set of questions. Looking at inquirer's own examples, this could be like the pizza example. There's nothing too special about this decorator, all it does is allow the underlying engine to find the appropriate question set when it is needed. The @QuestionSet() decorator takes an object of options defined below

property type required description
name string true The name that will be used by the InquirerService when getting a prompt to run.

Question

Here's where the options start to open up. Each @Question() should decorate a class method. This method will essentially become the filter property for inquirer. If you don't need any filtering done, simply return the value that comes into the method. All of the other properties come from, and adhere to the types of, Inquirer and their documentation can better illustrate what values are needed when and where.

Question Functional Properties

With Inquirer, several of the properties can have functions instead of simple types. For these properties, you can do one of two things: 1) pass the function to the decorator or 2) use the @*For()^ decorator. Each @*For() decorator takes in an object similar to the @Question() decorator as described below

property type required description
name string true The name that will be used to determine which @Question() this decorator belongs to.
Passing to the @Question() decorator

Below is an example of using the validate method in the @Question() decorator

@Question({
  type: 'input',
  name: 'phone',
  message: "What's your phone number?",
  validate: function(value: string) {
    const pass = value.match(
      /^([01]{1})?[-.\s]?\(?(\d{3})\)?[-.\s]?(\d{3})[-.\s]?(\d{4})\s?((?:#|ext\.?\s?|x\.?\s?){1}(?:\d+)?)?$/i,
    );
    if (pass) {
      return true;
    }
    return 'Please enter a valid phone number';
  }
})
parsePhone(val: string) {
  return val;
}
Using the @*For() decorator

Below is an example of a @Question() and @ValidateFor() decorator in use

@Question({
  type: 'input',
  name: 'phone',
  message: "What's your phone number?",
})
parsePhone(val: string) {
  return val;
}

@ValidateFor({ name: 'phone' })
validatePhone(value: string) {
  const pass = value.match(
    /^([01]{1})?[-.\s]?\(?(\d{3})\)?[-.\s]?(\d{3})[-.\s]?(\d{4})\s?((?:#|ext\.?\s?|x\.?\s?){1}(?:\d+)?)?$/i,
  );
  if (pass) {
    return true;
  }

  return 'Please enter a valid phone number';
}

As you can see, the name of both @Question() and @ValidateFor() align, allowing the underlying engine to properly map the validatePhone method to the phone's property set.

^ Please note that @*For() is shorthand for @ValidateFor(), @ChoicesFor(), @MessageFor(), @DefaultFor(), and @WhenFor().

InquirerService

The InquirerService is an injectable provider that allows you to call inquirer for a specific set of questions (named with @QuestionSet()). When calling the question set, you can pass in the already obtained options as well, and inquirer will skip over the options that are already answered, unless the askAnswered property is set to true as mentioned in their docs. You can use either InquirerService#ask or InquirerService#prompt, as they are aliases for each other. The return from the InquirerService#prompt method is the non-partial variant of the options passed in; in other words, the return is the answers that the user provided, mapping appropriately in the cases where necessary, such as lists. For an example usage, please check the pizza integration test.

Running the Command

Similar to how in a NestJS application we can use the NestFactory to create a server for us, and run it using listen, the nest-commander package exposes a simple to use API to run your server. Import the CommandFactory and use the static method run and pass in the root module of your application. This would probably look like below

import { CommandFactory } from 'nest-commander';
import { AppModule } from './app.module';

async function bootstrap() {
  await CommandFactory.run(AppModule);
}

bootstrap();

And that's it. Under the hood, CommandFactory will worry about calling NestFactory for you and calling app.close() when necessary, so you shouldn't need to worry about memory leaks there. If you need to add in some error handling, there's always try/catch wrapping the run command, or you can chain on some .catch() method to the bootstrap() call.

Error Handling

By default, nest-commander does not add in any error handling, other that the default that commander itself does. If you would like to use commander's exitOverride you can pass an errorHandler property to the options object of the CommandFactory.run method. This error handler should take in an error object, and return void.

import { CommandFactory } from 'nest-commander';
import { AppModule } from './app.module';

async function bootstrap() {
  await CommandFactory.run(AppModule, {
    errorHandler: (err) => {
      console.error(err);
      process.exit(1); // this could also be a 0 depending on how you want to handle the exit code
    }
  });
}

bootstrap();

Testing

There is a testing helper package called nest-commander-testing that works very similarly to @nestjs/testing. Check out it's documentation and examples for help.

Putting it All Together

The following class would equate to having a CLI command that can take in the subcommand basic or be called directly, with -n, -s, and -b (along with their long flags) all being supported and with custom parsers for each option. The --help flag is also supported, as is customary with commander.

import { Command, CommandRunner, Option } from 'nest-commander';
import { LogService } from './log.service';

interface BasicCommandOptions {
  string?: string;
  boolean?: boolean;
  number?: number;
}

@Command({ name: 'basic', description: 'A parameter parse' })
export class BasicCommand implements CommandRunner {
  constructor(private readonly logService: LogService) {}

  async run(passedParam: string[], options?: BasicCommandOptions): Promise<void> {
    if (options?.boolean !== undefined && options?.boolean !== null) {
      this.runWithBoolean(passedParam, options.boolean);
    } else if (options?.number) {
      this.runWithNumber(passedParam, options.number);
    } else if (options?.string) {
      this.runWithString(passedParam, options.string);
    } else {
      this.runWithNone(passedParam);
    }
  }

  @Option({
    flags: '-n, --number [number]',
    description: 'A basic number parser'
  })
  parseNumber(val: string): number {
    return Number(val);
  }

  @Option({
    flags: '-s, --string [string]',
    description: 'A string return'
  })
  parseString(val: string): string {
    return val;
  }

  @Option({
    flags: '-b, --boolean [boolean]',
    description: 'A boolean parser'
  })
  parseBoolean(val: string): boolean {
    return JSON.parse(val);
  }

  runWithString(param: string[], option: string): void {
    this.logService.log({ param, string: option });
  }

  runWithNumber(param: string[], option: number): void {
    this.logService.log({ param, number: option });
  }

  runWithBoolean(param: string[], option: boolean): void {
    this.logService.log({ param, boolean: option });
  }

  runWithNone(param: string[]): void {
    this.logService.log({ param });
  }
}

Make sure the command class is added to a module

@Module({
  providers: [LogService, BasicCommand]
})
export class AppModule {}

And now to be able to run the CLI in your main.ts you can do the following

async function bootstrap() {
  await CommandFactory.run(AppModule);
}

bootstrap();

And just like that, you've got a command line application. All that's left is to run your build command (usually nest build) and run start like normal (node dist/main). If you're looking to package the command line app for other devs consumption (making somethng like the @nestjs/cli or jest), then you can add the bin property to the package.json and map the command appropriately.

changelog (log de mudanças)

nest-commander

3.17.0

Minor Changes

  • 97ef074: Two new options exist, allowUnknownOptions and allowExcessArgs. Both are optional booleans that will tell the underlying commander command to allow for extra options and arguments, as is already supported by commander.

3.16.1

Patch Changes

  • f2d6228: Support dynamic modules in CommandFactory.run()

3.16.0

Minor Changes

  • ce3d4f4: Support NestJS v11

3.15.0

Minor Changes

  • 886be2d: feat: allow async serviceErrorHandler method

3.14.0

Minor Changes

  • 1cdac14: feat: Add option for Help Configuration using the .configureHelp() function in commander js

3.13.0

Minor Changes

  • c29737c: Enhance filesystem autocomplete support for Bash and Zsh by introducing an opt-in option based on an environment variable.

3.12.5

Patch Changes

  • 72b2a00: Move the fig completion package to an optional import to get around jest throwing an error about esm packages

3.12.4

Patch Changes

  • 0a47417: Update commnader version to satisfy fig-completion

3.12.3

Patch Changes

  • e66901a: update package @fig/complete-commander to v3.0.0 to support commander v11

3.12.2

Patch Changes

  • 15297ce: Use attributeName to handle dashed options

3.12.1

Patch Changes

  • a3b683d: Remap the options to the name passed in the @Options() decorator, if provided

3.12.0

Minor Changes

  • b2c6a13: feat: support completion factory for bash and zsh shells

3.11.1

Patch Changes

  • 8cc3109: The CommandRunnerService now re-throws the error regardless of the contents, it just adds a new log above the error as well

3.11.0

Minor Changes

  • a97ab68: Add The possibility to set the global version option

3.10.0

Minor Changes

  • 519018e: Add ability to set outputConfiguration.

    Now CommandFactory.run(), CommandFactory.runWithoutClosing() and CommandFactory.createWithoutRunning() accept the option outputConfiguration.

3.9.0

Minor Changes

  • 1fa92a0: Support NestJS v10

3.8.0

Minor Changes

  • 6cc1112: Add ability to pass NestApplicationContextOptions to CommandFactoryRunOptions.

    Now CommandFactory.createWithoutRunning() can accept more options, for example, bufferLogs to pre-save Nest startup logs.

3.7.1

Patch Changes

  • 1ceab9d: Log error and stack wtih custom error message instead of just custom error message

3.7.0

Minor Changes

  • 9a5f555: Add a new method to create an application but nott run it in case of needing to modify the logger or similar situations.

    Now the CommandFactory.createWithoutRunning() method can be used to create a Nest commander application without running the commandRunner.run(). To run the newly created application, CommandFactory.runApplicaiton(app) can be called. I may change this to be a simple app.run() in the future.

3.6.3

Patch Changes

  • 84b5067: Adds support for positional options and passthrough options

3.6.2

Patch Changes

  • 6fb3d91: Fix the Inquirer type to work above @types/inquirer@8.2.1

3.6.1

Patch Changes

  • c35e8cc: Fixed issue with parsing serviceErrorHandler option to properly override default behaviour

3.6.0

Minor Changes

  • 7f54ff8: Add serviceErrorHandler option

    This option allows for catching and handling errors at the Nest service execution level so that lifecycle hooks still properly work. By default it is set to (err: Error) => process.stderr.write(err.toString()).

  • 09b6134: Add the ability to have a Root command

    With the @RootCommand() the -h flag can now output the options of the default command along with the names of the other commands.

3.5.0

Minor Changes

  • d2e5fc8: Allow for use of request scoped providers through a new module decorator

    By making use of the @RequestModule() decorator for modules, as mock request object can be set as a singleton to help the use of REQUEST scoped providers in a singleton context. There's now also an error that is logged in the case of a property of undefined being called, as this is usually indicative of a REQUEST scoped provider being called from a SINGLETON context.

3.4.0

Minor Changes

  • fadb70d: Allow for a sub command to be set as the default sub command.
  • 74c88f5: Add new api registerWithSubCommand to CommandRunner Class
  • abff78d: Allow for options to be parsed positionally via an option passed to CommandFactory

3.3.0

Minor Changes

  • 8c639d3: fix: update module resolution to node16 so dynamic imports are not transpiled out during TS build

3.2.1

Patch Changes

  • 5c089a6: Fixed an issue preventing use of ESM packages as plugins in the command factory

3.2.0

Minor Changes

  • 84e6b95: Add env option to @Option() decorator

3.1.0

Minor Changes

  • 15da048: InquirerService now exposes inquirer publicly

3.0.0

Major Changes

  • d6ebe0e: Migrate CommandRunner from interface to abstract class and add .command

    This change was made so that devs could access this.command inside the CommandRunner instance and have access to the base command object from commander. This allows for access to the help commands in a programatic fashion.

    To update to this version, any implements CommandRunner should be changed to extends CommandRunner. If there is a constructor to the CommandRunner then it should also use super().

Minor Changes

  • 3d2aa9e: Update NestJS package to version 9
  • a8d109f: Upgrade commander to v9.4.0

Patch Changes

  • c30a4de: Ensure the parser for choices is always called

2.5.0

Minor Changes

  • 2d8a143: Added support for aliased subcommands
  • 6e39331: Allow for command options to have defined choices

    Option choices are now supported either as a static string array or via the @OptionChoicesFor() decorator on a class method. This decorator method approach allows for using a class's injected providers to give the chocies, which means they could come from a database or a config file somewhere if the CLI is set up to handle such a case

2.4.0

Minor Changes

  • eaa63fb: Adds a new CliUtilityService and @InjectCommander() decorator

    There is a new CliUtilityService and @InjectCommander() decorator that allows for direct access to the commander instance. The utility service has methods like parseBoolean, parseInt, and parseFloat. The number parsing methods are just simple wrappers around Number.parse*(), but the boolean parsing method handles true being yes, y, 1, true, and t and false being no, n, false, f, and 0.

2.3.5

Patch Changes

  • 55eb46d: Update peerDependencies for nest-commander to include @types/inquirer

2.3.4

Patch Changes

  • 3ad2c3a: Add cosmiconfig to the dependencies for proper publishing

2.3.3

Patch Changes

  • 8285a98: missing config file warning doesn't show

2.3.2

Patch Changes

  • 3c43005: fix: move plugin error code to onModuleInit

2.3.1

Patch Changes

  • 478c0d9: Make commands built with usePlugins: true not exit on non-found config file, just log extra data when an error happens

2.3.0

Minor Changes

  • 6c9eaa3: Commands can now be built with the expectation of reading in plugins to dynamically modify the CLI

    By using the usePlugins option for the CommandFactory, the built CLI can expect to find a configuration file at nest-commander.json (or several others, check the docs) to allow for users to plug commands in after the CLI is built.

  • 13723bd: Subcommands can now be created

    There's a new decorator, @SubCommand() for creating nested commands like docker compose up. There's also a new option on @Command() (subCommands) for setting up this sub command relationship.

2.2.0

Minor Changes

  • 3831e52: Adds a new @Help() decorator for custom commander help output

    nest-commander-testing now also uses a hex instead of utf-8 encoding when creating a random js file name during the CommandTestFactory command. This is to help create more predictable output names.

2.1.0

Minor Changes

  • 6df8964: Adds in a new metadata option for the @Option() decorator to make the option required, just like a required argument

2.0.0

Major Changes

  • ee001cc: Upgrade all Nest dependencies to version 8

    WHAT: Upgrade @nestjs/ dependencies to v8 and RxJS to v7 WHY: To support the latest version of Nest HOW: upgrading to Nest v8 should be all that's necessary (along with rxjs to v7)

1.3.0

Minor Changes

  • f3f687b: Allow for commands to be run indefinitely

    There is a new runWithoutClosing method in the CommandFactory class. This command allows for not having the created Nest Application get closed immediately, which should allow for the use of indefinitely runnable commands.

1.2.0

Minor Changes

  • 7cce284: Add ability to use error handler for commander errors

    Within the CommandFactory.run() now as a second parameter you can either keep passing just the logger, or you can pass in an object with the logger and an errorHandler. Ths errorHandler is a method that takes in an Error and returns void. The errorHandler will be passed to commander's exitOverride method, if it exists. This is useful for better handling errors and giving the dev more control over what is seen. There is also no longer an unhandledPromiseRejection on empty commands.