Merge upstream/main into subtle-ux-improvements

Resolves conflicts and converts new defer/undefer commands from
fatih/color to the lipgloss semantic color system.

Key changes:
- Added StatusDeferred case in graph.go with ui.RenderAccent
- Converted status.go to use ui package for colorized output
- Converted defer.go/undefer.go to use ui package
- Merged GroupID and Aliases for status command
- Updated pre-commit hook version to 0.31.0
- Ran go mod tidy to remove fatih/color dependency
This commit is contained in:
Ryan Snodgrass
2025-12-20 17:22:43 -08:00
45 changed files with 850 additions and 316 deletions

136
cmd/bd/undefer.go Normal file
View File

@@ -0,0 +1,136 @@
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/utils"
)
var undeferCmd = &cobra.Command{
Use: "undefer [id...]",
Short: "Undefer one or more issues (restore to open)",
Long: `Undefer issues to restore them to open status.
This brings issues back from the icebox so they can be worked on again.
Issues will appear in 'bd ready' if they have no blockers.
Examples:
bd undefer bd-abc # Undefer a single issue
bd undefer bd-abc bd-def # Undefer multiple issues`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
CheckReadonly("undefer")
ctx := rootCtx
// Resolve partial IDs first
var resolvedIDs []string
if daemonClient != nil {
for _, id := range args {
resolveArgs := &rpc.ResolveIDArgs{ID: id}
resp, err := daemonClient.ResolveID(resolveArgs)
if err != nil {
fmt.Fprintf(os.Stderr, "Error resolving ID %s: %v\n", id, err)
os.Exit(1)
}
var resolvedID string
if err := json.Unmarshal(resp.Data, &resolvedID); err != nil {
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
os.Exit(1)
}
resolvedIDs = append(resolvedIDs, resolvedID)
}
} else {
var err error
resolvedIDs, err = utils.ResolvePartialIDs(ctx, store, args)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
undeferredIssues := []*types.Issue{}
// If daemon is running, use RPC
if daemonClient != nil {
for _, id := range resolvedIDs {
status := string(types.StatusOpen)
updateArgs := &rpc.UpdateArgs{
ID: id,
Status: &status,
}
resp, err := daemonClient.Update(updateArgs)
if err != nil {
fmt.Fprintf(os.Stderr, "Error undeferring %s: %v\n", id, err)
continue
}
if jsonOutput {
var issue types.Issue
if err := json.Unmarshal(resp.Data, &issue); err == nil {
undeferredIssues = append(undeferredIssues, &issue)
}
} else {
fmt.Printf("%s Undeferred %s (now open)\n", ui.RenderPass("*"), id)
}
}
if jsonOutput && len(undeferredIssues) > 0 {
outputJSON(undeferredIssues)
}
return
}
// Fall back to direct storage access
if store == nil {
fmt.Fprintln(os.Stderr, "Error: database not initialized")
os.Exit(1)
}
for _, id := range args {
fullID, err := utils.ResolvePartialID(ctx, store, id)
if err != nil {
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", id, err)
continue
}
updates := map[string]interface{}{
"status": string(types.StatusOpen),
}
if err := store.UpdateIssue(ctx, fullID, updates, actor); err != nil {
fmt.Fprintf(os.Stderr, "Error undeferring %s: %v\n", fullID, err)
continue
}
if jsonOutput {
issue, _ := store.GetIssue(ctx, fullID)
if issue != nil {
undeferredIssues = append(undeferredIssues, issue)
}
} else {
fmt.Printf("%s Undeferred %s (now open)\n", ui.RenderPass("*"), fullID)
}
}
// Schedule auto-flush if any issues were undeferred
if len(args) > 0 {
markDirtyAndScheduleFlush()
}
if jsonOutput && len(undeferredIssues) > 0 {
outputJSON(undeferredIssues)
}
},
}
func init() {
rootCmd.AddCommand(undeferCmd)
}