Files
zeekay 8619f3ac59 Ask the node what it can do, and give an agent the same answer
Two commands, neither of which contains a list of anything.

lux call fetches the OpenAPI document a node derives from its own typed-op
registry and projects it back into a command tree. A service is a word in
the document, an operation is a route, and an operation's flags are the
fields of its input, so an op registered on the node this morning is a
command this afternoon with nothing rebuilt and no release of this binary.
It cannot address an operation the node does not serve, which is exactly
how the SDK came to send exchangevm.getBlock to a node that registers the
X-chain under xvm.

lux mcp relays MCP between an agent on stdio and the node's door on ZAP.
Frames on a socket: no HTTP listener, no request, no status code. A frame
renders as both the ZAP wire and the JSON-RPC 2.0 message, so this is a
relay and not a translation. It does not read inside what it carries, and
it holds no tool list, because the tools are the node's own ops and a copy
of them here would be one more thing to fall behind them.

A node with no typed ops serves no document at all, so lux call reads a
404 as "nothing to derive from" rather than "wrong address". That is the
state every node is in until its handlers are typed, and it is the first
thing anyone running this will hit.

Verified against a live zip service over a unix socket: tools/list offers
a registered op, tools/call runs it and brings the value back, a dead door
answers rather than hanging the agent, and call runs an op and binds a
flag onto its input. This module cannot compile any of it today, because
go.sum records bytes that are no longer served for luxfi/evm v1.104.10 and
luxfi/consensus v1.36.2 and that blocks graph loading for every package
here. The proof therefore ran in a module holding only these two packages,
cobra, zip and zapmcp. go.sum still needs the tidy the same breakage
blocks.
2026-08-29 17:50:26 -07:00

543 lines
18 KiB
Go

// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package cmd
import (
"errors"
"fmt"
"os"
"os/user"
"path/filepath"
"strings"
"time"
"github.com/luxfi/cli/cmd/ammcmd"
"github.com/luxfi/cli/cmd/configcmd"
"github.com/luxfi/log/level"
"github.com/luxfi/cli/cmd/backendcmd"
"github.com/luxfi/cli/cmd/callcmd"
"github.com/luxfi/cli/cmd/chaincmd"
"github.com/luxfi/cli/cmd/contractcmd"
"github.com/luxfi/cli/cmd/ctxcmd"
"github.com/luxfi/cli/cmd/cyclecmd"
"github.com/luxfi/cli/cmd/devcmd"
"github.com/luxfi/cli/cmd/downcmd"
"github.com/luxfi/cli/cmd/infocmd"
"github.com/luxfi/cli/cmd/explorecmd"
"github.com/luxfi/cli/cmd/dexcmd"
"github.com/luxfi/cli/cmd/gpucmd"
"github.com/luxfi/cli/cmd/keycmd"
"github.com/luxfi/cli/cmd/kmscmd"
"github.com/luxfi/cli/cmd/linkcmd"
"github.com/luxfi/cli/cmd/mcpcmd"
"github.com/luxfi/cli/cmd/mpccmd"
"github.com/luxfi/cli/cmd/netrunnercmd"
"github.com/luxfi/cli/cmd/networkcmd"
"github.com/luxfi/cli/cmd/nodecmd"
"github.com/luxfi/cli/cmd/primarycmd"
"github.com/luxfi/cli/cmd/pscmd"
"github.com/luxfi/cli/cmd/rpccmd"
"github.com/luxfi/cli/cmd/snapcmd"
"github.com/luxfi/cli/cmd/upcmd"
aicli "github.com/luxfi/ai/cli"
fhecli "github.com/luxfi/fhe/cli"
rtcli "github.com/luxfi/corona/cli"
tuicli "github.com/luxfi/tui/cli"
"github.com/luxfi/cli/cmd/selfcmd"
"github.com/luxfi/cli/cmd/snapshotcmd"
"github.com/luxfi/cli/cmd/updatecmd"
"github.com/luxfi/cli/cmd/validatorcmd"
"github.com/luxfi/cli/cmd/vmcmd"
"github.com/luxfi/cli/cmd/warpcmd"
"github.com/luxfi/cli/cmd/zkcmd"
"github.com/luxfi/cli/internal/migrations"
"github.com/luxfi/cli/pkg/application"
"github.com/luxfi/cli/pkg/config"
"github.com/luxfi/cli/pkg/lpmintegration"
"github.com/luxfi/cli/pkg/prompts"
"github.com/luxfi/cli/pkg/utils"
"github.com/luxfi/cli/pkg/ux"
"github.com/luxfi/constants"
"github.com/luxfi/filesystem/perms"
luxlog "github.com/luxfi/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
app *application.Lux
logFactory luxlog.Factory
logLevel string
Version = "1.22.5"
cfgFile string
skipCheck bool
nonInteractive bool
verboseFlag bool
debugFlag bool
quietFlag bool
)
func NewRootCmd() *cobra.Command {
// rootCmd represents the base command when called without any subcommands
rootCmd := &cobra.Command{
Use: "lux",
Long: `Lux CLI - Developer toolchain for blockchain development and deployment.
The Lux CLI provides a complete toolkit for creating, testing, and deploying
blockchains on the Lux network. It supports local development, testnet
deployment, and mainnet operations with a unified command structure.
COMMAND OVERVIEW:
network Manage local network runtime (start/stop/status/clean)
chain Blockchain lifecycle (create/deploy/import/export)
key Key and wallet management
validator Validator operations
config CLI configuration
ARCHITECTURE:
L1 (Sovereign) - Independent validator set, own tokenomics
L2 (Rollup) - Based on L1 sequencing (Lux, Ethereum, etc.)
L3 (App Chain) - Built on L2 for application-specific use
SEQUENCING OPTIONS:
lux 100ms blocks, lowest cost (default)
ethereum 12s blocks, highest security
op OP Stack compatible
NETWORK TYPES:
--mainnet Production network (3 validators, port 9630)
--testnet Test network (3 validators, port 9640)
--devnet Development network (3 validators, port 9650)
--dev Single-node dev mode with K=1 consensus
QUICK START:
# Start a local development network
lux network start --devnet
# Create a new blockchain
lux chain create mychain
# Deploy to local network
lux chain deploy mychain
# Check status
lux network status
lux chain list
For detailed command help, use: lux <command> --help`,
PersistentPreRunE: createApp,
Version: Version,
PersistentPostRun: handleTracking,
}
// Disable printing the completion command
rootCmd.CompletionOptions.HiddenDefaultCmd = true
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.lux/cli.json)")
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "ERROR", "log level for the application")
rootCmd.PersistentFlags().BoolVar(&skipCheck, constants.SkipUpdateFlag, false, "skip check for new versions")
rootCmd.PersistentFlags().BoolVar(&nonInteractive, "non-interactive", false,
"Disable prompts; fail if required values are missing (also enabled when stdin is not a TTY or CI=1)")
rootCmd.PersistentFlags().Bool("verbose", false, "Show verbose output (info level logs)")
rootCmd.PersistentFlags().Bool("debug", false, "Show debug output (debug level logs)")
rootCmd.PersistentFlags().Bool("quiet", false, "Show only errors (quiet mode)")
// add sub commands
// Sovereign-L1 verbs — the canonical multi-brand control plane.
// Read $LUX_BRAND_PATH (default ~/work/{lux,zoo,hanzo,pars}/universe).
rootCmd.AddCommand(upcmd.NewCmd()) // lux up <name>/<env>
rootCmd.AddCommand(downcmd.NewCmd()) // lux down <name>/<env>
rootCmd.AddCommand(snapcmd.NewCmd()) // lux snap <name>/<env>
rootCmd.AddCommand(cyclecmd.NewCmd()) // lux cycle <name>/<env>
rootCmd.AddCommand(infocmd.NewCmd()) // lux info <name>/<env>
rootCmd.AddCommand(pscmd.NewCmd()) // lux ps
rootCmd.AddCommand(callcmd.NewCmd()) // lux call — every operation the node publishes
rootCmd.AddCommand(mcpcmd.NewCmd()) // lux mcp — those operations, to an agent
rootCmd.AddCommand(ctxcmd.NewCmd()) // lux ctx
rootCmd.AddCommand(devcmd.NewCmd(app)) // dev (legacy: anvil-compat shortcut for lux/localnet)
rootCmd.AddCommand(explorecmd.NewCmd(app)) // explore (block explorer)
rootCmd.AddCommand(networkcmd.NewCmd(app)) // network (local network management)
rootCmd.AddCommand(networkcmd.NewStatusCmd()) // status alias (new version)
rootCmd.AddCommand(snapshotcmd.NewCmd(app)) // snapshot (native incremental backups)
rootCmd.AddCommand(primarycmd.NewCmd(app))
rootCmd.AddCommand(chaincmd.NewCmd(app)) // unified chain command (l1/l2/l3)
// add transaction command
// add config command
rootCmd.AddCommand(configcmd.NewCmd(app))
// add update command
rootCmd.AddCommand(updatecmd.NewCmd(app, Version))
// add self management command (version management like nvm)
rootCmd.AddCommand(selfcmd.NewCmd(app, Version))
// add link command (unified binary linking)
rootCmd.AddCommand(linkcmd.NewCmd())
// add warp command (cross-chain messaging)
rootCmd.AddCommand(warpcmd.NewCmd(app))
// add dex command (decentralized exchange)
rootCmd.AddCommand(dexcmd.NewCmd(app))
// add amm command (Uniswap-style AMM trading)
rootCmd.AddCommand(ammcmd.NewCmd(app))
// add contract command
rootCmd.AddCommand(contractcmd.NewCmd(app))
// add validator command
rootCmd.AddCommand(validatorcmd.NewCmd(app))
// add key management command
rootCmd.AddCommand(keycmd.NewCmd(app))
// add vm management command
rootCmd.AddCommand(vmcmd.NewCmd(app))
// add node management command
rootCmd.AddCommand(nodecmd.NewCmd(app))
// add mpc management command (threshold signing wallets)
rootCmd.AddCommand(mpccmd.NewCmd())
// add kms management command (key management service)
rootCmd.AddCommand(kmscmd.NewCmd())
// add netrunner management command
rootCmd.AddCommand(netrunnercmd.NewCmd(app))
// add gpu management command
rootCmd.AddCommand(gpucmd.NewCmd(app))
// add zk command (ceremony, prove, verify, SRS)
rootCmd.AddCommand(zkcmd.NewCmd(app))
// add ai command (chat, agents, models) — from github.com/luxfi/ai/cli
rootCmd.AddCommand(aicli.NewCmd())
// add tui command (interactive terminal UI) — from github.com/luxfi/tui/cli
rootCmd.AddCommand(tuicli.NewCmd())
// add fhe command (fully homomorphic encryption) — from github.com/luxfi/fhe/cli
rootCmd.AddCommand(fhecli.NewCmd())
// add rt command (Corona threshold signing) — from github.com/luxfi/corona/cli
rootCmd.AddCommand(rtcli.NewCmd())
// add rpc command for direct RPC calls
rootCmd.AddCommand(rpccmd.NewCmd(app))
// add hidden backend command (base)
rootCmd.AddCommand(backendcmd.NewCmd(app))
// add network-specific backend commands (lux-mainnet-grpc, lux-testnet-grpc, etc.)
for _, cmd := range backendcmd.NewAllNetworkCmds(app) {
rootCmd.AddCommand(cmd)
}
return rootCmd
}
func createApp(cmd *cobra.Command, _ []string) error {
baseDir, err := setupEnv()
if err != nil {
return err
}
log, err := setupLogging(baseDir)
if err != nil {
return err
}
// Adjust log level based on flags (must be done after flags are parsed)
if cmd.Flags().Changed("debug") {
logFactory.SetDisplayLevel("lux", luxlog.Level(-4)) // DEBUG
} else if cmd.Flags().Changed("verbose") {
logFactory.SetDisplayLevel("lux", luxlog.Level(0)) // INFO
} else if cmd.Flags().Changed("quiet") {
logFactory.SetDisplayLevel("lux", luxlog.Level(8)) // ERROR
} else if logLevel != "" {
level, err := luxlog.ToLevel(logLevel)
if err == nil {
logFactory.SetDisplayLevel("lux", level)
}
}
cf := config.New()
// Adjust log level based on flags BEFORE any logging happens
// Use only luxlog types to avoid mixing log libraries
if cmd.Flags().Changed("debug") {
logFactory.SetLogLevel("lux", luxlog.Level(level.Debug))
logFactory.SetDisplayLevel("lux", luxlog.Level(level.Debug))
} else if cmd.Flags().Changed("verbose") {
logFactory.SetLogLevel("lux", luxlog.Level(level.Info))
logFactory.SetDisplayLevel("lux", luxlog.Level(level.Info))
} else if cmd.Flags().Changed("quiet") {
logFactory.SetLogLevel("lux", luxlog.Level(level.Error))
logFactory.SetDisplayLevel("lux", luxlog.Level(level.Error))
} else if logLevel != "" {
level, err := luxlog.ToLevel(logLevel)
if err == nil {
logFactory.SetLogLevel("lux", level)
logFactory.SetDisplayLevel("lux", level)
}
}
// If --non-interactive flag is set, propagate to env so IsInteractive() sees it
// This allows TTY detection to work automatically while still respecting the flag
if nonInteractive {
_ = os.Setenv(prompts.EnvNonInteractive, "1")
}
// Interactive by default on TTY, non-interactive when:
// NON_INTERACTIVE=1, CI=1, --non-interactive flag, or stdin is piped
prompter := prompts.NewPrompterForMode(nonInteractive)
app.Setup(baseDir, log, cf, prompter, application.NewDownloader())
// Setup LPM, skip if running a hidden command
if !cmd.Hidden {
usr, err := user.Current()
if err != nil {
app.Log.Error("unable to get system user")
return err
}
lpmBaseDir := filepath.Join(usr.HomeDir, constants.LPMDir)
if err = lpmintegration.SetupLpm(app, lpmBaseDir); err != nil {
return err
}
}
initConfig()
if err := migrations.RunMigrations(app); err != nil {
return err
}
// Skip metrics prompt in non-interactive mode, E2E tests, or if config exists
if os.Getenv("RUN_E2E") == "" && prompts.IsInteractive() && !app.ConfigFileExists() {
err = utils.HandleUserMetricsPreference(app)
if err != nil {
return err
}
}
if err := checkForUpdates(cmd, app); err != nil {
return err
}
return nil
}
// checkForUpdates evaluates first if the user is maybe wanting to skip the update check
// if there's no skip, it runs the update check
func checkForUpdates(cmd *cobra.Command, app *application.Lux) error {
// If skip-update-check is enabled (via flag or config), skip silently
if skipCheck {
return nil
}
var (
lastActs *application.LastActions
err error
)
// we store a timestamp of the last skip check in a file
lastActs, err = app.ReadLastActionsFile()
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
app.Log.Warn("failed to read last-actions file! This is non-critical but is logged", "error", err)
}
lastActs = &application.LastActions{}
}
// if the user had requested to skipCheck less than 24 hrs ago via flag, we skip
if lastActs.LastSkipCheck != (time.Time{}) &&
time.Now().Before(lastActs.LastSkipCheck.Add(24*time.Hour)) {
return nil
}
// at this point we want to run the check
isUserCalled := false
commandList := strings.Fields(cmd.CommandPath())
if len(commandList) <= 1 || commandList[1] != "update" {
if err := updatecmd.Update(cmd, isUserCalled, Version); err != nil {
if errors.Is(err, updatecmd.ErrUserAbortedInstallation) {
return nil
}
if errors.Is(err, updatecmd.ErrNoVersion) {
ux.Logger.PrintToUser(
"Attempted to check if a new version is available, but couldn't find the currently running version information")
ux.Logger.PrintToUser(
"Make sure to follow official instructions, or automatic updates won't be available for you")
return nil
}
return err
}
}
return nil
}
func handleTracking(cmd *cobra.Command, _ []string) {
utils.HandleTracking(cmd, app, nil)
}
func setupEnv() (string, error) {
// Set base dir
usr, err := user.Current()
if err != nil {
// no logger here yet
fmt.Printf("unable to get system user %s\n", err)
return "", err
}
baseDir := filepath.Join(usr.HomeDir, constants.BaseDirName)
// Create base dir if it doesn't exist
err = os.MkdirAll(baseDir, 0o750)
if err != nil {
// no logger here yet
fmt.Printf("failed creating the basedir %s: %s\n", baseDir, err)
return "", err
}
// Create snapshots dir if it doesn't exist
snapshotsDir := filepath.Join(baseDir, constants.SnapshotsDirName)
if err = os.MkdirAll(snapshotsDir, 0o750); err != nil {
fmt.Printf("failed creating the snapshots dir %s: %s\n", snapshotsDir, err)
os.Exit(1)
}
// Create key dir if it doesn't exist
keyDir := filepath.Join(baseDir, constants.KeyDir)
if err = os.MkdirAll(keyDir, 0o750); err != nil {
fmt.Printf("failed creating the key dir %s: %s\n", keyDir, err)
os.Exit(1)
}
// Create custom vm dir if it doesn't exist
vmDir := filepath.Join(baseDir, constants.CustomVMDir)
if err = os.MkdirAll(vmDir, 0o750); err != nil {
fmt.Printf("failed creating the vm dir %s: %s\n", vmDir, err)
os.Exit(1)
}
// Create chain dir if it doesn't exist
chainDir := filepath.Join(baseDir, constants.ChainsDir)
if err = os.MkdirAll(chainDir, 0o750); err != nil {
fmt.Printf("failed creating the chain dir %s: %s\n", chainDir, err)
os.Exit(1)
}
// Create repos dir if it doesn't exist
repoDir := filepath.Join(baseDir, constants.ReposDir)
if err = os.MkdirAll(repoDir, 0o750); err != nil {
fmt.Printf("failed creating the repo dir %s: %s\n", repoDir, err)
os.Exit(1)
}
pluginDir := filepath.Join(baseDir, constants.PluginDir)
if err = os.MkdirAll(pluginDir, 0o750); err != nil {
fmt.Printf("failed creating the plugin dir %s: %s\n", pluginDir, err)
os.Exit(1)
}
return baseDir, nil
}
func setupLogging(baseDir string) (luxlog.Logger, error) {
var err error
config := luxlog.Config{}
config.LogLevel = luxlog.Level(-6) // Info level for file logging
// Set default display level to WARN (quiet by default)
config.DisplayLevel, _ = luxlog.ToLevel("WARN")
// Log level can be overridden by flags, but we'll handle that in createApp
// after flags are parsed, by adjusting the logger level dynamically
config.Directory = filepath.Join(baseDir, constants.LogDir)
if err := os.MkdirAll(config.Directory, perms.ReadWriteExecute); err != nil {
return nil, fmt.Errorf("failed creating log directory: %w", err)
}
// some logging config params
config.LogFormat = luxlog.Colors
config.MaxSize = constants.MaxLogFileSize
config.MaxFiles = constants.MaxNumOfLogFiles
config.MaxAge = constants.RetainOldFiles
// Register ux package as internal so caller tracking shows actual source, not the wrapper
luxlog.RegisterInternalPackages("github.com/luxfi/cli/pkg/ux")
factory := luxlog.NewFactoryWithConfig(config)
log, err := factory.Make("lux")
if err != nil {
factory.Close()
return nil, fmt.Errorf("failed setting up logging, exiting: %w", err)
}
// Store factory globally so we can adjust levels later
logFactory = factory
// create the user facing logger as a global var
// User output goes to stdout, logs go to stderr
ux.NewUserLog(log, os.Stdout)
return log, nil
}
// initConfig reads in config file and ENV variables if set.
// Priority: flags > env vars > config file > defaults
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Search for config in ~/.lux/ directory
home, err := os.UserHomeDir()
cobra.CheckErr(err)
luxDir := filepath.Join(home, constants.BaseDirName) // ~/.lux/
viper.AddConfigPath(luxDir)
viper.SetConfigType(constants.DefaultConfigFileType)
viper.SetConfigName(constants.DefaultConfigFileName) // cli.json
}
// Bind environment variables for binary paths
// NODE_PATH -> node-path, etc.
_ = viper.BindEnv(constants.ConfigNodePath, constants.EnvNodePath)
_ = viper.BindEnv(constants.ConfigNetrunnerPath, constants.EnvNetrunnerPath)
_ = viper.BindEnv(constants.ConfigEVMPath, constants.EnvEVMPath)
_ = viper.BindEnv(constants.ConfigPluginsDir, constants.EnvPluginsDir)
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
app.Log.Debug("using config file", "config-file", viper.ConfigFileUsed())
// Read skip-update-check from config file if not already set by flag
if !skipCheck && viper.IsSet(constants.SkipUpdateFlag) {
skipCheck = viper.GetBool(constants.SkipUpdateFlag)
}
}
// No config file is normal - most users don't have one, so we silently continue
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
app = application.New()
rootCmd := NewRootCmd()
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "\nERROR: %s\n", err)
os.Exit(1)
}
}