Skip to main content
Glama
template-resolver.ts11.8 kB
/** * TemplateResolver * Implementation for AC 6.10, 6.11, 6.12 - Template variable expansion and conditional logic * * Resolves template strings with variable substitution and conditional expressions. * Supports nested references, command execution, and complex conditional logic. * * CRITICAL: No mocks - uses real command execution and variable resolution. */ import * as fs from 'fs'; import { exec } from 'child_process'; import { promisify } from 'util'; import { EnvironmentValues } from './environment-value-provider'; const execAsync = promisify(exec); /** * Template resolution configuration */ export interface TemplateResolverConfig { maxRecursionDepth?: number; // Maximum recursion depth for nested variables (default: 10) commandTimeout?: number; // Timeout for command execution (default: 5000ms) } /** * TemplateResolver class */ export class TemplateResolver { private readonly environmentValues: EnvironmentValues; private readonly customVariables: Record<string, string>; private readonly config: Required<TemplateResolverConfig>; private readonly resolutionStack: Set<string> = new Set(); private recursionDepth: number = 0; constructor( environmentValues: EnvironmentValues, customVariables: Record<string, string> = {}, config: TemplateResolverConfig = {} ) { this.environmentValues = environmentValues; this.customVariables = customVariables; this.config = { maxRecursionDepth: config.maxRecursionDepth || 10, commandTimeout: config.commandTimeout || 5000 }; } /** * Resolve template string with variable substitution * AC 6.10: Template variable expansion system */ async resolve(template: string): Promise<string> { this.resolutionStack.clear(); this.recursionDepth = 0; return await this.resolveInternal(template); } /** * Internal template resolution with recursion protection */ private async resolveInternal(template: string): Promise<string> { if (this.recursionDepth > this.config.maxRecursionDepth) { throw new Error(`Maximum recursion depth exceeded (${this.config.maxRecursionDepth})`); } // Find all template variables in format ${VARIABLE_NAME} const variablePattern = /\$\{([^}]+)\}/g; let result = template; let match; let hasReplacements = true; // Keep resolving until no more variables are found while (hasReplacements) { hasReplacements = false; const matches: Array<{fullMatch: string, expression: string, index: number}> = []; // Collect all matches first while ((match = variablePattern.exec(result)) !== null) { matches.push({ fullMatch: match[0], expression: match[1], index: match.index }); } // Reset regex for next iteration variablePattern.lastIndex = 0; // Process matches in reverse order to maintain correct indices for (let i = matches.length - 1; i >= 0; i--) { const { fullMatch, expression } = matches[i]; // Check for circular references if (this.resolutionStack.has(fullMatch)) { throw new Error(`Circular reference detected: ${fullMatch}`); } try { this.resolutionStack.add(fullMatch); this.recursionDepth++; const resolvedValue = await this.resolveExpression(expression); result = result.replace(fullMatch, resolvedValue); hasReplacements = true; this.recursionDepth--; this.resolutionStack.delete(fullMatch); } catch (error) { this.resolutionStack.delete(fullMatch); this.recursionDepth--; throw error; } } } // Handle escaped variables result = result.replace(/\\(\$\{[^}]+\})/g, '$1'); return result; } /** * Resolve individual expression (variable, conditional, or command) */ private async resolveExpression(expression: string): Promise<string> { // Check for conditional expressions (AC 6.12) if (this.isConditionalExpression(expression)) { return await this.resolveConditional(expression); } // Check for pattern syntax if (expression.startsWith('pattern:')) { return expression; // Return as-is for pattern matching } // Check for file existence check if (expression.startsWith('FILE_EXISTS:')) { const filename = expression.substring(12); const exists = fs.existsSync(filename); return exists.toString(); } // Check for process existence check if (expression.startsWith('PROCESS_EXISTS:')) { const processName = expression.substring(15); return await this.checkProcessExists(processName); } // Check for command execution if (expression.startsWith('exec:')) { const command = expression.substring(5).trim(); const { stdout } = await execAsync(command, { timeout: this.config.commandTimeout }); return stdout.trim(); } // Check custom variables first if (this.customVariables.hasOwnProperty(expression)) { const customValue = this.customVariables[expression]; // Handle exec: commands in custom variables if (customValue.startsWith('exec:')) { const command = customValue.substring(5).trim(); try { const { stdout } = await execAsync(command, { timeout: this.config.commandTimeout }); return stdout.trim(); } catch (error) { throw new Error(`Command execution failed: ${error instanceof Error ? error.message : String(error)}`); } } // Recursively resolve if it contains template variables return await this.resolveInternal(customValue); } // Check environment values if (this.environmentValues.hasOwnProperty(expression)) { const value = this.environmentValues[expression]; if (Array.isArray(value)) { return JSON.stringify(value); } return value.toString(); } throw new Error(`Unknown template variable: ${expression}`); } /** * Check if expression is a conditional * AC 6.12: Conditional template resolution */ private isConditionalExpression(expression: string): boolean { return expression.includes('?') && expression.includes(':'); } /** * Resolve conditional expression * Format: condition ? true_value : false_value */ private async resolveConditional(expression: string): Promise<string> { const questionIndex = expression.indexOf('?'); const colonIndex = expression.lastIndexOf(':'); if (questionIndex === -1 || colonIndex === -1 || colonIndex <= questionIndex) { throw new Error(`Invalid conditional syntax: ${expression}`); } const condition = expression.substring(0, questionIndex).trim(); const trueValue = expression.substring(questionIndex + 1, colonIndex).trim(); const falseValue = expression.substring(colonIndex + 1).trim(); const conditionResult = await this.evaluateCondition(condition); if (conditionResult) { return await this.resolveConditionalValue(trueValue); } else { return await this.resolveConditionalValue(falseValue); } } /** * Resolve conditional value (handles quoted strings vs variables) */ private async resolveConditionalValue(value: string): Promise<string> { // If it's a quoted string, remove quotes and return as literal if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { return value.slice(1, -1); } // Otherwise, treat as template and resolve return await this.resolveInternal(value); } /** * Evaluate boolean condition */ private async evaluateCondition(condition: string): Promise<boolean> { // Handle file existence conditions if (condition.startsWith('FILE_EXISTS:')) { const filename = condition.substring(12); return fs.existsSync(filename); } // Handle equality conditions if (condition.includes('===')) { const [left, right] = condition.split('===').map(s => s.trim()); const leftValue = await this.resolveVariableValue(left); const rightValue = await this.resolveVariableValue(right); return leftValue === rightValue; } // Handle inequality conditions if (condition.includes('!==')) { const [left, right] = condition.split('!==').map(s => s.trim()); const leftValue = await this.resolveVariableValue(left); const rightValue = await this.resolveVariableValue(right); return leftValue !== rightValue; } // Handle contains conditions if (condition.includes('.includes(')) { const match = condition.match(/(\w+)\.includes\(([^)]+)\)/); if (match) { const varName = match[1]; const searchValue = match[2].replace(/['"]/g, ''); // Remove quotes const varValue = await this.resolveVariableValue(varName); if (Array.isArray(this.environmentValues[varName])) { return this.environmentValues[varName].includes(searchValue); } return varValue.includes(searchValue); } } // Handle length comparisons if (condition.includes('.length')) { const match = condition.match(/(\w+)\.length\s*([><=!]+)\s*(\d+)/); if (match) { const varName = match[1]; const operator = match[2]; const threshold = parseInt(match[3]); const varValue = this.environmentValues[varName]; let length = 0; if (Array.isArray(varValue)) { length = varValue.length; } else if (typeof varValue === 'string') { length = varValue.length; } switch (operator) { case '>': return length > threshold; case '>=': return length >= threshold; case '<': return length < threshold; case '<=': return length <= threshold; case '===': return length === threshold; case '!==': return length !== threshold; default: return false; } } } throw new Error(`Unsupported condition syntax: ${condition}`); } /** * Resolve variable value for condition evaluation */ private async resolveVariableValue(variable: string): Promise<string> { // Remove quotes if present const cleanVar = variable.replace(/^["']|["']$/g, ''); // If it's a quoted string, return as-is if (variable.startsWith('"') || variable.startsWith("'")) { return cleanVar; } // Try to resolve as variable try { return await this.resolveExpression(cleanVar); } catch { // If not a variable, return as literal return cleanVar; } } /** * Check if process exists * AC 6.9: Process information dynamic validation */ private async checkProcessExists(processName: string): Promise<string> { try { const { stdout } = await execAsync(`pgrep -f "${processName}"`, { timeout: this.config.commandTimeout }); return (stdout.trim().length > 0).toString(); } catch { return 'false'; } } /** * Validate template syntax */ static validateSyntax(template: string): void { let braceCount = 0; let inVariable = false; for (let i = 0; i < template.length; i++) { const char = template[i]; const nextChar = template[i + 1]; if (char === '$' && nextChar === '{') { braceCount++; inVariable = true; i++; // Skip next character } else if (char === '}' && inVariable) { braceCount--; if (braceCount === 0) { inVariable = false; } } } if (braceCount !== 0) { throw new Error(`Invalid template syntax: unmatched braces in "${template}"`); } } }

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/LightspeedDMS/ssh-mcp'

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