Building a Node.js CLI with TypeScript, packaged and distributed via Homebrew

Sniptt Official
Jun 20 · 5 min read

At Sniptt, most of our experience has been writing TypeScript and JavaScript for Node, so we made the decision to use the same technologies to build out the first few versions of the Snip CLI. Our initial testing has shown, however, that distributing security software exclusively over npm would not sit right with many of our users. While our long-term goal is to port the Snip CLI to Golang (see our first attempt here), we decided to take a pragmatic approach until then and package our JavaScript code as stand-alone executable binaries for both macOS and Linux.

In this tutorial, we’ll show you how to build a simple Node.js CLI with TypeScript, and then package and distribute it via Homebrew.

TLDR; To view a completed setup including workflow automation, take a look at how we’ve done this with our official CLI at 😎.

Project setup

Let’s start by initialising a new Node.js project called hello via npm init. This will create a package.json file where we’ll keep track of our dependencies and scripts.

Next, let’s install TypeScript and the required Node.js typings, and then register our project with the TypeScript compiler. This will create a tsconfig.json with default compiler options.

Let’s update tsconfig.json to match our needs:

NOTE: We’ve selected ES2020 as target because we assume Node.js version 14 or higher is being used.

Lastly, let’s add an entry-point to our CLI and verify that TypeScript compilation works as expected.

Handling commands

Let’s say we want to implement a simple greet command which takes name as a positional argument, and an optional --upper flag. The resulting CLI will work like this:

We will use yargs to help us parse arguments and flags, and scaffold the CLI command handlers. It also comes with a number of useful features for better user experience such as automatically generated help menu, and more.

Let’s create the greet command module.

The greet.ts file will look like this:

Next, let’s update the cli.ts entry-point.

NOTE: The #!/usr/bin/env node shebang character sequence “converts” the JavaScript file into a Node.js command-line script.

Let’s compile our source with TypeScript again and try out the script.

👏 Hooray! Our CLI works as expected.

Building a single executable

Let’s say you want to package your CLI for commercial use (without sources). In this case, you probably do not want to distribute via npm, but rather by making it available for download somewhere.

To package our hello CLI as a single executable, we will use the pkg module from Vercel.

We’ll need to update package.json to add a script that uses the pkg module to build for macOS, and a configuration to tell pkg to include all .js files in the build folder:

NOTE: We need to add the bin entry to correctly package the module with pkg. This also allows users to install the module via npm install -g hello provided it is published on npm, or via npm link locally.

Let’s try if this works as expected.

Awesome! Now, let’s make hello available via Homebrew.

Distributing via Homebrew

The easiest way to do this is to host a repository with a correctly configured formula. The name of the repository must follow the homebrew-[name] convention, so in our case homebrew-hello, which will make it installable via brew install sniptt-official/hello/hello.

Now, we need to host our binary somewhere. We have found that the simplest way is to create a release on GitHub and add the compressed archive as an attachment.

To create the archive, run:

To compute its signature:

We’ll need to take note of the signature as it is required for the Homebrew formula, in our case this is debe8f770becb128c666e64a415be0f6c62db90de4dfad853150e116164a6138.

Next, we’ll create a release and upload the artefact.

After confirming, you should see something like this.

The last step is to create the Homebrew formula in our new repo.

In hello.rb:

NOTE: The URL can be obtained in the Assets section of the published release, and the signature must match the one obtained earlier.

We’ll push our changes and then it’s finally testing time!

Once the formula has been pushed to GitHub, we can install the hello CLI via Homebrew:

And to test:

🎉 Yay, you’ve made it! Congratulations!

Next steps and advanced usage examples

Want to learn more? Visit our Snip CLI project on GitHub to learn more about:

  • Advanced CLI configuration with multi-command support
  • Packaging for Linux
  • Workflow automation with GitHub actions

and more.

Tired of using password managers to share API Keys, database passwords, configuration files, and other secrets with your team?

Try Sniptt, a new kind of secrets manager for developers that lets you share end-to-end encrypted secrets straight from the command line!

Geek Culture

Proud to geek out. Follow to join our 1M monthly readers.