Architecture¶
Project Structure¶
adguard-cli/
├── cmd/adguard-home/main.go # Entry point
├── commands/ # Cobra command definitions (one file per resource)
├── internal/
│ ├── api/client.go # AdGuard Home REST API client
│ ├── config/
│ │ ├── config.go # YAML config management
│ │ └── credentials.go # Keyring + encrypted file credential store
│ ├── errors/errors.go # CLIError with hints
│ ├── output/formatter.go # Table/JSON/YAML output
│ └── cmdutil/util.go # Stderr helpers
├── tools/gendocs/main.go # Auto-generate CLI reference docs
├── docs/ # MkDocs documentation
├── Makefile # Build, test, lint, cross-compile
├── .goreleaser.yml # Release automation
└── .github/workflows/ # CI/CD
Key Design Patterns¶
Constructor Root Command¶
The root command uses NewRootCommand(version, commit, date) returning *cobra.Command. No package-level state for the command tree, making it testable.
CLIError with Hints¶
Every error includes an actionable hint:
errors.ConnectionFailed(url, err)
// Output:
// Error [CONNECTION_ERROR]: cannot connect to AdGuard Home at http://...
// Cause: dial tcp: connection refused
// Hint: Check that AdGuard Home is running. Run 'adguard-home doctor' to diagnose.
stderr/stdout Separation¶
cmdutil.Infof()/cmdutil.Infoln()write to stderr (progress, confirmations)output.Print()/output.PrintJSON()write to stdout (structured data)
This makes piping reliable: adguard-home clients list -o json | jq .
Credential Store Interface¶
type CredentialStore interface {
Get(instance string) (string, error)
Set(instance, password string) error
Delete(instance string) error
}
Two implementations: keyringStore (system keyring) and encryptedFileStore (AES-256-GCM fallback). The NewCredentialStore() factory auto-detects which is available.
API Client¶
internal/api/client.go wraps AdGuard Home's REST API. All endpoints use the /control/ prefix with HTTP Basic Auth. The client handles JSON serialization, error responses (401/403 → auth error, 4xx → API error), and 15-second timeouts.
Adding a New Command¶
- Create
commands/{resource}.go - Define
func new{Resource}Cmd() *cobra.Commandwith subcommands - Add API methods to
internal/api/client.goif needed - Register in
root.go→root.AddCommand(new{Resource}Cmd()) - Use
output.Print(getFormat(), data, headers, rowBuilder)for output - Use
clierrors.WithHint()for user-friendly errors - Run
go run ./tools/gendocs/main.goto update CLI reference