How to Build a Command Line (CLI) Tool in Node.js

by Tapasweni Pathak

8 min read

Command Line Tool enables the user to complete required tasks directly from the terminal. User-friendly commands are interpreted as system calls internally and commands are executed. Node.js provides packages that provide another layer of abstraction and encapsulation for creating these command-line tools (Node-cmd tools).

Let’s see how to create a CLI tool and create our first command-line tool in Node.js (Nodejs CLI) for manipulating and triggering API calls of AWS ECS Cluster.

Introduction to Task

To create CLI tool with Node it's important to know that AWS Elastic Container Service (AWS ECS) is a collection of AWS EC2s. In simple terms, Linux machines run similar configurations, and in turn, create a network to develop a networking system. AWS ECS provides an API layer to directly manipulate and work with the AWS EC2s i.e., Linux machines.

For example, using AWS ECS APIs one can get a particular EC2 configuration, available memory space, idle time of the network e.t.c. So, let's start the Node js create command line tool operation by setting up project.

Setting up Project

To learn how to make a CLI tool, Node.js developers should start with creating a simple Node.js project. After npm init provides answers to questions asked. This is general information about the new npm Nodejs build command package that you are creating.

mkdir nodejs-cli-tool
cd nodejs-cli-tool
mkdir ecs-cluster-control
cd ecs-cluster-control

npm init

> ecs-cluster-control$ npm init
> This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

> See `npm help json` for definitive documentation on these fields
and exactly what they do.

> Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

> Press ^C at any time to quit.
> package name: (ecs-cluster-control) 
> version: (1.0.0) 
> description: how to build a command line tool in nodejs
> entry point: (index.js) 
> test command: 
> git repository: 
> keywords: 
> author: 
> license: (ISC) 
> About to write to /home/username/nodejs-cli-tool/ecs-cluster-control/package.json:

{
  "name": "ecs-cluster-control",
  "version": "1.0.0",
  "description": "how to build a command line tool in nodejs",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}


Is this OK? (yes) 


You can also set all the answers to defaults using npm init -y.

Node.js Packages as Dependencies

Let’s install the following packages for our Node build command:

easy-tableNice utility for rendering text tables with javascript.
eslintESLint is a tool for identifying and reporting on patterns found in ECMAScript/JavaScript code. In many ways, it is similar to JSLint and JSHint with a few exceptions:
eslint-plugin-nodeAdditional ESLint's rules for Node.js
meowNodejs CLI app helper
simple-statisticsA JavaScript implementation of descriptive, regression, and inference statistics.
sinonStandalone and test framework agnostic JavaScript test spies, stubs, and mocks (pronounced "sigh-non", named after Sinon, the warrior).
tap-specFormatted TAP output like Mocha's spec reporter
tapetap-producing test harness for node and browsers
@mapbox/mock-aws-sdk-jsA library that provides sinon-style stubs for aws-sdk-js service methods for use in testing.

npm i easy-table eslint eslint-plugin-node meow simple-statistics sinon tap-spec tape @mapbox/mock-aws-sdk-js

Initializing Node Command Line Tool Creation

Let’s create four main directories to provide a modular structure for our Node CLI tool. 


bin      → cli.js
commands → ecs-cluster-control.js
lib      → terminate-instance.js
         → pending-tasks.js
         → persist-disk-metrics.js
         → task-churn.js
test     → ecs-cluster-control.test.js
         → pending-tasks.test.js
         → task-churn.test.js

bin/cli.js

In this file, we will instantiate meow.js and provide the following:

  • usage command for the CLI tool Nodejs
  • commands 
  • options
  • description
  • aliases i.e., short forms for our long commands
  • include the file describing our commands
  • error messages for the Nodejs command line tool

So, let's move on with our Nodejs create CLI tool process.

The cli.js file will look something like this:

#!/usr/bin/env node

/* eslint-disable no-console */

var meow = require('meow');

var cli = meow({
  help: `
    USAGE: ecs-cluster-control <command> [OPTIONS]
    Commands:
      get-capacity          print spot fleet capacity information
      set-capacity          adjust the spot fleet's target capacity
      terminate-instance    gracefully shut down a single EC2
      teardown              gracefully delete an ecs-cluster stack
      scaling-activity      print recent spot fleet scaling activity
      fleet-events          print spot fleet event history
      instance-balance      print a table of instance type / region distribution
      docker-disk-metrics   open CloudWatch metrics for docker disk utilization
      persist-disk-metrics  open CloudWatch metrics for persistent disk utilization
      pending-metrics       open CloudWatch metrics for pending tasks per instance
      metric-offenders      find cluster instance max from the last 10 min for any System/Linux metric
      task-churn            print min/avg/max durations for tasks stopped in the last hour
      disable-scaledown     disables the scale-down schedule rule and error alarm actions
      enable-scaledown      enables the scale-down schedule rule and error alarm actions
      worker-capacity       calculates how many service tasks can be placed on cluster at current capacity
    Options:
      -h,  --help            show this help message
      -r,  --region          the region the cluster is in
      -cl, --cluster-name    the cluster name, e.g. production
      -c,  --capacity        the desired spot fleet target capacity
      -i,  --instance-id     an EC2 instance id
      -a,  --app-name        the application's service name on the cluster
      -m,  --me              your slack handle, e.g. '@rclark'
      --metric-name          the MetricName for a System/Linux metric
  `,
  description: 'Helper utilities for interacting with AWS ECS clusters'
}, {
  alias: {
    r: 'region',
    cl: 'cluster-name',
    c: 'capacity',
    i: 'instance-id',
    a: 'app-name',
    m: 'me'
  },
  string: ['region', 'cluster-name', 'instance-id', 'me'],
  number: ['capacity']
});

