This article is originally published on Medium
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.
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
Sub-Command: This is an optional word that comes after the command. In this case, it's the word
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
--saveor 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:
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
fooas it's package_name.
Now that's clear, let's get started with the actual project!
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
multias an argument, this will tell oclif to create a multi command CLI. Think of it like the
npmcommand where you can pass in subcommands like
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
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 .
The project structure will look like this:
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 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
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:
Now let's modify the description and examples to look like this:
Next, we add some more flags and arguments. It should look like this:
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:
With everything updated, the whole file should look like this:
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!
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:
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 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:
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!
In this part of the article, I want to discuss some improvements that, in my opinion, will make the CLI better.
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:
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.
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:
First, I introduce a new method called
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
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:
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!
Project Repo: https://github.com/kenanchristian/pizza-cli