Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
nest-commander
Advanced tools
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.
The nest-commander package is a utility for building command-line interfaces (CLI) using the NestJS framework. It allows developers to create and manage CLI commands in a structured and organized manner, leveraging the power of NestJS decorators and dependency injection.
Creating a Basic Command
This feature allows you to create a basic command using the @Command decorator. The command can then be executed from the CLI, and it will print 'Hello, World!' to the console.
```typescript
import { Command, CommandRunner } from 'nest-commander';
@Command({ name: 'hello', description: 'A hello world command' })
export class HelloCommand extends CommandRunner {
async run(passedParam: string[], options?: Record<string, any>): Promise<void> {
console.log('Hello, World!');
}
}
```
Handling Command Parameters
This feature demonstrates how to handle command parameters. The command 'greet' takes a single parameter 'name' and prints a personalized greeting message.
```typescript
import { Command, CommandRunner } from 'nest-commander';
@Command({ name: 'greet', arguments: '<name>' })
export class GreetCommand extends CommandRunner {
async run(passedParam: string[], options?: Record<string, any>): Promise<void> {
const [name] = passedParam;
console.log(`Hello, ${name}!`);
}
}
```
Using Command Options
This feature shows how to use command options. The 'hello' command has an option 'shout' that, when used, will print the greeting message in uppercase.
```typescript
import { Command, CommandRunner, Option } from 'nest-commander';
interface HelloOptions {
shout: boolean;
}
@Command({ name: 'hello', options: { shout: { alias: 's', description: 'Shout the greeting' } } })
export class HelloCommand extends CommandRunner {
async run(passedParam: string[], options?: HelloOptions): Promise<void> {
const message = 'Hello, World!';
if (options?.shout) {
console.log(message.toUpperCase());
} else {
console.log(message);
}
}
}
```
Commander is a popular, simple, and flexible library for building command-line interfaces in Node.js. It provides a straightforward API for defining commands, options, and arguments. Compared to nest-commander, it does not integrate with NestJS and lacks the dependency injection and decorator-based approach.
Yargs is another widely-used library for building CLI applications in Node.js. It offers powerful features for parsing arguments and generating help menus. While it is more feature-rich than commander, it also does not integrate with NestJS and does not use decorators or dependency injection.
Oclif is a framework for building command-line tools by Heroku. It provides a robust structure for creating complex CLI applications with plugins and multiple commands. Oclif is more comprehensive than nest-commander but does not integrate with NestJS and has a steeper learning curve.
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.
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
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.
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.
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.
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
.
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:
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. |
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.
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. |
@Question()
decoratorBelow 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;
}
@*For()
decoratorBelow 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()
.
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.
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.
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();
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.
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.
FAQs
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.
The npm package nest-commander receives a total of 172,282 weekly downloads. As such, nest-commander popularity was classified as popular.
We found that nest-commander demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.