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 Option from "effect/Option"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import { VcsDriverKind, type VcsDriverKind as VcsDriverKindType } from "@t3tools/contracts"; import { fromLenientJson } from "@t3tools/shared/schemaJson"; const ProjectVcsConfig = Schema.Struct({ vcs: Schema.optional( Schema.Struct({ kind: Schema.optional(VcsDriverKind), }), ), vcsKind: Schema.optional(VcsDriverKind), }); const ProjectVcsConfigJson = fromLenientJson(ProjectVcsConfig); const decodeProjectVcsConfigJson = Schema.decodeUnknownEffect(ProjectVcsConfigJson); type ProjectVcsConfigFile = typeof ProjectVcsConfig.Type; export interface VcsProjectConfigResolveInput { readonly cwd: string; readonly requestedKind?: VcsDriverKindType | "auto"; } export class VcsProjectConfigError extends Schema.TaggedErrorClass()( "VcsProjectConfigError", { operation: Schema.Literals(["inspect", "read", "decode"]), cwd: Schema.String, configPath: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to ${this.operation} VCS project config at ${this.configPath}.`; } } export class VcsProjectConfig extends Context.Service< VcsProjectConfig, { readonly resolveKind: ( input: VcsProjectConfigResolveInput, ) => Effect.Effect; } >()("t3/vcs/VcsProjectConfig") {} function configuredKind(config: ProjectVcsConfigFile): VcsDriverKindType | "auto" { return config.vcs?.kind ?? config.vcsKind ?? "auto"; } const logVcsProjectConfigError = (error: VcsProjectConfigError) => Effect.logWarning(error).pipe( Effect.annotateLogs({ operation: error.operation, cwd: error.cwd, configPath: error.configPath, errorTag: error._tag, }), ); export const make = Effect.gen(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"); const exists = yield* fileSystem.exists(candidate).pipe( Effect.mapError( (cause) => new VcsProjectConfigError({ operation: "inspect", cwd, configPath: candidate, cause, }), ), Effect.catchTags({ VcsProjectConfigError: (error) => logVcsProjectConfigError(error).pipe(Effect.as(false)), }), ); if (exists) { return Option.some(candidate); } const parent = path.dirname(current); if (parent === current) { return Option.none(); } current = parent; } }); const readConfiguredKind = Effect.fn("VcsProjectConfig.readConfiguredKind")(function* ( cwd: string, configPath: string, ) { const raw = yield* fileSystem.readFileString(configPath).pipe( Effect.mapError( (cause) => new VcsProjectConfigError({ operation: "read", cwd, configPath, cause, }), ), ); const parsed = yield* decodeProjectVcsConfigJson(raw).pipe( Effect.mapError( (cause) => new VcsProjectConfigError({ operation: "decode", cwd, configPath, cause, }), ), ); return configuredKind(parsed); }); const resolveKind: VcsProjectConfig["Service"]["resolveKind"] = Effect.fn( "VcsProjectConfig.resolveKind", )(function* (input) { if (input.requestedKind !== undefined && input.requestedKind !== "auto") { return input.requestedKind; } return yield* findConfigPath(input.cwd).pipe( Effect.flatMap( Option.match({ onNone: () => Effect.succeed("auto" as const), onSome: (configPath) => readConfiguredKind(input.cwd, configPath), }), ), Effect.catchTags({ VcsProjectConfigError: (error) => logVcsProjectConfigError(error).pipe(Effect.as("auto" as const)), }), ); }); return VcsProjectConfig.of({ resolveKind, }); }); export const layer = Layer.effect(VcsProjectConfig, make);