mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-13 15:44:56 +00:00
effect(util): migrate filesystem callers to AppFileSystem.Service
Swap Filesystem.* calls in Effect-context callers (one production service, one CLI handler, and six tests) to yield AppFileSystem.Service.
This commit is contained in:
@@ -7,7 +7,7 @@ import { SessionTable, MessageTable, PartTable } from "../../session/session.sql
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
import { EOL } from "os"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Effect, Schema } from "effect"
|
||||
|
||||
const decodeMessageInfo = Schema.decodeUnknownSync(MessageV2.Info)
|
||||
@@ -95,6 +95,7 @@ export const ImportCommand = effectCmd({
|
||||
|
||||
const runImport = Effect.fn("Cli.import.body")(function* (file: string, projectID: string) {
|
||||
const share = yield* ShareNext.Service
|
||||
const fs = yield* AppFileSystem.Service
|
||||
|
||||
let exportData: ExportData | undefined
|
||||
|
||||
@@ -149,9 +150,9 @@ const runImport = Effect.fn("Cli.import.body")(function* (file: string, projectI
|
||||
|
||||
exportData = transformed
|
||||
} else {
|
||||
exportData = yield* Effect.promise(() =>
|
||||
Filesystem.readJson<NonNullable<typeof exportData>>(file).catch(() => undefined),
|
||||
)
|
||||
exportData = (yield* fs.readJson(file).pipe(Effect.orElseSucceed(() => undefined))) as
|
||||
| NonNullable<typeof exportData>
|
||||
| undefined
|
||||
if (!exportData) {
|
||||
process.stdout.write(`File not found: ${file}`)
|
||||
process.stdout.write(EOL)
|
||||
|
||||
@@ -11,8 +11,8 @@ import { Auth } from "@/auth"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { EventSequenceTable, EventTable } from "@/sync/event.sql"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { Slug } from "@opencode-ai/core/util/slug"
|
||||
import { WorkspaceTable } from "./workspace.sql"
|
||||
@@ -175,6 +175,7 @@ export const layer = Layer.effect(
|
||||
const http = yield* HttpClient.HttpClient
|
||||
const sync = yield* SyncEvent.Service
|
||||
const vcs = yield* Vcs.Service
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const connections = new Map<WorkspaceID, ConnectionStatus>()
|
||||
const syncFibers = yield* FiberMap.make<WorkspaceID, void, SyncLoopError>()
|
||||
|
||||
@@ -500,7 +501,7 @@ export const layer = Layer.effect(
|
||||
if (!target) return
|
||||
|
||||
if (target.type === "local") {
|
||||
setStatus(space.id, (yield* Effect.promise(() => Filesystem.exists(target.directory))) ? "connected" : "error")
|
||||
setStatus(space.id, (yield* fs.existsSafe(target.directory)) ? "connected" : "error")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1039,6 +1040,7 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(SessionPrompt.defaultLayer),
|
||||
Layer.provide(Project.defaultLayer),
|
||||
Layer.provide(Vcs.defaultLayer),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Cause, Effect, Exit, Layer } from "effect"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { File } from "../../src/file"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { disposeAllInstances, TestInstance, withTmpdirInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
@@ -161,7 +160,7 @@ describe("file/index Filesystem patterns", () => {
|
||||
const filepath = path.join(test.directory, "test.json")
|
||||
yield* Effect.promise(() => fs.writeFile(filepath, '{"key": "value"}', "utf-8"))
|
||||
|
||||
expect(yield* Effect.promise(() => Filesystem.mimeType(filepath))).toContain("application/json")
|
||||
expect(AppFileSystem.mimeType(filepath)).toContain("application/json")
|
||||
|
||||
const result = yield* read("test.json")
|
||||
expect(result.type).toBe("text")
|
||||
@@ -181,7 +180,7 @@ describe("file/index Filesystem patterns", () => {
|
||||
for (const testCase of testCases) {
|
||||
const filepath = path.join(test.directory, `test.${testCase.ext}`)
|
||||
yield* Effect.promise(() => fs.writeFile(filepath, Buffer.from([0x00, 0x00, 0x00, 0x00])))
|
||||
expect(yield* Effect.promise(() => Filesystem.mimeType(filepath))).toContain(testCase.mime)
|
||||
expect(AppFileSystem.mimeType(filepath)).toContain(testCase.mime)
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -189,15 +188,16 @@ describe("file/index Filesystem patterns", () => {
|
||||
|
||||
describe("list() - Filesystem.exists() and readText()", () => {
|
||||
it.instance(
|
||||
"reads .gitignore via Filesystem.exists() and readText()",
|
||||
"reads .gitignore via AppFileSystem.existsSafe() and readFileString()",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const fsys = yield* AppFileSystem.Service
|
||||
const test = yield* TestInstance
|
||||
const gitignorePath = path.join(test.directory, ".gitignore")
|
||||
yield* Effect.promise(() => fs.writeFile(gitignorePath, "node_modules\ndist\n", "utf-8"))
|
||||
yield* fsys.writeFileString(gitignorePath, "node_modules\ndist\n")
|
||||
|
||||
expect(yield* Effect.promise(() => Filesystem.exists(gitignorePath))).toBe(true)
|
||||
expect(yield* Effect.promise(() => Filesystem.readText(gitignorePath))).toContain("node_modules")
|
||||
expect(yield* fsys.existsSafe(gitignorePath)).toBe(true)
|
||||
expect(yield* fsys.readFileString(gitignorePath)).toContain("node_modules")
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
@@ -206,12 +206,13 @@ describe("file/index Filesystem patterns", () => {
|
||||
"reads .ignore file similarly",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const fsys = yield* AppFileSystem.Service
|
||||
const test = yield* TestInstance
|
||||
const ignorePath = path.join(test.directory, ".ignore")
|
||||
yield* Effect.promise(() => fs.writeFile(ignorePath, "*.log\n.env\n", "utf-8"))
|
||||
yield* fsys.writeFileString(ignorePath, "*.log\n.env\n")
|
||||
|
||||
expect(yield* Effect.promise(() => Filesystem.exists(ignorePath))).toBe(true)
|
||||
expect(yield* Effect.promise(() => Filesystem.readText(ignorePath))).toContain("*.log")
|
||||
expect(yield* fsys.existsSafe(ignorePath)).toBe(true)
|
||||
expect(yield* fsys.readFileString(ignorePath)).toContain("*.log")
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
@@ -220,9 +221,10 @@ describe("file/index Filesystem patterns", () => {
|
||||
"handles missing .gitignore gracefully",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const fsys = yield* AppFileSystem.Service
|
||||
const test = yield* TestInstance
|
||||
const gitignorePath = path.join(test.directory, ".gitignore")
|
||||
expect(yield* Effect.promise(() => Filesystem.exists(gitignorePath))).toBe(false)
|
||||
expect(yield* fsys.existsSafe(gitignorePath)).toBe(false)
|
||||
|
||||
const nodes = yield* list()
|
||||
expect(Array.isArray(nodes)).toBe(true)
|
||||
@@ -231,16 +233,17 @@ describe("file/index Filesystem patterns", () => {
|
||||
)
|
||||
})
|
||||
|
||||
describe("File.changed() - Filesystem.readText() for untracked files", () => {
|
||||
describe("File.changed() - AppFileSystem.readFileString() for untracked files", () => {
|
||||
it.instance(
|
||||
"reads untracked files via Filesystem.readText()",
|
||||
"reads untracked files via AppFileSystem.readFileString()",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const fsys = yield* AppFileSystem.Service
|
||||
const test = yield* TestInstance
|
||||
const untrackedPath = path.join(test.directory, "untracked.txt")
|
||||
yield* Effect.promise(() => fs.writeFile(untrackedPath, "new content\nwith multiple lines", "utf-8"))
|
||||
yield* fsys.writeFileString(untrackedPath, "new content\nwith multiple lines")
|
||||
|
||||
const content = yield* Effect.promise(() => Filesystem.readText(untrackedPath))
|
||||
const content = yield* fsys.readFileString(untrackedPath)
|
||||
expect(content.split("\n").length).toBe(2)
|
||||
}),
|
||||
{ git: true },
|
||||
@@ -248,28 +251,26 @@ describe("file/index Filesystem patterns", () => {
|
||||
})
|
||||
|
||||
describe("Error handling", () => {
|
||||
it.instance("handles errors gracefully in Filesystem.readText()", () =>
|
||||
it.instance("handles errors gracefully in AppFileSystem.readFileString()", () =>
|
||||
Effect.gen(function* () {
|
||||
const fsys = yield* AppFileSystem.Service
|
||||
const test = yield* TestInstance
|
||||
yield* Effect.promise(() => fs.writeFile(path.join(test.directory, "readonly.txt"), "content", "utf-8"))
|
||||
yield* fsys.writeFileString(path.join(test.directory, "readonly.txt"), "content")
|
||||
|
||||
const nonExistentPath = path.join(test.directory, "does-not-exist.txt")
|
||||
expect(
|
||||
Exit.isFailure(yield* Effect.promise(() => Filesystem.readText(nonExistentPath)).pipe(Effect.exit)),
|
||||
).toBe(true)
|
||||
expect(Exit.isFailure(yield* fsys.readFileString(nonExistentPath).pipe(Effect.exit))).toBe(true)
|
||||
|
||||
const result = yield* read("does-not-exist.txt")
|
||||
expect(result.content).toBe("")
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("handles errors in Filesystem.readArrayBuffer()", () =>
|
||||
it.instance("handles errors in AppFileSystem.readFile()", () =>
|
||||
Effect.gen(function* () {
|
||||
const fsys = yield* AppFileSystem.Service
|
||||
const test = yield* TestInstance
|
||||
const nonExistentPath = path.join(test.directory, "does-not-exist.bin")
|
||||
const buffer = yield* Effect.promise(() =>
|
||||
Filesystem.readArrayBuffer(nonExistentPath).catch(() => new ArrayBuffer(0)),
|
||||
)
|
||||
const buffer = yield* fsys.readFile(nonExistentPath).pipe(Effect.orElseSucceed(() => new Uint8Array(0)))
|
||||
expect(buffer.byteLength).toBe(0)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -4,9 +4,9 @@ import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { disposeAllInstances, provideInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
|
||||
const disableDefault = process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS
|
||||
process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = "1"
|
||||
@@ -30,7 +30,7 @@ afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
})
|
||||
|
||||
const it = testEffect(CrossSpawnSpawner.defaultLayer)
|
||||
const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, AppFileSystem.defaultLayer))
|
||||
|
||||
function withTmp<T, A, E, R>(
|
||||
init: (dir: string) => Promise<T>,
|
||||
@@ -846,7 +846,7 @@ describe("plugin.loader.shared", () => {
|
||||
Effect.gen(function* () {
|
||||
yield* load(tmp.path)
|
||||
expect(
|
||||
yield* Effect.promise(() => Filesystem.readJson<{ source: string; enabled: boolean }>(tmp.extra.mark)),
|
||||
(yield* (yield* AppFileSystem.Service).readJson(tmp.extra.mark)) as { source: string; enabled: boolean },
|
||||
).toEqual({
|
||||
source: "tuple",
|
||||
enabled: true,
|
||||
@@ -980,7 +980,8 @@ export default {
|
||||
(tmp) =>
|
||||
Effect.gen(function* () {
|
||||
const file = path.join(tmp.extra.mod, "package.json")
|
||||
const json = yield* Effect.promise(() => Filesystem.readJson<Record<string, unknown>>(file))
|
||||
const fsys = yield* AppFileSystem.Service
|
||||
const json = (yield* fsys.readJson(file)) as Record<string, unknown>
|
||||
const list = readPackageThemes("acme-plugin", {
|
||||
dir: tmp.extra.mod,
|
||||
pkg: file,
|
||||
@@ -988,8 +989,8 @@ export default {
|
||||
})
|
||||
|
||||
expect(list).toEqual([
|
||||
Filesystem.resolve(path.join(tmp.extra.mod, "themes", "one.json")),
|
||||
Filesystem.resolve(path.join(tmp.extra.mod, "themes", "two.json")),
|
||||
AppFileSystem.resolve(path.join(tmp.extra.mod, "themes", "one.json")),
|
||||
AppFileSystem.resolve(path.join(tmp.extra.mod, "themes", "two.json")),
|
||||
])
|
||||
}),
|
||||
),
|
||||
@@ -1053,7 +1054,7 @@ export default {
|
||||
{
|
||||
spec: "acme-plugin@1.0.0",
|
||||
target: tmp.extra.mod,
|
||||
themes: [Filesystem.resolve(path.join(tmp.extra.mod, "themes", "night.json"))],
|
||||
themes: [AppFileSystem.resolve(path.join(tmp.extra.mod, "themes", "night.json"))],
|
||||
},
|
||||
])
|
||||
expect(missing).toHaveLength(0)
|
||||
@@ -1116,7 +1117,7 @@ export default {
|
||||
expect(loaded).toEqual([
|
||||
{
|
||||
spec: "acme-plugin@1.0.0",
|
||||
themes: [Filesystem.resolve(path.join(tmp.extra.mod, "themes", "night.json"))],
|
||||
themes: [AppFileSystem.resolve(path.join(tmp.extra.mod, "themes", "night.json"))],
|
||||
},
|
||||
])
|
||||
} finally {
|
||||
@@ -1137,7 +1138,8 @@ export default {
|
||||
},
|
||||
(tmp) =>
|
||||
Effect.gen(function* () {
|
||||
const json = yield* Effect.promise(() => Filesystem.readJson<Record<string, unknown>>(tmp.extra.file))
|
||||
const fsys = yield* AppFileSystem.Service
|
||||
const json = (yield* fsys.readJson(tmp.extra.file)) as Record<string, unknown>
|
||||
expect(() =>
|
||||
readPackageThemes("acme", {
|
||||
dir: tmp.extra.mod,
|
||||
|
||||
@@ -1195,7 +1195,7 @@ describe("tool.shell truncation", () => {
|
||||
const filepath = (result.metadata as { outputPath?: string }).outputPath
|
||||
expect(filepath).toBeTruthy()
|
||||
|
||||
const saved = yield* Effect.promise(() => Filesystem.readText(filepath!))
|
||||
const saved = yield* (yield* AppFileSystem.Service).readFileString(filepath!)
|
||||
const lines = saved.trim().split(/\r?\n/)
|
||||
expect(lines.length).toBe(lineCount)
|
||||
expect(lines[0]).toBe("1")
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Effect, FileSystem, Layer } from "effect"
|
||||
import { Truncate } from "@/tool/truncate"
|
||||
import { Config } from "@/config/config"
|
||||
import { Identifier } from "../../src/id/id"
|
||||
import { Process } from "@/util/process"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import path from "path"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { writeFileStringScoped } from "../lib/filesystem"
|
||||
@@ -14,10 +14,15 @@ import { TestConfig } from "../fixture/config"
|
||||
const FIXTURES_DIR = path.join(import.meta.dir, "fixtures")
|
||||
const ROOT = path.resolve(import.meta.dir, "..", "..")
|
||||
|
||||
const it = testEffect(Layer.mergeAll(Truncate.defaultLayer, NodeFileSystem.layer))
|
||||
const it = testEffect(Layer.mergeAll(Truncate.defaultLayer, NodeFileSystem.layer, AppFileSystem.defaultLayer))
|
||||
|
||||
const configuredLayer = (cfg: Config.Info) =>
|
||||
Layer.mergeAll(Truncate.defaultLayer, NodeFileSystem.layer, TestConfig.layer({ get: () => Effect.succeed(cfg) }))
|
||||
Layer.mergeAll(
|
||||
Truncate.defaultLayer,
|
||||
NodeFileSystem.layer,
|
||||
AppFileSystem.defaultLayer,
|
||||
TestConfig.layer({ get: () => Effect.succeed(cfg) }),
|
||||
)
|
||||
const configuredIt = (cfg: Config.Info) => testEffect(configuredLayer(cfg))
|
||||
|
||||
describe("Truncate", () => {
|
||||
@@ -25,7 +30,8 @@ describe("Truncate", () => {
|
||||
it.live("truncates large json file by bytes", () =>
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Truncate.Service
|
||||
const content = yield* Effect.promise(() => Filesystem.readText(path.join(FIXTURES_DIR, "models-api.json")))
|
||||
const fsys = yield* AppFileSystem.Service
|
||||
const content = yield* fsys.readFileString(path.join(FIXTURES_DIR, "models-api.json"))
|
||||
const result = yield* svc.output(content)
|
||||
|
||||
expect(result.truncated).toBe(true)
|
||||
@@ -158,7 +164,8 @@ describe("Truncate", () => {
|
||||
it.live("large single-line file truncates with byte message", () =>
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Truncate.Service
|
||||
const content = yield* Effect.promise(() => Filesystem.readText(path.join(FIXTURES_DIR, "models-api.json")))
|
||||
const fsys = yield* AppFileSystem.Service
|
||||
const content = yield* fsys.readFileString(path.join(FIXTURES_DIR, "models-api.json"))
|
||||
const result = yield* svc.output(content)
|
||||
|
||||
expect(result.truncated).toBe(true)
|
||||
@@ -180,7 +187,8 @@ describe("Truncate", () => {
|
||||
expect(result.outputPath).toBeDefined()
|
||||
expect(result.outputPath).toContain("tool_")
|
||||
|
||||
const written = yield* Effect.promise(() => Filesystem.readText(result.outputPath!))
|
||||
const fsys = yield* AppFileSystem.Service
|
||||
const written = yield* fsys.readFileString(result.outputPath!)
|
||||
expect(written).toBe(lines)
|
||||
}),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user