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,5 +1,6 @@
import { File } from "@/file"
import { Ripgrep } from "@/file/ripgrep"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { NonNegativeInt } from "@opencode-ai/core/schema"
import { LSP } from "@/lsp/lsp"
import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
@@ -37,6 +38,47 @@ export const FindSymbolQuery = Schema.Struct({
query: Schema.String,
})
export const LegacyEntry = Schema.Struct({
name: Schema.String,
path: Schema.String,
absolute: Schema.String,
type: Schema.Literals(["file", "directory"]),
ignored: Schema.Boolean,
}).annotate({ identifier: "FileNode" })
export const LegacyContent = Schema.Struct({
type: Schema.Literals(["text", "binary"]),
content: Schema.String,
diff: Schema.optional(Schema.String),
patch: Schema.optional(
Schema.Struct({
oldFileName: Schema.String,
newFileName: Schema.String,
oldHeader: Schema.optional(Schema.String),
newHeader: Schema.optional(Schema.String),
hunks: Schema.Array(
Schema.Struct({
oldStart: NonNegativeInt,
oldLines: NonNegativeInt,
newStart: NonNegativeInt,
newLines: NonNegativeInt,
lines: Schema.Array(Schema.String),
}),
),
index: Schema.optional(Schema.String),
}),
),
encoding: Schema.optional(Schema.Literal("base64")),
mimeType: Schema.optional(Schema.String),
}).annotate({ identifier: "FileContent" })
export const LegacyStatus = Schema.Struct({
path: Schema.String,
added: NonNegativeInt,
removed: NonNegativeInt,
status: Schema.Literals(["added", "deleted", "modified"]),
}).annotate({ identifier: "File" })
export const FilePaths = {
findText: "/find",
findFile: "/find/file",
@@ -82,7 +124,7 @@ export const FileApi = HttpApi.make("file")
),
HttpApiEndpoint.get("list", FilePaths.list, {
query: FileQuery,
success: described(Schema.Array(File.Node), "Files and directories"),
success: described(Schema.Array(LegacyEntry), "Files and directories"),
}).annotateMerge(
OpenApi.annotations({
identifier: "file.list",
@@ -92,7 +134,7 @@ export const FileApi = HttpApi.make("file")
),
HttpApiEndpoint.get("content", FilePaths.content, {
query: FileQuery,
success: described(File.Content, "File content"),
success: described(LegacyContent, "File content"),
}).annotateMerge(
OpenApi.annotations({
identifier: "file.read",
@@ -102,7 +144,7 @@ export const FileApi = HttpApi.make("file")
),
HttpApiEndpoint.get("status", FilePaths.status, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(File.Info), "File status"),
success: described(Schema.Array(LegacyStatus), "File status"),
}).annotateMerge(
OpenApi.annotations({
identifier: "file.status",
@@ -1,4 +1,4 @@
import { LocationFileSystem } from "@opencode-ai/core/location-filesystem"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { RelativePath } from "@opencode-ai/core/schema"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
@@ -21,7 +21,7 @@ export const FileSystemGroup = HttpApiGroup.make("v2.fs")
.add(
HttpApiEndpoint.get("read", "/api/fs/read", {
query: ReadQuery,
success: LocationFileSystem.Content,
success: FileSystem.Content,
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
@@ -35,7 +35,7 @@ export const FileSystemGroup = HttpApiGroup.make("v2.fs")
.add(
HttpApiEndpoint.get("list", "/api/fs/list", {
query: ListQuery,
success: Schema.Array(LocationFileSystem.Entry),
success: Schema.Array(FileSystem.Entry),
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
@@ -1,7 +1,7 @@
import { Catalog } from "@opencode-ai/core/catalog"
import { Location } from "@opencode-ai/core/location"
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { LocationFileSystem } from "@opencode-ai/core/location-filesystem"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { PermissionV2 } from "@opencode-ai/core/permission"
import { ProjectReference } from "@opencode-ai/core/project-reference"
import { AbsolutePath } from "@opencode-ai/core/schema"
@@ -42,7 +42,7 @@ export class V2LocationMiddleware extends HttpApiMiddleware.Service<
| PluginBoot.Service
| PermissionV2.Service
| ProjectReference.Service
| LocationFileSystem.Service
| FileSystem.Service
}
>()("@opencode/ExperimentalHttpApiV2Location") {}
@@ -1,14 +1,24 @@
import * as InstanceState from "@/effect/instance-state"
import { File } from "@/file"
import { Ripgrep } from "@/file/ripgrep"
import { Effect } from "effect"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
import { Effect, Layer } from "effect"
import path from "path"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../api"
export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handlers) =>
Effect.gen(function* () {
const svc = yield* File.Service
const ripgrep = yield* Ripgrep.Service
const locations = yield* LocationServiceMap
const filesystem = Effect.fnUntraced(function* <A, E, R>(effect: Effect.Effect<A, E, R>) {
return yield* effect.pipe(
Effect.provide(locations.get({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })),
)
})
const findText = Effect.fn("FileHttpApi.findText")(function* (ctx: { query: { pattern: string } }) {
return (yield* ripgrep
@@ -19,12 +29,15 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
const findFile = Effect.fn("FileHttpApi.findFile")(function* (ctx: {
query: { query: string; dirs?: "true" | "false"; type?: "file" | "directory"; limit?: number }
}) {
return yield* svc.search({
query: ctx.query.query,
limit: ctx.query.limit ?? 10,
dirs: ctx.query.dirs !== "false",
type: ctx.query.type,
})
return (yield* filesystem(
FileSystem.Service.use((fs) =>
fs.find({
query: ctx.query.query,
limit: ctx.query.limit ?? 10,
type: ctx.query.type ?? (ctx.query.dirs === "false" ? "file" : undefined),
}),
),
)).map((item) => item.path)
})
const findSymbol = Effect.fn("FileHttpApi.findSymbol")(function* () {
@@ -32,15 +45,42 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
})
const list = Effect.fn("FileHttpApi.list")(function* (ctx: { query: { path: string } }) {
return yield* svc.list(ctx.query.path)
const directory = (yield* InstanceState.context).directory
return yield* filesystem(
FileSystem.Service.use((fs) =>
fs.list({ path: RelativePath.make(ctx.query.path) }).pipe(
Effect.map((items) =>
items.map((item) => ({
name: path.basename(item.path),
path: item.path,
absolute: path.join(directory, item.path),
type: item.type,
ignored: fs.isIgnored(item.path, item.type),
})),
),
),
),
)
})
const content = Effect.fn("FileHttpApi.content")(function* (ctx: { query: { path: string } }) {
return yield* svc.read(ctx.query.path)
const directory = (yield* InstanceState.context).directory
const file = path.resolve(directory, ctx.query.path)
if (!FSUtil.contains(directory, file)) return yield* Effect.die(new Error("Path escapes the location"))
if (!(yield* FSUtil.Service.use((fs) => fs.existsSafe(file)))) return { type: "text" as const, content: "" }
return yield* filesystem(
FileSystem.Service.use((fs) => fs.read({ path: RelativePath.make(ctx.query.path) })),
).pipe(
Effect.map((item) => ({
type: item.type,
content: item.type === "text" ? item.content.trim() : item.content,
...(item.type === "binary" ? { encoding: item.encoding, mimeType: item.mime } : {}),
})),
)
})
const status = Effect.fn("FileHttpApi.status")(function* () {
return yield* svc.status()
return []
})
return handlers
@@ -51,4 +91,4 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
.handle("content", content)
.handle("status", status)
}),
)
).pipe(Layer.provide(LocationServiceMap.layer))
@@ -1,4 +1,4 @@
import { LocationFileSystem } from "@opencode-ai/core/location-filesystem"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../../api"
@@ -6,7 +6,7 @@ import { InstanceHttpApi } from "../../api"
export const fileSystemHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.fs", (handlers) =>
Effect.gen(function* () {
return handlers
.handle("read", (ctx) => LocationFileSystem.Service.use((fs) => fs.read(ctx.query)))
.handle("list", (ctx) => LocationFileSystem.Service.use((fs) => fs.list(ctx.query)))
.handle("read", (ctx) => FileSystem.Service.use((fs) => fs.read(ctx.query)))
.handle("list", (ctx) => FileSystem.Service.use((fs) => fs.list(ctx.query)))
}),
)
@@ -9,16 +9,14 @@ import {
HttpServerResponse,
} from "effect/unstable/http"
import * as Socket from "effect/unstable/socket/Socket"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Account } from "@/account/account"
import { Agent } from "@/agent/agent"
import { Auth } from "@/auth"
import { Config } from "@/config/config"
import { Command } from "@/command"
import * as Observability from "@opencode-ai/core/effect/observability"
import { File } from "@/file"
import { FileWatcher } from "@/file/watcher"
import { Ripgrep } from "@/file/ripgrep"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { Format } from "@/format"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { LSP } from "@/lsp/lsp"
@@ -166,7 +164,7 @@ const docRoute = HttpRouter.use((router) => router.add("GET", "/doc", () => Effe
const uiRoute = HttpRouter.use((router) =>
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const client = yield* HttpClient.HttpClient
const flags = yield* RuntimeFlags.Service
yield* router.add("*", "/*", (request) =>
@@ -198,8 +196,6 @@ export function createRoutes(
Auth.defaultLayer,
Command.defaultLayer,
Config.defaultLayer,
File.defaultLayer,
FileWatcher.defaultLayer,
Format.defaultLayer,
LSP.defaultLayer,
Installation.defaultLayer,
@@ -232,7 +228,7 @@ export function createRoutes(
Vcs.defaultLayer,
Workspace.defaultLayer,
Worktree.appLayer,
AppFileSystem.defaultLayer,
FSUtil.defaultLayer,
FetchHttpClient.layer,
HttpServer.layerServices,
]),