summaryrefslogtreecommitdiff
path: root/internal/appconfig/config.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/appconfig/config.go')
-rw-r--r--internal/appconfig/config.go63
1 files changed, 61 insertions, 2 deletions
diff --git a/internal/appconfig/config.go b/internal/appconfig/config.go
index 3077d42..87ad9ff 100644
--- a/internal/appconfig/config.go
+++ b/internal/appconfig/config.go
@@ -190,8 +190,12 @@ func Load(logger *log.Logger) App { return LoadWithOptions(logger, LoadOptions{}
// LoadOptions tune how configuration is loaded at runtime.
type LoadOptions struct {
// IgnoreEnv skips applying environment overrides when true.
- IgnoreEnv bool
+ IgnoreEnv bool
+ // ConfigPath overrides the global config file path (e.g. via --config flag).
ConfigPath string
+ // ProjectRoot overrides the project root directory for locating .hexaiconfig.toml.
+ // When empty, findGitRoot() is used to auto-detect from the current working directory.
+ ProjectRoot string
}
// LoadWithOptions reads configuration and applies the requested loading options.
@@ -201,6 +205,7 @@ func LoadWithOptions(logger *log.Logger, opts LoadOptions) App {
return cfg // Return defaults if no logger is provided (e.g. in tests)
}
+ // Step 1: Load global config file
configPath := strings.TrimSpace(opts.ConfigPath)
if configPath != "" {
if fileCfg, err := loadFromFile(configPath, logger); err == nil && fileCfg != nil {
@@ -217,8 +222,12 @@ func LoadWithOptions(logger *log.Logger, opts LoadOptions) App {
}
}
+ // Step 2: Load per-project config (.hexaiconfig.toml at git repo root).
+ // Project config overrides global config but is itself overridden by env vars.
+ loadProjectConfig(logger, opts, &cfg)
+
+ // Step 3: Environment overrides (always take precedence over all config files)
if !opts.IgnoreEnv {
- // Environment overrides (take precedence over file)
if envCfg := loadFromEnv(logger); envCfg != nil {
cfg.mergeWith(envCfg)
}
@@ -226,6 +235,22 @@ func LoadWithOptions(logger *log.Logger, opts LoadOptions) App {
return cfg
}
+// loadProjectConfig attempts to load .hexaiconfig.toml from the project root and
+// merges it into cfg. Uses opts.ProjectRoot if set, otherwise auto-detects via findGitRoot().
+func loadProjectConfig(logger *log.Logger, opts LoadOptions, cfg *App) {
+ projectRoot := strings.TrimSpace(opts.ProjectRoot)
+ if projectRoot == "" {
+ projectRoot = findGitRoot()
+ }
+ if projectRoot == "" {
+ return
+ }
+ projectCfgPath := filepath.Join(projectRoot, ProjectConfigFilename)
+ if projCfg, err := loadFromFile(projectCfgPath, logger); err == nil && projCfg != nil {
+ cfg.mergeWith(projCfg)
+ }
+}
+
// Private helpers
// Sectioned (table-based) file format only.
type fileConfig struct {
@@ -1110,6 +1135,40 @@ func ConfigPath() (string, error) {
return configPath, nil
}
+// ProjectConfigFilename is the name of the per-project config file placed at a git repo root.
+const ProjectConfigFilename = ".hexaiconfig.toml"
+
+// ProjectConfigPath returns the path to the per-project config file if a git repository
+// root is detected from the current working directory. Returns empty string otherwise.
+func ProjectConfigPath() string {
+ root := findGitRoot()
+ if root == "" {
+ return ""
+ }
+ return filepath.Join(root, ProjectConfigFilename)
+}
+
+// findGitRoot walks up from the current working directory looking for a .git
+// directory or file (worktrees use a .git file). Returns the directory
+// containing .git, or empty string if none is found.
+func findGitRoot() string {
+ dir, err := os.Getwd()
+ if err != nil {
+ return ""
+ }
+ for {
+ if info, err := os.Stat(filepath.Join(dir, ".git")); err == nil &&
+ (info.IsDir() || info.Mode().IsRegular()) {
+ return dir
+ }
+ parent := filepath.Dir(dir)
+ if parent == dir {
+ return "" // reached filesystem root
+ }
+ dir = parent
+ }
+}
+
// --- Environment overrides ---
// loadFromEnv constructs an App containing only fields set via HEXAI_* env vars.