feat(opencode): add filesystem read and list routes
This commit is contained in:
parent
0136f03fa9
commit
5937e606df
10 changed files with 414 additions and 15 deletions
|
|
@ -3,6 +3,7 @@ export * as LocationFileSystem from "./location-filesystem"
|
|||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { AppFileSystem } from "./filesystem"
|
||||
import { Location } from "./location"
|
||||
import { NonNegativeInt, PositiveInt, RelativePath } from "./schema"
|
||||
|
||||
|
|
@ -11,13 +12,22 @@ export const ReadInput = Schema.Struct({
|
|||
})
|
||||
export type ReadInput = typeof ReadInput.Type
|
||||
|
||||
export class Content extends Schema.Class<Content>("LocationFileSystem.Content")({
|
||||
type: Schema.Literals(["text", "binary"]),
|
||||
export class TextContent extends Schema.Class<TextContent>("LocationFileSystem.TextContent")({
|
||||
type: Schema.Literal("text"),
|
||||
content: Schema.String,
|
||||
encoding: Schema.Literal("base64").pipe(Schema.optional),
|
||||
mime: Schema.String.pipe(Schema.optional),
|
||||
mime: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class BinaryContent extends Schema.Class<BinaryContent>("LocationFileSystem.BinaryContent")({
|
||||
type: Schema.Literal("binary"),
|
||||
content: Schema.String,
|
||||
encoding: Schema.Literal("base64"),
|
||||
mime: Schema.String,
|
||||
}) {}
|
||||
|
||||
export const Content = Schema.Union([TextContent, BinaryContent]).pipe(Schema.toTaggedUnion("type"))
|
||||
export type Content = typeof Content.Type
|
||||
|
||||
export const ListInput = Schema.Struct({
|
||||
path: RelativePath.pipe(Schema.optional),
|
||||
})
|
||||
|
|
@ -70,7 +80,33 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
|||
export const locationLayer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const location = yield* Location.Service
|
||||
const root = yield* fs.realPath(location.directory).pipe(Effect.orDie)
|
||||
const resolve = Effect.fnUntraced(function* (input?: RelativePath) {
|
||||
if (input && path.isAbsolute(input)) return yield* Effect.die(new Error("Path must be relative to the location"))
|
||||
const absolute = path.resolve(location.directory, input ?? ".")
|
||||
if (!AppFileSystem.contains(location.directory, absolute))
|
||||
return yield* Effect.die(new Error("Path escapes the location"))
|
||||
const real = yield* fs.realPath(absolute).pipe(Effect.orDie)
|
||||
if (!AppFileSystem.contains(root, real)) return yield* Effect.die(new Error("Path escapes the location"))
|
||||
return { absolute, real }
|
||||
})
|
||||
const entry = Effect.fnUntraced(function* (absolute: string) {
|
||||
const real = yield* fs.realPath(absolute).pipe(Effect.catch(() => Effect.void))
|
||||
if (!real) return
|
||||
if (!AppFileSystem.contains(root, real)) return
|
||||
const info = yield* fs.stat(real).pipe(Effect.catch(() => Effect.void))
|
||||
if (!info) return
|
||||
const type = info.type === "Directory" ? "directory" : info.type === "File" ? "file" : undefined
|
||||
if (!type) return
|
||||
return new Entry({
|
||||
path: RelativePath.make(path.relative(location.directory, absolute)),
|
||||
uri: pathToFileURL(real).href,
|
||||
type,
|
||||
mime: type === "directory" ? "application/x-directory" : AppFileSystem.mimeType(real),
|
||||
})
|
||||
})
|
||||
const entries = [
|
||||
new Entry({
|
||||
path: RelativePath.make("README.md"),
|
||||
|
|
@ -87,11 +123,42 @@ export const locationLayer = Layer.effect(
|
|||
]
|
||||
|
||||
return Service.of({
|
||||
read: Effect.fn("LocationFileSystem.read")(function* () {
|
||||
return new Content({ type: "text", content: "# opencode\n", mime: "text/markdown" })
|
||||
read: Effect.fn("LocationFileSystem.read")(function* (input) {
|
||||
const file = yield* resolve(input.path)
|
||||
const info = yield* fs.stat(file.real).pipe(Effect.orDie)
|
||||
if (info.type !== "File") return yield* Effect.die(new Error("Path is not a file"))
|
||||
const bytes = yield* fs.readFile(file.real).pipe(Effect.orDie)
|
||||
const mime = AppFileSystem.mimeType(file.real)
|
||||
if (!bytes.includes(0)) {
|
||||
const content = yield* Effect.sync(() => new TextDecoder("utf-8", { fatal: true }).decode(bytes)).pipe(
|
||||
Effect.option,
|
||||
)
|
||||
if (content._tag === "Some") return new TextContent({ type: "text", content: content.value, mime })
|
||||
}
|
||||
return new BinaryContent({
|
||||
type: "binary",
|
||||
content: Buffer.from(bytes).toString("base64"),
|
||||
encoding: "base64",
|
||||
mime,
|
||||
})
|
||||
}),
|
||||
list: Effect.fn("LocationFileSystem.list")(function* () {
|
||||
return entries
|
||||
list: Effect.fn("LocationFileSystem.list")(function* (input = {}) {
|
||||
const directory = yield* resolve(input.path)
|
||||
const info = yield* fs.stat(directory.real).pipe(Effect.orDie)
|
||||
if (info.type !== "Directory") return yield* Effect.die(new Error("Path is not a directory"))
|
||||
return yield* fs.readDirectoryEntries(directory.real).pipe(
|
||||
Effect.orDie,
|
||||
Effect.flatMap((items) =>
|
||||
Effect.forEach(items, (item) => entry(path.join(directory.absolute, item.name)), {
|
||||
concurrency: "unbounded",
|
||||
}),
|
||||
),
|
||||
Effect.map((items) =>
|
||||
items
|
||||
.filter((item): item is Entry => item !== undefined)
|
||||
.sort((a, b) => (a.type === b.type ? a.path.localeCompare(b.path) : a.type === "directory" ? -1 : 1)),
|
||||
),
|
||||
)
|
||||
}),
|
||||
find: Effect.fn("LocationFileSystem.find")(function* (input) {
|
||||
return entries.filter((entry) => input.type === undefined || entry.type === input.type).slice(0, input.limit)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue