Skip to content

Commit 380b5bd

Browse files
authored
Use .lstk/config.toml for project-local config (#149)
1 parent 9925dae commit 380b5bd

6 files changed

Lines changed: 66 additions & 52 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ lstk always writes diagnostic logs to `$CONFIG_DIR/lstk.log` (appends across run
4343

4444
# Configuration
4545

46-
Uses Viper with TOML format. lstk uses the first config file found in this order:
47-
1. `./lstk.toml` (project-local)
46+
Uses Viper with TOML format. lstk uses the first `config.toml` found in this order:
47+
1. `./.lstk/config.toml` (project-local)
4848
2. `$HOME/.config/lstk/config.toml`
4949
3. **macOS**: `$HOME/Library/Application Support/lstk/config.toml` / **Windows**: `%AppData%\lstk\config.toml`
5050

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ The CLI supports multiple auth workflows. `lstk` resolves your auth token in thi
7070

7171
`lstk` uses a TOML config file, created automatically on first run.
7272

73-
`lstk` uses the first config file found in this order:
74-
1. `./lstk.toml` (project-local)
73+
`lstk` uses the first `config.toml` found in this order:
74+
1. `./.lstk/config.toml` (project-local)
7575
2. `$HOME/.config/lstk/config.toml`
7676
3. **macOS**: `$HOME/Library/Application Support/lstk/config.toml` / **Windows**: `%AppData%\lstk\config.toml`
7777

@@ -86,7 +86,7 @@ lstk config path
8686
You can also point `lstk` at a specific config file for any command:
8787

8888
```bash
89-
lstk --config /path/to/lstk.toml start
89+
lstk --config /path/to/config.toml start
9090
```
9191

9292
### Default config

internal/config/config.go

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -47,45 +47,57 @@ func InitFromPath(path string) error {
4747
}
4848

4949
func Init() error {
50-
// Reuse the same ordered path resolution used by ConfigFilePath.
51-
existingPath, found, err := firstExistingConfigPath()
50+
viper.Reset()
51+
setDefaults()
52+
viper.SetConfigName(configName)
53+
viper.SetConfigType(configType)
54+
55+
dirs, err := configSearchDirs()
5256
if err != nil {
5357
return err
5458
}
55-
if found {
56-
return loadConfig(existingPath)
59+
for _, dir := range dirs {
60+
viper.AddConfigPath(dir)
5761
}
5862

59-
// No config found anywhere, create one using creation policy.
60-
creationDir, err := configCreationDir()
61-
if err != nil {
62-
return err
63-
}
63+
if err := viper.ReadInConfig(); err != nil {
64+
var notFoundErr viper.ConfigFileNotFoundError
65+
if !errors.As(err, &notFoundErr) {
66+
return fmt.Errorf("failed to read config file: %w", err)
67+
}
6468

65-
if err := os.MkdirAll(creationDir, 0755); err != nil {
66-
return fmt.Errorf("failed to create config directory: %w", err)
67-
}
69+
// No config found anywhere, create one using creation policy.
70+
creationDir, err := configCreationDir()
71+
if err != nil {
72+
return err
73+
}
6874

69-
configPath := filepath.Join(creationDir, userConfigFileName)
70-
f, err := os.OpenFile(configPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
71-
if err != nil {
72-
if errors.Is(err, os.ErrExist) {
73-
return loadConfig(configPath)
75+
if err := os.MkdirAll(creationDir, 0755); err != nil {
76+
return fmt.Errorf("failed to create config directory: %w", err)
77+
}
78+
79+
configPath := filepath.Join(creationDir, configFileName)
80+
f, err := os.OpenFile(configPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
81+
if err != nil {
82+
if errors.Is(err, os.ErrExist) {
83+
return loadConfig(configPath)
84+
}
85+
return fmt.Errorf("failed to create config file: %w", err)
86+
}
87+
_, writeErr := f.WriteString(defaultConfigTemplate)
88+
closeErr := f.Close()
89+
if writeErr != nil {
90+
_ = os.Remove(configPath)
91+
return fmt.Errorf("failed to write config file: %w", writeErr)
92+
}
93+
if closeErr != nil {
94+
_ = os.Remove(configPath)
95+
return fmt.Errorf("failed to close config file: %w", closeErr)
7496
}
75-
return fmt.Errorf("failed to create config file: %w", err)
76-
}
77-
_, writeErr := f.WriteString(defaultConfigTemplate)
78-
closeErr := f.Close()
79-
if writeErr != nil {
80-
_ = os.Remove(configPath)
81-
return fmt.Errorf("failed to write config file: %w", writeErr)
82-
}
83-
if closeErr != nil {
84-
_ = os.Remove(configPath)
85-
return fmt.Errorf("failed to close config file: %w", closeErr)
86-
}
8797

88-
return loadConfig(configPath)
98+
return loadConfig(configPath)
99+
}
100+
return nil
89101
}
90102

91103
func resolvedConfigPath() string {

internal/config/containers.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ const (
1515
EmulatorSnowflake EmulatorType = "snowflake"
1616
EmulatorAzure EmulatorType = "azure"
1717

18-
dockerRegistry = "localstack"
19-
localConfigFileName = "lstk.toml"
20-
userConfigFileName = "config.toml"
18+
dockerRegistry = "localstack"
2119
)
2220

2321
var emulatorDisplayNames = map[EmulatorType]string{

internal/config/paths.go

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@ import (
66
"path/filepath"
77
)
88

9+
const (
10+
localConfigDir = ".lstk"
11+
configName = "config"
12+
configType = "toml"
13+
configFileName = configName + "." + configType
14+
)
15+
916
func ConfigFilePath() (string, error) {
1017
if resolved := resolvedConfigPath(); resolved != "" {
11-
// If Init already ran, use Viper's selected file directly.
1218
absResolved, err := filepath.Abs(resolved)
1319
if err != nil {
1420
return "", fmt.Errorf("failed to resolve absolute config path: %w", err)
@@ -34,7 +40,7 @@ func ConfigFilePath() (string, error) {
3440
return "", err
3541
}
3642

37-
creationPath := filepath.Join(creationDir, userConfigFileName)
43+
creationPath := filepath.Join(creationDir, configFileName)
3844
absCreationPath, err := filepath.Abs(creationPath)
3945
if err != nil {
4046
return "", fmt.Errorf("failed to resolve absolute config path: %w", err)
@@ -67,11 +73,9 @@ func osConfigDir() (string, error) {
6773
return filepath.Join(configHome, "lstk"), nil
6874
}
6975

70-
func localConfigPath() string {
71-
return filepath.Join(".", localConfigFileName)
72-
}
73-
74-
func configSearchPaths() ([]string, error) {
76+
// configSearchDirs returns directories to search for config.toml, in priority order:
77+
// project-local (.lstk/), XDG-style home config, OS-specific fallback.
78+
func configSearchDirs() ([]string, error) {
7579
xdgDir, err := xdgConfigDir()
7680
if err != nil {
7781
return nil, err
@@ -83,10 +87,9 @@ func configSearchPaths() ([]string, error) {
8387
}
8488

8589
return []string{
86-
// Priority order: project-local, then XDG-style home config, then OS-specific fallback.
87-
localConfigPath(),
88-
filepath.Join(xdgDir, userConfigFileName),
89-
filepath.Join(osDir, userConfigFileName),
90+
filepath.Join(".", localConfigDir),
91+
xdgDir,
92+
osDir,
9093
}, nil
9194
}
9295

@@ -111,12 +114,13 @@ func configCreationDir() (string, error) {
111114
}
112115

113116
func firstExistingConfigPath() (string, bool, error) {
114-
paths, err := configSearchPaths()
117+
dirs, err := configSearchDirs()
115118
if err != nil {
116119
return "", false, err
117120
}
118121

119-
for _, path := range paths {
122+
for _, dir := range dirs {
123+
path := filepath.Join(dir, configFileName)
120124
if _, err := os.Stat(path); err == nil {
121125
return path, true, nil
122126
} else if !os.IsNotExist(err) {

test/integration/config_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func TestLocalConfigTakesPrecedence(t *testing.T) {
9494
workDir := t.TempDir()
9595
xdgOverride := filepath.Join(tmpHome, "xdg-config-home")
9696

97-
localConfigFile := filepath.Join(workDir, "lstk.toml")
97+
localConfigFile := filepath.Join(workDir, ".lstk", "config.toml")
9898
writeConfigFile(t, localConfigFile)
9999
writeConfigFile(t, filepath.Join(tmpHome, ".config", "lstk", "config.toml"))
100100
writeConfigFile(t, filepath.Join(expectedOSConfigDir(tmpHome, xdgOverride), "config.toml"))

0 commit comments

Comments
 (0)