A Guide to CLIs with Node.js
Tags: snowpack, buildtool
Published: 2021-03-05 08:48:01

Ever wondered how `create-react-app .` or `git init` or simply `node -v` works? These are CLIs or Command Line Interfaces. I'm pretty sure most of us have used CLIs at some point of time in our lives. Even commands that we use everyday like `ls` or `cd` are also CLIs.

Today, I'm going to show you how to set up a very simple CLI with some of the common features we find in CLIs. Nothing too complex... Let's dive right into CLIs

###What are CLIs
CLIs are commands that we run in our terminal to do something. If you want a wikipedia definition 🙄 - 

>A command-line interface processes commands to a computer program in the form of lines of text. The program which handles the interface is called a command-line interpreter or command-line processor. Operating systems implement a command-line interface in a shell for interactive access to operating system functions or services.

###Why are CLIs necessary?
In the modern world of GUIs(Graphical User Interfaces), you might ask that why should we know about CLIs? Weren't they used in the 80s? I agree with you a 💯 percent. They are outdated but a lot of old applications still use CLIs. The terminal/command prompt generally has more permissions and access compared to GUI applications by default(It's bad user experience to allow 100 permissions to run an app).

###Why build CLIs with Node
The main advantage is the ecosystem of over 1 million packages we get with Node. Using these we can avoid boilerplate code and implement functionality easily.


###Getting Started with CLIs
Before doing anything else, we need to create a `package.json`.

1. Create an empty folder
2. Run `npm init` and quickly fill in the options.

Let's create our javascript file, I'm naming it `cli.js`. This is a common convention which is followed as the file immediately tells us about its function. I'm going to add a `console.log('CLI')` to our file so that we can know that everything is working.

We need to update the `package.json` to run our CLI. Add the `bin` property into the file.

```
"bin": {
  "hello-world": "cli.js"
}
```

The `hello-world` property is the command name we want to run and the `cli.js` is the filename. If you want to use another name or your file is stored in a different path, update it accordingly.


Let's install our NPM package. In your terminal/command prompt run the following - 

```
npm i -g .
```

We all have come across `npm install` in the past. We add the `-g` flag so that NPM installs the package globally. The `.` tells NPM to install the package. NPM installs the file that is there in the `main` property of the `package.json`. If you have changed

Now we can run the command name we set earlier(`hello-world`) and our CLI should boot up.

```
hello
```

However, we get the following error - 

![Screenshot 2021-03-05 at 08.49.55](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r0x8s5l56nyp7ey2ik3f.png)

The error seems to tell us that compiler is not able to understand that we have javascript code. So, to tell the compiler that it should use `node` to run our file. For this, we use a [shebang] (https://en.wikipedia.org/wiki/Shebang_(Unix)#:~:text=In%20computing%2C%20a%20shebang%20is,bang%2C%20or%20hash%2Dpling.). Add the following code at the top of your file.

```
#!/usr/bin/env node
```

This is the path to node. We tell *nix systems that the interpreter of our file should be at the specified path. IN windows, this line will be be ignored as it is specified as a comment but NPM will pick it up when the package is being installed.

There we go, now it should work.

![Screenshot 2021-03-05 at 09.05.06](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o91x6bd27p1xrxlr0muw.png)
 

We have not actually done anything in our CLI except logging some data. Let's begin by implementing common features in CLIs.

###Prompts
Prompts are questions that are asked to the user. You might have come across it in Javascript by calling the `prompt` function. I'm going to be using the [prompts] (https://www.npmjs.com/package/prompts) package. This package   simplifies allows us to prompt the user and get the response with just a few lines of code. 

Install it by running - 
```
npm i prompts
```

Next, add the following code into your file and we will walkthrough it together.

```
const prompts = require('prompts');
 
// IIFE for using async functions
(async () => {
  const response = await prompts({
    type: 'number',
    name: 'value',
    message: 'Enter your name', 
  });
 
  console.log(response.value); // the name the user entered.
})();
```

Note: IIFE's are function expressions that are immediately called. It shortens saving the function expression to a variable and the calling it.

Our basic CLI should look like this - 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kuqp8qff8565lpwzt274.png)


Awesome right? I think the terminal looks a bit dull, let's colour it up!

