import { ProjectId } from "@t3tools/contracts"; import { projectScriptRuntimeEnv, resolveProjectScripts, setupProjectScript, } from "@t3tools/shared/projectScripts"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts"; import * as ServerSettings from "../serverSettings.ts"; import * as TerminalManager from "../terminal/Manager.ts"; export interface ProjectSetupScriptRunnerResultNoScript { readonly status: "no-script"; } export interface ProjectSetupScriptRunnerResultStarted { readonly status: "started"; readonly scriptId: string; readonly scriptName: string; readonly terminalId: string; readonly cwd: string; } export type ProjectSetupScriptRunnerResult = | ProjectSetupScriptRunnerResultNoScript | ProjectSetupScriptRunnerResultStarted; export interface ProjectSetupScriptRunnerInput { readonly threadId: string; readonly projectId?: string; readonly projectCwd?: string; readonly worktreePath: string; readonly preferredTerminalId?: string; } export class ProjectSetupScriptOperationError extends Schema.TaggedError()( "ProjectSetupScriptOperationError", { threadId: Schema.String, projectId: Schema.optional(Schema.String), projectCwd: Schema.optional(Schema.String), worktreePath: Schema.String, operation: Schema.Literals(["resolveProject", "readSettings", "openTerminal", "writeCommand"]), cause: Schema.Defect(), }, ) { override get message(): string { return `Project setup script operation '${this.operation}' failed for thread '${this.threadId}' in '${this.worktreePath}'.`; } } export class ProjectSetupScriptProjectNotFoundError extends Schema.TaggedError()( "ProjectSetupScriptProjectNotFoundError", { threadId: Schema.String, projectId: Schema.optional(Schema.String), projectCwd: Schema.optional(Schema.String), worktreePath: Schema.String, }, ) { override get message(): string { return `Project was not found for setup script execution for thread '${this.threadId}' in '${this.worktreePath}'.`; } } export const ProjectSetupScriptRunnerError = Schema.Union([ ProjectSetupScriptOperationError, ProjectSetupScriptProjectNotFoundError, ]); export type ProjectSetupScriptRunnerError = typeof ProjectSetupScriptRunnerError.Type; export class ProjectSetupScriptRunner extends Context.Service< ProjectSetupScriptRunner, { readonly runForThread: ( input: ProjectSetupScriptRunnerInput, ) => Effect.Effect; } >()("t3/project/ProjectSetupScriptRunner") {} /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; const terminalManager = yield* TerminalManager.TerminalManager; const serverSettings = yield* ServerSettings.ServerSettingsService; const runForThread: ProjectSetupScriptRunner["Service"]["runForThread"] = Effect.fn( "ProjectSetupScriptRunner.runForThread", )(function* (input) { const errorContext = { threadId: input.threadId, worktreePath: input.worktreePath, ...(input.projectId === undefined ? {} : { projectId: input.projectId }), ...(input.projectCwd === undefined ? {} : { projectCwd: input.projectCwd }), }; const projectById = input.projectId ? yield* projectionSnapshotQuery.getProjectShellById(ProjectId.make(input.projectId)).pipe( Effect.map(Option.getOrUndefined), Effect.mapError( (cause) => new ProjectSetupScriptOperationError({ ...errorContext, operation: "resolveProject", cause, }), ), ) : null; const project = projectById ?? (input.projectCwd ? yield* projectionSnapshotQuery.getActiveProjectByWorkspaceRoot(input.projectCwd).pipe( Effect.map(Option.getOrUndefined), Effect.mapError( (cause) => new ProjectSetupScriptOperationError({ ...errorContext, operation: "resolveProject", cause, }), ), ) : null); if (!project) { return yield* new ProjectSetupScriptProjectNotFoundError(errorContext); } const settings = yield* serverSettings.getSettings.pipe( Effect.mapError( (cause) => new ProjectSetupScriptOperationError({ ...errorContext, operation: "readSettings", cause, }), ), ); const script = setupProjectScript(resolveProjectScripts(settings, project)); if (!script) { return { status: "no-script", } as const; } const terminalId = input.preferredTerminalId ?? `setup-${script.id}`; const cwd = input.worktreePath; const env = projectScriptRuntimeEnv({ project: { cwd: project.workspaceRoot }, worktreePath: input.worktreePath, }); yield* terminalManager .open({ threadId: input.threadId, terminalId, cwd, worktreePath: input.worktreePath, env, }) .pipe( Effect.mapError( (cause) => new ProjectSetupScriptOperationError({ ...errorContext, operation: "openTerminal", cause, }), ), ); yield* terminalManager .write({ threadId: input.threadId, terminalId, data: `${script.command}\r`, }) .pipe( Effect.mapError( (cause) => new ProjectSetupScriptOperationError({ ...errorContext, operation: "writeCommand", cause, }), ), ); return { status: "started", scriptId: script.id, scriptName: script.name, terminalId, cwd, } as const; }); return ProjectSetupScriptRunner.of({ runForThread }); }); export const layer = Layer.effect(ProjectSetupScriptRunner, make);