1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6import * as Path from "effect/Path";
7import * as Schema from "effect/Schema";
8
9import { VcsDriverKind, type VcsDriverKind as VcsDriverKindType } from "@t3tools/contracts";
10import { fromLenientJson } from "@t3tools/shared/schemaJson";
11
12const ProjectVcsConfig = Schema.Struct({
13 vcs: Schema.optional(
14 Schema.Struct({
15 kind: Schema.optional(VcsDriverKind),
16 }),
17 ),
18 vcsKind: Schema.optional(VcsDriverKind),
19});
20const ProjectVcsConfigJson = fromLenientJson(ProjectVcsConfig);
21const decodeProjectVcsConfigJson = Schema.decodeUnknownEffect(ProjectVcsConfigJson);
22
23type ProjectVcsConfigFile = typeof ProjectVcsConfig.Type;
24
25export interface VcsProjectConfigResolveInput {
26 readonly cwd: string;
27 readonly requestedKind?: VcsDriverKindType | "auto";
28}
29
30export class VcsProjectConfigError extends Schema.TaggedError<VcsProjectConfigError>()(
31 "VcsProjectConfigError",
32 {
33 operation: Schema.Literals(["inspect", "read", "decode"]),
34 cwd: Schema.String,
35 configPath: Schema.String,
36 cause: Schema.Defect(),
37 },
38) {
39 override get message(): string {
40 return `Failed to ${this.operation} VCS project config at ${this.configPath}.`;
41 }
42}
43
44export class VcsProjectConfig extends Context.Service<
45 VcsProjectConfig,
46 {
47 readonly resolveKind: (
48 input: VcsProjectConfigResolveInput,
49 ) => Effect.Effect<VcsDriverKindType | "auto">;
50 }
51>()("t3/vcs/VcsProjectConfig") {}
52
53function configuredKind(config: ProjectVcsConfigFile): VcsDriverKindType | "auto" {
54 return config.vcs?.kind ?? config.vcsKind ?? "auto";
55}
56
57const logVcsProjectConfigError = (error: VcsProjectConfigError) =>
58 Effect.logWarning(error).pipe(
59 Effect.annotateLogs({
60 operation: error.operation,
61 cwd: error.cwd,
62 configPath: error.configPath,
63 errorTag: error._tag,
64 }),
65 );
66
67/** @public Service construction is part of the canonical Effect module API. */
68export const make = Effect.gen(function* () {
69 const fileSystem = yield* FileSystem.FileSystem;
70 const path = yield* Path.Path;
71
72 const findConfigPath = Effect.fn("VcsProjectConfig.findConfigPath")(function* (cwd: string) {
73 let current = cwd;
74 while (true) {
75 const candidate = path.join(current, ".t3code", "vcs.json");
76 const exists = yield* fileSystem.exists(candidate).pipe(
77 Effect.mapError(
78 (cause) =>
79 new VcsProjectConfigError({
80 operation: "inspect",
81 cwd,
82 configPath: candidate,
83 cause,
84 }),
85 ),
86 Effect.catchTags({
87 VcsProjectConfigError: (error) => logVcsProjectConfigError(error).pipe(Effect.as(false)),
88 }),
89 );
90 if (exists) {
91 return Option.some(candidate);
92 }
93
94 const parent = path.dirname(current);
95 if (parent === current) {
96 return Option.none();
97 }
98 current = parent;
99 }
100 });
101
102 const readConfiguredKind = Effect.fn("VcsProjectConfig.readConfiguredKind")(function* (
103 cwd: string,
104 configPath: string,
105 ) {
106 const raw = yield* fileSystem.readFileString(configPath).pipe(
107 Effect.mapError(
108 (cause) =>
109 new VcsProjectConfigError({
110 operation: "read",
111 cwd,
112 configPath,
113 cause,
114 }),
115 ),
116 );
117 const parsed = yield* decodeProjectVcsConfigJson(raw).pipe(
118 Effect.mapError(
119 (cause) =>
120 new VcsProjectConfigError({
121 operation: "decode",
122 cwd,
123 configPath,
124 cause,
125 }),
126 ),
127 );
128 return configuredKind(parsed);
129 });
130
131 const resolveKind: VcsProjectConfig["Service"]["resolveKind"] = Effect.fn(
132 "VcsProjectConfig.resolveKind",
133 )(function* (input) {
134 if (input.requestedKind !== undefined && input.requestedKind !== "auto") {
135 return input.requestedKind;
136 }
137
138 return yield* findConfigPath(input.cwd).pipe(
139 Effect.flatMap(
140 Option.match({
141 onNone: () => Effect.succeed("auto" as const),
142 onSome: (configPath) => readConfiguredKind(input.cwd, configPath),
143 }),
144 ),
145 Effect.catchTags({
146 VcsProjectConfigError: (error) =>
147 logVcsProjectConfigError(error).pipe(Effect.as("auto" as const)),
148 }),
149 );
150 });
151
152 return VcsProjectConfig.of({
153 resolveKind,
154 });
155});
156
157export const layer = Layer.effect(VcsProjectConfig, make);