Socket
Socket
Sign inDemoInstall

interactive-cli-helper

Package Overview
Dependencies
1
Maintainers
1
Versions
14
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

    interactive-cli-helper

> Create a simple interactive CLI for your Node.js application


Version published
Weekly downloads
4
increased by100%
Maintainers
1
Install size
325 kB
Created
Weekly downloads
 

Readme

Source

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 executor: Will be displayed/called if none command matches
  onNoMatch: rest => `Command ${rest} not found.`
});

const get_command = cli.command(
  // command name: string or RegExp
  'get', 
  // executor: CliListener, object, string or function to call
  'Please specify a thing to get.'
);

// executors could be async!
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')}`;
});

// You can get what's after matched command with the first parameter
pies.command('add', async rest => {
  const added_pie = await addPie({ name: rest });
  // You can return plain objects!
  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({
  /* Mandatory. To execute if none of the commands are matched. */
  onNoMatch: CliExecutor, 
  /* See Suggestions part */
  onSuggest?: CliSuggestor,
  /* Enable suggestion on tab keypress. Defauls to true. */
  suggestions?: boolean,
  /* Function to call when CLI is closed by CTRL+C. */
  onCliClose?: Function,
  /* Function to call when a command throw something. */
  onCliError?: (error: Error) => void,
});

// Declare your commands here..

// Listen !
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: stringRegExp, executor: CliExecutor, options: CliListenerOptions);

// For example
// database
const database_cli = cli.command('database', 'Please enter a thing to do with database !');

// database test
database_cli.command('test', async () => {
  if (await Database.test()) {
    return 'Database works :D';
  }
  return 'Database is not working :(';
});

// database get
const getter = database_cli.command('get', 'Specify which thing to get');

// database get products
const products = getter.command('products', async () => {
  const products = await Database.getProducts();
  return products;
});
// database get products coffee-**
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!');

// The following cli tree will be created:
// database
//  database test
//  database get
//    database get products
//      database get products coffee-(.+)
//    database get users
// say-hello

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(
  // Name. We won't change it
  'database', 
  // Handler: it will be executed if database is not followed
  // by any command, if commad does not exists, or if
  // our next defined validator fails !
  (rest, _, __, validator_state) => {
    if (validator_state === false) {
      // Database is not open !
      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.';
  },
  {
    // define our validator here (could be async or not)
    // that returns a boolean
    async onValidateBefore(/* rest, matches */) {
      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 for Command2
  executor: CliExecutor = "Hello";
}

@CliCommand()
class Command1 {
  executor: CliExecutor = rest => {
    return "You just entered " + rest + ".";
  };

  // validator for Command1 command listener
  onValidateBefore: CliValidator = () => {
    return true;
  };

  // Include Command2 as 'two' command
  @Command('two', Command2)
  command_two!: CliCommandInstance<Command2>;

  // Declare a local clilistener for this instance
  @LocalCommand('local-1', 'localOne', { onValidateBefore: 'validateLocalOne' })
  local_one_cmd!: CliListener;

  // Executor for local-1
  localOne(rest: string) {
    return "This is local one command".
  }

  // Check if local-1 can be executed.
  validateLocalOne(rest: string) {
    return true;
  }
}

@CliMain({ suggestions: true })
class Main extends CliBase {
  onNoMatch: CliExecutor = "No command matched your search.";

  // Assign Command1 for 'one' command
  @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';

// Types of CliExecutor
const str_executor: CliExecutor = "Hello !";
const obj_executor: CliExecutor = { prop1: 'foo', prop2: 'bar' };
const fn_executor: CliExecutor = function (
  /** The rest of the string -after- matched command. */
  rest: string, 
  /** Stack of matched commands until this listener is called. */
  stack: CliStackItem[],
  /** If current listener command is a RegExp, this is the matches array. Else, null */
  matches: RegExpMatchArray | null, 
  /** If current listener has a validator, this is its state. */
  validator_state?: boolean
) {
  // You can return anything here. If you return a Promise, it is awaited before showing next prompt.
  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 a `boolean` or a `Promise<boolean>` to validate or not.
  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(/* the title */ 'database', {
  /* Mandatory: available commands and description for them */
  commands: {
    'find': 'Find something in the database',
    'destroy': 'Destroy something in the database'
  },
  /* 
    CliExecutor, optional: Is called when {rest} is not empty (
    so entered command hasn't matched anything) 
  */
  onNoMatch: (rest: string) => {
    return `${rest} is not a valid command for database. For help, type "database".`;
  },
  /* Optional: Description to show under the title */
  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']; // or Promise<string>
};

// Declare it
const something_cmd = cli.command('something', 'handler', {
  onSuggest: suggestor,
});

something_cmd.command(/^suggestion\d$/, 'Suggestions matched :D');

FAQs

Last updated on 21 Dec 2023

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.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc