Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171export * as FileSystem from "./filesystem.js"
import { makeLocationNode } from "@opencode/util/effect/app-node"import path from "path"import { Context, Effect, Layer, Schema } from "effect"import { FSUtil } from "@opencode/util/fs-util"import { Location } from "./location.js"import { AbsolutePath, PositiveInt, RelativePath } from "./schema.js"import { FileSystemSearch } from "./filesystem/search.js"import { Entry, FileSystem, FindInput, Write } from "@opencode/schema/filesystem"export { Entry, Match, Submatch } from "@opencode/schema/filesystem"
export const ReadInput = Schema.Struct({ path: RelativePath,})export type ReadInput = typeof ReadInput.Type
export const WriteInput = Schema.Struct({ /** Absolute, or relative to the location directory. */ path: Schema.String, data: Schema.Uint8Array,})export type WriteInput = typeof WriteInput.Type
export class NotFoundError extends Schema.TaggedError<NotFoundError>()("FileSystem.NotFoundError", { path: RelativePath,}) {}
export const Content = Schema.Struct({ uri: Schema.String, name: Schema.String.pipe(Schema.optional), content: Schema.String, encoding: Schema.Literals(["utf8", "base64"]), mime: Schema.String,}).annotate({ identifier: "FileSystem.Content" })export type Content = typeof Content.Type
export const ListInput = Schema.Struct({ path: Schema.String.pipe(Schema.optional),})export type ListInput = typeof ListInput.Type
export { FindInput, Write }
export const DEFAULT_SEARCH_LIMIT = 100export const DEFAULT_SEARCH_TIMEOUT_MS = 30_000
export class GlobInput extends Schema.Class<GlobInput>("FileSystem.GlobInput")({ pattern: Schema.String, path: Schema.optionalKey(RelativePath), hidden: Schema.optionalKey(Schema.Boolean), limit: Schema.optionalKey(PositiveInt),}) {}
export class GrepInput extends Schema.Class<GrepInput>("FileSystem.GrepInput")({ pattern: Schema.String, path: Schema.optionalKey(RelativePath), include: Schema.optionalKey(Schema.String), literal: Schema.optionalKey(Schema.Boolean), caseSensitive: Schema.optionalKey(Schema.Boolean), limit: Schema.optionalKey(PositiveInt),}) {}
export const Event = FileSystem.Event
export interface Interface { readonly read: ( input: ReadInput, ) => Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }, NotFoundError> readonly list: (input?: ListInput) => Effect.Effect<Entry[]> readonly find: (input: FindInput) => Effect.Effect<Entry[]> /** Writes a file at an absolute path or one relative to the location; not confined to it. */ readonly write: (input: WriteInput) => Effect.Effect<Write>}
export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem") {}
const baseLayer = Layer.effect( Service, Effect.gen(function* () { const fs = yield* FSUtil.Service const location = yield* Location.Service const search = yield* FileSystemSearch.Service // Workspace-placed directories exist only inside the workspace, so a host // realpath probe at boot consults the wrong filesystem and would block // construction on servers without a matching local directory. Treat the // configured directory as canonical; local placements keep symlink // canonicalization. This skip is boot-only: resolve/read/list below still // access the host filesystem per operation (tracked in #44568). const root = location.workspaceID ? location.directory : yield* fs.realPath(location.directory).pipe(Effect.orDie) const resolve = Effect.fnUntraced(function* (input?: RelativePath) { const absolute = path.resolve(location.directory, input ?? ".") if (!FSUtil.contains(location.directory, absolute)) return yield* Effect.die(new Error("Path escapes the location")) const real = yield* fs.realPath(absolute) if (!FSUtil.contains(root, real)) return yield* Effect.die(new Error("Path escapes the location")) return { absolute, real, directory: location.directory } }) return Service.of({ find: search.find, read: Effect.fn("FileSystem.read")(function* (input) { const target = yield* resolve(input.path).pipe( Effect.catchReason( "PlatformError", "NotFound", () => Effect.fail(new NotFoundError({ path: input.path })), (_, error) => Effect.die(error), ), ) const info = yield* fs.stat(target.real).pipe( Effect.catchReason( "PlatformError", "NotFound", () => Effect.fail(new NotFoundError({ path: input.path })), (_, error) => Effect.die(error), ), ) if (info.type !== "File") return yield* Effect.die(new Error("Path is not a file")) return { content: yield* fs.readFile(target.real).pipe( Effect.catchReason( "PlatformError", "NotFound", () => Effect.fail(new NotFoundError({ path: input.path })), (_, error) => Effect.die(error), ), ), mime: FSUtil.mimeType(target.real), } }), list: Effect.fn("FileSystem.list")(function* (input = {}) { // Navigation can leave the cwd without activating another Location. const directory = path.resolve(location.directory, input.path ?? ".") const info = yield* fs.stat(directory).pipe(Effect.orDie) if (info.type !== "Directory") return yield* Effect.die(new Error("Path is not a directory")) return yield* fs.readDirectoryEntries(directory).pipe( Effect.orDie, Effect.map((items) => items .flatMap((item) => { if (item.type !== "file" && item.type !== "directory") return [] const absolute = path.join(directory, item.name) const relative = path.relative(location.directory, absolute) || "." return [ Entry.make({ path: RelativePath.make(relative + (item.type === "directory" ? path.sep : "")), type: item.type, }), ] }) .sort((a, b) => (a.type === b.type ? a.path.localeCompare(b.path) : a.type === "directory" ? -1 : 1)), ), ) }), // Unlike read, write reaches outside the location so clients can stage files in the // server tmp directory, which the model is already told to prefer and permitted to access. write: Effect.fn("FileSystem.write")(function* (input) { const target = path.resolve(location.directory, input.path) yield* fs.writeWithDirs(target, input.data).pipe(Effect.orDie) return Write.make({ path: AbsolutePath.make(target) }) }), }) }),)
export const node = makeLocationNode({ service: Service, layer: baseLayer, deps: [FSUtil.node, Location.node, FileSystemSearch.node],})