Skip to content
Source
Sign inSign up

KnownWorkspaces.ts

150 lines6.3 KB

1// Client-supplied worktrees count only under managed folders or when Git lists them for the project.
2import * as HostProcess from "@t3tools/shared/HostProcess";
3import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";
4import * as Context from "effect/Context";
5import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";
7import * as Layer from "effect/Layer";
8import * as Path from "effect/Path";
9import * as Schema from "effect/Schema";
10import * as SqlClient from "effect/sql/SqlClient";
11​
12import * as ServerConfig from "../config.ts";
13import * as ProjectStore from "../orchestration-v2/ProjectStore.ts";
14import * as ServerSettings from "../serverSettings.ts";
15import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
16import { isFilesystemRoot, managedWorktreesDirectories } from "../worktreesDirectory.ts";
17​
18export class UnknownWorkspaceError extends Schema.TaggedError<UnknownWorkspaceError>()(
19 "UnknownWorkspaceError",
20 {
21 cwd: Schema.String,
22 cause: Schema.optional(Schema.Defect()),
23 },
24) {
25 override get message(): string {
26 return `'${this.cwd}' is not a registered project, a managed worktree, or a thread's worktree.`;
27 }
29​
30/** Resolve candidate and root consistently before checking containment. */
31export const isWithinRoot = (path: Path.Path, candidate: string, root: string) => {
32 const relative = path.relative(root, candidate);
33 return (
34 relative === "" ||
35 (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
36 );
37};
38​
39export class KnownWorkspaces extends Context.Service<
40 KnownWorkspaces,
41 {
42 /** Takes a symlink-resolved path. */
43 readonly contains: (realPath: string) => Effect.Effect<boolean>;
44 /** Fails unless `cwd` exists and resolves inside a known workspace. */
45 readonly assertContains: (cwd: string) => Effect.Effect<void, UnknownWorkspaceError>;
46 }
47>()("t3/workspace/KnownWorkspaces") {}
48​
49/** @public */
50export const make = Effect.gen(function* () {
51 const config = yield* ServerConfig.ServerConfig;
52 const fileSystem = yield* FileSystem.FileSystem;
53 const path = yield* Path.Path;
54 const settings = yield* ServerSettings.ServerSettingsService;
55 const projectStore = yield* ProjectStore.ProjectStoreV2;
56 const sql = yield* SqlClient.SqlClient;
57 const git = yield* GitVcsDriver.GitVcsDriver;
58​
59 // Missing roots keep lexical paths; other resolution failures and filesystem roots grant nothing.
60 const resolveRoot = (value: string) => {
61 const resolvedPath = path.resolve(value);
62 return fileSystem.realPath(resolvedPath).pipe(
63 Effect.catchTags({
64 PlatformError: (cause) =>
65 Effect.succeed(cause.reason._tag === "NotFound" ? resolvedPath : null),
66 }),
67 Effect.map((root) => (root === null || isFilesystemRoot(root, path) ? null : root)),
68 );
69 };
70​
71 const contains: KnownWorkspaces["Service"]["contains"] = Effect.fn("KnownWorkspaces.contains")(
72 function* (realPath) {
73 const within = (root: string | null) => root !== null && isWithinRoot(path, realPath, root);
74 const worktreeSettings = yield* settings.getSettings.pipe(
75 Effect.orElseSucceed(() => ({ worktreesDirectory: "", previousWorktreesDirectories: [] })),
76 );
77 const home = yield* HostProcess.HomeDirectory;
78 const managedDirectories = managedWorktreesDirectories(
79 worktreeSettings,
80 config.worktreesDir,
81 path,
82 home,
83 );
84 const managed = (yield* Effect.forEach(managedDirectories, resolveRoot)).filter(
85 (root) => root !== null,
86 );
87 if (managed.some(within)) return true;
88​
89 const projects = yield* projectStore.listShells().pipe(Effect.orElseSucceed(() => []));
90 for (const project of projects) {
91 if (within(yield* resolveRoot(project.workspaceRoot))) return true;
92 }
93​
94 const threads = yield* sql<{ readonly worktreePath: string; readonly projectId: string }>`
95 SELECT DISTINCT
96 json_extract(payload_json, '$.worktreePath') AS "worktreePath",
97 project_id AS "projectId"
98 FROM orchestration_v2_projection_threads
99 WHERE deleted_at IS NULL AND json_extract(payload_json, '$.worktreePath') IS NOT NULL
100 `.pipe(
101 NodeSqliteClient.readOnly,
102 Effect.orElseSucceed(() => []),
103 );
104 const projectWorktrees = new Map<string, ReadonlyArray<string>>();
105 const isProjectWorktree = Effect.fnUntraced(function* (
106 projectId: string,
107 worktreePath: string,
108 ) {
109 const project = projects.find((candidate) => candidate.id === projectId);
110 if (project === undefined) return false;
111 let listed = projectWorktrees.get(projectId);
112 if (listed === undefined) {
113 listed = yield* git.listWorktreePaths(project.workspaceRoot).pipe(
114 Effect.flatMap((paths) => Effect.forEach(paths, resolveRoot)),
115 Effect.map((paths) => paths.filter((listedPath) => listedPath !== null)),
116 Effect.orElseSucceed((): ReadonlyArray<string> => []),
117 );
118 projectWorktrees.set(projectId, listed);
119 }
120 const real = yield* resolveRoot(worktreePath);
121 return real !== null && listed.includes(real);
122 });
123 for (const { worktreePath, projectId } of threads) {
124 // Relative paths use server cwd; managed paths still need resolution to reject symlinks elsewhere.
125 if (!path.isAbsolute(worktreePath)) continue;
126 if (!within(yield* resolveRoot(worktreePath))) continue;
127 const lexical = path.resolve(worktreePath);
128 if (managedDirectories.some((directory) => isWithinRoot(path, lexical, directory))) {
129 return true;
130 }
131 if (yield* isProjectWorktree(projectId, worktreePath)) return true;
132 }
133 return false;
134 },
135 );
136​
137 const assertContains: KnownWorkspaces["Service"]["assertContains"] = Effect.fn(
138 "KnownWorkspaces.assertContains",
139 )(function* (cwd) {
140 // `..` collapses on the path as named before symlinks resolve, as the write does.
141 const realCwd = yield* fileSystem
142 .realPath(path.resolve(cwd))
143 .pipe(Effect.mapError((cause) => new UnknownWorkspaceError({ cwd, cause })));
144 if (!(yield* contains(realCwd))) return yield* new UnknownWorkspaceError({ cwd });
145 });
146​
147 return KnownWorkspaces.of({ contains, assertContains });
148});
149​
150export const layer = Layer.effect(KnownWorkspaces, make);