cli.md

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