Writing a CLI with Gluegun

Morgan Laco
Red Shift
Published in
5 min readApr 5, 2017

--

Here at Infinite Red we video conference a LOT! Why? Because everyone lives and works somewhere different! We’re a completely remote company. We use a video conference service called Zoom to stay connected with coworkers and clients.

With Zoom, you can have persistent online meeting locations called rooms, each with its own permanent url. These urls aren’t easy to remember, so we created a browser tool to connect to these rooms. In the browser tool, each room is given an alias (e.g. “Maple”) and when you click on an alias, you are connected to the corresponding room.

This works great, when we want to have a video chat with someone, we say “let’s meet in Maple”, and they’ll open the browser tool, click on Maple, and poof! You’re in a meeting together. But some people (like myself) like to do everything from the command line; it’s quicker! So I decided to build a CLI (command line interface) to bring the aliasing functionality of the browser tool to the command line. I used Gluegun to build this tool, which I called “Leaf CLI”. (By the way, we also used Gluegun to create Ignite!) In this article, I’ll explain how I did it.

Setting Up

To get started, I created a new npm project with npm init. Then I added Gluegun with npm i gluegun --save. Then in my index.js, I require, instantiate, and run Gluegun:

module.exports = async function () {
const { build } = require(‘gluegun’)
const runtime = build()
.brand(‘leaf_plugin’)
.loadDefault(`${__dirname}/plugins/leaf_plugin`)
.createRuntime()
const result = await runtime.run()
}

You may have noticed that this is using a slightly older version of EcmaScript. That’s because Node doesn’t support the latest bleeding edge of ES2016. Instead of import we use require and instead of export default we usemodule.exports = <the exported async function> .

The build method in Gluegun creates a runtime for us. It has a host of methods that you use to configure your CLI. Here I used loadDefault to load my plugin: leaf_plugin. I could have called this leaf_cli to match the npm project name, but I find it confusing when things share names and plugin is more accurate anyway, since it’s not a working CLI on its own.

loadDefault makes the leaf_plugin and all its commands available in the CLI. If you include multiple plugins then you can load at most one with
loadDefault, and the rest must be loaded with load. loadDefault tells your CLI to assume you want to use that plugin unless another is specified. There is also loadAll and some options you can use to control what plugins are loaded, but I won’t get into those here.

The Plugins

With the CLI set up and my plugin included, now it is time to add some commands to my plugin. Commands go in a ./commands directory of the plugin folder. In this case, it’s ./plugins/leaf_plugin/commands/. In leaf_cli’s plugin directory structure, this will give me commands invite, join, list, reserve, and setName. nameConfig.js and roomIds.js are utility files.

.
└── plugins
└── leaf_plugin
├── checkRoom.js
├── commands
│ ├── invite.js
│ ├── join.js
│ ├── list.js
│ ├── reserve.js
│ ├── rooms.js
│ └── setName.js
├── nameConfig.js
└── roomIds.js

For each additional plugin, you add a directory (whose name matches that of the plugin) containing another directory named commands, in which you place your commands. Note that if you’re creating a CLI with a single plugin you don’t need to include the plugins or titularly named (e.g.leaf_plugin) directories. Instead, you can place commands at the root of your project. I could have done this with Leaf CLI, but I wanted to illustrate the more general approach.

The first command I wrote was join; let’s take a look at it. Here’s the
command in its entirety, but I’ll break it down piece by piece too:

module.exports = async function (context) {
const { print, http, system, parameters } = context
const { info, warning, success } = print
const { getAndSetName, nameFromConfig } =
require(‘./../nameConfig’)
const { roomIds } = require(‘./../roomIds’)

room = parameters.second
user = parameters.third

if (user==null && nameFromConfig(context)==null) {
await getAndSetName(context)
}

name = nameFromConfig(context)
const roomId = roomIds[room]

if (roomId != null) {
const api = http.create({
baseURL: “http://leaf.infinite.red”,
headers: {‘Accept’: ‘text/html’},
maxRedirects: 0
})

const request = ‘/join/’ + room + ‘/’ + name
const response = await api.get(request)
system.run(“open zoommtg://zoom.us/join?confno=” + roomId)
} else {
warning(“That’s not a valid room”)
}
}

Now let’s break it down:

const { print, http, system, parameters } = context

First, I extract some tools from context, which Gluegun provides to plugin
commands. context is “packed with goodies”, as its author Steve Kellock would say. Those goodies include a bunch of third-party libraries that offer widely applicable, generic features, such as operating on files, interacting with APIs, command-line arguments and prompts. We’re about to see some of these in use in the join command.

room = parameters.second
user = parameters.third

Next I grab the command line arguments using parameters, one of the properties provided by Gluegun. The syntax of join is: leaf join <room> <user>.

const { getAndSetName, nameFromConfig } = require(‘./../nameConfig’)
// …

if (user==null && nameFromConfig(context)==null) {
await getAndSetName(context)
}

name = nameFromConfig(context)

Here, I’m using one of the utility files I mentioned earlier to get the user’s name. Leaf CLI stores the user’s name as configuration, so it checks both the
command line argument and the config file for it. If the user’s name isn’t stored yet, then the function getAndSetName(context) will prompt the user for it. I’m passing along context because it contains a third-party library that I’ll use for the prompt. Let’s peek inside nameConfig.js to see how that works.

// nameConfig.jsconst getAndSetName = async function(context) {
const { prompt } = context
prompt.question(‘name’, ‘What is your name?’)
return prompt.ask(‘name’)
.then(function(answer){
setName(context, answer.name)
})
}

As you can see, I extract prompt from context, then use it to prompt the
user for their name. setName does what it says on the tin. We won’t keep going down this particular rabbit hole; back to join.js!

// ...
const api = http.create({
baseURL: “REDACTED”,
headers: {‘Accept’: ‘text/html’},
maxRedirects: 0
})

const request = ‘/join/’ + room + ‘/’ + name
const response = await api.get(request)
system.run(“open zoommtg://zoom.us/join?confno=” + roomId)
// ...

After validating the room command line argument, we use yet another goody from Gluegun’s context, http, to make an http request. This particular request updates the browser version of Leaf to show the user’s presence. Finally, we use the last Gluegun goody, system, to open the Zoom app and join the room.

Here’s how it looks in action:

Leaf asks for my name, then joins the Zoom room

Conclusion

That covers the basics of Gluegun! It makes it quite painless to create new CLIs using existing plugins. Let us know if you find it useful in your own CLIs!

About Morgan

Morgan is a software engineer focusing on web projects at Infinite Red. She likes to use Rails and Phoenix. You can catch future posts by Morgan on the Red Shift publication. You can follow her on Medium and GitHub.

--

--