samer-artisan
v0.1.0
Published
Command Line Interface for Nodejs. inspired by Artisan (PHP)
Downloads
10
Readme
SamerArtisan
"Build CLI in a structured and organized way"
SamerArtisan is a CLI Inspired by Artisan (Laravel). Its very flexible, feature-rich and also easy to use.
Table of Contents
- Installation
- Quick Start
- Writing Commands
- Registering Commands
- Project Root
- Project Name
- Core Commands
- Contributing
Installation
npm install samer-artisan
Quick Start
We will build a minimal CLI with SamerArtisan.
To make it simple, we will do that in a single file (e.g cli.js).
// cli.js
const { SamerArtisan, Command } = require("samer-artisan");
//Create a custom command
class Greet extends Command {
//This should be unique
signature = "greet";
//This method will be invoked by SamerArtisan
handle() {
console.log("Hello user!");
}
}
//Register the command
SamerArtisan.add(new Greet());
// Start the CLI
SamerArtisan.parse();
Our CLI is ready to be used now.
node cli.js
The output will look like this Notice the bottom command on Available Commands section, that is added by us.
If you are thinking, what are list and make:command commands, then read about core commands
Writing Commands
For creating actions of your CLI, you have to write commands.
Generating Commands
To create a new command, you may use the make:command command.
This command will create a new command class in your specified directory.
Lets create a Greet command
node cli.js make:command Greet --dir=commands
Command Structure
After generating your command, you should define appropriate values for the signature and description properties of the class. These properties will be used when displaying your command using list. The signature property also allows you to define your command's input expectations. The handle method will be called when your command is executed. You may place your command logic in this method.
Let's take a look at an example command.
class Greet extends Command {
/**
* The name and signature of the console command.
*
* @var string
*/
signature = 'greet {name}';
/**
* The console command description.
*
* @var string
*/
description = 'Greet a person';
/**
* Execute the console command.
*/
handle() {
console.log("Hello ", this.argument("email"));
//
}
}
The handle method also can be asyncronous by returning promise
/**
* Execute the console command.
*/
async handle() {
//Do some DB operations
const users = await User.find();
}
Or by done callback on first argument of handle method.
/**
* Execute the console command.
*/
handle(done) {
//Do some DB operations
User.find().then(users => {
// ... Do something with users
done()
});
}
Defining Input Expectations
When writing console commands, it is common to gather input from the user through arguments or options.
SamerArtisan makes it very convenient to define the input you expect from the user using the signature
property on your commands. The signature property allows you to define the name, arguments, and options
for the command in a single, expressive, route-like syntax.
Arguments
All user supplied arguments and options are wrapped in curly braces. In the following example, the command defines one required argument: name:
/**
* The name and signature of the console command.
*
* @var string
*/
signature = 'greet {name}';
You may also make arguments optional or define default values for arguments:
// Optional argument...
'greet {name?}'
// Optional argument with default value...
'greet {name=foo}'
Options
Options, like arguments, are another form of user input. Options are prefixed by two hyphens (--) when they are provided via the command line. There are two types of options: those that receive a value and those that don't. Options that don't receive a value serve as a boolean "switch". Let's take a look at an example of this type of option:
/**
* The name and signature of the console command.
*
* @var string
*/
signature = 'greet {name} {--foo}';
In this example, the --foo switch may be specified when calling the command. If the --foo switch is passed, the value of the option will be true. Otherwise, the value will be false:
node cli.js Hasan --foo
Options With Values
Next, let's take a look at an option that expects a value. If the user must specify a value for an option, you should suffix the option name with a = sign:
/**
* The name and signature of the console command.
*
* @var string
*/
signature = 'greet {name} {--foo=}';
In this example, the user may pass a value for the option like so. If the option is not specified when invoking the command, its value will be null:
node cli.js greet Hasan --foo=bar
You may assign default values to options by specifying the default value after the option name. If no option value is passed by the user, the default value will be used:
'greet {name} {--foo=bar}'
Option Shortcuts
To assign a shortcut when defining an option, you may specify it before the option name and use the | character as a delimiter to separate the shortcut from the full option name:
'greet {name} {--F|foo}'
When invoking the command on your terminal, option shortcuts should be prefixed with a single hyphen and no = character should be included when specifying a value for the option:
node cli.js greet 1 -Fbar
Input Arrays
If you would like to define arguments or options to expect multiple input values, you may use the * character. First, let's take a look at an example that specifies such an argument:
'greet {name*}'
When calling this method, the name arguments may be passed in order to the command line. For example, the following command will set the value of user to an array with Hasan and Hossain as its values:
node cli.js greet Hasan Hossain
TODO This * character can be combined with an optional argument definition to allow zero or more instances of an argument:
'greet {name?*}'
Option Arrays
When defining an option that expects multiple input values, each option value passed to the command should be prefixed with the option name:
'greet {--name=*}'
Such a command may be invoked by passing multiple --name arguments:
node cli.js greet --name=Hasan --name=Hossain
Input Descriptions
You may assign descriptions to input arguments and options by separating the argument name from the description using a colon. If you need a little extra room to define your command, feel free to spread the definition across multiple lines:
/**
* The name and signature of the console command.
*
* @var string
*/
signature = `greet
{name : The name of the user}
{--foo : An example option}`;
Command I/O
Retrieving Input
While your command is executing, you will likely need to access the values for the arguments and options accepted by your command. To do so, you may use the argument and option methods.
/**
* Execute the console command.
*/
handle() {
const name = this.argument('user');
}
If you need to retrieve all of the arguments as an array, call the arguments method:
const arguments = this.arguments();
Options may be retrieved just as easily as arguments using the option method. To retrieve all of the options as an array, call the options method:
// Retrieve a specific option...
const queueName = this.option('queue');
// Retrieve all options as an array...
const options = this.options();
If you passed an argument or option name that doest not marked on the signature, the argument and option method will throw an error.
signature = "greet {name} {--foo}"
handle() {
//Works fine
this.argument('name');
this.option('foo');
//Will throw error
this.argument('age');
this.option('bar');
}
Prompting For Input
SamerArtisan prompting is built on the top of prompts package for providing you an elegant API and ease of use. If you need more flexibility over prompting, please use prompts package directly in your command.
Asking Question
In addition to displaying output, you may also ask the user to provide input during the execution of your command. The ask method will prompt the user with the given question, accept their input, and then return the user's input back to your command:
function handle() {
name = this.ask('What is your name?');
// ...
}
The secret method is similar to ask, but the user's input will not be visible to them as they type in the console. This method is useful when asking for sensitive information such as passwords:
password = this.secret('What is the password?');
Asking For Confirmation
If you need to ask the user for a simple "yes or no" confirmation, you may use the confirm method. By default, this method will return false.
if (this.confirm('Do you wish to continue?')) {
// ...
}
If necessary, you may specify that the confirmation prompt should return true by default by passing true as the second argument to the confirm method:
if (this.confirm('Do you wish to continue?', true)) {
// ...
}
Auto-Completion
The anticipate method can be used to provide auto-completion for possible choices. The user can still provide any answer, regardless of the auto-completion hints:
const name = this.anticipate('What is your name?', ['Hasan', 'Hossain']);
Alternatively, you may pass a closure as the second argument to the anticipate method. The closure will be called each time the user types an input character. The closure should accept a string parameter containing the user's input so far, and return an array of options for auto-completion:
const name = this.anticipate('What is your name?', input => {
// Return auto-completion options...
});
The closure also can be asyncronous
const name = this.anticipate('What is your name?', async input => {
// Do asyncronous operations e.g make some API calls
});
Multiple Choice Questions
If you need to give the user a predefined set of choices when asking a question, you may use the choice method. You may set the array index of the default value to be returned if no option is chosen by passing the index as the third argument to the method:
const name = this.choice(
'What is your name?',
['Hasan', 'hossain'],
defaultIndex
);
In addition, the choice method accepts optional fourth and fifth arguments for determining whether multiple selections are permitted and the maximum number of selection:
const name = this.choice(
'What is your name?',
['Taylor', 'Dayle'],
$defaultIndex,
allowMultipleSelections = false,
max = undefined
);
Writing Output
To send output to the console, you may use the info, comment, warn, error and fail methods. Each of these methods will use appropriate ANSI colors for their purpose. For example, let's display some general information to the user. Typically, the info method will display in the console as green colored text:
/**
* Execute the console command.
*/
handle() {
// ...
this.info('The command was successful!');
}
To display an error message, use the error method. Error message text is typically displayed in red:
this.error('Something went wrong!');
If you want to terminate the command with a single line error message, use fail method
handle() {
// ...
this.fail('Invalid Password!');
console.log("You will never see that");
}
The fail method expects an optional help message as second argument. It is used for adding a help message with the error.
this.fail('File already exists!', '(use -f to overrite)');
Table
The table method makes it easy to correctly format multiple rows / columns of data. SamerArtisan uses cli-table package for table generation. For more flexibility, use cli-table package directly in your command.
this.table(
['Name', 'Email'],
[
["Hasan", "[email protected]"],
["Hossain", "[email protected]"]
]);
Progress Bars
SamerArtisan uses cli-progress package to provide you a high level and elegant API. For more flexibility over progress bars, use cli-progress package directly in your command. For long running tasks, it can be helpful to show a progress bar that informs users how complete the task is. Using the withProgressBar method, SamerArtisan will display a progress bar and advance its progress for each iteration over a given iterable value:
const users = this.withProgressBar(, function (User $user) {
this.performTask(user);
});
Registering Commands
Adding a Command
let's say we have a command class in "./commands/Test.js" path
// ./commands/Test.js
class Test extends Command {
signature = "test";
handle() {
console.log("just testing.")
}
}
module.exports = Test;
Now to register this command. we can either do it by importing the command and passing its instance
const Test = require("./commands/Test");
SamerArtisan.add(new Test());
Or by passing its path
SamerArtisan.add("commands/Test.ts");
SamerArtisan will join that path with your projects root directory. read more about project root
Loading Commands From Directory
Assume you have 20 commands inside "./commands" directory. Do you have enough guts of adding them all manually?
Even if you registered them manually, the code may loose its maintainability.
So if you want to register all commands from a directory. You can do
SamerArtisan.load("commands");
If you have multiple directories of commands
SamerArtisan.load([
"dir1/commands",
"dir2/commands",
...
);
Or in chain syntax
SamerArtisan
.load("dir1/commands")
.load("dir2/commands")
...
Keep in mind, it may impact performance if you have loaded a lot of directories
as File i/o is slow in nodejs
Typescript
SamerArtisan has a strong typing support. Its very easy to write SamerArtisan commands with typescript.
Typesafe Command
The base Command class expects 2 generic type arguments. first one is Arguments and the second one is Options.
Let's look at an example typescript command
interface Arguments {
name: string;
age: string | null;
arr: string[];
}
interface Options {
foo: boolean;
bar: string | null;
baz: string;
}
class Greet extends Command<Arguments, Options> {
/**
* The name and signature of the console command.
*
* @var string
*/
signature = 'greet {name} {age?} {arr*} {--foo} {--bar=} {--baz=default}';
/**
* The console command description.
*
* @var string
*/
description = 'Greet a person';
/**
* Execute the console command.
*/
handle() {
//Now you can retrieve inputs safely
this.argument("name")
// ...
}
}
Project Root
SamerArtisan prefix every import with projects root so that you dont have to manually join path with __dirname everytime
// Without this feature
SamerArtisan.add(path.join(__dirname, "commands/Test.ts"));
// With this feature
SamerArtisan.add("commands/Test.ts");
You can see it makes your life easy.
But the question is how SamerArtisan detects the root directory?
It uses process.cwd() for detecting. The process.cwd function returns the path from where the process had been started.
It may not work if you started the process from somewhere else than root directory. In this case you have to specify root manually.
SamerArtisan.root(myProjectRootDir)
And it's always a best practice to manually specifing root
Project Name
When you run the cli any arguments
node cli.js
You will see a ASCII art of text SamerArtisan. That is projects name. if you want to change it, do that
SamerArtisan.name("YourProjectName");
Core Commands
Commands thoose are registered by SamerArtisan are called core command Here are the usage of these core commands in brief
list
This command is used for listing all available commands of your CLI
node [path/to/cli] list
And it will print all commands name with its description
make:command
This command used for generating command components
node cli.js make:command <Name of the command> --dir=<directory where the component should be putted>
The directory you provide will be resolved to absolute. Read about project root for better understanding
lets create a command
node cli.js make:command Test --dir=commands
It will create a command class in "/commands/Test.js" file
Contributing
Thank you for considering contributing to our project! We welcome contributions from everyone. Just follow coding best practices