From b2c097d01211bfa1e99932bb94a151efe7113620 Mon Sep 17 00:00:00 2001 From: Ryan Gast Date: Sat, 14 Mar 2026 22:14:54 -0500 Subject: [PATCH] Fix Linux desktop Codex CLI detection --- apps/desktop/src/syncShellEnvironment.ts | 9 ++++++--- apps/server/src/main.test.ts | 20 +++++++++++++++++++- apps/server/src/main.ts | 10 ++++++++-- apps/server/src/os-jank.ts | 7 +++---- packages/shared/src/shell.ts | 20 ++++++++++++++++++++ 5 files changed, 56 insertions(+), 10 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 83976e3d4c..a509a8bb6d 100644 --- a/apps/server/src/main.test.ts +++ b/apps/server/src/main.test.ts @@ -19,6 +19,7 @@ import { Server, type ServerShape } from "./wsServer"; 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* () { @@ -34,7 +35,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, { @@ -80,6 +81,7 @@ beforeEach(() => { resolvedConfig = null; start.mockImplementation(() => undefined); stop.mockImplementation(() => undefined); + fixPath.mockImplementation(() => undefined); findAvailablePort.mockImplementation((preferred: number) => Effect.succeed(preferred)); }); @@ -233,6 +235,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/main.ts b/apps/server/src/main.ts index 0a33be0cbb..07b939aa7d 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -193,10 +193,18 @@ const ServerConfigLive = (input: CliInput) => }), ); +const PathHydrationLive = Layer.effectDiscard( + Effect.gen(function* () { + const cliConfig = yield* CliConfig; + yield* cliConfig.fixPath; + }), +); + const LayerLive = (input: CliInput) => Layer.empty.pipe( Layer.provideMerge(makeServerRuntimeServicesLayer()), Layer.provideMerge(makeServerProviderLayer()), + Layer.provideMerge(PathHydrationLive), Layer.provideMerge(ProviderHealthLive), Layer.provideMerge(SqlitePersistence.layerConfig), Layer.provideMerge(ServerLoggerLive), @@ -237,10 +245,8 @@ export const recordStartupHeartbeat = Effect.gen(function* () { const makeServerProgram = (input: CliInput) => Effect.gen(function* () { - const cliConfig = yield* CliConfig; const { start, stopSignal } = yield* Server; const openDeps = yield* Open; - yield* cliConfig.fixPath; const config = yield* ServerConfig; diff --git a/apps/server/src/os-jank.ts b/apps/server/src/os-jank.ts index 586aca6f79..1fb166e672 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;