import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Path from "effect/Path"; import { WorkspaceFileSystem, WorkspaceFileSystemError, type WorkspaceFileSystemShape,} from "../Services/WorkspaceFileSystem.ts";import * as WorkspaceEntries from "../WorkspaceEntries.ts";import { WorkspacePaths } from "../Services/WorkspacePaths.ts"; const PROJECT_READ_FILE_MAX_BYTES = 1024 * 1024; function makeReadFileError(input: { readonly cwd: string; readonly relativePath: string; readonly detail: string; readonly cause?: unknown;}) { return new WorkspaceFileSystemError({ cwd: input.cwd, relativePath: input.relativePath, operation: "workspaceFileSystem.readFile", detail: input.detail, ...(input.cause === undefined ? {} : { cause: input.cause }), });} function causeMessage(cause: { readonly message: string }) { return cause.message;} export const makeWorkspaceFileSystem = Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const workspacePaths = yield* WorkspacePaths; const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const readFile: WorkspaceFileSystemShape["readFile"] = Effect.fn("WorkspaceFileSystem.readFile")( function* (input) { const target = yield* workspacePaths.resolveRelativePathWithinRoot({ workspaceRoot: input.cwd, relativePath: input.relativePath, }); const [realWorkspaceRoot, realTargetPath] = yield* Effect.all([ fileSystem.realPath(input.cwd), fileSystem.realPath(target.absolutePath), ]).pipe( Effect.mapError((cause) => makeReadFileError({ cwd: input.cwd, relativePath: input.relativePath, detail: causeMessage(cause), cause, }), ), ); const relativeRealPath = path.relative(realWorkspaceRoot, realTargetPath); if ( relativeRealPath.startsWith(`..${path.sep}`) || relativeRealPath === ".." || path.isAbsolute(relativeRealPath) ) { return yield* makeReadFileError({ cwd: input.cwd, relativePath: input.relativePath, detail: "Workspace file path resolves outside the project root.", }); } const result = yield* Effect.scoped( Effect.gen(function* () { const handle = yield* fileSystem.open(realTargetPath, { flag: "r" }).pipe( Effect.mapError((cause) => makeReadFileError({ cwd: input.cwd, relativePath: input.relativePath, detail: causeMessage(cause), cause, }), ), ); const stat = yield* handle.stat.pipe( Effect.mapError((cause) => makeReadFileError({ cwd: input.cwd, relativePath: input.relativePath, detail: causeMessage(cause), cause, }), ), ); if (stat.type !== "File") { return yield* makeReadFileError({ cwd: input.cwd, relativePath: input.relativePath, detail: "Workspace path is not a file.", }); } const truncated = stat.size > FileSystem.Size(PROJECT_READ_FILE_MAX_BYTES); const bytesToRead = truncated ? PROJECT_READ_FILE_MAX_BYTES : Number(stat.size); const buffer = new Uint8Array(bytesToRead); const bytesRead = yield* handle.read(buffer).pipe( Effect.mapError((cause) => makeReadFileError({ cwd: input.cwd, relativePath: input.relativePath, detail: causeMessage(cause), cause, }), ), ); const fileBytes = buffer.subarray(0, Number(bytesRead)); if (fileBytes.includes(0)) { return yield* makeReadFileError({ cwd: input.cwd, relativePath: input.relativePath, detail: "Binary files cannot be previewed as text.", }); } const contents = new TextDecoder("utf-8").decode(fileBytes); return { relativePath: target.relativePath, contents, byteLength: Number(stat.size), truncated, }; }), ); return result; }, ); const writeFile: WorkspaceFileSystemShape["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( (cause) => new WorkspaceFileSystemError({ cwd: input.cwd, relativePath: input.relativePath, operation: "workspaceFileSystem.makeDirectory", detail: cause.message, cause, }), ), ); yield* fileSystem.writeFileString(target.absolutePath, input.contents).pipe( Effect.mapError( (cause) => new WorkspaceFileSystemError({ cwd: input.cwd, relativePath: input.relativePath, operation: "workspaceFileSystem.writeFile", detail: cause.message, cause, }), ), ); yield* workspaceEntries.refresh(input.cwd); return { relativePath: target.relativePath }; }); return { readFile, writeFile } satisfies WorkspaceFileSystemShape;}); export const WorkspaceFileSystemLive = Layer.effect(WorkspaceFileSystem, makeWorkspaceFileSystem);
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
import {
WorkspaceFileSystem,
WorkspaceFileSystemError,
type WorkspaceFileSystemShape,
} from "../Services/WorkspaceFileSystem.ts";
import * as WorkspaceEntries from "../WorkspaceEntries.ts";
import { WorkspacePaths } from "../Services/WorkspacePaths.ts";
const PROJECT_READ_FILE_MAX_BYTES = 1024 * 1024;
function makeReadFileError(input: {
readonly cwd: string;
readonly relativePath: string;
readonly detail: string;
readonly cause?: unknown;
}) {
return new WorkspaceFileSystemError({
cwd: input.cwd,
relativePath: input.relativePath,
operation: "workspaceFileSystem.readFile",
detail: input.detail,
...(input.cause === undefined ? {} : { cause: input.cause }),
});
}
function causeMessage(cause: { readonly message: string }) {
return cause.message;
}
export const makeWorkspaceFileSystem = Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const workspacePaths = yield* WorkspacePaths;
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const readFile: WorkspaceFileSystemShape["readFile"] = Effect.fn("WorkspaceFileSystem.readFile")(
function* (input) {
const target = yield* workspacePaths.resolveRelativePathWithinRoot({
workspaceRoot: input.cwd,
relativePath: input.relativePath,
});
const [realWorkspaceRoot, realTargetPath] = yield* Effect.all([
fileSystem.realPath(input.cwd),
fileSystem.realPath(target.absolutePath),
]).pipe(
Effect.mapError((cause) =>
makeReadFileError({
cwd: input.cwd,
relativePath: input.relativePath,
detail: causeMessage(cause),
cause,
}),
),
);
const relativeRealPath = path.relative(realWorkspaceRoot, realTargetPath);
if (
relativeRealPath.startsWith(`..${path.sep}`) ||
relativeRealPath === ".." ||
path.isAbsolute(relativeRealPath)
) {
return yield* makeReadFileError({
cwd: input.cwd,
relativePath: input.relativePath,
detail: "Workspace file path resolves outside the project root.",
});
}
const result = yield* Effect.scoped(
Effect.gen(function* () {
const handle = yield* fileSystem.open(realTargetPath, { flag: "r" }).pipe(
Effect.mapError((cause) =>
makeReadFileError({
cwd: input.cwd,
relativePath: input.relativePath,
detail: causeMessage(cause),
cause,
}),
),
);
const stat = yield* handle.stat.pipe(
Effect.mapError((cause) =>
makeReadFileError({
cwd: input.cwd,
relativePath: input.relativePath,
detail: causeMessage(cause),
cause,
}),
),
);
if (stat.type !== "File") {
return yield* makeReadFileError({
cwd: input.cwd,
relativePath: input.relativePath,
detail: "Workspace path is not a file.",
});
}
const truncated = stat.size > FileSystem.Size(PROJECT_READ_FILE_MAX_BYTES);
const bytesToRead = truncated ? PROJECT_READ_FILE_MAX_BYTES : Number(stat.size);
const buffer = new Uint8Array(bytesToRead);
const bytesRead = yield* handle.read(buffer).pipe(
Effect.mapError((cause) =>
makeReadFileError({
cwd: input.cwd,
relativePath: input.relativePath,
detail: causeMessage(cause),
cause,
}),
),
);
const fileBytes = buffer.subarray(0, Number(bytesRead));
if (fileBytes.includes(0)) {
return yield* makeReadFileError({
cwd: input.cwd,
relativePath: input.relativePath,
detail: "Binary files cannot be previewed as text.",
});
}
const contents = new TextDecoder("utf-8").decode(fileBytes);
return {
relativePath: target.relativePath,
contents,
byteLength: Number(stat.size),
truncated,
};
}),
);
return result;
},
);
const writeFile: WorkspaceFileSystemShape["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(
(cause) =>
new WorkspaceFileSystemError({
cwd: input.cwd,
relativePath: input.relativePath,
operation: "workspaceFileSystem.makeDirectory",
detail: cause.message,
cause,
}),
),
);
yield* fileSystem.writeFileString(target.absolutePath, input.contents).pipe(
Effect.mapError(
(cause) =>
new WorkspaceFileSystemError({
cwd: input.cwd,
relativePath: input.relativePath,
operation: "workspaceFileSystem.writeFile",
detail: cause.message,
cause,
}),
),
);
yield* workspaceEntries.refresh(input.cwd);
return { relativePath: target.relativePath };
});
return { readFile, writeFile } satisfies WorkspaceFileSystemShape;
});
export const WorkspaceFileSystemLive = Layer.effect(WorkspaceFileSystem, makeWorkspaceFileSystem);