@@ -15,7 +15,9 @@ const (
1515 EmulatorSnowflake EmulatorType = "snowflake"
1616 EmulatorAzure EmulatorType = "azure"
1717
18- dockerRegistry = "localstack"
18+ dockerRegistry = "localstack"
19+ localConfigFileName = "lstk.toml"
20+ userConfigFileName = "config.toml"
1921)
2022
2123var emulatorImages = map [EmulatorType ]string {
@@ -74,62 +76,188 @@ func (c *ContainerConfig) ProductName() (string, error) {
7476 return productName , nil
7577}
7678
77- func ConfigDir () (string , error ) {
79+ func xdgConfigDir () (string , error ) {
80+ homeDir , err := os .UserHomeDir ()
81+ if err != nil {
82+ return "" , fmt .Errorf ("failed to get user home directory: %w" , err )
83+ }
84+ return filepath .Join (homeDir , ".config" , "lstk" ), nil
85+ }
86+
87+ func osConfigDir () (string , error ) {
7888 configHome , err := os .UserConfigDir ()
7989 if err != nil {
8090 return "" , fmt .Errorf ("failed to get user config directory: %w" , err )
8191 }
8292 return filepath .Join (configHome , "lstk" ), nil
8393}
8494
85- func Init () error {
86- dir , err := ConfigDir ()
95+ func localConfigPath () string {
96+ return filepath .Join ("." , localConfigFileName )
97+ }
98+
99+ func configSearchPaths () ([]string , error ) {
100+ xdgDir , err := xdgConfigDir ()
87101 if err != nil {
88- return err
102+ return nil , err
89103 }
90104
91- if err := os .MkdirAll (dir , 0755 ); err != nil {
92- return fmt .Errorf ("failed to create config directory: %w" , err )
105+ osDir , err := osConfigDir ()
106+ if err != nil {
107+ return nil , err
93108 }
94109
95- viper .SetConfigName ("config" )
96- viper .SetConfigType ("toml" )
97- viper .AddConfigPath (dir )
110+ return []string {
111+ // Priority order: project-local, then XDG-style home config, then OS-specific fallback.
112+ localConfigPath (),
113+ filepath .Join (xdgDir , userConfigFileName ),
114+ filepath .Join (osDir , userConfigFileName ),
115+ }, nil
116+ }
117+
118+ func configCreationDir () (string , error ) {
119+ homeDir , err := os .UserHomeDir ()
120+ if err != nil {
121+ return "" , fmt .Errorf ("failed to get user home directory: %w" , err )
122+ }
123+
124+ homeConfigDir := filepath .Join (homeDir , ".config" )
125+ // Creation policy differs from read fallback: prefer $HOME/.config only when it already exists.
126+ info , err := os .Stat (homeConfigDir )
127+ if err == nil {
128+ if info .IsDir () {
129+ return xdgConfigDir ()
130+ }
131+ } else if ! os .IsNotExist (err ) {
132+ return "" , fmt .Errorf ("failed to inspect %s: %w" , homeConfigDir , err )
133+ }
134+
135+ return osConfigDir ()
136+ }
98137
138+ func setDefaults () {
99139 viper .SetDefault ("containers" , []map [string ]any {
100140 {
101141 "type" : "aws" ,
102142 "tag" : "latest" ,
103143 "port" : "4566" ,
104144 },
105145 })
146+ }
106147
107- if err := viper .ReadInConfig (); err != nil {
108- if _ , ok := err .(viper.ConfigFileNotFoundError ); ok {
109- if err := viper .SafeWriteConfig (); err != nil {
110- return fmt .Errorf ("failed to write config file: %w" , err )
111- }
112- return nil
148+ func firstExistingConfigPath () (string , bool , error ) {
149+ paths , err := configSearchPaths ()
150+ if err != nil {
151+ return "" , false , err
152+ }
153+
154+ for _ , path := range paths {
155+ if _ , err := os .Stat (path ); err == nil {
156+ return path , true , nil
157+ } else if ! os .IsNotExist (err ) {
158+ return "" , false , fmt .Errorf ("failed to inspect config path %s: %w" , path , err )
113159 }
160+ }
161+
162+ return "" , false , nil
163+ }
114164
165+ func loadConfig (path string ) error {
166+ viper .Reset ()
167+ setDefaults ()
168+ viper .SetConfigFile (path )
169+
170+ if err := viper .ReadInConfig (); err != nil {
115171 return fmt .Errorf ("failed to read config file: %w" , err )
116172 }
117-
118173 return nil
119174}
120175
121- func Get () (* Config , error ) {
122- var cfg Config
123- if err := viper .Unmarshal (& cfg ); err != nil {
124- return nil , fmt .Errorf ("failed to unmarshal config: %w" , err )
176+ func Init () error {
177+ // Reuse the same ordered path resolution used by ConfigFilePath.
178+ existingPath , found , err := firstExistingConfigPath ()
179+ if err != nil {
180+ return err
125181 }
126- return & cfg , nil
182+ if found {
183+ return loadConfig (existingPath )
184+ }
185+
186+ // No config found anywhere, create one using creation policy.
187+ creationDir , err := configCreationDir ()
188+ if err != nil {
189+ return err
190+ }
191+
192+ if err := os .MkdirAll (creationDir , 0755 ); err != nil {
193+ return fmt .Errorf ("failed to create config directory: %w" , err )
194+ }
195+
196+ configPath := filepath .Join (creationDir , userConfigFileName )
197+ viper .Reset ()
198+ setDefaults ()
199+ viper .SetConfigType ("toml" )
200+ viper .SetConfigFile (configPath )
201+ if err := viper .SafeWriteConfigAs (configPath ); err != nil {
202+ return fmt .Errorf ("failed to write config file: %w" , err )
203+ }
204+
205+ return loadConfig (configPath )
206+ }
207+
208+ func ConfigDir () (string , error ) {
209+ configPath , err := ConfigFilePath ()
210+ if err != nil {
211+ return "" , err
212+ }
213+
214+ return filepath .Dir (configPath ), nil
215+ }
216+
217+ func ResolvedConfigPath () string {
218+ return viper .ConfigFileUsed ()
127219}
128220
129221func ConfigFilePath () (string , error ) {
130- dir , err := ConfigDir ()
222+ if resolved := ResolvedConfigPath (); resolved != "" {
223+ // If Init already ran, use Viper's selected file directly.
224+ absResolved , err := filepath .Abs (resolved )
225+ if err != nil {
226+ return "" , fmt .Errorf ("failed to resolve absolute config path: %w" , err )
227+ }
228+ return absResolved , nil
229+ }
230+
231+ existingPath , found , err := firstExistingConfigPath ()
232+ if err != nil {
233+ return "" , err
234+ }
235+ if found {
236+ // Side-effect-free resolution for commands that skip Init (e.g. `lstk config path`).
237+ absPath , err := filepath .Abs (existingPath )
238+ if err != nil {
239+ return "" , fmt .Errorf ("failed to resolve absolute config path: %w" , err )
240+ }
241+ return absPath , nil
242+ }
243+
244+ creationDir , err := configCreationDir ()
131245 if err != nil {
132246 return "" , err
133247 }
134- return filepath .Join (dir , "config.toml" ), nil
248+
249+ creationPath := filepath .Join (creationDir , userConfigFileName )
250+ absCreationPath , err := filepath .Abs (creationPath )
251+ if err != nil {
252+ return "" , fmt .Errorf ("failed to resolve absolute config path: %w" , err )
253+ }
254+ return absCreationPath , nil
255+ }
256+
257+ func Get () (* Config , error ) {
258+ var cfg Config
259+ if err := viper .Unmarshal (& cfg ); err != nil {
260+ return nil , fmt .Errorf ("failed to unmarshal config: %w" , err )
261+ }
262+ return & cfg , nil
135263}
0 commit comments