package main import ( "context" "log" "log/slog" "os" "path/filepath" "strings" "git.schreifuchs.ch/schreifuchs/pierre-bot/internal/chatter" "git.schreifuchs.ch/schreifuchs/pierre-bot/internal/gitadapters/bitbucket" "git.schreifuchs.ch/schreifuchs/pierre-bot/internal/gitadapters/gitea" "git.schreifuchs.ch/schreifuchs/pierre-bot/internal/pierre" "github.com/alecthomas/kong" kongyaml "github.com/alecthomas/kong-yaml" ) type BitbucketConfig struct { BaseURL string `help:"Bitbucket Base URL (e.g. https://bitbucket.example.com)" env:"BITBUCKET_URL"` Token string `help:"Bearer Token" env:"BITBUCKET_TOKEN"` } type GiteaConfig struct { BaseURL string `help:"Gitea Base URL (e.g. https://gitea.com)" env:"GITEA_URL"` Token string `help:"API Token" env:"GITEA_TOKEN"` } type RepoArgs struct { Owner string `arg:"" help:"Project Key or Owner" env:"PIERRE_OWNER"` Repo string `arg:"" help:"Repository Slug" env:"PIERRE_REPO"` PRID int `arg:"" help:"Pull Request ID" name:"pr"` } type LLMConfig struct { Provider string `help:"Provider for llm (ollama or gemini)" required:"" env:"LLM_PROVIDER"` BaseURL string `help:"Endpoint for provider (only for ollama)" env:"LLM_BASE_URL"` APIKey string `help:"APIKey for provider" env:"LLM_API_KEY"` Model string `help:"Model to use" env:"LLM_MODEL"` } // ReviewConfig holds the review‑specific CLI options. // The `default:"60000"` tag sets an integer default of 60 KB – Kong parses the string value into the int field, which can be confusing for readers. type ReviewConfig struct { MaxChunkSize int `help:"Maximum diff chunk size in bytes" default:"60000"` Guidelines []string `help:"Project guidelines to prepend" sep:","` DisableComments bool `help:"Disable posting comments (dry run)"` } type Config struct { LogLevel string `help:"Log verbosity: debug, info, warn, error"` // Embedding ReviewConfig with a prefix changes flag names to `--review-…`. // Existing configuration files using the old flag names will need to be updated. // Consider keeping backwards compatibility if required. Review ReviewConfig `embed:"" prefix:"review-"` GitProvider string `help:"Git provider (bitbucket or gitea)" env:"GIT_PROVIDER"` Bitbucket BitbucketConfig `embed:"" prefix:"bitbucket-"` Gitea GiteaConfig `embed:"" prefix:"gitea-"` Repo RepoArgs `embed:""` LLM LLMConfig `embed:"" prefix:"llm-"` Config kong.ConfigFlag `help:"Path to a YAML config file"` } func main() { cfg := &Config{} home, err := os.UserHomeDir() if err != nil { log.Fatalf("could not find home directory: %v", err) } defaultConfig := filepath.Join(home, ".config", "pierre", "config.yaml") // Parse flags, env vars, and config files kong.Parse(cfg, kong.Name("pierre"), kong.Description("AI-powered Pull Request reviewer"), kong.UsageOnError(), kong.Configuration(kongyaml.Loader, "config.yaml", defaultConfig), ) // Configure global slog logger based on the --log-level flag lvl := slog.LevelInfo switch strings.ToLower(cfg.LogLevel) { case "debug": lvl = slog.LevelDebug case "info": lvl = slog.LevelInfo case "warn": lvl = slog.LevelWarn case "error": lvl = slog.LevelError } // Logs are sent to stderr so that stdout can be safely piped. // The review output (fmt.Printf) stays on stdout unchanged. logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: lvl})) slog.SetDefault(logger) // Auto-detect provider provider := cfg.GitProvider if provider == "" { if cfg.Bitbucket.BaseURL != "" && cfg.Gitea.BaseURL == "" { provider = "bitbucket" } else if cfg.Gitea.BaseURL != "" && cfg.Bitbucket.BaseURL == "" { provider = "gitea" } else if cfg.Bitbucket.BaseURL != "" && cfg.Gitea.BaseURL != "" { log.Fatal("Multiple git providers configured. Please specify one using --git-provider.") } else { log.Fatal("No git provider configured. Please provide Bitbucket or Gitea configuration.") } } var git pierre.GitAdapter switch provider { case "bitbucket": if cfg.Bitbucket.BaseURL == "" { log.Fatal("Bitbucket Base URL is required when using bitbucket provider.") } git = bitbucket.NewBitbucket(cfg.Bitbucket.BaseURL, cfg.Bitbucket.Token) case "gitea": if cfg.Gitea.BaseURL == "" { log.Fatal("Gitea Base URL is required when using gitea provider.") } var err error git, err = gitea.New(cfg.Gitea.BaseURL, cfg.Gitea.Token) if err != nil { log.Fatalf("Error initializing Gitea adapter: %v", err) } default: log.Fatalf("Unknown git provider: %s", provider) } // Initialize AI Adapter var ai pierre.ChatAdapter switch cfg.LLM.Provider { case "gemini": ai, err = chatter.NewGeminiAdapter(context.Background(), cfg.LLM.APIKey, cfg.LLM.Model) case "ollama": ai, err = chatter.NewOllamaAdapter(cfg.LLM.BaseURL, cfg.LLM.Model) case "openai": ai = chatter.NewOpenAIAdapter(cfg.LLM.APIKey, cfg.LLM.Model, cfg.LLM.BaseURL) default: log.Fatalf("%s is not a valid llm provider", cfg.LLM.Provider) } if err != nil { log.Fatalf("Error initializing AI: %v", err) } pierreService := pierre.New(ai, git, cfg.Review.MaxChunkSize, cfg.Review.Guidelines, cfg.Review.DisableComments) if err := pierreService.MakeReview(context.Background(), cfg.Repo.Owner, cfg.Repo.Repo, cfg.Repo.PRID); err != nil { log.Fatalf("Error during review: %v", err) } }