cli.md

  1# CLIs
  2
  3Choose the smallest CLI shape that will still feel good to use.
  4
  5## Shape choices
  6
  7- **One or two flags, server-style binary**: stdlib flags are fine.
  8- **User-facing command with help/version/completions/subcommands**: use Cobra +
  9  Fang.
 10- Do not follow clix/kong guidance from generic Go skills for Amolith projects.
 11- **Interactive prompts**: add `huh/v2` only when prompts are central, not for a
 12  single confirmation.
 13- **Full-screen interaction**: this is a TUI; read `tui.md`.
 14
 15## Cobra + Fang baseline
 16
 17Use this layout for real CLIs:
 18
 19```text
 20cmd/<binary>/main.go      # constructs root command and calls fang.Execute
 21cmd/<binary>/<verb>.go    # subcommands
 22internal/<domain>/...     # domain logic; no CLI globals here
 23```
 24
 25Patterns:
 26
 27- root command has `SilenceUsage` and `SilenceErrors` when Fang handles output
 28- `context.Background()` enters at `main`, then command contexts carry through
 29- choose a version policy during setup: Fang/Go build info is fine for
 30  best-effort CLI display, but exact releases, jj-derived values, MCP metadata,
 31  package metadata, or tests need project-owned version resolution
 32- command functions return errors instead of printing and exiting deep in the
 33  stack
 34- shell completions, especially dynamic, are worthwhile for anything frequent
 35
 36## Version reporting
 37
 38Fang can display values passed with `WithVersion` and `WithCommit`, and it has
 39best-effort build-info fallback support. Do not rely on that fallback when the
 40version appears outside CLI help: MCP implementation metadata, package metadata,
 41release artifacts, tests, and jj-derived versions need project-owned values.
 42
 43Choose the policy with the user:
 44
 45- **No exact user-visible version**: do not add version plumbing just for its own
 46  sake.
 47- **Best-effort CLI display**: read `runtime/debug.BuildInfo` directly or let
 48  Fang's fallback handle it when `unknown` is acceptable.
 49- **Exact release version, jj-derived version/commit, or multiple version
 50  surfaces**: keep a project-owned resolver and pass the result to Fang, MCP
 51  server metadata, package tasks, and tests.
 52
 53For projects that inject exact release values, keep the variable boring:
 54
 55```go
 56var version = "dev"
 57```
 58
 59Then append an ldflag only in the release/package task:
 60
 61```toml
 62[tasks."build:release"]
 63description = "Build a release binary with an explicit version"
 64env = { CGO_ENABLED = "0" }
 65run = '''
 66version=${VERSION:?set VERSION}
 67ldflags="{{vars.static_ldflags}} -X main.version=$version"
 68go build \
 69  -trimpath \
 70  -tags '{{vars.static_tags}}' \
 71  -ldflags "$ldflags" \
 72  -o {{vars.binary}} \
 73  {{vars.main}}
 74'''
 75```
 76
 77For variables outside `main`, use the full import path plus variable name, such
 78as `example.com/project/internal/server.version`, not `main.version`.
 79
 80This static build shape is for pure-Go CLIs. If a CLI intentionally needs CGO,
 81keep the same versioning pattern but remove the static-only flags.
 82
 83## Config
 84
 85- TOML config: `BurntSushi/toml`.
 86- Default config file: read from `$XDG_CONFIG_HOME/<app-name>/config.toml`,
 87  falling back to `~/.config/<app-name>/config.toml` when `XDG_CONFIG_HOME` is
 88  unset.
 89- Env vars: read at the boundary, validate early, and store in a typed config
 90  struct.
 91- Config precedence is flag, then environment variable, then config file.
 92- Config discovery should be explicit and documented; avoid magical precedence
 93  unless the project truly needs layering.
 94
 95## UX rules
 96
 97- Human text goes to stderr when stdout may be piped.
 98- Machine output needs an explicit `--json`, `--jsonl`, or equivalent.
 99- Destructive commands need names and help text that make the effect obvious.
100- Interactive commands need a non-interactive path for scripts and CI.