|
|
@@ -1,266 +0,0 @@
|
|
|
-// Package config manages application configuration from various sources.
|
|
|
-package config
|
|
|
-
|
|
|
-import (
|
|
|
- "encoding/json"
|
|
|
- "fmt"
|
|
|
- "log/slog"
|
|
|
- "os"
|
|
|
- "os/user"
|
|
|
- "path/filepath"
|
|
|
- "strings"
|
|
|
-
|
|
|
- "github.com/spf13/viper"
|
|
|
-)
|
|
|
-
|
|
|
-// Data defines storage configuration.
|
|
|
-type Data struct {
|
|
|
- Directory string `json:"directory,omitempty"`
|
|
|
-}
|
|
|
-
|
|
|
-// TUIConfig defines the configuration for the Terminal User Interface.
|
|
|
-type TUIConfig struct {
|
|
|
- Theme string `json:"theme,omitempty"`
|
|
|
- CustomTheme map[string]any `json:"customTheme,omitempty"`
|
|
|
-}
|
|
|
-
|
|
|
-// ShellConfig defines the configuration for the shell used by the bash tool.
|
|
|
-type ShellConfig struct {
|
|
|
- Path string `json:"path,omitempty"`
|
|
|
- Args []string `json:"args,omitempty"`
|
|
|
-}
|
|
|
-
|
|
|
-// Config is the main configuration structure for the application.
|
|
|
-type Config struct {
|
|
|
- Data Data `json:"data"`
|
|
|
- WorkingDir string `json:"wd,omitempty"`
|
|
|
- Debug bool `json:"debug,omitempty"`
|
|
|
- DebugLSP bool `json:"debugLSP,omitempty"`
|
|
|
- ContextPaths []string `json:"contextPaths,omitempty"`
|
|
|
- TUI TUIConfig `json:"tui"`
|
|
|
- Shell ShellConfig `json:"shell,omitempty"`
|
|
|
-}
|
|
|
-
|
|
|
-// Application constants
|
|
|
-const (
|
|
|
- defaultDataDirectory = ".opencode"
|
|
|
- defaultLogLevel = "info"
|
|
|
- appName = "opencode"
|
|
|
-
|
|
|
- MaxTokensFallbackDefault = 4096
|
|
|
-)
|
|
|
-
|
|
|
-var defaultContextPaths = []string{
|
|
|
- ".github/copilot-instructions.md",
|
|
|
- ".cursorrules",
|
|
|
- ".cursor/rules/",
|
|
|
- "CLAUDE.md",
|
|
|
- "CLAUDE.local.md",
|
|
|
- "CONTEXT.md",
|
|
|
- "CONTEXT.local.md",
|
|
|
- "opencode.md",
|
|
|
- "opencode.local.md",
|
|
|
- "OpenCode.md",
|
|
|
- "OpenCode.local.md",
|
|
|
- "OPENCODE.md",
|
|
|
- "OPENCODE.local.md",
|
|
|
-}
|
|
|
-
|
|
|
-// Global configuration instance
|
|
|
-var cfg *Config
|
|
|
-
|
|
|
-// Load initializes the configuration from environment variables and config files.
|
|
|
-// If debug is true, debug mode is enabled and log level is set to debug.
|
|
|
-// It returns an error if configuration loading fails.
|
|
|
-func Load(workingDir string, debug bool) (*Config, error) {
|
|
|
- if cfg != nil {
|
|
|
- return cfg, nil
|
|
|
- }
|
|
|
-
|
|
|
- cfg = &Config{
|
|
|
- WorkingDir: workingDir,
|
|
|
- }
|
|
|
-
|
|
|
- configureViper()
|
|
|
- setDefaults(debug)
|
|
|
-
|
|
|
- // Read global config
|
|
|
- if err := readConfig(viper.ReadInConfig()); err != nil {
|
|
|
- return cfg, err
|
|
|
- }
|
|
|
-
|
|
|
- // Load and merge local config
|
|
|
- mergeLocalConfig(workingDir)
|
|
|
-
|
|
|
- // Apply configuration to the struct
|
|
|
- if err := viper.Unmarshal(cfg); err != nil {
|
|
|
- return cfg, fmt.Errorf("failed to unmarshal config: %w", err)
|
|
|
- }
|
|
|
-
|
|
|
- defaultLevel := slog.LevelInfo
|
|
|
- if cfg.Debug {
|
|
|
- defaultLevel = slog.LevelDebug
|
|
|
- }
|
|
|
- slog.SetLogLoggerLevel(defaultLevel)
|
|
|
-
|
|
|
- // Validate configuration
|
|
|
- if err := Validate(); err != nil {
|
|
|
- return cfg, fmt.Errorf("config validation failed: %w", err)
|
|
|
- }
|
|
|
- return cfg, nil
|
|
|
-}
|
|
|
-
|
|
|
-// configureViper sets up viper's configuration paths and environment variables.
|
|
|
-func configureViper() {
|
|
|
- viper.SetConfigName(fmt.Sprintf(".%s", appName))
|
|
|
- viper.SetConfigType("json")
|
|
|
- viper.AddConfigPath("$HOME")
|
|
|
- viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
|
|
|
- viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", appName))
|
|
|
- viper.SetEnvPrefix(strings.ToUpper(appName))
|
|
|
- viper.AutomaticEnv()
|
|
|
-}
|
|
|
-
|
|
|
-// setDefaults configures default values for configuration options.
|
|
|
-func setDefaults(debug bool) {
|
|
|
- viper.SetDefault("data.directory", defaultDataDirectory)
|
|
|
- viper.SetDefault("contextPaths", defaultContextPaths)
|
|
|
- viper.SetDefault("tui.theme", "opencode")
|
|
|
-
|
|
|
- if debug {
|
|
|
- viper.SetDefault("debug", true)
|
|
|
- viper.Set("log.level", "debug")
|
|
|
- } else {
|
|
|
- viper.SetDefault("debug", false)
|
|
|
- viper.SetDefault("log.level", defaultLogLevel)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// readConfig handles the result of reading a configuration file.
|
|
|
-func readConfig(err error) error {
|
|
|
- if err == nil {
|
|
|
- return nil
|
|
|
- }
|
|
|
-
|
|
|
- // It's okay if the config file doesn't exist
|
|
|
- if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
|
|
- return nil
|
|
|
- }
|
|
|
-
|
|
|
- return fmt.Errorf("failed to read config: %w", err)
|
|
|
-}
|
|
|
-
|
|
|
-// mergeLocalConfig loads and merges configuration from the local directory.
|
|
|
-func mergeLocalConfig(workingDir string) {
|
|
|
- local := viper.New()
|
|
|
- local.SetConfigName(fmt.Sprintf(".%s", appName))
|
|
|
- local.SetConfigType("json")
|
|
|
- local.AddConfigPath(workingDir)
|
|
|
-
|
|
|
- // Merge local config if it exists
|
|
|
- if err := local.ReadInConfig(); err == nil {
|
|
|
- viper.MergeConfigMap(local.AllSettings())
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// Validate checks if the configuration is valid and applies defaults where needed.
|
|
|
-func Validate() error {
|
|
|
- if cfg == nil {
|
|
|
- return fmt.Errorf("config not loaded")
|
|
|
- }
|
|
|
-
|
|
|
- return nil
|
|
|
-}
|
|
|
-
|
|
|
-// Get returns the current configuration.
|
|
|
-// It's safe to call this function multiple times.
|
|
|
-func Get() *Config {
|
|
|
- return cfg
|
|
|
-}
|
|
|
-
|
|
|
-// WorkingDirectory returns the current working directory from the configuration.
|
|
|
-func WorkingDirectory() string {
|
|
|
- if cfg == nil {
|
|
|
- panic("config not loaded")
|
|
|
- }
|
|
|
- return cfg.WorkingDir
|
|
|
-}
|
|
|
-
|
|
|
-// GetHostname returns the system hostname or "User" if it can't be determined
|
|
|
-func GetHostname() (string, error) {
|
|
|
- hostname, err := os.Hostname()
|
|
|
- if err != nil {
|
|
|
- return "User", err
|
|
|
- }
|
|
|
- return hostname, nil
|
|
|
-}
|
|
|
-
|
|
|
-// GetUsername returns the current user's username
|
|
|
-func GetUsername() (string, error) {
|
|
|
- currentUser, err := user.Current()
|
|
|
- if err != nil {
|
|
|
- return "User", err
|
|
|
- }
|
|
|
- return currentUser.Username, nil
|
|
|
-}
|
|
|
-
|
|
|
-func updateCfgFile(updateCfg func(config *Config)) error {
|
|
|
- if cfg == nil {
|
|
|
- return fmt.Errorf("config not loaded")
|
|
|
- }
|
|
|
-
|
|
|
- // Get the config file path
|
|
|
- configFile := viper.ConfigFileUsed()
|
|
|
- var configData []byte
|
|
|
- if configFile == "" {
|
|
|
- homeDir, err := os.UserHomeDir()
|
|
|
- if err != nil {
|
|
|
- return fmt.Errorf("failed to get home directory: %w", err)
|
|
|
- }
|
|
|
- configFile = filepath.Join(homeDir, fmt.Sprintf(".%s.json", appName))
|
|
|
- slog.Info("config file not found, creating new one", "path", configFile)
|
|
|
- configData = []byte(`{}`)
|
|
|
- } else {
|
|
|
- // Read the existing config file
|
|
|
- data, err := os.ReadFile(configFile)
|
|
|
- if err != nil {
|
|
|
- return fmt.Errorf("failed to read config file: %w", err)
|
|
|
- }
|
|
|
- configData = data
|
|
|
- }
|
|
|
-
|
|
|
- // Parse the JSON
|
|
|
- var userCfg *Config
|
|
|
- if err := json.Unmarshal(configData, &userCfg); err != nil {
|
|
|
- return fmt.Errorf("failed to parse config file: %w", err)
|
|
|
- }
|
|
|
-
|
|
|
- updateCfg(userCfg)
|
|
|
-
|
|
|
- // Write the updated config back to file
|
|
|
- updatedData, err := json.MarshalIndent(userCfg, "", " ")
|
|
|
- if err != nil {
|
|
|
- return fmt.Errorf("failed to marshal config: %w", err)
|
|
|
- }
|
|
|
-
|
|
|
- if err := os.WriteFile(configFile, updatedData, 0o644); err != nil {
|
|
|
- return fmt.Errorf("failed to write config file: %w", err)
|
|
|
- }
|
|
|
-
|
|
|
- return nil
|
|
|
-}
|
|
|
-
|
|
|
-// UpdateTheme updates the theme in the configuration and writes it to the config file.
|
|
|
-func UpdateTheme(themeName string) error {
|
|
|
- if cfg == nil {
|
|
|
- return fmt.Errorf("config not loaded")
|
|
|
- }
|
|
|
-
|
|
|
- // Update the in-memory config
|
|
|
- cfg.TUI.Theme = themeName
|
|
|
-
|
|
|
- // Update the file config
|
|
|
- return updateCfgFile(func(config *Config) {
|
|
|
- config.TUI.Theme = themeName
|
|
|
- })
|
|
|
-}
|