1import * as Schema from "effect/Schema";
2import { NonNegativeInt, TrimmedNonEmptyString } from "./baseSchemas.ts";
3
4export const VcsDriverKind = Schema.Literals(["git", "jj", "unknown"]);
5export type VcsDriverKind = typeof VcsDriverKind.Type;
6
7export const VcsFreshnessSource = Schema.Literals([
8 "live-local",
9 "cached-local",
10 "cached-remote",
11 "explicit-remote",
12]);
13export type VcsFreshnessSource = typeof VcsFreshnessSource.Type;
14
15export const VcsFreshness = Schema.Struct({
16 source: VcsFreshnessSource,
17 observedAt: Schema.DateTimeUtc,
18 expiresAt: Schema.Option(Schema.DateTimeUtc),
19});
20export type VcsFreshness = typeof VcsFreshness.Type;
21
22export const VcsDriverCapabilities = Schema.Struct({
23 kind: VcsDriverKind,
24 supportsWorktrees: Schema.Boolean,
25 supportsBookmarks: Schema.Boolean,
26 supportsAtomicSnapshot: Schema.Boolean,
27 supportsPushDefaultRemote: Schema.Boolean,
28 ignoreClassifier: Schema.Literals(["native", "git-compatible-fallback"]),
29});
30export type VcsDriverCapabilities = typeof VcsDriverCapabilities.Type;
31
32export const VcsRepositoryIdentity = Schema.Struct({
33 kind: VcsDriverKind,
34 rootPath: TrimmedNonEmptyString,
35 metadataPath: Schema.NullOr(TrimmedNonEmptyString),
36 freshness: VcsFreshness,
37});
38export type VcsRepositoryIdentity = typeof VcsRepositoryIdentity.Type;
39
40export const VcsListWorkspaceFilesResult = Schema.Struct({
41 paths: Schema.Array(TrimmedNonEmptyString),
42 truncated: Schema.Boolean,
43 freshness: VcsFreshness,
44});
45export type VcsListWorkspaceFilesResult = typeof VcsListWorkspaceFilesResult.Type;
46
47export const VcsRemote = Schema.Struct({
48 name: TrimmedNonEmptyString,
49 url: TrimmedNonEmptyString,
50 pushUrl: Schema.Option(TrimmedNonEmptyString),
51 isPrimary: Schema.Boolean,
52});
53export type VcsRemote = typeof VcsRemote.Type;
54
55export const VcsListRemotesResult = Schema.Struct({
56 remotes: Schema.Array(VcsRemote),
57 freshness: VcsFreshness,
58});
59export type VcsListRemotesResult = typeof VcsListRemotesResult.Type;
60
61export interface VcsProcessErrorContext {
62 readonly operation: string;
63 readonly command: string;
64 readonly cwd: string;
65 readonly argumentCount?: number;
66}
67
68export interface VcsProcessSpawnFailure {
69 readonly cause: unknown;
70}
71
72export interface VcsProcessTimeoutFailure {
73 readonly timeoutMs: number;
74}
75
76export const VcsProcessExitFailureKind = Schema.Literals([
77 "authentication",
78 "not-found",
79 "rate-limited",
80 "command-failed",
81]);
82export type VcsProcessExitFailureKind = typeof VcsProcessExitFailureKind.Type;
83
84export interface VcsProcessExitFailure {
85 readonly exitCode: number;
86 readonly stderr: string;
87 readonly stderrTruncated: boolean;
88}
89
90export class VcsProcessSpawnError extends Schema.TaggedError<VcsProcessSpawnError>()(
91 "VcsProcessSpawnError",
92 {
93 operation: Schema.String,
94 command: Schema.String,
95 cwd: Schema.String,
96 argumentCount: Schema.optional(NonNegativeInt),
97 cause: Schema.Defect(),
98 },
99) {
100 override get message(): string {
101 return `VCS process failed to spawn in ${this.operation}: ${this.command} (${this.cwd})`;
102 }
103
104 static fromProcessSpawnError(context: VcsProcessErrorContext, error: VcsProcessSpawnFailure) {
105 return new VcsProcessSpawnError({
106 ...context,
107 cause: error.cause,
108 });
109 }
110}
111
112export class VcsProcessExitError extends Schema.TaggedError<VcsProcessExitError>()(
113 "VcsProcessExitError",
114 {
115 operation: Schema.String,
116 command: Schema.String,
117 cwd: Schema.String,
118 argumentCount: Schema.optional(NonNegativeInt),
119 exitCode: Schema.Number,
120 detail: Schema.String,
121 failureKind: Schema.optional(VcsProcessExitFailureKind),
122 /** Process-boundary hint for a recognized transient failure; absence is not retryable. */
123 retryable: Schema.optional(Schema.Boolean),
124 stderrLength: Schema.optional(NonNegativeInt),
125 stderrTruncated: Schema.optional(Schema.Boolean),
126 },
127) {
128 override get message(): string {
129 return `VCS process failed in ${this.operation}: ${this.command} (${this.cwd}) exited with ${this.exitCode} - ${this.detail}`;
130 }
131
132 static fromProcessExit(
133 context: VcsProcessErrorContext,
134 error: VcsProcessExitFailure,
135 failureKind: VcsProcessExitFailureKind,
136 retryable?: boolean,
137 ) {
138 const detail =
139 failureKind === "authentication"
140 ? "Authentication failed."
141 : failureKind === "rate-limited"
142 ? "API rate limit exceeded."
143 : failureKind === "not-found"
144 ? context.command === "glab"
145 ? "Merge request not found."
146 : context.command === "gh" || context.command === "az"
147 ? "Pull request not found."
148 : "VCS resource not found."
149 : "Process exited with a non-zero status.";
150
151 return new VcsProcessExitError({
152 ...context,
153 exitCode: error.exitCode,
154 detail,
155 failureKind,
156 ...(retryable === true ? { retryable: true } : {}),
157 stderrLength: error.stderr.length,
158 stderrTruncated: error.stderrTruncated,
159 });
160 }
161}
162
163export class VcsProcessTimeoutError extends Schema.TaggedError<VcsProcessTimeoutError>()(
164 "VcsProcessTimeoutError",
165 {
166 operation: Schema.String,
167 command: Schema.String,
168 cwd: Schema.String,
169 argumentCount: Schema.optional(NonNegativeInt),
170 timeoutMs: Schema.Number,
171 },
172) {
173 override get message(): string {
174 return `VCS process timed out in ${this.operation}: ${this.command} (${this.cwd}) after ${this.timeoutMs}ms`;
175 }
176
177 static fromProcessTimeoutError(context: VcsProcessErrorContext, error: VcsProcessTimeoutFailure) {
178 return new VcsProcessTimeoutError({
179 ...context,
180 timeoutMs: error.timeoutMs,
181 });
182 }
183}
184
185const VcsProcessBoundaryErrorFields = {
186 operation: Schema.String,
187 command: Schema.String,
188 cwd: Schema.String,
189 argumentCount: Schema.optional(NonNegativeInt),
190};
191
192export class VcsProcessStdinWriteError extends Schema.TaggedError<VcsProcessStdinWriteError>()(
193 "VcsProcessStdinWriteError",
194 {
195 ...VcsProcessBoundaryErrorFields,
196 stdinBytes: NonNegativeInt,
197 cause: Schema.Defect(),
198 },
199) {
200 override get message(): string {
201 return `VCS process failed to write ${this.stdinBytes} bytes to stdin in ${this.operation}: ${this.command} (${this.cwd})`;
202 }
203}
204
205export class VcsProcessOutputReadError extends Schema.TaggedError<VcsProcessOutputReadError>()(
206 "VcsProcessOutputReadError",
207 {
208 ...VcsProcessBoundaryErrorFields,
209 stream: Schema.Literals(["stdout", "stderr", "exitCode"]),
210 cause: Schema.Defect(),
211 },
212) {
213 override get message(): string {
214 return `VCS process failed to read ${this.stream} in ${this.operation}: ${this.command} (${this.cwd})`;
215 }
216}
217
218export class VcsProcessOutputLimitError extends Schema.TaggedError<VcsProcessOutputLimitError>()(
219 "VcsProcessOutputLimitError",
220 {
221 ...VcsProcessBoundaryErrorFields,
222 stream: Schema.Literals(["stdout", "stderr"]),
223 maxBytes: NonNegativeInt,
224 observedBytes: NonNegativeInt,
225 },
226) {
227 override get message(): string {
228 return `VCS process ${this.stream} produced ${this.observedBytes} bytes in ${this.operation}: ${this.command} (${this.cwd}), exceeding the ${this.maxBytes} byte limit`;
229 }
230}
231
232export class VcsProcessMissingExitCodeError extends Schema.TaggedError<VcsProcessMissingExitCodeError>()(
233 "VcsProcessMissingExitCodeError",
234 VcsProcessBoundaryErrorFields,
235) {
236 override get message(): string {
237 return `VCS process completed without an exit code in ${this.operation}: ${this.command} (${this.cwd})`;
238 }
239}
240
241export const VcsOutputDecodeError = Schema.Union([
242 VcsProcessStdinWriteError,
243 VcsProcessOutputReadError,
244 VcsProcessOutputLimitError,
245 VcsProcessMissingExitCodeError,
246]);
247export type VcsOutputDecodeError = typeof VcsOutputDecodeError.Type;
248
249export class VcsRepositoryDetectionError extends Schema.TaggedError<VcsRepositoryDetectionError>()(
250 "VcsRepositoryDetectionError",
251 {
252 operation: Schema.String,
253 cwd: Schema.String,
254 detail: Schema.String,
255 cause: Schema.optional(Schema.Defect()),
256 },
257) {
258 override get message(): string {
259 return `VCS repository detection failed in ${this.operation}: ${this.cwd} - ${this.detail}`;
260 }
261}
262
263export class VcsUnsupportedOperationError extends Schema.TaggedError<VcsUnsupportedOperationError>()(
264 "VcsUnsupportedOperationError",
265 {
266 operation: Schema.String,
267 kind: VcsDriverKind,
268 detail: Schema.String,
269 },
270) {
271 override get message(): string {
272 return `VCS operation is unsupported for ${this.kind} in ${this.operation}: ${this.detail}`;
273 }
274}
275
276export const VcsError = Schema.Union([
277 VcsProcessSpawnError,
278 VcsProcessExitError,
279 VcsProcessTimeoutError,
280 VcsProcessStdinWriteError,
281 VcsProcessOutputReadError,
282 VcsProcessOutputLimitError,
283 VcsProcessMissingExitCodeError,
284 VcsRepositoryDetectionError,
285 VcsUnsupportedOperationError,
286]);
287export type VcsError = typeof VcsError.Type;