refactor(core): consolidate filesystem services (#30447)

This commit is contained in:
Dax
2026-06-02 16:09:26 -04:00
committed by GitHub
parent b93963e462
commit 604a5f781f
153 changed files with 2553 additions and 4312 deletions
+1 -7
View File
@@ -1,12 +1,10 @@
import { Plugin } from "../plugin"
import { Format } from "../format"
import { LSP } from "@/lsp/lsp"
import { File } from "../file"
import { Snapshot } from "../snapshot"
import * as Project from "./project"
import * as Vcs from "./vcs"
import { InstanceState } from "@/effect/instance-state"
import { FileWatcher } from "@/file/watcher"
import { ShareNext } from "@/share/share-next"
import { Effect, Layer } from "effect"
import { Config } from "@/config/config"
@@ -23,8 +21,6 @@ export const layer = Layer.effect(
// InstanceStore imports only the lightweight tag from bootstrap-service.ts,
// so it can depend on bootstrap without importing this implementation graph.
const config = yield* Config.Service
const file = yield* File.Service
const fileWatcher = yield* FileWatcher.Service
const format = yield* Format.Service
const lsp = yield* LSP.Service
const plugin = yield* Plugin.Service
@@ -44,7 +40,7 @@ export const layer = Layer.effect(
// Each service self-manages its own slow work via Effect.forkScoped against
// its per-instance state scope. We just await materialization here.
yield* Effect.forEach(
[reference, lsp, shareNext, format, file, fileWatcher, vcs, snapshot, project],
[reference, lsp, shareNext, format, vcs, snapshot, project],
(s) => s.init().pipe(Effect.catchCause((cause) => Effect.logWarning("init failed", { cause }))),
{ concurrency: "unbounded", discard: true },
).pipe(Effect.withSpan("InstanceBootstrap.init"))
@@ -57,8 +53,6 @@ export const layer = Layer.effect(
export const defaultLayer: Layer.Layer<Service> = layer.pipe(
Layer.provide([
Config.defaultLayer,
File.defaultLayer,
FileWatcher.defaultLayer,
Format.defaultLayer,
LSP.defaultLayer,
Plugin.defaultLayer,
@@ -1,5 +1,5 @@
import { LocalContext } from "@/util/local-context"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { FSUtil } from "@opencode-ai/core/fs-util"
import type * as Project from "./project"
export interface InstanceContext {
@@ -16,9 +16,9 @@ export const context = LocalContext.create<InstanceContext>("instance")
* Paths within the worktree but outside the working directory should not trigger external_directory permission.
*/
export function containsPath(filepath: string, ctx: InstanceContext): boolean {
if (AppFileSystem.contains(ctx.directory, filepath)) return true
if (FSUtil.contains(ctx.directory, filepath)) return true
// Non-git projects set worktree to "/" which would match ANY absolute path.
// Skip worktree check in this case to preserve external_directory permissions.
if (ctx.worktree === "/") return false
return AppFileSystem.contains(ctx.worktree, filepath)
return FSUtil.contains(ctx.worktree, filepath)
}
@@ -3,7 +3,7 @@ import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { WorkspaceContext } from "@/control-plane/workspace-context"
import { InstanceRef } from "@/effect/instance-ref"
import { disposeInstance as runDisposers } from "@/effect/instance-registry"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Context, Deferred, Duration, Effect, Exit, Layer, Scope } from "effect"
import { type InstanceContext } from "./instance-context"
import { InstanceBootstrap } from "./bootstrap-service"
@@ -104,7 +104,7 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
})
const load = (input: LoadInput): Effect.Effect<InstanceContext> => {
const directory = AppFileSystem.resolve(input.directory)
const directory = FSUtil.resolve(input.directory)
return Effect.uninterruptibleMask((restore) =>
Effect.gen(function* () {
const existing = cache.get(directory)
@@ -122,7 +122,7 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
}
const reload = (input: LoadInput): Effect.Effect<InstanceContext> => {
const directory = AppFileSystem.resolve(input.directory)
const directory = FSUtil.resolve(input.directory)
return Effect.uninterruptibleMask((restore) =>
Effect.gen(function* () {
const previous = cache.get(directory)
@@ -153,7 +153,7 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
})
const disposeDirectory = Effect.fn("InstanceStore.disposeDirectory")(function* (input: string) {
const directory = AppFileSystem.resolve(input)
const directory = FSUtil.resolve(input)
const entry = cache.get(directory)
if (!entry) return
const exit = yield* Deferred.await(entry.deferred).pipe(Effect.exit)
+5 -5
View File
@@ -6,12 +6,12 @@ import { WorkspaceTable } from "@opencode-ai/core/control-plane/workspace.sql"
import * as Log from "@opencode-ai/core/util/log"
import { Flag } from "@opencode-ai/core/flag/flag"
import { GlobalBus } from "@/bus/global"
import { which } from "../util/which"
import { which } from "@opencode-ai/core/util/which"
import { Command } from "@/command"
import { InstanceState } from "@/effect/instance-state"
import { Effect, Layer, Scope, Context, Stream, Types, Schema } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { AppProcess } from "@opencode-ai/core/process"
import { ProjectV2 } from "@opencode-ai/core/project"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
@@ -135,7 +135,7 @@ type GitResult = { code: number; text: string; stderr: string }
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const proc = yield* AppProcess.Service
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
const projectV2 = yield* ProjectV2.Service
@@ -326,7 +326,7 @@ export const layer = Layer.effect(
const buffer = yield* fs.readFile(shortest).pipe(Effect.orDie)
const base64 = Buffer.from(buffer).toString("base64")
const mime = AppFileSystem.mimeType(shortest)
const mime = FSUtil.mimeType(shortest)
const url = `data:${mime};base64,${base64}`
yield* update({ projectID: input.id, icon: { url } }).pipe(
Effect.catchTag("Project.NotFoundError", () => Effect.void),
@@ -468,7 +468,7 @@ export const defaultLayer = layer.pipe(
Layer.provide(ProjectV2.defaultLayer),
Layer.provide(AppProcess.defaultLayer),
Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
)
+3 -3
View File
@@ -1,7 +1,7 @@
import { Effect, Layer, Context, Schema, Stream, Scope } from "effect"
import { formatPatch, structuredPatch } from "diff"
import { InstanceState } from "@/effect/instance-state"
import { FileWatcher } from "@/file/watcher"
import { Watcher } from "@opencode-ai/core/filesystem/watcher"
import { Git } from "@/git"
import * as Log from "@opencode-ai/core/util/log"
import { EventV2Bridge } from "@/event-v2-bridge"
@@ -328,9 +328,9 @@ export const layer: Layer.Layer<Service, never, Git.Service | EventV2Bridge.Serv
log.info("initialized", { branch: value.current, default_branch: value.root?.name })
const unsubscribe = yield* events.listen((event) => {
if (event.type !== FileWatcher.Event.Updated.type || event.location?.directory !== ctx.directory)
if (event.type !== Watcher.Event.Updated.type || event.location?.directory !== ctx.directory)
return Effect.void
const data = event.data as EventV2.Data<typeof FileWatcher.Event.Updated>
const data = event.data as EventV2.Data<typeof Watcher.Event.Updated>
if (!data.file.endsWith("HEAD")) return Effect.void
return Effect.gen(function* () {
const next = yield* get()