Files
pcli/output/output.go
T
Steve Cliff 22d5848e1a feat: Add openspec-sync-specs and openspec-verify-change skills
- Introduced `openspec-sync-specs` skill to sync delta specs to main specs, allowing intelligent merging of requirements.
- Added `openspec-verify-change` skill to verify implementation against change artifacts, ensuring completeness, correctness, and coherence before archiving.

docs: Create CLAUDE.md for project guidance

- Added CLAUDE.md to provide an overview of the PCLI project, including build, test commands, architecture, and resource addition guidelines.

chore: Add new change and design documents for project filter in status command

- Created `.openspec.yaml`, `design.md`, `proposal.md`, and `tasks.md` for the `add-project-filter-to-status` change.
- Updated specs for CLI commands and status command to include project filtering functionality.

feat: Expand board included parsing in API client

- Added parsing for `labels`, `cardLabels`, and `cardMemberships` in the `GetBoard` response.
- Updated `ListCardsByBoard` to enrich card output with label names, enhancing usability in kanban sync workflows.
2026-02-18 21:27:02 +00:00

289 lines
7.5 KiB
Go

package output
import (
"encoding/json"
"fmt"
"io"
"os"
"reflect"
"text/tabwriter"
"git.franklin.lab/steve.cliff/pcli/model"
)
func Print(data any, format string, w io.Writer) error {
if format == "table" {
return printTable(data, w)
}
return printJSON(data, w)
}
func printJSON(data any, w io.Writer) error {
envelope := model.Envelope{
Data: data,
Error: nil,
}
encoder := json.NewEncoder(w)
encoder.SetIndent("", " ")
return encoder.Encode(envelope)
}
func PrintError(err error, format string, w io.Writer) error {
if format == "table" {
fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error())
return nil
}
errMsg := err.Error()
envelope := model.Envelope{
Data: nil,
Error: &errMsg,
}
encoder := json.NewEncoder(w)
encoder.SetIndent("", " ")
return encoder.Encode(envelope)
}
func PrintErrorWithCommand(err error, format string, w io.Writer, command string) error {
if format == "table" {
fmt.Fprintf(os.Stderr, "Error in %s command: %s\n", command, err.Error())
return nil
}
errMsg := fmt.Sprintf("Error in %s command: %s", command, err.Error())
envelope := model.Envelope{
Data: nil,
Error: &errMsg,
}
encoder := json.NewEncoder(w)
encoder.SetIndent("", " ")
return encoder.Encode(envelope)
}
func printTable(data any, w io.Writer) error {
if data == nil {
return nil
}
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
defer tw.Flush()
v := reflect.ValueOf(data)
if v.Kind() == reflect.Slice {
if v.Len() == 0 {
return nil
}
elemType := v.Index(0).Type()
switch elemType.Name() {
case "Project":
return printProjectTable(data.([]model.Project), tw)
case "Board":
return printBoardTable(data.([]model.Board), tw)
case "Card":
return printCardTable(data.([]model.Card), tw)
case "CardWithList":
return printCardWithListTable(data.([]model.CardWithList), tw)
case "Comment":
return printCommentTable(data.([]model.Comment), tw)
case "TaskList":
return printTaskListTable(data.([]model.TaskList), tw)
case "Task":
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:
return fmt.Errorf("unsupported slice type for table output: %s", elemType.Name())
}
}
switch data := data.(type) {
case *model.Project:
return printProjectTable([]model.Project{*data}, tw)
case *model.Board:
return printBoardTable([]model.Board{*data}, tw)
case *model.Card:
return printCardTable([]model.Card{*data}, tw)
case *model.CardDetail:
return printCardDetailTable(data, tw)
case *model.Comment:
return printCommentTable([]model.Comment{*data}, tw)
case *model.TaskList:
return printTaskListTable([]model.TaskList{*data}, tw)
case *model.Task:
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:
return printStatusTable(*data, tw)
default:
return fmt.Errorf("unsupported type for table output: %T", data)
}
}
func printProjectTable(projects []model.Project, tw *tabwriter.Writer) error {
fmt.Fprintln(tw, "ID\tNAME\tDESCRIPTION\tHIDDEN")
for _, p := range projects {
desc := ""
if p.Description != nil {
desc = *p.Description
}
fmt.Fprintf(tw, "%s\t%s\t%s\t%v\n", p.ID, p.Name, desc, p.IsHidden)
}
return nil
}
func printBoardTable(boards []model.Board, tw *tabwriter.Writer) error {
fmt.Fprintln(tw, "ID\tNAME\tPROJECT_ID\tDEFAULT_VIEW")
for _, b := range boards {
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", b.ID, b.Name, b.ProjectID, b.DefaultView)
}
return nil
}
func printCardTable(cards []model.Card, tw *tabwriter.Writer) error {
fmt.Fprintln(tw, "ID\tNAME\tLIST_ID\tTYPE\tCLOSED")
for _, c := range cards {
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%v\n", c.ID, c.Name, c.ListID, c.Type, c.IsClosed)
}
return nil
}
func printCardWithListTable(cards []model.CardWithList, tw *tabwriter.Writer) error {
fmt.Fprintln(tw, "ID\tNAME\tLIST\tTYPE\tCLOSED")
for _, c := range cards {
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%v\n", c.ID, c.Name, c.ListName, c.Type, c.IsClosed)
}
return nil
}
func printCardDetailTable(card *model.CardDetail, tw *tabwriter.Writer) error {
fmt.Fprintln(tw, "ID\tNAME\tLIST_ID\tTYPE\tCLOSED")
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%v\n", card.ID, card.Name, card.ListID, card.Type, card.IsClosed)
if len(card.TaskLists) > 0 {
fmt.Fprintln(tw)
fmt.Fprintln(tw, "TASK_LIST_ID\tTASK_LIST_NAME\tPOSITION")
for _, tl := range card.TaskLists {
fmt.Fprintf(tw, "%s\t%s\t%.0f\n", tl.ID, tl.Name, tl.Position)
}
}
if len(card.Tasks) > 0 {
fmt.Fprintln(tw)
fmt.Fprintln(tw, "TASK_ID\tTASK_NAME\tTASK_LIST_ID\tCOMPLETED")
for _, t := range card.Tasks {
fmt.Fprintf(tw, "%s\t%s\t%s\t%v\n", t.ID, t.Name, t.TaskListID, t.IsCompleted)
}
}
return nil
}
func printCommentTable(comments []model.Comment, tw *tabwriter.Writer) error {
fmt.Fprintln(tw, "ID\tCARD_ID\tTEXT\tCREATED_AT")
for _, c := range comments {
createdAt := ""
if c.CreatedAt != nil {
createdAt = *c.CreatedAt
}
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", c.ID, c.CardID, c.Text, createdAt)
}
return nil
}
func printTaskListTable(taskLists []model.TaskList, tw *tabwriter.Writer) error {
fmt.Fprintln(tw, "ID\tNAME\tCARD_ID\tPOSITION")
for _, tl := range taskLists {
fmt.Fprintf(tw, "%s\t%s\t%s\t%.0f\n", tl.ID, tl.Name, tl.CardID, tl.Position)
}
return nil
}
func printTaskTable(tasks []model.Task, tw *tabwriter.Writer) error {
fmt.Fprintln(tw, "ID\tNAME\tTASK_LIST_ID\tCOMPLETED")
for _, t := range tasks {
fmt.Fprintf(tw, "%s\t%s\t%s\t%v\n", t.ID, t.Name, t.TaskListID, t.IsCompleted)
}
return nil
}
func printLabelTable(labels []model.Label, tw *tabwriter.Writer) error {
fmt.Fprintln(tw, "ID\tNAME\tCOLOR\tBOARD_ID")
for _, l := range labels {
name := ""
if l.Name != nil {
name = *l.Name
}
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", l.ID, name, l.Color, l.BoardID)
}
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 {
createdAt := ""
if a.CreatedAt != nil {
createdAt = *a.CreatedAt
}
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", a.ID, a.Type, a.CardID, createdAt)
}
return nil
}
func printStatusTable(summary model.StatusSummary, tw *tabwriter.Writer) error {
// Print total board count
fmt.Fprintf(tw, "%d boards\n\n", summary.TotalBoards)
// Print each board with its lists
for i, board := range summary.Boards {
fmt.Fprintf(tw, "Board: %s\n", board.Name)
fmt.Fprintln(tw, "LIST\tCARDS")
for _, list := range board.Lists {
cardsText := fmt.Sprintf("%d", list.OpenCards)
if list.ClosedCards > 0 {
cardsText += fmt.Sprintf(" (%d closed)", list.ClosedCards)
}
fmt.Fprintf(tw, "%s\t%s\n", list.Name, cardsText)
}
// Add blank line between boards (except last one)
if i < len(summary.Boards)-1 {
fmt.Fprintln(tw)
}
}
return nil
}