authorgravatar for gbarros1095@gmail.comGuilherme Barros <gbarros1095@gmail.com> 2026-03-07 11:14:30+01:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-07 02:14:30-08:00
loge1c33dcd33949c18eb30986ba513c1334064677c
treed6430621fe13f1a4fc49ba484af18253d85c9474
parent0a902790cacb20d38c6f36fe331ac283a2c74b0f
signature Signed by PGP key B5690EEEBB952194

Fix Fish shell PATH detection and clean up lint warnings (#323)

* 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 initialization

9 files changed, 114 insertions(+), 20 deletions(-)

apps/desktop/src/fixPath.ts+2-5
...@@ -1,14 +1,11 @@...@@ -1,14 +1,11 @@
1import * as ChildProcess from "node:child_process";1import { readPathFromLoginShell } from "@t3tools/shared/shell";
22
3export function fixPath(): void {3export function fixPath(): void {
4 if (process.platform !== "darwin") return;4 if (process.platform !== "darwin") return;
55
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) => {
654654
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 @@
1import * as OS from "node:os";1import * as OS from "node:os";
2import { Effect, Path } from "effect";2import { Effect, Path } from "effect";
3import { execFileSync } from "node:child_process";3import { readPathFromLoginShell } from "@t3tools/shared/shell";
44
5export function fixPath(): void {5export function fixPath(): void {
6 if (process.platform !== "darwin") return;6 if (process.platform !== "darwin") return;
77
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";
24import { ServerConfig } from "../../config.ts";24import { ServerConfig } from "../../config.ts";
25import { ProviderAdapterValidationError } from "../Errors.ts";
26import { CodexAdapter } from "../Services/CodexAdapter.ts";25import { CodexAdapter } from "../Services/CodexAdapter.ts";
27import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts";26import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts";
28import { makeCodexAdapterLive } from "./CodexAdapter.ts";27import { 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 }
438438
439 return {439 const overrides: {
440 ...session,440 resumeCursor?: ProviderSession["resumeCursor"];
441 ...(session.resumeCursor === undefined && binding.resumeCursor !== undefined441 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 });
448452
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>
11501150
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 @@
1import { describe, expect, it, vi } from "vitest";
2
3import { extractPathFromShellOutput, readPathFromLoginShell } from "./shell";
4
5describe("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
25describe("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 @@
1import { execFileSync } from "node:child_process";
2
3const PATH_CAPTURE_START = "__T3CODE_PATH_START__";
4const PATH_CAPTURE_END = "__T3CODE_PATH_END__";
5const PATH_CAPTURE_COMMAND = [
6 `printf '%s\n' '${PATH_CAPTURE_START}'`,
7 "printenv PATH",
8 `printf '%s\n' '${PATH_CAPTURE_END}'`,
9].join("; ");
10
11type ExecFileSyncLike = (
12 file: string,
13 args: ReadonlyArray<string>,
14 options: { encoding: "utf8"; timeout: number },
15) => string;
16
17export 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
29export 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}