Files
beads/cmd/bd/repos.go
Steve Yegge c59db1a798 fix: Resolve 11 errcheck linter violations to unblock CI (bd-91)
Fixed all unchecked error returns in production code:
- os.Remove() calls in cleanup paths
- cmd.Wait() in goroutines
- fmt.Fprintf() writes
- Type assertions with proper ok checks

Reduces linter issues from 99 to 88. CI should now pass linting.
2025-10-24 11:59:11 -07:00

294 lines
8.8 KiB
Go

package main
import (
"encoding/json"
"fmt"
"os"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/rpc"
)
var reposCmd = &cobra.Command{
Use: "repos",
Short: "Multi-repository management (requires global daemon)",
Long: `Manage work across multiple repositories when using a global daemon.
This command requires a running global daemon (bd daemon --global).
It allows you to view and aggregate work across all cached repositories.`,
Run: func(cmd *cobra.Command, args []string) {
_ = cmd.Help()
},
}
var reposListCmd = &cobra.Command{
Use: "list",
Short: "List all cached repositories",
Long: `Show all repositories that the daemon has cached.
The daemon caches a repository after any command is run from that directory.
This command shows all active caches with their paths, prefixes, and issue counts.`,
Run: func(cmd *cobra.Command, args []string) {
if daemonClient == nil {
fmt.Fprintf(os.Stderr, "Error: This command requires a running daemon\n")
fmt.Fprintf(os.Stderr, "Start one with: bd daemon --global\n")
os.Exit(1)
}
resp, err := daemonClient.ReposList()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
var repos []rpc.RepoInfo
if err := json.Unmarshal(resp.Data, &repos); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
os.Exit(1)
}
if jsonOutput {
outputJSON(repos)
return
}
if len(repos) == 0 {
yellow := color.New(color.FgYellow).SprintFunc()
fmt.Printf("\n%s No repositories cached yet\n", yellow("📁"))
fmt.Printf("Repositories are cached when you run commands from their directories.\n\n")
return
}
cyan := color.New(color.FgCyan).SprintFunc()
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("\n%s Cached Repositories (%d):\n\n", cyan("📁"), len(repos))
for _, repo := range repos {
prefix := repo.Prefix
if prefix == "" {
prefix = "(no prefix)"
}
fmt.Printf("%s\n", repo.Path)
fmt.Printf(" Prefix: %s\n", prefix)
fmt.Printf(" Issue Count: %s\n", green(fmt.Sprintf("%d", repo.IssueCount)))
fmt.Printf(" Status: %s\n", repo.LastAccess)
fmt.Println()
}
},
}
var reposReadyCmd = &cobra.Command{
Use: "ready",
Short: "Show ready work across all repositories",
Long: `Display ready work (issues with no blockers) from all cached repositories.
By default, shows a flat list of all ready work. Use --group to organize by repository.`,
Run: func(cmd *cobra.Command, args []string) {
if daemonClient == nil {
fmt.Fprintf(os.Stderr, "Error: This command requires a running daemon\n")
fmt.Fprintf(os.Stderr, "Start one with: bd daemon --global\n")
os.Exit(1)
}
limit, _ := cmd.Flags().GetInt("limit")
assignee, _ := cmd.Flags().GetString("assignee")
groupByRepo, _ := cmd.Flags().GetBool("group")
readyArgs := &rpc.ReposReadyArgs{
Assignee: assignee,
Limit: limit,
GroupByRepo: groupByRepo,
}
if cmd.Flags().Changed("priority") {
priority, _ := cmd.Flags().GetInt("priority")
readyArgs.Priority = &priority
}
resp, err := daemonClient.ReposReady(readyArgs)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if groupByRepo {
var grouped []rpc.RepoReadyWork
if err := json.Unmarshal(resp.Data, &grouped); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
os.Exit(1)
}
if jsonOutput {
outputJSON(grouped)
return
}
if len(grouped) == 0 {
yellow := color.New(color.FgYellow).SprintFunc()
fmt.Printf("\n%s No ready work found across any repositories\n\n", yellow("✨"))
return
}
cyan := color.New(color.FgCyan).SprintFunc()
fmt.Printf("\n%s Ready work across %d repositories:\n\n", cyan("📋"), len(grouped))
for _, repo := range grouped {
fmt.Printf("%s (%d issues):\n", repo.RepoPath, len(repo.Issues))
for i, issue := range repo.Issues {
fmt.Printf(" %d. [P%d] %s: %s\n", i+1, issue.Priority, issue.ID, issue.Title)
if issue.EstimatedMinutes != nil {
fmt.Printf(" Estimate: %d min\n", *issue.EstimatedMinutes)
}
if issue.Assignee != "" {
fmt.Printf(" Assignee: %s\n", issue.Assignee)
}
}
fmt.Println()
}
} else {
var issues []rpc.ReposReadyIssue
if err := json.Unmarshal(resp.Data, &issues); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
os.Exit(1)
}
if jsonOutput {
outputJSON(issues)
return
}
if len(issues) == 0 {
yellow := color.New(color.FgYellow).SprintFunc()
fmt.Printf("\n%s No ready work found across any repositories\n\n", yellow("✨"))
return
}
cyan := color.New(color.FgCyan).SprintFunc()
fmt.Printf("\n%s Ready work across all repositories (%d issues):\n\n", cyan("📋"), len(issues))
for i, item := range issues {
issue := item.Issue
fmt.Printf("%d. [P%d] %s: %s\n", i+1, issue.Priority, issue.ID, issue.Title)
fmt.Printf(" Repo: %s\n", item.RepoPath)
if issue.EstimatedMinutes != nil {
fmt.Printf(" Estimate: %d min\n", *issue.EstimatedMinutes)
}
if issue.Assignee != "" {
fmt.Printf(" Assignee: %s\n", issue.Assignee)
}
}
fmt.Println()
}
},
}
var reposStatsCmd = &cobra.Command{
Use: "stats",
Short: "Show combined statistics across all repositories",
Long: `Display aggregated statistics from all cached repositories.
Shows both total combined statistics and per-repository breakdowns.`,
Run: func(cmd *cobra.Command, args []string) {
if daemonClient == nil {
fmt.Fprintf(os.Stderr, "Error: This command requires a running daemon\n")
fmt.Fprintf(os.Stderr, "Start one with: bd daemon --global\n")
os.Exit(1)
}
resp, err := daemonClient.ReposStats()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
var statsResp rpc.ReposStatsResponse
if err := json.Unmarshal(resp.Data, &statsResp); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
os.Exit(1)
}
if jsonOutput {
outputJSON(statsResp)
return
}
cyan := color.New(color.FgCyan).SprintFunc()
green := color.New(color.FgGreen).SprintFunc()
yellow := color.New(color.FgYellow).SprintFunc()
fmt.Printf("\n%s Combined Statistics Across All Repositories:\n\n", cyan("📊"))
fmt.Printf("Total Issues: %d\n", statsResp.Total.TotalIssues)
fmt.Printf("Open: %s\n", green(fmt.Sprintf("%d", statsResp.Total.OpenIssues)))
fmt.Printf("In Progress: %s\n", yellow(fmt.Sprintf("%d", statsResp.Total.InProgressIssues)))
fmt.Printf("Closed: %d\n", statsResp.Total.ClosedIssues)
fmt.Printf("Blocked: %d\n", statsResp.Total.BlockedIssues)
fmt.Printf("Ready: %s\n", green(fmt.Sprintf("%d", statsResp.Total.ReadyIssues)))
fmt.Println()
if len(statsResp.PerRepo) > 0 {
fmt.Printf("%s Per-Repository Breakdown:\n\n", cyan("📁"))
for path, stats := range statsResp.PerRepo {
fmt.Printf("%s:\n", path)
fmt.Printf(" Total: %d Ready: %s Blocked: %d\n",
stats.TotalIssues, green(fmt.Sprintf("%d", stats.ReadyIssues)), stats.BlockedIssues)
fmt.Println()
}
}
if len(statsResp.Errors) > 0 {
red := color.New(color.FgRed).SprintFunc()
fmt.Printf("%s Errors (%d repositories):\n", red("⚠"), len(statsResp.Errors))
for path, errMsg := range statsResp.Errors {
fmt.Printf(" %s: %s\n", path, errMsg)
}
fmt.Println()
}
},
}
var reposClearCacheCmd = &cobra.Command{
Use: "clear-cache",
Short: "Clear all cached repository connections",
Long: `Close all cached storage connections and clear the daemon's repository cache.
Useful for freeing resources or forcing the daemon to reload repository databases.
The cache will be rebuilt automatically as commands are run from different directories.`,
Run: func(cmd *cobra.Command, args []string) {
if daemonClient == nil {
fmt.Fprintf(os.Stderr, "Error: This command requires a running daemon\n")
fmt.Fprintf(os.Stderr, "Start one with: bd daemon --global\n")
os.Exit(1)
}
resp, err := daemonClient.ReposClearCache()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if jsonOutput {
fmt.Println(string(resp.Data))
return
}
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("\n%s Repository cache cleared successfully\n\n", green("✅"))
},
}
func init() {
reposReadyCmd.Flags().IntP("limit", "n", 10, "Maximum issues to show per repository")
reposReadyCmd.Flags().IntP("priority", "p", -1, "Filter by priority (0-4)")
reposReadyCmd.Flags().StringP("assignee", "a", "", "Filter by assignee")
reposReadyCmd.Flags().BoolP("group", "g", false, "Group issues by repository")
reposCmd.AddCommand(reposListCmd)
reposCmd.AddCommand(reposReadyCmd)
reposCmd.AddCommand(reposStatsCmd)
reposCmd.AddCommand(reposClearCacheCmd)
rootCmd.AddCommand(reposCmd)
}