diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 09f8663ed0..698027e1e0 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/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) { diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts index 60f52e403b..ae2e155b81 100644 --- a/packages/opencode/src/cli/cmd/agent.ts +++ b/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) diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index 074d9e5185..a660500c63 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/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}"`) diff --git a/packages/opencode/src/cli/cmd/import.ts b/packages/opencode/src/cli/cmd/import.ts index 1232f07422..a33185f4d1 100644 --- a/packages/opencode/src/cli/cmd/import.ts +++ b/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>(args.file).catch(() => undefined) + exportData = (await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.readJson(args.file))).catch( + () => undefined, + )) as NonNullable | undefined if (!exportData) { process.stdout.write(`File not found: ${args.file}`) process.stdout.write(EOL) diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index 3afedb356d..2f7685d297 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/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 } diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 17fc4bc087..f13e02200b 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/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", diff --git a/packages/opencode/src/cli/cmd/uninstall.ts b/packages/opencode/src/cli/cmd/uninstall.ts index 31830f0859..42edbf782e 100644 --- a/packages/opencode/src/cli/cmd/uninstall.ts +++ b/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 { .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 { } 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 { diff --git a/packages/opencode/src/config/markdown.ts b/packages/opencode/src/config/markdown.ts index 2f1483dca3..9a54edc6d3 100644 --- a/packages/opencode/src/config/markdown.ts +++ b/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 = /(? fs.readFileString(filePath))) try { const md = matter(template) diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index b9ac0a6b43..b44fbd6f3f 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/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 diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index abf7438dcc..f1940ff429 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/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() { diff --git a/packages/opencode/src/global/index.ts b/packages/opencode/src/global/index.ts index 869019e2ce..8c8d610e4c 100644 --- a/packages/opencode/src/global/index.ts +++ b/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), + ), + ) } diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 641411461d..b0e6e13fb7 100644 --- a/packages/opencode/src/index.ts +++ b/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