authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:28:33-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:28:33-07:00
log833c8ab7c66f4e976c9ae0641d5b1d2780348734
tree9a159bf4295404cc1d351030fdc0ebeeb9b2c436
parent5b1b35c773e8ad4a85ea2f3dd277b698bf329f21
signature Signed by PGP key B5690EEEBB952194

[codex] Remove project setup error constructor wrappers (#3329)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 101 insertions(+), 41 deletions(-)

apps/server/src/project/ProjectSetupScriptRunner.test.ts+51
......@@ -3,11 +3,16 @@ import { type OrchestrationProject, ProjectId } from "@t3tools/contracts";
33import * as Effect from "effect/Effect";
44import * as Layer from "effect/Layer";
55import * as Option from "effect/Option";
6import * as Schema from "effect/Schema";
67
78import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts";
89import * as TerminalManager from "../terminal/Manager.ts";
910import * as ProjectSetupScriptRunner from "./ProjectSetupScriptRunner.ts";
1011
12const isProjectSetupScriptOperationError = Schema.is(
13 ProjectSetupScriptRunner.ProjectSetupScriptOperationError,
14);
15
1116const makeProject = (scripts: OrchestrationProject["scripts"]): OrchestrationProject => ({
1217 id: ProjectId.make("project-1"),
1318 title: "Project",
......@@ -145,4 +150,50 @@ describe("ProjectSetupScriptRunner", () => {
145150 }).pipe(Effect.provide(testLayer(project, { open, write })));
146151 },
147152 );
153
154 it.effect("keeps terminal failures as the exact cause of a structured operation error", () => {
155 const rootCause = new Error("stat failed");
156 const terminalError = new TerminalManager.TerminalCwdError({
157 cwd: "/repo/worktrees/a",
158 reason: "statFailed",
159 cause: rootCause,
160 });
161 const project = makeProject([
162 {
163 id: "setup",
164 name: "Setup",
165 command: "bun install",
166 icon: "configure",
167 runOnWorktreeCreate: true,
168 },
169 ]);
170
171 return Effect.gen(function* () {
172 const runner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner;
173 const error = yield* runner
174 .runForThread({
175 threadId: "thread-1",
176 projectId: "project-1",
177 worktreePath: "/repo/worktrees/a",
178 })
179 .pipe(Effect.flip);
180
181 expect(isProjectSetupScriptOperationError(error)).toBe(true);
182 if (isProjectSetupScriptOperationError(error)) {
183 expect(error.operation).toBe("openTerminal");
184 expect(error.threadId).toBe("thread-1");
185 expect(error.projectId).toBe("project-1");
186 expect(error.worktreePath).toBe("/repo/worktrees/a");
187 expect(error.cause).toBe(terminalError);
188 expect(terminalError.cause).toBe(rootCause);
189 }
190 }).pipe(
191 Effect.provide(
192 testLayer(project, {
193 open: () => Effect.fail(terminalError),
194 write: () => Effect.die("unexpected write"),
195 }),
196 ),
197 );
198 });
148199});
apps/server/src/project/ProjectSetupScriptRunner.ts+50-41
......@@ -59,7 +59,7 @@ export class ProjectSetupScriptProjectNotFoundError extends Schema.TaggedErrorCl
5959 },
6060) {
6161 override get message(): string {
62 return "Project was not found for setup script execution.";
62 return `Project was not found for setup script execution for thread '${this.threadId}' in '${this.worktreePath}'.`;
6363 }
6464}
6565
......@@ -78,32 +78,6 @@ export class ProjectSetupScriptRunner extends Context.Service<
7878 }
7979>()("t3/project/ProjectSetupScriptRunner") {}
8080
81const isProjectSetupScriptRunnerError = Schema.is(ProjectSetupScriptRunnerError);
82
83function operationError(
84 input: ProjectSetupScriptRunnerInput,
85 operation: ProjectSetupScriptOperationError["operation"],
86 cause: unknown,
87): ProjectSetupScriptOperationError {
88 return new ProjectSetupScriptOperationError({
89 threadId: input.threadId,
90 worktreePath: input.worktreePath,
91 operation,
92 cause,
93 ...(input.projectId === undefined ? {} : { projectId: input.projectId }),
94 ...(input.projectCwd === undefined ? {} : { projectCwd: input.projectCwd }),
95 });
96}
97
98function mapRunnerError(
99 input: ProjectSetupScriptRunnerInput,
100 operation: ProjectSetupScriptOperationError["operation"],
101) {
102 return Effect.mapError((cause: unknown) =>
103 isProjectSetupScriptRunnerError(cause) ? cause : operationError(input, operation, cause),
104 );
105}
106
10781export const make = Effect.gen(function* () {
10882 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
10983 const terminalManager = yield* TerminalManager.TerminalManager;
......@@ -111,26 +85,43 @@ export const make = Effect.gen(function* () {
11185 const runForThread: ProjectSetupScriptRunner["Service"]["runForThread"] = Effect.fn(
11286 "ProjectSetupScriptRunner.runForThread",
11387 )(function* (input) {
88 const errorContext = {
89 threadId: input.threadId,
90 worktreePath: input.worktreePath,
91 ...(input.projectId === undefined ? {} : { projectId: input.projectId }),
92 ...(input.projectCwd === undefined ? {} : { projectCwd: input.projectCwd }),
93 };
11494 const projectById = input.projectId
115 ? yield* projectionSnapshotQuery
116 .getProjectShellById(ProjectId.make(input.projectId))
117 .pipe(Effect.map(Option.getOrUndefined), mapRunnerError(input, "resolveProject"))
95 ? yield* projectionSnapshotQuery.getProjectShellById(ProjectId.make(input.projectId)).pipe(
96 Effect.map(Option.getOrUndefined),
97 Effect.mapError(
98 (cause) =>
99 new ProjectSetupScriptOperationError({
100 ...errorContext,
101 operation: "resolveProject",
102 cause,
103 }),
104 ),
105 )
118106 : null;
119107 const project =
120108 projectById ??
121109 (input.projectCwd
122 ? yield* projectionSnapshotQuery
123 .getActiveProjectByWorkspaceRoot(input.projectCwd)
124 .pipe(Effect.map(Option.getOrUndefined), mapRunnerError(input, "resolveProject"))
110 ? yield* projectionSnapshotQuery.getActiveProjectByWorkspaceRoot(input.projectCwd).pipe(
111 Effect.map(Option.getOrUndefined),
112 Effect.mapError(
113 (cause) =>
114 new ProjectSetupScriptOperationError({
115 ...errorContext,
116 operation: "resolveProject",
117 cause,
118 }),
119 ),
120 )
125121 : null);
126122
127123 if (!project) {
128 return yield* new ProjectSetupScriptProjectNotFoundError({
129 threadId: input.threadId,
130 worktreePath: input.worktreePath,
131 ...(input.projectId === undefined ? {} : { projectId: input.projectId }),
132 ...(input.projectCwd === undefined ? {} : { projectCwd: input.projectCwd }),
133 });
124 return yield* new ProjectSetupScriptProjectNotFoundError(errorContext);
134125 }
135126
136127 const script = setupProjectScript(project.scripts);
......@@ -155,14 +146,32 @@ export const make = Effect.gen(function* () {
155146 worktreePath: input.worktreePath,
156147 env,
157148 })
158 .pipe(mapRunnerError(input, "openTerminal"));
149 .pipe(
150 Effect.mapError(
151 (cause) =>
152 new ProjectSetupScriptOperationError({
153 ...errorContext,
154 operation: "openTerminal",
155 cause,
156 }),
157 ),
158 );
159159 yield* terminalManager
160160 .write({
161161 threadId: input.threadId,
162162 terminalId,
163163 data: `${script.command}\r`,
164164 })
165 .pipe(mapRunnerError(input, "writeCommand"));
165 .pipe(
166 Effect.mapError(
167 (cause) =>
168 new ProjectSetupScriptOperationError({
169 ...errorContext,
170 operation: "writeCommand",
171 cause,
172 }),
173 ),
174 );
166175
167176 return {
168177 status: "started",