From d4ca8b626759b4e35c69a48b38a9e07522fd3593 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Jan 2026 17:37:27 +0000 Subject: [PATCH 1/2] Add CLI commands for config get/set operations Add new 'sequoia config' subcommands to read and modify configuration values: - `config get ` - retrieves a config value by key (supports dot notation for nested values) - `config set ` - sets a config value and writes to config.yaml - `config show` - displays the entire configuration Examples: sequoia config get queue_interval sequoia config set queue_interval 50 sequoia config get api_config.port sequoia config set api_config.port 8080 --- cmd/config/config.go | 258 +++++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 3 +- 2 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 cmd/config/config.go diff --git a/cmd/config/config.go b/cmd/config/config.go new file mode 100644 index 0000000..9488f47 --- /dev/null +++ b/cmd/config/config.go @@ -0,0 +1,258 @@ +package config + +import ( + "fmt" + "os" + "path" + "reflect" + "strconv" + "strings" + + "github.com/JackalLabs/sequoia/cmd/types" + "github.com/JackalLabs/sequoia/config" + "github.com/spf13/cobra" +) + +// ConfigCmd returns the parent command for config operations +func ConfigCmd() *cobra.Command { + c := &cobra.Command{ + Use: "config", + Short: "Config subcommands", + } + + c.AddCommand(getCmd(), setCmd(), showCmd()) + + return c +} + +// showCmd returns the command to show the entire config +func showCmd() *cobra.Command { + return &cobra.Command{ + Use: "show", + Short: "Show the entire configuration", + RunE: func(cmd *cobra.Command, args []string) error { + home, err := cmd.Flags().GetString(types.FlagHome) + if err != nil { + return err + } + + cfg, err := config.Init(home) + if err != nil { + return err + } + + data, err := cfg.Export() + if err != nil { + return err + } + + fmt.Print(string(data)) + return nil + }, + } +} + +// getCmd returns the command to get a config value +func getCmd() *cobra.Command { + return &cobra.Command{ + Use: "get [key]", + Short: "Get a config value", + Long: `Get a config value by key. Use dot notation for nested values. + +Examples: + sequoia config get queue_interval + sequoia config get api_config.port + sequoia config get chain_config.rpc_addr`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + key := args[0] + + home, err := cmd.Flags().GetString(types.FlagHome) + if err != nil { + return err + } + + cfg, err := config.Init(home) + if err != nil { + return err + } + + value, err := getConfigValue(cfg, key) + if err != nil { + return err + } + + fmt.Println(value) + return nil + }, + } +} + +// setCmd returns the command to set a config value +func setCmd() *cobra.Command { + return &cobra.Command{ + Use: "set [key] [value]", + Short: "Set a config value", + Long: `Set a config value by key. Use dot notation for nested values. + +Examples: + sequoia config set queue_interval 50 + sequoia config set api_config.port 8080 + sequoia config set chain_config.rpc_addr http://localhost:26657`, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + key := args[0] + value := args[1] + + home, err := cmd.Flags().GetString(types.FlagHome) + if err != nil { + return err + } + + cfg, err := config.Init(home) + if err != nil { + return err + } + + if err := setConfigValue(cfg, key, value); err != nil { + return err + } + + // Write the updated config back to file + directory := os.ExpandEnv(home) + if err := writeConfigFile(directory, cfg); err != nil { + return err + } + + fmt.Printf("%s set to %s\n", key, value) + return nil + }, + } +} + +// writeConfigFile writes the config to the config file +func writeConfigFile(directory string, cfg *config.Config) error { + data, err := cfg.Export() + if err != nil { + return err + } + + filePath := path.Join(directory, config.ConfigFileName) + return os.WriteFile(filePath, data, 0644) +} + +// getConfigValue gets a config value by key using reflection +func getConfigValue(cfg *config.Config, key string) (interface{}, error) { + parts := strings.Split(key, ".") + v := reflect.ValueOf(cfg).Elem() + + for _, part := range parts { + field, err := findFieldByTag(v, part) + if err != nil { + return nil, err + } + v = field + } + + return v.Interface(), nil +} + +// setConfigValue sets a config value by key using reflection +func setConfigValue(cfg *config.Config, key string, value string) error { + parts := strings.Split(key, ".") + v := reflect.ValueOf(cfg).Elem() + + // Navigate to the parent of the field we want to set + for i := 0; i < len(parts)-1; i++ { + field, err := findFieldByTag(v, parts[i]) + if err != nil { + return err + } + v = field + } + + // Find and set the final field + field, err := findFieldByTag(v, parts[len(parts)-1]) + if err != nil { + return err + } + + if !field.CanSet() { + return fmt.Errorf("cannot set field %s", key) + } + + // Convert and set the value based on the field type + switch field.Kind() { + case reflect.String: + field.SetString(value) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + intVal, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return fmt.Errorf("invalid integer value: %s", value) + } + field.SetInt(intVal) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + uintVal, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return fmt.Errorf("invalid unsigned integer value: %s", value) + } + field.SetUint(uintVal) + case reflect.Float32, reflect.Float64: + floatVal, err := strconv.ParseFloat(value, 64) + if err != nil { + return fmt.Errorf("invalid float value: %s", value) + } + field.SetFloat(floatVal) + case reflect.Bool: + boolVal, err := strconv.ParseBool(value) + if err != nil { + return fmt.Errorf("invalid boolean value: %s (use true/false)", value) + } + field.SetBool(boolVal) + default: + return fmt.Errorf("unsupported field type: %s", field.Kind()) + } + + return nil +} + +// findFieldByTag finds a struct field by its yaml/mapstructure tag +func findFieldByTag(v reflect.Value, tag string) (reflect.Value, error) { + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + if v.Kind() != reflect.Struct { + return reflect.Value{}, fmt.Errorf("expected struct, got %s", v.Kind()) + } + + t := v.Type() + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + + // Check yaml tag + yamlTag := field.Tag.Get("yaml") + if yamlTag != "" { + yamlTag = strings.Split(yamlTag, ",")[0] + if yamlTag == tag { + return v.Field(i), nil + } + } + + // Check mapstructure tag + msTag := field.Tag.Get("mapstructure") + if msTag != "" { + msTag = strings.Split(msTag, ",")[0] + if msTag == tag { + return v.Field(i), nil + } + } + + // Also check field name (case-insensitive) + if strings.EqualFold(field.Name, tag) { + return v.Field(i), nil + } + } + + return reflect.Value{}, fmt.Errorf("unknown config key: %s", tag) +} diff --git a/cmd/root.go b/cmd/root.go index 03c8e30..69cddfe 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,6 +6,7 @@ import ( "os" "strings" + configcmd "github.com/JackalLabs/sequoia/cmd/config" "github.com/JackalLabs/sequoia/cmd/database" walletTypes "github.com/desmos-labs/cosmos-go-wallet/types" @@ -175,7 +176,7 @@ func RootCmd() *cobra.Command { panic(err) } - r.AddCommand(StartCmd(), wallet.WalletCmd(), InitCmd(), VersionCmd(), IPFSCmd(), ShutdownCmd(), database.DataCmd()) + r.AddCommand(StartCmd(), wallet.WalletCmd(), InitCmd(), VersionCmd(), IPFSCmd(), ShutdownCmd(), database.DataCmd(), configcmd.ConfigCmd()) return r } From 48b3576cae39fc78b2bf150d0d421391e88a57ee Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Jan 2026 22:00:27 +0000 Subject: [PATCH 2/2] Address PR review feedback for config CLI commands - Change file permissions from 0644 to 0600 for security (config may contain sensitive values) - Fix potential integer overflow by using field.Type().Bits() instead of hardcoded 64 - Improve struct handling by serializing nested config sections as YAML for better readability --- cmd/config/config.go | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/cmd/config/config.go b/cmd/config/config.go index 9488f47..6919066 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -11,6 +11,7 @@ import ( "github.com/JackalLabs/sequoia/cmd/types" "github.com/JackalLabs/sequoia/config" "github.com/spf13/cobra" + "gopkg.in/yaml.v3" ) // ConfigCmd returns the parent command for config operations @@ -138,23 +139,32 @@ func writeConfigFile(directory string, cfg *config.Config) error { } filePath := path.Join(directory, config.ConfigFileName) - return os.WriteFile(filePath, data, 0644) + return os.WriteFile(filePath, data, 0600) } // getConfigValue gets a config value by key using reflection -func getConfigValue(cfg *config.Config, key string) (interface{}, error) { +func getConfigValue(cfg *config.Config, key string) (string, error) { parts := strings.Split(key, ".") v := reflect.ValueOf(cfg).Elem() for _, part := range parts { field, err := findFieldByTag(v, part) if err != nil { - return nil, err + return "", err } v = field } - return v.Interface(), nil + // For struct types, serialize as YAML for better readability + if v.Kind() == reflect.Struct { + data, err := yaml.Marshal(v.Interface()) + if err != nil { + return "", fmt.Errorf("failed to serialize struct: %w", err) + } + return strings.TrimSpace(string(data)), nil + } + + return fmt.Sprintf("%v", v.Interface()), nil } // setConfigValue sets a config value by key using reflection @@ -186,19 +196,19 @@ func setConfigValue(cfg *config.Config, key string, value string) error { case reflect.String: field.SetString(value) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - intVal, err := strconv.ParseInt(value, 10, 64) + intVal, err := strconv.ParseInt(value, 10, field.Type().Bits()) if err != nil { return fmt.Errorf("invalid integer value: %s", value) } field.SetInt(intVal) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - uintVal, err := strconv.ParseUint(value, 10, 64) + uintVal, err := strconv.ParseUint(value, 10, field.Type().Bits()) if err != nil { return fmt.Errorf("invalid unsigned integer value: %s", value) } field.SetUint(uintVal) case reflect.Float32, reflect.Float64: - floatVal, err := strconv.ParseFloat(value, 64) + floatVal, err := strconv.ParseFloat(value, field.Type().Bits()) if err != nil { return fmt.Errorf("invalid float value: %s", value) }