agentleFS
VelimirMuellerVelimirMueller/ai-development-skills/skills/cli/build-cli/SKILL.md

build-cli

Skill7 starsChanged 15 days agoMITView on GitHub

Use when creating or extending a command-line tool in TypeScript, Go, Python or Rust — subcommands, typed args, config precedence, stdout/stderr split, --json, exit codes, NO_COLOR/TTY handling, --dry-run, and spawn-the-binary tests.

  • Installs packages

4 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

3 rules. 0 are common, 2 are distinctive, and 1 is very specific to this project.

  • 0

    Common

    Most of the 3 edited versions we read keep these.

  • 2

    Distinctive

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    No CLI yet → full scaffold (steps 3-7).

    line 31
  2. Very specific:

    Run the live check first (npm view commander version, go list -m -versions github.com/spf13/cobra, PyPI JSON, crates.io), then add …

    line 51
  3. Distinctive:

    Run the real binary, not the handler:

    line 87

“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Build CLI

A CLI is called by people and by scripts. This skill makes the same tool work for both: one entry, subcommands, typed arguments, a fixed config order, data on stdout, diagnostics on stderr, --json with a stable schema, documented exit codes.

Rules with reasons: cli-ux.md. Library choice and structure: cli-patterns.md. Tested code per language: cli-examples.md. Versions: stack-versions.md (verify live first, per version-protocol).

1. Audit current state

Change nothing yet. Read .claude/stack-profile.md (then ~/.claude/stack-profile.md); if absent, detect.

cat .claude/stack-profile.md 2>/dev/null | sed -n '1,30p'
ls package.json go.mod pyproject.toml Cargo.toml 2>/dev/null          # language
grep -nE '"(commander|citty|yargs|cac|clipanion)"' package.json 2>/dev/null
grep -rnE 'spf13/cobra|urfave/cli|alecthomas/kong' go.mod 2>/dev/null
grep -nE '(typer|click|cyclopts|argparse)' pyproject.toml 2>/dev/null
grep -nE '^(clap|argh|lexopt)' Cargo.toml 2>/dev/null
ls src/cli* src/main.* cmd/*/main.go src/*/cli.py 2>/dev/null          # existing entry
grep -rnE 'NO_COLOR|isatty|IsTerminal|isTTY|--json' src cmd internal 2>/dev/null | head

Record: language, existing parser (keep it, see step 3), whether an entry file exists, whether --json, exit codes, config loading, tests-by-spawning are present. A monorepo with several languages: build the CLI in the language of the package that owns it; one question only if two candidates remain.

2. Decide what to do

  • No CLI yet → full scaffold (steps 3-7).
  • CLI exists, contract gaps → apply only the missing seams (step 5). Name each gap you will close.
  • Contract met (stdout/stderr split, exit codes documented, --json schema, spawn tests) → say "already in place" and stop.
  • Existing parser differs from the default (Click, yargs, urfave v2, clap 3) → keep it. Do not migrate inside this task.

3. Detect the track

Pick from languages in the profile or from step 1:

LanguageParserConfigTests
TypeScriptcommander 15, ESM, Node >= 22.18smol-toml + env-pathsVitest spawning dist/cli.mjs
Gocobra, entry cmd/<name>/main.go, Run(args, out, err) int seamgo-toml/v2go test on Run + one built-binary test
Pythontyper, uv_build, entry <pkg>.cli:maintomllib + platformdirspytest + subprocess on python -m <pkg>
Rustclap derive, thiserrortoml + serde + dirsassert_cmd + insta help snapshot

A profile with several languages gets one CLI per package, each following its own row. Lint and format come from the profile's lint_format.

4. Install only what is missing

Run the live check first (npm view commander version, go list -m -versions github.com/spf13/cobra, PyPI JSON, crates.io), then add dependencies with the package manager from the profile.

pnpm add commander env-paths smol-toml && pnpm add -D tsdown vitest   # TypeScript
go get github.com/spf13/cobra github.com/pelletier/go-toml/v2 golang.org/x/term   # Go
uv add typer platformdirs && uv add --dev pytest                       # Python
cargo add clap --features derive,env,wrap_help && cargo add serde --features derive && cargo add toml thiserror dirs   # Rust

TypeScript: runtime libraries go in dependencies (tsdown bundles devDependencies). Python: also set requires = ["uv_build>=<current>,<next minor>"].

5. Generate the seams

Four seams per CLI, copied from cli-examples.md and renamed. Each owns one concern; nothing else touches it.

  1. exit — an enum of codes (0 ok, 1 failure, 2 usage, domain codes from 3) and one typed error carrying a code and a hint.
  2. output — the only writer to stdout/stderr. Decides colour (NO_COLOR, TERM=dumb, TTY per stream) and interactive() (stdin and stdout TTY, CI unset). Secrets are never passed to it.
  3. config — builds a typed Config once: flags > env (<NAME>_<KEY>) > project .<name>.toml > user $XDG_CONFIG_HOME/<name>/config.toml > defaults. Unknown keys and bad values fail with exit 2.
  4. cli (entry) — builds the parser, runs the command, maps CliError to its code, prints through output. The only place that exits.

Commands are thin: parse, call domain code, print through output. Then, for every command:

  • Global flags --json, --quiet, --verbose, --no-color valid before and after the subcommand; --version; exit codes listed in the root --help.
  • --json prints one document {"schema": 1, ...} on stdout, nothing else.
  • Destructive commands: --dry-run (shared planning step, final write skipped) and --yes; prompt only when interactive, else fail with exit 2 naming the flag.
  • Long options first; the same flag means the same thing everywhere.

6. Wire it

  • Entry point: TypeScript "bin": {"<name>": "./dist/cli.mjs"} + tsdown; Python [project.scripts]; Go cmd/<name>/main.go; Rust [[bin]] (default).
  • Task verbs (dev, test, build, check) go through the repo's runner: see set-up-dev-toolchain. Release: release-cli.
  • README: install, quick start, the config precedence table, the exit-code table, the JSON schema version. Link to --help, do not copy it.
  • Completions: cobra and Typer have them built in, clap adds clap_complete; see cli-patterns.md.

7. Verify

Run the real binary, not the handler:

mycli --help; echo "exit=$?"                       # exit=0, lists exit codes
mycli list --json | jq -e '.schema == 1'            # stdout is pure JSON
mycli list --json >/dev/null 2>err.txt; test ! -s err.txt && echo clean   # nothing leaked to stderr
mycli list --bogus >/dev/null; echo "exit=$?"       # exit=2, stdout empty
NO_COLOR=1 mycli delete missing 2>&1 | grep -c $'\x1b'   # 0 escape sequences; the command exits 3
mycli delete x </dev/null; echo "exit=$?"           # not interactive: no hang
hyperfine --warmup 3 'mycli --version'              # under ~100 ms

Then the project check (mise run check, pnpm test, go test ./..., uv run pytest, cargo test) and the spawn tests from cli-examples.md pass. Expected: all exit codes as documented, --help snapshot committed.

References

  • cli-ux.md: the rules and their reasons (streams, exit codes, JSON schema, config order, colour/TTY, dry-run, errors, help, speed, tests).
  • cli-patterns.md: library choice per language, seams, completions, prompts.
  • cli-examples.md: the tested reference CLI in all four languages.
  • ../_shared/stack-versions.md: verified versions.
  • ../../core/_shared/engineering-principles.md, ../../core/_shared/security-baseline.md: seams, secrets.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.