refactor(core): consolidate filesystem services (#30447)
This commit is contained in:
@@ -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,
|
||||
]),
|
||||
|
||||
Reference in New Issue
Block a user