Skip to content

Commit d2fe889

Browse files
committed
Fix completions install on silicon macs
1 parent 5f22c35 commit d2fe889

File tree

2 files changed

+220
-7
lines changed

2 files changed

+220
-7
lines changed

pkg/cmd/completion.go

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ var completionInstallCmd = &cobra.Command{
7373
var buf bytes.Buffer
7474
var path string
7575
var name string
76+
// Track if user is using oh-my-zsh meaning no zshrc modification is needed
77+
var usingOhMyZsh bool
7678

7779
if utils.IsWindows() {
7880
utils.HandleError(errors.New("Completion files are not supported on Windows. You can use completion files with Windows Subsystem for Linux (WSL)."))
@@ -94,15 +96,32 @@ var completionInstallCmd = &cobra.Command{
9496
}
9597
// zsh completions file start with an underscore
9698
name = "_doppler"
97-
path = "/usr/local/share/zsh/site-functions"
99+
// Use appropriate zsh completions path based on OS and oh-my-zsh use
100+
if utils.IsMacOS() {
101+
homeDir, err := os.UserHomeDir()
102+
if err != nil {
103+
utils.HandleError(err, "Unable to determine home directory")
104+
}
105+
// Check if oh-my-zsh is installed - it auto-adds ~/.oh-my-zsh/completions to fpath
106+
omzDir := fmt.Sprintf("%s/.oh-my-zsh", homeDir)
107+
if utils.Exists(omzDir) {
108+
path = fmt.Sprintf("%s/.oh-my-zsh/completions", homeDir)
109+
usingOhMyZsh = true
110+
} else {
111+
// Fall back to ~/.zsh/completions for non-oh-my-zsh users
112+
path = fmt.Sprintf("%s/.zsh/completions", homeDir)
113+
}
114+
} else {
115+
path = "/usr/local/share/zsh/site-functions"
116+
}
98117
} else {
99118
utils.HandleError(errors.New("Your shell is not supported"))
100119
}
101120

102-
// create directory if it doesn't exist
121+
// create directory and intermediate directories if they don't exist
103122
if !utils.Exists(path) {
104123
// using 755 to mimic expected /etc/ perms
105-
err := os.Mkdir(path, 0755) // #nosec G301
124+
err := os.MkdirAll(path, 0755) // #nosec G301
106125
if err != nil {
107126
utils.HandleError(err, "Unable to write completion file")
108127
}
@@ -114,12 +133,27 @@ var completionInstallCmd = &cobra.Command{
114133
utils.HandleError(err, "Unable to write completion file")
115134
}
116135

136+
// For non-oh-my-zsh MacOS zsh users, update.zshrc to add completions path to fpath
137+
if strings.HasSuffix(shell, "/zsh") && utils.IsMacOS() && !usingOhMyZsh {
138+
if err := updateZshrcForCompletions(path); err != nil {
139+
utils.HandleError(err, "Unable to update .zshrc with completions path")
140+
}
141+
}
142+
117143
utils.Print("Your shell has been configured for Doppler CLI completions! Restart your shell to apply.")
118144
utils.Print("")
119-
if utils.IsMacOS() {
120-
utils.Print("Note: The homebrew 'bash-completion' package is required for completions to work. See https://docs.brew.sh/Shell-Completion for more info.")
121-
} else {
122-
utils.Print("Note: The 'bash-completion' package is required for completions to work. See https://github.com/scop/bash-completion for more info.")
145+
if strings.HasSuffix(shell, "/zsh") {
146+
if usingOhMyZsh {
147+
utils.Print("Completions installed to your oh-my-zsh completions directory.")
148+
} else if utils.IsMacOS() {
149+
utils.Print("Your .zshrc has been updated to include the completions path.")
150+
}
151+
} else if strings.HasSuffix(shell, "/bash") {
152+
if utils.IsMacOS() {
153+
utils.Print("Note: The homebrew 'bash-completion' package is required for completions to work. See https://docs.brew.sh/Shell-Completion for more info.")
154+
} else {
155+
utils.Print("Note: The 'bash-completion' package is required for completions to work. See https://github.com/scop/bash-completion for more info.")
156+
}
123157
}
124158
},
125159
}
@@ -141,6 +175,54 @@ func getShell(args []string) string {
141175
return shell
142176
}
143177

178+
// Adds the completions path to fpath in ~/.zshrc if not already present
179+
func updateZshrcForCompletions(completionsPath string) error {
180+
homeDir, err := os.UserHomeDir()
181+
if err != nil {
182+
return err
183+
}
184+
zshrcPath := fmt.Sprintf("%s/.zshrc", homeDir)
185+
fpathLine := fmt.Sprintf("fpath=(%s $fpath)", completionsPath)
186+
187+
// Read existing .zshrc content if it exists
188+
var existingContent string
189+
if utils.Exists(zshrcPath) {
190+
contentBytes, err := os.ReadFile(zshrcPath)
191+
if err != nil {
192+
return err
193+
}
194+
existingContent = string(contentBytes)
195+
}
196+
197+
// Check if fpath is already configured for this path
198+
if strings.Contains(existingContent, completionsPath) {
199+
utils.LogDebug("Completions path already configured in .zshrc")
200+
return nil
201+
}
202+
203+
// Append fpath configuration to .zshrc
204+
f, err := os.OpenFile(zshrcPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
205+
if err != nil {
206+
return err
207+
}
208+
defer f.Close()
209+
210+
// Add newline if file doesn't end with one
211+
if len(existingContent) > 0 && !strings.HasSuffix(existingContent, "\n") {
212+
if _, err := f.WriteString("\n"); err != nil {
213+
return err
214+
}
215+
}
216+
217+
// Write the fpath configuration block
218+
configBlock := fmt.Sprintf("\n# Doppler CLI completions\n%s\nautoload -Uz compinit && compinit\n", fpathLine)
219+
if _, err := f.WriteString(configBlock); err != nil {
220+
return err
221+
}
222+
223+
return nil
224+
}
225+
144226
func init() {
145227
rootCmd.AddCommand(completionCmd)
146228
completionCmd.AddCommand(completionInstallCmd)

tests/e2e/completion-install.sh

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/bin/bash
2+
3+
# E2E tests for zsh completion installation on macOS
4+
5+
set -euo pipefail
6+
7+
TEST_NAME="completion install"
8+
TEMP_HOME=""
9+
10+
cleanup() {
11+
exit_code=$?
12+
if [ "$exit_code" -ne 0 ]; then
13+
echo "ERROR: '$TEST_NAME' tests failed during execution"
14+
fi
15+
# Clean up temp directories
16+
if [ -n "$TEMP_HOME" ] && [ -d "$TEMP_HOME" ]; then
17+
rm -rf "$TEMP_HOME"
18+
fi
19+
exit "$exit_code"
20+
}
21+
trap cleanup EXIT
22+
trap cleanup INT
23+
24+
beforeAll() {
25+
echo "INFO: Executing '$TEST_NAME' tests"
26+
}
27+
28+
beforeEach() {
29+
# Clean up any previous temp home
30+
if [ -n "$TEMP_HOME" ] && [ -d "$TEMP_HOME" ]; then
31+
rm -rf "$TEMP_HOME"
32+
fi
33+
TEMP_HOME=$(mktemp -d)
34+
}
35+
36+
afterAll() {
37+
echo "INFO: Completed '$TEST_NAME' tests"
38+
if [ -n "$TEMP_HOME" ] && [ -d "$TEMP_HOME" ]; then
39+
rm -rf "$TEMP_HOME"
40+
fi
41+
}
42+
43+
error() {
44+
message=$1
45+
echo "ERROR: $message"
46+
exit 1
47+
}
48+
49+
beforeAll
50+
51+
# Test 1 - zsh without oh-my-zsh creates ~/.zsh/completions and updates .zshrc
52+
beforeEach
53+
echo "Testing: zsh without oh-my-zsh"
54+
HOME="$TEMP_HOME" SHELL="/bin/zsh" "$DOPPLER_BINARY" completion install --silent
55+
56+
# Verify completion file was created
57+
[[ -f "$TEMP_HOME/.zsh/completions/_doppler" ]] || error "completion file not created at ~/.zsh/completions/_doppler"
58+
59+
# Verify .zshrc was created and contains fpath
60+
[[ -f "$TEMP_HOME/.zshrc" ]] || error ".zshrc was not created"
61+
grep -q "fpath=.*\.zsh/completions" "$TEMP_HOME/.zshrc" || error "fpath not added to .zshrc"
62+
grep -Fq "autoload -Uz compinit && compinit" "$TEMP_HOME/.zshrc" || error "compinit not added to .zshrc"
63+
grep -q "# Doppler CLI completions" "$TEMP_HOME/.zshrc" || error "Doppler comment not added to .zshrc"
64+
65+
echo "PASS: zsh without oh-my-zsh"
66+
67+
# Test 2 - zsh with oh-my-zsh uses oh-my-zsh completions directory
68+
beforeEach
69+
echo "Testing: zsh with oh-my-zsh"
70+
mkdir -p "$TEMP_HOME/.oh-my-zsh"
71+
HOME="$TEMP_HOME" SHELL="/bin/zsh" "$DOPPLER_BINARY" completion install --silent
72+
73+
# Verify completion file was created in oh-my-zsh directory
74+
[[ -f "$TEMP_HOME/.oh-my-zsh/completions/_doppler" ]] || error "completion file not created at ~/.oh-my-zsh/completions/_doppler"
75+
76+
# Verify .zshrc was NOT created (oh-my-zsh handles fpath automatically)
77+
[[ ! -f "$TEMP_HOME/.zshrc" ]] || error ".zshrc should not be created/modified for oh-my-zsh users"
78+
79+
echo "PASS: zsh with oh-my-zsh"
80+
81+
# Test 3 - idempotent (running twice doesn't duplicate .zshrc entries)
82+
beforeEach
83+
echo "Testing: idempotent installation"
84+
HOME="$TEMP_HOME" SHELL="/bin/zsh" "$DOPPLER_BINARY" completion install --silent
85+
HOME="$TEMP_HOME" SHELL="/bin/zsh" "$DOPPLER_BINARY" completion install --silent
86+
87+
# Count occurrences of the Doppler comment block
88+
count=$(grep -c "# Doppler CLI completions" "$TEMP_HOME/.zshrc" || echo "0")
89+
[[ "$count" -eq 1 ]] || error ".zshrc has duplicate Doppler entries (found $count, expected 1)"
90+
91+
echo "PASS: idempotent installation"
92+
93+
# Test 4 - preserves existing .zshrc content
94+
beforeEach
95+
echo "Testing: preserves existing .zshrc"
96+
echo "# My custom zsh config" > "$TEMP_HOME/.zshrc"
97+
echo "export MY_VAR=hello" >> "$TEMP_HOME/.zshrc"
98+
HOME="$TEMP_HOME" SHELL="/bin/zsh" "$DOPPLER_BINARY" completion install --silent
99+
100+
# Verify original content is preserved
101+
grep -q "# My custom zsh config" "$TEMP_HOME/.zshrc" || error "original .zshrc content was not preserved"
102+
grep -q "export MY_VAR=hello" "$TEMP_HOME/.zshrc" || error "original .zshrc export was not preserved"
103+
104+
# Verify new content was added
105+
grep -q "fpath=.*\.zsh/completions" "$TEMP_HOME/.zshrc" || error "fpath not appended to existing .zshrc"
106+
107+
echo "PASS: preserves existing .zshrc"
108+
109+
# Test 5 - bash completions still work (regression test)
110+
beforeEach
111+
echo "Testing: bash completions"
112+
HOME="$TEMP_HOME" SHELL="/bin/bash" "$DOPPLER_BINARY" completion install --silent || true
113+
114+
# On non-macOS, this will try to write to /etc/bash_completion.d which may fail
115+
# On macOS, it will try /usr/local/etc/bash_completion.d
116+
# The important thing is that it doesn't crash or try to modify .zshrc
117+
[[ ! -f "$TEMP_HOME/.zshrc" ]] || error ".zshrc should not be created for bash users"
118+
119+
echo "PASS: bash completions (did not modify .zshrc)"
120+
121+
# Test 6 - completion file contains valid zsh completion code
122+
beforeEach
123+
echo "Testing: completion file content"
124+
HOME="$TEMP_HOME" SHELL="/bin/zsh" "$DOPPLER_BINARY" completion install --silent
125+
126+
# Verify the completion file contains expected zsh completion directives
127+
grep -q "compdef" "$TEMP_HOME/.zsh/completions/_doppler" || grep -q "#compdef" "$TEMP_HOME/.zsh/completions/_doppler" || error "completion file doesn't contain valid zsh completion directives"
128+
129+
echo "PASS: completion file content"
130+
131+
afterAll

0 commit comments

Comments
 (0)