fizzy-cli/
├── cmd/fizzy/ # Main entrypoint
├── internal/
│ ├── client/ # Legacy HTTP client (upload, download, multipart, migrate)
│ ├── commands/ # Command implementations
│ ├── config/ # Configuration management
│ ├── errors/ # Error handling and types
│ └── render/ # Output rendering (styled, markdown, columns)
├── e2e/ # Go integration tests
├── skills/ # Agent skills
└── .claude-plugin/ # Claude Code integration
Commands use the fizzy-sdk (github.com/basecamp/fizzy-sdk/go/pkg/fizzy) for API access:
getSDK()returns*fizzy.AccountClient— account-scoped SDK client for most commandsgetSDKClient()returns*fizzy.Client— root client for account-independent operations (e.g. identity)getClient()(deprecated) returnsclient.API— legacy client, used only for file upload, download, multipart PATCH, and board migration
SDK service methods return typed structs (e.g. *generated.Board, []generated.Card). Helper functions convert to the any types the output layer expects:
normalizeAny(data)— JSON-round-trips any value (typed structs,json.RawMessage, maps) tomap[string]any/[]map[string]anyjsonAnySlice(pages)— converts[]json.RawMessagefromGetAll()pagination to[]map[string]anyconvertSDKError(err)— converts SDK errors to*output.ErrortoSliceAny(v)— normalizes[]map[string]anyor[]anyto[]anyfor iteration
Account can be a slug or numeric ID.
API documentation: https://github.com/basecamp/fizzy/blob/main/docs/API.md
Key endpoints used by the CLI:
/boards.json- List boards/cards.json?board_ids[]=<id>- Cards on a board/cards/{number}.json- Show card by number/cards.json?terms[]=<query>- Search cards by text
Important: Cards use NUMBER for CLI commands, not internal ID. fizzy card show 42 uses the card number.
make build # Build binary to ./bin/fizzy
make test-unit # Run Go unit tests (no API required)
make test-e2e # Run e2e tests (requires credentials)
make test-run NAME=TestBoardCRUD # Run a specific testRequirements: Go 1.26+, API credentials for e2e tests.
Tests use SetTestModeWithSDK(mock) which creates an httptest server backed by MockClient:
mock := NewMockClient()
mock.GetResponse = &client.APIResponse{Data: map[string]any{"id": "1"}}
SetTestModeWithSDK(mock)
SetTestConfig("token", "account", "https://api.example.com")
defer resetTest()mock.OnGet(path, resp)— route-specific GET responsesmock.WithGetData(data)— set default GET response datamock.WithListData(data)— set GetWithPagination response (used as fallback for GET when GetResponse is default empty map)- JSON round-trip through httptest converts
int→float64— usefloat64(n)in assertions
E2E environment variables:
FIZZY_TEST_TOKEN- API token (required)FIZZY_TEST_ACCOUNT- Account slug (required)FIZZY_TEST_USER_ID- User ID for user tests (optional)
The CLI reads config from multiple sources with this priority:
- CLI flags (
--token,--profile,--api-url, and--boardon the commands that accept it — see below; a given--boardbeats every configured default) - Environment variables (
FIZZY_TOKEN,FIZZY_PROFILE,FIZZY_API_URL,FIZZY_BOARD) - Named profile settings (base URL, board from
~/.config/fizzy/config.json) - Local project config (
.fizzy.yaml) - Global config (
~/.config/fizzy/config.yamlor~/.fizzy/config.yaml)
FIZZY_ACCOUNT is accepted as a deprecated alias for FIZZY_PROFILE.
--board is command-scoped, not global. fizzy --board X ... fails with
unknown flag; it is declared per-command, by nineteen of them across the activity,
board, card, column and webhook groups. So fizzy card list --board X works while
fizzy --board X card list does not. Where it is accepted it sits at the top of the
ladder above: defaultBoard returns the flag value before consulting FIZZY_BOARD or
config. What happens when you omit it varies by command:
- Required —
requireBoardfalls back toFIZZY_BOARDor theboardkey in config, and errors if neither is set. Seventeen commands:board accesses,board closed,board postponed,board stream,card create,column list,column show,column create,column update,column delete, and all sevenwebhooksubcommands. - Defaulted —
card listusesdefaultBoard, the same fallback without the error; an empty board just goes unset. - Optional filter —
activity listhas no fallback at all. The board query parameter is added only when the flag is given, soFIZZY_BOARDhas no effect there.
Token-based via personal access tokens. Run fizzy setup for interactive configuration or fizzy auth login to save a token directly.
make check runs fmt-check vet lint tidy-check race-test. There is no surface-check
in that list, but the surface gate still runs: race-test is
go test -race -count=1 ./internal/..., which includes
internal/commands.TestSurfaceSnapshot.
SURFACE.txt at the repository root is a golden snapshot of the CLI's structure --
CMD, SUB, FLAG and ARG records -- and that test compares against it. It does not
capture Short/Long help text or flag descriptions, so rewording help leaves the
snapshot green. So a change to the CLI surface fails
both make check and CI until the snapshot is refreshed. Regenerate with
make surface-snapshot (or run the gate alone with make surface-check) and commit
the result in the same PR as the surface change.