diff --git a/internal/cmd/down.go b/internal/cmd/down.go index 63b3cc7c..41e47953 100644 --- a/internal/cmd/down.go +++ b/internal/cmd/down.go @@ -5,9 +5,9 @@ import ( "time" "github.com/spf13/cobra" - "github.com/steveyegge/gastown/internal/boot" "github.com/steveyegge/gastown/internal/daemon" "github.com/steveyegge/gastown/internal/events" + "github.com/steveyegge/gastown/internal/session" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/tmux" "github.com/steveyegge/gastown/internal/workspace" @@ -73,35 +73,20 @@ func runDown(cmd *cobra.Command, args []string) error { } } - // Get session names - mayorSession := getMayorSessionName() - deaconSession := getDeaconSessionName() - - // 2. Stop Mayor - if err := stopSession(t, mayorSession); err != nil { - printDownStatus("Mayor", false, err.Error()) - allOK = false - } else { - printDownStatus("Mayor", true, "stopped") + // 2. Stop town-level sessions (Mayor, Boot, Deacon) in correct order + for _, ts := range session.TownSessions() { + stopped, err := session.StopTownSession(t, ts, downForce) + if err != nil { + printDownStatus(ts.Name, false, err.Error()) + allOK = false + } else if stopped { + printDownStatus(ts.Name, true, "stopped") + } else { + printDownStatus(ts.Name, true, "not running") + } } - // 3. Stop Boot (Deacon's watchdog) - if err := stopSession(t, boot.SessionName); err != nil { - printDownStatus("Boot", false, err.Error()) - allOK = false - } else { - printDownStatus("Boot", true, "stopped") - } - - // 4. Stop Deacon - if err := stopSession(t, deaconSession); err != nil { - printDownStatus("Deacon", false, err.Error()) - allOK = false - } else { - printDownStatus("Deacon", true, "stopped") - } - - // 5. Stop Daemon last + // 3. Stop Daemon last running, _, _ := daemon.IsRunning(townRoot) if running { if err := daemon.StopDaemon(townRoot); err != nil { @@ -114,7 +99,7 @@ func runDown(cmd *cobra.Command, args []string) error { printDownStatus("Daemon", true, "not running") } - // 6. Kill tmux server if --all + // 4. Kill tmux server if --all if downAll { if err := t.KillServer(); err != nil { printDownStatus("Tmux server", false, err.Error()) diff --git a/internal/session/town.go b/internal/session/town.go new file mode 100644 index 00000000..05fdd44d --- /dev/null +++ b/internal/session/town.go @@ -0,0 +1,53 @@ +// Package session provides polecat session lifecycle management. +package session + +import ( + "fmt" + "time" + + "github.com/steveyegge/gastown/internal/boot" + "github.com/steveyegge/gastown/internal/tmux" +) + +// TownSession represents a town-level tmux session. +type TownSession struct { + Name string // Display name (e.g., "Mayor") + SessionID string // Tmux session ID (e.g., "gt-mayor") +} + +// TownSessions returns the list of town-level sessions in shutdown order. +// Order matters: Boot (Deacon's watchdog) must be stopped before Deacon, +// otherwise Boot will try to restart Deacon. +func TownSessions() []TownSession { + return []TownSession{ + {"Mayor", MayorSessionName()}, + {"Boot", boot.SessionName}, + {"Deacon", DeaconSessionName()}, + } +} + +// StopTownSession stops a single town-level tmux session. +// If force is true, skips graceful shutdown (Ctrl-C) and kills immediately. +// Returns true if the session was running and stopped, false if not running. +func StopTownSession(t *tmux.Tmux, ts TownSession, force bool) (bool, error) { + running, err := t.HasSession(ts.SessionID) + if err != nil { + return false, err + } + if !running { + return false, nil + } + + // Try graceful shutdown first (unless forced) + if !force { + _ = t.SendKeysRaw(ts.SessionID, "C-c") + time.Sleep(100 * time.Millisecond) + } + + // Kill the session + if err := t.KillSession(ts.SessionID); err != nil { + return false, fmt.Errorf("killing %s session: %w", ts.Name, err) + } + + return true, nil +}