package config import ( "fmt" "os" "path/filepath" "strings" "time" "github.com/spf13/viper" ) type Config struct { App AppConfig `mapstructure:"app"` OAuth OAuthConfig `mapstructure:"oauth"` Google GoogleConfig `mapstructure:"google"` Server ServerConfig `mapstructure:"server"` API APIConfig `mapstructure:"api"` MCP MCPConfig `mapstructure:"mcp"` Security SecurityConfig `mapstructure:"security"` Output OutputConfig `mapstructure:"output"` } type AppConfig struct { Name string `mapstructure:"name"` Env string `mapstructure:"env"` LogLevel string `mapstructure:"log_level"` DataDir string `mapstructure:"data_dir"` } type OAuthConfig struct { ClientCredentialsFile string `mapstructure:"client_credentials_file"` TokenStoreFile string `mapstructure:"token_store_file"` DefaultPort int `mapstructure:"default_port"` OpenBrowser bool `mapstructure:"open_browser"` ManualFallback bool `mapstructure:"manual_fallback"` CallbackPath string `mapstructure:"callback_path"` } type GoogleConfig struct { Scopes []string `mapstructure:"scopes"` DefaultCalendarID string `mapstructure:"default_calendar_id"` } type ServerConfig struct { Bind string `mapstructure:"bind"` Port int `mapstructure:"port"` ReadTimeout time.Duration `mapstructure:"read_timeout"` WriteTimeout time.Duration `mapstructure:"write_timeout"` ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"` } type APIConfig struct { Enabled bool `mapstructure:"enabled"` BasePath string `mapstructure:"base_path"` Auth AuthCfg `mapstructure:"auth"` } type AuthCfg struct { OAuthBearerEnabled bool `mapstructure:"oauth_bearer_enabled"` APIKeyEnabled bool `mapstructure:"api_key_enabled"` } type MCPConfig struct { Enabled bool `mapstructure:"enabled"` Stdio bool `mapstructure:"stdio"` HTTP bool `mapstructure:"http"` HTTPPath string `mapstructure:"http_path"` ProtocolVersion string `mapstructure:"protocol_version"` SessionRequired bool `mapstructure:"session_required"` } type SecurityConfig struct { APIKeysFile string `mapstructure:"api_keys_file"` RedactSecretsInLogs bool `mapstructure:"redact_secrets_in_logs"` RequireTLSForRemote bool `mapstructure:"require_tls_for_remote_http"` } type OutputConfig struct { Format string `mapstructure:"format"` Timezone string `mapstructure:"timezone"` } func Load(cfgFile, profile string) (*Config, error) { v := viper.New() setDefaults(v) v.SetEnvPrefix("GOCALGOO") v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.AutomaticEnv() if cfgFile != "" { v.SetConfigFile(cfgFile) } else { home, err := os.UserHomeDir() if err != nil { return nil, fmt.Errorf("get home dir: %w", err) } cfgName := "config" if profile != "" { cfgName = "config." + profile } v.SetConfigName(cfgName) v.SetConfigType("yaml") v.AddConfigPath(filepath.Join(home, ".config", "gocalgoo")) v.AddConfigPath(".") v.AddConfigPath("./configs") } if err := v.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); !ok { return nil, fmt.Errorf("read config: %w", err) } } var cfg Config if err := v.Unmarshal(&cfg); err != nil { return nil, fmt.Errorf("unmarshal config: %w", err) } cfg.OAuth.ClientCredentialsFile = expandHome(cfg.OAuth.ClientCredentialsFile) cfg.OAuth.TokenStoreFile = expandHome(cfg.OAuth.TokenStoreFile) cfg.Security.APIKeysFile = expandHome(cfg.Security.APIKeysFile) cfg.App.DataDir = expandHome(cfg.App.DataDir) return &cfg, nil } func Validate(cfg *Config) error { if cfg.OAuth.ClientCredentialsFile == "" { return fmt.Errorf("oauth.client_credentials_file is required") } if cfg.OAuth.TokenStoreFile == "" { return fmt.Errorf("oauth.token_store_file is required") } if len(cfg.Google.Scopes) == 0 { return fmt.Errorf("google.scopes must not be empty") } if cfg.Server.Port < 1 || cfg.Server.Port > 65535 { return fmt.Errorf("server.port must be 1-65535") } return nil } func setDefaults(v *viper.Viper) { home, _ := os.UserHomeDir() v.SetDefault("app.name", "GoCalGoo") v.SetDefault("app.env", "development") v.SetDefault("app.log_level", "info") v.SetDefault("app.data_dir", filepath.Join(home, ".local", "share", "gocalgoo")) v.SetDefault("oauth.client_credentials_file", filepath.Join(home, ".config", "gocalgoo", "credentials.json")) v.SetDefault("oauth.token_store_file", filepath.Join(home, ".config", "gocalgoo", "tokens.json")) v.SetDefault("oauth.default_port", 53682) v.SetDefault("oauth.open_browser", true) v.SetDefault("oauth.manual_fallback", true) v.SetDefault("oauth.callback_path", "/oauth/callback") v.SetDefault("google.scopes", []string{ "https://www.googleapis.com/auth/calendar", "https://www.googleapis.com/auth/contacts", }) v.SetDefault("google.default_calendar_id", "primary") v.SetDefault("server.bind", "127.0.0.1") v.SetDefault("server.port", 8080) v.SetDefault("server.read_timeout", "15s") v.SetDefault("server.write_timeout", "30s") v.SetDefault("server.shutdown_timeout", "10s") v.SetDefault("api.enabled", true) v.SetDefault("api.base_path", "/api/v1") v.SetDefault("api.auth.oauth_bearer_enabled", true) v.SetDefault("api.auth.api_key_enabled", true) v.SetDefault("mcp.enabled", true) v.SetDefault("mcp.stdio", true) v.SetDefault("mcp.http", true) v.SetDefault("mcp.http_path", "/mcp") v.SetDefault("mcp.protocol_version", "2025-06-18") v.SetDefault("mcp.session_required", true) v.SetDefault("security.api_keys_file", filepath.Join(home, ".config", "gocalgoo", "api-keys.yaml")) v.SetDefault("security.redact_secrets_in_logs", true) v.SetDefault("security.require_tls_for_remote_http", false) v.SetDefault("output.format", "table") v.SetDefault("output.timezone", "UTC") } func expandHome(path string) string { if path == "" { return path } if strings.HasPrefix(path, "~/") { home, err := os.UserHomeDir() if err != nil { return path } return filepath.Join(home, path[2:]) } return path }