Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5016d4c39c | |||
| 7937266262 | |||
| 22d5848e1a | |||
| 94dffdf8fc | |||
| 46b03e1a22 | |||
| ad384fe749 | |||
| c15a48cda3 | |||
| c03d05734a | |||
| 442b6eafcf |
@@ -0,0 +1,45 @@
|
||||
---
|
||||
description: Reconcile Planka board state with OpenSpec changes
|
||||
allowed-tools: Bash, Read
|
||||
---
|
||||
|
||||
# Planka <-> OpenSpec Reconciliation Sync
|
||||
|
||||
Runs the `kanban-project-sync` script to reconcile Planka board state with OpenSpec changes.
|
||||
|
||||
## How It Works
|
||||
|
||||
The sync is handled by the `kanban-project-sync` bash script (on PATH). It:
|
||||
1. Checks Planka connectivity
|
||||
2. Bootstraps project/board/lists/label infrastructure (idempotent)
|
||||
3. Reads OpenSpec state and maps changes to board lists
|
||||
4. Creates/moves/updates Planka cards and task checklists
|
||||
5. Moves orphaned cards to Done
|
||||
|
||||
**OpenSpec is the source of truth.** Planka is a read-only projection. Sync is one-directional (OpenSpec -> Planka) and idempotent.
|
||||
|
||||
## Running the Sync
|
||||
|
||||
Read project config and invoke the script in background mode:
|
||||
|
||||
```bash
|
||||
PROJECT_NAME=$(yq -r '.planka.project' project.yaml)
|
||||
BOARD_NAME=$(yq -r '.planka.board' project.yaml)
|
||||
kanban-project-sync --project "$PROJECT_NAME" --board "$BOARD_NAME" --background
|
||||
```
|
||||
|
||||
The `--background` flag makes the script fire-and-forget — it detaches and logs to `/tmp/kanban-project-sync-<project>-<board>.log`.
|
||||
|
||||
## Concurrency
|
||||
|
||||
The script handles its own concurrency:
|
||||
- Uses `flock` to ensure only one sync runs per project-board pair
|
||||
- If a sync is already running, sets a pending flag and exits immediately
|
||||
- The running sync re-runs after completion if the pending flag is set
|
||||
- Multiple pending requests coalesce into a single re-run
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Sync is **best-effort** — if Planka is unreachable or the script fails, log a warning and continue
|
||||
- Never block agentic work because of sync
|
||||
- If `kanban-project-sync` is not on PATH, log a warning and skip
|
||||
@@ -0,0 +1,227 @@
|
||||
---
|
||||
name: kanban
|
||||
description: Manage Planka project boards using the pcli CLI. Use when the user wants to interact with Planka boards, cards, lists, tasks, labels, or comments.
|
||||
---
|
||||
|
||||
# pcli - Planka CLI
|
||||
|
||||
CLI for the Planka project management API. All commands return JSON by default with envelope `{"data": ..., "error": null}`. Use `jq` to extract fields.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Ensure environment variables are set:
|
||||
```bash
|
||||
export PLANKA_URL="https://planka.example.com"
|
||||
export PLANKA_API_KEY="your-api-key"
|
||||
```
|
||||
|
||||
Ensure `jq` and `pcli` are installed and in the path.
|
||||
|
||||
## Global Flags
|
||||
|
||||
All commands accept: `--format json|table`, `--url <url>`, `--api-key <key>`, `--log-level debug|info|warn|error`
|
||||
|
||||
## Commands
|
||||
|
||||
### Status Overview
|
||||
|
||||
```bash
|
||||
pcli status
|
||||
```
|
||||
|
||||
Returns summary of all boards, lists, and card counts (open/closed per list).
|
||||
|
||||
### Projects
|
||||
|
||||
```bash
|
||||
pcli project list
|
||||
pcli project get <project-id>
|
||||
```
|
||||
|
||||
### Boards
|
||||
|
||||
```bash
|
||||
pcli board list
|
||||
pcli board get <board-id> # includes lists and cards
|
||||
pcli board actions <board-id> [--limit N]
|
||||
```
|
||||
|
||||
### Cards
|
||||
|
||||
```bash
|
||||
# List (one of --board or --list required, mutually exclusive)
|
||||
pcli card list --board <board-id> [--limit N]
|
||||
pcli card list --list <list-id> [--limit N]
|
||||
|
||||
# CRUD
|
||||
pcli card get <card-id>
|
||||
pcli card create --list <list-id> --name "Name" [--description "..."] [--type project|story] [--position N] [--due-date "ISO8601"] [--due-completed]
|
||||
pcli card update <card-id> [--name "..."] [--description "..."] [--type ...] [--position N] [--due-date "..."] [--due-completed]
|
||||
pcli card delete <card-id>
|
||||
pcli card duplicate <card-id> --name "Copy" [--position N]
|
||||
pcli card move <card-id> --list <target-list-id> [--position N]
|
||||
|
||||
# Members
|
||||
pcli card assign <card-id> --user <user-id>
|
||||
pcli card unassign <card-id> --user <user-id>
|
||||
|
||||
# Labels
|
||||
pcli card add-label <card-id> --label <label-id>
|
||||
pcli card remove-label <card-id> --label <label-id>
|
||||
|
||||
# Actions
|
||||
pcli card actions <card-id> [--limit N]
|
||||
```
|
||||
|
||||
### Comments
|
||||
|
||||
```bash
|
||||
pcli comment list --card <card-id> [--limit N]
|
||||
pcli comment create --card <card-id> --text "..."
|
||||
pcli comment update <comment-id> --text "..."
|
||||
pcli comment delete <comment-id>
|
||||
```
|
||||
|
||||
### Task Lists
|
||||
|
||||
```bash
|
||||
pcli task-list create --card <card-id> --name "Checklist" [--position N] [--show-on-front] [--hide-completed]
|
||||
pcli task-list get <task-list-id>
|
||||
pcli task-list update <task-list-id> [--name "..."] [--position N] [--show-on-front] [--hide-completed]
|
||||
pcli task-list delete <task-list-id>
|
||||
```
|
||||
|
||||
### Tasks
|
||||
|
||||
```bash
|
||||
pcli task create --task-list <task-list-id> --name "Item" [--position N] [--completed]
|
||||
pcli task update <task-id> [--name "..."] [--position N] [--completed]
|
||||
pcli task delete <task-id>
|
||||
```
|
||||
|
||||
### Lists
|
||||
|
||||
```bash
|
||||
pcli list create --board <board-id> --name "List Name" --position 65536 [--type active|closed]
|
||||
pcli list get <list-id>
|
||||
pcli list update <list-id> [--name "..."] [--position N] [--type active|closed] [--color "..."] [--board <board-id>]
|
||||
pcli list delete <list-id>
|
||||
```
|
||||
|
||||
### Labels
|
||||
|
||||
```bash
|
||||
pcli label create --board <board-id> --name "Bug" --color "berry-red" [--position N]
|
||||
pcli label update <label-id> [--name "..."] [--color "..."] [--position N]
|
||||
pcli label delete <label-id>
|
||||
```
|
||||
|
||||
## API Response Structure
|
||||
|
||||
### Board Get Response
|
||||
Board details include lists directly in `.data.lists[]`, not in an `included` section:
|
||||
```bash
|
||||
pcli board get <board-id> | jq '.data.lists[] | {id, name, position}'
|
||||
```
|
||||
|
||||
### Card List Labels
|
||||
Card list returns labels as **plain strings**, not objects:
|
||||
```bash
|
||||
# Labels are strings like "agent", NOT objects like {name: "agent"}
|
||||
pcli card list --board <board-id> | jq '.data[] | select(.labels[]? == "agent")'
|
||||
```
|
||||
|
||||
### Card Get Response
|
||||
Card get includes `taskLists` and `tasks` arrays (when they exist):
|
||||
```bash
|
||||
pcli card get <card-id> | jq '.data.taskLists[0].id'
|
||||
pcli card get <card-id> | jq '.data.tasks[] | {name, isCompleted}'
|
||||
```
|
||||
|
||||
### Finding Boards in a Project
|
||||
Use `board list --project` to find boards by project name:
|
||||
```bash
|
||||
pcli board list --project "<project-name>" | jq '.data[] | {id, name}'
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Project Configuration
|
||||
- Always strip quotes from yq output: `yq '.planka.project' project.yaml | tr -d '"'`
|
||||
- Exit with error if configured project cannot be found or created
|
||||
- The project name in project.yaml is the authoritative source
|
||||
|
||||
### Idempotent Operations
|
||||
- Use `2>/dev/null || true` for create operations that should not fail if resources already exist
|
||||
- Check for empty results before attempting operations: `if [ -z "$PROJECT_ID" ]; then`
|
||||
|
||||
## Extracting IDs from Output
|
||||
|
||||
All responses use `{"data": ..., "error": null}`. Extract IDs with jq:
|
||||
|
||||
```bash
|
||||
# Single object
|
||||
pcli card create --list <id> --name "X" | jq -r '.data.id'
|
||||
|
||||
# Array
|
||||
pcli card list --board <id> | jq -r '.data[].id'
|
||||
|
||||
# With error checking
|
||||
RESULT=$(pcli project list | jq -r --arg name "$PROJECT_NAME" '.data[] | select(.name == $name) | .id')
|
||||
if [ -z "$RESULT" ]; then
|
||||
echo "Not found"
|
||||
else
|
||||
echo "Found: $RESULT"
|
||||
fi
|
||||
```
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Bootstrap Project Infrastructure
|
||||
|
||||
```bash
|
||||
# Read project configuration
|
||||
PROJECT_NAME=$(yq '.planka.project' project.yaml | tr -d '"')
|
||||
BOARD_NAME=$(yq '.planka.board' project.yaml | tr -d '"')
|
||||
|
||||
# Find or create project (exit with error if fails)
|
||||
PROJECT_ID=$(pcli project list | jq -r --arg name "$PROJECT_NAME" '.data[] | select(.name == $name) | .id')
|
||||
if [ -z "$PROJECT_ID" ]; then
|
||||
echo "Error: Project '$PROJECT_NAME' not found and creation failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find or create board
|
||||
BOARD_ID=$(pcli board list --project "$PROJECT_NAME" | jq -r --arg name "$BOARD_NAME" '.data[] | select(.name == $name) | .id')
|
||||
if [ -z "$BOARD_ID" ]; then
|
||||
BOARD_ID=$(pcli board create --project $PROJECT_ID --name "$BOARD_NAME" | jq -r '.data.id')
|
||||
fi
|
||||
|
||||
# Create required lists (skip if exists)
|
||||
pcli list create --board $BOARD_ID --name "Backlog" --position 65536 2>/dev/null || true
|
||||
pcli list create --board $BOARD_ID --name "To Do" --position 131072 2>/dev/null || true
|
||||
pcli list create --board $BOARD_ID --name "Planning" --position 196608 2>/dev/null || true
|
||||
pcli list create --board $BOARD_ID --name "In Progress" --position 262144 2>/dev/null || true
|
||||
pcli list create --board $BOARD_ID --name "Review" --position 327680 2>/dev/null || true
|
||||
pcli list create --board $BOARD_ID --name "Done" --position 393216 2>/dev/null || true
|
||||
|
||||
# Create agent label
|
||||
pcli label create --board $BOARD_ID --name "agent" --color "berry-red" 2>/dev/null || true
|
||||
```
|
||||
|
||||
### Create a card with a checklist
|
||||
|
||||
```bash
|
||||
CARD_ID=$(pcli card create --list <list-id> --name "Task" | jq -r '.data.id')
|
||||
TL_ID=$(pcli task-list create --card $CARD_ID --name "Steps" | jq -r '.data.id')
|
||||
pcli task create --task-list $TL_ID --name "Step 1"
|
||||
pcli task create --task-list $TL_ID --name "Step 2"
|
||||
```
|
||||
|
||||
### Move all cards between lists
|
||||
|
||||
```bash
|
||||
pcli card list --list <source-list-id> | jq -r '.data[].id' | while read id; do
|
||||
pcli card move $id --list <target-list-id>
|
||||
done
|
||||
```
|
||||
@@ -1,5 +1,6 @@
|
||||
# Binary
|
||||
/pcli
|
||||
/pcli-*
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
PCLI is a Go CLI tool for interacting with the Planka (v2.0) project management API. It uses the Cobra framework with minimal dependencies (only Cobra + standard library).
|
||||
|
||||
## Build & Test Commands
|
||||
|
||||
```bash
|
||||
go build -o pcli . # Build for development
|
||||
./build.sh # Multi-platform release build (linux/darwin/windows)
|
||||
./test.sh # Integration tests (requires live Planka instance)
|
||||
```
|
||||
|
||||
Integration tests require `PLANKA_URL` and `PLANKA_API_KEY` environment variables pointing to a running Planka instance.
|
||||
|
||||
There is no unit test suite or linter configured — tests are shell-based integration tests in `test.sh`.
|
||||
|
||||
## Architecture
|
||||
|
||||
**Entry point**: `main.go` → `cmd.Execute()`
|
||||
|
||||
**Package layout**:
|
||||
- `cmd/` — Cobra command definitions, one file per resource (project.go, board.go, card.go, etc.)
|
||||
- `client/` — HTTP API client, one file per resource matching cmd/ structure
|
||||
- `model/types.go` — All domain types (Project, Board, Card, List, Label, Task, Comment, etc.)
|
||||
- `output/output.go` — Dual-format output: JSON envelope (`{data, error}`) or human-readable table via tabwriter
|
||||
- `logging/logging.go` — slog-based structured JSON logging to stderr
|
||||
|
||||
**Command hierarchy**: `pcli <resource> <action>` (e.g., `pcli card create --list <id> --name "task"`)
|
||||
|
||||
**Key patterns**:
|
||||
- Global flags (`--format`, `--url`, `--api-key`, `--log-level`) set in `cmd/root.go` PersistentPreRunE, which validates config and initializes the API client
|
||||
- Client methods follow `List*/Get*/Create*/Update*/Delete*` naming
|
||||
- API responses use typed structs with included relationships (e.g., board responses include lists and cards)
|
||||
- `friendlyAPIError()` in `cmd/errors.go` translates HTTP status codes to contextual messages
|
||||
- `DoWithFallback` in client supports trying alternate API endpoint patterns
|
||||
- Output uses reflection-based table rendering per type
|
||||
|
||||
**Configuration**: Environment variables `PLANKA_URL` and `PLANKA_API_KEY`, overridable via CLI flags.
|
||||
|
||||
## Adding a New Resource
|
||||
|
||||
1. Define model structs in `model/types.go`
|
||||
2. Add client methods in `client/<resource>.go`
|
||||
3. Add command definitions in `cmd/<resource>.go` with subcommands (list, get, create, update, delete)
|
||||
4. Wire the parent command into `root.go` init()
|
||||
|
||||
## Planka Sync
|
||||
|
||||
**IMPORTANT**
|
||||
After completing any opsx workflow (/opsx:new, /opsx:ff, /opsx:continue,
|
||||
/opsx:apply, /opsx:verify, /opsx:archive, /opsx:bulk-archive), automatically
|
||||
reconcile Planka board state by running the sync script:
|
||||
|
||||
```bash
|
||||
PROJECT_NAME=$(yq -r '.planka.project' project.yaml)
|
||||
BOARD_NAME=$(yq -r '.planka.board' project.yaml)
|
||||
kanban-project-sync --project "$PROJECT_NAME" --board "$BOARD_NAME" --background
|
||||
```
|
||||
|
||||
The script handles concurrency (flock-based locking with coalescing pending queue).
|
||||
If the script is not on PATH or fails, log a brief warning and continue.
|
||||
Never block or retry — Planka sync is best-effort.
|
||||
@@ -75,6 +75,9 @@ pcli project get <project-id>
|
||||
# List all accessible boards
|
||||
pcli board list
|
||||
|
||||
# List boards filtered by project name
|
||||
pcli board list --project "project1"
|
||||
|
||||
# Get a board
|
||||
pcli board get <board-id>
|
||||
|
||||
@@ -184,6 +187,9 @@ pcli label delete <label-id>
|
||||
```bash
|
||||
# Show status summary of all boards and their lists
|
||||
pcli status
|
||||
|
||||
# Show status filtered by project name
|
||||
pcli status --project "MyProject"
|
||||
```
|
||||
|
||||
## Output Formats
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CGO_ENABLED=0 go build -ldflags='-s -w -extldflags "-static"' -o pcli .
|
||||
LDFLAGS='-s -w'
|
||||
TARGETS=(
|
||||
"linux/amd64/pcli-linux-amd64"
|
||||
"darwin/amd64/pcli-darwin-amd64"
|
||||
"darwin/arm64/pcli-darwin-arm64"
|
||||
"windows/amd64/pcli-windows-amd64.exe"
|
||||
)
|
||||
|
||||
echo "Built: pcli"
|
||||
file pcli
|
||||
for target in "${TARGETS[@]}"; do
|
||||
IFS='/' read -r os arch output <<< "$target"
|
||||
echo "Building ${os}/${arch} -> ${output}"
|
||||
CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" go build -ldflags="$LDFLAGS" -o "$output" .
|
||||
done
|
||||
|
||||
# Symlink the linux binary as the default 'pcli' for local use
|
||||
ln -sf pcli-linux-amd64 pcli
|
||||
|
||||
echo ""
|
||||
echo "Built all targets:"
|
||||
ls -l pcli-* pcli.exe 2>/dev/null || true
|
||||
|
||||
+36
-2
@@ -9,6 +9,12 @@ import (
|
||||
"git.franklin.lab/steve.cliff/pcli/model"
|
||||
)
|
||||
|
||||
// BoardCreateFields represents the fields required to create a board
|
||||
type BoardCreateFields struct {
|
||||
Name string `json:"name"`
|
||||
Position float64 `json:"position"`
|
||||
}
|
||||
|
||||
func (c *Client) ListBoards(ctx context.Context) ([]model.Board, error) {
|
||||
data, err := c.DoNoBody(ctx, "GET", "/api/projects")
|
||||
if err != nil {
|
||||
@@ -37,8 +43,11 @@ func (c *Client) GetBoard(ctx context.Context, id string) (*model.Board, error)
|
||||
var response struct {
|
||||
Item model.Board `json:"item"`
|
||||
Included struct {
|
||||
Lists []model.List `json:"lists"`
|
||||
Cards []model.Card `json:"cards"`
|
||||
Lists []model.List `json:"lists"`
|
||||
Cards []model.Card `json:"cards"`
|
||||
Labels []model.Label `json:"labels"`
|
||||
CardLabels []model.CardLabel `json:"cardLabels"`
|
||||
CardMemberships []model.CardMembership `json:"cardMemberships"`
|
||||
} `json:"included"`
|
||||
}
|
||||
|
||||
@@ -48,6 +57,9 @@ func (c *Client) GetBoard(ctx context.Context, id string) (*model.Board, error)
|
||||
|
||||
response.Item.Lists = response.Included.Lists
|
||||
response.Item.Cards = response.Included.Cards
|
||||
response.Item.Labels = response.Included.Labels
|
||||
response.Item.CardLabels = response.Included.CardLabels
|
||||
response.Item.CardMemberships = response.Included.CardMemberships
|
||||
|
||||
return &response.Item, nil
|
||||
}
|
||||
@@ -96,3 +108,25 @@ func (c *Client) ListBoardActions(ctx context.Context, boardId string, limit int
|
||||
|
||||
return all, nil
|
||||
}
|
||||
|
||||
func (c *Client) CreateBoard(ctx context.Context, projectId string, fields BoardCreateFields) (*model.Board, error) {
|
||||
data, err := c.Do(ctx, "POST", fmt.Sprintf("/api/projects/%s/boards", projectId), fields)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Item model.Board `json:"item"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal board response: %w", err)
|
||||
}
|
||||
|
||||
return &response.Item, nil
|
||||
}
|
||||
|
||||
func (c *Client) DeleteBoard(ctx context.Context, id string) error {
|
||||
_, err := c.DoNoBody(ctx, "DELETE", fmt.Sprintf("/api/boards/%s", id))
|
||||
return err
|
||||
}
|
||||
|
||||
+36
-3
@@ -9,21 +9,31 @@ import (
|
||||
"git.franklin.lab/steve.cliff/pcli/model"
|
||||
)
|
||||
|
||||
func (c *Client) GetCard(ctx context.Context, id string) (*model.Card, error) {
|
||||
func (c *Client) GetCard(ctx context.Context, id string) (*model.CardDetail, error) {
|
||||
data, err := c.DoNoBody(ctx, "GET", fmt.Sprintf("/api/cards/%s", id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Item model.Card `json:"item"`
|
||||
Item model.Card `json:"item"`
|
||||
Included struct {
|
||||
TaskLists []model.TaskList `json:"taskLists"`
|
||||
Tasks []model.Task `json:"tasks"`
|
||||
} `json:"included"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal card response: %w", err)
|
||||
}
|
||||
|
||||
return &response.Item, nil
|
||||
result := &model.CardDetail{
|
||||
Card: response.Item,
|
||||
TaskLists: response.Included.TaskLists,
|
||||
Tasks: response.Included.Tasks,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) CreateCard(ctx context.Context, listId string, fields map[string]any) (*model.Card, error) {
|
||||
@@ -195,11 +205,34 @@ func (c *Client) ListCardsByBoard(ctx context.Context, boardId string, limit int
|
||||
listNames[list.ID] = name
|
||||
}
|
||||
|
||||
// Build label ID -> name map
|
||||
labelNames := make(map[string]string)
|
||||
for _, label := range board.Labels {
|
||||
name := ""
|
||||
if label.Name != nil {
|
||||
name = *label.Name
|
||||
}
|
||||
labelNames[label.ID] = name
|
||||
}
|
||||
|
||||
// Build card ID -> label names map
|
||||
cardLabelNames := make(map[string][]string)
|
||||
for _, cl := range board.CardLabels {
|
||||
if name, ok := labelNames[cl.LabelID]; ok {
|
||||
cardLabelNames[cl.CardID] = append(cardLabelNames[cl.CardID], name)
|
||||
}
|
||||
}
|
||||
|
||||
var allCards []model.CardWithList
|
||||
for _, card := range board.Cards {
|
||||
labels := cardLabelNames[card.ID]
|
||||
if labels == nil {
|
||||
labels = []string{}
|
||||
}
|
||||
cardWithList := model.CardWithList{
|
||||
Card: card,
|
||||
ListName: listNames[card.ListID],
|
||||
Labels: labels,
|
||||
}
|
||||
allCards = append(allCards, cardWithList)
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.franklin.lab/steve.cliff/pcli/model"
|
||||
)
|
||||
|
||||
// ListCreateFields represents the fields required to create a list
|
||||
type ListCreateFields struct {
|
||||
Name string `json:"name"`
|
||||
Position float64 `json:"position"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// ListUpdateFields represents the fields that can be updated for a list
|
||||
type ListUpdateFields struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Position *float64 `json:"position,omitempty"`
|
||||
Type *string `json:"type,omitempty"`
|
||||
Color *string `json:"color,omitempty"`
|
||||
BoardID *string `json:"boardId,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) CreateList(ctx context.Context, boardId string, fields ListCreateFields) (*model.List, error) {
|
||||
data, err := c.Do(ctx, "POST", fmt.Sprintf("/api/boards/%s/lists", boardId), fields)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Item model.List `json:"item"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal list response: %w", err)
|
||||
}
|
||||
|
||||
return &response.Item, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetList(ctx context.Context, id string) (*model.List, error) {
|
||||
data, err := c.DoNoBody(ctx, "GET", fmt.Sprintf("/api/lists/%s", id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Item model.List `json:"item"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal list response: %w", err)
|
||||
}
|
||||
|
||||
return &response.Item, nil
|
||||
}
|
||||
|
||||
func (c *Client) UpdateList(ctx context.Context, id string, fields ListUpdateFields) (*model.List, error) {
|
||||
data, err := c.Do(ctx, "PATCH", fmt.Sprintf("/api/lists/%s", id), fields)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Item model.List `json:"item"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal list response: %w", err)
|
||||
}
|
||||
|
||||
return &response.Item, nil
|
||||
}
|
||||
|
||||
func (c *Client) DeleteList(ctx context.Context, id string) error {
|
||||
_, err := c.DoNoBody(ctx, "DELETE", fmt.Sprintf("/api/lists/%s", id))
|
||||
return err
|
||||
}
|
||||
@@ -8,6 +8,13 @@ import (
|
||||
"git.franklin.lab/steve.cliff/pcli/model"
|
||||
)
|
||||
|
||||
// ProjectCreateFields represents the fields required to create a project
|
||||
type ProjectCreateFields struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) ListProjects(ctx context.Context) ([]model.Project, error) {
|
||||
data, err := c.DoNoBody(ctx, "GET", "/api/projects")
|
||||
if err != nil {
|
||||
@@ -41,3 +48,25 @@ func (c *Client) GetProject(ctx context.Context, id string) (*model.Project, err
|
||||
|
||||
return &response.Item, nil
|
||||
}
|
||||
|
||||
func (c *Client) CreateProject(ctx context.Context, fields ProjectCreateFields) (*model.Project, error) {
|
||||
data, err := c.Do(ctx, "POST", "/api/projects", fields)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Item model.Project `json:"item"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal project response: %w", err)
|
||||
}
|
||||
|
||||
return &response.Item, nil
|
||||
}
|
||||
|
||||
func (c *Client) DeleteProject(ctx context.Context, id string) error {
|
||||
_, err := c.DoNoBody(ctx, "DELETE", fmt.Sprintf("/api/projects/%s", id))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.franklin.lab/steve.cliff/pcli/client"
|
||||
"git.franklin.lab/steve.cliff/pcli/output"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -13,15 +16,47 @@ var boardCmd = &cobra.Command{
|
||||
Long: "Commands for managing Planka boards",
|
||||
}
|
||||
|
||||
func resolveProjectNameToID(projectName string) (string, error) {
|
||||
projects, err := getClient().ListProjects(getContext())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, project := range projects {
|
||||
if strings.EqualFold(project.Name, projectName) {
|
||||
return project.ID, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("project not found: %s", projectName)
|
||||
}
|
||||
|
||||
var boardListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all accessible boards",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
projectName, _ := cmd.Flags().GetString("project")
|
||||
|
||||
boards, err := getClient().ListBoards(getContext())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if projectName != "" {
|
||||
projectID, err := resolveProjectNameToID(projectName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var filteredBoards []interface{}
|
||||
for _, board := range boards {
|
||||
if board.ProjectID == projectID {
|
||||
filteredBoards = append(filteredBoards, board)
|
||||
}
|
||||
}
|
||||
return output.Print(filteredBoards, getFormat(), os.Stdout)
|
||||
}
|
||||
|
||||
return output.Print(boards, getFormat(), os.Stdout)
|
||||
},
|
||||
}
|
||||
@@ -56,11 +91,68 @@ var boardActionsCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
var boardCreateCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a new board",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
project, _ := cmd.Flags().GetString("project")
|
||||
name, _ := cmd.Flags().GetString("name")
|
||||
position, _ := cmd.Flags().GetFloat64("position")
|
||||
|
||||
// Validate required flags
|
||||
if project == "" {
|
||||
return cmd.Usage()
|
||||
}
|
||||
if name == "" {
|
||||
return cmd.Usage()
|
||||
}
|
||||
|
||||
fields := client.BoardCreateFields{
|
||||
Name: name,
|
||||
Position: position,
|
||||
}
|
||||
|
||||
board, err := getClient().CreateBoard(getContext(), project, fields)
|
||||
if err != nil {
|
||||
return friendlyAPIError(err, "create board", "requires project manager role")
|
||||
}
|
||||
|
||||
return output.Print(board, getFormat(), os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
var boardDeleteCmd = &cobra.Command{
|
||||
Use: "delete <id>",
|
||||
Short: "Delete a board",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := getClient().DeleteBoard(getContext(), args[0])
|
||||
if err != nil {
|
||||
return friendlyAPIError(err, "delete board", "requires project manager role")
|
||||
}
|
||||
|
||||
fmt.Println("Board deleted successfully")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(boardCmd)
|
||||
boardCmd.AddCommand(boardListCmd)
|
||||
boardCmd.AddCommand(boardGetCmd)
|
||||
boardCmd.AddCommand(boardActionsCmd)
|
||||
boardCmd.AddCommand(boardCreateCmd)
|
||||
boardCmd.AddCommand(boardDeleteCmd)
|
||||
|
||||
boardListCmd.Flags().String("project", "", "Filter boards by project name")
|
||||
|
||||
boardActionsCmd.Flags().Int("limit", 0, "Limit number of actions (0 = no limit)")
|
||||
|
||||
// Flags for board create
|
||||
boardCreateCmd.Flags().String("project", "", "Project ID (required)")
|
||||
boardCreateCmd.Flags().String("name", "", "Board name (required)")
|
||||
boardCreateCmd.Flags().Float64("position", 65536, "Board position (optional, default 65536)")
|
||||
|
||||
boardCreateCmd.MarkFlagRequired("project")
|
||||
boardCreateCmd.MarkFlagRequired("name")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.franklin.lab/steve.cliff/pcli/model"
|
||||
)
|
||||
|
||||
// friendlyAPIError translates APIError status codes into human-readable messages with operation context.
|
||||
func friendlyAPIError(err error, operation string, permissionHint string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if it's an APIError
|
||||
apiErr, ok := err.(*model.APIError)
|
||||
if !ok {
|
||||
// Not an API error, return as-is
|
||||
return err
|
||||
}
|
||||
|
||||
switch apiErr.StatusCode {
|
||||
case 401:
|
||||
return fmt.Errorf("%s: authentication failed — check your API key", operation)
|
||||
case 403:
|
||||
if permissionHint != "" {
|
||||
return fmt.Errorf("%s: permission denied (%s)", operation, permissionHint)
|
||||
}
|
||||
return fmt.Errorf("%s: permission denied", operation)
|
||||
case 404:
|
||||
return fmt.Errorf("%s: not found — the resource may not exist or you may not have access to it", operation)
|
||||
default:
|
||||
// Unknown status code, return original error
|
||||
return err
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.franklin.lab/steve.cliff/pcli/client"
|
||||
"git.franklin.lab/steve.cliff/pcli/output"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var listCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "Manage lists (board columns)",
|
||||
Long: "Commands for managing Planka lists (board columns)",
|
||||
}
|
||||
|
||||
var listCreateCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a new list",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
board, _ := cmd.Flags().GetString("board")
|
||||
name, _ := cmd.Flags().GetString("name")
|
||||
listType, _ := cmd.Flags().GetString("type")
|
||||
position, _ := cmd.Flags().GetFloat64("position")
|
||||
|
||||
// Validate required flags
|
||||
if board == "" {
|
||||
return cmd.Usage()
|
||||
}
|
||||
if name == "" {
|
||||
return cmd.Usage()
|
||||
}
|
||||
if listType == "" {
|
||||
listType = "active" // default to active
|
||||
}
|
||||
|
||||
fields := client.ListCreateFields{
|
||||
Name: name,
|
||||
Type: listType,
|
||||
Position: position,
|
||||
}
|
||||
|
||||
list, err := getClient().CreateList(getContext(), board, fields)
|
||||
if err != nil {
|
||||
return friendlyAPIError(err, "create list", "requires board editor role")
|
||||
}
|
||||
|
||||
return output.Print(list, getFormat(), os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
var listGetCmd = &cobra.Command{
|
||||
Use: "get <id>",
|
||||
Short: "Get a list by ID",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
list, err := getClient().GetList(getContext(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return output.Print(list, getFormat(), os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
var listUpdateCmd = &cobra.Command{
|
||||
Use: "update <id>",
|
||||
Short: "Update a list",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
fields := client.ListUpdateFields{}
|
||||
|
||||
if name, _ := cmd.Flags().GetString("name"); cmd.Flags().Changed("name") {
|
||||
fields.Name = &name
|
||||
}
|
||||
if listType, _ := cmd.Flags().GetString("type"); cmd.Flags().Changed("type") {
|
||||
fields.Type = &listType
|
||||
}
|
||||
if color, _ := cmd.Flags().GetString("color"); cmd.Flags().Changed("color") {
|
||||
fields.Color = &color
|
||||
}
|
||||
if pos, _ := cmd.Flags().GetFloat64("position"); cmd.Flags().Changed("position") {
|
||||
fields.Position = &pos
|
||||
}
|
||||
if board, _ := cmd.Flags().GetString("board"); cmd.Flags().Changed("board") {
|
||||
fields.BoardID = &board
|
||||
}
|
||||
|
||||
// Check if at least one field is being updated
|
||||
if fields.Name == nil && fields.Type == nil && fields.Color == nil && fields.Position == nil && fields.BoardID == nil {
|
||||
return fmt.Errorf("at least one field must be specified for update")
|
||||
}
|
||||
|
||||
list, err := getClient().UpdateList(getContext(), args[0], fields)
|
||||
if err != nil {
|
||||
return friendlyAPIError(err, "update list", "requires board editor role")
|
||||
}
|
||||
|
||||
return output.Print(list, getFormat(), os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
var listDeleteCmd = &cobra.Command{
|
||||
Use: "delete <id>",
|
||||
Short: "Delete a list",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := getClient().DeleteList(getContext(), args[0])
|
||||
if err != nil {
|
||||
return friendlyAPIError(err, "delete list", "requires board editor role")
|
||||
}
|
||||
|
||||
fmt.Println("List deleted successfully")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(listCmd)
|
||||
listCmd.AddCommand(listCreateCmd)
|
||||
listCmd.AddCommand(listGetCmd)
|
||||
listCmd.AddCommand(listUpdateCmd)
|
||||
listCmd.AddCommand(listDeleteCmd)
|
||||
|
||||
// Flags for list create
|
||||
listCreateCmd.Flags().String("board", "", "Board ID (required)")
|
||||
listCreateCmd.Flags().String("name", "", "List name (required)")
|
||||
listCreateCmd.Flags().String("type", "active", "List type (active|closed, default: active)")
|
||||
listCreateCmd.Flags().Float64("position", 65536, "List position (optional, default 65536)")
|
||||
|
||||
listCreateCmd.MarkFlagRequired("board")
|
||||
listCreateCmd.MarkFlagRequired("name")
|
||||
|
||||
// Flags for list update
|
||||
listUpdateCmd.Flags().String("name", "", "List name")
|
||||
listUpdateCmd.Flags().String("type", "", "List type (active|closed)")
|
||||
listUpdateCmd.Flags().String("color", "", "List color")
|
||||
listUpdateCmd.Flags().Float64("position", 0, "List position")
|
||||
listUpdateCmd.Flags().String("board", "", "Move list to different board (Board ID)")
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.franklin.lab/steve.cliff/pcli/client"
|
||||
"git.franklin.lab/steve.cliff/pcli/output"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -40,8 +42,67 @@ var projectGetCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
var projectCreateCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a new project",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name, _ := cmd.Flags().GetString("name")
|
||||
projectType, _ := cmd.Flags().GetString("type")
|
||||
description, _ := cmd.Flags().GetString("description")
|
||||
|
||||
// Validate required flags
|
||||
if name == "" {
|
||||
return cmd.Usage()
|
||||
}
|
||||
if projectType == "" {
|
||||
return cmd.Usage()
|
||||
}
|
||||
|
||||
fields := client.ProjectCreateFields{
|
||||
Type: projectType,
|
||||
Name: name,
|
||||
}
|
||||
|
||||
if description != "" {
|
||||
fields.Description = &description
|
||||
}
|
||||
|
||||
project, err := getClient().CreateProject(getContext(), fields)
|
||||
if err != nil {
|
||||
return friendlyAPIError(err, "create project", "")
|
||||
}
|
||||
|
||||
return output.Print(project, getFormat(), os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
var projectDeleteCmd = &cobra.Command{
|
||||
Use: "delete <id>",
|
||||
Short: "Delete a project",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := getClient().DeleteProject(getContext(), args[0])
|
||||
if err != nil {
|
||||
return friendlyAPIError(err, "delete project", "requires project manager role")
|
||||
}
|
||||
|
||||
fmt.Println("Project deleted successfully")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(projectCmd)
|
||||
projectCmd.AddCommand(projectListCmd)
|
||||
projectCmd.AddCommand(projectGetCmd)
|
||||
projectCmd.AddCommand(projectCreateCmd)
|
||||
projectCmd.AddCommand(projectDeleteCmd)
|
||||
|
||||
// Flags for project create
|
||||
projectCreateCmd.Flags().String("name", "", "Project name (required)")
|
||||
projectCreateCmd.Flags().String("type", "", "Project type (required, values: public/private)")
|
||||
projectCreateCmd.Flags().String("description", "", "Project description (optional)")
|
||||
|
||||
projectCreateCmd.MarkFlagRequired("name")
|
||||
projectCreateCmd.MarkFlagRequired("type")
|
||||
}
|
||||
|
||||
+22
-2
@@ -11,15 +11,33 @@ import (
|
||||
|
||||
var statusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show status summary of all boards and their lists",
|
||||
Long: "Displays a summary of all boards, their lists, and the number of cards in each list",
|
||||
Short: "Show status summary of boards and their lists",
|
||||
Long: "Displays a summary of boards, their lists, and the number of cards in each list.\nUse --project to filter by project name.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
projectName, _ := cmd.Flags().GetString("project")
|
||||
|
||||
// Get all boards
|
||||
boards, err := getClient().ListBoards(getContext())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list boards: %w", err)
|
||||
}
|
||||
|
||||
// Filter boards by project if --project flag is provided
|
||||
if projectName != "" {
|
||||
projectID, err := resolveProjectNameToID(projectName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filtered := boards[:0]
|
||||
for _, board := range boards {
|
||||
if board.ProjectID == projectID {
|
||||
filtered = append(filtered, board)
|
||||
}
|
||||
}
|
||||
boards = filtered
|
||||
}
|
||||
|
||||
// Build status summary with error collection
|
||||
summary := model.StatusSummary{
|
||||
TotalBoards: len(boards),
|
||||
@@ -102,4 +120,6 @@ var statusCmd = &cobra.Command{
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(statusCmd)
|
||||
|
||||
statusCmd.Flags().String("project", "", "Filter status by project name")
|
||||
}
|
||||
|
||||
@@ -48,6 +48,17 @@ pcli project get <project-id>
|
||||
pcli board list
|
||||
pcli board get <board-id> # includes lists and cards
|
||||
pcli board actions <board-id> [--limit N]
|
||||
pcli board create --project <project-id> --name "Board Name" [--position N]
|
||||
pcli board delete <board-id>
|
||||
```
|
||||
|
||||
### Lists (Board Columns)
|
||||
|
||||
```bash
|
||||
pcli list create --board <board-id> --name "Column Name" [--type active|closed] [--position N]
|
||||
pcli list get <list-id>
|
||||
pcli list update <list-id> [--name "..."] [--type active|closed] [--color "..."] [--position N] [--board <board-id>]
|
||||
pcli list delete <list-id>
|
||||
```
|
||||
|
||||
### Cards
|
||||
@@ -125,6 +136,22 @@ pcli card list --board <id> | jq -r '.data[].id'
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Create a complete Kanban board
|
||||
|
||||
```bash
|
||||
# Create board
|
||||
BOARD_ID=$(pcli board create --project <project-id> --name "Development Board" | jq -r '.data.id')
|
||||
|
||||
# Create columns
|
||||
pcli list create --board $BOARD_ID --name "Backlog" --type "active" --position 0
|
||||
TODO_ID=$(pcli list create --board $BOARD_ID --name "To Do" --type "active" --position 65536 | jq -r '.data.id')
|
||||
PROGRESS_ID=$(pcli list create --board $BOARD_ID --name "In Progress" --type "active" --position 131072 | jq -r '.data.id')
|
||||
DONE_ID=$(pcli list create --board $BOARD_ID --name "Done" --type "closed" --position 196608 | jq -r '.data.id')
|
||||
|
||||
# View the board
|
||||
pcli board get $BOARD_ID --format table
|
||||
```
|
||||
|
||||
### Create a card with a checklist
|
||||
|
||||
```bash
|
||||
@@ -0,0 +1,150 @@
|
||||
---
|
||||
name: "Kanban"
|
||||
description: "Manage Planka project boards using the pcli CLI"
|
||||
category: Workflow
|
||||
tags: [workflow, kanban, planka, project-management]
|
||||
---
|
||||
|
||||
Manage Planka project boards using the `pcli` CLI. Use the kanban skill for detailed command reference.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before running any commands, verify the environment is ready:
|
||||
|
||||
```bash
|
||||
// turbo
|
||||
pcli status
|
||||
```
|
||||
|
||||
If this fails, ensure `PLANKA_URL` and `PLANKA_API_KEY` environment variables are set and `pcli` is in PATH.
|
||||
|
||||
---
|
||||
|
||||
## How to Use
|
||||
|
||||
**Input**: The argument after `/kanban` is what the user wants to do. Could be:
|
||||
- A status check: "show me all boards" or "what's on my board"
|
||||
- A board setup: "create a kanban board with todo, in progress, done columns"
|
||||
- A list operation: "add a 'testing' column to my board" or "rename the done column"
|
||||
- A card operation: "create a card for fixing the login bug"
|
||||
- A board query: "list all cards in the backlog"
|
||||
- A move: "move card X to done"
|
||||
- A bulk operation: "move all cards from In Progress to Done"
|
||||
- Nothing (show overall status)
|
||||
|
||||
---
|
||||
|
||||
## Responding to Requests
|
||||
|
||||
### 1. Understand the request
|
||||
|
||||
Map the user's intent to `pcli` commands. Use `pcli status` for overview requests. For specific operations, identify the resource (project, board, card, list, label, task, comment) and action (list, get, create, update, delete, move).
|
||||
|
||||
### 2. Discover IDs when needed
|
||||
|
||||
Most commands require IDs. Discover them by querying first:
|
||||
|
||||
```bash
|
||||
# Find boards
|
||||
pcli board list | jq '.data[] | {id, title}'
|
||||
|
||||
# Find lists on a board
|
||||
pcli board get <board-id> | jq '.data.lists[] | {id, name}'
|
||||
|
||||
# Find cards on a list or board
|
||||
pcli card list --board <board-id> | jq '.data[] | {id, name}'
|
||||
pcli card list --list <list-id> | jq '.data[] | {id, name}'
|
||||
```
|
||||
|
||||
Always use `jq` to extract and format output for readability.
|
||||
|
||||
### 3. Execute the operation
|
||||
|
||||
Run the appropriate `pcli` command. For create/update/delete operations, confirm with the user before executing unless the intent is unambiguous.
|
||||
|
||||
### 4. Report results
|
||||
|
||||
Show the user a concise summary of what happened. Use tables or formatted output when listing multiple items.
|
||||
|
||||
---
|
||||
|
||||
## Command Quick Reference
|
||||
|
||||
| Resource | Commands |
|
||||
|----------|----------|
|
||||
| **Status** | `pcli status` |
|
||||
| **Projects** | `list`, `get`, `create`, `delete` |
|
||||
| **Boards** | `list`, `get`, `actions`, `create`, `delete` |
|
||||
| **Lists** | `create`, `get`, `update`, `delete` |
|
||||
| **Cards** | `list`, `get`, `create`, `update`, `delete`, `duplicate`, `move`, `assign`, `unassign`, `add-label`, `remove-label`, `actions` |
|
||||
| **Comments** | `list`, `create`, `update`, `delete` |
|
||||
| **Task Lists** | `create`, `get`, `update`, `delete` |
|
||||
| **Tasks** | `create`, `update`, `delete` |
|
||||
| **Labels** | `create`, `update`, `delete` |
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Create a complete Kanban board
|
||||
|
||||
```bash
|
||||
# Create board
|
||||
BOARD_ID=$(pcli board create --project <project-id> --name "Development Board" | jq -r '.data.id')
|
||||
|
||||
# Create standard columns
|
||||
pcli list create --board $BOARD_ID --name "Backlog" --type "active" --position 0
|
||||
TODO_ID=$(pcli list create --board $BOARD_ID --name "To Do" --type "active" --position 65536 | jq -r '.data.id')
|
||||
PROGRESS_ID=$(pcli list create --board $BOARD_ID --name "In Progress" --type "active" --position 131072 | jq -r '.data.id')
|
||||
DONE_ID=$(pcli list create --board $BOARD_ID --name "Done" --type "closed" --position 196608 | jq -r '.data.id')
|
||||
|
||||
# View the completed board
|
||||
pcli board get $BOARD_ID --format table
|
||||
```
|
||||
|
||||
### Add a new column to existing board
|
||||
|
||||
```bash
|
||||
# Find the board
|
||||
BOARD_ID=$(pcli board list | jq -r '.data[] | select(.name == "Development Board") | .id')
|
||||
|
||||
# Add new column
|
||||
pcli list create --board $BOARD_ID --name "Testing" --type "active" --position 163840
|
||||
```
|
||||
|
||||
### Create a card with a checklist
|
||||
|
||||
```bash
|
||||
CARD_ID=$(pcli card create --list <list-id> --name "Task" | jq -r '.data.id')
|
||||
TL_ID=$(pcli task-list create --card $CARD_ID --name "Steps" | jq -r '.data.id')
|
||||
pcli task create --task-list $TL_ID --name "Step 1"
|
||||
pcli task create --task-list $TL_ID --name "Step 2"
|
||||
```
|
||||
|
||||
### Move all cards between lists
|
||||
|
||||
```bash
|
||||
pcli card list --list <source-list-id> | jq -r '.data[].id' | while read id; do
|
||||
pcli card move $id --list <target-list-id>
|
||||
done
|
||||
```
|
||||
|
||||
### Extract IDs from output
|
||||
|
||||
```bash
|
||||
# Single object
|
||||
pcli card create --list <id> --name "X" | jq -r '.data.id'
|
||||
|
||||
# Array
|
||||
pcli card list --board <id> | jq -r '.data[].id'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **Discover before acting** - Always query for IDs rather than guessing
|
||||
- **Confirm destructive actions** - Ask before delete or bulk move operations
|
||||
- **Use jq for output** - Parse JSON responses with `jq` for clean, readable results
|
||||
- **Show context** - When listing cards, include the list/board name for context
|
||||
- **Global flags** - All commands accept `--format json|table` for output format
|
||||
+13
-3
@@ -30,8 +30,11 @@ type Board struct {
|
||||
ExpandTaskListsByDefault bool `json:"expandTaskListsByDefault"`
|
||||
CreatedAt *string `json:"createdAt"`
|
||||
UpdatedAt *string `json:"updatedAt"`
|
||||
Lists []List `json:"lists,omitempty"`
|
||||
Cards []Card `json:"cards,omitempty"`
|
||||
Lists []List `json:"lists,omitempty"`
|
||||
Cards []Card `json:"cards,omitempty"`
|
||||
Labels []Label `json:"labels,omitempty"`
|
||||
CardLabels []CardLabel `json:"cardLabels,omitempty"`
|
||||
CardMemberships []CardMembership `json:"cardMemberships,omitempty"`
|
||||
}
|
||||
|
||||
type List struct {
|
||||
@@ -71,9 +74,16 @@ type Card struct {
|
||||
UpdatedAt *string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type CardDetail struct {
|
||||
Card
|
||||
TaskLists []TaskList `json:"taskLists,omitempty"`
|
||||
Tasks []Task `json:"tasks,omitempty"`
|
||||
}
|
||||
|
||||
type CardWithList struct {
|
||||
Card
|
||||
ListName string `json:"listName"`
|
||||
ListName string `json:"listName"`
|
||||
Labels []string `json:"labels"`
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-12
|
||||
@@ -0,0 +1,49 @@
|
||||
## Context
|
||||
|
||||
The CLI currently supports read-only operations for projects and boards but lacks write operations. Users must use the web UI to create/delete projects and boards. Additionally, API permission errors return raw HTTP status codes rather than user-friendly guidance. The existing codebase has a clear separation: client layer handles API communication, cmd layer handles CLI commands and output formatting.
|
||||
|
||||
Current write operations (cards, comments, labels, tasks) follow a consistent pattern but do not have permission-aware error handling. The API client already returns structured APIError objects with status codes and messages.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Enable project and board CRUD operations via CLI
|
||||
- Provide clear, actionable error messages for permission failures
|
||||
- Establish a reusable pattern for permission-aware error handling
|
||||
- Maintain consistency with existing command structure and error handling
|
||||
|
||||
**Non-Goals:**
|
||||
- Support for Trello import or multipart file uploads
|
||||
- Changes to existing command behavior
|
||||
- New authentication mechanisms
|
||||
- Breaking changes to existing APIs
|
||||
|
||||
## Decisions
|
||||
|
||||
### Permission Error Translation Strategy
|
||||
Chose to handle permission errors at the command layer rather than client layer. This allows context-specific messages (e.g., "requires project manager role" for board creation) while keeping the client layer focused on API communication. The `friendlyAPIError` helper in `cmd/` package translates APIError status codes into human-readable messages.
|
||||
|
||||
### JSON-only for Board Creation
|
||||
Board creation API supports multipart/form-data for Trello import, but we're using `application/json` since we don't support import. This simplifies implementation and reuses the existing `Do()` method pattern.
|
||||
|
||||
### Incremental Adoption
|
||||
The permission error helper is added but existing commands are not modified. This allows the pattern to be proven out with the new commands before being applied elsewhere, reducing risk.
|
||||
|
||||
### Error Message Design
|
||||
Permission errors include both the operation context and specific permission requirements. For example: "create board: permission denied (requires project manager role)". This gives users clear guidance on what they need to do.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
[Risk] API error format changes could break the helper → Mitigation: Helper falls back to original error if status code mapping fails
|
||||
[Risk] Users might expect pre-flight permission checks → Mitigation: Clear error messages explain the permission requirement after the fact
|
||||
[Trade-off] More verbose error messages vs. brevity → Chose actionable messages that guide users to resolution
|
||||
|
||||
## Migration Plan
|
||||
|
||||
No migration needed - this is additive functionality. New commands are added alongside existing ones. The permission error helper can be adopted by existing commands incrementally.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should the permission error helper live in `cmd/` or `output/` package? (Chose `cmd/` since it's command-layer logic)
|
||||
- Should we add validation flags like `--dry-run`? (Deferred - can add later if needed)
|
||||
- Should board creation support additional optional fields from the API? (Started with minimal viable set)
|
||||
@@ -0,0 +1,31 @@
|
||||
## Why
|
||||
|
||||
The CLI currently supports read-only operations for projects and boards (list, get) but has no ability to create or delete them. Users who need to set up new projects and boards must use the Planka web UI. Additionally, write operations that fail due to insufficient permissions return raw API errors rather than actionable guidance.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add `project create` command with `--name`, `--type`, and optional `--description` flags
|
||||
- Add `project delete` command accepting a project ID
|
||||
- Add `board create` command with `--project`, `--name`, and optional `--position` flags
|
||||
- Add `board delete` command accepting a board ID
|
||||
- Add `CreateProject`, `DeleteProject`, `CreateBoard`, `DeleteBoard` client methods
|
||||
- Add a shared `friendlyAPIError` helper in the `cmd` package that translates API error status codes (401, 403, 404) into human-readable messages with permission context
|
||||
- New commands use the shared helper; existing commands are unchanged (can adopt it later)
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `permission-errors`: Shared helper for translating API permission/auth errors into user-friendly CLI messages
|
||||
|
||||
### Modified Capabilities
|
||||
- `api-client`: Adding CreateProject, DeleteProject, CreateBoard, DeleteBoard client methods
|
||||
- `cli-commands`: Adding project create/delete and board create/delete subcommands
|
||||
|
||||
## Impact
|
||||
|
||||
- `client/projects.go` — new CreateProject, DeleteProject methods
|
||||
- `client/boards.go` — new CreateBoard, DeleteBoard methods
|
||||
- `cmd/errors.go` — new file with friendlyAPIError helper
|
||||
- `cmd/project.go` — new create and delete subcommands
|
||||
- `cmd/board.go` — new create and delete subcommands
|
||||
- No new dependencies, no model changes, no breaking changes
|
||||
@@ -0,0 +1,39 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Project operations
|
||||
The client SHALL provide methods to list all accessible projects (`GET /projects`), get a single project by ID (`GET /projects/{id}`), create a project (`POST /projects`), and delete a project (`DELETE /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
|
||||
|
||||
#### Scenario: Create project
|
||||
- **WHEN** `CreateProject` is called with project fields (type, name, description)
|
||||
- **THEN** the client SHALL send `POST /projects` with the provided fields and return the created Project
|
||||
|
||||
#### Scenario: Delete project
|
||||
- **WHEN** `DeleteProject` is called with a project ID
|
||||
- **THEN** the client SHALL send `DELETE /projects/{id}`
|
||||
|
||||
### Requirement: Board operations
|
||||
The client SHALL provide a method to get a single board by ID (`GET /boards/{id}`), list board actions (`GET /boards/{boardId}/actions`) with pagination support, create a board (`POST /projects/{projectId}/boards`), and delete a board (`DELETE /boards/{id}`).
|
||||
|
||||
#### 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
|
||||
|
||||
#### Scenario: Create board
|
||||
- **WHEN** `CreateBoard` is called with a project ID and board fields (name, position)
|
||||
- **THEN** the client SHALL send `POST /projects/{projectId}/boards` with the provided fields and return the created Board
|
||||
|
||||
#### Scenario: Delete board
|
||||
- **WHEN** `DeleteBoard` is called with a board ID
|
||||
- **THEN** the client SHALL send `DELETE /boards/{id}`
|
||||
@@ -0,0 +1,83 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Project commands
|
||||
The system SHALL provide a `project` command group with subcommands `list`, `get`, `create`, and `delete`. `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. `pcli project create` SHALL create a new project. `pcli project delete <id>` SHALL delete a project.
|
||||
|
||||
#### 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
|
||||
|
||||
#### Scenario: Create project
|
||||
- **WHEN** `pcli project create --name "My Project" --type private --description "A test project"` is executed
|
||||
- **THEN** the system SHALL create the project and output the created project
|
||||
|
||||
#### Scenario: Create project missing required flags
|
||||
- **WHEN** `pcli project create` is executed without `--name` or `--type`
|
||||
- **THEN** the system SHALL print an error indicating the required flags and exit with code 1
|
||||
|
||||
#### Scenario: Create project with insufficient permissions
|
||||
- **WHEN** `pcli project create` is executed with invalid API credentials
|
||||
- **THEN** the system SHALL output "create project: authentication failed — check your API key"
|
||||
|
||||
#### Scenario: Delete project
|
||||
- **WHEN** `pcli project delete <id>` is executed with a valid project ID
|
||||
- **THEN** the system SHALL delete the project and output a success confirmation
|
||||
|
||||
#### Scenario: Delete project with insufficient permissions
|
||||
- **WHEN** `pcli project delete <id>` is executed by a user without project manager permissions
|
||||
- **THEN** the system SHALL output "delete project: permission denied (requires project manager role)"
|
||||
|
||||
#### Scenario: Delete project not found
|
||||
- **WHEN** `pcli project delete <id>` is executed with a non-existent project ID
|
||||
- **THEN** the system SHALL output "delete project: not found — the resource may not exist or you may not have access to it"
|
||||
|
||||
### Requirement: Board commands
|
||||
The system SHALL provide a `board` command group with subcommands `get`, `actions`, `create`, and `delete`. `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. `pcli board create` SHALL create a new board. `pcli board delete <id>` SHALL delete a board.
|
||||
|
||||
#### 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
|
||||
|
||||
#### Scenario: Create board
|
||||
- **WHEN** `pcli board create --project <id> --name "Development Board" --position 65536` is executed
|
||||
- **THEN** the system SHALL create the board and output the created board
|
||||
|
||||
#### Scenario: Create board missing required flags
|
||||
- **WHEN** `pcli board create` is executed without `--project` or `--name`
|
||||
- **THEN** the system SHALL print an error indicating the required flags and exit with code 1
|
||||
|
||||
#### Scenario: Create board with insufficient permissions
|
||||
- **WHEN** `pcli board create --project <id> --name "Board"` is executed by a user without project manager permissions on the project
|
||||
- **THEN** the system SHALL output "create board: permission denied (requires project manager role)"
|
||||
|
||||
#### Scenario: Create board project not found
|
||||
- **WHEN** `pcli board create --project <id> --name "Board"` is executed with a project the user cannot access
|
||||
- **THEN** the system SHALL output "create board: not found — the resource may not exist or you may not have access to it"
|
||||
|
||||
#### Scenario: Delete board
|
||||
- **WHEN** `pcli board delete <id>` is executed with a valid board ID
|
||||
- **THEN** the system SHALL delete the board and output a success confirmation
|
||||
|
||||
#### Scenario: Delete board with insufficient permissions
|
||||
- **WHEN** `pcli board delete <id>` is executed by a user without project manager permissions
|
||||
- **THEN** the system SHALL output "delete board: permission denied (requires project manager role)"
|
||||
|
||||
#### Scenario: Delete board not found
|
||||
- **WHEN** `pcli board delete <id>` is executed with a non-existent board ID
|
||||
- **THEN** the system SHALL output "delete board: not found — the resource may not exist or you may not have access to it"
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Permission error translation helper
|
||||
The system SHALL provide a `friendlyAPIError` function in the `cmd` package that translates APIError status codes into human-readable messages with operation context.
|
||||
|
||||
#### Scenario: Authentication error (401)
|
||||
- **WHEN** an APIError with StatusCode 401 is passed to friendlyAPIError with operation "create board"
|
||||
- **THEN** the function SHALL return an error with message "create board: authentication failed — check your API key"
|
||||
|
||||
#### Scenario: Permission denied (403) with hint
|
||||
- **WHEN** an APIError with StatusCode 403 is passed to friendlyAPIError with operation "create board" and permissionHint "requires project manager role"
|
||||
- **THEN** the function SHALL return an error with message "create board: permission denied (requires project manager role)"
|
||||
|
||||
#### Scenario: Permission denied (403) without hint
|
||||
- **WHEN** an APIError with StatusCode 403 is passed to friendlyAPIError with operation "create project" and empty permissionHint
|
||||
- **THEN** the function SHALL return an error with message "create project: permission denied"
|
||||
|
||||
#### Scenario: Not found (404)
|
||||
- **WHEN** an APIError with StatusCode 404 is passed to friendlyAPIError with operation "delete board"
|
||||
- **THEN** the function SHALL return an error with message "delete board: not found — the resource may not exist or you may not have access to it"
|
||||
|
||||
#### Scenario: Non-API error
|
||||
- **WHEN** a non-APIError (e.g., network error) is passed to friendlyAPIError
|
||||
- **THEN** the function SHALL return the original error unchanged
|
||||
|
||||
#### Scenario: Unknown status code
|
||||
- **WHEN** an APIError with StatusCode 500 is passed to friendlyAPIError
|
||||
- **THEN** the function SHALL return the original APIError unchanged
|
||||
@@ -0,0 +1,28 @@
|
||||
## 1. Permission Error Helper
|
||||
|
||||
- [x] 1.1 Create `cmd/errors.go` with `friendlyAPIError(err error, operation string, permissionHint string) error` function
|
||||
- [x] 1.2 Handle 401 (auth failed), 403 (permission denied with optional hint), 404 (not found / no access), and passthrough for non-API errors and unknown status codes
|
||||
|
||||
## 2. Client Methods
|
||||
|
||||
- [x] 2.1 Add `CreateProject(ctx, fields) (*model.Project, error)` to `client/projects.go` — POST /api/projects
|
||||
- [x] 2.2 Add `DeleteProject(ctx, id) error` to `client/projects.go` — DELETE /api/projects/{id}
|
||||
- [x] 2.3 Add `CreateBoard(ctx, projectId, fields) (*model.Board, error)` to `client/boards.go` — POST /api/projects/{projectId}/boards
|
||||
- [x] 2.4 Add `DeleteBoard(ctx, id) error` to `client/boards.go` — DELETE /api/boards/{id}
|
||||
|
||||
## 3. Project Commands
|
||||
|
||||
- [x] 3.1 Add `projectCreateCmd` to `cmd/project.go` with `--name` (required), `--type` (required, values: public/private), and `--description` (optional) flags
|
||||
- [x] 3.2 Add `projectDeleteCmd` to `cmd/project.go` accepting a positional project ID argument
|
||||
- [x] 3.3 Wire both commands with `friendlyAPIError` — create uses empty hint, delete uses "requires project manager role"
|
||||
|
||||
## 4. Board Commands
|
||||
|
||||
- [x] 4.1 Add `boardCreateCmd` to `cmd/board.go` with `--project` (required), `--name` (required), and `--position` (optional, default 65536) flags
|
||||
- [x] 4.2 Add `boardDeleteCmd` to `cmd/board.go` accepting a positional board ID argument
|
||||
- [x] 4.3 Wire both commands with `friendlyAPIError` using "requires project manager role" hint
|
||||
|
||||
## 5. Verification
|
||||
|
||||
- [x] 5.1 Run `go build` to verify compilation
|
||||
- [x] 5.2 Run existing test suite (`test.sh`) to verify no regressions
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-18
|
||||
@@ -0,0 +1,71 @@
|
||||
## Context
|
||||
|
||||
The `pcli board list` command currently calls `client.ListBoards()` which returns all boards accessible to the user across all projects. There's no filtering capability at the CLI level. The kanban-sync workflow needs to check if a board exists within a specific project before creating it, but without filtering, it risks creating duplicates.
|
||||
|
||||
The Planka API returns boards with a `projectId` field. Projects can be listed via `client.ListProjects()` which returns project objects with `id` and `name` fields. We need to bridge project names (user-friendly) to project IDs (API-level) for filtering.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Add `--project <name>` flag to `pcli board list` command
|
||||
- Filter boards by project name using case-insensitive matching
|
||||
- Maintain backward compatibility (no flag = list all boards)
|
||||
- Provide clear error message if project name doesn't match any accessible project
|
||||
|
||||
**Non-Goals:**
|
||||
- Filtering by multiple projects simultaneously
|
||||
- Fuzzy matching or partial name matching (exact case-insensitive match only)
|
||||
- Adding project filtering to other board commands (get, actions, etc.)
|
||||
- Caching project lookups across commands
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: Filter client-side, not API-side
|
||||
**Choice:** Fetch all boards via `ListBoards()`, then filter in CLI code based on `projectId`.
|
||||
|
||||
**Rationale:** The Planka API's `ListBoards()` endpoint doesn't support project filtering. Adding API-level filtering would require changes to the Planka server or a different endpoint. Client-side filtering is simpler and keeps changes contained to pcli.
|
||||
|
||||
**Alternatives considered:**
|
||||
- Use a different API endpoint: No suitable endpoint exists in the Planka API
|
||||
- Modify Planka API: Out of scope for this change
|
||||
|
||||
**Trade-off:** Fetches all boards even when filtering, but acceptable given typical board counts.
|
||||
|
||||
### Decision 2: Accept project name, not project ID
|
||||
**Choice:** The `--project` flag accepts a project name string and resolves it to an ID internally.
|
||||
|
||||
**Rationale:** Project names are more user-friendly and memorable than UUIDs. Users working with kanban-sync know project names, not IDs.
|
||||
|
||||
**Alternatives considered:**
|
||||
- Accept project ID: Less user-friendly, requires users to look up IDs first
|
||||
- Accept both name and ID: Adds complexity for minimal benefit
|
||||
|
||||
**Trade-off:** Requires an additional API call to `ListProjects()` to resolve the name, but this is a one-time cost per command execution.
|
||||
|
||||
### Decision 3: Case-insensitive exact match
|
||||
**Choice:** Match project name case-insensitively but require exact match (no partial/fuzzy matching).
|
||||
|
||||
**Rationale:** Case-insensitive matching improves usability (users don't need to remember exact casing). Exact matching avoids ambiguity when multiple projects have similar names.
|
||||
|
||||
**Alternatives considered:**
|
||||
- Case-sensitive matching: Too strict, poor UX
|
||||
- Fuzzy/partial matching: Could match multiple projects, requires disambiguation logic
|
||||
|
||||
**Trade-off:** Users must know the exact project name, but this is acceptable for the target use case.
|
||||
|
||||
### Decision 4: Error on no match
|
||||
**Choice:** If `--project` is provided but no accessible project matches the name, return an error and exit with code 1.
|
||||
|
||||
**Rationale:** Silent failure or empty results could confuse users. Explicit error makes it clear the project name is wrong or inaccessible.
|
||||
|
||||
**Alternatives considered:**
|
||||
- Return empty list: Ambiguous (no boards vs. wrong project name)
|
||||
- List similar project names: Adds complexity
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
**[Risk: Performance with many boards]** → Acceptable: Client-side filtering requires fetching all boards. For users with hundreds of boards, this could be slow. However, typical Planka instances have <100 boards, making this acceptable. If performance becomes an issue, we can optimize later with API-level filtering.
|
||||
|
||||
**[Risk: Project name collisions]** → Mitigated: If multiple projects have the same name (case-insensitive), the first match wins. This is unlikely in practice since project names are typically unique within an organization. If needed, users can use `pcli project list` to identify the correct project.
|
||||
|
||||
**[Risk: Additional API call overhead]** → Acceptable: Resolving project name to ID requires calling `ListProjects()`. This adds ~100-200ms latency but is necessary for the user-friendly interface. The call is made only once per command execution.
|
||||
@@ -0,0 +1,34 @@
|
||||
## Why
|
||||
|
||||
The `pcli board list` command currently returns all accessible boards across all projects, making it difficult to find boards within a specific project. When working with the kanban-sync workflow, we need to filter boards by project name to avoid creating duplicate boards. This change adds a `--project` flag to enable project-scoped board listing.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add `--project <name>` flag to `pcli board list` command
|
||||
- When `--project` is provided, filter boards to only show those belonging to the specified project
|
||||
- When `--project` is omitted, maintain current behavior (list all boards)
|
||||
- The flag accepts a project name (not ID) and performs case-insensitive matching
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `board-list-filtering`: Filter board list results by project name
|
||||
|
||||
### Modified Capabilities
|
||||
- `cli-commands`: Add `--project` flag to the `board list` command specification
|
||||
|
||||
## Impact
|
||||
|
||||
**Code:**
|
||||
- `cmd/board.go`: Add `--project` flag to `boardListCmd` and implement filtering logic
|
||||
- `client/boards.go`: May need to add project name resolution if not already available
|
||||
|
||||
**APIs:**
|
||||
- Uses existing `ListBoards()` and `ListProjects()` client methods
|
||||
- No new API endpoints required
|
||||
|
||||
**Dependencies:**
|
||||
- No new dependencies
|
||||
|
||||
**Workflows:**
|
||||
- Enables kanban-sync workflow to check for existing boards within a specific project before creating new ones
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Board list project filtering
|
||||
The system SHALL provide a `--project <name>` flag for the `board list` command. When the flag is provided, the system SHALL resolve the project name to a project ID by calling `ListProjects()` and performing a case-insensitive exact match on the project name. If no matching project is found, the system SHALL output an error message "project not found: <name>" and exit with code 1. If a matching project is found, the system SHALL filter the board list to include only boards where `projectId` matches the resolved project ID. When the `--project` flag is omitted, the system SHALL list all accessible boards without filtering.
|
||||
|
||||
#### Scenario: List boards without project filter
|
||||
- **WHEN** `pcli board list` is executed without the `--project` flag
|
||||
- **THEN** the system SHALL output all accessible boards across all projects
|
||||
|
||||
#### Scenario: List boards filtered by project name
|
||||
- **WHEN** `pcli board list --project "project1"` is executed
|
||||
- **THEN** the system SHALL resolve "project1" to a project ID
|
||||
- **AND** the system SHALL output only boards belonging to that project
|
||||
|
||||
#### Scenario: List boards with case-insensitive project match
|
||||
- **WHEN** `pcli board list --project "PROJECT1"` is executed and a project named "project1" exists
|
||||
- **THEN** the system SHALL match the project case-insensitively
|
||||
- **AND** the system SHALL output boards belonging to the matched project
|
||||
|
||||
#### Scenario: Project name not found
|
||||
- **WHEN** `pcli board list --project "nonexistent"` is executed
|
||||
- **THEN** the system SHALL output "project not found: nonexistent"
|
||||
- **AND** the system SHALL exit with code 1
|
||||
|
||||
#### Scenario: Project exists but has no boards
|
||||
- **WHEN** `pcli board list --project "empty-project"` is executed for a project with no boards
|
||||
- **THEN** the system SHALL output an empty list (or empty JSON array in JSON format)
|
||||
|
||||
#### Scenario: User lacks access to project
|
||||
- **WHEN** `pcli board list --project "restricted"` is executed for a project the user cannot access
|
||||
- **THEN** the system SHALL output "project not found: restricted"
|
||||
- **AND** the system SHALL exit with code 1
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Board list command
|
||||
The system SHALL provide a `board list` subcommand. `pcli board list` SHALL call the client's ListBoards method and output all accessible boards. The command SHALL accept an optional `--project <name>` flag (string). When `--project` is provided, the system SHALL filter boards to only those belonging to the specified project (see board-list-filtering spec for filtering behavior).
|
||||
|
||||
#### Scenario: List all boards
|
||||
- **WHEN** `pcli board list` is executed without flags
|
||||
- **THEN** the system SHALL output all accessible boards
|
||||
|
||||
#### Scenario: List boards filtered by project
|
||||
- **WHEN** `pcli board list --project "project1"` is executed
|
||||
- **THEN** the system SHALL output only boards belonging to the specified project
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Board commands
|
||||
The system SHALL provide a `board` command group with subcommands `list`, `get`, `actions`, `create`, and `delete`. `pcli board list` SHALL list all accessible boards and accept an optional `--project <name>` flag for filtering. `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. `pcli board create` SHALL create a new board. `pcli board delete <id>` SHALL delete a board.
|
||||
|
||||
#### 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
|
||||
|
||||
#### Scenario: Create board
|
||||
- **WHEN** `pcli board create --project <id> --name "Development Board" --position 65536` is executed
|
||||
- **THEN** the system SHALL create the board and output the created board
|
||||
|
||||
#### Scenario: Create board missing required flags
|
||||
- **WHEN** `pcli board create` is executed without `--project` or `--name`
|
||||
- **THEN** the system SHALL print an error indicating the required flags and exit with code 1
|
||||
|
||||
#### Scenario: Create board with insufficient permissions
|
||||
- **WHEN** `pcli board create --project <id> --name "Board"` is executed by a user without project manager permissions on the project
|
||||
- **THEN** the system SHALL output "create board: permission denied (requires project manager role)"
|
||||
|
||||
#### Scenario: Create board project not found
|
||||
- **WHEN** `pcli board create --project <id> --name "Board"` is executed with a project the user cannot access
|
||||
- **THEN** the system SHALL output "create board: not found — the resource may not exist or you may not have access to it"
|
||||
|
||||
#### Scenario: Delete board
|
||||
- **WHEN** `pcli board delete <id>` is executed with a valid board ID
|
||||
- **THEN** the system SHALL delete the board and output a success confirmation
|
||||
|
||||
#### Scenario: Delete board with insufficient permissions
|
||||
- **WHEN** `pcli board delete <id>` is executed by a user without project manager permissions
|
||||
- **THEN** the system SHALL output "delete board: permission denied (requires project manager role)"
|
||||
|
||||
#### Scenario: Delete board not found
|
||||
- **WHEN** `pcli board delete <id>` is executed with a non-existent board ID
|
||||
- **THEN** the system SHALL output "delete board: not found — the resource may not exist or you may not have access to it"
|
||||
@@ -0,0 +1,24 @@
|
||||
## 1. Add project name resolution helper
|
||||
|
||||
- [x] 1.1 Add helper function in cmd/board.go to resolve project name to ID by calling ListProjects() and performing case-insensitive match
|
||||
- [x] 1.2 Add error handling for project not found case with appropriate error message
|
||||
|
||||
## 2. Implement board list filtering
|
||||
|
||||
- [x] 2.1 Add --project flag to boardListCmd in cmd/board.go
|
||||
- [x] 2.2 Implement filtering logic: when --project is provided, resolve name to ID and filter boards by projectId
|
||||
- [x] 2.3 Maintain backward compatibility: when --project is omitted, list all boards
|
||||
|
||||
## 3. Update tests and documentation
|
||||
|
||||
- [x] 3.1 Add test cases for board list with project filter
|
||||
- [x] 3.2 Add test cases for project not found error
|
||||
- [x] 3.3 Add test cases for case-insensitive project matching
|
||||
- [x] 3.4 Update README or help text if needed
|
||||
|
||||
## 4. Verify integration
|
||||
|
||||
- [x] 4.1 Test board list without --project flag (should list all boards)
|
||||
- [x] 4.2 Test board list with valid --project flag (should filter correctly)
|
||||
- [x] 4.3 Test board list with invalid --project flag (should error appropriately)
|
||||
- [x] 4.4 Test board list with case variations in project name
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-18
|
||||
@@ -0,0 +1,38 @@
|
||||
## Context
|
||||
|
||||
The `pcli status` command fetches all boards and aggregates card counts per list. It has no filtering capability. The `board list` command already supports `--project <name>` filtering via a `resolveProjectNameToID()` helper in `cmd/board.go` that does case-insensitive name matching against all projects. The Board model already contains a `ProjectID` field.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Add `--project` flag to `pcli status` that filters boards by project name
|
||||
- Reuse the existing `resolveProjectNameToID()` pattern from `cmd/board.go`
|
||||
- Update help text and README to document the new flag
|
||||
|
||||
**Non-Goals:**
|
||||
- Filtering by project ID (only name-based, consistent with `board list`)
|
||||
- Multiple project filtering (single project only)
|
||||
- Caching project lookups across commands
|
||||
- Changing the status output structure or adding new fields
|
||||
|
||||
## Decisions
|
||||
|
||||
### Filter boards before fetching details
|
||||
**Decision**: Apply the project filter on the initial `ListBoards()` result, before calling `GetBoard()` for each board. This avoids unnecessary API calls for boards that will be filtered out.
|
||||
|
||||
**Alternative**: Filter after fetching all board details. Rejected because it wastes API calls and time for boards not in the target project.
|
||||
|
||||
### Reuse resolveProjectNameToID from board.go
|
||||
**Decision**: Call the existing `resolveProjectNameToID()` function directly — it is already exported within the `cmd` package.
|
||||
|
||||
**Alternative**: Extract to a shared utility. Rejected as premature — two call sites within the same package doesn't warrant a new abstraction.
|
||||
|
||||
### totalBoards reflects filtered count
|
||||
**Decision**: When `--project` is used, `totalBoards` in the output reflects only the matching boards, not all boards. This matches the semantics of "how many boards am I looking at."
|
||||
|
||||
**Alternative**: Show total and filtered counts separately. Rejected — adds complexity for minimal value.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[Extra API call]** → When `--project` is used, an additional `ListProjects()` call is made to resolve the name. This is lightweight and consistent with `board list --project`.
|
||||
- **[No partial match]** → Project name must match exactly (case-insensitive). Consistent with existing `board list` behavior. Users who misspell get a clear "project not found" error.
|
||||
@@ -0,0 +1,30 @@
|
||||
## Why
|
||||
|
||||
The `pcli status` command currently shows a summary of **all** boards across all projects. In instances with many projects, this produces noisy output and makes it difficult to focus on a specific project's boards. The `board list` command already supports `--project` filtering — the status command should offer the same capability for consistency and usability.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add an optional `--project` flag to `pcli status` that filters the status summary to boards belonging to the named project
|
||||
- When `--project` is provided, only boards matching that project are included; `totalBoards` reflects the filtered count
|
||||
- When `--project` is omitted, behavior is unchanged (all boards shown)
|
||||
- Update command help text (`Short`, `Long`) to mention the filtering capability
|
||||
- Update README status section with `--project` usage example
|
||||
- Reuse the existing `resolveProjectNameToID()` helper from `cmd/board.go`
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
_None — this enhances existing capabilities._
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `status-command`: Add optional `--project` flag for project-scoped filtering
|
||||
- `cli-commands`: Update status command documentation to include `--project` flag
|
||||
|
||||
## Impact
|
||||
|
||||
- **Code**: `cmd/status.go` (add flag, filter logic), `cmd/board.go` (no change — reuses existing helper)
|
||||
- **Documentation**: `README.md` status section, command help strings
|
||||
- **API**: No new API calls — uses existing `ListProjects()` for name resolution, same `ListBoards()` + `GetBoard()` flow
|
||||
- **Breaking changes**: None — flag is optional, default behavior preserved
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
## MODIFIED 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 accept an optional `--project <name>` flag (string) to filter boards by project name. The command SHALL fetch all boards via `ListBoards`, optionally filter by project, 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: Run status command with project filter
|
||||
- **WHEN** `pcli status --project "MyProject"` is executed
|
||||
- **THEN** the system SHALL output a summary of only boards belonging to "MyProject"
|
||||
|
||||
#### 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
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Status command project filtering
|
||||
The system SHALL accept an optional `--project <name>` flag on the `pcli status` command. When `--project` is provided, the system SHALL resolve the project name to a project ID using case-insensitive exact matching against all accessible projects. The system SHALL then filter the board list to include only boards whose `projectId` matches the resolved project ID. The `totalBoards` count in the output SHALL reflect the filtered board count. When `--project` is omitted, behavior SHALL be unchanged (all boards shown).
|
||||
|
||||
#### Scenario: Status filtered by project name
|
||||
- **WHEN** `pcli status --project "MyProject"` is executed and the project exists with 2 boards
|
||||
- **THEN** the output SHALL include only the 2 boards belonging to "MyProject"
|
||||
- **AND** `totalBoards` SHALL be 2
|
||||
|
||||
#### Scenario: Status filtered by project name case-insensitive
|
||||
- **WHEN** `pcli status --project "myproject"` is executed and a project named "MyProject" exists
|
||||
- **THEN** the output SHALL include boards belonging to "MyProject"
|
||||
|
||||
#### Scenario: Status with project filter and no matching boards
|
||||
- **WHEN** `pcli status --project "EmptyProject"` is executed and the project exists but has no boards
|
||||
- **THEN** the output SHALL show `totalBoards` as 0 and an empty boards array
|
||||
|
||||
#### Scenario: Status with project not found
|
||||
- **WHEN** `pcli status --project "NonExistent"` is executed and no project with that name exists
|
||||
- **THEN** the system SHALL output an error "project not found: NonExistent"
|
||||
- **AND** the system SHALL exit with code 1
|
||||
|
||||
#### Scenario: Status without project flag
|
||||
- **WHEN** `pcli status` is executed without `--project`
|
||||
- **THEN** the output SHALL include all boards across all projects (unchanged behavior)
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Status command summary output
|
||||
The system SHALL provide a top-level `pcli status` command that outputs a summary of all boards (or boards filtered by `--project`), 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
|
||||
@@ -0,0 +1,19 @@
|
||||
## 1. Core Implementation
|
||||
|
||||
- [x] 1.1 Add `--project` flag to `statusCmd` in `cmd/status.go`
|
||||
- [x] 1.2 Add project filtering logic: resolve name via `resolveProjectNameToID()`, filter boards by `ProjectID` before fetching details
|
||||
- [x] 1.3 Update `totalBoards` count to reflect filtered results
|
||||
|
||||
## 2. Command Help Text
|
||||
|
||||
- [x] 2.1 Update `Short` and `Long` descriptions on `statusCmd` to mention `--project` filtering
|
||||
|
||||
## 3. Documentation
|
||||
|
||||
- [x] 3.1 Update README.md status section with `--project` usage example
|
||||
|
||||
## 4. Verification
|
||||
|
||||
- [x] 4.1 Build and manually test `pcli status --project <name>` with an existing project
|
||||
- [x] 4.2 Test error case: `pcli status --project "nonexistent"` returns "project not found" error
|
||||
- [x] 4.3 Test default behavior: `pcli status` without `--project` still shows all boards
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-18
|
||||
@@ -0,0 +1,37 @@
|
||||
## Context
|
||||
|
||||
The Planka v2 API `GET /api/boards/:id` response includes an `included` object with: `users`, `boardMemberships`, `labels`, `lists`, `cards`, `cardMemberships`, `cardLabels`, `taskLists`, `tasks`, `attachments`, `customFieldGroups`, `customFields`, `customFieldValues`. Currently `GetBoard` only parses `lists` and `cards`, discarding everything else.
|
||||
|
||||
The `CardLabel` and `Label` types already exist in `model/types.go`. The `Board` struct has `Lists` and `Cards` fields but not `Labels`, `CardLabels`, or `CardMemberships`.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Parse `labels`, `cardLabels`, and `cardMemberships` from the `GetBoard` response
|
||||
- Make label data available on the `Board` struct for downstream consumers
|
||||
- Enrich `ListCardsByBoard` output so `card list --board` includes label names per card
|
||||
|
||||
**Non-Goals:**
|
||||
- Parsing all included fields (users, taskLists, attachments, customFields, etc.) — only what's needed now
|
||||
- Adding label data to `card list --list` (uses a different API endpoint that doesn't include labels)
|
||||
- Changing the `card get` response (already returns card-level data via a different endpoint)
|
||||
|
||||
## Decisions
|
||||
|
||||
### Add fields to Board struct rather than creating a separate BoardDetail type
|
||||
**Decision**: Add `Labels`, `CardLabels`, and `CardMemberships` as optional fields on the existing `Board` struct with `omitempty`.
|
||||
|
||||
**Alternative**: Create a `BoardDetail` struct for the enriched response. Rejected — the fields are simply absent when not fetched (e.g., `ListBoards`), and `omitempty` handles this cleanly without a type split.
|
||||
|
||||
### Add Labels field to CardWithList for card list --board output
|
||||
**Decision**: Add a `Labels` field (`[]string` of label names) to the `CardWithList` struct. `ListCardsByBoard` will resolve card→label associations via the `cardLabels` join table and `labels` list from the board response.
|
||||
|
||||
**Alternative**: Return full `Label` objects per card. Rejected — label names are sufficient for display and filtering; full objects add noise to output.
|
||||
|
||||
### Use join-table approach matching the API structure
|
||||
**Decision**: Keep `CardLabel` as the join entity (cardId + labelId), and resolve to label names at the point of use (in `ListCardsByBoard`). This mirrors the Planka API's relational structure.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[Additive JSON fields]** → `card list --board` output will include a new `labels` array per card. Existing consumers that don't use this field are unaffected. Scripts using strict JSON parsing may need updating.
|
||||
- **[Partial included parsing]** → We still skip users, taskLists, attachments, etc. This is intentional — parse only what's needed. Future changes can add more fields incrementally.
|
||||
@@ -0,0 +1,26 @@
|
||||
## Why
|
||||
|
||||
The `GetBoard` API response includes `cardLabels`, `labels`, and `cardMemberships` in its `included` data, but `pcli` only parses `lists` and `cards` — silently discarding the rest. This means there is no way to determine which labels are attached to which cards without making per-card API calls. The kanban sync workflow needs to identify agent-labelled cards from a board listing, and currently must fall back to name-matching because label data is unavailable.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Expand the `Board` struct in `model/types.go` to include `Labels`, `CardLabels`, and `CardMemberships` fields
|
||||
- Update `GetBoard` in `client/boards.go` to parse these additional `included` fields from the API response
|
||||
- Update `ListCardsByBoard` to enrich card output with label names (via `cardLabels` join table + `labels`)
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
_None — this enhances existing capabilities._
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `api-client`: `GetBoard` SHALL parse `labels`, `cardLabels`, and `cardMemberships` from the board response `included` data
|
||||
- `card-operations`: `ListCardsByBoard` (card list --board) SHALL include label names on each card
|
||||
|
||||
## Impact
|
||||
|
||||
- **Code**: `model/types.go` (Board struct), `client/boards.go` (GetBoard parsing), `client/cards.go` (ListCardsByBoard enrichment)
|
||||
- **API**: No new API calls — parsing data already returned by `GET /api/boards/:id`
|
||||
- **Breaking changes**: None — new fields are additive; JSON output gains new fields but existing fields unchanged
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Board operations
|
||||
The client SHALL provide a method to get a single board by ID (`GET /boards/{id}`), list board actions (`GET /boards/{boardId}/actions`) with pagination support, create a board (`POST /projects/{projectId}/boards`), and delete a board (`DELETE /boards/{id}`). `GetBoard` SHALL parse the `included` object from the response and populate the Board model with `lists`, `cards`, `labels`, `cardLabels`, and `cardMemberships`.
|
||||
|
||||
#### 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, cards, labels, cardLabels, and cardMemberships
|
||||
|
||||
#### Scenario: Get board includes labels
|
||||
- **WHEN** `GetBoard` is called and the board has labels defined
|
||||
- **THEN** the returned Board SHALL contain a `Labels` slice with all board labels
|
||||
|
||||
#### Scenario: Get board includes card-label associations
|
||||
- **WHEN** `GetBoard` is called and cards on the board have labels attached
|
||||
- **THEN** the returned Board SHALL contain a `CardLabels` slice with all card-label associations
|
||||
- **AND** each `CardLabel` entry SHALL contain `cardId` and `labelId` fields
|
||||
|
||||
#### Scenario: Get board includes card memberships
|
||||
- **WHEN** `GetBoard` is called and cards on the board have members assigned
|
||||
- **THEN** the returned Board SHALL contain a `CardMemberships` slice with all card-membership associations
|
||||
|
||||
#### Scenario: List board actions
|
||||
- **WHEN** `ListBoardActions` is called with a board ID and limit
|
||||
- **THEN** the client SHALL paginate through `GET /boards/{boardId}/actions` and return action items
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### 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 and a `labels` field. The operation SHALL: (1) call `GET /boards/{id}` to retrieve the board and its included lists, cards, labels, and cardLabels, (2) build a card-to-label-names map by joining `cardLabels` with `labels`, (3) inject `listName` and `labels` into each card. The `labels` field SHALL be an array of label name strings. 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 `labels` field with an array of label names attached to that card
|
||||
|
||||
#### Scenario: Card with multiple labels
|
||||
- **WHEN** a card has two labels ("bug" and "urgent") attached
|
||||
- **THEN** the `labels` array for that card SHALL contain both "bug" and "urgent"
|
||||
|
||||
#### Scenario: Card with no labels
|
||||
- **WHEN** a card has no labels attached
|
||||
- **THEN** the `labels` array for that card SHALL be an empty array (not null)
|
||||
|
||||
#### 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` and `labels` fields
|
||||
@@ -0,0 +1,15 @@
|
||||
## 1. Model Changes
|
||||
|
||||
- [x] 1.1 Add `Labels []Label`, `CardLabels []CardLabel`, and `CardMemberships []CardMembership` fields to `Board` struct in `model/types.go` (with `json:",omitempty"`)
|
||||
- [x] 1.2 Add `Labels []string` field to `CardWithList` struct in `model/types.go`
|
||||
|
||||
## 2. Client Changes
|
||||
|
||||
- [x] 2.1 Update `GetBoard` in `client/boards.go` to parse `labels`, `cardLabels`, and `cardMemberships` from `included` response
|
||||
- [x] 2.2 Update `ListCardsByBoard` in `client/cards.go` to build label-name map from board's `CardLabels` and `Labels`, and populate `Labels` on each `CardWithList`
|
||||
|
||||
## 3. Verification
|
||||
|
||||
- [x] 3.1 Build and test `pcli board get <id>` — verify JSON output includes labels and cardLabels when present
|
||||
- [x] 3.2 Test `pcli card list --board <id>` — verify each card includes a `labels` array
|
||||
- [x] 3.3 Test card with no labels returns empty array (not null)
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-19
|
||||
@@ -0,0 +1,86 @@
|
||||
## Context
|
||||
|
||||
The kanban sync workflow is currently a 280-line markdown document (`.claude/commands/kanban-sync.md`) that the LLM reads and interprets each time sync is triggered. The sync logic is deterministic — it reads OpenSpec state, compares it with Planka board state, and reconciles. No LLM judgment is needed. The overhead is significant: each sync consumes tokens for reasoning through ~10 phases and executing ~15-30 shell commands.
|
||||
|
||||
There is also no concurrency protection. If two opsx workflows complete in quick succession, two syncs could run simultaneously and produce conflicting Planka API calls.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Replace LLM-interpreted sync with a standalone bash script
|
||||
- Implement concurrency control so only one sync runs at a time
|
||||
- Support coalescing: if sync is requested while one runs, queue exactly one follow-up
|
||||
- Support foreground (default, for humans) and background (for LLM/skill invocation) modes
|
||||
- Keep the existing sync logic and board structure unchanged
|
||||
|
||||
**Non-Goals:**
|
||||
- Changing the sync algorithm or board layout
|
||||
- Adding bidirectional sync (Planka → OpenSpec)
|
||||
- Supporting multiple simultaneous boards
|
||||
- Adding a daemon/service mode — this remains an on-demand script
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Externally deployed bash script (`kanban-project-sync`)
|
||||
|
||||
**Rationale**: The sync logic uses `pcli`, `jq`, `yq`, and `openspec` CLI tools — all shell-native. A bash script is the natural fit. The script is deployed to developer instances via existing tooling and placed on PATH, like other shared scripts. It does not live in the project repo.
|
||||
|
||||
**Convention**: The script assumes it is run from the project root directory (uses `pwd` to locate `openspec/` directory). This is consistent with how `openspec` and `pcli` already work.
|
||||
|
||||
**Alternative considered**: A Go subcommand in pcli (`pcli sync`). Rejected because the sync orchestrates `openspec` (a separate tool) and would require shelling out anyway. The script approach keeps concerns separated.
|
||||
|
||||
**Alternative considered**: Placing the script in the project root alongside `build.sh`. Rejected — the script is a shared developer tool, not project-specific. External deployment keeps it consistent with other tooling.
|
||||
|
||||
### 2. `flock` for locking with pending file for coalescing
|
||||
|
||||
**Rationale**: `flock` is the standard POSIX advisory locking mechanism. It's atomic, handles process crashes (lock auto-releases), and avoids manual PID-file management. The pending flag file is a simple touch/rm mechanism that coalesces multiple queued requests into one re-run.
|
||||
|
||||
**Lock file**: `/tmp/kanban-project-sync-<project>-<board>.lock` — scoped per project-board to allow independent syncs for different boards.
|
||||
|
||||
**Pending file**: `/tmp/kanban-project-sync-<project>-<board>.pending` — touched by callers that can't acquire the lock.
|
||||
|
||||
**Flow**:
|
||||
```
|
||||
Caller:
|
||||
Try flock (non-blocking)
|
||||
├── Got lock:
|
||||
│ loop:
|
||||
│ run_sync()
|
||||
│ if pending file exists: rm pending, continue loop
|
||||
│ else: break
|
||||
│ release lock (fd close)
|
||||
│
|
||||
└── Lock held:
|
||||
touch pending file
|
||||
exit 0
|
||||
```
|
||||
|
||||
**Alternative considered**: PID file with `kill -0` checks. Rejected — racy, doesn't handle crashes cleanly, more complex than `flock`.
|
||||
|
||||
### 3. Foreground default, `--background` flag for detached mode
|
||||
|
||||
**Rationale**: Humans running the script manually want to see output. The LLM/skill always passes `--background` to avoid blocking. Background mode uses `nohup` + `&` with output redirected to a log file at `/tmp/kanban-project-sync-<project>-<board>.log`.
|
||||
|
||||
### 4. Script reads `project.yaml` for defaults but CLI args override
|
||||
|
||||
**Rationale**: `--project` and `--board` are required arguments. The script does NOT read `project.yaml` itself — that's the caller's concern. This keeps the script generic. The kanban-sync command/skill reads `project.yaml` and passes the values. A human can pass whatever project/board they want.
|
||||
|
||||
### 5. Exit codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 0 | Success (sync completed) or queued (pending flag set) |
|
||||
| 1 | Error (sync failed) |
|
||||
| 2 | Skipped (Planka offline / connectivity check failed) |
|
||||
|
||||
When a caller can't acquire the lock and sets the pending flag, it exits 0 — from the caller's perspective, the sync will happen.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
**[`flock` availability on macOS]** → macOS doesn't ship `flock` by default. Mitigation: document that `brew install util-linux` is needed. This is a dev tooling script, not a production binary — acceptable friction.
|
||||
|
||||
**[Background mode log management]** → Log files in `/tmp` accumulate. Mitigation: Each run overwrites the log file (not appends), so only the last run's output is kept per project-board pair.
|
||||
|
||||
**[Pending flag race window]** → Tiny window between "check pending" and "release lock" where a new pending could be missed. Mitigation: The check-and-release happens while still holding the flock, so no other process can set pending between check and release. The lock holder is the only one that reads/clears the pending file.
|
||||
|
||||
**[Script drift from sync logic]** → If the sync algorithm changes, the script must be updated manually (no longer auto-follows the markdown doc). Mitigation: The sync logic has been stable. The kanban-sync.md doc will reference the script, making it clear where changes go.
|
||||
@@ -0,0 +1,30 @@
|
||||
## Why
|
||||
|
||||
The kanban sync is currently implemented as a detailed workflow document that the LLM interprets and executes command-by-command. Each sync burns significant tokens as the LLM reasons through ~10 phases and executes ~15-30 `pcli` commands. The sync logic is entirely deterministic — there are no decisions that require LLM judgment. Moving this to a standalone bash script eliminates the overhead and makes sync near-instant. Additionally, there is no concurrency protection — two syncs could run simultaneously and clash on Planka API state.
|
||||
|
||||
## What Changes
|
||||
|
||||
- New `kanban-project-sync` bash script (deployed externally, on PATH) that performs the full 10-phase Planka board reconciliation
|
||||
- Script accepts `--project` and `--board` as required inputs, with optional `--background` flag
|
||||
- Script assumes it is run from the project root (uses `pwd` to locate `openspec/` directory)
|
||||
- Implements `flock`-based concurrency control with a coalescing pending flag (depth-1 queue)
|
||||
- Default mode is foreground (human-friendly); `--background` detaches for fire-and-forget use
|
||||
- `.claude/commands/kanban-sync.md` simplified to just invoke the script with `--background`
|
||||
- `CLAUDE.md` sync instructions updated to reference the script
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `kanban-sync-concurrency`: Concurrency control for sync execution — flock-based locking with a pending flag that coalesces multiple requests, and automatic re-run after completion if a sync was requested during execution
|
||||
|
||||
### Modified Capabilities
|
||||
_(none — no existing spec-level requirements change)_
|
||||
|
||||
## Impact
|
||||
|
||||
- **New external script**: `kanban-project-sync` — deployed to developer instances via existing tooling, placed on PATH
|
||||
- **Modified**: `.claude/commands/kanban-sync.md` — reduced from full workflow to script invocation
|
||||
- **Modified**: `CLAUDE.md` — updated Planka Sync section
|
||||
- **Dependencies**: Requires `flock` (standard on Linux, available on macOS via `brew install util-linux`), `jq`, `yq`, `pcli`, `openspec` on PATH
|
||||
- **No Go code changes** — this is entirely in the tooling/workflow layer
|
||||
- **Convention**: Script must be run from the project root directory (same as `openspec` and `pcli`)
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Exclusive sync execution
|
||||
The sync script SHALL use `flock` advisory locking to ensure only one sync instance runs at a time per project-board pair. The lock file SHALL be located at `/tmp/kanban-project-sync-<project>-<board>.lock`.
|
||||
|
||||
#### Scenario: First sync acquires lock
|
||||
- **WHEN** kanban-project-sync is invoked and no other sync is running for the same project-board
|
||||
- **THEN** the script acquires the flock and executes the full sync
|
||||
|
||||
#### Scenario: Second sync finds lock held
|
||||
- **WHEN** kanban-project-sync is invoked while another sync is running for the same project-board
|
||||
- **THEN** the script SHALL NOT execute the sync, SHALL touch the pending flag file, and SHALL exit with code 0
|
||||
|
||||
### Requirement: Coalescing pending queue
|
||||
The sync script SHALL implement a depth-1 coalescing queue using a pending flag file at `/tmp/kanban-project-sync-<project>-<board>.pending`. Multiple pending requests SHALL coalesce into a single re-run.
|
||||
|
||||
#### Scenario: Pending flag set during sync
|
||||
- **WHEN** a sync completes and the pending flag file exists
|
||||
- **THEN** the script SHALL clear the pending flag and re-run the sync before releasing the lock
|
||||
|
||||
#### Scenario: No pending flag after sync
|
||||
- **WHEN** a sync completes and no pending flag file exists
|
||||
- **THEN** the script SHALL release the lock and exit
|
||||
|
||||
#### Scenario: Multiple pending requests coalesce
|
||||
- **WHEN** three sync requests arrive while a sync is running
|
||||
- **THEN** only one additional sync SHALL run after the current one completes (not three)
|
||||
|
||||
### Requirement: Foreground and background modes
|
||||
The sync script SHALL default to foreground execution (output to stdout/stderr). When `--background` is passed, the script SHALL detach and redirect output to a log file at `/tmp/kanban-project-sync-<project>-<board>.log`.
|
||||
|
||||
#### Scenario: Foreground execution
|
||||
- **WHEN** kanban-project-sync is invoked without `--background`
|
||||
- **THEN** sync output SHALL be written to stdout/stderr and the script SHALL block until complete
|
||||
|
||||
#### Scenario: Background execution
|
||||
- **WHEN** kanban-project-sync is invoked with `--background`
|
||||
- **THEN** the script SHALL detach from the terminal, redirect output to the log file, and return immediately
|
||||
|
||||
### Requirement: Required arguments
|
||||
The sync script SHALL require `--project <name>` and `--board <name>` arguments. The script SHALL NOT read `project.yaml` directly.
|
||||
|
||||
#### Scenario: Missing arguments
|
||||
- **WHEN** kanban-project-sync is invoked without `--project` or `--board`
|
||||
- **THEN** the script SHALL print usage information and exit with code 1
|
||||
|
||||
### Requirement: Connectivity check
|
||||
The sync script SHALL run `pcli status` before syncing. If the check fails, the script SHALL exit with code 2.
|
||||
|
||||
#### Scenario: Planka offline
|
||||
- **WHEN** `pcli status` returns a non-zero exit code
|
||||
- **THEN** the script SHALL print a message indicating Planka is unreachable and exit with code 2
|
||||
|
||||
### Requirement: Idempotent sync phases
|
||||
The sync script SHALL implement all 10 phases of the existing kanban-sync workflow: connectivity check, bootstrap infrastructure (find-or-create project, board, lists, label), gather OpenSpec state, determine target lists, gather Planka agent cards, create missing cards, move cards to correct lists, sync task checklists, handle archived changes, and report. Each phase SHALL be idempotent — running the script twice with no state changes SHALL produce no Planka API mutations on the second run.
|
||||
|
||||
#### Scenario: Clean re-run with no changes
|
||||
- **WHEN** the sync script runs twice in succession with no OpenSpec or Planka state changes between runs
|
||||
- **THEN** the second run SHALL make no create/update/move API calls and SHALL report zero changes
|
||||
@@ -0,0 +1,32 @@
|
||||
## 1. Script Skeleton and Concurrency
|
||||
|
||||
- [x] 1.1 Create `kanban-project-sync` script with argument parsing (`--project`, `--board`, `--background`), usage help, and exit code constants
|
||||
- [x] 1.2 Implement `flock`-based locking with pending flag file and re-run loop
|
||||
- [x] 1.3 Implement `--background` mode (re-exec self detached with output to log file)
|
||||
- [x] 1.4 Add connectivity check (`pcli status`) with exit code 2 on failure
|
||||
|
||||
## 2. Bootstrap Phase
|
||||
|
||||
- [x] 2.1 Implement find-or-create project by name
|
||||
- [x] 2.2 Implement find-or-create board by name within project
|
||||
- [x] 2.3 Implement find-or-create lists (Backlog, To Do, Planning, In Progress, Review, Done) with correct positions
|
||||
- [x] 2.4 Implement find-or-create `agent` label on the board
|
||||
|
||||
## 3. Reconciliation Phases
|
||||
|
||||
- [x] 3.1 Gather OpenSpec state — list active changes, parse artifact completion and task checkbox state
|
||||
- [x] 3.2 Determine target list for each change based on OpenSpec state (Planning / In Progress / Review)
|
||||
- [x] 3.3 Gather existing Planka agent-labelled cards and build name→card map
|
||||
- [x] 3.4 Create missing cards (new OpenSpec changes without a Planka card) with agent label
|
||||
- [x] 3.5 Move cards to correct list when current list doesn't match target
|
||||
- [x] 3.6 Sync task checklists — create/update task lists and tasks on cards from `tasks.md`
|
||||
- [x] 3.7 Move orphaned agent cards (no matching active change) to Done
|
||||
|
||||
## 4. Reporting and Summary
|
||||
|
||||
- [x] 4.1 Add summary output — infrastructure created, cards created/moved, tasks synced, errors
|
||||
|
||||
## 5. Skill and Documentation Updates
|
||||
|
||||
- [x] 5.1 Simplify `.claude/commands/kanban-sync.md` to invoke `kanban-project-sync --background`
|
||||
- [x] 5.2 Update `CLAUDE.md` Planka Sync section to reference the script
|
||||
@@ -1,20 +0,0 @@
|
||||
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
|
||||
@@ -52,7 +52,7 @@ The system SHALL implement cursor-based pagination for all list endpoints that s
|
||||
- **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}`).
|
||||
The client SHALL provide methods to list all accessible projects (`GET /projects`), get a single project by ID (`GET /projects/{id}`), create a project (`POST /projects`), and delete a project (`DELETE /projects/{id}`).
|
||||
|
||||
#### Scenario: List projects
|
||||
- **WHEN** `ListProjects` is called
|
||||
@@ -62,17 +62,46 @@ The client SHALL provide methods to list all accessible projects (`GET /projects
|
||||
- **WHEN** `GetProject` is called with a project ID
|
||||
- **THEN** the client SHALL send `GET /projects/{id}` and return a Project model
|
||||
|
||||
#### Scenario: Create project
|
||||
- **WHEN** `CreateProject` is called with project fields (type, name, description)
|
||||
- **THEN** the client SHALL send `POST /projects` with the provided fields and return the created Project
|
||||
|
||||
#### Scenario: Delete project
|
||||
- **WHEN** `DeleteProject` is called with a project ID
|
||||
- **THEN** the client SHALL send `DELETE /projects/{id}`
|
||||
|
||||
### 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.
|
||||
The client SHALL provide a method to get a single board by ID (`GET /boards/{id}`), list board actions (`GET /boards/{boardId}/actions`) with pagination support, create a board (`POST /projects/{projectId}/boards`), and delete a board (`DELETE /boards/{id}`). `GetBoard` SHALL parse the `included` object from the response and populate the Board model with `lists`, `cards`, `labels`, `cardLabels`, and `cardMemberships`.
|
||||
|
||||
#### 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
|
||||
- **THEN** the client SHALL send `GET /boards/{id}` and return a Board model including its included lists, cards, labels, cardLabels, and cardMemberships
|
||||
|
||||
#### Scenario: Get board includes labels
|
||||
- **WHEN** `GetBoard` is called and the board has labels defined
|
||||
- **THEN** the returned Board SHALL contain a `Labels` slice with all board labels
|
||||
|
||||
#### Scenario: Get board includes card-label associations
|
||||
- **WHEN** `GetBoard` is called and cards on the board have labels attached
|
||||
- **THEN** the returned Board SHALL contain a `CardLabels` slice with all card-label associations
|
||||
- **AND** each `CardLabel` entry SHALL contain `cardId` and `labelId` fields
|
||||
|
||||
#### Scenario: Get board includes card memberships
|
||||
- **WHEN** `GetBoard` is called and cards on the board have members assigned
|
||||
- **THEN** the returned Board SHALL contain a `CardMemberships` slice with all card-membership associations
|
||||
|
||||
#### 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
|
||||
|
||||
#### Scenario: Create board
|
||||
- **WHEN** `CreateBoard` is called with a project ID and board fields (name, position)
|
||||
- **THEN** the client SHALL send `POST /projects/{projectId}/boards` with the provided fields and return the created Board
|
||||
|
||||
#### Scenario: Delete board
|
||||
- **WHEN** `DeleteBoard` is called with a board ID
|
||||
- **THEN** the client SHALL send `DELETE /boards/{id}`
|
||||
|
||||
### 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.
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Board List Filtering
|
||||
|
||||
## Purpose
|
||||
|
||||
Provides filtering capabilities for the `board list` command to scope results by project name.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Board list project filtering
|
||||
The system SHALL provide a `--project <name>` flag for the `board list` command. When the flag is provided, the system SHALL resolve the project name to a project ID by calling `ListProjects()` and performing a case-insensitive exact match on the project name. If no matching project is found, the system SHALL output an error message "project not found: <name>" and exit with code 1. If a matching project is found, the system SHALL filter the board list to include only boards where `projectId` matches the resolved project ID. When the `--project` flag is omitted, the system SHALL list all accessible boards without filtering.
|
||||
|
||||
#### Scenario: List boards without project filter
|
||||
- **WHEN** `pcli board list` is executed without the `--project` flag
|
||||
- **THEN** the system SHALL output all accessible boards across all projects
|
||||
|
||||
#### Scenario: List boards filtered by project name
|
||||
- **WHEN** `pcli board list --project "project1"` is executed
|
||||
- **THEN** the system SHALL resolve "project1" to a project ID
|
||||
- **AND** the system SHALL output only boards belonging to that project
|
||||
|
||||
#### Scenario: List boards with case-insensitive project match
|
||||
- **WHEN** `pcli board list --project "PROJECT1"` is executed and a project named "project1" exists
|
||||
- **THEN** the system SHALL match the project case-insensitively
|
||||
- **AND** the system SHALL output boards belonging to the matched project
|
||||
|
||||
#### Scenario: Project name not found
|
||||
- **WHEN** `pcli board list --project "nonexistent"` is executed
|
||||
- **THEN** the system SHALL output "project not found: nonexistent"
|
||||
- **AND** the system SHALL exit with code 1
|
||||
|
||||
#### Scenario: Project exists but has no boards
|
||||
- **WHEN** `pcli board list --project "empty-project"` is executed for a project with no boards
|
||||
- **THEN** the system SHALL output an empty list (or empty JSON array in JSON format)
|
||||
|
||||
#### Scenario: User lacks access to project
|
||||
- **WHEN** `pcli board list --project "restricted"` is executed for a project the user cannot access
|
||||
- **THEN** the system SHALL output "project not found: restricted"
|
||||
- **AND** the system SHALL exit with code 1
|
||||
@@ -65,18 +65,26 @@ The system SHALL provide `card add-label` and `card remove-label` operations to
|
||||
- **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.
|
||||
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 and a `labels` field. The operation SHALL: (1) call `GET /boards/{id}` to retrieve the board and its included lists, cards, labels, and cardLabels, (2) build a card-to-label-names map by joining `cardLabels` with `labels`, (3) inject `listName` and `labels` into each card. The `labels` field SHALL be an array of label name strings. 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
|
||||
- **AND** each card SHALL include a `labels` field with an array of label names attached to that card
|
||||
|
||||
#### Scenario: Card with multiple labels
|
||||
- **WHEN** a card has two labels ("bug" and "urgent") attached
|
||||
- **THEN** the `labels` array for that card SHALL contain both "bug" and "urgent"
|
||||
|
||||
#### Scenario: Card with no labels
|
||||
- **WHEN** a card has no labels attached
|
||||
- **THEN** the `labels` array for that card SHALL be an empty array (not null)
|
||||
|
||||
#### 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
|
||||
- **AND** each card SHALL include the `listName` and `labels` fields
|
||||
|
||||
#### Scenario: Board with no cards
|
||||
- **WHEN** `pcli card list --board <id>` is executed on a board with empty lists
|
||||
|
||||
@@ -22,7 +22,7 @@ The system SHALL provide a root command `pcli` that serves as the entry point. T
|
||||
- **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.
|
||||
The system SHALL provide a `project` command group with subcommands `list`, `get`, `create`, and `delete`. `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. `pcli project create` SHALL create a new project. `pcli project delete <id>` SHALL delete a project.
|
||||
|
||||
#### Scenario: List projects
|
||||
- **WHEN** `pcli project list` is executed
|
||||
@@ -36,8 +36,43 @@ The system SHALL provide a `project` command group with subcommands `list` and `
|
||||
- **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
|
||||
|
||||
#### Scenario: Create project
|
||||
- **WHEN** `pcli project create --name "My Project" --type private --description "A test project"` is executed
|
||||
- **THEN** the system SHALL create the project and output the created project
|
||||
|
||||
#### Scenario: Create project missing required flags
|
||||
- **WHEN** `pcli project create` is executed without `--name` or `--type`
|
||||
- **THEN** the system SHALL print an error indicating the required flags and exit with code 1
|
||||
|
||||
#### Scenario: Create project with insufficient permissions
|
||||
- **WHEN** `pcli project create` is executed with invalid API credentials
|
||||
- **THEN** the system SHALL output "create project: authentication failed — check your API key"
|
||||
|
||||
#### Scenario: Delete project
|
||||
- **WHEN** `pcli project delete <id>` is executed with a valid project ID
|
||||
- **THEN** the system SHALL delete the project and output a success confirmation
|
||||
|
||||
#### Scenario: Delete project with insufficient permissions
|
||||
- **WHEN** `pcli project delete <id>` is executed by a user without project manager permissions
|
||||
- **THEN** the system SHALL output "delete project: permission denied (requires project manager role)"
|
||||
|
||||
#### Scenario: Delete project not found
|
||||
- **WHEN** `pcli project delete <id>` is executed with a non-existent project ID
|
||||
- **THEN** the system SHALL output "delete project: not found — the resource may not exist or you may not have access to it"
|
||||
|
||||
### Requirement: Board list command
|
||||
The system SHALL provide a `board list` subcommand. `pcli board list` SHALL call the client's ListBoards method and output all accessible boards. The command SHALL accept an optional `--project <name>` flag (string). When `--project` is provided, the system SHALL filter boards to only those belonging to the specified project (see board-list-filtering spec for filtering behavior).
|
||||
|
||||
#### Scenario: List all boards
|
||||
- **WHEN** `pcli board list` is executed without flags
|
||||
- **THEN** the system SHALL output all accessible boards
|
||||
|
||||
#### Scenario: List boards filtered by project
|
||||
- **WHEN** `pcli board list --project "project1"` is executed
|
||||
- **THEN** the system SHALL output only boards belonging to the specified project
|
||||
|
||||
### 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.
|
||||
The system SHALL provide a `board` command group with subcommands `list`, `get`, `actions`, `create`, and `delete`. `pcli board list` SHALL list all accessible boards and accept an optional `--project <name>` flag for filtering. `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. `pcli board create` SHALL create a new board. `pcli board delete <id>` SHALL delete a board.
|
||||
|
||||
#### Scenario: Get board
|
||||
- **WHEN** `pcli board get <id>` is executed
|
||||
@@ -51,6 +86,34 @@ The system SHALL provide a `board` command group with subcommands `get` and `act
|
||||
- **WHEN** `pcli board actions <id> --limit 10` is executed
|
||||
- **THEN** the system SHALL output at most 10 action entries
|
||||
|
||||
#### Scenario: Create board
|
||||
- **WHEN** `pcli board create --project <id> --name "Development Board" --position 65536` is executed
|
||||
- **THEN** the system SHALL create the board and output the created board
|
||||
|
||||
#### Scenario: Create board missing required flags
|
||||
- **WHEN** `pcli board create` is executed without `--project` or `--name`
|
||||
- **THEN** the system SHALL print an error indicating the required flags and exit with code 1
|
||||
|
||||
#### Scenario: Create board with insufficient permissions
|
||||
- **WHEN** `pcli board create --project <id> --name "Board"` is executed by a user without project manager permissions on the project
|
||||
- **THEN** the system SHALL output "create board: permission denied (requires project manager role)"
|
||||
|
||||
#### Scenario: Create board project not found
|
||||
- **WHEN** `pcli board create --project <id> --name "Board"` is executed with a project the user cannot access
|
||||
- **THEN** the system SHALL output "create board: not found — the resource may not exist or you may not have access to it"
|
||||
|
||||
#### Scenario: Delete board
|
||||
- **WHEN** `pcli board delete <id>` is executed with a valid board ID
|
||||
- **THEN** the system SHALL delete the board and output a success confirmation
|
||||
|
||||
#### Scenario: Delete board with insufficient permissions
|
||||
- **WHEN** `pcli board delete <id>` is executed by a user without project manager permissions
|
||||
- **THEN** the system SHALL output "delete board: permission denied (requires project manager role)"
|
||||
|
||||
#### Scenario: Delete board not found
|
||||
- **WHEN** `pcli board delete <id>` is executed with a non-existent board ID
|
||||
- **THEN** the system SHALL output "delete board: not found — the resource may not exist or you may not have access to it"
|
||||
|
||||
### 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`.
|
||||
|
||||
@@ -178,12 +241,16 @@ The system SHALL provide a `task` command group with subcommands: `create`, `upd
|
||||
- **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.
|
||||
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 accept an optional `--project <name>` flag (string) to filter boards by project name. The command SHALL fetch all boards via `ListBoards`, optionally filter by project, 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: Run status command with project filter
|
||||
- **WHEN** `pcli status --project "MyProject"` is executed
|
||||
- **THEN** the system SHALL output a summary of only boards belonging to "MyProject"
|
||||
|
||||
#### Scenario: Status command respects format flag
|
||||
- **WHEN** `pcli status --format table` is executed
|
||||
- **THEN** the output SHALL be in table format
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
### Requirement: Exclusive sync execution
|
||||
The sync script SHALL use `flock` advisory locking to ensure only one sync instance runs at a time per project-board pair. The lock file SHALL be located at `/tmp/kanban-project-sync-<project>-<board>.lock`.
|
||||
|
||||
#### Scenario: First sync acquires lock
|
||||
- **WHEN** kanban-project-sync is invoked and no other sync is running for the same project-board
|
||||
- **THEN** the script acquires the flock and executes the full sync
|
||||
|
||||
#### Scenario: Second sync finds lock held
|
||||
- **WHEN** kanban-project-sync is invoked while another sync is running for the same project-board
|
||||
- **THEN** the script SHALL NOT execute the sync, SHALL touch the pending flag file, and SHALL exit with code 0
|
||||
|
||||
### Requirement: Coalescing pending queue
|
||||
The sync script SHALL implement a depth-1 coalescing queue using a pending flag file at `/tmp/kanban-project-sync-<project>-<board>.pending`. Multiple pending requests SHALL coalesce into a single re-run.
|
||||
|
||||
#### Scenario: Pending flag set during sync
|
||||
- **WHEN** a sync completes and the pending flag file exists
|
||||
- **THEN** the script SHALL clear the pending flag and re-run the sync before releasing the lock
|
||||
|
||||
#### Scenario: No pending flag after sync
|
||||
- **WHEN** a sync completes and no pending flag file exists
|
||||
- **THEN** the script SHALL release the lock and exit
|
||||
|
||||
#### Scenario: Multiple pending requests coalesce
|
||||
- **WHEN** three sync requests arrive while a sync is running
|
||||
- **THEN** only one additional sync SHALL run after the current one completes (not three)
|
||||
|
||||
### Requirement: Foreground and background modes
|
||||
The sync script SHALL default to foreground execution (output to stdout/stderr). When `--background` is passed, the script SHALL detach and redirect output to a log file at `/tmp/kanban-project-sync-<project>-<board>.log`.
|
||||
|
||||
#### Scenario: Foreground execution
|
||||
- **WHEN** kanban-project-sync is invoked without `--background`
|
||||
- **THEN** sync output SHALL be written to stdout/stderr and the script SHALL block until complete
|
||||
|
||||
#### Scenario: Background execution
|
||||
- **WHEN** kanban-project-sync is invoked with `--background`
|
||||
- **THEN** the script SHALL detach from the terminal, redirect output to the log file, and return immediately
|
||||
|
||||
### Requirement: Required arguments
|
||||
The sync script SHALL require `--project <name>` and `--board <name>` arguments. The script SHALL NOT read `project.yaml` directly.
|
||||
|
||||
#### Scenario: Missing arguments
|
||||
- **WHEN** kanban-project-sync is invoked without `--project` or `--board`
|
||||
- **THEN** the script SHALL print usage information and exit with code 1
|
||||
|
||||
### Requirement: Connectivity check
|
||||
The sync script SHALL run `pcli status` before syncing. If the check fails, the script SHALL exit with code 2.
|
||||
|
||||
#### Scenario: Planka offline
|
||||
- **WHEN** `pcli status` returns a non-zero exit code
|
||||
- **THEN** the script SHALL print a message indicating Planka is unreachable and exit with code 2
|
||||
|
||||
### Requirement: Idempotent sync phases
|
||||
The sync script SHALL implement all 10 phases of the existing kanban-sync workflow: connectivity check, bootstrap infrastructure (find-or-create project, board, lists, label), gather OpenSpec state, determine target lists, gather Planka agent cards, create missing cards, move cards to correct lists, sync task checklists, handle archived changes, and report. Each phase SHALL be idempotent — running the script twice with no state changes SHALL produce no Planka API mutations on the second run.
|
||||
|
||||
#### Scenario: Clean re-run with no changes
|
||||
- **WHEN** the sync script runs twice in succession with no OpenSpec or Planka state changes between runs
|
||||
- **THEN** the second run SHALL make no create/update/move API calls and SHALL report zero changes
|
||||
@@ -0,0 +1,28 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Permission error translation helper
|
||||
The system SHALL provide a `friendlyAPIError` function in the `cmd` package that translates APIError status codes into human-readable messages with operation context.
|
||||
|
||||
#### Scenario: Authentication error (401)
|
||||
- **WHEN** an APIError with StatusCode 401 is passed to friendlyAPIError with operation "create board"
|
||||
- **THEN** the function SHALL return an error with message "create board: authentication failed — check your API key"
|
||||
|
||||
#### Scenario: Permission denied (403) with hint
|
||||
- **WHEN** an APIError with StatusCode 403 is passed to friendlyAPIError with operation "create board" and permissionHint "requires project manager role"
|
||||
- **THEN** the function SHALL return an error with message "create board: permission denied (requires project manager role)"
|
||||
|
||||
#### Scenario: Permission denied (403) without hint
|
||||
- **WHEN** an APIError with StatusCode 403 is passed to friendlyAPIError with operation "create project" and empty permissionHint
|
||||
- **THEN** the function SHALL return an error with message "create project: permission denied"
|
||||
|
||||
#### Scenario: Not found (404)
|
||||
- **WHEN** an APIError with StatusCode 404 is passed to friendlyAPIError with operation "delete board"
|
||||
- **THEN** the function SHALL return an error with message "delete board: not found — the resource may not exist or you may not have access to it"
|
||||
|
||||
#### Scenario: Non-API error
|
||||
- **WHEN** a non-APIError (e.g., network error) is passed to friendlyAPIError
|
||||
- **THEN** the function SHALL return the original error unchanged
|
||||
|
||||
#### Scenario: Unknown status code
|
||||
- **WHEN** an APIError with StatusCode 500 is passed to friendlyAPIError
|
||||
- **THEN** the function SHALL return the original APIError unchanged
|
||||
@@ -1,7 +1,32 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Status command project filtering
|
||||
The system SHALL accept an optional `--project <name>` flag on the `pcli status` command. When `--project` is provided, the system SHALL resolve the project name to a project ID using case-insensitive exact matching against all accessible projects. The system SHALL then filter the board list to include only boards whose `projectId` matches the resolved project ID. The `totalBoards` count in the output SHALL reflect the filtered board count. When `--project` is omitted, behavior SHALL be unchanged (all boards shown).
|
||||
|
||||
#### Scenario: Status filtered by project name
|
||||
- **WHEN** `pcli status --project "MyProject"` is executed and the project exists with 2 boards
|
||||
- **THEN** the output SHALL include only the 2 boards belonging to "MyProject"
|
||||
- **AND** `totalBoards` SHALL be 2
|
||||
|
||||
#### Scenario: Status filtered by project name case-insensitive
|
||||
- **WHEN** `pcli status --project "myproject"` is executed and a project named "MyProject" exists
|
||||
- **THEN** the output SHALL include boards belonging to "MyProject"
|
||||
|
||||
#### Scenario: Status with project filter and no matching boards
|
||||
- **WHEN** `pcli status --project "EmptyProject"` is executed and the project exists but has no boards
|
||||
- **THEN** the output SHALL show `totalBoards` as 0 and an empty boards array
|
||||
|
||||
#### Scenario: Status with project not found
|
||||
- **WHEN** `pcli status --project "NonExistent"` is executed and no project with that name exists
|
||||
- **THEN** the system SHALL output an error "project not found: NonExistent"
|
||||
- **AND** the system SHALL exit with code 1
|
||||
|
||||
#### Scenario: Status without project flag
|
||||
- **WHEN** `pcli status` is executed without `--project`
|
||||
- **THEN** the output SHALL include all boards across all projects (unchanged behavior)
|
||||
|
||||
### 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.
|
||||
The system SHALL provide a top-level `pcli status` command that outputs a summary of all boards (or boards filtered by `--project`), 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
|
||||
|
||||
@@ -93,6 +93,8 @@ func printTable(data any, w io.Writer) error {
|
||||
return printTaskTable(data.([]model.Task), tw)
|
||||
case "Label":
|
||||
return printLabelTable(data.([]model.Label), tw)
|
||||
case "List":
|
||||
return printListTable(data.([]model.List), tw)
|
||||
case "Action":
|
||||
return printActionTable(data.([]model.Action), tw)
|
||||
default:
|
||||
@@ -107,6 +109,8 @@ func printTable(data any, w io.Writer) error {
|
||||
return printBoardTable([]model.Board{*data}, tw)
|
||||
case *model.Card:
|
||||
return printCardTable([]model.Card{*data}, tw)
|
||||
case *model.CardDetail:
|
||||
return printCardDetailTable(data, tw)
|
||||
case *model.Comment:
|
||||
return printCommentTable([]model.Comment{*data}, tw)
|
||||
case *model.TaskList:
|
||||
@@ -115,6 +119,8 @@ func printTable(data any, w io.Writer) error {
|
||||
return printTaskTable([]model.Task{*data}, tw)
|
||||
case *model.Label:
|
||||
return printLabelTable([]model.Label{*data}, tw)
|
||||
case *model.List:
|
||||
return printListTable([]model.List{*data}, tw)
|
||||
case model.StatusSummary:
|
||||
return printStatusTable(data, tw)
|
||||
case *model.StatusSummary:
|
||||
@@ -160,6 +166,29 @@ func printCardWithListTable(cards []model.CardWithList, tw *tabwriter.Writer) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func printCardDetailTable(card *model.CardDetail, tw *tabwriter.Writer) error {
|
||||
fmt.Fprintln(tw, "ID\tNAME\tLIST_ID\tTYPE\tCLOSED")
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%v\n", card.ID, card.Name, card.ListID, card.Type, card.IsClosed)
|
||||
|
||||
if len(card.TaskLists) > 0 {
|
||||
fmt.Fprintln(tw)
|
||||
fmt.Fprintln(tw, "TASK_LIST_ID\tTASK_LIST_NAME\tPOSITION")
|
||||
for _, tl := range card.TaskLists {
|
||||
fmt.Fprintf(tw, "%s\t%s\t%.0f\n", tl.ID, tl.Name, tl.Position)
|
||||
}
|
||||
}
|
||||
|
||||
if len(card.Tasks) > 0 {
|
||||
fmt.Fprintln(tw)
|
||||
fmt.Fprintln(tw, "TASK_ID\tTASK_NAME\tTASK_LIST_ID\tCOMPLETED")
|
||||
for _, t := range card.Tasks {
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\t%v\n", t.ID, t.Name, t.TaskListID, t.IsCompleted)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printCommentTable(comments []model.Comment, tw *tabwriter.Writer) error {
|
||||
fmt.Fprintln(tw, "ID\tCARD_ID\tTEXT\tCREATED_AT")
|
||||
for _, c := range comments {
|
||||
@@ -200,6 +229,26 @@ func printLabelTable(labels []model.Label, tw *tabwriter.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func printListTable(lists []model.List, tw *tabwriter.Writer) error {
|
||||
fmt.Fprintln(tw, "ID\tNAME\tTYPE\tBOARD_ID\tPOSITION\tCOLOR")
|
||||
for _, l := range lists {
|
||||
name := ""
|
||||
if l.Name != nil {
|
||||
name = *l.Name
|
||||
}
|
||||
color := ""
|
||||
if l.Color != nil {
|
||||
color = *l.Color
|
||||
}
|
||||
position := ""
|
||||
if l.Position != nil {
|
||||
position = fmt.Sprintf("%.0f", *l.Position)
|
||||
}
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\n", l.ID, name, l.Type, l.BoardID, position, color)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func printActionTable(actions []model.Action, tw *tabwriter.Writer) error {
|
||||
fmt.Fprintln(tw, "ID\tTYPE\tCARD_ID\tCREATED_AT")
|
||||
for _, a := range actions {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Project configuration
|
||||
# This file is read by workflows and agents for project-level settings.
|
||||
|
||||
planka:
|
||||
|
||||
# Planka project name
|
||||
project: "agentic"
|
||||
|
||||
# Planka board name to sync with
|
||||
board: "pcli"
|
||||
|
||||
# Future sections can be added here, e.g.:
|
||||
# openspec:
|
||||
# default-schema: "spec-driven"
|
||||
# team:
|
||||
# members: [...]
|
||||
+4
-1
@@ -16,6 +16,9 @@ tea release create \
|
||||
--note "Automated release for version $VERSION" \
|
||||
--tag "$VERSION" \
|
||||
--target "$(git rev-parse HEAD)" \
|
||||
--asset pcli
|
||||
--asset pcli-linux-amd64 \
|
||||
--asset pcli-darwin-amd64 \
|
||||
--asset pcli-darwin-arm64 \
|
||||
--asset pcli-windows-amd64.exe
|
||||
|
||||
echo "Release $VERSION created successfully!"
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# Testing List Commands
|
||||
|
||||
This document demonstrates how to use the new list management commands.
|
||||
|
||||
## Prerequisites
|
||||
1. Set up your Planka API credentials:
|
||||
```bash
|
||||
export PLANKA_URL="https://your-planka-instance.com"
|
||||
export PLANKA_API_KEY="your-api-key"
|
||||
```
|
||||
|
||||
2. Have a project and board available. You can create them with:
|
||||
```bash
|
||||
./pcli project create --name "Test Project"
|
||||
./pcli board create --project <project-id> --name "Test Board"
|
||||
```
|
||||
|
||||
## List Management Commands
|
||||
|
||||
### 1. Create a new list (column)
|
||||
```bash
|
||||
# Create an "To Do" list
|
||||
./pcli list create --board <board-id> --name "To Do" --type "active"
|
||||
|
||||
# Create a "Done" list with specific position
|
||||
./pcli list create --board <board-id> --name "Done" --type "closed" --position 131072
|
||||
```
|
||||
|
||||
### 2. Get list details
|
||||
```bash
|
||||
./pcli list get <list-id>
|
||||
```
|
||||
|
||||
### 3. Update a list
|
||||
```bash
|
||||
# Change the name
|
||||
./pcli list update <list-id> --name "In Progress"
|
||||
|
||||
# Change the color
|
||||
./pcli list update <list-id> --color "berry-red"
|
||||
|
||||
# Move list to a different position
|
||||
./pcli list update <list-id> --position 98304
|
||||
|
||||
# Move list to a different board
|
||||
./pcli list update <list-id> --board <other-board-id>
|
||||
```
|
||||
|
||||
### 4. Delete a list
|
||||
```bash
|
||||
./pcli list delete <list-id>
|
||||
```
|
||||
|
||||
## Output Formats
|
||||
|
||||
All commands support both JSON and table output formats:
|
||||
|
||||
```bash
|
||||
# JSON output (default)
|
||||
./pcli list get <list-id>
|
||||
|
||||
# Table output
|
||||
./pcli list get <list-id> --format table
|
||||
```
|
||||
|
||||
## Example Workflow
|
||||
|
||||
```bash
|
||||
# 1. List existing boards to get the board ID
|
||||
./pcli board list --format table
|
||||
|
||||
# 2. Create multiple lists for a Kanban board
|
||||
./pcli list create --board <board-id> --name "Backlog" --type "active" --position 0
|
||||
./pcli list create --board <board-id> --name "To Do" --type "active" --position 65536
|
||||
./pcli list create --board <board-id> --name "In Progress" --type "active" --position 131072
|
||||
./pcli list create --board <board-id> --name "Done" --type "closed" --position 196608
|
||||
|
||||
# 3. View the board with its lists
|
||||
./pcli board get <board-id> --format table
|
||||
|
||||
# 4. Update a list color
|
||||
./pcli list update <list-id> --color "lagoon-blue"
|
||||
|
||||
# 5. Clean up (optional)
|
||||
./pcli list delete <list-id>
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- List types can be "active" or "closed"
|
||||
- Active lists are for ongoing work
|
||||
- Closed lists are for completed/archived items
|
||||
- Position determines the order of lists on the board (lower numbers appear first)
|
||||
- Colors must be one of the supported Planka list colors
|
||||
- Deleting a list moves its cards to a trash list (doesn't permanently delete cards)
|
||||
Reference in New Issue
Block a user