UNPKG

meld

Version:

Meld: A template language for LLM prompts

192 lines (161 loc) 6.49 kB
import type { DirectiveNode, DirectiveContext, MeldNode, TextNode, StructuredPath } from 'meld-spec'; import type { IValidationService } from '@services/resolution/ValidationService/IValidationService.js'; import type { IStateService } from '@services/state/StateService/IStateService.js'; import type { IResolutionService } from '@services/resolution/ResolutionService/IResolutionService.js'; import type { IFileSystemService } from '@services/fs/FileSystemService/IFileSystemService.js'; import { DirectiveError, DirectiveErrorCode, DirectiveErrorSeverity } from '@services/pipeline/DirectiveService/errors/DirectiveError.js'; import { directiveLogger } from '../../../../../core/utils/logger.js'; import type { DirectiveResult } from '@services/pipeline/DirectiveService/types.js'; import type { IDirectiveHandler } from '@services/pipeline/DirectiveService/IDirectiveService.js'; import { ErrorSeverity } from '@core/errors/MeldError.js'; /** * Handler for @run directives * Executes commands and stores their output in state */ export class RunDirectiveHandler implements IDirectiveHandler { readonly kind = 'run'; constructor( private validationService: IValidationService, private resolutionService: IResolutionService, private stateService: IStateService, private fileSystemService: IFileSystemService ) {} async execute(node: DirectiveNode, context: DirectiveContext): Promise<DirectiveResult> { const { directive } = node; const { state } = context; const clonedState = state.clone(); try { // Validate the directive await this.validationService.validate(node); // Properly handle both string commands and command objects from AST const rawCommand = typeof directive.command === 'string' ? directive.command : directive.command.raw; // Resolve the command const resolvedCommand = await this.resolutionService.resolveInContext( rawCommand, context ); // Show feedback that command is running (skips in test env) this.showRunningCommandFeedback(resolvedCommand); try { // Execute the command const { stdout, stderr } = await this.fileSystemService.executeCommand( resolvedCommand, { cwd: context.workingDirectory || this.fileSystemService.getCwd() } ); // Clear the animated feedback after command completes this.clearCommandFeedback(); // Store the output in state variables if (node.directive.output) { clonedState.setTextVar(node.directive.output, stdout); } else { clonedState.setTextVar('stdout', stdout); } if (stderr) { clonedState.setTextVar('stderr', stderr); } // In transformation mode, return a replacement node with the command output if (clonedState.isTransformationEnabled()) { const content = stdout && stderr ? `${stdout}\n${stderr}` : stdout || stderr || ''; const replacement: TextNode = { type: 'Text', content, location: node.location }; // Copy variables from cloned state to context state if (node.directive.output) { context.state.setTextVar(node.directive.output, stdout); } else { context.state.setTextVar('stdout', stdout); } if (stderr) { context.state.setTextVar('stderr', stderr); } clonedState.transformNode(node, replacement); return { state: clonedState, replacement }; } // In normal mode, return a placeholder node const placeholder: TextNode = { type: 'Text', content: '[run directive output placeholder]', location: node.location }; return { state: clonedState, replacement: placeholder }; } catch (error) { // Make sure to clear animation on command execution error this.clearCommandFeedback(); throw error; } } catch (error) { // Clear any animation if there's an error this.clearCommandFeedback(); directiveLogger.error('Error executing run directive:', error); // If it's already a DirectiveError, just rethrow it if (error instanceof DirectiveError) { throw error; } // Otherwise wrap it with more context const message = error instanceof Error ? `Failed to execute command: ${error.message}` : 'Failed to execute command'; throw new DirectiveError( message, this.kind, DirectiveErrorCode.EXECUTION_FAILED, { node, error, severity: DirectiveErrorSeverity[DirectiveErrorCode.EXECUTION_FAILED] } ); } } // Reference to the interval for the animation private animationInterval: NodeJS.Timeout | null = null; // Determine if we're in a test environment private isTestEnvironment: boolean = process.env.NODE_ENV === 'test' || process.env.VITEST; /** * Display animated feedback that a command is running */ private showRunningCommandFeedback(command: string): void { // Skip animation in test environments if (this.isTestEnvironment) { return; } // Clear any existing animation this.clearCommandFeedback(); // Start position for the ellipses let count = 0; // Function to update the animation const updateAnimation = () => { // Create the ellipses string with the appropriate number of dots const ellipses = '.'.repeat(count % 4); // Clear the current line and print the message with animated ellipses process.stdout.write(`\r\x1b[K`); // Clear the line process.stdout.write(`Running \`${command}\`${ellipses}`); count++; }; // Initial display updateAnimation(); // Update the animation every 500ms this.animationInterval = setInterval(updateAnimation, 500); } /** * Clear the command feedback animation */ private clearCommandFeedback(): void { // Skip in test environments if (this.isTestEnvironment) { return; } if (this.animationInterval) { clearInterval(this.animationInterval); this.animationInterval = null; // Clear the line process.stdout.write(`\r\x1b[K`); } } }