/** * WorkspacePaths - Effect service contract for workspace path handling. * * Owns normalization and validation of workspace roots plus safe resolution of * workspace-root-relative paths. * * @module WorkspacePaths */ 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 Path from "effect/Path";import * as Schema from "effect/Schema"; import { expandHomePathWith } from "../pathExpansion.ts"; export class WorkspaceRootNotExistsError extends Schema.TaggedError<WorkspaceRootNotExistsError>()( "WorkspaceRootNotExistsError", { workspaceRoot: Schema.String, normalizedWorkspaceRoot: Schema.String, },) { override get message(): string { return `Workspace root does not exist: ${this.normalizedWorkspaceRoot}`; }} export class WorkspaceRootCreateFailedError extends Schema.TaggedError<WorkspaceRootCreateFailedError>()( "WorkspaceRootCreateFailedError", { workspaceRoot: Schema.String, normalizedWorkspaceRoot: Schema.String, cause: Schema.Defect(), },) { override get message(): string { return `Failed to create workspace root: ${this.normalizedWorkspaceRoot}`; }} export class WorkspaceRootStatFailedError extends Schema.TaggedError<WorkspaceRootStatFailedError>()( "WorkspaceRootStatFailedError", { workspaceRoot: Schema.String, normalizedWorkspaceRoot: Schema.String, phase: Schema.Literals(["validate-existing", "verify-created"]), cause: Schema.Defect(), },) { override get message(): string { return `Failed to stat workspace root '${this.normalizedWorkspaceRoot}' during '${this.phase}'.`; }} export class WorkspaceRootNotDirectoryError extends Schema.TaggedError<WorkspaceRootNotDirectoryError>()( "WorkspaceRootNotDirectoryError", { workspaceRoot: Schema.String, normalizedWorkspaceRoot: Schema.String, },) { override get message(): string { return `Workspace root is not a directory: ${this.normalizedWorkspaceRoot}`; }} export class WorkspacePathOutsideRootError extends Schema.TaggedError<WorkspacePathOutsideRootError>()( "WorkspacePathOutsideRootError", { workspaceRoot: Schema.String, relativePath: Schema.String, },) { override get message(): string { return `Workspace file path must be relative to the project root: ${this.relativePath}`; }} export const WorkspacePathsError = Schema.Union([ WorkspaceRootNotExistsError, WorkspaceRootCreateFailedError, WorkspaceRootStatFailedError, WorkspaceRootNotDirectoryError, WorkspacePathOutsideRootError,]);export type WorkspacePathsError = typeof WorkspacePathsError.Type; /** Service tag for workspace path normalization and resolution. */export class WorkspacePaths extends Context.Service< WorkspacePaths, { /** Normalize a user-provided workspace root and verify it exists as a directory. */ readonly normalizeWorkspaceRoot: ( workspaceRoot: string, options?: { readonly createIfMissing?: boolean }, ) => Effect.Effect< string, | WorkspaceRootNotExistsError | WorkspaceRootCreateFailedError | WorkspaceRootStatFailedError | WorkspaceRootNotDirectoryError >; /** * Resolve a relative path within a validated workspace root. * * Rejects absolute paths and traversal attempts outside the workspace root. */ readonly resolveRelativePathWithinRoot: (input: { workspaceRoot: string; relativePath: string; }) => Effect.Effect< { absolutePath: string; relativePath: string }, WorkspacePathOutsideRootError >; }>()("t3/workspace/WorkspacePaths") {} function toPosixRelativePath(input: string): string { return input.replaceAll("\\", "/");} export const make = Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const statWorkspaceRoot = Effect.fn("WorkspacePaths.statWorkspaceRoot")(function* ( workspaceRoot: string, normalizedWorkspaceRoot: string, phase: WorkspaceRootStatFailedError["phase"], ) { return yield* fileSystem.stat(normalizedWorkspaceRoot).pipe( Effect.matchEffect({ onFailure: (cause) => cause.reason._tag === "NotFound" ? Effect.succeed(null) : Effect.fail( new WorkspaceRootStatFailedError({ workspaceRoot, normalizedWorkspaceRoot, phase, cause, }), ), onSuccess: Effect.succeed, }), ); }); const normalizeWorkspaceRoot: WorkspacePaths["Service"]["normalizeWorkspaceRoot"] = Effect.fn( "WorkspacePaths.normalizeWorkspaceRoot", )(function* (workspaceRoot, options) { const normalizedWorkspaceRoot = path.resolve(expandHomePathWith(workspaceRoot.trim(), path)); let workspaceStat = yield* statWorkspaceRoot( workspaceRoot, normalizedWorkspaceRoot, "validate-existing", ); if (!workspaceStat && options?.createIfMissing) { yield* fileSystem.makeDirectory(normalizedWorkspaceRoot, { recursive: true }).pipe( Effect.mapError( (cause) => new WorkspaceRootCreateFailedError({ workspaceRoot, normalizedWorkspaceRoot, cause, }), ), ); workspaceStat = yield* statWorkspaceRoot( workspaceRoot, normalizedWorkspaceRoot, "verify-created", ); } if (!workspaceStat) { return yield* new WorkspaceRootNotExistsError({ workspaceRoot, normalizedWorkspaceRoot, }); } if (workspaceStat.type !== "Directory") { return yield* new WorkspaceRootNotDirectoryError({ workspaceRoot, normalizedWorkspaceRoot, }); } return normalizedWorkspaceRoot; }); const resolveRelativePathWithinRoot: WorkspacePaths["Service"]["resolveRelativePathWithinRoot"] = Effect.fn("WorkspacePaths.resolveRelativePathWithinRoot")(function* (input) { const normalizedInputPath = input.relativePath.trim(); if (path.isAbsolute(normalizedInputPath)) { return yield* new WorkspacePathOutsideRootError({ workspaceRoot: input.workspaceRoot, relativePath: input.relativePath, }); } const absolutePath = path.resolve(input.workspaceRoot, normalizedInputPath); const relativeToRoot = toPosixRelativePath(path.relative(input.workspaceRoot, absolutePath)); if ( relativeToRoot.length === 0 || relativeToRoot === "." || relativeToRoot.startsWith("../") || relativeToRoot === ".." || path.isAbsolute(relativeToRoot) ) { return yield* new WorkspacePathOutsideRootError({ workspaceRoot: input.workspaceRoot, relativePath: input.relativePath, }); } return { absolutePath, relativePath: relativeToRoot, }; }); return WorkspacePaths.of({ normalizeWorkspaceRoot, resolveRelativePathWithinRoot });}); export const layer = Layer.effect(WorkspacePaths, make);
/**
* WorkspacePaths - Effect service contract for workspace path handling.
*
* Owns normalization and validation of workspace roots plus safe resolution of
* workspace-root-relative paths.
*
* @module WorkspacePaths
*/
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 Path from "effect/Path";
import * as Schema from "effect/Schema";
import { expandHomePathWith } from "../pathExpansion.ts";
export class WorkspaceRootNotExistsError extends Schema.TaggedError<WorkspaceRootNotExistsError>()(
"WorkspaceRootNotExistsError",
{
workspaceRoot: Schema.String,
normalizedWorkspaceRoot: Schema.String,
},
) {
override get message(): string {
return `Workspace root does not exist: ${this.normalizedWorkspaceRoot}`;
}
}
export class WorkspaceRootCreateFailedError extends Schema.TaggedError<WorkspaceRootCreateFailedError>()(
"WorkspaceRootCreateFailedError",
{
workspaceRoot: Schema.String,
normalizedWorkspaceRoot: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to create workspace root: ${this.normalizedWorkspaceRoot}`;
}
}
export class WorkspaceRootStatFailedError extends Schema.TaggedError<WorkspaceRootStatFailedError>()(
"WorkspaceRootStatFailedError",
{
workspaceRoot: Schema.String,
normalizedWorkspaceRoot: Schema.String,
phase: Schema.Literals(["validate-existing", "verify-created"]),
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to stat workspace root '${this.normalizedWorkspaceRoot}' during '${this.phase}'.`;
}
}
export class WorkspaceRootNotDirectoryError extends Schema.TaggedError<WorkspaceRootNotDirectoryError>()(
"WorkspaceRootNotDirectoryError",
{
workspaceRoot: Schema.String,
normalizedWorkspaceRoot: Schema.String,
},
) {
override get message(): string {
return `Workspace root is not a directory: ${this.normalizedWorkspaceRoot}`;
}
}
export class WorkspacePathOutsideRootError extends Schema.TaggedError<WorkspacePathOutsideRootError>()(
"WorkspacePathOutsideRootError",
{
workspaceRoot: Schema.String,
relativePath: Schema.String,
},
) {
override get message(): string {
return `Workspace file path must be relative to the project root: ${this.relativePath}`;
}
}
export const WorkspacePathsError = Schema.Union([
WorkspaceRootNotExistsError,
WorkspaceRootCreateFailedError,
WorkspaceRootStatFailedError,
WorkspaceRootNotDirectoryError,
WorkspacePathOutsideRootError,
]);
export type WorkspacePathsError = typeof WorkspacePathsError.Type;
/** Service tag for workspace path normalization and resolution. */
export class WorkspacePaths extends Context.Service<
WorkspacePaths,
{
/** Normalize a user-provided workspace root and verify it exists as a directory. */
readonly normalizeWorkspaceRoot: (
workspaceRoot: string,
options?: { readonly createIfMissing?: boolean },
) => Effect.Effect<
string,
| WorkspaceRootNotExistsError
| WorkspaceRootCreateFailedError
| WorkspaceRootStatFailedError
| WorkspaceRootNotDirectoryError
>;
/**
* Resolve a relative path within a validated workspace root.
*
* Rejects absolute paths and traversal attempts outside the workspace root.
*/
readonly resolveRelativePathWithinRoot: (input: {
workspaceRoot: string;
relativePath: string;
}) => Effect.Effect<
{ absolutePath: string; relativePath: string },
WorkspacePathOutsideRootError
>;
}
>()("t3/workspace/WorkspacePaths") {}
function toPosixRelativePath(input: string): string {
return input.replaceAll("\\", "/");
}
export const make = Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const statWorkspaceRoot = Effect.fn("WorkspacePaths.statWorkspaceRoot")(function* (
workspaceRoot: string,
normalizedWorkspaceRoot: string,
phase: WorkspaceRootStatFailedError["phase"],
) {
return yield* fileSystem.stat(normalizedWorkspaceRoot).pipe(
Effect.matchEffect({
onFailure: (cause) =>
cause.reason._tag === "NotFound"
? Effect.succeed(null)
: Effect.fail(
new WorkspaceRootStatFailedError({
workspaceRoot,
normalizedWorkspaceRoot,
phase,
cause,
}),
),
onSuccess: Effect.succeed,
}),
);
});
const normalizeWorkspaceRoot: WorkspacePaths["Service"]["normalizeWorkspaceRoot"] = Effect.fn(
"WorkspacePaths.normalizeWorkspaceRoot",
)(function* (workspaceRoot, options) {
const normalizedWorkspaceRoot = path.resolve(expandHomePathWith(workspaceRoot.trim(), path));
let workspaceStat = yield* statWorkspaceRoot(
workspaceRoot,
normalizedWorkspaceRoot,
"validate-existing",
);
if (!workspaceStat && options?.createIfMissing) {
yield* fileSystem.makeDirectory(normalizedWorkspaceRoot, { recursive: true }).pipe(
Effect.mapError(
(cause) =>
new WorkspaceRootCreateFailedError({
workspaceRoot,
normalizedWorkspaceRoot,
cause,
}),
),
);
workspaceStat = yield* statWorkspaceRoot(
workspaceRoot,
normalizedWorkspaceRoot,
"verify-created",
);
}
if (!workspaceStat) {
return yield* new WorkspaceRootNotExistsError({
workspaceRoot,
normalizedWorkspaceRoot,
});
}
if (workspaceStat.type !== "Directory") {
return yield* new WorkspaceRootNotDirectoryError({
workspaceRoot,
normalizedWorkspaceRoot,
});
}
return normalizedWorkspaceRoot;
});
const resolveRelativePathWithinRoot: WorkspacePaths["Service"]["resolveRelativePathWithinRoot"] =
Effect.fn("WorkspacePaths.resolveRelativePathWithinRoot")(function* (input) {
const normalizedInputPath = input.relativePath.trim();
if (path.isAbsolute(normalizedInputPath)) {
return yield* new WorkspacePathOutsideRootError({
workspaceRoot: input.workspaceRoot,
relativePath: input.relativePath,
});
}
const absolutePath = path.resolve(input.workspaceRoot, normalizedInputPath);
const relativeToRoot = toPosixRelativePath(path.relative(input.workspaceRoot, absolutePath));
if (
relativeToRoot.length === 0 ||
relativeToRoot === "." ||
relativeToRoot.startsWith("../") ||
relativeToRoot === ".." ||
path.isAbsolute(relativeToRoot)
) {
return yield* new WorkspacePathOutsideRootError({
workspaceRoot: input.workspaceRoot,
relativePath: input.relativePath,
});
}
return {
absolutePath,
relativePath: relativeToRoot,
};
});
return WorkspacePaths.of({ normalizeWorkspaceRoot, resolveRelativePathWithinRoot });
});
export const layer = Layer.effect(WorkspacePaths, make);