github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/cmd/commands/cli/command.go (about)

     1  /*
     2   * Copyright (C) 2017 The "MysteriumNetwork/node" Authors.
     3   *
     4   * This program is free software: you can redistribute it and/or modify
     5   * it under the terms of the GNU General Public License as published by
     6   * the Free Software Foundation, either version 3 of the License, or
     7   * (at your option) any later version.
     8   *
     9   * This program is distributed in the hope that it will be useful,
    10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12   * GNU General Public License for more details.
    13   *
    14   * You should have received a copy of the GNU General Public License
    15   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16   */
    17  
    18  package cli
    19  
    20  import (
    21  	"errors"
    22  	"flag"
    23  	"fmt"
    24  	"io"
    25  	stdlog "log"
    26  	"path/filepath"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/anmitsu/go-shlex"
    31  	"github.com/chzyer/readline"
    32  	"github.com/rs/zerolog/log"
    33  	"github.com/urfave/cli/v2"
    34  
    35  	"github.com/mysteriumnetwork/node/cmd"
    36  	"github.com/mysteriumnetwork/node/cmd/commands/cli/clio"
    37  	"github.com/mysteriumnetwork/node/config"
    38  	"github.com/mysteriumnetwork/node/config/remote"
    39  	"github.com/mysteriumnetwork/node/core/connection"
    40  	"github.com/mysteriumnetwork/node/core/connection/connectionstate"
    41  	"github.com/mysteriumnetwork/node/datasize"
    42  	"github.com/mysteriumnetwork/node/metadata"
    43  	"github.com/mysteriumnetwork/node/money"
    44  	nattype "github.com/mysteriumnetwork/node/nat"
    45  	"github.com/mysteriumnetwork/node/services"
    46  	tequilapi_client "github.com/mysteriumnetwork/node/tequilapi/client"
    47  	"github.com/mysteriumnetwork/node/tequilapi/contract"
    48  	"github.com/mysteriumnetwork/node/utils"
    49  	"github.com/mysteriumnetwork/terms/terms-go"
    50  )
    51  
    52  // CommandName is the name which is used to call this command
    53  const CommandName = "cli"
    54  
    55  const serviceHelp = `service <action> [args]
    56  	start	<ProviderID> <ServiceType> [options]
    57  	stop	<ServiceID>
    58  	status	<ServiceID>
    59  	list
    60  	sessions
    61  
    62  	example: service start 0x7d5ee3557775aed0b85d691b036769c17349db23 openvpn --openvpn.port=1194 --openvpn.proto=UDP`
    63  
    64  // NewCommand constructs CLI based Mysterium UI with possibility to control quiting
    65  func NewCommand() *cli.Command {
    66  	return &cli.Command{
    67  		Name:  CommandName,
    68  		Usage: "Starts a CLI client with a Tequilapi",
    69  		Flags: []cli.Flag{&config.FlagAgreedTermsConditions, &config.FlagTequilapiAddress, &config.FlagTequilapiPort},
    70  		Action: func(ctx *cli.Context) error {
    71  			client, err := clio.NewTequilApiClient(ctx)
    72  			if err != nil {
    73  				return err
    74  			}
    75  
    76  			cfg, err := remote.NewConfig(client)
    77  			if err != nil {
    78  				return err
    79  			}
    80  
    81  			cmdCLI := newCliApp(cfg, client)
    82  
    83  			cmd.RegisterSignalCallback(utils.SoftKiller(cmdCLI.Kill))
    84  
    85  			return describeQuit(cmdCLI.Run(ctx))
    86  		},
    87  	}
    88  }
    89  
    90  func describeQuit(err error) error {
    91  	if err == nil || err == io.EOF || err == readline.ErrInterrupt {
    92  		log.Info().Msg("Stopping application")
    93  		return nil
    94  	}
    95  	log.Error().Err(err).Stack().Msg("Terminating application due to error")
    96  	return err
    97  }
    98  
    99  func newCliApp(rc *remote.Config, client *tequilapi_client.Client) *cliApp {
   100  	dataDir := rc.GetStringByFlag(config.FlagDataDir)
   101  	return &cliApp{
   102  		config:      rc,
   103  		tequilapi:   client,
   104  		historyFile: filepath.Join(dataDir, ".cli_history"),
   105  	}
   106  }
   107  
   108  // cliApp describes CLI based Mysterium UI
   109  type cliApp struct {
   110  	config           *remote.Config
   111  	historyFile      string
   112  	tequilapi        *tequilapi_client.Client
   113  	fetchedProposals []contract.ProposalDTO
   114  	completer        *readline.PrefixCompleter
   115  	reader           *readline.Instance
   116  
   117  	currentConsumerID string
   118  }
   119  
   120  const (
   121  	redColor                  = "\033[31m%s\033[0m"
   122  	identityDefaultPassphrase = ""
   123  	statusConnected           = string(connectionstate.Connected)
   124  	statusNotConnected        = string(connectionstate.NotConnected)
   125  )
   126  
   127  var errTermsNotAgreed = errors.New("you must agree with provider and consumer terms of use in order to use this command")
   128  
   129  var versionSummary = metadata.VersionAsSummary(metadata.LicenseCopyright(
   130  	"type 'license --warranty'",
   131  	"type 'license --conditions'",
   132  ))
   133  
   134  func (c *cliApp) handleTOS(ctx *cli.Context) error {
   135  	if ctx.Bool(config.FlagAgreedTermsConditions.Name) {
   136  		c.acceptTOS()
   137  		return nil
   138  	}
   139  
   140  	agreedC := c.config.GetBool(contract.TermsConsumerAgreed)
   141  
   142  	if !agreedC {
   143  		return errTermsNotAgreed
   144  	}
   145  
   146  	agreedP := c.config.GetBool(contract.TermsProviderAgreed)
   147  	if !agreedP {
   148  		return errTermsNotAgreed
   149  	}
   150  
   151  	version := c.config.GetString(contract.TermsVersion)
   152  	if version != terms.TermsVersion {
   153  		return fmt.Errorf("you've agreed to terms of use version %s, but version %s is required", version, terms.TermsVersion)
   154  	}
   155  
   156  	return nil
   157  }
   158  
   159  func (c *cliApp) acceptTOS() {
   160  	t := true
   161  	if err := c.tequilapi.UpdateTerms(contract.TermsRequest{
   162  		AgreedConsumer: &t,
   163  		AgreedProvider: &t,
   164  		AgreedVersion:  terms.TermsVersion,
   165  	}); err != nil {
   166  		clio.Info("Failed to save terms of use agreement, you will have to re-agree on next launch")
   167  	}
   168  }
   169  
   170  // Run runs CLI interface synchronously, in the same thread while blocking it
   171  func (c *cliApp) Run(ctx *cli.Context) (err error) {
   172  	if err := c.handleTOS(ctx); err != nil {
   173  		clio.PrintTOSError(err)
   174  		return nil
   175  	}
   176  
   177  	c.completer = newAutocompleter(c.tequilapi, c.fetchedProposals)
   178  	c.fetchedProposals = c.fetchProposals()
   179  
   180  	if ctx.Args().Len() > 0 {
   181  		return c.handleActions(ctx.Args().Slice())
   182  	}
   183  
   184  	c.reader, err = readline.NewEx(&readline.Config{
   185  		Prompt:          fmt.Sprintf(redColor, "ยป "),
   186  		HistoryFile:     c.historyFile,
   187  		AutoComplete:    c.completer,
   188  		InterruptPrompt: "^C",
   189  		EOFPrompt:       "exit",
   190  	})
   191  	if err != nil {
   192  		return err
   193  	}
   194  	// TODO Should overtake output of CommandRun
   195  	stdlog.SetOutput(c.reader.Stderr())
   196  
   197  	for {
   198  		line, err := c.reader.Readline()
   199  		if err == readline.ErrInterrupt && len(line) > 0 {
   200  			continue
   201  		} else if err != nil {
   202  			c.quit()
   203  			return err
   204  		}
   205  
   206  		args, err := shlex.Split(line, true)
   207  		if err != nil {
   208  			return err
   209  		}
   210  		c.handleActions(args)
   211  	}
   212  }
   213  
   214  // Kill stops cli
   215  func (c *cliApp) Kill() error {
   216  	c.reader.Clean()
   217  	return c.reader.Close()
   218  }
   219  
   220  func (c *cliApp) handleActions(args []string) error {
   221  	if len(args) == 0 {
   222  		return c.help()
   223  	}
   224  	cmd := strings.TrimSpace(args[0])
   225  
   226  	cmdArgs := make([]string, 0)
   227  	if len(args) > 1 {
   228  		cmdArgs = args[1:]
   229  	}
   230  
   231  	staticCmds := []struct {
   232  		command string
   233  		handler func() error
   234  	}{
   235  		{"exit", c.quit},
   236  		{"quit", c.quit},
   237  		{"help", c.help},
   238  		{"status", c.status},
   239  		{"healthcheck", c.healthcheck},
   240  		{"nat", c.nodeMonitoringStatus},
   241  		{"location", c.location},
   242  		{"disconnect", c.disconnect},
   243  		{"stop", c.stopClient},
   244  		{"version", c.version},
   245  	}
   246  
   247  	argCmds := []struct {
   248  		command string
   249  		handler func(args []string) error
   250  	}{
   251  		{"connect", c.connect},
   252  		{"identities", c.identities},
   253  		{"orders", c.order},
   254  		{"license", c.license},
   255  		{"proposals", c.proposals},
   256  		{"service", c.service},
   257  		{"stake", c.stake},
   258  		{"mmn", c.mmnApiKey},
   259  	}
   260  
   261  	for _, c := range staticCmds {
   262  		if cmd == c.command {
   263  			err := c.handler()
   264  			if err != nil {
   265  				clio.Error(formatForHuman(err))
   266  			}
   267  			return err
   268  		}
   269  	}
   270  
   271  	for _, c := range argCmds {
   272  		if cmd == c.command {
   273  			err := c.handler(cmdArgs)
   274  			if err != nil {
   275  				clio.Error(formatForHuman(err))
   276  			}
   277  			return err
   278  		}
   279  	}
   280  
   281  	// Command matched nothing
   282  	return c.help()
   283  }
   284  
   285  func (c *cliApp) connect(args []string) (err error) {
   286  	helpMsg := "Please type in the provider identity. connect <consumer-identity> <provider-identity> <service-type> [dns=auto|provider|system|1.1.1.1] [disable-kill-switch]"
   287  	if len(args) < 3 {
   288  		clio.Info(helpMsg)
   289  		return errWrongArgumentCount
   290  	}
   291  
   292  	consumerID, providerID, serviceType := args[0], args[1], args[2]
   293  	migrationStatus, err := c.tequilapi.MigrateHermesStatus(consumerID)
   294  	if migrationStatus.Status == contract.MigrationStatusRequired {
   295  		clio.Infof("Hermes migration status: %s\n", migrationStatus.Status)
   296  		clio.Info("Migration started")
   297  		err := c.tequilapi.MigrateHermes(consumerID)
   298  		if err != nil {
   299  			return err
   300  		}
   301  		clio.Info("Migration finished successfully")
   302  		clio.Info("Try to reconnect")
   303  		return nil
   304  	}
   305  
   306  	if !services.IsTypeValid(serviceType) {
   307  		return fmt.Errorf("invalid service type, expected one of: %s", strings.Join(services.Types(), ","))
   308  	}
   309  
   310  	var disableKillSwitch bool
   311  	var dns connection.DNSOption
   312  
   313  	for _, arg := range args[3:] {
   314  		if strings.HasPrefix(arg, "dns=") {
   315  			kv := strings.Split(arg, "=")
   316  			dns, err = connection.NewDNSOption(kv[1])
   317  			if err != nil {
   318  				clio.Info(helpMsg)
   319  				return fmt.Errorf("invalid value: %w", err)
   320  			}
   321  			continue
   322  		}
   323  		switch arg {
   324  		case "disable-kill-switch":
   325  			disableKillSwitch = true
   326  		default:
   327  			clio.Info(helpMsg)
   328  			return errUnknownArgument
   329  		}
   330  	}
   331  
   332  	connectOptions := contract.ConnectOptions{
   333  		DNS:               dns,
   334  		DisableKillSwitch: disableKillSwitch,
   335  	}
   336  
   337  	clio.Status("CONNECTING", "from:", consumerID, "to:", providerID)
   338  
   339  	hermesID, err := c.config.GetHermesID()
   340  	if err != nil {
   341  		return err
   342  	}
   343  
   344  	// Dont throw an error here incase user identity has a password on it
   345  	// or we failed to randomly unlock it. We can still try to connect
   346  	// if identity it locked, it will notify us anyway.
   347  	_ = c.tequilapi.Unlock(consumerID, "")
   348  
   349  	_, err = c.tequilapi.ConnectionCreate(consumerID, providerID, hermesID, serviceType, connectOptions)
   350  	if err != nil {
   351  		return err
   352  	}
   353  
   354  	c.currentConsumerID = consumerID
   355  
   356  	clio.Success("Connected.")
   357  	return nil
   358  }
   359  
   360  func (c *cliApp) mmnApiKey(args []string) (err error) {
   361  	profileUrl := strings.TrimSuffix(c.config.GetStringByFlag(config.FlagMMNAddress), "/") + "/me"
   362  	usage := "Set MMN's API key and claim this node:\nmmn <api-key>\nTo get the token, visit: " + profileUrl + "\n"
   363  
   364  	if len(args) == 0 {
   365  		clio.Info(usage)
   366  		return
   367  	}
   368  
   369  	apiKey := args[0]
   370  
   371  	err = c.tequilapi.SetMMNApiKey(contract.MMNApiKeyRequest{
   372  		ApiKey: apiKey,
   373  	})
   374  	if err != nil {
   375  		return fmt.Errorf("failed to set MMN API key: %w", err)
   376  	}
   377  
   378  	clio.Success("MMN API key configured.")
   379  	return nil
   380  }
   381  
   382  func (c *cliApp) disconnect() (err error) {
   383  	err = c.tequilapi.ConnectionDestroy(0)
   384  	if err != nil {
   385  		return err
   386  	}
   387  	c.currentConsumerID = ""
   388  	clio.Success("Disconnected.")
   389  	return nil
   390  }
   391  
   392  func (c *cliApp) status() (err error) {
   393  	status, err := c.tequilapi.ConnectionStatus(0)
   394  	if err != nil {
   395  		clio.Warn(err)
   396  	} else {
   397  		clio.Info("Status:", status.Status)
   398  		clio.Info("SID:", status.SessionID)
   399  	}
   400  
   401  	ip, err := c.tequilapi.ConnectionIP()
   402  	if err != nil {
   403  		clio.Warn(err)
   404  	} else {
   405  		clio.Info("IP:", ip.IP)
   406  	}
   407  
   408  	location, err := c.tequilapi.ConnectionLocation()
   409  	if err != nil {
   410  		clio.Warn(err)
   411  	} else {
   412  		clio.Info(fmt.Sprintf("Location: %s, %s (%s - %s)", location.City, location.Country, location.IPType, location.ISP))
   413  	}
   414  
   415  	if status.Status == statusConnected {
   416  		clio.Info("Proposal:", status.Proposal)
   417  
   418  		statistics, err := c.tequilapi.ConnectionStatistics()
   419  		if err != nil {
   420  			clio.Warn(err)
   421  		} else {
   422  			clio.Info(fmt.Sprintf("Connection duration: %s", time.Duration(statistics.Duration)*time.Second))
   423  			clio.Info(fmt.Sprintf("Data: %s/%s", datasize.FromBytes(statistics.BytesReceived), datasize.FromBytes(statistics.BytesSent)))
   424  			clio.Info(fmt.Sprintf("Throughput: %s/%s", datasize.BitSpeed(statistics.ThroughputReceived), datasize.BitSpeed(statistics.ThroughputSent)))
   425  			clio.Info(fmt.Sprintf("Spent: %s", money.New(statistics.TokensSpent)))
   426  		}
   427  	}
   428  	return nil
   429  }
   430  
   431  func (c *cliApp) healthcheck() (err error) {
   432  	healthcheck, err := c.tequilapi.Healthcheck()
   433  	if err != nil {
   434  		return err
   435  	}
   436  
   437  	clio.Info(fmt.Sprintf("Uptime: %v", healthcheck.Uptime))
   438  	clio.Info(fmt.Sprintf("Process: %v", healthcheck.Process))
   439  	clio.Info(fmt.Sprintf("Version: %v", healthcheck.Version))
   440  	buildString := metadata.FormatString(healthcheck.BuildInfo.Commit, healthcheck.BuildInfo.Branch, healthcheck.BuildInfo.BuildNumber)
   441  	clio.Info(buildString)
   442  	return nil
   443  }
   444  
   445  func (c *cliApp) nodeMonitoringStatus() (err error) {
   446  	status, err := c.tequilapi.NATStatus()
   447  	if err != nil {
   448  		return fmt.Errorf("failed to retrieve NAT traversal status: %w", err)
   449  	}
   450  
   451  	clio.Infof("Node Monitoring Status: %q\n", status.Status)
   452  
   453  	connStatus, err := c.tequilapi.ConnectionStatus(0)
   454  	if err != nil {
   455  		clio.Warn(err)
   456  		return
   457  	}
   458  
   459  	if connStatus.Status != statusNotConnected {
   460  		return nil
   461  	}
   462  	natType, err := c.tequilapi.NATType()
   463  	switch {
   464  	case err != nil:
   465  		clio.Warn(err)
   466  	case natType.Error != "":
   467  		clio.Warn(natType.Error)
   468  	default:
   469  		displayedNATType, ok := nattype.HumanReadableTypes[natType.Type]
   470  		if !ok {
   471  			displayedNATType = string(natType.Type)
   472  		}
   473  		clio.Info("NAT type:", displayedNATType)
   474  	}
   475  
   476  	return nil
   477  }
   478  
   479  func (c *cliApp) proposals(args []string) (err error) {
   480  	proposals := c.fetchProposals()
   481  	c.fetchedProposals = proposals
   482  
   483  	filter := ""
   484  	if len(args) > 0 {
   485  		filter = strings.Join(args, " ")
   486  	}
   487  	filterMsg := ""
   488  	if filter != "" {
   489  		filterMsg = fmt.Sprintf("(filter: '%s')", filter)
   490  	}
   491  	clio.Info(fmt.Sprintf("Found %v proposals %s", len(proposals), filterMsg))
   492  
   493  	for _, proposal := range proposals {
   494  		country := proposal.Location.Country
   495  		if country == "" {
   496  			country = "Unknown"
   497  		}
   498  
   499  		var policies []string
   500  		if proposal.AccessPolicies != nil {
   501  			for _, policy := range *proposal.AccessPolicies {
   502  				policies = append(policies, policy.ID)
   503  			}
   504  		}
   505  
   506  		msg := fmt.Sprintf("- provider id: %v\ttype: %v\tcountry: %v\taccess policies: %v\tprovider type: %v", proposal.ProviderID, proposal.ServiceType, country, strings.Join(policies, ","), proposal.Location.IPType)
   507  
   508  		if filter == "" ||
   509  			strings.Contains(proposal.ProviderID, filter) ||
   510  			strings.Contains(country, filter) {
   511  			clio.Info(msg)
   512  		}
   513  	}
   514  
   515  	return nil
   516  }
   517  
   518  func (c *cliApp) fetchProposals() []contract.ProposalDTO {
   519  	proposals, err := c.tequilapi.ProposalsNATCompatible()
   520  	if err != nil {
   521  		clio.Warn(err)
   522  		return []contract.ProposalDTO{}
   523  	}
   524  	return proposals
   525  }
   526  
   527  func (c *cliApp) location() (err error) {
   528  	location, err := c.tequilapi.OriginLocation()
   529  	if err != nil {
   530  		return err
   531  	}
   532  
   533  	clio.Info(fmt.Sprintf("Location: %s, %s (%s - %s)", location.City, location.Country, location.IPType, location.ISP))
   534  	return nil
   535  }
   536  
   537  func (c *cliApp) help() (err error) {
   538  	clio.Info("Mysterium CLI commands:")
   539  	fmt.Println(c.completer.Tree("  "))
   540  	return nil
   541  }
   542  
   543  // quit stops cli and client commands and exits application
   544  func (c *cliApp) quit() (err error) {
   545  	stop := utils.SoftKiller(c.Kill)
   546  	stop()
   547  	return nil
   548  }
   549  
   550  func (c *cliApp) stopClient() (err error) {
   551  	err = c.tequilapi.Stop()
   552  	if err != nil {
   553  		return fmt.Errorf("cannot stop the client: %w", err)
   554  	}
   555  	clio.Success("Client stopped")
   556  	return nil
   557  }
   558  
   559  func (c *cliApp) version() (err error) {
   560  	fmt.Println(versionSummary)
   561  	return nil
   562  }
   563  
   564  func (c *cliApp) license(args []string) (err error) {
   565  	arg := ""
   566  	if len(args) > 0 {
   567  		arg = args[0]
   568  	}
   569  	if arg == "warranty" {
   570  		fmt.Print(metadata.LicenseWarranty)
   571  	} else if arg == "conditions" {
   572  		fmt.Print(metadata.LicenseConditions)
   573  	} else {
   574  		clio.Info("identities command:\n    warranty\n    conditions")
   575  	}
   576  	return nil
   577  }
   578  
   579  func getIdentityOptionList(tequilapi *tequilapi_client.Client) func(string) []string {
   580  	return func(line string) []string {
   581  		var identities []string
   582  		ids, err := tequilapi.GetIdentities()
   583  		if err != nil {
   584  			clio.Warn(err)
   585  			return identities
   586  		}
   587  		for _, id := range ids {
   588  			identities = append(identities, id.Address)
   589  		}
   590  
   591  		return identities
   592  	}
   593  }
   594  
   595  func getProposalOptionList(proposals []contract.ProposalDTO) func(string) []string {
   596  	return func(line string) []string {
   597  		var providerIDS []string
   598  		for _, proposal := range proposals {
   599  			providerIDS = append(providerIDS, proposal.ProviderID)
   600  		}
   601  		return providerIDS
   602  	}
   603  }
   604  
   605  func newAutocompleter(tequilapi *tequilapi_client.Client, proposals []contract.ProposalDTO) *readline.PrefixCompleter {
   606  	connectOpts := []readline.PrefixCompleterInterface{
   607  		readline.PcItem("dns=auto"),
   608  		readline.PcItem("dns=provider"),
   609  		readline.PcItem("dns=system"),
   610  		readline.PcItem("dns=1.1.1.1"),
   611  	}
   612  	return readline.NewPrefixCompleter(
   613  		readline.PcItem(
   614  			"connect",
   615  			readline.PcItemDynamic(
   616  				getIdentityOptionList(tequilapi),
   617  				readline.PcItemDynamic(
   618  					getProposalOptionList(proposals),
   619  					readline.PcItem("noop", connectOpts...),
   620  					readline.PcItem("openvpn", connectOpts...),
   621  					readline.PcItem("wireguard", connectOpts...),
   622  				),
   623  			),
   624  		),
   625  		readline.PcItem(
   626  			"service",
   627  			readline.PcItem("start", readline.PcItemDynamic(
   628  				getIdentityOptionList(tequilapi),
   629  				readline.PcItem("noop"),
   630  				readline.PcItem("openvpn"),
   631  				readline.PcItem("wireguard"),
   632  			)),
   633  			readline.PcItem("stop"),
   634  			readline.PcItem("list"),
   635  			readline.PcItem("status"),
   636  			readline.PcItem("sessions"),
   637  		),
   638  		readline.PcItem(
   639  			"identities",
   640  			readline.PcItem("list"),
   641  			readline.PcItem("get", readline.PcItemDynamic(getIdentityOptionList(tequilapi))),
   642  			readline.PcItem("balance", readline.PcItemDynamic(getIdentityOptionList(tequilapi))),
   643  			readline.PcItem("new"),
   644  			readline.PcItem("unlock", readline.PcItemDynamic(getIdentityOptionList(tequilapi))),
   645  			readline.PcItem("register", readline.PcItemDynamic(getIdentityOptionList(tequilapi))),
   646  			readline.PcItem("beneficiary-status", readline.PcItemDynamic(getIdentityOptionList(tequilapi))),
   647  			readline.PcItem("beneficiary-set", readline.PcItemDynamic(getIdentityOptionList(tequilapi))),
   648  			readline.PcItem("settle", readline.PcItemDynamic(getIdentityOptionList(tequilapi))),
   649  			readline.PcItem("referralcode", readline.PcItemDynamic(getIdentityOptionList(tequilapi))),
   650  			readline.PcItem("export", readline.PcItemDynamic(getIdentityOptionList(tequilapi))),
   651  			readline.PcItem("import"),
   652  			readline.PcItem("withdraw", readline.PcItemDynamic(getIdentityOptionList(tequilapi))),
   653  			readline.PcItem("last-withdrawal", readline.PcItemDynamic(getIdentityOptionList(tequilapi))),
   654  			readline.PcItem("migrate-hermes", readline.PcItemDynamic(getIdentityOptionList(tequilapi))),
   655  			readline.PcItem("migrate-hermes-status", readline.PcItemDynamic(getIdentityOptionList(tequilapi))),
   656  		),
   657  		readline.PcItem("status"),
   658  		readline.PcItem(
   659  			"stake",
   660  			readline.PcItem("increase"),
   661  			readline.PcItem("decrease"),
   662  		),
   663  		readline.PcItem("orders",
   664  			readline.PcItem("create", readline.PcItemDynamic(getIdentityOptionList(tequilapi))),
   665  			readline.PcItem("get", readline.PcItemDynamic(getIdentityOptionList(tequilapi))),
   666  			readline.PcItem("get-all", readline.PcItemDynamic(getIdentityOptionList(tequilapi))),
   667  			readline.PcItem("gateways"),
   668  		),
   669  		readline.PcItem("healthcheck"),
   670  		readline.PcItem("nat"),
   671  		readline.PcItem("proposals"),
   672  		readline.PcItem("location"),
   673  		readline.PcItem("disconnect"),
   674  		readline.PcItem("mmn"),
   675  		readline.PcItem("help"),
   676  		readline.PcItem("quit"),
   677  		readline.PcItem("stop"),
   678  		readline.PcItem(
   679  			"license",
   680  			readline.PcItem("warranty"),
   681  			readline.PcItem("conditions"),
   682  		),
   683  	)
   684  }
   685  
   686  func parseStartFlags(serviceType string, args ...string) (services.StartOptions, error) {
   687  	var flags []cli.Flag
   688  	config.RegisterFlagsServiceStart(&flags)
   689  	config.RegisterFlagsServiceOpenvpn(&flags)
   690  	config.RegisterFlagsServiceWireguard(&flags)
   691  	config.RegisterFlagsServiceNoop(&flags)
   692  
   693  	set := flag.NewFlagSet("", flag.ContinueOnError)
   694  	for _, f := range flags {
   695  		f.Apply(set)
   696  	}
   697  	if err := set.Parse(args); err != nil {
   698  		return services.StartOptions{}, err
   699  	}
   700  
   701  	ctx := cli.NewContext(nil, set, nil)
   702  	config.ParseFlagsServiceStart(ctx)
   703  	config.ParseFlagsServiceOpenvpn(ctx)
   704  	config.ParseFlagsServiceWireguard(ctx)
   705  	config.ParseFlagsServiceNoop(ctx)
   706  
   707  	return services.GetStartOptions(serviceType)
   708  }