import { File } from "@/file" import { Ripgrep } from "@/file/ripgrep" import * as InstanceState from "@/effect/instance-state" import { LSP } from "@/lsp/lsp" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "./auth" const FileQuery = Schema.Struct({ path: Schema.String, }) const FindTextQuery = Schema.Struct({ pattern: Schema.String, }) const FindFileQuery = Schema.Struct({ query: Schema.String, dirs: Schema.optional(Schema.Literals(["true", "false"])), type: Schema.optional(Schema.Literals(["file", "directory"])), limit: Schema.optional( Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(200)), ), }) const FindSymbolQuery = Schema.Struct({ query: Schema.String, }) export const FilePaths = { findText: "/find", findFile: "/find/file", findSymbol: "/find/symbol", list: "/file", content: "/file/content", status: "/file/status", } as const export const FileApi = HttpApi.make("file") .add( HttpApiGroup.make("file") .add( HttpApiEndpoint.get("findText", FilePaths.findText, { query: FindTextQuery, success: Schema.Array(Ripgrep.SearchMatch), }).annotateMerge( OpenApi.annotations({ identifier: "find.text", summary: "Find text", description: "Search for text patterns across files in the project using ripgrep.", }), ), HttpApiEndpoint.get("findFile", FilePaths.findFile, { query: FindFileQuery, success: Schema.Array(Schema.String), }).annotateMerge( OpenApi.annotations({ identifier: "find.files", summary: "Find files", description: "Search for files or directories by name or pattern in the project directory.", }), ), HttpApiEndpoint.get("findSymbol", FilePaths.findSymbol, { query: FindSymbolQuery, success: Schema.Array(LSP.Symbol), }).annotateMerge( OpenApi.annotations({ identifier: "find.symbols", summary: "Find symbols", description: "Search for workspace symbols like functions, classes, and variables using LSP.", }), ), HttpApiEndpoint.get("list", FilePaths.list, { query: FileQuery, success: Schema.Array(File.Node), }).annotateMerge( OpenApi.annotations({ identifier: "file.list", summary: "List files", description: "List files and directories in a specified path.", }), ), HttpApiEndpoint.get("content", FilePaths.content, { query: FileQuery, success: File.Content, }).annotateMerge( OpenApi.annotations({ identifier: "file.read", summary: "Read file", description: "Read the content of a specified file.", }), ), HttpApiEndpoint.get("status", FilePaths.status, { success: Schema.Array(File.Info), }).annotateMerge( OpenApi.annotations({ identifier: "file.status", summary: "Get file status", description: "Get the git status of all files in the project.", }), ), ) .annotateMerge( OpenApi.annotations({ title: "file", description: "Experimental HttpApi file routes.", }), ) .middleware(Authorization), ) .annotateMerge( OpenApi.annotations({ title: "opencode experimental HttpApi", version: "0.0.1", description: "Experimental HttpApi surface for selected instance routes.", }), ) export const fileHandlers = Layer.unwrap( Effect.gen(function* () { const svc = yield* File.Service const ripgrep = yield* Ripgrep.Service const findText = Effect.fn("FileHttpApi.findText")(function* (ctx: { query: { pattern: string } }) { return (yield* ripgrep .search({ cwd: (yield* InstanceState.context).directory, pattern: ctx.query.pattern, limit: 10 }) .pipe(Effect.orDie)).items }) 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, }) }) const findSymbol = Effect.fn("FileHttpApi.findSymbol")(function* () { return [] }) const list = Effect.fn("FileHttpApi.list")(function* (ctx: { query: { path: string } }) { return yield* svc.list(ctx.query.path) }) const content = Effect.fn("FileHttpApi.content")(function* (ctx: { query: { path: string } }) { return yield* svc.read(ctx.query.path) }) const status = Effect.fn("FileHttpApi.status")(function* () { return yield* svc.status() }) return HttpApiBuilder.group(FileApi, "file", (handlers) => handlers .handle("findText", findText) .handle("findFile", findFile) .handle("findSymbol", findSymbol) .handle("list", list) .handle("content", content) .handle("status", status), ) }), ).pipe(Layer.provide(File.defaultLayer), Layer.provide(Ripgrep.defaultLayer))