diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/appconfig/config.go | 70 | ||||
| -rw-r--r-- | internal/appconfig/config_test.go | 72 | ||||
| -rw-r--r-- | internal/hexaicli/run_test.go | 2 | ||||
| -rw-r--r-- | internal/hexaicli/testhelpers_test.go | 11 |
4 files changed, 76 insertions, 79 deletions
diff --git a/internal/appconfig/config.go b/internal/appconfig/config.go index 92fdf19..9404607 100644 --- a/internal/appconfig/config.go +++ b/internal/appconfig/config.go @@ -1,8 +1,7 @@ -// Summary: Application configuration model and loader; reads ~/.config/hexai/config.json and merges defaults. +// Summary: Application configuration model and loader; reads ~/.config/hexai/config.toml and merges defaults. package appconfig import ( - "encoding/json" "fmt" "log" "os" @@ -10,58 +9,60 @@ import ( "slices" "strconv" "strings" + + "github.com/pelletier/go-toml/v2" ) -// App holds user-configurable settings read from ~/.config/hexai/config.json. +// App holds user-configurable settings read from ~/.config/hexai/config.toml. type App struct { - MaxTokens int `json:"max_tokens"` - ContextMode string `json:"context_mode"` - ContextWindowLines int `json:"context_window_lines"` - MaxContextTokens int `json:"max_context_tokens"` - LogPreviewLimit int `json:"log_preview_limit"` + MaxTokens int `json:"max_tokens" toml:"max_tokens"` + ContextMode string `json:"context_mode" toml:"context_mode"` + ContextWindowLines int `json:"context_window_lines" toml:"context_window_lines"` + MaxContextTokens int `json:"max_context_tokens" toml:"max_context_tokens"` + LogPreviewLimit int `json:"log_preview_limit" toml:"log_preview_limit"` // Single knob for LSP requests; if set, overrides hardcoded temps in LSP. - CodingTemperature *float64 `json:"coding_temperature"` + CodingTemperature *float64 `json:"coding_temperature" toml:"coding_temperature"` // Minimum identifier characters required for manual (TriggerKind=1) invoke // to proceed without structural triggers. 0 means always allow. - ManualInvokeMinPrefix int `json:"manual_invoke_min_prefix"` + ManualInvokeMinPrefix int `json:"manual_invoke_min_prefix" toml:"manual_invoke_min_prefix"` // Completion debounce in milliseconds. When > 0, the server waits until // there has been no text change for at least this duration before sending // an LLM completion request. - CompletionDebounceMs int `json:"completion_debounce_ms"` + CompletionDebounceMs int `json:"completion_debounce_ms" toml:"completion_debounce_ms"` // Completion throttle in milliseconds. When > 0, caps the minimum spacing // between LLM requests (both chat and code-completer paths). - CompletionThrottleMs int `json:"completion_throttle_ms"` + CompletionThrottleMs int `json:"completion_throttle_ms" toml:"completion_throttle_ms"` - TriggerCharacters []string `json:"trigger_characters"` - Provider string `json:"provider"` + TriggerCharacters []string `json:"trigger_characters" toml:"trigger_characters"` + Provider string `json:"provider" toml:"provider"` // Inline prompt trigger characters (default: >text> and >>text>) - InlineOpen string `json:"inline_open"` - InlineClose string `json:"inline_close"` + InlineOpen string `json:"inline_open" toml:"inline_open"` + InlineClose string `json:"inline_close" toml:"inline_close"` // In-editor chat triggers (default: suffix ">" after one of [?, !, :, ;]) - ChatSuffix string `json:"chat_suffix"` - ChatPrefixes []string `json:"chat_prefixes"` + ChatSuffix string `json:"chat_suffix" toml:"chat_suffix"` + ChatPrefixes []string `json:"chat_prefixes" toml:"chat_prefixes"` // Provider-specific options - OpenAIBaseURL string `json:"openai_base_url"` - OpenAIModel string `json:"openai_model"` + OpenAIBaseURL string `json:"openai_base_url" toml:"openai_base_url"` + OpenAIModel string `json:"openai_model" toml:"openai_model"` // Default temperature for OpenAI requests (nil means use provider default) - OpenAITemperature *float64 `json:"openai_temperature"` - OllamaBaseURL string `json:"ollama_base_url"` - OllamaModel string `json:"ollama_model"` + OpenAITemperature *float64 `json:"openai_temperature" toml:"openai_temperature"` + OllamaBaseURL string `json:"ollama_base_url" toml:"ollama_base_url"` + OllamaModel string `json:"ollama_model" toml:"ollama_model"` // Default temperature for Ollama requests (nil means use provider default) - OllamaTemperature *float64 `json:"ollama_temperature"` - CopilotBaseURL string `json:"copilot_base_url"` - CopilotModel string `json:"copilot_model"` + OllamaTemperature *float64 `json:"ollama_temperature" toml:"ollama_temperature"` + CopilotBaseURL string `json:"copilot_base_url" toml:"copilot_base_url"` + CopilotModel string `json:"copilot_model" toml:"copilot_model"` // Default temperature for Copilot requests (nil means use provider default) - CopilotTemperature *float64 `json:"copilot_temperature"` + CopilotTemperature *float64 `json:"copilot_temperature" toml:"copilot_temperature"` } // Constructor: defaults for App (kept first among functions) func newDefaultConfig() App { // Coding-friendly default temperature across providers - // Users can override per provider in config.json (including 0.0). + // Users can override per provider in config.toml (including 0.0). t := 0.2 return App{ MaxTokens: 4000, @@ -116,20 +117,23 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) { f, err := os.Open(path) if err != nil { if !os.IsNotExist(err) && logger != nil { - logger.Printf("cannot open config file %s: %v", path, err) + logger.Printf("cannot open TOML config file %s: %v", path, err) } return nil, err } defer f.Close() - dec := json.NewDecoder(f) + dec := toml.NewDecoder(f) var fileCfg App if err := dec.Decode(&fileCfg); err != nil { if logger != nil { - logger.Printf("invalid config file %s: %v", path, err) + logger.Printf("invalid TOML config file %s: %v", path, err) } return nil, err } + if logger != nil { + logger.Printf("loaded configuration from %s (TOML)", path) + } return &fileCfg, nil } @@ -221,13 +225,13 @@ func (a *App) mergeProviderFields(other *App) { func getConfigPath() (string, error) { var configPath string if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" { - configPath = filepath.Join(xdgConfigHome, "hexai", "config.json") + configPath = filepath.Join(xdgConfigHome, "hexai", "config.toml") } else { home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("cannot find user home directory: %v", err) } - configPath = filepath.Join(home, ".config", "hexai", "config.json") + configPath = filepath.Join(home, ".config", "hexai", "config.toml") } return configPath, nil } diff --git a/internal/appconfig/config_test.go b/internal/appconfig/config_test.go index f2e3f7a..bdf86da 100644 --- a/internal/appconfig/config_test.go +++ b/internal/appconfig/config_test.go @@ -1,7 +1,6 @@ package appconfig import ( - "encoding/json" "io" "log" "os" @@ -13,19 +12,13 @@ import ( func newLogger() *log.Logger { return log.New(io.Discard, "", 0) } -func writeJSON(t *testing.T, path string, v any) { +func writeFile(t *testing.T, path, content string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatalf("mkdir: %v", err) } - f, err := os.Create(path) - if err != nil { - t.Fatalf("create: %v", err) - } - defer f.Close() - enc := json.NewEncoder(f) - if err := enc.Encode(v); err != nil { - t.Fatalf("encode json: %v", err) + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("write: %v", err) } } @@ -59,31 +52,30 @@ func TestLoad_Defaults_WithLogger_NoFile_NoEnv(t *testing.T) { func TestLoad_FileMerge_And_EnvOverride(t *testing.T) { dir := t.TempDir() t.Setenv("XDG_CONFIG_HOME", dir) - cfgPath := filepath.Join(dir, "hexai", "config.json") - temp0 := 0.0 - fileCfg := App{ - MaxTokens: 123, - ContextMode: "file-on-new-func", - ContextWindowLines: 50, - MaxContextTokens: 999, - LogPreviewLimit: 0, - CodingTemperature: &temp0, - ManualInvokeMinPrefix: 2, - CompletionDebounceMs: 150, - CompletionThrottleMs: 300, - TriggerCharacters: []string{".", ":"}, - Provider: "openai", - OpenAIBaseURL: "https://api.example", - OpenAIModel: "gpt-x", - OpenAITemperature: &temp0, - OllamaBaseURL: "http://ollama", - OllamaModel: "llama", - OllamaTemperature: &temp0, - CopilotBaseURL: "http://copilot", - CopilotModel: "ghost", - CopilotTemperature: &temp0, - } - writeJSON(t, cfgPath, fileCfg) + cfgPath := filepath.Join(dir, "hexai", "config.toml") + // file configuration in TOML + writeFile(t, cfgPath, ` +max_tokens = 123 +context_mode = "file-on-new-func" +context_window_lines = 50 +max_context_tokens = 999 +log_preview_limit = 0 +coding_temperature = 0.0 +manual_invoke_min_prefix = 2 +completion_debounce_ms = 150 +completion_throttle_ms = 300 +trigger_characters = [".", ":"] +provider = "openai" +openai_base_url = "https://api.example" +openai_model = "gpt-x" +openai_temperature = 0.0 +ollama_base_url = "http://ollama" +ollama_model = "llama" +ollama_temperature = 0.0 +copilot_base_url = "http://copilot" +copilot_model = "ghost" +copilot_temperature = 0.0 +`) // Env overrides take precedence withEnv(t, "HEXAI_MAX_TOKENS", "321") @@ -163,23 +155,23 @@ func TestGetConfigPath_XDG(t *testing.T) { if err != nil { t.Fatalf("getConfigPath: %v", err) } - if !strings.HasPrefix(path, filepath.Join(dir, "hexai")) || !strings.HasSuffix(path, "config.json") { + if !strings.HasPrefix(path, filepath.Join(dir, "hexai")) || !strings.HasSuffix(path, "config.toml") { t.Fatalf("unexpected path: %s", path) } } -func TestLoadFromFile_InvalidJSON(t *testing.T) { +func TestLoadFromFile_InvalidTOML(t *testing.T) { dir := t.TempDir() t.Setenv("XDG_CONFIG_HOME", dir) - cfgPath := filepath.Join(dir, "hexai", "config.json") + cfgPath := filepath.Join(dir, "hexai", "config.toml") if err := os.MkdirAll(filepath.Dir(cfgPath), 0o755); err != nil { t.Fatal(err) } - if err := os.WriteFile(cfgPath, []byte("{ invalid"), 0o644); err != nil { + if err := os.WriteFile(cfgPath, []byte("invalid ="), 0o644); err != nil { t.Fatal(err) } _, err := loadFromFile(cfgPath, newLogger()) if err == nil { - t.Fatalf("expected error for invalid JSON") + t.Fatalf("expected error for invalid TOML") } } diff --git a/internal/hexaicli/run_test.go b/internal/hexaicli/run_test.go index 77daa8b..d192850 100644 --- a/internal/hexaicli/run_test.go +++ b/internal/hexaicli/run_test.go @@ -107,7 +107,7 @@ func TestRunWithClient_ErrorPrint(t *testing.T) { func TestRun_OpenAI_NoKey_ShowsError(t *testing.T) { dir := testingTempDir(t) // write config with provider=openai - writeJSON(t, filepath.Join(dir, "hexai", "config.json"), map[string]any{"provider": "openai", "openai_model": "gpt-x"}) + writeTOML(t, filepath.Join(dir, "hexai", "config.toml"), map[string]string{"provider": "openai", "openai_model": "gpt-x"}) t.Setenv("XDG_CONFIG_HOME", dir) // Ensure no OpenAI API key is present in environment t.Setenv("HEXAI_OPENAI_API_KEY", "") diff --git a/internal/hexaicli/testhelpers_test.go b/internal/hexaicli/testhelpers_test.go index 512a3ba..93f1e3d 100644 --- a/internal/hexaicli/testhelpers_test.go +++ b/internal/hexaicli/testhelpers_test.go @@ -3,7 +3,6 @@ package hexaicli import ( "context" - "encoding/json" "os" "path/filepath" "testing" @@ -62,8 +61,8 @@ func (s *fakeStreamer) ChatStream(ctx context.Context, messages []llm.Message, o return nil } -// small JSON writer for tests -func writeJSON(t *testing.T, path string, v any) { +// small TOML writer for tests (string values only) +func writeTOML(t *testing.T, path string, m map[string]string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatalf("mkdir: %v", err) @@ -73,8 +72,10 @@ func writeJSON(t *testing.T, path string, v any) { t.Fatalf("create: %v", err) } defer f.Close() - if err := json.NewEncoder(f).Encode(v); err != nil { - t.Fatalf("encode: %v", err) + for k, v := range m { + if _, err := f.WriteString(k + " = \"" + v + "\"\n"); err != nil { + t.Fatalf("write: %v", err) + } } } |
