cloudaxe-opencode/packages/simulation/src/backend/fs-util.ts
2026-07-05 00:23:05 +00:00

259 lines
9.8 KiB
TypeScript

import { Effect, FileSystem, Layer } from "effect"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Glob } from "@opencode-ai/core/util/glob"
import { makeGlobalNode } from "@opencode-ai/core/effect/app-node"
import { filesystem } from "@opencode-ai/core/effect/app-node-platform"
import path from "path"
import { SimulationLog } from "../log"
/**
* Simulation replacement for `FSUtil`.
*
* This implementation is intentionally self-contained and only uses the
* injected simulated `FileSystem.FileSystem`. The default FSUtil layer has a
* few helpers that reach host-node APIs directly; depending on it here makes it
* easy for mutation paths to escape or miss the in-memory project tree.
*/
const layer = Layer.effect(
FSUtil.Service,
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const existsSafe = Effect.fn("SimulationFSUtil.existsSafe")(function* (file: string) {
const result = yield* fs.exists(file).pipe(Effect.orElseSucceed(() => false))
SimulationLog.add("fsutil.existsSafe", { file, result })
return result
})
const isDir = Effect.fn("SimulationFSUtil.isDir")(function* (file: string) {
const info = yield* fs.stat(file).pipe(Effect.catch(() => Effect.succeed(undefined)))
const result = info?.type === "Directory"
SimulationLog.add("fsutil.isDir", { file, result, type: info?.type })
return result
})
const isFile = Effect.fn("SimulationFSUtil.isFile")(function* (file: string) {
const info = yield* fs.stat(file).pipe(Effect.catch(() => Effect.succeed(undefined)))
const result = info?.type === "File"
SimulationLog.add("fsutil.isFile", { file, result, type: info?.type })
return result
})
const realPath = Effect.fn("SimulationFSUtil.realPath")(function* (file: string) {
SimulationLog.add("fsutil.realPath", { file })
const result = yield* fs.realPath(file)
SimulationLog.add("fsutil.realPath.result", { file, result })
return result
})
const stat = Effect.fn("SimulationFSUtil.stat")(function* (file: string) {
SimulationLog.add("fsutil.stat", { file })
const result = yield* fs.stat(file)
SimulationLog.add("fsutil.stat.result", { file, type: result.type })
return result
})
const readFile = Effect.fn("SimulationFSUtil.readFile")(function* (file: string) {
SimulationLog.add("fsutil.readFile", { file })
const result = yield* fs.readFile(file)
SimulationLog.add("fsutil.readFile.result", { file, bytes: result.length })
return result
})
const readFileString = Effect.fn("SimulationFSUtil.readFileString")(function* (file: string) {
SimulationLog.add("fsutil.readFileString", { file })
const result = yield* fs.readFileString(file)
SimulationLog.add("fsutil.readFileString.result", { file, bytes: result.length })
return result
})
const readFileStringSafe = Effect.fn("SimulationFSUtil.readFileStringSafe")(function* (file: string) {
const result = yield* fs
.readFileString(file)
.pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined)))
SimulationLog.add("fsutil.readFileStringSafe", { file, found: result !== undefined, bytes: result?.length })
return result
})
const readJson = Effect.fn("SimulationFSUtil.readJson")(function* (file: string) {
const text = yield* readFileString(file)
return JSON.parse(text) as unknown
})
const writeFile = Effect.fn("SimulationFSUtil.writeFile")(function* (
file: string,
data: Uint8Array,
options?: Parameters<typeof fs.writeFile>[2],
) {
SimulationLog.add("fsutil.writeFile", { file, bytes: data.length })
const result = yield* fs.writeFile(file, data, options)
SimulationLog.add("fsutil.writeFile.result", { file, bytes: data.length })
return result
})
const writeFileString = Effect.fn("SimulationFSUtil.writeFileString")(function* (
file: string,
data: string,
options?: Parameters<typeof fs.writeFileString>[2],
) {
SimulationLog.add("fsutil.writeFileString", { file, bytes: data.length })
const result = yield* fs.writeFileString(file, data, options)
SimulationLog.add("fsutil.writeFileString.result", { file, bytes: data.length })
return result
})
const makeDirectory: FileSystem.FileSystem["makeDirectory"] = (file, options) => fs.makeDirectory(file, options)
const ensureDir = Effect.fn("SimulationFSUtil.ensureDir")(function* (file: string) {
SimulationLog.add("fsutil.ensureDir", { file })
yield* fs.makeDirectory(file, { recursive: true })
SimulationLog.add("fsutil.ensureDir.result", { file })
})
const writeWithDirs = Effect.fn("SimulationFSUtil.writeWithDirs")(function* (
file: string,
content: string | Uint8Array,
mode?: number,
) {
SimulationLog.add("fsutil.writeWithDirs", {
file,
bytes: typeof content === "string" ? content.length : content.length,
})
const write =
typeof content === "string"
? fs.writeFileString(file, content)
: fs.writeFile(file, content)
yield* write.pipe(
Effect.catchReason("PlatformError", "NotFound", () =>
fs.makeDirectory(path.dirname(file), { recursive: true }).pipe(Effect.andThen(write)),
),
)
if (mode !== undefined) yield* fs.chmod(file, mode)
SimulationLog.add("fsutil.writeWithDirs.result", { file })
})
const writeJson = Effect.fn("SimulationFSUtil.writeJson")(function* (file: string, data: unknown, mode?: number) {
yield* writeFileString(file, JSON.stringify(data, null, 2))
if (mode !== undefined) yield* fs.chmod(file, mode)
})
const readDirectoryEntries = Effect.fn("SimulationFSUtil.readDirectoryEntries")(function* (dirPath: string) {
SimulationLog.add("fsutil.readDirectoryEntries", { dirPath })
const names = yield* fs.readDirectory(dirPath)
return yield* Effect.forEach(names, (name) =>
fs.stat(path.join(dirPath, name)).pipe(
Effect.map(
(info): FSUtil.DirEntry => ({
name,
type:
info.type === "Directory"
? "directory"
: info.type === "File"
? "file"
: info.type === "SymbolicLink"
? "symlink"
: "other",
}),
),
Effect.orElseSucceed((): FSUtil.DirEntry => ({ name, type: "other" })),
),
)
})
const resolve = Effect.fn("SimulationFSUtil.resolve")(function* (input: string) {
const result = path.resolve(input)
SimulationLog.add("fsutil.resolve", { input, result })
return result
})
const glob = Effect.fn("SimulationFSUtil.glob")(function* (pattern: string, options?: Glob.Options) {
const cwd = path.resolve(options?.cwd ?? process.cwd())
SimulationLog.add("fsutil.glob", { pattern, cwd, options })
const entries = yield* fs
.readDirectory(cwd, { recursive: true })
.pipe(Effect.orElseSucceed(() => [] as string[]))
const matches = yield* Effect.forEach(entries, (entry) =>
fs.stat(path.join(cwd, entry)).pipe(
Effect.map((info) => ({ entry, type: info.type })),
Effect.orElseSucceed(() => undefined),
),
)
const result = matches
.filter((item) => item !== undefined)
.filter((item) => options?.include === "all" || item.type === "File")
.filter((item) => Glob.match(pattern, item.entry))
.map((item) => (options?.absolute ? path.join(cwd, item.entry) : item.entry))
.sort((a, b) => a.localeCompare(b))
SimulationLog.add("fsutil.glob.result", { pattern, cwd, result })
return result
})
const globUp = Effect.fn("SimulationFSUtil.globUp")(function* (pattern: string, start: string, stop?: string) {
SimulationLog.add("fsutil.globUp", { pattern, start, stop })
const result: string[] = []
let current = path.resolve(start)
while (true) {
result.push(...(yield* glob(pattern, { cwd: current, absolute: true, include: "file", dot: true })))
if (stop === current) break
const parent = path.dirname(current)
if (parent === current) break
current = parent
}
SimulationLog.add("fsutil.globUp.result", { pattern, start, stop, result })
return result
})
const up = Effect.fn("SimulationFSUtil.up")(function* (options: { targets: string[]; start: string; stop?: string }) {
SimulationLog.add("fsutil.up", options)
const result: string[] = []
let current = path.resolve(options.start)
while (true) {
for (const target of options.targets) {
const search = path.join(current, target)
if (yield* fs.exists(search)) result.push(search)
}
if (options.stop === current) break
const parent = path.dirname(current)
if (parent === current) break
current = parent
}
SimulationLog.add("fsutil.up.result", { ...options, result })
return result
})
const findUp = Effect.fn("SimulationFSUtil.findUp")(function* (target: string, start: string, stop?: string) {
return yield* up({ targets: [target], start, stop })
})
return FSUtil.Service.of({
...fs,
realPath,
stat,
readFile,
readFileString,
writeFile,
writeFileString,
makeDirectory,
isDir,
isFile,
existsSafe,
readFileStringSafe,
readJson,
writeJson,
ensureDir,
writeWithDirs,
readDirectoryEntries,
resolve,
findUp,
up,
globUp,
glob,
globMatch: Glob.match,
})
}),
)
export const node = makeGlobalNode({ service: FSUtil.Service, layer, deps: [filesystem] })
export * as SimulationFSUtil from "./fs-util"