Fix --json flag shadowing issue causing test failures

Fixed TestHashIDs_IdenticalContentDedup test failure by removing duplicate
--json flag definitions that were shadowing the global persistent flag.

Root cause: Commands had both a persistent --json flag (main.go) and local
--json flags (in individual command files). The local flags shadowed the
persistent flag, preventing jsonOutput variable from being set correctly.

Changes:
- Removed 31 duplicate --json flag definitions from 15 command files
- All commands now use the single persistent --json flag from main.go
- Commands now correctly output JSON when --json flag is specified

Test results:
- TestHashIDs_IdenticalContentDedup: Now passes (was failing)
- TestHashIDs_MultiCloneConverge: Passes without JSON parsing warnings
- All other tests: Pass with no regressions

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-11-02 18:52:44 -08:00
parent edf1f71fa7
commit e5f1e4b971
15 changed files with 7 additions and 588 deletions

View File

@@ -1,5 +1,4 @@
package main
import (
"bufio"
"encoding/json"
@@ -11,16 +10,13 @@ import (
"strings"
"text/tabwriter"
"time"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/daemon"
)
var daemonsCmd = &cobra.Command{
Use: "daemons",
Short: "Manage multiple bd daemons",
Long: `Manage bd daemon processes across all repositories and worktrees.
Subcommands:
list - Show all running daemons
health - Check health of all daemons
@@ -29,7 +25,6 @@ Subcommands:
killall - Stop all running daemons
restart - Restart a specific daemon (not yet implemented)`,
}
var daemonsListCmd = &cobra.Command{
Use: "list",
Short: "List all running bd daemons",
@@ -38,14 +33,12 @@ uptime, last activity, and exclusive lock status.`,
Run: func(cmd *cobra.Command, args []string) {
searchRoots, _ := cmd.Flags().GetStringSlice("search")
// Use global jsonOutput set by PersistentPreRun
// Discover daemons
daemons, err := daemon.DiscoverDaemons(searchRoots)
if err != nil {
fmt.Fprintf(os.Stderr, "Error discovering daemons: %v\n", err)
os.Exit(1)
}
// Auto-cleanup stale sockets (unless --no-cleanup flag is set)
noCleanup, _ := cmd.Flags().GetBool("no-cleanup")
if !noCleanup {
@@ -56,7 +49,6 @@ uptime, last activity, and exclusive lock status.`,
fmt.Fprintf(os.Stderr, "Cleaned up %d stale socket(s)\n", cleaned)
}
}
// Filter to only alive daemons
var aliveDaemons []daemon.DaemonInfo
for _, d := range daemons {
@@ -64,50 +56,40 @@ uptime, last activity, and exclusive lock status.`,
aliveDaemons = append(aliveDaemons, d)
}
}
if jsonOutput {
data, _ := json.MarshalIndent(aliveDaemons, "", " ")
fmt.Println(string(data))
return
}
// Human-readable table output
if len(aliveDaemons) == 0 {
fmt.Println("No running daemons found")
return
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintln(w, "WORKSPACE\tPID\tVERSION\tUPTIME\tLAST ACTIVITY\tLOCK")
for _, d := range aliveDaemons {
workspace := d.WorkspacePath
if workspace == "" {
workspace = "(unknown)"
}
uptime := formatDaemonDuration(d.UptimeSeconds)
lastActivity := "(unknown)"
if d.LastActivityTime != "" {
if t, err := time.Parse(time.RFC3339, d.LastActivityTime); err == nil {
lastActivity = formatDaemonRelativeTime(t)
}
}
lock := "-"
if d.ExclusiveLockActive {
lock = fmt.Sprintf("🔒 %s", d.ExclusiveLockHolder)
}
_, _ = fmt.Fprintf(w, "%s\t%d\t%s\t%s\t%s\t%s\n",
workspace, d.PID, d.Version, uptime, lastActivity, lock)
}
_ = w.Flush()
},
}
func formatDaemonDuration(seconds float64) string {
d := time.Duration(seconds * float64(time.Second))
if d < time.Minute {
@@ -119,7 +101,6 @@ func formatDaemonDuration(seconds float64) string {
}
return fmt.Sprintf("%.1fd", d.Hours()/24)
}
func formatDaemonRelativeTime(t time.Time) string {
d := time.Since(t)
if d < time.Minute {
@@ -131,7 +112,6 @@ func formatDaemonRelativeTime(t time.Time) string {
}
return fmt.Sprintf("%.1fd ago", d.Hours()/24)
}
var daemonsStopCmd = &cobra.Command{
Use: "stop <workspace-path|pid>",
Short: "Stop a specific bd daemon",
@@ -141,14 +121,12 @@ Sends shutdown command via RPC, with SIGTERM fallback if RPC fails.`,
Run: func(cmd *cobra.Command, args []string) {
target := args[0]
// Use global jsonOutput set by PersistentPreRun
// Discover all daemons
daemons, err := daemon.DiscoverDaemons(nil)
if err != nil {
fmt.Fprintf(os.Stderr, "Error discovering daemons: %v\n", err)
os.Exit(1)
}
// Find matching daemon by workspace path or PID
var targetDaemon *daemon.DaemonInfo
for _, d := range daemons {
@@ -157,7 +135,6 @@ Sends shutdown command via RPC, with SIGTERM fallback if RPC fails.`,
break
}
}
if targetDaemon == nil {
if jsonOutput {
outputJSON(map[string]string{"error": "daemon not found"})
@@ -166,7 +143,6 @@ Sends shutdown command via RPC, with SIGTERM fallback if RPC fails.`,
}
os.Exit(1)
}
// Stop the daemon
if err := daemon.StopDaemon(*targetDaemon); err != nil {
if jsonOutput {
@@ -176,7 +152,6 @@ Sends shutdown command via RPC, with SIGTERM fallback if RPC fails.`,
}
os.Exit(1)
}
if jsonOutput {
outputJSON(map[string]interface{}{
"workspace": targetDaemon.WorkspacePath,
@@ -188,7 +163,6 @@ Sends shutdown command via RPC, with SIGTERM fallback if RPC fails.`,
}
},
}
var daemonsRestartCmd = &cobra.Command{
Use: "restart <workspace-path|pid>",
Short: "Restart a specific bd daemon",
@@ -199,14 +173,12 @@ Stops the daemon gracefully, then starts a new one.`,
target := args[0]
searchRoots, _ := cmd.Flags().GetStringSlice("search")
// Use global jsonOutput set by PersistentPreRun
// Discover daemons
daemons, err := daemon.DiscoverDaemons(searchRoots)
if err != nil {
fmt.Fprintf(os.Stderr, "Error discovering daemons: %v\n", err)
os.Exit(1)
}
// Find the target daemon
var targetDaemon *daemon.DaemonInfo
for _, d := range daemons {
@@ -215,7 +187,6 @@ Stops the daemon gracefully, then starts a new one.`,
break
}
}
if targetDaemon == nil {
if jsonOutput {
outputJSON(map[string]string{"error": "daemon not found"})
@@ -224,9 +195,7 @@ Stops the daemon gracefully, then starts a new one.`,
}
os.Exit(1)
}
workspace := targetDaemon.WorkspacePath
// Stop the daemon
if !jsonOutput {
fmt.Printf("Stopping daemon for workspace: %s (PID %d)\n", workspace, targetDaemon.PID)
@@ -239,15 +208,12 @@ Stops the daemon gracefully, then starts a new one.`,
}
os.Exit(1)
}
// Wait a moment for cleanup
time.Sleep(500 * time.Millisecond)
// Start a new daemon by executing 'bd daemon' in the workspace directory
if !jsonOutput {
fmt.Printf("Starting new daemon for workspace: %s\n", workspace)
}
exe, err := os.Executable()
if err != nil {
if jsonOutput {
@@ -257,17 +223,14 @@ Stops the daemon gracefully, then starts a new one.`,
}
os.Exit(1)
}
// Check if workspace-local bd binary exists (preferred)
localBd := filepath.Join(workspace, "bd")
_, localErr := os.Stat(localBd)
bdPath := exe
if localErr == nil {
// Use local bd binary if it exists
bdPath = localBd
}
// Use bd daemon command with proper working directory
// The daemon will fork itself into the background
daemonCmd := &exec.Cmd{
@@ -276,7 +239,6 @@ Stops the daemon gracefully, then starts a new one.`,
Dir: workspace,
Env: os.Environ(),
}
if err := daemonCmd.Start(); err != nil {
if jsonOutput {
outputJSON(map[string]string{"error": fmt.Sprintf("failed to start daemon: %v", err)})
@@ -285,10 +247,8 @@ Stops the daemon gracefully, then starts a new one.`,
}
os.Exit(1)
}
// Don't wait for daemon to exit (it will fork and continue in background)
go func() { _ = daemonCmd.Wait() }()
if jsonOutput {
outputJSON(map[string]interface{}{
"workspace": workspace,
@@ -299,7 +259,6 @@ Stops the daemon gracefully, then starts a new one.`,
}
},
}
var daemonsLogsCmd = &cobra.Command{
Use: "logs <workspace-path|pid>",
Short: "View logs for a specific bd daemon",
@@ -311,7 +270,6 @@ Supports tail mode (last N lines) and follow mode (like tail -f).`,
// Use global jsonOutput set by PersistentPreRun
follow, _ := cmd.Flags().GetBool("follow")
lines, _ := cmd.Flags().GetInt("lines")
// Discover all daemons
daemons, err := daemon.DiscoverDaemons(nil)
if err != nil {
@@ -322,7 +280,6 @@ Supports tail mode (last N lines) and follow mode (like tail -f).`,
}
os.Exit(1)
}
// Find matching daemon by workspace path or PID
var targetDaemon *daemon.DaemonInfo
for _, d := range daemons {
@@ -331,7 +288,6 @@ Supports tail mode (last N lines) and follow mode (like tail -f).`,
break
}
}
if targetDaemon == nil {
if jsonOutput {
outputJSON(map[string]string{"error": "daemon not found"})
@@ -340,10 +296,8 @@ Supports tail mode (last N lines) and follow mode (like tail -f).`,
}
os.Exit(1)
}
// Determine log file path
logPath := filepath.Join(filepath.Dir(targetDaemon.SocketPath), "daemon.log")
// Check if log file exists
if _, err := os.Stat(logPath); err != nil {
if jsonOutput {
@@ -353,7 +307,6 @@ Supports tail mode (last N lines) and follow mode (like tail -f).`,
}
os.Exit(1)
}
if jsonOutput {
// JSON mode: read entire file
// #nosec G304 - controlled path from daemon discovery
@@ -369,7 +322,6 @@ Supports tail mode (last N lines) and follow mode (like tail -f).`,
})
return
}
// Human-readable mode
if follow {
tailFollow(logPath)
@@ -381,7 +333,6 @@ Supports tail mode (last N lines) and follow mode (like tail -f).`,
}
},
}
func tailLines(filePath string, n int) error {
// #nosec G304 - controlled path from daemon discovery
file, err := os.Open(filePath)
@@ -389,7 +340,6 @@ func tailLines(filePath string, n int) error {
return err
}
defer file.Close()
// Read all lines
var lines []string
scanner := bufio.NewScanner(file)
@@ -399,7 +349,6 @@ func tailLines(filePath string, n int) error {
if err := scanner.Err(); err != nil {
return err
}
// Print last N lines
start := 0
if len(lines) > n {
@@ -408,10 +357,8 @@ func tailLines(filePath string, n int) error {
for i := start; i < len(lines); i++ {
fmt.Println(lines[i])
}
return nil
}
func tailFollow(filePath string) {
// #nosec G304 - controlled path from daemon discovery
file, err := os.Open(filePath)
@@ -420,10 +367,8 @@ func tailFollow(filePath string) {
os.Exit(1)
}
defer file.Close()
// Seek to end
_, _ = file.Seek(0, io.SeekEnd)
reader := bufio.NewReader(file)
for {
line, err := reader.ReadString('\n')
@@ -439,7 +384,6 @@ func tailFollow(filePath string) {
fmt.Print(strings.TrimRight(line, "\n\r") + "\n")
}
}
var daemonsKillallCmd = &cobra.Command{
Use: "killall",
Short: "Stop all running bd daemons",
@@ -449,7 +393,6 @@ Uses escalating shutdown strategy: RPC (2s) → SIGTERM (3s) → SIGKILL (1s).`,
searchRoots, _ := cmd.Flags().GetStringSlice("search")
// Use global jsonOutput set by PersistentPreRun
force, _ := cmd.Flags().GetBool("force")
// Discover all daemons
daemons, err := daemon.DiscoverDaemons(searchRoots)
if err != nil {
@@ -460,7 +403,6 @@ Uses escalating shutdown strategy: RPC (2s) → SIGTERM (3s) → SIGKILL (1s).`,
}
os.Exit(1)
}
// Filter to alive daemons only
var aliveDaemons []daemon.DaemonInfo
for _, d := range daemons {
@@ -468,7 +410,6 @@ Uses escalating shutdown strategy: RPC (2s) → SIGTERM (3s) → SIGKILL (1s).`,
aliveDaemons = append(aliveDaemons, d)
}
}
if len(aliveDaemons) == 0 {
if jsonOutput {
outputJSON(map[string]interface{}{
@@ -480,10 +421,8 @@ Uses escalating shutdown strategy: RPC (2s) → SIGTERM (3s) → SIGKILL (1s).`,
}
return
}
// Kill all daemons
results := daemon.KillAllDaemons(aliveDaemons, force)
if jsonOutput {
outputJSON(results)
} else {
@@ -496,13 +435,11 @@ Uses escalating shutdown strategy: RPC (2s) → SIGTERM (3s) → SIGKILL (1s).`,
}
}
}
if results.Failed > 0 {
os.Exit(1)
}
},
}
var daemonsHealthCmd = &cobra.Command{
Use: "health",
Short: "Check health of all bd daemons",
@@ -511,14 +448,12 @@ stale sockets, version mismatches, and unresponsive daemons.`,
Run: func(cmd *cobra.Command, args []string) {
searchRoots, _ := cmd.Flags().GetStringSlice("search")
// Use global jsonOutput set by PersistentPreRun
// Discover daemons
daemons, err := daemon.DiscoverDaemons(searchRoots)
if err != nil {
fmt.Fprintf(os.Stderr, "Error discovering daemons: %v\n", err)
os.Exit(1)
}
type healthReport struct {
Workspace string `json:"workspace"`
SocketPath string `json:"socket_path"`
@@ -528,15 +463,12 @@ stale sockets, version mismatches, and unresponsive daemons.`,
Issue string `json:"issue,omitempty"`
VersionMismatch bool `json:"version_mismatch,omitempty"`
}
var reports []healthReport
healthyCount := 0
staleCount := 0
mismatchCount := 0
unresponsiveCount := 0
currentVersion := Version
for _, d := range daemons {
report := healthReport{
Workspace: d.WorkspacePath,
@@ -544,7 +476,6 @@ stale sockets, version mismatches, and unresponsive daemons.`,
PID: d.PID,
Version: d.Version,
}
if !d.Alive {
report.Status = "stale"
report.Issue = d.Error
@@ -558,10 +489,8 @@ stale sockets, version mismatches, and unresponsive daemons.`,
report.Status = "healthy"
healthyCount++
}
reports = append(reports, report)
}
if jsonOutput {
output := map[string]interface{}{
"total": len(reports),
@@ -575,61 +504,49 @@ stale sockets, version mismatches, and unresponsive daemons.`,
fmt.Println(string(data))
return
}
// Human-readable output
if len(reports) == 0 {
fmt.Println("No daemons found")
return
}
fmt.Printf("Health Check Summary:\n")
fmt.Printf(" Total: %d\n", len(reports))
fmt.Printf(" Healthy: %d\n", healthyCount)
fmt.Printf(" Stale: %d\n", staleCount)
fmt.Printf(" Mismatched: %d\n", mismatchCount)
fmt.Printf(" Unresponsive: %d\n\n", unresponsiveCount)
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintln(w, "WORKSPACE\tPID\tVERSION\tSTATUS\tISSUE")
for _, r := range reports {
workspace := r.Workspace
if workspace == "" {
workspace = "(unknown)"
}
pidStr := "-"
if r.PID != 0 {
pidStr = fmt.Sprintf("%d", r.PID)
}
version := r.Version
if version == "" {
version = "-"
}
status := r.Status
issue := r.Issue
if issue == "" {
issue = "-"
}
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
workspace, pidStr, version, status, issue)
}
_ = w.Flush()
// Exit with error if there are any issues
if staleCount > 0 || mismatchCount > 0 || unresponsiveCount > 0 {
os.Exit(1)
}
},
}
func init() {
rootCmd.AddCommand(daemonsCmd)
// Add subcommands
daemonsCmd.AddCommand(daemonsListCmd)
daemonsCmd.AddCommand(daemonsHealthCmd)
@@ -637,30 +554,18 @@ func init() {
daemonsCmd.AddCommand(daemonsLogsCmd)
daemonsCmd.AddCommand(daemonsKillallCmd)
daemonsCmd.AddCommand(daemonsRestartCmd)
// Flags for list command
daemonsListCmd.Flags().StringSlice("search", nil, "Directories to search for daemons (default: home, /tmp, cwd)")
daemonsListCmd.Flags().Bool("json", false, "Output in JSON format")
daemonsListCmd.Flags().Bool("no-cleanup", false, "Skip auto-cleanup of stale sockets")
// Flags for health command
daemonsHealthCmd.Flags().StringSlice("search", nil, "Directories to search for daemons (default: home, /tmp, cwd)")
daemonsHealthCmd.Flags().Bool("json", false, "Output in JSON format")
// Flags for stop command
daemonsStopCmd.Flags().Bool("json", false, "Output in JSON format")
// Flags for logs command
daemonsLogsCmd.Flags().BoolP("follow", "f", false, "Follow log output (like tail -f)")
daemonsLogsCmd.Flags().IntP("lines", "n", 50, "Number of lines to show from end of log")
daemonsLogsCmd.Flags().Bool("json", false, "Output in JSON format")
// Flags for killall command
daemonsKillallCmd.Flags().StringSlice("search", nil, "Directories to search for daemons (default: home, /tmp, cwd)")
daemonsKillallCmd.Flags().Bool("json", false, "Output in JSON format")
daemonsKillallCmd.Flags().Bool("force", false, "Use SIGKILL immediately if graceful shutdown fails")
// Flags for restart command
daemonsRestartCmd.Flags().StringSlice("search", nil, "Directories to search for daemons (default: home, /tmp, cwd)")
daemonsRestartCmd.Flags().Bool("json", false, "Output in JSON format")
}