fix(json): standardize JSON output for errors and empty arrays (bd-au0.7)

- Add FatalErrorRespectJSON helper for JSON-aware error output
- Fix bd comments list returning null instead of [] for empty arrays
- Remove redundant local --json flags from show/update/close commands
  that were shadowing the global persistent --json flag
- Update show command error handlers to use FatalErrorRespectJSON

🤖 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-23 23:53:00 -08:00
parent 12010b25e5
commit 422ec718e7
4 changed files with 556 additions and 539 deletions

File diff suppressed because one or more lines are too long

View File

@@ -35,7 +35,7 @@ Examples:
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
issueID := args[0] issueID := args[0]
var comments []*types.Comment comments := make([]*types.Comment, 0)
usedDaemon := false usedDaemon := false
if daemonClient != nil { if daemonClient != nil {
resp, err := daemonClient.ListComments(&rpc.CommentListArgs{ID: issueID}) resp, err := daemonClient.ListComments(&rpc.CommentListArgs{ID: issueID})
@@ -79,6 +79,11 @@ Examples:
comments = result comments = result
} }
// Normalize nil to empty slice for consistent JSON output
if comments == nil {
comments = make([]*types.Comment, 0)
}
if jsonOutput { if jsonOutput {
data, err := json.MarshalIndent(comments, "", " ") data, err := json.MarshalIndent(comments, "", " ")
if err != nil { if err != nil {

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"os" "os"
) )
@@ -23,6 +24,28 @@ func FatalError(format string, args ...interface{}) {
os.Exit(1) os.Exit(1)
} }
// FatalErrorRespectJSON writes an error message and exits with code 1.
// If --json flag is set, outputs structured JSON to stdout.
// Otherwise, outputs plain text to stderr.
//
// Use this for errors in commands that support --json output.
//
// Example:
//
// if err := store.GetIssue(ctx, id); err != nil {
// FatalErrorRespectJSON("%v", err)
// }
func FatalErrorRespectJSON(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
if jsonOutput {
data, _ := json.MarshalIndent(map[string]string{"error": msg}, "", " ")
fmt.Println(string(data))
} else {
fmt.Fprintf(os.Stderr, "Error: %s\n", msg)
}
os.Exit(1)
}
// FatalErrorWithHint writes an error message with a hint to stderr and exits. // FatalErrorWithHint writes an error message with a hint to stderr and exits.
// Use this when you can provide an actionable suggestion to fix the error. // Use this when you can provide an actionable suggestion to fix the error.
// //

View File

@@ -26,7 +26,6 @@ var showCmd = &cobra.Command{
Short: "Show issue details", Short: "Show issue details",
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
jsonOutput, _ := cmd.Flags().GetBool("json")
showThread, _ := cmd.Flags().GetBool("thread") showThread, _ := cmd.Flags().GetBool("thread")
ctx := rootCtx ctx := rootCtx
@@ -34,8 +33,7 @@ var showCmd = &cobra.Command{
// Skip check when using daemon (daemon auto-imports on staleness) // Skip check when using daemon (daemon auto-imports on staleness)
if daemonClient == nil { if daemonClient == nil {
if err := ensureDatabaseFresh(ctx); err != nil { if err := ensureDatabaseFresh(ctx); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err) FatalErrorRespectJSON("%v", err)
os.Exit(1)
} }
} }
@@ -47,13 +45,11 @@ var showCmd = &cobra.Command{
resolveArgs := &rpc.ResolveIDArgs{ID: id} resolveArgs := &rpc.ResolveIDArgs{ID: id}
resp, err := daemonClient.ResolveID(resolveArgs) resp, err := daemonClient.ResolveID(resolveArgs)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error resolving ID %s: %v\n", id, err) FatalErrorRespectJSON("resolving ID %s: %v", id, err)
os.Exit(1)
} }
var resolvedID string var resolvedID string
if err := json.Unmarshal(resp.Data, &resolvedID); err != nil { if err := json.Unmarshal(resp.Data, &resolvedID); err != nil {
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err) FatalErrorRespectJSON("unmarshaling resolved ID: %v", err)
os.Exit(1)
} }
resolvedIDs = append(resolvedIDs, resolvedID) resolvedIDs = append(resolvedIDs, resolvedID)
} }
@@ -62,8 +58,7 @@ var showCmd = &cobra.Command{
var err error var err error
resolvedIDs, err = utils.ResolvePartialIDs(ctx, store, args) resolvedIDs, err = utils.ResolvePartialIDs(ctx, store, args)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err) FatalErrorRespectJSON("%v", err)
os.Exit(1)
} }
} }
@@ -466,7 +461,6 @@ var updateCmd = &cobra.Command{
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
CheckReadonly("update") CheckReadonly("update")
jsonOutput, _ := cmd.Flags().GetBool("json")
updates := make(map[string]interface{}) updates := make(map[string]interface{})
if cmd.Flags().Changed("status") { if cmd.Flags().Changed("status") {
@@ -975,7 +969,6 @@ var closeCmd = &cobra.Command{
if reason == "" { if reason == "" {
reason = "Closed" reason = "Closed"
} }
jsonOutput, _ := cmd.Flags().GetBool("json")
force, _ := cmd.Flags().GetBool("force") force, _ := cmd.Flags().GetBool("force")
continueFlag, _ := cmd.Flags().GetBool("continue") continueFlag, _ := cmd.Flags().GetBool("continue")
noAuto, _ := cmd.Flags().GetBool("no-auto") noAuto, _ := cmd.Flags().GetBool("no-auto")
@@ -1383,7 +1376,6 @@ func findReplies(ctx context.Context, issueID string, daemonClient *rpc.Client,
} }
func init() { func init() {
showCmd.Flags().Bool("json", false, "Output JSON format")
showCmd.Flags().Bool("thread", false, "Show full conversation thread (for messages)") showCmd.Flags().Bool("thread", false, "Show full conversation thread (for messages)")
rootCmd.AddCommand(showCmd) rootCmd.AddCommand(showCmd)
@@ -1399,8 +1391,6 @@ func init() {
updateCmd.Flags().StringSlice("add-label", nil, "Add labels (repeatable)") updateCmd.Flags().StringSlice("add-label", nil, "Add labels (repeatable)")
updateCmd.Flags().StringSlice("remove-label", nil, "Remove labels (repeatable)") updateCmd.Flags().StringSlice("remove-label", nil, "Remove labels (repeatable)")
updateCmd.Flags().StringSlice("set-labels", nil, "Set labels, replacing all existing (repeatable)") updateCmd.Flags().StringSlice("set-labels", nil, "Set labels, replacing all existing (repeatable)")
updateCmd.Flags().Bool("json", false, "Output JSON format")
rootCmd.AddCommand(updateCmd) rootCmd.AddCommand(updateCmd)
editCmd.Flags().Bool("title", false, "Edit the title") editCmd.Flags().Bool("title", false, "Edit the title")
@@ -1411,7 +1401,6 @@ func init() {
rootCmd.AddCommand(editCmd) rootCmd.AddCommand(editCmd)
closeCmd.Flags().StringP("reason", "r", "", "Reason for closing") closeCmd.Flags().StringP("reason", "r", "", "Reason for closing")
closeCmd.Flags().Bool("json", false, "Output JSON format")
closeCmd.Flags().BoolP("force", "f", false, "Force close pinned issues") closeCmd.Flags().BoolP("force", "f", false, "Force close pinned issues")
closeCmd.Flags().Bool("continue", false, "Auto-advance to next step in molecule") closeCmd.Flags().Bool("continue", false, "Auto-advance to next step in molecule")
closeCmd.Flags().Bool("no-auto", false, "With --continue, show next step but don't claim it") closeCmd.Flags().Bool("no-auto", false, "With --continue, show next step but don't claim it")