| 1 | /** |
| 2 | * SourceControlHost — what a source control provider package may ask of the server it runs in. |
| 3 | * |
| 4 | * Providers run inside a T3 server but must not import it. The server provides this one |
| 5 | * service; everything a provider needs from its environment (settings, the process runner) |
| 6 | * goes through it, so a provider package depends only on `@t3tools/source-control-core` and |
| 7 | * its own API and CLI code. HTTP, the filesystem, and paths come from Effect's platform |
| 8 | * services directly. |
| 9 | * |
| 10 | * `git` is the subset of the server's git driver that checking out a change request needs; it |
| 11 | * grows only when a provider needs another operation. |
| 12 | * |
| 13 | * @module source-control-core/server/SourceControlHost |
| 14 | */ |
| 15 | import type { |
| 16 | GitCommandError, |
| 17 | ServerSettings, |
| 18 | ServerSettingsError, |
| 19 | VcsError, |
| 20 | VcsListRemotesResult, |
| 21 | VcsSwitchRefInput, |
| 22 | VcsSwitchRefResult, |
| 23 | } from "@t3tools/contracts"; |
| 24 | import * as Context from "effect/Context"; |
| 25 | import type * as Effect from "effect/Effect"; |
| 26 | import type * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; |
| 27 | |
| 28 | /** One CLI invocation. The server bounds concurrency, output, and time for every run. */ |
| 29 | export interface SourceControlProcessInput { |
| 30 | readonly operation: string; |
| 31 | readonly command: string; |
| 32 | readonly args: ReadonlyArray<string>; |
| 33 | readonly cwd: string; |
| 34 | readonly spawnCwd?: string; |
| 35 | readonly stdin?: string; |
| 36 | readonly onStdoutChunk?: (chunk: Uint8Array) => void; |
| 37 | readonly env?: NodeJS.ProcessEnv; |
| 38 | readonly allowNonZeroExit?: boolean; |
| 39 | readonly timeoutMs?: number; |
| 40 | readonly maxOutputBytes?: number; |
| 41 | /** What happens past `maxOutputBytes`: fail the run, or keep the first bytes. */ |
| 42 | readonly outputMode?: "error" | "truncate" | undefined; |
| 43 | readonly appendTruncationMarker?: boolean; |
| 44 | } |
| 45 | |
| 46 | export interface SourceControlProcessOutput { |
| 47 | readonly exitCode: ChildProcessSpawner.ExitCode; |
| 48 | readonly stdout: string; |
| 49 | readonly stderr: string; |
| 50 | readonly stdoutTruncated: boolean; |
| 51 | readonly stderrTruncated: boolean; |
| 52 | /** Present on real process output; optional so narrow test doubles remain lightweight. */ |
| 53 | readonly stdoutInvalidUtf8?: boolean; |
| 54 | readonly stderrInvalidUtf8?: boolean; |
| 55 | } |
| 56 | |
| 57 | export type SourceControlGitOutput = Omit< |
| 58 | SourceControlProcessOutput, |
| 59 | "stdoutInvalidUtf8" | "stderrInvalidUtf8" |
| 60 | >; |
| 61 | |
| 62 | export interface SourceControlGitRemoteBranch { |
| 63 | readonly cwd: string; |
| 64 | readonly remoteName: string; |
| 65 | readonly remoteBranch: string; |
| 66 | } |
| 67 | |
| 68 | export class SourceControlHost extends Context.Service< |
| 69 | SourceControlHost, |
| 70 | { |
| 71 | readonly settings: { |
| 72 | /** Read fresh on each call, so a credential saved in Settings applies without a restart. */ |
| 73 | readonly get: Effect.Effect<ServerSettings, ServerSettingsError>; |
| 74 | }; |
| 75 | readonly process: { |
| 76 | readonly run: ( |
| 77 | input: SourceControlProcessInput, |
| 78 | ) => Effect.Effect<SourceControlProcessOutput, VcsError>; |
| 79 | }; |
| 80 | readonly git: { |
| 81 | /** Runs `git -C cwd …args` through the server's git driver, with its limits and metrics. */ |
| 82 | readonly execute: (input: { |
| 83 | readonly operation: string; |
| 84 | readonly cwd: string; |
| 85 | readonly args: ReadonlyArray<string>; |
| 86 | readonly maxOutputBytes?: number; |
| 87 | readonly appendTruncationMarker?: boolean; |
| 88 | }) => Effect.Effect<SourceControlGitOutput, GitCommandError>; |
| 89 | readonly resolveCommit: (input: { |
| 90 | readonly cwd: string; |
| 91 | readonly revision: string; |
| 92 | }) => Effect.Effect<{ readonly commitSha: string }, GitCommandError>; |
| 93 | /** |
| 94 | * Lists the remotes of the repository at `cwd`. The outer effect resolves the repository |
| 95 | * and fails when the server cannot drive one there; the inner one lists its remotes. |
| 96 | */ |
| 97 | readonly remotes: ( |
| 98 | cwd: string, |
| 99 | ) => Effect.Effect<Effect.Effect<VcsListRemotesResult, VcsError>, VcsError>; |
| 100 | readonly readConfigValue: ( |
| 101 | cwd: string, |
| 102 | key: string, |
| 103 | ) => Effect.Effect<string | null, GitCommandError>; |
| 104 | readonly resolvePrimaryRemoteName: (cwd: string) => Effect.Effect<string, GitCommandError>; |
| 105 | /** Adds a remote for `url` unless one exists, and returns the name it is under. */ |
| 106 | readonly ensureRemote: (input: { |
| 107 | readonly cwd: string; |
| 108 | readonly preferredName: string; |
| 109 | readonly url: string; |
| 110 | }) => Effect.Effect<string, GitCommandError>; |
| 111 | readonly listLocalBranchNames: (cwd: string) => Effect.Effect<string[], GitCommandError>; |
| 112 | readonly fetchRemoteBranch: ( |
| 113 | input: SourceControlGitRemoteBranch & { readonly localBranch: string }, |
| 114 | ) => Effect.Effect<void, GitCommandError>; |
| 115 | readonly fetchRemoteTrackingBranch: ( |
| 116 | input: SourceControlGitRemoteBranch, |
| 117 | ) => Effect.Effect<void, GitCommandError>; |
| 118 | readonly setBranchUpstream: ( |
| 119 | input: SourceControlGitRemoteBranch & { readonly branch: string }, |
| 120 | ) => Effect.Effect<void, GitCommandError>; |
| 121 | readonly switchRef: ( |
| 122 | input: VcsSwitchRefInput, |
| 123 | ) => Effect.Effect<VcsSwitchRefResult, GitCommandError>; |
| 124 | }; |
| 125 | } |
| 126 | >()("@t3tools/source-control-core/server/SourceControlHost") {} |