609 lines · 20.5 KB · TypeScript
RawDownload
import {  AuthAdministrativeScopes,  CommandId,  EnvironmentHttpApi,  EnvironmentHttpCommonError,  type ProjectMutation,  type ProjectSnapshot,  ProjectId,} from "@t3tools/contracts";import * as Console from "effect/Console";import * as Crypto from "effect/Crypto";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as References from "effect/References";import * as Schema from "effect/Schema";import { Argument, Command, Flag, GlobalFlag } from "effect/unstable/cli";import { FetchHttpClient, HttpClient, HttpClientError } from "effect/unstable/http";import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient"; import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts"; import * as ServerConfig from "../config.ts";import { layerConfig as SqlitePersistenceLayerLive } from "../persistence/Layers/Sqlite.ts";import { ProjectServiceLayerLive } from "../orchestration-v2/runtimeLayer.ts";import * as ProjectEnrichmentService from "../project/ProjectEnrichmentService.ts";import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts";import * as RepositoryIdentityResolver from "../project/RepositoryIdentityResolver.ts";import * as ProjectService from "../project/ProjectService.ts";import * as T3ProjectFileLoader from "../project/T3ProjectFileLoader.ts";import {  clearPersistedServerRuntimeState,  readPersistedServerRuntimeState,} from "../serverRuntimeState.ts";import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";import { type CliAuthLocationFlags, projectLocationFlags, resolveCliAuthConfig } from "./config.ts"; type ProjectMutationTarget = {  readonly id: ProjectId;  readonly title: string;  readonly workspaceRoot: string;}; type ProjectCommandExecutionMode = "live" | "offline";type ProjectCliDispatchCommand = ProjectMutation; const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError); export class ProjectCommandIdGenerationError extends Schema.TaggedErrorClass<ProjectCommandIdGenerationError>()(  "ProjectCommandIdGenerationError",  {    operation: Schema.Literal("generateProjectCommandId"),    cause: Schema.Defect(),  },) {  override get message(): string {    return "Failed to generate a project command identifier.";  }} export class ProjectLiveServerDeclaredResponseError extends Schema.TaggedErrorClass<ProjectLiveServerDeclaredResponseError>()(  "ProjectLiveServerDeclaredResponseError",  {    operation: Schema.Literal("callLiveServer"),    code: Schema.String,    traceId: Schema.String,    cause: Schema.Defect(),  },) {  override get message(): string {    return `Server request failed (${this.code}, trace ${this.traceId}).`;  }} export class ProjectLiveServerUndeclaredStatusError extends Schema.TaggedErrorClass<ProjectLiveServerUndeclaredStatusError>()(  "ProjectLiveServerUndeclaredStatusError",  {    operation: Schema.Literal("callLiveServer"),    status: Schema.Int,    cause: Schema.Defect(),  },) {  override get message(): string {    return `Server request failed with undeclared status ${this.status}.`;  }} export class ProjectLiveServerRequestError extends Schema.TaggedErrorClass<ProjectLiveServerRequestError>()(  "ProjectLiveServerRequestError",  {    operation: Schema.Literal("callLiveServer"),    cause: Schema.Defect(),  },) {  override get message(): string {    return "Failed to call the running server.";  }} export class ProjectTitleEmptyError extends Schema.TaggedErrorClass<ProjectTitleEmptyError>()(  "ProjectTitleEmptyError",  {    operation: Schema.Literal("validateProjectTitle"),    title: Schema.String,  },) {  override get message(): string {    return "Project title cannot be empty.";  }} export class ProjectIdentifierEmptyError extends Schema.TaggedErrorClass<ProjectIdentifierEmptyError>()(  "ProjectIdentifierEmptyError",  {    operation: Schema.Literal("resolveProjectTarget"),    identifier: Schema.String,  },) {  override get message(): string {    return "Project identifier cannot be empty.";  }} export class ProjectNotFoundError extends Schema.TaggedErrorClass<ProjectNotFoundError>()(  "ProjectNotFoundError",  {    operation: Schema.Literal("resolveProjectTarget"),    identifier: Schema.String,    normalizedWorkspaceRoot: Schema.optional(Schema.String),    activeProjectCount: Schema.Number,    cause: Schema.optional(Schema.Defect()),  },) {  override get message(): string {    return `No active project found for '${this.identifier}'.`;  }} export class ProjectAlreadyExistsError extends Schema.TaggedErrorClass<ProjectAlreadyExistsError>()(  "ProjectAlreadyExistsError",  {    operation: Schema.Literal("addProject"),    projectId: ProjectId,    workspaceRoot: Schema.String,  },) {  override get message(): string {    return `An active project already exists for '${this.workspaceRoot}'.`;  }} export const ProjectCommandError = Schema.Union([  ProjectCommandIdGenerationError,  ProjectLiveServerDeclaredResponseError,  ProjectLiveServerUndeclaredStatusError,  ProjectLiveServerRequestError,  ProjectTitleEmptyError,  ProjectIdentifierEmptyError,  ProjectNotFoundError,  ProjectAlreadyExistsError,]);export type ProjectCommandError = typeof ProjectCommandError.Type; export function projectCommandErrorFromLiveServerRequest(cause: unknown): ProjectCommandError {  if (isEnvironmentHttpCommonError(cause)) {    return new ProjectLiveServerDeclaredResponseError({      operation: "callLiveServer",      code: cause.code,      traceId: cause.traceId,      cause,    });  }  if (HttpClientError.isHttpClientError(cause) && cause.response !== undefined) {    return new ProjectLiveServerUndeclaredStatusError({      operation: "callLiveServer",      status: cause.response.status,      cause,    });  }   return new ProjectLiveServerRequestError({ operation: "callLiveServer", cause });} const projectCommandUuid = Crypto.Crypto.pipe(  Effect.flatMap((crypto) => crypto.randomUUIDv4),  Effect.mapError(    (cause) =>      new ProjectCommandIdGenerationError({        operation: "generateProjectCommandId",        cause,      }),  ),); const ProjectCliRuntimeLive = ProjectServiceLayerLive.pipe(  Layer.provideMerge(ProjectEnrichmentService.layer),  Layer.provideMerge(RepositoryIdentityResolver.layer),  Layer.provideMerge(    ProjectFaviconResolver.layer.pipe(      Layer.provide(WorkspacePaths.layer),      Layer.provide(T3ProjectFileLoader.layer),    ),  ),  Layer.provideMerge(WorkspacePaths.layer),  Layer.provideMerge(SqlitePersistenceLayerLive),); const PROJECT_CLI_LIVE_SERVER_TIMEOUT = Duration.seconds(1);const withProjectCliSessionToken = <A, E, R>(  environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"],  run: (token: string) => Effect.Effect<A, E, R>,) =>  Effect.acquireUseRelease(    environmentAuth.issueSession({      scopes: AuthAdministrativeScopes,      label: "t3 project cli",    }),    (issued) => run(issued.token),    (issued) => environmentAuth.revokeSession(issued.sessionId).pipe(Effect.ignore({ log: true })),  ); const withProjectCliLiveServerTimeout = <A, E, R>(effect: Effect.Effect<A, E, R>) =>  effect.pipe(Effect.timeout(PROJECT_CLI_LIVE_SERVER_TIMEOUT)); const makeLiveServerClient = (origin: string) =>  HttpApiClient.make(EnvironmentHttpApi, {    baseUrl: origin,  }); const normalizeWorkspaceRootForProjectCommand = Effect.fn(  "normalizeWorkspaceRootForProjectCommand",)(function* (workspaceRoot: string) {  const workspacePaths = yield* WorkspacePaths.WorkspacePaths;  return yield* workspacePaths.normalizeWorkspaceRoot(workspaceRoot);}); const resolveProjectTitle = Effect.fn("resolveProjectTitle")(function* (  workspaceRoot: string,  explicitTitle?: string,) {  if (explicitTitle !== undefined) {    const trimmed = explicitTitle.trim();    if (trimmed.length > 0) {      return trimmed;    }    return yield* new ProjectTitleEmptyError({      operation: "validateProjectTitle",      title: explicitTitle,    });  }   const path = yield* Path.Path;  const basename = path.basename(workspaceRoot).trim();  return basename.length > 0 ? basename : "project";}); const findActiveProjectTarget = Effect.fn("findActiveProjectTarget")(function* (input: {  readonly snapshot: ProjectSnapshot;  readonly identifier: string;}) {  const trimmedIdentifier = input.identifier.trim();  if (trimmedIdentifier.length === 0) {    return yield* new ProjectIdentifierEmptyError({      operation: "resolveProjectTarget",      identifier: input.identifier,    });  }   const activeProjects = input.snapshot.projects.filter((project) => project.deletedAt === null);  const exactIdMatch = activeProjects.find((project) => project.id === trimmedIdentifier);  if (exactIdMatch) {    return {      id: exactIdMatch.id,      title: exactIdMatch.title,      workspaceRoot: exactIdMatch.workspaceRoot,    } satisfies ProjectMutationTarget;  }   const normalizedWorkspaceRootResult = yield* Effect.result(    normalizeWorkspaceRootForProjectCommand(trimmedIdentifier),  );  const normalizedWorkspaceRoot =    normalizedWorkspaceRootResult._tag === "Success" ? normalizedWorkspaceRootResult.success : null;   // A stored workspace path still identifies its project after the directory is gone.  const exactWorkspaceMatch = activeProjects.find(    (project) => project.workspaceRoot === (normalizedWorkspaceRoot ?? trimmedIdentifier),  );   const resolved = exactWorkspaceMatch;  if (!resolved) {    return yield* new ProjectNotFoundError({      operation: "resolveProjectTarget",      identifier: trimmedIdentifier,      activeProjectCount: activeProjects.length,      ...(normalizedWorkspaceRoot === null ? {} : { normalizedWorkspaceRoot }),      ...(normalizedWorkspaceRootResult._tag === "Failure"        ? { cause: normalizedWorkspaceRootResult.failure }        : {}),    });  }   return {    id: resolved.id,    title: resolved.title,    workspaceRoot: resolved.workspaceRoot,  } satisfies ProjectMutationTarget;}); const fetchLiveOrchestrationSnapshot = (origin: string, bearerToken: string) =>  Effect.gen(function* () {    const client = yield* makeLiveServerClient(origin);    return yield* client.projects.snapshot({      headers: { authorization: `Bearer ${bearerToken}` },    });  }).pipe(    withProjectCliLiveServerTimeout,    Effect.mapError(projectCommandErrorFromLiveServerRequest),  ); const dispatchLiveOrchestrationCommand = (  origin: string,  bearerToken: string,  command: ProjectCliDispatchCommand,) =>  Effect.gen(function* () {    const client = yield* makeLiveServerClient(origin);    yield* client.projects.mutate({      headers: { authorization: `Bearer ${bearerToken}` },      payload: command,    } as Parameters<typeof client.projects.mutate>[0]);  }).pipe(    withProjectCliLiveServerTimeout,    Effect.mapError(projectCommandErrorFromLiveServerRequest),  ); const getOfflineSnapshot = Effect.fn("getOfflineSnapshot")(function* () {  const projects = yield* ProjectService.ProjectService;  return yield* projects.snapshot;}); const tryResolveLiveProjectExecutionMode = Effect.fn("tryResolveLiveProjectExecutionMode")(  function* (    environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"],    config: ServerConfig.ServerConfig["Service"],  ) {    const runtimeState = yield* readPersistedServerRuntimeState(config.serverRuntimeStatePath);    if (Option.isNone(runtimeState)) {      return Option.none<{ readonly origin: string }>();    }     const attempt = withProjectCliSessionToken(environmentAuth, (token) =>      fetchLiveOrchestrationSnapshot(runtimeState.value.origin, token).pipe(        Effect.as({          origin: runtimeState.value.origin,        }),      ),    );     const attempted = yield* Effect.result(attempt);    if (attempted._tag === "Success") {      return Option.some(attempted.success);    }     yield* Effect.logDebug("Failed to connect to the persisted project CLI server.", {      origin: runtimeState.value.origin,      cause: attempted.failure,    });    yield* clearPersistedServerRuntimeState(config.serverRuntimeStatePath);    return Option.none<{ readonly origin: string }>();  },); const runProjectMutation = Effect.fn("runProjectMutation")(function* (  flags: CliAuthLocationFlags,  run: (input: {    readonly snapshot: ProjectSnapshot;    readonly dispatch: (      command: ProjectCliDispatchCommand,    ) => Effect.Effect<void, Error, FileSystem.FileSystem | HttpClient.HttpClient | Path.Path>;    readonly mode: ProjectCommandExecutionMode;  }) => Effect.Effect<    string,    Error,    | Crypto.Crypto    | FileSystem.FileSystem    | HttpClient.HttpClient    | Path.Path    | WorkspacePaths.WorkspacePaths  >,) {  const logLevel = yield* GlobalFlag.LogLevel;  const config = yield* resolveCliAuthConfig(flags, logLevel);  const minimumLogLevel = config.logLevel;   return yield* Effect.gen(function* () {    const environmentAuth = yield* EnvironmentAuth.EnvironmentAuth;    const liveMode = yield* tryResolveLiveProjectExecutionMode(environmentAuth, config);     if (Option.isSome(liveMode)) {      return yield* withProjectCliSessionToken(environmentAuth, (token) =>        Effect.gen(function* () {          const snapshot = yield* fetchLiveOrchestrationSnapshot(liveMode.value.origin, token);          const output = yield* run({            snapshot,            dispatch: (command) =>              dispatchLiveOrchestrationCommand(liveMode.value.origin, token, command),            mode: "live",          });          yield* Console.log(output);        }),      );    }     const offlineRuntimeLayer = ProjectCliRuntimeLive.pipe(      Layer.provide(ServerConfig.layer(config)),      Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),    );     return yield* Effect.gen(function* () {      const snapshot = yield* getOfflineSnapshot();      const projects = yield* ProjectService.ProjectService;      const output = yield* run({        snapshot,        dispatch: (command) =>          command.type === "project.create"            ? projects                .create({                  commandId: command.commandId,                  projectId: command.projectId,                  title: command.title,                  workspaceRoot: command.workspaceRoot,                  ...(command.defaultModelSelection === undefined                    ? {}                    : { defaultModelSelection: command.defaultModelSelection }),                  ...(command.scripts === undefined ? {} : { scripts: command.scripts }),                })                .pipe(Effect.asVoid)            : command.type === "project.update"              ? projects                  .update({                    commandId: command.commandId,                    projectId: command.projectId,                    ...(command.title === undefined ? {} : { title: command.title }),                    ...(command.workspaceRoot === undefined                      ? {}                      : { workspaceRoot: command.workspaceRoot }),                    ...(command.defaultModelSelection === undefined                      ? {}                      : { defaultModelSelection: command.defaultModelSelection }),                    ...(command.scripts === undefined ? {} : { scripts: command.scripts }),                  })                  .pipe(Effect.asVoid)              : projects                  .delete({                    commandId: command.commandId,                    projectId: command.projectId,                    ...(command.force === undefined ? {} : { force: command.force }),                  })                  .pipe(Effect.asVoid),        mode: "offline",      });      yield* Console.log(output);    }).pipe(Effect.provide(offlineRuntimeLayer));  }).pipe(    Effect.provide(      Layer.mergeAll(EnvironmentAuth.runtimeLayer, WorkspacePaths.layer).pipe(        Layer.provideMerge(FetchHttpClient.layer),        Layer.provide(ServerConfig.layer(config)),        Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),      ),    ),  );}); const projectAddCommand = Command.make("add", {  ...projectLocationFlags,  workspaceRoot: Argument.string("path").pipe(    Argument.withDescription("Workspace root to add as a project."),  ),  title: Flag.string("title").pipe(Flag.withDescription("Optional project title."), Flag.optional),}).pipe(  Command.withDescription("Add a project."),  Command.withHandler((flags) =>    runProjectMutation(      flags,      Effect.fn("projectAddMutation")(function* ({        snapshot,        dispatch,      }: {        readonly snapshot: ProjectSnapshot;        readonly dispatch: (          command: ProjectCliDispatchCommand,        ) => Effect.Effect<void, Error, FileSystem.FileSystem | HttpClient.HttpClient | Path.Path>;      }) {        const workspaceRoot = yield* normalizeWorkspaceRootForProjectCommand(flags.workspaceRoot);        const existingProject = snapshot.projects.find(

Lines 1–500 of 609.