116 lines
3.5 KiB
Go
116 lines
3.5 KiB
Go
// Command cyrene is the single Cyrene v2 binary. It assembles every subsystem
|
|
// (config, logging, database, bus, personas, model adapter, tool dispatcher,
|
|
// memory stack, agent loop, orchestration, execution, IoT, channel sessions and
|
|
// adapters) into one process and serves the gateway + embedded frontend on a
|
|
// single port (:8080), shutting down gracefully on a termination signal.
|
|
//
|
|
// Build with `go build -ldflags="-s -w" -trimpath -o cyrene ./cmd/cyrene/`.
|
|
// The frontend must be built first so the SPA can be embedded:
|
|
// `cd frontend/web && npm run build`.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io/fs"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
capreg "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/platform"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/webui"
|
|
)
|
|
|
|
// envPath is the .env file merged at boot (development only; production uses
|
|
// systemd/env_file). A missing .env is not an error.
|
|
const envPath = ".env"
|
|
|
|
// shutdownTimeout bounds how long the graceful teardown may take.
|
|
const shutdownTimeout = 15 * time.Second
|
|
|
|
// externalDistCandidates are the on-disk directories checked (in order) for a
|
|
// frontend override before falling back to the embedded dist. The first that
|
|
// exists wins. They are relative to the process working directory.
|
|
func externalDistCandidates() []string {
|
|
return []string{
|
|
filepath.Join("frontend", "web", "dist"),
|
|
filepath.Join("frontend", "dist"),
|
|
}
|
|
}
|
|
|
|
// resolveFrontendFS returns the filesystem serving the SPA: an external dist
|
|
// directory when present (deploy-time hot override), otherwise the embedded dist.
|
|
func resolveFrontendFS(log *slog.Logger) fs.FS {
|
|
for _, dir := range externalDistCandidates() {
|
|
if info, err := os.Stat(dir); err == nil && info.IsDir() {
|
|
log.Info("serving external frontend dist", "dir", dir)
|
|
return os.DirFS(dir)
|
|
}
|
|
}
|
|
log.Info("serving embedded frontend dist")
|
|
return webui.Assets()
|
|
}
|
|
|
|
func main() {
|
|
if err := run(); err != nil {
|
|
fmt.Fprintln(os.Stderr, "cyrene:", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// run boots the application, starts every service and the gateway, then blocks
|
|
// until a termination signal arrives or the gateway fails.
|
|
func run() error {
|
|
cfg := config.MustLoad(envPath)
|
|
format := logger.FormatText
|
|
if cfg.Env == "production" {
|
|
format = logger.FormatJSON
|
|
}
|
|
log := logger.New(logger.Options{Level: cfg.LogLevel, Format: string(format)})
|
|
log = logger.WithSource(log, "main")
|
|
|
|
ctx, stop := platform.SignalContext()
|
|
defer stop()
|
|
|
|
a, err := newApp(ctx, cfg, log)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := a.automation.Start(ctx); err != nil {
|
|
return fmt.Errorf("start automations: %w", err)
|
|
}
|
|
a.backthink.Start(ctx)
|
|
a.log.Info("cyrene subsystems started",
|
|
"personas", a.personas.CountPersonas(),
|
|
"tools", len(a.caps.List(capreg.KindTool)),
|
|
"channels", len(a.caps.List(capreg.KindChannel)))
|
|
a.log.Info("cyrene ready", "data_dir", resolveDataDir(cfg))
|
|
|
|
errCh, err := a.gw.Start(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("start gateway: %w", err)
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
a.log.Info("cyrene signal received, shutting down")
|
|
case gwErr := <-errCh:
|
|
if gwErr != nil {
|
|
shutCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
|
|
defer cancel()
|
|
a.close(shutCtx)
|
|
return fmt.Errorf("gateway failed: %w", gwErr)
|
|
}
|
|
}
|
|
|
|
shutCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
|
|
defer cancel()
|
|
a.close(shutCtx)
|
|
return nil
|
|
}
|