Initial commit: Beads issue tracker with security fixes
Core features:
- Dependency-aware issue tracking with SQLite backend
- Ready work detection (issues with no open blockers)
- Dependency tree visualization
- Cycle detection and prevention
- Full audit trail
- CLI with colored output
Security and correctness fixes applied:
- Fixed SQL injection vulnerability in UpdateIssue (whitelisted fields)
- Fixed race condition in ID generation (added mutex)
- Fixed cycle detection to return full paths (not just issue IDs)
- Added cycle prevention in AddDependency (validates before commit)
- Added comprehensive input validation (priority, status, types, etc.)
- Fixed N+1 query in GetBlockedIssues (using GROUP_CONCAT)
- Improved query building in GetReadyWork (proper string joining)
- Fixed P0 priority filter bug (using Changed() instead of value check)
All critical and major issues from code review have been addressed.
🤖 Generated with Claude Code
This commit is contained in:
138
cmd/beads/dep.go
Normal file
138
cmd/beads/dep.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyackey/beads/internal/types"
|
||||
)
|
||||
|
||||
var depCmd = &cobra.Command{
|
||||
Use: "dep",
|
||||
Short: "Manage dependencies",
|
||||
}
|
||||
|
||||
var depAddCmd = &cobra.Command{
|
||||
Use: "add [issue-id] [depends-on-id]",
|
||||
Short: "Add a dependency",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
depType, _ := cmd.Flags().GetString("type")
|
||||
|
||||
dep := &types.Dependency{
|
||||
IssueID: args[0],
|
||||
DependsOnID: args[1],
|
||||
Type: types.DependencyType(depType),
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if err := store.AddDependency(ctx, dep, actor); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Added dependency: %s depends on %s (%s)\n",
|
||||
green("✓"), args[0], args[1], depType)
|
||||
},
|
||||
}
|
||||
|
||||
var depRemoveCmd = &cobra.Command{
|
||||
Use: "remove [issue-id] [depends-on-id]",
|
||||
Short: "Remove a dependency",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ctx := context.Background()
|
||||
if err := store.RemoveDependency(ctx, args[0], args[1], actor); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Removed dependency: %s no longer depends on %s\n",
|
||||
green("✓"), args[0], args[1])
|
||||
},
|
||||
}
|
||||
|
||||
var depTreeCmd = &cobra.Command{
|
||||
Use: "tree [issue-id]",
|
||||
Short: "Show dependency tree",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ctx := context.Background()
|
||||
tree, err := store.GetDependencyTree(ctx, args[0], 50)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(tree) == 0 {
|
||||
fmt.Printf("\n%s has no dependencies\n", args[0])
|
||||
return
|
||||
}
|
||||
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
fmt.Printf("\n%s Dependency tree for %s:\n\n", cyan("🌲"), args[0])
|
||||
|
||||
hasTruncation := false
|
||||
for _, node := range tree {
|
||||
indent := ""
|
||||
for i := 0; i < node.Depth; i++ {
|
||||
indent += " "
|
||||
}
|
||||
fmt.Printf("%s→ %s: %s [P%d] (%s)\n",
|
||||
indent, node.ID, node.Title, node.Priority, node.Status)
|
||||
if node.Truncated {
|
||||
hasTruncation = true
|
||||
}
|
||||
}
|
||||
|
||||
if hasTruncation {
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
fmt.Printf("\n%s Warning: Tree truncated at depth 50 (safety limit)\n",
|
||||
yellow("⚠"))
|
||||
}
|
||||
fmt.Println()
|
||||
},
|
||||
}
|
||||
|
||||
var depCyclesCmd = &cobra.Command{
|
||||
Use: "cycles",
|
||||
Short: "Detect dependency cycles",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ctx := context.Background()
|
||||
cycles, err := store.DetectCycles(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(cycles) == 0 {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("\n%s No dependency cycles detected\n\n", green("✓"))
|
||||
return
|
||||
}
|
||||
|
||||
red := color.New(color.FgRed).SprintFunc()
|
||||
fmt.Printf("\n%s Found %d dependency cycles:\n\n", red("⚠"), len(cycles))
|
||||
for i, cycle := range cycles {
|
||||
fmt.Printf("%d. Cycle involving:\n", i+1)
|
||||
for _, issue := range cycle {
|
||||
fmt.Printf(" - %s: %s\n", issue.ID, issue.Title)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
depAddCmd.Flags().StringP("type", "t", "blocks", "Dependency type (blocks|related|parent-child)")
|
||||
depCmd.AddCommand(depAddCmd)
|
||||
depCmd.AddCommand(depRemoveCmd)
|
||||
depCmd.AddCommand(depTreeCmd)
|
||||
depCmd.AddCommand(depCyclesCmd)
|
||||
rootCmd.AddCommand(depCmd)
|
||||
}
|
||||
325
cmd/beads/main.go
Normal file
325
cmd/beads/main.go
Normal file
@@ -0,0 +1,325 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyackey/beads/internal/storage"
|
||||
"github.com/steveyackey/beads/internal/storage/sqlite"
|
||||
"github.com/steveyackey/beads/internal/types"
|
||||
)
|
||||
|
||||
var (
|
||||
dbPath string
|
||||
actor string
|
||||
store storage.Storage
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "beads",
|
||||
Short: "Beads - Dependency-aware issue tracker",
|
||||
Long: `Issues chained together like beads. A lightweight issue tracker with first-class dependency support.`,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
// Initialize storage
|
||||
if dbPath == "" {
|
||||
home, _ := os.UserHomeDir()
|
||||
dbPath = filepath.Join(home, ".beads", "beads.db")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Set actor from env or default
|
||||
if actor == "" {
|
||||
actor = os.Getenv("USER")
|
||||
if actor == "" {
|
||||
actor = "unknown"
|
||||
}
|
||||
}
|
||||
},
|
||||
PersistentPostRun: func(cmd *cobra.Command, args []string) {
|
||||
if store != nil {
|
||||
store.Close()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringVar(&dbPath, "db", "", "Database path (default: ~/.beads/beads.db)")
|
||||
rootCmd.PersistentFlags().StringVar(&actor, "actor", "", "Actor name for audit trail (default: $USER)")
|
||||
}
|
||||
|
||||
var createCmd = &cobra.Command{
|
||||
Use: "create [title]",
|
||||
Short: "Create a new issue",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
title := args[0]
|
||||
description, _ := cmd.Flags().GetString("description")
|
||||
design, _ := cmd.Flags().GetString("design")
|
||||
acceptance, _ := cmd.Flags().GetString("acceptance")
|
||||
priority, _ := cmd.Flags().GetInt("priority")
|
||||
issueType, _ := cmd.Flags().GetString("type")
|
||||
assignee, _ := cmd.Flags().GetString("assignee")
|
||||
labels, _ := cmd.Flags().GetStringSlice("labels")
|
||||
|
||||
issue := &types.Issue{
|
||||
Title: title,
|
||||
Description: description,
|
||||
Design: design,
|
||||
AcceptanceCriteria: acceptance,
|
||||
Status: types.StatusOpen,
|
||||
Priority: priority,
|
||||
IssueType: types.IssueType(issueType),
|
||||
Assignee: assignee,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if err := store.CreateIssue(ctx, issue, actor); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Add labels if specified
|
||||
for _, label := range labels {
|
||||
if err := store.AddLabel(ctx, issue.ID, label, actor); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to add label %s: %v\n", label, err)
|
||||
}
|
||||
}
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Created issue: %s\n", green("✓"), issue.ID)
|
||||
fmt.Printf(" Title: %s\n", issue.Title)
|
||||
fmt.Printf(" Priority: P%d\n", issue.Priority)
|
||||
fmt.Printf(" Status: %s\n", issue.Status)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
createCmd.Flags().StringP("description", "d", "", "Issue description")
|
||||
createCmd.Flags().String("design", "", "Design notes")
|
||||
createCmd.Flags().String("acceptance", "", "Acceptance criteria")
|
||||
createCmd.Flags().IntP("priority", "p", 2, "Priority (0-4, 0=highest)")
|
||||
createCmd.Flags().StringP("type", "t", "task", "Issue type (bug|feature|task|epic|chore)")
|
||||
createCmd.Flags().StringP("assignee", "a", "", "Assignee")
|
||||
createCmd.Flags().StringSliceP("labels", "l", []string{}, "Labels (comma-separated)")
|
||||
rootCmd.AddCommand(createCmd)
|
||||
}
|
||||
|
||||
var showCmd = &cobra.Command{
|
||||
Use: "show [id]",
|
||||
Short: "Show issue details",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ctx := context.Background()
|
||||
issue, err := store.GetIssue(ctx, args[0])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if issue == nil {
|
||||
fmt.Fprintf(os.Stderr, "Issue %s not found\n", args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
fmt.Printf("\n%s: %s\n", cyan(issue.ID), issue.Title)
|
||||
fmt.Printf("Status: %s\n", issue.Status)
|
||||
fmt.Printf("Priority: P%d\n", issue.Priority)
|
||||
fmt.Printf("Type: %s\n", issue.IssueType)
|
||||
if issue.Assignee != "" {
|
||||
fmt.Printf("Assignee: %s\n", issue.Assignee)
|
||||
}
|
||||
if issue.EstimatedMinutes != nil {
|
||||
fmt.Printf("Estimated: %d minutes\n", *issue.EstimatedMinutes)
|
||||
}
|
||||
fmt.Printf("Created: %s\n", issue.CreatedAt.Format("2006-01-02 15:04"))
|
||||
fmt.Printf("Updated: %s\n", issue.UpdatedAt.Format("2006-01-02 15:04"))
|
||||
|
||||
if issue.Description != "" {
|
||||
fmt.Printf("\nDescription:\n%s\n", issue.Description)
|
||||
}
|
||||
if issue.Design != "" {
|
||||
fmt.Printf("\nDesign:\n%s\n", issue.Design)
|
||||
}
|
||||
if issue.AcceptanceCriteria != "" {
|
||||
fmt.Printf("\nAcceptance Criteria:\n%s\n", issue.AcceptanceCriteria)
|
||||
}
|
||||
|
||||
// Show labels
|
||||
labels, _ := store.GetLabels(ctx, issue.ID)
|
||||
if len(labels) > 0 {
|
||||
fmt.Printf("\nLabels: %v\n", labels)
|
||||
}
|
||||
|
||||
// Show dependencies
|
||||
deps, _ := store.GetDependencies(ctx, issue.ID)
|
||||
if len(deps) > 0 {
|
||||
fmt.Printf("\nDepends on (%d):\n", len(deps))
|
||||
for _, dep := range deps {
|
||||
fmt.Printf(" → %s: %s [P%d]\n", dep.ID, dep.Title, dep.Priority)
|
||||
}
|
||||
}
|
||||
|
||||
// Show dependents
|
||||
dependents, _ := store.GetDependents(ctx, issue.ID)
|
||||
if len(dependents) > 0 {
|
||||
fmt.Printf("\nBlocks (%d):\n", len(dependents))
|
||||
for _, dep := range dependents {
|
||||
fmt.Printf(" ← %s: %s [P%d]\n", dep.ID, dep.Title, dep.Priority)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fmt.Printf("\nFound %d issues:\n\n", len(issues))
|
||||
for _, issue := range issues {
|
||||
fmt.Printf("%s [P%d] %s\n", issue.ID, issue.Priority, 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",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
updates := make(map[string]interface{})
|
||||
|
||||
if cmd.Flags().Changed("status") {
|
||||
status, _ := cmd.Flags().GetString("status")
|
||||
updates["status"] = status
|
||||
}
|
||||
if cmd.Flags().Changed("priority") {
|
||||
priority, _ := cmd.Flags().GetInt("priority")
|
||||
updates["priority"] = priority
|
||||
}
|
||||
if cmd.Flags().Changed("title") {
|
||||
title, _ := cmd.Flags().GetString("title")
|
||||
updates["title"] = title
|
||||
}
|
||||
if cmd.Flags().Changed("assignee") {
|
||||
assignee, _ := cmd.Flags().GetString("assignee")
|
||||
updates["assignee"] = assignee
|
||||
}
|
||||
|
||||
if len(updates) == 0 {
|
||||
fmt.Println("No updates specified")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if err := store.UpdateIssue(ctx, args[0], updates, actor); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Updated issue: %s\n", green("✓"), args[0])
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
updateCmd.Flags().StringP("status", "s", "", "New status")
|
||||
updateCmd.Flags().IntP("priority", "p", 0, "New priority")
|
||||
updateCmd.Flags().String("title", "", "New title")
|
||||
updateCmd.Flags().StringP("assignee", "a", "", "New assignee")
|
||||
rootCmd.AddCommand(updateCmd)
|
||||
}
|
||||
|
||||
var closeCmd = &cobra.Command{
|
||||
Use: "close [id...]",
|
||||
Short: "Close one or more issues",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
reason, _ := cmd.Flags().GetString("reason")
|
||||
if reason == "" {
|
||||
reason = "Closed"
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
for _, id := range args {
|
||||
if err := store.CloseIssue(ctx, id, reason, actor); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error closing %s: %v\n", id, err)
|
||||
continue
|
||||
}
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Closed %s: %s\n", green("✓"), id, reason)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
closeCmd.Flags().StringP("reason", "r", "", "Reason for closing")
|
||||
rootCmd.AddCommand(closeCmd)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
129
cmd/beads/ready.go
Normal file
129
cmd/beads/ready.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyackey/beads/internal/types"
|
||||
)
|
||||
|
||||
var readyCmd = &cobra.Command{
|
||||
Use: "ready",
|
||||
Short: "Show ready work (no blockers)",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
limit, _ := cmd.Flags().GetInt("limit")
|
||||
assignee, _ := cmd.Flags().GetString("assignee")
|
||||
|
||||
filter := types.WorkFilter{
|
||||
Status: types.StatusOpen,
|
||||
Limit: limit,
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
issues, err := store.GetReadyWork(ctx, filter)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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) {
|
||||
ctx := context.Background()
|
||||
blocked, err := store.GetBlockedIssues(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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)
|
||||
fmt.Printf(" Blocked by %d open dependencies: %v\n",
|
||||
issue.BlockedByCount, issue.BlockedBy)
|
||||
fmt.Println()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var statsCmd = &cobra.Command{
|
||||
Use: "stats",
|
||||
Short: "Show statistics",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ctx := context.Background()
|
||||
stats, err := store.GetStatistics(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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()
|
||||
},
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
rootCmd.AddCommand(readyCmd)
|
||||
rootCmd.AddCommand(blockedCmd)
|
||||
rootCmd.AddCommand(statsCmd)
|
||||
}
|
||||
Reference in New Issue
Block a user