diff --git a/cmd/devstream/apply.go b/cmd/devstream/apply.go index 904544a9d..aced08c4d 100644 --- a/cmd/devstream/apply.go +++ b/cmd/devstream/apply.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/devstream-io/devstream/internal/pkg/completion" "github.com/devstream-io/devstream/internal/pkg/pluginengine" "github.com/devstream-io/devstream/pkg/util/log" ) @@ -12,7 +13,7 @@ import ( var applyCMD = &cobra.Command{ Use: "apply", Short: "Create or update DevOps tools according to DevStream configuration file", - Long: `Create or update DevOps tools according to DevStream configuration file. + Long: `Create or update DevOps tools according to DevStream configuration file. DevStream will generate and execute a new plan based on the config file and the state file by default.`, Run: applyCMDFunc, } @@ -25,8 +26,11 @@ func applyCMDFunc(cmd *cobra.Command, args []string) { } log.Success("Apply finished.") } + func init() { applyCMD.Flags().StringVarP(&configFile, "config-file", "f", "config.yaml", "config file") applyCMD.Flags().StringVarP(&pluginDir, "plugin-dir", "d", pluginengine.DefaultPluginDir, "plugins directory") applyCMD.Flags().BoolVarP(&continueDirectly, "yes", "y", false, "apply directly without confirmation") + + completion.FlagConfigFileCompletion(applyCMD) } diff --git a/cmd/devstream/completion.go b/cmd/devstream/completion.go new file mode 100644 index 000000000..54ff064f9 --- /dev/null +++ b/cmd/devstream/completion.go @@ -0,0 +1,61 @@ +package main + +import ( + "io" + "os" + "path/filepath" + + "github.com/spf13/cobra" + + "github.com/devstream-io/devstream/internal/pkg/completion" +) + +func completionCMD(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "completion", + Short: "Generate the autocompletion script for dtm for the specified shell", + Long: "See each sub-command's help for details on how to use the generated script.", + DisableFlagsInUseLine: true, + Args: cobra.ExactValidArgs(1), + } + + binaryName := filepath.Base(os.Args[0]) + bash := &cobra.Command{ + Use: "bash", + Short: "generate autocompletion script for bash", + Example: completion.BashExample(binaryName), + RunE: func(cmd *cobra.Command, args []string) error { + return completion.CompletionBash(out, cmd) + }, + } + + zsh := &cobra.Command{ + Use: "zsh", + Short: "generate autocompletion script for zsh", + Example: completion.ZshExample(binaryName), + RunE: func(cmd *cobra.Command, args []string) error { + return completion.CompletionZsh(out, cmd) + }, + } + + fish := &cobra.Command{ + Use: "fish", + Short: "generate autocompletion script for fish", + Example: completion.FishExample(binaryName), + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Root().GenFishCompletion(out, true) + }, + } + + powershell := &cobra.Command{ + Use: "powershell", + Short: "generate autocompletion script for powershell", + Example: completion.PowershellExample(binaryName), + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Root().GenPowerShellCompletionWithDesc(out) + }, + } + cmd.AddCommand(bash, zsh, fish, powershell) + + return cmd +} diff --git a/cmd/devstream/delete.go b/cmd/devstream/delete.go index c1de99adc..0f8cf9762 100644 --- a/cmd/devstream/delete.go +++ b/cmd/devstream/delete.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/devstream-io/devstream/internal/pkg/completion" "github.com/devstream-io/devstream/internal/pkg/pluginengine" "github.com/devstream-io/devstream/pkg/util/log" ) @@ -14,7 +15,7 @@ var isForceDelete bool var deleteCMD = &cobra.Command{ Use: "delete", Short: "Delete DevOps tools according to DevStream configuration file", - Long: `Delete DevOps tools according to DevStream configuration file. + Long: `Delete DevOps tools according to DevStream configuration file. DevStream will delete everything defined in the config file, regardless of the state.`, Run: deleteCMDFunc, } @@ -34,4 +35,6 @@ func init() { deleteCMD.Flags().StringVarP(&configFile, "config-file", "f", "config.yaml", "config file") deleteCMD.Flags().StringVarP(&pluginDir, "plugin-dir", "d", pluginengine.DefaultPluginDir, "plugins directory") deleteCMD.Flags().BoolVarP(&continueDirectly, "yes", "y", false, "delete directly without confirmation") + + completion.FlagConfigFileCompletion(applyCMD) } diff --git a/cmd/devstream/destroy.go b/cmd/devstream/destroy.go index 4fb071beb..3e04bce78 100644 --- a/cmd/devstream/destroy.go +++ b/cmd/devstream/destroy.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/devstream-io/devstream/internal/pkg/completion" "github.com/devstream-io/devstream/internal/pkg/pluginengine" "github.com/devstream-io/devstream/pkg/util/log" ) @@ -28,4 +29,6 @@ func destroyCMDFunc(cmd *cobra.Command, args []string) { func init() { destroyCMD.Flags().StringVarP(&configFile, "config-file", "f", "config.yaml", "config file") destroyCMD.Flags().BoolVarP(&continueDirectly, "yes", "y", false, "destroy directly without confirmation") + + completion.FlagConfigFileCompletion(destroyCMD) } diff --git a/cmd/devstream/init.go b/cmd/devstream/init.go index 5d8c5e6f0..b1d61a416 100644 --- a/cmd/devstream/init.go +++ b/cmd/devstream/init.go @@ -3,6 +3,7 @@ package main import ( "github.com/spf13/cobra" + "github.com/devstream-io/devstream/internal/pkg/completion" "github.com/devstream-io/devstream/internal/pkg/configloader" "github.com/devstream-io/devstream/internal/pkg/pluginengine" "github.com/devstream-io/devstream/internal/pkg/pluginmanager" @@ -35,4 +36,6 @@ func initCMDFunc(cmd *cobra.Command, args []string) { func init() { initCMD.Flags().StringVarP(&configFile, "config-file", "f", "config.yaml", "config file") initCMD.Flags().StringVarP(&pluginDir, "plugin-dir", "d", pluginengine.DefaultPluginDir, "plugins directory") + + completion.FlagConfigFileCompletion(initCMD) } diff --git a/cmd/devstream/main.go b/cmd/devstream/main.go index 10b88722f..9277016d4 100644 --- a/cmd/devstream/main.go +++ b/cmd/devstream/main.go @@ -1,6 +1,7 @@ package main import ( + "os" "strings" "github.com/sirupsen/logrus" @@ -17,13 +18,13 @@ var ( Short: `DevStream is an open-source DevOps toolchain manager`, Long: `DevStream is an open-source DevOps toolchain manager -###### ##### -# # ###### # # # # ##### ##### ###### ## # # -# # # # # # # # # # # # ## ## -# # ##### # # ##### # # # ##### # # # ## # -# # # # # # # ##### # ###### # # -# # # # # # # # # # # # # # # -###### ###### ## ##### # # # ###### # # # # +###### ##### +# # ###### # # # # ##### ##### ###### ## # # +# # # # # # # # # # # # ## ## +# # ##### # # ##### # # # ##### # # # ## # +# # # # # # # ##### # ###### # # +# # # # # # # # # # # # # # # +###### ###### ## ##### # # # ###### # # # # `, PersistentPreRun: func(cmd *cobra.Command, args []string) { initLog() @@ -34,6 +35,7 @@ var ( func init() { cobra.OnInitialize(initConfig) rootCMD.PersistentFlags().BoolVarP(&isDebug, "debug", "", false, "debug level log") + rootCMD.AddCommand(completionCMD(os.Stdout)) rootCMD.AddCommand(versionCMD) rootCMD.AddCommand(initCMD) rootCMD.AddCommand(applyCMD) diff --git a/cmd/devstream/show.go b/cmd/devstream/show.go index e2e65dbbd..99e7a1f2b 100644 --- a/cmd/devstream/show.go +++ b/cmd/devstream/show.go @@ -3,6 +3,7 @@ package main import ( "github.com/spf13/cobra" + "github.com/devstream-io/devstream/internal/pkg/completion" "github.com/devstream-io/devstream/internal/pkg/pluginengine" "github.com/devstream-io/devstream/internal/pkg/show/config" "github.com/devstream-io/devstream/internal/pkg/show/status" @@ -58,10 +59,12 @@ func init() { showCMD.AddCommand(showStatusCMD) showConfigCMD.Flags().StringVarP(&plugin, "plugin", "p", "", "specify name with the plugin") + completion.FlagPluginsCompletion(showConfigCMD, "plugin") showStatusCMD.Flags().StringVarP(&plugin, "plugin", "p", "", "specify name with the plugin") showStatusCMD.Flags().StringVarP(&instanceID, "id", "i", "", "specify id with the plugin instance") showStatusCMD.Flags().BoolVarP(&statusAllFlag, "all", "a", false, "show all instances of all plugins status") showStatusCMD.Flags().StringVarP(&pluginDir, "plugin-dir", "d", pluginengine.DefaultPluginDir, "plugins directory") showStatusCMD.Flags().StringVarP(&configFile, "config-file", "f", "config.yaml", "config file") + completion.FlagPluginsCompletion(showStatusCMD, "plugin") } diff --git a/cmd/devstream/verify.go b/cmd/devstream/verify.go index 03a363036..b0bec3050 100644 --- a/cmd/devstream/verify.go +++ b/cmd/devstream/verify.go @@ -3,6 +3,7 @@ package main import ( "github.com/spf13/cobra" + "github.com/devstream-io/devstream/internal/pkg/completion" "github.com/devstream-io/devstream/internal/pkg/pluginengine" "github.com/devstream-io/devstream/pkg/util/log" ) @@ -26,4 +27,6 @@ func verifyCMDFunc(cmd *cobra.Command, args []string) { func init() { verifyCMD.Flags().StringVarP(&configFile, "config-file", "f", "config.yaml", "config file") verifyCMD.Flags().StringVarP(&pluginDir, "plugin-dir", "d", pluginengine.DefaultPluginDir, "plugins directory") + + completion.FlagConfigFileCompletion(verifyCMD) } diff --git a/internal/pkg/completion/completion.go b/internal/pkg/completion/completion.go new file mode 100644 index 000000000..ff27de0f0 --- /dev/null +++ b/internal/pkg/completion/completion.go @@ -0,0 +1,100 @@ +package completion + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/spf13/cobra" + + "github.com/devstream-io/devstream/cmd/devstream/list" + "github.com/devstream-io/devstream/pkg/util/log" +) + +func FlagPluginsCompletion(cmd *cobra.Command, flag string) { + if err := cmd.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return list.PluginsNameSlice(), cobra.ShellCompDirectiveDefault + }); err != nil { + log.Warn(err) + } +} + +func FlagConfigFileCompletion(cmd *cobra.Command) { + validConfigFilenames := []string{"yaml"} + + if err := cmd.Flags().SetAnnotation("config-file", cobra.BashCompFilenameExt, validConfigFilenames); err != nil { + log.Warn(err) + } +} + +func CompletionBash(out io.Writer, cmd *cobra.Command) error { + err := cmd.Root().GenBashCompletion(out) + + // The default binary name downloaded from the Releases page is dtm-{os}-amd64 + // solve the problem that autocompletion fails when the name of the binary is not dtm + if binary := filepath.Base(os.Args[0]); binary != "dtm" { + renamedBinary := ` +# the user renamed the dtm binary +if [[ $(type -t compopt) = "builtin" ]]; then + complete -o default -F __start_dtm %[1]s +else + complete -o default -o nospace -F __start_dtm %[1]s +fi +` + fmt.Fprintf(out, renamedBinary, binary) + } + + return err +} + +func CompletionZsh(out io.Writer, cmd *cobra.Command) error { + err := cmd.Root().GenZshCompletionNoDesc(out) + + // The default binary name downloaded from the Releases page is dtm-{os}-amd64 + // solve the problem that autocompletion fails when the name of the binary is not dtm + if binary := filepath.Base(os.Args[0]); binary != "dtm" { + renamedBinary := ` +# the user renamed the dtm binary +compdef _dtm %[1]s +` + fmt.Fprintf(out, renamedBinary, binary) + } + + fmt.Fprintf(out, "compdef _dtm dtm") + + return err +} + +func BashExample(binary string) string { + return fmt.Sprintf(`Load is completions in the current shell session: +# source <(%s completion bash) + +Load completions for every new session: +(in Linux)# %s completion bash > /etc/bash_completion.d/dtm +(in MacOS)# %s completion bash > $(brew --prefix)/etc/bash_completion.d/dtm`, binary, binary, binary) +} + +func ZshExample(binary string) string { + return fmt.Sprintf(`Load is completions in the current shell session: +# source <(%s completion zsh) + +Load completions for every new session: +# %s completion zsh > "${fpath[1]}/_dtm"`, binary, binary) +} + +func FishExample(binary string) string { + return fmt.Sprintf(`Load is completions in the current shell session: +# %s completion fish | source + +Load completions for every new session: +# %s completion fish > ~/.config/fish/completions/dtm.fish`, binary, binary) +} + +func PowershellExample(binary string) string { + return fmt.Sprintf(`Load is completions in the current shell session: +C:\> %s completion powershell | Out-String | Invoke-Expression + +Load completions for every new session: +add the output of the above command to powershell profile.`, binary) +}