Просмотр исходного кода

refactor(opencode): switch cli file io to AppFileSystem

Kit Langton 3 дней назад
Родитель
Сommit
042e63fdc3

+ 9 - 2
packages/opencode/src/acp/agent.ts

@@ -33,7 +33,6 @@ import {
 
 import { Log } from "../util/log"
 import { pathToFileURL } from "url"
-import { Filesystem } from "../util/filesystem"
 import { Hash } from "../util/hash"
 import { ACPSessionManager } from "./session"
 import type { ACPConfig } from "./types"
@@ -48,7 +47,9 @@ import { Todo } from "@/session/todo"
 import { z } from "zod"
 import { LoadAPIKeyError } from "ai"
 import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2"
+import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 import { applyPatch } from "diff"
+import { Effect } from "effect"
 
 type ModeOption = { id: string; name: string; description?: string }
 type ModelOption = { modelId: string; name: string }
@@ -238,7 +239,13 @@ export namespace ACP {
                 const metadata = permission.metadata || {}
                 const filepath = typeof metadata["filepath"] === "string" ? metadata["filepath"] : ""
                 const diff = typeof metadata["diff"] === "string" ? metadata["diff"] : ""
-                const content = (await Filesystem.exists(filepath)) ? await Filesystem.readText(filepath) : ""
+                const content = await AppRuntime.runPromise(
+                  AppFileSystem.Service.use((fs) =>
+                    fs
+                      .existsSafe(filepath)
+                      .pipe(Effect.flatMap((exists) => (exists ? fs.readFileString(filepath) : Effect.succeed("")))),
+                  ),
+                )
                 const newContent = getNewContent(content, diff)
 
                 if (newContent) {

+ 3 - 3
packages/opencode/src/cli/cmd/agent.ts

@@ -7,11 +7,11 @@ import { Agent } from "../../agent/agent"
 import { Provider } from "../../provider/provider"
 import path from "path"
 import fs from "fs/promises"
-import { Filesystem } from "../../util/filesystem"
 import matter from "gray-matter"
 import { Instance } from "../../project/instance"
 import { EOL } from "os"
 import type { Argv } from "yargs"
+import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 
 type AgentMode = "all" | "primary" | "subagent"
 
@@ -194,7 +194,7 @@ const AgentCreateCommand = cmd({
 
         await fs.mkdir(targetPath, { recursive: true })
 
-        if (await Filesystem.exists(filePath)) {
+        if (await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.existsSafe(filePath)))) {
           if (isFullyNonInteractive) {
             console.error(`Error: Agent file already exists: ${filePath}`)
             process.exit(1)
@@ -203,7 +203,7 @@ const AgentCreateCommand = cmd({
           throw new UI.CancelledError()
         }
 
-        await Filesystem.write(filePath, content)
+        await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.writeWithDirs(filePath, content)))
 
         if (isFullyNonInteractive) {
           console.log(filePath)

+ 8 - 4
packages/opencode/src/cli/cmd/github.ts

@@ -1,6 +1,5 @@
 import path from "path"
 import { exec } from "child_process"
-import { Filesystem } from "../../util/filesystem"
 import * as prompts from "@clack/prompts"
 import { map, pipe, sortBy, values } from "remeda"
 import { Octokit } from "@octokit/rest"
@@ -34,6 +33,7 @@ import { Git } from "@/git"
 import { setTimeout as sleep } from "node:timers/promises"
 import { Process } from "@/util/process"
 import { Effect } from "effect"
+import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 
 type GitHubAuthor = {
   login: string
@@ -381,9 +381,11 @@ export const GithubInstallCommand = cmd({
                 ? ""
                 : `\n        env:${providers[provider].env.map((e) => `\n          ${e}: \${{ secrets.${e} }}`).join("")}`
 
-            await Filesystem.write(
-              path.join(app.root, WORKFLOW_FILE),
-              `name: opencode
+            await AppRuntime.runPromise(
+              AppFileSystem.Service.use((fs) =>
+                fs.writeWithDirs(
+                  path.join(app.root, WORKFLOW_FILE),
+                  `name: opencode
 
 on:
   issue_comment:
@@ -414,6 +416,8 @@ jobs:
         uses: anomalyco/opencode/github@latest${envStr}
         with:
           model: ${provider}/${model}`,
+                ),
+              ),
             )
 
             prompts.log.success(`Added workflow file: "${WORKFLOW_FILE}"`)

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

@@ -9,8 +9,8 @@ import { SessionTable, MessageTable, PartTable } from "../../session/session.sql
 import { Instance } from "../../project/instance"
 import { ShareNext } from "../../share/share-next"
 import { EOL } from "os"
-import { Filesystem } from "../../util/filesystem"
 import { AppRuntime } from "@/effect/app-runtime"
+import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 
 /** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */
 export type ShareData =
@@ -140,7 +140,9 @@ export const ImportCommand = cmd({
 
         exportData = transformed
       } else {
-        exportData = await Filesystem.readJson<NonNullable<typeof exportData>>(args.file).catch(() => undefined)
+        exportData = (await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.readJson(args.file))).catch(
+          () => undefined,
+        )) as NonNullable<typeof exportData> | undefined
         if (!exportData) {
           process.stdout.write(`File not found: ${args.file}`)
           process.stdout.write(EOL)

+ 5 - 5
packages/opencode/src/cli/cmd/mcp.ts

@@ -13,10 +13,10 @@ import { Installation } from "../../installation"
 import path from "path"
 import { Global } from "../../global"
 import { modify, applyEdits } from "jsonc-parser"
-import { Filesystem } from "../../util/filesystem"
 import { Bus } from "../../bus"
 import { AppRuntime } from "../../effect/app-runtime"
 import { Effect } from "effect"
+import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 
 function getAuthStatusIcon(status: MCP.AuthStatus): string {
   switch (status) {
@@ -416,7 +416,7 @@ async function resolveConfigPath(baseDir: string, global = false) {
   }
 
   for (const candidate of candidates) {
-    if (await Filesystem.exists(candidate)) {
+    if (await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.existsSafe(candidate)))) {
       return candidate
     }
   }
@@ -427,8 +427,8 @@ async function resolveConfigPath(baseDir: string, global = false) {
 
 async function addMcpToConfig(name: string, mcpConfig: Config.Mcp, configPath: string) {
   let text = "{}"
-  if (await Filesystem.exists(configPath)) {
-    text = await Filesystem.readText(configPath)
+  if (await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.existsSafe(configPath)))) {
+    text = await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.readFileString(configPath)))
   }
 
   // Use jsonc-parser to modify while preserving comments
@@ -437,7 +437,7 @@ async function addMcpToConfig(name: string, mcpConfig: Config.Mcp, configPath: s
   })
   const result = applyEdits(text, edits)
 
-  await Filesystem.write(configPath, result)
+  await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.writeWithDirs(configPath, result)))
 
   return configPath
 }

+ 5 - 3
packages/opencode/src/cli/cmd/run.ts

@@ -1,12 +1,12 @@
 import type { Argv } from "yargs"
 import path from "path"
 import { pathToFileURL } from "url"
+import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 import { UI } from "../ui"
 import { cmd } from "./cmd"
 import { Flag } from "../../flag/flag"
 import { bootstrap } from "../bootstrap"
 import { EOL } from "os"
-import { Filesystem } from "../../util/filesystem"
 import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
 import { Server } from "../../server/server"
 import { Provider } from "../../provider/provider"
@@ -332,12 +332,14 @@ export const RunCommand = cmd({
 
       for (const filePath of list) {
         const resolvedPath = path.resolve(process.cwd(), filePath)
-        if (!(await Filesystem.exists(resolvedPath))) {
+        if (!(await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.existsSafe(resolvedPath))))) {
           UI.error(`File not found: ${filePath}`)
           process.exit(1)
         }
 
-        const mime = (await Filesystem.isDir(resolvedPath)) ? "application/x-directory" : "text/plain"
+        const mime = (await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.isDir(resolvedPath))))
+          ? "application/x-directory"
+          : "text/plain"
 
         files.push({
           type: "file",

+ 6 - 4
packages/opencode/src/cli/cmd/uninstall.ts

@@ -7,8 +7,8 @@ import { Global } from "../../global"
 import fs from "fs/promises"
 import path from "path"
 import os from "os"
-import { Filesystem } from "../../util/filesystem"
 import { Process } from "../../util/process"
+import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 
 interface UninstallArgs {
   keepConfig: boolean
@@ -266,7 +266,9 @@ async function getShellConfigFile(): Promise<string | null> {
       .catch(() => false)
     if (!exists) continue
 
-    const content = await Filesystem.readText(file).catch(() => "")
+    const content = await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.readFileString(file))).catch(
+      () => "",
+    )
     if (content.includes("# opencode") || content.includes(".opencode/bin")) {
       return file
     }
@@ -276,7 +278,7 @@ async function getShellConfigFile(): Promise<string | null> {
 }
 
 async function cleanShellConfig(file: string) {
-  const content = await Filesystem.readText(file)
+  const content = await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.readFileString(file)))
   const lines = content.split("\n")
 
   const filtered: string[] = []
@@ -312,7 +314,7 @@ async function cleanShellConfig(file: string) {
   }
 
   const output = filtered.join("\n") + "\n"
-  await Filesystem.write(file, output)
+  await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.writeWithDirs(file, output)))
 }
 
 async function getDirectorySize(dir: string): Promise<number> {

+ 3 - 2
packages/opencode/src/config/markdown.ts

@@ -1,7 +1,8 @@
 import { NamedError } from "@opencode-ai/shared/util/error"
+import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 import matter from "gray-matter"
 import { z } from "zod"
-import { Filesystem } from "../util/filesystem"
+import { AppRuntime } from "@/effect/app-runtime"
 
 export namespace ConfigMarkdown {
   export const FILE_REGEX = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g
@@ -69,7 +70,7 @@ export namespace ConfigMarkdown {
   }
 
   export async function parse(filePath: string) {
-    const template = await Filesystem.readText(filePath)
+    const template = await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.readFileString(filePath)))
 
     try {
       const md = matter(template)

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

@@ -9,9 +9,9 @@ import { SyncEvent } from "@/sync"
 import { EventTable } from "@/sync/event.sql"
 import { Flag } from "@/flag/flag"
 import { Log } from "@/util/log"
-import { Filesystem } from "@/util/filesystem"
 import { ProjectID } from "@/project/schema"
 import { Slug } from "@opencode-ai/shared/util/slug"
+import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 import { WorkspaceTable } from "./workspace.sql"
 import { getAdaptor } from "./adaptors"
 import { WorkspaceInfo } from "./types"
@@ -418,7 +418,7 @@ export namespace Workspace {
     if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return
 
     if (space.type === "worktree") {
-      void Filesystem.exists(space.directory!).then((exists) => {
+      void AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.existsSafe(space.directory!))).then((exists) => {
         setStatus(space.id, exists ? "connected" : "error", exists ? undefined : "directory does not exist")
       })
       return

+ 6 - 5
packages/opencode/src/file/ripgrep.ts

@@ -1,11 +1,11 @@
 import fs from "fs/promises"
 import path from "path"
 import { fileURLToPath } from "url"
+import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 import z from "zod"
 import { Cause, Context, Effect, Layer, Queue, Stream } from "effect"
 import { ripgrep } from "ripgrep"
 import { makeRuntime } from "@/effect/run-service"
-import { Filesystem } from "@/util/filesystem"
 import { Log } from "@/util/log"
 
 export namespace Ripgrep {
@@ -275,10 +275,11 @@ export namespace Ripgrep {
       return Effect.succeed(OPENCODE_RIPGREP_WORKER_PATH)
     }
     const js = new URL("./ripgrep.worker.js", import.meta.url)
-    return Effect.tryPromise({
-      try: () => Filesystem.exists(fileURLToPath(js)),
-      catch: toError,
-    }).pipe(Effect.map((exists) => (exists ? js : new URL("./ripgrep.worker.ts", import.meta.url))))
+    return Effect.gen(function* () {
+      const fs = yield* AppFileSystem.Service
+      const exists = yield* fs.exists(fileURLToPath(js)).pipe(Effect.orElseSucceed(() => false))
+      return exists ? js : new URL("./ripgrep.worker.ts", import.meta.url)
+    })
   }
 
   function worker() {

+ 12 - 3
packages/opencode/src/global/index.ts

@@ -1,8 +1,9 @@
 import fs from "fs/promises"
+import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir"
 import path from "path"
 import os from "os"
-import { Filesystem } from "../util/filesystem"
+import { Effect } from "effect"
 
 const app = "opencode"
 
@@ -36,7 +37,11 @@ await Promise.all([
 
 const CACHE_VERSION = "21"
 
-const version = await Filesystem.readText(path.join(Global.Path.cache, "version")).catch(() => "0")
+const version = await Effect.runPromise(
+  AppFileSystem.Service.use((fs) => fs.readFileString(path.join(Global.Path.cache, "version"))).pipe(
+    Effect.provide(AppFileSystem.defaultLayer),
+  ),
+).catch(() => "0")
 
 if (version !== CACHE_VERSION) {
   try {
@@ -50,5 +55,9 @@ if (version !== CACHE_VERSION) {
       ),
     )
   } catch (e) {}
-  await Filesystem.write(path.join(Global.Path.cache, "version"), CACHE_VERSION)
+  await Effect.runPromise(
+    AppFileSystem.Service.use((fs) => fs.writeWithDirs(path.join(Global.Path.cache, "version"), CACHE_VERSION)).pipe(
+      Effect.provide(AppFileSystem.defaultLayer),
+    ),
+  )
 }

+ 3 - 2
packages/opencode/src/index.ts

@@ -11,10 +11,10 @@ import { UninstallCommand } from "./cli/cmd/uninstall"
 import { ModelsCommand } from "./cli/cmd/models"
 import { UI } from "./cli/ui"
 import { Installation } from "./installation"
+import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 import { NamedError } from "@opencode-ai/shared/util/error"
 import { FormatError } from "./cli/error"
 import { ServeCommand } from "./cli/cmd/serve"
-import { Filesystem } from "./util/filesystem"
 import { DebugCommand } from "./cli/cmd/debug"
 import { StatsCommand } from "./cli/cmd/stats"
 import { McpCommand } from "./cli/cmd/mcp"
@@ -37,6 +37,7 @@ import { errorMessage } from "./util/error"
 import { PluginCommand } from "./cli/cmd/plug"
 import { Heap } from "./cli/heap"
 import { drizzle } from "drizzle-orm/bun-sqlite"
+import { AppRuntime } from "@/effect/app-runtime"
 
 process.on("unhandledRejection", (e) => {
   Log.Default.error("rejection", {
@@ -110,7 +111,7 @@ const cli = yargs(args)
     })
 
     const marker = path.join(Global.Path.data, "opencode.db")
-    if (!(await Filesystem.exists(marker))) {
+    if (!(await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.existsSafe(marker))))) {
       const tty = process.stderr.isTTY
       process.stderr.write("Performing one time database migration, may take a few minutes..." + EOL)
       const width = 36