Learn How to Build CLI Automation Tools with NodeJS

Learn How to Build CLI Automation Tools with NodeJS

Developers love CLI tools! So much so that, I’m sure, we will jump at the chance to do everything on a computer with the comfort of a command-line. And why not do just that? With the help of modern libraries and frameworks, building our own CLI is a matter of a few hours or even minutes. Especially in Node, you can easily build exciting command-line interfaces using the Oclif framework.

In this post, we are going to create a simple CLI tool to help us track our weight over time. It has simple features like adding a new weight record and displaying past records.

Since we are using Oclif to easily complete this task, let’s try to understand exactly what it is.

Developers love automating

Developers love automating


What Is Oclif?

Oclif is a framework used for creating CLI tools in Node. It supports both Javascript and Typescript implementations. Oclif provides a rich set of features to design and implement command-line programs that are easily extendable with plugins and hooks.


Single Command vs Multi Command

We can create two types of command-line tools in Oclif, single and multi command. Single command CLIs provide only one command option similar to ls and curl commands in Linux. Multi command programs support subcommands that proceed the main command. git and npm are good examples of multi command tools.

In this tutorial, we are building a multi command program that supports add and show subcommands to add new weight records and display old records.


Initialize the Project

We can initialize the project using a simple command.

npx oclif multi [project name]

Here, we use npx, the npm package runner, to initialize the project with Oclif. The command multi specifies that our project is a multi command CLI. When you run this command, you will be prompted to enter several details about the project that helps Oclif create initial project files including the package.json.

oclif project generator

oclif project generator

Since we are using Javascript in this project, make sure to enter “No” for the Typescript field.

When this step is completed, you will see that a new directory for our project with the following subdirectories has been created by oclif.

├── README.md
├── bin
│   ├── run
│   └── run.cmd
├── package.json
├── node_modules
├── src
│   ├── commands
│   │   └── hello.js
│   └── index.js
├── test
│   ├── commands
│   │   └── hello.test.js
│   └── mocha.opts
└── package-lock.json

This is the initial CLI Oclif creates for us. We can add new commands to convert it to the program we need. As you can see, our project already has a predefined command called “hello”. We can use the command-line to run this command.

./bin/run hello

If you want to access this application from a global scope using the oclif_cli command (which is the project name of our CLI), use the npm link command. t

npm link

Now, we can run our application on the command-line using oclif_cli command.

Running an oclif application

Running an oclif application


Adding New Commands to Our Application

As I mentioned earlier, our weight tracking tool has two main commands: add and show. The add command lets us add new records of weight and the show command displays past weight records.

When creating these commands, you will come across terms such as flags and arguments. Let’s first clarify what these terms mean.

Flags and arguments

If you have some experience working with command-line programs, you might already know what flags and arguments are.

Flags provide a way to specify options for running a particular command. For example, consider the following command.

npm install -g oclif

Here, we use the flag -g to specify that we need to install Oclif globally. But it’s not necessary to include this flag with every command. You only have to use a flag if you want to activate the option it represents.

Arguments are provided externally, most commonly by the user. In the above npm command, oclif is the user-provided argument. When we pass it as the argument, we are telling the command line to run the npm install command to install the provided package. Similarly, we can accept arguments with our commands and use them as inputs or targets when running the program.

Create the first command of the application

We can use the following command to add new commands to our program.

npx oclif command [command-name]

It creates all the files needed for the new command and updates the README file.

We will add our first command, add, following this format.

npx oclif command add

Adding a new command to the CLI is as simple as that. Now, oclif_cli add command is ready to be used. However, we still have to implement its internal logic. To achieve this, we should change the already created add.js file inside the src directory.

The fully-implemented add command should be able to accept an argument that specifies the weight of the user and record it with a timestamp. So, the code should be able to read a weight argument and save it in a file with the date and time the record was added.

For this, we change the AddCommand class (which is already defined by Oclif) to include the following implementation.

const {Command, flags} = require('@oclif/command')
const Weight = require('../api/weight')
const weight = new Weight()

class AddCommand extends Command {
  
  async run() {
    const {args} = this.parse(AddCommand)
    const newWeight = args.weight
    weight.add(newWeight)
    this.log(`new weight ${newWeight} kg added`)
  }
}

AddCommand.description = "add a new record of weight"

AddCommand.args = [{
  name: "weight",
  description: "current weight in kilograms; insert only the value, omit kg",
  required: true
}]  

module.exports = AddCommand

Here, we have given a description to the add command and defined the argument it accepts. We declare the argument name (weight) and add a description to give the users instruction on how to pass the argument. Since we have defined the argument as “required”, users won’t be able to run the oclif_cli add command without providing a weight value.

The most important part of the implementation is the code inside the asynchronous run function. It is called every time the user runs the add command. It reads the argument passed with the command and uses the add method of the Weight class (which we will implement later) to save the data to a file. Once the saving is successfully completed, it logs a success message to the console.

