Skip to main content
Glama
shell.ts5.31 kB
/** * Safe shell command execution with timeout handling */ import { spawn, SpawnOptions } from 'child_process'; import { Errors } from '../models/errors.js'; import { DEFAULTS } from '../models/constants.js'; export interface ShellResult { stdout: string; stderr: string; exitCode: number; } export interface BinaryShellResult { stdout: Buffer; stderr: string; exitCode: number; } export interface ShellOptions { timeoutMs?: number; cwd?: string; env?: Record<string, string>; silent?: boolean; } /** * Execute a shell command with timeout and proper error handling */ export async function executeShell( command: string, args: string[] = [], options: ShellOptions = {} ): Promise<ShellResult> { const { timeoutMs = DEFAULTS.SHELL_TIMEOUT_MS, cwd, env, silent = false } = options; return new Promise((resolve, reject) => { let stdout = ''; let stderr = ''; let killed = false; const spawnOptions: SpawnOptions = { cwd, env: { ...process.env, ...env }, shell: false, }; const child = spawn(command, args, spawnOptions); // Set up timeout const timeout = setTimeout(() => { killed = true; child.kill('SIGTERM'); // Give process time to clean up, then force kill setTimeout(() => { if (!child.killed) { child.kill('SIGKILL'); } }, 1000); }, timeoutMs); child.stdout?.on('data', (data: Buffer) => { stdout += data.toString(); }); child.stderr?.on('data', (data: Buffer) => { stderr += data.toString(); }); child.on('error', (error: Error) => { clearTimeout(timeout); reject( Errors.shellExecutionFailed( `${command} ${args.join(' ')}`, error.message ) ); }); child.on('close', (code: number | null) => { clearTimeout(timeout); if (killed) { reject(Errors.timeout(`${command} ${args.join(' ')}`, timeoutMs)); return; } const exitCode = code ?? 1; // Log if not silent and there's stderr if (!silent && stderr && exitCode !== 0) { console.error(`[shell] Command failed: ${command} ${args.join(' ')}`); console.error(`[shell] stderr: ${stderr.slice(0, 500)}`); } resolve({ stdout: stdout.trim(), stderr: stderr.trim(), exitCode, }); }); }); } /** * Execute a shell command returning binary stdout (for screenshots, etc.) */ export async function executeShellBinary( command: string, args: string[] = [], options: ShellOptions = {} ): Promise<BinaryShellResult> { const { timeoutMs = DEFAULTS.SHELL_TIMEOUT_MS, cwd, env } = options; return new Promise((resolve, reject) => { const chunks: Buffer[] = []; let stderr = ''; let killed = false; const spawnOptions: SpawnOptions = { cwd, env: { ...process.env, ...env }, shell: false, }; const child = spawn(command, args, spawnOptions); const timeout = setTimeout(() => { killed = true; child.kill('SIGTERM'); setTimeout(() => { if (!child.killed) child.kill('SIGKILL'); }, 1000); }, timeoutMs); child.stdout?.on('data', (data: Buffer) => { chunks.push(data); }); child.stderr?.on('data', (data: Buffer) => { stderr += data.toString(); }); child.on('error', (error: Error) => { clearTimeout(timeout); reject(Errors.shellExecutionFailed(`${command} ${args.join(' ')}`, error.message)); }); child.on('close', (code: number | null) => { clearTimeout(timeout); if (killed) { reject(Errors.timeout(`${command} ${args.join(' ')}`, timeoutMs)); return; } resolve({ stdout: Buffer.concat(chunks), stderr: stderr.trim(), exitCode: code ?? 1, }); }); }); } /** * Execute a shell command and throw if it fails */ export async function executeShellOrThrow( command: string, args: string[] = [], options: ShellOptions = {} ): Promise<ShellResult> { const result = await executeShell(command, args, options); if (result.exitCode !== 0) { throw Errors.shellExecutionFailed( `${command} ${args.join(' ')}`, result.stderr || `Exit code: ${result.exitCode}` ); } return result; } /** * Check if a command exists in PATH */ export async function commandExists(command: string): Promise<boolean> { try { const result = await executeShell('which', [command], { silent: true }); return result.exitCode === 0 && result.stdout.length > 0; } catch { return false; } } /** * Parse command output into lines, filtering empty lines */ export function parseLines(output: string): string[] { return output .split('\n') .map((line) => line.trim()) .filter((line) => line.length > 0); } /** * Execute multiple commands in sequence, stopping on first failure */ export async function executeSequence( commands: Array<{ command: string; args: string[]; options?: ShellOptions }> ): Promise<ShellResult[]> { const results: ShellResult[] = []; for (const { command, args, options } of commands) { const result = await executeShellOrThrow(command, args, options); results.push(result); } return results; }

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/abd3lraouf/specter-mcp'

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