summaryrefslogtreecommitdiff
path: root/internal/cli/serve.go
blob: e5bd873df897823ce7131ed46b1018434a67021b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
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
	var workspaceRoot string

	cmd := &cobra.Command{
		Use:   "serve",
		Short: "Start the Claudomator API server",
		RunE: func(cmd *cobra.Command, args []string) error {
			if cmd.Flags().Changed("workspace-root") {
				cfg.WorkspaceRoot = workspaceRoot
			}
			return serve(addr)
		},
	}

	cmd.Flags().StringVar(&addr, "addr", ":8484", "listen address")
	cmd.Flags().StringVar(&workspaceRoot, "workspace-root", "/workspace", "root directory for listing workspaces")

	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}
	}
	pool.RecoverStaleRunning()

	srv := api.NewServer(store, pool, logger, cfg.ClaudeBinaryPath, cfg.GeminiBinaryPath)
	if cfg.WebhookURL != "" {
		srv.SetNotifier(notify.NewWebhookNotifier(cfg.WebhookURL, logger))
	}
	if cfg.WorkspaceRoot != "" {
		srv.SetWorkspaceRoot(cfg.WorkspaceRoot)
	}

	// 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
}