From efdaa9378947efea2de9f817a08aaa28185dbdb5 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Thu, 25 Dec 2025 13:56:19 -0800 Subject: [PATCH] fix: Use FatalErrorRespectJSON across all commands (bd-28sq) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- cmd/bd/comments.go | 54 +++++++++----------------- cmd/bd/dep.go | 96 ++++++++++++++++------------------------------ cmd/bd/epic.go | 33 ++++++---------- cmd/bd/label.go | 34 ++++++---------- 4 files changed, 73 insertions(+), 144 deletions(-) diff --git a/cmd/bd/comments.go b/cmd/bd/comments.go index 8a166301..190e366e 100644 --- a/cmd/bd/comments.go +++ b/cmd/bd/comments.go @@ -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 diff --git a/cmd/bd/dep.go b/cmd/bd/dep.go index 50cef5f2..c2657bdb 100644 --- a/cmd/bd/dep.go +++ b/cmd/bd/dep.go @@ -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:: 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:: 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 { diff --git a/cmd/bd/epic.go b/cmd/bd/epic.go index 8fbc7cb0..6d074698 100644 --- a/cmd/bd/epic.go +++ b/cmd/bd/epic.go @@ -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)) diff --git a/cmd/bd/label.go b/cmd/bd/label.go index 8b19192f..30805f0e 100644 --- a/cmd/bd/label.go +++ b/cmd/bd/label.go @@ -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]++