- Renamed config.json to metadata.json to clarify purpose (database metadata) - Fixed config.yaml/config.json conflict by making Viper explicitly load only config.yaml - Added automatic migration from config.json to metadata.json on first read - Fixed jsonOutput variable shadowing across 22 command files - Updated bd init to create both metadata.json and config.yaml template - Fixed 5 failing JSON output tests - All tests passing Resolves config file confusion and makes config.yaml work correctly. Closes #178 (global flags), addresses config issues from #193 Amp-Thread-ID: https://ampcode.com/threads/T-e6ac8192-e18f-4ed7-83bc-4a5986718bb7 Co-authored-by: Amp <amp@ampcode.com>
310 lines
8.7 KiB
Go
310 lines
8.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/spf13/cobra"
|
|
"github.com/steveyegge/beads/internal/rpc"
|
|
"github.com/steveyegge/beads/internal/storage/sqlite"
|
|
"github.com/steveyegge/beads/internal/types"
|
|
)
|
|
|
|
var readyCmd = &cobra.Command{
|
|
Use: "ready",
|
|
Short: "Show ready work (no blockers, open or in-progress)",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
limit, _ := cmd.Flags().GetInt("limit")
|
|
assignee, _ := cmd.Flags().GetString("assignee")
|
|
sortPolicy, _ := cmd.Flags().GetString("sort")
|
|
// Use global jsonOutput set by PersistentPreRun (respects config.yaml + env vars)
|
|
|
|
filter := types.WorkFilter{
|
|
// Leave Status empty to get both 'open' and 'in_progress' (bd-165)
|
|
Limit: limit,
|
|
SortPolicy: types.SortPolicy(sortPolicy),
|
|
}
|
|
// 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
|
|
}
|
|
|
|
// Validate sort policy
|
|
if !filter.SortPolicy.IsValid() {
|
|
fmt.Fprintf(os.Stderr, "Error: invalid sort policy '%s'. Valid values: hybrid, priority, oldest\n", sortPolicy)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// If daemon is running, use RPC
|
|
if daemonClient != nil {
|
|
readyArgs := &rpc.ReadyArgs{
|
|
Assignee: assignee,
|
|
Limit: limit,
|
|
SortPolicy: sortPolicy,
|
|
}
|
|
if cmd.Flags().Changed("priority") {
|
|
priority, _ := cmd.Flags().GetInt("priority")
|
|
readyArgs.Priority = &priority
|
|
}
|
|
|
|
resp, err := daemonClient.Ready(readyArgs)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
var issues []*types.Issue
|
|
if err := json.Unmarshal(resp.Data, &issues); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if jsonOutput {
|
|
if issues == nil {
|
|
issues = []*types.Issue{}
|
|
}
|
|
outputJSON(issues)
|
|
return
|
|
}
|
|
|
|
if len(issues) == 0 {
|
|
yellow := color.New(color.FgYellow).SprintFunc()
|
|
fmt.Printf("\n%s No ready work found (all issues have blocking dependencies)\n\n",
|
|
yellow("✨"))
|
|
return
|
|
}
|
|
|
|
cyan := color.New(color.FgCyan).SprintFunc()
|
|
fmt.Printf("\n%s Ready work (%d issues with no blockers):\n\n", cyan("📋"), len(issues))
|
|
|
|
for i, issue := range 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()
|
|
return
|
|
}
|
|
|
|
// Direct mode
|
|
ctx := context.Background()
|
|
issues, err := store.GetReadyWork(ctx, filter)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// If no ready work found, check if git has issues and auto-import
|
|
if len(issues) == 0 {
|
|
if checkAndAutoImport(ctx, store) {
|
|
// Re-run the query after import
|
|
issues, err = store.GetReadyWork(ctx, filter)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
if jsonOutput {
|
|
// Always output array, even if empty
|
|
if issues == nil {
|
|
issues = []*types.Issue{}
|
|
}
|
|
outputJSON(issues)
|
|
return
|
|
}
|
|
|
|
if len(issues) == 0 {
|
|
yellow := color.New(color.FgYellow).SprintFunc()
|
|
fmt.Printf("\n%s No ready work found (all issues have blocking dependencies)\n\n",
|
|
yellow("✨"))
|
|
return
|
|
}
|
|
|
|
cyan := color.New(color.FgCyan).SprintFunc()
|
|
fmt.Printf("\n%s Ready work (%d issues with no blockers):\n\n", cyan("📋"), len(issues))
|
|
|
|
for i, issue := range 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()
|
|
},
|
|
}
|
|
|
|
var blockedCmd = &cobra.Command{
|
|
Use: "blocked",
|
|
Short: "Show blocked issues",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
// Use global jsonOutput set by PersistentPreRun (respects config.yaml + env vars)
|
|
|
|
// If daemon is running but doesn't support this command, use direct storage
|
|
if daemonClient != nil && store == nil {
|
|
var err error
|
|
store, err = sqlite.New(dbPath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: failed to open database: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer func() { _ = store.Close() }()
|
|
}
|
|
|
|
ctx := context.Background()
|
|
blocked, err := store.GetBlockedIssues(ctx)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if jsonOutput {
|
|
// Always output array, even if empty
|
|
if blocked == nil {
|
|
blocked = []*types.BlockedIssue{}
|
|
}
|
|
outputJSON(blocked)
|
|
return
|
|
}
|
|
|
|
if len(blocked) == 0 {
|
|
green := color.New(color.FgGreen).SprintFunc()
|
|
fmt.Printf("\n%s No blocked issues\n\n", green("✨"))
|
|
return
|
|
}
|
|
|
|
red := color.New(color.FgRed).SprintFunc()
|
|
fmt.Printf("\n%s Blocked issues (%d):\n\n", red("🚫"), len(blocked))
|
|
|
|
for _, issue := range blocked {
|
|
fmt.Printf("[P%d] %s: %s\n", issue.Priority, issue.ID, issue.Title)
|
|
blockedBy := issue.BlockedBy
|
|
if blockedBy == nil {
|
|
blockedBy = []string{}
|
|
}
|
|
fmt.Printf(" Blocked by %d open dependencies: %v\n",
|
|
issue.BlockedByCount, blockedBy)
|
|
fmt.Println()
|
|
}
|
|
},
|
|
}
|
|
|
|
var statsCmd = &cobra.Command{
|
|
Use: "stats",
|
|
Short: "Show statistics",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
// Use global jsonOutput set by PersistentPreRun (respects config.yaml + env vars)
|
|
|
|
// If daemon is running, use RPC
|
|
if daemonClient != nil {
|
|
resp, err := daemonClient.Stats()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
var stats types.Statistics
|
|
if err := json.Unmarshal(resp.Data, &stats); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if jsonOutput {
|
|
outputJSON(stats)
|
|
return
|
|
}
|
|
|
|
cyan := color.New(color.FgCyan).SprintFunc()
|
|
green := color.New(color.FgGreen).SprintFunc()
|
|
yellow := color.New(color.FgYellow).SprintFunc()
|
|
|
|
fmt.Printf("\n%s Beads Statistics:\n\n", cyan("📊"))
|
|
fmt.Printf("Total Issues: %d\n", stats.TotalIssues)
|
|
fmt.Printf("Open: %s\n", green(fmt.Sprintf("%d", stats.OpenIssues)))
|
|
fmt.Printf("In Progress: %s\n", yellow(fmt.Sprintf("%d", stats.InProgressIssues)))
|
|
fmt.Printf("Closed: %d\n", stats.ClosedIssues)
|
|
fmt.Printf("Blocked: %d\n", stats.BlockedIssues)
|
|
fmt.Printf("Ready: %s\n", green(fmt.Sprintf("%d", stats.ReadyIssues)))
|
|
if stats.AverageLeadTime > 0 {
|
|
fmt.Printf("Avg Lead Time: %.1f hours\n", stats.AverageLeadTime)
|
|
}
|
|
fmt.Println()
|
|
return
|
|
}
|
|
|
|
// Direct mode
|
|
ctx := context.Background()
|
|
stats, err := store.GetStatistics(ctx)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// If no issues found, check if git has issues and auto-import
|
|
if stats.TotalIssues == 0 {
|
|
if checkAndAutoImport(ctx, store) {
|
|
// Re-run the stats after import
|
|
stats, err = store.GetStatistics(ctx)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
if jsonOutput {
|
|
outputJSON(stats)
|
|
return
|
|
}
|
|
|
|
cyan := color.New(color.FgCyan).SprintFunc()
|
|
green := color.New(color.FgGreen).SprintFunc()
|
|
yellow := color.New(color.FgYellow).SprintFunc()
|
|
|
|
fmt.Printf("\n%s Beads Statistics:\n\n", cyan("📊"))
|
|
fmt.Printf("Total Issues: %d\n", stats.TotalIssues)
|
|
fmt.Printf("Open: %s\n", green(fmt.Sprintf("%d", stats.OpenIssues)))
|
|
fmt.Printf("In Progress: %s\n", yellow(fmt.Sprintf("%d", stats.InProgressIssues)))
|
|
fmt.Printf("Closed: %d\n", stats.ClosedIssues)
|
|
fmt.Printf("Blocked: %d\n", stats.BlockedIssues)
|
|
fmt.Printf("Ready: %s\n", green(fmt.Sprintf("%d", stats.ReadyIssues)))
|
|
if stats.EpicsEligibleForClosure > 0 {
|
|
fmt.Printf("Epics Ready to Close: %s\n", green(fmt.Sprintf("%d", stats.EpicsEligibleForClosure)))
|
|
}
|
|
if stats.AverageLeadTime > 0 {
|
|
fmt.Printf("Avg Lead Time: %.1f hours\n", stats.AverageLeadTime)
|
|
}
|
|
fmt.Println()
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
readyCmd.Flags().IntP("limit", "n", 10, "Maximum issues to show")
|
|
readyCmd.Flags().IntP("priority", "p", 0, "Filter by priority")
|
|
readyCmd.Flags().StringP("assignee", "a", "", "Filter by assignee")
|
|
readyCmd.Flags().StringP("sort", "s", "hybrid", "Sort policy: hybrid (default), priority, oldest")
|
|
readyCmd.Flags().Bool("json", false, "Output JSON format")
|
|
|
|
statsCmd.Flags().Bool("json", false, "Output JSON format")
|
|
blockedCmd.Flags().Bool("json", false, "Output JSON format")
|
|
|
|
rootCmd.AddCommand(readyCmd)
|
|
rootCmd.AddCommand(blockedCmd)
|
|
rootCmd.AddCommand(statsCmd)
|
|
}
|