fix: Use FatalErrorRespectJSON across all commands (bd-28sq)

Convert all fmt.Fprintf(os.Stderr, ...) + os.Exit(1) patterns to use
FatalErrorRespectJSON for consistent JSON error output:
- dep.go: dependency commands (add, remove, tree, cycles)
- label.go: label commands (add, remove, list, list-all)
- comments.go: comment commands (list, add)
- epic.go: epic commands (status, close-eligible)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-25 13:56:19 -08:00
parent f56f3615e8
commit efdaa93789
4 changed files with 73 additions and 144 deletions

View File

@@ -42,17 +42,14 @@ Examples:
if err != nil {
if isUnknownOperationError(err) {
if err := fallbackToDirectMode("daemon does not support comment_list RPC"); err != nil {
fmt.Fprintf(os.Stderr, "Error getting comments: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("getting comments: %v", err)
}
} else {
fmt.Fprintf(os.Stderr, "Error getting comments: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("getting comments: %v", err)
}
} else {
if err := json.Unmarshal(resp.Data, &comments); err != nil {
fmt.Fprintf(os.Stderr, "Error decoding comments: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("decoding comments: %v", err)
}
usedDaemon = true
}
@@ -60,21 +57,18 @@ Examples:
if !usedDaemon {
if err := ensureStoreActive(); err != nil {
fmt.Fprintf(os.Stderr, "Error getting comments: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("getting comments: %v", err)
}
ctx := rootCtx
fullID, err := utils.ResolvePartialID(ctx, store, issueID)
if err != nil {
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", issueID, err)
os.Exit(1)
FatalErrorRespectJSON("resolving %s: %v", issueID, err)
}
issueID = fullID
result, err := store.GetIssueComments(ctx, issueID)
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting comments: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("getting comments: %v", err)
}
comments = result
}
@@ -87,8 +81,7 @@ Examples:
if jsonOutput {
data, err := json.MarshalIndent(comments, "", " ")
if err != nil {
fmt.Fprintf(os.Stderr, "Error encoding JSON: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("encoding JSON: %v", err)
}
fmt.Println(string(data))
return
@@ -130,13 +123,11 @@ Examples:
// Read from file
data, err := os.ReadFile(commentText) // #nosec G304 - user-provided file path is intentional
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading file: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("reading file: %v", err)
}
commentText = string(data)
} else if len(args) < 2 {
fmt.Fprintf(os.Stderr, "Error: comment text required (use -f to read from file)\n")
os.Exit(1)
FatalErrorRespectJSON("comment text required (use -f to read from file)")
} else {
commentText = args[1]
}
@@ -167,18 +158,15 @@ Examples:
if err != nil {
if isUnknownOperationError(err) {
if err := fallbackToDirectMode("daemon does not support comment_add RPC"); err != nil {
fmt.Fprintf(os.Stderr, "Error adding comment: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("adding comment: %v", err)
}
} else {
fmt.Fprintf(os.Stderr, "Error adding comment: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("adding comment: %v", err)
}
} else {
var parsed types.Comment
if err := json.Unmarshal(resp.Data, &parsed); err != nil {
fmt.Fprintf(os.Stderr, "Error decoding comment: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("decoding comment: %v", err)
}
comment = &parsed
}
@@ -186,30 +174,26 @@ Examples:
if comment == nil {
if err := ensureStoreActive(); err != nil {
fmt.Fprintf(os.Stderr, "Error adding comment: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("adding comment: %v", err)
}
ctx := rootCtx
fullID, err := utils.ResolvePartialID(ctx, store, issueID)
if err != nil {
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", issueID, err)
os.Exit(1)
FatalErrorRespectJSON("resolving %s: %v", issueID, err)
}
issueID = fullID
comment, err = store.AddIssueComment(ctx, issueID, author, commentText)
if err != nil {
fmt.Fprintf(os.Stderr, "Error adding comment: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("adding comment: %v", err)
}
}
if jsonOutput {
data, err := json.MarshalIndent(comment, "", " ")
if err != nil {
fmt.Fprintf(os.Stderr, "Error encoding JSON: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("encoding JSON: %v", err)
}
fmt.Println(string(data))
return

View File

@@ -71,12 +71,10 @@ Examples:
resolveArgs := &rpc.ResolveIDArgs{ID: args[0]}
resp, err := daemonClient.ResolveID(resolveArgs)
if err != nil {
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
os.Exit(1)
FatalErrorRespectJSON("resolving issue ID %s: %v", args[0], err)
}
if err := json.Unmarshal(resp.Data, &fromID); err != nil {
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("unmarshaling resolved ID: %v", err)
}
if isExternalRef {
@@ -84,27 +82,23 @@ Examples:
toID = args[1]
// Validate format: external:<project>:<capability>
if err := validateExternalRef(toID); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("%v", err)
}
} else {
resolveArgs = &rpc.ResolveIDArgs{ID: args[1]}
resp, err = daemonClient.ResolveID(resolveArgs)
if err != nil {
fmt.Fprintf(os.Stderr, "Error resolving dependency ID %s: %v\n", args[1], err)
os.Exit(1)
FatalErrorRespectJSON("resolving dependency ID %s: %v", args[1], err)
}
if err := json.Unmarshal(resp.Data, &toID); err != nil {
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("unmarshaling resolved ID: %v", err)
}
}
} else {
var err error
fromID, err = utils.ResolvePartialID(ctx, store, args[0])
if err != nil {
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
os.Exit(1)
FatalErrorRespectJSON("resolving issue ID %s: %v", args[0], err)
}
if isExternalRef {
@@ -112,14 +106,12 @@ Examples:
toID = args[1]
// Validate format: external:<project>:<capability>
if err := validateExternalRef(toID); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("%v", err)
}
} else {
toID, err = utils.ResolvePartialID(ctx, store, args[1])
if err != nil {
fmt.Fprintf(os.Stderr, "Error resolving dependency ID %s: %v\n", args[1], err)
os.Exit(1)
FatalErrorRespectJSON("resolving dependency ID %s: %v", args[1], err)
}
}
}
@@ -127,10 +119,7 @@ Examples:
// Check for child→parent dependency anti-pattern (bd-nim5)
// This creates a deadlock: child can't start (parent open), parent can't close (children not done)
if isChildOf(fromID, toID) {
fmt.Fprintf(os.Stderr, "Error: Cannot add dependency: %s is already a child of %s.\n", fromID, toID)
fmt.Fprintf(os.Stderr, "Children inherit dependency on parent completion via hierarchy.\n")
fmt.Fprintf(os.Stderr, "Adding an explicit dependency would create a deadlock.\n")
os.Exit(1)
FatalErrorRespectJSON("cannot add dependency: %s is already a child of %s. Children inherit dependency on parent completion via hierarchy. Adding an explicit dependency would create a deadlock", fromID, toID)
}
// If daemon is running, use RPC
@@ -143,8 +132,7 @@ Examples:
resp, err := daemonClient.AddDependency(depArgs)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("%v", err)
}
if jsonOutput {
@@ -165,8 +153,7 @@ Examples:
}
if err := store.AddDependency(ctx, dep, actor); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("%v", err)
}
// Schedule auto-flush
@@ -225,36 +212,30 @@ var depRemoveCmd = &cobra.Command{
resolveArgs := &rpc.ResolveIDArgs{ID: args[0]}
resp, err := daemonClient.ResolveID(resolveArgs)
if err != nil {
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
os.Exit(1)
FatalErrorRespectJSON("resolving issue ID %s: %v", args[0], err)
}
if err := json.Unmarshal(resp.Data, &fromID); err != nil {
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("unmarshaling resolved ID: %v", err)
}
resolveArgs = &rpc.ResolveIDArgs{ID: args[1]}
resp, err = daemonClient.ResolveID(resolveArgs)
if err != nil {
fmt.Fprintf(os.Stderr, "Error resolving dependency ID %s: %v\n", args[1], err)
os.Exit(1)
FatalErrorRespectJSON("resolving dependency ID %s: %v", args[1], err)
}
if err := json.Unmarshal(resp.Data, &toID); err != nil {
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("unmarshaling resolved ID: %v", err)
}
} else {
var err error
fromID, err = utils.ResolvePartialID(ctx, store, args[0])
if err != nil {
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
os.Exit(1)
FatalErrorRespectJSON("resolving issue ID %s: %v", args[0], err)
}
toID, err = utils.ResolvePartialID(ctx, store, args[1])
if err != nil {
fmt.Fprintf(os.Stderr, "Error resolving dependency ID %s: %v\n", args[1], err)
os.Exit(1)
FatalErrorRespectJSON("resolving dependency ID %s: %v", args[1], err)
}
}
@@ -267,8 +248,7 @@ var depRemoveCmd = &cobra.Command{
resp, err := daemonClient.RemoveDependency(depArgs)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("%v", err)
}
if jsonOutput {
@@ -286,8 +266,7 @@ var depRemoveCmd = &cobra.Command{
fullToID := toID
if err := store.RemoveDependency(ctx, fullFromID, fullToID, actor); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("%v", err)
}
// Schedule auto-flush
@@ -332,19 +311,16 @@ Examples:
resolveArgs := &rpc.ResolveIDArgs{ID: args[0]}
resp, err := daemonClient.ResolveID(resolveArgs)
if err != nil {
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
os.Exit(1)
FatalErrorRespectJSON("resolving issue ID %s: %v", args[0], err)
}
if err := json.Unmarshal(resp.Data, &fullID); err != nil {
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("unmarshaling resolved ID: %v", err)
}
} else {
var err error
fullID, err = utils.ResolvePartialID(ctx, store, args[0])
if err != nil {
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", args[0], err)
os.Exit(1)
FatalErrorRespectJSON("resolving %s: %v", args[0], err)
}
}
@@ -353,8 +329,7 @@ Examples:
var err error
store, err = sqlite.New(rootCtx, dbPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to open database: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("failed to open database: %v", err)
}
defer func() { _ = store.Close() }()
}
@@ -375,13 +350,11 @@ Examples:
// Validate direction
if direction != "down" && direction != "up" && direction != "both" {
fmt.Fprintf(os.Stderr, "Error: --direction must be 'down', 'up', or 'both'\n")
os.Exit(1)
FatalErrorRespectJSON("--direction must be 'down', 'up', or 'both'")
}
if maxDepth < 1 {
fmt.Fprintf(os.Stderr, "Error: --max-depth must be >= 1\n")
os.Exit(1)
FatalErrorRespectJSON("--max-depth must be >= 1")
}
// For "both" direction, we need to fetch both trees and merge them
@@ -392,15 +365,13 @@ Examples:
// Get dependencies (down) - what blocks this issue
downTree, err := store.GetDependencyTree(ctx, fullID, maxDepth, showAllPaths, false)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("%v", err)
}
// Get dependents (up) - what this issue blocks
upTree, err := store.GetDependencyTree(ctx, fullID, maxDepth, showAllPaths, true)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("%v", err)
}
// Merge: root appears once, dependencies below, dependents above
@@ -410,8 +381,7 @@ Examples:
} else {
tree, err = store.GetDependencyTree(ctx, fullID, maxDepth, showAllPaths, direction == "up")
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("%v", err)
}
}
@@ -471,8 +441,7 @@ var depCyclesCmd = &cobra.Command{
var err error
store, err = sqlite.New(rootCtx, dbPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to open database: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("failed to open database: %v", err)
}
defer func() { _ = store.Close() }()
}
@@ -480,8 +449,7 @@ var depCyclesCmd = &cobra.Command{
ctx := rootCtx
cycles, err := store.DetectCycles(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("%v", err)
}
if jsonOutput {

View File

@@ -26,23 +26,19 @@ var epicStatusCmd = &cobra.Command{
EligibleOnly: eligibleOnly,
})
if err != nil {
fmt.Fprintf(os.Stderr, "Error communicating with daemon: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("communicating with daemon: %v", err)
}
if !resp.Success {
fmt.Fprintf(os.Stderr, "Error getting epic status: %s\n", resp.Error)
os.Exit(1)
FatalErrorRespectJSON("getting epic status: %s", resp.Error)
}
if err := json.Unmarshal(resp.Data, &epics); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("parsing response: %v", err)
}
} else {
ctx := rootCtx
epics, err = store.GetEpicsEligibleForClosure(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting epic status: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("getting epic status: %v", err)
}
if eligibleOnly {
filtered := []*types.EpicStatus{}
@@ -58,8 +54,7 @@ var epicStatusCmd = &cobra.Command{
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(epics); err != nil {
fmt.Fprintf(os.Stderr, "Error encoding JSON: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("encoding JSON: %v", err)
}
return
}
@@ -108,23 +103,19 @@ var closeEligibleEpicsCmd = &cobra.Command{
EligibleOnly: true,
})
if err != nil {
fmt.Fprintf(os.Stderr, "Error communicating with daemon: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("communicating with daemon: %v", err)
}
if !resp.Success {
fmt.Fprintf(os.Stderr, "Error getting eligible epics: %s\n", resp.Error)
os.Exit(1)
FatalErrorRespectJSON("getting eligible epics: %s", resp.Error)
}
if err := json.Unmarshal(resp.Data, &eligibleEpics); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("parsing response: %v", err)
}
} else {
ctx := rootCtx
epics, err := store.GetEpicsEligibleForClosure(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting eligible epics: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("getting eligible epics: %v", err)
}
for _, epic := range epics {
if epic.EligibleForClose {
@@ -145,8 +136,7 @@ var closeEligibleEpicsCmd = &cobra.Command{
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(eligibleEpics); err != nil {
fmt.Fprintf(os.Stderr, "Error encoding JSON: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("encoding JSON: %v", err)
}
} else {
fmt.Printf("Would close %d epic(s):\n", len(eligibleEpics))
@@ -191,8 +181,7 @@ var closeEligibleEpicsCmd = &cobra.Command{
"closed": closedIDs,
"count": len(closedIDs),
}); err != nil {
fmt.Fprintf(os.Stderr, "Error encoding JSON: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("encoding JSON: %v", err)
}
} else {
fmt.Printf("✓ Closed %d epic(s)\n", len(closedIDs))

View File

@@ -102,9 +102,7 @@ var labelAddCmd = &cobra.Command{
// Protect reserved label namespaces (bd-eijl)
// provides:* labels can only be added via 'bd ship' command
if strings.HasPrefix(label, "provides:") {
fmt.Fprintf(os.Stderr, "Error: 'provides:' labels are reserved for cross-project capabilities\n")
fmt.Fprintf(os.Stderr, "Hint: use 'bd ship %s' instead\n", strings.TrimPrefix(label, "provides:"))
os.Exit(1)
FatalErrorRespectJSON("'provides:' labels are reserved for cross-project capabilities. Hint: use 'bd ship %s' instead", strings.TrimPrefix(label, "provides:"))
}
processBatchLabelOperation(issueIDs, label, "added", jsonOutput,
@@ -176,19 +174,16 @@ var labelListCmd = &cobra.Command{
resolveArgs := &rpc.ResolveIDArgs{ID: args[0]}
resp, err := daemonClient.ResolveID(resolveArgs)
if err != nil {
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
os.Exit(1)
FatalErrorRespectJSON("resolving issue ID %s: %v", args[0], err)
}
if err := json.Unmarshal(resp.Data, &issueID); err != nil {
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("unmarshaling resolved ID: %v", err)
}
} else {
var err error
issueID, err = utils.ResolvePartialID(ctx, store, args[0])
if err != nil {
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", args[0], err)
os.Exit(1)
FatalErrorRespectJSON("resolving %s: %v", args[0], err)
}
}
var labels []string
@@ -196,13 +191,11 @@ var labelListCmd = &cobra.Command{
if daemonClient != nil {
resp, err := daemonClient.Show(&rpc.ShowArgs{ID: issueID})
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("%v", err)
}
var issue types.Issue
if err := json.Unmarshal(resp.Data, &issue); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("parsing response: %v", err)
}
labels = issue.Labels
} else {
@@ -210,8 +203,7 @@ var labelListCmd = &cobra.Command{
var err error
labels, err = store.GetLabels(ctx, issueID)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("%v", err)
}
}
if jsonOutput {
@@ -245,19 +237,16 @@ var labelListAllCmd = &cobra.Command{
if daemonClient != nil {
resp, err := daemonClient.List(&rpc.ListArgs{})
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("%v", err)
}
if err := json.Unmarshal(resp.Data, &issues); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("parsing response: %v", err)
}
} else {
// Direct mode
issues, err = store.SearchIssues(ctx, "", types.IssueFilter{})
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
FatalErrorRespectJSON("%v", err)
}
}
// Collect unique labels with counts
@@ -272,8 +261,7 @@ var labelListAllCmd = &cobra.Command{
// Direct mode - need to fetch labels
labels, err := store.GetLabels(ctx, issue.ID)
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting labels for %s: %v\n", issue.ID, err)
os.Exit(1)
FatalErrorRespectJSON("getting labels for %s: %v", issue.ID, err)
}
for _, label := range labels {
labelCounts[label]++