config.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. // Package config manages application configuration from various sources.
  2. package config
  3. import (
  4. "encoding/json"
  5. "fmt"
  6. "log/slog"
  7. "os"
  8. "os/user"
  9. "path/filepath"
  10. "strings"
  11. "github.com/spf13/viper"
  12. )
  13. // Data defines storage configuration.
  14. type Data struct {
  15. Directory string `json:"directory,omitempty"`
  16. }
  17. // TUIConfig defines the configuration for the Terminal User Interface.
  18. type TUIConfig struct {
  19. Theme string `json:"theme,omitempty"`
  20. CustomTheme map[string]any `json:"customTheme,omitempty"`
  21. }
  22. // ShellConfig defines the configuration for the shell used by the bash tool.
  23. type ShellConfig struct {
  24. Path string `json:"path,omitempty"`
  25. Args []string `json:"args,omitempty"`
  26. }
  27. // Config is the main configuration structure for the application.
  28. type Config struct {
  29. Data Data `json:"data"`
  30. WorkingDir string `json:"wd,omitempty"`
  31. Debug bool `json:"debug,omitempty"`
  32. DebugLSP bool `json:"debugLSP,omitempty"`
  33. ContextPaths []string `json:"contextPaths,omitempty"`
  34. TUI TUIConfig `json:"tui"`
  35. Shell ShellConfig `json:"shell,omitempty"`
  36. }
  37. // Application constants
  38. const (
  39. defaultDataDirectory = ".opencode"
  40. defaultLogLevel = "info"
  41. appName = "opencode"
  42. MaxTokensFallbackDefault = 4096
  43. )
  44. var defaultContextPaths = []string{
  45. ".github/copilot-instructions.md",
  46. ".cursorrules",
  47. ".cursor/rules/",
  48. "CLAUDE.md",
  49. "CLAUDE.local.md",
  50. "CONTEXT.md",
  51. "CONTEXT.local.md",
  52. "opencode.md",
  53. "opencode.local.md",
  54. "OpenCode.md",
  55. "OpenCode.local.md",
  56. "OPENCODE.md",
  57. "OPENCODE.local.md",
  58. }
  59. // Global configuration instance
  60. var cfg *Config
  61. // Load initializes the configuration from environment variables and config files.
  62. // If debug is true, debug mode is enabled and log level is set to debug.
  63. // It returns an error if configuration loading fails.
  64. func Load(workingDir string, debug bool) (*Config, error) {
  65. if cfg != nil {
  66. return cfg, nil
  67. }
  68. cfg = &Config{
  69. WorkingDir: workingDir,
  70. }
  71. configureViper()
  72. setDefaults(debug)
  73. // Read global config
  74. if err := readConfig(viper.ReadInConfig()); err != nil {
  75. return cfg, err
  76. }
  77. // Load and merge local config
  78. mergeLocalConfig(workingDir)
  79. // Apply configuration to the struct
  80. if err := viper.Unmarshal(cfg); err != nil {
  81. return cfg, fmt.Errorf("failed to unmarshal config: %w", err)
  82. }
  83. defaultLevel := slog.LevelInfo
  84. if cfg.Debug {
  85. defaultLevel = slog.LevelDebug
  86. }
  87. slog.SetLogLoggerLevel(defaultLevel)
  88. // Validate configuration
  89. if err := Validate(); err != nil {
  90. return cfg, fmt.Errorf("config validation failed: %w", err)
  91. }
  92. return cfg, nil
  93. }
  94. // configureViper sets up viper's configuration paths and environment variables.
  95. func configureViper() {
  96. viper.SetConfigName(fmt.Sprintf(".%s", appName))
  97. viper.SetConfigType("json")
  98. viper.AddConfigPath("$HOME")
  99. viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
  100. viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", appName))
  101. viper.SetEnvPrefix(strings.ToUpper(appName))
  102. viper.AutomaticEnv()
  103. }
  104. // setDefaults configures default values for configuration options.
  105. func setDefaults(debug bool) {
  106. viper.SetDefault("data.directory", defaultDataDirectory)
  107. viper.SetDefault("contextPaths", defaultContextPaths)
  108. viper.SetDefault("tui.theme", "opencode")
  109. if debug {
  110. viper.SetDefault("debug", true)
  111. viper.Set("log.level", "debug")
  112. } else {
  113. viper.SetDefault("debug", false)
  114. viper.SetDefault("log.level", defaultLogLevel)
  115. }
  116. }
  117. // readConfig handles the result of reading a configuration file.
  118. func readConfig(err error) error {
  119. if err == nil {
  120. return nil
  121. }
  122. // It's okay if the config file doesn't exist
  123. if _, ok := err.(viper.ConfigFileNotFoundError); ok {
  124. return nil
  125. }
  126. return fmt.Errorf("failed to read config: %w", err)
  127. }
  128. // mergeLocalConfig loads and merges configuration from the local directory.
  129. func mergeLocalConfig(workingDir string) {
  130. local := viper.New()
  131. local.SetConfigName(fmt.Sprintf(".%s", appName))
  132. local.SetConfigType("json")
  133. local.AddConfigPath(workingDir)
  134. // Merge local config if it exists
  135. if err := local.ReadInConfig(); err == nil {
  136. viper.MergeConfigMap(local.AllSettings())
  137. }
  138. }
  139. // Validate checks if the configuration is valid and applies defaults where needed.
  140. func Validate() error {
  141. if cfg == nil {
  142. return fmt.Errorf("config not loaded")
  143. }
  144. return nil
  145. }
  146. // Get returns the current configuration.
  147. // It's safe to call this function multiple times.
  148. func Get() *Config {
  149. return cfg
  150. }
  151. // WorkingDirectory returns the current working directory from the configuration.
  152. func WorkingDirectory() string {
  153. if cfg == nil {
  154. panic("config not loaded")
  155. }
  156. return cfg.WorkingDir
  157. }
  158. // GetHostname returns the system hostname or "User" if it can't be determined
  159. func GetHostname() (string, error) {
  160. hostname, err := os.Hostname()
  161. if err != nil {
  162. return "User", err
  163. }
  164. return hostname, nil
  165. }
  166. // GetUsername returns the current user's username
  167. func GetUsername() (string, error) {
  168. currentUser, err := user.Current()
  169. if err != nil {
  170. return "User", err
  171. }
  172. return currentUser.Username, nil
  173. }
  174. func updateCfgFile(updateCfg func(config *Config)) error {
  175. if cfg == nil {
  176. return fmt.Errorf("config not loaded")
  177. }
  178. // Get the config file path
  179. configFile := viper.ConfigFileUsed()
  180. var configData []byte
  181. if configFile == "" {
  182. homeDir, err := os.UserHomeDir()
  183. if err != nil {
  184. return fmt.Errorf("failed to get home directory: %w", err)
  185. }
  186. configFile = filepath.Join(homeDir, fmt.Sprintf(".%s.json", appName))
  187. slog.Info("config file not found, creating new one", "path", configFile)
  188. configData = []byte(`{}`)
  189. } else {
  190. // Read the existing config file
  191. data, err := os.ReadFile(configFile)
  192. if err != nil {
  193. return fmt.Errorf("failed to read config file: %w", err)
  194. }
  195. configData = data
  196. }
  197. // Parse the JSON
  198. var userCfg *Config
  199. if err := json.Unmarshal(configData, &userCfg); err != nil {
  200. return fmt.Errorf("failed to parse config file: %w", err)
  201. }
  202. updateCfg(userCfg)
  203. // Write the updated config back to file
  204. updatedData, err := json.MarshalIndent(userCfg, "", " ")
  205. if err != nil {
  206. return fmt.Errorf("failed to marshal config: %w", err)
  207. }
  208. if err := os.WriteFile(configFile, updatedData, 0o644); err != nil {
  209. return fmt.Errorf("failed to write config file: %w", err)
  210. }
  211. return nil
  212. }
  213. // UpdateTheme updates the theme in the configuration and writes it to the config file.
  214. func UpdateTheme(themeName string) error {
  215. if cfg == nil {
  216. return fmt.Errorf("config not loaded")
  217. }
  218. // Update the in-memory config
  219. cfg.TUI.Theme = themeName
  220. // Update the file config
  221. return updateCfgFile(func(config *Config) {
  222. config.TUI.Theme = themeName
  223. })
  224. }