1b4448afb4
teabot watches Gitea repos and dispatches one-shot Claude Code sessions in Docker containers to work issues and review PRs, acting as configurable bot personalities. Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
98 lines
2.5 KiB
Go
98 lines
2.5 KiB
Go
package cli
|
|
|
|
import (
|
|
"log/slog"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"git.unkin.net/unkin/teabot/internal/config"
|
|
"git.unkin.net/unkin/teabot/internal/dispatch"
|
|
"git.unkin.net/unkin/teabot/internal/docker"
|
|
"git.unkin.net/unkin/teabot/internal/gitea"
|
|
"git.unkin.net/unkin/teabot/internal/state"
|
|
)
|
|
|
|
func newRunCmd() *cobra.Command {
|
|
var (
|
|
once bool
|
|
logJSON bool
|
|
logLevel string
|
|
)
|
|
cmd := &cobra.Command{
|
|
Use: "run",
|
|
Short: "Run the teabot daemon (foreground)",
|
|
Long: "run starts teabot in the foreground: it polls the configured repos on an\n" +
|
|
"interval and dispatches Claude sessions. Use --once for a single poll cycle\n" +
|
|
"(handy for testing). This command is what the systemd user unit executes.",
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
logger := newLogger(logJSON, logLevel)
|
|
|
|
cfg, err := config.Load(configPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
store, err := state.New(cfg.StateDirOrDefault())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// One Gitea client, authenticated as the first personality, is
|
|
// enough for read-only polling; actions happen inside containers as
|
|
// the per-event personality.
|
|
pollTok := cfg.Personalities[0].Token
|
|
client := gitea.NewClient(cfg.GiteaURL, pollTok)
|
|
|
|
runner := docker.NewDockerRunner()
|
|
runner.Stdout = os.Stderr
|
|
|
|
d := dispatch.New(cfg, store, client, runner, logger)
|
|
|
|
logger.Info("teabot starting",
|
|
"repos", cfg.Repos,
|
|
"personalities", len(cfg.Personalities),
|
|
"poll_interval", cfg.PollInterval.String(),
|
|
"max_concurrent", cfg.MaxConcurrent,
|
|
"job_image", cfg.JobImage,
|
|
"once", once)
|
|
|
|
if once {
|
|
return d.PollOnce(cmd.Context(), true)
|
|
}
|
|
|
|
ctx, stop := signal.NotifyContext(cmd.Context(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer stop()
|
|
return d.Run(ctx)
|
|
},
|
|
}
|
|
cmd.Flags().BoolVar(&once, "once", false, "run a single poll cycle then exit")
|
|
cmd.Flags().BoolVar(&logJSON, "log-json", false, "emit structured JSON logs")
|
|
cmd.Flags().StringVar(&logLevel, "log-level", "info", "log level: debug, info, warn, error")
|
|
return cmd
|
|
}
|
|
|
|
func newLogger(jsonOut bool, level string) *slog.Logger {
|
|
var lvl slog.Level
|
|
switch level {
|
|
case "debug":
|
|
lvl = slog.LevelDebug
|
|
case "warn":
|
|
lvl = slog.LevelWarn
|
|
case "error":
|
|
lvl = slog.LevelError
|
|
default:
|
|
lvl = slog.LevelInfo
|
|
}
|
|
opts := &slog.HandlerOptions{Level: lvl}
|
|
var h slog.Handler
|
|
if jsonOut {
|
|
h = slog.NewJSONHandler(os.Stderr, opts)
|
|
} else {
|
|
h = slog.NewTextHandler(os.Stderr, opts)
|
|
}
|
|
return slog.New(h)
|
|
}
|