| author | |
| committer | |
| log | e1c33dcd33949c18eb30986ba513c1334064677c |
| tree | d6430621fe13f1a4fc49ba484af18253d85c9474 |
| parent | 0a902790cacb20d38c6f36fe331ac283a2c74b0f |
| signature | Signed by PGP key B5690EEEBB952194 |
* Fix Fish shell PATH detection for Codex
* refactor(provider): resolve lint warnings in provider layers
* refactor(terminal): simplify thread lock release initialization
* test(git): simplify fetch release callback initialization9 files changed, 114 insertions(+), 20 deletions(-)
apps/desktop/src/fixPath.ts+2-5| ... | ... | @@ -1,14 +1,11 @@ |
| 1 | import * as ChildProcess from "node:child_process"; | |
| 1 | import { readPathFromLoginShell } from "@t3tools/shared/shell"; | |
| 2 | 2 | |
| 3 | 3 | export function fixPath(): void { |
| 4 | 4 | if (process.platform !== "darwin") return; |
| 5 | 5 | |
| 6 | 6 | try { |
| 7 | 7 | const shell = process.env.SHELL ?? "/bin/zsh"; |
| 8 | const result = ChildProcess.execFileSync(shell, ["-ilc", "echo -n $PATH"], { | |
| 9 | encoding: "utf8", | |
| 10 | timeout: 5000, | |
| 11 | }); | |
| 8 | const result = readPathFromLoginShell(shell); | |
| 12 | 9 | if (result) { |
| 13 | 10 | process.env.PATH = result; |
| 14 | 11 | } |
apps/server/src/git/Layers/GitCore.test.ts+1-1| ... | ... | @@ -654,7 +654,7 @@ it.layer(TestLayer)("git integration", (it) => { |
| 654 | 654 | |
| 655 | 655 | const realGitService = yield* GitService; |
| 656 | 656 | let fetchStarted = false; |
| 657 | let releaseFetch: () => void = () => undefined; | |
| 657 | let releaseFetch!: () => void; | |
| 658 | 658 | const waitForReleasePromise = new Promise<void>((resolve) => { |
| 659 | 659 | releaseFetch = resolve; |
| 660 | 660 | }); |
apps/server/src/os-jank.ts+2-5| ... | ... | @@ -1,16 +1,13 @@ |
| 1 | 1 | import * as OS from "node:os"; |
| 2 | 2 | import { Effect, Path } from "effect"; |
| 3 | import { execFileSync } from "node:child_process"; | |
| 3 | import { readPathFromLoginShell } from "@t3tools/shared/shell"; | |
| 4 | 4 | |
| 5 | 5 | export function fixPath(): void { |
| 6 | 6 | if (process.platform !== "darwin") return; |
| 7 | 7 | |
| 8 | 8 | try { |
| 9 | 9 | const shell = process.env.SHELL ?? "/bin/zsh"; |
| 10 | const result = execFileSync(shell, ["-ilc", "echo -n $PATH"], { | |
| 11 | encoding: "utf8", | |
| 12 | timeout: 5000, | |
| 13 | }); | |
| 10 | const result = readPathFromLoginShell(shell); | |
| 14 | 11 | if (result) { |
| 15 | 12 | process.env.PATH = result; |
| 16 | 13 | } |
apps/server/src/provider/Layers/CodexAdapter.test.ts-1| ... | ... | @@ -22,7 +22,6 @@ import { |
| 22 | 22 | type CodexAppServerSendTurnInput, |
| 23 | 23 | } from "../../codexAppServerManager.ts"; |
| 24 | 24 | import { ServerConfig } from "../../config.ts"; |
| 25 | import { ProviderAdapterValidationError } from "../Errors.ts"; | |
| 26 | 25 | import { CodexAdapter } from "../Services/CodexAdapter.ts"; |
| 27 | 26 | import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; |
| 28 | 27 | import { makeCodexAdapterLive } from "./CodexAdapter.ts"; |
apps/server/src/provider/Layers/ProviderService.ts+11-7| ... | ... | @@ -436,13 +436,17 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 436 | 436 | return session; |
| 437 | 437 | } |
| 438 | 438 | |
| 439 | return { | |
| 440 | ...session, | |
| 441 | ...(session.resumeCursor === undefined && binding.resumeCursor !== undefined | |
| 442 | ? { resumeCursor: binding.resumeCursor } | |
| 443 | : {}), | |
| 444 | ...(binding.runtimeMode !== undefined ? { runtimeMode: binding.runtimeMode } : {}), | |
| 445 | }; | |
| 439 | const overrides: { | |
| 440 | resumeCursor?: ProviderSession["resumeCursor"]; | |
| 441 | runtimeMode?: ProviderSession["runtimeMode"]; | |
| 442 | } = {}; | |
| 443 | if (session.resumeCursor === undefined && binding.resumeCursor !== undefined) { | |
| 444 | overrides.resumeCursor = binding.resumeCursor; | |
| 445 | } | |
| 446 | if (binding.runtimeMode !== undefined) { | |
| 447 | overrides.runtimeMode = binding.runtimeMode; | |
| 448 | } | |
| 449 | return Object.assign({}, session, overrides); | |
| 446 | 450 | }); |
| 447 | 451 | }); |
| 448 | 452 |
apps/server/src/terminal/Layers/Manager.ts+1-1| ... | ... | @@ -1150,7 +1150,7 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents> |
| 1150 | 1150 | |
| 1151 | 1151 | private async runWithThreadLock<T>(threadId: string, task: () => Promise<T>): Promise<T> { |
| 1152 | 1152 | const previous = this.threadLocks.get(threadId) ?? Promise.resolve(); |
| 1153 | let release: () => void = () => {}; | |
| 1153 | let release!: () => void; | |
| 1154 | 1154 | const current = new Promise<void>((resolve) => { |
| 1155 | 1155 | release = resolve; |
| 1156 | 1156 | }); |
packages/shared/package.json+4| ... | ... | @@ -16,6 +16,10 @@ |
| 16 | 16 | "types": "./src/logging.ts", |
| 17 | 17 | "import": "./src/logging.ts" |
| 18 | 18 | }, |
| 19 | "./shell": { | |
| 20 | "types": "./src/shell.ts", | |
| 21 | "import": "./src/shell.ts" | |
| 22 | }, | |
| 19 | 23 | "./Net": { |
| 20 | 24 | "types": "./src/Net.ts", |
| 21 | 25 | "import": "./src/Net.ts" |
packages/shared/src/shell.test.ts created+55| ... | ... | @@ -0,0 +1,55 @@ |
| 1 | import { describe, expect, it, vi } from "vitest"; | |
| 2 | ||
| 3 | import { extractPathFromShellOutput, readPathFromLoginShell } from "./shell"; | |
| 4 | ||
| 5 | describe("extractPathFromShellOutput", () => { | |
| 6 | it("extracts the path between capture markers", () => { | |
| 7 | expect( | |
| 8 | extractPathFromShellOutput("__T3CODE_PATH_START__\n/opt/homebrew/bin:/usr/bin\n__T3CODE_PATH_END__\n"), | |
| 9 | ).toBe("/opt/homebrew/bin:/usr/bin"); | |
| 10 | }); | |
| 11 | ||
| 12 | it("ignores shell startup noise around the capture markers", () => { | |
| 13 | expect( | |
| 14 | extractPathFromShellOutput( | |
| 15 | "Welcome to fish\n__T3CODE_PATH_START__\n/opt/homebrew/bin:/usr/bin\n__T3CODE_PATH_END__\nBye\n", | |
| 16 | ), | |
| 17 | ).toBe("/opt/homebrew/bin:/usr/bin"); | |
| 18 | }); | |
| 19 | ||
| 20 | it("returns null when the markers are missing", () => { | |
| 21 | expect(extractPathFromShellOutput("/opt/homebrew/bin /usr/bin")).toBeNull(); | |
| 22 | }); | |
| 23 | }); | |
| 24 | ||
| 25 | describe("readPathFromLoginShell", () => { | |
| 26 | it("uses a shell-agnostic printenv PATH probe", () => { | |
| 27 | const execFile = vi.fn< | |
| 28 | ( | |
| 29 | file: string, | |
| 30 | args: ReadonlyArray<string>, | |
| 31 | options: { encoding: "utf8"; timeout: number }, | |
| 32 | ) => string | |
| 33 | >(() => "__T3CODE_PATH_START__\n/a:/b\n__T3CODE_PATH_END__\n"); | |
| 34 | ||
| 35 | expect(readPathFromLoginShell("/opt/homebrew/bin/fish", execFile)).toBe("/a:/b"); | |
| 36 | expect(execFile).toHaveBeenCalledTimes(1); | |
| 37 | ||
| 38 | const firstCall = execFile.mock.calls[0] as | |
| 39 | | [string, ReadonlyArray<string>, { encoding: "utf8"; timeout: number }] | |
| 40 | | undefined; | |
| 41 | expect(firstCall).toBeDefined(); | |
| 42 | if (!firstCall) { | |
| 43 | throw new Error("Expected execFile to be called"); | |
| 44 | } | |
| 45 | ||
| 46 | const [shell, args, options] = firstCall; | |
| 47 | expect(shell).toBe("/opt/homebrew/bin/fish"); | |
| 48 | expect(args).toHaveLength(2); | |
| 49 | expect(args?.[0]).toBe("-ilc"); | |
| 50 | expect(args?.[1]).toContain("printenv PATH"); | |
| 51 | expect(args?.[1]).toContain("__T3CODE_PATH_START__"); | |
| 52 | expect(args?.[1]).toContain("__T3CODE_PATH_END__"); | |
| 53 | expect(options).toEqual({ encoding: "utf8", timeout: 5000 }); | |
| 54 | }); | |
| 55 | }); |
packages/shared/src/shell.ts created+38| ... | ... | @@ -0,0 +1,38 @@ |
| 1 | import { execFileSync } from "node:child_process"; | |
| 2 | ||
| 3 | const PATH_CAPTURE_START = "__T3CODE_PATH_START__"; | |
| 4 | const PATH_CAPTURE_END = "__T3CODE_PATH_END__"; | |
| 5 | const PATH_CAPTURE_COMMAND = [ | |
| 6 | `printf '%s\n' '${PATH_CAPTURE_START}'`, | |
| 7 | "printenv PATH", | |
| 8 | `printf '%s\n' '${PATH_CAPTURE_END}'`, | |
| 9 | ].join("; "); | |
| 10 | ||
| 11 | type ExecFileSyncLike = ( | |
| 12 | file: string, | |
| 13 | args: ReadonlyArray<string>, | |
| 14 | options: { encoding: "utf8"; timeout: number }, | |
| 15 | ) => string; | |
| 16 | ||
| 17 | export function extractPathFromShellOutput(output: string): string | null { | |
| 18 | const startIndex = output.indexOf(PATH_CAPTURE_START); | |
| 19 | if (startIndex === -1) return null; | |
| 20 | ||
| 21 | const valueStartIndex = startIndex + PATH_CAPTURE_START.length; | |
| 22 | const endIndex = output.indexOf(PATH_CAPTURE_END, valueStartIndex); | |
| 23 | if (endIndex === -1) return null; | |
| 24 | ||
| 25 | const pathValue = output.slice(valueStartIndex, endIndex).trim(); | |
| 26 | return pathValue.length > 0 ? pathValue : null; | |
| 27 | } | |
| 28 | ||
| 29 | export function readPathFromLoginShell( | |
| 30 | shell: string, | |
| 31 | execFile: ExecFileSyncLike = execFileSync, | |
| 32 | ): string | undefined { | |
| 33 | const output = execFile(shell, ["-ilc", PATH_CAPTURE_COMMAND], { | |
| 34 | encoding: "utf8", | |
| 35 | timeout: 5000, | |
| 36 | }); | |
| 37 | return extractPathFromShellOutput(output) ?? undefined; | |
| 38 | } |