| 1 | import { |
| 2 | type SourceControlDiscoveryResult, |
| 3 | type VcsDiscoveryItem, |
| 4 | type VcsDriverKind, |
| 5 | } from "@t3tools/contracts"; |
| 6 | import * as Context from "effect/Context"; |
| 7 | import * as Effect from "effect/Effect"; |
| 8 | import * as Layer from "effect/Layer"; |
| 9 | import * as Option from "effect/Option"; |
| 10 | |
| 11 | import * as ServerConfig from "../config.ts"; |
| 12 | import * as VcsProcess from "../vcs/VcsProcess.ts"; |
| 13 | import { detailFromCause, firstNonEmptyLine } from "@t3tools/source-control-core/server/discovery"; |
| 14 | import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; |
| 15 | |
| 16 | interface DiscoveryProbe { |
| 17 | readonly label: string; |
| 18 | readonly executable?: string; |
| 19 | readonly versionArgs?: ReadonlyArray<string>; |
| 20 | readonly implemented: boolean; |
| 21 | readonly installHint: string; |
| 22 | } |
| 23 | |
| 24 | type VcsProbe = DiscoveryProbe & { |
| 25 | readonly kind: VcsDriverKind; |
| 26 | readonly executable: string; |
| 27 | readonly versionArgs: ReadonlyArray<string>; |
| 28 | }; |
| 29 | |
| 30 | interface 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 | |
| 41 | const 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 | |
| 60 | export 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. */ |
| 68 | export 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 | |
| 143 | export const layer = Layer.effect(SourceControlDiscovery, make); |