npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

interactive-cli-helper

v2.1.1

Published

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

Downloads

26

Readme

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: string | RegExp, 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');