From 46b03e1a22622d6a14aee7b05978b6db4db71e94 Mon Sep 17 00:00:00 2001 From: Steve Cliff Date: Wed, 18 Feb 2026 20:06:56 +0000 Subject: [PATCH] Added list management commands, board filtering by project name, and enhanced skill documentation with bootstrap workflow and error handling patterns. Also added plumbing in to "pcli" binary for status syncing with Planka --- .windsurf/rules/kanban-update.md | 15 + .windsurf/skills/kanban/SKILL.md | 74 +++++ .windsurf/workflows/kanban-sync.md | 266 ++++++++++++++++++ README.md | 3 + client/lists.go | 81 ++++++ cmd/board.go | 35 +++ cmd/list.go | 141 ++++++++++ example-skill/workflow-kanban.md | 121 -------- .../skills/kanban}/SKILL.md | 27 ++ .../workflows/kanban}/kanban.md | 33 ++- .../.openspec.yaml | 2 + .../design.md | 71 +++++ .../proposal.md | 34 +++ .../specs/board-list-filtering/spec.md | 32 +++ .../specs/cli-commands/spec.md | 57 ++++ .../tasks.md | 24 ++ openspec/specs/board-list-filtering/spec.md | 38 +++ openspec/specs/cli-commands/spec.md | 13 +- output/output.go | 24 ++ project.yaml | 12 + test-list-commands.md | 95 +++++++ 21 files changed, 1074 insertions(+), 124 deletions(-) create mode 100644 .windsurf/rules/kanban-update.md create mode 100644 .windsurf/workflows/kanban-sync.md create mode 100644 client/lists.go create mode 100644 cmd/list.go delete mode 100644 example-skill/workflow-kanban.md rename {example-skill => examples/skills/kanban}/SKILL.md (77%) rename {.windsurf/workflows => examples/workflows/kanban}/kanban.md (73%) create mode 100644 openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/.openspec.yaml create mode 100644 openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/design.md create mode 100644 openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/proposal.md create mode 100644 openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/specs/board-list-filtering/spec.md create mode 100644 openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/specs/cli-commands/spec.md create mode 100644 openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/tasks.md create mode 100644 openspec/specs/board-list-filtering/spec.md create mode 100644 project.yaml create mode 100644 test-list-commands.md diff --git a/.windsurf/rules/kanban-update.md b/.windsurf/rules/kanban-update.md new file mode 100644 index 0000000..3cb31ca --- /dev/null +++ b/.windsurf/rules/kanban-update.md @@ -0,0 +1,15 @@ +--- +trigger: always_on +--- + +## Planka Sync + +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 with OpenSpec by: + +1. Reading the kanban-sync workflow documentation at `.windsurf/workflows/kanban-sync.md` +2. Implementing the reconciliation steps programmatically using `pcli` commands +3. Following the workflow logic to sync OpenSpec changes to Planka cards + +This ensures the Planka dashboard stays current without modifying the opsx workflows themselves. + +If the sync fails (pcli unavailable, network issues), log a brief warning and continue. Never block or retry - Planka sync is best-effort. Do NOT attempt to execute the markdown file as a bash script - it is documentation that must be implemented programmatically. diff --git a/.windsurf/skills/kanban/SKILL.md b/.windsurf/skills/kanban/SKILL.md index 3151946..91046d3 100644 --- a/.windsurf/skills/kanban/SKILL.md +++ b/.windsurf/skills/kanban/SKILL.md @@ -103,6 +103,15 @@ pcli task update [--name "..."] [--position N] [--completed] pcli task delete ``` +### Lists + +```bash +pcli list create --board --name "List Name" --position 65536 [--type active|closed] +pcli list get +pcli list update [--name "..."] [--position N] [--type active|closed] [--color "..."] [--board ] +pcli list delete +``` + ### Labels ```bash @@ -111,6 +120,31 @@ pcli label update [--name "..."] [--color "..."] [--position N] pcli label delete ``` +## API Response Structure + +### Board Get Response +Board details include lists directly in `.data.lists[]`, not in an `included` section: +```bash +pcli board get | jq '.data.lists[] | {id, name, position}' +``` + +### Project Get Response +Project details include boards in `.data.included.boards[]`: +```bash +pcli project get | jq '.data.included.boards[] | {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: @@ -121,10 +155,50 @@ pcli card create --list --name "X" | jq -r '.data.id' # Array pcli card list --board | 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 project get $PROJECT_ID | jq -r --arg name "$BOARD_NAME" '.data.included.boards[] | 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 diff --git a/.windsurf/workflows/kanban-sync.md b/.windsurf/workflows/kanban-sync.md new file mode 100644 index 0000000..b123de1 --- /dev/null +++ b/.windsurf/workflows/kanban-sync.md @@ -0,0 +1,266 @@ +--- +name: "Kanban Sync" +description: Reconcile Planka board state with OpenSpec changes. Planka is a read-only dashboard - OpenSpec is the source of truth. +category: Workflow +tags: [workflow, kanban, planka, tasks, project-management, sync] +--- + +# Planka ↔ OpenSpec Reconciliation Sync + +**OpenSpec is the source of truth for all agentic work.** Planka provides a read-only view so humans and other agents can see what's happening. This workflow reads OpenSpec state and reconciles Planka to match. + +## Prerequisites + +Before syncing, verify connectivity: + +```bash +pcli status +``` + +If this fails, skip the Planka sync silently. Planka sync is best-effort - never block agentic work because Planka is unavailable. + +--- + +## Core Principles + +1. **OpenSpec owns the state** - `tasks.md`, artifacts, and change status live in OpenSpec +2. **Planka is a projection** - it reflects OpenSpec state, never the other way around +3. **Sync is one-directional** - OpenSpec → Planka, never Planka → OpenSpec +4. **Sync is idempotent** - running it twice produces the same result +5. **Sync is best-effort** - if Planka is down, work continues uninterrupted +6. **Non-agentic work** is managed directly in Planka (no OpenSpec involvement) + +--- + +## When This Runs + +This workflow is triggered automatically after any opsx workflow completes (via project-level instruction in `.windsurf/rules/kanban-update.md`). It can also be invoked manually via `/kanban-tasks`. + +--- + +## Board Structure + +The board lists map to OpenSpec lifecycle stages: + +| List | Position | Purpose | +|------|----------|---------| +| **Backlog** | 1 | Non-agentic work items (human-managed, read-write) | +| **To Do** | 2 | Non-agentic work items ready to start (human-managed, read-write) | +| **Planning** | 3 | OpenSpec changes with artifacts still being created (`opsx:new`, `opsx:continue`, `opsx:ff`) | +| **In Progress** | 4 | Active implementation — tasks being worked (`opsx:apply`) | +| **Review** | 5 | All tasks complete, awaiting verification (`opsx:verify`) | +| **Done** | 6 | Completed and archived (`opsx:archive`) | + +--- + +## Bootstrap: Ensure Project, Board, Lists, and Label Exist + +Before reconciling, ensure all required Planka infrastructure exists. This makes the sync self-bootstrapping — running it on a fresh Planka instance will create everything needed. + +### 1. Read project config + +```bash +PROJECT_NAME=$(yq '.planka.project' project.yaml) +BOARD_NAME=$(yq '.planka.board' project.yaml) +``` + +If `project.yaml` doesn't exist or has no `planka` section, ask the user for the project and board name, then offer to create the file. + +### 2. Find or create the project + +```bash +PROJECT_ID=$(pcli project list | jq -r --arg name "$PROJECT_NAME" '.data[] | select(.name == $name) | .id') +``` + +If no project found: +```bash +PROJECT_ID=$(pcli project create --name "$PROJECT_NAME" --type "public" | jq -r '.data.id') +``` + +### 3. Find or create the board + +```bash +# Get project details to find boards +BOARD_ID=$(pcli project get $PROJECT_ID | jq -r --arg name "$BOARD_NAME" '.data.included.boards[] | select(.name == $name) | .id') +``` + +If no board found: +```bash +BOARD_ID=$(pcli board create --project $PROJECT_ID --name "$BOARD_NAME" | jq -r '.data.id') +``` + +### 4. Find or create the lists + +After obtaining the board, get its current lists: +```bash +EXISTING_LISTS=$(pcli board get $BOARD_ID | jq -r '.data.included.lists[]? | .name') +``` + +Create any missing lists with explicit positions to maintain correct ordering: +```bash +# Only create lists that don't already exist +pcli list create --board $BOARD_ID --name "Backlog" --position 65536 +pcli list create --board $BOARD_ID --name "To Do" --position 131072 +pcli list create --board $BOARD_ID --name "Planning" --position 196608 +pcli list create --board $BOARD_ID --name "In Progress" --position 262144 +pcli list create --board $BOARD_ID --name "Review" --position 327680 +pcli list create --board $BOARD_ID --name "Done" --position 393216 +``` + +Skip any list that already exists (match by name). + +### 5. Find or create the `agent` label + +```bash +LABEL_ID=$(pcli board get $BOARD_ID | jq -r '.data.included.labels[]? | select(.name == "agent") | .id') +``` + +If no `agent` label found: +```bash +LABEL_ID=$(pcli label create --board $BOARD_ID --name "agent" --color "berry-red" | jq -r '.data.id') +``` + +--- + +## Reconciliation Steps + +### 1. Gather OpenSpec state + +```bash +openspec list --json +``` + +This returns all active changes with their names, schemas, and status. + +For each active change: +```bash +openspec status --change "" --json +``` + +Parse to get: +- Change name +- Schema name +- Artifact completion status (how many artifacts complete vs total) +- Whether all `applyRequires` artifacts are done +- Whether tasks exist and their completion state + +If a `tasks.md` exists, read it and parse checkbox state (`- [ ]` vs `- [x]`). + +### 2. Determine target list for each change + +Map each change to the correct board list based on its OpenSpec state: + +| Condition | Target List | +|-----------|-------------| +| Artifacts incomplete (not all `applyRequires` done) | **Planning** | +| Artifacts complete, tasks exist with incomplete items | **In Progress** | +| All tasks complete (all `[x]`) | **Review** | +| Change archived (not in active list) | **Done** | + +### 3. Gather Planka state + +```bash +pcli card list --board $BOARD_ID | jq '.data[] | select(.labels[]?.name == "agent")' +``` + +Build a map of existing agent-labelled cards by name, including which list they're currently in. + +### 4. Reconcile: create missing cards + +For each active OpenSpec change that has no matching Planka card: + +```bash +# Determine the correct list based on change state (see step 2) +LIST_ID=$(pcli board get $BOARD_ID | jq -r --arg list "" '.data.included.lists[] | select(.name == $list) | .id') +CARD_ID=$(pcli card create --list $LIST_ID --name "" --description "" | jq -r '.data.id') + +# Add agent label +pcli card add-label $CARD_ID --label $LABEL_ID +``` + +### 5. Reconcile: move cards to correct list + +For each agent-labelled card that exists but is in the wrong list (based on current OpenSpec state): + +```bash +TARGET_LIST_ID=$(pcli board get $BOARD_ID | jq -r --arg list "" '.data.included.lists[] | select(.name == $list) | .id') +pcli card move $CARD_ID --list $TARGET_LIST_ID +``` + +This ensures cards move through the board as work progresses: +- `Planning` → `In Progress` when artifacts are complete and apply begins +- `In Progress` → `Review` when all tasks are marked done +- `Review` → `Done` when the change is archived + +### 6. Reconcile: sync task lists + +For each OpenSpec change that has a `tasks.md`: + +- If the Planka card has no task list → create one and add all tasks +- If the Planka card has a task list → compare task names and completion state, update as needed + +```bash +# Create task list if missing +TL_ID=$(pcli task-list create --card $CARD_ID --name "Implementation" --show-on-front | jq -r '.data.id') + +# For each task in tasks.md: +pcli task create --task-list $TL_ID --name "" + +# For tasks already in Planka, update completion state to match tasks.md: +pcli task update --completed # if tasks.md shows [x] +``` + +### 7. Reconcile: move completed/archived changes + +For each agent-labelled Planka card that has no matching active OpenSpec change: +- The change was likely archived → move the card to "Done" + +```bash +DONE_LIST_ID=$(pcli board get $BOARD_ID | jq -r '.data.included.lists[] | select(.name == "Done") | .id') +pcli card move $CARD_ID --list $DONE_LIST_ID +``` + +### 8. Report + +After reconciliation, briefly summarise what changed: +- Infrastructure created (project/board/lists/label): yes/no +- Cards created: N +- Cards moved: N (list details) +- Tasks synced: N +- Cards moved to Done: N +- Errors (if any): list them + +--- + +## Non-Agentic Work + +Cards **without** the `agent` label are human-managed and fully read-write. The kanban skill (`/kanban`) handles these directly - creating cards, moving them, adding checklists, etc. + +The distinction: +- **Has `agent` label** → read-only projection, managed by this sync workflow +- **No `agent` label** → regular Planka card, managed directly by humans + +--- + +## ID Discovery + +Planka IDs cannot be cached across sessions. Each sync run must discover IDs dynamically: + +1. Read project and board name from `project.yaml` +2. Find the project by name: `pcli project list | jq ...` +3. Find the board by name within the project +4. Find lists on the board: `pcli board get | jq ...` +5. Find agent cards: `pcli card list --board | jq '.data[] | select(.labels[]?.name == "agent")'` +6. Match cards to changes by name + +--- + +## Guardrails + +- **Never modify an `agent`-labelled card outside of this sync workflow** +- **Never read Planka to determine what work to do** - query OpenSpec instead +- **Always discover IDs dynamically** - never hardcode or cache across sessions +- **Sync failures are silent** - log a warning but never block opsx workflows +- **One board per project** - if multiple boards exist, ask the user which to sync to +- **Idempotent** - safe to run multiple times, will not create duplicates +- **Bootstrap is safe** - creating project/board/lists is idempotent; existing resources are reused diff --git a/README.md b/README.md index a783a09..03c4524 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,9 @@ pcli project get # List all accessible boards pcli board list +# List boards filtered by project name +pcli board list --project "project1" + # Get a board pcli board get diff --git a/client/lists.go b/client/lists.go new file mode 100644 index 0000000..da18127 --- /dev/null +++ b/client/lists.go @@ -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 +} diff --git a/cmd/board.go b/cmd/board.go index c24ce7e..ac435be 100644 --- a/cmd/board.go +++ b/cmd/board.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "os" + "strings" "git.franklin.lab/steve.cliff/pcli/client" "git.franklin.lab/steve.cliff/pcli/output" @@ -15,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) }, } @@ -111,6 +144,8 @@ func init() { 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 diff --git a/cmd/list.go b/cmd/list.go new file mode 100644 index 0000000..ed3d2e9 --- /dev/null +++ b/cmd/list.go @@ -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 ", + 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 ", + 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 ", + 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)") +} diff --git a/example-skill/workflow-kanban.md b/example-skill/workflow-kanban.md deleted file mode 100644 index 125ba6b..0000000 --- a/example-skill/workflow-kanban.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -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 | jq '.data.included.lists[] | {id, title}' - -# Find cards on a list or board -pcli card list --board | jq '.data[] | {id, name}' -pcli card list --list | 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 --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 | jq -r '.data[].id' | while read id; do - pcli card move $id --list -done -``` - -### Extract IDs from output - -```bash -# Single object -pcli card create --list --name "X" | jq -r '.data.id' - -# Array -pcli card list --board | 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 diff --git a/example-skill/SKILL.md b/examples/skills/kanban/SKILL.md similarity index 77% rename from example-skill/SKILL.md rename to examples/skills/kanban/SKILL.md index d2aa64c..54ca6a4 100644 --- a/example-skill/SKILL.md +++ b/examples/skills/kanban/SKILL.md @@ -48,6 +48,17 @@ pcli project get pcli board list pcli board get # includes lists and cards pcli board actions [--limit N] +pcli board create --project --name "Board Name" [--position N] +pcli board delete +``` + +### Lists (Board Columns) + +```bash +pcli list create --board --name "Column Name" [--type active|closed] [--position N] +pcli list get +pcli list update [--name "..."] [--type active|closed] [--color "..."] [--position N] [--board ] +pcli list delete ``` ### Cards @@ -125,6 +136,22 @@ pcli card list --board | jq -r '.data[].id' ## Common Workflows +### Create a complete Kanban board + +```bash +# Create board +BOARD_ID=$(pcli board create --project --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 diff --git a/.windsurf/workflows/kanban.md b/examples/workflows/kanban/kanban.md similarity index 73% rename from .windsurf/workflows/kanban.md rename to examples/workflows/kanban/kanban.md index 125ba6b..7a3e480 100644 --- a/.windsurf/workflows/kanban.md +++ b/examples/workflows/kanban/kanban.md @@ -24,6 +24,8 @@ If this fails, ensure `PLANKA_URL` and `PLANKA_API_KEY` environment variables ar **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" @@ -71,8 +73,9 @@ Show the user a concise summary of what happened. Use tables or formatted output | Resource | Commands | |----------|----------| | **Status** | `pcli status` | -| **Projects** | `list`, `get` | -| **Boards** | `list`, `get`, `actions` | +| **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` | @@ -83,6 +86,32 @@ Show the user a concise summary of what happened. Use tables or formatted output ## Common Patterns +### Create a complete Kanban board + +```bash +# Create board +BOARD_ID=$(pcli board create --project --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 diff --git a/openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/.openspec.yaml b/openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/.openspec.yaml new file mode 100644 index 0000000..e3dce8f --- /dev/null +++ b/openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-02-18 diff --git a/openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/design.md b/openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/design.md new file mode 100644 index 0000000..9a53a96 --- /dev/null +++ b/openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/design.md @@ -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 ` 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. diff --git a/openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/proposal.md b/openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/proposal.md new file mode 100644 index 0000000..b6c8a84 --- /dev/null +++ b/openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/proposal.md @@ -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 ` 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 diff --git a/openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/specs/board-list-filtering/spec.md b/openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/specs/board-list-filtering/spec.md new file mode 100644 index 0000000..421c1dd --- /dev/null +++ b/openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/specs/board-list-filtering/spec.md @@ -0,0 +1,32 @@ +## ADDED Requirements + +### Requirement: Board list project filtering +The system SHALL provide a `--project ` 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: " 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 diff --git a/openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/specs/cli-commands/spec.md b/openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/specs/cli-commands/spec.md new file mode 100644 index 0000000..c7475f9 --- /dev/null +++ b/openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/specs/cli-commands/spec.md @@ -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 ` 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 ` flag for filtering. `pcli board get ` SHALL accept a board ID as a positional argument and output the board details. `pcli board actions ` 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 ` SHALL delete a board. + +#### Scenario: Get board +- **WHEN** `pcli board get ` is executed +- **THEN** the system SHALL output the board details including its lists + +#### Scenario: List board actions +- **WHEN** `pcli board actions ` is executed +- **THEN** the system SHALL output the board's action history + +#### Scenario: List board actions with limit +- **WHEN** `pcli board actions --limit 10` is executed +- **THEN** the system SHALL output at most 10 action entries + +#### Scenario: Create board +- **WHEN** `pcli board create --project --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 --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 --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 ` 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 ` 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 ` 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" diff --git a/openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/tasks.md b/openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/tasks.md new file mode 100644 index 0000000..4a263e4 --- /dev/null +++ b/openspec/changes/archive/2026-02-18-add-project-filter-to-board-list/tasks.md @@ -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 diff --git a/openspec/specs/board-list-filtering/spec.md b/openspec/specs/board-list-filtering/spec.md new file mode 100644 index 0000000..567ae5b --- /dev/null +++ b/openspec/specs/board-list-filtering/spec.md @@ -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 ` 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: " 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 diff --git a/openspec/specs/cli-commands/spec.md b/openspec/specs/cli-commands/spec.md index 803bf1a..b74a579 100644 --- a/openspec/specs/cli-commands/spec.md +++ b/openspec/specs/cli-commands/spec.md @@ -60,8 +60,19 @@ The system SHALL provide a `project` command group with subcommands `list`, `get - **WHEN** `pcli project delete ` 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 ` 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`, `actions`, `create`, and `delete`. `pcli board get ` SHALL accept a board ID as a positional argument and output the board details. `pcli board actions ` 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 ` SHALL delete a board. +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 ` flag for filtering. `pcli board get ` SHALL accept a board ID as a positional argument and output the board details. `pcli board actions ` 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 ` SHALL delete a board. #### Scenario: Get board - **WHEN** `pcli board get ` is executed diff --git a/output/output.go b/output/output.go index 5e80a98..7046c63 100644 --- a/output/output.go +++ b/output/output.go @@ -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: @@ -115,6 +117,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: @@ -200,6 +204,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 { diff --git a/project.yaml b/project.yaml new file mode 100644 index 0000000..487c3f6 --- /dev/null +++ b/project.yaml @@ -0,0 +1,12 @@ +# Project configuration +# This file is read by workflows and agents for project-level settings. + +planka: + project: "project1" # Planka project name + board: "pcli" # Planka board name to sync with + +# Future sections can be added here, e.g.: +# openspec: +# default-schema: "spec-driven" +# team: +# members: [...] diff --git a/test-list-commands.md b/test-list-commands.md new file mode 100644 index 0000000..8ba528f --- /dev/null +++ b/test-list-commands.md @@ -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 --name "Test Board" + ``` + +## List Management Commands + +### 1. Create a new list (column) +```bash +# Create an "To Do" list +./pcli list create --board --name "To Do" --type "active" + +# Create a "Done" list with specific position +./pcli list create --board --name "Done" --type "closed" --position 131072 +``` + +### 2. Get list details +```bash +./pcli list get +``` + +### 3. Update a list +```bash +# Change the name +./pcli list update --name "In Progress" + +# Change the color +./pcli list update --color "berry-red" + +# Move list to a different position +./pcli list update --position 98304 + +# Move list to a different board +./pcli list update --board +``` + +### 4. Delete a list +```bash +./pcli list delete +``` + +## Output Formats + +All commands support both JSON and table output formats: + +```bash +# JSON output (default) +./pcli list get + +# Table output +./pcli list get --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 --name "Backlog" --type "active" --position 0 +./pcli list create --board --name "To Do" --type "active" --position 65536 +./pcli list create --board --name "In Progress" --type "active" --position 131072 +./pcli list create --board --name "Done" --type "closed" --position 196608 + +# 3. View the board with its lists +./pcli board get --format table + +# 4. Update a list color +./pcli list update --color "lagoon-blue" + +# 5. Clean up (optional) +./pcli list delete +``` + +## 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)