From cf8ff4227dcc3c8d1c3661472eb05bb76a1bffc4 Mon Sep 17 00:00:00 2001 From: Travis Cline Date: Wed, 15 Oct 2025 14:09:34 -0700 Subject: [PATCH 1/4] list: extract command to separate file --- cmd/bd/list.go | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++ cmd/bd/main.go | 62 ------------------------------------------- 2 files changed, 72 insertions(+), 62 deletions(-) create mode 100644 cmd/bd/list.go diff --git a/cmd/bd/list.go b/cmd/bd/list.go new file mode 100644 index 00000000..5ec1959a --- /dev/null +++ b/cmd/bd/list.go @@ -0,0 +1,72 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/steveyegge/beads/internal/types" +) + +var listCmd = &cobra.Command{ + Use: "list", + Short: "List issues", + Run: func(cmd *cobra.Command, args []string) { + status, _ := cmd.Flags().GetString("status") + assignee, _ := cmd.Flags().GetString("assignee") + issueType, _ := cmd.Flags().GetString("type") + limit, _ := cmd.Flags().GetInt("limit") + + filter := types.IssueFilter{ + Limit: limit, + } + if status != "" { + s := types.Status(status) + filter.Status = &s + } + // Use Changed() to properly handle P0 (priority=0) + if cmd.Flags().Changed("priority") { + priority, _ := cmd.Flags().GetInt("priority") + filter.Priority = &priority + } + if assignee != "" { + filter.Assignee = &assignee + } + if issueType != "" { + t := types.IssueType(issueType) + filter.IssueType = &t + } + + ctx := context.Background() + issues, err := store.SearchIssues(ctx, "", filter) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + if jsonOutput { + outputJSON(issues) + return + } + + fmt.Printf("\nFound %d issues:\n\n", len(issues)) + for _, issue := range issues { + fmt.Printf("%s [P%d] [%s] %s\n", issue.ID, issue.Priority, issue.IssueType, issue.Status) + fmt.Printf(" %s\n", issue.Title) + if issue.Assignee != "" { + fmt.Printf(" Assignee: %s\n", issue.Assignee) + } + fmt.Println() + } + }, +} + +func init() { + listCmd.Flags().StringP("status", "s", "", "Filter by status") + listCmd.Flags().IntP("priority", "p", 0, "Filter by priority") + listCmd.Flags().StringP("assignee", "a", "", "Filter by assignee") + listCmd.Flags().StringP("type", "t", "", "Filter by type") + listCmd.Flags().IntP("limit", "n", 0, "Limit results") + rootCmd.AddCommand(listCmd) +} diff --git a/cmd/bd/main.go b/cmd/bd/main.go index 20046cad..3143cc0d 100644 --- a/cmd/bd/main.go +++ b/cmd/bd/main.go @@ -1031,68 +1031,6 @@ func init() { rootCmd.AddCommand(showCmd) } -var listCmd = &cobra.Command{ - Use: "list", - Short: "List issues", - Run: func(cmd *cobra.Command, args []string) { - status, _ := cmd.Flags().GetString("status") - assignee, _ := cmd.Flags().GetString("assignee") - issueType, _ := cmd.Flags().GetString("type") - limit, _ := cmd.Flags().GetInt("limit") - - filter := types.IssueFilter{ - Limit: limit, - } - if status != "" { - s := types.Status(status) - filter.Status = &s - } - // Use Changed() to properly handle P0 (priority=0) - if cmd.Flags().Changed("priority") { - priority, _ := cmd.Flags().GetInt("priority") - filter.Priority = &priority - } - if assignee != "" { - filter.Assignee = &assignee - } - if issueType != "" { - t := types.IssueType(issueType) - filter.IssueType = &t - } - - ctx := context.Background() - issues, err := store.SearchIssues(ctx, "", filter) - if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) - } - - if jsonOutput { - outputJSON(issues) - return - } - - fmt.Printf("\nFound %d issues:\n\n", len(issues)) - for _, issue := range issues { - fmt.Printf("%s [P%d] [%s] %s\n", issue.ID, issue.Priority, issue.IssueType, issue.Status) - fmt.Printf(" %s\n", issue.Title) - if issue.Assignee != "" { - fmt.Printf(" Assignee: %s\n", issue.Assignee) - } - fmt.Println() - } - }, -} - -func init() { - listCmd.Flags().StringP("status", "s", "", "Filter by status") - listCmd.Flags().IntP("priority", "p", 0, "Filter by priority") - listCmd.Flags().StringP("assignee", "a", "", "Filter by assignee") - listCmd.Flags().StringP("type", "t", "", "Filter by type") - listCmd.Flags().IntP("limit", "n", 0, "Limit results") - rootCmd.AddCommand(listCmd) -} - var updateCmd = &cobra.Command{ Use: "update [id]", Short: "Update an issue", From 0c3c61389053d3ae7e90e0b87266ccede37c7c2c Mon Sep 17 00:00:00 2001 From: Travis Cline Date: Wed, 15 Oct 2025 14:32:16 -0700 Subject: [PATCH 2/4] list: add filter value enums to help --- cmd/bd/list.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/bd/list.go b/cmd/bd/list.go index 5ec1959a..8640a930 100644 --- a/cmd/bd/list.go +++ b/cmd/bd/list.go @@ -63,10 +63,10 @@ var listCmd = &cobra.Command{ } func init() { - listCmd.Flags().StringP("status", "s", "", "Filter by status") - listCmd.Flags().IntP("priority", "p", 0, "Filter by priority") + listCmd.Flags().StringP("status", "s", "", "Filter by status (open, in_progress, blocked, closed)") + listCmd.Flags().IntP("priority", "p", 0, "Filter by priority (0-4: 0=critical, 1=high, 2=medium, 3=low, 4=backlog)") listCmd.Flags().StringP("assignee", "a", "", "Filter by assignee") - listCmd.Flags().StringP("type", "t", "", "Filter by type") + listCmd.Flags().StringP("type", "t", "", "Filter by type (bug, feature, task, epic, chore)") listCmd.Flags().IntP("limit", "n", 0, "Limit results") rootCmd.AddCommand(listCmd) } From 6f357ea536e6838666872f66d04b14a107c665f9 Mon Sep 17 00:00:00 2001 From: Travis Cline Date: Wed, 15 Oct 2025 14:10:46 -0700 Subject: [PATCH 3/4] list: add --format flag with template support Add flexible --format flag to 'bd list' command supporting: - Built-in presets: 'digraph' (basic 'from to' format) and 'dot' (Graphviz) - Custom Go templates for dependency output - Template variables: IssueID, DependsOnID, Type, Issue, Dependency This enables graph analysis with tools like golang.org/x/tools/cmd/digraph while allowing users to customize output format for their specific needs. Examples: bd list --format=digraph | digraph nodes bd list --format=dot | dot -Tsvg -o deps.svg bd list --format='{{.IssueID}} -> {{.DependsOnID}} [{{.Type}}]' --- cmd/bd/list.go | 131 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/cmd/bd/list.go b/cmd/bd/list.go index 8640a930..c5d81681 100644 --- a/cmd/bd/list.go +++ b/cmd/bd/list.go @@ -1,11 +1,14 @@ package main import ( + "bytes" "context" "fmt" "os" + "text/template" "github.com/spf13/cobra" + "github.com/steveyegge/beads/internal/storage" "github.com/steveyegge/beads/internal/types" ) @@ -17,6 +20,7 @@ var listCmd = &cobra.Command{ assignee, _ := cmd.Flags().GetString("assignee") issueType, _ := cmd.Flags().GetString("type") limit, _ := cmd.Flags().GetInt("limit") + formatStr, _ := cmd.Flags().GetString("format") filter := types.IssueFilter{ Limit: limit, @@ -45,6 +49,15 @@ var listCmd = &cobra.Command{ os.Exit(1) } + // Handle format flag + if formatStr != "" { + if err := outputFormattedList(ctx, store, issues, formatStr); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + return + } + if jsonOutput { outputJSON(issues) return @@ -68,5 +81,123 @@ func init() { listCmd.Flags().StringP("assignee", "a", "", "Filter by assignee") listCmd.Flags().StringP("type", "t", "", "Filter by type (bug, feature, task, epic, chore)") listCmd.Flags().IntP("limit", "n", 0, "Limit results") + listCmd.Flags().String("format", "", "Output format: 'digraph' (for golang.org/x/tools/cmd/digraph), 'dot' (Graphviz), or Go template") rootCmd.AddCommand(listCmd) } + +// outputDotFormat outputs issues in Graphviz DOT format +func outputDotFormat(ctx context.Context, store storage.Storage, issues []*types.Issue) error { + fmt.Println("digraph dependencies {") + fmt.Println(" rankdir=TB;") + fmt.Println(" node [shape=box, style=rounded];") + fmt.Println() + + // Build map of all issues for quick lookup + issueMap := make(map[string]*types.Issue) + for _, issue := range issues { + issueMap[issue.ID] = issue + } + + // Output nodes with labels + for _, issue := range issues { + // Escape quotes in title + title := issue.Title + title = fmt.Sprintf("%q", title) // Go's %q handles escaping + fmt.Printf(" %q [label=%s];\n", issue.ID, title) + } + fmt.Println() + + // Output edges with labels for dependency type + for _, issue := range issues { + deps, err := store.GetDependencyRecords(ctx, issue.ID) + if err != nil { + continue + } + for _, dep := range deps { + // Only output edges where both nodes are in the filtered list + if issueMap[dep.DependsOnID] != nil { + // Color code by dependency type + color := "black" + style := "solid" + switch dep.Type { + case "blocks": + color = "red" + style = "bold" + case "parent-child": + color = "blue" + case "discovered-from": + color = "green" + style = "dashed" + case "related": + color = "gray" + style = "dashed" + } + fmt.Printf(" %q -> %q [label=%q, color=%s, style=%s];\n", + issue.ID, dep.DependsOnID, dep.Type, color, style) + } + } + } + + fmt.Println("}") + return nil +} + +// outputFormattedList outputs issues in a custom format (preset or Go template) +func outputFormattedList(ctx context.Context, store storage.Storage, issues []*types.Issue, formatStr string) error { + // Handle special 'dot' format (Graphviz output) + if formatStr == "dot" { + return outputDotFormat(ctx, store, issues) + } + + // Built-in format presets + presets := map[string]string{ + "digraph": "{{.IssueID}} {{.DependsOnID}}", + } + + // Check if it's a preset + templateStr, isPreset := presets[formatStr] + if !isPreset { + templateStr = formatStr + } + + // Parse template + tmpl, err := template.New("format").Parse(templateStr) + if err != nil { + return fmt.Errorf("invalid format template: %w", err) + } + + // Build map of all issues for quick lookup + issueMap := make(map[string]bool) + for _, issue := range issues { + issueMap[issue.ID] = true + } + + // For each issue, output its dependencies using the template + for _, issue := range issues { + deps, err := store.GetDependencyRecords(ctx, issue.ID) + if err != nil { + continue + } + for _, dep := range deps { + // Only output edges where both nodes are in the filtered list + if issueMap[dep.DependsOnID] { + // Template data includes both issue and dependency info + data := map[string]interface{}{ + "IssueID": issue.ID, + "DependsOnID": dep.DependsOnID, + "Type": dep.Type, + "Issue": issue, + "Dependency": dep, + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, data); err != nil { + return fmt.Errorf("template execution error: %w", err) + } + fmt.Println(buf.String()) + } + } + } + + return nil +} From 4131e6bf38e4650de3f15d456058ee7586f36d64 Mon Sep 17 00:00:00 2001 From: Travis Cline Date: Wed, 15 Oct 2025 14:19:54 -0700 Subject: [PATCH 4/4] list: add status-based color coding to dot format Enhance Graphviz dot output with status-based fill colors: - open: white background (default) - in_progress: light yellow background - blocked: light coral background - closed: light gray background with dimmed text Node labels show: ID, type, priority, title, and status. Priority is visible in the label (e.g., [bug P0]) but not color-coded to keep the visualization clean and focused on status. --- cmd/bd/list.go | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/cmd/bd/list.go b/cmd/bd/list.go index c5d81681..e76b8f0b 100644 --- a/cmd/bd/list.go +++ b/cmd/bd/list.go @@ -98,12 +98,32 @@ func outputDotFormat(ctx context.Context, store storage.Storage, issues []*types issueMap[issue.ID] = issue } - // Output nodes with labels + // Output nodes with labels including ID, type, priority, and status for _, issue := range issues { - // Escape quotes in title - title := issue.Title - title = fmt.Sprintf("%q", title) // Go's %q handles escaping - fmt.Printf(" %q [label=%s];\n", issue.ID, title) + // Build label with ID, type, priority, and title (using actual newlines) + label := fmt.Sprintf("%s\n[%s P%d]\n%s\n(%s)", + issue.ID, + issue.IssueType, + issue.Priority, + issue.Title, + issue.Status) + + // Color by status only - keep it simple + fillColor := "white" + fontColor := "black" + + switch issue.Status { + case "closed": + fillColor = "lightgray" + fontColor = "dimgray" + case "in_progress": + fillColor = "lightyellow" + case "blocked": + fillColor = "lightcoral" + } + + fmt.Printf(" %q [label=%q, style=\"rounded,filled\", fillcolor=%q, fontcolor=%q];\n", + issue.ID, label, fillColor, fontColor) } fmt.Println()