interactive-cli-helper
Create a simple interactive CLI for your Node.js application
Features
- Assign commands (parts of string delimited by spaces) to functions or strings
- Suggest commands with auto-completion
- Wait for Promise completion when handler is a async function
- Fancy console writing of results
RegExp
based command support- Question-asking to end-user
- Fancy help messages
Getting started
Install the package with npm
.
npm i interactive-cli-helper
You can use this package through two different modes, functional or class-based declarations.
Simple usage
import CliHelper from 'interactive-cli-helper';
const cli = new CliHelper({
onNoMatch: rest => `Command ${rest} not found.`
});
const get_command = cli.command(
'get',
'Please specify a thing to get.'
);
get_command.command('ponies', async () => {
const ponies = await getPonies();
return ponies.map(e => e.name);
});
const pies = get_command.command('pies', () => {
return `Available pies are:\n${getPies().map(e => e.name).join('\n')}`;
});
pies.command('add', async rest => {
const added_pie = await addPie({ name: rest });
return added_pie;
});
cli.listen();
Example output for this example code:
> hello
cli: Command hello not found
> get
cli: Please specify a thing to get.
> get ponies
cli: [ 'twilight', 'rainbow dash' ]
> get pies
cli: Available pies are:
cake
> get pies add cake
Error encountered in CLI: Pie already exists. (Error: Pie already exists.
at addPie (basic.test.js:13:15)
at CliListener.executor (basic.test.js:38:29)
...)
> get pies add other cake
cli: { name: 'other cake' }
> get pies
cli: Available pies are:
cake
other cake
Functional mode
Main helper
To start using CLI helper, you need the main helper, that can handle stdin
listen actions.
import CliHelper from 'interactive-cli-helper';
const cli = new CliHelper({
onNoMatch: CliExecutor,
onSuggest?: CliSuggestor,
suggestions?: boolean,
onCliClose?: Function,
onCliError?: (error: Error) => void,
});
cli.listen();
Set commands
To declare command, use the .command()
method. This will return a new CliListener
instance to use if you want to declare sub-commands.
(CliHelper or CliListener).command(name: string | RegExp, executor: CliExecutor, options: CliListenerOptions);
const database_cli = cli.command('database', 'Please enter a thing to do with database !');
database_cli.command('test', async () => {
if (await Database.test()) {
return 'Database works :D';
}
return 'Database is not working :(';
});
const getter = database_cli.command('get', 'Specify which thing to get');
const products = getter.command('products', async () => {
const products = await Database.getProducts();
return products;
});
products.command(/coffee-(.+)/, async (_, matches) => {
const wanted = matches[1];
const coffee_products = await Database.getCoffeeProducts();
return coffee_products.filter(e => e.name.startsWith(wanted));;
});
getter.command('users', () => Database.getUsers());
const hello_sayer = cli.command('say-hello', 'Hello world!');
Use validators
Get back on our previous example. It could be useful if, before accessing every database
command, we check if database is accessible.
For this, we will modify the declaration of database_cli
handler.
const database_cli = cli.command(
'database',
(rest, _, __, validator_state) => {
if (validator_state === false) {
return 'Database is not ready. You can\'t use database functions.';
}
if (rest.trim()) {
return 'database: Command not found.';
}
return 'Please enter a thing to do with the database.';
},
{
async onValidateBefore() {
const test = await Database.testAvailability();
return test;
},
}
);
Now, all the sub-database commands will check if database is ready before executing.
Class-declarated mode
You need to use TypeScript in your project and enable experimental decorators to use this.
This declaration mode is experimental.
import { CliMain, CliBase, LocalCommand, Command, CliCommand, CliCommandInstance } from 'interactive-cli-helper';
@CliCommand()
class Command2 {
executor: CliExecutor = "Hello";
}
@CliCommand()
class Command1 {
executor: CliExecutor = rest => {
return "You just entered " + rest + ".";
};
onValidateBefore: CliValidator = () => {
return true;
};
@Command('two', Command2)
command_two!: CliCommandInstance<Command2>;
@LocalCommand('local-1', 'localOne', { onValidateBefore: 'validateLocalOne' })
local_one_cmd!: CliListener;
localOne(rest: string) {
return "This is local one command".
}
validateLocalOne(rest: string) {
return true;
}
}
@CliMain({ suggestions: true })
class Main extends CliBase {
onNoMatch: CliExecutor = "No command matched your search.";
@Command('one', Command1)
command_one!: CliCommandInstance<Command1>;
}
const helper = new Main();
helper.listen();
How it works
This package will listen on stdin
after you called .listen()
method of a main helper.
If the user enter commands, it will try to match the commands you've declarated.
To see API, see previous section.
Answer to entered command
When you declare a command handler, you must specify a matcher (either a string
or a RegExp
) and a CliExecutor
to execute.
A CliExecutor
is either a string
or object
to show, or a function
to call with specific arguments.
import { CliExecutor, CliStackItem } from 'interactive-cli-helper';
const str_executor: CliExecutor = "Hello !";
const obj_executor: CliExecutor = { prop1: 'foo', prop2: 'bar' };
const fn_executor: CliExecutor = function (
rest: string,
stack: CliStackItem[],
matches: RegExpMatchArray | null,
validator_state?: boolean
) {
return "Hello!";
};
Validate user entry before executing a command
If you want to check if user entry is valid before trying to match sub-commands, you can use a CliValidator
.
import { CliValidator } from 'interactive-cli-helper';
const validator: CliValidator = (rest: string, regex_matches: RegExpMatchArray | null) => {
return true;
};
If the validator fails (returns false
), then sub-commands will not be matched and
current failed command executor will be called with validator_state
parameter to false
.
Generate help messages
If you want to have a fancy help message to display to users,
you can use CliHelper.formatHelp()
to generate a help executor:
const database_help: CliExecutor = CliHelper.formatHelp( 'database', {
commands: {
'find': 'Find something in the database',
'destroy': 'Destroy something in the database'
},
onNoMatch: (rest: string) => {
return `${rest} is not a valid command for database. For help, type "database".`;
},
description: 'Helps to manage the database.',
});
In the both following examples we're gonna use the created database_help
.
Functional example
Assign the created executor in the second parameter of .command()
method.
const database = cli.command('database', database_help);
Class-based example
Put the created executor in the executor
property of your class.
@CliCommand()
class DatabaseCommand {
executor = database_help;
}
Suggest manually
When your command has user-defined suggestions, like database IDs or something, you can use manually a function to returns suggestions to user.
import { CliSuggestor } from 'interactive-cli-helper';
const suggestor: CliSuggestor = (rest: string) => {
return ['suggestion1', 'suggestion2'];
};
const something_cmd = cli.command('something', 'handler', {
onSuggest: suggestor,
});
something_cmd.command(/^suggestion\d$/, 'Suggestions matched :D');