import * as NodeOS from "node:os"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; 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 * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError); export interface SourceControlRepositoryServiceShape { readonly lookupRepository: ( input: SourceControlRepositoryLookupInput, ) => Effect.Effect; readonly cloneRepository: ( input: SourceControlCloneRepositoryInput, ) => Effect.Effect; readonly publishRepository: ( input: SourceControlPublishRepositoryInput, ) => Effect.Effect; } export class SourceControlRepositoryService extends Context.Service< SourceControlRepositoryService, SourceControlRepositoryServiceShape >()("t3/sourceControl/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) => isSourceControlRepositoryError(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 NodeOS.homedir(); } if (input.startsWith("~/") || input.startsWith("~\\")) { return path.join(NodeOS.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.GitVcsDriver; const path = yield* Path.Path; const providers = yield* SourceControlProviderRegistry.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* 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* 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* 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.orElseSucceed(() => false), ); if (!hasCommits) { const details = yield* git.statusDetails(input.cwd).pipe(Effect.orElseSucceed(() => 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());