From 7003a09d57c64edcfa1693e9a05b7335a943cac2 Mon Sep 17 00:00:00 2001 From: BB84 Date: Sun, 12 Jul 2026 12:50:12 -0400 Subject: [PATCH 1/5] fix(core): tolerate AlreadyExists in FSUtil.ensureDir Recursive makeDirectory should be idempotent, but some Windows runtimes surface AlreadyExists for a directory that already exists. Since v1.17.15, Config.ensureGitignore calls fs.ensureDir on every resolved config directory, so startup crashed with 'Unexpected server error' whenever a project's .opencode directory already existed. ensureDir now treats AlreadyExists as success when the path is already a directory, while still failing when a file occupies the path. Fixes #35828 --- packages/core/src/fs-util.ts | 10 ++- .../core/test/filesystem/filesystem.test.ts | 65 ++++++++++++++++++- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/packages/core/src/fs-util.ts b/packages/core/src/fs-util.ts index eb68627a55c3..177a0e599975 100644 --- a/packages/core/src/fs-util.ts +++ b/packages/core/src/fs-util.ts @@ -114,7 +114,15 @@ export namespace FSUtil { }) const ensureDir = Effect.fn("FileSystem.ensureDir")(function* (path: string) { - yield* fs.makeDirectory(path, { recursive: true }) + yield* fs.makeDirectory(path, { recursive: true }).pipe( + // Recursive makeDirectory should be idempotent, but some platforms (observed on Windows) + // surface AlreadyExists when the directory already exists. Tolerate that, but only when the + // path is genuinely a directory so a file occupying the path still fails loudly. + Effect.catchIf( + (e) => e.reason._tag === "AlreadyExists", + (e) => isDir(path).pipe(Effect.flatMap((exists) => (exists ? Effect.void : Effect.fail(e)))), + ), + ) }) const writeWithDirs = Effect.fn("FileSystem.writeWithDirs")(function* ( diff --git a/packages/core/test/filesystem/filesystem.test.ts b/packages/core/test/filesystem/filesystem.test.ts index fdce1b447644..13a54996f88e 100644 --- a/packages/core/test/filesystem/filesystem.test.ts +++ b/packages/core/test/filesystem/filesystem.test.ts @@ -1,14 +1,47 @@ import { describe, test, expect } from "bun:test" -import { Effect, FileSystem } from "effect" +import { Effect, Exit, FileSystem, Layer } from "effect" +import { PlatformError, SystemError } from "effect/PlatformError" +import { NodeFileSystem } from "@effect/platform-node" import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { FSUtil } from "@opencode-ai/core/fs-util" import { testEffect } from "../lib/effect" +import os from "os" import path from "path" +import { mkdtempSync, rmSync } from "fs" const live = LayerNode.compile(LayerNode.group([FSUtil.node, LayerNodePlatform.filesystem])) const { effect: it } = testEffect(live) +// A FileSystem whose makeDirectory always reports AlreadyExists, emulating the platforms/runtimes +// (observed on Windows) that surface it for an already-existing directory even with { recursive: true }. +const alreadyExistsFilesystem = Layer.effect( + FileSystem.FileSystem, + Effect.gen(function* () { + const real = yield* FileSystem.FileSystem + return { + ...real, + makeDirectory: (dir: string) => + Effect.fail( + new PlatformError( + new SystemError({ + _tag: "AlreadyExists", + module: "FileSystem", + method: "makeDirectory", + pathOrDescriptor: dir, + }), + ), + ), + } + }), +).pipe(Layer.provide(NodeFileSystem.layer)) + +const { effect: itAlreadyExists } = testEffect( + LayerNode.compile(LayerNode.group([FSUtil.node, LayerNodePlatform.filesystem]), [ + [LayerNodePlatform.filesystem, alreadyExistsFilesystem], + ]), +) + describe("FSUtil", () => { describe("isDir", () => { it( @@ -158,6 +191,36 @@ describe("FSUtil", () => { expect(info.type).toBe("Directory") }), ) + + it( + "fails when a file already occupies the path", + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + const file = path.join(tmp, "occupied") + yield* filesys.writeFileString(file, "x") + + // makeDirectory reports AlreadyExists for a file path too; ensureDir must not mask the collision. + const exit = yield* fs.ensureDir(file).pipe(Effect.exit) + expect(Exit.isFailure(exit)).toBe(true) + }), + ) + }) + + // Regression coverage for issue #35828: on some Windows runtimes a recursive makeDirectory reports + // AlreadyExists for a directory that already exists, which crashed startup when `.opencode` already + // existed. ensureDir must treat that as success. + describe("ensureDir tolerates platform-reported AlreadyExists", () => { + itAlreadyExists( + "succeeds when makeDirectory reports AlreadyExists for an existing directory", + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const dir = mkdtempSync(path.join(os.tmpdir(), "oc-ensuredir-")) + yield* fs.ensureDir(dir) + rmSync(dir, { recursive: true, force: true }) + }), + ) }) describe("writeWithDirs", () => { From 365e89afbe7d74b819ee5a5f67ff9f3a7089e0a8 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 14 Jul 2026 16:35:48 -0500 Subject: [PATCH 2/5] fix(core): simplify Windows ensureDir guard --- packages/core/src/fs-util.ts | 13 ++-- .../core/test/filesystem/filesystem.test.ts | 65 +------------------ 2 files changed, 5 insertions(+), 73 deletions(-) diff --git a/packages/core/src/fs-util.ts b/packages/core/src/fs-util.ts index 177a0e599975..456e09660659 100644 --- a/packages/core/src/fs-util.ts +++ b/packages/core/src/fs-util.ts @@ -114,15 +114,10 @@ export namespace FSUtil { }) const ensureDir = Effect.fn("FileSystem.ensureDir")(function* (path: string) { - yield* fs.makeDirectory(path, { recursive: true }).pipe( - // Recursive makeDirectory should be idempotent, but some platforms (observed on Windows) - // surface AlreadyExists when the directory already exists. Tolerate that, but only when the - // path is genuinely a directory so a file occupying the path still fails loudly. - Effect.catchIf( - (e) => e.reason._tag === "AlreadyExists", - (e) => isDir(path).pipe(Effect.flatMap((exists) => (exists ? Effect.void : Effect.fail(e)))), - ), - ) + // Bun on Windows can throw EEXIST here despite recursive mode. + // https://github.com/oven-sh/bun/issues/29521 + if (yield* isDir(path)) return + yield* fs.makeDirectory(path, { recursive: true }) }) const writeWithDirs = Effect.fn("FileSystem.writeWithDirs")(function* ( diff --git a/packages/core/test/filesystem/filesystem.test.ts b/packages/core/test/filesystem/filesystem.test.ts index 13a54996f88e..fdce1b447644 100644 --- a/packages/core/test/filesystem/filesystem.test.ts +++ b/packages/core/test/filesystem/filesystem.test.ts @@ -1,47 +1,14 @@ import { describe, test, expect } from "bun:test" -import { Effect, Exit, FileSystem, Layer } from "effect" -import { PlatformError, SystemError } from "effect/PlatformError" -import { NodeFileSystem } from "@effect/platform-node" +import { Effect, FileSystem } from "effect" import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { FSUtil } from "@opencode-ai/core/fs-util" import { testEffect } from "../lib/effect" -import os from "os" import path from "path" -import { mkdtempSync, rmSync } from "fs" const live = LayerNode.compile(LayerNode.group([FSUtil.node, LayerNodePlatform.filesystem])) const { effect: it } = testEffect(live) -// A FileSystem whose makeDirectory always reports AlreadyExists, emulating the platforms/runtimes -// (observed on Windows) that surface it for an already-existing directory even with { recursive: true }. -const alreadyExistsFilesystem = Layer.effect( - FileSystem.FileSystem, - Effect.gen(function* () { - const real = yield* FileSystem.FileSystem - return { - ...real, - makeDirectory: (dir: string) => - Effect.fail( - new PlatformError( - new SystemError({ - _tag: "AlreadyExists", - module: "FileSystem", - method: "makeDirectory", - pathOrDescriptor: dir, - }), - ), - ), - } - }), -).pipe(Layer.provide(NodeFileSystem.layer)) - -const { effect: itAlreadyExists } = testEffect( - LayerNode.compile(LayerNode.group([FSUtil.node, LayerNodePlatform.filesystem]), [ - [LayerNodePlatform.filesystem, alreadyExistsFilesystem], - ]), -) - describe("FSUtil", () => { describe("isDir", () => { it( @@ -191,36 +158,6 @@ describe("FSUtil", () => { expect(info.type).toBe("Directory") }), ) - - it( - "fails when a file already occupies the path", - Effect.gen(function* () { - const fs = yield* FSUtil.Service - const filesys = yield* FileSystem.FileSystem - const tmp = yield* filesys.makeTempDirectoryScoped() - const file = path.join(tmp, "occupied") - yield* filesys.writeFileString(file, "x") - - // makeDirectory reports AlreadyExists for a file path too; ensureDir must not mask the collision. - const exit = yield* fs.ensureDir(file).pipe(Effect.exit) - expect(Exit.isFailure(exit)).toBe(true) - }), - ) - }) - - // Regression coverage for issue #35828: on some Windows runtimes a recursive makeDirectory reports - // AlreadyExists for a directory that already exists, which crashed startup when `.opencode` already - // existed. ensureDir must treat that as success. - describe("ensureDir tolerates platform-reported AlreadyExists", () => { - itAlreadyExists( - "succeeds when makeDirectory reports AlreadyExists for an existing directory", - Effect.gen(function* () { - const fs = yield* FSUtil.Service - const dir = mkdtempSync(path.join(os.tmpdir(), "oc-ensuredir-")) - yield* fs.ensureDir(dir) - rmSync(dir, { recursive: true, force: true }) - }), - ) }) describe("writeWithDirs", () => { From eb47ec21b4b4e17d1434f4431728846a3dfa5187 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 14 Jul 2026 18:02:58 -0500 Subject: [PATCH 3/5] docs(core): link canonical Bun mkdir issue --- packages/core/src/fs-util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/fs-util.ts b/packages/core/src/fs-util.ts index 456e09660659..5fc65fe7e099 100644 --- a/packages/core/src/fs-util.ts +++ b/packages/core/src/fs-util.ts @@ -115,7 +115,7 @@ export namespace FSUtil { const ensureDir = Effect.fn("FileSystem.ensureDir")(function* (path: string) { // Bun on Windows can throw EEXIST here despite recursive mode. - // https://github.com/oven-sh/bun/issues/29521 + // https://github.com/oven-sh/bun/issues/21901 if (yield* isDir(path)) return yield* fs.makeDirectory(path, { recursive: true }) }) From e82a069693bb0079eda91e23c78ed0176d9c8b65 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 14 Jul 2026 18:12:10 -0500 Subject: [PATCH 4/5] fix(core): preserve concurrent ensureDir safety --- packages/core/src/fs-util.ts | 12 ++++-- .../core/test/filesystem/filesystem.test.ts | 37 ++++++++++++++++++- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/packages/core/src/fs-util.ts b/packages/core/src/fs-util.ts index 5fc65fe7e099..93a551c9b0a2 100644 --- a/packages/core/src/fs-util.ts +++ b/packages/core/src/fs-util.ts @@ -114,10 +114,14 @@ export namespace FSUtil { }) const ensureDir = Effect.fn("FileSystem.ensureDir")(function* (path: string) { - // Bun on Windows can throw EEXIST here despite recursive mode. - // https://github.com/oven-sh/bun/issues/21901 - if (yield* isDir(path)) return - yield* fs.makeDirectory(path, { recursive: true }) + yield* fs.makeDirectory(path, { recursive: true }).pipe( + // Bun on Windows can throw EEXIST here despite recursive mode. + // https://github.com/oven-sh/bun/issues/21901 + Effect.catchIf( + (error) => error.reason._tag === "AlreadyExists", + (error) => isDir(path).pipe(Effect.flatMap((exists) => (exists ? Effect.void : Effect.fail(error)))), + ), + ) }) const writeWithDirs = Effect.fn("FileSystem.writeWithDirs")(function* ( diff --git a/packages/core/test/filesystem/filesystem.test.ts b/packages/core/test/filesystem/filesystem.test.ts index fdce1b447644..9420b4087213 100644 --- a/packages/core/test/filesystem/filesystem.test.ts +++ b/packages/core/test/filesystem/filesystem.test.ts @@ -1,5 +1,6 @@ import { describe, test, expect } from "bun:test" -import { Effect, FileSystem } from "effect" +import { Effect, Exit, FileSystem, Layer } from "effect" +import { NodeFileSystem } from "@effect/platform-node" import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { FSUtil } from "@opencode-ai/core/fs-util" @@ -8,6 +9,18 @@ import path from "path" const live = LayerNode.compile(LayerNode.group([FSUtil.node, LayerNodePlatform.filesystem])) const { effect: it } = testEffect(live) +const alreadyExistsFilesystem = Layer.effect( + FileSystem.FileSystem, + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + return { ...fs, makeDirectory: (path: string) => fs.makeDirectory(path) } + }), +).pipe(Layer.provide(NodeFileSystem.layer)) +const { effect: itAlreadyExists } = testEffect( + LayerNode.compile(LayerNode.group([FSUtil.node, LayerNodePlatform.filesystem]), [ + [LayerNodePlatform.filesystem, alreadyExistsFilesystem], + ]), +) describe("FSUtil", () => { describe("isDir", () => { @@ -160,6 +173,28 @@ describe("FSUtil", () => { ) }) + describe("ensureDir with platform-reported AlreadyExists", () => { + itAlreadyExists( + "accepts an existing directory", + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const filesys = yield* FileSystem.FileSystem + yield* fs.ensureDir(yield* filesys.makeTempDirectoryScoped()) + }), + ) + + itAlreadyExists( + "rejects a file collision", + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const filesys = yield* FileSystem.FileSystem + const file = path.join(yield* filesys.makeTempDirectoryScoped(), "occupied") + yield* filesys.writeFileString(file, "x") + expect(Exit.isFailure(yield* fs.ensureDir(file).pipe(Effect.exit))).toBe(true) + }), + ) + }) + describe("writeWithDirs", () => { it( "creates parent directories if missing", From 23d880a44f286850412ed60bc45483d53463a85a Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 14 Jul 2026 20:56:58 -0500 Subject: [PATCH 5/5] test(core): remove synthetic ensureDir coverage --- .../core/test/filesystem/filesystem.test.ts | 37 +------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/packages/core/test/filesystem/filesystem.test.ts b/packages/core/test/filesystem/filesystem.test.ts index 9420b4087213..fdce1b447644 100644 --- a/packages/core/test/filesystem/filesystem.test.ts +++ b/packages/core/test/filesystem/filesystem.test.ts @@ -1,6 +1,5 @@ import { describe, test, expect } from "bun:test" -import { Effect, Exit, FileSystem, Layer } from "effect" -import { NodeFileSystem } from "@effect/platform-node" +import { Effect, FileSystem } from "effect" import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { FSUtil } from "@opencode-ai/core/fs-util" @@ -9,18 +8,6 @@ import path from "path" const live = LayerNode.compile(LayerNode.group([FSUtil.node, LayerNodePlatform.filesystem])) const { effect: it } = testEffect(live) -const alreadyExistsFilesystem = Layer.effect( - FileSystem.FileSystem, - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem - return { ...fs, makeDirectory: (path: string) => fs.makeDirectory(path) } - }), -).pipe(Layer.provide(NodeFileSystem.layer)) -const { effect: itAlreadyExists } = testEffect( - LayerNode.compile(LayerNode.group([FSUtil.node, LayerNodePlatform.filesystem]), [ - [LayerNodePlatform.filesystem, alreadyExistsFilesystem], - ]), -) describe("FSUtil", () => { describe("isDir", () => { @@ -173,28 +160,6 @@ describe("FSUtil", () => { ) }) - describe("ensureDir with platform-reported AlreadyExists", () => { - itAlreadyExists( - "accepts an existing directory", - Effect.gen(function* () { - const fs = yield* FSUtil.Service - const filesys = yield* FileSystem.FileSystem - yield* fs.ensureDir(yield* filesys.makeTempDirectoryScoped()) - }), - ) - - itAlreadyExists( - "rejects a file collision", - Effect.gen(function* () { - const fs = yield* FSUtil.Service - const filesys = yield* FileSystem.FileSystem - const file = path.join(yield* filesys.makeTempDirectoryScoped(), "occupied") - yield* filesys.writeFileString(file, "x") - expect(Exit.isFailure(yield* fs.ensureDir(file).pipe(Effect.exit))).toBe(true) - }), - ) - }) - describe("writeWithDirs", () => { it( "creates parent directories if missing",