import type { SourceControlProviderAuth, SourceControlProviderDiscoveryItem, SourceControlProviderInfo, SourceControlProviderKind, } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import type * as SourceControlProvider from "./SourceControlProvider.ts"; import type * as VcsProcess from "../vcs/VcsProcess.ts"; export interface SourceControlAuthProbeInput { readonly stdout: string; readonly stderr: string; readonly exitCode: VcsProcess.VcsProcessOutput["exitCode"]; } export interface SourceControlUnknownRemoteRefinementInput { readonly cwd: string; readonly context: SourceControlProvider.SourceControlProviderContext; readonly auth: SourceControlAuthProbeInput; } interface SourceControlDiscoverySpecBase { readonly kind: SourceControlProviderKind; readonly label: string; readonly installHint: string; } export type SourceControlCliDiscoverySpec = SourceControlDiscoverySpecBase & { readonly type: "cli"; readonly executable: string; readonly versionArgs: ReadonlyArray; readonly authArgs: ReadonlyArray; readonly probeTimeoutMs?: number; readonly parseAuth: (input: SourceControlAuthProbeInput) => SourceControlProviderAuth; readonly refineUnknownRemote?: ( input: SourceControlUnknownRemoteRefinementInput, ) => SourceControlProviderInfo | null; }; export type SourceControlApiDiscoverySpec = SourceControlDiscoverySpecBase & { readonly type: "api"; readonly probeAuth: Effect.Effect; }; export type SourceControlProviderDiscoverySpec = | SourceControlCliDiscoverySpec | SourceControlApiDiscoverySpec; type SourceControlCliRemoteRefinementSpec = SourceControlCliDiscoverySpec & { readonly refineUnknownRemote: NonNullable; }; // Most provider CLIs answer `--version` in well under a second, so a short budget keeps // discovery snappy. Specs whose CLI is known to be slower can raise it via probeTimeoutMs. const DEFAULT_PROBE_TIMEOUT_MS = 5_000; function probeTimeoutMs(spec: SourceControlCliDiscoverySpec): number { return spec.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS; } interface DiscoveryProbeResult { readonly kind: SourceControlProviderKind; readonly label: string; readonly executable: string; readonly status: "available" | "missing"; readonly version: Option.Option; readonly installHint: string; readonly detail: Option.Option; } export function firstNonEmptyLine(text: string): Option.Option { const line = text .split(/\r?\n/) .map((entry) => entry.trim()) .find((entry) => entry.length > 0); return line === undefined ? Option.none() : Option.some(line); } export function detailFromCause(cause: unknown): Option.Option { if (cause instanceof Error && cause.message.trim().length > 0) { return Option.some(cause.message.trim()); } return Option.none(); } function authAccount(account: string | undefined): Option.Option { const trimmed = account?.trim(); return trimmed === undefined || trimmed.length === 0 ? Option.none() : Option.some(trimmed); } function authHost(host: string | undefined): Option.Option { const trimmed = host?.trim(); return trimmed === undefined || trimmed.length === 0 ? Option.none() : Option.some(trimmed); } function authDetail(detail: string | undefined): Option.Option { const trimmed = detail?.trim(); return trimmed === undefined || trimmed.length === 0 ? Option.none() : Option.some(trimmed); } export function providerAuth(input: { readonly status: SourceControlProviderAuth["status"]; readonly account?: string | undefined; readonly host?: string | undefined; readonly detail?: string | undefined; }): SourceControlProviderAuth { return { status: input.status, account: authAccount(input.account), host: authHost(input.host), detail: authDetail(input.detail), }; } function unknownAuth(detail?: string): SourceControlProviderAuth { return providerAuth({ status: "unknown", detail }); } export function combinedAuthOutput(input: SourceControlAuthProbeInput): string { const parts: string[] = []; for (const entry of [input.stdout, input.stderr]) { if (entry.trim().length > 0) { parts.push(entry); } } return parts.join("\n"); } function sanitizedAuthLines(text: string): ReadonlyArray { const lines: string[] = []; for (const entry of text.split(/\r?\n/)) { const line = entry.trim(); if (line.length === 0) continue; if (/^[-\s]*token(?:\s+scopes?)?:/iu.test(line)) continue; lines.push(line); } return lines; } export function firstSafeAuthLine(text: string): string | undefined { return sanitizedAuthLines(text)[0]; } export function parseCliHost(text: string): string | undefined { return sanitizedAuthLines(text) .map((line) => line.replace(/^[^a-z0-9]+/iu, "")) .find((line) => /^[a-z0-9][a-z0-9.-]*(?::\d+)?$/iu.test(line)); } export function matchFirst(text: string, patterns: ReadonlyArray): string | undefined { for (const pattern of patterns) { const match = pattern.exec(text); const value = match?.[1]?.trim(); if (value && value.length > 0) return value; } return undefined; } function isCliRemoteRefinementSpec( spec: SourceControlProviderDiscoverySpec, ): spec is SourceControlCliRemoteRefinementSpec { return spec.type === "cli" && spec.refineUnknownRemote !== undefined; } function probeCli(input: { readonly spec: SourceControlCliDiscoverySpec; readonly process: VcsProcess.VcsProcess["Service"]; readonly cwd: string; }): Effect.Effect { return input.process .run({ operation: "source-control.discovery.probe", command: input.spec.executable, args: input.spec.versionArgs, cwd: input.cwd, timeoutMs: probeTimeoutMs(input.spec), maxOutputBytes: 8_000, appendTruncationMarker: true, }) .pipe( Effect.map( (result) => ({ kind: input.spec.kind, label: input.spec.label, executable: input.spec.executable, status: "available" as const, version: Option.orElse(firstNonEmptyLine(result.stdout), () => firstNonEmptyLine(result.stderr), ), installHint: input.spec.installHint, detail: Option.none(), }) satisfies DiscoveryProbeResult, ), Effect.catch((cause) => Effect.succeed({ kind: input.spec.kind, label: input.spec.label, executable: input.spec.executable, status: "missing" as const, version: Option.none(), installHint: input.spec.installHint, detail: detailFromCause(cause), } satisfies DiscoveryProbeResult), ), ); } export function probeSourceControlProvider(input: { readonly spec: SourceControlProviderDiscoverySpec; readonly process: VcsProcess.VcsProcess["Service"]; readonly cwd: string; }): Effect.Effect { if (input.spec.type === "api") { return input.spec.probeAuth.pipe( Effect.map( (auth) => ({ kind: input.spec.kind, label: input.spec.label, status: "available" as const, version: Option.none(), installHint: input.spec.installHint, detail: Option.none(), auth, }) satisfies SourceControlProviderDiscoveryItem, ), ); } const spec = input.spec; return probeCli({ spec, process: input.process, cwd: input.cwd, }).pipe( Effect.flatMap((item) => { if (item.status !== "available") { return Effect.succeed({ ...item, auth: unknownAuth("Hosting integration command was not found on the server PATH."), } satisfies SourceControlProviderDiscoveryItem); } return input.process .run({ operation: "source-control.discovery.auth", command: spec.executable, args: spec.authArgs, cwd: input.cwd, allowNonZeroExit: true, timeoutMs: probeTimeoutMs(spec), maxOutputBytes: 8_000, appendTruncationMarker: true, }) .pipe( Effect.map( (result) => ({ ...item, auth: spec.parseAuth(result), }) satisfies SourceControlProviderDiscoveryItem, ), Effect.catch((cause) => Effect.succeed({ ...item, auth: unknownAuth(Option.getOrUndefined(detailFromCause(cause))), } satisfies SourceControlProviderDiscoveryItem), ), ); }), ); } export const refineUnknownRemoteProvider = Effect.fn("refineUnknownRemoteProvider")( function* (input: { readonly specs: ReadonlyArray; readonly process: VcsProcess.VcsProcess["Service"]; readonly cwd: string; readonly context: SourceControlProvider.SourceControlProviderContext | null; }): Effect.fn.Return { if (input.context === null || input.context.provider.kind !== "unknown") { return input.context; } const context = input.context; const providers = yield* Effect.forEach(input.specs.filter(isCliRemoteRefinementSpec), (spec) => input.process .run({ operation: "source-control.discovery.refine-unknown-remote", command: spec.executable, args: spec.authArgs, cwd: input.cwd, allowNonZeroExit: true, timeoutMs: probeTimeoutMs(spec), maxOutputBytes: 8_000, appendTruncationMarker: true, }) .pipe( Effect.map((auth) => spec.refineUnknownRemote({ cwd: input.cwd, context, auth, }), ), Effect.orElseSucceed(() => null), ), ); const provider = providers.find((candidate) => candidate !== null); return provider ? { ...context, provider } : context; }, );