diff --git a/.gitignore b/.gitignore index 8c2cd2af..226c9929 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # Ignore Oasis CLI binary. oasis +# Ignore generated completions (created by goreleaser). +completions/ # Ignore Python cache directories. __pycache__/ # Ignore temporary files. diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 350b6d4f..ed36c6a6 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -5,6 +5,10 @@ project_name: Oasis CLI before: hooks: - go mod tidy + - mkdir -p completions + - sh -c "go run . completion bash > completions/oasis.bash" + - sh -c "go run . completion zsh > completions/oasis.zsh" + - sh -c "go run . completion fish > completions/oasis.fish" universal_binaries: - id: oasis-darwin-universal @@ -82,6 +86,8 @@ archives: format_overrides: - goos: windows formats: [ 'zip' ] + files: + - completions/* checksum: name_template: SHA256SUMS-{{.Version}}.txt diff --git a/cmd/account/allow.go b/cmd/account/allow.go index 08ba10a3..e8eb046a 100644 --- a/cmd/account/allow.go +++ b/cmd/account/allow.go @@ -14,9 +14,10 @@ import ( ) var allowCmd = &cobra.Command{ - Use: "allow ", - Short: "Configure beneficiary allowance", - Args: cobra.ExactArgs(2), + Use: "allow ", + Short: "Configure beneficiary allowance", + Args: cobra.ExactArgs(2), + ValidArgsFunction: common.AddressesAt(0), // at position 1. Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) @@ -63,6 +64,6 @@ var allowCmd = &cobra.Command{ } func init() { - allowCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(allowCmd) allowCmd.Flags().AddFlagSet(common.TxFlags) } diff --git a/cmd/account/amend_commission_schedule.go b/cmd/account/amend_commission_schedule.go index c263f46b..0387f0df 100644 --- a/cmd/account/amend_commission_schedule.go +++ b/cmd/account/amend_commission_schedule.go @@ -174,7 +174,7 @@ func init() { "The minimum rate is rate_min_numerator divided by %v, and the maximum rate is "+ "rate_max_numerator divided by %v", staking.CommissionRateDenominator, staking.CommissionRateDenominator, )) - amendCommissionScheduleCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(amendCommissionScheduleCmd) amendCommissionScheduleCmd.Flags().AddFlagSet(common.TxFlags) amendCommissionScheduleCmd.Flags().AddFlagSet(f) } diff --git a/cmd/account/burn.go b/cmd/account/burn.go index 1e14f376..df77ff3c 100644 --- a/cmd/account/burn.go +++ b/cmd/account/burn.go @@ -53,6 +53,6 @@ var burnCmd = &cobra.Command{ } func init() { - burnCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(burnCmd) burnCmd.Flags().AddFlagSet(common.TxFlags) } diff --git a/cmd/account/delegate.go b/cmd/account/delegate.go index 2e64bfa5..91f2107c 100644 --- a/cmd/account/delegate.go +++ b/cmd/account/delegate.go @@ -18,9 +18,10 @@ import ( ) var delegateCmd = &cobra.Command{ - Use: "delegate ", - Short: "Delegate given amount of tokens to an entity", - Args: cobra.ExactArgs(2), + Use: "delegate ", + Short: "Delegate given amount of tokens to an entity", + Args: cobra.ExactArgs(2), + ValidArgsFunction: common.AddressesAt(1), // at position 2. Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) @@ -122,6 +123,6 @@ var delegateCmd = &cobra.Command{ } func init() { - delegateCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(delegateCmd) delegateCmd.Flags().AddFlagSet(common.RuntimeTxFlags) } diff --git a/cmd/account/deposit.go b/cmd/account/deposit.go index 6f6bdd0e..31931fe2 100644 --- a/cmd/account/deposit.go +++ b/cmd/account/deposit.go @@ -20,9 +20,10 @@ import ( ) var depositCmd = &cobra.Command{ - Use: "deposit [to]", - Short: "Deposit tokens into ParaTime", - Args: cobra.RangeArgs(1, 2), + Use: "deposit [to]", + Short: "Deposit tokens into ParaTime", + Args: cobra.RangeArgs(1, 2), + ValidArgsFunction: common.AddressesAt(1), // [to] at position 2. Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) @@ -116,7 +117,7 @@ var depositCmd = &cobra.Command{ } func init() { - depositCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(depositCmd) depositCmd.Flags().AddFlagSet(common.RuntimeTxFlags) depositCmd.Flags().AddFlagSet(common.ForceFlag) } diff --git a/cmd/account/entity.go b/cmd/account/entity.go index e55b9ce2..2a7c4248 100644 --- a/cmd/account/entity.go +++ b/cmd/account/entity.go @@ -235,10 +235,10 @@ func init() { entityInitCmd.Flags().AddFlagSet(common.AccountFlag) entityInitCmd.Flags().AddFlagSet(common.AnswerYesFlag) - entityRegisterCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(entityRegisterCmd) entityRegisterCmd.Flags().AddFlagSet(common.TxFlags) - entityDeregisterCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(entityDeregisterCmd) entityDeregisterCmd.Flags().AddFlagSet(common.TxFlags) entityMetadataUpdateCmd.Flags().StringVarP(®istryPath, "registry-dir", "r", "", "path to the metadata registry directory") diff --git a/cmd/account/node_unfreeze.go b/cmd/account/node_unfreeze.go index 413a3804..f410aeeb 100644 --- a/cmd/account/node_unfreeze.go +++ b/cmd/account/node_unfreeze.go @@ -53,6 +53,6 @@ var nodeUnfreezeCmd = &cobra.Command{ } func init() { - nodeUnfreezeCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(nodeUnfreezeCmd) nodeUnfreezeCmd.Flags().AddFlagSet(common.TxFlags) } diff --git a/cmd/account/show/show.go b/cmd/account/show/show.go index 9db3ec03..79caa4cf 100644 --- a/cmd/account/show/show.go +++ b/cmd/account/show/show.go @@ -26,10 +26,11 @@ var ( showDelegations bool Cmd = &cobra.Command{ - Use: "show [address]", - Short: "Show balance and other information", - Aliases: []string{"s", "balance", "b"}, - Args: cobra.MaximumNArgs(1), + Use: "show [address]", + Short: "Show balance and other information", + Aliases: []string{"s", "balance", "b"}, + Args: cobra.MaximumNArgs(1), + ValidArgsFunction: common.CompleteAccountAndAddressBookNames, Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) @@ -241,7 +242,7 @@ var ( func init() { f := flag.NewFlagSet("", flag.ContinueOnError) f.BoolVar(&showDelegations, "show-delegations", false, "show incoming and outgoing delegations") - Cmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(Cmd) Cmd.Flags().AddFlagSet(common.HeightFlag) Cmd.Flags().AddFlagSet(f) } diff --git a/cmd/account/transfer.go b/cmd/account/transfer.go index 2ae52b6d..fbcc270a 100644 --- a/cmd/account/transfer.go +++ b/cmd/account/transfer.go @@ -18,10 +18,11 @@ import ( ) var transferCmd = &cobra.Command{ - Use: "transfer [] ", - Short: "Transfer given amount of tokens", - Aliases: []string{"t"}, - Args: cobra.RangeArgs(2, 3), + Use: "transfer [] ", + Short: "Transfer given amount of tokens", + Aliases: []string{"t"}, + Args: cobra.RangeArgs(2, 3), + ValidArgsFunction: common.AddressesAt(1, 2), // can be at position 2 or 3. Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) @@ -120,7 +121,7 @@ var transferCmd = &cobra.Command{ func init() { transferCmd.Flags().AddFlagSet(SubtractFeeFlags) - transferCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(transferCmd) transferCmd.Flags().AddFlagSet(common.RuntimeTxFlags) transferCmd.Flags().AddFlagSet(common.ForceFlag) } diff --git a/cmd/account/undelegate.go b/cmd/account/undelegate.go index 75b5bbac..789f40b0 100644 --- a/cmd/account/undelegate.go +++ b/cmd/account/undelegate.go @@ -18,9 +18,10 @@ import ( ) var undelegateCmd = &cobra.Command{ - Use: "undelegate ", - Short: "Undelegate given amount of shares from an entity", - Args: cobra.ExactArgs(2), + Use: "undelegate ", + Short: "Undelegate given amount of shares from an entity", + Args: cobra.ExactArgs(2), + ValidArgsFunction: common.AddressesAt(1), // at position 2. Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) @@ -118,6 +119,6 @@ var undelegateCmd = &cobra.Command{ } func init() { - undelegateCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(undelegateCmd) undelegateCmd.Flags().AddFlagSet(common.RuntimeTxFlags) } diff --git a/cmd/account/withdraw.go b/cmd/account/withdraw.go index 7999131f..a6366588 100644 --- a/cmd/account/withdraw.go +++ b/cmd/account/withdraw.go @@ -19,9 +19,10 @@ import ( ) var withdrawCmd = &cobra.Command{ - Use: "withdraw [to]", - Short: "Withdraw tokens from ParaTime", - Args: cobra.RangeArgs(1, 2), + Use: "withdraw [to]", + Short: "Withdraw tokens from ParaTime", + Args: cobra.RangeArgs(1, 2), + ValidArgsFunction: common.AddressesAt(1), // [to] at position 2. Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) @@ -133,7 +134,7 @@ var withdrawCmd = &cobra.Command{ func init() { withdrawCmd.Flags().AddFlagSet(SubtractFeeFlags) - withdrawCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(withdrawCmd) withdrawCmd.Flags().AddFlagSet(common.RuntimeTxFlags) withdrawCmd.Flags().AddFlagSet(common.ForceFlag) } diff --git a/cmd/addressbook.go b/cmd/addressbook.go index 73a2b9af..2be59116 100644 --- a/cmd/addressbook.go +++ b/cmd/addressbook.go @@ -6,6 +6,7 @@ import ( "github.com/spf13/cobra" + "github.com/oasisprotocol/cli/cmd/common" "github.com/oasisprotocol/cli/config" "github.com/oasisprotocol/cli/table" ) @@ -70,9 +71,10 @@ var ( } abShowCmd = &cobra.Command{ - Use: "show ", - Short: "Show address information", - Args: cobra.ExactArgs(1), + Use: "show ", + Short: "Show address information", + Args: cobra.ExactArgs(1), + ValidArgsFunction: common.CompleteAddressBookNames, Run: func(_ *cobra.Command, args []string) { name := args[0] abEntry, ok := config.Global().AddressBook.All[name] @@ -89,10 +91,11 @@ var ( } abRmCmd = &cobra.Command{ - Use: "remove ", - Aliases: []string{"rm"}, - Short: "Remove an address from address book", - Args: cobra.ExactArgs(1), + Use: "remove ", + Aliases: []string{"rm"}, + Short: "Remove an address from address book", + Args: cobra.ExactArgs(1), + ValidArgsFunction: common.CompleteAddressBookNames, Run: func(_ *cobra.Command, args []string) { cfg := config.Global() name := args[0] @@ -106,10 +109,11 @@ var ( } abRenameCmd = &cobra.Command{ - Use: "rename ", - Aliases: []string{"mv"}, - Short: "Rename address", - Args: cobra.ExactArgs(2), + Use: "rename ", + Aliases: []string{"mv"}, + Short: "Rename address", + Args: cobra.ExactArgs(2), + ValidArgsFunction: common.CompleteAddressBookNames, Run: func(_ *cobra.Command, args []string) { cfg := config.Global() oldName, newName := args[0], args[1] diff --git a/cmd/common/completion.go b/cmd/common/completion.go new file mode 100644 index 00000000..7952b3af --- /dev/null +++ b/cmd/common/completion.go @@ -0,0 +1,145 @@ +package common + +import ( + "sort" + + "github.com/spf13/cobra" + + "github.com/oasisprotocol/cli/config" +) + +// CobraCompletionFunc is the function signature for cobra completions. +type CobraCompletionFunc = func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) + +// Helpers. + +func mapKeys[T any](m map[string]T) []string { + names := make([]string, 0, len(m)) + for name := range m { + names = append(names, name) + } + return names +} + +func noFileComp(names []string) ([]string, cobra.ShellCompDirective) { + sort.Strings(names) + return names, cobra.ShellCompDirectiveNoFileComp +} + +func simpleComplete(fn func() []string) CobraCompletionFunc { + return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return noFileComp(fn()) + } +} + +func completeAt(fn func() []string, positions ...int) CobraCompletionFunc { + return func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) { + for _, pos := range positions { + if len(args) == pos { + return noFileComp(fn()) + } + } + return nil, cobra.ShellCompDirectiveNoFileComp + } +} + +// Data sources. + +func accountNames() []string { + return mapKeys(config.Global().Wallet.All) +} + +func addressBookNames() []string { + return mapKeys(config.Global().AddressBook.All) +} + +func addressNames() []string { + cfg := config.Global() + return append(mapKeys(cfg.Wallet.All), mapKeys(cfg.AddressBook.All)...) +} + +func networkNames() []string { + return mapKeys(config.Global().Networks.All) +} + +// Simple completers - complete at any position. + +// CompleteAccountNames provides completion for wallet account names. +var CompleteAccountNames = simpleComplete(accountNames) + +// CompleteAddressBookNames provides completion for address book entry names. +var CompleteAddressBookNames = simpleComplete(addressBookNames) + +// CompleteAccountAndAddressBookNames provides completion for both wallet accounts +// and address book entries. Useful for commands that accept either as an address. +var CompleteAccountAndAddressBookNames = simpleComplete(addressNames) + +// CompleteNetworkNames provides completion for network names. +var CompleteNetworkNames = simpleComplete(networkNames) + +// CompleteParaTimeNames provides completion for ParaTime names. +// It uses the default network if --network flag is not set. +func CompleteParaTimeNames(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + cfg := config.Global() + + // Get network from flag or use default. + networkName, _ := cmd.Flags().GetString("network") + if networkName == "" { + networkName = cfg.Networks.Default + } + if networkName == "" { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + network := cfg.Networks.All[networkName] + if network == nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + return noFileComp(mapKeys(network.ParaTimes.All)) +} + +// CompleteNetworkThenParaTime provides completion for commands that take +// as positional arguments. +func CompleteNetworkThenParaTime(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) { + // First argument: complete network names. + if len(args) == 0 { + return noFileComp(networkNames()) + } + + // Second argument: complete paratime names for the given network. + if len(args) == 1 { + network := config.Global().Networks.All[args[0]] + if network == nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return noFileComp(mapKeys(network.ParaTimes.All)) + } + + return nil, cobra.ShellCompDirectiveNoFileComp +} + +// Position-aware completers - complete only at specified positions. + +// AddressesAt returns a completion function for wallet + address book names at specified positions. +func AddressesAt(positions ...int) CobraCompletionFunc { + return completeAt(addressNames, positions...) +} + +// NetworksAt returns a completion function for network names at specified positions. +func NetworksAt(positions ...int) CobraCompletionFunc { + return completeAt(networkNames, positions...) +} + +// StaticAt returns a completion function for static values at specified positions. +func StaticAt(values []string, positions ...int) CobraCompletionFunc { + return completeAt(func() []string { return values }, positions...) +} + +// RegisterSelectorCompletions registers completion functions for the common +// selector flags (--network, --paratime, --account) on the given command. +func RegisterSelectorCompletions(cmd *cobra.Command) { + _ = cmd.RegisterFlagCompletionFunc("network", CompleteNetworkNames) + _ = cmd.RegisterFlagCompletionFunc("paratime", CompleteParaTimeNames) + _ = cmd.RegisterFlagCompletionFunc("account", CompleteAccountNames) +} diff --git a/cmd/common/selector.go b/cmd/common/selector.go index 734ad8f7..03dfe1d3 100644 --- a/cmd/common/selector.go +++ b/cmd/common/selector.go @@ -128,6 +128,40 @@ func (npa *NPASelection) ConsensusDenomination() (denom types.Denomination) { return denom } +// AddSelectorFlags adds network/paratime/account selector flags with completions. +func AddSelectorFlags(cmd *cobra.Command) { + cmd.Flags().AddFlagSet(SelectorFlags) + _ = cmd.RegisterFlagCompletionFunc("network", CompleteNetworkNames) + _ = cmd.RegisterFlagCompletionFunc("paratime", CompleteParaTimeNames) + _ = cmd.RegisterFlagCompletionFunc("account", CompleteAccountNames) +} + +// AddSelectorNPFlags adds network/paratime selector flags with completions. +func AddSelectorNPFlags(cmd *cobra.Command) { + cmd.Flags().AddFlagSet(SelectorNPFlags) + _ = cmd.RegisterFlagCompletionFunc("network", CompleteNetworkNames) + _ = cmd.RegisterFlagCompletionFunc("paratime", CompleteParaTimeNames) +} + +// AddSelectorNAFlags adds network/account selector flags with completions. +func AddSelectorNAFlags(cmd *cobra.Command) { + cmd.Flags().AddFlagSet(SelectorNAFlags) + _ = cmd.RegisterFlagCompletionFunc("network", CompleteNetworkNames) + _ = cmd.RegisterFlagCompletionFunc("account", CompleteAccountNames) +} + +// AddSelectorNFlags adds network selector flags with completions. +func AddSelectorNFlags(cmd *cobra.Command) { + cmd.Flags().AddFlagSet(SelectorNFlags) + _ = cmd.RegisterFlagCompletionFunc("network", CompleteNetworkNames) +} + +// AddAccountFlag adds account selector flag with completion. +func AddAccountFlag(cmd *cobra.Command) { + cmd.Flags().AddFlagSet(AccountFlag) + _ = cmd.RegisterFlagCompletionFunc("account", CompleteAccountNames) +} + func init() { AccountFlag = flag.NewFlagSet("", flag.ContinueOnError) AccountFlag.StringVar(&selectedAccount, "account", "", "explicitly set account to use") diff --git a/cmd/completion.go b/cmd/completion.go new file mode 100644 index 00000000..815deff0 --- /dev/null +++ b/cmd/completion.go @@ -0,0 +1,67 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +var completionCmd = &cobra.Command{ + Use: "completion [bash|zsh|fish|powershell]", + Short: "Generate shell completion script", + Long: `Generate shell completion script for the specified shell. + +To load completions: + +Bash: + $ source <(oasis completion bash) + + # To load completions for each session, execute once: + # Linux: + $ oasis completion bash > /etc/bash_completion.d/oasis + # macOS: + $ oasis completion bash > $(brew --prefix)/etc/bash_completion.d/oasis + +Zsh: + # If shell completion is not already enabled in your environment, + # you will need to enable it. You can execute the following once: + $ echo "autoload -U compinit; compinit" >> ~/.zshrc + + # To load completions for each session, execute once: + $ oasis completion zsh > "${fpath[1]}/_oasis" + + # You will need to start a new shell for this setup to take effect. + +Fish: + $ oasis completion fish | source + + # To load completions for each session, execute once: + $ oasis completion fish > ~/.config/fish/completions/oasis.fish + +PowerShell: + PS> oasis completion powershell | Out-String | Invoke-Expression + + # To load completions for every new session, run: + PS> oasis completion powershell > oasis.ps1 + # and source this file from your PowerShell profile. +`, + DisableFlagsInUseLine: true, + ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + RunE: func(cmd *cobra.Command, args []string) error { + out := cmd.OutOrStdout() + switch args[0] { + case "bash": + return cmd.Root().GenBashCompletion(out) + case "zsh": + return cmd.Root().GenZshCompletion(out) + case "fish": + return cmd.Root().GenFishCompletion(out, true) + case "powershell": + return cmd.Root().GenPowerShellCompletionWithDesc(out) + } + return nil + }, +} + +func init() { + rootCmd.AddCommand(completionCmd) +} diff --git a/cmd/contract.go b/cmd/contract.go index 87d7d573..7961e90f 100644 --- a/cmd/contract.go +++ b/cmd/contract.go @@ -487,15 +487,15 @@ func parseTokens(pt *config.ParaTime, tokens []string) []types.BaseUnits { } func init() { - contractShowCmd.Flags().AddFlagSet(common.SelectorFlags) - contractShowCodeCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(contractShowCmd) + common.AddSelectorFlags(contractShowCodeCmd) - contractDumpCodeCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(contractDumpCodeCmd) contractsUploadFlags := flag.NewFlagSet("", flag.ContinueOnError) contractsUploadFlags.StringVar(&contractInstantiatePolicy, "instantiate-policy", "everyone", "contract instantiation policy") - contractUploadCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(contractUploadCmd) contractUploadCmd.Flags().AddFlagSet(common.RuntimeTxFlags) contractUploadCmd.Flags().AddFlagSet(contractsUploadFlags) @@ -505,16 +505,16 @@ func init() { contractsInstantiateFlags := flag.NewFlagSet("", flag.ContinueOnError) contractsInstantiateFlags.StringVar(&contractUpgradesPolicy, "upgrades-policy", "owner", "contract upgrades policy") - contractInstantiateCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(contractInstantiateCmd) contractInstantiateCmd.Flags().AddFlagSet(common.RuntimeTxFlags) contractInstantiateCmd.Flags().AddFlagSet(contractsInstantiateFlags) contractInstantiateCmd.Flags().AddFlagSet(contractsCallFlags) - contractCallCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(contractCallCmd) contractCallCmd.Flags().AddFlagSet(common.RuntimeTxFlags) contractCallCmd.Flags().AddFlagSet(contractsCallFlags) - contractChangeUpgradePolicyCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(contractChangeUpgradePolicyCmd) contractChangeUpgradePolicyCmd.Flags().AddFlagSet(common.RuntimeTxFlags) contractsStorageDumpCmdFlags := flag.NewFlagSet("", flag.ContinueOnError) @@ -526,10 +526,10 @@ func init() { ) contractsStorageDumpCmdFlags.Uint64Var(&contractStorageDumpLimit, "limit", 0, "result set limit") contractsStorageDumpCmdFlags.Uint64Var(&contractStorageDumpOffset, "offset", 0, "result set offset") - contractStorageDumpCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(contractStorageDumpCmd) contractStorageDumpCmd.Flags().AddFlagSet(contractsStorageDumpCmdFlags) - contractStorageGetCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(contractStorageGetCmd) contractStorageCmd.AddCommand(contractStorageDumpCmd) contractStorageCmd.AddCommand(contractStorageGetCmd) diff --git a/cmd/network/governance/create.go b/cmd/network/governance/create.go index c7387e58..81902993 100644 --- a/cmd/network/governance/create.go +++ b/cmd/network/governance/create.go @@ -205,13 +205,13 @@ var ( ) func init() { - govCreateProposalUpgradeCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(govCreateProposalUpgradeCmd) govCreateProposalUpgradeCmd.Flags().AddFlagSet(common.TxFlags) - govCreateProposalParameterChangeCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(govCreateProposalParameterChangeCmd) govCreateProposalUpgradeCmd.Flags().AddFlagSet(common.TxFlags) - govCreateProposalCancelUpgradeCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(govCreateProposalCancelUpgradeCmd) govCreateProposalCancelUpgradeCmd.Flags().AddFlagSet(common.TxFlags) govCreateProposalCmd.AddCommand(govCreateProposalUpgradeCmd) diff --git a/cmd/network/governance/list.go b/cmd/network/governance/list.go index d4b71d89..0a114fc6 100644 --- a/cmd/network/governance/list.go +++ b/cmd/network/governance/list.go @@ -65,6 +65,6 @@ var govListCmd = &cobra.Command{ } func init() { - govListCmd.Flags().AddFlagSet(common.SelectorNFlags) + common.AddSelectorNFlags(govListCmd) govListCmd.Flags().AddFlagSet(common.HeightFlag) } diff --git a/cmd/network/governance/show.go b/cmd/network/governance/show.go index f7d652f9..894848a6 100644 --- a/cmd/network/governance/show.go +++ b/cmd/network/governance/show.go @@ -464,6 +464,6 @@ func init() { showVotesFlag := flag.NewFlagSet("", flag.ContinueOnError) showVotesFlag.BoolVar(&showVotes, "show-votes", false, "individual entity votes") govShowCmd.Flags().AddFlagSet(showVotesFlag) - govShowCmd.Flags().AddFlagSet(common.SelectorNFlags) + common.AddSelectorNFlags(govShowCmd) govShowCmd.Flags().AddFlagSet(common.HeightFlag) } diff --git a/cmd/network/governance/vote.go b/cmd/network/governance/vote.go index f5d82f14..d0a1a8ed 100644 --- a/cmd/network/governance/vote.go +++ b/cmd/network/governance/vote.go @@ -15,9 +15,10 @@ import ( ) var govCastVoteCmd = &cobra.Command{ - Use: "cast-vote { yes | no | abstain }", - Short: "Cast a governance vote on a proposal", - Args: cobra.ExactArgs(2), + Use: "cast-vote { yes | no | abstain }", + Short: "Cast a governance vote on a proposal", + Args: cobra.ExactArgs(2), + ValidArgsFunction: common.StaticAt([]string{"yes", "no", "abstain"}, 1), // Vote options at position 2. Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) @@ -65,6 +66,6 @@ var govCastVoteCmd = &cobra.Command{ } func init() { - govCastVoteCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(govCastVoteCmd) govCastVoteCmd.Flags().AddFlagSet(common.TxFlags) } diff --git a/cmd/network/remove.go b/cmd/network/remove.go index bbdbe1c1..e5f2a3a9 100644 --- a/cmd/network/remove.go +++ b/cmd/network/remove.go @@ -10,10 +10,11 @@ import ( ) var rmCmd = &cobra.Command{ - Use: "remove ", - Aliases: []string{"rm"}, - Short: "Remove an existing network", - Args: cobra.ExactArgs(1), + Use: "remove ", + Aliases: []string{"rm"}, + Short: "Remove an existing network", + Args: cobra.ExactArgs(1), + ValidArgsFunction: common.CompleteNetworkNames, Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() name := args[0] diff --git a/cmd/network/set_chain_context.go b/cmd/network/set_chain_context.go index 3c02f8c9..47391792 100644 --- a/cmd/network/set_chain_context.go +++ b/cmd/network/set_chain_context.go @@ -9,13 +9,15 @@ import ( "github.com/oasisprotocol/oasis-sdk/client-sdk/go/config" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/connection" + "github.com/oasisprotocol/cli/cmd/common" cliConfig "github.com/oasisprotocol/cli/config" ) var setChainContextCmd = &cobra.Command{ - Use: "set-chain-context [chain-context]", - Short: "Sets the chain context of the given network", - Args: cobra.RangeArgs(1, 2), + Use: "set-chain-context [chain-context]", + Short: "Sets the chain context of the given network", + Args: cobra.RangeArgs(1, 2), + ValidArgsFunction: common.CompleteNetworkNames, Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() name := args[0] diff --git a/cmd/network/set_default.go b/cmd/network/set_default.go index 0f542956..ae997836 100644 --- a/cmd/network/set_default.go +++ b/cmd/network/set_default.go @@ -3,13 +3,15 @@ package network import ( "github.com/spf13/cobra" + "github.com/oasisprotocol/cli/cmd/common" cliConfig "github.com/oasisprotocol/cli/config" ) var setDefaultCmd = &cobra.Command{ - Use: "set-default ", - Short: "Sets the given network as the default network", - Args: cobra.ExactArgs(1), + Use: "set-default ", + Short: "Sets the given network as the default network", + Args: cobra.ExactArgs(1), + ValidArgsFunction: common.CompleteNetworkNames, Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() name := args[0] diff --git a/cmd/network/set_rpc.go b/cmd/network/set_rpc.go index d71a4371..c10058da 100644 --- a/cmd/network/set_rpc.go +++ b/cmd/network/set_rpc.go @@ -5,13 +5,15 @@ import ( "github.com/spf13/cobra" + "github.com/oasisprotocol/cli/cmd/common" cliConfig "github.com/oasisprotocol/cli/config" ) var setRPCCmd = &cobra.Command{ - Use: "set-rpc ", - Short: "Sets the RPC endpoint of the given network", - Args: cobra.ExactArgs(2), + Use: "set-rpc ", + Short: "Sets the RPC endpoint of the given network", + Args: cobra.ExactArgs(2), + ValidArgsFunction: common.CompleteNetworkNames, Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() name, rpc := args[0], args[1] diff --git a/cmd/network/show.go b/cmd/network/show.go index 9ffe877f..09d2288f 100644 --- a/cmd/network/show.go +++ b/cmd/network/show.go @@ -552,7 +552,7 @@ func showParameters(ctx context.Context, npa *common.NPASelection, height int64, } func init() { - showCmd.Flags().AddFlagSet(common.SelectorNFlags) + common.AddSelectorNFlags(showCmd) showCmd.Flags().AddFlagSet(common.HeightFlag) showCmd.Flags().AddFlagSet(common.FormatFlag) } diff --git a/cmd/network/status.go b/cmd/network/status.go index 5be03444..d0674b1b 100644 --- a/cmd/network/status.go +++ b/cmd/network/status.go @@ -218,5 +218,5 @@ var statusCmd = &cobra.Command{ func init() { statusCmd.Flags().AddFlagSet(common.FormatFlag) - statusCmd.Flags().AddFlagSet(common.SelectorNFlags) + common.AddSelectorNFlags(statusCmd) } diff --git a/cmd/network/trust.go b/cmd/network/trust.go index 163f656e..c9d67ecc 100644 --- a/cmd/network/trust.go +++ b/cmd/network/trust.go @@ -163,5 +163,5 @@ func calcTrustPeriod(debondingPeriod time.Duration) time.Duration { func init() { trustCmd.Flags().AddFlagSet(common.FormatFlag) - trustCmd.Flags().AddFlagSet(common.SelectorNFlags) + common.AddSelectorNFlags(trustCmd) } diff --git a/cmd/paratime/add.go b/cmd/paratime/add.go index f78aba8e..a86a48aa 100644 --- a/cmd/paratime/add.go +++ b/cmd/paratime/add.go @@ -19,9 +19,10 @@ var ( description string addCmd = &cobra.Command{ - Use: "add ", - Short: "Add a new ParaTime", - Args: cobra.ExactArgs(3), + Use: "add ", + Short: "Add a new ParaTime", + Args: cobra.ExactArgs(3), + ValidArgsFunction: common.NetworksAt(0), // at position 1. Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() network, name, id := args[0], args[1], args[2] diff --git a/cmd/paratime/denomination/remove.go b/cmd/paratime/denomination/remove.go index 2088f29a..495cf4d6 100644 --- a/cmd/paratime/denomination/remove.go +++ b/cmd/paratime/denomination/remove.go @@ -8,13 +8,15 @@ import ( "github.com/oasisprotocol/oasis-sdk/client-sdk/go/config" + "github.com/oasisprotocol/cli/cmd/common" cliConfig "github.com/oasisprotocol/cli/config" ) var removeDenomCmd = &cobra.Command{ - Use: "remove ", - Short: "Remove denomination", - Args: cobra.ExactArgs(3), + Use: "remove ", + Short: "Remove denomination", + Args: cobra.ExactArgs(3), + ValidArgsFunction: common.CompleteNetworkThenParaTime, Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() networkArg, ptArg, denomArg := args[0], args[1], args[2] diff --git a/cmd/paratime/denomination/set-native.go b/cmd/paratime/denomination/set-native.go index 0df8f335..ad0c4d73 100644 --- a/cmd/paratime/denomination/set-native.go +++ b/cmd/paratime/denomination/set-native.go @@ -8,13 +8,15 @@ import ( "github.com/oasisprotocol/oasis-sdk/client-sdk/go/config" + "github.com/oasisprotocol/cli/cmd/common" cliConfig "github.com/oasisprotocol/cli/config" ) var setNativeDenomCmd = &cobra.Command{ - Use: "set-native ", - Short: "Set native denomination", - Args: cobra.ExactArgs(4), + Use: "set-native ", + Short: "Set native denomination", + Args: cobra.ExactArgs(4), + ValidArgsFunction: common.CompleteNetworkThenParaTime, Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() networkArg, ptArg, symbolArg, decimalsArg := args[0], args[1], args[2], args[3] diff --git a/cmd/paratime/denomination/set.go b/cmd/paratime/denomination/set.go index 988c2db1..5bc685d9 100644 --- a/cmd/paratime/denomination/set.go +++ b/cmd/paratime/denomination/set.go @@ -9,6 +9,7 @@ import ( "github.com/oasisprotocol/oasis-sdk/client-sdk/go/config" + "github.com/oasisprotocol/cli/cmd/common" cliConfig "github.com/oasisprotocol/cli/config" ) @@ -16,9 +17,10 @@ var ( symbol string setDenomCmd = &cobra.Command{ - Use: "set [--symbol ]", - Short: "Set denomination", - Args: cobra.ExactArgs(4), + Use: "set [--symbol ]", + Short: "Set denomination", + Args: cobra.ExactArgs(4), + ValidArgsFunction: common.CompleteNetworkThenParaTime, Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() networkArg, ptArg, denomArg, decimalsArg := args[0], args[1], args[2], args[3] diff --git a/cmd/paratime/register.go b/cmd/paratime/register.go index 776245f4..3715a3c4 100644 --- a/cmd/paratime/register.go +++ b/cmd/paratime/register.go @@ -57,6 +57,6 @@ var registerCmd = &cobra.Command{ } func init() { - registerCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(registerCmd) registerCmd.Flags().AddFlagSet(common.TxFlags) } diff --git a/cmd/paratime/remove.go b/cmd/paratime/remove.go index 81f9e9e9..53950afc 100644 --- a/cmd/paratime/remove.go +++ b/cmd/paratime/remove.go @@ -5,14 +5,16 @@ import ( "github.com/spf13/cobra" + "github.com/oasisprotocol/cli/cmd/common" cliConfig "github.com/oasisprotocol/cli/config" ) var removeCmd = &cobra.Command{ - Use: "remove ", - Aliases: []string{"rm"}, - Short: "Remove an existing ParaTime", - Args: cobra.ExactArgs(2), + Use: "remove ", + Aliases: []string{"rm"}, + Short: "Remove an existing ParaTime", + Args: cobra.ExactArgs(2), + ValidArgsFunction: common.CompleteNetworkThenParaTime, Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() network, name := args[0], args[1] diff --git a/cmd/paratime/set_default.go b/cmd/paratime/set_default.go index 45958bfd..c9cf041b 100644 --- a/cmd/paratime/set_default.go +++ b/cmd/paratime/set_default.go @@ -5,13 +5,15 @@ import ( "github.com/spf13/cobra" + "github.com/oasisprotocol/cli/cmd/common" cliConfig "github.com/oasisprotocol/cli/config" ) var setDefaultCmd = &cobra.Command{ - Use: "set-default ", - Short: "Sets the given ParaTime as the default ParaTime for the given network", - Args: cobra.ExactArgs(2), + Use: "set-default ", + Short: "Sets the given ParaTime as the default ParaTime for the given network", + Args: cobra.ExactArgs(2), + ValidArgsFunction: common.CompleteNetworkThenParaTime, Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() network, name := args[0], args[1] diff --git a/cmd/paratime/show.go b/cmd/paratime/show.go index 8f8ce3ff..b2d7f6a8 100644 --- a/cmd/paratime/show.go +++ b/cmd/paratime/show.go @@ -538,6 +538,6 @@ func init() { roundFlag.Uint64Var(&selectedRound, "round", client.RoundLatest, "explicitly set block round to use") showCmd.Flags().AddFlagSet(common.FormatFlag) - showCmd.Flags().AddFlagSet(common.SelectorNPFlags) + common.AddSelectorNPFlags(showCmd) showCmd.Flags().AddFlagSet(roundFlag) } diff --git a/cmd/paratime/statistics.go b/cmd/paratime/statistics.go index 2b375967..566fac89 100644 --- a/cmd/paratime/statistics.go +++ b/cmd/paratime/statistics.go @@ -530,6 +530,6 @@ func (s *runtimeStats) printEntityStats() { } func init() { - statsCmd.Flags().AddFlagSet(common.SelectorNPFlags) + common.AddSelectorNPFlags(statsCmd) statsCmd.Flags().StringVarP(&fileCSV, "output-file", "o", "", "output statistics into specified CSV file") } diff --git a/cmd/rofl/machine/mgmt.go b/cmd/rofl/machine/mgmt.go index df58a425..dcf0c851 100644 --- a/cmd/rofl/machine/mgmt.go +++ b/cmd/rofl/machine/mgmt.go @@ -393,25 +393,25 @@ func showCommandArgs[V any](npa *common.NPASelection, raw []byte, args V) { } func init() { - restartCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(restartCmd) restartCmd.Flags().AddFlagSet(common.RuntimeTxFlags) restartCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags) restartCmd.Flags().AddFlagSet(roflCommon.WipeFlags) - stopCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(stopCmd) stopCmd.Flags().AddFlagSet(common.RuntimeTxFlags) stopCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags) stopCmd.Flags().AddFlagSet(roflCommon.WipeFlags) - removeCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(removeCmd) removeCmd.Flags().AddFlagSet(common.RuntimeTxFlags) removeCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags) - changeAdminCmd.Flags().AddFlagSet(common.AccountFlag) + common.AddAccountFlag(changeAdminCmd) changeAdminCmd.Flags().AddFlagSet(common.RuntimeTxFlags) changeAdminCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags) - topUpCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(topUpCmd) topUpCmd.Flags().AddFlagSet(common.RuntimeTxFlags) topUpCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags) topUpCmd.Flags().AddFlagSet(roflCommon.TermFlags) diff --git a/cmd/rofl/machine/show.go b/cmd/rofl/machine/show.go index 1eaca3d8..08f9d5a3 100644 --- a/cmd/rofl/machine/show.go +++ b/cmd/rofl/machine/show.go @@ -221,6 +221,6 @@ func showMachinePorts(extraCfg *roflCmdBuild.AppExtraConfig, appID rofl.AppID, i } func init() { - showCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(showCmd) showCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags) } diff --git a/cmd/rofl/mgmt.go b/cmd/rofl/mgmt.go index 3e0898f7..24ffaf48 100644 --- a/cmd/rofl/mgmt.go +++ b/cmd/rofl/mgmt.go @@ -879,7 +879,7 @@ func init() { initCmd.Flags().BoolVar(&reset, "reset", false, "reset the existing ROFL manifest") initCmd.Flags().AddFlagSet(common.AnswerYesFlag) - createCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(createCmd) createCmd.Flags().AddFlagSet(common.RuntimeTxFlags) createCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags) createCmd.Flags().AddFlagSet(roflCommon.NoUpdateFlag) @@ -889,11 +889,11 @@ func init() { updateCmd.Flags().AddFlagSet(common.RuntimeTxFlags) updateCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags) - removeCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(removeCmd) removeCmd.Flags().AddFlagSet(common.RuntimeTxFlags) removeCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags) - showCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(showCmd) showCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags) showCmd.Flags().AddFlagSet(common.FormatFlag) diff --git a/cmd/rofl/provider/list.go b/cmd/rofl/provider/list.go index e4cb81e0..a565439f 100644 --- a/cmd/rofl/provider/list.go +++ b/cmd/rofl/provider/list.go @@ -219,6 +219,6 @@ func ShowOfferSummary(npa *common.NPASelection, offer *roflmarket.Offer) { func init() { listCmd.Flags().AddFlagSet(roflCommon.ShowOffersFlag) - listCmd.Flags().AddFlagSet(common.SelectorNPFlags) + common.AddSelectorNPFlags(listCmd) listCmd.Flags().AddFlagSet(common.FormatFlag) } diff --git a/cmd/rofl/provider/mgmt.go b/cmd/rofl/provider/mgmt.go index 49c82b7b..de345b2f 100644 --- a/cmd/rofl/provider/mgmt.go +++ b/cmd/rofl/provider/mgmt.go @@ -442,17 +442,17 @@ func maybeLoadManifestAndSetNPA(cfg *cliConfig.Config, npa *common.NPASelection) } func init() { - initCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(initCmd) - createCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(createCmd) createCmd.Flags().AddFlagSet(common.RuntimeTxFlags) - updateCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(updateCmd) updateCmd.Flags().AddFlagSet(common.RuntimeTxFlags) - updateOffersCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(updateOffersCmd) updateOffersCmd.Flags().AddFlagSet(common.RuntimeTxFlags) - removeCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(removeCmd) removeCmd.Flags().AddFlagSet(common.RuntimeTxFlags) } diff --git a/cmd/rofl/provider/show.go b/cmd/rofl/provider/show.go index c5b0a3c1..435de21d 100644 --- a/cmd/rofl/provider/show.go +++ b/cmd/rofl/provider/show.go @@ -154,6 +154,6 @@ func outputProviderText(npa *common.NPASelection, provider *roflmarket.Provider, } func init() { - showCmd.Flags().AddFlagSet(common.SelectorNPFlags) + common.AddSelectorNPFlags(showCmd) showCmd.Flags().AddFlagSet(common.FormatFlag) } diff --git a/cmd/rofl/trust_root.go b/cmd/rofl/trust_root.go index e99847f7..cb5a3056 100644 --- a/cmd/rofl/trust_root.go +++ b/cmd/rofl/trust_root.go @@ -47,6 +47,6 @@ var trustRootCmd = &cobra.Command{ } func init() { - trustRootCmd.Flags().AddFlagSet(common.SelectorNPFlags) + common.AddSelectorNPFlags(trustRootCmd) trustRootCmd.Flags().AddFlagSet(common.HeightFlag) } diff --git a/cmd/root.go b/cmd/root.go index 9fd244ab..adb3369e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -48,39 +48,62 @@ Go toolchain version: {{ toolchain }} `) } +// isCompletionCommand checks if the CLI is being invoked for shell completion. +// This is used to skip side effects (file creation, migrations) during tab-completion. +func isCompletionCommand() bool { + if len(os.Args) > 1 { + switch os.Args[1] { + case "completion", "__complete", "__completeNoDesc": + return true + } + } + return false +} + +// ensureConfigExists creates the config file with defaults if it doesn't exist. +func ensureConfigExists(v *viper.Viper, configPath string) { + if _, err := os.Stat(configPath); !errors.Is(err, fs.ErrNotExist) { + return + } + if _, err := os.Create(configPath); err != nil { + cobra.CheckErr(fmt.Errorf("failed to create configuration file: %w", err)) + } + config.ResetDefaults() + _ = config.Save(v) +} + func initConfig() { v := viper.New() + completionMode := isCompletionCommand() if cfgFile != "" { - // Use config file from the flag. v.SetConfigFile(cfgFile) } else { const configFilename = "cli.toml" configDir := config.DefaultDirectory() - configPath := filepath.Join(configDir, configFilename) v.AddConfigPath(configDir) v.SetConfigType("toml") v.SetConfigName(configFilename) - // Ensure the configuration file exists. - _ = os.MkdirAll(configDir, 0o700) - if _, err := os.Stat(configPath); errors.Is(err, fs.ErrNotExist) { - if _, err := os.Create(configPath); err != nil { - cobra.CheckErr(fmt.Errorf("failed to create configuration file: %w", err)) - } - - // Populate the initial configuration file with defaults. - config.ResetDefaults() - _ = config.Save(v) + // Skip file creation during completion to avoid side effects. + if !completionMode { + _ = os.MkdirAll(configDir, 0o700) + ensureConfigExists(v, filepath.Join(configDir, configFilename)) } } _ = v.ReadInConfig() - // Load and validate global configuration. + // Load global configuration. err := config.Load(v) cobra.CheckErr(err) + + // Skip migrations and validation during completion to avoid side effects. + if completionMode { + return + } + changes, err := config.Global().Migrate() cobra.CheckErr(err) if changes { diff --git a/cmd/tx.go b/cmd/tx.go index 15fab6ed..a44ec613 100644 --- a/cmd/tx.go +++ b/cmd/tx.go @@ -186,12 +186,12 @@ func tryDecodeTx(rawTx []byte) (any, error) { } func init() { - txSubmitCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(txSubmitCmd) - txSignCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(txSignCmd) txSignCmd.Flags().AddFlagSet(common.RuntimeTxFlags) - txShowCmd.Flags().AddFlagSet(common.SelectorNPFlags) + common.AddSelectorNPFlags(txShowCmd) txCmd.AddCommand(txSubmitCmd) txCmd.AddCommand(txSignCmd) diff --git a/cmd/wallet/export.go b/cmd/wallet/export.go index 2f6d1a62..08553726 100644 --- a/cmd/wallet/export.go +++ b/cmd/wallet/export.go @@ -10,9 +10,10 @@ import ( ) var exportCmd = &cobra.Command{ - Use: "export ", - Short: "Export secret account information", - Args: cobra.ExactArgs(1), + Use: "export ", + Short: "Export secret account information", + Args: cobra.ExactArgs(1), + ValidArgsFunction: common.CompleteAccountNames, Run: func(_ *cobra.Command, args []string) { name := args[0] diff --git a/cmd/wallet/remove.go b/cmd/wallet/remove.go index adaeda50..701a6fe3 100644 --- a/cmd/wallet/remove.go +++ b/cmd/wallet/remove.go @@ -13,10 +13,11 @@ import ( ) var rmCmd = &cobra.Command{ - Use: "remove [name ...]", - Aliases: []string{"rm"}, - Short: "Remove existing account(s)", - Args: cobra.MinimumNArgs(1), + Use: "remove [name ...]", + Aliases: []string{"rm"}, + Short: "Remove existing account(s)", + Args: cobra.MinimumNArgs(1), + ValidArgsFunction: common.CompleteAccountNames, Run: func(_ *cobra.Command, args []string) { cfg := config.Global() diff --git a/cmd/wallet/rename.go b/cmd/wallet/rename.go index b7201c3b..62c010e3 100644 --- a/cmd/wallet/rename.go +++ b/cmd/wallet/rename.go @@ -5,14 +5,16 @@ import ( "github.com/spf13/cobra" + "github.com/oasisprotocol/cli/cmd/common" "github.com/oasisprotocol/cli/config" ) var renameCmd = &cobra.Command{ - Use: "rename ", - Short: "Rename an existing account", - Aliases: []string{"mv"}, - Args: cobra.ExactArgs(2), + Use: "rename ", + Short: "Rename an existing account", + Aliases: []string{"mv"}, + Args: cobra.ExactArgs(2), + ValidArgsFunction: common.CompleteAccountNames, Run: func(_ *cobra.Command, args []string) { cfg := config.Global() oldName, newName := args[0], args[1] diff --git a/cmd/wallet/set_default.go b/cmd/wallet/set_default.go index 10218168..c8ae479c 100644 --- a/cmd/wallet/set_default.go +++ b/cmd/wallet/set_default.go @@ -3,13 +3,15 @@ package wallet import ( "github.com/spf13/cobra" + "github.com/oasisprotocol/cli/cmd/common" "github.com/oasisprotocol/cli/config" ) var setDefaultCmd = &cobra.Command{ - Use: "set-default ", - Short: "Sets the given account as the default account", - Args: cobra.ExactArgs(1), + Use: "set-default ", + Short: "Sets the given account as the default account", + Args: cobra.ExactArgs(1), + ValidArgsFunction: common.CompleteAccountNames, Run: func(_ *cobra.Command, args []string) { cfg := config.Global() name := args[0] diff --git a/cmd/wallet/show.go b/cmd/wallet/show.go index 14dab7b0..c23a5fbf 100644 --- a/cmd/wallet/show.go +++ b/cmd/wallet/show.go @@ -11,10 +11,11 @@ import ( ) var showCmd = &cobra.Command{ - Use: "show ", - Short: "Show public account information", - Aliases: []string{"s"}, - Args: cobra.ExactArgs(1), + Use: "show ", + Short: "Show public account information", + Aliases: []string{"s"}, + Args: cobra.ExactArgs(1), + ValidArgsFunction: common.CompleteAccountNames, Run: func(_ *cobra.Command, args []string) { name := args[0] diff --git a/docs/setup.mdx b/docs/setup.mdx index 4313ba23..54873977 100644 --- a/docs/setup.mdx +++ b/docs/setup.mdx @@ -151,6 +151,87 @@ This command will check for a newer version on GitHub, show you the release notes, and ask for confirmation before downloading and replacing the current binary. +## Shell Completion + +The Oasis CLI supports shell completion for Bash, Zsh, Fish, and PowerShell. +Completions allow you to press `Tab` to auto-complete commands, subcommands, +and flags. + + + + #### Temporary (current session) + + ```shell + source <(oasis completion bash) + ``` + + #### Permanent + + **Linux:** + ```shell + oasis completion bash > /etc/bash_completion.d/oasis + ``` + + **macOS (Homebrew):** + ```shell + oasis completion bash > $(brew --prefix)/etc/bash_completion.d/oasis + ``` + + + + #### Temporary (current session) + + ```shell + source <(oasis completion zsh) + ``` + + #### Permanent + + First, ensure completion is enabled in your `~/.zshrc`: + ```shell + echo "autoload -U compinit; compinit" >> ~/.zshrc + ``` + + Then install the completion: + ```shell + oasis completion zsh > "${fpath[1]}/_oasis" + ``` + + Start a new shell for the changes to take effect. + + + + #### Temporary (current session) + + ```shell + oasis completion fish | source + ``` + + #### Permanent + + ```shell + oasis completion fish > ~/.config/fish/completions/oasis.fish + ``` + + + + #### Temporary (current session) + + ```powershell + oasis completion powershell | Out-String | Invoke-Expression + ``` + + #### Permanent + + Add the following to your PowerShell profile: + ```powershell + oasis completion powershell | Out-String | Invoke-Expression + ``` + + To find your profile path, run `echo $PROFILE` in PowerShell. + + + ## Configuration When running the Oasis CLI for the first time, it will generate a configuration