22d5848e1a
- 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.
289 lines
7.5 KiB
Go
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
|
|
}
|