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 } if !cfg.HasAnyAllowlist() { logger.Warn("no allowed_authors configured; teabot is fail-closed and will dispatch NOTHING until an allowlist is set") } 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) }