1import {
2 type SourceControlDiscoveryResult,
3 type VcsDiscoveryItem,
4 type VcsDriverKind,
5} from "@t3tools/contracts";
6import * as Context from "effect/Context";
7import * as Effect from "effect/Effect";
8import * as Layer from "effect/Layer";
9import * as Option from "effect/Option";
10
11import * as ServerConfig from "../config.ts";
12import * as VcsProcess from "../vcs/VcsProcess.ts";
13import { detailFromCause, firstNonEmptyLine } from "@t3tools/source-control-core/server/discovery";
14import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts";
15
16interface DiscoveryProbe {
17 readonly label: string;
18 readonly executable?: string;
19 readonly versionArgs?: ReadonlyArray<string>;
20 readonly implemented: boolean;
21 readonly installHint: string;
22}
23
24type VcsProbe = DiscoveryProbe & {
25 readonly kind: VcsDriverKind;
26 readonly executable: string;
27 readonly versionArgs: ReadonlyArray<string>;
28};
29
30interface DiscoveryProbeResult<Kind extends string> {
31 readonly kind: Kind;
32 readonly label: string;
33 readonly executable?: string;
34 readonly implemented: boolean;
35 readonly status: "available" | "missing";
36 readonly version: Option.Option<string>;
37 readonly installHint: string;
38 readonly detail: Option.Option<string>;
39}
40
41const VCS_PROBES: ReadonlyArray<VcsProbe> = [
42 {
43 kind: "git",
44 label: "Git",
45 executable: "git",
46 versionArgs: ["--version"],
47 implemented: true,
48 installHint: "Install Git from https://git-scm.com/downloads or with your package manager.",
49 },
50 {
51 kind: "jj",
52 label: "Jujutsu",
53 executable: "jj",
54 versionArgs: ["--version"],
55 implemented: false,
56 installHint: "Install Jujutsu with `brew install jj` or from https://github.com/jj-vcs/jj.",
57 },
58];
59
60export class SourceControlDiscovery extends Context.Service<
61 SourceControlDiscovery,
62 {
63 readonly discover: Effect.Effect<SourceControlDiscoveryResult>;
64 }
65>()("t3/sourceControl/SourceControlDiscovery") {}
66
67/** @public Service construction is part of the canonical Effect module API. */
68export const make = Effect.gen(function* () {
69 const config = yield* ServerConfig.ServerConfig;
70 const process = yield* VcsProcess.VcsProcess;
71 const sourceControlProviders = yield* SourceControlProviderRegistry.SourceControlProviderRegistry;
72
73 const probe = <Kind extends VcsDriverKind>(
74 input: DiscoveryProbe & { readonly kind: Kind },
75 ): Effect.Effect<DiscoveryProbeResult<Kind>> => {
76 const executable = input.executable;
77 const versionArgs = input.versionArgs;
78
79 if (!executable || !versionArgs) {
80 return Effect.succeed({
81 kind: input.kind,
82 label: input.label,
83 implemented: input.implemented,
84 status: "missing" as const,
85 version: Option.none<string>(),
86 installHint: input.installHint,
87 detail: Option.some(input.installHint),
88 } satisfies DiscoveryProbeResult<Kind>);
89 }
90
91 return process
92 .run({
93 operation: "source-control.discovery.probe",
94 command: executable,
95 args: versionArgs,
96 cwd: config.cwd,
97 timeoutMs: 5_000,
98 maxOutputBytes: 8_000,
99 appendTruncationMarker: true,
100 })
101 .pipe(
102 Effect.map(
103 (result) =>
104 ({
105 kind: input.kind,
106 label: input.label,
107 executable,
108 implemented: input.implemented,
109 status: "available" as const,
110 version: Option.orElse(firstNonEmptyLine(result.stdout), () =>
111 firstNonEmptyLine(result.stderr),
112 ),
113 installHint: input.installHint,
114 detail: Option.none<string>(),
115 }) satisfies DiscoveryProbeResult<Kind>,
116 ),
117 Effect.catch((cause) =>
118 Effect.succeed({
119 kind: input.kind,
120 label: input.label,
121 executable,
122 implemented: input.implemented,
123 status: "missing" as const,
124 version: Option.none<string>(),
125 installHint: input.installHint,
126 detail: detailFromCause(cause),
127 } satisfies DiscoveryProbeResult<Kind>),
128 ),
129 );
130 };
131
132 return SourceControlDiscovery.of({
133 discover: Effect.all({
134 versionControlSystems: Effect.all(
135 VCS_PROBES.map((entry) => probe(entry)) as ReadonlyArray<Effect.Effect<VcsDiscoveryItem>>,
136 { concurrency: "unbounded" },
137 ),
138 sourceControlProviders: sourceControlProviders.discover,
139 }),
140 });
141});
142
143export const layer = Layer.effect(SourceControlDiscovery, make);