Skip to content

Latest commit

Β 

History

History
127 lines (98 loc) Β· 6.01 KB

File metadata and controls

127 lines (98 loc) Β· 6.01 KB

Fizzy CLI Development Context

Repository Structure

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
β”‚   β”œβ”€β”€ mcpserver/       # `fizzy mcp` MCP server (catalog/ synced from fizzy-mcp-server)
β”‚   └── render/          # Output rendering (styled, markdown, columns)
β”œβ”€β”€ e2e/                 # Go integration tests
β”œβ”€β”€ skills/              # Agent skills (mirrored to basecamp/skills on release)
└── .claude-plugin/      # Claude Code integration

SDK Architecture

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 commands
  • getSDKClient() returns *fizzy.Client β€” root client for account-independent operations (e.g. identity)
  • getClient() (deprecated) returns client.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) to map[string]any / []map[string]any
  • jsonAnySlice(pages) β€” converts []json.RawMessage from GetAll() pagination to []map[string]any
  • convertSDKError(err) β€” converts SDK errors to *output.Error
  • toSliceAny(v) β€” normalizes []map[string]any or []any to []any for iteration

Account can be a slug or numeric ID.

Fizzy API Reference

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.

Testing

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 test

Requirements: Go 1.26+, API credentials for e2e tests.

Unit Test Patterns

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 responses
  • mock.WithGetData(data) β€” set default GET response data
  • mock.WithListData(data) β€” set GetWithPagination response (used as fallback for GET when GetResponse is default empty map)
  • JSON round-trip through httptest converts int β†’ float64 β€” use float64(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)

Configuration

The CLI reads config from multiple sources with this priority:

  1. CLI flags (--token, --profile, --api-url, and --board on the commands that accept it β€” see below; a given --board beats every configured default)
  2. Environment variables (FIZZY_TOKEN, FIZZY_PROFILE, FIZZY_API_URL, FIZZY_BOARD)
  3. Named profile settings (base URL, board from ~/.config/fizzy/config.json)
  4. Local project config (.fizzy.yaml)
  5. Global config (~/.config/fizzy/config.yaml or ~/.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 β€” requireBoard falls back to FIZZY_BOARD or the board key 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 seven webhook subcommands.
  • Defaulted β€” card list uses defaultBoard, the same fallback without the error; an empty board just goes unset.
  • Optional filter β€” activity list has no fallback at all. The board query parameter is added only when the flag is given, so FIZZY_BOARD has no effect there.

Authentication

Token-based via personal access tokens. Run fizzy setup for interactive configuration or fizzy auth login to save a token directly.

Checks

make check runs fmt-check vet lint tidy-check race-test test-sync-skills. 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.