Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,31 @@ make build # Build from source
make install # Install globally (optional)
```

## Shell Completion

Enable tab completion for commands, services, and arguments:

```bash
# Bash
source <(xcli completion bash)

# To load completions for each session (Linux):
xcli completion bash > /etc/bash_completion.d/xcli

# To load completions for each session (macOS):
xcli completion bash > $(brew --prefix)/etc/bash_completion.d/xcli

# Zsh
echo "autoload -U compinit; compinit" >> ~/.zshrc # Enable completion (once)
xcli completion zsh > "${fpath[1]}/_xcli"

# Fish
xcli completion fish | source

# To load completions for each session:
xcli completion fish > ~/.config/fish/completions/xcli.fish
```

## Quick Start

```bash
Expand Down
1 change: 1 addition & 0 deletions cmd/xcli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ func main() {
// Add root-level commands
rootCmd.AddCommand(commands.NewInitCommand(log, configPath))
rootCmd.AddCommand(commands.NewConfigCommand(log, configPath))
rootCmd.AddCommand(commands.NewCompletionCommand())

// Add stack commands
rootCmd.AddCommand(commands.NewLabCommand(log, configPath))
Expand Down
69 changes: 69 additions & 0 deletions pkg/commands/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package commands

import (
"fmt"
"os"

"github.com/spf13/cobra"
)

// NewCompletionCommand creates the completion command for generating shell completion scripts.
func NewCompletionCommand() *cobra.Command {
return &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate shell completion scripts",
Long: fmt.Sprintf(`Generate shell completion scripts for xcli.

To load completions:

Bash:
$ source <(%[1]s completion bash)

# To load completions for each session, execute once:
# Linux:
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s
# macOS:
$ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s

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:
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s"

# You will need to start a new shell for this setup to take effect.

Fish:
$ %[1]s completion fish | source

# To load completions for each session, execute once:
$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish

PowerShell:
PS> %[1]s completion powershell | Out-String | Invoke-Expression

# To load completions for every new session, run:
PS> %[1]s completion powershell > %[1]s.ps1
# and source this file from your PowerShell profile.
`, "xcli"),
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
RunE: func(cmd *cobra.Command, args []string) error {
switch args[0] {
case "bash":
return cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
return cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
return cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
default:
return fmt.Errorf("unsupported shell: %s", args[0])
}
},
}
}
92 changes: 92 additions & 0 deletions pkg/commands/completion_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package commands

import (
"io"

"github.com/ethpandaops/xcli/pkg/config"
"github.com/ethpandaops/xcli/pkg/constants"
"github.com/ethpandaops/xcli/pkg/orchestrator"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

// completeServices returns a ValidArgsFunction that completes service names.
// It loads the config and creates an orchestrator to get the dynamic service list.
func completeServices(configPath string) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
return func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
// Don't complete if we already have an argument (for single-arg commands)
if len(args) > 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}

// Create a silent logger for completion (no output)
log := logrus.New()
log.SetOutput(io.Discard)

// Try to load config - fail gracefully
labCfg, cfgPath, err := config.LoadLabConfig(configPath)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}

// Try to create orchestrator - fail gracefully
orch, err := orchestrator.NewOrchestrator(log, labCfg, cfgPath)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}

return orch.GetValidServices(), cobra.ShellCompDirectiveNoFileComp
}
}

// completeModes returns a ValidArgsFunction that completes mode values.
func completeModes() func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
return func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
if len(args) > 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}

return []string{constants.ModeLocal, constants.ModeHybrid}, cobra.ShellCompDirectiveNoFileComp
}
}

// completeRebuildProjects returns a ValidArgsFunction that completes rebuild project names.
func completeRebuildProjects() func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
return func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
if len(args) > 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}

return []string{
"xatu-cbt",
"all",
"cbt",
"cbt-api",
"lab-backend",
"lab-frontend",
"prometheus",
"grafana",
}, cobra.ShellCompDirectiveNoFileComp
}
}

// completeReleasableProjects returns a ValidArgsFunction that completes releasable project names.
// Supports multiple arguments since release accepts multiple projects.
func completeReleasableProjects() func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
return func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
// Filter out already-provided arguments
alreadyUsed := make(map[string]bool, len(args))
for _, arg := range args {
alreadyUsed[arg] = true
}

