## Context This is a greenfield Go project. There is no existing codebase — the repository currently contains only the Planka OpenAPI 3.0 spec (`planka-api.json`) and OpenSpec configuration. The Planka API uses JWT bearer auth and returns JSON responses. The API follows a nested resource model: Projects → Boards → Lists → Cards, with sub-resources (comments, tasks, labels, memberships) hanging off cards and boards. Primary consumers are AI agents and CI/CD pipelines, not humans at a terminal. This shapes every design decision — JSON-first output, env-var auth, no interactive prompts. ## Goals / Non-Goals **Goals:** - Single statically-linked Go binary with zero runtime dependencies - Predictable, machine-parseable output for every command - Clean mapping from CLI commands to Planka API operations - Minimal abstraction — thin client, not an SDK - Easy to extend with new resources/commands later **Non-Goals:** - Interactive TUI or prompt-based workflows - Caching, offline mode, or local state - WebSocket/real-time integration (Planka supports it, but out of scope) - Admin operations (user management, project/board CRUD beyond read) - Attachment upload/download - Shell completions (can be added later, not v1) ## Decisions ### 1. Project layout — flat `internal/` package vs separate packages **Decision**: Separate packages under project root: `cmd/`, `client/`, `model/`, `output/` **Alternatives considered**: - `internal/` with sub-packages — adds nesting without benefit for a single-binary tool - Single `main` package — doesn't scale past ~10 files **Rationale**: Clear separation of concerns. `cmd/` owns CLI wiring, `client/` owns HTTP, `model/` owns types, `output/` owns formatting. Each package has a single responsibility and can be tested independently. ``` github.com/dcgsteve/pcli ├── main.go → entry point, calls cmd.Execute() ├── cmd/ │ ├── root.go → root command, global flags, env loading │ ├── project.go → project list, get │ ├── board.go → board get, actions │ ├── card.go → card CRUD, move, duplicate, assign, labels │ ├── comment.go → comment CRUD │ ├── task_list.go → task-list CRUD │ ├── task.go → task CRUD │ └── label.go → label CRUD ├── client/ │ ├── client.go → base HTTP client (URL, token, Do, error handling) │ ├── projects.go │ ├── boards.go │ ├── cards.go │ ├── comments.go │ ├── task_lists.go │ ├── tasks.go │ └── labels.go ├── model/ │ └── types.go → all API structs + response envelope ├── output/ │ └── output.go → Print(data, format), envelope wrapping, table rendering ├── logging/ │ └── logging.go → structured logger setup, level control ├── go.mod └── go.sum ``` ### 2. HTTP client — code-generated vs hand-rolled **Decision**: Hand-rolled thin client using `net/http` from the standard library. **Alternatives considered**: - `oapi-codegen` — generates typed client from OpenAPI spec. Produces verbose code, hard to customize, and we only cover ~30 of 96 endpoints. - `go-resty` or similar HTTP wrapper — adds a dependency for minimal benefit over `net/http`. **Rationale**: The API is straightforward REST/JSON. A base client with `Do(method, path, body) (*http.Response, error)` plus per-resource methods is simple, readable, and fully under our control. ~30 endpoints don't justify code generation overhead. ### 3. Base client design ```go type Client struct { BaseURL string Token string HTTPClient *http.Client Logger *slog.Logger } func (c *Client) Do(ctx context.Context, method, path string, body any) (json.RawMessage, error) ``` - `Do` handles: URL construction, bearer token header, JSON marshal/unmarshal, HTTP error → Go error mapping - `Do` logs every request at DEBUG level (`method`, `path`, `status`, `duration`) and errors at WARN - Per-resource files add typed methods: `func (c *Client) GetCard(ctx context.Context, id string) (*model.Card, error)` - API errors (4xx/5xx) are mapped to a structured `APIError` type with status code and message ### 4. Response envelope **Decision**: All CLI output wrapped in `{"data": ..., "error": null}`. ```go type Envelope struct { Data any `json:"data"` Error *string `json:"error"` } ``` - On success: `{"data": , "error": null}` + exit code 0 - On error: `{"data": null, "error": ""}` + exit code 1 - Table format (`--format=table`) bypasses the envelope — prints human-readable rows directly, errors go to stderr ### 5. Auth — env vars with flag override **Decision**: `PLANKA_URL` and `PLANKA_TOKEN` environment variables, overridable with `--url` and `--token` global flags. **Rationale**: Env vars are the standard for CI/CD and agent environments. Flag overrides allow ad-hoc use without modifying the environment. No config file, no login command, no token storage. Precedence: flag > env var. Missing values produce a clear error message and exit code 1. ### 6. CLI framework — Cobra **Decision**: `github.com/spf13/cobra` for command structure. **Alternatives considered**: - `urfave/cli` — simpler but less ecosystem support, no built-in nested subcommands - `kong` — struct-based, clean but less widely adopted **Rationale**: Cobra is the de facto standard for Go CLIs. Nested subcommand support maps directly to our `pcli ` pattern. Well-documented, widely understood. ### 7. `card list --board ` enrichment This command requires multiple API calls: 1. `GET /boards/{id}` → get board details (includes list of lists with IDs and names) 2. `GET /lists/{listId}/cards` for each list → get cards The client method composes these calls and injects `listName` into each card response. This is the only command that does multi-call enrichment — all others are 1:1 with API endpoints. ### 8. Model types — partial structs **Decision**: Model structs include all fields from the API schemas that are relevant to our scoped operations. Fields use `json` tags and pointer types for nullable/optional fields. **Rationale**: We don't need every field from every schema. Types are defined once in `model/types.go` and shared by both `client/` and `cmd/`. Using `json.RawMessage` for truly dynamic fields (like `Action.data`). ### 9. Pagination — cursor-based with auto-fetch The Planka API uses cursor-based pagination on 4 list endpoints: - `GET /boards/{boardId}/actions` → `beforeId` param - `GET /cards/{cardId}/actions` → `beforeId` param - `GET /lists/{listId}/cards` → `before` param - `GET /cards/{cardId}/comments` → `beforeId` param **Decision**: Build pagination into the client layer from the start. List methods accept an optional limit and automatically page through results. - `--limit N` flag on all list commands (actions, cards, comments). When set, fetch stops after N total items. When omitted, fetch all pages. - Client list methods accept a `limit` parameter (0 = no limit). ```go // Client list methods follow this pattern: func (c *Client) ListCardComments(ctx context.Context, cardId string, limit int) ([]model.Comment, error) { var all []model.Comment var beforeId string for { page, err := c.listCardCommentsPage(ctx, cardId, beforeId) if err != nil { return nil, err } all = append(all, page...) if len(page) == 0 { break } if limit > 0 && len(all) >= limit { all = all[:limit] break } beforeId = page[len(page)-1].ID // cursor = last item's ID } return all, nil } ``` **Rationale**: Cursor-based pagination is easy to get wrong if bolted on later — callers assume they get complete results. Building it in from day one means every list command returns complete data by default, while `--limit` gives agents control over response size and latency. The loop terminates when a page returns empty results or the limit is reached. Logging at DEBUG level tracks each page fetch so pagination behavior is observable. ### 10. Structured logging **Decision**: Use Go's standard library `log/slog` for structured logging. **Alternatives considered**: - `zerolog` — fast, but an external dependency for something `slog` handles well - `logrus` — widely used but effectively in maintenance mode; `slog` is the successor - No logging — makes debugging agent/CI failures opaque **Rationale**: `slog` is in the standard library (Go 1.21+), produces structured JSON or text output, and has zero dependencies. Perfect fit for a tool consumed by agents. **Log levels**: - `DEBUG` — HTTP request/response details, pagination page fetches, config resolution - `INFO` — command execution summary (what resource, what action) - `WARN` — non-fatal issues (e.g., unexpected API response fields) - `ERROR` — failures that cause non-zero exit **Control**: `--log-level` global flag (default: `WARN`). Agents can set `--log-level=debug` for full visibility. Logs go to **stderr** so they never pollute the JSON output on stdout. ```go // In root.go var logLevel string // global flag: --log-level func initLogger() *slog.Logger { var level slog.Level level.UnmarshalText([]byte(logLevel)) return slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: level})) } ``` ## Risks / Trade-offs - **API version coupling** → The client is built against Planka v2.0 API. Breaking API changes require pcli updates. Mitigation: pin to known API version, document compatibility. - **Pagination completeness** → Auto-fetching all pages could be slow for very large result sets (e.g., thousands of actions). Mitigation: `--limit N` flag available on all list commands to cap results. - **Multi-call `card list --board`** → Slower than a single API call, N+1 pattern (1 board call + N list calls). Mitigation: acceptable for typical board sizes (< 20 lists). Could parallelize list fetches if needed. - **No retry/backoff** → Network errors fail immediately. Mitigation: acceptable for v1. Agents can implement their own retry logic. Can add later with minimal changes to `client.Do`. - **Bearer token in env var** → Token visible in process environment. Mitigation: standard practice for CI/CD tools; better than on-disk storage or passing credentials.