meld
Version:
Meld: A template language for LLM prompts
192 lines (161 loc) • 6.49 kB
text/typescript
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`);
}
}
}