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>()( "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>()( "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<ProjectSetupScriptRunnerResult, ProjectSetupScriptRunnerError>; }>()("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);
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>()(
"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>()(
"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<ProjectSetupScriptRunnerResult, ProjectSetupScriptRunnerError>;
}
>()("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);