How to build an interactive CLI tool with NodeJS
Tags: node, cli
Published: 2019-10-28 21:51:20

---
title: How to build an interactive CLI tool with NodeJS
published: true
description: 
tags: Node, CLI, 
cover_image: https://thepracticaldev.s3.amazonaws.com/i/853ckps2h1g88dxmnyy1.png
---

Command-Line Interfaces(CLI) are great tools for automating repetitive tasks or allowing your computer to take care of the boring stuffs.

Node.js is an interesting choice for building CLIs as you can leverage its vast ecosystem. And thanks to package managers like ``npm`` and  ``yarn``, these can be easily distributed and consumed across multiple platforms. In this post, we'll look at why you might want to write a CLI, and how to use Node.js for it.

The CLI tool we'll be building will be called ``Prtfy``. This will simply set up a `Prettifier` in javascript directory. It will write the `Prettier Config` and prompts the user for their configuration settings.

 Let's get started. 

We'll familiarize ourself with the `npm` modules that will make coding process more simplified.

- **chalk**  - Terminal Styling, i.e colors etc.
- **figlet**  -  For making large letters out of ordinary text.
- **inquirer** - Collects user inputs from the command line.
- **shelljs** -  Portable Unix shell commands for Node.js

Let's jump to the code.

First, we need to setup a Node project. Go to the command line. and type
```javascript

mkdir prtfy
cd prtfy
npm init

```

Follow all the prompts to get the project going. You could also skip all the prompts by simply using`npm init -y` instead of `npm init`. By now you should have a basic Node project with the `package.json` file.

Next, we'll install all the core depedencies listed above. Type the below for this.
```javascript

npm install chalk figlet inquirer shelljs

```

### index.js file

Now create an `index.js` file and import the installed modules.
```javascript

const inquirer = require("inquirer");
const chalk = require("chalk");
const figlet = require("figlet");
const shell = require("shelljs");
```

### Let's plan the CLI
It does a couple of things. 
- Asks user their preffered prettier config.
- Install the prettier locally.
- Writes the config file.
- Configures a pre-commit hook.

With this in mind, let's wrote a pseudo-code for the this.`index.js`

```javascript

const run = async () => {
  // show prtfy introduction
  // install GitHook
  // ask questions
  // create the files
  // configures pre-commit hook
  // show success message
};

run();
```
For ease, we'll have a default configuration. For additional challenge, you can ask all this from the user. Our default config will be stored in a variable.

```javascript

prettierConfig = {
    trailingComma: "es5",
    tabWidth: 4,
    semi: false,
    singleQuote: true,
    useTabs: false,
    printWidth: 100,
    bracketSpacing: true,
    jsxBracketSameLine: false,
    arrowParens: "avoid",
}
```
Let's create these tasks one after the other.

```javascript

// initializes and displays the welcome screen
const init = async () => {
    clear()
    console.log(
        chalk.green(
            figlet.textSync('PrTfY', {
                horizontalLayout: 'full',
            })
        )
    )
}
```
You will notice we have a `clear()` function. This clears the console of any clutter when we run `prtfy`. We need to install the `clear` module. Run

```javascript

npm install clear

```
Let's configure `Git hook` more info an what that means [here](https://prettier.io/docs/en/precommit.html) 

```javascript

const installGitHook = async () => {
    const spinner = new Spinner('Configuring Git Hook..')
    return installHelper(
        'npx mrm lint-staged',
        () => console.log(chalk.green('Git hook configured 👍')),
        spinner
    )
}
```
Next, we need to prompt user for some answers.

```javascript

const askIfJsorTs= () => {
    const questions = [
        {
            name: 'ENV',
            type: 'list',
            choices: ['.Typescript', '.Javascript'],
            message: 'Please, select if this is a JavaScript or Typescript project',
            filter: function (val) {
                return (val === '.Typescript') ? 'ts' : 'js'
            },
        },
    ]
    return inquirer.prompt(questions)
}
```
What ` askIfJsorTs()` does basically is to ask if the user wants to setup prettier for Javascript or Typescript. The filter then returns 'ts' or 'js' based on the selection.

Next, we'll setup the configuration files based on user input. But, to make things more snazzy and realistic. We'll add a spinner to indicate when an asynchronous process like installing prettier or writing files is ongoing and when it's done. Something like below


