@@ -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
76321func (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