package config import ( "errors" "fmt" "io/fs" "os" "os/exec" "path/filepath" "strings" "github.com/BurntSushi/toml" ) const DefaultWebhookPort = 17421 type Settings struct { WebhookPort int `toml:"webhook_port"` BrainstormPrompt string `toml:"brainstorm_prompt"` BrainstormContinuePrompt string `toml:"brainstorm_continue_prompt"` ImplementPlanPrompt string `toml:"implement_plan_prompt"` ReviewPrompt string `toml:"review_prompt"` AutoReview bool `toml:"auto_review"` DevBranch string `toml:"dev_branch"` AutoBuildBranch string `toml:"auto_build_branch"` WorktreePostCreateScript string `toml:"worktree_post_create_script"` WorktreePreRemoveScript string `toml:"worktree_pre_remove_script"` ImplTabPreCreateScript string `toml:"impl_tab_pre_create_script"` ImplTabPostCloseScript string `toml:"impl_tab_post_close_script"` ProfilesDir string `toml:"profiles_dir"` SystemPromptCommand string `toml:"system_prompt_command"` } func DefaultSettings() *Settings { return &Settings{ WebhookPort: DefaultWebhookPort, AutoReview: true, DevBranch: "main", } } // configFilePath returns the path to the config file. It's a variable so tests can override it. var configFilePath = func() (string, error) { home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("resolve home dir: %w", err) } return filepath.Join(home, ".config", "superwork", "config.toml"), nil } func Load() (*Settings, error) { return LoadFrom("") } func LoadFrom(path string) (*Settings, error) { if path == "" { var err error path, err = configFilePath() if err != nil { return nil, err } } s := DefaultSettings() _, err := toml.DecodeFile(path, s) if errors.Is(err, fs.ErrNotExist) { return s, nil } if err != nil { return nil, fmt.Errorf("decode config %s: %w", path, err) } return s, nil } func Save(s *Settings) error { return SaveTo("", s) } func SaveTo(path string, s *Settings) error { if path == "" { var err error path, err = configFilePath() if err != nil { return err } } if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return fmt.Errorf("create config dir: %w", err) } f, err := os.Create(path) if err != nil { return fmt.Errorf("create config file: %w", err) } defer f.Close() if err := toml.NewEncoder(f).Encode(s); err != nil { return fmt.Errorf("encode config: %w", err) } return nil } func WorkspaceRoot() (string, error) { out, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() if err != nil { cwd, cerr := os.Getwd() if cerr != nil { return "", fmt.Errorf("get cwd: %w", cerr) } return cwd, nil } return strings.TrimSpace(string(out)), nil } func SpxDir(workspaceRoot string) string { return filepath.Join(workspaceRoot, ".spx") }