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 retryable: Schema.optional(Schema.Boolean),
123 stderrLength: Schema.optional(NonNegativeInt),
124 stderrTruncated: Schema.optional(Schema.Boolean),
125 },
126) {
127 override get message(): string {
128 return `VCS process failed in ${this.operation}: ${this.command} (${this.cwd}) exited with ${this.exitCode} - ${this.detail}`;
129 }
130
131 static fromProcessExit(
132 context: VcsProcessErrorContext,
133 error: VcsProcessExitFailure,
134 failureKind: VcsProcessExitFailureKind,
135 options?: { readonly retryable?: boolean; readonly notFoundDetail?: string },
136 ) {
137 const detail =
138 failureKind === "authentication"
139 ? "Authentication failed."
140 : failureKind === "rate-limited"
141 ? "API rate limit exceeded."
142 : failureKind === "not-found"
143 ? (options?.notFoundDetail ?? "VCS resource not found.")
144 : "Process exited with a non-zero status.";
145
146 return new VcsProcessExitError({
147 ...context,
148 exitCode: error.exitCode,
149 detail,
150 failureKind,
151 ...(options?.retryable === true ? { retryable: true } : {}),
152 stderrLength: error.stderr.length,
153 stderrTruncated: error.stderrTruncated,
154 });
155 }
156}
157
158export class VcsProcessTimeoutError extends Schema.TaggedError<VcsProcessTimeoutError>()(
159 "VcsProcessTimeoutError",
160 {
161 operation: Schema.String,
162 command: Schema.String,
163 cwd: Schema.String,
164 argumentCount: Schema.optional(NonNegativeInt),
165 timeoutMs: Schema.Number,
166 },
167) {
168 override get message(): string {
169 return `VCS process timed out in ${this.operation}: ${this.command} (${this.cwd}) after ${this.timeoutMs}ms`;
170 }
171
172 static fromProcessTimeoutError(context: VcsProcessErrorContext, error: VcsProcessTimeoutFailure) {
173 return new VcsProcessTimeoutError({
174 ...context,
175 timeoutMs: error.timeoutMs,
176 });
177 }
178}
179
180const VcsProcessBoundaryErrorFields = {
181 operation: Schema.String,
182 command: Schema.String,
183 cwd: Schema.String,
184 argumentCount: Schema.optional(NonNegativeInt),
185};
186
187export class VcsProcessStdinWriteError extends Schema.TaggedError<VcsProcessStdinWriteError>()(
188 "VcsProcessStdinWriteError",
189 {
190 ...VcsProcessBoundaryErrorFields,
191 stdinBytes: NonNegativeInt,
192 cause: Schema.Defect(),
193 },
194) {
195 override get message(): string {
196 return `VCS process failed to write ${this.stdinBytes} bytes to stdin in ${this.operation}: ${this.command} (${this.cwd})`;
197 }
198}
199
200export class VcsProcessOutputReadError extends Schema.TaggedError<VcsProcessOutputReadError>()(
201 "VcsProcessOutputReadError",
202 {
203 ...VcsProcessBoundaryErrorFields,
204 stream: Schema.Literals(["stdout", "stderr", "exitCode"]),
205 cause: Schema.Defect(),
206 },
207) {
208 override get message(): string {
209 return `VCS process failed to read ${this.stream} in ${this.operation}: ${this.command} (${this.cwd})`;
210 }
211}
212
213export class VcsProcessOutputLimitError extends Schema.TaggedError<VcsProcessOutputLimitError>()(
214 "VcsProcessOutputLimitError",
215 {
216 ...VcsProcessBoundaryErrorFields,
217 stream: Schema.Literals(["stdout", "stderr"]),
218 maxBytes: NonNegativeInt,
219 observedBytes: NonNegativeInt,
220 },
221) {
222 override get message(): string {
223 return `VCS process ${this.stream} produced ${this.observedBytes} bytes in ${this.operation}: ${this.command} (${this.cwd}), exceeding the ${this.maxBytes} byte limit`;
224 }
225}
226
227export class VcsProcessMissingExitCodeError extends Schema.TaggedError<VcsProcessMissingExitCodeError>()(
228 "VcsProcessMissingExitCodeError",
229 VcsProcessBoundaryErrorFields,
230) {
231 override get message(): string {
232 return `VCS process completed without an exit code in ${this.operation}: ${this.command} (${this.cwd})`;
233 }
234}
235
236export class VcsRepositoryDetectionError extends Schema.TaggedError<VcsRepositoryDetectionError>()(
237 "VcsRepositoryDetectionError",
238 {
239 operation: Schema.String,
240 cwd: Schema.String,
241 detail: Schema.String,
242 cause: Schema.optional(Schema.Defect()),
243 },
244) {
245 override get message(): string {
246 return `VCS repository detection failed in ${this.operation}: ${this.cwd} - ${this.detail}`;
247 }
248}
249
250export class VcsUnsupportedOperationError extends Schema.TaggedError<VcsUnsupportedOperationError>()(
251 "VcsUnsupportedOperationError",
252 {
253 operation: Schema.String,
254 kind: VcsDriverKind,
255 detail: Schema.String,
256 },
257) {
258 override get message(): string {
259 return `VCS operation is unsupported for ${this.kind} in ${this.operation}: ${this.detail}`;
260 }
261}
262
263export const VcsError = Schema.Union([
264 VcsProcessSpawnError,
265 VcsProcessExitError,
266 VcsProcessTimeoutError,
267 VcsProcessStdinWriteError,
268 VcsProcessOutputReadError,
269 VcsProcessOutputLimitError,
270 VcsProcessMissingExitCodeError,
271 VcsRepositoryDetectionError,
272 VcsUnsupportedOperationError,
273]);
274export type VcsError = typeof VcsError.Type;