From b8b3aecbcf7fc8827d63d9dfa5cb322d090d3104 Mon Sep 17 00:00:00 2001 From: Ryan Gast Date: Sat, 14 Mar 2026 22:14:54 -0500 Subject: [PATCH 1/3] Fix Linux desktop Codex CLI detection --- apps/desktop/src/syncShellEnvironment.ts | 9 ++++++--- apps/server/src/main.test.ts | 20 +++++++++++++++++++- apps/server/src/os-jank.ts | 7 +++---- packages/shared/src/shell.ts | 20 ++++++++++++++++++++ 4 files changed, 48 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/syncShellEnvironment.ts b/apps/desktop/src/syncShellEnvironment.ts index 2181bea0ca..e7b0a929de 100644 --- a/apps/desktop/src/syncShellEnvironment.ts +++ b/apps/desktop/src/syncShellEnvironment.ts @@ -1,4 +1,4 @@ -import { readEnvironmentFromLoginShell, ShellEnvironmentReader } from "@t3tools/shared/shell"; +import { readEnvironmentFromLoginShell, resolveLoginShell, ShellEnvironmentReader } from "@t3tools/shared/shell"; export function syncShellEnvironment( env: NodeJS.ProcessEnv = process.env, @@ -7,10 +7,13 @@ export function syncShellEnvironment( readEnvironment?: ShellEnvironmentReader; } = {}, ): void { - if ((options.platform ?? process.platform) !== "darwin") return; + const platform = options.platform ?? process.platform; + if (platform !== "darwin" && platform !== "linux") return; try { - const shell = env.SHELL ?? "/bin/zsh"; + const shell = resolveLoginShell(platform, env.SHELL); + if (!shell) return; + const shellEnvironment = (options.readEnvironment ?? readEnvironmentFromLoginShell)(shell, [ "PATH", "SSH_AUTH_SOCK", diff --git a/apps/server/src/main.test.ts b/apps/server/src/main.test.ts index c644b4778e..cb8b62fced 100644 --- a/apps/server/src/main.test.ts +++ b/apps/server/src/main.test.ts @@ -21,6 +21,7 @@ import { ServerSettingsService } from "./serverSettings"; const start = vi.fn(() => undefined); const stop = vi.fn(() => undefined); +const fixPath = vi.fn(() => undefined); let resolvedConfig: ServerConfigShape | null = null; const serverStart = Effect.acquireRelease( Effect.gen(function* () { @@ -36,7 +37,7 @@ const findAvailablePort = vi.fn((preferred: number) => Effect.succeed(preferred) const testLayer = Layer.mergeAll( Layer.succeed(CliConfig, { cwd: "/tmp/t3-test-workspace", - fixPath: Effect.void, + fixPath: Effect.sync(fixPath), resolveStaticDir: Effect.undefined, } satisfies CliConfigShape), Layer.succeed(NetService, { @@ -81,6 +82,7 @@ beforeEach(() => { resolvedConfig = null; start.mockImplementation(() => undefined); stop.mockImplementation(() => undefined); + fixPath.mockImplementation(() => undefined); findAvailablePort.mockImplementation((preferred: number) => Effect.succeed(preferred)); }); @@ -329,6 +331,22 @@ it.layer(testLayer)("server CLI command", (it) => { }), ); + it.effect("hydrates PATH before server startup", () => + Effect.gen(function* () { + yield* runCli([]); + + assert.equal(fixPath.mock.calls.length, 1); + assert.equal(start.mock.calls.length, 1); + const fixPathOrder = fixPath.mock.invocationCallOrder[0]; + const startOrder = start.mock.invocationCallOrder[0]; + assert.isTrue(typeof fixPathOrder === "number" && typeof startOrder === "number"); + if (typeof fixPathOrder !== "number" || typeof startOrder !== "number") { + throw new Error("Expected fixPath and start to be called"); + } + assert.isTrue(fixPathOrder < startOrder); + }), + ); + it.effect("records a startup heartbeat with thread/project counts", () => Effect.gen(function* () { const recordTelemetry = vi.fn( diff --git a/apps/server/src/os-jank.ts b/apps/server/src/os-jank.ts index 0721d0d9f8..904fc10e80 100644 --- a/apps/server/src/os-jank.ts +++ b/apps/server/src/os-jank.ts @@ -1,12 +1,11 @@ import * as OS from "node:os"; import { Effect, Path } from "effect"; -import { readPathFromLoginShell } from "@t3tools/shared/shell"; +import { readPathFromLoginShell, resolveLoginShell } from "@t3tools/shared/shell"; export function fixPath(): void { - if (process.platform !== "darwin") return; - try { - const shell = process.env.SHELL ?? "/bin/zsh"; + const shell = resolveLoginShell(process.platform, process.env.SHELL); + if (!shell) return; const result = readPathFromLoginShell(shell); if (result) { process.env.PATH = result; diff --git a/packages/shared/src/shell.ts b/packages/shared/src/shell.ts index f1d60bf334..d9e8a7881b 100644 --- a/packages/shared/src/shell.ts +++ b/packages/shared/src/shell.ts @@ -10,6 +10,26 @@ type ExecFileSyncLike = ( options: { encoding: "utf8"; timeout: number }, ) => string; +export function resolveLoginShell( + platform: NodeJS.Platform, + shell: string | undefined, +): string | undefined { + const trimmedShell = shell?.trim(); + if (trimmedShell) { + return trimmedShell; + } + + if (platform === "darwin") { + return "/bin/zsh"; + } + + if (platform === "linux") { + return "/bin/bash"; + } + + return undefined; +} + export function extractPathFromShellOutput(output: string): string | null { const startIndex = output.indexOf(PATH_CAPTURE_START); if (startIndex === -1) return null; From ee0a191d8429e77ddf55aab31bf0e6cdd19941e4 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 28 Mar 2026 10:56:09 -0700 Subject: [PATCH 2/3] use assert --- apps/server/src/main.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/server/src/main.test.ts b/apps/server/src/main.test.ts index cb8b62fced..0c96203dbd 100644 --- a/apps/server/src/main.test.ts +++ b/apps/server/src/main.test.ts @@ -339,9 +339,8 @@ it.layer(testLayer)("server CLI command", (it) => { assert.equal(start.mock.calls.length, 1); const fixPathOrder = fixPath.mock.invocationCallOrder[0]; const startOrder = start.mock.invocationCallOrder[0]; - assert.isTrue(typeof fixPathOrder === "number" && typeof startOrder === "number"); if (typeof fixPathOrder !== "number" || typeof startOrder !== "number") { - throw new Error("Expected fixPath and start to be called"); + assert.fail("Expected fixPath and start to be called"); } assert.isTrue(fixPathOrder < startOrder); }), From ccb8b259acaff86a0cb7a189c82c817be1bbf7c1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 28 Mar 2026 11:08:20 -0700 Subject: [PATCH 3/3] Support Linux login-shell PATH hydration - Extend PATH sync to Linux desktop and server startup - Add tests for Linux handling and non-matching platforms --- apps/desktop/src/syncShellEnvironment.test.ts | 28 +++++++++++-- apps/desktop/src/syncShellEnvironment.ts | 6 ++- apps/server/src/os-jank.test.ts | 39 +++++++++++++++++++ apps/server/src/os-jank.ts | 19 +++++++-- 4 files changed, 83 insertions(+), 9 deletions(-) create mode 100644 apps/server/src/os-jank.test.ts diff --git a/apps/desktop/src/syncShellEnvironment.test.ts b/apps/desktop/src/syncShellEnvironment.test.ts index 69e73da0aa..cda78a20b2 100644 --- a/apps/desktop/src/syncShellEnvironment.test.ts +++ b/apps/desktop/src/syncShellEnvironment.test.ts @@ -62,14 +62,13 @@ describe("syncShellEnvironment", () => { expect(env.SSH_AUTH_SOCK).toBe("/tmp/inherited.sock"); }); - it("does nothing outside macOS", () => { + it("hydrates PATH and missing SSH_AUTH_SOCK from the login shell on linux", () => { const env: NodeJS.ProcessEnv = { SHELL: "/bin/zsh", PATH: "/usr/bin", - SSH_AUTH_SOCK: "/tmp/inherited.sock", }; const readEnvironment = vi.fn(() => ({ - PATH: "/opt/homebrew/bin:/usr/bin", + PATH: "/home/linuxbrew/.linuxbrew/bin:/usr/bin", SSH_AUTH_SOCK: "/tmp/secretive.sock", })); @@ -78,8 +77,29 @@ describe("syncShellEnvironment", () => { readEnvironment, }); + expect(readEnvironment).toHaveBeenCalledWith("/bin/zsh", ["PATH", "SSH_AUTH_SOCK"]); + expect(env.PATH).toBe("/home/linuxbrew/.linuxbrew/bin:/usr/bin"); + expect(env.SSH_AUTH_SOCK).toBe("/tmp/secretive.sock"); + }); + + it("does nothing outside macOS and linux", () => { + const env: NodeJS.ProcessEnv = { + SHELL: "C:/Program Files/Git/bin/bash.exe", + PATH: "C:\\Windows\\System32", + SSH_AUTH_SOCK: "/tmp/inherited.sock", + }; + const readEnvironment = vi.fn(() => ({ + PATH: "/usr/local/bin:/usr/bin", + SSH_AUTH_SOCK: "/tmp/secretive.sock", + })); + + syncShellEnvironment(env, { + platform: "win32", + readEnvironment, + }); + expect(readEnvironment).not.toHaveBeenCalled(); - expect(env.PATH).toBe("/usr/bin"); + expect(env.PATH).toBe("C:\\Windows\\System32"); expect(env.SSH_AUTH_SOCK).toBe("/tmp/inherited.sock"); }); }); diff --git a/apps/desktop/src/syncShellEnvironment.ts b/apps/desktop/src/syncShellEnvironment.ts index e7b0a929de..13036149b8 100644 --- a/apps/desktop/src/syncShellEnvironment.ts +++ b/apps/desktop/src/syncShellEnvironment.ts @@ -1,4 +1,8 @@ -import { readEnvironmentFromLoginShell, resolveLoginShell, ShellEnvironmentReader } from "@t3tools/shared/shell"; +import { + readEnvironmentFromLoginShell, + resolveLoginShell, + ShellEnvironmentReader, +} from "@t3tools/shared/shell"; export function syncShellEnvironment( env: NodeJS.ProcessEnv = process.env, diff --git a/apps/server/src/os-jank.test.ts b/apps/server/src/os-jank.test.ts new file mode 100644 index 0000000000..ca03ab5868 --- /dev/null +++ b/apps/server/src/os-jank.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it, vi } from "vitest"; + +import { fixPath } from "./os-jank"; + +describe("fixPath", () => { + it("hydrates PATH on linux using the resolved login shell", () => { + const env: NodeJS.ProcessEnv = { + SHELL: "/bin/zsh", + PATH: "/usr/bin", + }; + const readPath = vi.fn(() => "/opt/homebrew/bin:/usr/bin"); + + fixPath({ + env, + platform: "linux", + readPath, + }); + + expect(readPath).toHaveBeenCalledWith("/bin/zsh"); + expect(env.PATH).toBe("/opt/homebrew/bin:/usr/bin"); + }); + + it("does nothing outside macOS and linux even when SHELL is set", () => { + const env: NodeJS.ProcessEnv = { + SHELL: "C:/Program Files/Git/bin/bash.exe", + PATH: "C:\\Windows\\System32", + }; + const readPath = vi.fn(() => "/usr/local/bin:/usr/bin"); + + fixPath({ + env, + platform: "win32", + readPath, + }); + + expect(readPath).not.toHaveBeenCalled(); + expect(env.PATH).toBe("C:\\Windows\\System32"); + }); +}); diff --git a/apps/server/src/os-jank.ts b/apps/server/src/os-jank.ts index 904fc10e80..c3629e8fde 100644 --- a/apps/server/src/os-jank.ts +++ b/apps/server/src/os-jank.ts @@ -2,13 +2,24 @@ import * as OS from "node:os"; import { Effect, Path } from "effect"; import { readPathFromLoginShell, resolveLoginShell } from "@t3tools/shared/shell"; -export function fixPath(): void { +export function fixPath( + options: { + env?: NodeJS.ProcessEnv; + platform?: NodeJS.Platform; + readPath?: typeof readPathFromLoginShell; + } = {}, +): void { + const platform = options.platform ?? process.platform; + if (platform !== "darwin" && platform !== "linux") return; + + const env = options.env ?? process.env; + try { - const shell = resolveLoginShell(process.platform, process.env.SHELL); + const shell = resolveLoginShell(platform, env.SHELL); if (!shell) return; - const result = readPathFromLoginShell(shell); + const result = (options.readPath ?? readPathFromLoginShell)(shell); if (result) { - process.env.PATH = result; + env.PATH = result; } } catch { // Silently ignore — keep default PATH