From 637f38edca396731ffec235a0159ecc2cb361bdb Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 30 Dec 2025 10:53:10 -0800 Subject: [PATCH] Remove os.Exit() from library code (gt-fm75) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor to return errors instead of calling os.Exit() directly: - Add SilentExitError type for commands that signal status via exit code - Update mail.go runMailPeek() and runMailCheck() to return errors - Change Execute() to return int exit code instead of calling os.Exit() - Move os.Exit() call to main() where it belongs This improves testability, enables graceful shutdown, and follows Go conventions for library code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/gt/main.go | 8 ++++++-- internal/cmd/errors.go | 31 +++++++++++++++++++++++++++++++ internal/cmd/mail.go | 17 ++++++----------- internal/cmd/root.go | 14 ++++++++++---- 4 files changed, 53 insertions(+), 17 deletions(-) create mode 100644 internal/cmd/errors.go diff --git a/cmd/gt/main.go b/cmd/gt/main.go index f1cc7e75..2dd46bf1 100644 --- a/cmd/gt/main.go +++ b/cmd/gt/main.go @@ -1,8 +1,12 @@ // gt is the Gas Town CLI for managing multi-agent workspaces. package main -import "github.com/steveyegge/gastown/internal/cmd" +import ( + "os" + + "github.com/steveyegge/gastown/internal/cmd" +) func main() { - cmd.Execute() + os.Exit(cmd.Execute()) } diff --git a/internal/cmd/errors.go b/internal/cmd/errors.go new file mode 100644 index 00000000..2347de3e --- /dev/null +++ b/internal/cmd/errors.go @@ -0,0 +1,31 @@ +package cmd + +import "fmt" + +// SilentExitError signals that the command should exit with a specific code +// without printing an error message. This is used for scripting purposes +// where exit codes convey status (e.g., "no mail" = exit 1). +type SilentExitError struct { + Code int +} + +func (e *SilentExitError) Error() string { + return fmt.Sprintf("exit %d", e.Code) +} + +// NewSilentExit creates a SilentExitError with the given exit code. +func NewSilentExit(code int) *SilentExitError { + return &SilentExitError{Code: code} +} + +// IsSilentExit checks if an error is a SilentExitError and returns its code. +// Returns 0 and false if err is nil or not a SilentExitError. +func IsSilentExit(err error) (int, bool) { + if err == nil { + return 0, false + } + if se, ok := err.(*SilentExitError); ok { + return se.Code, true + } + return 0, false +} diff --git a/internal/cmd/mail.go b/internal/cmd/mail.go index 9e27211d..eeedd940 100644 --- a/internal/cmd/mail.go +++ b/internal/cmd/mail.go @@ -579,23 +579,20 @@ func runMailPeek(cmd *cobra.Command, args []string) error { // All mail uses town beads (two-level architecture) workDir, err := findMailWorkDir() if err != nil { - os.Exit(1) // Silent exit - no workspace - return nil + return NewSilentExit(1) // Silent exit - no workspace } // Get mailbox router := mail.NewRouter(workDir) mailbox, err := router.GetMailbox(address) if err != nil { - os.Exit(1) // Silent exit - can't access mailbox - return nil + return NewSilentExit(1) // Silent exit - can't access mailbox } // Get unread messages messages, err := mailbox.ListUnread() if err != nil || len(messages) == 0 { - os.Exit(1) // Silent exit - no unread - return nil + return NewSilentExit(1) // Silent exit - no unread } // Show first unread message @@ -923,12 +920,10 @@ func runMailCheck(cmd *cobra.Command, args []string) error { // Normal mode if unread > 0 { fmt.Printf("%s %d unread message(s)\n", style.Bold.Render("📬"), unread) - os.Exit(0) - } else { - fmt.Println("No new mail") - os.Exit(1) + return NewSilentExit(0) } - return nil + fmt.Println("No new mail") + return NewSilentExit(1) } func runMailThread(cmd *cobra.Command, args []string) error { diff --git a/internal/cmd/root.go b/internal/cmd/root.go index d1878861..75ae1a11 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -2,7 +2,6 @@ package cmd import ( - "os" "strings" "github.com/spf13/cobra" @@ -29,11 +28,18 @@ across distributed teams of AI agents working on shared codebases.`, }, } -// Execute runs the root command -func Execute() { +// Execute runs the root command and returns an exit code. +// The caller (main) should call os.Exit with this code. +func Execute() int { if err := rootCmd.Execute(); err != nil { - os.Exit(1) + // Check for silent exit (scripting commands that signal status via exit code) + if code, ok := IsSilentExit(err); ok { + return code + } + // Other errors already printed by cobra + return 1 } + return 0 } // Command group IDs - used by subcommands to organize help output