Skip to content

Commit ebb13d8

Browse files
committed
fix config quirks (again)
1 parent f5748e5 commit ebb13d8

4 files changed

Lines changed: 109 additions & 66 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,10 @@ An agentless SSH key manager that just does the job.
8989

9090
2. **Initialize the Database:**
9191
Run Keymaster for the first time. It will automatically create `keymaster.db`
92-
and a default `config.yaml` in the standard user configuration directory.
93-
- **Linux/macOS:** `~/.config/keymaster/`
92+
and a default `keymaster.yaml` in the standard user configuration directory.
93+
- **Linux:** `~/.config/keymaster/`
9494
- **Windows:** `C:\Users\<user>\AppData\Roaming\keymaster\`
95+
- **macOS:** `~/Library/Application Support/keymaster/`
9596
9697
For backward compatibility, it will also read an existing `.keymaster.yaml` from the current directory.
9798
```sh

‎cmd/keymaster/main.go‎

Lines changed: 52 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -46,28 +46,7 @@ var fullRestore bool // Flag for the restore command
4646

4747
var password string // Flag for rotate-key password
4848
// TODO should be moved to project root
49-
// main is the entry point of the application.
50-
func main() {
51-
// Install signal handler for graceful shutdown of bootstrap sessions
52-
bootstrap.InstallSignalHandler()
53-
54-
// Set up cleanup store for bootstrap operations
55-
defer func() {
56-
if err := bootstrap.CleanupAllActiveSessions(); err != nil {
57-
log.Printf("Error during final cleanup: %w", err)
58-
} else {
59-
log.Println("Bootstrap cleanup complete.")
60-
}
61-
}()
62-
63-
rootCmd := NewRootCmd()
64-
65-
if err := rootCmd.Execute(); err != nil {
66-
// TODO consider adding bootstrap logic
67-
// The error is already printed by Cobra on failure.
68-
os.Exit(1)
69-
}
70-
}
49+
var appConfig config.Config
7150

