UNPKG

jay-code

Version:

Streamlined AI CLI orchestration engine with mathematical rigor and enterprise-grade reliability

336 lines (284 loc) 9.43 kB
/** * VSCode terminal adapter implementation */ import { platform } from 'os'; import type { ITerminalAdapter, Terminal } from './base.js'; import type { ILogger } from '../../core/logger.js'; import { TerminalError } from '../../utils/errors.js'; import { generateId, delay, timeout, createDeferred } from '../../utils/helpers.js'; /** * VSCode API interface (injected via extension) */ interface VSCodeAPI { window: { createTerminal(options: { name: string; shellPath?: string; shellArgs?: string[]; env?: Record<string, string>; }): VSCodeTerminal; onDidCloseTerminal(listener: (terminal: VSCodeTerminal) => void): { dispose(): void }; }; } interface VSCodeTerminal { name: string; processId: Promise<number | undefined>; sendText(text: string, addNewLine?: boolean): void; show(preserveFocus?: boolean): void; hide(): void; dispose(): void; } /** * VSCode terminal implementation */ class VSCodeTerminalWrapper implements Terminal { id: string; pid?: number; private vscodeTerminal?: VSCodeTerminal; private outputBuffer = ''; private commandMarker: string; private outputDeferred = createDeferred<string>(); private isDisposed = false; constructor( private vscodeApi: VSCodeAPI, private shellType: string, private logger: ILogger, ) { this.id = generateId('vscode-term'); this.commandMarker = `__JAY_CODE_${this.id}__`; } async initialize(): Promise<void> { try { // Create VSCode terminal const shellPath = this.getShellPath(); const terminalOptions: any = { name: `Jay-Code Terminal ${this.id}`, shellArgs: this.getShellArgs(), env: { JAY_CODE_TERMINAL: 'true', JAY_CODE_TERMINAL_ID: this.id, PS1: '$ ', // Simple prompt }, }; if (shellPath !== undefined) { terminalOptions.shellPath = shellPath; } this.vscodeTerminal = this.vscodeApi.window.createTerminal(terminalOptions); // Get process ID const processId = await this.vscodeTerminal.processId; if (processId !== undefined) { this.pid = processId; } // Show terminal (but don't steal focus) this.vscodeTerminal.show(true); // Wait for terminal to be ready await this.waitForReady(); this.logger.debug('VSCode terminal initialized', { id: this.id, pid: this.pid }); } catch (error) { throw new TerminalError('Failed to create VSCode terminal', { error }); } } async executeCommand(command: string): Promise<string> { if (!this.vscodeTerminal || !this.isAlive()) { throw new TerminalError('Terminal is not alive'); } try { // Clear output buffer this.outputBuffer = ''; this.outputDeferred = createDeferred<string>(); // Send command with marker const markedCommand = `${command} && echo "${this.commandMarker}"`; this.vscodeTerminal.sendText(markedCommand, true); // Wait for command to complete const output = await timeout(this.outputDeferred.promise, 30000, 'Command execution timeout'); return output; } catch (error) { throw new TerminalError('Failed to execute command', { command, error }); } } async write(data: string): Promise<void> { if (!this.vscodeTerminal || !this.isAlive()) { throw new TerminalError('Terminal is not alive'); } this.vscodeTerminal.sendText(data, false); } async read(): Promise<string> { if (!this.vscodeTerminal || !this.isAlive()) { throw new TerminalError('Terminal is not alive'); } // Return buffered output const output = this.outputBuffer; this.outputBuffer = ''; return output; } isAlive(): boolean { return !this.isDisposed && this.vscodeTerminal !== undefined; } async kill(): Promise<void> { if (this.vscodeTerminal && !this.isDisposed) { try { // Try graceful shutdown first this.vscodeTerminal.sendText('exit', true); await delay(500); // Dispose terminal this.vscodeTerminal.dispose(); this.isDisposed = true; } catch (error) { this.logger.warn('Error killing VSCode terminal', { id: this.id, error }); } } } /** * Process terminal output (called by extension) */ processOutput(data: string): void { this.outputBuffer += data; // Check for command completion marker const markerIndex = this.outputBuffer.indexOf(this.commandMarker); if (markerIndex !== -1) { // Extract output before marker const output = this.outputBuffer.substring(0, markerIndex).trim(); // Clear buffer up to after marker this.outputBuffer = this.outputBuffer .substring(markerIndex + this.commandMarker.length) .trim(); // Resolve pending command this.outputDeferred.resolve(output); } } private getShellPath(): string | undefined { switch (this.shellType) { case 'bash': return '/bin/bash'; case 'zsh': return '/bin/zsh'; case 'powershell': return platform() === 'win32' ? 'powershell.exe' : 'pwsh'; case 'cmd': return platform() === 'win32' ? 'cmd.exe' : undefined; default: return undefined; } } private getShellArgs(): string[] { switch (this.shellType) { case 'bash': return ['--norc', '--noprofile']; case 'zsh': return ['--no-rcs']; case 'powershell': return ['-NoProfile', '-NonInteractive']; case 'cmd': return ['/Q']; default: return []; } } private async waitForReady(): Promise<void> { // Send a test command to ensure terminal is ready this.vscodeTerminal!.sendText('echo "READY"', true); 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'); } } /** * VSCode terminal adapter */ export class VSCodeAdapter implements ITerminalAdapter { private terminals = new Map<string, VSCodeTerminalWrapper>(); private vscodeApi?: VSCodeAPI; private shellType: string; private terminalCloseListener?: { dispose(): void }; constructor(private logger: ILogger) { this.shellType = this.detectShell(); } async initialize(): Promise<void> { this.logger.info('Initializing VSCode terminal adapter'); // Check if running in VSCode extension context if (!this.isVSCodeExtensionContext()) { throw new TerminalError('Not running in VSCode extension context'); } // Get VSCode API from global this.vscodeApi = (globalThis as any).vscode; if (!this.vscodeApi) { throw new TerminalError('VSCode API not available'); } // Register terminal close listener this.terminalCloseListener = this.vscodeApi.window.onDidCloseTerminal((terminal) => { // Find and clean up closed terminal for (const [id, wrapper] of this.terminals.entries()) { if ((wrapper as any).vscodeTerminal === terminal) { this.logger.info('VSCode terminal closed', { id }); this.terminals.delete(id); break; } } }); this.logger.info('VSCode terminal adapter initialized'); } async shutdown(): Promise<void> { this.logger.info('Shutting down VSCode terminal adapter'); // Dispose listener if (this.terminalCloseListener) { this.terminalCloseListener.dispose(); } // 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> { if (!this.vscodeApi) { throw new TerminalError('VSCode API not initialized'); } const terminal = new VSCodeTerminalWrapper(this.vscodeApi, this.shellType, this.logger); await terminal.initialize(); this.terminals.set(terminal.id, terminal); // Register output processor if extension provides it const outputProcessor = (globalThis as any).registerTerminalOutputProcessor; if (outputProcessor) { outputProcessor(terminal.id, (data: string) => terminal.processOutput(data)); } return terminal; } async destroyTerminal(terminal: Terminal): Promise<void> { await terminal.kill(); this.terminals.delete(terminal.id); } private isVSCodeExtensionContext(): boolean { // Check for VSCode extension environment return ( typeof (globalThis as any).vscode !== 'undefined' && typeof (globalThis as any).vscode.window !== 'undefined' ); } private detectShell(): string { // Get default shell from VSCode settings or environment const osplatform = platform(); if (osplatform === 'win32') { // Windows defaults const comspec = process.env.COMSPEC; if (comspec?.toLowerCase().includes('powershell')) { return 'powershell'; } return 'cmd'; } else { // Unix-like defaults const shell = process.env.SHELL; if (shell) { const shellName = shell.split('/').pop(); if (shellName && ['bash', 'zsh', 'fish', 'sh'].includes(shellName)) { return shellName; } } return 'bash'; } } }