Files
gastown/internal/cmd/daemon.go
gastown/crew/joe df46e75a51 Fix: Unknown subcommands now error instead of silently showing help
Parent commands (mol, mail, crew, polecat, etc.) previously showed help
and exited 0 for unknown subcommands like "gt mol foobar". This masked
errors in scripts and confused users.

Added requireSubcommand() helper to root.go and applied it to all parent
commands. Now unknown subcommands properly error with exit code 1.

Example before: gt mol unhook → shows help, exits 0
Example after:  gt mol unhook → "Error: unknown command "unhook"", exits 1

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 20:52:23 -08:00

236 lines
5.8 KiB
Go

package cmd
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/daemon"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/workspace"
)
var daemonCmd = &cobra.Command{
Use: "daemon",
GroupID: GroupServices,
Short: "Manage the Gas Town daemon",
RunE: requireSubcommand,
Long: `Manage the Gas Town background daemon.
The daemon is a simple Go process that:
- Pokes agents periodically (heartbeat)
- Processes lifecycle requests (cycle, restart, shutdown)
- Restarts sessions when agents request cycling
The daemon is a "dumb scheduler" - all intelligence is in agents.`,
}
var daemonStartCmd = &cobra.Command{
Use: "start",
Short: "Start the daemon",
Long: `Start the Gas Town daemon in the background.
The daemon will run until stopped with 'gt daemon stop'.`,
RunE: runDaemonStart,
}
var daemonStopCmd = &cobra.Command{
Use: "stop",
Short: "Stop the daemon",
Long: `Stop the running Gas Town daemon.`,
RunE: runDaemonStop,
}
var daemonStatusCmd = &cobra.Command{
Use: "status",
Short: "Show daemon status",
Long: `Show the current status of the Gas Town daemon.`,
RunE: runDaemonStatus,
}
var daemonLogsCmd = &cobra.Command{
Use: "logs",
Short: "View daemon logs",
Long: `View the daemon log file.`,
RunE: runDaemonLogs,
}
var daemonRunCmd = &cobra.Command{
Use: "run",
Short: "Run daemon in foreground (internal)",
Hidden: true,
RunE: runDaemonRun,
}
var (
daemonLogLines int
daemonLogFollow bool
)
func init() {
daemonCmd.AddCommand(daemonStartCmd)
daemonCmd.AddCommand(daemonStopCmd)
daemonCmd.AddCommand(daemonStatusCmd)
daemonCmd.AddCommand(daemonLogsCmd)
daemonCmd.AddCommand(daemonRunCmd)
daemonLogsCmd.Flags().IntVarP(&daemonLogLines, "lines", "n", 50, "Number of lines to show")
daemonLogsCmd.Flags().BoolVarP(&daemonLogFollow, "follow", "f", false, "Follow log output")
rootCmd.AddCommand(daemonCmd)
}
func runDaemonStart(cmd *cobra.Command, args []string) error {
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
return fmt.Errorf("not in a Gas Town workspace: %w", err)
}
// Check if already running
running, pid, err := daemon.IsRunning(townRoot)
if err != nil {
return fmt.Errorf("checking daemon status: %w", err)
}
if running {
return fmt.Errorf("daemon already running (PID %d)", pid)
}
// Start daemon in background
// We use 'gt daemon run' as the actual daemon process
gtPath, err := os.Executable()
if err != nil {
return fmt.Errorf("finding executable: %w", err)
}
daemonCmd := exec.Command(gtPath, "daemon", "run")
daemonCmd.Dir = townRoot
// Detach from terminal
daemonCmd.Stdin = nil
daemonCmd.Stdout = nil
daemonCmd.Stderr = nil
if err := daemonCmd.Start(); err != nil {
return fmt.Errorf("starting daemon: %w", err)
}
// Wait a moment for the daemon to initialize
time.Sleep(200 * time.Millisecond)
// Verify it started
running, pid, err = daemon.IsRunning(townRoot)
if err != nil {
return fmt.Errorf("checking daemon status: %w", err)
}
if !running {
return fmt.Errorf("daemon failed to start (check logs with 'gt daemon logs')")
}
fmt.Printf("%s Daemon started (PID %d)\n", style.Bold.Render("✓"), pid)
return nil
}
func runDaemonStop(cmd *cobra.Command, args []string) error {
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
return fmt.Errorf("not in a Gas Town workspace: %w", err)
}
running, pid, err := daemon.IsRunning(townRoot)
if err != nil {
return fmt.Errorf("checking daemon status: %w", err)
}
if !running {
return fmt.Errorf("daemon is not running")
}
if err := daemon.StopDaemon(townRoot); err != nil {
return fmt.Errorf("stopping daemon: %w", err)
}
fmt.Printf("%s Daemon stopped (was PID %d)\n", style.Bold.Render("✓"), pid)
return nil
}
func runDaemonStatus(cmd *cobra.Command, args []string) error {
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
return fmt.Errorf("not in a Gas Town workspace: %w", err)
}
running, pid, err := daemon.IsRunning(townRoot)
if err != nil {
return fmt.Errorf("checking daemon status: %w", err)
}
if running {
fmt.Printf("%s Daemon is %s (PID %d)\n",
style.Bold.Render("●"),
style.Bold.Render("running"),
pid)
// Load state for more details
state, err := daemon.LoadState(townRoot)
if err == nil && !state.StartedAt.IsZero() {
fmt.Printf(" Started: %s\n", state.StartedAt.Format("2006-01-02 15:04:05"))
if !state.LastHeartbeat.IsZero() {
fmt.Printf(" Last heartbeat: %s (#%d)\n",
state.LastHeartbeat.Format("15:04:05"),
state.HeartbeatCount)
}
}
} else {
fmt.Printf("%s Daemon is %s\n",
style.Dim.Render("○"),
"not running")
fmt.Printf("\nStart with: %s\n", style.Dim.Render("gt daemon start"))
}
return nil
}
func runDaemonLogs(cmd *cobra.Command, args []string) error {
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
return fmt.Errorf("not in a Gas Town workspace: %w", err)
}
logFile := filepath.Join(townRoot, "daemon", "daemon.log")
if _, err := os.Stat(logFile); os.IsNotExist(err) {
return fmt.Errorf("no log file found at %s", logFile)
}
if daemonLogFollow {
// Use tail -f for following
tailCmd := exec.Command("tail", "-f", logFile)
tailCmd.Stdout = os.Stdout
tailCmd.Stderr = os.Stderr
return tailCmd.Run()
}
// Use tail -n for last N lines
tailCmd := exec.Command("tail", "-n", fmt.Sprintf("%d", daemonLogLines), logFile)
tailCmd.Stdout = os.Stdout
tailCmd.Stderr = os.Stderr
return tailCmd.Run()
}
func runDaemonRun(cmd *cobra.Command, args []string) error {
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
return fmt.Errorf("not in a Gas Town workspace: %w", err)
}
config := daemon.DefaultConfig(townRoot)
d, err := daemon.New(config)
if err != nil {
return fmt.Errorf("creating daemon: %w", err)
}
return d.Run()
}