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.json file 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 type yarn 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 a package.json entry 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!

  1. Setting environment variables to pass options to Node.js directly
  2. Passing flags along to node in addition to the NODE_OPTIONS variable
  3. Pointing to our server/index.ts file

And more still that would not be clear to most folks on the team, let alone a brand-new employee:

  1. What are we inspect-ing here?
  2. How does this relate to the other dev commands? Does this start the server in some debug mode?
  3. Why do we need to give this thing 12 GB of memory to work with?
  4. 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:

  1. Create a single, unified CLI tool that would be the main interface to our monorepo for all engineers. Relegate yarn to its core internal commands (install, prepare, etc.)
  2. Migrate every single old script into the new system as a part of the initial rollout (more on this below)
  3. 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 run function that accepts a typed context object
    • Every script must accept a --dry-run flag 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 --help or by pressing ? in the interactive UI mode
  4. 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
  5. 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 higharc entry point.
  6. Provide fast, comprehensive tab completion for all commands, flags, and flag values where applicable.
  7. 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:

  1. 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.
  2. Consistency: Documentation must be present. Tools must work with --dry-run mode. Etc.
  3. Strong guardrails: Scripts must accept typed context/metadata objects, export a run function, be written in a supported language, etc. CLI itself has lots of tests and encourages testability for individual scripts.
  4. 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:

  1. 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.
  2. 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.
  3. 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)
  • Enter to select
  • Escape to clear search / go back
  • Backspace to delete search / go back
  • ? to view detailed help for selected command
  • D to toggle dry-run mode
  • E to 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 higharc directly (and optionally, an h alias)
  • 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:

  1. 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.
  2. Reaching out to me to submit feedback and bug reports. Not staying silent is huge!
  3. 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.