Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/core/src/fs-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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* (
Expand Down
65 changes: 64 additions & 1 deletion packages/core/test/filesystem/filesystem.test.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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", () => {
Expand Down
Loading