package cli import ( "context" "fmt" "net/http" "os" "os/signal" "path/filepath" "syscall" "time" "github.com/thepeterstone/claudomator/internal/api" "github.com/thepeterstone/claudomator/internal/executor" "github.com/thepeterstone/claudomator/internal/notify" "github.com/thepeterstone/claudomator/internal/storage" "github.com/thepeterstone/claudomator/internal/version" "github.com/spf13/cobra" ) func newServeCmd() *cobra.Command { var addr string cmd := &cobra.Command{ Use: "serve", Short: "Start the Claudomator API server", RunE: func(cmd *cobra.Command, args []string) error { return serve(addr) }, } cmd.Flags().StringVar(&addr, "addr", ":8484", "listen address") return cmd } func serve(addr string) error { if err := cfg.EnsureDirs(); err != nil { return fmt.Errorf("creating dirs: %w", err) } store, err := storage.Open(cfg.DBPath) if err != nil { return fmt.Errorf("opening db: %w", err) } defer store.Close() logger := newLogger(verbose) apiURL := "http://localhost" + addr if len(addr) > 0 && addr[0] != ':' { apiURL = "http://" + addr } runners := map[string]executor.Runner{ "claude": &executor.ClaudeRunner{ BinaryPath: cfg.ClaudeBinaryPath, Logger: logger, LogDir: cfg.LogDir, APIURL: apiURL, }, "gemini": &executor.GeminiRunner{ BinaryPath: cfg.GeminiBinaryPath, Logger: logger, LogDir: cfg.LogDir, APIURL: apiURL, }, } pool := executor.NewPool(cfg.MaxConcurrent, runners, store, logger) if cfg.GeminiBinaryPath != "" { pool.Classifier = &executor.Classifier{GeminiBinaryPath: cfg.GeminiBinaryPath} } srv := api.NewServer(store, pool, logger, cfg.ClaudeBinaryPath, cfg.GeminiBinaryPath) if cfg.WebhookURL != "" { srv.SetNotifier(notify.NewWebhookNotifier(cfg.WebhookURL, logger)) } // Register scripts. wd, _ := os.Getwd() srv.SetScripts(api.ScriptRegistry{ "start-next-task": filepath.Join(wd, "scripts", "start-next-task"), "deploy": filepath.Join(wd, "scripts", "deploy"), }) srv.StartHub() httpSrv := &http.Server{ Addr: addr, Handler: srv.Handler(), } // Graceful shutdown. ctx, cancel := context.WithCancel(context.Background()) defer cancel() sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) go func() { <-sigCh logger.Info("shutting down server...") shutdownCtx, shutdownCancel := context.WithTimeout(ctx, 5*time.Second) defer shutdownCancel() if err := httpSrv.Shutdown(shutdownCtx); err != nil { logger.Warn("shutdown error", "err", err) } }() fmt.Printf("Claudomator %s listening on %s\n", version.Version(), addr) if err := httpSrv.ListenAndServe(); err != http.ErrServerClosed { return err } return nil }