UNPKG

meld

Version:

Meld: A template language for LLM prompts

308 lines (268 loc) 9.45 kB
import * as fsExtra from 'fs-extra'; import { filesystemLogger as logger } from '@core/utils/logger.js'; import { IFileSystemService } from './IFileSystemService.js'; import { IPathOperationsService } from './IPathOperationsService.js'; import { IFileSystem } from './IFileSystem.js'; import { NodeFileSystem } from './NodeFileSystem.js'; import { MeldError } from '@core/errors/MeldError.js'; import { MeldFileNotFoundError } from '@core/errors/MeldFileNotFoundError.js'; import { exec } from 'child_process'; import { promisify } from 'util'; import { MeldFileSystemError } from '@core/errors/MeldFileSystemError.js'; import { IPathService } from '../PathService/IPathService.js'; const execAsync = promisify(exec); interface FileOperationContext { operation: string; path: string; details?: Record<string, unknown>; [key: string]: unknown; } export class FileSystemService implements IFileSystemService { private fs: IFileSystem; private pathService?: IPathService; constructor( private readonly pathOps: IPathOperationsService, fileSystem?: IFileSystem ) { this.fs = fileSystem || new NodeFileSystem(); } setFileSystem(fileSystem: IFileSystem): void { this.fs = fileSystem; } getFileSystem(): IFileSystem { return this.fs; } setPathService(pathService: IPathService): void { this.pathService = pathService; } private resolvePath(filePath: string): string { // If we have a PathService, use it for resolving paths if (this.pathService) { return this.pathService.resolvePath(filePath); } // Fall back to direct path usage if PathService is not available return filePath; } // File operations async readFile(filePath: string): Promise<string> { const resolvedPath = this.resolvePath(filePath); const context: FileOperationContext = { operation: 'readFile', path: filePath, resolvedPath }; try { logger.debug('Reading file', context); const content = await this.fs.readFile(resolvedPath); logger.debug('Successfully read file', { ...context, contentLength: content.length }); return content; } catch (error) { const err = error as Error; if (err.message.includes('ENOENT')) { logger.error('File not found', { ...context, error: err }); throw new MeldFileNotFoundError(filePath, { cause: err }); } logger.error('Error reading file', { ...context, error: err }); throw new MeldError(`Error reading file: ${filePath}`, { cause: err, filePath }); } } async writeFile(filePath: string, content: string): Promise<void> { const resolvedPath = this.resolvePath(filePath); const context: FileOperationContext = { operation: 'writeFile', path: filePath, resolvedPath, details: { contentLength: content.length } }; try { logger.debug('Writing file', context); await this.ensureDir(this.pathOps.dirname(resolvedPath)); await this.fs.writeFile(resolvedPath, content); logger.debug('Successfully wrote file', context); } catch (error) { const err = error as Error; logger.error('Failed to write file', { ...context, error: err }); throw new MeldError(`Failed to write file: ${filePath}`, { cause: err, filePath }); } } async exists(filePath: string): Promise<boolean> { const resolvedPath = this.resolvePath(filePath); const context: FileOperationContext = { operation: 'exists', path: filePath, resolvedPath }; try { logger.debug('Checking if path exists', context); const exists = await this.fs.exists(resolvedPath); logger.debug('Path existence check complete', { ...context, exists }); return exists; } catch (error) { const err = error as Error; logger.error('Failed to check path existence', { ...context, error: err }); throw new MeldError(`Failed to check if path exists: ${filePath}`, { cause: err, filePath }); } } async stat(filePath: string): Promise<fsExtra.Stats> { const resolvedPath = this.resolvePath(filePath); const context: FileOperationContext = { operation: 'stat', path: filePath, resolvedPath }; try { logger.debug('Getting file stats', context); const stats = await this.fs.stat(resolvedPath); logger.debug('Successfully got file stats', { ...context, isDirectory: stats.isDirectory() }); return stats; } catch (error) { const err = error as Error; logger.error('Failed to get file stats', { ...context, error: err }); throw new MeldError(`Failed to get file stats: ${filePath}`, { cause: err, filePath }); } } // Directory operations async readDir(dirPath: string): Promise<string[]> { const resolvedPath = this.resolvePath(dirPath); const context: FileOperationContext = { operation: 'readDir', path: dirPath, resolvedPath }; try { logger.debug('Reading directory', context); const files = await this.fs.readDir(resolvedPath); logger.debug('Successfully read directory', { ...context, fileCount: files.length }); return files; } catch (error) { const err = error as Error; logger.error('Failed to read directory', { ...context, error: err }); throw new MeldError(`Failed to read directory: ${dirPath}`, { cause: err, filePath: dirPath }); } } async ensureDir(dirPath: string): Promise<void> { const resolvedPath = this.resolvePath(dirPath); const context: FileOperationContext = { operation: 'ensureDir', path: dirPath, resolvedPath }; try { logger.debug('Ensuring directory exists', context); await this.fs.mkdir(resolvedPath); logger.debug('Successfully ensured directory exists', context); } catch (error) { const err = error as Error; logger.error('Failed to ensure directory exists', { ...context, error: err }); throw new MeldError(`Failed to ensure directory exists: ${dirPath}`, { cause: err, filePath: dirPath }); } } async isDirectory(filePath: string): Promise<boolean> { const resolvedPath = this.resolvePath(filePath); const context: FileOperationContext = { operation: 'isDirectory', path: filePath, resolvedPath }; try { logger.debug('Checking if path is directory', context); const isDir = await this.fs.isDirectory(resolvedPath); logger.debug('Path directory check complete', { ...context, isDirectory: isDir }); return isDir; } catch (error) { const err = error as Error; logger.error('Failed to check if path is directory', { ...context, error: err }); throw new MeldError(`Failed to check if path is directory: ${filePath}`, { cause: err, filePath }); } } async isFile(filePath: string): Promise<boolean> { const resolvedPath = this.resolvePath(filePath); const context: FileOperationContext = { operation: 'isFile', path: filePath, resolvedPath }; try { logger.debug('Checking if path is file', context); const isFile = await this.fs.isFile(resolvedPath); logger.debug('Path file check complete', { ...context, isFile }); return isFile; } catch (error) { const err = error as Error; logger.error('Failed to check if path is file', { ...context, error: err }); throw new MeldError(`Failed to check if path is file: ${filePath}`, { cause: err, filePath }); } } getCwd(): string { return process.cwd(); } // Add dirname method that delegates to PathOperationsService dirname(filePath: string): string { return this.pathOps.dirname(filePath); } watch(path: string, options?: { recursive?: boolean }): AsyncIterableIterator<{ filename: string; eventType: string }> { const resolvedPath = this.resolvePath(path); const context: FileOperationContext = { operation: 'watch', path, resolvedPath, details: { options } }; try { logger.debug('Starting file watch', context); return this.fs.watch(resolvedPath, options); } catch (error) { const err = error as Error; logger.error('Failed to watch file', { ...context, error: err }); throw new MeldError(`Failed to watch file: ${path}`, { cause: err, filePath: path }); } } async executeCommand(command: string, options?: { cwd?: string }): Promise<{ stdout: string; stderr: string }> { // We don't need to resolve paths for command execution const context = { operation: 'executeCommand', command, cwd: options?.cwd }; try { logger.debug('Executing command', context); const { stdout, stderr } = await this.fs.executeCommand(command, options); logger.debug('Command executed successfully', { ...context, stdout, stderr }); return { stdout, stderr }; } catch (error) { const err = error as Error; logger.error('Failed to execute command', { ...context, error: err }); throw new MeldFileSystemError(`Failed to execute command: ${command}`, { cause: err, command }); } } }