diff --git a/src/main/startup/hydrate-shell-path.test.ts b/src/main/startup/hydrate-shell-path.test.ts index 77213324903..5496fa93a8d 100644 --- a/src/main/startup/hydrate-shell-path.test.ts +++ b/src/main/startup/hydrate-shell-path.test.ts @@ -186,14 +186,30 @@ describe('mergePathSegments', () => { ) }) - it('skips segments already on PATH so re-hydration is a no-op', () => { + it('promotes shell segments already on PATH so shell ordering wins', () => { process.env.PATH = joinPath('/Users/tester/.cargo/bin', '/usr/bin') const added = mergePathSegments(['/Users/tester/.cargo/bin', '/Users/tester/.opencode/bin']) expect(added).toEqual(['/Users/tester/.opencode/bin']) expect(process.env.PATH).toBe( - joinPath('/Users/tester/.opencode/bin', '/Users/tester/.cargo/bin', '/usr/bin') + joinPath('/Users/tester/.cargo/bin', '/Users/tester/.opencode/bin', '/usr/bin') + ) + }) + + it('moves user-local shell paths ahead of packaged Homebrew fallbacks', () => { + process.env.PATH = joinPath( + '/opt/homebrew/bin', + '/Users/tester/.local/bin', + '/usr/bin', + '/bin' + ) + + const added = mergePathSegments(['/Users/tester/.local/bin', '/opt/homebrew/bin']) + + expect(added).toEqual([]) + expect(process.env.PATH).toBe( + joinPath('/Users/tester/.local/bin', '/opt/homebrew/bin', '/usr/bin', '/bin') ) }) diff --git a/src/main/startup/hydrate-shell-path.ts b/src/main/startup/hydrate-shell-path.ts index 96c22145ba2..81768bbc272 100644 --- a/src/main/startup/hydrate-shell-path.ts +++ b/src/main/startup/hydrate-shell-path.ts @@ -181,25 +181,31 @@ export function hydrateShellPath(options: HydrateOptions = {}): Promise !existing.has(segment)) + const merged = [ + ...shellSegments, + ...currentSegments.filter((segment) => !shellSegmentSet.has(segment)) + ] + const next = merged.join(delimiter) + if (next === current) { return [] } - // Why: prepend so shell-provided entries win over the hardcoded fallbacks. - // The user's rc files are the source of truth for `which`-style resolution. - process.env.PATH = [...added, ...current.split(delimiter).filter(Boolean)].join(delimiter) + // Why: shell-provided entries must win over hardcoded packaged-app fallbacks. + // A seeded fallback can point at a stale CLI while the user's shell resolves + // a healthy one from the same directory list in a different order. + process.env.PATH = next return added }