UNPKG

claude-flow-tbowman01

Version:

Enterprise-grade AI agent orchestration with ruv-swarm integration (Alpha Release)

414 lines 14 kB
import * as process from 'node:process'; /** * Native terminal adapter implementation */ import { spawn } from 'child_process'; import { platform } from 'os'; import { TerminalError, TerminalCommandError } from '../../utils/errors.js'; import { generateId, delay, timeout, createDeferred } from '../../utils/helpers.js'; /** * Native terminal implementation using Deno subprocess */ class NativeTerminal { logger; id; pid; process; encoder = new TextEncoder(); decoder = new TextDecoder(); shell; outputBuffer = ''; errorBuffer = ''; commandMarker; commandDeferred; outputListeners = new Set(); alive = true; stdoutData = ''; stderrData = ''; constructor(shell, logger) { this.logger = logger; this.id = generateId('native-term'); this.shell = shell; this.commandMarker = `__CLAUDE_FLOW_${this.id}__`; } async initialize() { 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) { if (!this.process || !this.isAlive()) { throw new TerminalError('Terminal is not alive'); } try { // Create deferred for this command this.commandDeferred = createDeferred(); // 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) { 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() { if (!this.process || !this.isAlive()) { throw new TerminalError('Terminal is not alive'); } // Return buffered output const output = this.outputBuffer; this.outputBuffer = ''; return output; } isAlive() { return this.alive && this.process !== undefined; } async kill() { 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) { this.outputListeners.add(listener); } /** * Remove output listener */ removeOutputListener(listener) { this.outputListeners.delete(listener); } getShellConfig() { 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: '$ ' }, }; } } wrapCommand(command) { 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)`; } } setupOutputHandlers() { if (!this.process) return; // Handle stdout this.process.stdout?.on('data', (data) => { const text = data.toString(); this.processOutput(text); }); // Handle stderr this.process.stderr?.on('data', (data) => { 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 }); } }); } processOutput(text) { 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; } } notifyListeners(data) { this.outputListeners.forEach(listener => { try { listener(data); } catch (error) { this.logger.error('Error in output listener', { id: this.id, error }); } }); } async monitorProcess() { 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); } }); } async waitForReady() { // 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 { logger; terminals = new Map(); shell; constructor(logger) { this.logger = logger; // Detect available shell this.shell = this.detectShell(); } async initialize() { 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() { 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() { const terminal = new NativeTerminal(this.shell, this.logger); await terminal.initialize(); this.terminals.set(terminal.id, terminal); return terminal; } async destroyTerminal(terminal) { await terminal.kill(); this.terminals.delete(terminal.id); } detectShell() { 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'; } } isShellSupported(shell) { return ['bash', 'zsh', 'sh', 'fish', 'dash', 'powershell', 'cmd'].includes(shell); } getTestCommand() { 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'] }; } } } //# sourceMappingURL=native.js.map