Released v1
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-09
|
||||
@@ -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.
|
||||
@@ -0,0 +1,35 @@
|
||||
## Why
|
||||
|
||||
Planka is a self-hosted Kanban board application with a REST API, but there is no CLI tool to interact with it programmatically. AI agents and automated workflows need a simple, scriptable interface to manage cards, tasks, and comments without using the web UI. A single Go binary (`pcli`) provides a lightweight, dependency-free tool that can be dropped into any CI/CD pipeline or agent environment.
|
||||
|
||||
## What Changes
|
||||
|
||||
- New Go binary `pcli` providing a Cobra-based CLI to interact with the Planka API
|
||||
- Nested command structure: `pcli <resource> <action> [args] [flags]`
|
||||
- Authentication via bearer token from environment variables (`PLANKA_URL`, `PLANKA_TOKEN`) — no login flow, no on-disk token storage
|
||||
- JSON output by default with consistent `{"data": ..., "error": null}` response envelope
|
||||
- Optional `--format=table` for human-readable output
|
||||
- Read-only access to projects and boards
|
||||
- Full CRUD for cards, comments, task lists, tasks, and labels
|
||||
- Card operations: move, duplicate, assign/unassign users, add/remove labels
|
||||
- Board and card activity log (actions) — read-only
|
||||
- `card list --board <id>` enriches responses with `listName` (resolved from list data)
|
||||
- Lists treated as an implementation detail — not exposed as top-level commands
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `api-client`: HTTP client layer for Planka API — base client with auth, request/response handling, and per-resource methods
|
||||
- `cli-commands`: Cobra command tree — project, board, card, comment, task-list, task, label subcommands with flags and argument parsing
|
||||
- `output-formatting`: Response envelope and format switching — JSON default, table option, consistent error handling
|
||||
- `card-operations`: Card-specific composite operations — move (list + position), duplicate, assign/unassign members, add/remove labels, enriched board-level card listing
|
||||
|
||||
### Modified Capabilities
|
||||
None — greenfield project.
|
||||
|
||||
## Impact
|
||||
|
||||
- **New repository structure**: `main.go`, `cmd/`, `client/`, `model/`, `output/` packages
|
||||
- **Dependencies**: Go module `github.com/dcgsteve/pcli`, Cobra library, standard library HTTP client
|
||||
- **External API**: Depends on Planka v2.0 API (OpenAPI 3.0 spec in `planka-api.json`)
|
||||
- **Environment**: Requires `PLANKA_URL` and `PLANKA_TOKEN` environment variables at runtime
|
||||
@@ -0,0 +1,195 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Base HTTP client
|
||||
The system SHALL provide a base HTTP client that sends requests to the Planka API. The client SHALL construct URLs by joining the configured base URL with the API path. The client SHALL attach an `Authorization: Bearer <token>` header to every request. The client SHALL send and receive JSON (`Content-Type: application/json`). The client SHALL accept a `*slog.Logger` and log every request at DEBUG level with method, path, status code, and duration. The client SHALL log errors at WARN level.
|
||||
|
||||
#### Scenario: Successful API request
|
||||
- **WHEN** the client sends a request to a valid endpoint
|
||||
- **THEN** the response body SHALL be returned as parsed JSON
|
||||
- **AND** the request SHALL include the bearer token header
|
||||
- **AND** a DEBUG log entry SHALL be emitted with method, path, status, and duration
|
||||
|
||||
#### Scenario: API returns error status
|
||||
- **WHEN** the API responds with a 4xx or 5xx status code
|
||||
- **THEN** the client SHALL return an `APIError` containing the HTTP status code and response message
|
||||
- **AND** a WARN log entry SHALL be emitted
|
||||
|
||||
#### Scenario: Network failure
|
||||
- **WHEN** the HTTP request fails due to a network error (connection refused, timeout, DNS failure)
|
||||
- **THEN** the client SHALL return a Go error wrapping the underlying network error
|
||||
|
||||
### Requirement: Authentication from environment
|
||||
The system SHALL read `PLANKA_URL` from the environment to determine the API base URL. The system SHALL read `PLANKA_TOKEN` from the environment to determine the bearer token. Global flags `--url` and `--token` SHALL override the corresponding environment variables. Flag values SHALL take precedence over environment variables.
|
||||
|
||||
#### Scenario: Auth from environment variables
|
||||
- **WHEN** `PLANKA_URL` and `PLANKA_TOKEN` are set in the environment
|
||||
- **AND** no `--url` or `--token` flags are provided
|
||||
- **THEN** the client SHALL use the environment variable values
|
||||
|
||||
#### Scenario: Flag overrides environment
|
||||
- **WHEN** `--url` or `--token` flags are provided
|
||||
- **THEN** the flag values SHALL take precedence over environment variables
|
||||
|
||||
#### Scenario: Missing configuration
|
||||
- **WHEN** neither the environment variable nor the flag is set for URL or token
|
||||
- **THEN** the system SHALL print an error message and exit with code 1
|
||||
|
||||
### Requirement: Cursor-based pagination
|
||||
The system SHALL implement cursor-based pagination for all list endpoints that support it. Paginated endpoints are: `GET /boards/{boardId}/actions` (`beforeId`), `GET /cards/{cardId}/actions` (`beforeId`), `GET /lists/{listId}/cards` (`before`), `GET /cards/{cardId}/comments` (`beforeId`). List methods SHALL accept a `limit` parameter. When `limit` is 0, the client SHALL fetch all pages. When `limit` is greater than 0, the client SHALL stop fetching after accumulating at least `limit` items and truncate the result to exactly `limit` items. Each page fetch SHALL be logged at DEBUG level.
|
||||
|
||||
#### Scenario: Fetch all pages
|
||||
- **WHEN** a list method is called with limit 0
|
||||
- **THEN** the client SHALL fetch pages until an empty page is returned
|
||||
- **AND** all items from all pages SHALL be returned
|
||||
|
||||
#### Scenario: Fetch with limit
|
||||
- **WHEN** a list method is called with limit N (N > 0)
|
||||
- **THEN** the client SHALL stop fetching after accumulating N or more items
|
||||
- **AND** the returned slice SHALL contain exactly N items
|
||||
|
||||
#### Scenario: Single page result
|
||||
- **WHEN** the API returns fewer items than a page size and no more pages exist
|
||||
- **THEN** the client SHALL return those items without making additional requests
|
||||
|
||||
### Requirement: Project operations
|
||||
The client SHALL provide methods to list all accessible projects (`GET /projects`) and get a single project by ID (`GET /projects/{id}`).
|
||||
|
||||
#### Scenario: List projects
|
||||
- **WHEN** `ListProjects` is called
|
||||
- **THEN** the client SHALL send `GET /projects` and return a slice of Project models
|
||||
|
||||
#### Scenario: Get project
|
||||
- **WHEN** `GetProject` is called with a project ID
|
||||
- **THEN** the client SHALL send `GET /projects/{id}` and return a Project model
|
||||
|
||||
### Requirement: Board operations
|
||||
The client SHALL provide a method to get a single board by ID (`GET /boards/{id}`) and list board actions (`GET /boards/{boardId}/actions`) with pagination support.
|
||||
|
||||
#### Scenario: Get board
|
||||
- **WHEN** `GetBoard` is called with a board ID
|
||||
- **THEN** the client SHALL send `GET /boards/{id}` and return a Board model including its included lists
|
||||
|
||||
#### Scenario: List board actions
|
||||
- **WHEN** `ListBoardActions` is called with a board ID and limit
|
||||
- **THEN** the client SHALL send paginated `GET /boards/{boardId}/actions` requests and return a slice of Action models
|
||||
|
||||
### Requirement: Card CRUD operations
|
||||
The client SHALL provide methods for: get card (`GET /cards/{id}`), create card (`POST /lists/{listId}/cards`), update card (`PATCH /cards/{id}`), delete card (`DELETE /cards/{id}`), and duplicate card (`POST /cards/{id}/duplicate`). The client SHALL provide a method to list cards in a list (`GET /lists/{listId}/cards`) with pagination support. The client SHALL provide a method to list card actions (`GET /cards/{cardId}/actions`) with pagination support.
|
||||
|
||||
#### Scenario: Get card
|
||||
- **WHEN** `GetCard` is called with a card ID
|
||||
- **THEN** the client SHALL send `GET /cards/{id}` and return a Card model
|
||||
|
||||
#### Scenario: Create card
|
||||
- **WHEN** `CreateCard` is called with a list ID and card fields (name, description, type, position, dueDate, isDueCompleted)
|
||||
- **THEN** the client SHALL send `POST /lists/{listId}/cards` with the provided fields and return the created Card
|
||||
|
||||
#### Scenario: Update card
|
||||
- **WHEN** `UpdateCard` is called with a card ID and update fields
|
||||
- **THEN** the client SHALL send `PATCH /cards/{id}` with only the provided fields and return the updated Card
|
||||
|
||||
#### Scenario: Delete card
|
||||
- **WHEN** `DeleteCard` is called with a card ID
|
||||
- **THEN** the client SHALL send `DELETE /cards/{id}`
|
||||
|
||||
#### Scenario: Duplicate card
|
||||
- **WHEN** `DuplicateCard` is called with a card ID, name, and position
|
||||
- **THEN** the client SHALL send `POST /cards/{id}/duplicate` and return the new Card
|
||||
|
||||
#### Scenario: List cards in list
|
||||
- **WHEN** `ListCards` is called with a list ID and limit
|
||||
- **THEN** the client SHALL send paginated `GET /lists/{listId}/cards` requests and return a slice of Card models
|
||||
|
||||
#### Scenario: List card actions
|
||||
- **WHEN** `ListCardActions` is called with a card ID and limit
|
||||
- **THEN** the client SHALL send paginated `GET /cards/{cardId}/actions` requests and return a slice of Action models
|
||||
|
||||
### Requirement: Comment operations
|
||||
The client SHALL provide methods for: list comments (`GET /cards/{cardId}/comments`) with pagination, create comment (`POST /cards/{cardId}/comments`), update comment (`PATCH /comments/{id}`), and delete comment (`DELETE /comments/{id}`).
|
||||
|
||||
#### Scenario: List comments
|
||||
- **WHEN** `ListComments` is called with a card ID and limit
|
||||
- **THEN** the client SHALL send paginated `GET /cards/{cardId}/comments` requests and return a slice of Comment models
|
||||
|
||||
#### Scenario: Create comment
|
||||
- **WHEN** `CreateComment` is called with a card ID and text
|
||||
- **THEN** the client SHALL send `POST /cards/{cardId}/comments` with the text and return the created Comment
|
||||
|
||||
#### Scenario: Update comment
|
||||
- **WHEN** `UpdateComment` is called with a comment ID and text
|
||||
- **THEN** the client SHALL send `PATCH /comments/{id}` and return the updated Comment
|
||||
|
||||
#### Scenario: Delete comment
|
||||
- **WHEN** `DeleteComment` is called with a comment ID
|
||||
- **THEN** the client SHALL send `DELETE /comments/{id}`
|
||||
|
||||
### Requirement: Task list operations
|
||||
The client SHALL provide methods for: create task list (`POST /cards/{cardId}/task-lists`), get task list (`GET /task-lists/{id}`), update task list (`PATCH /task-lists/{id}`), and delete task list (`DELETE /task-lists/{id}`).
|
||||
|
||||
#### Scenario: Create task list
|
||||
- **WHEN** `CreateTaskList` is called with a card ID and fields (name, position, showOnFrontOfCard, hideCompletedTasks)
|
||||
- **THEN** the client SHALL send `POST /cards/{cardId}/task-lists` and return the created TaskList
|
||||
|
||||
#### Scenario: Get task list
|
||||
- **WHEN** `GetTaskList` is called with a task list ID
|
||||
- **THEN** the client SHALL send `GET /task-lists/{id}` and return a TaskList model
|
||||
|
||||
#### Scenario: Update task list
|
||||
- **WHEN** `UpdateTaskList` is called with a task list ID and update fields
|
||||
- **THEN** the client SHALL send `PATCH /task-lists/{id}` and return the updated TaskList
|
||||
|
||||
#### Scenario: Delete task list
|
||||
- **WHEN** `DeleteTaskList` is called with a task list ID
|
||||
- **THEN** the client SHALL send `DELETE /task-lists/{id}`
|
||||
|
||||
### Requirement: Task operations
|
||||
The client SHALL provide methods for: create task (`POST /task-lists/{taskListId}/tasks`), update task (`PATCH /tasks/{id}`), and delete task (`DELETE /tasks/{id}`).
|
||||
|
||||
#### Scenario: Create task
|
||||
- **WHEN** `CreateTask` is called with a task list ID and fields (name, position, isCompleted, assigneeUserId, linkedCardId)
|
||||
- **THEN** the client SHALL send `POST /task-lists/{taskListId}/tasks` and return the created Task
|
||||
|
||||
#### Scenario: Update task
|
||||
- **WHEN** `UpdateTask` is called with a task ID and update fields (name, position, isCompleted, assigneeUserId, taskListId)
|
||||
- **THEN** the client SHALL send `PATCH /tasks/{id}` and return the updated Task
|
||||
|
||||
#### Scenario: Delete task
|
||||
- **WHEN** `DeleteTask` is called with a task ID
|
||||
- **THEN** the client SHALL send `DELETE /tasks/{id}`
|
||||
|
||||
### Requirement: Label operations
|
||||
The client SHALL provide methods for: create label (`POST /boards/{boardId}/labels`), update label (`PATCH /labels/{id}`), and delete label (`DELETE /labels/{id}`).
|
||||
|
||||
#### Scenario: Create label
|
||||
- **WHEN** `CreateLabel` is called with a board ID and fields (name, color, position)
|
||||
- **THEN** the client SHALL send `POST /boards/{boardId}/labels` and return the created Label
|
||||
|
||||
#### Scenario: Update label
|
||||
- **WHEN** `UpdateLabel` is called with a label ID and update fields (name, color, position)
|
||||
- **THEN** the client SHALL send `PATCH /labels/{id}` and return the updated Label
|
||||
|
||||
#### Scenario: Delete label
|
||||
- **WHEN** `DeleteLabel` is called with a label ID
|
||||
- **THEN** the client SHALL send `DELETE /labels/{id}`
|
||||
|
||||
### Requirement: Card label operations
|
||||
The client SHALL provide methods for: add label to card (`POST /cards/{cardId}/card-labels`) and remove label from card (`DELETE /cards/{cardId}/card-labels/labelId:{labelId}`).
|
||||
|
||||
#### Scenario: Add label to card
|
||||
- **WHEN** `AddCardLabel` is called with a card ID and label ID
|
||||
- **THEN** the client SHALL send `POST /cards/{cardId}/card-labels` with the label ID
|
||||
|
||||
#### Scenario: Remove label from card
|
||||
- **WHEN** `RemoveCardLabel` is called with a card ID and label ID
|
||||
- **THEN** the client SHALL send `DELETE /cards/{cardId}/card-labels/labelId:{labelId}`
|
||||
|
||||
### Requirement: Card membership operations
|
||||
The client SHALL provide methods for: assign user to card (`POST /cards/{cardId}/card-memberships`) and unassign user from card (`DELETE /cards/{cardId}/card-memberships/userId:{userId}`).
|
||||
|
||||
#### Scenario: Assign user to card
|
||||
- **WHEN** `AddCardMember` is called with a card ID and user ID
|
||||
- **THEN** the client SHALL send `POST /cards/{cardId}/card-memberships` with the user ID
|
||||
|
||||
#### Scenario: Unassign user from card
|
||||
- **WHEN** `RemoveCardMember` is called with a card ID and user ID
|
||||
- **THEN** the client SHALL send `DELETE /cards/{cardId}/card-memberships/userId:{userId}`
|
||||
@@ -0,0 +1,88 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Move card between lists
|
||||
The system SHALL provide a `card move` operation that updates a card's `listId` to move it to a different list. The operation SHALL accept an optional `position` to place the card at a specific position within the target list. The move operation SHALL be implemented as a `PATCH /cards/{id}` call with `listId` and optionally `position` fields. The CLI command SHALL be `pcli card move <id> --list <listId> [--position N]`.
|
||||
|
||||
#### Scenario: Move card to another list
|
||||
- **WHEN** `pcli card move <id> --list <targetListId>` is executed
|
||||
- **THEN** the system SHALL send `PATCH /cards/{id}` with `{"listId": "<targetListId>"}`
|
||||
- **AND** the system SHALL output the updated card with its new listId
|
||||
|
||||
#### Scenario: Move card to specific position
|
||||
- **WHEN** `pcli card move <id> --list <targetListId> --position 0` is executed
|
||||
- **THEN** the system SHALL send `PATCH /cards/{id}` with `{"listId": "<targetListId>", "position": 0}`
|
||||
- **AND** the card SHALL appear at the specified position in the target list
|
||||
|
||||
#### Scenario: Move card missing list flag
|
||||
- **WHEN** `pcli card move <id>` is executed without `--list`
|
||||
- **THEN** the system SHALL print an error indicating `--list` is required and exit with code 1
|
||||
|
||||
### Requirement: Duplicate card
|
||||
The system SHALL provide a `card duplicate` operation that creates a copy of an existing card. The operation SHALL call `POST /cards/{id}/duplicate`. The CLI command SHALL be `pcli card duplicate <id> [--name <name>] [--position N]`. If `--name` is not provided, the API determines the name of the duplicate.
|
||||
|
||||
#### Scenario: Duplicate card with defaults
|
||||
- **WHEN** `pcli card duplicate <id>` is executed
|
||||
- **THEN** the system SHALL send `POST /cards/{id}/duplicate`
|
||||
- **AND** the system SHALL output the newly created duplicate card
|
||||
|
||||
#### Scenario: Duplicate card with custom name
|
||||
- **WHEN** `pcli card duplicate <id> --name "Copy of task"` is executed
|
||||
- **THEN** the system SHALL send `POST /cards/{id}/duplicate` with `{"name": "Copy of task"}`
|
||||
- **AND** the duplicate card SHALL have the specified name
|
||||
|
||||
### Requirement: Assign and unassign card members
|
||||
The system SHALL provide `card assign` and `card unassign` operations to manage card memberships. `card assign` SHALL call `POST /cards/{cardId}/card-memberships` with the user ID. `card unassign` SHALL call `DELETE /cards/{cardId}/card-memberships/userId:{userId}`.
|
||||
|
||||
#### Scenario: Assign user to card
|
||||
- **WHEN** `pcli card assign <cardId> --user <userId>` is executed
|
||||
- **THEN** the system SHALL send `POST /cards/{cardId}/card-memberships` with `{"userId": "<userId>"}`
|
||||
- **AND** the system SHALL output a success confirmation
|
||||
|
||||
#### Scenario: Unassign user from card
|
||||
- **WHEN** `pcli card unassign <cardId> --user <userId>` is executed
|
||||
- **THEN** the system SHALL send `DELETE /cards/{cardId}/card-memberships/userId:{userId}`
|
||||
- **AND** the system SHALL output a success confirmation
|
||||
|
||||
#### Scenario: Assign missing user flag
|
||||
- **WHEN** `pcli card assign <cardId>` is executed without `--user`
|
||||
- **THEN** the system SHALL print an error indicating `--user` is required and exit with code 1
|
||||
|
||||
### Requirement: Add and remove card labels
|
||||
The system SHALL provide `card add-label` and `card remove-label` operations to manage labels on cards. `card add-label` SHALL call `POST /cards/{cardId}/card-labels` with the label ID. `card remove-label` SHALL call `DELETE /cards/{cardId}/card-labels/labelId:{labelId}`.
|
||||
|
||||
#### Scenario: Add label to card
|
||||
- **WHEN** `pcli card add-label <cardId> --label <labelId>` is executed
|
||||
- **THEN** the system SHALL send `POST /cards/{cardId}/card-labels` with `{"labelId": "<labelId>"}`
|
||||
- **AND** the system SHALL output a success confirmation
|
||||
|
||||
#### Scenario: Remove label from card
|
||||
- **WHEN** `pcli card remove-label <cardId> --label <labelId>` is executed
|
||||
- **THEN** the system SHALL send `DELETE /cards/{cardId}/card-labels/labelId:{labelId}`
|
||||
- **AND** the system SHALL output a success confirmation
|
||||
|
||||
#### Scenario: Add label missing label flag
|
||||
- **WHEN** `pcli card add-label <cardId>` is executed without `--label`
|
||||
- **THEN** the system SHALL print an error indicating `--label` is required and exit with code 1
|
||||
|
||||
### Requirement: Enriched board-level card listing
|
||||
The system SHALL provide a `card list --board <id>` operation that returns all cards across all lists in a board, with each card enriched with the `listName` field. The operation SHALL: (1) call `GET /boards/{id}` to retrieve the board and its included lists, (2) call `GET /lists/{listId}/cards` for each list to retrieve cards (with pagination support), (3) inject `listName` into each card based on the list it belongs to. The `--limit` flag SHALL apply to the total number of cards returned across all lists.
|
||||
|
||||
#### Scenario: List all cards on a board
|
||||
- **WHEN** `pcli card list --board <id>` is executed
|
||||
- **THEN** the system SHALL return all cards from all lists in the board
|
||||
- **AND** each card SHALL include a `listName` field with the name of its containing list
|
||||
- **AND** each card SHALL include a `listId` field
|
||||
|
||||
#### Scenario: Board card listing with limit
|
||||
- **WHEN** `pcli card list --board <id> --limit 10` is executed
|
||||
- **THEN** the system SHALL return at most 10 cards total across all lists
|
||||
- **AND** each card SHALL include the `listName` field
|
||||
|
||||
#### Scenario: Board with no cards
|
||||
- **WHEN** `pcli card list --board <id>` is executed on a board with empty lists
|
||||
- **THEN** the system SHALL return an empty array
|
||||
|
||||
#### Scenario: Board with multiple lists
|
||||
- **WHEN** a board has lists "To Do", "In Progress", and "Done" each containing cards
|
||||
- **THEN** the returned cards SHALL have `listName` set to the respective list name
|
||||
- **AND** cards from all lists SHALL be included in the result
|
||||
@@ -0,0 +1,188 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Root command and global flags
|
||||
The system SHALL provide a root command `pcli` that serves as the entry point. The root command SHALL register global flags: `--format` (string, default `json`, values `json` or `table`), `--url` (string, overrides `PLANKA_URL`), `--token` (string, overrides `PLANKA_TOKEN`), and `--log-level` (string, default `warn`, values `debug`, `info`, `warn`, `error`). The root command SHALL initialize the logger based on `--log-level` and configure it to write structured JSON to stderr. The root command SHALL validate that URL and token are available (from flags or environment) before executing any subcommand.
|
||||
|
||||
#### Scenario: Display help
|
||||
- **WHEN** `pcli` is run with no arguments or `--help`
|
||||
- **THEN** the system SHALL display usage information listing all resource subcommands and global flags
|
||||
|
||||
#### Scenario: Invalid format flag
|
||||
- **WHEN** `--format` is set to an unsupported value
|
||||
- **THEN** the system SHALL print an error and exit with code 1
|
||||
|
||||
#### Scenario: Log level controls output
|
||||
- **WHEN** `--log-level=debug` is set
|
||||
- **THEN** DEBUG-level log entries SHALL appear on stderr
|
||||
- **AND** stdout SHALL contain only the command's data output
|
||||
|
||||
### Requirement: Project commands
|
||||
The system SHALL provide a `project` command group with subcommands `list` and `get`. `pcli project list` SHALL call the client's ListProjects method and output the result. `pcli project get <id>` SHALL accept a project ID as a positional argument, call GetProject, and output the result.
|
||||
|
||||
#### Scenario: List projects
|
||||
- **WHEN** `pcli project list` is executed
|
||||
- **THEN** the system SHALL output all accessible projects
|
||||
|
||||
#### Scenario: Get project by ID
|
||||
- **WHEN** `pcli project get <id>` is executed with a valid project ID
|
||||
- **THEN** the system SHALL output the project details
|
||||
|
||||
#### Scenario: Get project missing ID
|
||||
- **WHEN** `pcli project get` is executed without an ID argument
|
||||
- **THEN** the system SHALL print an error indicating the ID is required and exit with code 1
|
||||
|
||||
### Requirement: Board commands
|
||||
The system SHALL provide a `board` command group with subcommands `get` and `actions`. `pcli board get <id>` SHALL accept a board ID as a positional argument and output the board details. `pcli board actions <id>` SHALL accept a board ID and an optional `--limit` flag (int, default 0) and output the board's action history.
|
||||
|
||||
#### Scenario: Get board
|
||||
- **WHEN** `pcli board get <id>` is executed
|
||||
- **THEN** the system SHALL output the board details including its lists
|
||||
|
||||
#### Scenario: List board actions
|
||||
- **WHEN** `pcli board actions <id>` is executed
|
||||
- **THEN** the system SHALL output the board's action history
|
||||
|
||||
#### Scenario: List board actions with limit
|
||||
- **WHEN** `pcli board actions <id> --limit 10` is executed
|
||||
- **THEN** the system SHALL output at most 10 action entries
|
||||
|
||||
### Requirement: Card commands
|
||||
The system SHALL provide a `card` command group with subcommands: `list`, `get`, `create`, `update`, `delete`, `duplicate`, `move`, `assign`, `unassign`, `add-label`, `remove-label`, `actions`.
|
||||
|
||||
`pcli card list` SHALL require either `--board <id>` or `--list <id>` flag and accept an optional `--limit` flag. `pcli card get <id>` SHALL accept a card ID as a positional argument. `pcli card create` SHALL require `--list <id>` and `--name <name>` flags and accept optional flags: `--description`, `--type`, `--position`, `--due-date`, `--due-completed`. `pcli card update <id>` SHALL accept a card ID and optional flags for each updatable field: `--name`, `--description`, `--type`, `--position`, `--due-date`, `--due-completed`. `pcli card delete <id>` SHALL accept a card ID. `pcli card duplicate <id>` SHALL accept a card ID and optional `--name` and `--position` flags. `pcli card move <id>` SHALL require `--list <id>` and accept optional `--position` flag. `pcli card assign <id>` SHALL require `--user <userId>`. `pcli card unassign <id>` SHALL require `--user <userId>`. `pcli card add-label <id>` SHALL require `--label <labelId>`. `pcli card remove-label <id>` SHALL require `--label <labelId>`. `pcli card actions <id>` SHALL accept an optional `--limit` flag.
|
||||
|
||||
#### Scenario: List cards by board
|
||||
- **WHEN** `pcli card list --board <id>` is executed
|
||||
- **THEN** the system SHALL output all cards across all lists in the board, each enriched with listName
|
||||
|
||||
#### Scenario: List cards by list
|
||||
- **WHEN** `pcli card list --list <id>` is executed
|
||||
- **THEN** the system SHALL output cards in the specified list
|
||||
|
||||
#### Scenario: List cards with limit
|
||||
- **WHEN** `pcli card list --list <id> --limit 5` is executed
|
||||
- **THEN** the system SHALL output at most 5 cards
|
||||
|
||||
#### Scenario: Card list missing board or list flag
|
||||
- **WHEN** `pcli card list` is executed without `--board` or `--list`
|
||||
- **THEN** the system SHALL print an error indicating one is required and exit with code 1
|
||||
|
||||
#### Scenario: Get card
|
||||
- **WHEN** `pcli card get <id>` is executed
|
||||
- **THEN** the system SHALL output the card details
|
||||
|
||||
#### Scenario: Create card
|
||||
- **WHEN** `pcli card create --list <id> --name "Task name"` is executed
|
||||
- **THEN** the system SHALL create the card and output the created card
|
||||
|
||||
#### Scenario: Create card missing required flags
|
||||
- **WHEN** `pcli card create` is executed without `--list` or `--name`
|
||||
- **THEN** the system SHALL print an error and exit with code 1
|
||||
|
||||
#### Scenario: Update card
|
||||
- **WHEN** `pcli card update <id> --name "New name"` is executed
|
||||
- **THEN** the system SHALL update the card and output the updated card
|
||||
|
||||
#### Scenario: Delete card
|
||||
- **WHEN** `pcli card delete <id>` is executed
|
||||
- **THEN** the system SHALL delete the card and output a success confirmation
|
||||
|
||||
#### Scenario: Duplicate card
|
||||
- **WHEN** `pcli card duplicate <id>` is executed
|
||||
- **THEN** the system SHALL duplicate the card and output the new card
|
||||
|
||||
#### Scenario: Move card
|
||||
- **WHEN** `pcli card move <id> --list <listId>` is executed
|
||||
- **THEN** the system SHALL update the card's listId (and optionally position) and output the updated card
|
||||
|
||||
#### Scenario: Assign user to card
|
||||
- **WHEN** `pcli card assign <id> --user <userId>` is executed
|
||||
- **THEN** the system SHALL add the user as a card member
|
||||
|
||||
#### Scenario: Unassign user from card
|
||||
- **WHEN** `pcli card unassign <id> --user <userId>` is executed
|
||||
- **THEN** the system SHALL remove the user from the card's members
|
||||
|
||||
#### Scenario: Add label to card
|
||||
- **WHEN** `pcli card add-label <id> --label <labelId>` is executed
|
||||
- **THEN** the system SHALL add the label to the card
|
||||
|
||||
#### Scenario: Remove label from card
|
||||
- **WHEN** `pcli card remove-label <id> --label <labelId>` is executed
|
||||
- **THEN** the system SHALL remove the label from the card
|
||||
|
||||
#### Scenario: List card actions
|
||||
- **WHEN** `pcli card actions <id>` is executed
|
||||
- **THEN** the system SHALL output the card's action history
|
||||
|
||||
### Requirement: Comment commands
|
||||
The system SHALL provide a `comment` command group with subcommands: `list`, `create`, `update`, `delete`. `pcli comment list` SHALL require `--card <id>` and accept optional `--limit`. `pcli comment create` SHALL require `--card <id>` and `--text <text>`. `pcli comment update <id>` SHALL require `--text <text>`. `pcli comment delete <id>` SHALL accept a comment ID.
|
||||
|
||||
#### Scenario: List comments
|
||||
- **WHEN** `pcli comment list --card <id>` is executed
|
||||
- **THEN** the system SHALL output comments for the card
|
||||
|
||||
#### Scenario: Create comment
|
||||
- **WHEN** `pcli comment create --card <id> --text "comment text"` is executed
|
||||
- **THEN** the system SHALL create the comment and output the created comment
|
||||
|
||||
#### Scenario: Update comment
|
||||
- **WHEN** `pcli comment update <id> --text "updated text"` is executed
|
||||
- **THEN** the system SHALL update the comment and output the updated comment
|
||||
|
||||
#### Scenario: Delete comment
|
||||
- **WHEN** `pcli comment delete <id>` is executed
|
||||
- **THEN** the system SHALL delete the comment and output a success confirmation
|
||||
|
||||
### Requirement: Task list commands
|
||||
The system SHALL provide a `task-list` command group with subcommands: `create`, `get`, `update`, `delete`. `pcli task-list create` SHALL require `--card <id>` and `--name <name>` and accept optional flags: `--position`, `--show-on-front`, `--hide-completed`. `pcli task-list get <id>` SHALL accept a task list ID. `pcli task-list update <id>` SHALL accept optional flags for each updatable field. `pcli task-list delete <id>` SHALL accept a task list ID.
|
||||
|
||||
#### Scenario: Create task list
|
||||
- **WHEN** `pcli task-list create --card <id> --name "Checklist"` is executed
|
||||
- **THEN** the system SHALL create the task list and output the created task list
|
||||
|
||||
#### Scenario: Get task list
|
||||
- **WHEN** `pcli task-list get <id>` is executed
|
||||
- **THEN** the system SHALL output the task list details including its tasks
|
||||
|
||||
#### Scenario: Update task list
|
||||
- **WHEN** `pcli task-list update <id> --name "Renamed"` is executed
|
||||
- **THEN** the system SHALL update the task list and output the updated task list
|
||||
|
||||
#### Scenario: Delete task list
|
||||
- **WHEN** `pcli task-list delete <id>` is executed
|
||||
- **THEN** the system SHALL delete the task list and output a success confirmation
|
||||
|
||||
### Requirement: Task commands
|
||||
The system SHALL provide a `task` command group with subcommands: `create`, `update`, `delete`. `pcli task create` SHALL require `--task-list <id>` and `--name <name>` and accept optional flags: `--position`, `--completed`, `--assignee`, `--linked-card`. `pcli task update <id>` SHALL accept optional flags for each updatable field: `--name`, `--position`, `--completed`, `--assignee`, `--task-list`. `pcli task delete <id>` SHALL accept a task ID.
|
||||
|
||||
#### Scenario: Create task
|
||||
- **WHEN** `pcli task create --task-list <id> --name "Do something"` is executed
|
||||
- **THEN** the system SHALL create the task and output the created task
|
||||
|
||||
#### Scenario: Update task
|
||||
- **WHEN** `pcli task update <id> --completed` is executed
|
||||
- **THEN** the system SHALL mark the task as completed and output the updated task
|
||||
|
||||
#### Scenario: Move task to different list
|
||||
- **WHEN** `pcli task update <id> --task-list <newListId>` is executed
|
||||
- **THEN** the system SHALL move the task to the specified task list
|
||||
|
||||
#### Scenario: Delete task
|
||||
- **WHEN** `pcli task delete <id>` is executed
|
||||
- **THEN** the system SHALL delete the task and output a success confirmation
|
||||
|
||||
### Requirement: Label commands
|
||||
The system SHALL provide a `label` command group with subcommands: `create`, `update`, `delete`. `pcli label create` SHALL require `--board <id>` and `--name <name>` and accept optional flags: `--color`, `--position`. `pcli label update <id>` SHALL accept optional flags: `--name`, `--color`, `--position`. `pcli label delete <id>` SHALL accept a label ID.
|
||||
|
||||
#### Scenario: Create label
|
||||
- **WHEN** `pcli label create --board <id> --name "Bug" --color red` is executed
|
||||
- **THEN** the system SHALL create the label and output the created label
|
||||
|
||||
#### Scenario: Update label
|
||||
- **WHEN** `pcli label update <id> --name "Feature" --color green` is executed
|
||||
- **THEN** the system SHALL update the label and output the updated label
|
||||
|
||||
#### Scenario: Delete label
|
||||
- **WHEN** `pcli label delete <id>` is executed
|
||||
- **THEN** the system SHALL delete the label and output a success confirmation
|
||||
@@ -0,0 +1,66 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: JSON output envelope
|
||||
The system SHALL wrap all successful command output in a JSON envelope with the structure `{"data": <result>, "error": null}`. The system SHALL wrap all error output in a JSON envelope with the structure `{"data": null, "error": "<message>"}`. The envelope SHALL be written to stdout. The `data` field SHALL contain the direct result of the command (object or array). The `error` field SHALL be null on success and a string message on failure.
|
||||
|
||||
#### Scenario: Successful command output
|
||||
- **WHEN** a command completes successfully in JSON format
|
||||
- **THEN** stdout SHALL contain `{"data": <result>, "error": null}`
|
||||
- **AND** the process SHALL exit with code 0
|
||||
|
||||
#### Scenario: Error command output
|
||||
- **WHEN** a command fails in JSON format
|
||||
- **THEN** stdout SHALL contain `{"data": null, "error": "<message>"}`
|
||||
- **AND** the process SHALL exit with code 1
|
||||
|
||||
#### Scenario: Envelope structure is consistent
|
||||
- **WHEN** any command is executed in JSON format
|
||||
- **THEN** the output SHALL always contain exactly the keys `data` and `error` at the top level
|
||||
|
||||
### Requirement: Table format output
|
||||
The system SHALL support a `--format=table` flag that outputs results as human-readable tabular text. Table output SHALL be written to stdout. Table output SHALL NOT use the JSON envelope. When table format is active and an error occurs, the error message SHALL be written to stderr (not stdout). Each resource type SHALL define its own column set for table rendering.
|
||||
|
||||
#### Scenario: Table output for a list of items
|
||||
- **WHEN** a list command is executed with `--format=table`
|
||||
- **THEN** stdout SHALL contain a header row followed by one row per item
|
||||
- **AND** columns SHALL be aligned and separated by whitespace
|
||||
|
||||
#### Scenario: Table output for a single item
|
||||
- **WHEN** a get command is executed with `--format=table`
|
||||
- **THEN** stdout SHALL contain a key-value representation of the item
|
||||
|
||||
#### Scenario: Error in table format
|
||||
- **WHEN** a command fails with `--format=table`
|
||||
- **THEN** the error message SHALL be written to stderr
|
||||
- **AND** the process SHALL exit with code 1
|
||||
|
||||
### Requirement: Format flag default
|
||||
The system SHALL default to JSON format when no `--format` flag is provided. The `--format` flag SHALL accept values `json` and `table`. Any other value SHALL cause an error and exit with code 1.
|
||||
|
||||
#### Scenario: Default format is JSON
|
||||
- **WHEN** a command is executed without `--format`
|
||||
- **THEN** the output SHALL be in JSON envelope format
|
||||
|
||||
#### Scenario: Explicit JSON format
|
||||
- **WHEN** a command is executed with `--format=json`
|
||||
- **THEN** the output SHALL be in JSON envelope format
|
||||
|
||||
#### Scenario: Explicit table format
|
||||
- **WHEN** a command is executed with `--format=table`
|
||||
- **THEN** the output SHALL be in table format
|
||||
|
||||
#### Scenario: Invalid format value
|
||||
- **WHEN** a command is executed with `--format=xml`
|
||||
- **THEN** the system SHALL print an error and exit with code 1
|
||||
|
||||
### Requirement: Log output separation
|
||||
All log output SHALL be written to stderr using structured JSON via `log/slog`. Log output SHALL never appear on stdout. This ensures that stdout contains only the command's data output (JSON envelope or table) and is safe to pipe or parse programmatically.
|
||||
|
||||
#### Scenario: Logs do not pollute stdout
|
||||
- **WHEN** a command is executed with `--log-level=debug`
|
||||
- **THEN** all log entries SHALL appear on stderr
|
||||
- **AND** stdout SHALL contain only the command's data output
|
||||
|
||||
#### Scenario: Logs are structured JSON
|
||||
- **WHEN** a log entry is emitted
|
||||
- **THEN** it SHALL be a valid JSON object with at minimum `time`, `level`, and `msg` fields
|
||||
@@ -0,0 +1,133 @@
|
||||
## 1. Project Scaffolding
|
||||
|
||||
- [x] 1.1 Initialize Go module (`go mod init github.com/dcgsteve/pcli`)
|
||||
- [x] 1.2 Create directory structure: `cmd/`, `client/`, `model/`, `output/`, `logging/`
|
||||
- [x] 1.3 Add Cobra dependency (`go get github.com/spf13/cobra`)
|
||||
- [x] 1.4 Create `main.go` entry point that calls `cmd.Execute()`
|
||||
|
||||
## 2. Logging
|
||||
|
||||
- [x] 2.1 Implement `logging/logging.go` — `NewLogger(level string) *slog.Logger` that parses level string, creates JSON handler writing to stderr
|
||||
|
||||
## 3. Model Types
|
||||
|
||||
- [x] 3.1 Define `model/types.go` — Project, Board, List, Card, Comment, TaskList, Task, Label, Action, CardLabel, CardMembership structs with JSON tags and pointer types for nullable fields
|
||||
- [x] 3.2 Define `Envelope` struct (`Data any`, `Error *string`) in `model/types.go`
|
||||
- [x] 3.3 Define `APIError` struct (StatusCode int, Message string) implementing the `error` interface
|
||||
|
||||
## 4. Output Formatting
|
||||
|
||||
- [x] 4.1 Implement `output/output.go` — `Print(data any, format string, w io.Writer)` function that switches on format
|
||||
- [x] 4.2 Implement JSON output mode — marshal `Envelope{Data: data, Error: nil}` to stdout
|
||||
- [x] 4.3 Implement error output — `PrintError(err error, format string, w io.Writer)` that writes `Envelope{Data: nil, Error: msg}` for JSON, or error to stderr for table
|
||||
- [x] 4.4 Implement table output mode — tabwriter-based rendering with per-resource column definitions
|
||||
|
||||
## 5. Base HTTP Client
|
||||
|
||||
- [x] 5.1 Implement `client/client.go` — `Client` struct with BaseURL, Token, HTTPClient, Logger fields
|
||||
- [x] 5.2 Implement `NewClient(baseURL, token string, logger *slog.Logger) *Client`
|
||||
- [x] 5.3 Implement `Do(ctx, method, path string, body any) (json.RawMessage, error)` — URL construction, bearer header, JSON marshal/unmarshal, DEBUG logging with method/path/status/duration
|
||||
- [x] 5.4 Implement `APIError` handling — map 4xx/5xx responses to APIError, WARN logging on errors
|
||||
- [x] 5.5 Implement `DoNoBody(ctx, method, path string) (json.RawMessage, error)` convenience method for GET/DELETE
|
||||
|
||||
## 6. Root Command and Global Flags
|
||||
|
||||
- [x] 6.1 Implement `cmd/root.go` — root Cobra command with `--format`, `--url`, `--token`, `--log-level` persistent flags
|
||||
- [x] 6.2 Implement `PersistentPreRunE` — resolve URL/token from flags or env vars (flag > env), validate both present, initialize logger, create client instance
|
||||
- [x] 6.3 Wire output format into a shared variable accessible by all subcommands
|
||||
|
||||
## 7. Client — Project Operations
|
||||
|
||||
- [x] 7.1 Implement `client/projects.go` — `ListProjects(ctx) ([]model.Project, error)`
|
||||
- [x] 7.2 Implement `GetProject(ctx, id string) (*model.Project, error)`
|
||||
|
||||
## 8. Client — Board Operations
|
||||
|
||||
- [x] 8.1 Implement `client/boards.go` — `GetBoard(ctx, id string) (*model.Board, error)`
|
||||
- [x] 8.2 Implement `ListBoardActions(ctx, boardId string, limit int) ([]model.Action, error)` with cursor-based pagination using `beforeId`
|
||||
|
||||
## 9. Client — Card Operations
|
||||
|
||||
- [x] 9.1 Implement `client/cards.go` — `GetCard(ctx, id string) (*model.Card, error)`
|
||||
- [x] 9.2 Implement `CreateCard(ctx, listId string, fields) (*model.Card, error)`
|
||||
- [x] 9.3 Implement `UpdateCard(ctx, id string, fields) (*model.Card, error)`
|
||||
- [x] 9.4 Implement `DeleteCard(ctx, id string) error`
|
||||
- [x] 9.5 Implement `DuplicateCard(ctx, id string, name *string, position *float64) (*model.Card, error)`
|
||||
- [x] 9.6 Implement `ListCards(ctx, listId string, limit int) ([]model.Card, error)` with cursor-based pagination using `before`
|
||||
- [x] 9.7 Implement `ListCardActions(ctx, cardId string, limit int) ([]model.Action, error)` with cursor-based pagination using `beforeId`
|
||||
- [x] 9.8 Implement `ListCardsByBoard(ctx, boardId string, limit int) ([]model.CardWithList, error)` — multi-call enrichment: get board → get cards per list → inject listName
|
||||
- [x] 9.9 Implement `AddCardLabel(ctx, cardId, labelId string) error`
|
||||
- [x] 9.10 Implement `RemoveCardLabel(ctx, cardId, labelId string) error`
|
||||
- [x] 9.11 Implement `AddCardMember(ctx, cardId, userId string) error`
|
||||
- [x] 9.12 Implement `RemoveCardMember(ctx, cardId, userId string) error`
|
||||
|
||||
## 10. Client — Comment Operations
|
||||
|
||||
- [x] 10.1 Implement `client/comments.go` — `ListComments(ctx, cardId string, limit int) ([]model.Comment, error)` with cursor-based pagination using `beforeId`
|
||||
- [x] 10.2 Implement `CreateComment(ctx, cardId, text string) (*model.Comment, error)`
|
||||
- [x] 10.3 Implement `UpdateComment(ctx, id, text string) (*model.Comment, error)`
|
||||
- [x] 10.4 Implement `DeleteComment(ctx, id string) error`
|
||||
|
||||
## 11. Client — Task List Operations
|
||||
|
||||
- [x] 11.1 Implement `client/task_lists.go` — `CreateTaskList(ctx, cardId string, fields) (*model.TaskList, error)`
|
||||
- [x] 11.2 Implement `GetTaskList(ctx, id string) (*model.TaskList, error)`
|
||||
- [x] 11.3 Implement `UpdateTaskList(ctx, id string, fields) (*model.TaskList, error)`
|
||||
- [x] 11.4 Implement `DeleteTaskList(ctx, id string) error`
|
||||
|
||||
## 12. Client — Task Operations
|
||||
|
||||
- [x] 12.1 Implement `client/tasks.go` — `CreateTask(ctx, taskListId string, fields) (*model.Task, error)`
|
||||
- [x] 12.2 Implement `UpdateTask(ctx, id string, fields) (*model.Task, error)`
|
||||
- [x] 12.3 Implement `DeleteTask(ctx, id string) error`
|
||||
|
||||
## 13. Client — Label Operations
|
||||
|
||||
- [x] 13.1 Implement `client/labels.go` — `CreateLabel(ctx, boardId string, fields) (*model.Label, error)`
|
||||
- [x] 13.2 Implement `UpdateLabel(ctx, id string, fields) (*model.Label, error)`
|
||||
- [x] 13.3 Implement `DeleteLabel(ctx, id string) error`
|
||||
|
||||
## 14. CLI — Project Commands
|
||||
|
||||
- [x] 14.1 Implement `cmd/project.go` — `project` parent command, `project list` subcommand, `project get <id>` subcommand
|
||||
|
||||
## 15. CLI — Board Commands
|
||||
|
||||
- [x] 15.1 Implement `cmd/board.go` — `board` parent command, `board get <id>` subcommand, `board actions <id>` subcommand with `--limit` flag
|
||||
|
||||
## 16. CLI — Card Commands
|
||||
|
||||
- [x] 16.1 Implement `cmd/card.go` — `card` parent command
|
||||
- [x] 16.2 Implement `card list` subcommand with mutually required `--board` / `--list` flags and `--limit`
|
||||
- [x] 16.3 Implement `card get <id>` subcommand
|
||||
- [x] 16.4 Implement `card create` subcommand with `--list`, `--name` (required), `--description`, `--type`, `--position`, `--due-date`, `--due-completed` flags
|
||||
- [x] 16.5 Implement `card update <id>` subcommand with optional update flags
|
||||
- [x] 16.6 Implement `card delete <id>` subcommand
|
||||
- [x] 16.7 Implement `card duplicate <id>` subcommand with optional `--name`, `--position`
|
||||
- [x] 16.8 Implement `card move <id>` subcommand with required `--list` and optional `--position`
|
||||
- [x] 16.9 Implement `card assign <id>` and `card unassign <id>` subcommands with required `--user`
|
||||
- [x] 16.10 Implement `card add-label <id>` and `card remove-label <id>` subcommands with required `--label`
|
||||
- [x] 16.11 Implement `card actions <id>` subcommand with `--limit`
|
||||
|
||||
## 17. CLI — Comment Commands
|
||||
|
||||
- [x] 17.1 Implement `cmd/comment.go` — `comment` parent command, `comment list` with `--card` and `--limit`, `comment create` with `--card` and `--text`, `comment update <id>` with `--text`, `comment delete <id>`
|
||||
|
||||
## 18. CLI — Task List Commands
|
||||
|
||||
- [x] 18.1 Implement `cmd/task_list.go` — `task-list` parent command, `task-list create` with `--card`, `--name`, optional flags, `task-list get <id>`, `task-list update <id>`, `task-list delete <id>`
|
||||
|
||||
## 19. CLI — Task Commands
|
||||
|
||||
- [x] 19.1 Implement `cmd/task.go` — `task` parent command, `task create` with `--task-list`, `--name`, optional flags, `task update <id>` with optional flags, `task delete <id>`
|
||||
|
||||
## 20. CLI — Label Commands
|
||||
|
||||
- [x] 20.1 Implement `cmd/label.go` — `label` parent command, `label create` with `--board`, `--name`, optional `--color`, `--position`, `label update <id>`, `label delete <id>`
|
||||
|
||||
## 21. Build and Verify
|
||||
|
||||
- [x] 21.1 Verify `go build` produces a clean binary
|
||||
- [x] 21.2 Verify `pcli --help` displays all commands and global flags
|
||||
- [x] 21.3 Verify error output when `PLANKA_URL` / `PLANKA_TOKEN` are missing
|
||||
- [x] 21.4 Add README.md with installation, configuration, and usage examples
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-11
|
||||
@@ -0,0 +1,45 @@
|
||||
## Context
|
||||
|
||||
pcli is a Cobra-based CLI for the Planka project management API. Commands follow a `pcli <resource> <action>` pattern (e.g., `pcli board get`, `pcli card list`). The client layer already provides `ListBoards()` (via `/api/projects`) and `GetBoard(id)` (via `/api/boards/:id`), where `GetBoard` returns the board's lists and cards in the `included` block. Output is handled by `output.Print()` which dispatches to JSON envelope or table rendering based on the `--format` flag.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Provide a single top-level `pcli status` command that summarizes all boards, their lists, and card counts
|
||||
- Reuse existing client methods with no new API endpoints
|
||||
- Support both JSON and table output formats via the existing `--format` flag
|
||||
- Show open card counts as the primary number, with closed cards noted separately
|
||||
|
||||
**Non-Goals:**
|
||||
- Concurrent/parallel board fetching (sequential is sufficient for expected board counts)
|
||||
- Filtering by project, board, or list (this is a full overview command)
|
||||
- Showing card-level detail (names, assignees, due dates) — that's what `card list` is for
|
||||
- Caching or incremental updates
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Top-level command, not a subcommand
|
||||
The `status` command will be registered directly on `rootCmd`, not under `board` or any other resource group. It's a cross-cutting summary, not a resource operation.
|
||||
|
||||
**Alternative considered:** `pcli board status` — rejected because the command spans all boards and is conceptually similar to `git status` as a quick overview.
|
||||
|
||||
### 2. Data aggregation in the command layer
|
||||
The status command will call `ListBoards()` then `GetBoard(id)` for each board, aggregating card counts per list in the command handler. No new client method is needed.
|
||||
|
||||
**Alternative considered:** A dedicated `client.GetStatus()` method — rejected because there's no single Planka API endpoint for this; the aggregation is purely a CLI concern.
|
||||
|
||||
### 3. New model types for status data
|
||||
A `StatusSummary` struct will hold the aggregated data, containing a slice of `BoardSummary` structs (board name + list summaries), each containing a slice of `ListSummary` structs (list name, open count, closed count). This gives both JSON and table formatters a clean data structure to work with.
|
||||
|
||||
**Alternative considered:** Reusing existing `Board`/`List` types with card slices — rejected because the status output is a derived summary, not raw API data.
|
||||
|
||||
### 4. Table output uses per-board sections
|
||||
In table format, each board is rendered as a headed section with a list/cards table beneath it. A total board count line appears first. This matches the natural reading pattern for a summary view.
|
||||
|
||||
### 5. JSON output uses the standard envelope
|
||||
JSON output wraps the `StatusSummary` in the existing `{"data": ..., "error": null}` envelope, consistent with all other commands.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **N+1 API calls** — One `ListBoards` call plus one `GetBoard` per board. For typical usage (< 20 boards) this is fine. If it becomes a problem, concurrent fetching can be added later without changing the interface. → Mitigation: defer optimization until needed.
|
||||
- **Stale data between calls** — Board state could change between sequential `GetBoard` calls. → Mitigation: acceptable for a summary view; not a transactional operation.
|
||||
@@ -0,0 +1,30 @@
|
||||
## Why
|
||||
|
||||
There is no quick way to get an overview of the state of all boards in Planka from the CLI. Users currently need to run multiple commands (`board list`, then `board get` per board, then mentally tally cards) to understand what's where. A single `pcli status` command provides an at-a-glance summary — similar in spirit to `git status`.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add a new top-level `pcli status` command that:
|
||||
- Lists the total number of boards
|
||||
- For each board, displays a table of its lists with card counts
|
||||
- Card counts show open cards as the primary number, with closed cards in parentheses (e.g., `12 (2 closed)`)
|
||||
- Empty lists are shown with `0` cards
|
||||
- Respects the global `--format` flag: JSON by default, table when `--format table` is specified
|
||||
- Uses existing `ListBoards` and `GetBoard` client methods — no new API endpoints needed
|
||||
- Sequential board fetching (one `GetBoard` call per board)
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `status-command`: The top-level `pcli status` command, its data aggregation logic, and its output formatting (both JSON and table)
|
||||
|
||||
### Modified Capabilities
|
||||
- `cli-commands`: Adding the new `status` top-level command to the CLI command tree
|
||||
|
||||
## Impact
|
||||
|
||||
- **New file**: `cmd/status.go` — command definition and aggregation logic
|
||||
- **Modified file**: `output/output.go` — table rendering for the status summary type
|
||||
- **Modified file**: `model/types.go` — new types for the status summary data structure
|
||||
- No new dependencies required
|
||||
- No breaking changes
|
||||
@@ -0,0 +1,16 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Status command
|
||||
The system SHALL provide a top-level `status` command registered directly on the root command (not as a subcommand of any resource group). `pcli status` SHALL take no positional arguments. The command SHALL fetch all boards via `ListBoards`, then fetch each board's details via `GetBoard` sequentially, aggregate card counts per list, and output the result via the standard `output.Print` mechanism respecting the global `--format` flag.
|
||||
|
||||
#### Scenario: Run status command
|
||||
- **WHEN** `pcli status` is executed
|
||||
- **THEN** the system SHALL output a summary of all boards with their lists and card counts
|
||||
|
||||
#### Scenario: Status command respects format flag
|
||||
- **WHEN** `pcli status --format table` is executed
|
||||
- **THEN** the output SHALL be in table format
|
||||
|
||||
#### Scenario: Status command default format
|
||||
- **WHEN** `pcli status` is executed without a `--format` flag
|
||||
- **THEN** the output SHALL be in JSON envelope format
|
||||
@@ -0,0 +1,53 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Status command summary output
|
||||
The system SHALL provide a top-level `pcli status` command that outputs a summary of all boards, their lists, and card counts. The summary SHALL include the total number of boards. For each board, the summary SHALL include the board name and a breakdown of each list within that board showing the list name, the number of open cards (where `isClosed` is false), and the number of closed cards (where `isClosed` is true). Empty lists SHALL be included in the output with 0 open and 0 closed cards.
|
||||
|
||||
#### Scenario: Status with multiple boards and lists
|
||||
- **WHEN** `pcli status` is executed and there are boards with lists containing cards
|
||||
- **THEN** the output SHALL include the total board count
|
||||
- **AND** each board SHALL list all its lists with open and closed card counts
|
||||
|
||||
#### Scenario: Status with empty lists
|
||||
- **WHEN** a board contains a list with no cards
|
||||
- **THEN** that list SHALL appear in the output with 0 open cards and 0 closed cards
|
||||
|
||||
#### Scenario: Status with no boards
|
||||
- **WHEN** `pcli status` is executed and there are no boards
|
||||
- **THEN** the output SHALL indicate 0 boards
|
||||
|
||||
#### Scenario: Status with closed cards
|
||||
- **WHEN** a list contains both open and closed cards
|
||||
- **THEN** the open card count SHALL exclude closed cards
|
||||
- **AND** the closed card count SHALL be shown separately
|
||||
|
||||
### Requirement: Status command JSON output
|
||||
The system SHALL output the status summary in the standard JSON envelope format (`{"data": ..., "error": null}`) when `--format json` is used or no format flag is provided. The `data` field SHALL contain an object with `totalBoards` (integer) and `boards` (array). Each board object SHALL contain `id` (string), `name` (string), and `lists` (array). Each list object SHALL contain `id` (string), `name` (string), `openCards` (integer), and `closedCards` (integer).
|
||||
|
||||
#### Scenario: JSON output structure
|
||||
- **WHEN** `pcli status` is executed with `--format json` or no format flag
|
||||
- **THEN** the output SHALL be a JSON envelope with the status summary as the `data` field
|
||||
|
||||
#### Scenario: JSON output field types
|
||||
- **WHEN** the JSON output is parsed
|
||||
- **THEN** `totalBoards` SHALL be an integer
|
||||
- **AND** each list's `openCards` and `closedCards` SHALL be integers
|
||||
|
||||
### Requirement: Status command table output
|
||||
The system SHALL output the status summary in a human-readable table format when `--format table` is specified. The table output SHALL begin with a line showing the total number of boards (e.g., `3 boards`). For each board, the output SHALL display a board header line (e.g., `Board: Sprint Planning`) followed by a table with columns `LIST` and `CARDS`. The `CARDS` column SHALL display the open card count, and if there are closed cards, append ` (<n> closed)` (e.g., `12 (2 closed)`). If there are no closed cards, only the open count SHALL be displayed (e.g., `12`).
|
||||
|
||||
#### Scenario: Table output with closed cards
|
||||
- **WHEN** `pcli status --format table` is executed and a list has 12 open and 2 closed cards
|
||||
- **THEN** the CARDS column for that list SHALL display `12 (2 closed)`
|
||||
|
||||
#### Scenario: Table output with no closed cards
|
||||
- **WHEN** `pcli status --format table` is executed and a list has 5 open and 0 closed cards
|
||||
- **THEN** the CARDS column for that list SHALL display `5`
|
||||
|
||||
#### Scenario: Table output with empty list
|
||||
- **WHEN** `pcli status --format table` is executed and a list has 0 open and 0 closed cards
|
||||
- **THEN** the CARDS column for that list SHALL display `0`
|
||||
|
||||
#### Scenario: Table output board count line
|
||||
- **WHEN** `pcli status --format table` is executed
|
||||
- **THEN** the first line of output SHALL show the total board count (e.g., `3 boards`)
|
||||
@@ -0,0 +1,17 @@
|
||||
## 1. Model Types
|
||||
|
||||
- [x] 1.1 Add `StatusSummary`, `BoardSummary`, and `ListSummary` structs to `model/types.go`
|
||||
|
||||
## 2. Command Implementation
|
||||
|
||||
- [x] 2.1 Create `cmd/status.go` with the top-level `pcli status` command registered on `rootCmd`
|
||||
- [x] 2.2 Implement the command handler: call `ListBoards`, then `GetBoard` per board, aggregate open/closed card counts per list, build `StatusSummary`, and pass to `output.Print`
|
||||
|
||||
## 3. Output Formatting
|
||||
|
||||
- [x] 3.1 Add `printStatusTable` function to `output/output.go` rendering the board count header line, per-board sections with LIST/CARDS columns, and closed card counts in parentheses
|
||||
- [x] 3.2 Register `StatusSummary` in the `printTable` dispatch logic (both direct and pointer cases)
|
||||
|
||||
## 4. Verification
|
||||
|
||||
- [x] 4.1 Build and manually test `pcli status` with `--format json` (default) and `--format table`
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-11
|
||||
@@ -0,0 +1,43 @@
|
||||
## Context
|
||||
|
||||
pcli currently authenticates to the Planka API using two mechanisms:
|
||||
1. **Bearer token** — `Authorization: Bearer <jwt>` header on every request
|
||||
2. **OIDC mode** — Bearer token + `httpOnlyToken` cookie sent together
|
||||
|
||||
Both rely on session-based JWTs obtained externally. The `Client` struct carries both `Token` and `HttpOnlyToken` fields, and the `Do()` method conditionally attaches the cookie. The CLI exposes `PLANKA_TOKEN`, `--token`, `PLANKA_HTTP_TOKEN`, and `--http-token`.
|
||||
|
||||
Planka now supports user-level API keys authenticated via the `x-api-key` header. This is simpler, long-lived, and eliminates the dual-mode complexity.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Replace all authentication with a single `x-api-key` header
|
||||
- Simplify the `Client` struct and constructor
|
||||
- Rename env var to `PLANKA_API_KEY` and flag to `--api-key`
|
||||
- Remove all OIDC/httpOnlyToken support
|
||||
|
||||
**Non-Goals:**
|
||||
- Supporting both old and new auth simultaneously (no transition period)
|
||||
- API key management (creation/rotation/deletion) via pcli
|
||||
- Any changes to API endpoint paths or request/response formats
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: Use `x-api-key` header directly
|
||||
**Choice**: Set `x-api-key: <key>` header instead of `Authorization: Bearer <key>`.
|
||||
**Rationale**: This is the header format documented by Planka for API key auth. Using the standard Planka format ensures compatibility.
|
||||
**Alternatives considered**: Using `Authorization: Bearer <api-key>` — rejected because Planka's API key auth specifically uses the `x-api-key` header.
|
||||
|
||||
### Decision 2: Clean break, no backward compatibility
|
||||
**Choice**: Remove `PLANKA_TOKEN` and `--token` entirely, replace with `PLANKA_API_KEY` and `--api-key`.
|
||||
**Rationale**: Supporting both old and new env vars adds complexity for no real benefit. This is a CLI tool where users control their own environment. A clean break with clear error messages is simpler.
|
||||
**Alternatives considered**: Supporting both `PLANKA_TOKEN` and `PLANKA_API_KEY` with deprecation warnings — rejected as unnecessary complexity for a CLI tool.
|
||||
|
||||
### Decision 3: Single field on Client struct
|
||||
**Choice**: Replace `Token` + `HttpOnlyToken` fields with a single `APIKey` field.
|
||||
**Rationale**: API key auth has no secondary credential. One field, one header, one code path.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Breaking change for all users** → Mitigated by clear naming (`PLANKA_API_KEY`) and error message that tells users what's needed. Users must generate an API key in Planka before upgrading.
|
||||
- **API key shown only once at creation** → Not a pcli concern, but worth noting in README that users should store their key securely.
|
||||
@@ -0,0 +1,31 @@
|
||||
## Why
|
||||
|
||||
Planka now supports user-level API key authentication. The current pcli authentication uses session-based JWT tokens (via `Authorization: Bearer <token>`) with an optional OIDC httpOnlyToken cookie path. API keys are simpler, long-lived, and eliminate the need for multiple auth modes. Replacing the current auth with API key auth simplifies both the codebase and the user experience.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **BREAKING**: Replace `Authorization: Bearer <token>` header with `x-api-key: <key>` header on all API requests
|
||||
- **BREAKING**: Rename environment variable `PLANKA_TOKEN` → `PLANKA_API_KEY`
|
||||
- **BREAKING**: Rename CLI flag `--token` → `--api-key`
|
||||
- Remove `PLANKA_HTTP_TOKEN` environment variable support
|
||||
- Remove `--http-token` CLI flag
|
||||
- Remove `HttpOnlyToken` field from the `Client` struct and all OIDC cookie logic
|
||||
- Simplify `NewClient` constructor to accept only base URL, API key, and logger
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
(none)
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `api-client`: Authentication header changes from `Authorization: Bearer` to `x-api-key`. Client struct drops `HttpOnlyToken` field. `NewClient` signature simplifies. OIDC cookie logic removed.
|
||||
- `cli-commands`: Root command global flags change: `--token` → `--api-key`, `--http-token` removed. Environment variable changes: `PLANKA_TOKEN` → `PLANKA_API_KEY`, `PLANKA_HTTP_TOKEN` removed.
|
||||
|
||||
## Impact
|
||||
|
||||
- **Code**: `client/client.go` (struct, constructor, `Do()` method), `cmd/root.go` (flags, env vars, client init)
|
||||
- **Users**: All existing users must update their environment variables and any scripts from `PLANKA_TOKEN` to `PLANKA_API_KEY` and generate an API key in Planka
|
||||
- **Dependencies**: No dependency changes
|
||||
- **API**: No Planka API endpoint changes — only the authentication mechanism used by pcli changes
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Base HTTP client
|
||||
The system SHALL provide a base HTTP client that sends requests to the Planka API. The client SHALL construct URLs by joining the configured base URL with the API path. The client SHALL attach an `x-api-key: <key>` header to every request. The client SHALL send and receive JSON (`Content-Type: application/json`). The client SHALL accept a `*slog.Logger` and log every request at DEBUG level with method, path, status code, and duration. The client SHALL log errors at WARN level.
|
||||
|
||||
#### Scenario: Successful API request
|
||||
- **WHEN** the client sends a request to a valid endpoint
|
||||
- **THEN** the response body SHALL be returned as parsed JSON
|
||||
- **AND** the request SHALL include the `x-api-key` header
|
||||
- **AND** a DEBUG log entry SHALL be emitted with method, path, status, and duration
|
||||
|
||||
#### Scenario: API returns error status
|
||||
- **WHEN** the API responds with a 4xx or 5xx status code
|
||||
- **THEN** the client SHALL return an `APIError` containing the HTTP status code and response message
|
||||
- **AND** a WARN log entry SHALL be emitted
|
||||
|
||||
#### Scenario: Network failure
|
||||
- **WHEN** the HTTP request fails due to a network error (connection refused, timeout, DNS failure)
|
||||
- **THEN** the client SHALL return a Go error wrapping the underlying network error
|
||||
|
||||
### Requirement: Authentication from environment
|
||||
The system SHALL read `PLANKA_URL` from the environment to determine the API base URL. The system SHALL read `PLANKA_API_KEY` from the environment to determine the API key. Global flags `--url` and `--api-key` SHALL override the corresponding environment variables. Flag values SHALL take precedence over environment variables.
|
||||
|
||||
#### Scenario: Auth from environment variables
|
||||
- **WHEN** `PLANKA_URL` and `PLANKA_API_KEY` are set in the environment
|
||||
- **AND** no `--url` or `--api-key` flags are provided
|
||||
- **THEN** the client SHALL use the environment variable values
|
||||
|
||||
#### Scenario: Flag overrides environment
|
||||
- **WHEN** `--url` or `--api-key` flags are provided
|
||||
- **THEN** the flag values SHALL take precedence over environment variables
|
||||
|
||||
#### Scenario: Missing configuration
|
||||
- **WHEN** neither the environment variable nor the flag is set for URL or API key
|
||||
- **THEN** the system SHALL print an error message and exit with code 1
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Root command and global flags
|
||||
The system SHALL provide a root command `pcli` that serves as the entry point. The root command SHALL register global flags: `--format` (string, default `json`, values `json` or `table`), `--url` (string, overrides `PLANKA_URL`), `--api-key` (string, overrides `PLANKA_API_KEY`), and `--log-level` (string, default `warn`, values `debug`, `info`, `warn`, `error`). The root command SHALL initialize the logger based on `--log-level` and configure it to write structured JSON to stderr. The root command SHALL validate that URL and API key are available (from flags or environment) before executing any subcommand.
|
||||
|
||||
#### Scenario: Display help
|
||||
- **WHEN** `pcli` is run with no arguments or `--help`
|
||||
- **THEN** the system SHALL display usage information listing all resource subcommands and global flags
|
||||
|
||||
#### Scenario: Invalid format flag
|
||||
- **WHEN** `--format` is set to an unsupported value
|
||||
- **THEN** the system SHALL print an error and exit with code 1
|
||||
|
||||
#### Scenario: Log level controls output
|
||||
- **WHEN** `--log-level=debug` is set
|
||||
- **THEN** DEBUG-level log entries SHALL appear on stderr
|
||||
- **AND** stdout SHALL contain only the command's data output
|
||||
|
||||
#### Scenario: Missing API key
|
||||
- **WHEN** neither `PLANKA_API_KEY` environment variable nor `--api-key` flag is provided
|
||||
- **THEN** the system SHALL print an error indicating `PLANKA_API_KEY` must be set via `--api-key` flag or `PLANKA_API_KEY` environment variable
|
||||
- **AND** the system SHALL exit with code 1
|
||||
@@ -0,0 +1,20 @@
|
||||
## 1. Client Authentication
|
||||
|
||||
- [x] 1.1 Rename `Token` and `HttpOnlyToken` fields to single `APIKey` field in `Client` struct in `client/client.go`
|
||||
- [x] 1.2 Update `NewClient` constructor to accept `(baseURL, apiKey string, logger *slog.Logger)` — remove `httpOnlyToken` parameter
|
||||
- [x] 1.3 Replace `Authorization: Bearer` header with `x-api-key` header in `Do()` method
|
||||
- [x] 1.4 Remove OIDC cookie logic (`httpOnlyToken` cookie attachment) from `Do()` method
|
||||
|
||||
## 2. CLI Flags and Environment Variables
|
||||
|
||||
- [x] 2.1 Rename `flagToken` to `flagAPIKey` and `flagHttpToken` removal in `cmd/root.go`
|
||||
- [x] 2.2 Replace `--token` flag with `--api-key` flag and remove `--http-token` flag in `cmd/root.go`
|
||||
- [x] 2.3 Replace `PLANKA_TOKEN` env var lookup with `PLANKA_API_KEY` in `PersistentPreRunE`
|
||||
- [x] 2.4 Remove `PLANKA_HTTP_TOKEN` env var lookup from `PersistentPreRunE`
|
||||
- [x] 2.5 Update error message to reference `PLANKA_API_KEY` and `--api-key`
|
||||
- [x] 2.6 Update `NewClient` call site in `PersistentPreRunE` to match new constructor signature
|
||||
- [x] 2.7 Remove OIDC-related debug log line (`oidc_mode` slog field)
|
||||
|
||||
## 3. Documentation
|
||||
|
||||
- [x] 3.1 Update `README.md` authentication section to reference `PLANKA_API_KEY` and `--api-key`
|
||||
@@ -0,0 +1,20 @@
|
||||
schema: spec-driven
|
||||
|
||||
# Project context (optional)
|
||||
# This is shown to AI when creating artifacts.
|
||||
# Add your tech stack, conventions, style guides, domain knowledge, etc.
|
||||
# Example:
|
||||
# context: |
|
||||
# Tech stack: TypeScript, React, Node.js
|
||||
# We use conventional commits
|
||||
# Domain: e-commerce platform
|
||||
|
||||
# Per-artifact rules (optional)
|
||||
# Add custom rules for specific artifacts.
|
||||
# Example:
|
||||
# rules:
|
||||
# proposal:
|
||||
# - Keep proposals under 500 words
|
||||
# - Always include a "Non-goals" section
|
||||
# tasks:
|
||||
# - Break tasks into chunks of max 2 hours
|
||||
@@ -0,0 +1,195 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Base HTTP client
|
||||
The system SHALL provide a base HTTP client that sends requests to the Planka API. The client SHALL construct URLs by joining the configured base URL with the API path. The client SHALL attach an `x-api-key: <key>` header to every request. The client SHALL send and receive JSON (`Content-Type: application/json`). The client SHALL accept a `*slog.Logger` and log every request at DEBUG level with method, path, status code, and duration. The client SHALL log errors at WARN level.
|
||||
|
||||
#### Scenario: Successful API request
|
||||
- **WHEN** the client sends a request to a valid endpoint
|
||||
- **THEN** the response body SHALL be returned as parsed JSON
|
||||
- **AND** the request SHALL include the `x-api-key` header
|
||||
- **AND** a DEBUG log entry SHALL be emitted with method, path, status, and duration
|
||||
|
||||
#### Scenario: API returns error status
|
||||
- **WHEN** the API responds with a 4xx or 5xx status code
|
||||
- **THEN** the client SHALL return an `APIError` containing the HTTP status code and response message
|
||||
- **AND** a WARN log entry SHALL be emitted
|
||||
|
||||
#### Scenario: Network failure
|
||||
- **WHEN** the HTTP request fails due to a network error (connection refused, timeout, DNS failure)
|
||||
- **THEN** the client SHALL return a Go error wrapping the underlying network error
|
||||
|
||||
### Requirement: Authentication from environment
|
||||
The system SHALL read `PLANKA_URL` from the environment to determine the API base URL. The system SHALL read `PLANKA_API_KEY` from the environment to determine the API key. Global flags `--url` and `--api-key` SHALL override the corresponding environment variables. Flag values SHALL take precedence over environment variables.
|
||||
|
||||
#### Scenario: Auth from environment variables
|
||||
- **WHEN** `PLANKA_URL` and `PLANKA_API_KEY` are set in the environment
|
||||
- **AND** no `--url` or `--api-key` flags are provided
|
||||
- **THEN** the client SHALL use the environment variable values
|
||||
|
||||
#### Scenario: Flag overrides environment
|
||||
- **WHEN** `--url` or `--api-key` flags are provided
|
||||
- **THEN** the flag values SHALL take precedence over environment variables
|
||||
|
||||
#### Scenario: Missing configuration
|
||||
- **WHEN** neither the environment variable nor the flag is set for URL or API key
|
||||
- **THEN** the system SHALL print an error message and exit with code 1
|
||||
|
||||
### Requirement: Cursor-based pagination
|
||||
The system SHALL implement cursor-based pagination for all list endpoints that support it. Paginated endpoints are: `GET /boards/{boardId}/actions` (`beforeId`), `GET /cards/{cardId}/actions` (`beforeId`), `GET /lists/{listId}/cards` (`before`), `GET /cards/{cardId}/comments` (`beforeId`). List methods SHALL accept a `limit` parameter. When `limit` is 0, the client SHALL fetch all pages. When `limit` is greater than 0, the client SHALL stop fetching after accumulating at least `limit` items and truncate the result to exactly `limit` items. Each page fetch SHALL be logged at DEBUG level.
|
||||
|
||||
#### Scenario: Fetch all pages
|
||||
- **WHEN** a list method is called with limit 0
|
||||
- **THEN** the client SHALL fetch pages until an empty page is returned
|
||||
- **AND** all items from all pages SHALL be returned
|
||||
|
||||
#### Scenario: Fetch with limit
|
||||
- **WHEN** a list method is called with limit N (N > 0)
|
||||
- **THEN** the client SHALL stop fetching after accumulating N or more items
|
||||
- **AND** the returned slice SHALL contain exactly N items
|
||||
|
||||
#### Scenario: Single page result
|
||||
- **WHEN** the API returns fewer items than a page size and no more pages exist
|
||||
- **THEN** the client SHALL return those items without making additional requests
|
||||
|
||||
### Requirement: Project operations
|
||||
The client SHALL provide methods to list all accessible projects (`GET /projects`) and get a single project by ID (`GET /projects/{id}`).
|
||||
|
||||
#### Scenario: List projects
|
||||
- **WHEN** `ListProjects` is called
|
||||
- **THEN** the client SHALL send `GET /projects` and return a slice of Project models
|
||||
|
||||
#### Scenario: Get project
|
||||
- **WHEN** `GetProject` is called with a project ID
|
||||
- **THEN** the client SHALL send `GET /projects/{id}` and return a Project model
|
||||
|
||||
### Requirement: Board operations
|
||||
The client SHALL provide a method to get a single board by ID (`GET /boards/{id}`) and list board actions (`GET /boards/{boardId}/actions`) with pagination support.
|
||||
|
||||
#### Scenario: Get board
|
||||
- **WHEN** `GetBoard` is called with a board ID
|
||||
- **THEN** the client SHALL send `GET /boards/{id}` and return a Board model including its included lists
|
||||
|
||||
#### Scenario: List board actions
|
||||
- **WHEN** `ListBoardActions` is called with a board ID and limit
|
||||
- **THEN** the client SHALL send paginated `GET /boards/{boardId}/actions` requests and return a slice of Action models
|
||||
|
||||
### Requirement: Card CRUD operations
|
||||
The client SHALL provide methods for: get card (`GET /cards/{id}`), create card (`POST /lists/{listId}/cards`), update card (`PATCH /cards/{id}`), delete card (`DELETE /cards/{id}`), and duplicate card (`POST /cards/{id}/duplicate`). The client SHALL provide a method to list cards in a list (`GET /lists/{listId}/cards`) with pagination support. The client SHALL provide a method to list card actions (`GET /cards/{cardId}/actions`) with pagination support.
|
||||
|
||||
#### Scenario: Get card
|
||||
- **WHEN** `GetCard` is called with a card ID
|
||||
- **THEN** the client SHALL send `GET /cards/{id}` and return a Card model
|
||||
|
||||
#### Scenario: Create card
|
||||
- **WHEN** `CreateCard` is called with a list ID and card fields (name, description, type, position, dueDate, isDueCompleted)
|
||||
- **THEN** the client SHALL send `POST /lists/{listId}/cards` with the provided fields and return the created Card
|
||||
|
||||
#### Scenario: Update card
|
||||
- **WHEN** `UpdateCard` is called with a card ID and update fields
|
||||
- **THEN** the client SHALL send `PATCH /cards/{id}` with only the provided fields and return the updated Card
|
||||
|
||||
#### Scenario: Delete card
|
||||
- **WHEN** `DeleteCard` is called with a card ID
|
||||
- **THEN** the client SHALL send `DELETE /cards/{id}`
|
||||
|
||||
#### Scenario: Duplicate card
|
||||
- **WHEN** `DuplicateCard` is called with a card ID, name, and position
|
||||
- **THEN** the client SHALL send `POST /cards/{id}/duplicate` and return the new Card
|
||||
|
||||
#### Scenario: List cards in list
|
||||
- **WHEN** `ListCards` is called with a list ID and limit
|
||||
- **THEN** the client SHALL send paginated `GET /lists/{listId}/cards` requests and return a slice of Card models
|
||||
|
||||
#### Scenario: List card actions
|
||||
- **WHEN** `ListCardActions` is called with a card ID and limit
|
||||
- **THEN** the client SHALL send paginated `GET /cards/{cardId}/actions` requests and return a slice of Action models
|
||||
|
||||
### Requirement: Comment operations
|
||||
The client SHALL provide methods for: list comments (`GET /cards/{cardId}/comments`) with pagination, create comment (`POST /cards/{cardId}/comments`), update comment (`PATCH /comments/{id}`), and delete comment (`DELETE /comments/{id}`).
|
||||
|
||||
#### Scenario: List comments
|
||||
- **WHEN** `ListComments` is called with a card ID and limit
|
||||
- **THEN** the client SHALL send paginated `GET /cards/{cardId}/comments` requests and return a slice of Comment models
|
||||
|
||||
#### Scenario: Create comment
|
||||
- **WHEN** `CreateComment` is called with a card ID and text
|
||||
- **THEN** the client SHALL send `POST /cards/{cardId}/comments` with the text and return the created Comment
|
||||
|
||||
#### Scenario: Update comment
|
||||
- **WHEN** `UpdateComment` is called with a comment ID and text
|
||||
- **THEN** the client SHALL send `PATCH /comments/{id}` and return the updated Comment
|
||||
|
||||
#### Scenario: Delete comment
|
||||
- **WHEN** `DeleteComment` is called with a comment ID
|
||||
- **THEN** the client SHALL send `DELETE /comments/{id}`
|
||||
|
||||
### Requirement: Task list operations
|
||||
The client SHALL provide methods for: create task list (`POST /cards/{cardId}/task-lists`), get task list (`GET /task-lists/{id}`), update task list (`PATCH /task-lists/{id}`), and delete task list (`DELETE /task-lists/{id}`).
|
||||
|
||||
#### Scenario: Create task list
|
||||
- **WHEN** `CreateTaskList` is called with a card ID and fields (name, position, showOnFrontOfCard, hideCompletedTasks)
|
||||
- **THEN** the client SHALL send `POST /cards/{cardId}/task-lists` and return the created TaskList
|
||||
|
||||
#### Scenario: Get task list
|
||||
- **WHEN** `GetTaskList` is called with a task list ID
|
||||
- **THEN** the client SHALL send `GET /task-lists/{id}` and return a TaskList model
|
||||
|
||||
#### Scenario: Update task list
|
||||
- **WHEN** `UpdateTaskList` is called with a task list ID and update fields
|
||||
- **THEN** the client SHALL send `PATCH /task-lists/{id}` and return the updated TaskList
|
||||
|
||||
#### Scenario: Delete task list
|
||||
- **WHEN** `DeleteTaskList` is called with a task list ID
|
||||
- **THEN** the client SHALL send `DELETE /task-lists/{id}`
|
||||
|
||||
### Requirement: Task operations
|
||||
The client SHALL provide methods for: create task (`POST /task-lists/{taskListId}/tasks`), update task (`PATCH /tasks/{id}`), and delete task (`DELETE /tasks/{id}`).
|
||||
|
||||
#### Scenario: Create task
|
||||
- **WHEN** `CreateTask` is called with a task list ID and fields (name, position, isCompleted, assigneeUserId, linkedCardId)
|
||||
- **THEN** the client SHALL send `POST /task-lists/{taskListId}/tasks` and return the created Task
|
||||
|
||||
#### Scenario: Update task
|
||||
- **WHEN** `UpdateTask` is called with a task ID and update fields (name, position, isCompleted, assigneeUserId, taskListId)
|
||||
- **THEN** the client SHALL send `PATCH /tasks/{id}` and return the updated Task
|
||||
|
||||
#### Scenario: Delete task
|
||||
- **WHEN** `DeleteTask` is called with a task ID
|
||||
- **THEN** the client SHALL send `DELETE /tasks/{id}`
|
||||
|
||||
### Requirement: Label operations
|
||||
The client SHALL provide methods for: create label (`POST /boards/{boardId}/labels`), update label (`PATCH /labels/{id}`), and delete label (`DELETE /labels/{id}`).
|
||||
|
||||
#### Scenario: Create label
|
||||
- **WHEN** `CreateLabel` is called with a board ID and fields (name, color, position)
|
||||
- **THEN** the client SHALL send `POST /boards/{boardId}/labels` and return the created Label
|
||||
|
||||
#### Scenario: Update label
|
||||
- **WHEN** `UpdateLabel` is called with a label ID and update fields (name, color, position)
|
||||
- **THEN** the client SHALL send `PATCH /labels/{id}` and return the updated Label
|
||||
|
||||
#### Scenario: Delete label
|
||||
- **WHEN** `DeleteLabel` is called with a label ID
|
||||
- **THEN** the client SHALL send `DELETE /labels/{id}`
|
||||
|
||||
### Requirement: Card label operations
|
||||
The client SHALL provide methods for: add label to card (`POST /cards/{cardId}/card-labels`) and remove label from card (`DELETE /cards/{cardId}/card-labels/labelId:{labelId}`).
|
||||
|
||||
#### Scenario: Add label to card
|
||||
- **WHEN** `AddCardLabel` is called with a card ID and label ID
|
||||
- **THEN** the client SHALL send `POST /cards/{cardId}/card-labels` with the label ID
|
||||
|
||||
#### Scenario: Remove label from card
|
||||
- **WHEN** `RemoveCardLabel` is called with a card ID and label ID
|
||||
- **THEN** the client SHALL send `DELETE /cards/{cardId}/card-labels/labelId:{labelId}`
|
||||
|
||||
### Requirement: Card membership operations
|
||||
The client SHALL provide methods for: assign user to card (`POST /cards/{cardId}/card-memberships`) and unassign user from card (`DELETE /cards/{cardId}/card-memberships/userId:{userId}`).
|
||||
|
||||
#### Scenario: Assign user to card
|
||||
- **WHEN** `AddCardMember` is called with a card ID and user ID
|
||||
- **THEN** the client SHALL send `POST /cards/{cardId}/card-memberships` with the user ID
|
||||
|
||||
#### Scenario: Unassign user from card
|
||||
- **WHEN** `RemoveCardMember` is called with a card ID and user ID
|
||||
- **THEN** the client SHALL send `DELETE /cards/{cardId}/card-memberships/userId:{userId}`
|
||||
@@ -0,0 +1,88 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Move card between lists
|
||||
The system SHALL provide a `card move` operation that updates a card's `listId` to move it to a different list. The operation SHALL accept an optional `position` to place the card at a specific position within the target list. The move operation SHALL be implemented as a `PATCH /cards/{id}` call with `listId` and optionally `position` fields. The CLI command SHALL be `pcli card move <id> --list <listId> [--position N]`.
|
||||
|
||||
#### Scenario: Move card to another list
|
||||
- **WHEN** `pcli card move <id> --list <targetListId>` is executed
|
||||
- **THEN** the system SHALL send `PATCH /cards/{id}` with `{"listId": "<targetListId>"}`
|
||||
- **AND** the system SHALL output the updated card with its new listId
|
||||
|
||||
#### Scenario: Move card to specific position
|
||||
- **WHEN** `pcli card move <id> --list <targetListId> --position 0` is executed
|
||||
- **THEN** the system SHALL send `PATCH /cards/{id}` with `{"listId": "<targetListId>", "position": 0}`
|
||||
- **AND** the card SHALL appear at the specified position in the target list
|
||||
|
||||
#### Scenario: Move card missing list flag
|
||||
- **WHEN** `pcli card move <id>` is executed without `--list`
|
||||
- **THEN** the system SHALL print an error indicating `--list` is required and exit with code 1
|
||||
|
||||
### Requirement: Duplicate card
|
||||
The system SHALL provide a `card duplicate` operation that creates a copy of an existing card. The operation SHALL call `POST /cards/{id}/duplicate`. The CLI command SHALL be `pcli card duplicate <id> [--name <name>] [--position N]`. If `--name` is not provided, the API determines the name of the duplicate.
|
||||
|
||||
#### Scenario: Duplicate card with defaults
|
||||
- **WHEN** `pcli card duplicate <id>` is executed
|
||||
- **THEN** the system SHALL send `POST /cards/{id}/duplicate`
|
||||
- **AND** the system SHALL output the newly created duplicate card
|
||||
|
||||
#### Scenario: Duplicate card with custom name
|
||||
- **WHEN** `pcli card duplicate <id> --name "Copy of task"` is executed
|
||||
- **THEN** the system SHALL send `POST /cards/{id}/duplicate` with `{"name": "Copy of task"}`
|
||||
- **AND** the duplicate card SHALL have the specified name
|
||||
|
||||
### Requirement: Assign and unassign card members
|
||||
The system SHALL provide `card assign` and `card unassign` operations to manage card memberships. `card assign` SHALL call `POST /cards/{cardId}/card-memberships` with the user ID. `card unassign` SHALL call `DELETE /cards/{cardId}/card-memberships/userId:{userId}`.
|
||||
|
||||
#### Scenario: Assign user to card
|
||||
- **WHEN** `pcli card assign <cardId> --user <userId>` is executed
|
||||
- **THEN** the system SHALL send `POST /cards/{cardId}/card-memberships` with `{"userId": "<userId>"}`
|
||||
- **AND** the system SHALL output a success confirmation
|
||||
|
||||
#### Scenario: Unassign user from card
|
||||
- **WHEN** `pcli card unassign <cardId> --user <userId>` is executed
|
||||
- **THEN** the system SHALL send `DELETE /cards/{cardId}/card-memberships/userId:{userId}`
|
||||
- **AND** the system SHALL output a success confirmation
|
||||
|
||||
#### Scenario: Assign missing user flag
|
||||
- **WHEN** `pcli card assign <cardId>` is executed without `--user`
|
||||
- **THEN** the system SHALL print an error indicating `--user` is required and exit with code 1
|
||||
|
||||
### Requirement: Add and remove card labels
|
||||
The system SHALL provide `card add-label` and `card remove-label` operations to manage labels on cards. `card add-label` SHALL call `POST /cards/{cardId}/card-labels` with the label ID. `card remove-label` SHALL call `DELETE /cards/{cardId}/card-labels/labelId:{labelId}`.
|
||||
|
||||
#### Scenario: Add label to card
|
||||
- **WHEN** `pcli card add-label <cardId> --label <labelId>` is executed
|
||||
- **THEN** the system SHALL send `POST /cards/{cardId}/card-labels` with `{"labelId": "<labelId>"}`
|
||||
- **AND** the system SHALL output a success confirmation
|
||||
|
||||
#### Scenario: Remove label from card
|
||||
- **WHEN** `pcli card remove-label <cardId> --label <labelId>` is executed
|
||||
- **THEN** the system SHALL send `DELETE /cards/{cardId}/card-labels/labelId:{labelId}`
|
||||
- **AND** the system SHALL output a success confirmation
|
||||
|
||||
#### Scenario: Add label missing label flag
|
||||
- **WHEN** `pcli card add-label <cardId>` is executed without `--label`
|
||||
- **THEN** the system SHALL print an error indicating `--label` is required and exit with code 1
|
||||
|
||||
### Requirement: Enriched board-level card listing
|
||||
The system SHALL provide a `card list --board <id>` operation that returns all cards across all lists in a board, with each card enriched with the `listName` field. The operation SHALL: (1) call `GET /boards/{id}` to retrieve the board and its included lists, (2) call `GET /lists/{listId}/cards` for each list to retrieve cards (with pagination support), (3) inject `listName` into each card based on the list it belongs to. The `--limit` flag SHALL apply to the total number of cards returned across all lists.
|
||||
|
||||
#### Scenario: List all cards on a board
|
||||
- **WHEN** `pcli card list --board <id>` is executed
|
||||
- **THEN** the system SHALL return all cards from all lists in the board
|
||||
- **AND** each card SHALL include a `listName` field with the name of its containing list
|
||||
- **AND** each card SHALL include a `listId` field
|
||||
|
||||
#### Scenario: Board card listing with limit
|
||||
- **WHEN** `pcli card list --board <id> --limit 10` is executed
|
||||
- **THEN** the system SHALL return at most 10 cards total across all lists
|
||||
- **AND** each card SHALL include the `listName` field
|
||||
|
||||
#### Scenario: Board with no cards
|
||||
- **WHEN** `pcli card list --board <id>` is executed on a board with empty lists
|
||||
- **THEN** the system SHALL return an empty array
|
||||
|
||||
#### Scenario: Board with multiple lists
|
||||
- **WHEN** a board has lists "To Do", "In Progress", and "Done" each containing cards
|
||||
- **THEN** the returned cards SHALL have `listName` set to the respective list name
|
||||
- **AND** cards from all lists SHALL be included in the result
|
||||
@@ -0,0 +1,208 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Root command and global flags
|
||||
The system SHALL provide a root command `pcli` that serves as the entry point. The root command SHALL register global flags: `--format` (string, default `json`, values `json` or `table`), `--url` (string, overrides `PLANKA_URL`), `--api-key` (string, overrides `PLANKA_API_KEY`), and `--log-level` (string, default `warn`, values `debug`, `info`, `warn`, `error`). The root command SHALL initialize the logger based on `--log-level` and configure it to write structured JSON to stderr. The root command SHALL validate that URL and API key are available (from flags or environment) before executing any subcommand.
|
||||
|
||||
#### Scenario: Display help
|
||||
- **WHEN** `pcli` is run with no arguments or `--help`
|
||||
- **THEN** the system SHALL display usage information listing all resource subcommands and global flags
|
||||
|
||||
#### Scenario: Invalid format flag
|
||||
- **WHEN** `--format` is set to an unsupported value
|
||||
- **THEN** the system SHALL print an error and exit with code 1
|
||||
|
||||
#### Scenario: Log level controls output
|
||||
- **WHEN** `--log-level=debug` is set
|
||||
- **THEN** DEBUG-level log entries SHALL appear on stderr
|
||||
- **AND** stdout SHALL contain only the command's data output
|
||||
|
||||
#### Scenario: Missing API key
|
||||
- **WHEN** neither `PLANKA_API_KEY` environment variable nor `--api-key` flag is provided
|
||||
- **THEN** the system SHALL print an error indicating `PLANKA_API_KEY` must be set via `--api-key` flag or `PLANKA_API_KEY` environment variable
|
||||
- **AND** the system SHALL exit with code 1
|
||||
|
||||
### Requirement: Project commands
|
||||
The system SHALL provide a `project` command group with subcommands `list` and `get`. `pcli project list` SHALL call the client's ListProjects method and output the result. `pcli project get <id>` SHALL accept a project ID as a positional argument, call GetProject, and output the result.
|
||||
|
||||
#### Scenario: List projects
|
||||
- **WHEN** `pcli project list` is executed
|
||||
- **THEN** the system SHALL output all accessible projects
|
||||
|
||||
#### Scenario: Get project by ID
|
||||
- **WHEN** `pcli project get <id>` is executed with a valid project ID
|
||||
- **THEN** the system SHALL output the project details
|
||||
|
||||
#### Scenario: Get project missing ID
|
||||
- **WHEN** `pcli project get` is executed without an ID argument
|
||||
- **THEN** the system SHALL print an error indicating the ID is required and exit with code 1
|
||||
|
||||
### Requirement: Board commands
|
||||
The system SHALL provide a `board` command group with subcommands `get` and `actions`. `pcli board get <id>` SHALL accept a board ID as a positional argument and output the board details. `pcli board actions <id>` SHALL accept a board ID and an optional `--limit` flag (int, default 0) and output the board's action history.
|
||||
|
||||
#### Scenario: Get board
|
||||
- **WHEN** `pcli board get <id>` is executed
|
||||
- **THEN** the system SHALL output the board details including its lists
|
||||
|
||||
#### Scenario: List board actions
|
||||
- **WHEN** `pcli board actions <id>` is executed
|
||||
- **THEN** the system SHALL output the board's action history
|
||||
|
||||
#### Scenario: List board actions with limit
|
||||
- **WHEN** `pcli board actions <id> --limit 10` is executed
|
||||
- **THEN** the system SHALL output at most 10 action entries
|
||||
|
||||
### Requirement: Card commands
|
||||
The system SHALL provide a `card` command group with subcommands: `list`, `get`, `create`, `update`, `delete`, `duplicate`, `move`, `assign`, `unassign`, `add-label`, `remove-label`, `actions`.
|
||||
|
||||
`pcli card list` SHALL require either `--board <id>` or `--list <id>` flag and accept an optional `--limit` flag. `pcli card get <id>` SHALL accept a card ID as a positional argument. `pcli card create` SHALL require `--list <id>` and `--name <name>` flags and accept optional flags: `--description`, `--type`, `--position`, `--due-date`, `--due-completed`. `pcli card update <id>` SHALL accept a card ID and optional flags for each updatable field: `--name`, `--description`, `--type`, `--position`, `--due-date`, `--due-completed`. `pcli card delete <id>` SHALL accept a card ID. `pcli card duplicate <id>` SHALL accept a card ID and optional `--name` and `--position` flags. `pcli card move <id>` SHALL require `--list <id>` and accept optional `--position` flag. `pcli card assign <id>` SHALL require `--user <userId>`. `pcli card unassign <id>` SHALL require `--user <userId>`. `pcli card add-label <id>` SHALL require `--label <labelId>`. `pcli card remove-label <id>` SHALL require `--label <labelId>`. `pcli card actions <id>` SHALL accept an optional `--limit` flag.
|
||||
|
||||
#### Scenario: List cards by board
|
||||
- **WHEN** `pcli card list --board <id>` is executed
|
||||
- **THEN** the system SHALL output all cards across all lists in the board, each enriched with listName
|
||||
|
||||
#### Scenario: List cards by list
|
||||
- **WHEN** `pcli card list --list <id>` is executed
|
||||
- **THEN** the system SHALL output cards in the specified list
|
||||
|
||||
#### Scenario: List cards with limit
|
||||
- **WHEN** `pcli card list --list <id> --limit 5` is executed
|
||||
- **THEN** the system SHALL output at most 5 cards
|
||||
|
||||
#### Scenario: Card list missing board or list flag
|
||||
- **WHEN** `pcli card list` is executed without `--board` or `--list`
|
||||
- **THEN** the system SHALL print an error indicating one is required and exit with code 1
|
||||
|
||||
#### Scenario: Get card
|
||||
- **WHEN** `pcli card get <id>` is executed
|
||||
- **THEN** the system SHALL output the card details
|
||||
|
||||
#### Scenario: Create card
|
||||
- **WHEN** `pcli card create --list <id> --name "Task name"` is executed
|
||||
- **THEN** the system SHALL create the card and output the created card
|
||||
|
||||
#### Scenario: Create card missing required flags
|
||||
- **WHEN** `pcli card create` is executed without `--list` or `--name`
|
||||
- **THEN** the system SHALL print an error and exit with code 1
|
||||
|
||||
#### Scenario: Update card
|
||||
- **WHEN** `pcli card update <id> --name "New name"` is executed
|
||||
- **THEN** the system SHALL update the card and output the updated card
|
||||
|
||||
#### Scenario: Delete card
|
||||
- **WHEN** `pcli card delete <id>` is executed
|
||||
- **THEN** the system SHALL delete the card and output a success confirmation
|
||||
|
||||
#### Scenario: Duplicate card
|
||||
- **WHEN** `pcli card duplicate <id>` is executed
|
||||
- **THEN** the system SHALL duplicate the card and output the new card
|
||||
|
||||
#### Scenario: Move card
|
||||
- **WHEN** `pcli card move <id> --list <listId>` is executed
|
||||
- **THEN** the system SHALL update the card's listId (and optionally position) and output the updated card
|
||||
|
||||
#### Scenario: Assign user to card
|
||||
- **WHEN** `pcli card assign <id> --user <userId>` is executed
|
||||
- **THEN** the system SHALL add the user as a card member
|
||||
|
||||
#### Scenario: Unassign user from card
|
||||
- **WHEN** `pcli card unassign <id> --user <userId>` is executed
|
||||
- **THEN** the system SHALL remove the user from the card's members
|
||||
|
||||
#### Scenario: Add label to card
|
||||
- **WHEN** `pcli card add-label <id> --label <labelId>` is executed
|
||||
- **THEN** the system SHALL add the label to the card
|
||||
|
||||
#### Scenario: Remove label from card
|
||||
- **WHEN** `pcli card remove-label <id> --label <labelId>` is executed
|
||||
- **THEN** the system SHALL remove the label from the card
|
||||
|
||||
#### Scenario: List card actions
|
||||
- **WHEN** `pcli card actions <id>` is executed
|
||||
- **THEN** the system SHALL output the card's action history
|
||||
|
||||
### Requirement: Comment commands
|
||||
The system SHALL provide a `comment` command group with subcommands: `list`, `create`, `update`, `delete`. `pcli comment list` SHALL require `--card <id>` and accept optional `--limit`. `pcli comment create` SHALL require `--card <id>` and `--text <text>`. `pcli comment update <id>` SHALL require `--text <text>`. `pcli comment delete <id>` SHALL accept a comment ID.
|
||||
|
||||
#### Scenario: List comments
|
||||
- **WHEN** `pcli comment list --card <id>` is executed
|
||||
- **THEN** the system SHALL output comments for the card
|
||||
|
||||
#### Scenario: Create comment
|
||||
- **WHEN** `pcli comment create --card <id> --text "comment text"` is executed
|
||||
- **THEN** the system SHALL create the comment and output the created comment
|
||||
|
||||
#### Scenario: Update comment
|
||||
- **WHEN** `pcli comment update <id> --text "updated text"` is executed
|
||||
- **THEN** the system SHALL update the comment and output the updated comment
|
||||
|
||||
#### Scenario: Delete comment
|
||||
- **WHEN** `pcli comment delete <id>` is executed
|
||||
- **THEN** the system SHALL delete the comment and output a success confirmation
|
||||
|
||||
### Requirement: Task list commands
|
||||
The system SHALL provide a `task-list` command group with subcommands: `create`, `get`, `update`, `delete`. `pcli task-list create` SHALL require `--card <id>` and `--name <name>` and accept optional flags: `--position`, `--show-on-front`, `--hide-completed`. `pcli task-list get <id>` SHALL accept a task list ID. `pcli task-list update <id>` SHALL accept optional flags for each updatable field. `pcli task-list delete <id>` SHALL accept a task list ID.
|
||||
|
||||
#### Scenario: Create task list
|
||||
- **WHEN** `pcli task-list create --card <id> --name "Checklist"` is executed
|
||||
- **THEN** the system SHALL create the task list and output the created task list
|
||||
|
||||
#### Scenario: Get task list
|
||||
- **WHEN** `pcli task-list get <id>` is executed
|
||||
- **THEN** the system SHALL output the task list details including its tasks
|
||||
|
||||
#### Scenario: Update task list
|
||||
- **WHEN** `pcli task-list update <id> --name "Renamed"` is executed
|
||||
- **THEN** the system SHALL update the task list and output the updated task list
|
||||
|
||||
#### Scenario: Delete task list
|
||||
- **WHEN** `pcli task-list delete <id>` is executed
|
||||
- **THEN** the system SHALL delete the task list and output a success confirmation
|
||||
|
||||
### Requirement: Task commands
|
||||
The system SHALL provide a `task` command group with subcommands: `create`, `update`, `delete`. `pcli task create` SHALL require `--task-list <id>` and `--name <name>` and accept optional flags: `--position`, `--completed`, `--assignee`, `--linked-card`. `pcli task update <id>` SHALL accept optional flags for each updatable field: `--name`, `--position`, `--completed`, `--assignee`, `--task-list`. `pcli task delete <id>` SHALL accept a task ID.
|
||||
|
||||
#### Scenario: Create task
|
||||
- **WHEN** `pcli task create --task-list <id> --name "Do something"` is executed
|
||||
- **THEN** the system SHALL create the task and output the created task
|
||||
|
||||
#### Scenario: Update task
|
||||
- **WHEN** `pcli task update <id> --completed` is executed
|
||||
- **THEN** the system SHALL mark the task as completed and output the updated task
|
||||
|
||||
#### Scenario: Move task to different list
|
||||
- **WHEN** `pcli task update <id> --task-list <newListId>` is executed
|
||||
- **THEN** the system SHALL move the task to the specified task list
|
||||
|
||||
#### Scenario: Delete task
|
||||
- **WHEN** `pcli task delete <id>` is executed
|
||||
- **THEN** the system SHALL delete the task and output a success confirmation
|
||||
|
||||
### Requirement: Status command
|
||||
The system SHALL provide a top-level `status` command registered directly on the root command (not as a subcommand of any resource group). `pcli status` SHALL take no positional arguments. The command SHALL fetch all boards via `ListBoards`, then fetch each board's details via `GetBoard` sequentially, aggregate card counts per list, and output the result via the standard `output.Print` mechanism respecting the global `--format` flag.
|
||||
|
||||
#### Scenario: Run status command
|
||||
- **WHEN** `pcli status` is executed
|
||||
- **THEN** the system SHALL output a summary of all boards with their lists and card counts
|
||||
|
||||
#### Scenario: Status command respects format flag
|
||||
- **WHEN** `pcli status --format table` is executed
|
||||
- **THEN** the output SHALL be in table format
|
||||
|
||||
#### Scenario: Status command default format
|
||||
- **WHEN** `pcli status` is executed without a `--format` flag
|
||||
- **THEN** the output SHALL be in JSON envelope format
|
||||
|
||||
### Requirement: Label commands
|
||||
The system SHALL provide a `label` command group with subcommands: `create`, `update`, `delete`. `pcli label create` SHALL require `--board <id>` and `--name <name>` and accept optional flags: `--color`, `--position`. `pcli label update <id>` SHALL accept optional flags: `--name`, `--color`, `--position`. `pcli label delete <id>` SHALL accept a label ID.
|
||||
|
||||
#### Scenario: Create label
|
||||
- **WHEN** `pcli label create --board <id> --name "Bug" --color red` is executed
|
||||
- **THEN** the system SHALL create the label and output the created label
|
||||
|
||||
#### Scenario: Update label
|
||||
- **WHEN** `pcli label update <id> --name "Feature" --color green` is executed
|
||||
- **THEN** the system SHALL update the label and output the updated label
|
||||
|
||||
#### Scenario: Delete label
|
||||
- **WHEN** `pcli label delete <id>` is executed
|
||||
- **THEN** the system SHALL delete the label and output a success confirmation
|
||||
@@ -0,0 +1,66 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: JSON output envelope
|
||||
The system SHALL wrap all successful command output in a JSON envelope with the structure `{"data": <result>, "error": null}`. The system SHALL wrap all error output in a JSON envelope with the structure `{"data": null, "error": "<message>"}`. The envelope SHALL be written to stdout. The `data` field SHALL contain the direct result of the command (object or array). The `error` field SHALL be null on success and a string message on failure.
|
||||
|
||||
#### Scenario: Successful command output
|
||||
- **WHEN** a command completes successfully in JSON format
|
||||
- **THEN** stdout SHALL contain `{"data": <result>, "error": null}`
|
||||
- **AND** the process SHALL exit with code 0
|
||||
|
||||
#### Scenario: Error command output
|
||||
- **WHEN** a command fails in JSON format
|
||||
- **THEN** stdout SHALL contain `{"data": null, "error": "<message>"}`
|
||||
- **AND** the process SHALL exit with code 1
|
||||
|
||||
#### Scenario: Envelope structure is consistent
|
||||
- **WHEN** any command is executed in JSON format
|
||||
- **THEN** the output SHALL always contain exactly the keys `data` and `error` at the top level
|
||||
|
||||
### Requirement: Table format output
|
||||
The system SHALL support a `--format=table` flag that outputs results as human-readable tabular text. Table output SHALL be written to stdout. Table output SHALL NOT use the JSON envelope. When table format is active and an error occurs, the error message SHALL be written to stderr (not stdout). Each resource type SHALL define its own column set for table rendering.
|
||||
|
||||
#### Scenario: Table output for a list of items
|
||||
- **WHEN** a list command is executed with `--format=table`
|
||||
- **THEN** stdout SHALL contain a header row followed by one row per item
|
||||
- **AND** columns SHALL be aligned and separated by whitespace
|
||||
|
||||
#### Scenario: Table output for a single item
|
||||
- **WHEN** a get command is executed with `--format=table`
|
||||
- **THEN** stdout SHALL contain a key-value representation of the item
|
||||
|
||||
#### Scenario: Error in table format
|
||||
- **WHEN** a command fails with `--format=table`
|
||||
- **THEN** the error message SHALL be written to stderr
|
||||
- **AND** the process SHALL exit with code 1
|
||||
|
||||
### Requirement: Format flag default
|
||||
The system SHALL default to JSON format when no `--format` flag is provided. The `--format` flag SHALL accept values `json` and `table`. Any other value SHALL cause an error and exit with code 1.
|
||||
|
||||
#### Scenario: Default format is JSON
|
||||
- **WHEN** a command is executed without `--format`
|
||||
- **THEN** the output SHALL be in JSON envelope format
|
||||
|
||||
#### Scenario: Explicit JSON format
|
||||
- **WHEN** a command is executed with `--format=json`
|
||||
- **THEN** the output SHALL be in JSON envelope format
|
||||
|
||||
#### Scenario: Explicit table format
|
||||
- **WHEN** a command is executed with `--format=table`
|
||||
- **THEN** the output SHALL be in table format
|
||||
|
||||
#### Scenario: Invalid format value
|
||||
- **WHEN** a command is executed with `--format=xml`
|
||||
- **THEN** the system SHALL print an error and exit with code 1
|
||||
|
||||
### Requirement: Log output separation
|
||||
All log output SHALL be written to stderr using structured JSON via `log/slog`. Log output SHALL never appear on stdout. This ensures that stdout contains only the command's data output (JSON envelope or table) and is safe to pipe or parse programmatically.
|
||||
|
||||
#### Scenario: Logs do not pollute stdout
|
||||
- **WHEN** a command is executed with `--log-level=debug`
|
||||
- **THEN** all log entries SHALL appear on stderr
|
||||
- **AND** stdout SHALL contain only the command's data output
|
||||
|
||||
#### Scenario: Logs are structured JSON
|
||||
- **WHEN** a log entry is emitted
|
||||
- **THEN** it SHALL be a valid JSON object with at minimum `time`, `level`, and `msg` fields
|
||||
@@ -0,0 +1,53 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Status command summary output
|
||||
The system SHALL provide a top-level `pcli status` command that outputs a summary of all boards, their lists, and card counts. The summary SHALL include the total number of boards. For each board, the summary SHALL include the board name and a breakdown of each list within that board showing the list name, the number of open cards (where `isClosed` is false), and the number of closed cards (where `isClosed` is true). Empty lists SHALL be included in the output with 0 open and 0 closed cards.
|
||||
|
||||
#### Scenario: Status with multiple boards and lists
|
||||
- **WHEN** `pcli status` is executed and there are boards with lists containing cards
|
||||
- **THEN** the output SHALL include the total board count
|
||||
- **AND** each board SHALL list all its lists with open and closed card counts
|
||||
|
||||
#### Scenario: Status with empty lists
|
||||
- **WHEN** a board contains a list with no cards
|
||||
- **THEN** that list SHALL appear in the output with 0 open cards and 0 closed cards
|
||||
|
||||
#### Scenario: Status with no boards
|
||||
- **WHEN** `pcli status` is executed and there are no boards
|
||||
- **THEN** the output SHALL indicate 0 boards
|
||||
|
||||
#### Scenario: Status with closed cards
|
||||
- **WHEN** a list contains both open and closed cards
|
||||
- **THEN** the open card count SHALL exclude closed cards
|
||||
- **AND** the closed card count SHALL be shown separately
|
||||
|
||||
### Requirement: Status command JSON output
|
||||
The system SHALL output the status summary in the standard JSON envelope format (`{"data": ..., "error": null}`) when `--format json` is used or no format flag is provided. The `data` field SHALL contain an object with `totalBoards` (integer) and `boards` (array). Each board object SHALL contain `id` (string), `name` (string), and `lists` (array). Each list object SHALL contain `id` (string), `name` (string), `openCards` (integer), and `closedCards` (integer).
|
||||
|
||||
#### Scenario: JSON output structure
|
||||
- **WHEN** `pcli status` is executed with `--format json` or no format flag
|
||||
- **THEN** the output SHALL be a JSON envelope with the status summary as the `data` field
|
||||
|
||||
#### Scenario: JSON output field types
|
||||
- **WHEN** the JSON output is parsed
|
||||
- **THEN** `totalBoards` SHALL be an integer
|
||||
- **AND** each list's `openCards` and `closedCards` SHALL be integers
|
||||
|
||||
### Requirement: Status command table output
|
||||
The system SHALL output the status summary in a human-readable table format when `--format table` is specified. The table output SHALL begin with a line showing the total number of boards (e.g., `3 boards`). For each board, the output SHALL display a board header line (e.g., `Board: Sprint Planning`) followed by a table with columns `LIST` and `CARDS`. The `CARDS` column SHALL display the open card count, and if there are closed cards, append ` (<n> closed)` (e.g., `12 (2 closed)`). If there are no closed cards, only the open count SHALL be displayed (e.g., `12`).
|
||||
|
||||
#### Scenario: Table output with closed cards
|
||||
- **WHEN** `pcli status --format table` is executed and a list has 12 open and 2 closed cards
|
||||
- **THEN** the CARDS column for that list SHALL display `12 (2 closed)`
|
||||
|
||||
#### Scenario: Table output with no closed cards
|
||||
- **WHEN** `pcli status --format table` is executed and a list has 5 open and 0 closed cards
|
||||
- **THEN** the CARDS column for that list SHALL display `5`
|
||||
|
||||
#### Scenario: Table output with empty list
|
||||
- **WHEN** `pcli status --format table` is executed and a list has 0 open and 0 closed cards
|
||||
- **THEN** the CARDS column for that list SHALL display `0`
|
||||
|
||||
#### Scenario: Table output board count line
|
||||
- **WHEN** `pcli status --format table` is executed
|
||||
- **THEN** the first line of output SHALL show the total board count (e.g., `3 boards`)
|
||||
Reference in New Issue
Block a user