import * as Schema from "effect/Schema"; import { NonNegativeInt, TrimmedNonEmptyString } from "./baseSchemas.ts"; export const VcsDriverKind = Schema.Literals(["git", "jj", "unknown"]); export type VcsDriverKind = typeof VcsDriverKind.Type; export const VcsFreshnessSource = Schema.Literals([ "live-local", "cached-local", "cached-remote", "explicit-remote", ]); export type VcsFreshnessSource = typeof VcsFreshnessSource.Type; export const VcsFreshness = Schema.Struct({ source: VcsFreshnessSource, observedAt: Schema.DateTimeUtc, expiresAt: Schema.Option(Schema.DateTimeUtc), }); export type VcsFreshness = typeof VcsFreshness.Type; export const VcsDriverCapabilities = Schema.Struct({ kind: VcsDriverKind, supportsWorktrees: Schema.Boolean, supportsBookmarks: Schema.Boolean, supportsAtomicSnapshot: Schema.Boolean, supportsPushDefaultRemote: Schema.Boolean, ignoreClassifier: Schema.Literals(["native", "git-compatible-fallback"]), }); export type VcsDriverCapabilities = typeof VcsDriverCapabilities.Type; export const VcsRepositoryIdentity = Schema.Struct({ kind: VcsDriverKind, rootPath: TrimmedNonEmptyString, metadataPath: Schema.NullOr(TrimmedNonEmptyString), freshness: VcsFreshness, }); export type VcsRepositoryIdentity = typeof VcsRepositoryIdentity.Type; export const VcsListWorkspaceFilesResult = Schema.Struct({ paths: Schema.Array(TrimmedNonEmptyString), truncated: Schema.Boolean, freshness: VcsFreshness, }); export type VcsListWorkspaceFilesResult = typeof VcsListWorkspaceFilesResult.Type; export const VcsRemote = Schema.Struct({ name: TrimmedNonEmptyString, url: TrimmedNonEmptyString, pushUrl: Schema.Option(TrimmedNonEmptyString), isPrimary: Schema.Boolean, }); export type VcsRemote = typeof VcsRemote.Type; export const VcsListRemotesResult = Schema.Struct({ remotes: Schema.Array(VcsRemote), freshness: VcsFreshness, }); export type VcsListRemotesResult = typeof VcsListRemotesResult.Type; export interface VcsProcessErrorContext { readonly operation: string; readonly command: string; readonly cwd: string; readonly argumentCount?: number; } export interface VcsProcessSpawnFailure { readonly cause: unknown; } export interface VcsProcessStdinFailure { readonly cause: unknown; } export interface VcsProcessReadFailure { readonly stream: "stdout" | "stderr" | "exitCode"; readonly cause: unknown; } export interface VcsProcessOutputLimitFailure { readonly stream: "stdout" | "stderr"; readonly maxBytes: number; } export interface VcsProcessTimeoutFailure { readonly timeoutMs: number; } export const VcsProcessExitFailureKind = Schema.Literals([ "authentication", "not-found", "command-failed", ]); export type VcsProcessExitFailureKind = typeof VcsProcessExitFailureKind.Type; export interface VcsProcessExitFailure { readonly exitCode: number; readonly stderr: string; readonly stderrTruncated: boolean; } export class VcsProcessSpawnError extends Schema.TaggedErrorClass()( "VcsProcessSpawnError", { operation: Schema.String, command: Schema.String, cwd: Schema.String, argumentCount: Schema.optional(NonNegativeInt), cause: Schema.Defect(), }, ) { override get message(): string { return `VCS process failed to spawn in ${this.operation}: ${this.command} (${this.cwd})`; } static fromProcessSpawnError(context: VcsProcessErrorContext, error: VcsProcessSpawnFailure) { return new VcsProcessSpawnError({ ...context, cause: error.cause, }); } } export class VcsProcessExitError extends Schema.TaggedErrorClass()( "VcsProcessExitError", { operation: Schema.String, command: Schema.String, cwd: Schema.String, argumentCount: Schema.optional(NonNegativeInt), exitCode: Schema.Number, detail: Schema.String, failureKind: Schema.optional(VcsProcessExitFailureKind), stderrLength: Schema.optional(NonNegativeInt), stderrTruncated: Schema.optional(Schema.Boolean), }, ) { override get message(): string { return `VCS process failed in ${this.operation}: ${this.command} (${this.cwd}) exited with ${this.exitCode} - ${this.detail}`; } static fromProcessExit( context: VcsProcessErrorContext, error: VcsProcessExitFailure, failureKind: VcsProcessExitFailureKind, ) { const detail = failureKind === "authentication" ? "Authentication failed." : failureKind === "not-found" ? context.command === "glab" ? "Merge request not found." : context.command === "gh" || context.command === "az" ? "Pull request not found." : "VCS resource not found." : "Process exited with a non-zero status."; return new VcsProcessExitError({ ...context, exitCode: error.exitCode, detail, failureKind, stderrLength: error.stderr.length, stderrTruncated: error.stderrTruncated, }); } } export class VcsProcessTimeoutError extends Schema.TaggedErrorClass()( "VcsProcessTimeoutError", { operation: Schema.String, command: Schema.String, cwd: Schema.String, argumentCount: Schema.optional(NonNegativeInt), timeoutMs: Schema.Number, }, ) { override get message(): string { return `VCS process timed out in ${this.operation}: ${this.command} (${this.cwd}) after ${this.timeoutMs}ms`; } static fromProcessTimeoutError(context: VcsProcessErrorContext, error: VcsProcessTimeoutFailure) { return new VcsProcessTimeoutError({ ...context, timeoutMs: error.timeoutMs, }); } } export class VcsOutputDecodeError extends Schema.TaggedErrorClass()( "VcsOutputDecodeError", { operation: Schema.String, command: Schema.String, cwd: Schema.String, argumentCount: Schema.optional(NonNegativeInt), detail: Schema.String, cause: Schema.optional(Schema.Defect()), }, ) { override get message(): string { return `VCS output decode failed in ${this.operation}: ${this.command} (${this.cwd}) - ${this.detail}`; } static fromProcessStdinError(context: VcsProcessErrorContext, error: VcsProcessStdinFailure) { return new VcsOutputDecodeError({ ...context, detail: "failed to write process stdin", cause: error.cause, }); } static fromProcessReadError(context: VcsProcessErrorContext, error: VcsProcessReadFailure) { return new VcsOutputDecodeError({ ...context, detail: error.stream === "exitCode" ? "failed to read process exit code" : `failed to read process ${error.stream}`, cause: error.cause, }); } static fromProcessOutputLimitError( context: VcsProcessErrorContext, error: VcsProcessOutputLimitFailure, ) { return new VcsOutputDecodeError({ ...context, detail: `process ${error.stream} exceeded ${error.maxBytes} bytes`, }); } static missingExitCode(context: VcsProcessErrorContext) { return new VcsOutputDecodeError({ ...context, detail: "process completed without an exit code", }); } } export class VcsRepositoryDetectionError extends Schema.TaggedErrorClass()( "VcsRepositoryDetectionError", { operation: Schema.String, cwd: Schema.String, detail: Schema.String, cause: Schema.optional(Schema.Defect()), }, ) { override get message(): string { return `VCS repository detection failed in ${this.operation}: ${this.cwd} - ${this.detail}`; } } export class VcsUnsupportedOperationError extends Schema.TaggedErrorClass()( "VcsUnsupportedOperationError", { operation: Schema.String, kind: VcsDriverKind, detail: Schema.String, }, ) { override get message(): string { return `VCS operation is unsupported for ${this.kind} in ${this.operation}: ${this.detail}`; } } export const VcsError = Schema.Union([ VcsProcessSpawnError, VcsProcessExitError, VcsProcessTimeoutError, VcsOutputDecodeError, VcsRepositoryDetectionError, VcsUnsupportedOperationError, ]); export type VcsError = typeof VcsError.Type;