How to build a CLI using NodeJS 💻
Tags: node, javascript, tutorial
Published: 2022-01-04 11:50:40

## How to build a CLI using NodeJS 💻

CLI (Command Line Interface) is one of the most basic and powerful applications ever created by mankind. We all use CLI every day, whether it be npm, git, or any other CLI. Does your daily basis workflow have something that you have to do over and over again? 🤔. The chances are that it could be automated using CLI ✨

So let's get started 🏄

![](https://c.tenor.com/r3XdvPsAV3kAAAAC/despicable-me-minions.gif)

Today we are going to be building a CLI which would generate starter templates with TailwindCSS, ESLint, and Prettier pre-installed.

# Prerequisites

Here are a few tools which you would need to follow along with the tutorial:

1. A LTS (Long Term Support) version of [NodeJS](https://nodejs.org/en/) installed.
2. A text editor.

# Setting up the project

Let's initialize a NodeJS project

1. Open up your terminal
2. Create a folder for your project

```bash
mkdir tailwindcli
```

3. Navigate into it

```bash
cd tailwindcli
```

4. Initializing a NodeJS project

```bash
npm init
```

# Building the CLI

Now that we have our NodeJS setup ready. Let's start building our CLI

1. Create a folder named `bin` in the root directory of your project folder.
2. Create a file called `index.js` in the `bin` folder. This is going to be the main file of the CLI.
3. Now open up the `package.json` file and change the value of the key `main` to `./bin/index.js`.
4. Now add an entry into the `package.json` file called `bin` and add set its key to `tcli` and its value to `./bin/index.js`

   > The word `tcli` is the keyword which we would be using to call our CLI.

After making the changes the `package.json` file should look something like this:

```JSON
{
  "name": "tailwindcli",
  "version": "1.0.0",
  "description": "A CLI for generating starter files with TailwindCSS pre-installed",
  "main": "./bin/index.js",
  "bin": {
    "tcli": "./bin/index.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": ["cli", "tailwindcss", "nodejs"],
  "author": "Your name",
  "license": "MIT"
}
```

6. Open `bin/index.js` file and add this line at the top of the file

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

> The line starting with a `#!` is called a shebang line. A shebang line is used to tell the absolute path to the interpreter that will run the below code.

Let's add some JS code so that we can test the CLI out 🚀.

7. Adding some JS code

```javascript
console.log('The CLI is working 🚀');
```

8. Installing and testing the CLI out

A CLI is meant to be called from anywhere in the system so let's install it globally by using the following command

```bash
npm install -g .
```

Let's test our CLI by running `tcli` command.

![](https://imgur.com/KRSPgnC.png)

🎉 **Tada**, our CLI is working

# Installing and working with Inquirer

Inquirer is a package that is used to make interactive CLI interfaces. Such as :

![](https://raw.githubusercontent.com/SBoudrias/Inquirer.js/869cd40cf2424414b522a2470a1bc80a713cac8d/assets/screenshots/list.svg)

![](https://raw.githubusercontent.com/SBoudrias/Inquirer.js/869cd40cf2424414b522a2470a1bc80a713cac8d/assets/screenshots/checkbox.svg)

To install run the following command

```bash
npm install inquirer
```

## Adding the boilerplate of inquirer

Here is the boilerplate for inquirer

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

const inquirer = require('inquirer');

inquirer
  .prompt([
    /* Pass your questions in here */
  ])
  .then((answers) => {
    // Use user feedback for... whatever!!
  });
```

## Adding questions

We have to pass questions as objects. Let's add the first question asking about the JS framework.

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

const inquirer = require('inquirer');

inquirer
  .prompt([
    {
      type: 'list',
      name: 'framework',
      message: 'Choose the JS framework which you are using:',
      choices: ['React', 'NextJS', 'Angular', 'Svelte', 'VueJS'],
    },
  ])
  .then((answers) => {});
```

Let's break it down and understand what each part means

- `type` : Inquirer currently has 9 different CLI user interfaces.
  <br>
  <br>
  ![](https://imgur.com/x02hyEd.png)

- `name` : Inquirer returns the answers in the form of an object. For example:

  - If we add `console.log(answers);`, then we would get a result something like this
    <br>
    <br>
    ![](https://imgur.com/4INxAWe.png)

  So here the `name` is the `key` of the object

- `message` : It is the question which is been displayed to the user
- `choices` : These are the options given to user
  <br>
  <br>
  ![](https://imgur.com/pP8d7QA.png)

## Cleaning up the codebase [Optional]

We could create a folder inside the `bin` folder named `utils` and create a file inside the `utils` folder named `questions.js`. In the `questions.js` we can store the questions and import them into the `index.js` file

`utils/questions.js`

```javascript
// This question would be shown at the starting
const questions = [
  {
    type: 'list',
    name: 'framework',
    message: 'Choose the JS framework which you are using:',
    choices: ['React', 'NextJS', 'Angular', 'Svelte', 'VueJS'],
  },
];

// This question would be shown only when the user choose either React or NextJS
const questionsTs = [
  {
    type: 'list',
    name: 'typescript',
    message: 'Does your project use TypeScript?',
    choices: ['Yes', 'No'],
  },
];

module.exports.questions = questions;
module.exports.questionsTs = questionsTs;
```

`index.js`

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

const inquirer = require('inquirer');

const { questions, questionsTs } = require('./utils/questions.js');

inquirer.prompt(questions).then((answers) => {
  // Use user feedback for... whatever!!
});
```

# Adding logic

It's time to add some logic as we are doing creating questions.

Accessing answers to questions are similar to accessing the value of a key from an object. The value of the answer of a specific question is `answers.<name-of-the-question>`

As we are creating starter files, let's use [ShellJS](https://github.com/shelljs/shelljs) to run commands like `git clone`, `mkdir`...

## Installing ShellJS

To install ShellJS run the following command

```bash
npm install shelljs
```

## Working with ShellJS

Let's add a few if and else blocks for logic

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

const inquirer = require('inquirer');
const shell = require('shelljs');

const { questions, questionsTs } = require('./utils/questions.js');

inquirer.prompt(questions).then((answers) => {
  if (answers.framework === 'React') {
    inquirer.prompt(questionsTs).then((answersTs) => {
      if (answersTs.typescript === 'Yes') {
        // If the user has choosen React and want to use TypeScript
      } else {
        // If the user has choosen React but doesn't want to use TypeScript
      }
    });
  } else if (answers.framework === 'NextJS') {
    inquirer.prompt(questionsTs).then((answersTs) => {
      if (answersTs.typescript === 'Yes') {
        // If the user has choosen NextJS and want to use TypeScript
      } else {
        // If the user has choosen NextJS but doesn't want to use TypeScript
      }
    });
  else if (answers.framework === 'Svelte') {
    // If the user has choosen Svelte
  } else {
    // If the user has choosen VueJS
  }
});
```

Let's find some templates for the JS frameworks integrated with TailwindCSS

- [React + TailwindCSS](https://github.com/YashKumarVerma/react-tailwind-template) by [YashKumarVerma](https://github.com/YashKumarVerma)
- [NextJS + TailwindCSS](https://github.com/Neeraj1005/Nextjs-tailwind-template) by [Neeraj1005](https://github.com/Neeraj1005)
- [React + TailwindCSS + TypeScript](https://github.com/GKaszewski/react-tailwind-typescript-template) by [GKaszewski](https://github.com/GKaszewski)
- [NextJS + TailwindCSS + TypeScript](https://github.com/avneesh0612/next-starter) by [avneesh0612](https://github.com/avneesh0612)
- [Svelte + TailwindCSS](https://github.com/jhanca-vm/Svelte-Tailwind) by [jhanca-vm](https://github.com/jhanca-vm)
- [VueJS + TailwindCSS](https://github.com/web2033/vite-vue3-tailwind-starter) by [web2023](https://github.com/web2033)

Thanks a lot to the wonderful people who have made these great templates for the community ✨

To run a `git clone` command, use ShellJS we have just used the `exec` method

```javascript
shell.exec('git clone <repo-link>');
```

Let's fill up the if and else blocks now

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

const inquirer = require('inquirer');
const shell = require('shelljs');

const path = process.cwd();

const { questions, questionsTs } = require('./utils/questions.js');

inquirer.prompt(questions).then((answers) => {
  if (answers.framework === 'React') {
    inquirer.prompt(questionsTs).then((answersTs) => {
      if (answersTs.typescript === 'Yes') {
        shell.exec(`mkdir ${answers.projectName}`);
        shell.exec(
          `git clone https://github.com/GKaszewski/react-tailwind-typescript-template ${answers.projectName}`
        );
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
        );
      } else {
        shell.exec(`mkdir ${answers.projectName}`);
        shell.exec(
          `git clone https://github.com/YashKumarVerma/react-tailwind-template ${answers.projectName}`
        );
        console.log('🛠️  Successfully build the required files');
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
        );
      }
    });
  } else if (answers.framework === 'NextJS') {
    inquirer.prompt(questionsTs).then((answersTs) => {
      if (answersTs.typescript === 'Yes') {
        shell.exec(`mkdir ${answers.projectName}`);
        shell.exec(
          `git clone https://github.com/avneesh0612/next-starter ${answers.projectName}`
        );
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
        );
      } else {
        shell.exec(`mkdir ${answers.projectName}`);
        shell.exec(
          `git clone https://github.com/Neeraj1005/Nextjs-tailwind-template ${answers.projectName}`
        );
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
        );
      }
    });
  } else if (answers.framework === 'Svelte') {
    shell.exec(`mkdir ${answers.projectName}`);
    shell.exec(
      `git clone https://github.com/jhanca-vm/Svelte-Tailwind ${answers.projectName}`
    );
    shell.cd(`${path}/${answers.projectName}`);
    shell.exec(`npm i`);
    console.log(
      '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
    );
  } else {
    shell.exec(`mkdir ${answers.projectName}`);
    shell.exec(
      `git clone https://github.com/web2033/vite-vue3-tailwind-starter ${answers.projectName}`
    );
    shell.cd(`${path}/${answers.projectName}`);
    shell.exec(`npm i`);
    console.log(
      '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
    );
  }
});
```

## Cleaning up the codebase [Optional]

Let's create a new file in `utils` folder named `links.js`. Let's create a hashmap where we will store the GitHub repository links for the template repos.

```javascript
let links = new Map([
  ['React', 'https://github.com/YashKumarVerma/react-tailwind-template'],
  [
    'React-TS',
    'https://github.com/GKaszewski/react-tailwind-typescript-template',
  ],
  ['NextJS', 'https://github.com/Neeraj1005/Nextjs-tailwind-template'],
  ['NextJS-TS', 'https://github.com/avneesh0612/next-starter'],
  ['Svelte', 'https://github.com/jhanca-vm/Svelte-Tailwind'],
  ['Vue', 'https://github.com/web2033/vite-vue3-tailwind-starter'],
]);

module.exports = links;
```

Let's import `utils/index.js` and replace the GitHub template repositories links.

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

const inquirer = require('inquirer');
const shell = require('shelljs');

const path = process.cwd();

const { questions, questionsTs } = require('./utils/questions.js');
const links = require('./utils/links.js');

inquirer.prompt(questions).then((answers) => {
  if (answers.framework === 'React') {
    inquirer.prompt(questionsTs).then((answersTs) => {
      if (answersTs.typescript === 'Yes') {
        shell.exec(`mkdir ${answers.projectName}`);
        console.log('📁 Created a folder for the project');
        shell.exec(`git clone ${links.get('React-TS')} ${answers.projectName}`);
        console.log(`🖨️  Cloned started files into ${answers.projectName}`);
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
        );
      } else {
        shell.exec(`mkdir ${answers.projectName}`);
        console.log('📁 Created a folder for the project');
        shell.exec(`git clone ${links.get('React')} ${answers.projectName}`);
        console.log(`🖨️  Cloned started files into ${answers.projectName}`);
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
        );
      }
    });
  } else if (answers.framework === 'NextJS') {
    inquirer.prompt(questionsTs).then((answersTs) => {
      if (answersTs.typescript === 'Yes') {
        shell.exec(`mkdir ${answers.projectName}`);
        console.log('📁 Created a folder for the project');
        shell.exec(
          `git clone ${links.get('NextJS-TS')} ${answers.projectName}`
        );
        console.log(`🖨️  Cloned started files into ${answers.projectName}`);
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
        );
      } else {
        shell.exec(`mkdir ${answers.projectName}`);
        console.log('📁 Created a folder for the project');
        shell.exec(`git clone ${links.get('NextJS')} ${answers.projectName}`);
        console.log(`🖨️  Cloned started files into ${answers.projectName}`);
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
        );
      }
    });
  } else if (answers.framework === 'Svelte') {
    shell.exec(`mkdir ${answers.projectName}`);
    console.log('📁 Created a folder for the project');
    shell.exec(`git clone ${links.get('Svelte')} ${answers.projectName}`);
    console.log(`🖨️  Cloned started files into ${answers.projectName}`);
    shell.cd(`${path}/${answers.projectName}`);
    shell.exec(`npm i`);
    console.log(
      '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
    );
  } else {
    shell.exec(`mkdir ${answers.projectName}`);
    console.log('📁 Created a folder for the project');
    shell.exec(`git clone ${links.get('Vue')} ${answers.projectName}`);
    console.log(`🖨️  Cloned started files into ${answers.projectName}`);
    shell.cd(`${path}/${answers.projectName}`);
    shell.exec(`npm i`);
    console.log(
      '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
    );
  }
});
```

# Beautification using Chalk

We add colors to the text by using [Chalk](https://github.com/chalk/chalk)

To install chalk, use the following command:

```bash
npm install chalk
```

Let's now import chalk into our `index.js` file

```javascript
const chalk = require('chalk');
```

Chalk have few pre built color methods

![](https://imgur.com/kfl2xzF.png)

Chalk also offers a `hex` method by which you can use any color

Let's add green color to our success output

```javascript
console.log(chalk.green('Hey 👀, I am a green colored text')); // This is how we can add colors by using chalk
```

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

const inquirer = require('inquirer');
const shell = require('shelljs');
const chalk = require('chalk');

const path = process.cwd();

const { questions, questionsTs } = require('./utils/questions.js');
const links = require('./utils/links.js');

inquirer.prompt(questions).then((answers) => {
  if (answers.framework === 'React') {
    inquirer.prompt(questionsTs).then((answersTs) => {
      if (answersTs.typescript === 'Yes') {
        shell.exec(`mkdir ${answers.projectName}`);
        console.log(chalk.green('📁 Created a folder for the project'));
        shell.exec(`git clone ${links.get('React-TS')} ${answers.projectName}`);
        console.log(
          chalk.green(`🖨️  Cloned started files into ${answers.projectName}`)
        );
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          chalk.green(
            '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
          )
        );
      } else {
        shell.exec(`mkdir ${answers.projectName}`);
        console.log(chalk.green('📁 Created a folder for the project'));
        shell.exec(`git clone ${links.get('React')} ${answers.projectName}`);
        console.log(
          chalk.green(`🖨️  Cloned started files into ${answers.projectName}`)
        );
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          chalk.green(
            '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
          )
        );
      }
    });
  } else if (answers.framework === 'NextJS') {
    inquirer.prompt(questionsTs).then((answersTs) => {
      if (answersTs.typescript === 'Yes') {
        shell.exec(`mkdir ${answers.projectName}`);
        console.log(chalk.green('📁 Created a folder for the project'));
        shell.exec(
          `git clone ${links.get('NextJS-TS')} ${answers.projectName}`
        );
        console.log(
          chalk.green(`🖨️  Cloned started files into ${answers.projectName}`)
        );
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          chalk.green(
            '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
          )
        );
      } else {
        shell.exec(`mkdir ${answers.projectName}`);
        console.log(chalk.green('📁 Created a folder for the project'));
        shell.exec(`git clone ${links.get('NextJS')} ${answers.projectName}`);
        console.log(
          chalk.green(`🖨️  Cloned started files into ${answers.projectName}`)
        );
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          chalk.green(
            '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
          )
        );
      }
    });
  } else if (answers.framework === 'Svelte') {
    shell.exec(`mkdir ${answers.projectName}`);
    console.log(chalk.green('📁 Created a folder for the project'));
    shell.exec(`git clone ${links.get('Svelte')} ${answers.projectName}`);
    console.log(
      chalk.green(`🖨️  Cloned started files into ${answers.projectName}`)
    );
    shell.cd(`${path}/${answers.projectName}`);
    shell.exec(`npm i`);
    console.log(
      chalk.green(
        '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
      )
    );
  } else {
    shell.exec(`mkdir ${answers.projectName}`);
    console.log(chalk.green('📁 Created a folder for the project'));
    shell.exec(`git clone ${links.get('Vue')} ${answers.projectName}`);
    console.log(
      chalk.green(`🖨️  Cloned started files into ${answers.projectName}`)
    );
    shell.cd(`${path}/${answers.projectName}`);
    shell.exec(`npm i`);
    console.log(
      chalk.green(
        '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
      )
    );
  }
});
```

# Publishing it to npm 🚀

We have successfully completed building our CLI 🥳. Let's now deploy it to npm, so that other developers can use our CLI.

## Creating a npm account

Go over to [npmjs.org](https://www.npmjs.com/) and create an account and make sure that you are verifying it as well

## Unique package name

npm packages have unique names. npm doesn't allow publishing a package with a name that is already taken. Go over to [npmjs.org](https://www.npmjs.com/) and check whether your package name is taken or not.

`tailwindcli` is already taken by [this](https://www.npmjs.com/package/tailwindcli) package. So I have to change the name to `tailwindcsscli`

### Changing name of the package

If your package is unique and is not taken, skip this step, if it isn't then follow along with this step.

1. Open `package.json` file
2. Change the value of the key `name` to a unique name, in my case I am changing it to `tailwindcsscli`

## Adding keywords

Let's add a few keywords related to our package. As we have built a CLI during this tutorial, let's have the following as keywords :

- cli
- tailwindcss
- nodejs

## Adding license

Check out [license-templates](https://github.com/licenses/license-templates) GitHub repository for license templates which you could use in your project. In my case, I am using MIT license

## Adding repository link

If you have a repository on any git provider, such as GitHub, GitLab, you could link to that in a new entry named `repository` with the keys as `type` and `url` and the values as `git` and `git+<your-git-repo-link>.git` respectively. It would look something like this

```json
"repository": {
  "type": "git",
  "url": "git+<your-git-repo-link>.git"
}
```

In my case, the repo link is https://github.com/Kira272921/tailwindcsscli. So it would look something like this

```json
"repository": {
  "type": "git",
  "url": "git+https://github.com/Kira272921/tailwindcsscli.git"
}
```

### Adding link to bugs reports

Let's add the link to the site/place where the users do report bugs about our package. Generally, it would be the link to the issues page in the GitHub repository

```json
"bugs": {
  "url": "https://github.com/Kira272921/tailwindcsscli/issues"
}
```

### Adding link to the homepage

Let's add the link to the homepage of our npm package. Generally, it would be the link to the README link of the GitHub repository

```json
"homepage": "https://github.com/Kira272921/tailwindcsscli/issues#readme"
```

## Login into your npm account via npm CLI

Let's now login in to our npm account via npm CLI so that we can publish our package to npm. To login into your npm account, run the following command and enter the correct credentials.

```bash
npm login
```

## Publishing your npm package

Let's now publish our npm package by using the following command

```bash
npm publish
```

😱 Oh no! I got an error

![](https://imgur.com/422d1uG.png)

Let's change the name of our package accordingly and publish using the command recommended. My `package.json` looks something like this now

```json
{
  "name": "@kira272921/tailwindcsscli",
  "version": "1.0.0",
  "description": "A CLI for generating starter files for different JS frameworks with tailwindCSS pre-installed",
  "main": "./bin/index.js",
  "bin": {
    "tcli": "./bin/index.js"
  },
  "scripts": {
    "start": "node ./bin/index.js"
  },
  "keywords": ["cli", "tailwindcss", "nodejs"],
  "author": "Kira272921",
  "license": "MIT",
  "dependencies": {
    "inquirer": "^8.2.0",
    "shelljs": "^0.8.4"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/Kira272921/tailwindcsscli.git"
  },
  "bugs": {
    "url": "https://github.com/Kira272921/tailwindcsscli/issues"
  },
  "homepage": "https://github.com/Kira272921/tailwindcsscli/issues#readme"
}
```

Let's try publishing it now again by using the following command

```bash
npm publish --access=public
```

Fingers crossed 🤞. Yay! We have successfully published our CLI to npm 🥳

![](https://imgur.com/DIGgZ0Z.png)

# The end

The code for this tutorial is available on Github
https://github.com/Kira272921/tailwindcsscli

That's for this blog folks. Hope that you have learned something new from this blog post. Meet y'all in the next blog post 👋.