Architecture — knife
High-level overview
user terminal
|
v
knife CLI (cmd/)
|
v
apiclient (internal/apiclient/) — builds authenticated HTTP client
|
v
generated client (internal/client/) — type-safe HTTP calls
|
v
snackbox REST API
knife is a thin command layer. Each command builds a request using the generated client, calls the snackbox API, and renders the response with the output package.
Package descriptions
cmd/
Contains one file per command group, plus root.go and the main.go entry point under cmd/knife/.
root.go— registers global persistent flags (--server,--json), binds them to Viper, and callsconfig.Load()on startup.login.go,logout.go,logout_all.go,refresh.go,change_password.go— top-level auth commands.users.go— theuserscommand group and all its subcommands.me.go— themecommand group.settings.go— thesettingscommand group, including thesettings socialsub-group.docs.go— thecompletionanddocs(man page) commands, provided by Cobra andcobra/doc.
Commands follow a resource-first structure (knife <resource> <verb>) so that shell completion groups related operations together.
internal/apiclient/
Factory functions for the generated HTTP client.
New()— returns an authenticated client that reads the bearer token from Viper/config.NewUnauthenticated()— returns a client with no auth header, used forloginandrefreshwhich do not have a valid token yet.server()— resolves the server URL from Viper and appends a trailing slash. This normalization is required becauseoapi-codegenresolves paths relative to the base URL, and a base without a trailing slash drops its last path segment during URL resolution.
internal/client/
Contains snackbox.gen.go, which is generated from api/openapi.yaml by oapi-codegen. It provides:
- Go structs for every request and response body (
UserInput,SiteSettings, etc.). - A
ClientWithResponsestype with typed methods such asListUsersWithResponse,UpdateSettingsWithResponse, etc.
Do not edit this file manually. Regenerate it with make generate.
internal/config/
Reads and writes the XDG config file using Viper.
Load()— configures Viper with the config file path, sets the env prefixKNIFE_, and enables automatic env binding.Save()— writes the current Viper state to~/.config/knife/config.yaml, creating the directory with mode0700if needed.dir()— resolves the config directory, honoringXDG_CONFIG_HOME.
Config keys are exposed as constants (KeyServer, KeyToken, KeyRefreshToken) to avoid magic strings.
internal/output/
Rendering helpers used by all commands.
JSON(w, v)— encodesvas indented JSON.Table(w, headers, rows)— renders a column-aligned table usingtext/tabwriter.Record(w, fields)— renders a two-column key/value list, also usingtext/tabwriter.
Key design decisions
Resource-first command structure
Commands are grouped as knife <resource> <verb> (e.g. knife users list) rather than knife <verb> <resource>. This groups related operations in shell completion, makes the command tree easier to extend, and matches the mental model of an API resource hierarchy.
Auth operations (login, logout, etc.) are frequent enough and stateful enough that they live at the top level without a resource prefix.
oapi-codegen for type-safe client generation
The snackbox REST API is described in api/openapi.yaml. Rather than writing HTTP calls by hand, knife uses oapi-codegen to generate a fully typed client. This means compiler errors if the API contract is violated and zero manual maintenance of request/response structs.
To regenerate:
make generate
The codegen configuration lives in api/oapi-codegen.yaml.
XDG config
The config file lives at ~/.config/knife/config.yaml (or $XDG_CONFIG_HOME/knife/config.yaml). This is the only state knife writes. Tokens are stored here and never passed as flags or environment variables, to prevent them appearing in shell history or process listings.
Simulated PATCH for settings
The snackbox API (as of v1.0.x) only exposes a PUT /settings endpoint that replaces the entire settings object. knife settings update simulates a partial update by:
- Fetching the current settings with
GET /settings. - Applying only the flags the user explicitly passed (detected via
cobra.Command.Flags().Changed()). - Sending the merged result back with
PUT /settings.
A TODO comment in cmd/settings.go marks the location to update once snackbox v1.1.0 introduces a real PATCH /settings endpoint.
Trailing-slash normalization
oapi-codegen resolves API paths relative to the base URL. If the base URL does not end with a slash (e.g. https://host/api), Go's url.ResolveReference drops the last path segment and produces incorrect URLs. apiclient.server() appends a trailing slash unconditionally to prevent this.
Generated code
internal/client/snackbox.gen.go is generated and must not be edited manually.
To update it after changing api/openapi.yaml:
make generate
The Makefile target runs:
go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen \
-generate types,client \
-package client \
api/openapi.yaml > internal/client/snackbox.gen.go