320 lines · 10.4 KB · TypeScript
RawDownload
import OS from "node:os";import { Context, Effect, FileSystem, Layer, Path, Schema } from "effect"; import {  SourceControlRepositoryError,  type SourceControlCloneRepositoryInput,  type SourceControlCloneRepositoryResult,  type SourceControlCloneProtocol,  type SourceControlProviderKind,  type SourceControlPublishRepositoryInput,  type SourceControlPublishRepositoryResult,  type SourceControlRepositoryCloneUrls,  type SourceControlRepositoryInfo,  type SourceControlRepositoryLookupInput,} from "@t3tools/contracts"; import { ServerConfig } from "../config.ts";import { GitVcsDriver } from "../vcs/GitVcsDriver.ts";import { SourceControlProviderRegistry } from "./SourceControlProviderRegistry.ts"; export interface SourceControlRepositoryServiceShape {  readonly lookupRepository: (    input: SourceControlRepositoryLookupInput,  ) => Effect.Effect<SourceControlRepositoryInfo, SourceControlRepositoryError>;  readonly cloneRepository: (    input: SourceControlCloneRepositoryInput,  ) => Effect.Effect<SourceControlCloneRepositoryResult, SourceControlRepositoryError>;  readonly publishRepository: (    input: SourceControlPublishRepositoryInput,  ) => Effect.Effect<SourceControlPublishRepositoryResult, SourceControlRepositoryError>;} export class SourceControlRepositoryService extends Context.Service<  SourceControlRepositoryService,  SourceControlRepositoryServiceShape>()("t3/source-control/SourceControlRepositoryService") {} function detailFromUnknown(cause: unknown): string {  if (typeof cause === "object" && cause !== null) {    if ("detail" in cause && typeof cause.detail === "string" && cause.detail.length > 0) {      return cause.detail;    }    if ("message" in cause && typeof cause.message === "string" && cause.message.length > 0) {      return cause.message;    }  }   return "An unexpected source control error occurred.";} function repositoryError(input: {  readonly operation: string;  readonly provider: SourceControlProviderKind;  readonly detail: string;  readonly cause?: unknown;}): SourceControlRepositoryError {  return new SourceControlRepositoryError({    provider: input.provider,    operation: input.operation,    detail: input.detail,    ...(input.cause === undefined ? {} : { cause: input.cause }),  });} function mapRepositoryError(operation: string, provider: SourceControlProviderKind) {  return Effect.mapError((cause: unknown) =>    Schema.is(SourceControlRepositoryError)(cause)      ? cause      : repositoryError({          operation,          provider,          detail: detailFromUnknown(cause),          cause,        }),  );} function toRepositoryInfo(  provider: SourceControlProviderKind,  urls: SourceControlRepositoryCloneUrls,): SourceControlRepositoryInfo {  return {    provider,    nameWithOwner: urls.nameWithOwner,    url: urls.url,    sshUrl: urls.sshUrl,  };} function selectRemoteUrl(  urls: SourceControlRepositoryCloneUrls,  protocol: SourceControlCloneProtocol | undefined,): string {  switch (protocol ?? "auto") {    case "https":      return urls.url;    case "ssh":    case "auto":      return urls.sshUrl;  }} function expandHomePath(input: string, path: Path.Path): string {  if (input === "~") {    return OS.homedir();  }  if (input.startsWith("~/") || input.startsWith("~\\")) {    return path.join(OS.homedir(), input.slice(2));  }  return input;} export const make = Effect.fn("makeSourceControlRepositoryService")(function* () {  const config = yield* ServerConfig;  const fileSystem = yield* FileSystem.FileSystem;  const git = yield* GitVcsDriver;  const path = yield* Path.Path;  const providers = yield* SourceControlProviderRegistry;   const ensureConcreteProvider = (input: {    readonly operation: string;    readonly provider: SourceControlProviderKind;  }) => {    if (input.provider !== "unknown") {      return Effect.succeed(input.provider);    }     return Effect.fail(      repositoryError({        operation: input.operation,        provider: input.provider,        detail: "Choose a source control provider before continuing.",      }),    );  };   const lookupRepository = Effect.fn("SourceControlRepositoryService.lookupRepository")(function* (    input: SourceControlRepositoryLookupInput,  ) {    const providerKind = yield* ensureConcreteProvider({      operation: "lookupRepository",      provider: input.provider,    });    const provider = yield* providers.get(providerKind);    const urls = yield* provider.getRepositoryCloneUrls({      cwd: input.cwd ?? config.cwd,      repository: input.repository.trim(),    });    return toRepositoryInfo(providerKind, urls);  });   const normalizeDestinationPath = Effect.fn("SourceControlRepositoryService.normalizeDestination")(    function* (destinationPath: string) {      const trimmed = destinationPath.trim();      if (trimmed.length === 0) {        return yield* Effect.fail(          repositoryError({            operation: "cloneRepository",            provider: "unknown",            detail: "Choose a destination path before cloning.",          }),        );      }       return path.resolve(expandHomePath(trimmed, path));    },  );   const prepareDestination = Effect.fn("SourceControlRepositoryService.prepareDestination")(    function* (destinationPath: string) {      const normalizedDestination = yield* normalizeDestinationPath(destinationPath);      if (yield* fileSystem.exists(normalizedDestination).pipe(Effect.orElseSucceed(() => false))) {        const entries = yield* fileSystem          .readDirectory(normalizedDestination, { recursive: false })          .pipe(            Effect.mapError((cause) =>              repositoryError({                operation: "cloneRepository",                provider: "unknown",                detail: "Destination path already exists and is not a directory.",                cause,              }),            ),          );        if (entries.length > 0) {          return yield* Effect.fail(            repositoryError({              operation: "cloneRepository",              provider: "unknown",              detail: "Destination path already exists and is not empty.",            }),          );        }      } else {        yield* fileSystem.makeDirectory(path.dirname(normalizedDestination), { recursive: true });      }       return {        destinationPath: normalizedDestination,        parentPath: path.dirname(normalizedDestination),        directoryName: path.basename(normalizedDestination),      };    },  );   const cloneRepository = Effect.fn("SourceControlRepositoryService.cloneRepository")(function* (    input: SourceControlCloneRepositoryInput,  ) {    const preparedDestination = yield* prepareDestination(input.destinationPath);    let repository: SourceControlRepositoryInfo | null = null;    let remoteUrl = input.remoteUrl?.trim() ?? null;    let provider: SourceControlProviderKind = input.provider ?? "unknown";     if (input.provider && input.repository) {      repository = yield* lookupRepository({        provider: input.provider,        repository: input.repository,        cwd: preparedDestination.parentPath,      });      remoteUrl = selectRemoteUrl(repository, input.protocol);      provider = input.provider;    }     if (!remoteUrl) {      return yield* Effect.fail(        repositoryError({          operation: "cloneRepository",          provider,          detail: "Enter a repository path or clone URL before cloning.",        }),      );    }     yield* git.execute({      operation: "SourceControlRepositoryService.cloneRepository",      cwd: preparedDestination.parentPath,      args: ["clone", remoteUrl, preparedDestination.directoryName],      timeoutMs: 120_000,      maxOutputBytes: 256 * 1024,    });     return {      cwd: preparedDestination.destinationPath,      remoteUrl,      repository,    };  });   const publishRepository = Effect.fn("SourceControlRepositoryService.publishRepository")(    function* (input: SourceControlPublishRepositoryInput) {      const providerKind = yield* ensureConcreteProvider({        operation: "publishRepository",        provider: input.provider,      });      const provider = yield* providers.get(providerKind);      const urls = yield* provider.createRepository({        cwd: input.cwd,        repository: input.repository.trim(),        visibility: input.visibility,      });      const remoteUrl = selectRemoteUrl(urls, input.protocol);      const remoteName = yield* git.ensureRemote({        cwd: input.cwd,        preferredName: input.remoteName?.trim() || "origin",        url: remoteUrl,      });       // An empty local repo (no commits) would make `git push HEAD:...` fail      // with an opaque "src refspec HEAD does not match any". Treat this as a      // partial success: the remote was created and wired up, but there is      // nothing to push yet.      const hasCommits = yield* git        .execute({          operation: "SourceControlRepositoryService.publishRepository.headCheck",          cwd: input.cwd,          args: ["rev-parse", "--verify", "HEAD"],        })        .pipe(          Effect.map(() => true),          Effect.catch(() => Effect.succeed(false)),        );      if (!hasCommits) {        const details = yield* git          .statusDetails(input.cwd)          .pipe(Effect.catch(() => Effect.succeed(null)));        return {          repository: toRepositoryInfo(providerKind, urls),          remoteName,          remoteUrl,          branch: details?.branch ?? "main",          status: "remote_added" as const,        };      }       const pushResult = yield* git.pushCurrentBranch(input.cwd, null, { remoteName });       return {        repository: toRepositoryInfo(providerKind, urls),        remoteName,        remoteUrl,        branch: pushResult.branch,        ...(pushResult.upstreamBranch ? { upstreamBranch: pushResult.upstreamBranch } : {}),        status: "pushed" as const,      };    },  );   return SourceControlRepositoryService.of({    lookupRepository: (input) =>      lookupRepository(input).pipe(mapRepositoryError("lookupRepository", input.provider)),    cloneRepository: (input) =>      cloneRepository(input).pipe(        mapRepositoryError("cloneRepository", input.provider ?? "unknown"),      ),    publishRepository: (input) =>      publishRepository(input).pipe(mapRepositoryError("publishRepository", input.provider)),  });}); export const layer = Layer.effect(SourceControlRepositoryService, make());