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)