Fixed TestHashIDs_IdenticalContentDedup test failure by removing duplicate --json flag definitions that were shadowing the global persistent flag. Root cause: Commands had both a persistent --json flag (main.go) and local --json flags (in individual command files). The local flags shadowed the persistent flag, preventing jsonOutput variable from being set correctly. Changes: - Removed 31 duplicate --json flag definitions from 15 command files - All commands now use the single persistent --json flag from main.go - Commands now correctly output JSON when --json flag is specified Test results: - TestHashIDs_IdenticalContentDedup: Now passes (was failing) - TestHashIDs_MultiCloneConverge: Passes without JSON parsing warnings - All other tests: Pass with no regressions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
265 lines
7.2 KiB
Go
265 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/steveyegge/beads/internal/types"
|
|
)
|
|
|
|
// StatusOutput represents the complete status output
|
|
type StatusOutput struct {
|
|
Summary *StatusSummary `json:"summary"`
|
|
RecentActivity *RecentActivitySummary `json:"recent_activity,omitempty"`
|
|
}
|
|
|
|
// StatusSummary represents counts by state
|
|
type StatusSummary struct {
|
|
TotalIssues int `json:"total_issues"`
|
|
OpenIssues int `json:"open_issues"`
|
|
InProgressIssues int `json:"in_progress_issues"`
|
|
BlockedIssues int `json:"blocked_issues"`
|
|
ClosedIssues int `json:"closed_issues"`
|
|
ReadyIssues int `json:"ready_issues"`
|
|
}
|
|
|
|
// RecentActivitySummary represents activity over the last 7 days
|
|
type RecentActivitySummary struct {
|
|
DaysTracked int `json:"days_tracked"`
|
|
IssuesCreated int `json:"issues_created"`
|
|
IssuesClosed int `json:"issues_closed"`
|
|
IssuesUpdated int `json:"issues_updated"`
|
|
}
|
|
|
|
var statusCmd = &cobra.Command{
|
|
Use: "status",
|
|
Short: "Show issue database overview",
|
|
Long: `Show a quick snapshot of the issue database state.
|
|
|
|
This command provides a summary of issue counts by state (open, in-progress,
|
|
blocked, closed), ready work, and recent activity over the last 7 days.
|
|
|
|
Similar to how 'git status' shows working tree state, 'bd status' gives you
|
|
a quick overview of your issue database without needing multiple queries.
|
|
|
|
Use cases:
|
|
- Quick project health check
|
|
- Onboarding for new contributors
|
|
- Integration with shell prompts or CI/CD
|
|
- Daily standup reference
|
|
|
|
Examples:
|
|
bd status # Show summary
|
|
bd status --json # JSON format output
|
|
bd status --assigned # Show issues assigned to current user
|
|
bd status --all # Show all issues (same as default)`,
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
showAll, _ := cmd.Flags().GetBool("all")
|
|
showAssigned, _ := cmd.Flags().GetBool("assigned")
|
|
jsonFormat, _ := cmd.Flags().GetBool("json")
|
|
|
|
// Override global jsonOutput if --json flag is set
|
|
if jsonFormat {
|
|
jsonOutput = true
|
|
}
|
|
|
|
// Get statistics
|
|
var stats *types.Statistics
|
|
var err error
|
|
|
|
// If daemon is running, use RPC
|
|
if daemonClient != nil {
|
|
resp, rpcErr := daemonClient.Stats()
|
|
if rpcErr != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", rpcErr)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if err := json.Unmarshal(resp.Data, &stats); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
} else {
|
|
// Direct mode
|
|
ctx := context.Background()
|
|
stats, err = store.GetStatistics(ctx)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// Build summary
|
|
summary := &StatusSummary{
|
|
TotalIssues: stats.TotalIssues,
|
|
OpenIssues: stats.OpenIssues,
|
|
InProgressIssues: stats.InProgressIssues,
|
|
BlockedIssues: stats.BlockedIssues,
|
|
ClosedIssues: stats.ClosedIssues,
|
|
ReadyIssues: stats.ReadyIssues,
|
|
}
|
|
|
|
// Get recent activity (last 7 days)
|
|
var recentActivity *RecentActivitySummary
|
|
if daemonClient != nil {
|
|
// TODO(bd-28db): Add RPC support for recent activity
|
|
// For now, skip recent activity in daemon mode
|
|
recentActivity = nil
|
|
} else {
|
|
ctx := context.Background()
|
|
var assigneeFilter *string
|
|
if showAssigned {
|
|
assigneeFilter = &actor
|
|
}
|
|
recentActivity = getRecentActivity(ctx, 7, assigneeFilter)
|
|
}
|
|
|
|
// Filter by assignee if requested
|
|
if showAssigned {
|
|
// Get filtered statistics for assigned issues
|
|
summary = getAssignedStatus(actor)
|
|
}
|
|
|
|
output := &StatusOutput{
|
|
Summary: summary,
|
|
RecentActivity: recentActivity,
|
|
}
|
|
|
|
// JSON output
|
|
if jsonOutput {
|
|
outputJSON(output)
|
|
return
|
|
}
|
|
|
|
// Human-readable output
|
|
fmt.Println("\nIssue Database Status")
|
|
fmt.Println("=====================")
|
|
fmt.Printf("\nSummary:\n")
|
|
fmt.Printf(" Total Issues: %d\n", summary.TotalIssues)
|
|
fmt.Printf(" Open: %d\n", summary.OpenIssues)
|
|
fmt.Printf(" In Progress: %d\n", summary.InProgressIssues)
|
|
fmt.Printf(" Blocked: %d\n", summary.BlockedIssues)
|
|
fmt.Printf(" Closed: %d\n", summary.ClosedIssues)
|
|
fmt.Printf(" Ready to Work: %d\n", summary.ReadyIssues)
|
|
|
|
if recentActivity != nil {
|
|
fmt.Printf("\nRecent Activity (last %d days):\n", recentActivity.DaysTracked)
|
|
fmt.Printf(" Issues Created: %d\n", recentActivity.IssuesCreated)
|
|
fmt.Printf(" Issues Closed: %d\n", recentActivity.IssuesClosed)
|
|
fmt.Printf(" Issues Updated: %d\n", recentActivity.IssuesUpdated)
|
|
}
|
|
|
|
// Show hint for more details
|
|
fmt.Printf("\nFor more details, use 'bd list' to see individual issues.\n")
|
|
fmt.Println()
|
|
|
|
// Suppress showAll flag (it's the default behavior, included for CLI familiarity)
|
|
_ = showAll
|
|
},
|
|
}
|
|
|
|
// getRecentActivity calculates activity stats for the last N days
|
|
// If assignee is provided, only count issues assigned to that user
|
|
func getRecentActivity(ctx context.Context, days int, assignee *string) *RecentActivitySummary {
|
|
if store == nil {
|
|
return nil
|
|
}
|
|
|
|
// Calculate the cutoff time
|
|
cutoff := time.Now().AddDate(0, 0, -days)
|
|
|
|
// Get all issues to check creation/update times
|
|
filter := types.IssueFilter{
|
|
Assignee: assignee,
|
|
}
|
|
issues, err := store.SearchIssues(ctx, "", filter)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
activity := &RecentActivitySummary{
|
|
DaysTracked: days,
|
|
}
|
|
|
|
for _, issue := range issues {
|
|
// Check if created recently
|
|
if issue.CreatedAt.After(cutoff) {
|
|
activity.IssuesCreated++
|
|
}
|
|
|
|
// Check if closed recently
|
|
if issue.Status == types.StatusClosed && issue.UpdatedAt.After(cutoff) {
|
|
// Verify it was actually closed recently (not just updated)
|
|
// For now, we'll count any closed issue updated recently
|
|
activity.IssuesClosed++
|
|
}
|
|
|
|
// Check if updated recently (but not created recently)
|
|
if issue.UpdatedAt.After(cutoff) && !issue.CreatedAt.After(cutoff) {
|
|
activity.IssuesUpdated++
|
|
}
|
|
}
|
|
|
|
return activity
|
|
}
|
|
|
|
// getAssignedStatus returns status summary for issues assigned to a specific user
|
|
func getAssignedStatus(assignee string) *StatusSummary {
|
|
if store == nil {
|
|
return nil
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
// Filter by assignee
|
|
assigneePtr := assignee
|
|
filter := types.IssueFilter{
|
|
Assignee: &assigneePtr,
|
|
}
|
|
|
|
issues, err := store.SearchIssues(ctx, "", filter)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
summary := &StatusSummary{
|
|
TotalIssues: len(issues),
|
|
}
|
|
|
|
// Count by status
|
|
for _, issue := range issues {
|
|
switch issue.Status {
|
|
case types.StatusOpen:
|
|
summary.OpenIssues++
|
|
case types.StatusInProgress:
|
|
summary.InProgressIssues++
|
|
case types.StatusBlocked:
|
|
summary.BlockedIssues++
|
|
case types.StatusClosed:
|
|
summary.ClosedIssues++
|
|
}
|
|
}
|
|
|
|
// Get ready work count for this assignee
|
|
readyFilter := types.WorkFilter{
|
|
Assignee: &assigneePtr,
|
|
}
|
|
readyIssues, err := store.GetReadyWork(ctx, readyFilter)
|
|
if err == nil {
|
|
summary.ReadyIssues = len(readyIssues)
|
|
}
|
|
|
|
return summary
|
|
}
|
|
|
|
func init() {
|
|
statusCmd.Flags().Bool("all", false, "Show all issues (default behavior)")
|
|
statusCmd.Flags().Bool("assigned", false, "Show issues assigned to current user")
|
|
// Note: --json flag is defined as a persistent flag in main.go, not here
|
|
rootCmd.AddCommand(statusCmd)
|
|
}
|