UNPKG

claude-flow-multilang

Version:

Revolutionary multilingual AI orchestration framework with cultural awareness and DDD architecture

490 lines (409 loc) 12.9 kB
import * as process from 'node:process'; /** * Native terminal adapter implementation */ import { spawn, ChildProcess } from 'child_process'; import { platform } from 'os'; import type { ITerminalAdapter, Terminal } from './base.js'; import type { ILogger } from '../../core/logger.js'; import { TerminalError, TerminalCommandError } from '../../utils/errors.js'; import { generateId, delay, timeout, createDeferred } from '../../utils/helpers.js'; /** * Platform-specific shell configuration */ interface ShellConfig { path: string; args: string[]; env?: Record<string, string>; } /** * Native terminal implementation using Deno subprocess */ class NativeTerminal implements Terminal { id: string; pid?: number; private process?: ChildProcess | undefined; private encoder = new TextEncoder(); private decoder = new TextDecoder(); private shell: string; private outputBuffer = ''; private errorBuffer = ''; private commandMarker: string; private commandDeferred?: ReturnType<typeof createDeferred<string>> | undefined; private outputListeners = new Set<(data: string) => void>(); private alive = true; private stdoutData = ''; private stderrData = ''; constructor( shell: string, private logger: ILogger, ) { this.id = generateId('native-term'); this.shell = shell; this.commandMarker = `__CLAUDE_FLOW_${this.id}__`; } async initialize(): Promise<void> { try { const shellConfig = this.getShellConfig(); // Start shell process this.process = spawn(shellConfig.path, shellConfig.args, { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, ...shellConfig.env, CLAUDE_FLOW_TERMINAL: 'true', CLAUDE_FLOW_TERMINAL_ID: this.id, }, }); // Get PID this.pid = this.process.pid; // Set up output handlers this.setupOutputHandlers(); // Monitor process status this.monitorProcess(); // Wait for shell to be ready await this.waitForReady(); this.logger.debug('Native terminal initialized', { id: this.id, pid: this.pid, shell: this.shell, }); } catch (error) { this.alive = false; throw new TerminalError('Failed to create native terminal', { error }); } } async executeCommand(command: string): Promise<string> { if (!this.process || !this.isAlive()) { throw new TerminalError('Terminal is not alive'); } try { // Create deferred for this command this.commandDeferred = createDeferred<string>(); // Clear output buffer this.outputBuffer = ''; // Send command with marker const markedCommand = this.wrapCommand(command); await this.write(markedCommand + '\n'); // Wait for command to complete const output = await timeout( this.commandDeferred.promise, 30000, 'Command execution timeout', ); return output; } catch (error) { throw new TerminalCommandError('Failed to execute command', { command, error }); } } async write(data: string): Promise<void> { if (!this.process || !this.isAlive()) { throw new TerminalError('Terminal is not alive'); } return new Promise((resolve, reject) => { if (!this.process?.stdin) { reject(new TerminalError('Process stdin not available')); return; } this.process.stdin.write(data, (error) => { if (error) { reject(error); } else { resolve(); } }); }); } async read(): Promise<string> { if (!this.process || !this.isAlive()) { throw new TerminalError('Terminal is not alive'); } // Return buffered output const output = this.outputBuffer; this.outputBuffer = ''; return output; } isAlive(): boolean { return this.alive && this.process !== undefined; } async kill(): Promise<void> { if (!this.process) return; try { this.alive = false; // Close streams if (this.process.stdin && !this.process.stdin.destroyed) { this.process.stdin.end(); } // Try graceful shutdown first try { await this.write('exit\n'); await delay(500); } catch { // Ignore write errors during shutdown } // Force kill if still alive try { this.process.kill('SIGTERM'); await delay(500); // Use SIGKILL if SIGTERM didn't work if (!this.process.killed) { this.process.kill('SIGKILL'); } } catch { // Process might already be dead } } catch (error) { this.logger.warn('Error killing native terminal', { id: this.id, error }); } finally { this.process = undefined; } } /** * Add output listener */ addOutputListener(listener: (data: string) => void): void { this.outputListeners.add(listener); } /** * Remove output listener */ removeOutputListener(listener: (data: string) => void): void { this.outputListeners.delete(listener); } private getShellConfig(): ShellConfig { const osplatform = platform(); switch (this.shell) { case 'bash': return { path: osplatform === 'win32' ? 'C:\\Program Files\\Git\\bin\\bash.exe' : '/bin/bash', args: ['--norc', '--noprofile'], env: { PS1: '$ ' }, }; case 'zsh': return { path: '/bin/zsh', args: ['--no-rcs'], env: { PS1: '$ ' }, }; case 'powershell': return { path: osplatform === 'win32' ? 'powershell.exe' : 'pwsh', args: ['-NoProfile', '-NonInteractive', '-NoLogo'], }; case 'cmd': return { path: 'cmd.exe', args: ['/Q', '/K', 'prompt $G'], }; case 'sh': default: return { path: '/bin/sh', args: [], env: { PS1: '$ ' }, }; } } private wrapCommand(command: string): string { const osplatform = platform(); if (this.shell === 'powershell') { // PowerShell command wrapping return `${command}; Write-Host "${this.commandMarker}"`; } else if (this.shell === 'cmd' && osplatform === 'win32') { // Windows CMD command wrapping return `${command} & echo ${this.commandMarker}`; } else { // Unix-like shell command wrapping return `${command} && echo "${this.commandMarker}" || (echo "${this.commandMarker}"; false)`; } } private setupOutputHandlers(): void { if (!this.process) return; // Handle stdout this.process.stdout?.on('data', (data: Buffer) => { const text = data.toString(); this.processOutput(text); }); // Handle stderr this.process.stderr?.on('data', (data: Buffer) => { const text = data.toString(); this.errorBuffer += text; // Also send stderr to output listeners this.notifyListeners(text); }); // Handle process errors this.process.on('error', (error) => { if (this.alive) { this.logger.error('Process error', { id: this.id, error }); } }); } private processOutput(text: string): void { this.outputBuffer += text; // Notify listeners this.notifyListeners(text); // Check for command completion marker const markerIndex = this.outputBuffer.indexOf(this.commandMarker); if (markerIndex !== -1 && this.commandDeferred) { // Extract output before marker const output = this.outputBuffer.substring(0, markerIndex).trim(); // Include any stderr output const fullOutput = this.errorBuffer ? `${output}\n${this.errorBuffer}` : output; this.errorBuffer = ''; // Clear buffer up to after marker this.outputBuffer = this.outputBuffer .substring(markerIndex + this.commandMarker.length) .trim(); // Resolve pending command this.commandDeferred.resolve(fullOutput); this.commandDeferred = undefined; } } private notifyListeners(data: string): void { this.outputListeners.forEach((listener) => { try { listener(data); } catch (error) { this.logger.error('Error in output listener', { id: this.id, error }); } }); } private async monitorProcess(): Promise<void> { if (!this.process) return; this.process.on('exit', (code, signal) => { this.logger.info('Terminal process exited', { id: this.id, code, signal, }); this.alive = false; // Reject any pending command if (this.commandDeferred) { this.commandDeferred.reject(new Error('Terminal process exited')); } }); this.process.on('error', (error) => { this.logger.error('Error monitoring process', { id: this.id, error }); this.alive = false; // Reject any pending command if (this.commandDeferred) { this.commandDeferred.reject(error); } }); } private async waitForReady(): Promise<void> { // Send a test command to ensure shell is ready const testCommand = this.shell === 'powershell' ? 'Write-Host "READY"' : 'echo "READY"'; await this.write(testCommand + '\n'); const startTime = Date.now(); while (Date.now() - startTime < 5000) { if (this.outputBuffer.includes('READY')) { this.outputBuffer = ''; return; } await delay(100); } throw new TerminalError('Terminal failed to become ready'); } } /** * Native terminal adapter */ export class NativeAdapter implements ITerminalAdapter { private terminals = new Map<string, NativeTerminal>(); private shell: string; constructor(private logger: ILogger) { // Detect available shell this.shell = this.detectShell(); } async initialize(): Promise<void> { this.logger.info('Initializing native terminal adapter', { shell: this.shell }); // Verify shell is available try { const testConfig = this.getTestCommand(); const { spawnSync } = require('child_process'); const result = spawnSync(testConfig.cmd, testConfig.args, { stdio: 'ignore' }); if (result.status !== 0) { throw new Error('Shell test failed'); } } catch (error) { this.logger.warn(`Shell ${this.shell} not available, falling back to sh`, { error }); this.shell = 'sh'; } } async shutdown(): Promise<void> { this.logger.info('Shutting down native terminal adapter'); // Kill all terminals const terminals = Array.from(this.terminals.values()); await Promise.all(terminals.map((term) => term.kill())); this.terminals.clear(); } async createTerminal(): Promise<Terminal> { const terminal = new NativeTerminal(this.shell, this.logger); await terminal.initialize(); this.terminals.set(terminal.id, terminal); return terminal; } async destroyTerminal(terminal: Terminal): Promise<void> { await terminal.kill(); this.terminals.delete(terminal.id); } private detectShell(): string { const osplatform = platform(); if (osplatform === 'win32') { // Windows shell detection const comspec = process.env.COMSPEC; if (comspec?.toLowerCase().includes('powershell')) { return 'powershell'; } // Check if PowerShell is available try { const { spawnSync } = require('child_process'); const result = spawnSync('powershell', ['-Version'], { stdio: 'ignore' }); if (result.status === 0) { return 'powershell'; } } catch { // PowerShell not available } return 'cmd'; } else { // Unix-like shell detection const shell = process.env.SHELL; if (shell) { const shellName = shell.split('/').pop(); if (shellName && this.isShellSupported(shellName)) { return shellName; } } // Try common shells in order of preference const shells = ['bash', 'zsh', 'sh']; for (const shell of shells) { try { const { spawnSync } = require('child_process'); const result = spawnSync('which', [shell], { stdio: 'ignore' }); if (result.status === 0) { return shell; } } catch { // Continue to next shell } } // Default to sh return 'sh'; } } private isShellSupported(shell: string): boolean { return ['bash', 'zsh', 'sh', 'fish', 'dash', 'powershell', 'cmd'].includes(shell); } private getTestCommand(): { cmd: string; args: string[] } { switch (this.shell) { case 'powershell': return { cmd: 'powershell', args: ['-Version'] }; case 'cmd': return { cmd: 'cmd', args: ['/C', 'echo test'] }; default: return { cmd: this.shell, args: ['--version'] }; } } }