A minimal, TUI-ready Go CLI template based on the architecture and terminal styling patterns from ndev.
- Cobra command layer (
cmd/) that stays intentionally thin. - Clean layers under
internal/:internal/domain/business logic (no UI dependencies)internal/presentation/Bubble Tea models + renderinginternal/infrastructure/config persistence (Viper + YAML) + secrets (OS keyring)internal/shared/constants, validation, and centralizeduistyling
go run . --help
go run . init
go run . run- Update module path in
go.mod - Rename
rootCmd.Useincmd/root.go(currentlyapp) - Update
internal/shared/constants(app name + config dir) - Update
.goreleaser.yamland.github/workflows/release.yml
Sensitive data (like API tokens) should never be stored in plain YAML config files. This template includes internal/infrastructure/secrets with a 3-tier fallback strategy inspired by GitHub CLI:
When retrieving secrets, the following order is used:
- Environment variables (highest priority)
- Format:
APP_<KEY>(e.g.,APP_API_TOKEN) - Works everywhere (desktop, Docker, CI/CD)
- Explicit override mechanism
- Format:
- OS keyring (secure desktop storage)
- macOS: Keychain
- Windows: Credential Manager
- Linux: Secret Service (GNOME Keyring)
- 3-second timeout to prevent hanging
- Config file (insecure fallback for headless environments)
- Stored in
~/.cli-tool-template/secrets.ymlwith0600permissions - Used when keyring is unavailable (Docker, headless servers)
- A warning is shown when this fallback is used
- Stored in
When storing secrets:
- OS keyring (attempted first on desktop environments)
- Config file fallback (used when keyring unavailable/times out)
- Returns
insecure=trueflag when this happens - UI shows warning: "⚠ API token stored in config file (insecure)"
- Returns
import "github.com/nezdemkovski/cli-tool-template/internal/infrastructure/secrets"
// Store a secret (returns source + insecure flag)
source, insecure, err := secrets.Set(secrets.KeyAPIToken, "sk-abc123")
if err != nil {
// handle error
}
if insecure {
fmt.Println("Warning: Using insecure file storage (keyring unavailable)")
}
// Retrieve a secret (returns value + source + error)
token, source, err := secrets.Get(secrets.KeyAPIToken)
if err != nil {
// handle error
}
fmt.Printf("Token loaded from: %s\n", source) // "environment", "keyring", "config_file"
// Delete a secret (removes from all locations)
if err := secrets.Delete(secrets.KeyAPIToken); err != nil {
// handle error
}For headless environments where the OS keyring is unavailable:
# Preferred: Use environment variables
export APP_API_TOKEN="your-token-here"
./app run
# Alternative: Let it fall back to file storage (insecure)
./app init # Will warn about insecure storage- Desktop: Secrets stored securely in OS keyring (protected by system authentication)
- Servers/Docker: Explicit environment variables (standard practice in 2026)
- Emergency fallback: File storage ensures CLI works everywhere, with clear warnings
This template supports publishing a formula to a tap repo via GoReleaser (brews: in .goreleaser.yaml).
- Create a tap repository:
homebrew-tap - Add a GitHub Actions secret:
RELEASE_GITHUB_TOKEN(PAT with repo access to the tap repo) - Update
.goreleaser.yamlplaceholders:brews[0].namebrews[0].repository.owner/brews[0].repository.namehomepage,description,license
- Add
cmd/mycmd.go(argument parsing + orchestration) - Add
internal/domain/myfeature/(business logic + result structs) - Add
internal/presentation/mycmd.go(Bubble Tea model +Render*Completion)
No business logic should live in cmd/ or internal/presentation/.