![ezgif.com-video-to-gif (1).gif](https://cdn.hashnode.com/res/hashnode/image/upload/v1572293568562/7gW_JDpp8.gif)


This is probably the trickiest part as we need to handle some `async` logic elegantly. We'll start by installing the `spinner`. Run

```javascript

npm install clui

```
Also, don't forget to add the spinner to your list of imports. Like so

```javascript

const clui = require('clui')
const Spinner = clui.Spinner

```

Now, we write the `async` logic to help us out with this. We need to `await` the child process installing the prettier and other modules before writing the config files. You can check the [clui](https://www.npmjs.com/package/clui) docs for more info

```
const installHelper = (command, onSuccess, spinner) => {
    return new Promise((resolve, reject) => {
        var process = spawn(command, { shell: true })
        spinner.start()
        process.on('exit', () => {
            spinner.stop()
            onSuccess()
            resolve()
        })
    })
}
```

Install prettier 

```javascript
const installPrettier = async () => {
    const spinner = new Spinner('Installing Prettier...')
    return installHelper(
        'yarn add -D prettier',
        () => console.log(chalk.green('Prettier has been installed! 👍')),
        spinner
    )
}

```

Finally, putting everything toeghter we write a `prettier` file based on all the information we have.
```
#!/usr/bin / env node
const cli = require('clui')
const shell = require('shelljs')
const Spinner = cli.Spinner
const clear = require('clear')
const spawn = require('child_process').spawn
const chalk = require('chalk')
const inquirer = require('inquirer')
const figlet = require('figlet')
const config = require('./config')

// initializes and displays the welcome screen
const init = async () => {
    clear()
    console.log(
        chalk.green(
            figlet.textSync('PrTfY', {
                horizontalLayout: 'full',
            })
        )
    )
}

const installHelper = (command, onSuccess, spinner) => {
    return new Promise((resolve, reject) => {
        var process = spawn(command, { shell: true })
        spinner.start()
        process.on('exit', () => {
            spinner.stop()
            onSuccess()
            resolve()
        })
    })
}

const installPrettier = async () => {
    const spinner = new Spinner('Installing Prettier...')
    return installHelper(
        'yarn add -D prettier',
        () => console.log(chalk.green('Prettier has been installed! 👍')),
        spinner
    )
}

const installGitHook = async () => {
    const spinner = new Spinner('Configuring Git Hook..')
    return installHelper(
        'npx mrm lint-staged',
        () => console.log(chalk.green('Git hook configured 👍')),
        spinner
    )
}

const askIfJsorTs = () => {
    const questions = [
        {
            name: 'ENV',
            type: 'list',
            choices: ['.Typescript', '.Javascript'],
            message: 'Please, select if this is a JavaScript or Typescript project',
            filter: function(val) {
                return val === '.Typescript' ? 'ts' : 'js'
            },
        },
    ]
    return inquirer.prompt(questions)
}

const setPrettierConfig = async () => {
    shell.ShellString(config).to(`.prettierrc.js`)
}
const success = () => {
    console.log(chalk.blue.bold(`Prettier Config completed`))
};

(async () => {
    init()
    await installPrettier()
    await setPrettierConfig()
    await installGitHook()
    const answer = await askIfJsorTs()
    const { ENV } = answer
    if (ENV === 'js') {
        await installPrettier()
        await setPrettierConfig()
    }
    if (ENV == 'ts') {
        const tsConfig = {
            parser: '@typescript-eslint/parser',
            extends: [
                'plugin:react/recommended',
                'plugin:@typescript-eslint/recommended',
                'prettier/@typescript-eslint',
                'plugin:prettier/recommended',
            ],
            parserOptions: {
                ecmaVersion: 2018,
                sourceType: 'module',
                ecmaFeatures: {
                    jsx: true,
                },
            },
            rules: {},
            settings: {
                react: {
                    version: 'detect',
                },
            },
        }

        // install eslint plugins
        const pluginSpinner = new Spinner('Installing plugin configs...')
        await installHelper(
            'npm install eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --dev',
            () => console.log(chalk.green('Eslint Typescript plugin installed 👍')),
            pluginSpinner
        )

        // write eslintrc.js
        await shell.ShellString(tsConfig).to(`.eslintrc.js`)

        // install typescript prettier config
        const tsSpinner = new Spinner('Installing Typescript prettier configs...')
        await installHelper(
            'npm install prettier eslint-config-prettier eslint-plugin-prettier --dev',
            () => console.log(chalk.green('Eslint Typescript prettier configs installed 👍')),
            tsSpinner
        )
    }

    success()
})()

```
To test the CLI, simply run the below inside the root directory
```
node index
```
One last thing, notice the expression on the first line of `index.js` 
```
#!/usr/bin / env node
```
It enables you to simply run `prtfy` inside any directory and have the cli run and install the configurations. I will leave you to do this. You can also publish as an `npm module` if you so wish.