var command = cli.input[0];
var fn;
try { fn = require(`../lib/${command}`); }
catch(err) {
  console.error(err.message);
  cli.showHelp(1);
}

const preamble = cli.flags.region && cli.flags.clusterName ?
  require('../lib/cluster-info.js') : () => Promise.resolve();

preamble(cli.flags)
  .then((info) => fn(cli.flags, info))
  .catch((err) => {
    console.error(`ERROR: ${err.message}`);
    process.exit(1);
  });

commands/ecs-cluster-control.js

This will be our core file to provide an abstraction layer over every command code file inclusion. 
The ECS-cluster-control.js will look something like this:

'use strict';
const fs = require('fs');
const path = require('path');

/**
 * Description of ecs-cluster-control commands to be used in registration with mbxcli.
 */
module.exports.configuration = {
  description: 'Helper utilities for interacting with AWS ECS clusters',
  help: `
    USAGE: source ecs-cluster-control <command> [OPTIONS]
    command:
      task-churn                  print min/avg/max durations for tasks stopped in the last hour
      terminate-instance          gracefully terminates the specified instance
      pending-tasks               giveb a cluster name and region print currently pending tasks.
    Options:
      -a,  --account              [default] a comma-delimited list of accounts
                                  to create the stack in
      -r,  --region               [us-east-1] a comma-delimited list of regions
                                  to create the stack in
      -h,  --help                 shows this help message
  `
};


/**
 * Function called when command is run through source cli tool.
 * @param  {object} cli     parsed command-line arguments
 * @param  {object} context information about the context in which the command was requested
 */
module.exports.run = (cli, context) => {
  const command = cli.input[0];
  const files = fs.readdirSync(path.resolve(__dirname, '..', 'lib'));
  const commands = new Set(files.map((filename) => filename.replace('.js', '')));

  //check if command corresponds with a file in ./lib
  const valid = commands.has(command);

  if(valid){
    const run = require(`../lib/${command}.js`).run;
    return run(cli, context);
  } else {
    return Promise.resolve()
      .then(() => { console.log(`Command ${cli.input[0]} not valid`); });
  }
};

lib/command-name.js

This will contain every command the file system calls triggers. Let’s take as an example terminating an AWS ECS cluster instance, i.e., calling a Terminate Your Instance AWS ECS API with an ID.

Our command file terminate-instance.js will look something like this:

'use strict';

/**
 * Given a cluster name, region and instance id, terminates that instance.
 *
 * @param {String} A region, a cluster name and instance id.
 *
 * @returns {Object} Terminating the instance.
 */

module.exports.run = (cli, context) => {
  const instanceID = cli.input[1];
  const region = cli.flags.regions[0] || 'us-east-1';
  const accounts = cli.flags.accounts[0] || 'default';
  if(!instanceID)
    return Promise.reject(new Error('Please provide an instance ID'));
  console.log(`Terminating AutoScaling Instance ${cli.input[1]}`);

  return Promise.resolve()
    .then(() => {
      const params = {
        InstanceId: instanceID,
        ShouldDecrementDesiredCapacity: false
      };
      context.access.getClient('AutoScaling', accounts, { region: region })
        .then((autoscaling) => new Promise((resolve, reject) => {
          autoscaling.terminateInstanceInAutoScalingGroup(params, function(err, data) {
            if(err) return reject(err);
            else resolve(data);
          });
        }));
    });
};

This completes our first tool with Nodejs command line.

Usage

ecs-cluster-control terminate-instance <id>

Sample output:

Terminating AutoScaling Instance instance-id

Tests

Now let’s write some tests about the Nodejs create command line tool process.


test/file-name.js: will contain tests per file. 

  1. Instantiate, mock-aws-api, tape and sinon.
  2. Replicate ideal instance object values and assign dummy values.
  3. Call mocked AWS API, this will not trigger the AWS API but replication present in your node_modules/.
  4. Assert true and false based on the values that you should get and what you are getting. 

Full code files can be found here. Let me know what you create once you build command for Node js!

Final Word

Did we just simplify your most difficult and time-consuming problem of interacting with AWS APIs? Let us know! Now you know how to create Node CLI tool, so don't waste any more time to start the process.

Aside from this Node CLI tutorial, if you want to broaden your knowledge of Node.js, check out these articles on Sails js tutorial, Node js architecture, Node js discord bot, containerizing Node.js apps with docker, and Node js queue.

FAQs

Q: How to distribute and install the developed CLI tool?
Distribute and install a developed CLI tool by packaging it as an npm module, publishing it to the npm registry, and then installing it globally using npm or yarn. 
Q: How to update the CLI tool with new features or bug fixes?
Update the CLI tool by incrementing the version in package.json, adding new features or fixing bugs in the code, then republishing the updated package to npm. Users can update by running npm update <package-name> globally.
Q: How to handle user input validation within the CLI tool?
Use libraries like yargs or commander for parsing command line arguments and include checks or validation functions to ensure inputs meet expected formats or values. 
Tapasweni Pathak
Tapasweni Pathak
Senior Product Manager

Tapasweni is a Senior Product Manager at Microsoft. She likes solving real-world problems and working on open-source projects. Her main interests include operating systems, distributed systems, economics, and statistics.

Expertise
  • Jira
  • Product Strategy
  • Product Roadmaps
  • Analytics
  • Product

Ready to start?

Get in touch or schedule a call.