From 77134c3c7865e52ce935bd012932d3a702b76b27 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Thu, 14 May 2026 20:48:12 -0500 Subject: [PATCH] Rationalized the default configuration file path and updated documentation --- docs/config/config-yml.md | 2 +- internal/adapter/cli/run.go | 11 +++++++- internal/config/config.go | 29 +++++++++++++++++++-- internal/config/config_test.go | 47 ++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/docs/config/config-yml.md b/docs/config/config-yml.md index f245418..937d5d3 100644 --- a/docs/config/config-yml.md +++ b/docs/config/config-yml.md @@ -2,7 +2,7 @@ `config.yml` defines application-level defaults used by CLI commands. -By default, Scriptorium looks for `/etc/scriptorium/config.yml`. You can also pass `--config PATH`. +By default, Scriptorium looks for `/usr/local/etc/scriptorium/config.yml` and, if not present, then for `/etc/scriptorium/config.yml`. You can also pass `--config PATH`. ## Complete Example diff --git a/internal/adapter/cli/run.go b/internal/adapter/cli/run.go index 91d3550..519ffe7 100644 --- a/internal/adapter/cli/run.go +++ b/internal/adapter/cli/run.go @@ -450,7 +450,16 @@ func (c *serveConfig) addrIfSet(fs *flag.FlagSet) string { } func registerConfigPathFlag(fs *flag.FlagSet, target *string) { - fs.StringVar(target, "config", "", fmt.Sprintf("application config path (default %s)", appconfig.DefaultConfigPath)) + fs.StringVar( + target, + "config", + "", + fmt.Sprintf( + "application config path (default search: %s, then %s)", + appconfig.DefaultConfigPathLocal, + appconfig.DefaultConfigPath, + ), + ) } func resolveAppSettings(fs *flag.FlagSet, configPath string, overrides appconfig.CLIOverrides) (appconfig.AppSettings, error) { diff --git a/internal/config/config.go b/internal/config/config.go index 2fef250..59527c6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -14,7 +14,15 @@ import ( "gopkg.in/yaml.v3" ) -const DefaultConfigPath = "/etc/scriptorium/config.yml" +const ( + DefaultConfigPath = "/etc/scriptorium/config.yml" + DefaultConfigPathLocal = "/usr/local/etc/scriptorium/config.yml" +) + +var defaultConfigSearchPaths = []string{ + DefaultConfigPathLocal, + DefaultConfigPath, +} var ( ErrConfigNotFound = errors.New("config file not found") @@ -74,7 +82,24 @@ func LoadConfig(path string, explicit bool) (AppSettings, error) { resolved := BuiltInDefaults() configPath := strings.TrimSpace(path) if configPath == "" { - configPath = DefaultConfigPath + if explicit { + configPath = DefaultConfigPath + } else { + for _, candidate := range defaultConfigSearchPaths { + if _, err := os.Stat(candidate); err == nil { + configPath = candidate + break + } else if !errors.Is(err, os.ErrNotExist) { + return AppSettings{}, fmt.Errorf("failed to stat config file %q: %w", candidate, err) + } + } + } + } + if configPath == "" { + if explicit { + return AppSettings{}, fmt.Errorf("%w: %s", ErrConfigNotFound, DefaultConfigPath) + } + return resolved, nil } configPath = filepath.Clean(configPath) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 953d9b4..0a813f0 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -132,6 +132,53 @@ func TestLoadConfigEmptyFileResolvesToBuiltInDefaults(t *testing.T) { } } +func TestLoadConfigImplicitSearchPrefersUsrLocalEtcOverEtc(t *testing.T) { + tmp := t.TempDir() + localPath := filepath.Join(tmp, "usr-local.yml") + etcPath := filepath.Join(tmp, "etc.yml") + + if err := os.WriteFile(localPath, []byte("prompt_dir: ./from-usr-local\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(etcPath, []byte("prompt_dir: ./from-etc\n"), 0o644); err != nil { + t.Fatal(err) + } + + orig := defaultConfigSearchPaths + defaultConfigSearchPaths = []string{localPath, etcPath} + t.Cleanup(func() { defaultConfigSearchPaths = orig }) + + got, err := LoadConfig("", false) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if got.PromptDir != filepath.Clean("./from-usr-local") { + t.Fatalf("expected usr-local config to win, got prompt_dir=%q", got.PromptDir) + } +} + +func TestLoadConfigImplicitSearchFallsBackToEtcWhenUsrLocalMissing(t *testing.T) { + tmp := t.TempDir() + missingLocal := filepath.Join(tmp, "missing-local.yml") + etcPath := filepath.Join(tmp, "etc.yml") + + if err := os.WriteFile(etcPath, []byte("prompt_dir: ./from-etc\n"), 0o644); err != nil { + t.Fatal(err) + } + + orig := defaultConfigSearchPaths + defaultConfigSearchPaths = []string{missingLocal, etcPath} + t.Cleanup(func() { defaultConfigSearchPaths = orig }) + + got, err := LoadConfig("", false) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if got.PromptDir != filepath.Clean("./from-etc") { + t.Fatalf("expected etc fallback config, got prompt_dir=%q", got.PromptDir) + } +} + func TestApplyCLIOverridesAppliesPrecedence(t *testing.T) { base := AppSettings{ PromptDir: "/from/config/prompts",