UNPKG

meld

Version:

Meld: A template language for LLM prompts

293 lines (258 loc) 8.64 kB
import { IFileSystem } from '@services/fs/FileSystemService/IFileSystem.js'; import * as path from 'path'; import { Stats } from 'fs-extra'; /** * Simple in-memory file system implementation for use with the runMeld API. * This allows processing meld content without touching the real file system. */ export class MemoryFileSystem implements IFileSystem { private files: Map<string, string> = new Map(); private dirs: Set<string> = new Set(); isTestEnvironment: boolean = true; constructor() { // Initialize with root directory this.dirs.add('/'); } /** * Read a file from memory */ async readFile(filePath: string): Promise<string> { const normalizedPath = this.normalizePath(filePath); if (!this.files.has(normalizedPath)) { throw new Error(`File not found: ${filePath}`); } return this.files.get(normalizedPath) || ''; } /** * Write a file to memory */ async writeFile(filePath: string, content: string): Promise<void> { const normalizedPath = this.normalizePath(filePath); // Ensure parent directory exists const dirPath = path.dirname(normalizedPath); await this.mkdir(dirPath); this.files.set(normalizedPath, content); } /** * Check if a file exists in memory */ async exists(filePath: string): Promise<boolean> { const normalizedPath = this.normalizePath(filePath); return this.files.has(normalizedPath) || this.dirs.has(normalizedPath); } /** * Create a directory in memory */ async mkdir(dirPath: string, options?: { recursive?: boolean }): Promise<void> { const normalizedPath = this.normalizePath(dirPath); if (options?.recursive) { // Create parent directories if needed const parts = normalizedPath.split('/').filter(Boolean); let currentPath = '/'; this.dirs.add(currentPath); for (const part of parts) { currentPath = path.join(currentPath, part); this.dirs.add(currentPath); } } else { // Ensure parent directory exists for non-recursive mkdir const parentDir = path.dirname(normalizedPath); if (parentDir !== normalizedPath && !this.dirs.has(parentDir)) { // Create parent directory recursively await this.mkdir(parentDir, { recursive: true }); } // Add this directory this.dirs.add(normalizedPath); } } /** * Check if a path is a file */ async isFile(filePath: string): Promise<boolean> { const normalizedPath = this.normalizePath(filePath); return this.files.has(normalizedPath); } /** * Check if a path is a directory */ async isDirectory(filePath: string): Promise<boolean> { const normalizedPath = this.normalizePath(filePath); return this.dirs.has(normalizedPath); } /** * List directory contents */ async readDir(dirPath: string): Promise<string[]> { const normalizedPath = this.normalizePath(dirPath); if (!this.dirs.has(normalizedPath)) { throw new Error(`Directory not found: ${dirPath}`); } const entries: string[] = []; const prefix = normalizedPath === '/' ? '/' : normalizedPath + '/'; // Find all files directly in this directory for (const filePath of this.files.keys()) { if (filePath.startsWith(prefix)) { const relativePath = filePath.substring(prefix.length); if (!relativePath.includes('/')) { entries.push(relativePath); } } } // Find all subdirectories directly in this directory for (const dirPath of this.dirs) { if (dirPath !== normalizedPath && dirPath.startsWith(prefix)) { const relativePath = dirPath.substring(prefix.length); if (!relativePath.includes('/')) { entries.push(relativePath); } } } return entries; } /** * Get file stats */ async stat(filePath: string): Promise<Stats> { const normalizedPath = this.normalizePath(filePath); if (this.files.has(normalizedPath)) { // It's a file const content = this.files.get(normalizedPath) || ''; return { isFile: () => true, isDirectory: () => false, size: content.length, mtime: new Date(), ctime: new Date(), atime: new Date(), birthtime: new Date(), mode: 0o666, // Add other required Stats properties } as unknown as Stats; } if (this.dirs.has(normalizedPath)) { // It's a directory return { isFile: () => false, isDirectory: () => true, size: 0, mtime: new Date(), ctime: new Date(), atime: new Date(), birthtime: new Date(), mode: 0o777, // Add other required Stats properties } as unknown as Stats; } throw new Error(`Path not found: ${filePath}`); } /** * Watch a file or directory for changes * This is a minimal implementation that doesn't actually watch anything * since it's for the runMeld API which doesn't need watching */ async *watch( path: string, options?: { recursive?: boolean } ): AsyncIterableIterator<{ filename: string; eventType: string }> { // This is a no-op implementation since we don't need actual watching // for the runMeld API return; } /** * Execute a command * This is a minimal implementation that doesn't actually execute anything * but returns empty stdout/stderr */ async executeCommand( command: string, options?: { cwd?: string } ): Promise<{ stdout: string; stderr: string }> { // This is a simplified implementation for in-memory usage // Just return empty output return { stdout: '', stderr: '' }; } /** * Rename a file or directory */ async rename(oldPath: string, newPath: string): Promise<void> { const normalizedOldPath = this.normalizePath(oldPath); const normalizedNewPath = this.normalizePath(newPath); if (this.files.has(normalizedOldPath)) { // Rename file const content = this.files.get(normalizedOldPath) || ''; this.files.set(normalizedNewPath, content); this.files.delete(normalizedOldPath); } else if (this.dirs.has(normalizedOldPath)) { // Rename directory this.dirs.add(normalizedNewPath); this.dirs.delete(normalizedOldPath); // Move all files in this directory const oldPrefix = normalizedOldPath === '/' ? '/' : normalizedOldPath + '/'; const newPrefix = normalizedNewPath === '/' ? '/' : normalizedNewPath + '/'; for (const [filePath, content] of [...this.files.entries()]) { if (filePath.startsWith(oldPrefix)) { const newFilePath = newPrefix + filePath.substring(oldPrefix.length); this.files.set(newFilePath, content); this.files.delete(filePath); } } // Move all subdirectories for (const dirPath of [...this.dirs]) { if (dirPath.startsWith(oldPrefix)) { const newDirPath = newPrefix + dirPath.substring(oldPrefix.length); this.dirs.add(newDirPath); this.dirs.delete(dirPath); } } } else { throw new Error(`Path not found: ${oldPath}`); } } /** * Delete a file */ async unlink(filePath: string): Promise<void> { const normalizedPath = this.normalizePath(filePath); if (!this.files.has(normalizedPath)) { throw new Error(`File not found: ${filePath}`); } this.files.delete(normalizedPath); } /** * Delete a directory */ async rmdir(dirPath: string): Promise<void> { const normalizedPath = this.normalizePath(dirPath); if (!this.dirs.has(normalizedPath)) { throw new Error(`Directory not found: ${dirPath}`); } // Check if directory is empty const prefix = normalizedPath === '/' ? '/' : normalizedPath + '/'; for (const filePath of this.files.keys()) { if (filePath.startsWith(prefix)) { throw new Error(`Directory not empty: ${dirPath}`); } } for (const subDirPath of this.dirs) { if (subDirPath !== normalizedPath && subDirPath.startsWith(prefix)) { throw new Error(`Directory not empty: ${dirPath}`); } } this.dirs.delete(normalizedPath); } /** * Helper method to normalize a path */ private normalizePath(filePath: string): string { return path.normalize(filePath).replace(/\\/g, '/'); } /** * Required by interface but no-op for in-memory FS */ setFileSystem(fileSystem: IFileSystem): void { // No-op } }