javascript is awesome for CLIs
- the npm build, publish, share cycle is so easy
npx
allows execution without installation- npm ecosystem is ripe for CLI wrappers, just write the
stdio
handler for your favorite package
npx business card
Let’s create a pointless CLI just to walk thru some best practices and patterns for publishing any node CLI.
Inspired by johnkpaul and searls, we’ll create a npx
enabled business card. The final output might look something like this:
npm init
Start a new node
project and name it whatever you want. You could choose to name it after the executable you want to expose. It’s not necessary but conventions are nice, and it makes your binary more npx
friendly.
mkdir jackboberg
cd jackboberg
npm init -y
At first, let’s get the necessary CLI working.
touch cli.js
chmod +x cli.js
Now open the new file in your favorite editor. I’ve been using spacemacs
recently, but am still very partial to vim
.
#!/usr/bin/env node
console.log('doing business')
You can execute your new CLI like this:
./cli.js
ship it
That’s an entirely functional first release! You just need to modify your package.json
to let npm
know to link your executable and you are off:
{
// ...
"bin": "./cli.js",
// ....
}
By default, npm
will expose your binary using the same name as your package. You can configure this if it’s not what you want, but it will make your executable harder to use with npx
.
Share your new CLI with the world!
npm version patch
npm publish
Now anyone can leverage your new CLI using npx
, without needing to install it locally:
npx $YOUR_PACKAGE_NAME
Put your logic in a separate file
What if you want to tell people more about the business you are doing. Create an object to hold the data for our CLI. I like to keep even small projects organized, so I’ll create a new file in lib
for this.
# ./lib/data.js
module.exports = {
name: 'Jack Boberg',
title: 'Software Engineer',
github: 'jackboberg',
urls: [
'https://jackboberg.info',
'https://studioelsa.se'
]
}
When your CLI needs to do additional work, put that code in index.js
or a local module. Consumers will be able to use your package programmatically, and it gives you a discrete location to do all your stateful business logic. In this case, we will use the main module to format our data as a pretty string.
Let’s get a library that will help make our output a bit more stylish.
npm i prettyjson
We can use this package, or any other useful utility we find on npm, to improve our CLI utility.
# ./index.js
const { render } = require('prettyjson')
const data = require('./lib/data')
const renderOpts = {
dashColor: 'cyan',
keysColor: 'blue',
stringColor: 'white'
}
module.exports = () => render(data, renderOpts)
A small update to our ./cli.js
script and we can leverage our new module.
#!/usr/bin/env node
const pkg = require('..')
console.log(pkg())
Handle stdio in your CLI module explicitly
That’s not bad, but what if someone wants to get the raw JSON. Let’s add a simple flag to modify our scripts behavior.
npx jackboberg -j
Keeping your cli.js
focused on parsing input and producing output is a good idea; it keeps your files small and reasonable. Resist the urge to write any business logic in this file.
#!/usr/bin/env node
const minimist = require('minimist')
const pkg = require('.')
const options = {
alias: { json: 'j' }
}
const argv = minimist(process.argv.slice(2), options)
console.log(pkg(argv))
prettyjson
already includes minimist
as a dependency, so you can add it for ‘free’ and dress up your CLI from there. You should still include it explicitly in dependencies
. Let’s make a small change to our main script to support this new option.
# ./index.js
// ...
module.exports = ({ json }) => json ? JSON.stringify(data) : render(data, renderOpts)
Don’t forget to publish your final npx business card when you are finished adding tons of unnecessary additional features. I made mine include cowsay
, because of reasons.
npm version major
npm publish
Now you have a globally executable script, that can leverage all of npm
, to perform awesome utilites (or silly ideas like this). If you find you are writing a ‘big’ CLI with lots of parameters or sub-commands, you will want more options around how to define your CLI and should consider trying yargs
instead of minimist
.