/** * Environment configuration for Claude Code engine. * * Builds an env dictionary to pass via SDK Options.env * instead of mutating process.env directly. * This avoids global state race conditions when multiple projects * stream concurrently. */ import { join } from 'path'; import { engineQueries } from '../../../database/queries'; import { debug } from '$shared/utils/logger'; import { getCleanSpawnEnv, getEngineUserConfigDir } from '../../../utils/index.js'; let _ready = false; let _initPromise: Promise | null = null; let _envOverrides: Record = {}; /** * Returns the isolated Claude config directory under {clopenDir}/engine/claude/user/ */ export function getClaudeUserConfigDir(): string { return getEngineUserConfigDir('claude'); } /** * Resets environment state so the next setupEnvironmentOnce() re-reads * the active account token from DB. Called after account switch/delete. */ export function resetEnvironment(): void { _ready = false; _initPromise = null; _envOverrides = {}; } /** * Idempotent, concurrency-safe environment setup. * Multiple concurrent calls share a single initialization promise. */ export async function setupEnvironmentOnce(): Promise { if (_ready) return; if (_initPromise) return _initPromise; _initPromise = _doSetup(); try { await _initPromise; } catch (error) { _initPromise = null; throw error; } } /** * Returns the env dictionary for SDK Options.env. * Merges process.env with our overrides so the SDK subprocess * inherits everything it needs. * * When accountId is provided, overrides the OAuth token with that * specific account's token instead of the globally active account. */ export function getEngineEnv(accountId?: number): Record { // Start from clean env (no Bun/npm/Vite pollution) const env = getCleanSpawnEnv(); // Apply our overrides Object.assign(env, _envOverrides); // Override with specific account token if requested if (accountId !== undefined) { try { const account = engineQueries.getAccount(accountId); if (account) { env['CLAUDE_CODE_OAUTH_TOKEN'] = account.credential; debug.log('engine', `Claude Code: Per-session account override → "${account.name}"`); } } catch { // Ignore — fall back to default token from overrides } } return env; } async function _doSetup(): Promise { const overrides: Record = {}; // Bypass permissions for Claude Code overrides['CLAUDE_CODE_PERMISSION_MODE'] = 'bypassPermissions'; // Isolate Claude config to ~/.clopen/claude/user/ const claudeUserDir = getClaudeUserConfigDir(); await ensureDirectory(claudeUserDir); overrides['CLAUDE_CONFIG_DIR'] = claudeUserDir; // Inject OAuth token from active account (if any) try { const activeAccount = engineQueries.getActiveAccountForEngine('claude-code'); if (activeAccount) { overrides['CLAUDE_CODE_OAUTH_TOKEN'] = activeAccount.credential; debug.log('engine', `✅ Claude Code: Using account "${activeAccount.name}"`); } } catch { // DB may not be initialized yet during first startup debug.warn('engine', '⚠️ Claude Code: Could not read active account (DB may not be ready)'); } // Terminal environment variables overrides['FORCE_COLOR'] = '1'; overrides['COLORTERM'] = 'truecolor'; if (!process.env.TERM) { overrides['TERM'] = 'xterm-256color'; } _envOverrides = overrides; _ready = true; debug.log('engine', '✅ Environment configured (one-time setup)'); } async function ensureDirectory(dirPath: string): Promise { try { await Bun.file(dirPath).stat(); } catch { // Directory doesn't exist, create via temp file workaround const tempFile = join(dirPath, '.init'); await Bun.write(tempFile, ''); try { if (await Bun.file(tempFile).exists()) { if (process.platform === 'win32') { await Bun.spawn(['cmd', '/c', 'del', '/f', '/q', tempFile.replace(/\//g, '\\')], { stdout: 'ignore', stderr: 'ignore' }).exited; } else { await Bun.spawn(['rm', '-f', tempFile], { stdout: 'ignore', stderr: 'ignore' }).exited; } } } catch { // Ignore cleanup errors } } }