Build an Interactive CLI Using TypeScript
Tags: javascript, typescript, cli
Published: 2020-07-20 14:21:29

> _This article is originally published on [My Blog](https://www.christiandimas.com/build-interactive-cli-using-typescript/)_

In this article, we're going to build a CLI using Typescript and a framework called OCLIF. We'll make it interactive so that it's really easy to pick up and use for the first time.

---

## Introduction
> _Note: In this part, I'm going to explain how a CLI is structured. Feel free to skip if you already know this._

Before we continue let's take a look at how a CLI is constructed. I'm going to use the npm `CLI` here as an example. We usually call the npm command like this:
```
npm install --save package_name
```

A complete CLI is usually made of four parts:
* **Command**: This is the first word that we type when using a CLI in this case, it's the word `npm`.
* **Sub-Command**: This is an optional word that comes after the command. In this case, it's the word `install`.
* **Flags**: This is one of the ways to send an option to the CLI. It is started with the dash (`-`) symbol. In this case, it's the `--save` or a shorter version of it, the `-S`. The flag can also contain a value; when it needs a value it will be added like this: `--foo=bar` or `-f=bar`.
* **Arguments**: This is the other way to send an option to the CLI. The difference from using flags is that the argument doesn't start with a dash and must be added in the correct order. In this case, it's the `package_name` - you might notice the package_name argument is the first one to be added. If you call it, like `npm install foo package_name`, then the install process will get `foo` as it's package_name.

Now that's clear, let's get started with the actual project!


---

## Getting Started
We are going to need two NPM libraries for our core functionality. The first one is called `OCLIF`, which stands for Open CLI Framework. This library provides us with a complete workflow of building a CLI. The other library we need is called `Inquirer`, this will help us make the CLI interactive and user friendly.

There are two ways to create a new project with OCLIF.
The first is by installing it globally, then running the command like this:

```
yarn global add oclif 
oclif multi pizza-cli
```
The other way is to simply use `npx`, like this:
```
npx oclif multi pizza-cli
```

> _Note: We are calling the oclif command with `multi` as an argument, this will tell oclif to create a multi command CLI. Think of it like the `npm` command where you can pass in subcommands like `npm install`, `npm uninstall`, etc._

OCLIF also supports creating a single command CLI. Something like the `ls` command, where it only has one functionality

This command will give us a few questions, which will impact how the project scaffold will be laid out

![OCLIF Init](https://dev-to-uploads.s3.amazonaws.com/i/dz50ys8qrwvoy66izcm8.png)

Most of the questions are self-explanatory and will be added to your package.json file. Two questions that you should note:
* **NPM Package Name**: This will be used when you are publishing the CLI to NPM.
* **Command bind name the CLI will export**: This is the command that you type on the Terminal to use this CLI like npm, ls, etc.

After the scaffolding process is completed, move to your project directory and open it in your code editor (I'll be using VSCode in this article):

```
cd pizza-cli
code .
```


---

## Project Structure
The project structure will look like this:

![Project Structure](https://dev-to-uploads.s3.amazonaws.com/i/mk3kfs0o169gb2nv1xxm.png)

As you can see, you already have a file inside the command folder called `hello.ts`. This file is the only thing we need to have a hello command.

Let's try it out! Back in your terminal, type this:
```
./bin/run hello     # This will call the hello subcommand
```

You can also run:
```
./bin/run --version   # This will show the cli version
./bin/run --help      # This will show a generated help for the CLI
```

Cool! You just created your first CLI!
Now, let's see what's inside the `hello.ts` file:

{% gist https://gist.github.com/kenanchristian/293d4b38171cf9e13aa1b9c6bd21068e %}

Your `hello.ts` file will look something like the snippet above. Let's look at a couple of interesting things:
* **Description and Examples**: This will show up when you run the help subcommand, and is used to provide more info for the user that's using it.
* **Flags**: This is where you define all your available flags for the subcommand. This will be parsed as JSON in the code.
* **Args**: This is where you define all your available arguments. One thing to note here is that the order of the argument matters because it will affect how the CLI is used.
* **Run** method: The run() method is the one that's executed when you call the CLI. There are no parameters to this method but we can get all the arguments and flags by using the this.parse() method, as you can see at line 23.

Now that we've understood the content of the file. Let's modify it a little, so that it matches our needs.

First, let's change the filename from `hello.ts` to `create.ts`, and the Class name from Hello to Create. This will change the subcommand from hello to create, and we can call it like this: 
```
./bin/run create.
```

Now let's modify the description and examples to look like this:

{% gist https://gist.github.com/kenanchristian/42cb19f21d6f400fe44d2a47575dce72 %}

Next, we add some more flags and arguments. It should look like this:

{% gist https://gist.github.com/kenanchristian/d0fd5dae1e086f7b1996f415ff72a7b5 %}

The last step is updating the `run()` method so we can see what the args and flags look like. The updated `run()` method should look like this:

{% gist https://gist.github.com/kenanchristian/09df2bd09577bb68cf74c2e10485a529 %}

With everything updated, the whole file should look like this:

{% gist https://gist.github.com/kenanchristian/bdb23988b46c888147b61c5068587147 %}

Now, when you go back to the terminal, you can call the command like this:
```
./bin/run create 2 -t=pepperoni -c=thin -x
```

Or if you prefer the more verbose way, you can also do this:
```
./bin/run create 2 --toppings=pepperoni --crust=thin --extraSauce
```

You will see all the flags and arguments that we passed in formatted as a nice JSON object that's easy to work with.
Now that we have all the core functionality implemented, it's time to make it more interactive!

---

## Making It Interactive
To make the CLI more interactive and user friendly, we'll need an additional NPM package called Inquirer. You can install it like this:

```
yarn add inquirer
yarn add -D @types/inquirer
```

After that's installed, let's modify our run method to look something like this:

{% gist https://gist.github.com/kenanchristian/5ee0a9ad4d8363de3ddb6cf5138c6d4b %}

In line 1 we're importing the prompt() method from inquirer, then in the run() method, instead of using `this.parse()` to get all the arguments and flags that are passed in, we call the `prompt()` method.

The `prompt()` method takes an array of questions that the user is asked when they run the CLI subcommand. The most basic question contains a type and message key, for the full options that you can use in the question please go here.

With everything now set up, now you can execute the CLI like this:

```
./bin/run create
```

Now, instead of adding all the arguments and flags when executing the CLI, it will ask you interactively for the data that it needs.

Congratulations! You just built your first, super user-friendly, interactive CLI!

![Interactive CLI](https://dev-to-uploads.s3.amazonaws.com/i/njrt9y7v5fcd55bx2gbf.png)

---

## Further Improvements
In this part of the article, I want to discuss some improvements that, in my opinion, will make the CLI better.

### Make the interactive prompt optional
This might sound a bit weird. Why would I make the optional prompt optional when it has a better user experience than the usual CLI?
My argument is that for a power user who's already familiar with the CLI it's actually faster to just add all the arguments and flags they need, instead of going through the interactive prompt.
To do this, we need to modify the run() method slightly, and make it look like this:

{% gist https://gist.github.com/kenanchristian/9ea9307658c9d998481e5fd979d5112e %}

I'm moving the prompt process to a new method, and in the run method, we are checking the arguments and the flags. If it exists then we use that - but if not, we run the prompt process.
With this implementation, the user now has two ways of using the CLI.


### Adding color and loading bar
The next improvement I want to make is to make the CLI nicer to look at and use. Firstly, by adding color to the this.log method, so it's not just white. Secondly, by showing a loading bar when a process is running to give a better user experience.
To do those, we need to install two packages. We need chalk, for adding color to the `this.log` and we need `cli-progress` to show a loading bar. 

We can install it like this:
```
yarn add cli-progress chalk
yarn add -D @types/cli-progress @types/chalk
```

With these packages installed, let's update our code again:

{% gist https://gist.github.com/kenanchristian/59cf4713e78cbbd695610e556d8805a3 %}

First, I introduce a new method called `makePizza()`.
This is just to simulate a process running.
Inside that method, I'm calling a `sleep()` method. This is just a simple helper method to make sure the process doesn't finish too quickly.

Then use the chalk package to add color to our logging is actually pretty simple, we just need to import the color method that we need. In this case, we are using yellow, green, and cyan. Then we can just wrap the text with that method. As Simple as that, we get a colored log!

The next thing we do is add the loading bar. 
First, we import the SingleBar and Presets from `cli-progress`.
Then, on line 20, we initialize the loading bar and giving it a custom format. On line 24 we call the `progressBar.start(length, startFrom)` method, this is used to set the loading bar length and start value.

To simulate a process, we loop for each pizza to make a topping, to increment the loading bar value by one. With all of this now set up, our CLI looks like this:

![Final Result](https://dev-to-uploads.s3.amazonaws.com/i/t0sua57gu0decoay9yq5.gif)

Pretty cool!

To learn more about all of the things that we've used, visit the links below. Thanks for reading this far and see you in the next article!

---

Resources:
OCLIF: https://oclif.io/
Inquirer: https://github.com/SBoudrias/Inquirer.js/
Chalk: https://github.com/chalk/chalk
CLI-Progress: https://github.com/AndiDittrich/Node.CLI-Progress
Project Repo: https://github.com/kenanchristian/pizza-cli