refactor: Extract runBdCommand helper to DRY mail package (gt-8i6bg)
Extracted duplicate bd command execution pattern from mailbox.go and router.go into a new helper function in bd.go. This reduces code duplication and provides consistent error handling via the bdError type. Changes: - Added internal/mail/bd.go with runBdCommand helper and bdError type - Refactored 5 functions in mailbox.go to use runBdCommand - Refactored 5 functions in router.go to use runBdCommand - Net reduction of 55 lines of code 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
254288800d
commit
4ebb96fbbc
62
internal/mail/bd.go
Normal file
62
internal/mail/bd.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// bdError represents an error from running a bd command.
|
||||||
|
// It wraps the underlying error and includes the stderr output for inspection.
|
||||||
|
type bdError struct {
|
||||||
|
Err error
|
||||||
|
Stderr string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error implements the error interface.
|
||||||
|
func (e *bdError) Error() string {
|
||||||
|
if e.Stderr != "" {
|
||||||
|
return e.Stderr
|
||||||
|
}
|
||||||
|
if e.Err != nil {
|
||||||
|
return e.Err.Error()
|
||||||
|
}
|
||||||
|
return "unknown bd error"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap returns the underlying error for errors.Is/As compatibility.
|
||||||
|
func (e *bdError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainsError checks if the stderr message contains the given substring.
|
||||||
|
func (e *bdError) ContainsError(substr string) bool {
|
||||||
|
return strings.Contains(e.Stderr, substr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// runBdCommand executes a bd command with proper environment setup.
|
||||||
|
// workDir is the directory to run the command in.
|
||||||
|
// beadsDir is the BEADS_DIR environment variable value.
|
||||||
|
// extraEnv contains additional environment variables to set (e.g., "BD_IDENTITY=...").
|
||||||
|
// Returns stdout bytes on success, or a *bdError on failure.
|
||||||
|
func runBdCommand(args []string, workDir, beadsDir string, extraEnv ...string) ([]byte, error) {
|
||||||
|
cmd := exec.Command("bd", args...) //nolint:gosec // G204: bd is a trusted internal tool
|
||||||
|
cmd.Dir = workDir
|
||||||
|
|
||||||
|
env := append(cmd.Environ(), "BEADS_DIR="+beadsDir)
|
||||||
|
env = append(env, extraEnv...)
|
||||||
|
cmd.Env = env
|
||||||
|
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return nil, &bdError{
|
||||||
|
Err: err,
|
||||||
|
Stderr: strings.TrimSpace(stderr.String()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stdout.Bytes(), nil
|
||||||
|
}
|
||||||
@@ -2,16 +2,13 @@ package mail
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveyegge/gastown/internal/beads"
|
"github.com/steveyegge/gastown/internal/beads"
|
||||||
@@ -168,34 +165,23 @@ func (m *Mailbox) identityVariants() []string {
|
|||||||
|
|
||||||
// queryMessages runs a bd list query with the given filter flag and value.
|
// queryMessages runs a bd list query with the given filter flag and value.
|
||||||
func (m *Mailbox) queryMessages(beadsDir, filterFlag, filterValue, status string) ([]*Message, error) {
|
func (m *Mailbox) queryMessages(beadsDir, filterFlag, filterValue, status string) ([]*Message, error) {
|
||||||
cmd := exec.Command("bd", "list",
|
args := []string{"list",
|
||||||
"--type", "message",
|
"--type", "message",
|
||||||
filterFlag, filterValue,
|
filterFlag, filterValue,
|
||||||
"--status", status,
|
"--status", status,
|
||||||
"--json",
|
"--json",
|
||||||
)
|
|
||||||
cmd.Dir = m.workDir
|
|
||||||
cmd.Env = append(cmd.Environ(),
|
|
||||||
"BEADS_DIR="+beadsDir,
|
|
||||||
)
|
|
||||||
|
|
||||||
var stdout, stderr bytes.Buffer
|
|
||||||
cmd.Stdout = &stdout
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
errMsg := strings.TrimSpace(stderr.String())
|
|
||||||
if errMsg != "" {
|
|
||||||
return nil, errors.New(errMsg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stdout, err := runBdCommand(args, m.workDir, beadsDir)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse JSON output
|
// Parse JSON output
|
||||||
var beadsMsgs []BeadsMessage
|
var beadsMsgs []BeadsMessage
|
||||||
if err := json.Unmarshal(stdout.Bytes(), &beadsMsgs); err != nil {
|
if err := json.Unmarshal(stdout, &beadsMsgs); err != nil {
|
||||||
// Empty inbox returns empty array or nothing
|
// Empty inbox returns empty array or nothing
|
||||||
if len(stdout.Bytes()) == 0 || stdout.String() == "null" {
|
if len(stdout) == 0 || string(stdout) == "null" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -281,28 +267,19 @@ func (m *Mailbox) getBeads(id string) (*Message, error) {
|
|||||||
|
|
||||||
// getFromDir retrieves a message from a beads directory.
|
// getFromDir retrieves a message from a beads directory.
|
||||||
func (m *Mailbox) getFromDir(id, beadsDir string) (*Message, error) {
|
func (m *Mailbox) getFromDir(id, beadsDir string) (*Message, error) {
|
||||||
cmd := exec.Command("bd", "show", id, "--json")
|
args := []string{"show", id, "--json"}
|
||||||
cmd.Dir = m.workDir
|
|
||||||
cmd.Env = append(cmd.Environ(), "BEADS_DIR="+beadsDir)
|
|
||||||
|
|
||||||
var stdout, stderr bytes.Buffer
|
stdout, err := runBdCommand(args, m.workDir, beadsDir)
|
||||||
cmd.Stdout = &stdout
|
if err != nil {
|
||||||
cmd.Stderr = &stderr
|
if bdErr, ok := err.(*bdError); ok && bdErr.ContainsError("not found") {
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
errMsg := strings.TrimSpace(stderr.String())
|
|
||||||
if strings.Contains(errMsg, "not found") {
|
|
||||||
return nil, ErrMessageNotFound
|
return nil, ErrMessageNotFound
|
||||||
}
|
}
|
||||||
if errMsg != "" {
|
|
||||||
return nil, errors.New(errMsg)
|
|
||||||
}
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// bd show --json returns an array
|
// bd show --json returns an array
|
||||||
var bms []BeadsMessage
|
var bms []BeadsMessage
|
||||||
if err := json.Unmarshal(stdout.Bytes(), &bms); err != nil {
|
if err := json.Unmarshal(stdout, &bms); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(bms) == 0 {
|
if len(bms) == 0 {
|
||||||
@@ -346,21 +323,12 @@ func (m *Mailbox) closeInDir(id, beadsDir string) error {
|
|||||||
if sessionID := os.Getenv("CLAUDE_SESSION_ID"); sessionID != "" {
|
if sessionID := os.Getenv("CLAUDE_SESSION_ID"); sessionID != "" {
|
||||||
args = append(args, "--session="+sessionID)
|
args = append(args, "--session="+sessionID)
|
||||||
}
|
}
|
||||||
cmd := exec.Command("bd", args...) //nolint:gosec // G204: bd is a trusted internal tool
|
|
||||||
cmd.Dir = m.workDir
|
|
||||||
cmd.Env = append(cmd.Environ(), "BEADS_DIR="+beadsDir)
|
|
||||||
|
|
||||||
var stderr bytes.Buffer
|
_, err := runBdCommand(args, m.workDir, beadsDir)
|
||||||
cmd.Stderr = &stderr
|
if err != nil {
|
||||||
|
if bdErr, ok := err.(*bdError); ok && bdErr.ContainsError("not found") {
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
errMsg := strings.TrimSpace(stderr.String())
|
|
||||||
if strings.Contains(errMsg, "not found") {
|
|
||||||
return ErrMessageNotFound
|
return ErrMessageNotFound
|
||||||
}
|
}
|
||||||
if errMsg != "" {
|
|
||||||
return errors.New(errMsg)
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,21 +365,13 @@ func (m *Mailbox) MarkUnread(id string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Mailbox) markUnreadBeads(id string) error {
|
func (m *Mailbox) markUnreadBeads(id string) error {
|
||||||
cmd := exec.Command("bd", "reopen", id)
|
args := []string{"reopen", id}
|
||||||
cmd.Dir = m.workDir
|
|
||||||
cmd.Env = append(cmd.Environ(), "BEADS_DIR="+m.beadsDir)
|
|
||||||
|
|
||||||
var stderr bytes.Buffer
|
_, err := runBdCommand(args, m.workDir, m.beadsDir)
|
||||||
cmd.Stderr = &stderr
|
if err != nil {
|
||||||
|
if bdErr, ok := err.(*bdError); ok && bdErr.ContainsError("not found") {
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
errMsg := strings.TrimSpace(stderr.String())
|
|
||||||
if strings.Contains(errMsg, "not found") {
|
|
||||||
return ErrMessageNotFound
|
return ErrMessageNotFound
|
||||||
}
|
}
|
||||||
if errMsg != "" {
|
|
||||||
return errors.New(errMsg)
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,29 +757,16 @@ func (m *Mailbox) ListByThread(threadID string) ([]*Message, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Mailbox) listByThreadBeads(threadID string) ([]*Message, error) {
|
func (m *Mailbox) listByThreadBeads(threadID string) ([]*Message, error) {
|
||||||
// bd message thread <thread-id> --json
|
args := []string{"message", "thread", threadID, "--json"}
|
||||||
cmd := exec.Command("bd", "message", "thread", threadID, "--json")
|
|
||||||
cmd.Dir = m.workDir
|
|
||||||
cmd.Env = append(cmd.Environ(),
|
|
||||||
"BD_IDENTITY="+m.identity,
|
|
||||||
"BEADS_DIR="+m.beadsDir,
|
|
||||||
)
|
|
||||||
|
|
||||||
var stdout, stderr bytes.Buffer
|
stdout, err := runBdCommand(args, m.workDir, m.beadsDir, "BD_IDENTITY="+m.identity)
|
||||||
cmd.Stdout = &stdout
|
if err != nil {
|
||||||
cmd.Stderr = &stderr
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
errMsg := strings.TrimSpace(stderr.String())
|
|
||||||
if errMsg != "" {
|
|
||||||
return nil, errors.New(errMsg)
|
|
||||||
}
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var beadsMsgs []BeadsMessage
|
var beadsMsgs []BeadsMessage
|
||||||
if err := json.Unmarshal(stdout.Bytes(), &beadsMsgs); err != nil {
|
if err := json.Unmarshal(stdout, &beadsMsgs); err != nil {
|
||||||
if len(stdout.Bytes()) == 0 || stdout.String() == "null" {
|
if len(stdout) == 0 || string(stdout) == "null" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
package mail
|
package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -453,24 +451,13 @@ func (r *Router) queryAgents(descContains string) ([]*agentBead, error) {
|
|||||||
args = append(args, "--desc-contains="+descContains)
|
args = append(args, "--desc-contains="+descContains)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command("bd", args...)
|
stdout, err := runBdCommand(args, filepath.Dir(beadsDir), beadsDir)
|
||||||
cmd.Env = append(cmd.Environ(), "BEADS_DIR="+beadsDir)
|
if err != nil {
|
||||||
cmd.Dir = filepath.Dir(beadsDir)
|
|
||||||
|
|
||||||
var stdout, stderr bytes.Buffer
|
|
||||||
cmd.Stdout = &stdout
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
errMsg := strings.TrimSpace(stderr.String())
|
|
||||||
if errMsg != "" {
|
|
||||||
return nil, errors.New(errMsg)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("querying agents: %w", err)
|
return nil, fmt.Errorf("querying agents: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var agents []*agentBead
|
var agents []*agentBead
|
||||||
if err := json.Unmarshal(stdout.Bytes(), &agents); err != nil {
|
if err := json.Unmarshal(stdout, &agents); err != nil {
|
||||||
return nil, fmt.Errorf("parsing agent query result: %w", err)
|
return nil, fmt.Errorf("parsing agent query result: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -622,20 +609,8 @@ func (r *Router) sendToSingle(msg *Message) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beadsDir := r.resolveBeadsDir(msg.To)
|
beadsDir := r.resolveBeadsDir(msg.To)
|
||||||
cmd := exec.Command("bd", args...) //nolint:gosec // G204: bd is a trusted internal tool
|
_, err := runBdCommand(args, filepath.Dir(beadsDir), beadsDir)
|
||||||
cmd.Env = append(cmd.Environ(),
|
if err != nil {
|
||||||
"BEADS_DIR="+beadsDir,
|
|
||||||
)
|
|
||||||
cmd.Dir = filepath.Dir(beadsDir) // Run in parent of .beads
|
|
||||||
|
|
||||||
var stderr bytes.Buffer
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
errMsg := strings.TrimSpace(stderr.String())
|
|
||||||
if errMsg != "" {
|
|
||||||
return errors.New(errMsg)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("sending message: %w", err)
|
return fmt.Errorf("sending message: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -744,20 +719,8 @@ func (r *Router) sendToQueue(msg *Message) error {
|
|||||||
|
|
||||||
// Queue messages go to town-level beads (shared location)
|
// Queue messages go to town-level beads (shared location)
|
||||||
beadsDir := r.resolveBeadsDir("")
|
beadsDir := r.resolveBeadsDir("")
|
||||||
cmd := exec.Command("bd", args...) //nolint:gosec // G204: args are constructed internally, not from user input
|
_, err = runBdCommand(args, filepath.Dir(beadsDir), beadsDir)
|
||||||
cmd.Env = append(cmd.Environ(),
|
if err != nil {
|
||||||
"BEADS_DIR="+beadsDir,
|
|
||||||
)
|
|
||||||
cmd.Dir = filepath.Dir(beadsDir) // Run in parent of .beads
|
|
||||||
|
|
||||||
var stderr bytes.Buffer
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
errMsg := strings.TrimSpace(stderr.String())
|
|
||||||
if errMsg != "" {
|
|
||||||
return errors.New(errMsg)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("sending to queue %s: %w", queueName, err)
|
return fmt.Errorf("sending to queue %s: %w", queueName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -827,20 +790,8 @@ func (r *Router) sendToAnnounce(msg *Message) error {
|
|||||||
|
|
||||||
// Announce messages go to town-level beads (shared location)
|
// Announce messages go to town-level beads (shared location)
|
||||||
beadsDir := r.resolveBeadsDir("")
|
beadsDir := r.resolveBeadsDir("")
|
||||||
cmd := exec.Command("bd", args...) //nolint:gosec // G204: args are constructed internally, not from user input
|
_, err = runBdCommand(args, filepath.Dir(beadsDir), beadsDir)
|
||||||
cmd.Env = append(cmd.Environ(),
|
if err != nil {
|
||||||
"BEADS_DIR="+beadsDir,
|
|
||||||
)
|
|
||||||
cmd.Dir = filepath.Dir(beadsDir) // Run in parent of .beads
|
|
||||||
|
|
||||||
var stderr bytes.Buffer
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
errMsg := strings.TrimSpace(stderr.String())
|
|
||||||
if errMsg != "" {
|
|
||||||
return errors.New(errMsg)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("sending to announce %s: %w", announceName, err)
|
return fmt.Errorf("sending to announce %s: %w", announceName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -869,19 +820,8 @@ func (r *Router) pruneAnnounce(announceName string, retainCount int) error {
|
|||||||
"--asc", // Oldest first
|
"--asc", // Oldest first
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command("bd", args...) //nolint:gosec // G204: args are constructed internally
|
stdout, err := runBdCommand(args, filepath.Dir(beadsDir), beadsDir)
|
||||||
cmd.Env = append(cmd.Environ(), "BEADS_DIR="+beadsDir)
|
if err != nil {
|
||||||
cmd.Dir = filepath.Dir(beadsDir)
|
|
||||||
|
|
||||||
var stdout, stderr bytes.Buffer
|
|
||||||
cmd.Stdout = &stdout
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
errMsg := strings.TrimSpace(stderr.String())
|
|
||||||
if errMsg != "" {
|
|
||||||
return errors.New(errMsg)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("querying announce messages: %w", err)
|
return fmt.Errorf("querying announce messages: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -889,7 +829,7 @@ func (r *Router) pruneAnnounce(announceName string, retainCount int) error {
|
|||||||
var messages []struct {
|
var messages []struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(stdout.Bytes(), &messages); err != nil {
|
if err := json.Unmarshal(stdout, &messages); err != nil {
|
||||||
return fmt.Errorf("parsing announce messages: %w", err)
|
return fmt.Errorf("parsing announce messages: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -904,12 +844,8 @@ func (r *Router) pruneAnnounce(announceName string, retainCount int) error {
|
|||||||
// Delete oldest messages
|
// Delete oldest messages
|
||||||
for i := 0; i < toDelete && i < len(messages); i++ {
|
for i := 0; i < toDelete && i < len(messages); i++ {
|
||||||
deleteArgs := []string{"close", messages[i].ID, "--reason=retention pruning"}
|
deleteArgs := []string{"close", messages[i].ID, "--reason=retention pruning"}
|
||||||
deleteCmd := exec.Command("bd", deleteArgs...) //nolint:gosec // G204: args are constructed internally
|
|
||||||
deleteCmd.Env = append(deleteCmd.Environ(), "BEADS_DIR="+beadsDir)
|
|
||||||
deleteCmd.Dir = filepath.Dir(beadsDir)
|
|
||||||
|
|
||||||
// Best-effort deletion - don't fail if one delete fails
|
// Best-effort deletion - don't fail if one delete fails
|
||||||
_ = deleteCmd.Run()
|
_, _ = runBdCommand(deleteArgs, filepath.Dir(beadsDir), beadsDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Reference in New Issue
Block a user