ScuttleBot

feat: wire daemon — ergo lifecycle, registry, REST API server Boots ergo, waits for health, wires registry + HTTP API on :8080. Generates and prints API token on startup, graceful SIGINT/SIGTERM shutdown. Closes #21

lmata 2026-03-31 05:09 trunk
Commit b781baa40cb6bb76cd62b5eeb8af83eaaa4f74a9e85abf17d41f3971706247bd
1 file changed +97 -2
--- cmd/scuttlebot/main.go
+++ cmd/scuttlebot/main.go
@@ -1,9 +1,104 @@
11
package main
22
3
-import "fmt"
3
+import (
4
+ "context"
5
+ "crypto/rand"
6
+ "encoding/hex"
7
+ "fmt"
8
+ "log/slog"
9
+ "net/http"
10
+ "os"
11
+ "os/signal"
12
+ "syscall"
13
+ "time"
14
+
15
+ "github.com/conflicthq/scuttlebot/internal/api"
16
+ "github.com/conflicthq/scuttlebot/internal/config"
17
+ "github.com/conflicthq/scuttlebot/internal/ergo"
18
+ "github.com/conflicthq/scuttlebot/internal/registry"
19
+)
420
521
var version = "dev"
622
723
func main() {
8
- fmt.Printf("scuttlebot %s\n", version)
24
+ log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))
25
+
26
+ cfg := &config.Config{}
27
+ cfg.Defaults()
28
+
29
+ // Generate an API token for the Ergo management API if not set.
30
+ if cfg.Ergo.APIToken == "" {
31
+ cfg.Ergo.APIToken = mustGenToken()
32
+ }
33
+
34
+ ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
35
+ defer cancel()
36
+
37
+ log.Info("scuttlebot starting", "version", version)
38
+
39
+ // Start Ergo.
40
+ manager := ergo.NewManager(cfg.Ergo, log)
41
+ ergoErr := make(chan error, 1)
42
+ go func() {
43
+ if err := manager.Start(ctx); err != nil {
44
+ ergoErr <- err
45
+ }
46
+ }()
47
+
48
+ // Wait for Ergo to become healthy before starting the rest.
49
+ healthCtx, healthCancel := context.WithTimeout(ctx, 30*time.Second)
50
+ defer healthCancel()
51
+ for {
52
+ if _, err := manager.API().Status(); err == nil {
53
+ break
54
+ }
55
+ select {
56
+ case <-healthCtx.Done():
57
+ log.Error("ergo did not become healthy in time")
58
+ os.Exit(1)
59
+ case err := <-ergoErr:
60
+ log.Error("ergo failed to start", "err", err)
61
+ os.Exit(1)
62
+ case <-time.After(500 * time.Millisecond):
63
+ }
64
+ }
65
+ log.Info("ergo healthy")
66
+
67
+ // Build registry backed by Ergo's NickServ API.
68
+ signingKey := []byte(mustGenToken())
69
+ reg := registry.New(manager.API(), signingKey)
70
+
71
+ // Start HTTP API server.
72
+ apiToken := mustGenToken()
73
+ log.Info("api token", "token", apiToken) // printed once on startup — user copies this
74
+ apiSrv := api.New(reg, []string{apiToken}, log)
75
+ httpServer := &http.Server{
76
+ Addr: ":8080",
77
+ Handler: apiSrv.Handler(),
78
+ }
79
+
80
+ go func() {
81
+ log.Info("api server listening", "addr", httpServer.Addr)
82
+ if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
83
+ log.Error("api server error", "err", err)
84
+ }
85
+ }()
86
+
87
+ <-ctx.Done()
88
+ log.Info("shutting down")
89
+
90
+ shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
91
+ defer shutdownCancel()
92
+ _ = httpServer.Shutdown(shutdownCtx)
93
+
94
+ log.Info("goodbye")
95
+}
96
+
97
+func mustGenToken() string {
98
+ b := make([]byte, 24)
99
+ if _, err := rand.Read(b); err != nil {
100
+ fmt.Fprintf(os.Stderr, "failed to generate token: %v\n", err)
101
+ os.Exit(1)
102
+ }
103
+ return hex.EncodeToString(b)
9104
}
10105
--- cmd/scuttlebot/main.go
+++ cmd/scuttlebot/main.go
@@ -1,9 +1,104 @@
1 package main
2
3 import "fmt"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
5 var version = "dev"
6
7 func main() {
8 fmt.Printf("scuttlebot %s\n", version)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9 }
10
--- cmd/scuttlebot/main.go
+++ cmd/scuttlebot/main.go
@@ -1,9 +1,104 @@
1 package main
2
3 import (
4 "context"
5 "crypto/rand"
6 "encoding/hex"
7 "fmt"
8 "log/slog"
9 "net/http"
10 "os"
11 "os/signal"
12 "syscall"
13 "time"
14
15 "github.com/conflicthq/scuttlebot/internal/api"
16 "github.com/conflicthq/scuttlebot/internal/config"
17 "github.com/conflicthq/scuttlebot/internal/ergo"
18 "github.com/conflicthq/scuttlebot/internal/registry"
19 )
20
21 var version = "dev"
22
23 func main() {
24 log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))
25
26 cfg := &config.Config{}
27 cfg.Defaults()
28
29 // Generate an API token for the Ergo management API if not set.
30 if cfg.Ergo.APIToken == "" {
31 cfg.Ergo.APIToken = mustGenToken()
32 }
33
34 ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
35 defer cancel()
36
37 log.Info("scuttlebot starting", "version", version)
38
39 // Start Ergo.
40 manager := ergo.NewManager(cfg.Ergo, log)
41 ergoErr := make(chan error, 1)
42 go func() {
43 if err := manager.Start(ctx); err != nil {
44 ergoErr <- err
45 }
46 }()
47
48 // Wait for Ergo to become healthy before starting the rest.
49 healthCtx, healthCancel := context.WithTimeout(ctx, 30*time.Second)
50 defer healthCancel()
51 for {
52 if _, err := manager.API().Status(); err == nil {
53 break
54 }
55 select {
56 case <-healthCtx.Done():
57 log.Error("ergo did not become healthy in time")
58 os.Exit(1)
59 case err := <-ergoErr:
60 log.Error("ergo failed to start", "err", err)
61 os.Exit(1)
62 case <-time.After(500 * time.Millisecond):
63 }
64 }
65 log.Info("ergo healthy")
66
67 // Build registry backed by Ergo's NickServ API.
68 signingKey := []byte(mustGenToken())
69 reg := registry.New(manager.API(), signingKey)
70
71 // Start HTTP API server.
72 apiToken := mustGenToken()
73 log.Info("api token", "token", apiToken) // printed once on startup — user copies this
74 apiSrv := api.New(reg, []string{apiToken}, log)
75 httpServer := &http.Server{
76 Addr: ":8080",
77 Handler: apiSrv.Handler(),
78 }
79
80 go func() {
81 log.Info("api server listening", "addr", httpServer.Addr)
82 if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
83 log.Error("api server error", "err", err)
84 }
85 }()
86
87 <-ctx.Done()
88 log.Info("shutting down")
89
90 shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
91 defer shutdownCancel()
92 _ = httpServer.Shutdown(shutdownCtx)
93
94 log.Info("goodbye")
95 }
96
97 func mustGenToken() string {
98 b := make([]byte, 24)
99 if _, err := rand.Read(b); err != nil {
100 fmt.Fprintf(os.Stderr, "failed to generate token: %v\n", err)
101 os.Exit(1)
102 }
103 return hex.EncodeToString(b)
104 }
105

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button