feat: Add RPC support for epic commands in daemon mode

- Added OpEpicStatus operation to protocol
- Implemented handleEpicStatus() in RPC server
- Added EpicStatus() method to RPC client
- Updated epic.go to use daemon RPC when available
- Server-side filtering for close-eligible reduces RPC payload
- Both 'bd epic status' and 'bd epic close-eligible' now work in daemon mode

Fixes #62
Closes bd-87

Amp-Thread-ID: https://ampcode.com/threads/T-44c6044e-de04-40a0-bac3-b26238c32a17
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-10-23 22:04:13 -07:00
parent 72d16229e0
commit 0b819e1f40
5 changed files with 150 additions and 45 deletions

View File

@@ -8,6 +8,7 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/types"
)
@@ -23,29 +24,42 @@ var epicStatusCmd = &cobra.Command{
eligibleOnly, _ := cmd.Flags().GetBool("eligible-only")
jsonOutput, _ := cmd.Flags().GetBool("json")
// TODO: Add RPC support when daemon is running
var epics []*types.EpicStatus
var err error
if daemonClient != nil {
fmt.Fprintf(os.Stderr, "Error: epic commands not yet supported in daemon mode\n")
fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag for direct mode\n")
os.Exit(1)
}
ctx := context.Background()
epics, err := store.GetEpicsEligibleForClosure(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting epic status: %v\n", err)
os.Exit(1)
}
// Filter if eligible-only flag is set
if eligibleOnly {
filtered := []*types.EpicStatus{}
for _, epic := range epics {
if epic.EligibleForClose {
filtered = append(filtered, epic)
}
resp, err := daemonClient.EpicStatus(&rpc.EpicStatusArgs{
EligibleOnly: eligibleOnly,
})
if err != nil {
fmt.Fprintf(os.Stderr, "Error communicating with daemon: %v\n", err)
os.Exit(1)
}
if !resp.Success {
fmt.Fprintf(os.Stderr, "Error getting epic status: %s\n", resp.Error)
os.Exit(1)
}
if err := json.Unmarshal(resp.Data, &epics); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
os.Exit(1)
}
} else {
ctx := context.Background()
epics, err = store.GetEpicsEligibleForClosure(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting epic status: %v\n", err)
os.Exit(1)
}
if eligibleOnly {
filtered := []*types.EpicStatus{}
for _, epic := range epics {
if epic.EligibleForClose {
filtered = append(filtered, epic)
}
}
epics = filtered
}
epics = filtered
}
if jsonOutput {
@@ -103,25 +117,35 @@ var closeEligibleEpicsCmd = &cobra.Command{
dryRun, _ := cmd.Flags().GetBool("dry-run")
jsonOutput, _ := cmd.Flags().GetBool("json")
// TODO: Add RPC support when daemon is running
var eligibleEpics []*types.EpicStatus
if daemonClient != nil {
fmt.Fprintf(os.Stderr, "Error: epic commands not yet supported in daemon mode\n")
fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag for direct mode\n")
os.Exit(1)
}
ctx := context.Background()
epics, err := store.GetEpicsEligibleForClosure(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting eligible epics: %v\n", err)
os.Exit(1)
}
// Filter to only eligible ones
eligibleEpics := []*types.EpicStatus{}
for _, epic := range epics {
if epic.EligibleForClose {
eligibleEpics = append(eligibleEpics, epic)
resp, err := daemonClient.EpicStatus(&rpc.EpicStatusArgs{
EligibleOnly: true,
})
if err != nil {
fmt.Fprintf(os.Stderr, "Error communicating with daemon: %v\n", err)
os.Exit(1)
}
if !resp.Success {
fmt.Fprintf(os.Stderr, "Error getting eligible epics: %s\n", resp.Error)
os.Exit(1)
}
if err := json.Unmarshal(resp.Data, &eligibleEpics); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
os.Exit(1)
}
} else {
ctx := context.Background()
epics, err := store.GetEpicsEligibleForClosure(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting eligible epics: %v\n", err)
os.Exit(1)
}
for _, epic := range epics {
if epic.EligibleForClose {
eligibleEpics = append(eligibleEpics, epic)
}
}
}
@@ -154,10 +178,28 @@ var closeEligibleEpicsCmd = &cobra.Command{
// Actually close the epics
closedIDs := []string{}
for _, epicStatus := range eligibleEpics {
err := store.CloseIssue(ctx, epicStatus.Epic.ID, "All children completed", "system")
if err != nil {
fmt.Fprintf(os.Stderr, "Error closing %s: %v\n", epicStatus.Epic.ID, err)
continue
if daemonClient != nil {
resp, err := daemonClient.CloseIssue(&rpc.CloseArgs{
ID: epicStatus.Epic.ID,
Reason: "All children completed",
})
if err != nil || !resp.Success {
errMsg := "unknown error"
if err != nil {
errMsg = err.Error()
} else {
errMsg = resp.Error
}
fmt.Fprintf(os.Stderr, "Error closing %s: %s\n", epicStatus.Epic.ID, errMsg)
continue
}
} else {
ctx := context.Background()
err := store.CloseIssue(ctx, epicStatus.Epic.ID, "All children completed", "system")
if err != nil {
fmt.Fprintf(os.Stderr, "Error closing %s: %v\n", epicStatus.Epic.ID, err)
continue
}
}
closedIDs = append(closedIDs, epicStatus.Epic.ID)
}