meld
Version:
Meld: A template language for LLM prompts
293 lines (258 loc) • 8.64 kB
text/typescript
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
}
}