Learn more about asynchronous operations and promises in JavaScript .

Create the “show” command

Now, let’s create the 2nd command of our program, show. It is used to display weight data already saved in the file on the command line.

We can create the show command in the same way we created the add command.

npx oclif command show

Let’s implement the show command as we did before. The only difference here is that the show command accepts an optional flag. This optional flag allows the users to specify how many past records they want to see.

We can define the flag (named count) like this.

ShowCommand.flags = {
  count: flags.string({char: 'c', description: 'count of past records to be displayed'}),
  help: flags.help({char: 'h'})
}

With the flags.string function that creates our new flag, we can pass a character to use for the flag and a description. When the string function is used to create flag, users can pass an argument with it. For our program, we have declared this argument as required. So, this argument is essential when using the count flag.

You can find out more about Oclif’s different types of flags and accepted options in the oclif official flags documentation .

We are also creating a help flag using Oclif’s in-built flags.help function so that the users can get help on using the command.

The complete showCommand class of our program is like this.

const {Command, flags} = require('@oclif/command')
const Weight = require('../api/weight')
const weight = new Weight()

class ShowCommand extends Command {
  async run() {
    const {flags} = this.parse(ShowCommand)
    const count = flags.count 
    const records = weight.show(parseInt(count))
    for (let i =records.length -1 ; i >= 0; i--){
      let record = records[i]
      this.log(` Date: ${record.date}, Weight: ${record.weight}`)
    }
  }
}

ShowCommand.description = "show past weight records"

ShowCommand.flags = {
  count: flags.string({char: 'c', description: 'count of past records to be displayed'}),
  help: flags.help({char: 'h'})
}

module.exports = ShowCommand

Again, inside the run function, we define what happens when the user runs this command. It parses the flags we have passed (if any) and reads the argument passed with the count flag. It invokes the show method of the Weight class to retrieve the necessary number of past records. Finally, it logs retrieved data to the console in the descending order based on the record created time.

Implement the Weight class

We used two methods from the Weight class in our program in previous implementations. We create this class inside a new directory called api in the src directory. Inside the api directory, we also create a folder named weightTracker to save the JSON file, in which we are storing our weight records. The JSON file is given the name, weights.json.

I won’t go into much detail about how this class is implemented. In abstract terms, what we have done here is this.

  • Read the content of the weights.json file and parse it into an array in the class constructor.
  • Add a new weight record to the weights array and call the saveWeight method to save the new array of records in the add method.
  • Return all or a given number of past records (passed as the count argument) in the show method.
  • Convert the array of objects into JSON and write it to the weights.json file in the saveWeight method.
const fs = require('fs')
const path = require('path')

const weightFile = path.join(__dirname, "weightTracker", 'weights.json')

class Weight{

    constructor(){  
        this.weights = []
        let content = fs.readFileSync(weightFile, {encoding: 'utf-8'})
        if (content){
            this.weights = JSON.parse(content)
        }
    }

    add(weight){
        let date = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '')
        let newWeight = {
            date: date,
            weight: weight
        }
        this.weights.push(newWeight)
        this.saveWeight()
    }

    show(count){
        let len = this.weights.length
        if (count && len>count){
            return this.weights.slice(len - count, len)
        }
        return this.weights
    }

    saveWeight(){
        if (!fs.existsSync(path.dirname(weightFile))){
            fs.mkdirSync(path.dirname(weightFile))
        }
        const records = JSON.stringify(this.weights)
        fs.writeFileSync(weightFile, records, {encoding: 'utf-8'})
    }

}

module.exports = Weight

Test Our Weight Tracking CLI Tool

We have now fully implemented our program. All that is left to do is play around with it and see how it all works together.

Testing our weight tracker application

Testing our weight tracker application

Summary

As we saw in this tutorial, oclif makes task automation super easy, and using on of our favorite programming languages. It handles the boring parts like documentation on its own and let’s us have fun with the actual implementation of the commands. So, I hope that I have convinced you to give Oclif a chance to become your favorite CLI framework in Node. If you dig deeper on this topic, you’ll be able to realize how powerful CLI framework Oclif is.

Next time you plan on automating a boring task, remember to use oclif, and don’t forget to share it with the world by publishing your package on npm .

Thanks for reading!

If you liked what you saw, please support my work!

Juan Cruz Martinez - Author @ Live Code Stream

Juan Cruz Martinez

Juan has made it his mission to help aspiring developers unlock their full potential. With over two decades of hands-on programming experience, he understands the challenges and rewards of learning to code. By providing accessible and engaging educational content, Juan has cultivated a community of learners who share their passion for coding. Leveraging his expertise and empathetic teaching approach, Juan has successfully guided countless students on their journey to becoming skilled developers, transforming lives through the power of technology.