This commit is contained in:
2026-06-23 05:02:15 +08:00
commit e6f1776d4f
264 changed files with 54215 additions and 0 deletions
+114
View File
@@ -0,0 +1,114 @@
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")
}
+95
View File
@@ -0,0 +1,95 @@
package config_test
import (
"path/filepath"
"testing"
"superwork-tui/internal/config"
)
func TestDefaultSettings(t *testing.T) {
s := config.DefaultSettings()
if s.WebhookPort != 17421 {
t.Errorf("WebhookPort: got %d, want 17421", s.WebhookPort)
}
if !s.AutoReview {
t.Error("AutoReview: want true")
}
if s.DevBranch != "main" {
t.Errorf("DevBranch: got %q, want main", s.DevBranch)
}
if s.ProfilesDir != "" {
t.Errorf("ProfilesDir: want empty, got %q", s.ProfilesDir)
}
}
func TestSaveLoadRoundtrip(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.toml")
original := &config.Settings{
WebhookPort: 12345,
AutoReview: false,
DevBranch: "develop",
AutoBuildBranch: "ci",
ProfilesDir: "/some/path",
}
if err := config.SaveTo(path, original); err != nil {
t.Fatalf("SaveTo: %v", err)
}
loaded, err := config.LoadFrom(path)
if err != nil {
t.Fatalf("LoadFrom: %v", err)
}
if loaded.WebhookPort != original.WebhookPort {
t.Errorf("WebhookPort: got %d, want %d", loaded.WebhookPort, original.WebhookPort)
}
if loaded.AutoReview != original.AutoReview {
t.Errorf("AutoReview: got %v, want %v", loaded.AutoReview, original.AutoReview)
}
if loaded.DevBranch != original.DevBranch {
t.Errorf("DevBranch: got %q, want %q", loaded.DevBranch, original.DevBranch)
}
if loaded.ProfilesDir != original.ProfilesDir {
t.Errorf("ProfilesDir: got %q, want %q", loaded.ProfilesDir, original.ProfilesDir)
}
}
func TestSystemPromptCommandDefault(t *testing.T) {
s := config.DefaultSettings()
if s.SystemPromptCommand != "" {
t.Errorf("SystemPromptCommand: want empty, got %q", s.SystemPromptCommand)
}
}
func TestSystemPromptCommandRoundtrip(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.toml")
original := &config.Settings{
WebhookPort: 12345,
SystemPromptCommand: "echo hello",
}
if err := config.SaveTo(path, original); err != nil {
t.Fatalf("SaveTo: %v", err)
}
loaded, err := config.LoadFrom(path)
if err != nil {
t.Fatalf("LoadFrom: %v", err)
}
if loaded.SystemPromptCommand != original.SystemPromptCommand {
t.Errorf("SystemPromptCommand: got %q, want %q", loaded.SystemPromptCommand, original.SystemPromptCommand)
}
}
func TestLoadMissingFile_ReturnsDefaults(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "nonexistent.toml")
s, err := config.LoadFrom(path)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if s.WebhookPort != 17421 {
t.Errorf("WebhookPort: got %d, want 17421", s.WebhookPort)
}
}