import fs, { Stats } from 'fs'; /** * FileSystem interface provides a strongly-typed abstraction * over common Node.js filesystem operations. */ export interface CBFileSystem { /** Checks whether a given path exists on the filesystem. */ existsSync: (path: string) => boolean; /** Reads the contents of a directory and returns an array of entries. */ readdirSync: (path: string, options?: any) => any[]; /** Creates a new directory, optionally allowing recursive creation. */ mkdirSync: (path: string, options?: { recursive: boolean }) => void; /** Deletes a file at the given path. */ unlinkSync: (path: string) => void; /** Removes a directory at the given path. */ rmdirSync: (path: string) => void; /** Returns stats for a given path, including directory check and size. */ statSync: (path: string) => { isDirectory: () => boolean; size: number }; /** Creates a writable stream to a file at the given path. */ createWriteStream: (path: string) => NodeJS.WritableStream; /** Reads a file's contents as a string with specified encoding. */ readFileSync: (path: string, encoding: string) => string; /** Writes string data to a file at the given path. */ writeFileSync: (path: string, data: string) => void; /** Copies a file from source to destination. */ copyFileSync: (src: string, dest: string) => void; /** Returns stats for a given path, including symbolic link check. */ lstatSync: (path: string) => Stats; /** Renames a file or directory. */ renameSync: (oldPath: string, newPath: string) => void; /** Copies a file or directory from source to destination. */ cpSync: (src: string, dest: string, options?: { recursive: boolean }) => void; } /** * FileSystemService provides a class-based implementation of the FileSystem interface. * Use this for dependency injection for all the filesystem operations, and for testing we can use custom implemenatation if needed. */ export class FileSystemService implements CBFileSystem { existsSync(path: string): boolean { return fs.existsSync(path); } readdirSync(path: string, options?: any): any[] { return fs.readdirSync(path, options); } mkdirSync(path: string, options?: { recursive: boolean }): void { fs.mkdirSync(path, options); } unlinkSync(path: string): void { fs.unlinkSync(path); } rmdirSync(path: string): void { fs.rmdirSync(path); } statSync(path: string): { isDirectory: () => boolean; size: number } { return fs.statSync(path); } createWriteStream(path: string): fs.WriteStream { return fs.createWriteStream(path); } readFileSync(path: string, encoding: string): string { return fs.readFileSync(path, encoding as BufferEncoding); } writeFileSync(path: string, data: string): void { fs.writeFileSync(path, data); } copyFileSync(src: string, dest: string): void { fs.copyFileSync(src, dest); } lstatSync(path: string): Stats { return fs.lstatSync(path); } renameSync(oldPath: string, newPath: string): void { try { fs.renameSync(oldPath, newPath); } catch (err: any) { if (err.code === 'EXDEV') { // Cross-device rename (e.g. Docker overlay filesystem) — fall back to copy + delete try { fs.cpSync(oldPath, newPath, { recursive: true }); fs.rmSync(oldPath, { recursive: true, force: true }); } catch (copyErr) { try { fs.rmSync(newPath, { recursive: true, force: true }); } catch { // best-effort cleanup; ignore errors } throw copyErr; } } else { throw err; } } } cpSync(src: string, dest: string, options?: { recursive: boolean; }): void { fs.cpSync(src, dest, options); } }