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 { VcsDriverKind, type VcsDriverKind as VcsDriverKindType } from "@t3tools/contracts"; const ProjectVcsConfig = Schema.Struct({ vcs: Schema.optional( Schema.Struct({ kind: Schema.optional(VcsDriverKind), }), ), vcsKind: Schema.optional(VcsDriverKind), }); const isProjectVcsConfig = Schema.is(ProjectVcsConfig); interface ProjectVcsConfigFile { readonly vcs?: | { readonly kind?: VcsDriverKindType | undefined; } | undefined; readonly vcsKind?: VcsDriverKindType | undefined; } export interface VcsProjectConfigResolveInput { readonly cwd: string; readonly requestedKind?: VcsDriverKindType | "auto"; } export interface VcsProjectConfigShape { readonly resolveKind: ( input: VcsProjectConfigResolveInput, ) => Effect.Effect; } export class VcsProjectConfig extends Context.Service()( "t3/vcs/VcsProjectConfig", ) {} function configuredKind(config: ProjectVcsConfigFile): VcsDriverKindType | "auto" { return config.vcs?.kind ?? config.vcsKind ?? "auto"; } function parseConfig(raw: string): ProjectVcsConfigFile | null { try { const parsed = JSON.parse(raw) as unknown; return isProjectVcsConfig(parsed) ? parsed : null; } catch { return null; } } export const make = Effect.fn("makeVcsProjectConfig")(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const findConfigPath = Effect.fn("VcsProjectConfig.findConfigPath")(function* (cwd: string) { let current = cwd; while (true) { const candidate = path.join(current, ".t3code", "vcs.json"); if (yield* fileSystem.exists(candidate).pipe(Effect.orElseSucceed(() => false))) { return candidate; } const parent = path.dirname(current); if (parent === current) { return null; } current = parent; } }); const readConfiguredKind = Effect.fn("VcsProjectConfig.readConfiguredKind")(function* ( configPath: string, ) { const raw = yield* fileSystem.readFileString(configPath).pipe( Effect.catch((error) => Effect.logWarning("failed to read VCS project config", { configPath, error, }).pipe(Effect.as(null)), ), ); if (raw === null) { return "auto" as const; } const parsed = parseConfig(raw); if (parsed === null) { yield* Effect.logWarning("invalid VCS project config", { configPath, }); return "auto" as const; } return configuredKind(parsed); }); const resolveKind: VcsProjectConfigShape["resolveKind"] = Effect.fn( "VcsProjectConfig.resolveKind", )(function* (input) { if (input.requestedKind !== undefined && input.requestedKind !== "auto") { return input.requestedKind; } const configPath = yield* findConfigPath(input.cwd); if (configPath === null) { return "auto"; } return yield* readConfiguredKind(configPath); }); return VcsProjectConfig.of({ resolveKind, }); }); export const layer = Layer.effect(VcsProjectConfig, make());