220 lines · 6.4 KB · TypeScript
RawDownload
import type {  CommandId,  EnvironmentId,  OrchestrationCommand,  ProjectId,  SourceControlDiscoveryResult,  SourceControlProviderKind,  SourceControlRepositoryInfo,} from "@t3tools/contracts";import * as Arr from "effect/Array";import * as Option from "effect/Option";import * as Order from "effect/Order"; import {  ensureBrowseDirectoryPath,  findProjectByPath,  inferProjectTitleFromPath,  isExplicitRelativeProjectPath,  isUnsupportedWindowsProjectPath,  resolveProjectPathForDispatch,} from "../state/projects.ts";import type { EnvironmentProject } from "../state/models.ts"; export type AddProjectRemoteProviderKind = Extract<  SourceControlProviderKind,  "github" | "gitlab" | "bitbucket" | "azure-devops">;export type AddProjectRemoteSource = AddProjectRemoteProviderKind | "url"; export type AddProjectRemoteSourceReadiness = Record<  AddProjectRemoteSource,  { readonly ready: boolean; readonly hint: string | null }>; export type AddProjectCloneFlow =  | {      readonly step: "repository";      readonly environmentId: EnvironmentId;      readonly source: AddProjectRemoteSource;    }  | {      readonly step: "confirm";      readonly environmentId: EnvironmentId;      readonly source: AddProjectRemoteSource;      readonly repositoryInput: string;      readonly repository: SourceControlRepositoryInfo | null;      readonly remoteUrl: string;    }; const ADD_PROJECT_REMOTE_SOURCES: ReadonlyArray<AddProjectRemoteSource> = [  "url",  "github",  "gitlab",  "bitbucket",  "azure-devops",]; const ADD_PROJECT_REMOTE_PROVIDER_SOURCES: ReadonlyArray<AddProjectRemoteProviderKind> = [  "github",  "gitlab",  "bitbucket",  "azure-devops",]; export function addProjectRemoteSourceLabel(source: AddProjectRemoteSource): string {  switch (source) {    case "github":      return "GitHub";    case "gitlab":      return "GitLab";    case "bitbucket":      return "Bitbucket";    case "azure-devops":      return "Azure DevOps";    case "url":      return "Git URL";  }} export function addProjectRemoteSourcePathHint(source: AddProjectRemoteSource): string {  switch (source) {    case "github":      return "owner/repo";    case "gitlab":      return "group/project";    case "bitbucket":      return "workspace/repository";    case "azure-devops":      return "project/repository";    case "url":      return "URL";  }} export function addProjectRemoteSourceProvider(  source: AddProjectRemoteSource,): AddProjectRemoteProviderKind | null {  return source === "url" ? null : source;} export function sortAddProjectProviderSources(  readinessBySource: AddProjectRemoteSourceReadiness,): ReadonlyArray<AddProjectRemoteProviderKind> {  return Arr.sort(    ADD_PROJECT_REMOTE_PROVIDER_SOURCES,    Order.mapInput(      Order.Struct({        ready: Order.flip(Order.Boolean),        label: Order.String,      }),      (source: AddProjectRemoteProviderKind) => ({        ready: readinessBySource[source].ready,        label: addProjectRemoteSourceLabel(source),      }),    ),  );} export function buildAddProjectRemoteSourceReadiness(  discovery: SourceControlDiscoveryResult | null,): AddProjectRemoteSourceReadiness {  const unavailable = {    ready: false,    hint: "Provider status unavailable. Open Source Control settings and rescan.",  } as const;  const readiness: AddProjectRemoteSourceReadiness = {    url: { ready: true, hint: null },    github: unavailable,    gitlab: unavailable,    bitbucket: unavailable,    "azure-devops": unavailable,  };   if (!discovery) {    return readiness;  }   const providerByKind = new Map(    discovery.sourceControlProviders.map((provider) => [provider.kind, provider]),  );  for (const source of ADD_PROJECT_REMOTE_SOURCES) {    const kind = addProjectRemoteSourceProvider(source);    if (!kind) continue;    const provider = providerByKind.get(kind);    if (!provider) {      readiness[source] = unavailable;      continue;    }    if (provider.status !== "available") {      readiness[source] = { ready: false, hint: provider.installHint };      continue;    }    if (provider.auth.status === "unauthenticated") {      readiness[source] = {        ready: false,        hint:          Option.getOrNull(provider.auth.detail) ??          `${provider.label} is not authenticated. Open Source Control settings for setup guidance.`,      };      continue;    }    readiness[source] = { ready: true, hint: null };  }  return readiness;} export function getAddProjectInitialQuery(baseDirectory: string | null | undefined): string {  const trimmed = baseDirectory?.trim() ?? "";  return trimmed.length === 0 ? "~/" : ensureBrowseDirectoryPath(trimmed);} export function resolveAddProjectPath(input: {  readonly rawPath: string;  readonly currentProjectCwd?: string | null;  readonly platform: string;}): { readonly ok: true; readonly path: string } | { readonly ok: false; readonly error: string } {  const rawPath = input.rawPath.trim();  if (rawPath.length === 0) {    return { ok: false, error: "Enter a project path." };  }  if (isUnsupportedWindowsProjectPath(rawPath, input.platform)) {    return { ok: false, error: "Windows-style paths are only supported on Windows environments." };  }  if (isExplicitRelativeProjectPath(rawPath) && !input.currentProjectCwd) {    return { ok: false, error: "Relative paths require an active project in this environment." };  }  const path = resolveProjectPathForDispatch(rawPath, input.currentProjectCwd);  return path.length === 0 ? { ok: false, error: "Enter a project path." } : { ok: true, path };} export function findExistingAddProject(input: {  readonly projects: ReadonlyArray<EnvironmentProject>;  readonly environmentId: EnvironmentId;  readonly path: string;}): EnvironmentProject | null {  return (    findProjectByPath(      input.projects.filter((project) => project.environmentId === input.environmentId),      input.path,    ) ?? null  );} export function buildProjectCreateCommand(input: {  readonly commandId: CommandId;  readonly projectId: ProjectId;  readonly workspaceRoot: string;  readonly createdAt: string;}): Extract<OrchestrationCommand, { type: "project.create" }> {  return {    type: "project.create",    commandId: input.commandId,    projectId: input.projectId,    title: inferProjectTitleFromPath(input.workspaceRoot),    workspaceRoot: input.workspaceRoot,    createWorkspaceRootIfMissing: true,    defaultModelSelection: null,    createdAt: input.createdAt,  };}