diff --git a/cmd/configcmd/config.go b/cmd/configcmd/config.go index 42ed17af3..5f3c8af87 100644 --- a/cmd/configcmd/config.go +++ b/cmd/configcmd/config.go @@ -24,7 +24,11 @@ func NewCmd(injectedApp *application.Lux) *cobra.Command { }, } app = injectedApp - // set user metrics collection preferences cmd + // config subcommands + cmd.AddCommand(newInitCmd()) + cmd.AddCommand(newSetCmd()) + cmd.AddCommand(newGetCmd()) + cmd.AddCommand(newListCmd()) cmd.AddCommand(newMetricsCmd()) return cmd diff --git a/cmd/configcmd/get.go b/cmd/configcmd/get.go new file mode 100644 index 000000000..5050a3b7c --- /dev/null +++ b/cmd/configcmd/get.go @@ -0,0 +1,157 @@ +// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package configcmd + +import ( + "fmt" + "strings" + + "github.com/luxfi/cli/pkg/globalconfig" + "github.com/spf13/cobra" +) + +var getShowSource bool + +func newGetCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "get ", + Short: "Get a configuration value", + Long: `Get a configuration value, showing the effective value after merging all config sources. + +Keys use dot notation for nested values: + local.numNodes - Number of nodes for local network + local.autoTrackSubnets - Auto-track subnets in dev mode + network.defaultNetwork - Default network + evm.defaultTokenName - Default token name for EVM chains + +Use --source to also show where the value came from (default, global, project). + +Examples: + lux config get local.numNodes + lux config get --source evm.defaultTokenName`, + Args: cobra.ExactArgs(1), + RunE: runGet, + } + + cmd.Flags().BoolVar(&getShowSource, "source", false, "Show the source of the value") + + return cmd +} + +func runGet(_ *cobra.Command, args []string) error { + key := args[0] + baseDir := app.GetBaseDir() + + merged, err := globalconfig.GetEffectiveConfig(baseDir) + if err != nil { + return fmt.Errorf("failed to load config: %w", err) + } + + value, source, err := getConfigValue(merged, key) + if err != nil { + return err + } + + if getShowSource { + fmt.Printf("%s = %s (source: %s)\n", key, value, source) + } else { + fmt.Printf("%s = %s\n", key, value) + } + + return nil +} + +func getConfigValue(merged *globalconfig.MergedConfig, key string) (string, globalconfig.ConfigSource, error) { + parts := strings.SplitN(key, ".", 2) + if len(parts) != 2 { + return "", "", fmt.Errorf("invalid key format: use section.setting (e.g., local.numNodes)") + } + + section := parts[0] + setting := parts[1] + + switch section { + case "local": + return getLocalValue(merged, setting) + case "network": + return getNetworkValue(merged, setting) + case "evm": + return getEVMValue(merged, setting) + case "staking": + return getStakingValue(merged, setting) + case "node": + return getNodeValue(merged, setting) + default: + return "", "", fmt.Errorf("unknown section: %s", section) + } +} + +func getLocalValue(merged *globalconfig.MergedConfig, setting string) (string, globalconfig.ConfigSource, error) { + switch setting { + case "numNodes": + if merged.Config.Local.NumNodes != nil { + return fmt.Sprintf("%d", *merged.Config.Local.NumNodes), merged.Sources.NumNodes, nil + } + return fmt.Sprintf("%d", globalconfig.DefaultNumNodes), globalconfig.SourceDefault, nil + case "autoTrackSubnets": + if merged.Config.Local.AutoTrackSubnets != nil { + return fmt.Sprintf("%t", *merged.Config.Local.AutoTrackSubnets), merged.Sources.AutoTrackSubnets, nil + } + return "true", globalconfig.SourceDefault, nil + default: + return "", "", fmt.Errorf("unknown local setting: %s", setting) + } +} + +func getNetworkValue(merged *globalconfig.MergedConfig, setting string) (string, globalconfig.ConfigSource, error) { + switch setting { + case "defaultNetwork": + return merged.Config.Network.DefaultNetwork, merged.Sources.DefaultNetwork, nil + case "luxdVersion": + return merged.Config.Network.LuxdVersion, merged.Sources.LuxdVersion, nil + default: + return "", "", fmt.Errorf("unknown network setting: %s", setting) + } +} + +func getEVMValue(merged *globalconfig.MergedConfig, setting string) (string, globalconfig.ConfigSource, error) { + switch setting { + case "defaultTokenName": + return merged.Config.EVM.DefaultTokenName, merged.Sources.DefaultTokenName, nil + case "defaultTokenSymbol": + return merged.Config.EVM.DefaultTokenSymbol, merged.Sources.DefaultTokenSymbol, nil + case "defaultTokenSupply": + return merged.Config.EVM.DefaultTokenSupply, merged.Sources.DefaultTokenSupply, nil + default: + return "", "", fmt.Errorf("unknown evm setting: %s", setting) + } +} + +func getStakingValue(merged *globalconfig.MergedConfig, setting string) (string, globalconfig.ConfigSource, error) { + switch setting { + case "bootstrapValidatorBalance": + if merged.Config.Staking.BootstrapValidatorBalance != nil { + return fmt.Sprintf("%.2f", *merged.Config.Staking.BootstrapValidatorBalance), merged.Sources.BootstrapValidatorBalance, nil + } + return fmt.Sprintf("%.2f", globalconfig.DefaultBootstrapValidatorBalance), globalconfig.SourceDefault, nil + case "bootstrapValidatorWeight": + if merged.Config.Staking.BootstrapValidatorWeight != nil { + return fmt.Sprintf("%d", *merged.Config.Staking.BootstrapValidatorWeight), merged.Sources.BootstrapValidatorWeight, nil + } + return fmt.Sprintf("%d", globalconfig.DefaultBootstrapValidatorWeight), globalconfig.SourceDefault, nil + default: + return "", "", fmt.Errorf("unknown staking setting: %s", setting) + } +} + +func getNodeValue(merged *globalconfig.MergedConfig, setting string) (string, globalconfig.ConfigSource, error) { + switch setting { + case "defaultInstanceType": + return merged.Config.Node.DefaultInstanceType, merged.Sources.DefaultInstanceType, nil + case "defaultRegion": + return merged.Config.Node.DefaultRegion, merged.Sources.DefaultRegion, nil + default: + return "", "", fmt.Errorf("unknown node setting: %s", setting) + } +} diff --git a/cmd/configcmd/init.go b/cmd/configcmd/init.go new file mode 100644 index 000000000..d82482159 --- /dev/null +++ b/cmd/configcmd/init.go @@ -0,0 +1,101 @@ +// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package configcmd + +import ( + "fmt" + + "github.com/luxfi/cli/pkg/globalconfig" + "github.com/spf13/cobra" +) + +var ( + initProject bool + initForce bool +) + +func newInitCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "init", + Short: "Initialize configuration with smart defaults", + Long: `Initialize a new configuration file with smart defaults based on your environment. + +By default, creates a global configuration at ~/.lux/config.json. +Use --project to create a project-local configuration at .luxconfig.json. + +The command auto-detects your environment (CI, Codespace, development, production) +and suggests optimal defaults.`, + RunE: runInit, + } + + cmd.Flags().BoolVar(&initProject, "project", false, "Create project-local config instead of global") + cmd.Flags().BoolVar(&initForce, "force", false, "Overwrite existing config file") + + return cmd +} + +func runInit(_ *cobra.Command, _ []string) error { + // Get smart defaults based on environment + smart := globalconfig.GetSmartDefaults() + + fmt.Printf("Detected environment: %s\n", smart.Environment) + fmt.Printf("Suggested nodes: %d\n", smart.SuggestedNumNodes) + fmt.Printf("Suggested instance: %s\n", smart.SuggestedInstance) + + if initProject { + return initProjectConfig(smart) + } + return initGlobalConfig(smart) +} + +func initGlobalConfig(smart *globalconfig.SmartDefaults) error { + baseDir := app.GetBaseDir() + + // Check if config exists + existing, err := globalconfig.LoadGlobalConfig(baseDir) + if err != nil { + return fmt.Errorf("failed to check existing config: %w", err) + } + if existing != nil && !initForce { + return fmt.Errorf("config already exists at %s/config.json (use --force to overwrite)", baseDir) + } + + // Create config with smart defaults + config := globalconfig.DefaultGlobalConfig() + config.Local.NumNodes = &smart.SuggestedNumNodes + config.Node.DefaultInstanceType = smart.SuggestedInstance + + if err := globalconfig.SaveGlobalConfig(baseDir, &config); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + fmt.Printf("Created global config at %s/config.json\n", baseDir) + return nil +} + +func initProjectConfig(smart *globalconfig.SmartDefaults) error { + // Check if project config exists + existing, err := globalconfig.LoadProjectConfig() + if err != nil { + return fmt.Errorf("failed to check existing config: %w", err) + } + if existing != nil && !initForce { + return fmt.Errorf("project config already exists (use --force to overwrite)") + } + + // Create config with smart defaults + config := &globalconfig.ProjectConfig{ + GlobalConfig: globalconfig.DefaultGlobalConfig(), + ProjectName: "", + } + config.Local.NumNodes = &smart.SuggestedNumNodes + config.Node.DefaultInstanceType = smart.SuggestedInstance + + if err := globalconfig.SaveProjectConfig(config); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + fmt.Println("Created project config at .luxconfig.json") + return nil +} diff --git a/cmd/configcmd/list.go b/cmd/configcmd/list.go new file mode 100644 index 000000000..f0eba0bd9 --- /dev/null +++ b/cmd/configcmd/list.go @@ -0,0 +1,89 @@ +// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package configcmd + +import ( + "fmt" + + "github.com/luxfi/cli/pkg/globalconfig" + "github.com/spf13/cobra" +) + +var listShowSources bool + +func newListCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List all configuration values", + Long: `List all configuration values with their effective values. + +Shows the merged configuration from all sources (defaults, global, project). +Use --sources to see where each value comes from.`, + RunE: runList, + } + + cmd.Flags().BoolVar(&listShowSources, "sources", false, "Show the source of each value") + + return cmd +} + +func runList(_ *cobra.Command, _ []string) error { + baseDir := app.GetBaseDir() + + merged, err := globalconfig.GetEffectiveConfig(baseDir) + if err != nil { + return fmt.Errorf("failed to load config: %w", err) + } + + fmt.Println("Configuration:") + fmt.Println() + + // Network settings + fmt.Println("[network]") + printValue("defaultNetwork", merged.Config.Network.DefaultNetwork, merged.Sources.DefaultNetwork) + printValue("luxdVersion", merged.Config.Network.LuxdVersion, merged.Sources.LuxdVersion) + fmt.Println() + + // Local settings + fmt.Println("[local]") + if merged.Config.Local.NumNodes != nil { + printValue("numNodes", fmt.Sprintf("%d", *merged.Config.Local.NumNodes), merged.Sources.NumNodes) + } + if merged.Config.Local.AutoTrackSubnets != nil { + printValue("autoTrackSubnets", fmt.Sprintf("%t", *merged.Config.Local.AutoTrackSubnets), merged.Sources.AutoTrackSubnets) + } + fmt.Println() + + // EVM settings + fmt.Println("[evm]") + printValue("defaultTokenName", merged.Config.EVM.DefaultTokenName, merged.Sources.DefaultTokenName) + printValue("defaultTokenSymbol", merged.Config.EVM.DefaultTokenSymbol, merged.Sources.DefaultTokenSymbol) + printValue("defaultTokenSupply", merged.Config.EVM.DefaultTokenSupply, merged.Sources.DefaultTokenSupply) + fmt.Println() + + // Staking settings + fmt.Println("[staking]") + if merged.Config.Staking.BootstrapValidatorBalance != nil { + printValue("bootstrapValidatorBalance", fmt.Sprintf("%.2f", *merged.Config.Staking.BootstrapValidatorBalance), merged.Sources.BootstrapValidatorBalance) + } + if merged.Config.Staking.BootstrapValidatorWeight != nil { + printValue("bootstrapValidatorWeight", fmt.Sprintf("%d", *merged.Config.Staking.BootstrapValidatorWeight), merged.Sources.BootstrapValidatorWeight) + } + fmt.Println() + + // Node settings + fmt.Println("[node]") + printValue("defaultInstanceType", merged.Config.Node.DefaultInstanceType, merged.Sources.DefaultInstanceType) + printValue("defaultRegion", merged.Config.Node.DefaultRegion, merged.Sources.DefaultRegion) + + return nil +} + +func printValue(key, value string, source globalconfig.ConfigSource) { + if listShowSources { + fmt.Printf(" %s = %s (%s)\n", key, value, source) + } else { + fmt.Printf(" %s = %s\n", key, value) + } +} diff --git a/cmd/configcmd/set.go b/cmd/configcmd/set.go new file mode 100644 index 000000000..e4be8e190 --- /dev/null +++ b/cmd/configcmd/set.go @@ -0,0 +1,207 @@ +// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package configcmd + +import ( + "fmt" + "strconv" + "strings" + + "github.com/luxfi/cli/pkg/globalconfig" + "github.com/spf13/cobra" +) + +var setProject bool + +func newSetCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "set ", + Short: "Set a configuration value", + Long: `Set a configuration value in the global or project config. + +Keys use dot notation for nested values: + local.numNodes - Number of nodes for local network + local.autoTrackSubnets - Auto-track subnets in dev mode + network.defaultNetwork - Default network (local, testnet, mainnet) + network.luxdVersion - Default luxd version + evm.defaultTokenName - Default token name for EVM chains + evm.defaultTokenSymbol - Default token symbol + evm.defaultTokenSupply - Default token supply + staking.bootstrapValidatorBalance - Bootstrap validator balance + staking.bootstrapValidatorWeight - Bootstrap validator weight + node.defaultInstanceType - Default instance type + node.defaultRegion - Default region + +Examples: + lux config set local.numNodes 3 + lux config set evm.defaultTokenName "MyToken" + lux config set --project local.autoTrackSubnets true`, + Args: cobra.ExactArgs(2), + RunE: runSet, + } + + cmd.Flags().BoolVar(&setProject, "project", false, "Set value in project config instead of global") + + return cmd +} + +func runSet(_ *cobra.Command, args []string) error { + key := args[0] + value := args[1] + + if setProject { + return setProjectValue(key, value) + } + return setGlobalValue(key, value) +} + +func setGlobalValue(key, value string) error { + baseDir := app.GetBaseDir() + + config, err := globalconfig.LoadGlobalConfig(baseDir) + if err != nil { + return fmt.Errorf("failed to load config: %w", err) + } + if config == nil { + config = &globalconfig.GlobalConfig{Version: globalconfig.ConfigVersion} + } + + if err := applyConfigValue(config, key, value); err != nil { + return err + } + + if err := globalconfig.SaveGlobalConfig(baseDir, config); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + fmt.Printf("Set %s = %s in global config\n", key, value) + return nil +} + +func setProjectValue(key, value string) error { + config, err := globalconfig.LoadProjectConfig() + if err != nil { + return fmt.Errorf("failed to load config: %w", err) + } + if config == nil { + config = &globalconfig.ProjectConfig{ + GlobalConfig: globalconfig.GlobalConfig{Version: globalconfig.ConfigVersion}, + } + } + + if err := applyConfigValue(&config.GlobalConfig, key, value); err != nil { + return err + } + + if err := globalconfig.SaveProjectConfig(config); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + fmt.Printf("Set %s = %s in project config\n", key, value) + return nil +} + +func applyConfigValue(config *globalconfig.GlobalConfig, key, value string) error { + parts := strings.SplitN(key, ".", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid key format: use section.setting (e.g., local.numNodes)") + } + + section := parts[0] + setting := parts[1] + + switch section { + case "local": + return applyLocalSetting(config, setting, value) + case "network": + return applyNetworkSetting(config, setting, value) + case "evm": + return applyEVMSetting(config, setting, value) + case "staking": + return applyStakingSetting(config, setting, value) + case "node": + return applyNodeSetting(config, setting, value) + default: + return fmt.Errorf("unknown section: %s", section) + } +} + +func applyLocalSetting(config *globalconfig.GlobalConfig, setting, value string) error { + switch setting { + case "numNodes": + n, err := strconv.ParseUint(value, 10, 32) + if err != nil { + return fmt.Errorf("invalid numNodes value: %w", err) + } + num := uint32(n) + config.Local.NumNodes = &num + case "autoTrackSubnets": + b, err := strconv.ParseBool(value) + if err != nil { + return fmt.Errorf("invalid autoTrackSubnets value: %w", err) + } + config.Local.AutoTrackSubnets = &b + default: + return fmt.Errorf("unknown local setting: %s", setting) + } + return nil +} + +func applyNetworkSetting(config *globalconfig.GlobalConfig, setting, value string) error { + switch setting { + case "defaultNetwork": + config.Network.DefaultNetwork = value + case "luxdVersion": + config.Network.LuxdVersion = value + default: + return fmt.Errorf("unknown network setting: %s", setting) + } + return nil +} + +func applyEVMSetting(config *globalconfig.GlobalConfig, setting, value string) error { + switch setting { + case "defaultTokenName": + config.EVM.DefaultTokenName = value + case "defaultTokenSymbol": + config.EVM.DefaultTokenSymbol = value + case "defaultTokenSupply": + config.EVM.DefaultTokenSupply = value + default: + return fmt.Errorf("unknown evm setting: %s", setting) + } + return nil +} + +func applyStakingSetting(config *globalconfig.GlobalConfig, setting, value string) error { + switch setting { + case "bootstrapValidatorBalance": + f, err := strconv.ParseFloat(value, 64) + if err != nil { + return fmt.Errorf("invalid bootstrapValidatorBalance value: %w", err) + } + config.Staking.BootstrapValidatorBalance = &f + case "bootstrapValidatorWeight": + n, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return fmt.Errorf("invalid bootstrapValidatorWeight value: %w", err) + } + config.Staking.BootstrapValidatorWeight = &n + default: + return fmt.Errorf("unknown staking setting: %s", setting) + } + return nil +} + +func applyNodeSetting(config *globalconfig.GlobalConfig, setting, value string) error { + switch setting { + case "defaultInstanceType": + config.Node.DefaultInstanceType = value + case "defaultRegion": + config.Node.DefaultRegion = value + default: + return fmt.Errorf("unknown node setting: %s", setting) + } + return nil +} diff --git a/cmd/networkcmd/deploy.go b/cmd/networkcmd/deploy.go index ec940d022..c2d876db1 100644 --- a/cmd/networkcmd/deploy.go +++ b/cmd/networkcmd/deploy.go @@ -17,6 +17,7 @@ import ( "github.com/luxfi/cli/pkg/cobrautils" "github.com/luxfi/cli/pkg/constants" "github.com/luxfi/cli/pkg/dependencies" + "github.com/luxfi/cli/pkg/globalconfig" "github.com/luxfi/cli/pkg/interchain/relayer" "github.com/luxfi/cli/pkg/keychain" "github.com/luxfi/cli/pkg/localnet" @@ -592,11 +593,16 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { } ux.Logger.PrintToUser("") + // Get effective numNodes from config hierarchy if not overridden by flag + effectiveNumNodes := numNodes + if configNodes, err := globalconfig.GetNumNodes(app.GetBaseDir(), numNodes, false); err == nil { + effectiveNumNodes = configNodes + } if err := Start( StartFlags{ UserProvidedLuxdVersion: luxdVersion, LuxdBinaryPath: deployFlags.LocalMachineFlags.LuxdBinaryPath, - NumNodes: numNodes, + NumNodes: effectiveNumNodes, }, false, ); err != nil { diff --git a/pkg/globalconfig/app_integration.go b/pkg/globalconfig/app_integration.go new file mode 100644 index 000000000..ebc31525e --- /dev/null +++ b/pkg/globalconfig/app_integration.go @@ -0,0 +1,113 @@ +// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package globalconfig + +// GetNumNodes returns the effective number of nodes considering config hierarchy +// Priority: flagValue (if flagChanged) > project config > global config > smart defaults +func GetNumNodes(baseDir string, flagValue uint32, flagChanged bool) (uint32, error) { + if flagChanged { + return flagValue, nil + } + + merged, err := GetEffectiveConfig(baseDir) + if err != nil { + return SuggestNumNodes(), nil + } + + if merged.Config.Local.NumNodes != nil { + return *merged.Config.Local.NumNodes, nil + } + + return SuggestNumNodes(), nil +} + +// GetLuxdVersion returns the effective luxd version considering config hierarchy +func GetLuxdVersion(baseDir string, flagValue string, flagChanged bool) (string, error) { + if flagChanged && flagValue != "" { + return flagValue, nil + } + + merged, err := GetEffectiveConfig(baseDir) + if err != nil { + return "latest", nil + } + + if merged.Config.Network.LuxdVersion != "" { + return merged.Config.Network.LuxdVersion, nil + } + + return "latest", nil +} + +// GetAutoTrackSubnets returns whether subnets should be auto-tracked +func GetAutoTrackSubnets(baseDir string) (bool, error) { + merged, err := GetEffectiveConfig(baseDir) + if err != nil { + return IsAutoTrackRecommended(), nil + } + + if merged.Config.Local.AutoTrackSubnets != nil { + return *merged.Config.Local.AutoTrackSubnets, nil + } + + return IsAutoTrackRecommended(), nil +} + +// GetDefaultTokenSupply returns the default token supply from config +func GetDefaultTokenSupply(baseDir string) (string, error) { + merged, err := GetEffectiveConfig(baseDir) + if err != nil { + smart := GetSmartDefaults() + return SuggestTokenSupply(smart.IsTestnet), nil + } + + if merged.Config.EVM.DefaultTokenSupply != "" { + return merged.Config.EVM.DefaultTokenSupply, nil + } + + smart := GetSmartDefaults() + return SuggestTokenSupply(smart.IsTestnet), nil +} + +// GetBootstrapValidatorBalance returns the default bootstrap validator balance +func GetBootstrapValidatorBalance(baseDir string) (float64, error) { + merged, err := GetEffectiveConfig(baseDir) + if err != nil { + return DefaultBootstrapValidatorBalance, nil + } + + if merged.Config.Staking.BootstrapValidatorBalance != nil { + return *merged.Config.Staking.BootstrapValidatorBalance, nil + } + + return DefaultBootstrapValidatorBalance, nil +} + +// GetBootstrapValidatorWeight returns the default bootstrap validator weight +func GetBootstrapValidatorWeight(baseDir string) (uint64, error) { + merged, err := GetEffectiveConfig(baseDir) + if err != nil { + return DefaultBootstrapValidatorWeight, nil + } + + if merged.Config.Staking.BootstrapValidatorWeight != nil { + return *merged.Config.Staking.BootstrapValidatorWeight, nil + } + + return DefaultBootstrapValidatorWeight, nil +} + +// GetDefaultInstanceType returns the default instance type for node deployment +func GetDefaultInstanceType(baseDir string) (string, error) { + merged, err := GetEffectiveConfig(baseDir) + if err != nil { + return SuggestInstanceType(), nil + } + + if merged.Config.Node.DefaultInstanceType != "" { + return merged.Config.Node.DefaultInstanceType, nil + } + + return SuggestInstanceType(), nil +} diff --git a/pkg/globalconfig/defaults.go b/pkg/globalconfig/defaults.go new file mode 100644 index 000000000..9e8f30d24 --- /dev/null +++ b/pkg/globalconfig/defaults.go @@ -0,0 +1,63 @@ +// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package globalconfig + +const ( + // ConfigVersion is the current version of the config schema + ConfigVersion = "1.0.0" + + // Default local network settings + DefaultNumNodes = uint32(5) + + // Default staking settings + DefaultBootstrapValidatorBalance = float64(1000) + DefaultBootstrapValidatorWeight = uint64(20) + + // Default EVM settings + DefaultTokenName = "TEST" + DefaultTokenSymbol = "TST" + DefaultTokenSupply = "1000000000000000000000000" // 1 million tokens with 18 decimals + + // Default network + DefaultNetwork = "local" + + // Default node settings + DefaultInstanceType = "default" + DefaultRegion = "us-east-1" +) + +// DefaultGlobalConfig returns a new GlobalConfig with default values +func DefaultGlobalConfig() GlobalConfig { + metricsEnabled := true + numNodes := DefaultNumNodes + autoTrack := true + validatorBalance := DefaultBootstrapValidatorBalance + validatorWeight := DefaultBootstrapValidatorWeight + + return GlobalConfig{ + Version: ConfigVersion, + MetricsEnabled: &metricsEnabled, + Network: NetworkConfig{ + DefaultNetwork: DefaultNetwork, + LuxdVersion: "latest", + }, + Local: LocalConfig{ + NumNodes: &numNodes, + AutoTrackSubnets: &autoTrack, + }, + EVM: EVMConfig{ + DefaultTokenName: DefaultTokenName, + DefaultTokenSymbol: DefaultTokenSymbol, + DefaultTokenSupply: DefaultTokenSupply, + }, + Staking: StakingConfig{ + BootstrapValidatorBalance: &validatorBalance, + BootstrapValidatorWeight: &validatorWeight, + }, + Node: NodeConfig{ + DefaultInstanceType: DefaultInstanceType, + DefaultRegion: DefaultRegion, + }, + } +} diff --git a/pkg/globalconfig/loader.go b/pkg/globalconfig/loader.go new file mode 100644 index 000000000..b76932454 --- /dev/null +++ b/pkg/globalconfig/loader.go @@ -0,0 +1,139 @@ +// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package globalconfig + +import ( + "encoding/json" + "os" + "path/filepath" + "sync" +) + +const ( + GlobalConfigFile = "config.json" + ProjectConfigFile = ".luxconfig.json" +) + +var ( + globalConfigCache *GlobalConfig + cacheMu sync.RWMutex +) + +// LoadGlobalConfig loads the global config from ~/.lux/config.json +func LoadGlobalConfig(baseDir string) (*GlobalConfig, error) { + cacheMu.RLock() + if globalConfigCache != nil { + defer cacheMu.RUnlock() + return globalConfigCache, nil + } + cacheMu.RUnlock() + + configPath := filepath.Join(baseDir, GlobalConfigFile) + data, err := os.ReadFile(configPath) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + + var config GlobalConfig + if err := json.Unmarshal(data, &config); err != nil { + return nil, err + } + + cacheMu.Lock() + globalConfigCache = &config + cacheMu.Unlock() + + return &config, nil +} + +// SaveGlobalConfig saves the global config to ~/.lux/config.json +func SaveGlobalConfig(baseDir string, config *GlobalConfig) error { + if err := os.MkdirAll(baseDir, 0o755); err != nil { + return err + } + + configPath := filepath.Join(baseDir, GlobalConfigFile) + data, err := json.MarshalIndent(config, "", " ") + if err != nil { + return err + } + + cacheMu.Lock() + globalConfigCache = config + cacheMu.Unlock() + + return os.WriteFile(configPath, data, 0o644) +} + +// LoadProjectConfig loads the project config by searching upward from cwd +func LoadProjectConfig() (*ProjectConfig, error) { + cwd, err := os.Getwd() + if err != nil { + return nil, err + } + + projectRoot, err := FindProjectRoot(cwd) + if err != nil { + return nil, nil // No project config found + } + + configPath := filepath.Join(projectRoot, ProjectConfigFile) + data, err := os.ReadFile(configPath) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + + var config ProjectConfig + if err := json.Unmarshal(data, &config); err != nil { + return nil, err + } + + return &config, nil +} + +// SaveProjectConfig saves the project config to .luxconfig.json in cwd +func SaveProjectConfig(config *ProjectConfig) error { + cwd, err := os.Getwd() + if err != nil { + return err + } + + configPath := filepath.Join(cwd, ProjectConfigFile) + data, err := json.MarshalIndent(config, "", " ") + if err != nil { + return err + } + + return os.WriteFile(configPath, data, 0o644) +} + +// FindProjectRoot searches upward from startDir to find .luxconfig.json +func FindProjectRoot(startDir string) (string, error) { + dir := startDir + for { + configPath := filepath.Join(dir, ProjectConfigFile) + if _, err := os.Stat(configPath); err == nil { + return dir, nil + } + + parent := filepath.Dir(dir) + if parent == dir { + return "", os.ErrNotExist + } + dir = parent + } +} + +// ClearCache clears the global config cache +func ClearCache() { + cacheMu.Lock() + globalConfigCache = nil + cacheMu.Unlock() +} diff --git a/pkg/globalconfig/loader_test.go b/pkg/globalconfig/loader_test.go new file mode 100644 index 000000000..75286d30c --- /dev/null +++ b/pkg/globalconfig/loader_test.go @@ -0,0 +1,133 @@ +// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package globalconfig + +import ( + "os" + "path/filepath" + "testing" +) + +func TestLoadGlobalConfig(t *testing.T) { + // Create temp directory + tmpDir := t.TempDir() + + // Test loading non-existent config returns nil + ClearCache() + config, err := LoadGlobalConfig(tmpDir) + if err != nil { + t.Fatalf("unexpected error loading non-existent config: %v", err) + } + if config != nil { + t.Fatal("expected nil config for non-existent file") + } + + // Test loading valid config + testConfig := DefaultGlobalConfig() + err = SaveGlobalConfig(tmpDir, &testConfig) + if err != nil { + t.Fatalf("failed to save config: %v", err) + } + + ClearCache() + loaded, err := LoadGlobalConfig(tmpDir) + if err != nil { + t.Fatalf("failed to load config: %v", err) + } + if loaded == nil { + t.Fatal("expected non-nil config") + } + if loaded.Version != ConfigVersion { + t.Errorf("expected version %s, got %s", ConfigVersion, loaded.Version) + } +} + +func TestSaveGlobalConfig(t *testing.T) { + tmpDir := t.TempDir() + + config := DefaultGlobalConfig() + err := SaveGlobalConfig(tmpDir, &config) + if err != nil { + t.Fatalf("failed to save config: %v", err) + } + + // Verify file exists + configPath := filepath.Join(tmpDir, GlobalConfigFile) + if _, err := os.Stat(configPath); os.IsNotExist(err) { + t.Fatal("config file was not created") + } +} + +func TestFindProjectRoot(t *testing.T) { + // Create temp directory structure + tmpDir := t.TempDir() + subDir := filepath.Join(tmpDir, "sub", "dir") + err := os.MkdirAll(subDir, 0o755) + if err != nil { + t.Fatalf("failed to create subdirectory: %v", err) + } + + // Test not found + _, err = FindProjectRoot(subDir) + if !os.IsNotExist(err) { + t.Fatal("expected ErrNotExist for missing project config") + } + + // Create project config at root + configPath := filepath.Join(tmpDir, ProjectConfigFile) + data := []byte(`{"projectName":"test-project","version":"1.0.0"}`) + err = os.WriteFile(configPath, data, 0o644) + if err != nil { + t.Fatalf("failed to write project config: %v", err) + } + + // Test finding from subdirectory + foundRoot, err := FindProjectRoot(subDir) + if err != nil { + t.Fatalf("failed to find project root: %v", err) + } + if foundRoot != tmpDir { + t.Errorf("expected root %s, got %s", tmpDir, foundRoot) + } +} + +func TestCacheClearing(t *testing.T) { + tmpDir := t.TempDir() + + // Save initial config + config1 := DefaultGlobalConfig() + numNodes := uint32(3) + config1.Local.NumNodes = &numNodes + err := SaveGlobalConfig(tmpDir, &config1) + if err != nil { + t.Fatalf("failed to save config: %v", err) + } + + // Load and verify + ClearCache() + loaded1, err := LoadGlobalConfig(tmpDir) + if err != nil { + t.Fatalf("failed to load config: %v", err) + } + if *loaded1.Local.NumNodes != 3 { + t.Errorf("expected numNodes=3, got %d", *loaded1.Local.NumNodes) + } + + // Update config + numNodes2 := uint32(7) + config1.Local.NumNodes = &numNodes2 + err = SaveGlobalConfig(tmpDir, &config1) + if err != nil { + t.Fatalf("failed to save updated config: %v", err) + } + + // Load again (cache should be updated) + loaded2, err := LoadGlobalConfig(tmpDir) + if err != nil { + t.Fatalf("failed to load updated config: %v", err) + } + if *loaded2.Local.NumNodes != 7 { + t.Errorf("expected numNodes=7 after update, got %d", *loaded2.Local.NumNodes) + } +} diff --git a/pkg/globalconfig/merged.go b/pkg/globalconfig/merged.go new file mode 100644 index 000000000..c46f9894c --- /dev/null +++ b/pkg/globalconfig/merged.go @@ -0,0 +1,204 @@ +// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package globalconfig + +// ConfigSource indicates where a config value came from +type ConfigSource string + +const ( + SourceDefault ConfigSource = "default" + SourceGlobal ConfigSource = "global" + SourceProject ConfigSource = "project" + SourceFlag ConfigSource = "flag" +) + +// MergedConfig holds the final merged configuration with source tracking +type MergedConfig struct { + Config GlobalConfig + Sources ConfigSources +} + +// ConfigSources tracks where each config value originated +type ConfigSources struct { + NumNodes ConfigSource + AutoTrackSubnets ConfigSource + DefaultNetwork ConfigSource + LuxdVersion ConfigSource + DefaultTokenName ConfigSource + DefaultTokenSymbol ConfigSource + DefaultTokenSupply ConfigSource + BootstrapValidatorBalance ConfigSource + BootstrapValidatorWeight ConfigSource + DefaultInstanceType ConfigSource + DefaultRegion ConfigSource +} + +// Merge combines default, global, and project configs with proper precedence +// Hierarchy: project > global > defaults +func Merge(global *GlobalConfig, project *ProjectConfig) *MergedConfig { + defaults := DefaultGlobalConfig() + merged := &MergedConfig{ + Config: defaults, + Sources: ConfigSources{ + NumNodes: SourceDefault, + AutoTrackSubnets: SourceDefault, + DefaultNetwork: SourceDefault, + LuxdVersion: SourceDefault, + DefaultTokenName: SourceDefault, + DefaultTokenSymbol: SourceDefault, + DefaultTokenSupply: SourceDefault, + BootstrapValidatorBalance: SourceDefault, + BootstrapValidatorWeight: SourceDefault, + DefaultInstanceType: SourceDefault, + DefaultRegion: SourceDefault, + }, + } + + // Apply global config + if global != nil { + mergeGlobalConfig(merged, global) + } + + // Apply project config (highest precedence) + if project != nil { + mergeProjectConfig(merged, project) + } + + return merged +} + +func mergeGlobalConfig(merged *MergedConfig, global *GlobalConfig) { + if global.MetricsEnabled != nil { + merged.Config.MetricsEnabled = global.MetricsEnabled + } + + // Network settings + if global.Network.DefaultNetwork != "" { + merged.Config.Network.DefaultNetwork = global.Network.DefaultNetwork + merged.Sources.DefaultNetwork = SourceGlobal + } + if global.Network.LuxdVersion != "" { + merged.Config.Network.LuxdVersion = global.Network.LuxdVersion + merged.Sources.LuxdVersion = SourceGlobal + } + + // Local settings + if global.Local.NumNodes != nil { + merged.Config.Local.NumNodes = global.Local.NumNodes + merged.Sources.NumNodes = SourceGlobal + } + if global.Local.AutoTrackSubnets != nil { + merged.Config.Local.AutoTrackSubnets = global.Local.AutoTrackSubnets + merged.Sources.AutoTrackSubnets = SourceGlobal + } + + // EVM settings + if global.EVM.DefaultTokenName != "" { + merged.Config.EVM.DefaultTokenName = global.EVM.DefaultTokenName + merged.Sources.DefaultTokenName = SourceGlobal + } + if global.EVM.DefaultTokenSymbol != "" { + merged.Config.EVM.DefaultTokenSymbol = global.EVM.DefaultTokenSymbol + merged.Sources.DefaultTokenSymbol = SourceGlobal + } + if global.EVM.DefaultTokenSupply != "" { + merged.Config.EVM.DefaultTokenSupply = global.EVM.DefaultTokenSupply + merged.Sources.DefaultTokenSupply = SourceGlobal + } + + // Staking settings + if global.Staking.BootstrapValidatorBalance != nil { + merged.Config.Staking.BootstrapValidatorBalance = global.Staking.BootstrapValidatorBalance + merged.Sources.BootstrapValidatorBalance = SourceGlobal + } + if global.Staking.BootstrapValidatorWeight != nil { + merged.Config.Staking.BootstrapValidatorWeight = global.Staking.BootstrapValidatorWeight + merged.Sources.BootstrapValidatorWeight = SourceGlobal + } + + // Node settings + if global.Node.DefaultInstanceType != "" { + merged.Config.Node.DefaultInstanceType = global.Node.DefaultInstanceType + merged.Sources.DefaultInstanceType = SourceGlobal + } + if global.Node.DefaultRegion != "" { + merged.Config.Node.DefaultRegion = global.Node.DefaultRegion + merged.Sources.DefaultRegion = SourceGlobal + } +} + +func mergeProjectConfig(merged *MergedConfig, project *ProjectConfig) { + if project.MetricsEnabled != nil { + merged.Config.MetricsEnabled = project.MetricsEnabled + } + + // Network settings + if project.Network.DefaultNetwork != "" { + merged.Config.Network.DefaultNetwork = project.Network.DefaultNetwork + merged.Sources.DefaultNetwork = SourceProject + } + if project.Network.LuxdVersion != "" { + merged.Config.Network.LuxdVersion = project.Network.LuxdVersion + merged.Sources.LuxdVersion = SourceProject + } + + // Local settings + if project.Local.NumNodes != nil { + merged.Config.Local.NumNodes = project.Local.NumNodes + merged.Sources.NumNodes = SourceProject + } + if project.Local.AutoTrackSubnets != nil { + merged.Config.Local.AutoTrackSubnets = project.Local.AutoTrackSubnets + merged.Sources.AutoTrackSubnets = SourceProject + } + + // EVM settings + if project.EVM.DefaultTokenName != "" { + merged.Config.EVM.DefaultTokenName = project.EVM.DefaultTokenName + merged.Sources.DefaultTokenName = SourceProject + } + if project.EVM.DefaultTokenSymbol != "" { + merged.Config.EVM.DefaultTokenSymbol = project.EVM.DefaultTokenSymbol + merged.Sources.DefaultTokenSymbol = SourceProject + } + if project.EVM.DefaultTokenSupply != "" { + merged.Config.EVM.DefaultTokenSupply = project.EVM.DefaultTokenSupply + merged.Sources.DefaultTokenSupply = SourceProject + } + + // Staking settings + if project.Staking.BootstrapValidatorBalance != nil { + merged.Config.Staking.BootstrapValidatorBalance = project.Staking.BootstrapValidatorBalance + merged.Sources.BootstrapValidatorBalance = SourceProject + } + if project.Staking.BootstrapValidatorWeight != nil { + merged.Config.Staking.BootstrapValidatorWeight = project.Staking.BootstrapValidatorWeight + merged.Sources.BootstrapValidatorWeight = SourceProject + } + + // Node settings + if project.Node.DefaultInstanceType != "" { + merged.Config.Node.DefaultInstanceType = project.Node.DefaultInstanceType + merged.Sources.DefaultInstanceType = SourceProject + } + if project.Node.DefaultRegion != "" { + merged.Config.Node.DefaultRegion = project.Node.DefaultRegion + merged.Sources.DefaultRegion = SourceProject + } +} + +// GetEffectiveConfig loads and merges all config sources +func GetEffectiveConfig(baseDir string) (*MergedConfig, error) { + global, err := LoadGlobalConfig(baseDir) + if err != nil { + return nil, err + } + + project, err := LoadProjectConfig() + if err != nil { + return nil, err + } + + return Merge(global, project), nil +} diff --git a/pkg/globalconfig/merged_test.go b/pkg/globalconfig/merged_test.go new file mode 100644 index 000000000..e688aadda --- /dev/null +++ b/pkg/globalconfig/merged_test.go @@ -0,0 +1,141 @@ +// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package globalconfig + +import ( + "testing" +) + +func TestMergeDefaults(t *testing.T) { + merged := Merge(nil, nil) + + if merged.Config.Local.NumNodes == nil || *merged.Config.Local.NumNodes != DefaultNumNodes { + t.Errorf("expected default numNodes %d", DefaultNumNodes) + } + if merged.Sources.NumNodes != SourceDefault { + t.Errorf("expected source %s, got %s", SourceDefault, merged.Sources.NumNodes) + } +} + +func TestMergeGlobalOverridesDefaults(t *testing.T) { + numNodes := uint32(10) + global := &GlobalConfig{ + Local: LocalConfig{ + NumNodes: &numNodes, + }, + } + + merged := Merge(global, nil) + + if *merged.Config.Local.NumNodes != 10 { + t.Errorf("expected numNodes 10, got %d", *merged.Config.Local.NumNodes) + } + if merged.Sources.NumNodes != SourceGlobal { + t.Errorf("expected source %s, got %s", SourceGlobal, merged.Sources.NumNodes) + } +} + +func TestMergeProjectOverridesGlobal(t *testing.T) { + globalNodes := uint32(10) + global := &GlobalConfig{ + Local: LocalConfig{ + NumNodes: &globalNodes, + }, + } + + projectNodes := uint32(3) + project := &ProjectConfig{ + GlobalConfig: GlobalConfig{ + Local: LocalConfig{ + NumNodes: &projectNodes, + }, + }, + } + + merged := Merge(global, project) + + if *merged.Config.Local.NumNodes != 3 { + t.Errorf("expected numNodes 3, got %d", *merged.Config.Local.NumNodes) + } + if merged.Sources.NumNodes != SourceProject { + t.Errorf("expected source %s, got %s", SourceProject, merged.Sources.NumNodes) + } +} + +func TestMergePartialOverride(t *testing.T) { + // Global sets numNodes, project sets autoTrack + globalNodes := uint32(7) + global := &GlobalConfig{ + Local: LocalConfig{ + NumNodes: &globalNodes, + }, + } + + autoTrack := false + project := &ProjectConfig{ + GlobalConfig: GlobalConfig{ + Local: LocalConfig{ + AutoTrackSubnets: &autoTrack, + }, + }, + } + + merged := Merge(global, project) + + // numNodes should come from global + if *merged.Config.Local.NumNodes != 7 { + t.Errorf("expected numNodes 7 from global, got %d", *merged.Config.Local.NumNodes) + } + if merged.Sources.NumNodes != SourceGlobal { + t.Errorf("expected numNodes source %s, got %s", SourceGlobal, merged.Sources.NumNodes) + } + + // autoTrack should come from project + if *merged.Config.Local.AutoTrackSubnets != false { + t.Error("expected autoTrackSubnets false from project") + } + if merged.Sources.AutoTrackSubnets != SourceProject { + t.Errorf("expected autoTrack source %s, got %s", SourceProject, merged.Sources.AutoTrackSubnets) + } +} + +func TestMergeAllSettings(t *testing.T) { + balance := float64(2000) + weight := uint64(50) + global := &GlobalConfig{ + Network: NetworkConfig{ + DefaultNetwork: "testnet", + LuxdVersion: "v1.0.0", + }, + EVM: EVMConfig{ + DefaultTokenName: "GLOBAL", + DefaultTokenSymbol: "GLB", + DefaultTokenSupply: "5000000", + }, + Staking: StakingConfig{ + BootstrapValidatorBalance: &balance, + BootstrapValidatorWeight: &weight, + }, + Node: NodeConfig{ + DefaultInstanceType: "large", + DefaultRegion: "eu-west-1", + }, + } + + merged := Merge(global, nil) + + // Verify all settings applied + if merged.Config.Network.DefaultNetwork != "testnet" { + t.Errorf("expected network testnet, got %s", merged.Config.Network.DefaultNetwork) + } + if merged.Config.EVM.DefaultTokenName != "GLOBAL" { + t.Errorf("expected token name GLOBAL, got %s", merged.Config.EVM.DefaultTokenName) + } + if *merged.Config.Staking.BootstrapValidatorBalance != 2000 { + t.Errorf("expected balance 2000, got %f", *merged.Config.Staking.BootstrapValidatorBalance) + } + if merged.Config.Node.DefaultRegion != "eu-west-1" { + t.Errorf("expected region eu-west-1, got %s", merged.Config.Node.DefaultRegion) + } +} diff --git a/pkg/globalconfig/schema.go b/pkg/globalconfig/schema.go new file mode 100644 index 000000000..f108706dc --- /dev/null +++ b/pkg/globalconfig/schema.go @@ -0,0 +1,53 @@ +// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package globalconfig + +// GlobalConfig represents the global configuration stored in ~/.lux/config.json +type GlobalConfig struct { + Version string `json:"version"` + MetricsEnabled *bool `json:"metricsEnabled,omitempty"` + Network NetworkConfig `json:"network"` + Local LocalConfig `json:"local"` + EVM EVMConfig `json:"evm"` + Staking StakingConfig `json:"staking"` + Node NodeConfig `json:"node"` +} + +// NetworkConfig contains network-related settings +type NetworkConfig struct { + DefaultNetwork string `json:"defaultNetwork,omitempty"` + LuxdVersion string `json:"luxdVersion,omitempty"` +} + +// LocalConfig contains local network settings +type LocalConfig struct { + NumNodes *uint32 `json:"numNodes,omitempty"` + AutoTrackSubnets *bool `json:"autoTrackSubnets,omitempty"` +} + +// EVMConfig contains EVM chain defaults +type EVMConfig struct { + DefaultTokenName string `json:"defaultTokenName,omitempty"` + DefaultTokenSymbol string `json:"defaultTokenSymbol,omitempty"` + DefaultTokenSupply string `json:"defaultTokenSupply,omitempty"` + DefaultChainID uint64 `json:"defaultChainId,omitempty"` +} + +// StakingConfig contains staking defaults +type StakingConfig struct { + BootstrapValidatorBalance *float64 `json:"bootstrapValidatorBalance,omitempty"` + BootstrapValidatorWeight *uint64 `json:"bootstrapValidatorWeight,omitempty"` +} + +// NodeConfig contains node deployment defaults +type NodeConfig struct { + DefaultInstanceType string `json:"defaultInstanceType,omitempty"` + DefaultRegion string `json:"defaultRegion,omitempty"` +} + +// ProjectConfig represents project-local configuration in .luxconfig.json +type ProjectConfig struct { + GlobalConfig + ProjectName string `json:"projectName,omitempty"` +} diff --git a/pkg/globalconfig/smart.go b/pkg/globalconfig/smart.go new file mode 100644 index 000000000..2d8c93f69 --- /dev/null +++ b/pkg/globalconfig/smart.go @@ -0,0 +1,119 @@ +// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package globalconfig + +import ( + "os" + "runtime" +) + +// Environment represents the detected development environment +type Environment string + +const ( + EnvDevelopment Environment = "development" + EnvCI Environment = "ci" + EnvCodespace Environment = "codespace" + EnvProduction Environment = "production" +) + +// SmartDefaults provides intelligent default suggestions based on environment +type SmartDefaults struct { + Environment Environment + SuggestedNumNodes uint32 + SuggestedInstance string + IsTestnet bool +} + +// DetectEnvironment analyzes the current environment +func DetectEnvironment() Environment { + // Check for CI environments + if os.Getenv("CI") != "" || os.Getenv("GITHUB_ACTIONS") != "" || + os.Getenv("GITLAB_CI") != "" || os.Getenv("JENKINS_URL") != "" { + return EnvCI + } + + // Check for GitHub Codespaces + if os.Getenv("CODESPACES") != "" || os.Getenv("CODESPACE_NAME") != "" { + return EnvCodespace + } + + // Check for production indicators + if os.Getenv("PRODUCTION") != "" || os.Getenv("NODE_ENV") == "production" { + return EnvProduction + } + + return EnvDevelopment +} + +// GetSmartDefaults returns environment-aware default configurations +func GetSmartDefaults() *SmartDefaults { + env := DetectEnvironment() + + defaults := &SmartDefaults{ + Environment: env, + SuggestedNumNodes: DefaultNumNodes, + SuggestedInstance: DefaultInstanceType, + IsTestnet: true, + } + + switch env { + case EnvCI: + // CI environments typically need fewer nodes and faster execution + defaults.SuggestedNumNodes = 3 + defaults.SuggestedInstance = "small" + case EnvCodespace: + // Codespaces have limited resources + defaults.SuggestedNumNodes = 3 + defaults.SuggestedInstance = "small" + case EnvProduction: + // Production needs more robust setup + defaults.SuggestedNumNodes = 5 + defaults.SuggestedInstance = "large" + defaults.IsTestnet = false + case EnvDevelopment: + // Development uses standard defaults + defaults.SuggestedNumNodes = 5 + defaults.SuggestedInstance = "default" + } + + return defaults +} + +// SuggestNumNodes returns the suggested number of nodes based on environment +func SuggestNumNodes() uint32 { + smart := GetSmartDefaults() + return smart.SuggestedNumNodes +} + +// SuggestInstanceType returns the suggested instance type based on environment and resources +func SuggestInstanceType() string { + smart := GetSmartDefaults() + + // Also consider available system resources + numCPU := runtime.NumCPU() + if numCPU <= 2 { + return "small" + } else if numCPU >= 8 { + return "large" + } + + return smart.SuggestedInstance +} + +// SuggestTokenSupply returns the suggested token supply based on whether it's a testnet +func SuggestTokenSupply(isTestnet bool) string { + if isTestnet { + return DefaultTokenSupply // 1 million tokens + } + // For production, suggest a more conservative supply + return "100000000000000000000000000" // 100 million tokens +} + +// IsAutoTrackRecommended returns whether auto-tracking subnets is recommended +func IsAutoTrackRecommended() bool { + env := DetectEnvironment() + // Auto-track is recommended for development and CI + return env == EnvDevelopment || env == EnvCI || env == EnvCodespace +} diff --git a/pkg/globalconfig/smart_test.go b/pkg/globalconfig/smart_test.go new file mode 100644 index 000000000..41333ed74 --- /dev/null +++ b/pkg/globalconfig/smart_test.go @@ -0,0 +1,170 @@ +// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package globalconfig + +import ( + "os" + "testing" +) + +func TestDetectEnvironmentDevelopment(t *testing.T) { + // Clear CI-related env vars + originalCI := os.Getenv("CI") + originalGH := os.Getenv("GITHUB_ACTIONS") + originalCS := os.Getenv("CODESPACES") + originalProd := os.Getenv("PRODUCTION") + + os.Unsetenv("CI") + os.Unsetenv("GITHUB_ACTIONS") + os.Unsetenv("GITLAB_CI") + os.Unsetenv("JENKINS_URL") + os.Unsetenv("CODESPACES") + os.Unsetenv("CODESPACE_NAME") + os.Unsetenv("PRODUCTION") + os.Unsetenv("NODE_ENV") + + defer func() { + if originalCI != "" { + os.Setenv("CI", originalCI) + } + if originalGH != "" { + os.Setenv("GITHUB_ACTIONS", originalGH) + } + if originalCS != "" { + os.Setenv("CODESPACES", originalCS) + } + if originalProd != "" { + os.Setenv("PRODUCTION", originalProd) + } + }() + + env := DetectEnvironment() + if env != EnvDevelopment { + t.Errorf("expected %s, got %s", EnvDevelopment, env) + } +} + +func TestDetectEnvironmentCI(t *testing.T) { + original := os.Getenv("CI") + os.Setenv("CI", "true") + defer func() { + if original != "" { + os.Setenv("CI", original) + } else { + os.Unsetenv("CI") + } + }() + + env := DetectEnvironment() + if env != EnvCI { + t.Errorf("expected %s, got %s", EnvCI, env) + } +} + +func TestDetectEnvironmentGitHubActions(t *testing.T) { + original := os.Getenv("GITHUB_ACTIONS") + os.Setenv("GITHUB_ACTIONS", "true") + defer func() { + if original != "" { + os.Setenv("GITHUB_ACTIONS", original) + } else { + os.Unsetenv("GITHUB_ACTIONS") + } + }() + + env := DetectEnvironment() + if env != EnvCI { + t.Errorf("expected %s for GitHub Actions, got %s", EnvCI, env) + } +} + +func TestDetectEnvironmentCodespaces(t *testing.T) { + originalCI := os.Getenv("CI") + originalCS := os.Getenv("CODESPACES") + os.Unsetenv("CI") + os.Setenv("CODESPACES", "true") + defer func() { + if originalCI != "" { + os.Setenv("CI", originalCI) + } + if originalCS != "" { + os.Setenv("CODESPACES", originalCS) + } else { + os.Unsetenv("CODESPACES") + } + }() + + env := DetectEnvironment() + if env != EnvCodespace { + t.Errorf("expected %s, got %s", EnvCodespace, env) + } +} + +func TestGetSmartDefaultsCI(t *testing.T) { + original := os.Getenv("CI") + os.Setenv("CI", "true") + defer func() { + if original != "" { + os.Setenv("CI", original) + } else { + os.Unsetenv("CI") + } + }() + + defaults := GetSmartDefaults() + + if defaults.Environment != EnvCI { + t.Errorf("expected env %s, got %s", EnvCI, defaults.Environment) + } + if defaults.SuggestedNumNodes != 3 { + t.Errorf("expected 3 nodes for CI, got %d", defaults.SuggestedNumNodes) + } + if defaults.SuggestedInstance != "small" { + t.Errorf("expected small instance for CI, got %s", defaults.SuggestedInstance) + } +} + +func TestSuggestTokenSupply(t *testing.T) { + testnetSupply := SuggestTokenSupply(true) + if testnetSupply != DefaultTokenSupply { + t.Errorf("expected testnet supply %s, got %s", DefaultTokenSupply, testnetSupply) + } + + prodSupply := SuggestTokenSupply(false) + if prodSupply == DefaultTokenSupply { + t.Error("expected different supply for production") + } +} + +func TestIsAutoTrackRecommended(t *testing.T) { + // Clear environment + originalCI := os.Getenv("CI") + originalProd := os.Getenv("PRODUCTION") + os.Unsetenv("CI") + os.Unsetenv("GITHUB_ACTIONS") + os.Unsetenv("GITLAB_CI") + os.Unsetenv("JENKINS_URL") + os.Unsetenv("CODESPACES") + os.Unsetenv("PRODUCTION") + + defer func() { + if originalCI != "" { + os.Setenv("CI", originalCI) + } + if originalProd != "" { + os.Setenv("PRODUCTION", originalProd) + } + }() + + // Development should recommend auto-track + if !IsAutoTrackRecommended() { + t.Error("expected auto-track recommended in development") + } + + // CI should recommend auto-track + os.Setenv("CI", "true") + if !IsAutoTrackRecommended() { + t.Error("expected auto-track recommended in CI") + } +}