diff --git a/apps/desktop/src/fixPath.ts b/apps/desktop/src/fixPath.ts index 8853248b2..54071ba26 100644 --- a/apps/desktop/src/fixPath.ts +++ b/apps/desktop/src/fixPath.ts @@ -1,4 +1,4 @@ -import { readPathFromLoginShell } from "@t3tools/shared/shell"; +import { ensureCommonMacPaths, readPathFromLoginShell } from "@t3tools/shared/shell"; export function fixPath(): void { if (process.platform !== "darwin") return; @@ -12,4 +12,8 @@ export function fixPath(): void { } catch { // Keep inherited PATH if shell lookup fails. } + + // Ensure well-known macOS binary directories (e.g. Homebrew) are on PATH + // even when the login-shell probe fails or returns a partial result. + ensureCommonMacPaths(); } diff --git a/apps/server/src/os-jank.ts b/apps/server/src/os-jank.ts index 586aca6f7..e3bbdc697 100644 --- a/apps/server/src/os-jank.ts +++ b/apps/server/src/os-jank.ts @@ -1,6 +1,6 @@ import * as OS from "node:os"; import { Effect, Path } from "effect"; -import { readPathFromLoginShell } from "@t3tools/shared/shell"; +import { ensureCommonMacPaths, readPathFromLoginShell } from "@t3tools/shared/shell"; export function fixPath(): void { if (process.platform !== "darwin") return; @@ -14,6 +14,10 @@ export function fixPath(): void { } catch { // Silently ignore — keep default PATH } + + // Ensure well-known macOS binary directories (e.g. Homebrew) are on PATH + // even when the login-shell probe fails or returns a partial result. + ensureCommonMacPaths(); } export const expandHomePath = Effect.fn(function* (input: string) { diff --git a/packages/shared/src/shell.test.ts b/packages/shared/src/shell.test.ts index 83006988b..eef3fbfda 100644 --- a/packages/shared/src/shell.test.ts +++ b/packages/shared/src/shell.test.ts @@ -1,6 +1,6 @@ -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; -import { extractPathFromShellOutput, readPathFromLoginShell } from "./shell"; +import { COMMON_MACOS_PATHS, ensureCommonMacPaths, extractPathFromShellOutput, readPathFromLoginShell } from "./shell"; describe("extractPathFromShellOutput", () => { it("extracts the path between capture markers", () => { @@ -53,3 +53,61 @@ describe("readPathFromLoginShell", () => { expect(options).toEqual({ encoding: "utf8", timeout: 5000 }); }); }); + +describe("ensureCommonMacPaths", () => { + const originalPlatform = process.platform; + const originalPath = process.env.PATH; + + afterEach(() => { + Object.defineProperty(process, "platform", { value: originalPlatform }); + process.env.PATH = originalPath; + }); + + it("appends missing Homebrew paths on darwin", () => { + Object.defineProperty(process, "platform", { value: "darwin" }); + process.env.PATH = "/usr/bin:/bin"; + + ensureCommonMacPaths(); + + const dirs = process.env.PATH!.split(":"); + for (const p of COMMON_MACOS_PATHS) { + expect(dirs).toContain(p); + } + // Original paths are still present at the start + expect(dirs[0]).toBe("/usr/bin"); + expect(dirs[1]).toBe("/bin"); + }); + + it("does not duplicate paths already present", () => { + Object.defineProperty(process, "platform", { value: "darwin" }); + process.env.PATH = `/usr/bin:/bin:${COMMON_MACOS_PATHS.join(":")}`; + + ensureCommonMacPaths(); + + const dirs = process.env.PATH!.split(":"); + for (const p of COMMON_MACOS_PATHS) { + expect(dirs.filter((d) => d === p)).toHaveLength(1); + } + }); + + it("handles empty PATH", () => { + Object.defineProperty(process, "platform", { value: "darwin" }); + process.env.PATH = ""; + + ensureCommonMacPaths(); + + const dirs = process.env.PATH!.split(":"); + for (const p of COMMON_MACOS_PATHS) { + expect(dirs).toContain(p); + } + }); + + it("is a no-op on non-darwin platforms", () => { + Object.defineProperty(process, "platform", { value: "linux" }); + process.env.PATH = "/usr/bin:/bin"; + + ensureCommonMacPaths(); + + expect(process.env.PATH).toBe("/usr/bin:/bin"); + }); +}); diff --git a/packages/shared/src/shell.ts b/packages/shared/src/shell.ts index e6029c443..a153322a5 100644 --- a/packages/shared/src/shell.ts +++ b/packages/shared/src/shell.ts @@ -36,3 +36,31 @@ export function readPathFromLoginShell( }); return extractPathFromShellOutput(output) ?? undefined; } + +/** + * Well-known macOS binary directories that should always be on PATH + * so that tools installed via Homebrew are discoverable even when the + * app is launched from the Dock / Finder (which inherits a minimal PATH). + */ +export const COMMON_MACOS_PATHS = [ + "/opt/homebrew/bin", // Apple Silicon Homebrew + "/opt/homebrew/sbin", + "/usr/local/bin", // Intel Homebrew / user binaries + "/usr/local/sbin", +] as const; + +/** + * Append any missing well-known macOS binary directories to + * `process.env.PATH`. This is a no-op on non-darwin platforms. + */ +export function ensureCommonMacPaths(): void { + if (process.platform !== "darwin") return; + + const currentPath = process.env.PATH ?? ""; + const currentDirs = new Set(currentPath.split(":").filter(Boolean)); + const missing = COMMON_MACOS_PATHS.filter((p) => !currentDirs.has(p)); + + if (missing.length > 0) { + process.env.PATH = [currentPath, ...missing].filter(Boolean).join(":"); + } +}