Released v1
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
## 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": <result>, "error": null}` + exit code 0
|
||||
- On error: `{"data": null, "error": "<message>"}` + 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 <resource> <action>` pattern. Well-documented, widely understood.
|
||||
|
||||
### 7. `card list --board <id>` 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.
|
||||
Reference in New Issue
Block a user