# CLIs

Choose the smallest CLI shape that will still feel good to use.

## Shape choices

- **One or two flags, server-style binary**: stdlib flags are fine.
- **User-facing command with help/version/completions/subcommands**: use Cobra +
  Fang.
- Do not follow clix/kong guidance from generic Go skills for Amolith projects.
- **Interactive prompts**: add `huh/v2` only when prompts are central, not for a
  single confirmation.
- **Full-screen interaction**: this is a TUI; read `tui.md`.

## Cobra + Fang baseline

Use this layout for real CLIs:

```text
cmd/<binary>/main.go      # constructs root command and calls fang.Execute
cmd/<binary>/<verb>.go    # subcommands
internal/<domain>/...     # domain logic; no CLI globals here
```

Patterns:

- root command has `SilenceUsage` and `SilenceErrors` when Fang handles output
- `context.Background()` enters at `main`, then command contexts carry through
- choose a version policy during setup: Fang/Go build info is fine for
  best-effort CLI display, but exact releases, jj-derived values, MCP metadata,
  package metadata, or tests need project-owned version resolution
- command functions return errors instead of printing and exiting deep in the
  stack
- shell completions, especially dynamic, are worthwhile for anything frequent

## Version reporting

Fang can display values passed with `WithVersion` and `WithCommit`, and it has
best-effort build-info fallback support. Do not rely on that fallback when the
version appears outside CLI help: MCP implementation metadata, package metadata,
release artifacts, tests, and jj-derived versions need project-owned values.

Choose the policy with the user:

- **No exact user-visible version**: do not add version plumbing just for its own
  sake.
- **Best-effort CLI display**: read `runtime/debug.BuildInfo` directly or let
  Fang's fallback handle it when `unknown` is acceptable.
- **Exact release version, jj-derived version/commit, or multiple version
  surfaces**: keep a project-owned resolver and pass the result to Fang, MCP
  server metadata, package tasks, and tests.

For projects that inject exact release values, keep the variable boring:

```go
var version = "dev"
```

Then append an ldflag only in the release/package task:

```toml
[tasks."build:release"]
description = "Build a release binary with an explicit version"
env = { CGO_ENABLED = "0" }
run = '''
version=${VERSION:?set VERSION}
ldflags="{{vars.static_ldflags}} -X main.version=$version"
go build \
  -trimpath \
  -tags '{{vars.static_tags}}' \
  -ldflags "$ldflags" \
  -o {{vars.binary}} \
  {{vars.main}}
'''
```

For variables outside `main`, use the full import path plus variable name, such
as `example.com/project/internal/server.version`, not `main.version`.

This static build shape is for pure-Go CLIs. If a CLI intentionally needs CGO,
keep the same versioning pattern but remove the static-only flags.

## Config

- TOML config: `BurntSushi/toml`.
- Env vars: read at the boundary, validate early, and store in a typed config
  struct.
- Config discovery should be explicit and documented; avoid magical precedence
  unless the project truly needs layering.

## UX rules

- Human text goes to stderr when stdout may be piped.
- Machine output needs an explicit `--json`, `--jsonl`, or equivalent.
- Destructive commands need names and help text that make the effect obvious.
- Interactive commands need a non-interactive path for scripts and CI.
