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 @@
1import * as ChildProcess from "node:child_process";
1import { readPathFromLoginShell } from "@t3tools/shared/shell";
22
33export function fixPath(): void {
44 if (process.platform !== "darwin") return;
55
66 try {
77 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);
129 if (result) {
1310 process.env.PATH = result;
1411 }
apps/server/src/git/Layers/GitCore.test.ts+1-1
......@@ -654,7 +654,7 @@ it.layer(TestLayer)("git integration", (it) => {
654654
655655 const realGitService = yield* GitService;
656656 let fetchStarted = false;
657 let releaseFetch: () => void = () => undefined;
657 let releaseFetch!: () => void;
658658 const waitForReleasePromise = new Promise<void>((resolve) => {
659659 releaseFetch = resolve;
660660 });
apps/server/src/os-jank.ts+2-5
......@@ -1,16 +1,13 @@
11import * as OS from "node:os";
22import { Effect, Path } from "effect";
3import { execFileSync } from "node:child_process";
3import { readPathFromLoginShell } from "@t3tools/shared/shell";
44
55export function fixPath(): void {
66 if (process.platform !== "darwin") return;
77
88 try {
99 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);
1411 if (result) {
1512 process.env.PATH = result;
1613 }
apps/server/src/provider/Layers/CodexAdapter.test.ts-1
......@@ -22,7 +22,6 @@ import {
2222 type CodexAppServerSendTurnInput,
2323} from "../../codexAppServerManager.ts";
2424import { ServerConfig } from "../../config.ts";
25import { ProviderAdapterValidationError } from "../Errors.ts";
2625import { CodexAdapter } from "../Services/CodexAdapter.ts";
2726import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts";
2827import { makeCodexAdapterLive } from "./CodexAdapter.ts";
apps/server/src/provider/Layers/ProviderService.ts+11-7
......@@ -436,13 +436,17 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
436436 return session;
437437 }
438438
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);
446450 });
447451 });
448452
apps/server/src/terminal/Layers/Manager.ts+1-1
......@@ -1150,7 +1150,7 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents>
11501150
11511151 private async runWithThreadLock<T>(threadId: string, task: () => Promise<T>): Promise<T> {
11521152 const previous = this.threadLocks.get(threadId) ?? Promise.resolve();
1153 let release: () => void = () => {};
1153 let release!: () => void;
11541154 const current = new Promise<void>((resolve) => {
11551155 release = resolve;
11561156 });
packages/shared/package.json+4
......@@ -16,6 +16,10 @@
1616 "types": "./src/logging.ts",
1717 "import": "./src/logging.ts"
1818 },
19 "./shell": {
20 "types": "./src/shell.ts",
21 "import": "./src/shell.ts"
22 },
1923 "./Net": {
2024 "types": "./src/Net.ts",
2125 "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}