Add daemon support for label commands and populate labels in issue queries

- Updated label CLI commands to support both daemon and direct modes
- Added label fetching to GetIssue() and scanIssues() methods
- All label operations (add, remove, list, list-all) work with daemon
- Closed bd-162 (label CLI commands), bd-166 (duplicate), bd-141 (daemon support)

Amp-Thread-ID: https://ampcode.com/threads/T-4858f62e-ad06-4cc7-ad05-17ee76861f86
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-10-19 21:14:23 -07:00
parent 8a48b599ae
commit c3023cd5f7
6 changed files with 284 additions and 190 deletions

File diff suppressed because one or more lines are too long

View File

@@ -3,6 +3,7 @@ package main
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"os" "os"
"sort" "sort"
@@ -10,6 +11,7 @@ import (
"github.com/fatih/color" "github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
) )
@@ -21,13 +23,36 @@ var labelCmd = &cobra.Command{
// executeLabelCommand executes a label operation and handles output // executeLabelCommand executes a label operation and handles output
func executeLabelCommand(issueID, label, operation string, operationFunc func(context.Context, string, string, string) error) { func executeLabelCommand(issueID, label, operation string, operationFunc func(context.Context, string, string, string) error) {
ctx := context.Background() ctx := context.Background()
if err := operationFunc(ctx, issueID, label, actor); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err) // Use daemon if available
os.Exit(1) if daemonClient != nil {
} var err error
if operation == "added" {
_, err = daemonClient.AddLabel(&rpc.LabelAddArgs{
ID: issueID,
Label: label,
})
} else {
_, err = daemonClient.RemoveLabel(&rpc.LabelRemoveArgs{
ID: issueID,
Label: label,
})
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
} else {
// Direct mode
if err := operationFunc(ctx, issueID, label, actor); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
// Schedule auto-flush // Schedule auto-flush
markDirtyAndScheduleFlush() markDirtyAndScheduleFlush()
}
if jsonOutput { if jsonOutput {
outputJSON(map[string]interface{}{ outputJSON(map[string]interface{}{
@@ -49,7 +74,9 @@ var labelAddCmd = &cobra.Command{
Short: "Add a label to an issue", Short: "Add a label to an issue",
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
executeLabelCommand(args[0], args[1], "added", store.AddLabel) executeLabelCommand(args[0], args[1], "added", func(ctx context.Context, issueID, label, actor string) error {
return store.AddLabel(ctx, issueID, label, actor)
})
}, },
} }
@@ -58,7 +85,9 @@ var labelRemoveCmd = &cobra.Command{
Short: "Remove a label from an issue", Short: "Remove a label from an issue",
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
executeLabelCommand(args[0], args[1], "removed", store.RemoveLabel) executeLabelCommand(args[0], args[1], "removed", func(ctx context.Context, issueID, label, actor string) error {
return store.RemoveLabel(ctx, issueID, label, actor)
})
}, },
} }
@@ -70,10 +99,30 @@ var labelListCmd = &cobra.Command{
issueID := args[0] issueID := args[0]
ctx := context.Background() ctx := context.Background()
labels, err := store.GetLabels(ctx, issueID) var labels []string
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err) // Use daemon if available
os.Exit(1) if daemonClient != nil {
resp, err := daemonClient.Show(&rpc.ShowArgs{ID: issueID})
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
var issue types.Issue
if err := json.Unmarshal(resp.Data, &issue); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
os.Exit(1)
}
labels = issue.Labels
} else {
// Direct mode
var err error
labels, err = store.GetLabels(ctx, issueID)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
} }
if jsonOutput { if jsonOutput {
@@ -105,23 +154,48 @@ var labelListAllCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background() ctx := context.Background()
// Get all issues to collect labels var issues []*types.Issue
issues, err := store.SearchIssues(ctx, "", types.IssueFilter{}) var err error
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err) // Use daemon if available
os.Exit(1) if daemonClient != nil {
resp, err := daemonClient.List(&rpc.ListArgs{})
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if err := json.Unmarshal(resp.Data, &issues); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
os.Exit(1)
}
} else {
// Direct mode
issues, err = store.SearchIssues(ctx, "", types.IssueFilter{})
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
} }
// Collect unique labels with counts // Collect unique labels with counts
labelCounts := make(map[string]int) labelCounts := make(map[string]int)
for _, issue := range issues { for _, issue := range issues {
labels, err := store.GetLabels(ctx, issue.ID) if daemonClient != nil {
if err != nil { // Labels are already in the issue from daemon
fmt.Fprintf(os.Stderr, "Error getting labels for %s: %v\n", issue.ID, err) for _, label := range issue.Labels {
os.Exit(1) labelCounts[label]++
} }
for _, label := range labels { } else {
labelCounts[label]++ // Direct mode - need to fetch labels
labels, err := store.GetLabels(ctx, issue.ID)
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting labels for %s: %v\n", issue.ID, err)
os.Exit(1)
}
for _, label := range labels {
labelCounts[label]++
}
} }
} }

