Skip to content

Commit e6f6f7a

Browse files
authored
refactor: replace Filesystem util with AppFileSystem service (#20127)
1 parent 48e97b4 commit e6f6f7a

8 files changed

Lines changed: 81 additions & 96 deletions

File tree

packages/opencode/src/agent/agent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ export namespace Agent {
393393
)
394394

395395
export const defaultLayer = layer.pipe(
396-
Layer.provide(Auth.layer),
396+
Layer.provide(Auth.defaultLayer),
397397
Layer.provide(Config.defaultLayer),
398398
Layer.provide(Skill.defaultLayer),
399399
)

packages/opencode/src/auth/index.ts

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Effect, Layer, Record, Result, Schema, ServiceMap } from "effect"
33
import { makeRuntime } from "@/effect/run-service"
44
import { zod } from "@/util/effect-zod"
55
import { Global } from "../global"
6-
import { Filesystem } from "../util/filesystem"
6+
import { AppFileSystem } from "../filesystem"
77

88
export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key"
99

@@ -53,17 +53,13 @@ export namespace Auth {
5353
export const layer = Layer.effect(
5454
Service,
5555
Effect.gen(function* () {
56+
const fsys = yield* AppFileSystem.Service
5657
const decode = Schema.decodeUnknownOption(Info)
5758

58-
const all = Effect.fn("Auth.all")(() =>
59-
Effect.tryPromise({
60-
try: async () => {
61-
const data = await Filesystem.readJson<Record<string, unknown>>(file).catch(() => ({}))
62-
return Record.filterMap(data, (value) => Result.fromOption(decode(value), () => undefined))
63-
},
64-
catch: fail("Failed to read auth data"),
65-
}),
66-
)
59+
const all = Effect.fn("Auth.all")(function* () {
60+
const data = (yield* fsys.readJson(file).pipe(Effect.orElseSucceed(() => ({})))) as Record<string, unknown>
61+
return Record.filterMap(data, (value) => Result.fromOption(decode(value), () => undefined))
62+
})
6763

6864
const get = Effect.fn("Auth.get")(function* (providerID: string) {
6965
return (yield* all())[providerID]
@@ -74,28 +70,24 @@ export namespace Auth {
7470
const data = yield* all()
7571
if (norm !== key) delete data[key]
7672
delete data[norm + "/"]
77-
yield* Effect.tryPromise({
78-
try: () => Filesystem.writeJson(file, { ...data, [norm]: info }, 0o600),
79-
catch: fail("Failed to write auth data"),
80-
})
73+
yield* fsys.writeJson(file, { ...data, [norm]: info }, 0o600).pipe(Effect.mapError(fail("Failed to write auth data")))
8174
})
8275

8376
const remove = Effect.fn("Auth.remove")(function* (key: string) {
8477
const norm = key.replace(/\/+$/, "")
8578
const data = yield* all()
8679
delete data[key]
8780
delete data[norm]
88-
yield* Effect.tryPromise({
89-
try: () => Filesystem.writeJson(file, data, 0o600),
90-
catch: fail("Failed to write auth data"),
91-
})
81+
yield* fsys.writeJson(file, data, 0o600).pipe(Effect.mapError(fail("Failed to write auth data")))
9282
})
9383

9484
return Service.of({ get, all, set, remove })
9585
}),
9686
)
9787

98-
const { runPromise } = makeRuntime(Service, layer)
88+
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
89+
90+
const { runPromise } = makeRuntime(Service, defaultLayer)
9991

10092
export async function get(providerID: string) {
10193
return runPromise((service) => service.get(providerID))

packages/opencode/src/config/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1540,7 +1540,7 @@ export namespace Config {
15401540

15411541
export const defaultLayer = layer.pipe(
15421542
Layer.provide(AppFileSystem.defaultLayer),
1543-
Layer.provide(Auth.layer),
1543+
Layer.provide(Auth.defaultLayer),
15441544
Layer.provide(Account.defaultLayer),
15451545
)
15461546

packages/opencode/src/file/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,7 @@ export namespace File {
541541
const exists = yield* appFs.existsSafe(full)
542542
if (!exists) return { type: "text" as const, content: "" }
543543

544-
const mimeType = Filesystem.mimeType(full)
544+
const mimeType = AppFileSystem.mimeType(full)
545545
const encode = knownText ? false : shouldEncode(mimeType)
546546

547547
if (encode && !isImage(mimeType)) return { type: "binary" as const, content: "", mimeType }

packages/opencode/src/file/time.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { DateTime, Effect, Layer, Semaphore, ServiceMap } from "effect"
1+
import { DateTime, Effect, Layer, Option, Semaphore, ServiceMap } from "effect"
22
import { InstanceState } from "@/effect/instance-state"
33
import { makeRuntime } from "@/effect/run-service"
4+
import { AppFileSystem } from "@/filesystem"
45
import { Flag } from "@/flag/flag"
56
import type { SessionID } from "@/session/schema"
6-
import { Filesystem } from "../util/filesystem"
77
import { Log } from "../util/log"
88

99
export namespace FileTime {
@@ -12,21 +12,9 @@ export namespace FileTime {
1212
export type Stamp = {
1313
readonly read: Date
1414
readonly mtime: number | undefined
15-
readonly ctime: number | undefined
1615
readonly size: number | undefined
1716
}
1817

19-
const stamp = Effect.fnUntraced(function* (file: string) {
20-
const stat = Filesystem.stat(file)
21-
const size = typeof stat?.size === "bigint" ? Number(stat.size) : stat?.size
22-
return {
23-
read: yield* DateTime.nowAsDate,
24-
mtime: stat?.mtime?.getTime(),
25-
ctime: stat?.ctime?.getTime(),
26-
size,
27-
}
28-
})
29-
3018
const session = (reads: Map<SessionID, Map<string, Stamp>>, sessionID: SessionID) => {
3119
const value = reads.get(sessionID)
3220
if (value) return value
@@ -53,7 +41,17 @@ export namespace FileTime {
5341
export const layer = Layer.effect(
5442
Service,
5543
Effect.gen(function* () {
44+
const fsys = yield* AppFileSystem.Service
5645
const disableCheck = yield* Flag.OPENCODE_DISABLE_FILETIME_CHECK
46+
47+
const stamp = Effect.fnUntraced(function* (file: string) {
48+
const info = yield* fsys.stat(file).pipe(Effect.catch(() => Effect.succeed(undefined)))
49+
return {
50+
read: yield* DateTime.nowAsDate,
51+
mtime: info ? Option.getOrUndefined(info.mtime)?.getTime() : undefined,
52+
size: info ? Number(info.size) : undefined,
53+
}
54+
})
5755
const state = yield* InstanceState.make<State>(
5856
Effect.fn("FileTime.state")(() =>
5957
Effect.succeed({
@@ -92,7 +90,7 @@ export namespace FileTime {
9290
if (!time) throw new Error(`You must read file ${filepath} before overwriting it. Use the Read tool first`)
9391

9492
const next = yield* stamp(filepath)
95-
const changed = next.mtime !== time.mtime || next.ctime !== time.ctime || next.size !== time.size
93+
const changed = next.mtime !== time.mtime || next.size !== time.size
9694
if (!changed) return
9795

9896
throw new Error(
@@ -108,7 +106,9 @@ export namespace FileTime {
108106
}),
109107
).pipe(Layer.orDie)
110108

111-
const { runPromise } = makeRuntime(Service, layer)
109+
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
110+
111+
const { runPromise } = makeRuntime(Service, defaultLayer)
112112

113113
export function read(sessionID: SessionID, file: string) {
114114
return runPromise((s) => s.read(sessionID, file))

packages/opencode/src/provider/auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ export namespace ProviderAuth {
230230
}),
231231
)
232232

233-
export const defaultLayer = layer.pipe(Layer.provide(Auth.layer))
233+
export const defaultLayer = layer.pipe(Layer.provide(Auth.defaultLayer))
234234

235235
const { runPromise } = makeRuntime(Service, defaultLayer)
236236

packages/opencode/src/session/prompt.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1704,7 +1704,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
17041704
Layer.provide(Permission.layer),
17051705
Layer.provide(MCP.defaultLayer),
17061706
Layer.provide(LSP.defaultLayer),
1707-
Layer.provide(FileTime.layer),
1707+
Layer.provide(FileTime.defaultLayer),
17081708
Layer.provide(ToolRegistry.defaultLayer),
17091709
Layer.provide(Truncate.layer),
17101710
Layer.provide(AppFileSystem.defaultLayer),

packages/opencode/src/skill/index.ts

Lines changed: 49 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { makeRuntime } from "@/effect/run-service"
1111
import { Flag } from "@/flag/flag"
1212
import { Global } from "@/global"
1313
import { Permission } from "@/permission"
14-
import { Filesystem } from "@/util/filesystem"
14+
import { AppFileSystem } from "@/filesystem"
1515
import { Config } from "../config/config"
1616
import { ConfigMarkdown } from "../config/markdown"
1717
import { Glob } from "../util/glob"
@@ -139,28 +139,20 @@ export namespace Skill {
139139
config: Config.Interface,
140140
discovery: Discovery.Interface,
141141
bus: Bus.Interface,
142+
fsys: AppFileSystem.Interface,
142143
directory: string,
143144
worktree: string,
144145
) {
145146
if (!Flag.OPENCODE_DISABLE_EXTERNAL_SKILLS) {
146147
for (const dir of EXTERNAL_DIRS) {
147148
const root = path.join(Global.Path.home, dir)
148-
const isDir = yield* Effect.promise(() => Filesystem.isDir(root))
149-
if (!isDir) continue
149+
if (!(yield* fsys.isDir(root))) continue
150150
yield* scan(state, bus, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "global" })
151151
}
152152

153-
const upDirs = yield* Effect.promise(async () => {
154-
const dirs: string[] = []
155-
for await (const root of Filesystem.up({
156-
targets: EXTERNAL_DIRS,
157-
start: directory,
158-
stop: worktree,
159-
})) {
160-
dirs.push(root)
161-
}
162-
return dirs
163-
})
153+
const upDirs = yield* fsys
154+
.up({ targets: EXTERNAL_DIRS, start: directory, stop: worktree })
155+
.pipe(Effect.catch(() => Effect.succeed([] as string[])))
164156

165157
for (const root of upDirs) {
166158
yield* scan(state, bus, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "project" })
@@ -176,8 +168,7 @@ export namespace Skill {
176168
for (const item of cfg.skills?.paths ?? []) {
177169
const expanded = item.startsWith("~/") ? path.join(os.homedir(), item.slice(2)) : item
178170
const dir = path.isAbsolute(expanded) ? expanded : path.join(directory, expanded)
179-
const isDir = yield* Effect.promise(() => Filesystem.isDir(dir))
180-
if (!isDir) {
171+
if (!(yield* fsys.isDir(dir))) {
181172
log.warn("skill path not found", { path: dir })
182173
continue
183174
}
@@ -198,50 +189,52 @@ export namespace Skill {
198189

199190
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Skill") {}
200191

201-
export const layer: Layer.Layer<Service, never, Discovery.Service | Config.Service | Bus.Service> = Layer.effect(
202-
Service,
203-
Effect.gen(function* () {
204-
const discovery = yield* Discovery.Service
205-
const config = yield* Config.Service
206-
const bus = yield* Bus.Service
207-
const state = yield* InstanceState.make(
208-
Effect.fn("Skill.state")(function* (ctx) {
209-
const s: State = { skills: {}, dirs: new Set() }
210-
yield* loadSkills(s, config, discovery, bus, ctx.directory, ctx.worktree)
211-
return s
212-
}),
213-
)
214-
215-
const get = Effect.fn("Skill.get")(function* (name: string) {
216-
const s = yield* InstanceState.get(state)
217-
return s.skills[name]
218-
})
219-
220-
const all = Effect.fn("Skill.all")(function* () {
221-
const s = yield* InstanceState.get(state)
222-
return Object.values(s.skills)
223-
})
224-
225-
const dirs = Effect.fn("Skill.dirs")(function* () {
226-
const s = yield* InstanceState.get(state)
227-
return Array.from(s.dirs)
228-
})
229-
230-
const available = Effect.fn("Skill.available")(function* (agent?: Agent.Info) {
231-
const s = yield* InstanceState.get(state)
232-
const list = Object.values(s.skills).toSorted((a, b) => a.name.localeCompare(b.name))
233-
if (!agent) return list
234-
return list.filter((skill) => Permission.evaluate("skill", skill.name, agent.permission).action !== "deny")
235-
})
236-
237-
return Service.of({ get, all, dirs, available })
238-
}),
239-
)
192+
export const layer = Layer.effect(
193+
Service,
194+
Effect.gen(function* () {
195+
const discovery = yield* Discovery.Service
196+
const config = yield* Config.Service
197+
const bus = yield* Bus.Service
198+
const fsys = yield* AppFileSystem.Service
199+
const state = yield* InstanceState.make(
200+
Effect.fn("Skill.state")(function* (ctx) {
201+
const s: State = { skills: {}, dirs: new Set() }
202+
yield* loadSkills(s, config, discovery, bus, fsys, ctx.directory, ctx.worktree)
203+
return s
204+
}),
205+
)
206+
207+
const get = Effect.fn("Skill.get")(function* (name: string) {
208+
const s = yield* InstanceState.get(state)
209+
return s.skills[name]
210+
})
211+
212+
const all = Effect.fn("Skill.all")(function* () {
213+
const s = yield* InstanceState.get(state)
214+
return Object.values(s.skills)
215+
})
216+
217+
const dirs = Effect.fn("Skill.dirs")(function* () {
218+
const s = yield* InstanceState.get(state)
219+
return Array.from(s.dirs)
220+
})
221+
222+
const available = Effect.fn("Skill.available")(function* (agent?: Agent.Info) {
223+
const s = yield* InstanceState.get(state)
224+
const list = Object.values(s.skills).toSorted((a, b) => a.name.localeCompare(b.name))
225+
if (!agent) return list
226+
return list.filter((skill) => Permission.evaluate("skill", skill.name, agent.permission).action !== "deny")
227+
})
228+
229+
return Service.of({ get, all, dirs, available })
230+
}),
231+
)
240232

241-
export const defaultLayer: Layer.Layer<Service> = layer.pipe(
233+
export const defaultLayer = layer.pipe(
242234
Layer.provide(Discovery.defaultLayer),
243235
Layer.provide(Config.defaultLayer),
244236
Layer.provide(Bus.layer),
237+
Layer.provide(AppFileSystem.defaultLayer),
245238
)
246239

247240
export function fmt(list: Info[], opts: { verbose: boolean }) {

0 commit comments

Comments
 (0)