package config import ( "fmt" "os" "path/filepath" "github.com/muyue/muyue/internal/secret" "gopkg.in/yaml.v3" ) type Profile struct { Name string `yaml:"name"` Pseudo string `yaml:"pseudo"` Email string `yaml:"email"` Languages []string `yaml:"languages"` Preferences struct { Editor string `yaml:"editor"` Shell string `yaml:"shell"` Theme string `yaml:"theme"` DefaultAI string `yaml:"default_ai"` AutoUpdate bool `yaml:"auto_update"` CheckOnStart bool `yaml:"check_on_start"` Language string `yaml:"language"` KeyboardLayout string `yaml:"keyboard_layout"` } `yaml:"preferences"` } type AIProvider struct { Name string `yaml:"name"` APIKey string `yaml:"api_key,omitempty"` BaseURL string `yaml:"base_url,omitempty"` Model string `yaml:"model"` Active bool `yaml:"active"` } type ToolConfig struct { Name string `yaml:"name"` Installed bool `yaml:"installed"` Version string `yaml:"version"` AutoUpdate bool `yaml:"auto_update"` } type MuyueConfig struct { Version string `yaml:"version"` Profile Profile `yaml:"profile"` AI struct { Providers []AIProvider `yaml:"providers"` } `yaml:"ai"` Tools []ToolConfig `yaml:"tools"` BMAD struct { Installed bool `yaml:"installed"` Version string `yaml:"version"` Global bool `yaml:"global"` } `yaml:"bmad"` Terminal struct { CustomPrompt bool `yaml:"custom_prompt"` PromptTheme string `yaml:"prompt_theme"` } `yaml:"terminal"` } func ConfigDir() (string, error) { configDir, err := os.UserConfigDir() if err != nil { return "", err } dir := filepath.Join(configDir, "muyue") legacyDir := filepath.Join(homeDir(), ".muyue") if _, err := os.Stat(legacyDir); err == nil { if _, err := os.Stat(dir); err != nil { os.Rename(legacyDir, dir) } } return dir, nil } func homeDir() string { home, err := os.UserHomeDir() if err != nil { return "/" } return home } func ConfigPath() (string, error) { dir, err := ConfigDir() if err != nil { return "", err } return filepath.Join(dir, "config.yaml"), nil } func Exists() bool { path, err := ConfigPath() if err != nil { return false } _, err = os.Stat(path) return err == nil } func Load() (*MuyueConfig, error) { path, err := ConfigPath() if err != nil { return nil, err } data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("reading config: %w", err) } var cfg MuyueConfig if err := yaml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("parsing config: %w", err) } // Decrypt API keys for i := range cfg.AI.Providers { if cfg.AI.Providers[i].APIKey != "" { decrypted, err := secret.Decrypt(cfg.AI.Providers[i].APIKey) if err != nil { decrypted = cfg.AI.Providers[i].APIKey } cfg.AI.Providers[i].APIKey = decrypted } } return &cfg, nil } func Save(cfg *MuyueConfig) error { dir, err := ConfigDir() if err != nil { return err } if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("creating config dir: %w", err) } // Encrypt API keys before saving saveCfg := *cfg saveCfg.AI.Providers = make([]AIProvider, len(cfg.AI.Providers)) for i, p := range cfg.AI.Providers { saveCfg.AI.Providers[i] = p if p.APIKey != "" && !secret.IsEncrypted(p.APIKey) { enc, err := secret.Encrypt(p.APIKey) if err == nil { saveCfg.AI.Providers[i].APIKey = enc } } } path := filepath.Join(dir, "config.yaml") data, err := yaml.Marshal(&saveCfg) if err != nil { return fmt.Errorf("marshaling config: %w", err) } if err := os.WriteFile(path, data, 0600); err != nil { return fmt.Errorf("writing config: %w", err) } return nil } func Default() *MuyueConfig { cfg := &MuyueConfig{ Version: "0.1.0", Profile: Profile{ Name: "", Pseudo: "muyue", Languages: []string{}, }, } cfg.Profile.Preferences.DefaultAI = "minimax" cfg.Profile.Preferences.AutoUpdate = true cfg.Profile.Preferences.CheckOnStart = true cfg.Profile.Preferences.Theme = "charm" cfg.Profile.Preferences.Language = "fr" cfg.Profile.Preferences.KeyboardLayout = "azerty" cfg.AI.Providers = []AIProvider{ { Name: "minimax", Model: "MiniMax-M2.7", BaseURL: "https://api.minimax.io/v1", Active: true, }, { Name: "zai", Model: "glm", Active: false, }, { Name: "anthropic", Model: "claude-sonnet-4-20250514", Active: false, }, { Name: "openai", Model: "gpt-4o", BaseURL: "https://api.openai.com/v1", Active: false, }, { Name: "ollama", Model: "llama3", BaseURL: "http://localhost:11434/api", Active: false, }, } cfg.BMAD.Global = true cfg.Terminal.CustomPrompt = true cfg.Terminal.PromptTheme = "zerotwo" return cfg }