Преглед изворни кода

refactor: replace Filesystem util with AppFileSystem service (#20127)

Kit Langton пре 2 недеља
родитељ
комит
e6f6f7aff1

+ 1 - 1
packages/opencode/src/agent/agent.ts

@@ -393,7 +393,7 @@ export namespace Agent {
   )
 
   export const defaultLayer = layer.pipe(
-    Layer.provide(Auth.layer),
+    Layer.provide(Auth.defaultLayer),
     Layer.provide(Config.defaultLayer),
     Layer.provide(Skill.defaultLayer),
   )

+ 11 - 19
packages/opencode/src/auth/index.ts

@@ -3,7 +3,7 @@ import { Effect, Layer, Record, Result, Schema, ServiceMap } from "effect"
 import { makeRuntime } from "@/effect/run-service"
 import { zod } from "@/util/effect-zod"
 import { Global } from "../global"
-import { Filesystem } from "../util/filesystem"
+import { AppFileSystem } from "../filesystem"
 
 export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key"
 
@@ -53,17 +53,13 @@ export namespace Auth {
   export const layer = Layer.effect(
     Service,
     Effect.gen(function* () {
+      const fsys = yield* AppFileSystem.Service
       const decode = Schema.decodeUnknownOption(Info)
 
-      const all = Effect.fn("Auth.all")(() =>
-        Effect.tryPromise({
-          try: async () => {
-            const data = await Filesystem.readJson<Record<string, unknown>>(file).catch(() => ({}))
-            return Record.filterMap(data, (value) => Result.fromOption(decode(value), () => undefined))
-          },
-          catch: fail("Failed to read auth data"),
-        }),
-      )
+      const all = Effect.fn("Auth.all")(function* () {
+        const data = (yield* fsys.readJson(file).pipe(Effect.orElseSucceed(() => ({})))) as Record<string, unknown>
+        return Record.filterMap(data, (value) => Result.fromOption(decode(value), () => undefined))
+      })
 
       const get = Effect.fn("Auth.get")(function* (providerID: string) {
         return (yield* all())[providerID]
@@ -74,10 +70,7 @@ export namespace Auth {
         const data = yield* all()
         if (norm !== key) delete data[key]
         delete data[norm + "/"]
-        yield* Effect.tryPromise({
-          try: () => Filesystem.writeJson(file, { ...data, [norm]: info }, 0o600),
-          catch: fail("Failed to write auth data"),
-        })
+        yield* fsys.writeJson(file, { ...data, [norm]: info }, 0o600).pipe(Effect.mapError(fail("Failed to write auth data")))
       })
 
       const remove = Effect.fn("Auth.remove")(function* (key: string) {
@@ -85,17 +78,16 @@ export namespace Auth {
         const data = yield* all()
         delete data[key]
         delete data[norm]
-        yield* Effect.tryPromise({
-          try: () => Filesystem.writeJson(file, data, 0o600),
-          catch: fail("Failed to write auth data"),
-        })
+        yield* fsys.writeJson(file, data, 0o600).pipe(Effect.mapError(fail("Failed to write auth data")))
       })
 
       return Service.of({ get, all, set, remove })
     }),
   )
 
-  const { runPromise } = makeRuntime(Service, layer)
+  export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
+
+  const { runPromise } = makeRuntime(Service, defaultLayer)
 
   export async function get(providerID: string) {
     return runPromise((service) => service.get(providerID))

+ 1 - 1
packages/opencode/src/config/config.ts

@@ -1540,7 +1540,7 @@ export namespace Config {
 
   export const defaultLayer = layer.pipe(
     Layer.provide(AppFileSystem.defaultLayer),
-    Layer.provide(Auth.layer),
+    Layer.provide(Auth.defaultLayer),
     Layer.provide(Account.defaultLayer),
   )
 

+ 1 - 1
packages/opencode/src/file/index.ts

@@ -541,7 +541,7 @@ export namespace File {
         const exists = yield* appFs.existsSafe(full)
         if (!exists) return { type: "text" as const, content: "" }
 
-        const mimeType = Filesystem.mimeType(full)
+        const mimeType = AppFileSystem.mimeType(full)
         const encode = knownText ? false : shouldEncode(mimeType)
 
         if (encode && !isImage(mimeType)) return { type: "binary" as const, content: "", mimeType }

+ 16 - 16
packages/opencode/src/file/time.ts

@@ -1,9 +1,9 @@
-import { DateTime, Effect, Layer, Semaphore, ServiceMap } from "effect"
+import { DateTime, Effect, Layer, Option, Semaphore, ServiceMap } from "effect"
 import { InstanceState } from "@/effect/instance-state"
 import { makeRuntime } from "@/effect/run-service"
+import { AppFileSystem } from "@/filesystem"
 import { Flag } from "@/flag/flag"
 import type { SessionID } from "@/session/schema"
-import { Filesystem } from "../util/filesystem"
 import { Log } from "../util/log"
 
 export namespace FileTime {
@@ -12,21 +12,9 @@ export namespace FileTime {
   export type Stamp = {
     readonly read: Date
     readonly mtime: number | undefined
-    readonly ctime: number | undefined
     readonly size: number | undefined
   }
 
-  const stamp = Effect.fnUntraced(function* (file: string) {
-    const stat = Filesystem.stat(file)
-    const size = typeof stat?.size === "bigint" ? Number(stat.size) : stat?.size
-    return {
-      read: yield* DateTime.nowAsDate,
-      mtime: stat?.mtime?.getTime(),
-      ctime: stat?.ctime?.getTime(),
-      size,
-    }
-  })
-
   const session = (reads: Map<SessionID, Map<string, Stamp>>, sessionID: SessionID) => {
     const value = reads.get(sessionID)
     if (value) return value
@@ -53,7 +41,17 @@ export namespace FileTime {
   export const layer = Layer.effect(
     Service,
     Effect.gen(function* () {
+      const fsys = yield* AppFileSystem.Service
       const disableCheck = yield* Flag.OPENCODE_DISABLE_FILETIME_CHECK
+
+      const stamp = Effect.fnUntraced(function* (file: string) {
+        const info = yield* fsys.stat(file).pipe(Effect.catch(() => Effect.succeed(undefined)))
+        return {
+          read: yield* DateTime.nowAsDate,
+          mtime: info ? Option.getOrUndefined(info.mtime)?.getTime() : undefined,
+          size: info ? Number(info.size) : undefined,
+        }
+      })
       const state = yield* InstanceState.make<State>(
         Effect.fn("FileTime.state")(() =>
           Effect.succeed({
@@ -92,7 +90,7 @@ export namespace FileTime {
         if (!time) throw new Error(`You must read file ${filepath} before overwriting it. Use the Read tool first`)
 
         const next = yield* stamp(filepath)
-        const changed = next.mtime !== time.mtime || next.ctime !== time.ctime || next.size !== time.size
+        const changed = next.mtime !== time.mtime || next.size !== time.size
         if (!changed) return
 
         throw new Error(
@@ -108,7 +106,9 @@ export namespace FileTime {
     }),
   ).pipe(Layer.orDie)
 
-  const { runPromise } = makeRuntime(Service, layer)
+  export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
+
+  const { runPromise } = makeRuntime(Service, defaultLayer)
 
   export function read(sessionID: SessionID, file: string) {
     return runPromise((s) => s.read(sessionID, file))

+ 1 - 1
packages/opencode/src/provider/auth.ts

@@ -230,7 +230,7 @@ export namespace ProviderAuth {
     }),
   )
 
-  export const defaultLayer = layer.pipe(Layer.provide(Auth.layer))
+  export const defaultLayer = layer.pipe(Layer.provide(Auth.defaultLayer))
 
   const { runPromise } = makeRuntime(Service, defaultLayer)
 

+ 1 - 1
packages/opencode/src/session/prompt.ts

@@ -1704,7 +1704,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
         Layer.provide(Permission.layer),
         Layer.provide(MCP.defaultLayer),
         Layer.provide(LSP.defaultLayer),
-        Layer.provide(FileTime.layer),
+        Layer.provide(FileTime.defaultLayer),
         Layer.provide(ToolRegistry.defaultLayer),
         Layer.provide(Truncate.layer),
         Layer.provide(AppFileSystem.defaultLayer),

+ 49 - 56
packages/opencode/src/skill/index.ts

@@ -11,7 +11,7 @@ import { makeRuntime } from "@/effect/run-service"
 import { Flag } from "@/flag/flag"
 import { Global } from "@/global"
 import { Permission } from "@/permission"
-import { Filesystem } from "@/util/filesystem"
+import { AppFileSystem } from "@/filesystem"
 import { Config } from "../config/config"
 import { ConfigMarkdown } from "../config/markdown"
 import { Glob } from "../util/glob"
@@ -139,28 +139,20 @@ export namespace Skill {
     config: Config.Interface,
     discovery: Discovery.Interface,
     bus: Bus.Interface,
+    fsys: AppFileSystem.Interface,
     directory: string,
     worktree: string,
   ) {
     if (!Flag.OPENCODE_DISABLE_EXTERNAL_SKILLS) {
       for (const dir of EXTERNAL_DIRS) {
         const root = path.join(Global.Path.home, dir)
-        const isDir = yield* Effect.promise(() => Filesystem.isDir(root))
-        if (!isDir) continue
+        if (!(yield* fsys.isDir(root))) continue
         yield* scan(state, bus, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "global" })
       }
 
-      const upDirs = yield* Effect.promise(async () => {
-        const dirs: string[] = []
-        for await (const root of Filesystem.up({
-          targets: EXTERNAL_DIRS,
-          start: directory,
-          stop: worktree,
-        })) {
-          dirs.push(root)
-        }
-        return dirs
-      })
+      const upDirs = yield* fsys
+        .up({ targets: EXTERNAL_DIRS, start: directory, stop: worktree })
+        .pipe(Effect.catch(() => Effect.succeed([] as string[])))
 
       for (const root of upDirs) {
         yield* scan(state, bus, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "project" })
@@ -176,8 +168,7 @@ export namespace Skill {
     for (const item of cfg.skills?.paths ?? []) {
       const expanded = item.startsWith("~/") ? path.join(os.homedir(), item.slice(2)) : item
       const dir = path.isAbsolute(expanded) ? expanded : path.join(directory, expanded)
-      const isDir = yield* Effect.promise(() => Filesystem.isDir(dir))
-      if (!isDir) {
+      if (!(yield* fsys.isDir(dir))) {
         log.warn("skill path not found", { path: dir })
         continue
       }
@@ -198,50 +189,52 @@ export namespace Skill {
 
   export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Skill") {}
 
-  export const layer: Layer.Layer<Service, never, Discovery.Service | Config.Service | Bus.Service> = Layer.effect(
-    Service,
-    Effect.gen(function* () {
-      const discovery = yield* Discovery.Service
-      const config = yield* Config.Service
-      const bus = yield* Bus.Service
-      const state = yield* InstanceState.make(
-        Effect.fn("Skill.state")(function* (ctx) {
-          const s: State = { skills: {}, dirs: new Set() }
-          yield* loadSkills(s, config, discovery, bus, ctx.directory, ctx.worktree)
-          return s
-        }),
-      )
-
-      const get = Effect.fn("Skill.get")(function* (name: string) {
-        const s = yield* InstanceState.get(state)
-        return s.skills[name]
-      })
-
-      const all = Effect.fn("Skill.all")(function* () {
-        const s = yield* InstanceState.get(state)
-        return Object.values(s.skills)
-      })
-
-      const dirs = Effect.fn("Skill.dirs")(function* () {
-        const s = yield* InstanceState.get(state)
-        return Array.from(s.dirs)
-      })
-
-      const available = Effect.fn("Skill.available")(function* (agent?: Agent.Info) {
-        const s = yield* InstanceState.get(state)
-        const list = Object.values(s.skills).toSorted((a, b) => a.name.localeCompare(b.name))
-        if (!agent) return list
-        return list.filter((skill) => Permission.evaluate("skill", skill.name, agent.permission).action !== "deny")
-      })
-
-      return Service.of({ get, all, dirs, available })
-    }),
-  )
+  export const layer = Layer.effect(
+      Service,
+      Effect.gen(function* () {
+        const discovery = yield* Discovery.Service
+        const config = yield* Config.Service
+        const bus = yield* Bus.Service
+        const fsys = yield* AppFileSystem.Service
+        const state = yield* InstanceState.make(
+          Effect.fn("Skill.state")(function* (ctx) {
+            const s: State = { skills: {}, dirs: new Set() }
+            yield* loadSkills(s, config, discovery, bus, fsys, ctx.directory, ctx.worktree)
+            return s
+          }),
+        )
+
+        const get = Effect.fn("Skill.get")(function* (name: string) {
+          const s = yield* InstanceState.get(state)
+          return s.skills[name]
+        })
+
+        const all = Effect.fn("Skill.all")(function* () {
+          const s = yield* InstanceState.get(state)
+          return Object.values(s.skills)
+        })
+
+        const dirs = Effect.fn("Skill.dirs")(function* () {
+          const s = yield* InstanceState.get(state)
+          return Array.from(s.dirs)
+        })
+
+        const available = Effect.fn("Skill.available")(function* (agent?: Agent.Info) {
+          const s = yield* InstanceState.get(state)
+          const list = Object.values(s.skills).toSorted((a, b) => a.name.localeCompare(b.name))
+          if (!agent) return list
+          return list.filter((skill) => Permission.evaluate("skill", skill.name, agent.permission).action !== "deny")
+        })
+
+        return Service.of({ get, all, dirs, available })
+      }),
+    )
 
-  export const defaultLayer: Layer.Layer<Service> = layer.pipe(
+  export const defaultLayer = layer.pipe(
     Layer.provide(Discovery.defaultLayer),
     Layer.provide(Config.defaultLayer),
     Layer.provide(Bus.layer),
+    Layer.provide(AppFileSystem.defaultLayer),
   )
 
   export function fmt(list: Info[], opts: { verbose: boolean }) {