refactor(cmd): replace map[string]interface{} with typed JSON response structs (bd-u2sc.1)

Added typed response structs for JSON output in CLI commands:

compact.go:
- CompactDryRunResponse, CompactSuccessResponse
- CompactNoCandidatesResponse, CompactBatchSuccessResponse
- CompactStatsResponse, CompactTierStats
- CompactApplyResponse, TombstonePrunedInfo

cleanup.go:
- CleanupEmptyResponse

daemons.go:
- DaemonStopResponse, DaemonRestartResponse
- DaemonLogsResponse, DaemonKillallEmptyResponse
- DaemonHealthResponse, DaemonHealthReport

daemon_lifecycle.go:
- DaemonStatusResponse

Benefits:
- Compile-time type checking for JSON output
- IDE autocompletion for response fields
- Self-documenting API structure
- Easier refactoring

Note: RPC args and storage update maps remain as-is (require
interface changes for internal APIs).

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-22 15:48:36 -08:00
parent e67712dcd4
commit 4c38075520
4 changed files with 247 additions and 122 deletions

View File

@@ -1,4 +1,5 @@
package main
import (
"bufio"
"encoding/json"
@@ -10,9 +11,59 @@ import (
"strings"
"text/tabwriter"
"time"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/daemon"
)
// JSON response types for daemons commands
// DaemonStopResponse is returned when a daemon is stopped
type DaemonStopResponse struct {
Workspace string `json:"workspace"`
PID int `json:"pid"`
Stopped bool `json:"stopped"`
}
// DaemonRestartResponse is returned when a daemon is restarted
type DaemonRestartResponse struct {
Workspace string `json:"workspace"`
Action string `json:"action"`
}
// DaemonLogsResponse is returned for daemon logs in JSON mode
type DaemonLogsResponse struct {
Workspace string `json:"workspace"`
LogPath string `json:"log_path"`
Content string `json:"content"`
}
// DaemonKillallEmptyResponse is returned when no daemons are running
type DaemonKillallEmptyResponse struct {
Stopped int `json:"stopped"`
Failed int `json:"failed"`
}
// DaemonHealthReport is a single daemon health report entry
type DaemonHealthReport struct {
Workspace string `json:"workspace"`
SocketPath string `json:"socket_path"`
PID int `json:"pid,omitempty"`
Version string `json:"version,omitempty"`
Status string `json:"status"`
Issue string `json:"issue,omitempty"`
VersionMismatch bool `json:"version_mismatch,omitempty"`
}
// DaemonHealthResponse is returned for daemon health check
type DaemonHealthResponse struct {
Total int `json:"total"`
Healthy int `json:"healthy"`
Stale int `json:"stale"`
Mismatched int `json:"mismatched"`
Unresponsive int `json:"unresponsive"`
Daemons []DaemonHealthReport `json:"daemons"`
}
var daemonsCmd = &cobra.Command{
Use: "daemons",
GroupID: "sync",
@@ -154,10 +205,10 @@ Sends shutdown command via RPC, with SIGTERM fallback if RPC fails.`,
os.Exit(1)
}
if jsonOutput {
outputJSON(map[string]interface{}{
"workspace": targetDaemon.WorkspacePath,
"pid": targetDaemon.PID,
"stopped": true,
outputJSON(DaemonStopResponse{
Workspace: targetDaemon.WorkspacePath,
PID: targetDaemon.PID,
Stopped: true,
})
} else {
fmt.Printf("Stopped daemon for %s (PID %d)\n", targetDaemon.WorkspacePath, targetDaemon.PID)
@@ -268,9 +319,9 @@ Stops the daemon gracefully, then starts a new one.`,
}
}()
if jsonOutput {
outputJSON(map[string]interface{}{
"workspace": workspace,
"action": "restarted",
outputJSON(DaemonRestartResponse{
Workspace: workspace,
Action: "restarted",
})
} else {
fmt.Printf("Successfully restarted daemon for workspace: %s\n", workspace)
@@ -333,10 +384,10 @@ Supports tail mode (last N lines) and follow mode (like tail -f).`,
outputJSON(map[string]string{"error": err.Error()})
os.Exit(1)
}
outputJSON(map[string]interface{}{
"workspace": targetDaemon.WorkspacePath,
"log_path": logPath,
"content": string(content),
outputJSON(DaemonLogsResponse{
Workspace: targetDaemon.WorkspacePath,
LogPath: logPath,
Content: string(content),
})
return
}
@@ -430,9 +481,9 @@ Uses escalating shutdown strategy: RPC (2s) → SIGTERM (3s) → SIGKILL (1s).`,
}
if len(aliveDaemons) == 0 {
if jsonOutput {
outputJSON(map[string]interface{}{
"stopped": 0,
"failed": 0,
outputJSON(DaemonKillallEmptyResponse{
Stopped: 0,
Failed: 0,
})
} else {
fmt.Println("No running daemons found")
@@ -472,23 +523,14 @@ stale sockets, version mismatches, and unresponsive daemons.`,
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"`
PID int `json:"pid,omitempty"`
Version string `json:"version,omitempty"`
Status string `json:"status"`
Issue string `json:"issue,omitempty"`
VersionMismatch bool `json:"version_mismatch,omitempty"`
}
var reports []healthReport
var reports []DaemonHealthReport
healthyCount := 0
staleCount := 0
mismatchCount := 0
unresponsiveCount := 0
currentVersion := Version
for _, d := range daemons {
report := healthReport{
report := DaemonHealthReport{
Workspace: d.WorkspacePath,
SocketPath: d.SocketPath,
PID: d.PID,
@@ -510,16 +552,14 @@ stale sockets, version mismatches, and unresponsive daemons.`,
reports = append(reports, report)
}
if jsonOutput {
output := map[string]interface{}{
"total": len(reports),
"healthy": healthyCount,
"stale": staleCount,
"mismatched": mismatchCount,
"unresponsive": unresponsiveCount,
"daemons": reports,
}
data, _ := json.MarshalIndent(output, "", " ")
fmt.Println(string(data))
outputJSON(DaemonHealthResponse{
Total: len(reports),
Healthy: healthyCount,
Stale: staleCount,
Mismatched: mismatchCount,
Unresponsive: unresponsiveCount,
Daemons: reports,
})
return
}
// Human-readable output