From f71574b36df5f0191fe7ae5a12fe3a4530970a25 Mon Sep 17 00:00:00 2001 From: adminliu-main Date: Thu, 27 Aug 2026 23:21:39 +0800 Subject: [PATCH] fix(agent-adapter): support fish when loading the project shell environment --- .../agent-adapter/src/__tests__/shell-env.test.ts | 14 +++++++++++++- packages/host/agent-adapter/src/shell-env.ts | 12 +++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/host/agent-adapter/src/__tests__/shell-env.test.ts b/packages/host/agent-adapter/src/__tests__/shell-env.test.ts index ccff6f896..b8e94c6b2 100644 --- a/packages/host/agent-adapter/src/__tests__/shell-env.test.ts +++ b/packages/host/agent-adapter/src/__tests__/shell-env.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { parseShellEnvironment } from '../shell-env'; +import { parseShellEnvironment, shellProbeCommand } from '../shell-env'; describe('project shell environment', () => { it('ignores shell output and parses the marked JSON environment', () => { @@ -22,3 +22,15 @@ describe('project shell environment', () => { ); }); }); + +describe('shell probe command', () => { + it('emits fish syntax for fish and POSIX syntax otherwise', () => { + expect(shellProbeCommand('/opt/homebrew/bin/fish', 'PRINT_ENV')).toBe( + 'if command -v direnv >/dev/null 2>&1; exec direnv exec "$PWD" PRINT_ENV; else; exec PRINT_ENV; end', + ); + expect(shellProbeCommand('/bin/zsh', 'PRINT_ENV')).toBe( + 'if command -v direnv >/dev/null 2>&1; then exec direnv exec "$PWD" PRINT_ENV; else exec PRINT_ENV; fi', + ); + expect(shellProbeCommand('/bin/bash', 'PRINT_ENV')).toContain('then exec'); + }); +}); diff --git a/packages/host/agent-adapter/src/shell-env.ts b/packages/host/agent-adapter/src/shell-env.ts index 911dfc051..9c78f4804 100644 --- a/packages/host/agent-adapter/src/shell-env.ts +++ b/packages/host/agent-adapter/src/shell-env.ts @@ -11,6 +11,8 @@ const CAPTURE_ENV = { ELECTRON_NO_ATTACH_CONSOLE: '1', LINKCODE_RESOLVING_ENVIRONMENT: '1', }; +const FISH_SHELL_PATTERN = /(?:^|\/)fish$/; + const ShellEnvironmentSchema = z .record(z.string(), z.string()) .refine((value) => Boolean(value.PATH), 'PATH is required'); @@ -28,7 +30,7 @@ export async function resolveShellEnvironment( const marker = randomUUID().replaceAll('-', ''); const expression = `"${marker}" + JSON.stringify(process.env) + "${marker}"`; const printEnv = `${quoteShellArg(process.execPath)} -p ${quoteShellArg(expression)}`; - const command = `if command -v direnv >/dev/null 2>&1; then exec direnv exec "$PWD" ${printEnv}; else exec ${printEnv}; fi`; + const command = shellProbeCommand(shell, printEnv); let stdout: string; try { ({ stdout } = await execFileAsync(shell, ['-i', '-l', '-c', command], { @@ -54,6 +56,14 @@ export async function resolveShellEnvironment( return resolved; } +/** fish rejects POSIX if/then/else — it alone gets its own syntax; every other login shell is POSIX. */ +export function shellProbeCommand(shell: string, printEnv: string): string { + if (FISH_SHELL_PATTERN.test(shell)) { + return `if command -v direnv >/dev/null 2>&1; exec direnv exec "$PWD" ${printEnv}; else; exec ${printEnv}; end`; + } + return `if command -v direnv >/dev/null 2>&1; then exec direnv exec "$PWD" ${printEnv}; else exec ${printEnv}; fi`; +} + export function parseShellEnvironment( stdout: string, marker: string,