diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 53c655d1b3..96b71f816b 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 2ccc1edff1..7227f1bbec 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>(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 + 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 d02a1b2707..ad804c892f 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 fec1b4bc93..08b2faf6b1 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 d33848000d..08f7e9a951 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: 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( 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 0b39a06a63..fbfab6c3b9 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 c627f0a100..48ec08c20b 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 e92e45b1ce..8145110abf 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()("@opencode/Skill") {} - export const layer: Layer.Layer = 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 - }), - ) + 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 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 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 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") - }) + 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 }) - }), - ) + return Service.of({ get, all, dirs, available }) + }), + ) - export const defaultLayer: Layer.Layer = 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 }) {