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.