In this guide you’ll learn how you can start to build a custom CLI on top of the Airbotics API.

The CLI will implement only a few basic functions but you can easily extend this to use as many APIs as you need.

Goals

We’re going to create a CLI for the fictitious ACME robotics company.

By the end of this guide, you’ll have a CLI that can perform the following commands:

# Print a list of robots with:
acme robots list

# Configure logs for a robot with:
acme robots config-logs --enabled=true --robot_id=dev_bot_1

# Print a list of airbotics commands:
acme commands list 

Prerequisites

  • NodeJs installed on your development machine.
  • An Airbotics account with:
    • At least 1 robot created.
    • At least 1 API key created.

Create a new project

We’re going to utilise the oclif framework to speed up the CLI development.

Initialise a new project acme

mkdir ~/airbotics-cli-guide
cd ~/airbotics-cli-guide
npx oclif generate acme

You will see the following prompts. Feel free to use the defaults or change them to suit your needs:

? npm package name (acme) 
? command bin name the CLI will export (acme)
? description (oclif example Hello World CLI)
? author ( @RobAirbotics)
? license (MIT)
? Who is the GitHub owner of repository (https://github.com/OWNER/repo) (RobAirbotics)
? What is the GitHub name of repository (https://github.com/owner/REPO) (acme)
? Select a package manager (Use arrow keys)

When the project finishes initialising open the acme folder in your favourite code editor or IDE.

Creating a topic

Creating a topic is not strictly necessary but it will help as your CLI grows. Remember the goal was to create a CLI with the following convention:

acme [topic] [command]

To achieve this first create a new sub directory for the topic:

mkdir src/commands/robots

Now open the package.json file and look for the topics key within oclif. Replace the default hello world value with:

"topics": {
  "robots": { "description": "Robot operations" }
}

Create the first command

Now we are ready to implement our first command within the list topic.

npx oclif generate command robots/list

The oclif generator command updates your project config and provides a skeleton implementation of the command.

Open up the newly created src/commands/robots/list.ts and replace the file with:

import { Args, Command, Flags } from '@oclif/core';

export default class RobotsList extends Command {

  static description = 'list your robots'

  public async run(): Promise<void> {
    this.log('TODO: Fetch robots')    
  }
  
}

Testing the first command

Now we can check if our new CLI is working as expected:

./bin/dev robots list

You should see the following output:

TODO: Fetch robots

Note: In production when you install the CLI, the package manager will take care of adding the executable to your path so you can run as acme robots list

Great, our CLI is working but we still need to implement the logic to actually fetch the robots.

Implement the command logic

We need to replace our TODO log statement with the logic to actually fetch the robot list.

To do this we need to make an API call. To achieve this we’ll make use of the axios library:

npm install axios

Now replace the run function with the updated code that actually fetches the list of robots:

import axios from 'axios';

public async run(): Promise<void> {
  try {
    const apiKey = 'aak_d3***********************';
    const authHeader = { 'air-api-key': apiKey };
    const response = await axios.get('https://api.airbotics.io/robots', { 'headers': authHeader })
    this.log(response.data);
  } catch (e) {
    this.log('unable to get a list of robots');
    this.exit(1);
  }
}

You will need to update the apiKey variable with an API key from your account.

Under normal circumstances you should never do this and this step is for demonstration purposes only, we will refactor this later in the guide.

Running the CLI again:

./bin/dev robots list

You should now see a list of your robots like this:

[
  {
    id: 'dev_bot_1',
    name: 'Dev Bot 1',
    provisioned: true,
    token_hint: 'art_49b**************************eff',
    agent_version: '2023.08.08',
    online: false,
    online_updated_at: '2023-08-08T13:59:31.909Z',
    created_at: '2023-08-08T12:33:08.904Z',
    vitals: { cpu: 11.1, battery: 100, ram: 92.5, disk: 7.57 }
  },
  {
    id: 'dev_bot_2',
    name: 'Dev Bot 2',
    provisioned: true,
    token_hint: 'art_49b**************************fss',
    agent_version: '2023.08.08',
    online: true,
    online_updated_at: '2023-08-08T14:59:31.909Z',
    created_at: '2023-08-08T15:33:08.904Z',
    vitals: { cpu: 41.1, battery: 34, ram: 92.5, disk: 37.33 }
  }
]

Create a hook

Hooks are very useful when you want to perform operations during certain lifecycle events, for example before or after each command executes.

We have a working CLI but we have hard coded an API key in the source code, which should never be done.

A possible solution to this is to store the API key as an environment variable and check for it’s presence before the CLI executes a command.

Create a new prerun hook.

npx oclif generate hook key_check --event prerun

Open the newly created src/hooks/prerun/key_check.ts and replace the file with:

import { Hook } from '@oclif/core';

export const hook: Hook<'prerun'> = async function (opts) {
  if(process.env.AIR_API_KEY === undefined) {
    process.stderr.write('AIR_API_KEY was not found\n')
    process.exit(1);
  }
}

This hook executes every time before a command is executed and checks your environment for the AIR_API_KEY environment variable. If the environment variable is not found it prints an error and exits.

Running the robots list command again you should see the following output:

./bin/dev robots list

AIR_API_KEY was not found

Update your environment

Open src/robots/list.ts and and replace:

const apiKey = 'aak_d3***********************';

with:

const apiKey = process.env.AIR_API_KEY;

Now the API key will be read from the environment on the development machine rather than source code.

In the terminal you are executing the CLI from set the environment variable:

export AIR_API_KEY="aak_***********************"

Replace the value with your API key.

Implementing the config-logs command

Use the generator to create a new command to configure logs:

npx oclif generate command robots/config-logs

Open up the newly created src/commands/robots/config-logs.ts and replace the file with:

import { Args, Command, Flags } from '@oclif/core';
import axios from 'axios';

export default class ConfigLogs extends Command {
  
  static description = 'configure logs on a robot';

  static flags = {
    enabled: Flags.string({char: 'e', description: 'enable logs', required: true, options: ['true', 'false']}),
    robot_id: Flags.string({char: 'r', description: 'robot id', required: true})
  }

  public async run(): Promise<void> {

    const { flags}  = await this.parse(ConfigLogs);

    const enabled: boolean = flags.enabled === 'true' ? true: false;
    const robotId: string = flags.robot_id
   
    try {
      const apiKey = process.env.AIR_API_KEY;
      const authHeader = { 'air-api-key': apiKey };
      const url = `https://api.airbotics.io/robots/${robotId}/logs/config`;
      const body = { enabled: enabled };
      const response = await axios.patch(url, body, { 'headers': authHeader });
      this.log(response.data);
    } catch (e) {
      this.log('unable to configure logs for robot');
      this.exit(1);
    }
  }
  
}

This code is similar to the list command but with the extra steps of configuring the expected flags and then parsing the values of those flags to send to the API.

Try out the new command:

./bin/dev robots config-logs --enabled=true --robot_id=dev_bot_1

You should now see a confirmation message like this:

{ message: 'You have configured logs for that robot.' }

If you fail to pass in the required flags, the CLI will gracefully print out the error.

Create the second topic

We have created two commands under the robot topic, to finish off we’ll create another topic that can be used to group another set of related commands. Confusingly enough the topic will be called commands, in Airbotics we also have the notion of commands, all will become clear shortly.

Create the new topic:

mkdir src/commands/commands

Open the package.json again and update the topics key within oclif to:

"topics": {
  "robots": { "description": "Robot operations" },
  "commands": { "description": "Command operations" },
}

Implement the final command

The last command we need to implement is the command to list all the created Airbotics commands.

npx oclif generate command commands/list

Open up the newly created src/commands/commands/list.ts and replace the file with:

import { Args, Command, Flags } from '@oclif/core'
import axios from 'axios';

export default class CommandsList extends Command {

  static description = 'list your commands'

  public async run(): Promise<void> {
    try {
      const apiKey = process.env.AIR_API_KEY;
      const authHeader = { 'air-api-key': apiKey};
      const response = await axios.get('https://api.airbotics.io/commands', { 'headers': authHeader })
      this.log(response.data);
    } catch (e) {
      this.log('unable to get a list of robots');
      this.log(e as string)
      this.exit(1);
    }
  }

}

Try out the new command:

./bin/dev commands list

You should be presented with a list of your created Airbotics commands!

Wrapping up

In this product example you have built your own custom CLI!

We covered a lot, but to summarise you have:

  • An understanding of topics.
  • An understanding of commands.
  • An understanding of hooks.
  • A fully working CLI that you can use to:
    • read your API key from your environment.
    • list your robots.
    • list your commands.
    • configure logs on your robots.

You can see all the source code on GitHub, feel free to extend this repo or implement your own CLI from scratch!