7251
func setupDefaultServices(cmd *cobra.Command, args []string) error {
7352
// Load optional config file argument from cli
@@ -77,30 +56,47 @@ func setupDefaultServices(cmd *cobra.Command, args []string) error {
7756
}
7857

7958
// Load config
80-
type Config struct {
81-
Database struct {
82-
Type string `mapstructure:"type"`
83-
Dsn string `mapstructure:"dsn"`
84-
} `mapstructure:"database"`
85-
Language string `mapstructure:"language"`
86-
}
87-
8859
defauls := map[string]any{
8960
"database.type": "sqlite",
9061
"database.dsn": "./keymaster.db",
9162
"language": "en",
9263
}
9364

94-
config, err := config.LoadConfig[Config](cmd, defauls, optional_config_path)
95-
if err != nil {
65+
appConfig, err = config.LoadConfig[config.Config](cmd, defauls, optional_config_path)
66+
// A "file not found" error is expected on first run, so we handle it specifically.
67+
// Other errors during loading are fatal.
68+
if errors.As(err, &viper.ConfigFileNotFoundError{}) {
69+
// This is the first run, or the config file was deleted. Create a default one.
70+
if writeErr := config.WriteConfigFile(&appConfig, false); writeErr != nil {
71+
// Log a warning but don't fail, as the app can run on defaults.
72+
log.Printf("Warning: could not write default config file: %v", writeErr)
73+
}
74+
} else if err != nil {
75+
// Any other error during config loading is a problem.
9676
return fmt.Errorf("error loading config: %w", err)
9777
}
9878

79+
// Post-process config to ensure critical values are not empty, falling back to defaults.
80+
// This handles cases where the user's config file has empty values for these fields.
81+
// We also update viper's internal state to ensure subsequent saves are correct.
82+
if appConfig.Database.Type == "" {
83+
appConfig.Database.Type = defauls["database.type"].(string)
84+
viper.Set("database.type", appConfig.Database.Type)
85+
}
86+
if appConfig.Database.Dsn == "" {
87+
appConfig.Database.Dsn = defauls["database.dsn"].(string)
88+
viper.Set("database.dsn", appConfig.Database.Dsn)
89+
}
90+
if appConfig.Language == "" {
91+
appConfig.Language = defauls["language"].(string)
92+
viper.Set("language", appConfig.Language)
93+
}
94+
9995
// Initialize i18n
100-
i18n.Init(config.Language)
96+
i18n.Init(appConfig.Language)
10197

10298
// Initialize the database
103-
if err := db.InitDB(config.Database.Type, config.Database.Dsn); err != nil {
99+
if err := db.InitDB(appConfig.Database.Type, appConfig.Database.Dsn); err != nil {
104100
return errors.New(i18n.T("config.error_init_db", err))
105101
}
106102

@@ -115,6 +111,27 @@ func setupDefaultServices(cmd *cobra.Command, args []string) error {
115111
return nil
116112
}
117113

114+
// main is the entry point of the application.
115+
func main() {
116+
// Install signal handler for graceful shutdown of bootstrap sessions
117+
bootstrap.InstallSignalHandler()
118+
119+
// Set up cleanup store for bootstrap operations
120+
defer func() {
121+
if err := bootstrap.CleanupAllActiveSessions(); err != nil {
122+
log.Printf("Error during final cleanup: %w", err)
123+
} else {
124+
log.Println("Bootstrap cleanup complete.")
125+
}
126+
}()
127+
128+
rootCmd := NewRootCmd()
129+
130+
if err := rootCmd.Execute(); err != nil {
131+
os.Exit(1)
132+
}
133+
}
134+
118135
func applyDefaultFlags(cmd *cobra.Command) {
119136
cmd.Flags().String("database.type", "sqlite", "Database type (e.g., sqlite, postgres)")
120137
cmd.Flags().String("database.dsn", "./keymaster.db", "Database connection string (DSN)")
@@ -160,7 +177,7 @@ Running without a subcommand will launch the interactive TUI.`,
160177
PreRunE: setupDefaultServices,
161178
Run: func(cmd *cobra.Command, args []string) {
162179
// The database is already initialized by PersistentPreRunE.
163-
i18n.Init(viper.GetString("language")) // Initialize i18n for the TUI
180+
// i18n is also initialized, so we can just run the TUI.
164181
tui.Run()
165182
},
166183
Version: version,

‎internal/config/config.go‎

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,17 @@ import (
1212
"github.com/spf13/viper"
1313
)
1414

15-
// getConfigPath returns the full path for the configuration file.
16-
func getConfigPath(system bool) (string, error) {
15+
// Config holds the application's configuration, loaded from file/env/flags.
16+
type Config struct {
17+
Database struct {
18+
Type string `mapstructure:"type"`
19+
Dsn string `mapstructure:"dsn"`
20+
} `mapstructure:"database"`
21+
Language string `mapstructure:"language"`
22+
}
23+
24+
// GetConfigPath returns the full path for the configuration file.
25+
func GetConfigPath(system bool) (string, error) {
1726
var configDir string
1827
var err error
1928

@@ -39,61 +48,63 @@ func getConfigPath(system bool) (string, error) {
3948

4049
func LoadConfig[T any](cmd *cobra.Command, defaults map[string]any, additional_config_file_path *string) (T, error) {
4150
var c T
42-
v := viper.New()
4351

4452
// 1. Set defaults
4553
for key, value := range defaults {
46-
v.SetDefault(key, value)
54+
viper.SetDefault(key, value)
4755
}
4856

4957
// 2. Set up file search paths (new format: keymaster.yaml)
50-
v.SetConfigName("keymaster")
51-
v.SetConfigType("yaml")
58+
viper.SetConfigName("keymaster")
59+
viper.SetConfigType("yaml")
5260

5361
// 3. Add explicit config file path if provided via --config flag.
5462
// This has the highest precedence for file-based configuration.
5563
if additional_config_file_path != nil {
56-
v.SetConfigFile(*additional_config_file_path)
64+
viper.SetConfigFile(*additional_config_file_path)
5765
}
5866

5967
// 3. Add standard config locations
60-
if userConfigPath, err := getConfigPath(false); err == nil {
61-
v.AddConfigPath(filepath.Dir(userConfigPath))
68+
if userConfigPath, err := GetConfigPath(false); err == nil {
69+
viper.AddConfigPath(filepath.Dir(userConfigPath))
6270
}
63-
if systemConfigPath, err := getConfigPath(true); err == nil {
64-
v.AddConfigPath(filepath.Dir(systemConfigPath))
71+
if systemConfigPath, err := GetConfigPath(true); err == nil {
72+
viper.AddConfigPath(filepath.Dir(systemConfigPath))
6573
}
66-
v.AddConfigPath(".") // Look for keymaster.yaml in current dir
74+
viper.AddConfigPath(".") // Look for keymaster.yaml in current dir
6775

6876
// 5. Read in the primary config file.
69-
if err := v.ReadInConfig(); err != nil {
77+
// We declare readErr here so it's available in the function's scope for the final return.
78+
readErr := viper.ReadInConfig()
79+
if readErr != nil {
7080
// It's okay if the file is not found, but other errors are fatal.
71-
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
72-
return c, err
81+
if _, ok := readErr.(viper.ConfigFileNotFoundError); !ok {
82+
return c, readErr
7383
}
7484
}
7585

7686
// 6. For backward compatibility, check for and merge `.keymaster.yaml` in the current directory.
77-
mergeLegacyConfig(v)
87+
mergeLegacyConfig(viper.GetViper())
7888

7989
// 7. Read from environment variables
80-
v.AutomaticEnv()
81-
v.AllowEmptyEnv(true)
82-
v.SetEnvPrefix("keymaster")
83-
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
90+
viper.AutomaticEnv()
91+
viper.AllowEmptyEnv(true)
92+
viper.SetEnvPrefix("keymaster")
93+
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
8494

8595
// cli
8696
// TODO maybe needs to trigger additional parsing beferohand (most likely nots)
87-
if err := v.BindPFlags(cmd.Flags()); err != nil {
97+
if err := viper.BindPFlags(cmd.Flags()); err != nil {
8898
return c, err
8999
}
90100

91101
// parse config
92-
if err := v.Unmarshal(&c); err != nil {
102+
if err := viper.Unmarshal(&c); err != nil {
93103
return c, err
94104
}
95105

96-
return c, nil
106+
// Return the readErr (which will be nil or ConfigFileNotFoundError)
107+
return c, readErr
97108
}
98109

99110
// mergeLegacyConfig checks for a `.keymaster.yaml` file in the current directory
@@ -113,7 +124,7 @@ func mergeLegacyConfig(v *viper.Viper) {
113124
}
114125

115126
func WriteConfigFile[T any](c *T, system bool) error {
116-
path, err := getConfigPath(system)
127+
path, err := GetConfigPath(system)
117128
if err != nil {
118129
return err
119130
}
@@ -136,3 +147,16 @@ func WriteConfigFile[T any](c *T, system bool) error {
136147

137148
return nil
138149
}
150+
151+
// Save persists the current Viper configuration to the user's config file.
152+
// It unmarshals the current state into the Config struct to ensure the
153+
// file structure (e.g., nested database keys) is preserved.
154+
func Save() error {
155+
var currentConfig Config
156+
if err := viper.Unmarshal(&currentConfig); err != nil {
157+
return fmt.Errorf("failed to unmarshal current config for saving: %w", err)
158+
}
159+
160+
// Write the structured config to the user-specific file.
161+
return WriteConfigFile(&currentConfig, false)
162+
}

‎internal/tui/tui.go‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ import (
1313
"sort"
1414
"strings"
1515

16+
tea "github.com/charmbracelet/bubbletea"
1617
"github.com/charmbracelet/lipgloss"
18+
"github.com/spf13/viper"
19+
"github.com/toeirei/keymaster/internal/config"
1720
"github.com/toeirei/keymaster/internal/db"
1821
"github.com/toeirei/keymaster/internal/i18n"
1922
"github.com/toeirei/keymaster/internal/model"
20-
21-
tea "github.com/charmbracelet/bubbletea"
22-
"github.com/spf13/viper"
2323
)
2424

2525
// viewState represents which part of the UI is currently active.
@@ -293,8 +293,9 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
293293
langCode := m.language.orderedKeys[m.language.cursor]
294294
i18n.SetLang(langCode)
295295
viper.Set("language", langCode)
296-
// We can ignore the error here as it's not critical for the session.
297-
_ = viper.WriteConfig()
296+
if err := config.Save(); err != nil {
297+
m.err = fmt.Errorf("failed to save config: %w", err)
298+
}
298299

299300
// Signal that the language has changed so the entire UI can be re-initialized.
300301
return m, func() tea.Msg { return languageChangedMsg{} }

0 commit comments

Comments
 (0)