4 Commits

21 changed files with 980 additions and 8 deletions
+1
View File
@@ -1,5 +1,6 @@
# Binary # Binary
/pcli /pcli
/pcli-*
# IDE # IDE
.idea/ .idea/
+143
View File
@@ -0,0 +1,143 @@
---
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.
compatibility: Requires pcli binary in PATH and PLANKA_URL + PLANKA_API_KEY environment variables set
metadata:
author: steve-cliff
version: "1.0"
---
# 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>
```
### 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>
```
## 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'
```
## Common Workflows
### 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
```
+121
View File
@@ -0,0 +1,121 @@
---
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 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.included.lists[] | {id, title}'
# 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` |
| **Boards** | `list`, `get`, `actions` |
| **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 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
+19 -3
View File
@@ -1,7 +1,23 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail 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" for target in "${TARGETS[@]}"; do
file pcli 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
+28
View File
@@ -9,6 +9,12 @@ import (
"git.franklin.lab/steve.cliff/pcli/model" "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) { func (c *Client) ListBoards(ctx context.Context) ([]model.Board, error) {
data, err := c.DoNoBody(ctx, "GET", "/api/projects") data, err := c.DoNoBody(ctx, "GET", "/api/projects")
if err != nil { if err != nil {
@@ -96,3 +102,25 @@ func (c *Client) ListBoardActions(ctx context.Context, boardId string, limit int
return all, nil 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
}
+29
View File
@@ -8,6 +8,13 @@ import (
"git.franklin.lab/steve.cliff/pcli/model" "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) { func (c *Client) ListProjects(ctx context.Context) ([]model.Project, error) {
data, err := c.DoNoBody(ctx, "GET", "/api/projects") data, err := c.DoNoBody(ctx, "GET", "/api/projects")
if err != nil { if err != nil {
@@ -41,3 +48,25 @@ func (c *Client) GetProject(ctx context.Context, id string) (*model.Project, err
return &response.Item, nil 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
}
+57
View File
@@ -1,8 +1,10 @@
package cmd package cmd
import ( import (
"fmt"
"os" "os"
"git.franklin.lab/steve.cliff/pcli/client"
"git.franklin.lab/steve.cliff/pcli/output" "git.franklin.lab/steve.cliff/pcli/output"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -56,11 +58,66 @@ 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() { func init() {
rootCmd.AddCommand(boardCmd) rootCmd.AddCommand(boardCmd)
boardCmd.AddCommand(boardListCmd) boardCmd.AddCommand(boardListCmd)
boardCmd.AddCommand(boardGetCmd) boardCmd.AddCommand(boardGetCmd)
boardCmd.AddCommand(boardActionsCmd) boardCmd.AddCommand(boardActionsCmd)
boardCmd.AddCommand(boardCreateCmd)
boardCmd.AddCommand(boardDeleteCmd)
boardActionsCmd.Flags().Int("limit", 0, "Limit number of actions (0 = no limit)") 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")
} }
+36
View File
@@ -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
}
}
+61
View File
@@ -1,8 +1,10 @@
package cmd package cmd
import ( import (
"fmt"
"os" "os"
"git.franklin.lab/steve.cliff/pcli/client"
"git.franklin.lab/steve.cliff/pcli/output" "git.franklin.lab/steve.cliff/pcli/output"
"github.com/spf13/cobra" "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() { func init() {
rootCmd.AddCommand(projectCmd) rootCmd.AddCommand(projectCmd)
projectCmd.AddCommand(projectListCmd) projectCmd.AddCommand(projectListCmd)
projectCmd.AddCommand(projectGetCmd) 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")
} }
+121
View File
@@ -0,0 +1,121 @@
---
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 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.included.lists[] | {id, title}'
# 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` |
| **Boards** | `list`, `get`, `actions` |
| **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 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
@@ -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"
@@ -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
+18 -2
View File
@@ -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 - **THEN** the client SHALL return those items without making additional requests
### Requirement: Project operations ### 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 #### Scenario: List projects
- **WHEN** `ListProjects` is called - **WHEN** `ListProjects` is called
@@ -62,8 +62,16 @@ The client SHALL provide methods to list all accessible projects (`GET /projects
- **WHEN** `GetProject` is called with a project ID - **WHEN** `GetProject` is called with a project ID
- **THEN** the client SHALL send `GET /projects/{id}` and return a Project model - **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 ### 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}`).
#### Scenario: Get board #### Scenario: Get board
- **WHEN** `GetBoard` is called with a board ID - **WHEN** `GetBoard` is called with a board ID
@@ -73,6 +81,14 @@ The client SHALL provide a method to get a single board by ID (`GET /boards/{id}
- **WHEN** `ListBoardActions` is called with a board ID and limit - **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 - **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 ### 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. 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.
+54 -2
View File
@@ -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 - **AND** the system SHALL exit with code 1
### Requirement: Project commands ### 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 #### Scenario: List projects
- **WHEN** `pcli project list` is executed - **WHEN** `pcli project list` is executed
@@ -36,8 +36,32 @@ The system SHALL provide a `project` command group with subcommands `list` and `
- **WHEN** `pcli project get` is executed without an ID argument - **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 - **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 ### 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 `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 #### Scenario: Get board
- **WHEN** `pcli board get <id>` is executed - **WHEN** `pcli board get <id>` is executed
@@ -51,6 +75,34 @@ The system SHALL provide a `board` command group with subcommands `get` and `act
- **WHEN** `pcli board actions <id> --limit 10` is executed - **WHEN** `pcli board actions <id> --limit 10` is executed
- **THEN** the system SHALL output at most 10 action entries - **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 ### 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`. The system SHALL provide a `card` command group with subcommands: `list`, `get`, `create`, `update`, `delete`, `duplicate`, `move`, `assign`, `unassign`, `add-label`, `remove-label`, `actions`.
+28
View File
@@ -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
+4 -1
View File
@@ -16,6 +16,9 @@ tea release create \
--note "Automated release for version $VERSION" \ --note "Automated release for version $VERSION" \
--tag "$VERSION" \ --tag "$VERSION" \
--target "$(git rev-parse HEAD)" \ --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!" echo "Release $VERSION created successfully!"