diff --git a/packages/core/src/filesystem.ts b/packages/core/src/filesystem.ts index 8a1cc3a08fc3..0a9e3b783fd4 100644 --- a/packages/core/src/filesystem.ts +++ b/packages/core/src/filesystem.ts @@ -1,6 +1,7 @@ import { NodeFileSystem } from "@effect/platform-node" import { dirname, join, relative, resolve as pathResolve } from "path" import { realpathSync } from "fs" +import { randomUUID } from "crypto" import * as NFS from "fs/promises" import { lookup } from "mime-types" import { Effect, FileSystem, Layer, Schema, Context } from "effect" @@ -84,12 +85,6 @@ export namespace AppFileSystem { return JSON.parse(text) }) - const writeJson = Effect.fn("FileSystem.writeJson")(function* (path: string, data: unknown, mode?: number) { - const content = JSON.stringify(data, null, 2) - yield* fs.writeFileString(path, content) - if (mode) yield* fs.chmod(path, mode) - }) - const ensureDir = Effect.fn("FileSystem.ensureDir")(function* (path: string) { yield* fs.makeDirectory(path, { recursive: true }) }) @@ -99,19 +94,25 @@ export namespace AppFileSystem { content: string | Uint8Array, mode?: number, ) { - const write = typeof content === "string" ? fs.writeFileString(path, content) : fs.writeFile(path, content) - - yield* write.pipe( - Effect.catchIf( - (e) => e.reason._tag === "NotFound", - () => - Effect.gen(function* () { - yield* fs.makeDirectory(dirname(path), { recursive: true }) - yield* write - }), - ), - ) - if (mode) yield* fs.chmod(path, mode) + yield* Effect.tryPromise({ + try: async () => { + await NFS.mkdir(dirname(path), { recursive: true }) + const tmp = path + "." + process.pid + "." + randomUUID() + ".tmp" + try { + await NFS.writeFile(tmp, content, mode ? { mode } : undefined) + if (mode) await NFS.chmod(tmp, mode) + await NFS.rename(tmp, path) + } catch (e) { + await NFS.rm(tmp, { force: true }).catch(() => {}) + throw e + } + }, + catch: (cause) => new FileSystemError({ method: "writeWithDirs", cause }), + }) + }) + + const writeJson = Effect.fn("FileSystem.writeJson")(function* (path: string, data: unknown, mode?: number) { + yield* writeWithDirs(path, JSON.stringify(data, null, 2), mode) }) const glob = Effect.fn("FileSystem.glob")(function* (pattern: string, options?: Glob.Options) { diff --git a/packages/opencode/test/filesystem/filesystem.test.ts b/packages/opencode/test/filesystem/filesystem.test.ts index 2d9271e873eb..b297dbd8255b 100644 --- a/packages/opencode/test/filesystem/filesystem.test.ts +++ b/packages/opencode/test/filesystem/filesystem.test.ts @@ -76,6 +76,21 @@ describe("AppFileSystem", () => { expect(result).toEqual(data) }), ) + + it( + "writes JSON through a complete replacement file", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const tmp = yield* fs.makeTempDirectoryScoped() + const file = path.join(tmp, "nested", "data.json") + + yield* fs.writeJson(file, { value: "old" }) + yield* fs.writeJson(file, { value: "new" }) + + expect(yield* fs.readJson(file)).toEqual({ value: "new" }) + expect(yield* fs.glob("*.tmp", { cwd: path.dirname(file) })).toEqual([]) + }), + ) }) describe("ensureDir", () => {