| 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,14 +1,11 @@ |
| 1 | import * as ChildProcess from "node:child_process"; | 1 | import { readPathFromLoginShell } from "@t3tools/shared/shell"; |
| 2 | 2 | ||
| 3 | export function fixPath(): void { | 3 | export function fixPath(): void { |
| 4 | if (process.platform !== "darwin") return; | 4 | if (process.platform !== "darwin") return; |
| 5 | 5 | ||
| 6 | try { | 6 | try { |
| 7 | const shell = process.env.SHELL ?? "/bin/zsh"; | 7 | const shell = process.env.SHELL ?? "/bin/zsh"; |
| 8 | const result = ChildProcess.execFileSync(shell, ["-ilc", "echo -n $PATH"], { | 8 | const result = readPathFromLoginShell(shell); |
| 9 | encoding: "utf8", | ||
| 10 | timeout: 5000, | ||
| 11 | }); | ||
| 12 | if (result) { | 9 | if (result) { |
| 13 | process.env.PATH = result; | 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,7 +654,7 @@ it.layer(TestLayer)("git integration", (it) => { |
| 654 | 654 | ||
| 655 | const realGitService = yield* GitService; | 655 | const realGitService = yield* GitService; |
| 656 | let fetchStarted = false; | 656 | let fetchStarted = false; |
| 657 | let releaseFetch: () => void = () => undefined; | 657 | let releaseFetch!: () => void; |
| 658 | const waitForReleasePromise = new Promise<void>((resolve) => { | 658 | const waitForReleasePromise = new Promise<void>((resolve) => { |
| 659 | releaseFetch = resolve; | 659 | releaseFetch = resolve; |
| 660 | }); | 660 | }); |
apps/server/src/os-jank.ts+2-5| ... | @@ -1,16 +1,13 @@ | ... | @@ -1,16 +1,13 @@ |
| 1 | import * as OS from "node:os"; | 1 | import * as OS from "node:os"; |
| 2 | import { Effect, Path } from "effect"; | 2 | import { Effect, Path } from "effect"; |
| 3 | import { execFileSync } from "node:child_process"; | 3 | import { readPathFromLoginShell } from "@t3tools/shared/shell"; |
| 4 | 4 | ||
| 5 | export function fixPath(): void { | 5 | export function fixPath(): void { |
| 6 | if (process.platform !== "darwin") return; | 6 | if (process.platform !== "darwin") return; |
| 7 | 7 | ||
| 8 | try { | 8 | try { |
| 9 | const shell = process.env.SHELL ?? "/bin/zsh"; | 9 | const shell = process.env.SHELL ?? "/bin/zsh"; |
| 10 | const result = execFileSync(shell, ["-ilc", "echo -n $PATH"], { | 10 | const result = readPathFromLoginShell(shell); |
| 11 | encoding: "utf8", | ||
| 12 | timeout: 5000, | ||
| 13 | }); | ||
| 14 | if (result) { | 11 | if (result) { |
| 15 | process.env.PATH = result; | 12 | process.env.PATH = result; |
| 16 | } | 13 | } |
apps/server/src/provider/Layers/CodexAdapter.test.ts-1| ... | @@ -22,7 +22,6 @@ import { | ... | @@ -22,7 +22,6 @@ import { |
| 22 | type CodexAppServerSendTurnInput, | 22 | type CodexAppServerSendTurnInput, |
| 23 | } from "../../codexAppServerManager.ts"; | 23 | } from "../../codexAppServerManager.ts"; |
| 24 | import { ServerConfig } from "../../config.ts"; | 24 | import { ServerConfig } from "../../config.ts"; |
| 25 | import { ProviderAdapterValidationError } from "../Errors.ts"; | ||
| 26 | import { CodexAdapter } from "../Services/CodexAdapter.ts"; | 25 | import { CodexAdapter } from "../Services/CodexAdapter.ts"; |
| 27 | import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; | 26 | import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; |
| 28 | import { makeCodexAdapterLive } from "./CodexAdapter.ts"; | 27 | import { makeCodexAdapterLive } from "./CodexAdapter.ts"; |
apps/server/src/provider/Layers/ProviderService.ts+11-7| ... | @@ -436,13 +436,17 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -436,13 +436,17 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 436 | return session; | 436 | return session; |
| 437 | } | 437 | } |
| 438 | 438 | ||
| 439 | return { | 439 | const overrides: { |
| 440 | ...session, | 440 | resumeCursor?: ProviderSession["resumeCursor"]; |
| 441 | ...(session.resumeCursor === undefined && binding.resumeCursor !== undefined | 441 | runtimeMode?: ProviderSession["runtimeMode"]; |
| 442 | ? { resumeCursor: binding.resumeCursor } | 442 | } = {}; |
| 443 | : {}), | 443 | if (session.resumeCursor === undefined && binding.resumeCursor !== undefined) { |
| 444 | ...(binding.runtimeMode !== undefined ? { runtimeMode: binding.runtimeMode } : {}), | 444 | overrides.resumeCursor = binding.resumeCursor; |
| 445 | }; | 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,7 +1150,7 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents> |
| 1150 | 1150 | ||
| 1151 | private async runWithThreadLock<T>(threadId: string, task: () => Promise<T>): Promise<T> { | 1151 | private async runWithThreadLock<T>(threadId: string, task: () => Promise<T>): Promise<T> { |
| 1152 | const previous = this.threadLocks.get(threadId) ?? Promise.resolve(); | 1152 | const previous = this.threadLocks.get(threadId) ?? Promise.resolve(); |
| 1153 | let release: () => void = () => {}; | 1153 | let release!: () => void; |
| 1154 | const current = new Promise<void>((resolve) => { | 1154 | const current = new Promise<void>((resolve) => { |
| 1155 | release = resolve; | 1155 | release = resolve; |
| 1156 | }); | 1156 | }); |
packages/shared/package.json+4| ... | @@ -16,6 +16,10 @@ | ... | @@ -16,6 +16,10 @@ |
| 16 | "types": "./src/logging.ts", | 16 | "types": "./src/logging.ts", |
| 17 | "import": "./src/logging.ts" | 17 | "import": "./src/logging.ts" |
| 18 | }, | 18 | }, |
| 19 | "./shell": { | ||
| 20 | "types": "./src/shell.ts", | ||
| 21 | "import": "./src/shell.ts" | ||
| 22 | }, | ||
| 19 | "./Net": { | 23 | "./Net": { |
| 20 | "types": "./src/Net.ts", | 24 | "types": "./src/Net.ts", |
| 21 | "import": "./src/Net.ts" | 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 | } | ||