How to build a CLI using NodeJS ๐Ÿ’ป

How to build a CLI using NodeJS ๐Ÿ’ป

ยท

13 min read

Featured on Hashnode

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 ๐Ÿ„

Today we are going to be building a CLI which would generate starter templates with TailwindCSS 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 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
mkdir tailwindcli
  1. Navigate into it
cd tailwindcli
  1. Initializing a NodeJS project
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:

{
  "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"
}
  1. Open bin/index.js file and add this line at the top of the file
#! /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 ๐Ÿš€.

  1. Adding some JS code
console.log('The CLI is working ๐Ÿš€');
  1. 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

npm install -g .

Let's test our CLI by running tcli command.

๐ŸŽ‰ Tada, our CLI is working

Installing and working with Inquirer

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

To install run the following command

npm install inquirer

Adding the boilerplate of inquirer

Here is the boilerplate for inquirer

#! /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.

#! /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.

  • 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

    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

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

// 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

#! /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 to run commands like git clone, mkdir...

Installing ShellJS

To install ShellJS run the following command

npm install shelljs

Working with ShellJS

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

#! /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

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

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

Let's fill up the if and else blocks now

#! /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.

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.

#! /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

To install chalk, use the following command:

npm install chalk

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

const chalk = require('chalk');

Chalk have few pre built color methods

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

Let's add green color to our success output

console.log(chalk.green('Hey ๐Ÿ‘€, I am a green colored text')); // This is how we can add colors by using chalk
#! /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 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 and check whether your package name is taken or not.

tailwindcli is already taken by this 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 GitHub repository for license templates which you could use in your project. In my case, I am using MIT license

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

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

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

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

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

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

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

"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.

npm login

Publishing your npm package

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

npm publish

๐Ÿ˜ฑ Oh no! I got an error

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

{
  "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

npm publish --access=public

Fingers crossed ๐Ÿคž. Yay! We have successfully published our CLI to npm ๐Ÿฅณ

The end

The code for this tutorial is available on Github 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 ๐Ÿ‘‹.