Cursor AgentFormat Effect refactorsfaf9d874 months agoHistory
266 lines · 8.5 KB · TypeScript
RawDownload
/** * WorkspaceFileSystem - Effect service contract for workspace file mutations. * * Owns workspace-root-relative file read/write operations and their associated * safety checks and cache invalidation hooks. * * @module WorkspaceFileSystem */import type {  ProjectReadFileInput,  ProjectReadFileResult,  ProjectWriteFileInput,  ProjectWriteFileResult,} from "@t3tools/contracts";import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as Schema from "effect/Schema"; import * as WorkspaceEntries from "./WorkspaceEntries.ts";import * as WorkspacePaths from "./WorkspacePaths.ts"; const PROJECT_READ_FILE_MAX_BYTES = 1024 * 1024;const PROJECT_READ_FILE_MAX_SIZE = FileSystem.Size(PROJECT_READ_FILE_MAX_BYTES); export class WorkspaceFileSystemOperationError extends Schema.TaggedErrorClass<WorkspaceFileSystemOperationError>()(  "WorkspaceFileSystemOperationError",  {    workspaceRoot: Schema.String,    relativePath: Schema.String,    resolvedPath: Schema.String,    operationPath: Schema.String,    operation: Schema.Literals([      "realpath-workspace-root",      "realpath-target",      "open",      "stat",      "read",      "close",      "make-directory",      "write-file",    ]),    cause: Schema.Defect(),  },) {  override get message(): string {    return `Workspace file operation '${this.operation}' failed at '${this.operationPath}' for resolved path '${this.resolvedPath}' (requested as '${this.relativePath}' in '${this.workspaceRoot}').`;  }} export class WorkspaceFilePathEscapeError extends Schema.TaggedErrorClass<WorkspaceFilePathEscapeError>()(  "WorkspaceFilePathEscapeError",  {    workspaceRoot: Schema.String,    relativePath: Schema.String,    resolvedWorkspaceRoot: Schema.String,    resolvedPath: Schema.String,  },) {  override get message(): string {    return `Workspace file '${this.relativePath}' resolves outside workspace root '${this.workspaceRoot}': ${this.resolvedPath}`;  }} export class WorkspacePathNotFileError extends Schema.TaggedErrorClass<WorkspacePathNotFileError>()(  "WorkspacePathNotFileError",  {    workspaceRoot: Schema.String,    relativePath: Schema.String,    resolvedPath: Schema.String,  },) {  override get message(): string {    return `Workspace path '${this.relativePath}' in '${this.workspaceRoot}' is not a file: ${this.resolvedPath}`;  }} export class WorkspaceBinaryFileError extends Schema.TaggedErrorClass<WorkspaceBinaryFileError>()(  "WorkspaceBinaryFileError",  {    workspaceRoot: Schema.String,    relativePath: Schema.String,    resolvedPath: Schema.String,  },) {  override get message(): string {    return `Workspace file '${this.relativePath}' in '${this.workspaceRoot}' is binary and cannot be previewed as text.`;  }} export const WorkspaceFileSystemError = Schema.Union([  WorkspaceFileSystemOperationError,  WorkspaceFilePathEscapeError,  WorkspacePathNotFileError,  WorkspaceBinaryFileError,]);export type WorkspaceFileSystemError = typeof WorkspaceFileSystemError.Type; /** Service tag for workspace file operations. */export class WorkspaceFileSystem extends Context.Service<  WorkspaceFileSystem,  {    /** Read a UTF-8 text file relative to the workspace root. */    readonly readFile: (      input: ProjectReadFileInput,    ) => Effect.Effect<      ProjectReadFileResult,      WorkspaceFileSystemError | WorkspacePaths.WorkspacePathOutsideRootError    >;    /**     * Write a file relative to the workspace root.     *     * Creates parent directories as needed and rejects paths that escape the     * workspace root.     */    readonly writeFile: (      input: ProjectWriteFileInput,    ) => Effect.Effect<      ProjectWriteFileResult,      WorkspaceFileSystemError | WorkspacePaths.WorkspacePathOutsideRootError    >;  }>()("t3/workspace/WorkspaceFileSystem") {} export const make = Effect.gen(function* () {  const fileSystem = yield* FileSystem.FileSystem;  const path = yield* Path.Path;  const workspacePaths = yield* WorkspacePaths.WorkspacePaths;  const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;   const operationError =    (      input: ProjectReadFileInput | ProjectWriteFileInput,      resolvedPath: string,      operationPath: string,      operation: WorkspaceFileSystemOperationError["operation"],    ) =>    (cause: unknown) =>      new WorkspaceFileSystemOperationError({        workspaceRoot: input.cwd,        relativePath: input.relativePath,        resolvedPath,        operationPath,        operation,        cause,      });   const readFile: WorkspaceFileSystem["Service"]["readFile"] = Effect.fn(    "WorkspaceFileSystem.readFile",  )(function* (input) {    const target = yield* workspacePaths.resolveRelativePathWithinRoot({      workspaceRoot: input.cwd,      relativePath: input.relativePath,    });     const realWorkspaceRoot = yield* fileSystem      .realPath(input.cwd)      .pipe(        Effect.mapError(          operationError(input, target.absolutePath, input.cwd, "realpath-workspace-root"),        ),      );    const realTargetPath = yield* fileSystem      .realPath(target.absolutePath)      .pipe(        Effect.mapError(          operationError(input, target.absolutePath, target.absolutePath, "realpath-target"),        ),      );    const relativeRealPath = path.relative(realWorkspaceRoot, realTargetPath);    if (      relativeRealPath.startsWith(`..${path.sep}`) ||      relativeRealPath === ".." ||      path.isAbsolute(relativeRealPath)    ) {      return yield* new WorkspaceFilePathEscapeError({        workspaceRoot: input.cwd,        relativePath: input.relativePath,        resolvedWorkspaceRoot: realWorkspaceRoot,        resolvedPath: realTargetPath,      });    }     return yield* Effect.scoped(      Effect.gen(function* () {        const file = yield* fileSystem          .open(realTargetPath, { flag: "r" })          .pipe(Effect.mapError(operationError(input, realTargetPath, realTargetPath, "open")));        const stat = yield* file.stat.pipe(          Effect.mapError(operationError(input, realTargetPath, realTargetPath, "stat")),        );        if (stat.type !== "File") {          return yield* new WorkspacePathNotFileError({            workspaceRoot: input.cwd,            relativePath: input.relativePath,            resolvedPath: realTargetPath,          });        }         const bytesToRead =          stat.size > PROJECT_READ_FILE_MAX_SIZE ? PROJECT_READ_FILE_MAX_BYTES : Number(stat.size);        const fileBytes =          bytesToRead === 0            ? new Uint8Array()            : yield* file                .readAlloc(bytesToRead)                .pipe(                  Effect.map(Option.getOrElse(() => new Uint8Array())),                  Effect.mapError(operationError(input, realTargetPath, realTargetPath, "read")),                );        if (fileBytes.includes(0)) {          return yield* new WorkspaceBinaryFileError({            workspaceRoot: input.cwd,            relativePath: input.relativePath,            resolvedPath: realTargetPath,          });        }         return {          relativePath: target.relativePath,          contents: new TextDecoder("utf-8").decode(fileBytes),          byteLength: Number(stat.size),          truncated: stat.size > PROJECT_READ_FILE_MAX_SIZE,        };      }),    );  });   const writeFile: WorkspaceFileSystem["Service"]["writeFile"] = Effect.fn(    "WorkspaceFileSystem.writeFile",  )(function* (input) {    const target = yield* workspacePaths.resolveRelativePathWithinRoot({      workspaceRoot: input.cwd,      relativePath: input.relativePath,    });     yield* fileSystem      .makeDirectory(path.dirname(target.absolutePath), { recursive: true })      .pipe(        Effect.mapError(          operationError(            input,            target.absolutePath,            path.dirname(target.absolutePath),            "make-directory",          ),        ),      );    yield* fileSystem      .writeFileString(target.absolutePath, input.contents)      .pipe(        Effect.mapError(          operationError(input, target.absolutePath, target.absolutePath, "write-file"),        ),      );    yield* workspaceEntries.refresh(input.cwd);    return { relativePath: target.relativePath };  });   return WorkspaceFileSystem.of({ readFile, writeFile });}); export const layer = Layer.effect(WorkspaceFileSystem, make);