refactor: replace Filesystem util with AppFileSystem service (#20127)
parent
48e97b47af
commit
e6f6f7aff1
|
|
@ -393,7 +393,7 @@ export namespace Agent {
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(
|
export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(Auth.layer),
|
Layer.provide(Auth.defaultLayer),
|
||||||
Layer.provide(Config.defaultLayer),
|
Layer.provide(Config.defaultLayer),
|
||||||
Layer.provide(Skill.defaultLayer),
|
Layer.provide(Skill.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { Effect, Layer, Record, Result, Schema, ServiceMap } from "effect"
|
||||||
import { makeRuntime } from "@/effect/run-service"
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
import { zod } from "@/util/effect-zod"
|
import { zod } from "@/util/effect-zod"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { AppFileSystem } from "../filesystem"
|
||||||
|
|
||||||
export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key"
|
export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key"
|
||||||
|
|
||||||
|
|
@ -53,17 +53,13 @@ export namespace Auth {
|
||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
const fsys = yield* AppFileSystem.Service
|
||||||
const decode = Schema.decodeUnknownOption(Info)
|
const decode = Schema.decodeUnknownOption(Info)
|
||||||
|
|
||||||
const all = Effect.fn("Auth.all")(() =>
|
const all = Effect.fn("Auth.all")(function* () {
|
||||||
Effect.tryPromise({
|
const data = (yield* fsys.readJson(file).pipe(Effect.orElseSucceed(() => ({})))) as Record<string, unknown>
|
||||||
try: async () => {
|
return Record.filterMap(data, (value) => Result.fromOption(decode(value), () => undefined))
|
||||||
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 get = Effect.fn("Auth.get")(function* (providerID: string) {
|
const get = Effect.fn("Auth.get")(function* (providerID: string) {
|
||||||
return (yield* all())[providerID]
|
return (yield* all())[providerID]
|
||||||
|
|
@ -74,10 +70,7 @@ export namespace Auth {
|
||||||
const data = yield* all()
|
const data = yield* all()
|
||||||
if (norm !== key) delete data[key]
|
if (norm !== key) delete data[key]
|
||||||
delete data[norm + "/"]
|
delete data[norm + "/"]
|
||||||
yield* Effect.tryPromise({
|
yield* fsys.writeJson(file, { ...data, [norm]: info }, 0o600).pipe(Effect.mapError(fail("Failed to write auth data")))
|
||||||
try: () => Filesystem.writeJson(file, { ...data, [norm]: info }, 0o600),
|
|
||||||
catch: fail("Failed to write auth data"),
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const remove = Effect.fn("Auth.remove")(function* (key: string) {
|
const remove = Effect.fn("Auth.remove")(function* (key: string) {
|
||||||
|
|
@ -85,17 +78,16 @@ export namespace Auth {
|
||||||
const data = yield* all()
|
const data = yield* all()
|
||||||
delete data[key]
|
delete data[key]
|
||||||
delete data[norm]
|
delete data[norm]
|
||||||
yield* Effect.tryPromise({
|
yield* fsys.writeJson(file, data, 0o600).pipe(Effect.mapError(fail("Failed to write auth data")))
|
||||||
try: () => Filesystem.writeJson(file, data, 0o600),
|
|
||||||
catch: fail("Failed to write auth data"),
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return Service.of({ get, all, set, remove })
|
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) {
|
export async function get(providerID: string) {
|
||||||
return runPromise((service) => service.get(providerID))
|
return runPromise((service) => service.get(providerID))
|
||||||
|
|
|
||||||
|
|
@ -1540,7 +1540,7 @@ export namespace Config {
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(
|
export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(AppFileSystem.defaultLayer),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
Layer.provide(Auth.layer),
|
Layer.provide(Auth.defaultLayer),
|
||||||
Layer.provide(Account.defaultLayer),
|
Layer.provide(Account.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -541,7 +541,7 @@ export namespace File {
|
||||||
const exists = yield* appFs.existsSafe(full)
|
const exists = yield* appFs.existsSafe(full)
|
||||||
if (!exists) return { type: "text" as const, content: "" }
|
if (!exists) return { type: "text" as const, content: "" }
|
||||||
|
|
||||||
const mimeType = Filesystem.mimeType(full)
|
const mimeType = AppFileSystem.mimeType(full)
|
||||||
const encode = knownText ? false : shouldEncode(mimeType)
|
const encode = knownText ? false : shouldEncode(mimeType)
|
||||||
|
|
||||||
if (encode && !isImage(mimeType)) return { type: "binary" as const, content: "", mimeType }
|
if (encode && !isImage(mimeType)) return { type: "binary" as const, content: "", mimeType }
|
||||||
|
|
|
||||||
|
|
@ -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 { InstanceState } from "@/effect/instance-state"
|
||||||
import { makeRuntime } from "@/effect/run-service"
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
|
import { AppFileSystem } from "@/filesystem"
|
||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
import type { SessionID } from "@/session/schema"
|
import type { SessionID } from "@/session/schema"
|
||||||
import { Filesystem } from "../util/filesystem"
|
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
|
|
||||||
export namespace FileTime {
|
export namespace FileTime {
|
||||||
|
|
@ -12,21 +12,9 @@ export namespace FileTime {
|
||||||
export type Stamp = {
|
export type Stamp = {
|
||||||
readonly read: Date
|
readonly read: Date
|
||||||
readonly mtime: number | undefined
|
readonly mtime: number | undefined
|
||||||
readonly ctime: number | undefined
|
|
||||||
readonly size: 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 session = (reads: Map<SessionID, Map<string, Stamp>>, sessionID: SessionID) => {
|
||||||
const value = reads.get(sessionID)
|
const value = reads.get(sessionID)
|
||||||
if (value) return value
|
if (value) return value
|
||||||
|
|
@ -53,7 +41,17 @@ export namespace FileTime {
|
||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
const fsys = yield* AppFileSystem.Service
|
||||||
const disableCheck = yield* Flag.OPENCODE_DISABLE_FILETIME_CHECK
|
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>(
|
const state = yield* InstanceState.make<State>(
|
||||||
Effect.fn("FileTime.state")(() =>
|
Effect.fn("FileTime.state")(() =>
|
||||||
Effect.succeed({
|
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`)
|
if (!time) throw new Error(`You must read file ${filepath} before overwriting it. Use the Read tool first`)
|
||||||
|
|
||||||
const next = yield* stamp(filepath)
|
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
|
if (!changed) return
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -108,7 +106,9 @@ export namespace FileTime {
|
||||||
}),
|
}),
|
||||||
).pipe(Layer.orDie)
|
).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) {
|
export function read(sessionID: SessionID, file: string) {
|
||||||
return runPromise((s) => s.read(sessionID, file))
|
return runPromise((s) => s.read(sessionID, file))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(Permission.layer),
|
||||||
Layer.provide(MCP.defaultLayer),
|
Layer.provide(MCP.defaultLayer),
|
||||||
Layer.provide(LSP.defaultLayer),
|
Layer.provide(LSP.defaultLayer),
|
||||||
Layer.provide(FileTime.layer),
|
Layer.provide(FileTime.defaultLayer),
|
||||||
Layer.provide(ToolRegistry.defaultLayer),
|
Layer.provide(ToolRegistry.defaultLayer),
|
||||||
Layer.provide(Truncate.layer),
|
Layer.provide(Truncate.layer),
|
||||||
Layer.provide(AppFileSystem.defaultLayer),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { makeRuntime } from "@/effect/run-service"
|
||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
import { Global } from "@/global"
|
import { Global } from "@/global"
|
||||||
import { Permission } from "@/permission"
|
import { Permission } from "@/permission"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { AppFileSystem } from "@/filesystem"
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
import { ConfigMarkdown } from "../config/markdown"
|
import { ConfigMarkdown } from "../config/markdown"
|
||||||
import { Glob } from "../util/glob"
|
import { Glob } from "../util/glob"
|
||||||
|
|
@ -139,28 +139,20 @@ export namespace Skill {
|
||||||
config: Config.Interface,
|
config: Config.Interface,
|
||||||
discovery: Discovery.Interface,
|
discovery: Discovery.Interface,
|
||||||
bus: Bus.Interface,
|
bus: Bus.Interface,
|
||||||
|
fsys: AppFileSystem.Interface,
|
||||||
directory: string,
|
directory: string,
|
||||||
worktree: string,
|
worktree: string,
|
||||||
) {
|
) {
|
||||||
if (!Flag.OPENCODE_DISABLE_EXTERNAL_SKILLS) {
|
if (!Flag.OPENCODE_DISABLE_EXTERNAL_SKILLS) {
|
||||||
for (const dir of EXTERNAL_DIRS) {
|
for (const dir of EXTERNAL_DIRS) {
|
||||||
const root = path.join(Global.Path.home, dir)
|
const root = path.join(Global.Path.home, dir)
|
||||||
const isDir = yield* Effect.promise(() => Filesystem.isDir(root))
|
if (!(yield* fsys.isDir(root))) continue
|
||||||
if (!isDir) continue
|
|
||||||
yield* scan(state, bus, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "global" })
|
yield* scan(state, bus, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "global" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const upDirs = yield* Effect.promise(async () => {
|
const upDirs = yield* fsys
|
||||||
const dirs: string[] = []
|
.up({ targets: EXTERNAL_DIRS, start: directory, stop: worktree })
|
||||||
for await (const root of Filesystem.up({
|
.pipe(Effect.catch(() => Effect.succeed([] as string[])))
|
||||||
targets: EXTERNAL_DIRS,
|
|
||||||
start: directory,
|
|
||||||
stop: worktree,
|
|
||||||
})) {
|
|
||||||
dirs.push(root)
|
|
||||||
}
|
|
||||||
return dirs
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const root of upDirs) {
|
for (const root of upDirs) {
|
||||||
yield* scan(state, bus, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "project" })
|
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 ?? []) {
|
for (const item of cfg.skills?.paths ?? []) {
|
||||||
const expanded = item.startsWith("~/") ? path.join(os.homedir(), item.slice(2)) : item
|
const expanded = item.startsWith("~/") ? path.join(os.homedir(), item.slice(2)) : item
|
||||||
const dir = path.isAbsolute(expanded) ? expanded : path.join(directory, expanded)
|
const dir = path.isAbsolute(expanded) ? expanded : path.join(directory, expanded)
|
||||||
const isDir = yield* Effect.promise(() => Filesystem.isDir(dir))
|
if (!(yield* fsys.isDir(dir))) {
|
||||||
if (!isDir) {
|
|
||||||
log.warn("skill path not found", { path: dir })
|
log.warn("skill path not found", { path: dir })
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -198,50 +189,52 @@ export namespace Skill {
|
||||||
|
|
||||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/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(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const discovery = yield* Discovery.Service
|
const discovery = yield* Discovery.Service
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const bus = yield* Bus.Service
|
const bus = yield* Bus.Service
|
||||||
const state = yield* InstanceState.make(
|
const fsys = yield* AppFileSystem.Service
|
||||||
Effect.fn("Skill.state")(function* (ctx) {
|
const state = yield* InstanceState.make(
|
||||||
const s: State = { skills: {}, dirs: new Set() }
|
Effect.fn("Skill.state")(function* (ctx) {
|
||||||
yield* loadSkills(s, config, discovery, bus, ctx.directory, ctx.worktree)
|
const s: State = { skills: {}, dirs: new Set() }
|
||||||
return s
|
yield* loadSkills(s, config, discovery, bus, fsys, ctx.directory, ctx.worktree)
|
||||||
}),
|
return s
|
||||||
)
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const get = Effect.fn("Skill.get")(function* (name: string) {
|
const get = Effect.fn("Skill.get")(function* (name: string) {
|
||||||
const s = yield* InstanceState.get(state)
|
const s = yield* InstanceState.get(state)
|
||||||
return s.skills[name]
|
return s.skills[name]
|
||||||
})
|
})
|
||||||
|
|
||||||
const all = Effect.fn("Skill.all")(function* () {
|
const all = Effect.fn("Skill.all")(function* () {
|
||||||
const s = yield* InstanceState.get(state)
|
const s = yield* InstanceState.get(state)
|
||||||
return Object.values(s.skills)
|
return Object.values(s.skills)
|
||||||
})
|
})
|
||||||
|
|
||||||
const dirs = Effect.fn("Skill.dirs")(function* () {
|
const dirs = Effect.fn("Skill.dirs")(function* () {
|
||||||
const s = yield* InstanceState.get(state)
|
const s = yield* InstanceState.get(state)
|
||||||
return Array.from(s.dirs)
|
return Array.from(s.dirs)
|
||||||
})
|
})
|
||||||
|
|
||||||
const available = Effect.fn("Skill.available")(function* (agent?: Agent.Info) {
|
const available = Effect.fn("Skill.available")(function* (agent?: Agent.Info) {
|
||||||
const s = yield* InstanceState.get(state)
|
const s = yield* InstanceState.get(state)
|
||||||
const list = Object.values(s.skills).toSorted((a, b) => a.name.localeCompare(b.name))
|
const list = Object.values(s.skills).toSorted((a, b) => a.name.localeCompare(b.name))
|
||||||
if (!agent) return list
|
if (!agent) return list
|
||||||
return list.filter((skill) => Permission.evaluate("skill", skill.name, agent.permission).action !== "deny")
|
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<Service> = layer.pipe(
|
export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(Discovery.defaultLayer),
|
Layer.provide(Discovery.defaultLayer),
|
||||||
Layer.provide(Config.defaultLayer),
|
Layer.provide(Config.defaultLayer),
|
||||||
Layer.provide(Bus.layer),
|
Layer.provide(Bus.layer),
|
||||||
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
export function fmt(list: Info[], opts: { verbose: boolean }) {
|
export function fmt(list: Info[], opts: { verbose: boolean }) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue