diff --git a/apps/x/apps/main/src/main.ts b/apps/x/apps/main/src/main.ts index c3618000..b7d0a491 100644 --- a/apps/x/apps/main/src/main.ts +++ b/apps/x/apps/main/src/main.ts @@ -112,7 +112,9 @@ function initializeExecutionEnvironment(): void { ).trim(); const env = JSON.parse(stdout) as Record; - process.env = { ...env, ...process.env }; + // Let the user's shell environment win for overlapping keys like PATH. + // Finder/launched GUI apps on macOS often start with a stripped PATH. + process.env = { ...process.env, ...env }; } catch (error) { console.error('Failed to load shell environment', error); } diff --git a/apps/x/packages/core/src/application/assistant/runtime-context.ts b/apps/x/packages/core/src/application/assistant/runtime-context.ts index f1011c2c..a9baffc2 100644 --- a/apps/x/packages/core/src/application/assistant/runtime-context.ts +++ b/apps/x/packages/core/src/application/assistant/runtime-context.ts @@ -9,7 +9,15 @@ export interface RuntimeContext { } export function getExecutionShell(platform: NodeJS.Platform = process.platform): string { - return platform === 'win32' ? (process.env.ComSpec || 'cmd.exe') : '/bin/sh'; + if (platform === 'win32') { + return process.env.ComSpec || 'cmd.exe'; + } + + if (process.env.SHELL) { + return process.env.SHELL; + } + + return platform === 'darwin' ? '/bin/zsh' : '/bin/sh'; } export function getRuntimeContext(platform: NodeJS.Platform = process.platform): RuntimeContext { diff --git a/apps/x/packages/core/src/application/lib/command-executor.ts b/apps/x/packages/core/src/application/lib/command-executor.ts index 611bde45..11b15d90 100644 --- a/apps/x/packages/core/src/application/lib/command-executor.ts +++ b/apps/x/packages/core/src/application/lib/command-executor.ts @@ -8,7 +8,6 @@ const execPromise = promisify(exec); const COMMAND_SPLIT_REGEX = /(?:\|\||&&|;|\||\n|`|\$\(|\(|\))/; const ENV_ASSIGNMENT_REGEX = /^[A-Za-z_][A-Za-z0-9_]*=.*/; const WRAPPER_COMMANDS = new Set(['sudo', 'env', 'time', 'command']); -const EXECUTION_SHELL = getExecutionShell(); function sanitizeToken(token: string): string { return token.trim().replace(/^['"()]+|['"()]+$/g, ''); @@ -84,11 +83,12 @@ export async function executeCommand( } ): Promise { try { + const shell = getExecutionShell(); const { stdout, stderr } = await execPromise(command, { cwd: options?.cwd, timeout: options?.timeout, maxBuffer: options?.maxBuffer || 1024 * 1024, // default 1MB - shell: EXECUTION_SHELL, + shell, }); return { @@ -161,8 +161,9 @@ export function executeCommandAbortable( }; } + const shell = getExecutionShell(); const proc = spawn(command, [], { - shell: EXECUTION_SHELL, + shell, cwd: options?.cwd, detached: process.platform !== 'win32', // Create process group on Unix stdio: ['ignore', 'pipe', 'pipe'], @@ -272,11 +273,12 @@ export function executeCommandSync( } ): CommandResult { try { + const shell = getExecutionShell(); const stdout = execSync(command, { cwd: options?.cwd, timeout: options?.timeout, encoding: 'utf-8', - shell: EXECUTION_SHELL, + shell, }); return {