import { InMemoryFs } from "./fs/in-memory-fs"; import type { CpOptions, FileSystem, FsStat, InitialFiles, MkdirOptions, RmOptions } from "./fs/interface"; import type { StateArchiveCreateResult, StateArchiveExtractResult, StateArchiveEntry, StateApplyEditsOptions, StateApplyEditsResult, StateBackend, StateCapabilities, StateCopyOptions, StateDirent, StateEdit, StateEditInstruction, StateEditPlan, StateFileDetection, StateFileSearchResult, StateFindEntry, StateFindOptions, StateHashOptions, StateJsonUpdateOperation, StateJsonUpdateResult, StateJsonWriteOptions, StateMkdirOptions, StateMoveOptions, StateCompressionResult, StateReplaceInFilesOptions, StateReplaceInFilesResult, StateReplaceResult, StateRmOptions, StateSearchOptions, StateStat, StateTreeNode, StateTreeOptions, StateTreeSummary } from "./backend"; import { buildTar, buildTree, detectFile as detectFileFromBytes, extractTar, findInTree, gzipBytes, gunzipBytes, hashBytes, listTar, queryJsonValue, summarizeTree, updateJsonValue, type TarInputEntry } from "./extras"; import { applyTextEdits, collectFileReplaceResults, collectFileSearchResults, diffContent, parseJsonFileContent, planTextEdits, planToStateEdits, replaceTextContent, searchTextContent, stateDirent, stringifyJsonFileContent, toStateStat } from "./helpers"; // ── FileSystemStateBackend ──────────────────────────────────────────── // // The single StateBackend implementation. Accepts any FileSystem — // pass `new InMemoryFs()` for ephemeral in-memory state or a // `WorkspaceFileSystem` wrapping a durable Workspace. export interface FileSystemStateBackendOptions { files?: InitialFiles; fs?: FileSystem; } export class FileSystemStateBackend implements StateBackend { readonly fs: FileSystem; constructor(fsOrOptions: FileSystem | FileSystemStateBackendOptions = {}) { if (isFileSystem(fsOrOptions)) { this.fs = fsOrOptions; } else { this.fs = fsOrOptions.fs ?? new InMemoryFs(fsOrOptions.files); } } async getCapabilities(): Promise { return { chmod: true, utimes: true, hardLinks: true }; } async readFile(path: string): Promise { return this.fs.readFile(path); } async readFileBytes(path: string): Promise { return this.fs.readFileBytes(path); } async writeFile(path: string, content: string): Promise { await this.fs.writeFile(path, content); } async writeFileBytes(path: string, content: Uint8Array): Promise { await this.fs.writeFileBytes(path, content); } async appendFile(path: string, content: string | Uint8Array): Promise { await this.fs.appendFile(path, content); } async readJson(path: string): Promise { return parseJsonFileContent(await this.readFile(path), path); } async writeJson( path: string, value: unknown, options?: StateJsonWriteOptions ): Promise { await this.writeFile( path, stringifyJsonFileContent(value, path, options?.spaces) ); } async queryJson(path: string, query: string): Promise { return queryJsonValue(await this.readJson(path), query); } async updateJson( path: string, operations: StateJsonUpdateOperation[] ): Promise { const current = await this.readJson(path); const updated = updateJsonValue(current, operations, path); await this.writeFile(path, updated.content); return updated; } async exists(path: string): Promise { return this.fs.exists(path); } async stat(path: string): Promise { return this.tryStat(() => this.fs.stat(path)); } async lstat(path: string): Promise { return this.tryStat(() => this.fs.lstat(path)); } async mkdir(path: string, options?: StateMkdirOptions): Promise { await this.fs.mkdir(path, options as MkdirOptions | undefined); } async readdir(path: string): Promise { return this.fs.readdir(path); } async readdirWithFileTypes(path: string): Promise { const entries = await this.fs.readdirWithFileTypes(path); return sortDirents(entries.map((e) => stateDirent(e.name, e.type))); } async find( path: string, options?: StateFindOptions ): Promise { return findInTree(path, this.createTreeOps(), options); } async walkTree( path: string, options?: StateTreeOptions ): Promise { return buildTree(path, this.createTreeOps(), options); } async summarizeTree( path: string, options?: StateTreeOptions ): Promise { return summarizeTree(path, this.createTreeOps(), options); } async searchText( path: string, query: string, options?: StateSearchOptions ): Promise> { return searchTextContent(await this.readFile(path), query, options); } async searchFiles( pattern: string, query: string, options?: StateSearchOptions ): Promise { const paths = await this.getFilePaths(pattern); return collectFileSearchResults( paths, this.readFile.bind(this), (content) => searchTextContent(content, query, options) ); } async replaceInFile( path: string, search: string, replacement: string, options?: StateSearchOptions ): Promise { const current = await this.readFile(path); const result = replaceTextContent(current, search, replacement, options); if (result.replaced > 0) { await this.writeFile(path, result.content); } return result; } async replaceInFiles( pattern: string, search: string, replacement: string, options?: StateReplaceInFilesOptions ): Promise { const paths = await this.getFilePaths(pattern); return collectFileReplaceResults( paths, this.readFile.bind(this), this.writeFile.bind(this), this.deleteFile.bind(this), search, replacement, options ); } async rm(path: string, options?: StateRmOptions): Promise { await this.fs.rm(path, options as RmOptions | undefined); } async cp( src: string, dest: string, options?: StateCopyOptions ): Promise { await this.fs.cp(src, dest, options as CpOptions | undefined); } async mv( src: string, dest: string, options?: StateMoveOptions ): Promise { const stat = await this.stat(src); if (stat?.type === "directory" && !options?.recursive) { throw new Error( `EISDIR: cannot move directory without recursive: ${src}` ); } await this.fs.mv(src, dest); } async symlink(target: string, linkPath: string): Promise { await this.fs.symlink(target, linkPath); } async readlink(path: string): Promise { return this.fs.readlink(path); } async realpath(path: string): Promise { return this.fs.realpath(path); } async resolvePath(base: string, path: string): Promise { return this.fs.resolvePath(base, path); } async glob(pattern: string): Promise { return this.fs.glob(pattern); } async diff(pathA: string, pathB: string): Promise { const [before, after] = await Promise.all([ this.readFile(pathA), this.readFile(pathB) ]); return diffContent(before, after, pathA, pathB); } async diffContent(path: string, newContent: string): Promise { return diffContent(await this.readFile(path), newContent, path, path); } async createArchive( path: string, sources: string[] ): Promise { const entries = await this.collectArchiveEntries(sources); const tar = buildTar(entries); await this.writeFileBytes(path, tar); return { path, entries: entries.map((entry) => ({ path: entry.path, type: entry.type, size: entry.type === "file" ? entry.bytes.byteLength : 0 })), bytesWritten: tar.byteLength }; } async listArchive(path: string): Promise { return listTar(await this.readFileBytes(path)); } async extractArchive( path: string, destination: string ): Promise { const entries = extractTar(await this.readFileBytes(path)); for (const entry of entries) { const destPath = destination === "/" ? `/${entry.path}` : `${destination}/${entry.path}`; if (entry.type === "directory") { await this.mkdir(destPath, { recursive: true }); } else if (entry.bytes) { await this.writeFileBytes(destPath, entry.bytes); } } return { destination, entries: entries.map((entry) => ({ path: entry.path, type: entry.type, size: entry.bytes?.byteLength ?? 0 })) }; } async compressFile( path: string, destination?: string ): Promise { const dest = destination ?? `${path}.gz`; const bytes = await gzipBytes(await this.readFileBytes(path)); await this.writeFileBytes(dest, bytes); return { path, destination: dest, bytesWritten: bytes.byteLength }; } async decompressFile( path: string, destination?: string ): Promise { const dest = destination ?? path.replace(/\.gz$/i, ""); const bytes = await gunzipBytes(await this.readFileBytes(path)); await this.writeFileBytes(dest, bytes); return { path, destination: dest, bytesWritten: bytes.byteLength }; } async hashFile(path: string, options?: StateHashOptions): Promise { return hashBytes(await this.readFileBytes(path), options); } async detectFile(path: string): Promise { return detectFileFromBytes(path, await this.readFileBytes(path)); } async removeTree(path: string): Promise { await this.fs.rm(path, { recursive: true, force: true }); } async copyTree(src: string, dest: string): Promise { await this.fs.cp(src, dest, { recursive: true }); } async moveTree(src: string, dest: string): Promise { await this.fs.mv(src, dest); } async planEdits( instructions: StateEditInstruction[] ): Promise { return planTextEdits(instructions, async (path) => { try { return await this.readFile(path); } catch (error) { if (isEnoent(error)) return null; throw error; } }); } async applyEditPlan( plan: StateEditPlan, options?: StateApplyEditsOptions ): Promise { return this.applyEdits(planToStateEdits(plan), options); } async applyEdits( edits: StateEdit[], options?: StateApplyEditsOptions ): Promise { return applyTextEdits( edits, async (path) => { try { return await this.readFile(path); } catch (error) { if (isEnoent(error)) return null; throw error; } }, this.writeFile.bind(this), this.deleteFile.bind(this), options ); } // ── Private helpers ─────────────────────────────────────────────── private async tryStat( getStat: () => Promise ): Promise { try { return toStateStat(await getStat()); } catch (error) { if (isEnoent(error)) return null; throw error; } } private async getFilePaths(pattern: string): Promise { const paths = await this.glob(pattern); const files: string[] = []; for (const path of paths) { const stat = await this.lstat(path); if (stat?.type === "file") { files.push(path); } } return files; } private async deleteFile(path: string): Promise { await this.fs.rm(path, { force: true }); } private createTreeOps() { return { lstat: this.lstat.bind(this), readdirWithFileTypes: this.readdirWithFileTypes.bind(this), resolvePath: this.resolvePath.bind(this) }; } private async collectArchiveEntries( sources: string[] ): Promise { const entries: TarInputEntry[] = []; for (const source of sources) { const tree = await this.walkTree(source); await appendArchiveNode(tree, entries, this.readFileBytes.bind(this)); } return entries; } } // ── Factory helpers ─────────────────────────────────────────────────── /** @deprecated Use `new FileSystemStateBackend()` or `createMemoryStateBackend()` */ export type MemoryStateBackendOptions = FileSystemStateBackendOptions; /** @deprecated Use `FileSystemStateBackend` */ export const MemoryStateBackend = FileSystemStateBackend; export function createMemoryStateBackend( options: FileSystemStateBackendOptions = {} ): FileSystemStateBackend { return new FileSystemStateBackend(options); } // ── Utilities ───────────────────────────────────────────────────────── function isFileSystem(value: unknown): value is FileSystem { return ( typeof value === "object" && value !== null && "readFile" in value && "writeFile" in value && "glob" in value ); } function isEnoent(error: unknown): boolean { return error instanceof Error && error.message.includes("ENOENT"); } function sortDirents(entries: StateDirent[]): StateDirent[] { return [...entries].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0 ); } async function appendArchiveNode( node: StateTreeNode, entries: TarInputEntry[], readFileBytes: (path: string) => Promise ): Promise { const relativePath = node.path.replace(/^\/+/, ""); if (node.type === "directory") { entries.push({ path: relativePath || ".", type: "directory" }); for (const child of node.children ?? []) { await appendArchiveNode(child, entries, readFileBytes); } return; } if (node.type === "file") { entries.push({ path: relativePath, type: "file", bytes: await readFileBytes(node.path) }); } }