summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-03-30 17:48:28 -0400
committerGitHub <[email protected]>2026-03-30 21:48:28 +0000
commite6f6f7aff14e3d3c6950e1a0a4b8288a65d2e6b1 (patch)
tree8b9446781f5c8f4a4e9688e913ace50646af21d5
parent48e97b47afaa201ebd15566cb51ab615a0423f47 (diff)
downloadopencode-e6f6f7aff14e3d3c6950e1a0a4b8288a65d2e6b1.tar.gz
opencode-e6f6f7aff14e3d3c6950e1a0a4b8288a65d2e6b1.zip
refactor: replace Filesystem util with AppFileSystem service (#20127)
-rw-r--r--packages/opencode/src/agent/agent.ts2
-rw-r--r--packages/opencode/src/auth/index.ts30
-rw-r--r--packages/opencode/src/config/config.ts2
-rw-r--r--packages/opencode/src/file/index.ts2
-rw-r--r--packages/opencode/src/file/time.ts32
-rw-r--r--packages/opencode/src/provider/auth.ts2
-rw-r--r--packages/opencode/src/session/prompt.ts2
-rw-r--r--packages/opencode/src/skill/index.ts105
8 files changed, 81 insertions, 96 deletions
diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts
index 53c655d1b..96b71f816 100644
--- a/packages/opencode/src/agent/agent.ts
+++ b/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),
)
diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts
index 2ccc1edff..7227f1bbe 100644
--- a/packages/opencode/src/auth/index.ts
+++ b/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))
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index d02a1b270..ad804c892 100644
--- a/packages/opencode/src/config/config.ts
+++ b/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),
)
diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts
index fec1b4bc9..08b2faf6b 100644
--- a/packages/opencode/src/file/index.ts
+++ b/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 }
diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts
index d33848000..08f7e9a95 100644
--- a/packages/opencode/src/file/time.ts
+++ b/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))
diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts
index 0b39a06a6..fbfab6c3b 100644
--- a/packages/opencode/src/provider/auth.ts
+++ b/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)
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index c627f0a10..48ec08c20 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/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),
diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts
index e92e45b1c..8145110ab 100644
--- a/packages/opencode/src/skill/index.ts
+++ b/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 }) {