diff --git a/cmd/daytona/config/config.go b/cmd/daytona/config/config.go index 35a9cae23c..9989751578 100644 --- a/cmd/daytona/config/config.go +++ b/cmd/daytona/config/config.go @@ -11,6 +11,7 @@ import ( "os/exec" "path/filepath" + "github.com/daytonaio/daytona/pkg/cmd/autocomplete" "github.com/google/uuid" ) @@ -51,6 +52,9 @@ func GetConfig() (*Config, error) { _, err = os.Stat(configFilePath) if os.IsNotExist(err) { + // Setup autocompletion when adding initial config + _ = autocomplete.DetectShellAndSetupAutocompletion(autocomplete.AutoCompleteCmd.Root()) + config := &Config{ Id: uuid.NewString(), DefaultIdeId: getInitialDefaultIde(), diff --git a/docs/daytona.md b/docs/daytona.md index d3478ddd68..d58d1b4c78 100644 --- a/docs/daytona.md +++ b/docs/daytona.md @@ -20,7 +20,7 @@ daytona [flags] ### SEE ALSO * [daytona api-key](daytona_api-key.md) - Api Key commands -* [daytona autocomplete](daytona_autocomplete.md) - Adds completion script for your shell enviornment +* [daytona autocomplete](daytona_autocomplete.md) - Adds a completion script for your shell environment * [daytona build](daytona_build.md) - Manage builds * [daytona code](daytona_code.md) - Open a workspace in your preferred IDE * [daytona config](daytona_config.md) - Output Daytona configuration diff --git a/docs/daytona_autocomplete.md b/docs/daytona_autocomplete.md index 68d18b9ced..2a2d749125 100644 --- a/docs/daytona_autocomplete.md +++ b/docs/daytona_autocomplete.md @@ -1,6 +1,6 @@ ## daytona autocomplete -Adds completion script for your shell enviornment +Adds a completion script for your shell environment ``` daytona autocomplete [bash|zsh|fish|powershell] [flags] diff --git a/docs/workspace_mode/daytona.md b/docs/workspace_mode/daytona.md index f2f608d6ee..2719fb1581 100644 --- a/docs/workspace_mode/daytona.md +++ b/docs/workspace_mode/daytona.md @@ -20,7 +20,7 @@ daytona [flags] ### SEE ALSO * [daytona agent](daytona_agent.md) - Start the agent process -* [daytona autocomplete](daytona_autocomplete.md) - Adds completion script for your shell enviornment +* [daytona autocomplete](daytona_autocomplete.md) - Adds a completion script for your shell environment * [daytona docs](daytona_docs.md) - Opens the Daytona documentation in your default browser. * [daytona expose](daytona_expose.md) - Expose a local port over stdout - Used by the Daytona CLI to make direct connections to the project * [daytona forward](daytona_forward.md) - Forward a port publicly via an URL diff --git a/docs/workspace_mode/daytona_autocomplete.md b/docs/workspace_mode/daytona_autocomplete.md index 37f322fe9d..4689ddb46a 100644 --- a/docs/workspace_mode/daytona_autocomplete.md +++ b/docs/workspace_mode/daytona_autocomplete.md @@ -1,6 +1,6 @@ ## daytona autocomplete -Adds completion script for your shell enviornment +Adds a completion script for your shell environment ``` daytona autocomplete [bash|zsh|fish|powershell] [flags] diff --git a/hack/docs/daytona.yaml b/hack/docs/daytona.yaml index afb2e123ba..d3fce5d42e 100644 --- a/hack/docs/daytona.yaml +++ b/hack/docs/daytona.yaml @@ -12,7 +12,7 @@ options: usage: Display the version of Daytona see_also: - daytona api-key - Api Key commands - - daytona autocomplete - Adds completion script for your shell enviornment + - daytona autocomplete - Adds a completion script for your shell environment - daytona build - Manage builds - daytona code - Open a workspace in your preferred IDE - daytona config - Output Daytona configuration diff --git a/hack/docs/daytona_autocomplete.yaml b/hack/docs/daytona_autocomplete.yaml index 9ff949504b..3d7cd97846 100644 --- a/hack/docs/daytona_autocomplete.yaml +++ b/hack/docs/daytona_autocomplete.yaml @@ -1,5 +1,5 @@ name: daytona autocomplete -synopsis: Adds completion script for your shell enviornment +synopsis: Adds a completion script for your shell environment usage: daytona autocomplete [bash|zsh|fish|powershell] [flags] inherited_options: - name: help diff --git a/hack/docs/workspace_mode/daytona.yaml b/hack/docs/workspace_mode/daytona.yaml index cf2f053a09..6ebcdd6cb7 100644 --- a/hack/docs/workspace_mode/daytona.yaml +++ b/hack/docs/workspace_mode/daytona.yaml @@ -12,7 +12,7 @@ options: usage: Display the version of Daytona see_also: - daytona agent - Start the agent process - - daytona autocomplete - Adds completion script for your shell enviornment + - daytona autocomplete - Adds a completion script for your shell environment - daytona docs - Opens the Daytona documentation in your default browser. - daytona expose - Expose a local port over stdout - Used by the Daytona CLI to make direct connections to the project - daytona forward - Forward a port publicly via an URL diff --git a/hack/docs/workspace_mode/daytona_autocomplete.yaml b/hack/docs/workspace_mode/daytona_autocomplete.yaml index d398896550..e0efb0960c 100644 --- a/hack/docs/workspace_mode/daytona_autocomplete.yaml +++ b/hack/docs/workspace_mode/daytona_autocomplete.yaml @@ -1,5 +1,5 @@ name: daytona autocomplete -synopsis: Adds completion script for your shell enviornment +synopsis: Adds a completion script for your shell environment usage: daytona autocomplete [bash|zsh|fish|powershell] [flags] inherited_options: - name: help diff --git a/pkg/cmd/autocomplete.go b/pkg/cmd/autocomplete.go deleted file mode 100644 index 6dc2fb8a8a..0000000000 --- a/pkg/cmd/autocomplete.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2024 Daytona Platforms Inc. -// SPDX-License-Identifier: Apache-2.0 - -package cmd - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/spf13/cobra" -) - -var AutoCompleteCmd = &cobra.Command{ - Use: "autocomplete [bash|zsh|fish|powershell]", - Short: "Adds completion script for your shell enviornment", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - shell := args[0] - homeDir, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("error finding user home directory: %s", err) - } - - var filePath, profilePath string - switch shell { - case "bash": - filePath = filepath.Join(homeDir, ".daytona.completion_script.bash") - profilePath = filepath.Join(homeDir, ".bashrc") - case "zsh": - filePath = filepath.Join(homeDir, ".daytona.completion_script.zsh") - profilePath = filepath.Join(homeDir, ".zshrc") - case "fish": - filePath = filepath.Join(homeDir, ".config", "fish", "daytona.completion_script.fish") - profilePath = filepath.Join(homeDir, ".config", "fish", "config.fish") - case "powershell": - filePath = filepath.Join(homeDir, "daytona.completion_script.ps1") - profilePath = filepath.Join(homeDir, "Documents", "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1") - default: - return errors.New("unsupported shell type. Please use bash, zsh, fish, or powershell") - } - - file, err := os.Create(filePath) - if err != nil { - return fmt.Errorf("error creating completion script file: %s", err) - } - defer file.Close() - - switch shell { - case "bash": - err = cmd.Root().GenBashCompletion(file) - case "zsh": - err = cmd.Root().GenZshCompletion(file) - case "fish": - err = cmd.Root().GenFishCompletion(file, true) - case "powershell": - err = cmd.Root().GenPowerShellCompletionWithDesc(file) - } - - if err != nil { - return fmt.Errorf("error generating completion script: %s", err) - } - - sourceCommand := fmt.Sprintf("\nsource %s\n", filePath) - if shell == "powershell" { - sourceCommand = fmt.Sprintf(". %s\n", filePath) - } - - alreadyPresent := false - // Read existing content from the file - profile, err := os.ReadFile(profilePath) - - if err != nil && !os.IsNotExist(err) { - fmt.Printf("error while reading profile (%s): %s\n", profilePath, err) - } - - if strings.Contains(string(profile), strings.TrimSpace(sourceCommand)) { - alreadyPresent = true - } - - if !alreadyPresent { - // Append the source command to the shell's profile file if not present - profile, err := os.OpenFile(profilePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) - if err != nil { - return fmt.Errorf("error opening profile file (%s): %s", profilePath, err) - } - defer profile.Close() - - if _, err := profile.WriteString(sourceCommand); err != nil { - return fmt.Errorf("error writing to profile file (%s): %s", profilePath, err) - } - } - - fmt.Println("Autocomplete script generated and injected successfully.") - fmt.Printf("Please source your %s profile to apply the changes or restart your terminal.\n", shell) - fmt.Printf("For manual sourcing, use: source %s\n", profilePath) - if shell == "bash" { - fmt.Println("Please make sure that you have bash-completion installed in order to get full autocompletion functionality.") - fmt.Println("On how to install bash-completion, please refer to the following link: https://www.daytona.io/docs/tools/cli/#daytona-autocomplete") - } - - return nil - }, -} diff --git a/pkg/cmd/autocomplete/autocomplete.go b/pkg/cmd/autocomplete/autocomplete.go new file mode 100644 index 0000000000..a2fa53ffd6 --- /dev/null +++ b/pkg/cmd/autocomplete/autocomplete.go @@ -0,0 +1,139 @@ +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package autocomplete + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" +) + +var supportedShells = []string{"bash", "zsh", "fish", "powershell"} + +var AutoCompleteCmd = &cobra.Command{ + Use: fmt.Sprintf("autocomplete [%s]", strings.Join(supportedShells, "|")), + Short: "Adds a completion script for your shell environment", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + shell := args[0] + + profilePath, err := SetupAutocompletionForShell(cmd.Root(), shell) + if err != nil { + return err + } + + fmt.Println("Autocomplete script generated and injected successfully.") + fmt.Printf("Please source your %s profile to apply the changes or restart your terminal.\n", shell) + fmt.Printf("For manual sourcing, use: source %s\n", profilePath) + if shell == "bash" { + fmt.Println("Please make sure that you have bash-completion installed in order to get full autocompletion functionality.") + fmt.Println("On how to install bash-completion, please refer to the following link: https://www.daytona.io/docs/tools/cli/#daytona-autocomplete") + } + + return nil + }, +} + +func DetectShellAndSetupAutocompletion(rootCmd *cobra.Command) error { + shell := os.Getenv("SHELL") + if shell == "" { + return fmt.Errorf("unable to detect the shell, please use a supported one: %s", strings.Join(supportedShells, ", ")) + } + + for _, supportedShell := range supportedShells { + if strings.Contains(shell, supportedShell) { + shell = supportedShell + break + } + } + + _, err := SetupAutocompletionForShell(rootCmd, shell) + if err != nil { + return err + } + + return nil +} + +func SetupAutocompletionForShell(rootCmd *cobra.Command, shell string) (string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("error finding user home directory: %s", err) + } + + var filePath, profilePath string + switch shell { + case "bash": + filePath = filepath.Join(homeDir, ".daytona.completion_script.bash") + profilePath = filepath.Join(homeDir, ".bashrc") + case "zsh": + filePath = filepath.Join(homeDir, ".daytona.completion_script.zsh") + profilePath = filepath.Join(homeDir, ".zshrc") + case "fish": + filePath = filepath.Join(homeDir, ".config", "fish", "daytona.completion_script.fish") + profilePath = filepath.Join(homeDir, ".config", "fish", "config.fish") + case "powershell": + filePath = filepath.Join(homeDir, "daytona.completion_script.ps1") + profilePath = filepath.Join(homeDir, "Documents", "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1") + default: + return "", errors.New("unsupported shell type. Please use bash, zsh, fish, or powershell") + } + + file, err := os.Create(filePath) + if err != nil { + return "", fmt.Errorf("error creating completion script file: %s", err) + } + defer file.Close() + + switch shell { + case "bash": + err = rootCmd.GenBashCompletion(file) + case "zsh": + err = rootCmd.GenZshCompletion(file) + case "fish": + err = rootCmd.GenFishCompletion(file, true) + case "powershell": + err = rootCmd.GenPowerShellCompletionWithDesc(file) + } + + if err != nil { + return "", fmt.Errorf("error generating completion script: %s", err) + } + + sourceCommand := fmt.Sprintf("\nsource %s\n", filePath) + if shell == "powershell" { + sourceCommand = fmt.Sprintf(". %s\n", filePath) + } + + alreadyPresent := false + // Read existing content from the file + profile, err := os.ReadFile(profilePath) + + if err != nil && !os.IsNotExist(err) { + return "", fmt.Errorf("error while reading profile (%s): %s", profilePath, err) + } + + if strings.Contains(string(profile), strings.TrimSpace(sourceCommand)) { + alreadyPresent = true + } + + if !alreadyPresent { + // Append the source command to the shell's profile file if not present + profile, err := os.OpenFile(profilePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + return "", fmt.Errorf("error opening profile file (%s): %s", profilePath, err) + } + defer profile.Close() + + if _, err := profile.WriteString(sourceCommand); err != nil { + return "", fmt.Errorf("error writing to profile file (%s): %s", profilePath, err) + } + } + + return profilePath, nil +} diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 6ea24f5fac..599ffdf23a 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -15,6 +15,7 @@ import ( "github.com/daytonaio/daytona/internal" . "github.com/daytonaio/daytona/internal/util" . "github.com/daytonaio/daytona/pkg/cmd/apikey" + . "github.com/daytonaio/daytona/pkg/cmd/autocomplete" . "github.com/daytonaio/daytona/pkg/cmd/build" . "github.com/daytonaio/daytona/pkg/cmd/containerregistry" . "github.com/daytonaio/daytona/pkg/cmd/gitprovider"