Sfoglia il codice sorgente

effect(util): migrate filesystem callers to AppFileSystem.Service (#27152)

Kit Langton 3 mesi fa
parent
commit
ccb207f946

+ 5 - 4
packages/opencode/src/cli/cmd/import.ts

@@ -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)

+ 4 - 2
packages/opencode/src/control-plane/workspace.ts

@@ -10,9 +10,9 @@ import { GlobalBus } from "@/bus/global"
 import { Auth } from "@/auth"
 import { SyncEvent } from "@/sync"
 import { EventSequenceTable, EventTable } from "@/sync/event.sql"
+import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import * as Log from "@opencode-ai/core/util/log"
 import { RuntimeFlags } from "@/effect/runtime-flags"
-import { Filesystem } from "@/util/filesystem"
 import { ProjectID } from "@/project/schema"
 import { Slug } from "@opencode-ai/core/util/slug"
 import { WorkspaceTable } from "./workspace.sql"
@@ -176,6 +176,7 @@ export const layer = Layer.effect(
     const sync = yield* SyncEvent.Service
     const vcs = yield* Vcs.Service
     const flags = yield* RuntimeFlags.Service
+    const fs = yield* AppFileSystem.Service
     const connections = new Map<WorkspaceID, ConnectionStatus>()
     const syncFibers = yield* FiberMap.make<WorkspaceID, void, SyncLoopError>()
 
@@ -501,7 +502,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
       }
 
@@ -1040,6 +1041,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),
   Layer.provide(RuntimeFlags.defaultLayer),
 )

+ 2 - 0
packages/opencode/test/control-plane/workspace.test.ts

@@ -8,6 +8,7 @@ import { NodeHttpServer } from "@effect/platform-node"
 import { Effect, Layer, Schema } from "effect"
 import { FetchHttpClient, HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
 import { eq } from "drizzle-orm"
+import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import * as Log from "@opencode-ai/core/util/log"
 import { Flag } from "@opencode-ai/core/flag/flag"
 import { GlobalBus, type GlobalEvent } from "@/bus/global"
@@ -59,6 +60,7 @@ const workspaceLayer = (experimentalWorkspaces: boolean) =>
     Layer.provide(Project.defaultLayer),
     Layer.provide(Vcs.defaultLayer),
     Layer.provide(FetchHttpClient.layer),
+    Layer.provide(AppFileSystem.defaultLayer),
     Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces })),
     Layer.provide(InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer))),
   )

+ 25 - 24
packages/opencode/test/file/index.test.ts

@@ -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)
       }),
     )

+ 11 - 9
packages/opencode/test/plugin/loader-shared.test.ts

@@ -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 { Plugin } = await import("../../src/plugin/index")
 const { PluginLoader } = await import("../../src/plugin/loader")
@@ -20,7 +20,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>,
@@ -837,7 +837,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,
@@ -960,7 +960,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,
@@ -968,8 +969,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")),
           ])
         }),
     ),
@@ -1033,7 +1034,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)
@@ -1096,7 +1097,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 {
@@ -1117,7 +1118,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,

+ 1 - 0
packages/opencode/test/plugin/workspace-adapter.test.ts

@@ -56,6 +56,7 @@ const workspaceLayer = Workspace.layer.pipe(
   Layer.provide(Project.defaultLayer),
   Layer.provide(Vcs.defaultLayer),
   Layer.provide(FetchHttpClient.layer),
+  Layer.provide(AppFileSystem.defaultLayer),
   Layer.provide(InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrapLayer))),
   Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces: true })),
 )

+ 1 - 1
packages/opencode/test/tool/shell.test.ts

@@ -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")

+ 14 - 6
packages/opencode/test/tool/truncation.test.ts

@@ -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)
       }),
     )