183 lines
5.7 KiB

package cmd
import (
// todo: some of this config definition/usage can be moved to other packages
type config struct {
// bot credentials + github info
selfHandle string
selfEmail string
githubToken string
openAIToken string
// remote repo info
repos []string
// local paths
localRepoPath string
// program settings
usersToListenTo []string
requiredIssueLabels []string
waitDuration time.Duration
debugDir string
func getConfig() config {
return config{
selfHandle: viper.GetString("handle"),
selfEmail: viper.GetString("email"),
githubToken: viper.GetString("github-token"),
openAIToken: viper.GetString("open-ai-token"),
repos: viper.GetStringSlice("repos"),
localRepoPath: viper.GetString("local-repo-path"),
usersToListenTo: viper.GetStringSlice("users-to-listen-to"),
requiredIssueLabels: viper.GetStringSlice("required-issue-labels"),
waitDuration: viper.GetDuration("wait-duration"),
debugDir: viper.GetString("debug-dir"),
func getPullPal(ctx context.Context, cfg config) (*pullpal.PullPal, error) {
// TODO figure out debug logging
log, err := zap.NewProduction()
if err != nil {
//log := zap.L()
author := vc.Author{
Email: cfg.selfEmail,
Handle: cfg.selfHandle,
Token: cfg.githubToken,
listIssueOptions := vc.ListIssueOptions{
Handles: cfg.usersToListenTo,
Labels: cfg.requiredIssueLabels,
// TODO make model configurable
ppCfg := pullpal.Config{
WaitDuration: cfg.waitDuration,
LocalRepoPath: cfg.localRepoPath,
Repos: cfg.repos,
Self: author,
ListIssueOptions: listIssueOptions,
// TODO configurable model
Model: openai.GPT4,
OpenAIToken: cfg.openAIToken,
DebugDir: cfg.debugDir,
p, err := pullpal.NewPullPal(ctx, log.Named("pullpal"), ppCfg)
return p, err
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Short: "run an automated digital assitant to monitor and make code changes to a github repository",
Run: func(cmd *cobra.Command, args []string) {
cfg := getConfig()
p, err := getPullPal(cmd.Context(), cfg)
if err != nil {
fmt.Println("error creating new pull pal", err)
fmt.Println("Successfully initialized pull pal")
err = p.Run()
if err != nil {
fmt.Println("error running", err)
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
var cfgFile string
func init() {
// TODO make config values requried
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.pull-pal.yaml)")
rootCmd.PersistentFlags().StringP("handle", "u", "HANDLE", "handle to use for version control actions")
rootCmd.PersistentFlags().StringP("email", "e", "EMAIL", "email to use for version control actions")
rootCmd.PersistentFlags().StringP("github-token", "t", "GITHUB TOKEN", "token for authenticating Github actions")
rootCmd.PersistentFlags().StringP("open-ai-token", "k", "OPENAI TOKEN", "token for authenticating OpenAI")
rootCmd.PersistentFlags().StringSliceP("repos", "r", []string{}, "a list of git repositories that Pull Pal will monitor")
rootCmd.PersistentFlags().StringP("local-repo-path", "l", "/tmp/pullpalrepo", "local path to check out ephemeral repository in")
rootCmd.PersistentFlags().StringSliceP("users-to-listen-to", "a", []string{}, "a list of Github users that Pull Pal will respond to")
rootCmd.PersistentFlags().StringSliceP("required-issue-labels", "i", []string{}, "a list of labels that are required for Pull Pal to select an issue")
rootCmd.PersistentFlags().Duration("wait-time", 30*time.Second, "the amount of time Pull Pal should wait when no issues or comments are found to address")
rootCmd.PersistentFlags().StringP("debug-dir", "d", "", "the path to use for the pull pal debug directory")
viper.BindPFlag("handle", rootCmd.PersistentFlags().Lookup("handle"))
viper.BindPFlag("email", rootCmd.PersistentFlags().Lookup("email"))
viper.BindPFlag("github-token", rootCmd.PersistentFlags().Lookup("github-token"))
viper.BindPFlag("open-ai-token", rootCmd.PersistentFlags().Lookup("open-ai-token"))
viper.BindPFlag("repos", rootCmd.PersistentFlags().Lookup("repos"))
viper.BindPFlag("local-repo-path", rootCmd.PersistentFlags().Lookup("local-repo-path"))
viper.BindPFlag("users-to-listen-to", rootCmd.PersistentFlags().Lookup("users-to-listen-to"))
viper.BindPFlag("required-issue-labels", rootCmd.PersistentFlags().Lookup("required-issue-labels"))
viper.BindPFlag("wait-time", rootCmd.PersistentFlags().Lookup("wait-time"))
viper.BindPFlag("debug-dir", rootCmd.PersistentFlags().Lookup("debug-dir"))
func initConfig() {
if cfgFile != "" {
fmt.Println("cfg file exists")
// Use config file from the flag.
} else {
// Find home directory.
home, err := os.UserHomeDir()
// Search config in home directory with name ".cobra" (without extension).
// TODO figure out how to get env variables to work
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())