Skip to content

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 calls config.Load() on startup.
  • login.go, logout.go, logout_all.go, refresh.go, change_password.go — top-level auth commands.
  • users.go — the users command group and all its subcommands.
  • me.go — the me command group.
  • settings.go — the settings command group, including the settings social sub-group.
  • docs.go — the completion and docs (man page) commands, provided by Cobra and cobra/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 for login and refresh which do not have a valid token yet.
  • server() — resolves the server URL from Viper and appends a trailing slash. This normalization is required because oapi-codegen resolves 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 ClientWithResponses type with typed methods such as ListUsersWithResponse, 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 prefix KNIFE_, and enables automatic env binding.
  • Save() — writes the current Viper state to ~/.config/knife/config.yaml, creating the directory with mode 0700 if needed.
  • dir() — resolves the config directory, honoring XDG_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) — encodes v as indented JSON.
  • Table(w, headers, rows) — renders a column-aligned table using text/tabwriter.
  • Record(w, fields) — renders a two-column key/value list, also using text/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:

  1. Fetching the current settings with GET /settings.
  2. Applying only the flags the user explicitly passed (detected via cobra.Command.Flags().Changed()).
  3. 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