Skip to content

Commit cdfb6e9

Browse files
committed
Implement environment variable management in autostart functionality
- Added methods to discover and set environment paths for Homebrew, Node.js, and Python installations. - Created plist files for main application and environment setup, ensuring global environment variables are set at login. - Enhanced autostart manager to handle loading and unloading of environment setup agents. - Updated existing plist to include environment variables for better integration.
1 parent 12a7673 commit cdfb6e9

2 files changed

Lines changed: 337 additions & 13 deletions

File tree

internal/tray/autostart.go

Lines changed: 323 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,43 @@ const (
3333
<string>%s/main-error.log</string>
3434
<key>WorkingDirectory</key>
3535
<string>%s</string>
36+
<key>EnvironmentVariables</key>
37+
<dict>
38+
<key>PATH</key>
39+
<string>%s</string>
40+
<key>HOME</key>
41+
<string>%s</string>
42+
<key>SHELL</key>
43+
<string>%s</string>
44+
<key>HOMEBREW_PREFIX</key>
45+
<string>%s</string>
46+
<key>HOMEBREW_CELLAR</key>
47+
<string>%s</string>
48+
<key>HOMEBREW_REPOSITORY</key>
49+
<string>%s</string>
50+
</dict>
51+
</dict>
52+
</plist>`
53+
54+
// Environment setup launch agent that runs at login to set global environment variables
55+
envSetupAgentTemplate = `<?xml version="1.0" encoding="UTF-8"?>
56+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
57+
<plist version="1.0">
58+
<dict>
59+
<key>Label</key>
60+
<string>com.smartmcpproxy.environment</string>
61+
<key>ProgramArguments</key>
62+
<array>
63+
<string>sh</string>
64+
<string>-c</string>
65+
<string>%s</string>
66+
</array>
67+
<key>RunAtLoad</key>
68+
<true/>
69+
<key>StandardOutPath</key>
70+
<string>%s/env-setup.log</string>
71+
<key>StandardErrorPath</key>
72+
<string>%s/env-setup-error.log</string>
3673
</dict>
3774
</plist>`
3875
)
@@ -72,6 +109,214 @@ func NewAutostartManager() (*AutostartManager, error) {
72109
}, nil
73110
}
74111

112+
// discoverEnvironmentPaths discovers common tool installation paths
113+
func (m *AutostartManager) discoverEnvironmentPaths() (string, map[string]string) {
114+
homeDir, _ := os.UserHomeDir()
115+
116+
// Start with essential system paths
117+
paths := []string{
118+
"/usr/bin",
119+
"/bin",
120+
"/usr/sbin",
121+
"/sbin",
122+
}
123+
124+
// Discover Homebrew paths
125+
brewPaths := m.discoverBrewPaths()
126+
paths = append(paths, brewPaths...)
127+
128+
// Discover Node.js/npm paths
129+
nodePaths := m.discoverNodePaths()
130+
paths = append(paths, nodePaths...)
131+
132+
// Discover Python/uvx/pipx paths
133+
pythonPaths := m.discoverPythonPaths()
134+
paths = append(paths, pythonPaths...)
135+
136+
// Discover other common tool paths
137+
commonPaths := []string{
138+
"/usr/local/bin",
139+
filepath.Join(homeDir, ".local", "bin"),
140+
filepath.Join(homeDir, ".cargo", "bin"),
141+
filepath.Join(homeDir, "go", "bin"),
142+
"/usr/local/go/bin",
143+
}
144+
145+
// Filter existing paths
146+
var validPaths []string
147+
for _, path := range append(paths, commonPaths...) {
148+
if m.pathExists(path) && !m.containsPath(validPaths, path) {
149+
validPaths = append(validPaths, path)
150+
}
151+
}
152+
153+
// Build environment variables
154+
envVars := make(map[string]string)
155+
156+
// Set Homebrew variables if available
157+
if brewPrefix := m.getBrewPrefix(); brewPrefix != "" {
158+
envVars["HOMEBREW_PREFIX"] = brewPrefix
159+
envVars["HOMEBREW_CELLAR"] = filepath.Join(brewPrefix, "Cellar")
160+
envVars["HOMEBREW_REPOSITORY"] = brewPrefix
161+
}
162+
163+
return strings.Join(validPaths, ":"), envVars
164+
}
165+
166+
// discoverBrewPaths discovers Homebrew installation paths
167+
func (m *AutostartManager) discoverBrewPaths() []string {
168+
var paths []string
169+
170+
// Try common Homebrew locations
171+
brewPaths := []string{
172+
"/opt/homebrew/bin", // Apple Silicon
173+
"/opt/homebrew/sbin",
174+
"/usr/local/bin", // Intel
175+
"/usr/local/sbin",
176+
"/home/linuxbrew/.linuxbrew/bin", // Linux (just in case)
177+
}
178+
179+
for _, path := range brewPaths {
180+
if m.pathExists(path) {
181+
paths = append(paths, path)
182+
}
183+
}
184+
185+
return paths
186+
}
187+
188+
// discoverNodePaths discovers Node.js/npm/npx paths
189+
func (m *AutostartManager) discoverNodePaths() []string {
190+
homeDir, _ := os.UserHomeDir()
191+
var paths []string
192+
193+
// Check for nvm installations
194+
nvmDir := filepath.Join(homeDir, ".nvm", "versions", "node")
195+
if entries, err := os.ReadDir(nvmDir); err == nil {
196+
for _, entry := range entries {
197+
if entry.IsDir() {
198+
binPath := filepath.Join(nvmDir, entry.Name(), "bin")
199+
if m.pathExists(binPath) {
200+
paths = append(paths, binPath)
201+
}
202+
}
203+
}
204+
}
205+
206+
// Check for global npm installations
207+
npmGlobalPaths := []string{
208+
filepath.Join(homeDir, ".npm-global", "bin"),
209+
filepath.Join(homeDir, ".npm-packages", "bin"),
210+
}
211+
212+
for _, path := range npmGlobalPaths {
213+
if m.pathExists(path) {
214+
paths = append(paths, path)
215+
}
216+
}
217+
218+
return paths
219+
}
220+
221+
// discoverPythonPaths discovers Python/uvx/pipx paths
222+
func (m *AutostartManager) discoverPythonPaths() []string {
223+
homeDir, _ := os.UserHomeDir()
224+
var paths []string
225+
226+
// Check for pipx installations
227+
pipxPaths := []string{
228+
filepath.Join(homeDir, ".local", "bin"),
229+
filepath.Join(homeDir, ".pipx", "bin"),
230+
}
231+
232+
// Check for uv installations
233+
uvPaths := []string{
234+
filepath.Join(homeDir, ".cargo", "bin"), // uv is often installed via cargo
235+
filepath.Join(homeDir, ".local", "bin"),
236+
}
237+
238+
// Check for pyenv installations
239+
pyenvPath := filepath.Join(homeDir, ".pyenv", "bin")
240+
if m.pathExists(pyenvPath) {
241+
paths = append(paths, pyenvPath)
242+
}
243+
244+
for _, path := range append(pipxPaths, uvPaths...) {
245+
if m.pathExists(path) {
246+
paths = append(paths, path)
247+
}
248+
}
249+
250+
return paths
251+
}
252+
253+
// getBrewPrefix gets the Homebrew prefix
254+
func (m *AutostartManager) getBrewPrefix() string {
255+
// Try to get from brew command if available
256+
if cmd := exec.Command("brew", "--prefix"); cmd.Err == nil {
257+
if output, err := cmd.Output(); err == nil {
258+
return strings.TrimSpace(string(output))
259+
}
260+
}
261+
262+
// Fallback to common locations
263+
commonPrefixes := []string{
264+
"/opt/homebrew", // Apple Silicon
265+
"/usr/local", // Intel
266+
"/home/linuxbrew/.linuxbrew", // Linux
267+
}
268+
269+
for _, prefix := range commonPrefixes {
270+
if m.pathExists(filepath.Join(prefix, "bin", "brew")) {
271+
return prefix
272+
}
273+
}
274+
275+
return ""
276+
}
277+
278+
// pathExists checks if a path exists
279+
func (m *AutostartManager) pathExists(path string) bool {
280+
_, err := os.Stat(path)
281+
return err == nil
282+
}
283+
284+
// containsPath checks if a path is already in the slice
285+
func (m *AutostartManager) containsPath(paths []string, path string) bool {
286+
for _, p := range paths {
287+
if p == path {
288+
return true
289+
}
290+
}
291+
return false
292+
}
293+
294+
// buildEnvironmentSetupScript builds the script for setting up global environment variables
295+
func (m *AutostartManager) buildEnvironmentSetupScript() string {
296+
discoveredPath, envVars := m.discoverEnvironmentPaths()
297+
298+
var script strings.Builder
299+
300+
// Set PATH
301+
script.WriteString(fmt.Sprintf("launchctl setenv PATH \"%s\";\n", discoveredPath))
302+
303+
// Set other environment variables
304+
for key, value := range envVars {
305+
script.WriteString(fmt.Sprintf("launchctl setenv %s \"%s\";\n", key, value))
306+
}
307+
308+
// Set HOME and SHELL
309+
if homeDir, err := os.UserHomeDir(); err == nil {
310+
script.WriteString(fmt.Sprintf("launchctl setenv HOME \"%s\";\n", homeDir))
311+
}
312+
313+
if shell := os.Getenv("SHELL"); shell != "" {
314+
script.WriteString(fmt.Sprintf("launchctl setenv SHELL \"%s\";\n", shell))
315+
}
316+
317+
return script.String()
318+
}
319+
75320
// IsEnabled checks if autostart is currently enabled
76321
func (m *AutostartManager) IsEnabled() bool {
77322
if runtime.GOOS != osDarwin {
@@ -110,25 +355,77 @@ func (m *AutostartManager) Enable() error {
110355
return fmt.Errorf("failed to create log directory: %w", err)
111356
}
112357

113-
// Create plist content
358+
// Discover environment paths and variables
359+
discoveredPath, envVars := m.discoverEnvironmentPaths()
360+
361+
// Get environment variable values with defaults
362+
shell := os.Getenv("SHELL")
363+
if shell == "" {
364+
shell = "/bin/zsh"
365+
}
366+
367+
brewPrefix := envVars["HOMEBREW_PREFIX"]
368+
if brewPrefix == "" {
369+
brewPrefix = "/opt/homebrew"
370+
}
371+
372+
brewCellar := envVars["HOMEBREW_CELLAR"]
373+
if brewCellar == "" {
374+
brewCellar = filepath.Join(brewPrefix, "Cellar")
375+
}
376+
377+
brewRepository := envVars["HOMEBREW_REPOSITORY"]
378+
if brewRepository == "" {
379+
brewRepository = brewPrefix
380+
}
381+
382+
// Create main application plist content
114383
plistContent := fmt.Sprintf(launchAgentTemplate,
115384
m.executablePath,
116385
m.logDir,
117386
m.logDir,
118-
m.workingDir)
387+
m.workingDir,
388+
discoveredPath,
389+
homeDir,
390+
shell,
391+
brewPrefix,
392+
brewCellar,
393+
brewRepository,
394+
)
119395

120-
// Write plist file
396+
// Write main application plist file
121397
plistPath := filepath.Join(launchAgentsDir, "com.smartmcpproxy.mcpproxy.plist")
122398
if err := os.WriteFile(plistPath, []byte(plistContent), 0600); err != nil {
123399
return fmt.Errorf("failed to write plist file: %w", err)
124400
}
125401

126-
// Load the launch agent
127-
cmd := exec.Command("launchctl", "load", "-w", plistPath)
402+
// Create environment setup plist for global environment variables
403+
envScript := m.buildEnvironmentSetupScript()
404+
envPlistContent := fmt.Sprintf(envSetupAgentTemplate,
405+
envScript,
406+
m.logDir,
407+
m.logDir,
408+
)
409+
410+
// Write environment setup plist file
411+
envPlistPath := filepath.Join(launchAgentsDir, "com.smartmcpproxy.environment.plist")
412+
if err := os.WriteFile(envPlistPath, []byte(envPlistContent), 0600); err != nil {
413+
return fmt.Errorf("failed to write environment plist file: %w", err)
414+
}
415+
416+
// Load the environment setup agent first
417+
cmd := exec.Command("launchctl", "load", "-w", envPlistPath)
418+
if output, err := cmd.CombinedOutput(); err != nil {
419+
if !strings.Contains(string(output), "already loaded") {
420+
return fmt.Errorf("failed to load environment setup agent: %w, output: %s", err, output)
421+
}
422+
}
423+
424+
// Load the main application launch agent
425+
cmd = exec.Command("launchctl", "load", "-w", plistPath)
128426
if output, err := cmd.CombinedOutput(); err != nil {
129-
// If already loaded, that's okay
130427
if !strings.Contains(string(output), "already loaded") {
131-
return fmt.Errorf("failed to load launch agent: %w, output: %s", err, output)
428+
return fmt.Errorf("failed to load main launch agent: %w, output: %s", err, output)
132429
}
133430
}
134431

@@ -146,21 +443,35 @@ func (m *AutostartManager) Disable() error {
146443
return fmt.Errorf("failed to get home directory: %w", err)
147444
}
148445

446+
// Paths to both plist files
149447
plistPath := filepath.Join(homeDir, "Library", "LaunchAgents", "com.smartmcpproxy.mcpproxy.plist")
448+
envPlistPath := filepath.Join(homeDir, "Library", "LaunchAgents", "com.smartmcpproxy.environment.plist")
150449

151-
// Unload the launch agent if it exists
450+
// Unload and remove main application launch agent
152451
if _, err := os.Stat(plistPath); err == nil {
153452
cmd := exec.Command("launchctl", "unload", "-w", plistPath)
154453
if output, err := cmd.CombinedOutput(); err != nil {
155-
// If not loaded, that's okay
156454
if !strings.Contains(string(output), "not loaded") {
157-
return fmt.Errorf("failed to unload launch agent: %w, output: %s", err, output)
455+
return fmt.Errorf("failed to unload main launch agent: %w, output: %s", err, output)
158456
}
159457
}
160458

161-
// Remove the plist file
162459
if err := os.Remove(plistPath); err != nil {
163-
return fmt.Errorf("failed to remove plist file: %w", err)
460+
return fmt.Errorf("failed to remove main plist file: %w", err)
461+
}
462+
}
463+
464+
// Unload and remove environment setup launch agent
465+
if _, err := os.Stat(envPlistPath); err == nil {
466+
cmd := exec.Command("launchctl", "unload", "-w", envPlistPath)
467+
if output, err := cmd.CombinedOutput(); err != nil {
468+
if !strings.Contains(string(output), "not loaded") {
469+
return fmt.Errorf("failed to unload environment setup agent: %w, output: %s", err, output)
470+
}
471+
}
472+
473+
if err := os.Remove(envPlistPath); err != nil {
474+
return fmt.Errorf("failed to remove environment plist file: %w", err)
164475
}
165476
}
166477

0 commit comments

Comments
 (0)