The daemon RPC server was crashing with a nil pointer dereference when the global daemon received list, ready, stats, or other storage-dependent RPC requests. The global daemon is created with nil storage, causing these operations to panic when they attempted to access storage methods. This fix adds defensive nil checks at the beginning of all RPC handlers that require storage access. When storage is unavailable, they now return a proper JSON error response instead of crashing the daemon. The error message also informs users that the global daemon is deprecated and they should use local daemons instead. Handlers fixed: - handleCreate, handleUpdate, handleClose - handleList, handleShow, handleReady, handleStale - handleResolveID, handleStats, handleEpicStatus - handleCompact, handleCompactStats - handleDepAdd (and via handleSimpleStoreOp for all label/dep/comment ops) Co-authored-by: Test User <test@example.com>
781 lines
19 KiB
Go
781 lines
19 KiB
Go
package rpc
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/steveyegge/beads/internal/storage/sqlite"
|
|
"github.com/steveyegge/beads/internal/types"
|
|
"github.com/steveyegge/beads/internal/util"
|
|
"github.com/steveyegge/beads/internal/utils"
|
|
)
|
|
|
|
// parseTimeRPC parses time strings in multiple formats (RFC3339, YYYY-MM-DD, etc.)
|
|
// Matches the parseTimeFlag behavior in cmd/bd/list.go for CLI parity
|
|
func parseTimeRPC(s string) (time.Time, error) {
|
|
// Try RFC3339 first (ISO 8601 with timezone)
|
|
if t, err := time.Parse(time.RFC3339, s); err == nil {
|
|
return t, nil
|
|
}
|
|
|
|
// Try YYYY-MM-DD format (common user input)
|
|
if t, err := time.Parse("2006-01-02", s); err == nil {
|
|
return t, nil
|
|
}
|
|
|
|
// Try YYYY-MM-DD HH:MM:SS format
|
|
if t, err := time.Parse("2006-01-02 15:04:05", s); err == nil {
|
|
return t, nil
|
|
}
|
|
|
|
return time.Time{}, fmt.Errorf("unsupported date format: %q (use YYYY-MM-DD or RFC3339)", s)
|
|
}
|
|
|
|
func strValue(p *string) string {
|
|
if p == nil {
|
|
return ""
|
|
}
|
|
return *p
|
|
}
|
|
|
|
func updatesFromArgs(a UpdateArgs) map[string]interface{} {
|
|
u := map[string]interface{}{}
|
|
if a.Title != nil {
|
|
u["title"] = *a.Title
|
|
}
|
|
if a.Description != nil {
|
|
u["description"] = *a.Description
|
|
}
|
|
if a.Status != nil {
|
|
u["status"] = *a.Status
|
|
}
|
|
if a.Priority != nil {
|
|
u["priority"] = *a.Priority
|
|
}
|
|
if a.Design != nil {
|
|
u["design"] = *a.Design
|
|
}
|
|
if a.AcceptanceCriteria != nil {
|
|
u["acceptance_criteria"] = *a.AcceptanceCriteria
|
|
}
|
|
if a.Notes != nil {
|
|
u["notes"] = *a.Notes
|
|
}
|
|
if a.Assignee != nil {
|
|
u["assignee"] = *a.Assignee
|
|
}
|
|
return u
|
|
}
|
|
|
|
func (s *Server) handleCreate(req *Request) Response {
|
|
var createArgs CreateArgs
|
|
if err := json.Unmarshal(req.Args, &createArgs); err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("invalid create args: %v", err),
|
|
}
|
|
}
|
|
|
|
// Check for conflicting flags
|
|
if createArgs.ID != "" && createArgs.Parent != "" {
|
|
return Response{
|
|
Success: false,
|
|
Error: "cannot specify both ID and Parent",
|
|
}
|
|
}
|
|
|
|
store := s.storage
|
|
if store == nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)",
|
|
}
|
|
}
|
|
ctx := s.reqCtx(req)
|
|
|
|
// If parent is specified, generate child ID
|
|
issueID := createArgs.ID
|
|
if createArgs.Parent != "" {
|
|
childID, err := store.GetNextChildID(ctx, createArgs.Parent)
|
|
if err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to generate child ID: %v", err),
|
|
}
|
|
}
|
|
issueID = childID
|
|
}
|
|
|
|
var design, acceptance, assignee *string
|
|
if createArgs.Design != "" {
|
|
design = &createArgs.Design
|
|
}
|
|
if createArgs.AcceptanceCriteria != "" {
|
|
acceptance = &createArgs.AcceptanceCriteria
|
|
}
|
|
if createArgs.Assignee != "" {
|
|
assignee = &createArgs.Assignee
|
|
}
|
|
|
|
issue := &types.Issue{
|
|
ID: issueID,
|
|
Title: createArgs.Title,
|
|
Description: createArgs.Description,
|
|
IssueType: types.IssueType(createArgs.IssueType),
|
|
Priority: createArgs.Priority,
|
|
Design: strValue(design),
|
|
AcceptanceCriteria: strValue(acceptance),
|
|
Assignee: strValue(assignee),
|
|
Status: types.StatusOpen,
|
|
}
|
|
|
|
// Check if any dependencies are discovered-from type
|
|
// If so, inherit source_repo from the parent issue
|
|
var discoveredFromParentID string
|
|
for _, depSpec := range createArgs.Dependencies {
|
|
depSpec = strings.TrimSpace(depSpec)
|
|
if depSpec == "" {
|
|
continue
|
|
}
|
|
|
|
var depType types.DependencyType
|
|
var dependsOnID string
|
|
|
|
if strings.Contains(depSpec, ":") {
|
|
parts := strings.SplitN(depSpec, ":", 2)
|
|
if len(parts) == 2 {
|
|
depType = types.DependencyType(strings.TrimSpace(parts[0]))
|
|
dependsOnID = strings.TrimSpace(parts[1])
|
|
|
|
if depType == types.DepDiscoveredFrom {
|
|
discoveredFromParentID = dependsOnID
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we found a discovered-from dependency, inherit source_repo from parent
|
|
if discoveredFromParentID != "" {
|
|
parentIssue, err := store.GetIssue(ctx, discoveredFromParentID)
|
|
if err == nil && parentIssue.SourceRepo != "" {
|
|
issue.SourceRepo = parentIssue.SourceRepo
|
|
}
|
|
// If error getting parent or parent has no source_repo, continue with default
|
|
}
|
|
|
|
if err := store.CreateIssue(ctx, issue, s.reqActor(req)); err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to create issue: %v", err),
|
|
}
|
|
}
|
|
|
|
// Add labels if specified
|
|
for _, label := range createArgs.Labels {
|
|
if err := store.AddLabel(ctx, issue.ID, label, s.reqActor(req)); err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to add label %s: %v", label, err),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add dependencies if specified
|
|
for _, depSpec := range createArgs.Dependencies {
|
|
depSpec = strings.TrimSpace(depSpec)
|
|
if depSpec == "" {
|
|
continue
|
|
}
|
|
|
|
var depType types.DependencyType
|
|
var dependsOnID string
|
|
|
|
if strings.Contains(depSpec, ":") {
|
|
parts := strings.SplitN(depSpec, ":", 2)
|
|
if len(parts) != 2 {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("invalid dependency format '%s', expected 'type:id' or 'id'", depSpec),
|
|
}
|
|
}
|
|
depType = types.DependencyType(strings.TrimSpace(parts[0]))
|
|
dependsOnID = strings.TrimSpace(parts[1])
|
|
} else {
|
|
depType = types.DepBlocks
|
|
dependsOnID = depSpec
|
|
}
|
|
|
|
if !depType.IsValid() {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("invalid dependency type '%s' (valid: blocks, related, parent-child, discovered-from)", depType),
|
|
}
|
|
}
|
|
|
|
dep := &types.Dependency{
|
|
IssueID: issue.ID,
|
|
DependsOnID: dependsOnID,
|
|
Type: depType,
|
|
}
|
|
if err := store.AddDependency(ctx, dep, s.reqActor(req)); err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to add dependency %s -> %s: %v", issue.ID, dependsOnID, err),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Emit mutation event for event-driven daemon
|
|
s.emitMutation(MutationCreate, issue.ID)
|
|
|
|
data, _ := json.Marshal(issue)
|
|
return Response{
|
|
Success: true,
|
|
Data: data,
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleUpdate(req *Request) Response {
|
|
var updateArgs UpdateArgs
|
|
if err := json.Unmarshal(req.Args, &updateArgs); err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("invalid update args: %v", err),
|
|
}
|
|
}
|
|
|
|
store := s.storage
|
|
if store == nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)",
|
|
}
|
|
}
|
|
|
|
ctx := s.reqCtx(req)
|
|
updates := updatesFromArgs(updateArgs)
|
|
if len(updates) == 0 {
|
|
return Response{Success: true}
|
|
}
|
|
|
|
if err := store.UpdateIssue(ctx, updateArgs.ID, updates, s.reqActor(req)); err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to update issue: %v", err),
|
|
}
|
|
}
|
|
|
|
// Emit mutation event for event-driven daemon
|
|
s.emitMutation(MutationUpdate, updateArgs.ID)
|
|
|
|
issue, err := store.GetIssue(ctx, updateArgs.ID)
|
|
if err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to get updated issue: %v", err),
|
|
}
|
|
}
|
|
|
|
data, _ := json.Marshal(issue)
|
|
return Response{
|
|
Success: true,
|
|
Data: data,
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleClose(req *Request) Response {
|
|
var closeArgs CloseArgs
|
|
if err := json.Unmarshal(req.Args, &closeArgs); err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("invalid close args: %v", err),
|
|
}
|
|
}
|
|
|
|
store := s.storage
|
|
if store == nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)",
|
|
}
|
|
}
|
|
|
|
ctx := s.reqCtx(req)
|
|
if err := store.CloseIssue(ctx, closeArgs.ID, closeArgs.Reason, s.reqActor(req)); err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to close issue: %v", err),
|
|
}
|
|
}
|
|
|
|
// Emit mutation event for event-driven daemon
|
|
s.emitMutation(MutationUpdate, closeArgs.ID)
|
|
|
|
issue, _ := store.GetIssue(ctx, closeArgs.ID)
|
|
data, _ := json.Marshal(issue)
|
|
return Response{
|
|
Success: true,
|
|
Data: data,
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleList(req *Request) Response {
|
|
var listArgs ListArgs
|
|
if err := json.Unmarshal(req.Args, &listArgs); err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("invalid list args: %v", err),
|
|
}
|
|
}
|
|
|
|
store := s.storage
|
|
if store == nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)",
|
|
}
|
|
}
|
|
|
|
filter := types.IssueFilter{
|
|
Limit: listArgs.Limit,
|
|
}
|
|
|
|
// Normalize status: treat "" or "all" as unset (no filter)
|
|
if listArgs.Status != "" && listArgs.Status != "all" {
|
|
status := types.Status(listArgs.Status)
|
|
filter.Status = &status
|
|
}
|
|
|
|
if listArgs.IssueType != "" {
|
|
issueType := types.IssueType(listArgs.IssueType)
|
|
filter.IssueType = &issueType
|
|
}
|
|
if listArgs.Assignee != "" {
|
|
filter.Assignee = &listArgs.Assignee
|
|
}
|
|
if listArgs.Priority != nil {
|
|
filter.Priority = listArgs.Priority
|
|
}
|
|
|
|
// Normalize and apply label filters
|
|
labels := util.NormalizeLabels(listArgs.Labels)
|
|
labelsAny := util.NormalizeLabels(listArgs.LabelsAny)
|
|
// Support both old single Label and new Labels array (backward compat)
|
|
if len(labels) > 0 {
|
|
filter.Labels = labels
|
|
} else if listArgs.Label != "" {
|
|
filter.Labels = []string{strings.TrimSpace(listArgs.Label)}
|
|
}
|
|
if len(labelsAny) > 0 {
|
|
filter.LabelsAny = labelsAny
|
|
}
|
|
if len(listArgs.IDs) > 0 {
|
|
ids := util.NormalizeLabels(listArgs.IDs)
|
|
if len(ids) > 0 {
|
|
filter.IDs = ids
|
|
}
|
|
}
|
|
|
|
// Pattern matching
|
|
filter.TitleContains = listArgs.TitleContains
|
|
filter.DescriptionContains = listArgs.DescriptionContains
|
|
filter.NotesContains = listArgs.NotesContains
|
|
|
|
// Date ranges - use parseTimeRPC helper for flexible formats
|
|
if listArgs.CreatedAfter != "" {
|
|
t, err := parseTimeRPC(listArgs.CreatedAfter)
|
|
if err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("invalid --created-after date: %v", err),
|
|
}
|
|
}
|
|
filter.CreatedAfter = &t
|
|
}
|
|
if listArgs.CreatedBefore != "" {
|
|
t, err := parseTimeRPC(listArgs.CreatedBefore)
|
|
if err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("invalid --created-before date: %v", err),
|
|
}
|
|
}
|
|
filter.CreatedBefore = &t
|
|
}
|
|
if listArgs.UpdatedAfter != "" {
|
|
t, err := parseTimeRPC(listArgs.UpdatedAfter)
|
|
if err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("invalid --updated-after date: %v", err),
|
|
}
|
|
}
|
|
filter.UpdatedAfter = &t
|
|
}
|
|
if listArgs.UpdatedBefore != "" {
|
|
t, err := parseTimeRPC(listArgs.UpdatedBefore)
|
|
if err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("invalid --updated-before date: %v", err),
|
|
}
|
|
}
|
|
filter.UpdatedBefore = &t
|
|
}
|
|
if listArgs.ClosedAfter != "" {
|
|
t, err := parseTimeRPC(listArgs.ClosedAfter)
|
|
if err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("invalid --closed-after date: %v", err),
|
|
}
|
|
}
|
|
filter.ClosedAfter = &t
|
|
}
|
|
if listArgs.ClosedBefore != "" {
|
|
t, err := parseTimeRPC(listArgs.ClosedBefore)
|
|
if err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("invalid --closed-before date: %v", err),
|
|
}
|
|
}
|
|
filter.ClosedBefore = &t
|
|
}
|
|
|
|
// Empty/null checks
|
|
filter.EmptyDescription = listArgs.EmptyDescription
|
|
filter.NoAssignee = listArgs.NoAssignee
|
|
filter.NoLabels = listArgs.NoLabels
|
|
|
|
// Priority range
|
|
filter.PriorityMin = listArgs.PriorityMin
|
|
filter.PriorityMax = listArgs.PriorityMax
|
|
|
|
// Guard against excessive ID lists to avoid SQLite parameter limits
|
|
const maxIDs = 1000
|
|
if len(filter.IDs) > maxIDs {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("--id flag supports at most %d issue IDs, got %d", maxIDs, len(filter.IDs)),
|
|
}
|
|
}
|
|
|
|
ctx := s.reqCtx(req)
|
|
issues, err := store.SearchIssues(ctx, listArgs.Query, filter)
|
|
if err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to list issues: %v", err),
|
|
}
|
|
}
|
|
|
|
// Populate labels for each issue
|
|
for _, issue := range issues {
|
|
labels, _ := store.GetLabels(ctx, issue.ID)
|
|
issue.Labels = labels
|
|
}
|
|
|
|
// Get dependency counts in bulk (single query instead of N queries)
|
|
issueIDs := make([]string, len(issues))
|
|
for i, issue := range issues {
|
|
issueIDs[i] = issue.ID
|
|
}
|
|
depCounts, _ := store.GetDependencyCounts(ctx, issueIDs)
|
|
|
|
// Build response with counts
|
|
issuesWithCounts := make([]*types.IssueWithCounts, len(issues))
|
|
for i, issue := range issues {
|
|
counts := depCounts[issue.ID]
|
|
if counts == nil {
|
|
counts = &types.DependencyCounts{DependencyCount: 0, DependentCount: 0}
|
|
}
|
|
issuesWithCounts[i] = &types.IssueWithCounts{
|
|
Issue: issue,
|
|
DependencyCount: counts.DependencyCount,
|
|
DependentCount: counts.DependentCount,
|
|
}
|
|
}
|
|
|
|
data, _ := json.Marshal(issuesWithCounts)
|
|
return Response{
|
|
Success: true,
|
|
Data: data,
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleResolveID(req *Request) Response {
|
|
var args ResolveIDArgs
|
|
if err := json.Unmarshal(req.Args, &args); err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("invalid resolve_id args: %v", err),
|
|
}
|
|
}
|
|
|
|
if s.storage == nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)",
|
|
}
|
|
}
|
|
|
|
ctx := s.reqCtx(req)
|
|
resolvedID, err := utils.ResolvePartialID(ctx, s.storage, args.ID)
|
|
if err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to resolve ID: %v", err),
|
|
}
|
|
}
|
|
|
|
data, _ := json.Marshal(resolvedID)
|
|
return Response{
|
|
Success: true,
|
|
Data: data,
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleShow(req *Request) Response {
|
|
var showArgs ShowArgs
|
|
if err := json.Unmarshal(req.Args, &showArgs); err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("invalid show args: %v", err),
|
|
}
|
|
}
|
|
|
|
store := s.storage
|
|
if store == nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)",
|
|
}
|
|
}
|
|
|
|
ctx := s.reqCtx(req)
|
|
issue, err := store.GetIssue(ctx, showArgs.ID)
|
|
if err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to get issue: %v", err),
|
|
}
|
|
}
|
|
if issue == nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("issue not found: %s", showArgs.ID),
|
|
}
|
|
}
|
|
|
|
// Populate labels, dependencies (with metadata), and dependents (with metadata)
|
|
labels, _ := store.GetLabels(ctx, issue.ID)
|
|
|
|
// Get dependencies and dependents with metadata (including dependency type)
|
|
var deps []*types.IssueWithDependencyMetadata
|
|
var dependents []*types.IssueWithDependencyMetadata
|
|
if sqliteStore, ok := store.(*sqlite.SQLiteStorage); ok {
|
|
deps, _ = sqliteStore.GetDependenciesWithMetadata(ctx, issue.ID)
|
|
dependents, _ = sqliteStore.GetDependentsWithMetadata(ctx, issue.ID)
|
|
} else {
|
|
// Fallback for non-SQLite storage (won't have dependency type metadata)
|
|
regularDeps, _ := store.GetDependencies(ctx, issue.ID)
|
|
for _, d := range regularDeps {
|
|
deps = append(deps, &types.IssueWithDependencyMetadata{
|
|
Issue: *d,
|
|
DependencyType: types.DepBlocks, // default
|
|
})
|
|
}
|
|
regularDependents, _ := store.GetDependents(ctx, issue.ID)
|
|
for _, d := range regularDependents {
|
|
dependents = append(dependents, &types.IssueWithDependencyMetadata{
|
|
Issue: *d,
|
|
DependencyType: types.DepBlocks, // default
|
|
})
|
|
}
|
|
}
|
|
|
|
// Create detailed response with related data
|
|
type IssueDetails struct {
|
|
*types.Issue
|
|
Labels []string `json:"labels,omitempty"`
|
|
Dependencies []*types.IssueWithDependencyMetadata `json:"dependencies,omitempty"`
|
|
Dependents []*types.IssueWithDependencyMetadata `json:"dependents,omitempty"`
|
|
}
|
|
|
|
details := &IssueDetails{
|
|
Issue: issue,
|
|
Labels: labels,
|
|
Dependencies: deps,
|
|
Dependents: dependents,
|
|
}
|
|
|
|
data, _ := json.Marshal(details)
|
|
return Response{
|
|
Success: true,
|
|
Data: data,
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleReady(req *Request) Response {
|
|
var readyArgs ReadyArgs
|
|
if err := json.Unmarshal(req.Args, &readyArgs); err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("invalid ready args: %v", err),
|
|
}
|
|
}
|
|
|
|
store := s.storage
|
|
if store == nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)",
|
|
}
|
|
}
|
|
|
|
wf := types.WorkFilter{
|
|
Status: types.StatusOpen,
|
|
Priority: readyArgs.Priority,
|
|
Limit: readyArgs.Limit,
|
|
SortPolicy: types.SortPolicy(readyArgs.SortPolicy),
|
|
Labels: util.NormalizeLabels(readyArgs.Labels),
|
|
LabelsAny: util.NormalizeLabels(readyArgs.LabelsAny),
|
|
}
|
|
if readyArgs.Assignee != "" {
|
|
wf.Assignee = &readyArgs.Assignee
|
|
}
|
|
|
|
ctx := s.reqCtx(req)
|
|
issues, err := store.GetReadyWork(ctx, wf)
|
|
if err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to get ready work: %v", err),
|
|
}
|
|
}
|
|
|
|
data, _ := json.Marshal(issues)
|
|
return Response{
|
|
Success: true,
|
|
Data: data,
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleStale(req *Request) Response {
|
|
var staleArgs StaleArgs
|
|
if err := json.Unmarshal(req.Args, &staleArgs); err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("invalid stale args: %v", err),
|
|
}
|
|
}
|
|
|
|
store := s.storage
|
|
if store == nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)",
|
|
}
|
|
}
|
|
|
|
filter := types.StaleFilter{
|
|
Days: staleArgs.Days,
|
|
Status: staleArgs.Status,
|
|
Limit: staleArgs.Limit,
|
|
}
|
|
|
|
ctx := s.reqCtx(req)
|
|
issues, err := store.GetStaleIssues(ctx, filter)
|
|
if err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to get stale issues: %v", err),
|
|
}
|
|
}
|
|
|
|
data, _ := json.Marshal(issues)
|
|
return Response{
|
|
Success: true,
|
|
Data: data,
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleStats(req *Request) Response {
|
|
store := s.storage
|
|
if store == nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)",
|
|
}
|
|
}
|
|
|
|
ctx := s.reqCtx(req)
|
|
stats, err := store.GetStatistics(ctx)
|
|
if err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to get statistics: %v", err),
|
|
}
|
|
}
|
|
|
|
data, _ := json.Marshal(stats)
|
|
return Response{
|
|
Success: true,
|
|
Data: data,
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleEpicStatus(req *Request) Response {
|
|
var epicArgs EpicStatusArgs
|
|
if err := json.Unmarshal(req.Args, &epicArgs); err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("invalid epic status args: %v", err),
|
|
}
|
|
}
|
|
|
|
store := s.storage
|
|
if store == nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)",
|
|
}
|
|
}
|
|
|
|
ctx := s.reqCtx(req)
|
|
epics, err := store.GetEpicsEligibleForClosure(ctx)
|
|
if err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to get epic status: %v", err),
|
|
}
|
|
}
|
|
|
|
if epicArgs.EligibleOnly {
|
|
filtered := []*types.EpicStatus{}
|
|
for _, epic := range epics {
|
|
if epic.EligibleForClose {
|
|
filtered = append(filtered, epic)
|
|
}
|
|
}
|
|
epics = filtered
|
|
}
|
|
|
|
data, err := json.Marshal(epics)
|
|
if err != nil {
|
|
return Response{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to marshal epics: %v", err),
|
|
}
|
|
}
|
|
|
|
return Response{
|
|
Success: true,
|
|
Data: data,
|
|
}
|
|
}
|