Skip to content

MatthewDlr/cli-template

Repository files navigation

template-cli

A batteries-included Bun + TypeScript CLI template with functional error handling, interactive prompts, and zero-compromise tooling.

Important

This is a template. Before starting development, rename every occurrence of template-cli to your own CLI name — in package.json (name, bin, build script), cli.ts (startSection), and this README.md.

Tech Stack

Tool Purpose
Bun Runtime, package manager, test runner, bundler
TypeScript 7 Type safety (native-speed via @typescript/native-preview)
neverthrow Functional error handling — no throws, only Result<T, E>
@clack/prompts Interactive terminal prompts and spinners
picocolors Colored terminal output for intros/outros
zod Runtime schema validation
oxlint Fast Rust-based linter
oxfmt Fast Rust-based formatter

Prerequisites

Quick Start

bun install
bun run dev

Available Scripts

Script Description
bun run dev Run CLI in watch mode (auto-restarts on file changes)
bun run fmt Format all source files
bun run fmt:check Check formatting without writing
bun run lint Lint all source files
bun run lint:fix Lint and auto-fix
bun run typecheck Type-check without emitting
bun run test Run tests with coverage
bun run check Run fmt:check + lint + typecheck + test (CI gate)
bun run build Compile to a single self-contained binary

Project Structure

cli.ts                      # Entry point — reads command name, delegates to src/commands/
src/
  commands/                 # One file per command (mirrors CLI command structure)
    registry.ts             # COMMAND_REGISTRY — maps command names to lazy imports
    help.ts                 # `template-cli help`
    version.ts              # `template-cli version`
  utils/                    # Reusable utilities, one file per feature
    shell.ts                # Bun $ wrapper returning Result<string, string>
    shell.spec.ts           # Tests for shell.ts
  mocks/                    # Shared test factories, reusable across spec files
    shell.mock.ts           # mockShellSuccess / mockShellFailure factories

How to Add a New Command

1. Create src/commands/<name>.ts and export run:

import { ok, type Result } from "neverthrow";

export const description = "Short summary shown in `help` output";

/**
 * Runs the `<name>` command.
 *
 * @param args - Remaining argv after the executable and command name.
 * @returns Ok on success, Err with an error message on failure.
 */
export async function run(args: string[]): Promise<Result<void, string>> {
  // Use helpers from src/utils/logger.ts for all terminal output.
  // Use runCommand() from src/utils/shell.ts for shell/git commands.
  // Use parseArgs({ args, strict: true, options: { ... } }) to parse flags.
  // For subcommands, dispatch on args[0] and pass args.slice(1) to their run().
  return ok(undefined);
}

2. Register it in src/commands/registry.ts:

export const COMMAND_REGISTRY: CommandRegistry = {
  help: () => import("./help"),
  version: () => import("./version"),
  <name>: () => import("./<name>"),
};

3. Run cli <name> — it works immediately.

4. Write tests in src/commands/<name>.spec.ts following the AAA pattern.

The registry is a single, explicit map of lazy imports. It exists so the same code works in dev (bun cli.ts) and in the compiled binary (bun build --compile), where the bundler must see every import statically.

Key Conventions

Convention Rule
Types No any — use unknown and type guards
Assertions No as Type casts — use type guards. as const is the only exception
Errors No throw — return Result<T, E> via neverthrow everywhere
Shell output Always use runShell() from src/utils/shell.ts; never print raw output
Imports Always use the node: protocol for built-ins (node:fs, node:path)
Exports All exported functions require JSDoc
Tests Every exported function needs a .spec.ts file; use the AAA pattern
Flag parsing Each command uses parseArgs({ args, strict: true }) on its own args parameter

Building and Distributing

bun run build

Produces a single self-contained binary (no Bun runtime required on the target machine). To rename the output binary, update the build script in package.json:

"build": "bun build --compile --minify cli.ts --outfile your-cli-name"

About

Opinionated CLI template with Bun

Resources

Stars

Watchers

Forks

Contributors