import EventEmitter from 'events'; import pty from 'node-pty'; class TerminalService extends EventEmitter { constructor() { super(); this.lines = []; this.partial = ''; this.ptyProcess = null; this.started = false; this.maxLines = 1000; } start() { if (this.started) return; const shell = process.platform === 'win32' ? 'powershell.exe' : 'bash'; const cols = process.stdout && process.stdout.columns ? process.stdout.columns : 120; const rows = process.stdout && process.stdout.rows ? process.stdout.rows : 30; const isWindows = process.platform === 'win32'; const userShell = process.env.SHELL && !isWindows ? process.env.SHELL : null; const shellPath = userShell || (isWindows ? 'powershell.exe' : '/bin/bash'); const args = ['--rcfile','rc']; this.ptyProcess = pty.spawn(shellPath, args, { name: 'xterm-256color', cols, rows, cwd: process.cwd(), env: { ...process.env, TERM: 'xterm-256color', PS1: '> ' }, }); this.ptyProcess.onData((data) => { // Normalize line endings const normalized = String(data).replace(/\r\n/g, '\n').replace(/\r/g, '\n'); const parts = normalized.split('\n'); // First part joins the existing partial if (parts.length > 0) { this.partial += parts[0]; } // For each subsequent part before the last, we have completed lines for (let i = 1; i < parts.length; i += 1) { this.lines.push(this.partial); this.partial = parts[i]; } // Enforce max lines buffer if (this.lines.length > this.maxLines) { this.lines.splice(0, this.lines.length - this.maxLines); } // Emit lines including current partial to ensure prompts (no trailing newline) are visible const display = this.partial ? [...this.lines, this.partial] : this.lines.slice(); this.emit('update', display); }); // Resize with the host TTY const onResize = () => { try { const newCols = process.stdout.columns || cols; const newRows = process.stdout.rows || rows; this.ptyProcess.resize(newCols, newRows); } catch { // ignore } }; if (process.stdout && process.stdout.on) { process.stdout.on('resize', onResize); } this.ptyProcess.onExit(({ exitCode, signal }) => { this.emit('exit', { exitCode, signal }); }); this.started = true; } getLines() { return this.lines.slice(); } write(input) { if (!this.ptyProcess) return; this.ptyProcess.write(input); } resize(columns, rows) { if (!this.ptyProcess) return; try { const cols = Math.max(1, Number(columns) || 1); const r = rows ? Math.max(1, Number(rows) || 1) : undefined; if (r !== undefined) { this.ptyProcess.resize(cols, r); } else { this.ptyProcess.resize(cols, this.ptyProcess.rows || 24); } } catch { // ignore } } dispose() { try { if (this.ptyProcess) { this.ptyProcess.kill(); this.ptyProcess = null; } } catch { // ignore } } } const terminalService = new TerminalService(); export default terminalService;