@@ -5,11 +5,9 @@ import (
55 "fmt"
66 "os"
77 "path/filepath"
8- "reflect"
98 "strings"
109 "text/tabwriter"
1110
12- "github.com/BurntSushi/toml"
1311 "github.com/roborev-dev/roborev/internal/config"
1412 "github.com/roborev-dev/roborev/internal/git"
1513 "github.com/spf13/cobra"
@@ -384,27 +382,39 @@ func printKeyValues(kvs []config.KeyValue) {
384382 }
385383}
386384
387- // setConfigKey sets a key in a TOML file using raw map manipulation
388- // to avoid writing default values for every field.
389- // isGlobal determines which struct (Config vs RepoConfig) validates the key.
385+ // setConfigKey sets a key in a TOML config file.
390386func setConfigKey (path , key , value string , isGlobal bool ) error {
391- validationCfg , err := validateKeyForScope (key , value , isGlobal )
387+ _ , err := validateKeyForScope (key , value , isGlobal )
392388 if err != nil {
393389 return err
394390 }
395391
396- raw , err := loadRawConfig (path )
392+ if isGlobal {
393+ cfg , err := config .LoadGlobalFrom (path )
394+ if err != nil {
395+ return fmt .Errorf ("parse %s: %w" , path , err )
396+ }
397+ if err := config .SetConfigValue (cfg , key , value ); err != nil {
398+ return err
399+ }
400+ return config .SaveGlobalTo (path , cfg )
401+ }
402+
403+ repoDir := filepath .Dir (path )
404+ repoCfg , err := config .LoadRepoConfig (repoDir )
397405 if err != nil {
406+ return fmt .Errorf ("parse %s: %w" , path , err )
407+ }
408+ if repoCfg == nil {
409+ repoCfg = & config.RepoConfig {}
410+ }
411+ if err := config .SetConfigValue (repoCfg , key , value ); err != nil {
398412 return err
399413 }
400-
401- setRawMapKey (raw , key , coerceValue (validationCfg , key , value ))
402-
403- return atomicWriteConfig (path , raw , isGlobal )
414+ return config .SaveRepoConfigTo (path , repoCfg )
404415}
405416
406- // validateKeyForScope validates a key against the appropriate config struct
407- // and returns the populated struct for type coercion.
417+ // validateKeyForScope validates a key against the appropriate config scope.
408418func validateKeyForScope (key , value string , isGlobal bool ) (any , error ) {
409419 if isGlobal {
410420 cfg := config .DefaultConfig ()
@@ -429,72 +439,6 @@ func validateKeyForScope(key, value string, isGlobal bool) (any, error) {
429439 return repoCfg , nil
430440}
431441
432- // loadRawConfig loads an existing TOML file as a raw map, or returns
433- // an empty map if the file doesn't exist.
434- func loadRawConfig (path string ) (map [string ]any , error ) {
435- raw := make (map [string ]any )
436- if _ , err := os .Stat (path ); err == nil {
437- if _ , err := toml .DecodeFile (path , & raw ); err != nil {
438- return nil , fmt .Errorf ("parse %s: %w" , path , err )
439- }
440- }
441- return raw , nil
442- }
443-
444- // atomicWriteConfig writes a config map to a TOML file atomically using
445- // a temp file and rename. It creates parent directories as needed.
446- func atomicWriteConfig (path string , raw map [string ]any , isGlobal bool ) error {
447- // Ensure directory exists. Use restrictive perms for the global config dir
448- // since it may contain secrets (API keys, DB credentials).
449- dirMode := os .FileMode (0755 )
450- if isGlobal {
451- dirMode = 0700
452- }
453- dir := filepath .Dir (path )
454- if err := os .MkdirAll (dir , dirMode ); err != nil {
455- return err
456- }
457- // MkdirAll is a no-op for existing dirs. Tighten global config dir
458- // in case it was created with a permissive umask.
459- if isGlobal {
460- if err := os .Chmod (dir , dirMode ); err != nil {
461- return err
462- }
463- }
464-
465- // For global config, always enforce 0600 since it may contain secrets
466- // (API keys, DB credentials). Don't inherit existing permissions — the file
467- // may have been created manually with a permissive umask (0644).
468- // For repo config, preserve existing permissions (may be tracked in git).
469- var mode os.FileMode = 0644
470- if isGlobal {
471- mode = 0600
472- } else if info , err := os .Stat (path ); err == nil {
473- mode = info .Mode ()
474- }
475-
476- f , err := os .CreateTemp (filepath .Dir (path ), ".roborev-config-*.toml" )
477- if err != nil {
478- return err
479- }
480- tmpPath := f .Name ()
481- defer os .Remove (tmpPath )
482-
483- if err := toml .NewEncoder (f ).Encode (raw ); err != nil {
484- f .Close ()
485- return err
486- }
487- if err := f .Close (); err != nil {
488- return err
489- }
490-
491- if err := os .Chmod (tmpPath , mode ); err != nil {
492- return err
493- }
494-
495- return os .Rename (tmpPath , path )
496- }
497-
498442// setRawMapKey sets a value in a nested map using dot-separated keys.
499443func setRawMapKey (m map [string ]any , key string , value any ) {
500444 parts := strings .Split (key , "." )
@@ -525,47 +469,3 @@ func setRawMapKey(m map[string]any, key string, value any) {
525469
526470 current [parts [len (parts )- 1 ]] = value
527471}
528-
529- // coerceValue uses the typed config struct to determine the correct TOML type
530- // for the given key's value.
531- func coerceValue (validationCfg any , key , rawVal string ) any {
532- v := reflect .ValueOf (validationCfg )
533- if v .Kind () == reflect .Ptr {
534- v = v .Elem ()
535- }
536- field , err := config .FindFieldByTOMLKey (v , key )
537- if err != nil {
538- // Unreachable: key was already validated by SetConfigValue above.
539- // Fall back to raw string to avoid panicking on impossible paths.
540- return rawVal
541- }
542-
543- switch field .Kind () {
544- case reflect .String :
545- return rawVal
546- case reflect .Bool :
547- return field .Bool ()
548- case reflect .Int , reflect .Int64 :
549- return field .Int ()
550- case reflect .Slice :
551- if field .Type ().Elem ().Kind () == reflect .String {
552- result := make ([]any , field .Len ())
553- for i := 0 ; i < field .Len (); i ++ {
554- result [i ] = field .Index (i ).String ()
555- }
556- return result
557- }
558- return rawVal
559- case reflect .Ptr :
560- if field .IsNil () {
561- return rawVal
562- }
563- elem := field .Elem ()
564- if elem .Kind () == reflect .Bool {
565- return elem .Bool ()
566- }
567- return rawVal
568- default :
569- return rawVal
570- }
571- }
0 commit comments