1import * as Schema from "effect/Schema";
2import { NonNegativeInt, TrimmedNonEmptyString } from "./baseSchemas.ts";
3
4const FILESYSTEM_PATH_MAX_LENGTH = 512;
5
6export const FILESYSTEM_METADATA_BATCH_LIMIT = 64;
7
8export const FilesystemGetMetadataInput = Schema.Struct({
9 // Absolute host paths, including ~/ paths. Relative paths need a workspace
10 // anchor on the client; the server never resolves them against its own cwd.
11 paths: Schema.Array(
12 TrimmedNonEmptyString.check(Schema.isMaxLength(FILESYSTEM_PATH_MAX_LENGTH)),
13 ).check(Schema.isMinLength(1), Schema.isMaxLength(FILESYSTEM_METADATA_BATCH_LIMIT)),
14});
15export type FilesystemGetMetadataInput = typeof FilesystemGetMetadataInput.Type;
16
17export const FilesystemEntryMetadata = Schema.Struct({
18 kind: Schema.Literals(["file", "directory", "other"]),
19 byteLength: Schema.optionalKey(NonNegativeInt),
20 mimeType: Schema.optionalKey(TrimmedNonEmptyString),
21});
22export type FilesystemEntryMetadata = typeof FilesystemEntryMetadata.Type;
23
24// Results follow request order so paths do not have to travel back over the wire.
25// A null entry means the path is missing, unreadable, or unsupported.
26export const FilesystemGetMetadataResult = Schema.Struct({
27 entries: Schema.Array(Schema.NullOr(FilesystemEntryMetadata)),
28});
29export type FilesystemGetMetadataResult = typeof FilesystemGetMetadataResult.Type;
30
31export const FilesystemBrowseInput = Schema.Struct({
32 partialPath: TrimmedNonEmptyString.check(Schema.isMaxLength(FILESYSTEM_PATH_MAX_LENGTH)),
33 cwd: Schema.optional(TrimmedNonEmptyString.check(Schema.isMaxLength(FILESYSTEM_PATH_MAX_LENGTH))),
34});
35export type FilesystemBrowseInput = typeof FilesystemBrowseInput.Type;
36
37export const FilesystemBrowseEntry = Schema.Struct({
38 name: TrimmedNonEmptyString,
39 fullPath: TrimmedNonEmptyString,
40});
41export type FilesystemBrowseEntry = typeof FilesystemBrowseEntry.Type;
42
43export const FilesystemBrowseResult = Schema.Struct({
44 parentPath: TrimmedNonEmptyString,
45 entries: Schema.Array(FilesystemBrowseEntry),
46});
47export type FilesystemBrowseResult = typeof FilesystemBrowseResult.Type;
48
49export const FilesystemBrowseFailure = Schema.Literals([
50 "windows_path_unsupported",
51 "current_project_required",
52 "read_directory_failed",
53]);
54export type FilesystemBrowseFailure = typeof FilesystemBrowseFailure.Type;
55
56function decodedFilesystemBrowseErrorMessage(props: object): string | undefined {
57 if (!("message" in props)) return undefined;
58 return typeof props.message === "string" ? props.message : undefined;
59}
60
61export class FilesystemBrowseError extends Schema.TaggedError<FilesystemBrowseError>()(
62 "FilesystemBrowseError",
63 {
64 partialPath: Schema.optional(TrimmedNonEmptyString),
65 cwd: Schema.optional(TrimmedNonEmptyString),
66 failure: Schema.optional(FilesystemBrowseFailure),
67 parentPath: Schema.optional(TrimmedNonEmptyString),
68 platform: Schema.optional(TrimmedNonEmptyString),
69 message: TrimmedNonEmptyString,
70 cause: Schema.optional(Schema.Defect()),
71 },
72) {
73 // Structured diagnostics stay optional for rolling compatibility with legacy message-only
74 // payloads, while new call sites must provide the request context and failure classification.
75 // @effect-diagnostics-next-line overriddenSchemaConstructor:off
76 constructor(props: {
77 readonly partialPath: string;
78 readonly cwd?: string | undefined;
79 readonly failure: FilesystemBrowseFailure;
80 readonly parentPath?: string;
81 readonly platform?: string;
82 readonly cause?: unknown;
83 }) {
84 const cwd = props.cwd === undefined ? "" : ` from '${props.cwd}'`;
85 super({
86 ...props,
87 message:
88 decodedFilesystemBrowseErrorMessage(props) ??
89 `Failed to browse filesystem path '${props.partialPath}'${cwd}.`,
90 } as any);
91 }
92}