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>()( "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<VcsDriverKindType | "auto">; }>()("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);
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>()(
"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<VcsDriverKindType | "auto">;
}
>()("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);