Use Go Modules: go mod tidy, go test ./...
Use Mage: go tool mage [target] (e.g. go tool mage Build)
- Testing: Use standard
testingandgithub.com/google/go-cmp/cmponly. No frameworks (Testify, Ginkgo, etc.). - Linting: Run
GolangCI-Lintv2. Configure via project-specific.golangci.yml. - Build: Use
way-magefileskill. - Encore: Use
encore-go-*skills. Encore conventions (e.g., globals) take precedence.
- Way Go Style: Use
.agents/skills/way-go-style/SKILL.md - Way Magefile: Use
.agents/skills/way-magefile/SKILL.md - Agents.md: Use
.agents/skills/agents-md/SKILL.md
The CLI is split into two layers to keep credential storage pluggable:
cli/
├── cli.go # Store interface, Credentials, FileStore, Options
└── command.go # NewCommand() — full command tree
cmd/mbz/
└── main.go # Thin wrapper: wires FileStore to XDG paths
cli.Store— interface withRead(any),Write(any),Clear()methodscli.NewCommand(...Option)— builds the Cobra command tree; receives stores via functional options (WithCredentialStore,WithTokenStore)cmd/mbz/main.go— only wiresFileStoreinstances and callscli.NewCommand()
This separation lets consumers embed the CLI in a larger tool or swap the storage backend (e.g. use an in-memory store in tests, or a keychain-backed store) without forking.
The Mercedes-Benz API uses two auth methods depending on the endpoint:
- OAuth2 client credentials — for vehicle management, data services, delta push
- API key — for vehicle specification and images
Both are stored in the credential store. The OAuth2 token is cached separately in the token store.
The CLI can be embedded as a subcommand in a larger tool (e.g. a unified way CLI). Key design rules:
- Never use
cmd.Root()— resolves to the parent CLI's root when embedded, breaking flag lookups. Usecmd.Flags()instead (works for both persistent and local flags). WithHTTPClient— the parent injects an*http.Clientviacli.WithHTTPClient(). The SDK layers (auth, retry) stack on top of the injected client's transport.DebugTransport— exported indebug.gowith a lazyEnabled *boolfield. The parent owns the--debugflag and pointsEnabledat the flag variable. The transport checks the pointer at request time, solving the chicken-and-egg problem (transport constructed before flag parsing).
var debug bool
cmd := cli.NewCommand(
cli.WithCredentialStore(store),
cli.WithTokenStore(tokenStore),
cli.WithHTTPClient(&http.Client{
Transport: &mbz.DebugTransport{
Enabled: &debug,
Next: http.DefaultTransport,
},
}),
)
cmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug logging")Three separate Go modules prevent Cobra/CLI dependencies from leaking into the SDK library:
go.mod # SDK client library (no cobra, no CLI deps)
cli/go.mod # CLI commands (depends on root SDK + cobra)
cmd/mbz/go.mod # Standalone binary (depends on cli module)
Consumers who only need the Go client import the root module without pulling in CLI dependencies.
- Subcommands are organized by entity using
cobra.Group - Flat command structure:
vehicles,services, notvehicles list