From bcf7f083e482300ee5a7b628e5402590b9215c29 Mon Sep 17 00:00:00 2001 From: Austin Moses Date: Tue, 6 Jan 2026 10:48:30 -0700 Subject: [PATCH] Fix completions install on silicon macs --- pkg/cmd/completion.go | 70 +++++++++++++++++--- tests/e2e.sh | 1 + tests/e2e/completion-install.sh | 114 ++++++++++++++++++++++++++++++++ 3 files changed, 177 insertions(+), 8 deletions(-) create mode 100755 tests/e2e/completion-install.sh diff --git a/pkg/cmd/completion.go b/pkg/cmd/completion.go index 47112a1b..9931c075 100644 --- a/pkg/cmd/completion.go +++ b/pkg/cmd/completion.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "os" + "path/filepath" "strings" "github.com/DopplerHQ/cli/pkg/utils" @@ -73,6 +74,8 @@ var completionInstallCmd = &cobra.Command{ var buf bytes.Buffer var path string var name string + // Track if using fallback path (requires manual fpath configuration) + var usingFallbackPath bool if utils.IsWindows() { utils.HandleError(errors.New("Completion files are not supported on Windows. You can use completion files with Windows Subsystem for Linux (WSL).")) @@ -94,15 +97,31 @@ var completionInstallCmd = &cobra.Command{ } // zsh completions file start with an underscore name = "_doppler" - path = "/usr/local/share/zsh/site-functions" + // Try standard path first, fall back to user directory if not writable + standardPath := "/usr/local/share/zsh/site-functions" + if isDirectoryWritable(standardPath) { + path = standardPath + } else { + // Fall back to XDG_DATA_HOME/doppler or ~/.local/share/doppler + dataHome := os.Getenv("XDG_DATA_HOME") + if dataHome == "" { + homeDir, err := os.UserHomeDir() + if err != nil { + utils.HandleError(err, "Unable to determine home directory") + } + dataHome = fmt.Sprintf("%s/.local/share", homeDir) + } + path = fmt.Sprintf("%s/doppler/zsh/completions", dataHome) + usingFallbackPath = true + } } else { utils.HandleError(errors.New("Your shell is not supported")) } - // create directory if it doesn't exist + // create directory and intermediate directories if they don't exist if !utils.Exists(path) { // using 755 to mimic expected /etc/ perms - err := os.Mkdir(path, 0755) // #nosec G301 + err := os.MkdirAll(path, 0755) // #nosec G301 if err != nil { utils.HandleError(err, "Unable to write completion file") } @@ -114,12 +133,18 @@ var completionInstallCmd = &cobra.Command{ utils.HandleError(err, "Unable to write completion file") } - utils.Print("Your shell has been configured for Doppler CLI completions! Restart your shell to apply.") + utils.Print("Completions have been installed. Restart your shell to apply.") utils.Print("") - if utils.IsMacOS() { - utils.Print("Note: The homebrew 'bash-completion' package is required for completions to work. See https://docs.brew.sh/Shell-Completion for more info.") - } else { - utils.Print("Note: The 'bash-completion' package is required for completions to work. See https://github.com/scop/bash-completion for more info.") + if strings.HasSuffix(shell, "/zsh") && usingFallbackPath { + utils.Print("To enable completions, add the following to your ~/.zshrc:") + utils.Print("") + utils.Print(fmt.Sprintf(" fpath=(%s $fpath)", path)) + } else if strings.HasSuffix(shell, "/bash") { + if utils.IsMacOS() { + utils.Print("Note: The homebrew 'bash-completion' package is required for completions to work. See https://docs.brew.sh/Shell-Completion for more info.") + } else { + utils.Print("Note: The 'bash-completion' package is required for completions to work. See https://github.com/scop/bash-completion for more info.") + } } }, } @@ -141,6 +166,35 @@ func getShell(args []string) string { return shell } +func isDirectoryWritable(path string) bool { + // If directory exists, check if it's writable + if info, err := os.Stat(path); err == nil { + if !info.IsDir() { + return false + } + // Try to create a temp file to test write access + testFile := fmt.Sprintf("%s/.doppler_write_test", path) + if f, err := os.Create(testFile); err == nil { + f.Close() + os.Remove(testFile) + return true + } + return false + } + + // Directory doesn't exist, check if parent is writable + parent := filepath.Dir(path) + if info, err := os.Stat(parent); err == nil && info.IsDir() { + testFile := fmt.Sprintf("%s/.doppler_write_test", parent) + if f, err := os.Create(testFile); err == nil { + f.Close() + os.Remove(testFile) + return true + } + } + return false +} + func init() { rootCmd.AddCommand(completionCmd) completionCmd.AddCommand(completionInstallCmd) diff --git a/tests/e2e.sh b/tests/e2e.sh index 83137135..b64dc6a8 100755 --- a/tests/e2e.sh +++ b/tests/e2e.sh @@ -27,6 +27,7 @@ export DOPPLER_ENABLE_VERSION_CHECK=false "$DIR/e2e/global-flags.sh" "$DIR/e2e/update.sh" "$DIR/e2e/flags.sh" +"$DIR/e2e/completion-install.sh" echo -e "\nAll tests completed successfully!" exit 0 diff --git a/tests/e2e/completion-install.sh b/tests/e2e/completion-install.sh new file mode 100755 index 00000000..9b86041a --- /dev/null +++ b/tests/e2e/completion-install.sh @@ -0,0 +1,114 @@ +#!/bin/bash + +# E2E tests for zsh completion installation + +set -euo pipefail + +TEST_NAME="completion install" +TEMP_HOME="" + +cleanup() { + exit_code=$? + if [ "$exit_code" -ne 0 ]; then + echo "ERROR: '$TEST_NAME' tests failed during execution" + fi + # Clean up temp directories + if [ -n "$TEMP_HOME" ] && [ -d "$TEMP_HOME" ]; then + rm -rf "$TEMP_HOME" + fi + exit "$exit_code" +} +trap cleanup EXIT +trap cleanup INT + +beforeAll() { + echo "INFO: Executing '$TEST_NAME' tests" +} + +beforeEach() { + # Clean up any previous temp home + if [ -n "$TEMP_HOME" ] && [ -d "$TEMP_HOME" ]; then + rm -rf "$TEMP_HOME" + fi + TEMP_HOME=$(mktemp -d) +} + +afterAll() { + echo "INFO: Completed '$TEST_NAME' tests" + if [ -n "$TEMP_HOME" ] && [ -d "$TEMP_HOME" ]; then + rm -rf "$TEMP_HOME" + fi +} + +error() { + message=$1 + echo "ERROR: $message" + exit 1 +} + +beforeAll + +# Test 1 - zsh uses fallback path when standard path is not writable +beforeEach +echo "Testing: zsh fallback to XDG_DATA_HOME/doppler/zsh/completions" +HOME="$TEMP_HOME" SHELL="/bin/zsh" "$DOPPLER_BINARY" completion install --silent + +# Verify completion file was created in XDG-compliant directory (defaults to ~/.local/share) +[[ -f "$TEMP_HOME/.local/share/doppler/zsh/completions/_doppler" ]] || error "completion file not created at ~/.local/share/doppler/zsh/completions/_doppler" + +echo "PASS: zsh fallback path" + +# Test 2 - prints fpath instructions when using fallback path +beforeEach +echo "Testing: prints fpath instructions" +output=$(HOME="$TEMP_HOME" SHELL="/bin/zsh" "$DOPPLER_BINARY" completion install 2>&1) + +# Verify instructions are printed +echo "$output" | grep -q "fpath=" || error "fpath instructions not printed" +echo "$output" | grep -q "doppler/zsh/completions" || error "doppler path not in instructions" + +echo "PASS: prints fpath instructions" + +# Test 3 - bash completions still work (regression test) +beforeEach +echo "Testing: bash completions" +HOME="$TEMP_HOME" SHELL="/bin/bash" "$DOPPLER_BINARY" completion install --silent || true + +# The important thing is that it doesn't crash or try to use zsh paths +# Bash will fail on most systems due to /etc or /usr/local/etc permissions +echo "PASS: bash completions (no crash)" + +# Test 4 - completion file contains valid zsh completion code +beforeEach +echo "Testing: completion file content" +HOME="$TEMP_HOME" SHELL="/bin/zsh" "$DOPPLER_BINARY" completion install --silent + +# Verify the completion file contains expected zsh completion directives +grep -q "compdef" "$TEMP_HOME/.local/share/doppler/zsh/completions/_doppler" || grep -q "#compdef" "$TEMP_HOME/.local/share/doppler/zsh/completions/_doppler" || error "completion file doesn't contain valid zsh completion directives" + +echo "PASS: completion file content" + +# Test 5 - directory structure is created correctly +beforeEach +echo "Testing: directory structure" +HOME="$TEMP_HOME" SHELL="/bin/zsh" "$DOPPLER_BINARY" completion install --silent + +# Verify the full directory structure exists +[[ -d "$TEMP_HOME/.local/share/doppler" ]] || error "~/.local/share/doppler directory not created" +[[ -d "$TEMP_HOME/.local/share/doppler/zsh" ]] || error "~/.local/share/doppler/zsh directory not created" +[[ -d "$TEMP_HOME/.local/share/doppler/zsh/completions" ]] || error "~/.local/share/doppler/zsh/completions directory not created" + +echo "PASS: directory structure" + +# Test 6 - respects XDG_DATA_HOME when set +beforeEach +echo "Testing: XDG_DATA_HOME is respected" +mkdir -p "$TEMP_HOME/custom-data" +HOME="$TEMP_HOME" XDG_DATA_HOME="$TEMP_HOME/custom-data" SHELL="/bin/zsh" "$DOPPLER_BINARY" completion install --silent + +# Verify completion file was created in custom XDG_DATA_HOME +[[ -f "$TEMP_HOME/custom-data/doppler/zsh/completions/_doppler" ]] || error "completion file not created in custom XDG_DATA_HOME" + +echo "PASS: XDG_DATA_HOME is respected" + +afterAll