magicli

Automagically generates command-line interfaces (CLI) for any module. Expected options and help sections are created automatically based on parameters names, with support to async. It can be installed globally, in order to *execute* any module, or .js fil

Usage no npm install needed!

<script type="module">
  import magicli from 'https://cdn.skypack.dev/magicli';
</script>

README

MagiCLI

Build Status npm npm

Automagically generates command-line interfaces (CLI), for any module. Just require('magicli')(); and your module is ready to be executed via CLI.

The main goal is to have any module prepared to be executed via CLI (installed globally with -g, or by using npx):

To see why I believe you should plug it on your module, even if you don't need a CLI (it probably will serve someone on the community), read here: Introducing MagiCLI: Automagically generates a command-line interface (CLI) for any module

It can be installed globally, in order to execute any module or .js file via CLI.

Goals

  • Minimal setup (one line)
  • Automatic options names based on functions parameters
  • Out of the box support to async functions (Promises, or any thenable lib)
  • A specific help section for each nested property ("subcommands")
  • Name, Description and Version extracted from package.json
  • Simple API to hook into the execution flow (stdin, before, after)
  • Cover all possible cases of module.exports (Function, Object with nested properties, Destructuring parameters)
  • Provide a CLI to be used to execute any given module or .js file via CLI

Usage (the most simple and minimal way)

  • npm install magicli
  • Add the property bin to your package.json containing the value ./bin/magicli.js
  • Create the file ./bin/magicli.js with the following content:
#!/usr/bin/env node

require('magicli')();

Done! Install your module with -g, or use it via npx, and run it with --help to see the result. The --version option will show the same value found at package.json. In the same way you can just run node ./bin/magicli.js --help to test it quickly, without installing it.

Let's suppose that your-module exports the function:

module.exports = function(param1, param2) {
    return param1 + param2;
}

When calling it via CLI, with --help, you will get:

Description:

  Same description found at package.json

Usage:

  $ your-module [options]

Options:

  --param1
  --param2

The program will be expecting options with the same name as the parameters declared at the exported function, and it doesn't need to follow the same order. Example:

$ your-module --param2="K" --param1="Z" would result in: ZK.

Important: MagiCLI requires the module in order to analyse it, and provide the command-line interface for it. Keep that in mind in case your module does something just by being required.

Usage via CLI

In order to execute any module or .js file via CLI, install it globally:

$ npm install magicli -g

Then just pass in as the first argument, the path to a module or a .js file. Examples:

  • $ magicli . --help
  • $ magicli ./path/to-some-module --help
  • $ magicli ./path/to-a-file.js --help

Or use it via npx without the need to install it.

Let's suppose that you have a simple .js file as this one:

module.exports = {
    sum: (n1, n2) => n1 + n2,
    ec: {
        ho: str => `${str} !!!`
    }
}

Just execute magicli on it, as $ magicli ./path/to-the-file-above.js --help and you will get:

Commands:
  sum
  ec-ho

$ magicli ./path/to-the-file-above.js sum --help will give you:

Usage:
  $ sum [options]

Options:
  --n1
  --n2

and $ magicli ./path/to-the-file-above.js sum --n1=4 --n2=2 will result in 6

How it works

MagiCLI is capable of handling many styles of exports, like:

  • Functions
  • Object Literal
  • Nested properties
  • Class with static methods

And also any kind of parameters declaration (Destructuring Parameters, Rest Parameters).

If your-module were like this:

// An Arrow function with Destructuring assignment and Default values
const mainMethod = ([p1, [p2]] = ['p1Default', ['p2Default']], { p3 = 'p3Default' } = {}) => `${p1}-${p2}-${p3}`;

// Object Literal containing a nested method
module.exports = {
    mainMethod,
    nested: {
        method: param => `nested method param value is: "${param}`
    }
};

$ your-module --help would result in:

Description:

  Same description found at package.json

Usage:

  $ your-module <command>

Commands:

  mainMethod
  nested-method

$ your-module mainMethod --help would be:

Usage:

  $ your-module mainMethod [options]

Options:

  --p1
  --p2
  --p3

$ your-module nested-method --help returns:

Usage:

  $ your-module nested-method [options]

Options:

  --param

Calling mainMethod without any parameter: $ your-module mainMethod

results in: p1Default-p2Default-p3Default

While defining the parameter for nested-method: $ your-module mainMethod nested-method --param=paramValue

would return: nested method param value is: "paramValue"

Note: Nested methods/properties will be turned into commands separated by -, and it can be configurable via options (subcommandDelimiter).

Usage Options

magicli({ commands = {}, validateRequiredParameters = false, help = {}, version = {}, pipe = {}, enumerability = 'enumerable', subcommandDelimiter = '-'})

Options are provided to add more information about commands and its options, and also to support a better control of a command execution flow, without the need to change the source code of the module itself (for example, to JSON.stringify an Object Literal that is returned).

enumerability

By default, only the enumerable nested properties will be considered. The possible values are: 'enumerable' (default), 'nonenumerable' or 'all'.

validateRequiredParameters

MagiCLI can validate the required parameters for a command and show the help in case some of them are missing. The default value is false.

help

help.option To define a different option name to show the help section. For example, if 'modulehelp' is chosen, --modulehelp must be used instead of --help to show the help section.

help.stripAnsi Set to true to strip all ansi escape codes (colors, underline, etc.) and output just a raw text.

version

