From f2506088b694c6bce1ca0456b72f9614a884c9a3 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 23 Dec 2025 20:42:57 -0800 Subject: [PATCH] fix(json): audit and standardize JSON output across commands (bd-au0.7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit findings: - All commands properly respect --json flag for success output - Added outputJSONError() helper for consistent JSON error output - Removed redundant local --json flag from stale.go (inherited from rootCmd) - Fixed stale_test.go to check InheritedFlags() instead of local Flags() JSON output patterns verified across: - Query commands: ready, blocked, stale, count, stats, status - Dep commands: dep add/remove/tree/cycles - Label commands: label add/remove/list/list-all - Comment commands: comments add/list - Epic commands: epic status/close-eligible 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/bd/autoflush.go | 18 ++++++++++++++++++ cmd/bd/stale.go | 2 +- cmd/bd/stale_test.go | 5 +++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/cmd/bd/autoflush.go b/cmd/bd/autoflush.go index fddd5225..8d3902a9 100644 --- a/cmd/bd/autoflush.go +++ b/cmd/bd/autoflush.go @@ -33,6 +33,24 @@ func outputJSON(v interface{}) { } } +// outputJSONError outputs an error as JSON to stderr and exits with code 1. +// Use this when jsonOutput is true and an error occurs, to ensure consistent +// machine-readable error output. The error is formatted as: +// +// {"error": "error message", "code": "error_code"} +// +// The code parameter is optional (pass "" to omit). +func outputJSONError(err error, code string) { + errObj := map[string]string{"error": err.Error()} + if code != "" { + errObj["code"] = code + } + encoder := json.NewEncoder(os.Stderr) + encoder.SetIndent("", " ") + _ = encoder.Encode(errObj) + os.Exit(1) +} + // findJSONLPath finds the JSONL file path for the current database // findJSONLPath discovers the JSONL file path for the current database and ensures // the parent directory exists. Uses beads.FindJSONLPath() for discovery (checking diff --git a/cmd/bd/stale.go b/cmd/bd/stale.go index 1b1abcb7..5fca2598 100644 --- a/cmd/bd/stale.go +++ b/cmd/bd/stale.go @@ -110,6 +110,6 @@ func init() { staleCmd.Flags().IntP("days", "d", 30, "Issues not updated in this many days") staleCmd.Flags().StringP("status", "s", "", "Filter by status (open|in_progress|blocked|deferred)") staleCmd.Flags().IntP("limit", "n", 50, "Maximum issues to show") - staleCmd.Flags().BoolVar(&jsonOutput, "json", false, "Output JSON format") + // Note: --json flag is defined as a persistent flag in main.go, not here rootCmd.AddCommand(staleCmd) } diff --git a/cmd/bd/stale_test.go b/cmd/bd/stale_test.go index b2577c08..4722e1e4 100644 --- a/cmd/bd/stale_test.go +++ b/cmd/bd/stale_test.go @@ -403,7 +403,8 @@ func TestStaleCommandInit(t *testing.T) { if flags.Lookup("limit") == nil { t.Error("staleCmd should have --limit flag") } - if flags.Lookup("json") == nil { - t.Error("staleCmd should have --json flag") + // --json is inherited from rootCmd as a persistent flag + if staleCmd.InheritedFlags().Lookup("json") == nil { + t.Error("staleCmd should inherit --json flag from rootCmd") } }