###Colours with Chalk
Do you know how colours are added into terminals? ANSI escape codes are used for this(sounds like a code for a prison). If you've ever tried to read binary, reading ANSI is quite similar.


![giphy](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iu0yt9up7ik48i9jzcd6.gif)
  
Thankfully, there are packages that help us with this. I'm going to use [chalk] (https://www.npmjs.com/package/chalk) for the purpose of this tutorial. 

Install it by running - 
```
npm i chalk
```


Let's get started by replacing the original `console.log()` with the following.

```
console.log(chalk.bgCyan.white(`Hello ${response.value}`)); 
console.log(chalk.bgYellowBright.black(`Welcome to "Hello World with CLIs"`));
```

As you can see, the first `log` chains the `bgCyan` and the `white` methods. Check the [official documentation] (https://github.com/chalk/chalk#readme) to know more.

After this, our code looks like this - 

```
const response = await prompts({
  type: 'text',
    name: 'value',
    message: 'What is your name?',
});

console.log(chalk.bgCyan.white(`Hello ${response.value}`)); 
console.log(chalk.bgYellowBright.black(`Welcome to "Hello World with CLIs"`));
```

Here's a demo of our CLI - 
![Screenshot 2021-03-04 at 21.02.09](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c1s4qcc6lb3wc84xwrgm.png)

That's a good start, now let's bump up the font size. I can barely see anything.

![giphy (1)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kp56xe996jc636fb31dr.gif)

###Playing with Fonts
Changing fonts are quite difficult through javascript and I spent a lot of time searching for a package that does just that. Thankfully, I got it. We'll be using the [cfonts] (https://www.npmjs.com/package/cfonts) package. Go ahead and install it by running the following - 

```
npm i cfonts
```

Begin by replacing our previous `console.log` with the following - 

```
CFonts.say(`Hello ${response.value}`, {
  font: 'tiny',
  colors: ['cyanBright'],
});

CFonts.say(`Welcome to Hello World with CLIs`, {
  font: 'simple',
  colors: ['cyanBright'],
});
```

Our Hello World CLI looks like this - 

![Screenshot 2021-03-04 at 22.25.23](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/etdb3zurda3bq7rbgsu1.png).

Let's add some basic arguments like `-n` or `--name` and `-v` or `--version`. We'll be using the [yargs] (https://www.npmjs.com/package/yargs) package to simplify the process. If you don't want to use it, you can use `process.argv` to access the arguments.

Begin by installing the `yargs` package.
```
npm i yargs
```

Let's import it into our code - 
```
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
const argv = yargs(hideBin(process.argv)).argv;
```

The `argv` stands for all the arguments specified by the user. We use the `hideBin()` method to remove the `--` in front of arguments. Example - 

```
hideBin('--example'); // output -> example
```


Now we can add a series of `IF and ELSEIF` checks for the arguments provided. You can also use a `Switch` statement.

```
const packageJson = require('./package.json');

if (argv.version || argv.v) {
	console.log(packageJson.version);
} else if (argv.name || argv.n) {
	console.log(packageJson.name);
} else if (argv.init) {
  // Put our functionality of printing hello world, asking name etc.
} else {
  console.log('Please specify an argument/command');
}
```

Since package.json is essentially a javascript object, we can pull off the properties from it. `yargs` helpfully provides an object of the specified arguments. We can check if the required properties. There's also an `else` check to see if the user has not given any arguments. Now let me show you some other common features in CLIs.


###Loaders
Asynchronous and time consuming operations are very common in CLIs. We don't want to leave the user thinking his computer has hung up. We'll use the [ora] (https://www.npmjs.com/package/ora) package for loaders. Begin by installing `ora` - 

```
npm i ora
```


Let's require the package and setup a simple loader - 

```
const ora = require('ora');
const spinner = ora('Loading...').start();
```

This loader will run forever because we haven't stopped it. We don't have any code that takes a substantial amount of time(API requests, data processing etc), We'll use `setTimeout()` to simulate the end of loading.

```
const spinner = ora('Loading...').start();

setTimeout(() => {
	spinner.stop();

	(async () => {
		const response = await prompts({
			type: 'text',
			name: 'value',
			message: 'What is your name?',
		});

		CFonts.say(`Hello ${response.value}`, {
			font: 'tiny',
			colors: ['cyanBright'],
		});

		CFonts.say(`Welcome to Hello World with CLIs`, {
			font: 'simple',
			colors: ['cyanBright'],
		});
	})();
}, 1000);
```

I've put our IIFE function inside our `setTimeout()` so that it gets executed only after the loader finishes. Our loader runs for `2000` milliseconds or `2` seconds.

Let's do a quick refactor. We have a callback in our `setTimeout()` which can be made `async`. We can now remove the `IIFE` function.

```
setTimeout(async () => {
  spinner.stop();
  // Body of IIFE function goes here.

}, 2000);
```

There's a lot lot more to do with [ora] (https://www.npmjs.com/package/ora), check out the [official docs] (https://github.com/sindresorhus/ora#readme)  to know more.

###Lists
Remember the `IF ELSEIF` we had setup for arguments? Let's add a `--help` argument that lists out all the commands. Lists are a very important part of CLIs. I'm using a helpful package [listr] (https://www.npmjs.com/package/listr) to handle these lists. Let's add the following code into our file in the place where we have our `IF` checks.

```
else if (argv.help) {
  const tasks = new Listr([
    {
      title: '--init: Start the CLI',
    },
    {
      title: '--name: Gives the name of the package',
    },
    {
      title: '--version: Gives the version of the package',
    },
    {
      title: '--help: Lists all available commands',
    },
  ]);

  tasks
    .run()
    .then(() => {
      console.log('Done');
    })
    .catch((error) => {
	console.log(error);
    });
} 
```

We have created a new if check for `--help`. Inside that, we passed an array of tasks into the `Listr` class. THis will basically list the following. This can be done by `console.log()` but the reason I have used `listr` is because in each object in the tasks array, we can also specify a task property with an arrow function. Check out the [documentation] (https://github.com/SamVerschueren/listr#readme) to know more.


Now our CLI looks like this - 
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1tzewz5tslanufjxh8zg.png)

###Executing Commands from Code
A lot of CLIs will want to access other CLIs and run commands such as `git init` or `npm install` etc. I'm using the package [execa] (https://www.npmjs.com/package/execa) for the purpose of this tutorial. Begin by installing the module.

```
npm i execa
```

Then, require the module at the top of the file.
```
const execa = require('execa');
```

In the `ELSE` block for our arguments condition, add the following code. If you remember, we already had a `console.log()` asking us to specify a command. Let's run our previous `--help` command and list out the available commands.

```
else {
  console.log('Please specify a command');

  const { stdout } = await execa('hello-world', ['--help']);
  console.log(stdout);
}
```

We call the `execa()` function and pass in the name of the command. Then, in an array we pass in the arguments to be provided. We will `await` this and then destructure the output. Then we will log the output to simulate running the command.

Our CLI should finally look like this - 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hzg3vsp6zc5ybsgohlc9.png)

That's it guys, thank you for reading this post. I hope you guys liked it. If you found the post useful or liked it, please follow me to get notified about new posts. If you have questions, ask them in the comments and I'll try my best to answer them. As a bonus, I have made a list of some packages that can be used while developing CLIs.


##Helpful Packages
These are a list of packages that  might help you while developing CLIs. Keep in mind that this is by no means an exhaustive list. There are over 1 million packages on NPM and it is impossible to cover them all. 


**Inputs**
* [prompts] (https://www.npmjs.com/package/prompts)
* [inquirer] (https://www.npmjs.com/package/inquirer)
* [enquirer] (https://www.npmjs.com/package/enquirer)
* [cli-prompt] (https://www.npmjs.com/package/cli-prompt)
* [email-prompt] (npmjs.com/package/email-prompt)

**Coloured Responses/Output**
* [chalk] (https://www.npmjs.com/package/chalk)
* [kleur] (https://www.npmjs.com/package/kleur)

**Loaders**
* [cli-spinners] (https://www.npmjs.com/package/cli-spinners)
* [cli-spinner] (https://www.npmjs.com/package/cli-spinner)
* [ora] (https://www.npmjs.com/package/ora)


**Boxes Around Output**
* [boxen] (http://npm.im/boxen)
* [cli-boxes] (https://www.npmjs.com/package/cli-boxes)
* [ascii-art] (https://www.npmjs.com/package/ascii-art)