Skip to main content
Glama
godot_shell.ts4.06 kB
import { spawn, ChildProcess } from 'child_process'; import path from 'path'; import fs from 'fs/promises'; import { ParserGodot4, ErrorObject } from './parser_godot4'; export interface TestResult { pass: boolean; log: string; first_error?: ErrorObject; } export interface GameResult { pid: number; started: boolean; } export class GodotShell { private projectRoot: string; private parser: ParserGodot4; private sentinelDir: string; private errorCallback?: (error: ErrorObject) => void; constructor(projectRoot: string) { this.projectRoot = projectRoot; this.parser = new ParserGodot4(); this.sentinelDir = path.join(process.env.HOME || '', '.sentinel'); } public onError(callback: (error: ErrorObject) => void): void { this.errorCallback = callback; } public async runTests(): Promise<TestResult> { const logPath = path.join(this.sentinelDir, 'last_run.log'); try { // Run gdUnit4 headless tests const output = await this.executeCommand([ 'godot', '--headless', '-s', 'res://addons/gdUnit4/bin/gdUnit4.gd', '-gexit', '--verbose' ], this.projectRoot); // Write log to file await fs.writeFile(logPath, output); // Parse first error const firstError = this.parser.parseFirstError(output); const pass = !firstError && !output.toLowerCase().includes('failed'); if (firstError && this.errorCallback) { this.errorCallback(firstError); } return { pass, log: output, first_error: firstError }; } catch (error) { const errorLog = error instanceof Error ? error.message : String(error); await fs.writeFile(logPath, errorLog); return { pass: false, log: errorLog, first_error: this.parser.parseFirstError(errorLog) }; } } public async runGame(opts?: { scene?: string }): Promise<GameResult> { try { const args = ['godot']; if (opts?.scene) { args.push('--scene', opts.scene); } const child = spawn(args[0], args.slice(1), { cwd: this.projectRoot, detached: true, stdio: ['ignore', 'pipe', 'pipe'] }); if (child.stdout && child.stderr) { // Monitor output for errors child.stdout.on('data', (data) => { this.monitorOutput(data.toString()); }); child.stderr.on('data', (data) => { this.monitorOutput(data.toString()); }); } // Give the process a moment to start await new Promise(resolve => setTimeout(resolve, 1000)); return { pid: child.pid || 0, started: !child.killed }; } catch (error) { return { pid: 0, started: false }; } } private async executeCommand(args: string[], cwd: string): Promise<string> { return new Promise((resolve, reject) => { const child = spawn(args[0], args.slice(1), { cwd }); let stdout = ''; let stderr = ''; if (child.stdout) { child.stdout.on('data', (data) => { stdout += data.toString(); }); } if (child.stderr) { child.stderr.on('data', (data) => { stderr += data.toString(); }); } child.on('close', (code) => { const output = stdout + stderr; if (code === 0 || output.includes('All tests passed')) { resolve(output); } else { reject(new Error(output || `Process exited with code ${code}`)); } }); child.on('error', reject); }); } private monitorOutput(output: string): void { const error = this.parser.parseFirstError(output); if (error && this.errorCallback) { this.errorCallback(error); } } public async getLastLog(): Promise<string> { try { const logPath = path.join(this.sentinelDir, 'last_run.log'); return await fs.readFile(logPath, 'utf8'); } catch (error) { return ''; } } }

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/Snack-JPG/Godot-Sentinel-MCP'

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