completions := make([]string, 0, len(constants.ReleasableProjects))
for _, project := range constants.ReleasableProjects {
if !alreadyUsed[project] {
completions = append(completions, project)
}
}

return completions, cobra.ShellCompDirectiveNoFileComp
}
}
7 changes: 4 additions & 3 deletions pkg/commands/lab_logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ func NewLabLogsCommand(log logrus.FieldLogger, configPath string) *cobra.Command
var follow bool

cmd := &cobra.Command{
Use: "logs [service]",
Short: "Show lab service logs",
Long: `Show logs for all lab services or a specific service.`,
Use: "logs [service]",
Short: "Show lab service logs",
Long: `Show logs for all lab services or a specific service.`,
ValidArgsFunction: completeServices(configPath),
RunE: func(cmd *cobra.Command, args []string) error {
labCfg, cfgPath, err := config.LoadLabConfig(configPath)
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion pkg/commands/lab_mode.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ After switching modes, restart the stack for changes to take effect:
Examples:
xcli lab mode local # Switch to fully local mode
xcli lab mode hybrid # Switch to hybrid mode (requires external ClickHouse config)`,
Args: cobra.ExactArgs(1),
Args: cobra.ExactArgs(1),
ValidArgsFunction: completeModes(),
RunE: func(cmd *cobra.Command, args []string) error {
mode := args[0]
if mode != constants.ModeLocal && mode != constants.ModeHybrid {
Expand Down
3 changes: 2 additions & 1 deletion pkg/commands/lab_rebuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ Examples:
xcli lab rebuild grafana # Reload custom dashboards

Note: All rebuild commands automatically restart their respective services if running.`,
Args: cobra.ExactArgs(1),
Args: cobra.ExactArgs(1),
ValidArgsFunction: completeRebuildProjects(),
RunE: func(cmd *cobra.Command, args []string) error {
project := args[0]

Expand Down
3 changes: 2 additions & 1 deletion pkg/commands/lab_release.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ Options:
xcli lab release xatu-cbt --no-deps # Skip dependency prompts

Note: Requires GitHub CLI (gh) to be installed and authenticated.`,
Args: cobra.ArbitraryArgs,
Args: cobra.ArbitraryArgs,
ValidArgsFunction: completeReleasableProjects(),
RunE: func(cmd *cobra.Command, args []string) error {
return runRelease(cmd.Context(), log, args, stackFlag, bumpFlag,
yesFlag, !noWatchFlag, noDepsFlag, timeoutFlag)
Expand Down
9 changes: 5 additions & 4 deletions pkg/commands/lab_restart.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import (
// NewLabRestartCommand creates the lab restart command.
func NewLabRestartCommand(log logrus.FieldLogger, configPath string) *cobra.Command {
return &cobra.Command{
Use: "restart <service>",
Short: "Restart a lab service",
Long: `Restart a specific lab service.`,
Args: cobra.ExactArgs(1),
Use: "restart <service>",
Short: "Restart a lab service",
Long: `Restart a specific lab service.`,
Args: cobra.ExactArgs(1),
ValidArgsFunction: completeServices(configPath),
RunE: func(cmd *cobra.Command, args []string) error {
labCfg, cfgPath, err := config.LoadLabConfig(configPath)
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion pkg/commands/lab_start.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ Available services:
Example:
xcli lab start lab-backend
xcli lab start cbt-mainnet`,
Args: cobra.ExactArgs(1),
Args: cobra.ExactArgs(1),
ValidArgsFunction: completeServices(configPath),
RunE: func(cmd *cobra.Command, args []string) error {
labCfg, cfgPath, err := config.LoadLabConfig(configPath)
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion pkg/commands/lab_stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ Available services:
Example:
xcli lab stop lab-backend
xcli lab stop cbt-mainnet`,
Args: cobra.ExactArgs(1),
Args: cobra.ExactArgs(1),
ValidArgsFunction: completeServices(configPath),
RunE: func(cmd *cobra.Command, args []string) error {
labCfg, cfgPath, err := config.LoadLabConfig(configPath)
if err != nil {
Expand Down