diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx
index 0c0658e743..40dd5df533 100644
--- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx
@@ -1,8 +1,10 @@
import { CliRenderEvents, SyntaxStyle, RGBA, type TerminalColors } from "@opentui/core"
import path from "path"
+import { Effect } from "effect"
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
import { createSimpleContext } from "./helper"
import { Glob } from "@opencode-ai/shared/util/glob"
+import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import aura from "./theme/aura.json" with { type: "json" }
import ayu from "./theme/ayu.json" with { type: "json" }
import catppuccin from "./theme/catppuccin.json" with { type: "json" }
@@ -39,8 +41,8 @@ import carbonfox from "./theme/carbonfox.json" with { type: "json" }
import { useKV } from "./kv"
import { useRenderer } from "@opentui/solid"
import { createStore, produce } from "solid-js/store"
+import { AppRuntime } from "@/effect/app-runtime"
import { Global } from "@/global"
-import { Filesystem } from "@/util/filesystem"
import { useTuiConfig } from "./tui-config"
import { isRecord } from "@/util/record"
import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui"
@@ -479,10 +481,13 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
async function getCustomThemes() {
const directories = [
Global.Path.config,
- ...(await Array.fromAsync(
- Filesystem.up({
- targets: [".opencode"],
- start: process.cwd(),
+ ...(await AppRuntime.runPromise(
+ Effect.gen(function* () {
+ const fs = yield* AppFileSystem.Service
+ return yield* fs.up({
+ targets: [".opencode"],
+ start: process.cwd(),
+ })
}),
)),
]
@@ -496,7 +501,12 @@ async function getCustomThemes() {
symlink: true,
})) {
const name = path.basename(item, ".json")
- result[name] = await Filesystem.readJson(item)
+ result[name] = (await AppRuntime.runPromise(
+ Effect.gen(function* () {
+ const fs = yield* AppFileSystem.Service
+ return yield* fs.readJson(item)
+ }),
+ )) as ThemeJson
}
}
return result
diff --git a/packages/opencode/src/config/paths.ts b/packages/opencode/src/config/paths.ts
index 884a774499..3e5ca2f452 100644
--- a/packages/opencode/src/config/paths.ts
+++ b/packages/opencode/src/config/paths.ts
@@ -2,30 +2,74 @@ import path from "path"
import os from "os"
import z from "zod"
import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"
+import { Effect } from "effect"
+import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { NamedError } from "@opencode-ai/shared/util/error"
-import { Filesystem } from "@/util/filesystem"
import { Flag } from "@/flag/flag"
import { Global } from "@/global"
+import { AppRuntime } from "@/effect/app-runtime"
+
+async function withFs(fn: (fs: AppFileSystem.Interface) => Effect.Effect) {
+ return AppRuntime.runPromise(
+ Effect.gen(function* () {
+ const fs = yield* AppFileSystem.Service
+ return yield* fn(fs)
+ }),
+ )
+}
+
+function missing(err: unknown) {
+ if (typeof err !== "object" || err === null) return false
+ if ("code" in err && err.code === "ENOENT") return true
+ return (
+ "reason" in err &&
+ typeof err.reason === "object" &&
+ err.reason !== null &&
+ "_tag" in err.reason &&
+ err.reason._tag === "NotFound"
+ )
+}
export namespace ConfigPaths {
export async function projectFiles(name: string, directory: string, worktree: string) {
- return Filesystem.findUp([`${name}.json`, `${name}.jsonc`], directory, worktree, { rootFirst: true })
+ return withFs(
+ Effect.fn("ConfigPaths.projectFiles")(function* (fs) {
+ const dirs = [directory]
+ let dir = directory
+ while (true) {
+ if (worktree === dir) break
+ const parent = path.dirname(dir)
+ if (parent === dir) break
+ dirs.push(parent)
+ dir = parent
+ }
+
+ const out: string[] = []
+ for (const dir of dirs.toReversed()) {
+ for (const target of [`${name}.json`, `${name}.jsonc`]) {
+ const file = path.join(dir, target)
+ if (yield* fs.existsSafe(file)) out.push(file)
+ }
+ }
+ return out
+ }),
+ )
}
export async function directories(directory: string, worktree: string) {
return [
Global.Path.config,
...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG
- ? await Array.fromAsync(
- Filesystem.up({
+ ? await withFs((fs) =>
+ fs.up({
targets: [".opencode"],
start: directory,
stop: worktree,
}),
)
: []),
- ...(await Array.fromAsync(
- Filesystem.up({
+ ...(await withFs((fs) =>
+ fs.up({
targets: [".opencode"],
start: Global.Path.home,
stop: Global.Path.home,
@@ -58,8 +102,8 @@ export namespace ConfigPaths {
/** Read a config file, returning undefined for missing files and throwing JsonError for other failures. */
export async function readFile(filepath: string) {
- return Filesystem.readText(filepath).catch((err: NodeJS.ErrnoException) => {
- if (err.code === "ENOENT") return
+ return withFs((fs) => fs.readFileString(filepath)).catch((err: unknown) => {
+ if (missing(err)) return
throw new JsonError({ path: filepath }, { cause: err })
})
}
@@ -108,11 +152,11 @@ export namespace ConfigPaths {
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
const fileContent = (
- await Filesystem.readText(resolvedPath).catch((error: NodeJS.ErrnoException) => {
+ await withFs((fs) => fs.readFileString(resolvedPath)).catch((error: unknown) => {
if (missing === "empty") return ""
const errMsg = `bad file reference: "${token}"`
- if (error.code === "ENOENT") {
+ if (missing(error)) {
throw new InvalidError(
{
path: configSource,
diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts
index 6c17310ff8..5a7cc9076e 100644
--- a/packages/opencode/src/format/formatter.ts
+++ b/packages/opencode/src/format/formatter.ts
@@ -1,10 +1,33 @@
+import { Effect } from "effect"
+import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Npm } from "../npm"
import { Instance } from "../project/instance"
-import { Filesystem } from "../util/filesystem"
import { Process } from "../util/process"
import { which } from "../util/which"
+import { AppRuntime } from "@/effect/app-runtime"
import { Flag } from "@/flag/flag"
+async function withFs(fn: (fs: AppFileSystem.Interface) => Effect.Effect) {
+ return AppRuntime.runPromise(
+ Effect.gen(function* () {
+ const fs = yield* AppFileSystem.Service
+ return yield* fn(fs)
+ }),
+ )
+}
+
+async function find(target: string) {
+ return withFs((fs) => fs.findUp(target, Instance.directory, Instance.worktree))
+}
+
+async function readJson(file: string) {
+ return withFs((fs) => fs.readJson(file).pipe(Effect.map((json) => json as T)))
+}
+
+async function readText(file: string) {
+ return withFs((fs) => fs.readFileString(file))
+}
+
export interface Info {
name: string
environment?: Record
@@ -66,9 +89,9 @@ export const prettier: Info = {
".gql",
],
async enabled() {
- const items = await Filesystem.findUp("package.json", Instance.directory, Instance.worktree)
+ const items = await find("package.json")
for (const item of items) {
- const json = await Filesystem.readJson<{
+ const json = await readJson<{
dependencies?: Record
devDependencies?: Record
}>(item)
@@ -89,9 +112,9 @@ export const oxfmt: Info = {
extensions: [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"],
async enabled() {
if (!Flag.OPENCODE_EXPERIMENTAL_OXFMT) return false
- const items = await Filesystem.findUp("package.json", Instance.directory, Instance.worktree)
+ const items = await find("package.json")
for (const item of items) {
- const json = await Filesystem.readJson<{
+ const json = await readJson<{
dependencies?: Record
devDependencies?: Record
}>(item)
@@ -140,7 +163,7 @@ export const biome: Info = {
async enabled() {
const configs = ["biome.json", "biome.jsonc"]
for (const config of configs) {
- const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree)
+ const found = await find(config)
if (found.length > 0) {
const bin = await Npm.which("@biomejs/biome")
if (bin) return [bin, "format", "--write", "$FILE"]
@@ -164,7 +187,7 @@ export const clang: Info = {
name: "clang-format",
extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"],
async enabled() {
- const items = await Filesystem.findUp(".clang-format", Instance.directory, Instance.worktree)
+ const items = await find(".clang-format")
if (items.length > 0) {
const match = which("clang-format")
if (match) return [match, "-i", "$FILE"]
@@ -190,10 +213,10 @@ export const ruff: Info = {
if (!which("ruff")) return false
const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"]
for (const config of configs) {
- const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree)
+ const found = await find(config)
if (found.length > 0) {
if (config === "pyproject.toml") {
- const content = await Filesystem.readText(found[0])
+ const content = await readText(found[0])
if (content.includes("[tool.ruff]")) return ["ruff", "format", "$FILE"]
} else {
return ["ruff", "format", "$FILE"]
@@ -202,9 +225,9 @@ export const ruff: Info = {
}
const deps = ["requirements.txt", "pyproject.toml", "Pipfile"]
for (const dep of deps) {
- const found = await Filesystem.findUp(dep, Instance.directory, Instance.worktree)
+ const found = await find(dep)
if (found.length > 0) {
- const content = await Filesystem.readText(found[0])
+ const content = await readText(found[0])
if (content.includes("ruff")) return ["ruff", "format", "$FILE"]
}
}
@@ -288,7 +311,7 @@ export const ocamlformat: Info = {
extensions: [".ml", ".mli"],
async enabled() {
if (!which("ocamlformat")) return false
- const items = await Filesystem.findUp(".ocamlformat", Instance.directory, Instance.worktree)
+ const items = await find(".ocamlformat")
if (items.length > 0) return ["ocamlformat", "-i", "$FILE"]
return false
},
@@ -358,9 +381,9 @@ export const pint: Info = {
name: "pint",
extensions: [".php"],
async enabled() {
- const items = await Filesystem.findUp("composer.json", Instance.directory, Instance.worktree)
+ const items = await find("composer.json")
for (const item of items) {
- const json = await Filesystem.readJson<{
+ const json = await readJson<{
require?: Record
"require-dev"?: Record
}>(item)