Some background
I have worked for Higharc for just over five years now. I was employee #14, and have been here to see us grow to over ten times that size.
I am also an unabashed lover of the command line, and text-based interfaces broadly
speaking. At the time of writing, Iโm one of two vim users on the engineering team,
(well, neovim, Iโm not that allergic to new things), and if youโre reading this
on my new website, youโve probably noticed the extremely over-the-top keyboard mode
Iโve built into it. You can navigate the entire website without a mouse!
I also lead our DevOps/infratructure team, and have a deep appreciation for good tools that are fun to use, do not get in the way, and meet people where they are at. I love a good high-learning-curve tool, but I also want things to be easy to pick up and donโt force somebody, who has a job to do after all, to pour time and energy into learning things they might not care about or enjoy.
What I observed at Higharc
A few years ago, as our engineering team really started to grow, I noticed a few things:
- Being able to plop arbitrary scripts into a
package.jsonfile is great when you have 20 scripts. When you have 200โฆ different story. - I have seen some version of the same questions (How do I reset my local database? How do I update baselines for this test suite? How do I run the server without live reload? Etc.) asked approximately 1.2 trillion times over the last five years. Donโt check my math on that one.
- Attempting to โnamespaceโ commands with
:(e.g.build:server,build:next,test:unit,test:e2e) helps, but falls apart once you typeyarn test:<tab>and get presented with 120 options. - We had very few conventions around how to build command line tools, beyond most people
following the patterns of dropping TypeScript files into
tools/and setting up apackage.jsonentry that usually looked something like this:
{
"scripts": {
"dev:inspect": "cross-env NODE_OPTIONS=\"--max-old-space-size=12288\" TS_NODE_PROJECT=tsconfig.server.json node --inspect -r ts-node/register server/index.ts"
}
}
Thereโs quite a bit going on in that single command!
- Setting environment variables to pass options to Node.js directly
- Passing flags along to
nodein addition to theNODE_OPTIONSvariable - Pointing to our
server/index.tsfile
And more still that would not be clear to most folks on the team, let alone a brand-new employee:
- What are we
inspect-ing here? - How does this relate to the other
devcommands? Does this start the server in some debug mode? - Why do we need to give this thing 12 GB of memory to work with?
- Will I ever need to run this? If not, why is it mixed in with all the important stuff?
My goals
I had a strong vision in mind:
- Create a single, unified CLI tool that would be the main interface to our monorepo
for all engineers. Relegate
yarnto its core internal commands (install,prepare, etc.) - Migrate every single old script into the new system as a part of the initial rollout (more on this below)
- Enforcement of a few strong conventions:
- Every script must support Linux, macOS, and Windows unless there is good reason not to (e.g. something that will exclusively run on a specific server, but this is rare)
- Every script must export a
runfunction that accepts a typed context object - Every script must accept a
--dry-runflag and behave accordingly, unless explicitly blacklisted - Every script must be testable, either through unit tests or end-to-end tests
- Every script must be documented, with a help screen available through
--helpor by pressing?in the interactive UI mode
- Make adding new scripts easy, with a command to generate a new script, and a simple structure that
only requires dropping a file into the
cli/folder to register it - Provide a first-class interactive experience, where dropping into a folder or sub-folder without
a command specified will present the same searchable, filterable list of commands available at
that level that we do for the main
higharcentry point. - Provide fast, comprehensive tab completion for all commands, flags, and flag values where applicable.
- Provide versioning and a changelog so that folks know when updates are available, and can see what has been changing over time.
Some design principles
This is a highly opinionated tool. I understood from the outuset that not every decision made would resonate with everyone, and I knew Iโd have to spend the first few weeks after release being very responsive to feedback, criticism, and bug reports.
As its sole designer, I felt very strongly about the following principles:
- Searchability / discoverability: Fast tab completion for all command names, flags, and flag values when applicable. Interactive mode as a first class / consistent experience at each โlayerโ of the tool. Docs everywhere.
- Consistency: Documentation must be present. Tools must work with
--dry-runmode. Etc. - Strong guardrails: Scripts must accept typed context/metadata objects, export a
runfunction, be written in a supported language, etc. CLI itself has lots of tests and encourages testability for individual scripts. - Enjoyment for the user: Colorful, interactive, random fun easter eggs hidden in the tool, etc. Life should be cute!
With all of the above in mind, letโs get to the fun part!
The higharc CLI
Two years, a holiday on-call rotation, and a huge burst of motivation later, I was able
to ring in 2026 by announcing the new higharc CLI to our engineering team.
The fruits of my labor
What this tool is built on
I picked a few core technologies to build this tool, taking care to keep dependencies minimal and solidly in our wheelhouse:
- TypeScript, as our entire monorepo is one big TypeScript project, broken up into individual packages / project references. The CLI is no different and conforms to the same tooling and expectations.
yargs, to act as the core command-line argument parser and handler. It provides strong support for sub-commands, tab completion, help screens, and more, and was a good and well-understood foundation to build on top of.- Claude Code running Opus 4.5 to assist me in pulling off the thing that had scared me away from finishing this tool years ago: the one-shot migration of every single old script into the new system. More on why I felt this was necessary below.
Thatโs really it.
Architecture and core features
The tool is best explained by presenting the same overview document I provided to the team to act as a reference. Here it is, slightly modified for public consumption.
Quick start
# Run with yarn
yarn higharc --help
# Interactive mode - browse all commands
yarn higharc
# Run a specific command
yarn higharc db create
yarn higharc migrate up
Setup for Direct Access
To use higharc without the yarn prefix:
yarn higharc setup
This adds the bin/ directory to your shellโs PATH and sets up auto-complete.
After restarting your terminal:
higharc --help
Features
Interactive Menus
Run any command group without arguments to see an interactive menu:
higharc # Show all command groups
higharc db # Show db commands
higharc migrate # Show migrate commands
Navigation:
- Arrow keys to navigate
- Type to search - filter commands by typing (prioritizes command names over descriptions)
Enterto selectEscapeto clear search / go backBackspaceto delete search / go back?to view detailed help for selected commandDto toggle dry-run modeEto open script code in default editor- Page Up/Down for long lists
Custom Aliases
Running higharc config alias allows you to view and set custom command aliases, local to
your project folder and stored in .higharc.yml. Here is a useful example set:
Configured aliases:
tc = typecheck run --all
l = lint check
d = dev app
kill = util kill-port
ch = changelog
a = config alias
ba = benchmark analyze
b = build app
s = start app
cc = debug cache-completions
reset = db reset && migrate up && db seed
ports = dev ports --watch
up = migrate up
e2e = build app --e2e && start app --e2e
bs = build app && start app
tf = deploy terraform
Searchability
Run higharc query scripts and pass in your search terms to find relevant commands.
It searches command names, flags, descriptions, and more, so you can quickly find what you need.
Auto-Discovery
Commands are automatically discovered from the cli/ directory. The directory structure determines the command path:
cli/
โโโ db/
โ โโโ create.ts โ higharc db create
โ โโโ reset.ts โ higharc db reset
โ โโโ seed.ts โ higharc db seed
โโโ migrate/
โ โโโ up.ts โ higharc migrate up
โ โโโ down.ts โ higharc migrate down
โโโ docs.ts โ higharc docs
โโโ setup.ts โ higharc setup
Multi-Language Support
Scripts can be written in:
- TypeScript (
.ts) - recommended - JavaScript (
.js) - Bash (
.sh) - Python (
.py) - requires Python 3 - Go (
.go) - requires Go
Shell Completions
The higharc setup command automatically configures shell completions:
higharc setup # Auto-detects your shell
higharc setup --shell bash # Explicit shell selection
higharc setup --shell zsh
higharc setup --shell fish
higharc setup --shell powershell
This installs:
- PATH configuration so you can run
higharcdirectly (and optionally, anhalias) - Fast cached tab completion for commands and flags (~70ms vs ~2s)
Supported shells: bash, zsh, fish, PowerShell, Windows CMD
Completion & Meta Cache
Completions are pre-compiled to a JSON cache for fast response times. The cache is regenerated automatically when updating or changing branches, but you can manually regenerate it after adding/modifying CLI commands. The same applies to the metadata cache we use for making help docs available without loading all scripts up front.
higharc debug cache-completions
higharc debug cache-meta
Troubleshooting Completions
If tab completion isnโt working, enable debug logging:
export HIGHARC_COMPLETION_DEBUG=1
rm -f /tmp/higharc_completion.log
higharc db <TAB>
cat /tmp/higharc_completion.log
This logs COMP_WORDS, COMP_CWORD, and the completions returned by node to /tmp/higharc_completion.log.
Writing Scripts
Scaffolding
Use the built-in scaffolding command:
higharc scripts new
This interactively creates a new script with the correct structure.
TypeScript
// cli/example/hello.ts
import type { HigharcContext, HigharcScriptMeta } from "../../tools/higharc/types";
export const meta: HigharcScriptMeta = {
description: "Say hello",
docs: `
Detailed documentation that appears in --help.
Can be multi-line.
`,
dangerous: false, // Set true to require confirmation
hidden: false, // Set true to hide from help
aliases: ["hi"], // Alternative command names
flags: {
name: {
type: "string",
alias: "n",
description: "Name to greet",
default: "World",
},
loud: {
type: "boolean",
alias: "l",
description: "Shout the greeting",
default: false,
},
},
positionals: [
{
name: "target",
description: "Who to greet",
required: false,
type: "string",
},
],
examples: ["higharc example hello", "higharc example hello --name Alice", "higharc example hello --loud"],
};
export async function run(ctx: HigharcContext): Promise<void> {
const { log, args, prompt } = ctx;
const name = (args.name as string) || "World";
const greeting = `Hello, ${name}!`;
if (args.loud) {
log.logSuccess(greeting.toUpperCase());
} else {
log.logInfo(greeting);
}
// Interactive prompts (skipped in CI)
if (prompt) {
const again = await prompt.confirm("Say it again?");
if (again) {
console.log(greeting);
}
}
}
Bash
#!/bin/bash
# cli/example/build.sh
# @description Build the project
# @docs Runs the full build pipeline including tests
# @dangerous false
# @flag --clean, -c, boolean, Clean before building
# @flag --target, -t, string, Build target (dev|prod)
# @example higharc example build
# @example higharc example build --clean --target prod
set -e
if [[ "$HIGHARC_FLAG_CLEAN" == "true" ]]; then
echo "Cleaning..."
rm -rf build/
fi
TARGET="${HIGHARC_FLAG_TARGET:-dev}"
echo "Building for $TARGET..."
Script Context
The HigharcContext provides:
interface HigharcContext {
args: Record<string, unknown>; // Parsed flags and positionals
log: {
logInfo(msg: string): void;
logSuccess(msg: string): void;
logWarn(msg: string): void;
logError(msg: string): void;
};
prompt?: {
// undefined in CI
confirm(msg: string): Promise<boolean>;
input(msg: string, defaultValue?: string): Promise<string>;
select<T>(msg: string, choices: Array<{ name: string; value: T }>): Promise<T>;
multiselect<T>(msg: string, choices: Array<{ name: string; value: T }>): Promise<T[]>;
};
projectRoot: string; // Absolute path to repo root
isCI: boolean; // True in CI environments
argv: string[]; // Raw process.argv
dryRun: boolean; // True if --dry-run flag or D key pressed
exec: {
// Dry-run aware command execution
command(cmd: string, opts?: { description?: string }): { status: number; skipped: boolean };
wouldDo(action: string): void; // Log what would happen in dry-run
isDryRun(): boolean;
};
}
Global Flags
--help,-h- Show help for any command--version,-v- Show CLI version--force,-f- Skip confirmation prompts--dry-run- Preview changes without executing--holiday <season>- Override seasonal theme
Seasonal Themes
The CLI automatically displays seasonal branding during holidays:
- New Yearโs (Jan 1-14) - Fireworks and celebration
- St. Patrickโs Day (Mar 16-18) - Green with clovers
- Pride (June) - Rainbow colors and pride flags
- Halloween (Oct 25-31) - Orange/purple with spooky emojis
- Thanksgiving (Nov 20-28) - Fall colors with autumn emojis
- Winter (Dec 17-31) - Red/green with holiday emojis
Use --holiday <season> to preview different themes (newyears, stpatricks, pride, halloween, thanksgiving, winter, default).
Local Settings
Use higharc config to view and modify local CLI settings (stored in .higharc.yml):
# List all settings
higharc config --list
# View a setting
higharc config logoSize
# Set a setting
higharc config logoSize compact
As an example - the compact logo (logoSize compact) displays โhigharcโ with rainbow
colors during Pride month ๐ณ๏ธโ๐, and seasonal colors/emojis for other holidays, but prints
it on a single line to save vertical space.
File structure
tools/higharc/
โโโ cli.ts # Main entry point
โโโ types.ts # TypeScript interfaces
โโโ discovery.ts # Script auto-discovery
โโโ complete.js # Fast shell completion
โโโ runner.ts # Execution context
โโโ prompts.ts # Interactive prompts & menus
โโโ logo.ts # ASCII branding & seasonal themes
โโโ flags.ts # Flag parsing utilities
โโโ helpers.ts # Shared helper functions
โโโ exec.ts # Command execution utilities
โโโ dependencies.ts # Dependency checking
โโโ testing.ts # Test utilities for CLI scripts
โโโ executors/
โโโ typescript.ts
โโโ javascript.ts
โโโ bash.ts
โโโ python.ts
โโโ go.ts
cli/ # Auto-discovered scripts
โโโ db/ # Database management
โโโ migrate/ # Database migrations
โโโ gen/ # Code generation (schemas, types)
โโโ verify/ # Verification checks (CI)
โโโ typecheck/ # TypeScript type checking
โโโ lint/ # Linting commands
โโโ test/ # Test runners
โโโ dev/ # Development utilities
โโโ build/ # Build commands
โโโ turbo/ # Turborepo incremental build commands
โโโ deploy/ # Deployment (terraform, etc.)
โโโ benchmark/ # Performance benchmarks
โโโ make/ # Fun generators (home, chess, music)
โโโ scripts/ # Script scaffolding
โโโ docs.ts # Documentation browser
โโโ setup.ts # Shell setup & completions
โโโ feedback.ts # Submit feedback
โโโ etc...
bin/
โโโ higharc # Direct execution wrapper
If youโve made it to this point, I am so happy you find this as interesting and fun as I do!
Some examples
Below, Iโve included screenshots and videos of the tool in action, to give you a sense of what it feels like to use. There are probably 250 to 300 commands in our monorepo, so this is a tiny slice of what the tool can do!
Finding a test to run
Watching common ports for process information and resource usage
Browsing the toolโs own changelog
Creating a new scriptโฆ with unit tests!
Finding a Markdown document within the codebase
Showing some holiday spirit!
Hopefully you can see how much time and energy went into making this a thorough and fun experience for folks. I had a good time, and am happy to shift away from being a developer of it to a user.
Rollout and reception from the team
As much as I enjoyed building and using this tool, I was nervous about how folks would feel about coming back from vacation and needing to throw every single command theyโve memorized out the window. After all, not everybody is a huge nerd about this stuff in the same way I am, and their reaction might be a bit less ๐ฅฐ and a bit more ๐คฎ. Developer experience might be a part of my job, but it isnโt necessarily theirs!
Pulling off the one-shot migration
Building a slick new tool was honestly the easy part. I am a huge command-line nerd, spend a lot of my life staring at terminals, and have a good sense of what makes a good CLI experience.
For this to work, and fulfill my goal of having it be our main interface to the monorepo, I needed to avoid a situation where the old scripts and this tool co-existed. So, I insisted on a one-shot migration to rip out all of the old tools, make them conform to the new structure, and test every single one through automation and manual effort.
I had my Linux desktop, a MacBook Pro, and Windows 11 in a VM ready to go, to make sure I was not leaving anyone on the team behind with this change. I learned more about PowerShell in two weeks than I ever wanted to in my life!
To soften the blow, I made sure to keep the references to the old scripts and point to the new tool with a clear deprecation warning:
Running an old script within the new system
How it was received
Truly, the reception has been 95% positive. I was overjoyed to see that within the first week or so, folks were engaging with the tool in a variety of ways:
- Writing new scripts! This validated my assumption that if you provide something highly structured and well-documented, people will be able to pick it up and run with it. Off the top of my head, six different people added or modified scripts within the first two weeks.
- Reaching out to me to submit feedback and bug reports. Not staying silent is huge!
- Expressing gratitude that this tool existed now, and that they could discover commands within our monorepo more easily.
Most complaints I observed had to do with rough edges in the tool, such as a platform-specific bug
or a rarely-used command that I didnโt test well enough. The biggest issues came from the tooling
I put around the internal changelog, which came with too many git conflicts and required
constant rebasing on our base branch to make sure your branch was updating and bumping the changelog
version with each change to the cli/ or tools/higharc/ directories. Well-intentioned, but
created too much friction on pull requests. I addressed that feedback by building a fragment-based
changelog instead, where developers can drop in small changelog fragments that get compiled into a full
CHANGELOG.md update separately.
Whatโs next
I would love to take the core of this tool and make it an open-source project, as I am extremely proud of it! It is my love letter to the command line, and the tools Iโve enjoyed using over the years as an engineer.
Within Higharc, itโs a core part of our toolset now, and weโll keep tweaking it and adding to it over the years to come. Weโre growing, so I look forward to observing how much it helps (or doesnโt help) with onboarding new engineers.
Comments