| import { feature } from 'bun:bundle' |
| import { access } from 'fs/promises' |
| import { tmpdir as osTmpdir } from 'os' |
| import { join as nativeJoin } from 'path' |
| import { join as posixJoin } from 'path/posix' |
| import { rearrangePipeCommand } from '../bash/bashPipeCommand.js' |
| import { createAndSaveSnapshot } from '../bash/ShellSnapshot.js' |
| import { formatShellPrefixCommand } from '../bash/shellPrefix.js' |
| import { quote } from '../bash/shellQuote.js' |
| import { |
| quoteShellCommand, |
| rewriteWindowsNullRedirect, |
| shouldAddStdinRedirect, |
| } from '../bash/shellQuoting.js' |
| import { logForDebugging } from '../debug.js' |
| import { getPlatform } from '../platform.js' |
| import { getSessionEnvironmentScript } from '../sessionEnvironment.js' |
| import { getSessionEnvVars } from '../sessionEnvVars.js' |
| import { |
| ensureSocketInitialized, |
| getClaudeTmuxEnv, |
| hasTmuxToolBeenUsed, |
| } from '../tmuxSocket.js' |
| import { windowsPathToPosixPath } from '../windowsPaths.js' |
| import type { ShellProvider } from './shellProvider.js' |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function getDisableExtglobCommand(shellPath: string): string | null { |
| |
| |
| if (process.env.CLAUDE_CODE_SHELL_PREFIX) { |
| |
| |
| return '{ shopt -u extglob || setopt NO_EXTENDED_GLOB; } >/dev/null 2>&1 || true' |
| } |
|
|
| |
| if (shellPath.includes('bash')) { |
| return 'shopt -u extglob 2>/dev/null || true' |
| } else if (shellPath.includes('zsh')) { |
| return 'setopt NO_EXTENDED_GLOB 2>/dev/null || true' |
| } |
| |
| return null |
| } |
|
|
| export async function createBashShellProvider( |
| shellPath: string, |
| options?: { skipSnapshot?: boolean }, |
| ): Promise<ShellProvider> { |
| let currentSandboxTmpDir: string | undefined |
| const snapshotPromise: Promise<string | undefined> = options?.skipSnapshot |
| ? Promise.resolve(undefined) |
| : createAndSaveSnapshot(shellPath).catch(error => { |
| logForDebugging(`Failed to create shell snapshot: ${error}`) |
| return undefined |
| }) |
| |
| let lastSnapshotFilePath: string | undefined |
|
|
| return { |
| type: 'bash', |
| shellPath, |
| detached: true, |
|
|
| async buildExecCommand( |
| command: string, |
| opts: { |
| id: number | string |
| sandboxTmpDir?: string |
| useSandbox: boolean |
| }, |
| ): Promise<{ commandString: string; cwdFilePath: string }> { |
| let snapshotFilePath = await snapshotPromise |
| |
| |
| |
| |
| |
| |
| |
| if (snapshotFilePath) { |
| try { |
| await access(snapshotFilePath) |
| } catch { |
| logForDebugging( |
| `Snapshot file missing, falling back to login shell: ${snapshotFilePath}`, |
| ) |
| snapshotFilePath = undefined |
| } |
| } |
| lastSnapshotFilePath = snapshotFilePath |
|
|
| |
| currentSandboxTmpDir = opts.sandboxTmpDir |
|
|
| const tmpdir = osTmpdir() |
| const isWindows = getPlatform() === 'windows' |
| const shellTmpdir = isWindows ? windowsPathToPosixPath(tmpdir) : tmpdir |
|
|
| |
| |
| |
| |
| const shellCwdFilePath = opts.useSandbox |
| ? posixJoin(opts.sandboxTmpDir!, `cwd-${opts.id}`) |
| : posixJoin(shellTmpdir, `claude-${opts.id}-cwd`) |
| const cwdFilePath = opts.useSandbox |
| ? posixJoin(opts.sandboxTmpDir!, `cwd-${opts.id}`) |
| : nativeJoin(tmpdir, `claude-${opts.id}-cwd`) |
|
|
| |
| |
| |
| |
| const normalizedCommand = rewriteWindowsNullRedirect(command) |
| const addStdinRedirect = shouldAddStdinRedirect(normalizedCommand) |
| let quotedCommand = quoteShellCommand(normalizedCommand, addStdinRedirect) |
|
|
| |
| |
| if ( |
| feature('COMMIT_ATTRIBUTION') && |
| (command.includes('<<') || command.includes('\n')) |
| ) { |
| logForDebugging( |
| `Shell: Command before quoting (first 500 chars):\n${command.slice(0, 500)}`, |
| ) |
| logForDebugging( |
| `Shell: Quoted command (first 500 chars):\n${quotedCommand.slice(0, 500)}`, |
| ) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| if (normalizedCommand.includes('|') && addStdinRedirect) { |
| quotedCommand = rearrangePipeCommand(normalizedCommand) |
| } |
|
|
| const commandParts: string[] = [] |
|
|
| |
| |
| |
| if (snapshotFilePath) { |
| const finalPath = |
| getPlatform() === 'windows' |
| ? windowsPathToPosixPath(snapshotFilePath) |
| : snapshotFilePath |
| commandParts.push(`source ${quote([finalPath])} 2>/dev/null || true`) |
| } |
|
|
| |
| const sessionEnvScript = await getSessionEnvironmentScript() |
| if (sessionEnvScript) { |
| commandParts.push(sessionEnvScript) |
| } |
|
|
| |
| const disableExtglobCmd = getDisableExtglobCommand(shellPath) |
| if (disableExtglobCmd) { |
| commandParts.push(disableExtglobCmd) |
| } |
|
|
| |
| |
| |
| commandParts.push(`eval ${quotedCommand}`) |
| |
| commandParts.push(`pwd -P >| ${quote([shellCwdFilePath])}`) |
| let commandString = commandParts.join(' && ') |
|
|
| |
| if (process.env.CLAUDE_CODE_SHELL_PREFIX) { |
| commandString = formatShellPrefixCommand( |
| process.env.CLAUDE_CODE_SHELL_PREFIX, |
| commandString, |
| ) |
| } |
|
|
| return { commandString, cwdFilePath } |
| }, |
|
|
| getSpawnArgs(commandString: string): string[] { |
| const skipLoginShell = lastSnapshotFilePath !== undefined |
| if (skipLoginShell) { |
| logForDebugging('Spawning shell without login (-l flag skipped)') |
| } |
| return ['-c', ...(skipLoginShell ? [] : ['-l']), commandString] |
| }, |
|
|
| async getEnvironmentOverrides( |
| command: string, |
| ): Promise<Record<string, string>> { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const commandUsesTmux = command.includes('tmux') |
| if ( |
| process.env.USER_TYPE === 'ant' && |
| (hasTmuxToolBeenUsed() || commandUsesTmux) |
| ) { |
| await ensureSocketInitialized() |
| } |
| const claudeTmuxEnv = getClaudeTmuxEnv() |
| const env: Record<string, string> = {} |
| |
| |
| |
| if (claudeTmuxEnv) { |
| env.TMUX = claudeTmuxEnv |
| } |
| if (currentSandboxTmpDir) { |
| let posixTmpDir = currentSandboxTmpDir |
| if (getPlatform() === 'windows') { |
| posixTmpDir = windowsPathToPosixPath(posixTmpDir) |
| } |
| env.TMPDIR = posixTmpDir |
| env.CLAUDE_CODE_TMPDIR = posixTmpDir |
| |
| |
| |
| |
| env.TMPPREFIX = posixJoin(posixTmpDir, 'zsh') |
| } |
| |
| for (const [key, value] of getSessionEnvVars()) { |
| env[key] = value |
| } |
| return env |
| }, |
| } |
| } |
|
|