View File

@@ -363,7 +363,7 @@ func (s *SQLiteStorage) GetDependencies(ctx context.Context, issueID string) ([]
} }
defer rows.Close() defer rows.Close()
return scanIssues(rows) return s.scanIssues(ctx, rows)
} }
// GetDependents returns issues that depend on this issue // GetDependents returns issues that depend on this issue
@@ -382,7 +382,7 @@ func (s *SQLiteStorage) GetDependents(ctx context.Context, issueID string) ([]*t
} }
defer rows.Close() defer rows.Close()
return scanIssues(rows) return s.scanIssues(ctx, rows)
} }
// GetDependencyRecords returns raw dependency records for an issue // GetDependencyRecords returns raw dependency records for an issue
@@ -644,7 +644,7 @@ func (s *SQLiteStorage) DetectCycles(ctx context.Context) ([][]*types.Issue, err
} }
// Helper function to scan issues from rows // Helper function to scan issues from rows
func scanIssues(rows *sql.Rows) ([]*types.Issue, error) { func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*types.Issue, error) {
var issues []*types.Issue var issues []*types.Issue
for rows.Next() { for rows.Next() {
var issue types.Issue var issue types.Issue
@@ -677,6 +677,13 @@ func scanIssues(rows *sql.Rows) ([]*types.Issue, error) {
issue.ExternalRef = &externalRef.String issue.ExternalRef = &externalRef.String
} }
// Fetch labels for this issue
labels, err := s.GetLabels(ctx, issue.ID)
if err != nil {
return nil, fmt.Errorf("failed to get labels for issue %s: %w", issue.ID, err)
}
issue.Labels = labels
issues = append(issues, &issue) issues = append(issues, &issue)
} }

View File

@@ -112,5 +112,5 @@ func (s *SQLiteStorage) GetIssuesByLabel(ctx context.Context, label string) ([]*
} }
defer rows.Close() defer rows.Close()
return scanIssues(rows) return s.scanIssues(ctx, rows)
} }

View File

@@ -109,7 +109,7 @@ func (s *SQLiteStorage) GetReadyWork(ctx context.Context, filter types.WorkFilte
} }
defer rows.Close() defer rows.Close()
return scanIssues(rows) return s.scanIssues(ctx, rows)
} }
// GetBlockedIssues returns issues that are blocked by dependencies // GetBlockedIssues returns issues that are blocked by dependencies

View File

@@ -925,6 +925,13 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
issue.OriginalSize = int(originalSize.Int64) issue.OriginalSize = int(originalSize.Int64)
} }
// Fetch labels for this issue
labels, err := s.GetLabels(ctx, issue.ID)
if err != nil {
return nil, fmt.Errorf("failed to get labels: %w", err)
}
issue.Labels = labels
return &issue, nil return &issue, nil
} }
@@ -1693,7 +1700,7 @@ func (s *SQLiteStorage) SearchIssues(ctx context.Context, query string, filter t
} }
defer rows.Close() defer rows.Close()
return scanIssues(rows) return s.scanIssues(ctx, rows)
} }
// SetConfig sets a configuration value // SetConfig sets a configuration value