480 lines · 16.3 KB · TypeScript
RawDownload
import {  CommandId,  AuthAdministrativeScopes,  EnvironmentHttpApi,  EnvironmentHttpCommonError,  type OrchestrationReadModel,  ProjectId,  type ClientOrchestrationCommand,} from "@t3tools/contracts";import * as Console from "effect/Console";import * as Crypto from "effect/Crypto";import * as DateTime from "effect/DateTime";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 * as OrchestrationEngine from "../orchestration/Services/OrchestrationEngine.ts";import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts";import { OrchestrationLayerLive } from "../orchestration/runtimeLayer.ts";import { layerConfig as SqlitePersistenceLayerLive } from "../persistence/Layers/Sqlite.ts";import * as RepositoryIdentityResolver from "../project/RepositoryIdentityResolver.ts";import * as ServerRuntimeStartup from "../serverRuntimeStartup.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 = Extract<  ClientOrchestrationCommand,  { type: "project.create" | "project.meta.update" | "project.delete" }>; const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError);const ProjectCommandOperation = Schema.Literals([  "generateProjectCommandId",  "callLiveServer",  "validateProjectTitle",  "resolveProjectTarget",  "addProject",]); export class ProjectCommandError extends Schema.TaggedErrorClass<ProjectCommandError>()(  "ProjectCommandError",  {    operation: ProjectCommandOperation,    detail: Schema.String,    code: Schema.optional(Schema.String),    traceId: Schema.optional(Schema.String),    status: Schema.optional(Schema.Number),    cause: Schema.optional(Schema.Defect()),  },) {  static fromLiveServerRequest(cause: unknown): ProjectCommandError {    if (isEnvironmentHttpCommonError(cause)) {      return new ProjectCommandError({        operation: "callLiveServer",        detail: `Server request failed (${cause.code}, trace ${cause.traceId}).`,        code: cause.code,        traceId: cause.traceId,        cause,      });    }    if (HttpClientError.isHttpClientError(cause) && cause.response !== undefined) {      return new ProjectCommandError({        operation: "callLiveServer",        detail: `Server request failed with undeclared status ${cause.response.status}.`,        status: cause.response.status,        cause,      });    }    return new ProjectCommandError({      operation: "callLiveServer",      detail: "Failed to call the running server.",      cause,    });  }   override get message(): string {    return this.detail;  }} const projectCommandUuid = Crypto.Crypto.pipe(  Effect.flatMap((crypto) => crypto.randomUUIDv4),  Effect.mapError(    (cause) =>      new ProjectCommandError({        operation: "generateProjectCommandId",        detail: "Failed to generate a project command identifier.",        cause,      }),  ),); const ProjectCliRuntimeLive = Layer.mergeAll(  WorkspacePaths.layer,  OrchestrationLayerLive.pipe(    Layer.provideMerge(RepositoryIdentityResolver.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 ProjectCommandError({      operation: "validateProjectTitle",      detail: "Project title cannot be empty.",    });  }   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: OrchestrationReadModel;  readonly identifier: string;}) {  const trimmedIdentifier = input.identifier.trim();  if (trimmedIdentifier.length === 0) {    return yield* new ProjectCommandError({      operation: "resolveProjectTarget",      detail: "Project identifier cannot be empty.",    });  }   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;   const exactWorkspaceMatch =    normalizedWorkspaceRoot === null      ? undefined      : activeProjects.find((project) => project.workspaceRoot === normalizedWorkspaceRoot);   const resolved = exactWorkspaceMatch;  if (!resolved) {    return yield* new ProjectCommandError({      operation: "resolveProjectTarget",      detail: `No active project found for '${trimmedIdentifier}'.`,      ...(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.orchestration.snapshot({      headers: { authorization: `Bearer ${bearerToken}` },    });  }).pipe(    withProjectCliLiveServerTimeout,    Effect.mapError(ProjectCommandError.fromLiveServerRequest),  ); const dispatchLiveOrchestrationCommand = (  origin: string,  bearerToken: string,  command: ProjectCliDispatchCommand,) =>  Effect.gen(function* () {    const client = yield* makeLiveServerClient(origin);    yield* client.orchestration.dispatch({      headers: { authorization: `Bearer ${bearerToken}` },      payload: command,    } as Parameters<typeof client.orchestration.dispatch>[0]);  }).pipe(    withProjectCliLiveServerTimeout,    Effect.mapError(ProjectCommandError.fromLiveServerRequest),  ); const getOfflineSnapshot = Effect.fn("getOfflineSnapshot")(function* () {  const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;  return yield* projectionSnapshotQuery.getSnapshot();}); 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: OrchestrationReadModel;    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 orchestrationEngine = yield* OrchestrationEngine.OrchestrationEngineService;      const output = yield* run({        snapshot,        dispatch: (command) => orchestrationEngine.dispatch(command),        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: OrchestrationReadModel;        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(          (project) => project.deletedAt === null && project.workspaceRoot === workspaceRoot,        );        if (existingProject) {          return yield* new ProjectCommandError({            operation: "addProject",            detail: `An active project already exists for '${workspaceRoot}'.`,          });        }         const title = yield* resolveProjectTitle(workspaceRoot, Option.getOrUndefined(flags.title));        const projectId = ProjectId.make(yield* projectCommandUuid);        yield* dispatch({          type: "project.create",          commandId: CommandId.make(yield* projectCommandUuid),          projectId,          title,          workspaceRoot,          defaultModelSelection: ServerRuntimeStartup.getAutoBootstrapDefaultModelSelection(),          createdAt: DateTime.formatIso(yield* DateTime.now),        });        return `Added project ${projectId} (${title}) at ${workspaceRoot}.`;      }),    ),  ),); const projectRemoveCommand = Command.make("remove", {  ...projectLocationFlags,  project: Argument.string("project").pipe(    Argument.withDescription("Project id or workspace root to remove."),  ),}).pipe(  Command.withDescription("Remove a project."),  Command.withHandler((flags) =>    runProjectMutation(      flags,      Effect.fn("projectRemoveMutation")(function* ({        snapshot,        dispatch,      }: {        readonly snapshot: OrchestrationReadModel;        readonly dispatch: (          command: ProjectCliDispatchCommand,        ) => Effect.Effect<void, Error, FileSystem.FileSystem | HttpClient.HttpClient | Path.Path>;      }) {        const project = yield* findActiveProjectTarget({          snapshot,          identifier: flags.project,        });        yield* dispatch({          type: "project.delete",          commandId: CommandId.make(yield* projectCommandUuid),          projectId: project.id,        });        return `Removed project ${project.id} (${project.title}).`;      }),    ),  ),); const projectRenameCommand = Command.make("rename", {  ...projectLocationFlags,  project: Argument.string("project").pipe(    Argument.withDescription("Project id or workspace root to rename."),  ),  title: Argument.string("title").pipe(Argument.withDescription("New project title.")),}).pipe(  Command.withDescription("Rename a project."),  Command.withHandler((flags) =>    runProjectMutation(      flags,      Effect.fn("projectRenameMutation")(function* ({        snapshot,        dispatch,      }: {        readonly snapshot: OrchestrationReadModel;        readonly dispatch: (          command: ProjectCliDispatchCommand,        ) => Effect.Effect<void, Error, FileSystem.FileSystem | HttpClient.HttpClient | Path.Path>;      }) {        const project = yield* findActiveProjectTarget({          snapshot,          identifier: flags.project,        });        const nextTitle = yield* resolveProjectTitle(project.workspaceRoot, flags.title);        if (nextTitle === project.title) {          return `Project ${project.id} is already named ${nextTitle}.`;        }         yield* dispatch({          type: "project.meta.update",          commandId: CommandId.make(yield* projectCommandUuid),          projectId: project.id,          title: nextTitle,        });        return `Renamed project ${project.id} to ${nextTitle}.`;      }),    ),  ),); export const projectCommand = Command.make("project").pipe(  Command.withDescription("Manage projects."),  Command.withSubcommands([projectAddCommand, projectRemoveCommand, projectRenameCommand]),);