version.option To define a different option name to show the version. For example, if 'moduleversion' is chosen, --moduleversion must be used instead of --version to show the version number.

pipe (stdin, before and after)

The pipeline of a command execution is:

stdin (command.pipe.stdin || magicliOptions.pipe.stdin) =>

magicliOptions.pipe.before =>

command.pipe.before =>

command.action (the method in case) =>

command.pipe.after =>

magicliOptions.pipe.after =>

stdout

Where each of these steps can be handled if needed.

As it can be defined on commands option, for each command, pipe can also be defined in options to implement a common handler for all commands. The expected properties are:

pipe.stdin (stdinValue, args, positionalArgs, argsAfterEndOfOptions)

Useful to get a value from stdin and set it to one of the expected args.

pipe.before (args, positionalArgs, argsAfterEndOfOptions)

To transform the data being input, before it is passed in to the main command action.

pipe.after (result, parsedArgs, positionalArgs, argsAfterEndOfOptions)

Note: stdin and before must always return args, and after must always return result, as these values will be passed in for the next function in the pipeline.

commands

The options are effortlessly extracted from the parameters names, however it is possible to give more information about a command and its options, and also give instructions to the options parser.

commands expects an Object Literal where each key is the command name. It would be the module's name for the main function that is exported, and the command's name as it is shown at the Commands: section of --help. For example:

commands: {
    'mainmodulename': {},
    'some-nested-method': {}
}

For each command the following properties can be configurable:

options

Is an Array of Objects, where each contains:

name (required) The name of the parameter that will be described

required To tell if the parameter is required.

description To give hints or explain what the option is about.

type To define how the parser should treat the option (Array, Object, String, Number, etc.). Check yargs-parser for instructions about type, as it is the engine being used to parse the options.

alias To define an alias for the option.

pipe (stdin, before and after)

The pipeline of a command execution is:

stdin (command.pipe.stdin || magicliOptions.pipe.stdin) =>

magicliOptions.pipe.before =>

command.pipe.before =>

command.action (the method in case) =>

command.pipe.after =>

magicliOptions.pipe.after =>

stdout

Where each of these steps can be handled if needed.

As it can be defined on options to implement a common handler for all commands, pipe can also be defined for each command.

pipe.stdin (stdinValue, args, positionalArgs, argsAfterEndOfOptions)

Useful to get a value from stdin and set it to one of the expected args.

pipe.before (args, positionalArgs, argsAfterEndOfOptions)

To transform the data being input, before it is passed in to the main command action.

pipe.after (result, parsedArgs, positionalArgs, argsAfterEndOfOptions)

Note: stdin and before must always return args, and after must always return result, as these values will be passed in for the next function in the pipeline.

If needed, a more thorough guide about this section can be found at cliss (as this is the module under the hood to handle that)

A full featured use of the module would look like:

magicli({
    commands,
    enumerability,
    subcommandDelimiter,
    validateRequiredParameters,
    help: {
        option,
        stripAnsi
    },
    version: {
        option
    },
    pipe: {
        stdin: (stdinValue, args, positionalArgs, argsAfterEndOfOptions) => {},
        before: (args, positionalArgs, argsAfterEndOfOptions) => {},
        after: (result, parsedArgs, positionalArgs, argsAfterEndOfOptions) => {}
    }
});

Example

To better explain with an example, let's get the following module and configure it with MagiCLI to:

  • Define p1 as String (mainMethod)
  • Write a description for p2 (mainMethod)
  • Define p3 as required (mainMethod)
  • Get p2 from stdin (mainMethod)
  • Use before (command) to upper case param (nested-method)
  • Use after (command) to JSON.stringify the result of (nested-method)
  • Use after (options) to decorate all outputs (nested-method)

module ("main" property of package.json)

'use strict';

module.exports = {
    mainMethod: (p1, p2, { p3 = 'p3Default' } = {}) => `${p1}-${p2}-${p3}`,
    nested: {
        method: param => {

            // Example of a Promise being handled
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    resolve({ param });
                }, 2000);
            });
        }
    }
};

magicli.js ("bin" property of package.json)

#!/usr/bin/env node


require('../magicli')({
    commands: {
        'mainMethod': {
            options: [{
                name: 'p1',
                description: 'Number will be converted to String',
                type: 'String'
            }, {
                name: 'p2',
                description: 'This parameter can be defined via stdin'
            }, {
                name: 'p3',
                required: true
            }],
            pipe: {
                stdin: (stdinValue, args, positionalArgs, argsAfterEndOfOptions) => {
                    args.p2 = stdinValue;
                    return args;
                }
            }
        },
        'nested-method': {
            options: [{
                name: 'param',
                description: 'Wait for it...'
            }],
            pipe: {
                before: (args, positionalArgs, argsAfterEndOfOptions) => {
                    if (args.param) {
                        args.param = args.param.toUpperCase();
                    }
                    return args;
                },

                after: JSON.stringify
            }
        }
    },
    pipe: {
        after: (result, positionalArgs, argsAfterEndOfOptions) => `======\n${result}\n======`
    }
});

Tests

There is another repository called MagiCLI Test Machine, where many real published modules are being successfully tested. As the idea is to keep increasing the number of real modules tested, it made more sense to maintain a separated repository for that, instead of being constantly increasing the size of MagiCLI itself over time. I ask you to contribute with the growing numbers of those tests by adding your own module there via a pull request.

If you find some case that isn't being handled properly, please open an issue or feel free to create a PR ;)