Skip to main content
Glama
terminal-controller.ts8.3 kB
/** * Terminal Controller * * Handles terminal command execution with proper error handling, * timeout management, and cross-platform compatibility. */ import { spawn, exec } from 'child_process'; import { promisify } from 'util'; import path from 'path'; const execAsync = promisify(exec); export interface CommandResult { success: boolean; stdout: string; stderr: string; exitCode: number; executionTime: number; command: string; } export interface CommandOptions { cwd?: string; timeout?: number; env?: Record<string, string>; shell?: boolean; encoding?: BufferEncoding; } export class TerminalController { private defaultTimeout: number = 30000; // 30 seconds private defaultOptions: CommandOptions = { shell: true, encoding: 'utf8' }; /** * Execute command with promise-based interface */ async executeCommand( command: string, args: string[] = [], options: CommandOptions = {} ): Promise<CommandResult> { const startTime = Date.now(); const mergedOptions = { ...this.defaultOptions, ...options, timeout: options.timeout || this.defaultTimeout }; return new Promise<CommandResult>((resolve) => { const child = spawn(command, args, { cwd: mergedOptions.cwd, env: { ...process.env, ...mergedOptions.env }, shell: false }); let stdout = ''; let stderr = ''; child.stdout?.on('data', (data) => { stdout += data.toString(); }); child.stderr?.on('data', (data) => { stderr += data.toString(); }); let finished = false; const finish = (code: number | null) => { if (finished) return; finished = true; resolve({ success: code === 0, stdout: stdout.trim(), stderr: stderr.trim(), exitCode: code === null ? 1 : code, executionTime: Date.now() - startTime, command: `${command} ${args.join(' ')}` }); }; child.on('close', (code) => finish(code)); child.on('error', (err) => { stderr += err.message; finish(1); }); // Handle timeout if (mergedOptions.timeout) { setTimeout(() => { try { child.kill('SIGTERM'); } catch {} finish(1); }, mergedOptions.timeout); } }); } /** * Execute command in specific directory */ async executeInDirectory( directory: string, command: string, args: string[] = [], options: CommandOptions = {} ): Promise<CommandResult> { const resolvedPath = path.resolve(directory); return this.executeCommand(command, args, { ...options, cwd: resolvedPath }); } /** * Check if command exists in system PATH */ async commandExists(command: string): Promise<boolean> { try { const args = process.platform === 'win32' ? [command] : [command]; const cmd = process.platform === 'win32' ? 'where' : 'which'; const result = await this.executeCommand(cmd, args); return result.success && result.stdout.length > 0; } catch { return false; } } /** * Get current working directory */ getCurrentDirectory(): string { return process.cwd(); } /** * Change working directory for subsequent commands */ setWorkingDirectory(directory: string): void { process.chdir(path.resolve(directory)); } /** * Execute multiple commands in sequence */ async executeSequence( commands: Array<{ command: string; args?: string[]; options?: CommandOptions }>, stopOnError: boolean = true ): Promise<CommandResult[]> { const results: CommandResult[] = []; for (const cmd of commands) { const result = await this.executeCommand( cmd.command, cmd.args || [], cmd.options || {} ); results.push(result); if (!result.success && stopOnError) { break; } } return results; } /** * Execute command with real-time output streaming */ async executeWithStreaming( command: string, args: string[] = [], options: CommandOptions & { onStdout?: (data: string) => void; onStderr?: (data: string) => void; } = {} ): Promise<CommandResult> { const startTime = Date.now(); const fullCommand = `${command} ${args.join(' ')}`; return new Promise((resolve) => { const child = spawn(command, args, { cwd: options.cwd, env: { ...process.env, ...options.env }, shell: options.shell !== false }); let stdout = ''; let stderr = ''; child.stdout?.on('data', (data) => { const text = data.toString(); stdout += text; options.onStdout?.(text); }); child.stderr?.on('data', (data) => { const text = data.toString(); stderr += text; options.onStderr?.(text); }); child.on('close', (code) => { resolve({ success: code === 0, stdout: stdout.trim(), stderr: stderr.trim(), exitCode: code || 0, executionTime: Date.now() - startTime, command: fullCommand }); }); child.on('error', (error) => { resolve({ success: false, stdout: stdout.trim(), stderr: error.message, exitCode: 1, executionTime: Date.now() - startTime, command: fullCommand }); }); // Handle timeout if (options.timeout) { setTimeout(() => { child.kill('SIGTERM'); }, options.timeout); } }); } /** * Search for text in files using grep or findstr */ async searchInFiles( directory: string, query: string, options: { caseSensitive?: boolean; filePattern?: string; maxResults?: number; } = {} ): Promise<CommandResult> { const isWindows = process.platform === 'win32'; if (isWindows) { // Use findstr on Windows const args: string[] = ['/S', '/N']; if (!options.caseSensitive) args.push('/I'); const filePattern = options.filePattern && options.filePattern !== '*' ? options.filePattern : '*'; // findstr usage: findstr [options] "searchString" filespec args.push(query); args.push(filePattern); const result = await this.executeInDirectory(directory, 'findstr', args); // findstr returns exit code 1 when no matches are found — treat as success with empty output if (result.exitCode === 1 && !result.stdout.trim()) { return { success: true, stdout: '', stderr: '', exitCode: 0, executionTime: result.executionTime, command: result.command }; } return result; } else { // Use grep on Unix-like systems const args = ['-r', '-n']; if (!options.caseSensitive) args.push('-i'); if (options.filePattern && options.filePattern !== '*') { args.push('--include', options.filePattern); } args.push(query, '.'); const result = await this.executeInDirectory(directory, 'grep', args); // grep exit code 1 means no matches — normalize to success with empty output if (result.exitCode === 1 && !result.stdout.trim()) { return { success: true, stdout: '', stderr: '', exitCode: 0, executionTime: result.executionTime, command: result.command }; } return result; } } /** * Validate directory exists and is accessible */ async validateDirectory(directory: string): Promise<{ exists: boolean; accessible: boolean; isDirectory: boolean; error?: string; }> { try { const fs = await import('fs/promises'); const stats = await fs.stat(directory); return { exists: true, accessible: true, isDirectory: stats.isDirectory(), }; } catch (error: any) { return { exists: false, accessible: false, isDirectory: false, error: error.message }; } } } // Export singleton instance export const terminalController = new TerminalController();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Andre-Buzeli/git-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server