diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-16 03:58:02 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-16 03:58:02 +0200 |
| commit | 52938e05c1ab250cae1c19c29eaa050351559b3b (patch) | |
| tree | 85c7eab51fc4622838003e32afac1c744c1e1dcf /internal | |
| parent | 9e8ca4696f4fcbc1657eb7802aa52f8684fab202 (diff) | |
Split config_load.go into config_load.go and config_env.go
Move environment variable handling functions into config_env.go (269L),
keeping config_load.go at 697L. Both well under the 1000-line limit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/appconfig/config_env.go | 269 | ||||
| -rw-r--r-- | internal/appconfig/config_load.go | 261 |
2 files changed, 269 insertions, 261 deletions
diff --git a/internal/appconfig/config_env.go b/internal/appconfig/config_env.go new file mode 100644 index 0000000..5f576e0 --- /dev/null +++ b/internal/appconfig/config_env.go @@ -0,0 +1,269 @@ +package appconfig + +import ( + "log" + "os" + "strconv" + "strings" +) + +// --- Environment overrides --- + +// loadFromEnv constructs an App containing only fields set via HEXAI_* env vars. +// These values should take precedence over file config when merged. +func loadFromEnv(logger *log.Logger) *App { + var out App + any := applyCoreEnv(&out, logger) + any = applyProviderEnv(&out, logger) || any + any = applySurfaceEnv(&out, logger) || any + any = applyIgnoreEnv(&out) || any + any = applyMCPEnv(&out) || any + if !any { + return nil + } + return &out +} + +func applyCoreEnv(out *App, logger *log.Logger) bool { + any := false + any = applyEnvInt(&out.MaxTokens, "HEXAI_MAX_TOKENS", logger) || any + any = applyEnvString(&out.ContextMode, "HEXAI_CONTEXT_MODE") || any + any = applyEnvInt(&out.ContextWindowLines, "HEXAI_CONTEXT_WINDOW_LINES", logger) || any + any = applyEnvInt(&out.MaxContextTokens, "HEXAI_MAX_CONTEXT_TOKENS", logger) || any + any = applyEnvInt(&out.LogPreviewLimit, "HEXAI_LOG_PREVIEW_LIMIT", logger) || any + any = applyEnvInt(&out.RequestTimeout, "HEXAI_REQUEST_TIMEOUT", logger) || any + any = applyEnvInt(&out.ManualInvokeMinPrefix, "HEXAI_MANUAL_INVOKE_MIN_PREFIX", logger) || any + any = applyEnvInt(&out.CompletionDebounceMs, "HEXAI_COMPLETION_DEBOUNCE_MS", logger) || any + any = applyEnvInt(&out.CompletionThrottleMs, "HEXAI_COMPLETION_THROTTLE_MS", logger) || any + any = applyEnvFloat(&out.CodingTemperature, "HEXAI_CODING_TEMPERATURE", logger) || any + any = applyEnvCSV(&out.TriggerCharacters, "HEXAI_TRIGGER_CHARACTERS") || any + any = applyEnvString(&out.InlineOpen, "HEXAI_INLINE_OPEN") || any + any = applyEnvString(&out.InlineClose, "HEXAI_INLINE_CLOSE") || any + any = applyEnvString(&out.ChatSuffix, "HEXAI_CHAT_SUFFIX") || any + any = applyEnvCSV(&out.ChatPrefixes, "HEXAI_CHAT_PREFIXES") || any + any = applyEnvString(&out.Provider, "HEXAI_PROVIDER") || any + return any +} + +func applyProviderEnv(out *App, logger *log.Logger) bool { + picker := newModelPicker(out.Provider) + any := false + any = applyEnvString(&out.OpenAIBaseURL, "HEXAI_OPENAI_BASE_URL") || any + if model, ok := picker.pick("openai", getenvTrim("HEXAI_OPENAI_MODEL")); ok { + out.OpenAIModel = model + any = true + } + any = applyEnvFloat(&out.OpenAITemperature, "HEXAI_OPENAI_TEMPERATURE", logger) || any + + any = applyEnvString(&out.OpenRouterBaseURL, "HEXAI_OPENROUTER_BASE_URL") || any + if model, ok := picker.pick("openrouter", getenvTrim("HEXAI_OPENROUTER_MODEL")); ok { + out.OpenRouterModel = model + any = true + } + any = applyEnvFloat(&out.OpenRouterTemperature, "HEXAI_OPENROUTER_TEMPERATURE", logger) || any + + any = applyEnvString(&out.OllamaBaseURL, "HEXAI_OLLAMA_BASE_URL") || any + if model, ok := picker.pick("ollama", getenvTrim("HEXAI_OLLAMA_MODEL")); ok { + out.OllamaModel = model + any = true + } + any = applyEnvFloat(&out.OllamaTemperature, "HEXAI_OLLAMA_TEMPERATURE", logger) || any + + any = applyEnvString(&out.AnthropicBaseURL, "HEXAI_ANTHROPIC_BASE_URL") || any + if model, ok := picker.pick("anthropic", getenvTrim("HEXAI_ANTHROPIC_MODEL")); ok { + out.AnthropicModel = model + any = true + } + any = applyEnvFloat(&out.AnthropicTemperature, "HEXAI_ANTHROPIC_TEMPERATURE", logger) || any + return any +} + +func applySurfaceEnv(out *App, logger *log.Logger) bool { + any := false + if entries, ok := buildSurfaceEntryFromEnv("HEXAI_MODEL_COMPLETION", "HEXAI_TEMPERATURE_COMPLETION", "HEXAI_PROVIDER_COMPLETION", logger); ok { + out.CompletionConfigs = entries + any = true + } + if entries, ok := buildSurfaceEntryFromEnv("HEXAI_MODEL_CODE_ACTION", "HEXAI_TEMPERATURE_CODE_ACTION", "HEXAI_PROVIDER_CODE_ACTION", logger); ok { + out.CodeActionConfigs = entries + any = true + } + if entries, ok := buildSurfaceEntryFromEnv("HEXAI_MODEL_CHAT", "HEXAI_TEMPERATURE_CHAT", "HEXAI_PROVIDER_CHAT", logger); ok { + out.ChatConfigs = entries + any = true + } + if entries, ok := buildSurfaceEntryFromEnv("HEXAI_MODEL_CLI", "HEXAI_TEMPERATURE_CLI", "HEXAI_PROVIDER_CLI", logger); ok { + out.CLIConfigs = entries + any = true + } + return any +} + +func applyIgnoreEnv(out *App) bool { + any := false + any = applyEnvBoolPtr(&out.IgnoreGitignore, "HEXAI_IGNORE_GITIGNORE") || any + any = applyEnvCSV(&out.IgnoreExtraPatterns, "HEXAI_IGNORE_EXTRA_PATTERNS") || any + any = applyEnvBoolPtr(&out.IgnoreLSPNotify, "HEXAI_IGNORE_LSP_NOTIFY") || any + return any +} + +func applyMCPEnv(out *App) bool { + any := false + any = applyEnvString(&out.MCPPromptsDir, "HEXAI_MCP_PROMPTS_DIR") || any + any = applyEnvBool(&out.MCPSlashCommandSync, "HEXAI_MCP_SLASHCOMMAND_SYNC") || any + any = applyEnvString(&out.MCPSlashCommandDir, "HEXAI_MCP_SLASHCOMMAND_DIR") || any + return any +} + +func buildSurfaceEntryFromEnv(modelKey, tempKey, providerKey string, logger *log.Logger) ([]SurfaceConfig, bool) { + model := getenvTrim(modelKey) + tempPtr, tempSet := parseEnvFloatPtr(tempKey, logger) + provider := getenvTrim(providerKey) + if model == "" && provider == "" && !tempSet { + return nil, false + } + entry := SurfaceConfig{Provider: provider, Model: model} + if tempSet { + entry.Temperature = tempPtr + } + return []SurfaceConfig{entry}, true +} + +func applyEnvString(target *string, key string) bool { + value := getenvTrim(key) + if value == "" { + return false + } + *target = value + return true +} + +func applyEnvInt(target *int, key string, logger *log.Logger) bool { + value, ok := parseEnvInt(key, logger) + if !ok { + return false + } + *target = value + return true +} + +func applyEnvFloat(target **float64, key string, logger *log.Logger) bool { + value, ok := parseEnvFloatPtr(key, logger) + if !ok { + return false + } + *target = value + return true +} + +func applyEnvCSV(target *[]string, key string) bool { + value := getenvTrim(key) + if value == "" { + return false + } + parts := strings.Split(value, ",") + *target = nil + for _, p := range parts { + if t := strings.TrimSpace(p); t != "" { + *target = append(*target, t) + } + } + return true +} + +func applyEnvBool(target *bool, key string) bool { + value := getenvTrim(key) + if value == "" { + return false + } + *target = value == "true" || value == "1" + return true +} + +func applyEnvBoolPtr(target **bool, key string) bool { + value := getenvTrim(key) + if value == "" { + return false + } + parsed := value == "true" || value == "1" + *target = &parsed + return true +} + +func getenvTrim(key string) string { + return strings.TrimSpace(os.Getenv(key)) +} + +func parseEnvInt(key string, logger *log.Logger) (int, bool) { + value := getenvTrim(key) + if value == "" { + return 0, false + } + n, err := strconv.Atoi(value) + if err != nil { + if logger != nil { + logger.Printf("invalid %s: %v", key, err) + } + return 0, false + } + return n, true +} + +func parseEnvFloatPtr(key string, logger *log.Logger) (*float64, bool) { + value := getenvTrim(key) + if value == "" { + return nil, false + } + f, err := strconv.ParseFloat(value, 64) + if err != nil { + if logger != nil { + logger.Printf("invalid %s: %v", key, err) + } + return nil, false + } + return &f, true +} + +type modelPicker struct { + providerLower string + modelForce string + modelGeneric string + forceUsed bool + genericUsed bool +} + +func newModelPicker(provider string) *modelPicker { + return &modelPicker{ + providerLower: strings.ToLower(strings.TrimSpace(provider)), + modelForce: getenvTrim("HEXAI_MODEL_FORCE"), + modelGeneric: getenvTrim("HEXAI_MODEL"), + } +} + +func (p *modelPicker) pick(providerName, specific string) (string, bool) { + specific = strings.TrimSpace(specific) + nameLower := strings.ToLower(strings.TrimSpace(providerName)) + if p.modelForce != "" { + if p.providerLower == nameLower { + p.forceUsed = true + return p.modelForce, true + } + if p.providerLower == "" && !p.forceUsed { + p.forceUsed = true + return p.modelForce, true + } + } + if specific != "" { + return specific, true + } + if p.modelGeneric != "" { + if p.providerLower == nameLower { + return p.modelGeneric, true + } + if p.providerLower == "" && !p.genericUsed { + p.genericUsed = true + return p.modelGeneric, true + } + } + return "", false +} diff --git a/internal/appconfig/config_load.go b/internal/appconfig/config_load.go index b098b48..99461ca 100644 --- a/internal/appconfig/config_load.go +++ b/internal/appconfig/config_load.go @@ -695,264 +695,3 @@ func floatPtr(v float64) *float64 { f := v return &f } - -// --- Environment overrides --- - -// loadFromEnv constructs an App containing only fields set via HEXAI_* env vars. -// These values should take precedence over file config when merged. -func loadFromEnv(logger *log.Logger) *App { - var out App - any := applyCoreEnv(&out, logger) - any = applyProviderEnv(&out, logger) || any - any = applySurfaceEnv(&out, logger) || any - any = applyIgnoreEnv(&out) || any - any = applyMCPEnv(&out) || any - if !any { - return nil - } - return &out -} - -func applyCoreEnv(out *App, logger *log.Logger) bool { - any := false - any = applyEnvInt(&out.MaxTokens, "HEXAI_MAX_TOKENS", logger) || any - any = applyEnvString(&out.ContextMode, "HEXAI_CONTEXT_MODE") || any - any = applyEnvInt(&out.ContextWindowLines, "HEXAI_CONTEXT_WINDOW_LINES", logger) || any - any = applyEnvInt(&out.MaxContextTokens, "HEXAI_MAX_CONTEXT_TOKENS", logger) || any - any = applyEnvInt(&out.LogPreviewLimit, "HEXAI_LOG_PREVIEW_LIMIT", logger) || any - any = applyEnvInt(&out.RequestTimeout, "HEXAI_REQUEST_TIMEOUT", logger) || any - any = applyEnvInt(&out.ManualInvokeMinPrefix, "HEXAI_MANUAL_INVOKE_MIN_PREFIX", logger) || any - any = applyEnvInt(&out.CompletionDebounceMs, "HEXAI_COMPLETION_DEBOUNCE_MS", logger) || any - any = applyEnvInt(&out.CompletionThrottleMs, "HEXAI_COMPLETION_THROTTLE_MS", logger) || any - any = applyEnvFloat(&out.CodingTemperature, "HEXAI_CODING_TEMPERATURE", logger) || any - any = applyEnvCSV(&out.TriggerCharacters, "HEXAI_TRIGGER_CHARACTERS") || any - any = applyEnvString(&out.InlineOpen, "HEXAI_INLINE_OPEN") || any - any = applyEnvString(&out.InlineClose, "HEXAI_INLINE_CLOSE") || any - any = applyEnvString(&out.ChatSuffix, "HEXAI_CHAT_SUFFIX") || any - any = applyEnvCSV(&out.ChatPrefixes, "HEXAI_CHAT_PREFIXES") || any - any = applyEnvString(&out.Provider, "HEXAI_PROVIDER") || any - return any -} - -func applyProviderEnv(out *App, logger *log.Logger) bool { - picker := newModelPicker(out.Provider) - any := false - any = applyEnvString(&out.OpenAIBaseURL, "HEXAI_OPENAI_BASE_URL") || any - if model, ok := picker.pick("openai", getenvTrim("HEXAI_OPENAI_MODEL")); ok { - out.OpenAIModel = model - any = true - } - any = applyEnvFloat(&out.OpenAITemperature, "HEXAI_OPENAI_TEMPERATURE", logger) || any - - any = applyEnvString(&out.OpenRouterBaseURL, "HEXAI_OPENROUTER_BASE_URL") || any - if model, ok := picker.pick("openrouter", getenvTrim("HEXAI_OPENROUTER_MODEL")); ok { - out.OpenRouterModel = model - any = true - } - any = applyEnvFloat(&out.OpenRouterTemperature, "HEXAI_OPENROUTER_TEMPERATURE", logger) || any - - any = applyEnvString(&out.OllamaBaseURL, "HEXAI_OLLAMA_BASE_URL") || any - if model, ok := picker.pick("ollama", getenvTrim("HEXAI_OLLAMA_MODEL")); ok { - out.OllamaModel = model - any = true - } - any = applyEnvFloat(&out.OllamaTemperature, "HEXAI_OLLAMA_TEMPERATURE", logger) || any - - any = applyEnvString(&out.AnthropicBaseURL, "HEXAI_ANTHROPIC_BASE_URL") || any - if model, ok := picker.pick("anthropic", getenvTrim("HEXAI_ANTHROPIC_MODEL")); ok { - out.AnthropicModel = model - any = true - } - any = applyEnvFloat(&out.AnthropicTemperature, "HEXAI_ANTHROPIC_TEMPERATURE", logger) || any - return any -} - -func applySurfaceEnv(out *App, logger *log.Logger) bool { - any := false - if entries, ok := buildSurfaceEntryFromEnv("HEXAI_MODEL_COMPLETION", "HEXAI_TEMPERATURE_COMPLETION", "HEXAI_PROVIDER_COMPLETION", logger); ok { - out.CompletionConfigs = entries - any = true - } - if entries, ok := buildSurfaceEntryFromEnv("HEXAI_MODEL_CODE_ACTION", "HEXAI_TEMPERATURE_CODE_ACTION", "HEXAI_PROVIDER_CODE_ACTION", logger); ok { - out.CodeActionConfigs = entries - any = true - } - if entries, ok := buildSurfaceEntryFromEnv("HEXAI_MODEL_CHAT", "HEXAI_TEMPERATURE_CHAT", "HEXAI_PROVIDER_CHAT", logger); ok { - out.ChatConfigs = entries - any = true - } - if entries, ok := buildSurfaceEntryFromEnv("HEXAI_MODEL_CLI", "HEXAI_TEMPERATURE_CLI", "HEXAI_PROVIDER_CLI", logger); ok { - out.CLIConfigs = entries - any = true - } - return any -} - -func applyIgnoreEnv(out *App) bool { - any := false - any = applyEnvBoolPtr(&out.IgnoreGitignore, "HEXAI_IGNORE_GITIGNORE") || any - any = applyEnvCSV(&out.IgnoreExtraPatterns, "HEXAI_IGNORE_EXTRA_PATTERNS") || any - any = applyEnvBoolPtr(&out.IgnoreLSPNotify, "HEXAI_IGNORE_LSP_NOTIFY") || any - return any -} - -func applyMCPEnv(out *App) bool { - any := false - any = applyEnvString(&out.MCPPromptsDir, "HEXAI_MCP_PROMPTS_DIR") || any - any = applyEnvBool(&out.MCPSlashCommandSync, "HEXAI_MCP_SLASHCOMMAND_SYNC") || any - any = applyEnvString(&out.MCPSlashCommandDir, "HEXAI_MCP_SLASHCOMMAND_DIR") || any - return any -} - -func buildSurfaceEntryFromEnv(modelKey, tempKey, providerKey string, logger *log.Logger) ([]SurfaceConfig, bool) { - model := getenvTrim(modelKey) - tempPtr, tempSet := parseEnvFloatPtr(tempKey, logger) - provider := getenvTrim(providerKey) - if model == "" && provider == "" && !tempSet { - return nil, false - } - entry := SurfaceConfig{Provider: provider, Model: model} - if tempSet { - entry.Temperature = tempPtr - } - return []SurfaceConfig{entry}, true -} - -func applyEnvString(target *string, key string) bool { - value := getenvTrim(key) - if value == "" { - return false - } - *target = value - return true -} - -func applyEnvInt(target *int, key string, logger *log.Logger) bool { - value, ok := parseEnvInt(key, logger) - if !ok { - return false - } - *target = value - return true -} - -func applyEnvFloat(target **float64, key string, logger *log.Logger) bool { - value, ok := parseEnvFloatPtr(key, logger) - if !ok { - return false - } - *target = value - return true -} - -func applyEnvCSV(target *[]string, key string) bool { - value := getenvTrim(key) - if value == "" { - return false - } - parts := strings.Split(value, ",") - *target = nil - for _, p := range parts { - if t := strings.TrimSpace(p); t != "" { - *target = append(*target, t) - } - } - return true -} - -func applyEnvBool(target *bool, key string) bool { - value := getenvTrim(key) - if value == "" { - return false - } - *target = value == "true" || value == "1" - return true -} - -func applyEnvBoolPtr(target **bool, key string) bool { - value := getenvTrim(key) - if value == "" { - return false - } - parsed := value == "true" || value == "1" - *target = &parsed - return true -} - -func getenvTrim(key string) string { - return strings.TrimSpace(os.Getenv(key)) -} - -func parseEnvInt(key string, logger *log.Logger) (int, bool) { - value := getenvTrim(key) - if value == "" { - return 0, false - } - n, err := strconv.Atoi(value) - if err != nil { - if logger != nil { - logger.Printf("invalid %s: %v", key, err) - } - return 0, false - } - return n, true -} - -func parseEnvFloatPtr(key string, logger *log.Logger) (*float64, bool) { - value := getenvTrim(key) - if value == "" { - return nil, false - } - f, err := strconv.ParseFloat(value, 64) - if err != nil { - if logger != nil { - logger.Printf("invalid %s: %v", key, err) - } - return nil, false - } - return &f, true -} - -type modelPicker struct { - providerLower string - modelForce string - modelGeneric string - forceUsed bool - genericUsed bool -} - -func newModelPicker(provider string) *modelPicker { - return &modelPicker{ - providerLower: strings.ToLower(strings.TrimSpace(provider)), - modelForce: getenvTrim("HEXAI_MODEL_FORCE"), - modelGeneric: getenvTrim("HEXAI_MODEL"), - } -} - -func (p *modelPicker) pick(providerName, specific string) (string, bool) { - specific = strings.TrimSpace(specific) - nameLower := strings.ToLower(strings.TrimSpace(providerName)) - if p.modelForce != "" { - if p.providerLower == nameLower { - p.forceUsed = true - return p.modelForce, true - } - if p.providerLower == "" && !p.forceUsed { - p.forceUsed = true - return p.modelForce, true - } - } - if specific != "" { - return specific, true - } - if p.modelGeneric != "" { - if p.providerLower == nameLower { - return p.modelGeneric, true - } - if p.providerLower == "" && !p.genericUsed { - p.genericUsed = true - return p.modelGeneric, true - } - } - return "", false -} |
